├── .gitignore ├── hardhat.config.cjs ├── scripts ├── deploy.js └── index.js ├── package.json ├── README.md ├── backdate_commits_real_dates.py ├── contracts └── SmartContract.sol └── test └── Lock.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | coverage 4 | coverage.json 5 | typechain 6 | typechain-types 7 | abi_json 8 | run_hardhat.sh 9 | 10 | # Hardhat files 11 | cache 12 | artifacts 13 | 14 | -------------------------------------------------------------------------------- /hardhat.config.cjs: -------------------------------------------------------------------------------- 1 | // https://eth-sepolia.g.alchemy.com/v2/poW824z7baY51XHHw5_9oqfNZo7Mcnav 2 | 3 | require("@nomiclabs/hardhat-waffle"); 4 | const dotenv = require("dotenv"); 5 | dotenv.config(); 6 | 7 | const customObject = {}; 8 | customObject[process.env.NETWORK] = { 9 | url: process.env.URL, 10 | accounts: [process.env.ACCOUNTS], 11 | }; 12 | 13 | module.exports = { 14 | solidity: "0.8.0", 15 | networks: customObject 16 | }; 17 | -------------------------------------------------------------------------------- /scripts/deploy.js: -------------------------------------------------------------------------------- 1 | const main = async () => { 2 | const SmartContract = await hre.ethers.getContractFactory("SmartContract"); 3 | const deploySmartContract = await SmartContract.deploy(); 4 | 5 | await deploySmartContract.deployed(); 6 | console.log(deploySmartContract.address); 7 | }; 8 | 9 | const runMain = async () => { 10 | try { 11 | await main(); 12 | process.exit(0); 13 | } catch (error) { 14 | console.log(error); 15 | process.exit(1); 16 | } 17 | } 18 | 19 | runMain(); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smart_contract", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "main": "index.js", 7 | "scripts": { 8 | "dev": "nodemon --experimental-modules --es-module-specifier-resolution=node scripts/index", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@nomiclabs/hardhat-ethers": "^2.2.3", 16 | "@nomiclabs/hardhat-waffle": "^2.0.6", 17 | "chai": "^4.3.7", 18 | "ethereum-waffle": "^4.0.10", 19 | "ethers": "^5.7.2", 20 | "hardhat": "^2.15.0", 21 | "nodemon": "^2.0.22" 22 | }, 23 | "dependencies": { 24 | "cors": "^2.8.5", 25 | "dotenv": "^16.3.1", 26 | "express": "^4.18.2", 27 | "joi": "^17.9.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Smart Contract Server for Plugin Donate Crypto 2 | 3 | This server runs on NodeJS with Hardhat configuration for smart contract system, this server can run on local server or VPS. This server has the function to create smart contracts using Hardhat. Before deploying a new smart contract, you must have a personal account key (Etherium), network name, and network API key url. 4 | 5 | ## Installation 6 | 7 | Download or Clone this repository. After that you open the terminal in the project and type the command as below and enter, to install all the packages used. 8 | 9 | ```bash 10 | npm install 11 | ``` 12 | 13 | ## Running the server 14 | 15 | The server can run if you type and enter 16 | 17 | ```bash 18 | node scripts/index.js 19 | ``` 20 | ## API Reference 21 | 22 | #### Create Smart Contract 23 | 24 | ```http 25 | POST /createSmartContract 26 | ``` 27 | 28 | | Parameter | Type | Description | 29 | | :-------------------- | :------- | :---------------------------------------------- | 30 | | `network` | `string` | **Required**. Type of network | 31 | | `url_api_key` | `string` | **Required**. URL API KEY of network | 32 | | `private_key_account` | `string` | **Required**. Private Key Account from metamask | 33 | 34 | If you run this API endpoint, you will get a JSON result like this 35 | ```json 36 | { 37 | "status": true, 38 | "message": "Process Successfully", 39 | "data": { 40 | "address_contract": "0x71F3e30b1f00AD6201d4fBea7a223cB91B8f5614", 41 | "abi_json_url": "http://localhost:3000/abi_json/ABI_FILE_JSON_SMARTCONTRACT.json" 42 | } 43 | } 44 | ``` 45 | ## Tech Stack 46 | 47 | **System:** NodeJS, Javascripts, ExpressJS, Hardhat 48 | -------------------------------------------------------------------------------- /backdate_commits_real_dates.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import random 4 | from datetime import datetime, timedelta 5 | 6 | # === CONFIGURATION === 7 | REPO_DIR = "." # Use current directory 8 | COMMIT_FILE = "commit_messages.txt" 9 | NUM_COMMITS = 1000 10 | 11 | # 🗓️ SET YOUR DATE RANGE HERE 12 | # Example: from October 21, 2024 to October 21, 2025 13 | START_DATE = datetime(2012, 1, 1) 14 | END_DATE = datetime(2012, 12, 31) 15 | 16 | def load_commit_messages(path): 17 | with open(path, "r", encoding="utf-8") as f: 18 | lines = [line.strip() for line in f if line.strip()] 19 | if len(lines) < NUM_COMMITS: 20 | raise ValueError(f"Expected at least {NUM_COMMITS} unique commit messages.") 21 | random.shuffle(lines) 22 | return lines[:NUM_COMMITS] 23 | 24 | def generate_random_dates(n, start_date, end_date): 25 | delta = (end_date - start_date).days 26 | dates = set() 27 | while len(dates) < n: 28 | rand_day = start_date + timedelta(days=random.randint(0, delta)) 29 | rand_time = timedelta(hours=random.randint(0, 23), minutes=random.randint(0, 59)) 30 | full_datetime = rand_day + rand_time 31 | dates.add(full_datetime.strftime("%Y-%m-%dT%H:%M:%S")) 32 | return sorted(dates) 33 | 34 | def setup_repo(path): 35 | if not os.path.exists(os.path.join(path, ".git")): 36 | subprocess.run(["git", "init"], cwd=path) 37 | 38 | def make_commit(repo_path, message, date_iso): 39 | dummy_file = os.path.join(repo_path, "log.txt") 40 | with open(dummy_file, "a", encoding="utf-8") as f: 41 | f.write(f"{message}\n") 42 | 43 | env = os.environ.copy() 44 | env["GIT_AUTHOR_DATE"] = date_iso 45 | env["GIT_COMMITTER_DATE"] = date_iso 46 | 47 | subprocess.run(["git", "add", "."], cwd=repo_path, env=env) 48 | subprocess.run(["git", "commit", "-m", message], cwd=repo_path, env=env) 49 | 50 | def main(): 51 | print("📄 Loading commit messages...") 52 | messages = load_commit_messages(COMMIT_FILE) 53 | 54 | print(f"📅 Generating random commit dates between {START_DATE.date()} and {END_DATE.date()}...") 55 | dates = generate_random_dates(NUM_COMMITS, START_DATE, END_DATE) 56 | 57 | print(f"📁 Setting up Git repo at: {REPO_DIR}") 58 | setup_repo(REPO_DIR) 59 | 60 | print("⏳ Creating commits...") 61 | for i in range(NUM_COMMITS): 62 | print(f"📦 Commit {i+1}/{NUM_COMMITS} → {dates[i]} | {messages[i]}") 63 | make_commit(REPO_DIR, messages[i], dates[i]) 64 | 65 | print(f"\n✅ Done! All commits backdated randomly between {START_DATE.date()} and {END_DATE.date()}.") 66 | 67 | if __name__ == "__main__": 68 | main() 69 | -------------------------------------------------------------------------------- /scripts/index.js: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process"; 2 | import express from "express"; 3 | import fs from "fs"; 4 | import cors from "cors"; 5 | import joi from "joi"; 6 | import bodyParser from "body-parser"; 7 | 8 | const app = express(); 9 | 10 | const corsConfig = { 11 | origin: "*", 12 | }; 13 | 14 | app.use(cors(corsConfig)); 15 | app.use(bodyParser.json()); 16 | app.use(bodyParser.urlencoded({ extended: false })); 17 | app.use("/abi_json", express.static("abi_json")); 18 | 19 | const validateCreateContract = (requestData) => { 20 | const createContract = joi 21 | .object({ 22 | network: joi.string().required(), 23 | url_api_key: joi.string().required(), 24 | private_key_account: joi.string().hex().required(), 25 | }) 26 | .options({ abortEarly: false }); 27 | return createContract.validate(requestData); 28 | }; 29 | 30 | const sendResponse = (status, message, response = []) => { 31 | const payload = { 32 | status: status, 33 | message: message, 34 | }; 35 | if ( 36 | Object.keys(response).length > 0 || 37 | (typeof response !== "undefined" && response.length > 0) 38 | ) 39 | payload.data = response; 40 | 41 | return payload; 42 | }; 43 | 44 | const responseServer400 = (res, msg) => 45 | res.status(400).json(sendResponse(false, msg)); 46 | 47 | const responseServer404 = (res, msg) => 48 | res.status(404).json(sendResponse(false, msg)); 49 | 50 | const responseServer200 = (res, msg, data = "") => 51 | res.status(200).json(sendResponse(true, msg, data)); 52 | 53 | const responseServer500 = (res, msg, data = "") => 54 | res.status(500).json(sendResponse(false, msg, data)); 55 | 56 | app.post("/createSmartContract", (req, res) => { 57 | const response_error = {}; 58 | const { error } = validateCreateContract(req.body); 59 | if (error) { 60 | error.details.forEach((err_msg) => { 61 | response_error[err_msg.path[0]] = err_msg.message; 62 | }); 63 | } else { 64 | fs.writeFileSync( 65 | ".env", 66 | `NETWORK=${req.body.network}\r\nURL=${req.body.url_api_key}\r\nACCOUNTS=${req.body.private_key_account}` 67 | ); 68 | fs.writeFileSync( 69 | "run_hardhat.sh", 70 | `npx hardhat run scripts/deploy.js --network ${req.body.network}` 71 | ); 72 | } 73 | Object.keys(response_error).length === 0 74 | ? exec("sh run_hardhat.sh", (error, stdout, stderr) => { 75 | if (error !== null) 76 | return responseServer500(res, "Process Failure", stderr); 77 | if (!fs.existsSync("abi_json")) fs.mkdirSync("abi_json"); 78 | 79 | const nameFile = `ABI_FILE_JSON_SMARTCONTRACT.json`; 80 | if (!fs.existsSync(`abi_json/${nameFile}`)) { 81 | fs.copyFileSync( 82 | "artifacts/contracts/SmartContract.sol/SmartContract.json", 83 | `abi_json/${nameFile}` 84 | ); 85 | } 86 | 87 | const abi_url = `${req.protocol}://${req.get( 88 | "host" 89 | )}/abi_json/${nameFile}`; 90 | 91 | const response = { 92 | address_contract: stdout.split("\n")[2], 93 | abi_json_url: abi_url, 94 | }; 95 | return responseServer200(res, "Process Successfully", response); 96 | }) 97 | : responseServer500( 98 | res, 99 | "failed to process endpoint", 100 | JSON.parse(JSON.stringify(response_error).replace(/\\"/g, "")) 101 | ); 102 | }); 103 | 104 | app.use("/", (req, res) => { 105 | return responseServer500(res, "Something Wrong!, Check Again!"); 106 | }); 107 | 108 | app.listen(3000, () => { 109 | console.log(`server runing on http://localhost:3000`); 110 | }); 111 | -------------------------------------------------------------------------------- /contracts/SmartContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | contract SmartContract { 5 | // Struct to represent a transaction 6 | struct TransferStruct { 7 | address sender; // Address of the sender 8 | address receiver; // Address of the receiver 9 | uint256 amount; // Amount of the transaction 10 | string documentHash; // DocumentHash of the sender 11 | string doi; // DOI of the sender 12 | string message; // Optional message associated with the transaction 13 | uint256 timestamp; // Timestamp of the transaction 14 | } 15 | 16 | uint256 transactionCount; // Counter for the total number of transactions 17 | TransferStruct[] transactions; // Array to store all the transactions 18 | event Transfer(address indexed from, address indexed receiver, uint256 amount, string documentHash, string doi, string message, uint256 timestamp); 19 | 20 | // Function to add a transaction 21 | function addTransaction(uint256[3] memory percentages, uint256[4] memory limits, address payable[] memory receivers, uint256[] memory percentages_authors, address payable[] memory receivers_authors, string memory documentHash, string memory doi, string memory message) public payable { 22 | // Perform various checks and validations before proceeding with the transaction 23 | require(limits.length == 4, "Invalid limits array length"); 24 | require(percentages.length == 3, "Invalid percentages array length"); 25 | require(receivers.length > 0, "No receivers specified"); 26 | require(percentages_authors.length > 0, "No percentage authors specified"); 27 | require(receivers_authors.length > 0, "No receivers authors specified"); 28 | require(msg.sender.balance > 0, "Insufficient balance"); 29 | require(msg.value > 0, "Value not provided"); 30 | 31 | // Calculate the amount to be split among the receivers => publishers 32 | uint256 amount = msg.value * percentages[0] / 100; 33 | 34 | for (uint256 i = limits[0]; i < limits[1]; i++) { 35 | _addTransaction(msg.sender, receivers[i], amount, documentHash, doi, message); 36 | _transferEther(receivers[i], amount); 37 | } 38 | 39 | // Repeat the same process for the next set of limits => reviewers 40 | amount = msg.value * percentages[1] / 100; 41 | uint256 diffLimits = limits[3] - limits[2]; 42 | uint256 splitAmount = amount / diffLimits; 43 | 44 | for (uint256 i = limits[2]; i < limits[3]; i++) { 45 | _addTransaction(msg.sender, receivers[i], splitAmount, documentHash, doi, message); 46 | _transferEther(receivers[i], splitAmount); 47 | } 48 | 49 | // Repeat the same process for the final set of limits => editors 50 | amount = msg.value * percentages[2] / 100; 51 | uint256 allTotalAuthors = 0; 52 | 53 | for (uint256 i = 0; i < receivers_authors.length; i++) { 54 | uint256 amountAuthor = amount * percentages_authors[i] / 100; 55 | allTotalAuthors += amountAuthor; 56 | _addTransaction(msg.sender, receivers_authors[i], amountAuthor, documentHash, doi, message); 57 | _transferEther(receivers_authors[i], amountAuthor); 58 | } 59 | 60 | // Send remaining ether to main author 61 | uint256 totalRemaining = amount - allTotalAuthors; 62 | if (totalRemaining != 0) { 63 | _addTransaction(msg.sender, receivers_authors[0], totalRemaining, documentHash, doi, message); 64 | _transferEther(receivers_authors[0], totalRemaining); 65 | } 66 | } 67 | 68 | // Internal function to add a transaction to the array and emit an event 69 | function _addTransaction(address sender, address receiver, uint256 amount, string memory documentHash, string memory doi, string memory message) internal { 70 | transactionCount += 1; 71 | transactions.push(TransferStruct(sender, receiver, amount, documentHash, doi, message, block.timestamp)); 72 | emit Transfer(sender, receiver, amount, documentHash, doi, message, block.timestamp); 73 | } 74 | 75 | // Internal function to transfer ether to a receiver 76 | function _transferEther(address payable receiver, uint256 amount) internal { 77 | (bool sent, ) = receiver.call{value: amount}(""); 78 | require(sent, "Failed to send Ether"); 79 | } 80 | 81 | // Function to get all the transactions 82 | function getAllTransaction() public view returns (TransferStruct[] memory) { 83 | return transactions; 84 | } 85 | 86 | // Function to get the total number of transactions 87 | function getTransactionCount() public view returns (uint256) { 88 | return transactionCount; 89 | } 90 | } -------------------------------------------------------------------------------- /test/Lock.js: -------------------------------------------------------------------------------- 1 | const { 2 | time, 3 | loadFixture, 4 | } = require("@nomicfoundation/hardhat-toolbox/network-helpers"); 5 | const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs"); 6 | const { expect } = require("chai"); 7 | 8 | describe("Lock", function () { 9 | // We define a fixture to reuse the same setup in every test. 10 | // We use loadFixture to run this setup once, snapshot that state, 11 | // and reset Hardhat Network to that snapshot in every test. 12 | async function deployOneYearLockFixture() { 13 | const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60; 14 | const ONE_GWEI = 1_000_000_000; 15 | 16 | const lockedAmount = ONE_GWEI; 17 | const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS; 18 | 19 | // Contracts are deployed using the first signer/account by default 20 | const [owner, otherAccount] = await ethers.getSigners(); 21 | 22 | const Lock = await ethers.getContractFactory("Lock"); 23 | const lock = await Lock.deploy(unlockTime, { value: lockedAmount }); 24 | 25 | return { lock, unlockTime, lockedAmount, owner, otherAccount }; 26 | } 27 | 28 | describe("Deployment", function () { 29 | it("Should set the right unlockTime", async function () { 30 | const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture); 31 | 32 | expect(await lock.unlockTime()).to.equal(unlockTime); 33 | }); 34 | 35 | it("Should set the right owner", async function () { 36 | const { lock, owner } = await loadFixture(deployOneYearLockFixture); 37 | 38 | expect(await lock.owner()).to.equal(owner.address); 39 | }); 40 | 41 | it("Should receive and store the funds to lock", async function () { 42 | const { lock, lockedAmount } = await loadFixture( 43 | deployOneYearLockFixture 44 | ); 45 | 46 | expect(await ethers.provider.getBalance(lock.target)).to.equal( 47 | lockedAmount 48 | ); 49 | }); 50 | 51 | it("Should fail if the unlockTime is not in the future", async function () { 52 | // We don't use the fixture here because we want a different deployment 53 | const latestTime = await time.latest(); 54 | const Lock = await ethers.getContractFactory("Lock"); 55 | await expect(Lock.deploy(latestTime, { value: 1 })).to.be.revertedWith( 56 | "Unlock time should be in the future" 57 | ); 58 | }); 59 | }); 60 | 61 | describe("Withdrawals", function () { 62 | describe("Validations", function () { 63 | it("Should revert with the right error if called too soon", async function () { 64 | const { lock } = await loadFixture(deployOneYearLockFixture); 65 | 66 | await expect(lock.withdraw()).to.be.revertedWith( 67 | "You can't withdraw yet" 68 | ); 69 | }); 70 | 71 | it("Should revert with the right error if called from another account", async function () { 72 | const { lock, unlockTime, otherAccount } = await loadFixture( 73 | deployOneYearLockFixture 74 | ); 75 | 76 | // We can increase the time in Hardhat Network 77 | await time.increaseTo(unlockTime); 78 | 79 | // We use lock.connect() to send a transaction from another account 80 | await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWith( 81 | "You aren't the owner" 82 | ); 83 | }); 84 | 85 | it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () { 86 | const { lock, unlockTime } = await loadFixture( 87 | deployOneYearLockFixture 88 | ); 89 | 90 | // Transactions are sent using the first signer by default 91 | await time.increaseTo(unlockTime); 92 | 93 | await expect(lock.withdraw()).not.to.be.reverted; 94 | }); 95 | }); 96 | 97 | describe("Events", function () { 98 | it("Should emit an event on withdrawals", async function () { 99 | const { lock, unlockTime, lockedAmount } = await loadFixture( 100 | deployOneYearLockFixture 101 | ); 102 | 103 | await time.increaseTo(unlockTime); 104 | 105 | await expect(lock.withdraw()) 106 | .to.emit(lock, "Withdrawal") 107 | .withArgs(lockedAmount, anyValue); // We accept any value as `when` arg 108 | }); 109 | }); 110 | 111 | describe("Transfers", function () { 112 | it("Should transfer the funds to the owner", async function () { 113 | const { lock, unlockTime, lockedAmount, owner } = await loadFixture( 114 | deployOneYearLockFixture 115 | ); 116 | 117 | await time.increaseTo(unlockTime); 118 | 119 | await expect(lock.withdraw()).to.changeEtherBalances( 120 | [owner, lock], 121 | [lockedAmount, -lockedAmount] 122 | ); 123 | }); 124 | }); 125 | }); 126 | }); 127 | --------------------------------------------------------------------------------