mirror of
https://github.com/KeyZox71/knl_meowscendence.git
synced 2025-08-14 04:22:54 +02:00
「🔀」 merge(Blockchain): module (closes #3)
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@ -8,4 +8,9 @@ node_modules/
|
|||||||
.env
|
.env
|
||||||
|
|
||||||
# built files
|
# built files
|
||||||
dist/*
|
dist
|
||||||
|
|
||||||
|
# foundry files
|
||||||
|
lib
|
||||||
|
out
|
||||||
|
cache
|
||||||
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal 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
3
.solhint.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "solhint:recommended"
|
||||||
|
}
|
11
Justfile
11
Justfile
@ -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
|
@ -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
6
foundry.toml
Normal 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
1
lib/forge-std
Submodule
Submodule lib/forge-std added at 60acb7aaad
@ -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
2970
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
27
src/api/scoreStore/addTx.js
Normal file
27
src/api/scoreStore/addTx.js
Normal 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" });
|
||||||
|
}
|
||||||
|
}
|
32
src/api/scoreStore/default.js
Normal file
32
src/api/scoreStore/default.js
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
31
src/api/scoreStore/getTx.js
Normal file
31
src/api/scoreStore/getTx.js
Normal 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" });
|
||||||
|
}
|
||||||
|
}
|
1
src/contract/scoreStore.json
Normal file
1
src/contract/scoreStore.json
Normal 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"}]
|
45
src/contract/scoreStore.sol
Normal file
45
src/contract/scoreStore.sol
Normal 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];
|
||||||
|
}
|
||||||
|
}
|
11
src/start.js
11
src/start.js
@ -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
52
src/utils/scoreDB.js
Normal 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;
|
88
src/utils/scoreStore_contract.js
Normal file
88
src/utils/scoreStore_contract.js
Normal 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
33
test/scoreStore.t.sol
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user