Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@
"version": "1.0.0",
"description": "",
"main": "server/index.js",
"workspaces": [
"server"
],
"scripts": {
"dev": "node scripts/dev.mjs",
"dev:vite": "vite",
"dev:all": "node scripts/dev.mjs",
"server": "node server/index.js",
"start": "node server/index.js",
"dev:all": "node scripts/dev.mjs",
"start:all": "node scripts/dev.mjs",
"build": "vite build",
"build:all": "npm run build && npm run build --workspace server --if-present",
"preview": "vite preview",
"prepare": "husky",
"test": "vitest run",
Expand Down
2 changes: 2 additions & 0 deletions server/package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
{
"name": "helphone-prover",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"start": "node index.js",
"build": "node -e \"process.exit(0)\"",
"test": "node --test *.test.js"
},
"dependencies": {
Expand Down
76 changes: 76 additions & 0 deletions src/components/MapboxWrapper.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { forwardRef } from "react";
import Map, { NavigationControl } from "react-map-gl/mapbox";
import "mapbox-gl/dist/mapbox-gl.css";

/**
* MapboxWrapper (#87)
*
* Encapsulates Mapbox / react-map-gl initialisation so pages don't have to
* repeat the access-token wiring, the default view state and the standard
* on-map controls. Everything specific to a screen — markers, sources,
* layers, popups, controllers — is passed as `children` and rendered inside
* the underlying `<Map>` exactly as before.
*
* The component forwards its ref to the react-map-gl `<Map>` instance, so
* callers that need the imperative map handle (`ref.current.getMap()`) keep
* working.
*
* Props:
* - `mapStyle` Mapbox style URL (required).
* - `onMapClick` Click handler, receives the react-map-gl event
* (`e.lngLat` etc). Optional.
* - `initialViewState` Overrides the default world view. Optional.
* - `accessToken` Overrides `VITE_MAPBOX_TOKEN`. Optional.
* - `showNavigationControl` Render the built-in zoom/compass control
* (default `true`).
* - `navigationControlPosition` default `"bottom-right"`.
* - `style` Container style, defaults to fill the parent.
* - `children` Map overlays.
* - any other prop is forwarded to `<Map>`.
*/

const DEFAULT_TOKEN = import.meta.env.VITE_MAPBOX_TOKEN;

const DEFAULT_VIEW_STATE = {
// Centre of the world, fully zoomed out — the same starting point Help.jsx
// used before this component existed.
longitude: 0,
latitude: 20,
zoom: 2,
};

const FILL_PARENT = { width: "100%", height: "100%" };

const MapboxWrapper = forwardRef(function MapboxWrapper(
{
mapStyle,
onMapClick,
initialViewState,
accessToken,
showNavigationControl = true,
navigationControlPosition = "bottom-right",
style = FILL_PARENT,
children,
...rest
},
ref,
) {
return (
<Map
ref={ref}
mapboxAccessToken={accessToken ?? DEFAULT_TOKEN}
initialViewState={initialViewState ?? DEFAULT_VIEW_STATE}
style={style}
mapStyle={mapStyle}
onClick={onMapClick}
{...rest}
>
{showNavigationControl && (
<NavigationControl position={navigationControlPosition} />
)}
{children}
</Map>
);
});

export default MapboxWrapper;
151 changes: 151 additions & 0 deletions src/lib/provers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* ZK proof-generation strategy (#86).
*
* `generateLocationProof` in `zk.js` used to branch between server-side and
* browser-side proving with an inline `if (proverUrl) { … } else { … }`.
* This file pulls that decision into a small strategy: a common
* {@link ProofGenerator} shape, two concrete implementations
* ({@link ServerProver}, {@link BrowserProver}), and a selector.
*
* The concrete proving work still lives in `zk.js` — the provers are thin
* adapters that hold the runtime context (prover URL, fallback flag) and
* call back into the implementation functions passed to their constructors.
* Behaviour, error messages and the single-flight lock are unchanged; only
* the dispatch is restructured.
*/

/**
* @typedef {object} ProofResult
* @property {Uint8Array} proof
* @property {Uint8Array} publicInputsBytes
* @property {Uint8Array} publicInputsPrefix
* @property {string} nullifier
* @property {object} zone
*/

/**
* Common interface every prover implements.
*
* `isAvailable()` is a cheap, synchronous check of whether this strategy is
* even eligible for the current runtime (a configured URL, an opt-in flag).
* It is **not** a network health check — a `ServerProver` can be "available"
* and still fail in `generate()` if the server is down; the caller decides
* whether to fall through to the next prover on that failure.
*
* `generate(opts)` runs the proof and resolves a {@link ProofResult}, or
* rejects with a caller-facing `Error`.
*/
export class ProofGenerator {
/** @returns {string} stable identifier, e.g. `'server'` / `'browser'` */
get name() {
throw new Error('ProofGenerator.name is abstract')
}

/** @returns {boolean} */
isAvailable() {
throw new Error('ProofGenerator.isAvailable() is abstract')
}

/**
* @param {object} _opts
* @returns {Promise<ProofResult>}
*/
async generate(_opts) {
throw new Error('ProofGenerator.generate() is abstract')
}
}

/**
* Proves via the remote ZK prover server (health check + `/prove`).
*
* Always eligible when a `proverUrl` is configured; a dead server surfaces
* as a rejection from {@link generate}, at which point the dispatcher in
* `zk.js` decides whether {@link BrowserProver} may take over.
*/
export class ServerProver extends ProofGenerator {
/**
* @param {{ proverUrl: string, request: (opts: object) => Promise<ProofResult> }} deps
* `request` is `zk.js`'s `_requestServerProof` (health check included).
*/
constructor({ proverUrl, request }) {
super()
this.proverUrl = proverUrl
this._request = request
}

get name() {
return 'server'
}

isAvailable() {
return Boolean(this.proverUrl)
}

generate(opts) {
return this._request({ ...opts, proverUrl: this.proverUrl })
}
}

/**
* Proves in-browser with Noir + Barretenberg.
*
* Only eligible when `VITE_ZK_BROWSER_FALLBACK === 'true'` *as a fallback
* after a server failure*; when there is no server configured at all it runs
* unconditionally (matching the previous behaviour, where a missing
* `VITE_ZK_PROVER_URL` fell straight through to browser proving). The
* `run` implementation keeps the existing single-flight lock so a second
* concurrent call awaits the first proof rather than starting another.
*/
export class BrowserProver extends ProofGenerator {
/**
* @param {{ allowed: boolean, run: (opts: object) => Promise<ProofResult> }} deps
* `run` is `zk.js`'s lock-wrapped browser proof runner.
*/
constructor({ allowed, run }) {
super()
this._allowed = Boolean(allowed)
this._run = run
}

get name() {
return 'browser'
}

isAvailable() {
return this._allowed
}

generate(opts) {
return this._run(opts)
}
}

/**
* Builds the ordered prover list for the current runtime: `ServerProver`
* first when a URL is configured, then `BrowserProver`. `zk.js` walks this
* list, trying the server and (subject to `BrowserProver.isAvailable()`)
* falling through to the browser.
*
* @param {{
* proverUrl: string,
* allowBrowserFallback: boolean,
* requestServerProof: (opts: object) => Promise<ProofResult>,
* runBrowserProof: (opts: object) => Promise<ProofResult>,
* }} ctx
* @returns {ProofGenerator[]}
*/
export function selectProvers({
proverUrl,
allowBrowserFallback,
requestServerProof,
runBrowserProof,
}) {
const provers = []
if (proverUrl) {
provers.push(new ServerProver({ proverUrl, request: requestServerProof }))
}
provers.push(
new BrowserProver({ allowed: allowBrowserFallback, run: runBrowserProof }),
)
return provers
}
54 changes: 39 additions & 15 deletions src/lib/zk.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { StrKey } from '@stellar/stellar-sdk'
import { selectProvers } from './provers'

let _noir = null
let _backend = null
Expand Down Expand Up @@ -386,16 +387,50 @@ function buildCampaignPrefix(publicInputsBytes) {
* @param {{ lat: number, lng: number, campaignId?: string, recipientAddress: string, zone?: object }} opts
* @returns {{ proof: Uint8Array, publicInputsBytes: Uint8Array, nullifier: string }}
*/
/**
* Runs the in-browser proof under the module-level single-flight lock: a
* second concurrent call awaits the first proof instead of starting another.
* Extracted from {@link generateLocationProof} so it can be handed to
* {@link BrowserProver} as its `run` implementation — behaviour is identical
* to the previous inline block.
*/
function _browserProofSingleFlight(args) {
if (_proofLock) {
args.onLog('Proof already in progress — waiting for it to complete')
return _proofLock
}

_proofLock = _browserProof(args)

return _proofLock.finally(() => {
_proofLock = null
})
}

export async function generateLocationProof({ lat, lng, campaignId = '1', recipientAddress, zone, onLog = () => {} }) {
const proverUrl = resolveProverUrl()
const allowBrowserFallback = import.meta.env.VITE_ZK_BROWSER_FALLBACK === 'true'
const proofZone = normalizeZone(zone)
const args = { lat, lng, campaignId, recipientAddress, zone: proofZone, onLog }

// #86 — dispatch through the prover strategy instead of an inline branch.
// selectProvers() returns [ServerProver, BrowserProver] when a prover URL
// is configured, or [BrowserProver] when it is not.
const provers = selectProvers({
proverUrl,
allowBrowserFallback,
requestServerProof: _requestServerProof,
runBrowserProof: _browserProofSingleFlight,
})

const server = provers.find((p) => p.name === 'server')
const browser = provers.find((p) => p.name === 'browser')

if (proverUrl) {
if (server) {
try {
return await _requestServerProof({ lat, lng, campaignId, recipientAddress, zone: proofZone, onLog, proverUrl })
return await server.generate(args)
} catch (err) {
if (!allowBrowserFallback) {
if (!browser.isAvailable()) {
onLog('ZK prover server is not available')
const hint = import.meta.env.PROD
? 'Set VITE_ZK_PROVER_URL to your hosted ZK prover (see README → Deploy).'
Expand All @@ -406,18 +441,7 @@ export async function generateLocationProof({ lat, lng, campaignId = '1', recipi
}
}

if (_proofLock) {
onLog('Proof already in progress — waiting for it to complete')
return _proofLock
}

_proofLock = _browserProof({ lat, lng, campaignId, recipientAddress, zone: proofZone, onLog })

try {
return await _proofLock
} finally {
_proofLock = null
}
return browser.generate(args)
}

function resolveProverUrl() {
Expand Down
Loading