diff --git a/.github/workflows/nextjs.yml b/.github/workflows/nextjs.yml index 8c031b2..86a115b 100644 --- a/.github/workflows/nextjs.yml +++ b/.github/workflows/nextjs.yml @@ -11,6 +11,7 @@ on: # Runs on pushes targeting the default branch push: branches: ["main"] + paths: ["frontend/**"] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -34,15 +35,18 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10 - name: Setup Node uses: actions/setup-node@v4 with: - node-version: "20" - - name: Install and test modules + node-version: "24" + - name: Install and build run: | cd ./frontend - yarn - yarn build + pnpm install + pnpm run build - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: diff --git a/.github/workflows/tests-rs.yml b/.github/workflows/tests-rs.yml index 96e34ad..991f472 100644 --- a/.github/workflows/tests-rs.yml +++ b/.github/workflows/tests-rs.yml @@ -1,5 +1,12 @@ name: Tests Contract RS -on: push +on: + push: + paths: ["contract-rs/**"] + pull_request: + paths: ["contract-rs/**"] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: workflows: strategy: @@ -8,9 +15,19 @@ jobs: runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v4 + + - name: Install system dependencies + if: matrix.platform == 'ubuntu-latest' + run: sudo apt-get update && sudo apt-get install -y libudev-dev + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: contract-rs + - name: Install cargo-near run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/near/cargo-near/releases/latest/download/cargo-near-installer.sh | sh - - name: Install and test modules + + - name: Test contract run: | cd ./contract-rs cargo test diff --git a/.github/workflows/tests-ts.yml b/.github/workflows/tests-ts.yml index a06b46b..4946f44 100644 --- a/.github/workflows/tests-ts.yml +++ b/.github/workflows/tests-ts.yml @@ -1,9 +1,12 @@ name: Tests Contract TS on: - pull_request: push: - branches: - - main + paths: ["contract-ts/**"] + pull_request: + paths: ["contract-ts/**"] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: workflows: strategy: @@ -11,12 +14,15 @@ jobs: platform: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "24" - name: Install and test modules run: | cd ./contract-ts - yarn - yarn test + pnpm install + pnpm test diff --git a/contract-rs/Cargo.toml b/contract-rs/Cargo.toml index 85e5a91..c87704a 100644 --- a/contract-rs/Cargo.toml +++ b/contract-rs/Cargo.toml @@ -13,21 +13,16 @@ crate-type = ["cdylib", "rlib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -near-sdk = "5.24.0" +near-sdk = "5.29.0" [dev-dependencies] -near-sdk = { version = "5.24.0", features = ["unit-testing"] } +near-sdk = { version = "5.29.0", features = ["unit-testing"] } near-sandbox = "0.3" near-api = "0.8" -cargo-near-build = "0.10" -tokio = { version = "1.12.0", features = ["full"] } +cargo-near-build = "0.11" +tokio = { version = "1.48.0", features = ["full"] } serde_json = "1" testresult = "0.4.1" -# This is temporary fix for the build error since those crates with a higher version require a higher version of Rust compiler (1.88.0) -cargo-platform = "=0.3.1" -darling = "=0.20.11" -bon = "=3.8.1" -home = "=0.5.9" [profile.release] codegen-units = 1 diff --git a/contract-rs/rust-toolchain.toml b/contract-rs/rust-toolchain.toml index 5ff54e3..4fd50cb 100644 --- a/contract-rs/rust-toolchain.toml +++ b/contract-rs/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.86.0" +channel = "1.97.1" components = ["rustfmt", "clippy", "rust-analyzer"] targets = ["wasm32-unknown-unknown"] diff --git a/contract-ts/package.json b/contract-ts/package.json index 45fe82b..adad921 100644 --- a/contract-ts/package.json +++ b/contract-ts/package.json @@ -5,21 +5,27 @@ "type": "module", "scripts": { "build": "near-sdk-js build src/contract.ts build/counter.wasm", - "test": "$npm_execpath run build && ava -- ./build/counter.wasm" + "test": "$npm_execpath run build && ava --serial -- ./build/counter.wasm" }, "dependencies": { "near-sdk-js": "2.0.0" }, "devDependencies": { - "ava": "^6.1.3", - "near-workspaces": "^3.5.0", - "typescript": "^5.4.5", - "near-cli": "^4.0.13" + "ava": "^8.0.1", + "near-api-js": "^7.2.0", + "near-sandbox": "^0.3.1", + "typescript": "^5.9.3" }, "ava": { - "timeout": "20000", + "timeout": "60000", "files": [ "sandbox-test/*.ava.js" ] + }, + "pnpm": { + "onlyBuiltDependencies": [ + "near-sdk-js", + "near-sandbox" + ] } -} \ No newline at end of file +} diff --git a/contract-ts/sandbox-test/main.ava.js b/contract-ts/sandbox-test/main.ava.js index c01587f..7bf649f 100644 --- a/contract-ts/sandbox-test/main.ava.js +++ b/contract-ts/sandbox-test/main.ava.js @@ -1,96 +1,103 @@ import anyTest from 'ava'; -import { Worker, NEAR } from 'near-workspaces'; -import { setDefaultResultOrder } from 'dns'; setDefaultResultOrder('ipv4first'); // temp fix for node >v17 +import { readFileSync } from 'fs'; +import { Sandbox, DEFAULT_ACCOUNT_ID, DEFAULT_PRIVATE_KEY } from 'near-sandbox'; +import { Account, JsonRpcProvider, KeyPair, KeyPairSigner, nearToYocto } from 'near-api-js'; /** - * @typedef {import('near-workspaces').NearAccount} NearAccount - * @type {import('ava').TestFn<{worker: Worker, accounts: Record}>} + * @type {import('ava').TestFn<{sandbox: import('near-sandbox').Sandbox, provider: JsonRpcProvider, alice: Account, contract: Account}>} */ const test = anyTest; test.beforeEach(async (t) => { - // Init the worker and start a Sandbox server - const worker = t.context.worker = await Worker.init(); - const root = worker.rootAccount; + // Start a fresh sandbox for each test (NEAR_SANDBOX_VERSION overrides the binary, e.g. on older glibc) + const sandbox = await Sandbox.start({ version: process.env.NEAR_SANDBOX_VERSION }); + const provider = new JsonRpcProvider({ url: sandbox.rpcUrl }); - // some test accounts - const alice = await root.createSubAccount("alice", { - initialBalance: NEAR.parse("30 N").toJSON(), - }); - const contract = await root.createSubAccount("contract", { - initialBalance: NEAR.parse("30 N").toJSON(), - }); + // All accounts share the sandbox genesis key for simplicity + const keyPair = KeyPair.fromString(DEFAULT_PRIVATE_KEY); + const signer = new KeyPairSigner(keyPair); + + const root = new Account(DEFAULT_ACCOUNT_ID, provider, signer); + + for (const prefix of ['alice', 'contract']) { + await root.createSubAccount({ + accountOrPrefix: prefix, + publicKey: keyPair.getPublicKey(), + nearToTransfer: nearToYocto('30'), + }); + } + + const alice = new Account(`alice.${DEFAULT_ACCOUNT_ID}`, provider, signer); + const contract = new Account(`contract.${DEFAULT_ACCOUNT_ID}`, provider, signer); - // Get wasm file path from package.json test script in folder above - await contract.deploy(process.argv[2]); + // Deploy the wasm file passed by the package.json test script + await contract.deployContract(readFileSync(process.argv[2])); // Save state for test runs, it is unique for each test - t.context.accounts = { contract, alice }; + t.context = { sandbox, provider, alice, contract }; }); test.afterEach.always(async (t) => { - // Stop Sandbox server - await t.context.worker.tearDown().catch((error) => { - console.log("Failed to stop the Sandbox:", error); + // Stop the sandbox and clean up temporary files + await t.context.sandbox.tearDown().catch((error) => { + console.log('Failed to stop the Sandbox:', error); }); }); -test("can be incremented", async (t) => { - const { alice, contract } = t.context.accounts; - const startCounter = await contract.view("get_num", {}); - await alice.call(contract, "increment", {}); - const endCounter = await contract.view("get_num", {}); - t.is(endCounter, startCounter + 1); +const getNum = (t) => t.context.provider.callFunction({ + contractId: t.context.contract.accountId, + method: 'get_num', + args: {}, +}); + +const call = (t, methodName, args = {}) => t.context.alice.callFunction({ + contractId: t.context.contract.accountId, + methodName, + args, +}); + +test('can be incremented', async (t) => { + const startCounter = await getNum(t); + await call(t, 'increment'); + t.is(await getNum(t), startCounter + 1); }); -test("can be incremented 10 points", async (t) => { - const { alice, contract } = t.context.accounts; - const startCounter = await contract.view("get_num", {}); - await alice.call(contract, "increment", { number: 10 }); - const endCounter = await contract.view("get_num", {}); - t.is(endCounter, startCounter + 10); +test('can be incremented 10 points', async (t) => { + const startCounter = await getNum(t); + await call(t, 'increment', { number: 10 }); + t.is(await getNum(t), startCounter + 10); }); -test("can be incremented -10 points", async (t) => { - const { alice, contract } = t.context.accounts; - const startCounter = await contract.view("get_num", {}); - await alice.call(contract, "increment", { number: -10 }); - const endCounter = await contract.view("get_num", {}); - t.is(endCounter, startCounter - 10); +test('can be incremented -10 points', async (t) => { + const startCounter = await getNum(t); + await call(t, 'increment', { number: -10 }); + t.is(await getNum(t), startCounter - 10); }); -test("can be decremented", async (t) => { - const { alice, contract } = t.context.accounts; - await alice.call(contract, "increment", {}); - const startCounter = await contract.view("get_num", {}); - await alice.call(contract, "decrement", {}); - const endCounter = await contract.view("get_num", {}); - t.is(endCounter, startCounter - 1); +test('can be decremented', async (t) => { + await call(t, 'increment'); + const startCounter = await getNum(t); + await call(t, 'decrement'); + t.is(await getNum(t), startCounter - 1); }); -test("can be decremented 10 points", async (t) => { - const { alice, contract } = t.context.accounts; - await alice.call(contract, "increment", {}); - const startCounter = await contract.view("get_num", {}); - await alice.call(contract, "decrement", { number: 10 }); - const endCounter = await contract.view("get_num", {}); - t.is(endCounter, startCounter - 10); +test('can be decremented 10 points', async (t) => { + await call(t, 'increment'); + const startCounter = await getNum(t); + await call(t, 'decrement', { number: 10 }); + t.is(await getNum(t), startCounter - 10); }); -test("can be decremented -10 points", async (t) => { - const { alice, contract } = t.context.accounts; - await alice.call(contract, "increment", {}); - const startCounter = await contract.view("get_num", {}); - await alice.call(contract, "decrement", { number: -10 }); - const endCounter = await contract.view("get_num", {}); - t.is(endCounter, startCounter + 10); +test('can be decremented -10 points', async (t) => { + await call(t, 'increment'); + const startCounter = await getNum(t); + await call(t, 'decrement', { number: -10 }); + t.is(await getNum(t), startCounter + 10); }); -test("can be reset", async (t) => { - const { alice, contract } = t.context.accounts; - await alice.call(contract, "increment", {}); - await alice.call(contract, "increment", {}); - await alice.call(contract, "reset", {}); - const endCounter = await contract.view("get_num", {}); - t.is(endCounter, 0); +test('can be reset', async (t) => { + await call(t, 'increment'); + await call(t, 'increment'); + await call(t, 'reset'); + t.is(await getNum(t), 0); }); diff --git a/contract-ts/tsconfig.json b/contract-ts/tsconfig.json index c3d38e6..eb0890e 100644 --- a/contract-ts/tsconfig.json +++ b/contract-ts/tsconfig.json @@ -1,14 +1,16 @@ { "compilerOptions": { "experimentalDecorators": true, - "target": "ES5", + "target": "es2020", + "module": "esnext", + "moduleResolution": "bundler", "noEmit": true, - "noImplicitAny": false, + "noImplicitAny": false }, "files": [ "src/contract.ts" ], "exclude": [ "node_modules" - ], -} \ No newline at end of file + ] +} diff --git a/frontend/package.json b/frontend/package.json index 61c2073..6f42e2a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "engines": { - "node": ">=18" + "node": ">=20" }, "scripts": { "dev": "next dev", @@ -12,34 +12,15 @@ "lint": "next lint" }, "dependencies": { - "@near-js/providers": "^1.0.1", - "@near-wallet-selector/bitte-wallet": "^8.10.0", - "@near-wallet-selector/core": "^8.10.0", - "@near-wallet-selector/ethereum-wallets": "^8.10.0", - "@near-wallet-selector/here-wallet": "^8.10.0", - "@near-wallet-selector/hot-wallet": "^8.10.0", - "@near-wallet-selector/ledger": "^8.10.0", - "@near-wallet-selector/meteor-wallet": "^8.10.0", - "@near-wallet-selector/meteor-wallet-app": "^8.10.0", - "@near-wallet-selector/modal-ui": "^8.10.0", - "@near-wallet-selector/my-near-wallet": "^8.10.0", - "@near-wallet-selector/near-mobile-wallet": "^8.10.0", - "@near-wallet-selector/nightly": "^8.10.0", - "@near-wallet-selector/react-hook": "^8.9.15", - "@near-wallet-selector/sender": "^8.10.0", - "@near-wallet-selector/welldone-wallet": "^8.10.0", - "@reown/appkit": "^1.6.9", - "@reown/appkit-adapter-wagmi": "^1.6.9", - "bootstrap": "^5", - "bootstrap-icons": "^1.11.3", - "near-api-js": "^5.0.1", - "next": "15.2.1", - "react": "^18", - "react-dom": "^18" + "bootstrap": "^5.3.8", + "near-api-js": "^7.2.0", + "near-connect-hooks": "^1.1.6", + "next": "16.2.10", + "react": "^19.2.7", + "react-dom": "^19.2.7" }, "devDependencies": { - "encoding": "^0.1.13", - "eslint": "^9.16.0", - "eslint-config-next": "15.0.3" + "eslint": "^10.7.0", + "eslint-config-next": "16.2.10" } } diff --git a/frontend/src/components/Navigation.jsx b/frontend/src/components/Navigation.jsx index 1efc3d5..7be13f0 100644 --- a/frontend/src/components/Navigation.jsx +++ b/frontend/src/components/Navigation.jsx @@ -1,12 +1,13 @@ import Image from 'next/image'; import Link from 'next/link'; import { useEffect, useState } from 'react'; -import { useWalletSelector } from '@near-wallet-selector/react-hook'; +import { useNearWallet } from 'near-connect-hooks'; -import NearLogo from '/public/near-logo.svg'; +import NearLogo from '../../public/near-logo.svg'; +import { CounterContract } from '@/config'; export const Navigation = () => { - const { signedAccountId, signIn, signOut } = useWalletSelector(); + const { signedAccountId, signIn, signOut } = useNearWallet(); const [action, setAction] = useState(() => { }); const [label, setLabel] = useState('Loading...'); @@ -15,21 +16,36 @@ export const Navigation = () => { setAction(() => signOut); setLabel(`Logout ${signedAccountId}`); } else { - setAction(() => signIn); + // Create a function-call access key for the contract during sign-in + // (replaces the old wallet-selector `createAccessKeyFor` option) + setAction(() => () => signIn({ + addFunctionCallKey: { + contractId: CounterContract, + allowMethods: { anyMethod: false, methodNames: ['increment', 'decrement', 'reset'] }, + gasAllowance: { kind: 'limited', amount: '250000000000000000000000' }, + }, + })); setLabel('Login'); } }, [signedAccountId]); return ( -