diff --git a/.github/workflows/deploy-vue.yml b/.github/workflows/deploy-vue.yml index 1f41a66..aa73053 100644 --- a/.github/workflows/deploy-vue.yml +++ b/.github/workflows/deploy-vue.yml @@ -18,19 +18,21 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '18' + node-version: "18" - name: Install dependencies + working-directory: ./app run: yarn install - name: Build + working-directory: ./app run: yarn build-only - name: Deploy uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./dist + publish_dir: ./app/dist notify-on-failure: runs-on: ubuntu-latest diff --git a/.eslintrc.cjs b/app/.eslintrc.cjs similarity index 100% rename from .eslintrc.cjs rename to app/.eslintrc.cjs diff --git a/.prettierrc.json b/app/.prettierrc.json similarity index 100% rename from .prettierrc.json rename to app/.prettierrc.json diff --git a/.vscode/extensions.json b/app/.vscode/extensions.json similarity index 100% rename from .vscode/extensions.json rename to app/.vscode/extensions.json diff --git a/.vscode/settings.json b/app/.vscode/settings.json similarity index 100% rename from .vscode/settings.json rename to app/.vscode/settings.json diff --git a/README.md b/app/README.md similarity index 100% rename from README.md rename to app/README.md diff --git a/env.d.ts b/app/env.d.ts similarity index 100% rename from env.d.ts rename to app/env.d.ts diff --git a/index.html b/app/index.html similarity index 100% rename from index.html rename to app/index.html diff --git a/package.json b/app/package.json similarity index 100% rename from package.json rename to app/package.json diff --git a/public/favicon.ico b/app/public/favicon.ico similarity index 100% rename from public/favicon.ico rename to app/public/favicon.ico diff --git a/src/App.vue b/app/src/App.vue similarity index 100% rename from src/App.vue rename to app/src/App.vue diff --git a/src/assets/base.css b/app/src/assets/base.css similarity index 100% rename from src/assets/base.css rename to app/src/assets/base.css diff --git a/src/assets/logo.svg b/app/src/assets/logo.svg similarity index 100% rename from src/assets/logo.svg rename to app/src/assets/logo.svg diff --git a/src/assets/main.css b/app/src/assets/main.css similarity index 100% rename from src/assets/main.css rename to app/src/assets/main.css diff --git a/src/auth.ts b/app/src/auth.ts similarity index 100% rename from src/auth.ts rename to app/src/auth.ts diff --git a/src/axios.ts b/app/src/axios.ts similarity index 100% rename from src/axios.ts rename to app/src/axios.ts diff --git a/src/components/LeaderBoard.vue b/app/src/components/LeaderBoard.vue similarity index 100% rename from src/components/LeaderBoard.vue rename to app/src/components/LeaderBoard.vue diff --git a/src/components/NearLogin.vue b/app/src/components/NearLogin.vue similarity index 100% rename from src/components/NearLogin.vue rename to app/src/components/NearLogin.vue diff --git a/src/components/PlayGame.vue b/app/src/components/PlayGame.vue similarity index 100% rename from src/components/PlayGame.vue rename to app/src/components/PlayGame.vue diff --git a/src/components/icons/IconCommunity.vue b/app/src/components/icons/IconCommunity.vue similarity index 100% rename from src/components/icons/IconCommunity.vue rename to app/src/components/icons/IconCommunity.vue diff --git a/src/components/icons/IconDocumentation.vue b/app/src/components/icons/IconDocumentation.vue similarity index 100% rename from src/components/icons/IconDocumentation.vue rename to app/src/components/icons/IconDocumentation.vue diff --git a/src/components/icons/IconEcosystem.vue b/app/src/components/icons/IconEcosystem.vue similarity index 100% rename from src/components/icons/IconEcosystem.vue rename to app/src/components/icons/IconEcosystem.vue diff --git a/src/components/icons/IconSupport.vue b/app/src/components/icons/IconSupport.vue similarity index 100% rename from src/components/icons/IconSupport.vue rename to app/src/components/icons/IconSupport.vue diff --git a/src/components/icons/IconTooling.vue b/app/src/components/icons/IconTooling.vue similarity index 100% rename from src/components/icons/IconTooling.vue rename to app/src/components/icons/IconTooling.vue diff --git a/src/crypto.ts b/app/src/crypto.ts similarity index 100% rename from src/crypto.ts rename to app/src/crypto.ts diff --git a/src/game.ts b/app/src/game.ts similarity index 100% rename from src/game.ts rename to app/src/game.ts diff --git a/src/main.ts b/app/src/main.ts similarity index 100% rename from src/main.ts rename to app/src/main.ts diff --git a/src/pages/IndexPage.vue b/app/src/pages/IndexPage.vue similarity index 100% rename from src/pages/IndexPage.vue rename to app/src/pages/IndexPage.vue diff --git a/src/pages/LoginPage.vue b/app/src/pages/LoginPage.vue similarity index 100% rename from src/pages/LoginPage.vue rename to app/src/pages/LoginPage.vue diff --git a/src/quasar-variables.sass b/app/src/quasar-variables.sass similarity index 100% rename from src/quasar-variables.sass rename to app/src/quasar-variables.sass diff --git a/src/router/index.ts b/app/src/router/index.ts similarity index 100% rename from src/router/index.ts rename to app/src/router/index.ts diff --git a/src/ws.js b/app/src/ws.js similarity index 100% rename from src/ws.js rename to app/src/ws.js diff --git a/tsconfig.app.json b/app/tsconfig.app.json similarity index 100% rename from tsconfig.app.json rename to app/tsconfig.app.json diff --git a/tsconfig.json b/app/tsconfig.json similarity index 100% rename from tsconfig.json rename to app/tsconfig.json diff --git a/tsconfig.node.json b/app/tsconfig.node.json similarity index 100% rename from tsconfig.node.json rename to app/tsconfig.node.json diff --git a/vite.config.ts b/app/vite.config.ts similarity index 100% rename from vite.config.ts rename to app/vite.config.ts diff --git a/yarn.lock b/app/yarn.lock similarity index 100% rename from yarn.lock rename to app/yarn.lock diff --git a/logic/leaderboard/Cargo.toml b/logic/leaderboard/Cargo.toml new file mode 100644 index 0000000..c95134d --- /dev/null +++ b/logic/leaderboard/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "leaderboard" +description = "leaderboard" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +near-sdk.workspace = true + +[dev-dependencies] +near-sdk = { workspace = true, features = ["unit-testing"] } +tokio.workspace = true +near-workspaces.workspace = true +serde_json.workspace = true diff --git a/logic/leaderboard/build.sh b/logic/leaderboard/build.sh new file mode 100755 index 0000000..b088a17 --- /dev/null +++ b/logic/leaderboard/build.sh @@ -0,0 +1,17 @@ +#!/bin/sh +rustup target add wasm32-unknown-unknown +set -e + +cd "$(dirname $0)" + +TARGET="${CARGO_TARGET_DIR:-../../target}" + +cargo build --target wasm32-unknown-unknown --profile app-release + +mkdir -p res + +cp $TARGET/wasm32-unknown-unknown/app-release/leaderboard.wasm ./res/ + +if command -v wasm-opt > /dev/null; then + wasm-opt -Oz ./res/leaderboard.wasm -o ./res/leaderboard.wasm +fi diff --git a/logic/leaderboard/relayer/mock-ws-server.js b/logic/leaderboard/relayer/mock-ws-server.js new file mode 100644 index 0000000..261d09a --- /dev/null +++ b/logic/leaderboard/relayer/mock-ws-server.js @@ -0,0 +1,41 @@ +const { WebSocketServer } = require('ws'); +const prompt = require('prompt-sync')({ sigint: true }); + +const wss = new WebSocketServer({ port: 8080 }); + +console.log('Waiting for a new connection...'); + +wss.on('connection', function connection(ws) { + let keepAsking = true; + ws.on('error', () => keepAsking = false); + + ws.on('message', function message(data) { + console.log('received: %s', data); + }); + + while (keepAsking) { + console.log('-'.repeat(process.stdout.columns)); + const action = prompt('What to do? add-score/get-score: '); + if (action === 'add-score') { + const account = prompt('Account: '); + const app = prompt('Application: '); + const score = parseInt(prompt('score: ')); + ws.send(JSON.stringify({ + action: 'add-score', + app, + account, + score + })); + } else if (action === 'get-score') { + const account = prompt('Account: '); + const app = prompt('Application: '); + ws.send(JSON.stringify({ + action: 'get-score', + app, + account, + })); + } else { + ws.close(); + } + } +}); diff --git a/logic/leaderboard/relayer/package.json b/logic/leaderboard/relayer/package.json new file mode 100644 index 0000000..6c94ab6 --- /dev/null +++ b/logic/leaderboard/relayer/package.json @@ -0,0 +1,13 @@ +{ + "name": "relayer", + "version": "1.0.0", + "main": "index.js", + "author": "Saeed", + "license": "MIT", + "dependencies": { + "command-line-args": "^5.2.1", + "near-api-js": "^4.0.1", + "prompt-sync": "^4.2.0", + "ws": "^8.17.0" + } +} diff --git a/logic/leaderboard/relayer/relayer.js b/logic/leaderboard/relayer/relayer.js new file mode 100644 index 0000000..9f4eba5 --- /dev/null +++ b/logic/leaderboard/relayer/relayer.js @@ -0,0 +1,149 @@ +const nearAPI = require('near-api-js'); +const fs = require('fs'); +const commandLineArgs = require('command-line-args'); +const GameEventListener = require('./ws'); + +const { Contract } = nearAPI; + +const createKeyStore = async () => { + const { KeyPair, keyStores } = nearAPI; + + const ACCOUNT_ID = 'highfalutin-act.testnet'; + const NETWORK_ID = 'testnet'; + const KEY_PATH = + '/home/saeed/.near-credentials/testnet/highfalutin-act.testnet.json'; + + const credentials = JSON.parse(fs.readFileSync(KEY_PATH)); + const myKeyStore = new keyStores.InMemoryKeyStore(); + myKeyStore.setKey( + NETWORK_ID, + ACCOUNT_ID, + KeyPair.fromString(credentials.private_key) + ); + + return myKeyStore; +}; + +let keyStore; +const connectToNear = async () => { + keyStore = await createKeyStore(); + const connectionConfig = { + networkId: 'testnet', + keyStore, + nodeUrl: 'https://rpc.testnet.near.org', + walletUrl: 'https://testnet.mynearwallet.com/', + helperUrl: 'https://helper.testnet.near.org', + explorerUrl: 'https://testnet.nearblocks.io', + }; + const { connect } = nearAPI; + const nearConnection = await connect(connectionConfig); + return nearConnection; +}; + +const addScore = async (account_id, app_name, score) => { + if (contract === null) { + throw new Error('Contract is not initialized'); + } + + const account = await near.account('highfalutin-act.testnet'); + await contract.add_score({ + signerAccount: account, + args: { + app_name, + account_id, + score, + }, + }); +}; + +const getScore = async (account_id, app_name) => { + if (contract === null) { + throw new Error('Contract is not initialized'); + } + + return await contract.get_score({ + app_name, + account_id, + }); +}; + +const getScores = async (app_name) => { + if (contract === null) { + throw new Error('Contract is not initialized'); + } + + return await contract.get_scores({ + app_name, + }); +}; + +let contract = null; +let near = null; + +async function main() { + const optionDefinitions = [ + { name: 'subscribe', type: Boolean }, + { name: 'add-score', type: Boolean }, + { name: 'get-score', type: Boolean }, + { name: 'get-scores', type: Boolean }, + { name: 'account', type: String }, + { name: 'score', type: Number }, + { name: 'app', type: String }, + { name: 'applicationId', type: String }, + { name: 'nodeUrl', type: String }, + ]; + + const options = commandLineArgs(optionDefinitions); + + const nearConnection = await connectToNear(); + near = nearConnection; + contract = new Contract( + nearConnection.connection, + 'highfalutin-act.testnet', + { + changeMethods: ['add_score'], + viewMethods: ['get_version', 'get_score', 'get_scores'], + } + ); + if (options.subscribe) { + const { applicationId, nodeUrl } = options; + console.log(`Subscribed for the events of ${applicationId}`); + subscribe(applicationId, nodeUrl); + } else if (options['add-score']) { + const { account, app, score } = options; + await addScore(account, app, score); + console.log( + `Score added for account: ${account}, app: ${app}, score: ${score}` + ); + } else if (options['get-score']) { + const { account, app } = options; + const score = await getScore(account, app); + console.log(`${account} score is: ${score}`); + } else if (options['get-scores']) { + const { app } = options; + const scores = await getScores(app); + console.log(`Scores for ${app}: ${JSON.stringify(scores)}`); + } +} + +let eventListener; +let players = {}; +const subscribe = (applicationId, nodeUrl) => { + eventListener = new GameEventListener(nodeUrl, applicationId); + eventListener.on('NewPlayer', (player) => { + players[player.id] = player.name; + }); + + eventListener.on('GameOver', (winner) => { + addScore(players[winner.winner], 'rsp', 1000).then(() => + console.log(`Score added for ${players[winner.winner]}`) + ).catch(e => { + console.error(`Failed to add the score. ${e}`); + }); + }); +}; + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/logic/leaderboard/relayer/ws.js b/logic/leaderboard/relayer/ws.js new file mode 100644 index 0000000..334e52c --- /dev/null +++ b/logic/leaderboard/relayer/ws.js @@ -0,0 +1,50 @@ + +// Listen for messages +const WebSocket = require('ws'); + +module.exports = class GameEventListener { + constructor(nodeUrl, applicationId) { + this.ws = new WebSocket(`${nodeUrl}/ws`); + this.ws.on('open', () => { + const request = { + id: this.getRandomRequestId(), + method: 'subscribe', + params: { + applicationIds: [applicationId], + }, + }; + this.ws.send(JSON.stringify(request)); + }); + + this.events = {}; + this.ws.on('message', async (event) => { + const utf8Decoder = new TextDecoder('UTF-8'); + const data = utf8Decoder.decode(event); + await this.parseMessage(data); + }); + } + + on(event, func) { + this.events[event] = func; + } + + parseMessage(msg) { + try { + const event = JSON.parse(msg); + for (const e of event.result.data.events) { + if (e.kind in this.events) { + let bytes = new Int8Array(e.data); + let str = new TextDecoder().decode(bytes); + this.events[e.kind](JSON.parse(str)); + } + } + } catch (e) { + console.error(`Failed to parse the json: ${e}`); + } + } + + getRandomRequestId() { + return Math.floor(Math.random() * Math.pow(2, 32)); + } +}; + diff --git a/logic/leaderboard/relayer/yarn.lock b/logic/leaderboard/relayer/yarn.lock new file mode 100644 index 0000000..7f09932 --- /dev/null +++ b/logic/leaderboard/relayer/yarn.lock @@ -0,0 +1,379 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@near-js/accounts@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@near-js/accounts/-/accounts-1.2.0.tgz#d0ffe0ddd976c04b0f27122d4bd737202fb82747" + integrity sha512-0D/Tl7i2rqqVydGwu9zWBFOk6P7t4Zs2Gfo7l+8jNjOoioYsH/YCWaOheoH7SVu4wQ3xP9YEyjvZ8JL6xzYyHA== + dependencies: + "@near-js/crypto" "1.2.3" + "@near-js/providers" "0.2.1" + "@near-js/signers" "0.1.3" + "@near-js/transactions" "1.2.1" + "@near-js/types" "0.2.0" + "@near-js/utils" "0.2.1" + borsh "1.0.0" + depd "2.0.0" + is-my-json-valid "^2.20.6" + lru_map "0.4.1" + near-abi "0.1.1" + +"@near-js/crypto@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@near-js/crypto/-/crypto-1.2.3.tgz#ba318d77b9eed79ef92a86f7a2c84562cb2f6b9d" + integrity sha512-BuNE+tdcxwImxktFtuAxLiVejFDtn1X92kejcDcYc6f7e0ku9yMntdw98LMb+5ls+xlRuF1UDoi/hUF1LPVpyQ== + dependencies: + "@near-js/types" "0.2.0" + "@near-js/utils" "0.2.1" + "@noble/curves" "1.2.0" + borsh "1.0.0" + randombytes "2.1.0" + +"@near-js/keystores-browser@0.0.11": + version "0.0.11" + resolved "https://registry.yarnpkg.com/@near-js/keystores-browser/-/keystores-browser-0.0.11.tgz#dc0dab662fb2045f978fe1c725f03f0fd11f8267" + integrity sha512-AQ86ST+keKjM5iektKLXu3q94lN8pG6R/LXVoIgm5/hi63n2QzhAd0XlUj9fcyPrfoGOckwUu6pFtXPbveBypw== + dependencies: + "@near-js/crypto" "1.2.3" + "@near-js/keystores" "0.0.11" + +"@near-js/keystores-node@0.0.11": + version "0.0.11" + resolved "https://registry.yarnpkg.com/@near-js/keystores-node/-/keystores-node-0.0.11.tgz#b711c6fc0451d4115936734690e08de51e440ef4" + integrity sha512-KeBl7oL8AwUwTilYPV3apEcL1P+UMAGJQvmkEFl9lyK7mftyjogehdqjqFREAdQpR+4jX5NXvU8ZJIShebK3ZA== + dependencies: + "@near-js/crypto" "1.2.3" + "@near-js/keystores" "0.0.11" + +"@near-js/keystores@0.0.11": + version "0.0.11" + resolved "https://registry.yarnpkg.com/@near-js/keystores/-/keystores-0.0.11.tgz#570c70c4c5bb6ba64a94b8bff4cc71cc23265aec" + integrity sha512-B/VkSNIT8vxMozDbK9O54YQGa4JT/rFnB0W+0cN3na38sQHdvzK015X2RHK8mfS0isP/iIT9QzIQtYZcI3M83Q== + dependencies: + "@near-js/crypto" "1.2.3" + "@near-js/types" "0.2.0" + +"@near-js/providers@0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@near-js/providers/-/providers-0.2.1.tgz#1d195638b07fd542e202a61dd8c571034ec7cd62" + integrity sha512-F5ZVlXynGopg3BjK3ihyA28tnOk/cM7kUhc/bw5aJg+m+oa1yuBkaAp9JbihagbLZpWOZiDJmkrdkpvTvQlHag== + dependencies: + "@near-js/transactions" "1.2.1" + "@near-js/types" "0.2.0" + "@near-js/utils" "0.2.1" + borsh "1.0.0" + http-errors "1.7.2" + optionalDependencies: + node-fetch "2.6.7" + +"@near-js/signers@0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@near-js/signers/-/signers-0.1.3.tgz#7ac9c630536457c3cd94c4faf901b7033a76b6bc" + integrity sha512-Eim6ZsQUgsaSzi+oyR9cQesOO2QcZmhK+tawZan1vni8y+JvKnSH6r3krzbtvKWqIlx/kJ+PsIV74YIxPY5Uhw== + dependencies: + "@near-js/crypto" "1.2.3" + "@near-js/keystores" "0.0.11" + "@noble/hashes" "1.3.3" + +"@near-js/transactions@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@near-js/transactions/-/transactions-1.2.1.tgz#f9a304cd2a35f292557c3764473127b231c06892" + integrity sha512-w2EXgTRXJ+Zxqh8lVnQuRnpCEm6Cq7NxqAcfH6x0BPuSXye5kR9d0n2ut8AGkSXWeooKKEUnDhi6UcXadfoerg== + dependencies: + "@near-js/crypto" "1.2.3" + "@near-js/signers" "0.1.3" + "@near-js/types" "0.2.0" + "@near-js/utils" "0.2.1" + "@noble/hashes" "1.3.3" + borsh "1.0.0" + +"@near-js/types@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@near-js/types/-/types-0.2.0.tgz#5370c3e9230103222b2827dbd6370f03c4e996d1" + integrity sha512-pTahjni0+PzStseFtnnI9nqmh+ZrHqBqeERo3B3OCXUC/qEie0ZSBMSMt80SgqnaGAy5/CqkCLO9zOx1gA8Cwg== + +"@near-js/utils@0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@near-js/utils/-/utils-0.2.1.tgz#6798cf8c3a6ed8057da002401e24409c49454a82" + integrity sha512-u7yR1fmxIcYoiITR1spTvqciXbMXNvlrmRcneNt9DWeQP7yPdbCQtRB7lMN2KI7ONkUf3U7xiheQDDmk2vFI0w== + dependencies: + "@near-js/types" "0.2.0" + bs58 "4.0.0" + depd "2.0.0" + mustache "4.0.0" + +"@near-js/wallet-account@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@near-js/wallet-account/-/wallet-account-1.2.1.tgz#f94ebd9c0e58e437045e17467c283d692c3ad6e4" + integrity sha512-T1k15LN9YIgz1Ca3u76GFxtyDSSKNeBTqEKOJZiOMPse9HjXeiI/ycrOVzmEG/a+ZJ5tipQwcDDChUsY4nTQ1w== + dependencies: + "@near-js/accounts" "1.2.0" + "@near-js/crypto" "1.2.3" + "@near-js/keystores" "0.0.11" + "@near-js/providers" "0.2.1" + "@near-js/signers" "0.1.3" + "@near-js/transactions" "1.2.1" + "@near-js/types" "0.2.0" + "@near-js/utils" "0.2.1" + borsh "1.0.0" + +"@noble/curves@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" + integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== + dependencies: + "@noble/hashes" "1.3.2" + +"@noble/hashes@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== + +"@noble/hashes@1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" + integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== + +"@types/json-schema@^7.0.11": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +ansi-regex@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== + +array-back@^3.0.1, array-back@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" + integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== + +base-x@^2.0.1: + version "2.0.6" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-2.0.6.tgz#4582a91ebcec99ee06f4e4032030b0cf1c2941d8" + integrity sha512-UAmjxz9KbK+YIi66xej+pZVo/vxUOh49ubEvZW5egCbxhur05pBb+hwuireQwKO4nDpsNm64/jEei17LEpsr5g== + dependencies: + safe-buffer "^5.0.1" + +borsh@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/borsh/-/borsh-1.0.0.tgz#b564c8cc8f7a91e3772b9aef9e07f62b84213c1f" + integrity sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ== + +bs58@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.0.tgz#65f5deaf6d74e6135a99f763ca6209ab424b9172" + integrity sha512-/jcGuUuSebyxwLLfKrbKnCJttxRf9PM51EnHTwmFKBxl4z1SGkoAhrfd6uZKE0dcjQTfm6XzTP8DPr1tzE4KIw== + dependencies: + base-x "^2.0.1" + +command-line-args@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e" + integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg== + dependencies: + array-back "^3.1.0" + find-replace "^3.0.0" + lodash.camelcase "^4.3.0" + typical "^4.0.0" + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +find-replace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" + integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== + dependencies: + array-back "^3.0.1" + +generate-function@^2.0.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" + integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== + dependencies: + is-property "^1.0.2" + +generate-object-property@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" + integrity sha512-TuOwZWgJ2VAMEGJvAyPWvpqxSANF0LDpmyHauMjFYzaACvn+QTT/AZomvPCzVBV7yDN3OmwHQ5OvHaeLKre3JQ== + dependencies: + is-property "^1.0.0" + +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + +is-my-ip-valid@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.1.tgz#f7220d1146257c98672e6fba097a9f3f2d348442" + integrity sha512-jxc8cBcOWbNK2i2aTkCZP6i7wkHF1bqKFrwEHuN5Jtg5BSaZHUZQ/JTOJwoV41YvHnOaRyWWh72T/KvfNz9DJg== + +is-my-json-valid@^2.20.6: + version "2.20.6" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.20.6.tgz#a9d89e56a36493c77bda1440d69ae0dc46a08387" + integrity sha512-1JQwulVNjx8UqkPE/bqDaxtH4PXCe/2VRh/y3p99heOV87HG4Id5/VfDswd+YiAfHcRTfDlWgISycnHuhZq1aw== + dependencies: + generate-function "^2.0.0" + generate-object-property "^1.1.0" + is-my-ip-valid "^1.0.0" + jsonpointer "^5.0.0" + xtend "^4.0.0" + +is-property@^1.0.0, is-property@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + integrity sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g== + +jsonpointer@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" + integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + +lru_map@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.4.1.tgz#f7b4046283c79fb7370c36f8fca6aee4324b0a98" + integrity sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg== + +mustache@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.0.0.tgz#7f02465dbb5b435859d154831c032acdfbbefb31" + integrity sha512-FJgjyX/IVkbXBXYUwH+OYwQKqWpFPLaLVESd70yHjSDunwzV2hZOoTBvPf4KLoxesUzzyfTH6F784Uqd7Wm5yA== + +near-abi@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/near-abi/-/near-abi-0.1.1.tgz#b7ead408ca4ad11de4fe3e595d30a7a8bc5307e0" + integrity sha512-RVDI8O+KVxRpC3KycJ1bpfVj9Zv+xvq9PlW1yIFl46GhrnLw83/72HqHGjGDjQ8DtltkcpSjY9X3YIGZ+1QyzQ== + dependencies: + "@types/json-schema" "^7.0.11" + +near-api-js@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/near-api-js/-/near-api-js-4.0.1.tgz#ac99ef0fc11f24a733ba9c93bd7f094311f891dd" + integrity sha512-lBLgxhXhY7M05UI0ppBOzfArkMCXRvtKiHVgpxrSqN/mp3WCcWV5C+Y/dwx9U6JtaNG8UWxrvB8kHqKyWGeisw== + dependencies: + "@near-js/accounts" "1.2.0" + "@near-js/crypto" "1.2.3" + "@near-js/keystores" "0.0.11" + "@near-js/keystores-browser" "0.0.11" + "@near-js/keystores-node" "0.0.11" + "@near-js/providers" "0.2.1" + "@near-js/signers" "0.1.3" + "@near-js/transactions" "1.2.1" + "@near-js/types" "0.2.0" + "@near-js/utils" "0.2.1" + "@near-js/wallet-account" "1.2.1" + "@noble/curves" "1.2.0" + borsh "1.0.0" + depd "2.0.0" + http-errors "1.7.2" + near-abi "0.1.1" + node-fetch "2.6.7" + +node-fetch@2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + +prompt-sync@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/prompt-sync/-/prompt-sync-4.2.0.tgz#0198f73c5b70e3b03e4b9033a50540a7c9a1d7f4" + integrity sha512-BuEzzc5zptP5LsgV5MZETjDaKSWfchl5U9Luiu8SKp7iZWD5tZalOxvNcZRwv+d2phNFr8xlbxmFNcRKfJOzJw== + dependencies: + strip-ansi "^5.0.0" + +randombytes@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +safe-buffer@^5.0.1, safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +"statuses@>= 1.5.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +strip-ansi@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +typical@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" + integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +ws@^8.17.0: + version "8.17.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea" + integrity sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow== + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== diff --git a/logic/leaderboard/res/leaderboard.wasm b/logic/leaderboard/res/leaderboard.wasm new file mode 100755 index 0000000..3e0376b Binary files /dev/null and b/logic/leaderboard/res/leaderboard.wasm differ diff --git a/logic/leaderboard/src/lib.rs b/logic/leaderboard/src/lib.rs new file mode 100644 index 0000000..745f4a2 --- /dev/null +++ b/logic/leaderboard/src/lib.rs @@ -0,0 +1,85 @@ +use std::collections::BTreeMap; + +use near_sdk::json_types::U128; +use near_sdk::near; +use near_sdk::store::{LookupMap, UnorderedMap}; + +type UserName = String; + +#[near(contract_state)] +pub struct LeaderBoard { + scores: LookupMap>, // Key is app name, value is the leaderboard itself +} + +impl Default for LeaderBoard { + fn default() -> Self { + Self { + scores: LookupMap::new(b"m"), + } + } +} + +#[near] +impl LeaderBoard { + pub fn add_score(&mut self, app_name: String, account_id: UserName, score: U128) { + let app_leaderboard = self + .scores + .entry(app_name.clone()) + .or_insert_with(|| UnorderedMap::new(app_name.as_bytes())); + + let new_score = app_leaderboard.entry(account_id.clone()).or_default().0 + score.0; + app_leaderboard.insert(account_id, U128(new_score)); + } + + pub fn get_score(&self, app_name: String, account_id: UserName) -> Option { + self.scores + .get(&app_name)? + .get(&account_id) + .map(|score| score.clone()) + } + + pub fn get_scores(&self, app_name: String) -> Option> { + let mut map = BTreeMap::new(); + for (k, v) in self.scores.get(&app_name)? { + map.insert(k.to_string(), v.0); + } + Some(map) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn add_score() { + let mut leader_board = LeaderBoard::default(); + let account = "alice.testnet".to_string(); + leader_board.add_score("test_app".to_string(), account.clone(), U128(10)); + + let score = leader_board.get_score("test_app".to_string(), account); + assert_eq!(score, Some(U128(10))); + } + + #[test] + fn get_score_of_absent_account() { + let mut leader_board = LeaderBoard::default(); + let account = "alice.testnet".to_string(); + leader_board.add_score("test_app".to_string(), account.clone(), U128(10)); + + let bob_account = "bob.testnet".to_string(); + + let score = leader_board.get_score("test_app".to_string(), bob_account); + assert_eq!(score, None); + } + + #[test] + fn get_score_of_absent_app() { + let mut leader_board = LeaderBoard::default(); + let account = "alice.testnet".to_string(); + leader_board.add_score("test_app".to_string(), account.clone(), U128(10)); + + let score = leader_board.get_score("test_app_2".to_string(), account); + assert_eq!(score, None); + } +} diff --git a/logic/leaderboard/tests/sandbox.rs b/logic/leaderboard/tests/sandbox.rs new file mode 100644 index 0000000..df615cf --- /dev/null +++ b/logic/leaderboard/tests/sandbox.rs @@ -0,0 +1,66 @@ +use near_sdk::json_types::U128; +use near_sdk::NearToken; +use serde_json::json; + +#[tokio::test] +async fn test_score_board_contract() -> Result<(), Box> { + let sandbox = near_workspaces::sandbox().await?; + let wasm = tokio::fs::read("res/leaderboard.wasm").await?; + + let contract = sandbox.dev_deploy(&wasm).await?; + + let alice_account = sandbox.dev_create_account().await?; + let bob_account = sandbox.dev_create_account().await?; + + let alice_outcome = alice_account + .call(contract.id(), "add_score") + .args_json(json!({"app_name": "test_app", "account_id": alice_account.id(), "score": "10"})) + .deposit(NearToken::from_near(0)) + .transact() + .await?; + + assert!(alice_outcome.is_success()); + + let score: Option = contract + .view("get_score") + .args_json(json!({"app_name": "test_app", "account_id": alice_account.id()})) + .await? + .json()?; + + assert_eq!(score, Some(U128(10))); + + let score: Option = contract + .view("get_score") + .args_json(json!({"app_name": "test_app", "account_id": bob_account.id()})) + .await? + .json()?; + + assert_eq!(score, None); + + let alice_outcome = alice_account + .call(contract.id(), "add_score") + .args_json( + json!({"app_name": "test_app_2", "account_id": alice_account.id(), "score": "100"}), + ) + .deposit(NearToken::from_near(0)) + .transact() + .await?; + + assert!(alice_outcome.is_success()); + + let score: Option = contract + .view("get_score") + .args_json(json!({"app_name": "test_app", "account_id": alice_account.id()})) + .await? + .json()?; + assert_eq!(score, Some(U128(10))); + + let score: Option = contract + .view("get_score") + .args_json(json!({"app_name": "test_app_2", "account_id": alice_account.id()})) + .await? + .json()?; + assert_eq!(score, Some(U128(100))); + + Ok(()) +} diff --git a/logic/rock-paper-scissors/Cargo.toml b/logic/rock-paper-scissors/Cargo.toml new file mode 100644 index 0000000..5f650be --- /dev/null +++ b/logic/rock-paper-scissors/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "rock-paper-scissors" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true + +[lib] +crate-type = ["cdylib"] + +[dependencies] +bs58.workspace = true +calimero-sdk = { path = "../../crates/sdk" } +ed25519-dalek = { workspace = true, features = ["rand_core"] } +rand_chacha.workspace = true +sha3.workspace = true diff --git a/logic/rock-paper-scissors/build.sh b/logic/rock-paper-scissors/build.sh new file mode 100755 index 0000000..cdf7c2b --- /dev/null +++ b/logic/rock-paper-scissors/build.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e + +cd "$(dirname $0)" + +TARGET="${CARGO_TARGET_DIR:-../../target}" + +rustup target add wasm32-unknown-unknown + +cargo build --target wasm32-unknown-unknown --profile app-release + +mkdir -p res + +cp $TARGET/wasm32-unknown-unknown/app-release/rock_paper_scissors.wasm ./res/ + +if command -v wasm-opt > /dev/null; then + wasm-opt -Oz ./res/rock_paper_scissors.wasm -o ./res/rock_paper_scissors.wasm +fi diff --git a/logic/rock-paper-scissors/res/rock_paper_scissors.wasm b/logic/rock-paper-scissors/res/rock_paper_scissors.wasm new file mode 100755 index 0000000..9b70f80 Binary files /dev/null and b/logic/rock-paper-scissors/res/rock_paper_scissors.wasm differ diff --git a/logic/rock-paper-scissors/src/choice.rs b/logic/rock-paper-scissors/src/choice.rs new file mode 100644 index 0000000..653c1e6 --- /dev/null +++ b/logic/rock-paper-scissors/src/choice.rs @@ -0,0 +1,44 @@ +use std::cmp::Ordering; + +use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize}; +use calimero_sdk::serde::{Deserialize, Serialize}; + +use crate::commit::{Commitment, Nonce}; + +#[derive( + Eq, Copy, Clone, Debug, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, +)] +#[borsh(crate = "calimero_sdk::borsh")] +#[serde(crate = "calimero_sdk::serde")] +#[repr(u8)] +pub enum Choice { + Rock, + Paper, + Scissors, +} + +use Choice::*; + +impl Choice { + pub fn determine(commitment: &Commitment, nonce: &Nonce) -> Option { + let choices = [Rock, Paper, Scissors]; + + for choice in choices { + if *commitment == Commitment::of(choice, nonce) { + return Some(choice); + } + } + + None + } +} + +impl PartialOrd for Choice { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (Rock, Scissors) | (Scissors, Paper) | (Paper, Rock) => Some(Ordering::Greater), + (Scissors, Rock) | (Paper, Scissors) | (Rock, Paper) => Some(Ordering::Less), + _ => Some(Ordering::Equal), + } + } +} diff --git a/logic/rock-paper-scissors/src/commit.rs b/logic/rock-paper-scissors/src/commit.rs new file mode 100644 index 0000000..06f2c42 --- /dev/null +++ b/logic/rock-paper-scissors/src/commit.rs @@ -0,0 +1,35 @@ +use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize}; +use sha3::{Digest, Sha3_256}; + +use crate::Choice; + +#[derive(Eq, Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)] +#[borsh(crate = "calimero_sdk::borsh")] +pub struct Commitment([u8; 32]); + +pub type Nonce = [u8; 32]; + +impl Commitment { + pub fn of(choice: Choice, nonce: &Nonce) -> Self { + let mut hasher = Sha3_256::new(); + + hasher.update(&[choice as u8]); + hasher.update(nonce); + + Commitment(hasher.finalize().into()) + } + + pub const fn from_bytes(bytes: &[u8; 32]) -> Self { + Commitment(*bytes) + } + + pub const fn to_bytes(&self) -> [u8; 32] { + self.0 + } +} + +impl AsRef<[u8]> for Commitment { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} diff --git a/logic/rock-paper-scissors/src/errors.rs b/logic/rock-paper-scissors/src/errors.rs new file mode 100644 index 0000000..2d53551 --- /dev/null +++ b/logic/rock-paper-scissors/src/errors.rs @@ -0,0 +1,30 @@ +use calimero_sdk::serde::Serialize; + +#[derive(Debug, Serialize)] +#[serde(crate = "calimero_sdk::serde")] +pub enum JoinError { + GameFull, +} + +#[derive(Debug, Serialize)] +#[serde(crate = "calimero_sdk::serde")] +pub enum CommitError { + NotReady, + AlreadyCommitted, + InvalidSignature, +} + +#[derive(Debug, Serialize)] +#[serde(crate = "calimero_sdk::serde")] +pub enum RevealError { + NotReady, + NotCommitted, + InvalidNonce, +} + +#[derive(Debug, Serialize)] +#[serde(crate = "calimero_sdk::serde")] +pub enum ResetError { + NotReady, + InvalidSignature, +} diff --git a/logic/rock-paper-scissors/src/key.rs b/logic/rock-paper-scissors/src/key.rs new file mode 100644 index 0000000..f091757 --- /dev/null +++ b/logic/rock-paper-scissors/src/key.rs @@ -0,0 +1,82 @@ +use calimero_sdk::serde::{Deserialize, Serialize}; +use ed25519_dalek::{Signature, SigningKey, VerifyingKey}; + +use crate::commit::Commitment; +use crate::repr::{Repr, ReprBytes}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(crate = "calimero_sdk::serde")] +pub struct KeyComponents { + pub pk: Repr, + pub sk: Repr, +} + +impl ReprBytes for VerifyingKey { + type Bytes = [u8; 32]; + + fn to_bytes(&self) -> Self::Bytes { + self.to_bytes() + } + + fn from_bytes(f: F) -> Option> + where + F: FnOnce(&mut Self::Bytes) -> Option, + { + let mut bytes = [0; 32]; + if let Some(err) = f(&mut bytes) { + return Some(Err(err)); + } + Some(Ok(VerifyingKey::from_bytes(&bytes).ok()?)) + } +} + +impl ReprBytes for SigningKey { + type Bytes = [u8; 32]; + + fn to_bytes(&self) -> Self::Bytes { + self.to_bytes() + } + + fn from_bytes(f: F) -> Option> + where + F: FnOnce(&mut Self::Bytes) -> Option, + { + let mut bytes = [0; 32]; + + Some(f(&mut bytes).map_or_else(|| Ok(SigningKey::from_bytes(&bytes)), Err)) + } +} + +impl ReprBytes for Signature { + type Bytes = [u8; 64]; + + fn to_bytes(&self) -> Self::Bytes { + self.to_bytes() + } + + fn from_bytes(f: F) -> Option> + where + F: FnOnce(&mut Self::Bytes) -> Option, + { + let mut bytes = [0; 64]; + + Some(f(&mut bytes).map_or_else(|| Ok(Signature::from_bytes(&bytes)), Err)) + } +} + +impl ReprBytes for Commitment { + type Bytes = [u8; 32]; + + fn to_bytes(&self) -> Self::Bytes { + self.to_bytes() + } + + fn from_bytes(f: F) -> Option> + where + F: FnOnce(&mut Self::Bytes) -> Option, + { + let mut bytes = [0; 32]; + + Some(f(&mut bytes).map_or_else(|| Ok(Commitment::from_bytes(&bytes)), Err)) + } +} diff --git a/logic/rock-paper-scissors/src/lib.rs b/logic/rock-paper-scissors/src/lib.rs new file mode 100644 index 0000000..79b81a9 --- /dev/null +++ b/logic/rock-paper-scissors/src/lib.rs @@ -0,0 +1,217 @@ +use std::cmp::Ordering; + +use calimero_sdk::app; +use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize}; +use calimero_sdk::serde::{Deserialize, Serialize}; +use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; +use rand_chacha::rand_core::SeedableRng; +use rand_chacha::ChaCha20Rng; + +mod choice; +mod commit; +mod errors; +mod key; +mod player_idx; +mod repr; + +use choice::Choice; +use commit::{Commitment, Nonce}; +use errors::{CommitError, JoinError, ResetError, RevealError}; +use key::KeyComponents; +use player_idx::PlayerIdx; +use repr::Repr; + +#[app::state(emits = for<'a> Event<'a>)] +#[derive(Default, Debug, BorshSerialize, BorshDeserialize)] +#[borsh(crate = "calimero_sdk::borsh")] +struct Game { + players: [Option; 2], +} + +#[derive(Default, Debug, BorshSerialize, BorshDeserialize)] +#[borsh(crate = "calimero_sdk::borsh")] +struct Player { + state: Option, + public_key: Repr, + name: String, +} + +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize, Deserialize, Serialize)] +#[borsh(crate = "calimero_sdk::borsh")] +#[serde(crate = "calimero_sdk::serde")] +enum State { + Committed(Repr), + Revealed(Choice), +} + +#[app::event] +pub enum Event<'a> { + PlayerCommited { id: PlayerIdx }, + NewPlayer { id: PlayerIdx, name: &'a str }, + PlayerRevealed { id: PlayerIdx, reveal: &'a Choice }, + GameOver { winner: Option }, + StateDumped, +} + +pub type Seed = [u8; 32]; + +#[app::logic] +impl Game { + pub fn create_keypair(seed: Seed) -> KeyComponents { + let mut csprng = ChaCha20Rng::from_seed(seed); + + let keypair = SigningKey::generate(&mut csprng); + + KeyComponents { + pk: Repr::from(keypair.verifying_key()), + sk: Repr::from(keypair), + } + } + + pub fn join( + &mut self, + player_name: String, + public_key: Repr, + ) -> Result { + let Some((index, player)) = self + .players + .iter_mut() + .enumerate() + .find(|(_, player)| player.is_none()) + else { + return Err(JoinError::GameFull); + }; + + app::emit!(Event::NewPlayer { + id: PlayerIdx(index), + name: &player_name + }); + + *player = Some(Player { + state: None, + public_key: Repr::from(public_key), + name: player_name, + }); + + Ok(index) + } + + pub fn state(&self) -> [Option<(&str, &State)>; 2] { + let mut states = [None, None]; + + for (i, player) in self.players.iter().enumerate() { + if let Some(Player { + state: Some(state), + name, + .. + }) = player + { + states[i] = Some((name.as_str(), state)); + } + } + + states + } + + pub fn prepare( + signing_key: Repr, + choice: Choice, + nonce: Nonce, + ) -> (Repr, Repr) { + let commitment = Commitment::of(choice, &nonce); + + let signature = signing_key.sign(commitment.as_ref()); + + (Repr::from(commitment), Repr::from(signature)) + } + + fn players(&mut self, my_idx: PlayerIdx) -> (Option<&mut Player>, Option<&mut Player>) { + let [a, b] = self.players.each_mut(); + if my_idx.is_first() { + return (a.as_mut(), b.as_mut()); + } + (b.as_mut(), a.as_mut()) + } + + pub fn commit( + &mut self, + player_idx: PlayerIdx, + commitment: Repr, + signature: Repr, + ) -> Result<(), CommitError> { + let (Some(player), Some(_)) = self.players(player_idx) else { + return Err(CommitError::NotReady); + }; + + if player.state.is_some() { + return Err(CommitError::AlreadyCommitted); + } + + player + .public_key + .verify(commitment.as_ref(), &signature) + .map_err(|_| CommitError::InvalidSignature)?; + + app::emit!(Event::PlayerCommited { id: player_idx }); + + player.state = Some(State::Committed(commitment)); + + Ok(()) + } + + pub fn reveal(&mut self, player_idx: PlayerIdx, nonce: Nonce) -> Result<(), RevealError> { + let (Some(player), Some(other_player)) = self.players(player_idx) else { + return Err(RevealError::NotReady); + }; + + let Some(State::Committed(commitment)) = &player.state else { + return Err(RevealError::NotCommitted); + }; + + let choice = Choice::determine(commitment, &nonce).ok_or(RevealError::InvalidNonce)?; + + app::emit!(Event::PlayerRevealed { + id: player_idx, + reveal: &choice + }); + + player.state = Some(State::Revealed(choice)); + + if let Some(State::Revealed(other)) = &other_player.state { + match choice.partial_cmp(other) { + Some(Ordering::Less) => app::emit!(Event::GameOver { + winner: Some(player_idx.other()) + }), + Some(Ordering::Equal) => app::emit!(Event::GameOver { winner: None }), + Some(Ordering::Greater) => app::emit!(Event::GameOver { + winner: Some(player_idx) + }), + None => {} + } + } + + Ok(()) + } + + pub fn reset( + &mut self, + player_idx: PlayerIdx, + commitment: Repr, + signature: Repr, + ) -> Result<(), ResetError> { + let (Some(player), _) = self.players(player_idx) else { + return Err(ResetError::NotReady); + }; + + player + .public_key + .verify(commitment.as_ref(), &signature) + .map_err(|_| ResetError::InvalidSignature)?; + + self.players = Default::default(); + + app::emit!(Event::StateDumped); + + Ok(()) + } +} diff --git a/logic/rock-paper-scissors/src/player_idx.rs b/logic/rock-paper-scissors/src/player_idx.rs new file mode 100644 index 0000000..8ed00bd --- /dev/null +++ b/logic/rock-paper-scissors/src/player_idx.rs @@ -0,0 +1,30 @@ +use calimero_sdk::serde::{Deserialize, Deserializer, Serialize}; + +#[derive(Copy, Clone, Serialize)] +#[serde(crate = "calimero_sdk::serde")] +pub struct PlayerIdx(pub usize); + +impl PlayerIdx { + pub fn other(&self) -> PlayerIdx { + PlayerIdx(1 - self.0) + } + + pub fn is_first(&self) -> bool { + self.0 == 0 + } +} + +impl<'de> Deserialize<'de> for PlayerIdx { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = Deserialize::deserialize(deserializer)?; + match value { + 0 | 1 => Ok(PlayerIdx(value)), + _ => Err(calimero_sdk::serde::de::Error::custom( + "Player index must be 0 or 1", + )), + } + } +} diff --git a/logic/rock-paper-scissors/src/repr.rs b/logic/rock-paper-scissors/src/repr.rs new file mode 100644 index 0000000..e90c5f0 --- /dev/null +++ b/logic/rock-paper-scissors/src/repr.rs @@ -0,0 +1,152 @@ +use std::marker::PhantomData; +use std::ops::Deref; +use std::{fmt, io}; + +use bs58::decode::DecodeTarget; +use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize}; +use calimero_sdk::serde::{de, ser, Deserialize, Serialize}; + +#[derive(Eq, Copy, Clone, PartialEq)] +pub enum Bs58 {} + +#[derive(Eq, Copy, Clone, PartialEq)] +pub enum Raw {} + +mod private { + pub trait Sealed {} +} + +pub trait ReprFormat: private::Sealed {} + +impl private::Sealed for Bs58 {} +impl ReprFormat for Bs58 {} + +impl private::Sealed for Raw {} +impl ReprFormat for Raw {} + +#[derive(Eq, Copy, Clone, PartialEq)] +pub struct Repr { + data: T, + _phantom: PhantomData, +} + +pub trait ReprBytes { + type Bytes: AsRef<[u8]>; + + fn to_bytes(&self) -> Self::Bytes; + fn from_bytes(f: F) -> Option> + where + F: FnOnce(&mut Self::Bytes) -> Option, + Self: Sized; +} + +impl From for Repr { + fn from(data: T) -> Self { + Repr { + data, + _phantom: PhantomData, + } + } +} + +impl From> for Repr { + fn from(repr: Repr) -> Self { + Repr { + data: repr.data, + _phantom: PhantomData, + } + } +} + +impl Deref for Repr { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl Default for Repr { + fn default() -> Self { + Repr::from(T::default()) + } +} + +impl fmt::Debug for Repr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.data.fmt(f) + } +} + +impl Serialize for Repr { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + let bytes = self.data.to_bytes(); + let encoded = bs58::encode(bytes).into_string(); + serializer.serialize_str(&encoded) + } +} + +impl<'de, T: ReprBytes> Deserialize<'de> for Repr +where + T::Bytes: DecodeTarget, +{ + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let encoded = ::deserialize(deserializer)?; + + let data = match T::from_bytes(|bytes| bs58::decode(&encoded).onto(bytes).err()) { + Some(data) => data.map_err(de::Error::custom)?, + None => return Err(de::Error::custom("Invalid key")), + }; + + Ok(Repr::from(data)) + } +} + +impl BorshSerialize for Repr +where + T::Bytes: BorshSerialize, +{ + fn serialize(&self, writer: &mut W) -> Result<(), io::Error> { + self.data.to_bytes().serialize(writer) + } +} + +impl BorshDeserialize for Repr +where + T::Bytes: BorshDeserialize, +{ + fn deserialize_reader(reader: &mut R) -> io::Result { + let bytes = T::Bytes::deserialize_reader(reader)?; + + let data = match T::from_bytes(|data| { + *data = bytes; + + None::<()> + }) { + Some(data) => unsafe { data.unwrap_unchecked() }, + None => return Err(io::ErrorKind::InvalidData.into()), + }; + + Ok(Repr::from(data)) + } +} + +impl BorshSerialize for Repr { + fn serialize(&self, writer: &mut W) -> Result<(), io::Error> { + self.data.serialize(writer) + } +} + +impl BorshDeserialize for Repr { + fn deserialize_reader(reader: &mut R) -> io::Result { + let data = T::deserialize_reader(reader)?; + + Ok(Repr::from(data)) + } +}