From 041bb2deb5100aec30e00bf0610d91f647d3f8a0 Mon Sep 17 00:00:00 2001 From: adjoly Date: Wed, 30 Jul 2025 16:04:18 +0200 Subject: [PATCH] =?UTF-8?q?=E3=80=8C=F0=9F=8F=97=EF=B8=8F=E3=80=8D=20wip:?= =?UTF-8?q?=20seems=20to=20be=20working?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Justfile | 9 ++++ package.json | 1 + pnpm-lock.yaml | 85 ++++++++++++++++++++++++++++-- src/api/scoreStore/addTx.js | 26 ++++++++++ src/api/scoreStore/default.js | 23 ++++++++- src/api/scoreStore/getTx.js | 11 +++- src/contract/scoreStore.json | 1 + src/start.js | 14 +++++ src/utils/scoreDB.js | 6 +-- src/utils/scoreStore_contract.js | 88 ++++++++++++++++++++++++++++++++ 10 files changed, 252 insertions(+), 12 deletions(-) create mode 100644 src/api/scoreStore/addTx.js create mode 100644 src/contract/scoreStore.json create mode 100644 src/utils/scoreStore_contract.js diff --git a/Justfile b/Justfile index 6a884a6..17db908 100644 --- a/Justfile +++ b/Justfile @@ -11,6 +11,9 @@ set dotenv-load @user $FASTIFY_LOG_LEVEL="info" $FASTIFY_PRETTY_LOGS="true": 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 @apis: node src/dev.js @@ -44,3 +47,9 @@ set dotenv-load # To clean only the container launched by the compose @clean-compose: stop-docker 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} diff --git a/package.json b/package.json index e03c068..a46bd97 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "base32.js": "^0.1.0", "bcrypt": "^6.0.0", "better-sqlite3": "^12.2.0", + "ethers": "^6.15.0", "fastify": "^5.4.0", "fastify-cli": "^7.4.0", "google-auth-library": "^10.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a454ff7..bf6d865 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: better-sqlite3: specifier: ^12.2.0 version: 12.2.0 + ethers: + specifier: ^6.15.0 + version: 6.15.0 fastify: specifier: ^5.4.0 version: 5.4.0 @@ -50,7 +53,7 @@ importers: devDependencies: '@tailwindcss/vite': specifier: ^4.1.11 - version: 4.1.11(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1)) + version: 4.1.11(vite@6.3.5(@types/node@22.7.5)(jiti@2.4.2)(lightningcss@1.30.1)) pino-pretty: specifier: ^13.0.0 version: 13.0.0 @@ -62,10 +65,13 @@ importers: version: 5.8.3 vite: specifier: ^6.3.5 - version: 6.3.5(jiti@2.4.2)(lightningcss@1.30.1) + version: 6.3.5(@types/node@22.7.5)(jiti@2.4.2)(lightningcss@1.30.1) packages: + '@adraffy/ens-normalize@1.10.1': + resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==} + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -298,9 +304,16 @@ packages: resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} engines: {node: '>=8'} + '@noble/curves@1.2.0': + resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} + '@noble/curves@1.3.0': resolution: {integrity: sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==} + '@noble/hashes@1.3.2': + resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} + engines: {node: '>= 16'} + '@noble/hashes@1.3.3': resolution: {integrity: sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==} engines: {node: '>= 16'} @@ -534,9 +547,15 @@ packages: '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + '@types/node@22.7.5': + resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + abstract-logging@2.0.1: resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + aes-js@4.0.0-beta.5: + resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==} + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -809,6 +828,10 @@ packages: engines: {node: '>=18'} hasBin: true + ethers@6.15.0: + resolution: {integrity: sha512-Kf/3ZW54L4UT0pZtsY/rf+EkBU7Qi5nnhonjUb8yTXcxH3cdcWrV2cRyk0Xk/4jK6OoHhxxZHriyhje20If2hQ==} + engines: {node: '>=14.0.0'} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -1604,6 +1627,9 @@ packages: resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} engines: {node: '>=12'} + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -1612,6 +1638,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -1672,6 +1701,18 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -1686,6 +1727,8 @@ packages: snapshots: + '@adraffy/ens-normalize@1.10.1': {} + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.12 @@ -1852,10 +1895,16 @@ snapshots: '@lukeed/ms@2.0.2': {} + '@noble/curves@1.2.0': + dependencies: + '@noble/hashes': 1.3.2 + '@noble/curves@1.3.0': dependencies: '@noble/hashes': 1.3.3 + '@noble/hashes@1.3.2': {} + '@noble/hashes@1.3.3': {} '@noble/secp256k1@2.0.0': {} @@ -2008,19 +2057,25 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.11 '@tailwindcss/oxide-win32-x64-msvc': 4.1.11 - '@tailwindcss/vite@4.1.11(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1))': + '@tailwindcss/vite@4.1.11(vite@6.3.5(@types/node@22.7.5)(jiti@2.4.2)(lightningcss@1.30.1))': dependencies: '@tailwindcss/node': 4.1.11 '@tailwindcss/oxide': 4.1.11 tailwindcss: 4.1.11 - vite: 6.3.5(jiti@2.4.2)(lightningcss@1.30.1) + vite: 6.3.5(@types/node@22.7.5)(jiti@2.4.2)(lightningcss@1.30.1) '@types/estree@1.0.8': {} '@types/http-cache-semantics@4.0.4': {} + '@types/node@22.7.5': + dependencies: + undici-types: 6.19.8 + abstract-logging@2.0.1: {} + aes-js@4.0.0-beta.5: {} + agent-base@7.1.4: {} ajv-errors@1.0.1(ajv@6.12.6): @@ -2302,6 +2357,19 @@ snapshots: '@esbuild/win32-ia32': 0.25.6 '@esbuild/win32-x64': 0.25.6 + ethers@6.15.0: + dependencies: + '@adraffy/ens-normalize': 1.10.1 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@types/node': 22.7.5 + aes-js: 4.0.0-beta.5 + tslib: 2.7.0 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + expand-template@2.0.3: {} extend@3.0.2: {} @@ -3160,12 +3228,16 @@ snapshots: toad-cache@3.7.0: {} + tslib@2.7.0: {} + tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 typescript@5.8.3: {} + undici-types@6.19.8: {} + universalify@2.0.1: {} uri-js@4.4.1: @@ -3174,7 +3246,7 @@ snapshots: util-deprecate@1.0.2: {} - vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1): + vite@6.3.5(@types/node@22.7.5)(jiti@2.4.2)(lightningcss@1.30.1): dependencies: esbuild: 0.25.6 fdir: 6.4.6(picomatch@4.0.2) @@ -3183,6 +3255,7 @@ snapshots: rollup: 4.44.2 tinyglobby: 0.2.14 optionalDependencies: + '@types/node': 22.7.5 fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.30.1 @@ -3195,6 +3268,8 @@ snapshots: wrappy@1.0.2: {} + ws@8.17.1: {} + xtend@4.0.2: {} yallist@5.0.0: {} diff --git a/src/api/scoreStore/addTx.js b/src/api/scoreStore/addTx.js new file mode 100644 index 0000000..d1640bb --- /dev/null +++ b/src/api/scoreStore/addTx.js @@ -0,0 +1,26 @@ +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(); + /** @type ContractTransactionResponse */ + const tx = await callAddScore(request.body.p1, request.body.p2, request.body.p1Score, request.body.p2Score); + + scoreDB.addTx(id, tx.hash); + + return reply.code(200).send({ + tx: tx.hash + }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/scoreStore/default.js b/src/api/scoreStore/default.js index 627fbc1..954fcc8 100644 --- a/src/api/scoreStore/default.js +++ b/src/api/scoreStore/default.js @@ -1,6 +1,8 @@ -import { evm } from '@avalabs/avalanchejs'; -import scoreDB from '../../utils/scoreDB.js'; import { getTx } from './getTx.js'; +import { addTx } from './addTx.js'; +import scoreDB from '../../utils/scoreDB.js'; + +scoreDB.prepareDB(); /** * @param {import('fastify').FastifyInstance} fastify @@ -10,4 +12,21 @@ 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); + }); } diff --git a/src/api/scoreStore/getTx.js b/src/api/scoreStore/getTx.js index 8cbdb84..ffd8363 100644 --- a/src/api/scoreStore/getTx.js +++ b/src/api/scoreStore/getTx.js @@ -1,4 +1,5 @@ import scoreDB from "../../utils/scoreDB.js"; +import { callGetScore } from "../../utils/scoreStore_contract.js"; /** * @async @@ -10,10 +11,16 @@ import scoreDB from "../../utils/scoreDB.js"; */ export async function getTx(request, reply, fastify) { try { - + const tx = scoreDB.getTx(request.params.id); + + const score = callGetScore(request.params.id); + + return reply.code(200).send({ + score: score, + tx: tx + }); } catch (err) { fastify.log.error(err); return reply.code(500).send({ error: "Internal server error" }); } } - diff --git a/src/contract/scoreStore.json b/src/contract/scoreStore.json new file mode 100644 index 0000000..552e1bc --- /dev/null +++ b/src/contract/scoreStore.json @@ -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"}] diff --git a/src/start.js b/src/start.js index 6111c96..7573f69 100644 --- a/src/start.js +++ b/src/start.js @@ -1,6 +1,7 @@ import Fastify from 'fastify'; import authApi from './api/auth/default.js'; import userApi from './api/user/default.js'; +import scoreApi from './api/scoreStore/default.js'; const loggerOption = { transport: { @@ -47,6 +48,19 @@ async function start() { console.log('User API listening on http://localhost:3002'); } } + + if (target === 'scoreStore' || target === 'all') { + const scoreStore = Fastify({ logger: loggerOption }); + scoreStore.register(scoreApi); + if (target !== 'all') { + await scoreStore.listen({ port: 3000, host: '0.0.0.0' }); + console.log('scoreStore API listening on http://0.0.0.0:3000'); + } + else { + await scoreStore.listen({ port: 3002, host: '127.0.0.1'}); + console.log('scoreStore API listening on http://localhost:3002'); + } + } } start().catch(console.error); diff --git a/src/utils/scoreDB.js b/src/utils/scoreDB.js index c1e0ffb..f2f7a16 100644 --- a/src/utils/scoreDB.js +++ b/src/utils/scoreDB.js @@ -17,7 +17,7 @@ if (!env || env === 'development') { function prepareDB() { database.exec(` CREATE TABLE IF NOT EXISTS scoresTx ( - id INTEGER PRIMARY KEY, + id INT PRIMARY KEY, txHash TEXT ) STRICT `); @@ -25,7 +25,7 @@ function prepareDB() { /** * @description Can be used to add a score hash to the DB - * @param {Int} The id of the score + * @param {Number} The id of the score * @param {String} The hash of the score */ function addTx(id, txHash) { @@ -39,7 +39,7 @@ function addTx(id, txHash) { * @returns {String} The tx hash */ function getTx(id) { - const txGet = database.prepare('SELECT txHash FROM credentials WHERE id = ?;') + const txGet = database.prepare('SELECT txHash FROM scoresTx WHERE id = ?;') return txGet.get(id); } diff --git a/src/utils/scoreStore_contract.js b/src/utils/scoreStore_contract.js new file mode 100644 index 0000000..dd7c69e --- /dev/null +++ b/src/utils/scoreStore_contract.js @@ -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} 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} 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} 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 +};