🔀」 merge(Blockchain): module (closes #3)

This commit is contained in:
Adam
2025-07-30 16:16:23 +02:00
committed by GitHub
18 changed files with 356 additions and 2973 deletions

7
.gitignore vendored
View File

@ -8,4 +8,9 @@ node_modules/
.env .env
# built files # built files
dist/* dist
# foundry files
lib
out
cache

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std

3
.solhint.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "solhint:recommended"
}

View File

@ -11,6 +11,9 @@ set dotenv-load
@user $FASTIFY_LOG_LEVEL="info" $FASTIFY_PRETTY_LOGS="true": @user $FASTIFY_LOG_LEVEL="info" $FASTIFY_PRETTY_LOGS="true":
fastify start src/api/user/default.js fastify start src/api/user/default.js
@scoreStore $FASTIFY_LOG_LEVEL="info" $FASTIFY_PRETTY_LOGS="true":
fastify start src/api/scoreStore/default.js
# To launch all apis # To launch all apis
@apis: @apis:
node src/start.js node src/start.js
@ -45,5 +48,11 @@ set dotenv-load
@clean-compose: stop-docker @clean-compose: stop-docker
docker compose -f docker/docker-compose.yml rm docker compose -f docker/docker-compose.yml rm
@deploy-contract-scoreStore:
forge create scoreStore --rpc-url=${RPC_URL} --private-key=${PRIVATE_KEY}
@verify-contract:
forge verify-contract --chain-id 43113 --rpc-url=${AVAX_RPC_URL} --watch ${AVAX_CONTRACT_ADDR}
@status: @status:
docker compose -f docker/docker-compose.yml ps docker compose -f docker/docker-compose.yml ps

View File

@ -39,12 +39,17 @@
nodejs_22 nodejs_22
pnpm pnpm
just just
foundry
]; ];
shellHook = '' shellHook = ''
if [ ! -d node_modules/ ]; then if [ ! -d node_modules/ ]; then
echo Installing node env echo Installing node env
pnpm install pnpm install
fi fi
if [ ! -d lib/ ]; then
echo Installing foundry env
forge i
fi
export PATH+=:$(pwd)/node_modules/.bin export PATH+=:$(pwd)/node_modules/.bin
echo entering ft_trans env echo entering ft_trans env
''; '';

6
foundry.toml Normal file
View File

@ -0,0 +1,6 @@
[profile.default]
src = "src/contract"
out = "out"
libs = ["node_modules", "lib"]
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

1
lib/forge-std Submodule

Submodule lib/forge-std added at 60acb7aaad

View File

@ -1,5 +1,6 @@
{ {
"dependencies": { "dependencies": {
"@avalabs/avalanchejs": "^5.0.0",
"@fastify/cookie": "^11.0.2", "@fastify/cookie": "^11.0.2",
"@fastify/env": "^5.0.2", "@fastify/env": "^5.0.2",
"@fastify/jwt": "^9.1.0", "@fastify/jwt": "^9.1.0",
@ -7,10 +8,10 @@
"base32.js": "^0.1.0", "base32.js": "^0.1.0",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"better-sqlite3": "^12.2.0", "better-sqlite3": "^12.2.0",
"ethers": "^6.15.0",
"fastify": "^5.4.0", "fastify": "^5.4.0",
"fastify-cli": "^7.4.0", "fastify-cli": "^7.4.0",
"pino": "^9.7.0", "pino": "^9.7.0",
"pino-logstash": "^1.0.0",
"prom-client": "^15.1.3", "prom-client": "^15.1.3",
"solhint": "^6.0.0" "solhint": "^6.0.0"
}, },

2970
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
import { ContractTransactionResponse } from "ethers";
import scoreDB from "../../utils/scoreDB.js";
import { callAddScore, callLastId } from "../../utils/scoreStore_contract.js";
/**
* @async
* @param {import("fastify").FastifyRequest} request
* @param {import("fastify").FastifyReply} reply
* @param {import("fastify").FastifyInstance} fastify
*/
export async function addTx(request, reply, fastify) {
try {
const id = await callLastId();
const tx = callAddScore(request.body.p1, request.body.p2, request.body.p1Score, request.body.p2Score);
tx.then(tx => {
scoreDB.addTx(id, tx.hash);
});
return reply.code(200).send({
id: id
});
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

View File

@ -0,0 +1,32 @@
import { getTx } from './getTx.js';
import { addTx } from './addTx.js';
import scoreDB from '../../utils/scoreDB.js';
scoreDB.prepareDB();
/**
* @param {import('fastify').FastifyInstance} fastify
* @param {import('fastify').FastifyPluginOptions} options
*/
export default async function(fastify, options) {
fastify.get("/:id", async (request, reply) => {
return getTx(request, reply, fastify);
});
fastify.post("/", {
schema: {
body: {
type: 'object',
required: ['p1', 'p2', 'p1Score', 'p2Score'],
properties: {
p1: { type: 'string', minLength: 1 },
p2: { type: 'string', minLength: 1 },
p1Score: { type: 'integer', minimum: 0 },
p2Score: { type: 'integer', minimum: 0 },
}
}
}
}, async (request, reply) => {
return addTx(request, reply, fastify);
});
}

View File

@ -0,0 +1,31 @@
import scoreDB from "../../utils/scoreDB.js";
import { callGetScore } from "../../utils/scoreStore_contract.js";
/**
* @async
* @param {import("fastify".FastifyRequest)} request
* @param {import("fastify").FastifyReply} reply
* @param {import("fastify").FastifyInstance} fastify
*
* @returns {import('fastify').FastifyReply}
*/
export async function getTx(request, reply, fastify) {
try {
const tx = scoreDB.getTx(request.params.id);
const score = await callGetScore(request.params.id);
return reply.code(200).send({
score: {
p1: score.p1,
p2: score.p2,
p1Score: Number(score.p1Score),
p2Score: Number(score.p2Score)
},
tx: tx
});
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

View File

@ -0,0 +1 @@
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"string","name":"p1","type":"string"},{"internalType":"string","name":"p2","type":"string"},{"internalType":"uint128","name":"p1Score","type":"uint128"},{"internalType":"uint128","name":"p2Score","type":"uint128"}],"name":"addScore","outputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"getScore","outputs":[{"components":[{"internalType":"string","name":"p1","type":"string"},{"internalType":"string","name":"p2","type":"string"},{"internalType":"uint128","name":"p1Score","type":"uint128"},{"internalType":"uint128","name":"p2Score","type":"uint128"}],"internalType":"struct score","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"lastId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"scores","outputs":[{"internalType":"string","name":"p1","type":"string"},{"internalType":"string","name":"p2","type":"string"},{"internalType":"uint128","name":"p1Score","type":"uint128"},{"internalType":"uint128","name":"p2Score","type":"uint128"}],"stateMutability":"view","type":"function"}]

View File

@ -0,0 +1,45 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
struct score {
string p1;
string p2;
uint128 p1Score;
uint128 p2Score;
}
contract scoreStore {
address public owner;
uint public lastId;
mapping (uint => score) public scores;
constructor() {
owner = msg.sender;
lastId = 0;
}
modifier ownerOnly {
require(msg.sender == owner, "Need to be contract owner");
_;
}
function addScore(string memory p1, string memory p2, uint128 p1Score, uint128 p2Score) external ownerOnly returns (uint id) {
score memory s;
s.p1 = p1;
s.p2 = p2;
s.p1Score = p1Score;
s.p2Score = p2Score;
scores[lastId] = s;
id = lastId;
lastId++;
return (id);
}
function getScore(uint id) external view returns (score memory) {
return scores[id];
}
}

View File

@ -1,6 +1,7 @@
import Fastify from 'fastify'; import Fastify from 'fastify';
import authApi from './api/auth/default.js'; import authApi from './api/auth/default.js';
import userApi from './api/user/default.js'; import userApi from './api/user/default.js';
import scoreApi from './api/scoreStore/default.js';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
@ -56,6 +57,16 @@ async function start() {
console.log(`User API listening on http://${host}:${port}`); console.log(`User API listening on http://${host}:${port}`);
servers.push(user); servers.push(user);
} }
if (target === 'scoreScore' || target === 'all') {
const score = Fastify({ logger: loggerOption('scoreStore') });
score.register(scoreApi);
const port = target === 'all' ? 3002 : 3000;
const host = target === 'all' ? '127.0.0.1' : '0.0.0.0';
await score.listen({ port, host });
console.log(`ScoreStore API listening on http://${host}:${port}`);
servers.push(score);
}
// Graceful shutdown on SIGINT // Graceful shutdown on SIGINT
process.on('SIGINT', async () => { process.on('SIGINT', async () => {

52
src/utils/scoreDB.js Normal file
View File

@ -0,0 +1,52 @@
import { Int } from "@avalabs/avalanchejs";
import Database from "better-sqlite3";
var env = process.env.NODE_ENV || 'development';
let database;
if (!env || env === 'development') {
database = new Database(":memory:", { verbose: console.log });
} else {
var dbPath = process.env.DB_PATH || '/db/db.sqlite';
database = new Database(dbPath);
}
/**
* @description Can be used to prepare the database
*/
function prepareDB() {
database.exec(`
CREATE TABLE IF NOT EXISTS scoresTx (
id INT PRIMARY KEY,
txHash TEXT
) STRICT
`);
}
/**
* @description Can be used to add a score hash to the DB
* @param {Number} The id of the score
* @param {String} The hash of the score
*/
function addTx(id, txHash) {
const txAdd = database.prepare('INSERT INTO scoresTx (id, txHash) VALUES (?, ?)');
txAdd.run(id, txHash);
}
/**
* @description Can be used to get a tx hash from an id
* @param {Int} The id to get
* @returns {String} The tx hash
*/
function getTx(id) {
const txGet = database.prepare('SELECT txHash FROM scoresTx WHERE id = ?;')
return txGet.get(id);
}
const scoreDB = {
prepareDB,
addTx,
getTx
};
export default scoreDB;

View File

@ -0,0 +1,88 @@
import { ethers } from "ethers";
import { readFile } from "fs/promises";
export const rpc_url = process.env.AVAX_RPC_URL;
export const contract_addr = process.env.AVAX_CONTRACT_ADDR;
export const owner_priv_key = process.env.AVAX_PRIVATE_KEY;
const provider = new ethers.JsonRpcProvider(rpc_url);
const wallet = new ethers.Wallet(owner_priv_key, provider);
async function loadContract() {
try {
const contractABI = JSON.parse(await readFile(new URL('../contract/scoreStore.json', import.meta.url)));
const contract = new ethers.Contract(contract_addr, contractABI, wallet);
return contract;
} catch (error) {
console.error('Error loading contract ABI:', error);
throw error;
}
}
/**
* @param {int} id
* @returns {Promise<Object>} A promise that resolves to the score details if successful.
* @throws {Error} Throws an error if the function call fails.
*/
async function callGetScore(id) {
try {
const contract = await loadContract();
const result = await contract.getScore(id);
return result;
} catch (error) {
console.error('Error calling view function:', error);
throw error;
}
}
/**
* Adds a new score to the smart contract.
*
* @async
* @param {string} p1 - The name of the first player.
* @param {string} p2 - The name of the second player.
* @param {number} p1Score - The score of the first player.
* @param {number} p2Score - The score of the second player.
* @returns {Promise<ethers.ContractTransactionResponse>} A promise that resolves to the transaction response if successful.
* @throws {Error} Throws an error if the function call fails.
*/
async function callAddScore(p1, p2, p1Score, p2Score) {
try {
const contract = await loadContract();
const tx = await contract.addScore(p1, p2, p1Score, p2Score);
console.log('Transaction sent:', tx.hash);
await tx.wait(); // Wait for the transaction to be mined
console.log('Transaction confirmed');
return tx;
} catch (error) {
console.error('Error calling addScore function:', error);
throw error;
}
}
/**
* Fetches the last ID from the smart contract.
*
* @async
* @returns {Promise<number>} A promise that resolves to the last ID.
* @throws {Error} Throws an error if the function call fails.
*/
async function callLastId() {
try {
const contract = await loadContract();
const lastId = await contract.lastId();
console.log('Last ID:', lastId.toString());
return lastId;
} catch (error) {
console.error('Error calling lastId function:', error);
throw error;
}
}
export {
callAddScore,
callGetScore,
callLastId
};

33
test/scoreStore.t.sol Normal file
View File

@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import "src/contract/scoreStore.sol";
import "forge-std/Test.sol";
contract scoreStoreTest is Test {
scoreStore scoreS;
address nonOwner = address(1);
function setUp() public {
scoreS = new scoreStore();
}
function testAddScore() public {
uint id = scoreS.addScore("omg", "test", 5, 8);
score memory s = scoreS.getScore(id);
assertEq(s.p1, "omg");
assertEq(s.p2, "test");
assertEq(s.p1Score, 5);
assertEq(s.p2Score, 8);
id = scoreS.addScore("ahhhhh", "test", 7, 8);
s = scoreS.getScore(id);
assertEq(s.p1, "ahhhhh");
assertEq(s.p2, "test");
assertEq(s.p1Score, 7);
assertEq(s.p2Score, 8);
}
}