A working starter for Neo N3 dApps with a Java/Kotlin smart contract compiled via neow3j and a React/Vite UI that talks to the chain via neon-js + Reown AppKit. Forks cleanly into a real project.
contract/ Java contract (CounterContract — placeholder), compiled to NEF
via the neow3j gradle plugin. Has a JUnit 5 test suite that
runs against a Dockerized single-node privatenet via
neow3j-test (Docker must be running).
ui/ React + Vite SPA. Connects via NeoLine (extension) and/or
WalletConnect (Reown AppKit). Reads + writes the contract.
Vitest unit tests for the chain glue + a Playwright e2e suite
(no wallet — see "Testing").
localnet/ Convenience scripts to spin up an AxLabs neo3-privatenet via
docker for local end-to-end work.
.github/ CI: gradle test; ui unit tests, typecheck, build, e2e,
bundle-size budget gate.
# 1. Compile the contract (writes NEF + manifest into contract/build/neow3j/)
./gradlew :contract:neow3jCompile
# 2. Run the contract tests (JDK 17 or 21; not 25 — see STACK_NOTES.md)
./gradlew :contract:test
# 3. Start the UI (auto-runs sync scripts that bundle the freshly-compiled NEF)
cd ui
npm install
npm run devThe UI defaults to mainnet RPC. For local end-to-end:
./localnet/start.sh # docker compose up -d
VITE_NETWORK=localnet npm run dev # uses the Vite /__rpc proxySearch-and-replace these tokens, in order:
CounterContract→ your contract class name (Java + tests + sync scripts).com.example.counter→ your Java package. Also set the@ManifestExtraAuthorvalue inCounterContract.java(ships asyour-name-here).neow3j-react-starter→ your repo / app name (rootsettings.gradle.kts,ui/package.json,ui/src/lib/appkit.tsmetadata).neow3jstarter.*storage-key prefixes inui/src/lib/connection.tsxandui/src/lib/rpc.ts.
Then write your contract, replace the methods in ui/src/lib/contract.ts
(and its tests in ui/src/lib/contract.test.ts, plus the stubbed methods
in ui/e2e/mock-rpc.ts), and rewrite App.tsx around them. Everything else
(vite config, gradle, CI, wallet adapters, the test harnesses) stays.
Copy ui/.env.example to ui/.env.local and fill in what you need. All
vars are optional — with none set, the UI runs against mainnet RPC in
NeoLine-only mode.
VITE_NETWORK=mainnet # mainnet | testnet | localnet
VITE_RPC_URL= # optional override (any neo-cli RPC)
VITE_WC_PROJECT_ID= # Reown project id; empty = NeoLine-only
VITE_CONTRACT_HASH=0x… # default contract for the UI to read
Contract:
gradle.properties:neow3jVersion, JVM flags.contract/build.gradle.kts:classNamemust point at your contract.
Contract — JUnit 5 against a Dockerized single-node privatenet (Docker must be running):
./gradlew :contract:test # full suite
./gradlew :contract:test --tests CounterContractTest.increment_byOwner_bumpsCountAndFiresEventThe test extension boots a Neo node in a Docker container, deploys all @ContractTest
contracts as the genesis multi-sig, and exposes them via ext.getDeployedContract.
See helpers/TestHelper.java for the genesis-funded GAS helper and the
assertAborted pattern (read FAULT messages from the application log).
UI — two layers:
cd ui
npm run test # vitest: unit tests of src/lib (RPC mocked) — fast, no chain
npm run e2e:install # one-time: download the chromium binary
npm run e2e # playwright: builds, serves dist/, drives a real browservitest covers the chain glue in src/lib — stack-item decoding,
contractExists, waitForTx, and the shape of the invocation increment()
hands the wallet — with the RPC layer mocked.
playwright (e2e/) drives the built bundle in a headless browser. Its
job is less "UI coverage" and more "did the polyfill / manualChunks config
in vite.config.ts survive" — that config only fails at runtime, so a build
that passes can still ship a blank page. It also exercises the read path
against a tiny stub RPC (e2e/mock-rpc.ts).
There's no wallet-driven e2e on purpose. NeoLine is a browser extension
and WalletConnect needs a real wallet on the other end; automating either
buys little over the increment() unit test and is a flake magnet. If you
need it, inject a fake window.NEOLineN3 via page.addInitScript whose
invoke signs with a funded localnet key — but for most forks the unit +
no-wallet-e2e split is enough.
MIT.