@zkproofport-app/sdk 0.4.2 → 0.5.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -1,4 +1,9 @@
1
- import { ethers } from 'ethers';
1
+ import { parseProofForOnChain, verifyProofOnChain, getVerifierAddress, getVerifierChainId, extractScopeFromPublicInputs, extractNullifierFromPublicInputs, extractDomainFromPublicInputs } from '@zkproofport-app/core';
2
+ export { COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, MDL_KR_PUBLIC_INPUT_LAYOUT, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, extractDomainFromPublicInputs, extractNullifierFromPublicInputs, extractScopeFromPublicInputs } from '@zkproofport-app/core';
3
+ import { isCircuitId, ALL_CIRCUIT_IDS, circuitActionBinding, CIRCUIT_VK_PATHS, CIRCUIT_NEEDS_WALLET_SIGNATURE } from '@zkproofport-app/core/circuits';
4
+ export { ALL_CIRCUIT_IDS, CIRCUIT_ACTION_BINDING, CIRCUIT_IDS, CIRCUIT_SUPPORT_STATUS, CIRCUIT_VK_PATHS, EXPERIMENTAL_CIRCUIT_IDS, PLANNED_CIRCUIT_IDS, SUPPORTED_CIRCUIT_IDS, circuitActionBinding, getCircuitSupportStatus, isCircuitId, isSupportedCircuitId } from '@zkproofport-app/core/circuits';
5
+ import { validateTypedAction } from '@zkproofport-app/core/typedAction';
6
+ export { parseTypedAction, validateTypedAction } from '@zkproofport-app/core/typedAction';
2
7
 
3
8
  /**
4
9
  * ZKProofport SDK Constants
@@ -132,50 +137,6 @@ const CIRCUIT_METADATA = {
132
137
  publicInputNames: ['scope', 'nullifier_value', 'region_code'],
133
138
  },
134
139
  };
135
- /**
136
- * Standard verifier contract ABI shared across all Barretenberg-generated verifiers.
137
- * This ABI defines the interface for calling the verify function on deployed verifier contracts.
138
- *
139
- * Uses ethers v6 human-readable ABI format.
140
- *
141
- * @example
142
- * ```typescript
143
- * import { Contract } from 'ethers';
144
- *
145
- * const verifier = new Contract(verifierAddress, VERIFIER_ABI, provider);
146
- * const isValid = await verifier.verify(proofBytes, publicInputs);
147
- * ```
148
- */
149
- const VERIFIER_ABI = [
150
- 'function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) external view returns (bool)',
151
- ];
152
- /**
153
- * Public RPC endpoint URLs for supported blockchain networks.
154
- * Used as fallback when no custom provider is supplied.
155
- *
156
- * Supported networks:
157
- * - 84532: Base Sepolia (testnet)
158
- * - 8453: Base Mainnet (production)
159
- * - 91342: GIWA Sepolia (testnet) — where the GIWA account circuit verifies
160
- *
161
- * A chain missing from this map is not a quiet degradation: `getDefaultProvider`
162
- * throws "No RPC endpoint configured for chain N". That is what a caller saw
163
- * when verifying a GIWA proof on chain 91342 — the verifier contract had been
164
- * deployed and the proof was valid, but the SDK had no way to reach the chain
165
- * to ask. Deploying a verifier to a new network means adding it here too.
166
- *
167
- * @example
168
- * ```typescript
169
- * import { JsonRpcProvider } from 'ethers';
170
- *
171
- * const provider = new JsonRpcProvider(RPC_ENDPOINTS[84532]);
172
- * ```
173
- */
174
- const RPC_ENDPOINTS = {
175
- 84532: 'https://sepolia.base.org', // Base Sepolia
176
- 8453: 'https://mainnet.base.org', // Base Mainnet
177
- 91342: 'https://sepolia-rpc.giwa.io', // GIWA Sepolia
178
- };
179
140
  /**
180
141
  * Default proof request expiration time in milliseconds.
181
142
  * Requests older than this are considered expired and should not be processed.
@@ -207,458 +168,6 @@ const DEFAULT_REQUEST_EXPIRY_MS = 10 * 60 * 1000;
207
168
  * ```
208
169
  */
209
170
  const MAX_QR_DATA_SIZE = 2953; // Version 40 with L error correction
210
- /**
211
- * Coinbase Attestation circuit public input layout (byte offsets).
212
- * Defines the byte positions of each field in the flattened public inputs array.
213
- *
214
- * Public inputs are packed as bytes32 values:
215
- * - signal_hash: bytes 0-31
216
- * - merkle_root: bytes 32-63
217
- * - scope: bytes 64-95
218
- *
219
- * @example
220
- * ```typescript
221
- * const publicInputs = response.publicInputs;
222
- * const signalHash = publicInputs.slice(
223
- * COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT.SIGNAL_HASH_START,
224
- * COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT.SIGNAL_HASH_END + 1
225
- * );
226
- * ```
227
- */
228
- const COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT = {
229
- SIGNAL_HASH_START: 0,
230
- SIGNAL_HASH_END: 31,
231
- MERKLE_ROOT_START: 32,
232
- MERKLE_ROOT_END: 63,
233
- SCOPE_START: 64,
234
- SCOPE_END: 95,
235
- NULLIFIER_START: 96,
236
- NULLIFIER_END: 127,
237
- };
238
- /**
239
- * Coinbase Country Attestation circuit public input layout (byte offsets).
240
- * Defines the byte positions of each field in the flattened public inputs array.
241
- *
242
- * Public inputs are packed as bytes32 values:
243
- * - signal_hash: bytes 0-31
244
- * - merkle_root: bytes 32-63
245
- * - country_list: bytes 64-83 (20 bytes for 10 countries)
246
- * - country_list_length: byte 84
247
- * - is_included: byte 85
248
- * - scope: bytes 86-117
249
- *
250
- * @example
251
- * ```typescript
252
- * const publicInputs = response.publicInputs;
253
- * const countryList = publicInputs.slice(
254
- * COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT.COUNTRY_LIST_START,
255
- * COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT.COUNTRY_LIST_END + 1
256
- * );
257
- * ```
258
- */
259
- const COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT = {
260
- SIGNAL_HASH_START: 0,
261
- SIGNAL_HASH_END: 31,
262
- MERKLE_ROOT_START: 32,
263
- MERKLE_ROOT_END: 63,
264
- COUNTRY_LIST_START: 64,
265
- COUNTRY_LIST_END: 83,
266
- COUNTRY_LIST_LENGTH: 84,
267
- IS_INCLUDED: 85,
268
- SCOPE_START: 86,
269
- SCOPE_END: 117,
270
- NULLIFIER_START: 118,
271
- NULLIFIER_END: 149,
272
- };
273
- /**
274
- * Korea Mobile ID (mDL) circuit public input layout (field offsets).
275
- * All three mDL circuits share the same prefix: scope [0..31] and
276
- * nullifier_value [32..63], each stored one byte per field element.
277
- * The suffix differs per predicate circuit:
278
- * - mdl_kr_ownership (97 inputs): disclose_flags [64], owner_commit [65..96]
279
- * - mdl_kr_age (66 inputs): age_threshold [64], current_year [65]
280
- * - mdl_kr_region (96 inputs): region_code [64..95]
281
- *
282
- * @example
283
- * ```typescript
284
- * const nullifier = publicInputs.slice(
285
- * MDL_KR_PUBLIC_INPUT_LAYOUT.NULLIFIER_START,
286
- * MDL_KR_PUBLIC_INPUT_LAYOUT.NULLIFIER_END + 1
287
- * );
288
- * ```
289
- */
290
- const MDL_KR_PUBLIC_INPUT_LAYOUT = {
291
- SCOPE_START: 0,
292
- SCOPE_END: 31,
293
- NULLIFIER_START: 32,
294
- NULLIFIER_END: 63,
295
- OWNERSHIP_DISCLOSE_FLAGS: 64,
296
- OWNERSHIP_OWNER_COMMIT_START: 65,
297
- OWNERSHIP_OWNER_COMMIT_END: 96,
298
- AGE_THRESHOLD: 64,
299
- AGE_CURRENT_YEAR: 65,
300
- REGION_CODE_START: 64,
301
- REGION_CODE_END: 95,
302
- };
303
- /**
304
- * OIDC Domain Attestation circuit public input layout (field offsets).
305
- * Defines the field positions in the flattened public inputs array (148 fields total).
306
- *
307
- * Circuit public inputs (from main.nr):
308
- * - pubkey_modulus_limbs: pub [u128; 18] → 18 fields (0–17)
309
- * - domain: pub BoundedVec<u8, 64> → 1 len + 64 storage = 65 fields (18–82)
310
- * - scope: pub [u8; 32] → 32 fields (83–114)
311
- * - nullifier: pub [u8; 32] → 32 fields (115–146)
312
- * - provider: pub u8 → 1 field (147)
313
- *
314
- * @example
315
- * ```typescript
316
- * const scope = publicInputs.slice(
317
- * OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT.SCOPE_START,
318
- * OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT.SCOPE_END + 1
319
- * );
320
- * ```
321
- */
322
- const OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT = {
323
- PUBKEY_MODULUS_START: 0,
324
- PUBKEY_MODULUS_END: 17,
325
- DOMAIN_STORAGE_START: 18,
326
- DOMAIN_STORAGE_END: 81,
327
- DOMAIN_LEN: 82,
328
- SCOPE_START: 83,
329
- SCOPE_END: 114,
330
- NULLIFIER_START: 115,
331
- NULLIFIER_END: 146,
332
- PROVIDER: 147,
333
- /** @deprecated Use DOMAIN_STORAGE_START */
334
- DOMAIN_START: 18,
335
- /** @deprecated Use DOMAIN_LEN */
336
- DOMAIN_END: 82,
337
- };
338
-
339
- /**
340
- * Canonical ZKProofport circuit identifiers.
341
- *
342
- * This module is the single source of truth for circuit IDs. Every other layer
343
- * — the mobile app, the demo, the relay, server-side provers — reads its list
344
- * from here instead of keeping a private copy that silently drifts.
345
- *
346
- * It has **no imports and no dependencies**, so it can be pulled in on its own:
347
- *
348
- * ```typescript
349
- * import { CIRCUIT_IDS, isCircuitId } from '@zkproofport-app/sdk/circuits';
350
- * ```
351
- *
352
- * The same names are also re-exported from the package root
353
- * (`@zkproofport-app/sdk`) for callers that already import the SDK.
354
- *
355
- * ## Identifiers are canonical and case-sensitive
356
- *
357
- * Each ID is the circuit's `name` in its `Nargo.toml`, verbatim. The format is
358
- * lowercase with underscores. Hyphenated spellings (`coinbase-kyc`,
359
- * `mdl-kr-age`) are directory or UI route names in other repositories — they
360
- * are **not** circuit IDs, and passing one produces a nullifier mismatch or a
361
- * failed on-chain lookup rather than a clear error.
362
- */
363
- /**
364
- * Canonical circuit identifiers, keyed by a stable constant name.
365
- *
366
- * Prefer these constants over string literals so a rename is a compile error
367
- * rather than a runtime surprise.
368
- *
369
- * @example
370
- * ```typescript
371
- * import { CIRCUIT_IDS } from '@zkproofport-app/sdk/circuits';
372
- *
373
- * const relay = await sdk.createRelayRequest(CIRCUIT_IDS.COINBASE_ATTESTATION, {
374
- * scope: 'myapp.com',
375
- * });
376
- * ```
377
- */
378
- const CIRCUIT_IDS = Object.freeze({
379
- /** Coinbase KYC attestation. Officially supported. */
380
- COINBASE_ATTESTATION: 'coinbase_attestation',
381
- /** Coinbase country attestation (inclusion / exclusion). Officially supported. */
382
- COINBASE_COUNTRY_ATTESTATION: 'coinbase_country_attestation',
383
- /** OIDC email-domain attestation. Officially supported. */
384
- OIDC_DOMAIN_ATTESTATION: 'oidc_domain_attestation',
385
- /** GIWA attestation. Planned — not officially supported yet. */
386
- GIWA_ATTESTATION: 'giwa_attestation',
387
- /** Dojang Playground on GIWA Sepolia. Experimental — testnet only. */
388
- DOJANG_PLAYGROUND: 'dojang_playground',
389
- /** Korea Mobile ID ownership. Planned — not officially supported yet. */
390
- MDL_KR_OWNERSHIP: 'mdl_kr_ownership',
391
- /** Korea Mobile ID age threshold. Planned — not officially supported yet. */
392
- MDL_KR_AGE: 'mdl_kr_age',
393
- /** Korea Mobile ID si/do region. Planned — not officially supported yet. */
394
- MDL_KR_REGION: 'mdl_kr_region',
395
- });
396
- /**
397
- * Support status for every circuit.
398
- *
399
- * The record is exhaustive over {@link CircuitId}: adding an identifier without
400
- * giving it a status is a compile error, so this list cannot drift from
401
- * {@link CIRCUIT_IDS}.
402
- *
403
- * @example
404
- * ```typescript
405
- * import { CIRCUIT_SUPPORT_STATUS } from '@zkproofport-app/sdk/circuits';
406
- *
407
- * CIRCUIT_SUPPORT_STATUS.coinbase_attestation; // 'supported'
408
- * CIRCUIT_SUPPORT_STATUS.mdl_kr_age; // 'planned'
409
- * ```
410
- */
411
- const CIRCUIT_SUPPORT_STATUS = Object.freeze({
412
- coinbase_attestation: 'supported',
413
- coinbase_country_attestation: 'supported',
414
- oidc_domain_attestation: 'supported',
415
- // Experimental, not planned: the circuit is built, its verifier is
416
- // deployed on GIWA Sepolia and the demo requests it. What keeps it out of
417
- // 'supported' is that it is testnet-only, so the app offers it behind
418
- // Developer Mode and reads its files from main rather than a release tag.
419
- giwa_attestation: 'experimental',
420
- // Same footing as GIWA: built, verifier deployed on GIWA Sepolia, a real
421
- // proof verifies there. Testnet only.
422
- dojang_playground: 'experimental',
423
- mdl_kr_ownership: 'planned',
424
- mdl_kr_age: 'planned',
425
- mdl_kr_region: 'planned',
426
- });
427
- /**
428
- * Where each circuit's verification key FILE lives inside the
429
- * `zkproofport/circuits` repository, relative to its root.
430
- *
431
- * Off-chain verification needs the verification key and nothing else — no
432
- * circuit bytecode, no witness. The key is public by nature: it is what anyone
433
- * checking a proof must already have, and the Solidity verifiers deployed
434
- * on-chain embed the same key.
435
- *
436
- * NOTE THE TRAILING `/vk` TWICE. In that repository `target/vk` is a DIRECTORY
437
- * holding `vk`, `vk_hash` and `SHA256SUMS`; the key itself is `target/vk/vk`.
438
- * These paths were first copied here from the mobile app, which keeps the
439
- * directory and the filename as two fields and joins them — so taking only the
440
- * directory produced a URL that answers 404, and the first browser to try
441
- * off-chain verification said "Could not fetch the verification key (HTTP 404)".
442
- *
443
- * Exhaustive over {@link CircuitId} on purpose. A new circuit whose key path is
444
- * not listed here fails to compile rather than failing at verification time,
445
- * where the only symptom would be a proof that cannot be checked.
446
- */
447
- const CIRCUIT_VK_PATHS = Object.freeze({
448
- coinbase_attestation: 'coinbase-attestation/target/vk/vk',
449
- coinbase_country_attestation: 'coinbase-country-attestation/target/vk/vk',
450
- oidc_domain_attestation: 'oidc-domain-attestation/target/vk/vk',
451
- giwa_attestation: 'giwa-attestation/target/vk/vk',
452
- dojang_playground: 'dojang-playground/target/vk/vk',
453
- mdl_kr_ownership: 'mdl/kr-ownership/target/vk/vk',
454
- mdl_kr_age: 'mdl/kr-age/target/vk/vk',
455
- mdl_kr_region: 'mdl/kr-region/target/vk/vk',
456
- });
457
- /**
458
- * Every canonical circuit identifier, in declaration order.
459
- *
460
- * Includes circuits that are still `planned`. Use {@link SUPPORTED_CIRCUIT_IDS}
461
- * when you only want the ones that are officially supported today.
462
- */
463
- const ALL_CIRCUIT_IDS = Object.freeze(Object.values(CIRCUIT_IDS));
464
- /**
465
- * Circuits that are officially supported today: the two Coinbase circuits and
466
- * OIDC domain attestation.
467
- *
468
- * Derived from {@link CIRCUIT_SUPPORT_STATUS}, so it cannot fall out of sync.
469
- */
470
- const SUPPORTED_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter((id) => CIRCUIT_SUPPORT_STATUS[id] === 'supported'));
471
- /**
472
- * Circuits whose identifiers are reserved but which are not officially
473
- * supported yet: GIWA attestation and the three Korea Mobile ID circuits.
474
- *
475
- * Derived from {@link CIRCUIT_SUPPORT_STATUS}, so it cannot fall out of sync.
476
- */
477
- /**
478
- * Circuits you can prove today, on a testnet, with everything subject to change.
479
- *
480
- * `giwa_attestation` is in this state. Calling it `planned` would be wrong —
481
- * the circuit compiles, the verifier is deployed and a proof verifies against
482
- * it — and calling it `supported` would be worse, because the only deployment
483
- * is a testnet (GIWA Sepolia). Neither existing word was true, so the honest
484
- * move was a third word rather than picking the closer lie.
485
- */
486
- const EXPERIMENTAL_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter(id => CIRCUIT_SUPPORT_STATUS[id] === 'experimental'));
487
- const PLANNED_CIRCUIT_IDS = Object.freeze(ALL_CIRCUIT_IDS.filter((id) => CIRCUIT_SUPPORT_STATUS[id] === 'planned'));
488
- /**
489
- * Narrows an unknown value to a canonical circuit identifier.
490
- *
491
- * Returns `false` for hyphenated route names such as `'coinbase-kyc'`, which
492
- * are not circuit IDs.
493
- *
494
- * @example
495
- * ```typescript
496
- * isCircuitId('coinbase_attestation'); // true
497
- * isCircuitId('coinbase-kyc'); // false
498
- * ```
499
- */
500
- function isCircuitId(value) {
501
- return typeof value === 'string' && ALL_CIRCUIT_IDS.includes(value);
502
- }
503
- /**
504
- * Narrows an unknown value to a circuit identifier that is officially supported
505
- * today. Planned circuits return `false`.
506
- *
507
- * @example
508
- * ```typescript
509
- * isSupportedCircuitId('oidc_domain_attestation'); // true
510
- * isSupportedCircuitId('mdl_kr_age'); // false — planned
511
- * ```
512
- */
513
- function isSupportedCircuitId(value) {
514
- return isCircuitId(value) && CIRCUIT_SUPPORT_STATUS[value] === 'supported';
515
- }
516
- /**
517
- * Returns the support status of a circuit.
518
- *
519
- * @param circuit - A canonical circuit identifier.
520
- * @throws Error when the value is not a canonical circuit identifier.
521
- *
522
- * @example
523
- * ```typescript
524
- * getCircuitSupportStatus('giwa_attestation'); // 'planned'
525
- * ```
526
- */
527
- function getCircuitSupportStatus(circuit) {
528
- if (!isCircuitId(circuit)) {
529
- throw new Error(`Unknown circuit '${String(circuit)}'. Expected one of: ${ALL_CIRCUIT_IDS.join(', ')}`);
530
- }
531
- return CIRCUIT_SUPPORT_STATUS[circuit];
532
- }
533
- const CIRCUIT_ACTION_BINDING = {
534
- coinbase_attestation: 'none',
535
- coinbase_country_attestation: 'none',
536
- // The signature lives inside the identity token; no wallet, no action.
537
- oidc_domain_attestation: 'none',
538
- // Branches in-circuit: with an action the wallet signs the typed data,
539
- // without one it personal_signs the request's signal hash.
540
- giwa_attestation: 'optional',
541
- // Same branch as GIWA; see dojang-playground/src/main.nr.
542
- dojang_playground: 'optional',
543
- mdl_kr_ownership: 'none',
544
- mdl_kr_age: 'none',
545
- mdl_kr_region: 'none',
546
- };
547
- /*
548
- * The union is declared above rather than derived from the table's values.
549
- *
550
- * Deriving it narrowed the type to whatever the table happens to hold today --
551
- * 'none' | 'optional' -- and TypeScript then called every `=== 'required'`
552
- * check impossible. The publish typecheck caught it; `rollup` and `vitest` did
553
- * not. 'required' is a state a circuit can legitimately be in (an EIP-712 path
554
- * and nothing else), so the union says so and the table is checked against it.
555
- */
556
- /**
557
- * How this circuit treats an action, or an error naming the id that is not one.
558
- *
559
- * No default: a circuit nobody has classified must not inherit 'none', which
560
- * would silently drop an action a dapp meant to bind.
561
- */
562
- function circuitActionBinding(circuit) {
563
- const binding = CIRCUIT_ACTION_BINDING[circuit];
564
- if (!binding) {
565
- throw new Error(`Unknown circuit '${circuit}'. Expected: ${ALL_CIRCUIT_IDS.join(', ')}`);
566
- }
567
- return binding;
568
- }
569
- const CIRCUIT_NEEDS_WALLET_SIGNATURE = {
570
- coinbase_attestation: true,
571
- coinbase_country_attestation: true,
572
- // The signature is inside the OIDC identity token, checked by the prover.
573
- oidc_domain_attestation: false,
574
- // Web2 (OmniOne CX) flows with no wallet binding at all.
575
- mdl_kr_ownership: false,
576
- mdl_kr_age: false,
577
- mdl_kr_region: false,
578
- // A GIWA-attested wallet signing an EIP-712 action (or its signal hash).
579
- // This read `false` while the circuit had always verified a wallet signature
580
- // -- it sat under the Web2 comment above and was never revisited. A request
581
- // for it would have reached the relay with no signer.
582
- giwa_attestation: true,
583
- // The wallet signs the request, and the same key must have signed its
584
- // Playground faucet transaction.
585
- dojang_playground: true,
586
- };
587
-
588
- const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
589
- /**
590
- * The reason the value is not a usable action, or `null` when it is one.
591
- *
592
- * Structural validation only, not application authorization policy. Consumers
593
- * (e.g. EligibilityGate) must independently enforce the expected chain, verifying
594
- * contract, actor, action parameters, nonce and deadline as applicable.
595
- * EIP-712 signs only fields declared in types, not extra message properties.
596
- *
597
- * A string rather than a boolean because every caller shows it to somebody —
598
- * a dapp developer reading a validation error, or a person looking at the
599
- * mobile app's demo screen.
600
- */
601
- function validateTypedAction(value) {
602
- if (value === undefined || value === null) {
603
- return 'action is required. The circuit proves that a wallet authorised ONE EIP-712 action; there is nothing to prove without it.';
604
- }
605
- if (typeof value !== 'object' || Array.isArray(value)) {
606
- return 'action must be an object.';
607
- }
608
- const a = value;
609
- const d = a.domain;
610
- if (!d || typeof d !== 'object') {
611
- return 'action.domain is required.';
612
- }
613
- for (const k of ['name', 'version', 'verifyingContract']) {
614
- if (typeof d[k] !== 'string' || !d[k]) {
615
- return `action.domain.${k} must be a non-empty string.`;
616
- }
617
- }
618
- if (typeof d.chainId !== 'number' || !Number.isInteger(d.chainId)) {
619
- return 'action.domain.chainId must be an integer.';
620
- }
621
- if (!ADDRESS.test(d.verifyingContract)) {
622
- return 'action.domain.verifyingContract must be a 20-byte hex address.';
623
- }
624
- if (!a.types || typeof a.types !== 'object' || Array.isArray(a.types)) {
625
- return 'action.types is required.';
626
- }
627
- if (typeof a.primaryType !== 'string' || !a.primaryType) {
628
- return 'action.primaryType is required.';
629
- }
630
- if (!(a.primaryType in a.types)) {
631
- return `action.primaryType '${a.primaryType}' is not one of the structs in action.types.`;
632
- }
633
- if (!a.message || typeof a.message !== 'object' || Array.isArray(a.message)) {
634
- return 'action.message is required.';
635
- }
636
- // Every field the struct declares must be present, or the wallet signs a
637
- // different message than the one the caller described.
638
- const fields = a.types[a.primaryType];
639
- if (!Array.isArray(fields) || fields.length === 0) {
640
- return `action.types.${a.primaryType} declares no fields.`;
641
- }
642
- const missing = fields
643
- .map(f => f?.name)
644
- .filter(n => typeof n === 'string' && !(n in a.message));
645
- if (missing.length) {
646
- return `action.message is missing: ${missing.join(', ')}`;
647
- }
648
- return null;
649
- }
650
- /** The action, or the reason it is not one. For text a person typed. */
651
- function parseTypedAction(text) {
652
- let raw;
653
- try {
654
- raw = JSON.parse(text);
655
- }
656
- catch (e) {
657
- return { error: `Not valid JSON: ${e.message}` };
658
- }
659
- const error = validateTypedAction(raw);
660
- return error ? { error } : { action: raw };
661
- }
662
171
 
663
172
  /**
664
173
  * Deep Link utilities for ZKProofport SDK
@@ -4293,370 +3802,6 @@ function estimateQRDataSize(requestOrUrl, scheme = DEFAULT_SCHEME) {
4293
3802
  };
4294
3803
  }
4295
3804
 
4296
- /**
4297
- * On-chain verification utilities for ZKProofport SDK
4298
- *
4299
- * Compatible with both ethers v5 and v6.
4300
- */
4301
- // ethers v5/v6 compatibility shims
4302
- const _ethers = ethers;
4303
- /** @internal ethers v5/v6 compatibility shim */
4304
- function hexZeroPad(value, length) {
4305
- // v6: ethers.zeroPadValue, v5: ethers.utils.hexZeroPad
4306
- if (typeof _ethers.zeroPadValue === 'function')
4307
- return _ethers.zeroPadValue(value, length);
4308
- if (_ethers.utils?.hexZeroPad)
4309
- return _ethers.utils.hexZeroPad(value, length);
4310
- // manual fallback
4311
- const hex = value.startsWith('0x') ? value.slice(2) : value;
4312
- return '0x' + hex.padStart(length * 2, '0');
4313
- }
4314
- /** @internal ethers v5/v6 compatibility shim */
4315
- function createJsonRpcProvider(url) {
4316
- // v6: ethers.JsonRpcProvider, v5: ethers.providers.JsonRpcProvider
4317
- if (typeof _ethers.JsonRpcProvider === 'function')
4318
- return new _ethers.JsonRpcProvider(url);
4319
- if (_ethers.providers?.JsonRpcProvider)
4320
- return new _ethers.providers.JsonRpcProvider(url);
4321
- throw new Error('No JsonRpcProvider found in ethers');
4322
- }
4323
- /**
4324
- * @internal Resolve verifier from SDK config or proof response.
4325
- * SDK config (customVerifier) takes priority over response-provided verifier.
4326
- */
4327
- function resolveVerifier(customVerifier, responseVerifier) {
4328
- if (customVerifier)
4329
- return customVerifier;
4330
- if (responseVerifier?.verifierAddress) {
4331
- return {
4332
- address: responseVerifier.verifierAddress,
4333
- chainId: responseVerifier.chainId ?? 0,
4334
- abi: VERIFIER_ABI,
4335
- };
4336
- }
4337
- return null;
4338
- }
4339
- /**
4340
- * Get verifier contract instance for interacting with on-chain verifier contracts.
4341
- *
4342
- * @param providerOrSigner - ethers.js Provider or Signer instance (v5 or v6 compatible)
4343
- * @param verifier - Verifier contract configuration containing address and ABI
4344
- * @returns ethers.Contract instance connected to the verifier
4345
- *
4346
- * @example
4347
- * ```typescript
4348
- * const provider = new ethers.JsonRpcProvider(rpcUrl);
4349
- * const contract = getVerifierContract(provider, {
4350
- * address: '0x...',
4351
- * chainId: 11155111,
4352
- * abi: VERIFIER_ABI
4353
- * });
4354
- * ```
4355
- */
4356
- function getVerifierContract(providerOrSigner, verifier) {
4357
- return new ethers.Contract(verifier.address, verifier.abi, providerOrSigner);
4358
- }
4359
- /**
4360
- * Get default JSON-RPC provider for a chain using pre-configured RPC endpoints.
4361
- *
4362
- * @param chainId - The chain ID (e.g., 11155111 for Sepolia, 84532 for Base Sepolia)
4363
- * @returns ethers.JsonRpcProvider instance for the specified chain
4364
- * @throws Error if no RPC endpoint is configured for the chain
4365
- *
4366
- * @example
4367
- * ```typescript
4368
- * const provider = getDefaultProvider(11155111); // Sepolia
4369
- * ```
4370
- */
4371
- function getDefaultProvider(chainId) {
4372
- const rpcUrl = RPC_ENDPOINTS[chainId];
4373
- if (!rpcUrl) {
4374
- throw new Error(`No RPC endpoint configured for chain ${chainId}. Known chains: ${Object.keys(RPC_ENDPOINTS).join(', ')}`);
4375
- }
4376
- return createJsonRpcProvider(rpcUrl);
4377
- }
4378
- /**
4379
- * Verify a zero-knowledge proof on-chain by calling the verifier smart contract.
4380
- *
4381
- * This function resolves the verifier contract from SDK config or proof response,
4382
- * connects to the blockchain, and calls the verify() method with the proof and public inputs.
4383
- *
4384
- * @param circuit - The canonical circuit identifier. REQUIRED.
4385
- * @param parsedProof - Parsed proof object containing proofHex and publicInputsHex
4386
- * @param providerOrSigner - Optional ethers.js Provider or Signer instance. If not provided, uses default RPC for the chain
4387
- * @param customVerifier - Optional custom verifier contract config (takes priority over responseVerifier)
4388
- * @param responseVerifier - Optional verifier info from proof generation response
4389
- * @returns Promise resolving to verification result with valid flag and optional error message
4390
- *
4391
- * @example
4392
- * ```typescript
4393
- * const parsed = parseProofForOnChain(proof, publicInputs, numPublicInputs);
4394
- * const result = await verifyProofOnChain(
4395
- * 'coinbase_attestation',
4396
- * parsed,
4397
- * provider,
4398
- * { address: '0x...', chainId: 11155111, abi: VERIFIER_ABI }
4399
- * );
4400
- *
4401
- * if (result.valid) {
4402
- * console.log('Proof is valid!');
4403
- * } else {
4404
- * console.error('Verification failed:', result.error);
4405
- * }
4406
- * ```
4407
- */
4408
- async function verifyProofOnChain(circuit, parsedProof, providerOrSigner, customVerifier, responseVerifier) {
4409
- const verifier = resolveVerifier(customVerifier, responseVerifier);
4410
- if (!verifier) {
4411
- return {
4412
- valid: false,
4413
- error: 'No verifier address provided. Configure via SDK or ensure proof response includes verifierAddress.',
4414
- };
4415
- }
4416
- const provider = providerOrSigner || (verifier.chainId > 0 ? getDefaultProvider(verifier.chainId) : null);
4417
- if (!provider) {
4418
- return {
4419
- valid: false,
4420
- error: 'No provider available. Provide a provider or ensure chainId is set for RPC lookup.',
4421
- };
4422
- }
4423
- const contract = getVerifierContract(provider, verifier);
4424
- try {
4425
- const isValid = await contract.verify(parsedProof.proofHex, parsedProof.publicInputsHex);
4426
- return { valid: isValid };
4427
- }
4428
- catch (error) {
4429
- const errorMessage = error instanceof Error ? error.message : String(error);
4430
- return { valid: false, error: errorMessage };
4431
- }
4432
- }
4433
- /** @internal Ensure a hex string has the 0x prefix */
4434
- function ensureHexPrefix(hex) {
4435
- return hex.startsWith('0x') ? hex : `0x${hex}`;
4436
- }
4437
- /**
4438
- * Parse proof response into format suitable for on-chain verification.
4439
- *
4440
- * Converts proof and public inputs from relay response format to the format
4441
- * expected by Solidity verifier contracts. Public inputs are zero-padded to
4442
- * 32 bytes (bytes32) to match Solidity's bytes32[] type.
4443
- *
4444
- * @param proof - Proof bytes as hex string (with or without 0x prefix)
4445
- * @param publicInputs - Array of public input values as hex strings
4446
- * @param numPublicInputs - Number of public inputs (for validation)
4447
- * @returns Parsed proof object ready for on-chain verification
4448
- *
4449
- * @example
4450
- * ```typescript
4451
- * const parsed = parseProofForOnChain(
4452
- * '0x1a2b3c...',
4453
- * ['0x01', '0x02', '0x03'],
4454
- * 3
4455
- * );
4456
- *
4457
- * // parsed.proofHex: '0x1a2b3c...'
4458
- * // parsed.publicInputsHex: ['0x0000...01', '0x0000...02', '0x0000...03']
4459
- * ```
4460
- */
4461
- function parseProofForOnChain(proof, publicInputs, numPublicInputs) {
4462
- const proofHex = ensureHexPrefix(proof);
4463
- const publicInputsHex = publicInputs.map((input) => {
4464
- return hexZeroPad(ensureHexPrefix(input), 32);
4465
- });
4466
- return {
4467
- proofHex,
4468
- publicInputsHex,
4469
- numPublicInputs,
4470
- };
4471
- }
4472
- /** @internal Require a verifier or throw with a helpful message */
4473
- function requireVerifier(circuit, verifier) {
4474
- if (!verifier) {
4475
- throw new Error(`No verifier configured for circuit '${circuit}'. Configure via SDK verifiers option.`);
4476
- }
4477
- return verifier;
4478
- }
4479
- /**
4480
- * Get verifier contract address for a circuit.
4481
- *
4482
- * @param circuit - The canonical circuit identifier. REQUIRED.
4483
- * @param customVerifier - Optional custom verifier contract config
4484
- * @returns Verifier contract address as hex string
4485
- * @throws Error if no verifier is configured for the circuit
4486
- *
4487
- * @example
4488
- * ```typescript
4489
- * const address = getVerifierAddress('coinbase_attestation', verifierConfig);
4490
- * console.log(address); // '0x1234...'
4491
- * ```
4492
- */
4493
- function getVerifierAddress(circuit, customVerifier) {
4494
- return requireVerifier(circuit, customVerifier).address;
4495
- }
4496
- /**
4497
- * Get chain ID for a circuit's verifier contract.
4498
- *
4499
- * @param circuit - The canonical circuit identifier. REQUIRED.
4500
- * @param customVerifier - Optional custom verifier contract config
4501
- * @returns Chain ID number (e.g., 11155111 for Sepolia, 84532 for Base Sepolia)
4502
- * @throws Error if no verifier is configured for the circuit
4503
- *
4504
- * @example
4505
- * ```typescript
4506
- * const chainId = getVerifierChainId('coinbase_attestation', verifierConfig);
4507
- * console.log(chainId); // 11155111
4508
- * ```
4509
- */
4510
- function getVerifierChainId(circuit, customVerifier) {
4511
- return requireVerifier(circuit, customVerifier).chainId;
4512
- }
4513
- /**
4514
- * Where `scope` and `nullifier` sit in each circuit's public inputs.
4515
- *
4516
- * Keyed by `CircuitId`, which is the point: adding a circuit to the SDK makes
4517
- * this object a compile error until somebody opens that circuit's `fn main`
4518
- * and writes its offsets down. Nothing here can be inferred from the proof —
4519
- * the public inputs are a flat array of bytes with no framing.
4520
- *
4521
- * It replaced an `if / else if / else` chain whose final `else` handed every
4522
- * unrecognised circuit — and every call that named no circuit at all —
4523
- * Coinbase's offsets. For `giwa_attestation` that is not a near miss: its
4524
- * public inputs are signal_hash, domain_separator, action_hash, merkle root,
4525
- * scope, nullifier, so the nullifier slot under Coinbase's offsets holds the
4526
- * MERKLE ROOT, a value identical for every user. Duplicate detection built on
4527
- * it would report one person.
4528
- */
4529
- const PUBLIC_INPUT_OFFSETS = Object.freeze({
4530
- // signal_hash, signer_list_merkle_root, scope, nullifier
4531
- coinbase_attestation: { scope: [64, 95], nullifier: [96, 127] },
4532
- // No longer Coinbase's layout. The GIWA circuit gained the EIP-712 pair, so
4533
- // it reads: signal_hash, domain_separator, action_hash, merkle root, then
4534
- // scope and nullifier 64 bytes further along than Coinbase.
4535
- giwa_attestation: { scope: [128, 159], nullifier: [160, 191] },
4536
- // signal_hash, domain_separator, action_hash, scope, nullifier: GIWA's
4537
- // order without the merkle root, since there is no issuer list.
4538
- dojang_playground: { scope: [96, 127], nullifier: [128, 159] },
4539
- // country_list, country_list_length and is_included sit before scope.
4540
- coinbase_country_attestation: { scope: [86, 117], nullifier: [118, 149] },
4541
- // pubkey_modulus_limbs and the bounded domain sit before scope.
4542
- oidc_domain_attestation: { scope: [83, 114], nullifier: [115, 146] },
4543
- // The Korea mDL circuits open with the pair; signal_hash is commented out.
4544
- mdl_kr_ownership: { scope: [0, 31], nullifier: [32, 63] },
4545
- mdl_kr_age: { scope: [0, 31], nullifier: [32, 63] },
4546
- mdl_kr_region: { scope: [0, 31], nullifier: [32, 63] },
4547
- });
4548
- /**
4549
- * The offsets for a circuit, or an error naming what was asked for.
4550
- *
4551
- * Throwing is the whole point. A caller that omits the circuit, or names one
4552
- * this SDK has never heard of, is asking a question with no answer; returning
4553
- * a guess produced a scope and a nullifier that belong to different bytes and
4554
- * looked exactly like real ones.
4555
- */
4556
- function offsetsFor(circuit) {
4557
- if (circuit === undefined) {
4558
- throw new Error(`A circuit id is required to read public inputs: the layout differs per circuit and cannot be inferred from the proof. Pass one of: ${ALL_CIRCUIT_IDS.join(', ')}`);
4559
- }
4560
- if (!isCircuitId(circuit)) {
4561
- throw new Error(`Unknown circuit '${circuit}'. Expected one of: ${ALL_CIRCUIT_IDS.join(', ')}`);
4562
- }
4563
- return PUBLIC_INPUT_OFFSETS[circuit];
4564
- }
4565
- /**
4566
- * Extract scope value from public inputs array.
4567
- *
4568
- * The scope is a bytes32 value encoded across 32 consecutive field elements
4569
- * in the public inputs. The exact position depends on the circuit type.
4570
- *
4571
- * @param publicInputsHex - Array of public input hex strings (zero-padded to 32 bytes)
4572
- * @param circuit - Optional circuit identifier to determine field positions
4573
- * @returns Reconstructed scope as hex string with 0x prefix, or null if inputs are insufficient
4574
- *
4575
- * @example
4576
- * ```typescript
4577
- * const scope = extractScopeFromPublicInputs(publicInputsHex, 'coinbase_attestation');
4578
- * console.log(scope); // '0x7a6b70726f6f66706f72742e636f6d...'
4579
- * ```
4580
- */
4581
- function extractScopeFromPublicInputs(publicInputsHex, circuit) {
4582
- const [start, end] = offsetsFor(circuit).scope;
4583
- if (publicInputsHex.length <= end)
4584
- return null;
4585
- const scopeFields = publicInputsHex.slice(start, end + 1);
4586
- return reconstructBytes32FromFields(scopeFields);
4587
- }
4588
- /**
4589
- * Extracts the nullifier (bytes32) from public inputs based on circuit type.
4590
- *
4591
- * The nullifier is a unique, deterministic hash derived from the user's attestation
4592
- * and scope. It serves as a privacy-preserving user identifier — the same user
4593
- * with the same scope always produces the same nullifier, enabling duplicate
4594
- * detection without revealing the wallet address.
4595
- *
4596
- * @param publicInputsHex - Array of hex-encoded field elements
4597
- * @param circuit - Circuit id. REQUIRED: the layout differs per circuit and
4598
- * cannot be inferred from the proof. It used to default to
4599
- * `coinbase_attestation`, which returned the right-shaped bytes read from
4600
- * the wrong place for every other circuit.
4601
- * @returns Nullifier as hex string (bytes32), or null if publicInputs too short
4602
- *
4603
- * @example
4604
- * ```typescript
4605
- * const nullifier = extractNullifierFromPublicInputs(publicInputs, 'coinbase_attestation');
4606
- * console.log(nullifier); // '0xabc123...'
4607
- * ```
4608
- */
4609
- function extractNullifierFromPublicInputs(publicInputsHex, circuit) {
4610
- const [start, end] = offsetsFor(circuit).nullifier;
4611
- if (publicInputsHex.length <= end)
4612
- return null;
4613
- const nullifierFields = publicInputsHex.slice(start, end + 1);
4614
- return reconstructBytes32FromFields(nullifierFields);
4615
- }
4616
- /**
4617
- * Extract domain string from OIDC Domain Attestation public inputs.
4618
- *
4619
- * The domain is stored as a Noir BoundedVec<u8, 64>, which serializes as
4620
- * [storage[0..64], len]. Each storage element is a u8 value in a field element.
4621
- *
4622
- * @param publicInputsHex - Array of public input hex strings
4623
- * @param circuit - Circuit identifier (must be 'oidc_domain_attestation')
4624
- * @returns Domain as ASCII string, or null if circuit doesn't match or inputs are insufficient
4625
- *
4626
- * @example
4627
- * ```typescript
4628
- * const domain = extractDomainFromPublicInputs(publicInputs, 'oidc_domain_attestation');
4629
- * console.log(domain); // 'example.com'
4630
- * ```
4631
- */
4632
- function extractDomainFromPublicInputs(publicInputsHex, circuit) {
4633
- if (circuit !== 'oidc_domain_attestation')
4634
- return null;
4635
- const layout = OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT;
4636
- if (publicInputsHex.length <= layout.DOMAIN_LEN)
4637
- return null;
4638
- const len = Number(BigInt(publicInputsHex[layout.DOMAIN_LEN]) & 0xffn);
4639
- if (len === 0 || len > 64)
4640
- return null;
4641
- const storageFields = publicInputsHex.slice(layout.DOMAIN_STORAGE_START, layout.DOMAIN_STORAGE_START + len);
4642
- const chars = storageFields.map(f => {
4643
- const byte = Number(BigInt(f) & 0xffn);
4644
- return String.fromCharCode(byte);
4645
- });
4646
- return chars.join('');
4647
- }
4648
- /** @internal Reconstruct a bytes32 value from 32 individual field elements */
4649
- function reconstructBytes32FromFields(fields) {
4650
- if (fields.length !== 32) {
4651
- throw new Error(`Expected 32 fields, got ${fields.length}`);
4652
- }
4653
- const bytes = fields.map(f => {
4654
- const byte = BigInt(f) & 0xffn;
4655
- return byte.toString(16).padStart(2, '0');
4656
- }).join('');
4657
- return '0x' + bytes;
4658
- }
4659
-
4660
3805
  /**
4661
3806
  * Off-chain proof verification — checking a proof against its verification key,
4662
3807
  * with no chain, no RPC, and no gas.
@@ -4670,27 +3815,28 @@ function reconstructBytes32FromFields(fields) {
4670
3815
  * (through mopro's native Barretenberg); it was only the JavaScript side that
4671
3816
  * had no such thing.
4672
3817
  *
4673
- * HOW IT WORKS. Proofs here are made with `--oracle_hash keccak` and ZK on, so
4674
- * the matching check is `acirVerifyUltraKeccakZkHonk(proof, vk)`. Two things
4675
- * have to line up:
3818
+ * HOW IT WORKS. Proofs here are made for the EVM target (`bb prove -t evm`:
3819
+ * keccak transcript, ZK on), so the matching check is bb.js's
3820
+ * `UltraHonkVerifierBackend.verifyProof(..., { verifierTarget: 'evm' })`. Two
3821
+ * things have to line up:
4676
3822
  *
4677
- * The version. `@aztec/bb.js@1.0.0-nightly.20250723` is the WASM build of the
4678
- * same bb the circuits are compiled with. Versions are not interchangeable:
4679
- * measured on the Korea Mobile ID age circuit, the pinned bb writes a 16256-
4680
- * byte proof where bb 1.2.0 writes 16224 for the identical circuit and
4681
- * witness — and the verification key comes out the SAME either way, so the
4682
- * key cannot warn you. A mismatch shows up only as verification failing.
3823
+ * The version. `@aztec/bb.js` must be the WASM build of the same bb the
3824
+ * circuits are compiled with -- `BB_VERSION` in circuits/toolchain.env,
3825
+ * 4.2.0-aztecnr-rc.2 since the beta.19 toolchain. Versions are not
3826
+ * interchangeable: on the Korea Mobile ID age circuit the 2025 nightly wrote
3827
+ * a 16256-byte proof where bb 1.2.0 wrote 16224 for the identical circuit and
3828
+ * witness, with the SAME verification key, so the key cannot warn you. A
3829
+ * mismatch shows up only as verification failing.
4683
3830
  *
4684
- * The flavour. There are four verify entry points and picking the wrong one
4685
- * is not a polite false. `acirVerifyUltraZKHonk` (poseidon) answers false for
4686
- * a perfectly good proof, and `acirVerifyUltraKeccakHonk` — keccak without ZK
4687
- * — TRAPS the WebAssembly with a bare "unreachable", which looks exactly like
4688
- * a corrupt proof, a mismatched key, or a missing SRS.
3831
+ * The target. Verifying an EVM proof as any other target is not a polite
3832
+ * false. With the 2025 nightly, keccak-without-ZK trapped the WebAssembly
3833
+ * with a bare "unreachable" and the poseidon flavour answered false for a
3834
+ * good proof.
4689
3835
  *
4690
3836
  * THE PROOF ARRIVES IN TWO PIECES. The mobile app splits what Barretenberg
4691
3837
  * produced into `proof` and `publicInputs` before sending it, because the
4692
- * Solidity verifier takes them separately. Verifying needs them joined back, in
4693
- * that order, which is what `reconstructHonkProof` does.
3838
+ * Solidity verifier takes them separately. bb.js 4.x's verifier takes the same
3839
+ * two pieces, so nothing is joined back here.
4694
3840
  */
4695
3841
  /**
4696
3842
  * Where verification keys are published.
@@ -4754,8 +3900,8 @@ function hexToBytes(hex) {
4754
3900
  return out;
4755
3901
  }
4756
3902
  /**
4757
- * @internal Public inputs as the flat byte run Barretenberg expects: each one a
4758
- * 32-byte big-endian field element, concatenated in order.
3903
+ * @internal Public inputs as 32-byte big-endian field elements in hex, the
3904
+ * shape bb.js's verifier takes.
4759
3905
  *
4760
3906
  * The relay carries them as hex strings that are USUALLY already 32 bytes, but
4761
3907
  * a value that happens to have leading zeros can arrive short (`0x01`). Left-
@@ -4763,16 +3909,16 @@ function hexToBytes(hex) {
4763
3909
  * verification that works and one that fails on roughly one proof in a hundred,
4764
3910
  * for no reason the caller can see.
4765
3911
  */
4766
- function packPublicInputs(publicInputs) {
4767
- const out = new Uint8Array(publicInputs.length * 32);
4768
- publicInputs.forEach((input, i) => {
3912
+ function normalisePublicInputs(publicInputs) {
3913
+ return publicInputs.map((input, i) => {
4769
3914
  const bytes = hexToBytes(input);
4770
3915
  if (bytes.length > 32) {
4771
3916
  throw new Error(`Public input ${i} is ${bytes.length} bytes; a field element is at most 32`);
4772
3917
  }
4773
- out.set(bytes, i * 32 + (32 - bytes.length));
3918
+ const padded = new Uint8Array(32);
3919
+ padded.set(bytes, 32 - bytes.length);
3920
+ return '0x' + Array.from(padded, b => b.toString(16).padStart(2, '0')).join('');
4774
3921
  });
4775
- return out;
4776
3922
  }
4777
3923
  /**
4778
3924
  * Fetch a circuit's verification key, with a small cache.
@@ -4868,35 +4014,15 @@ async function verifyProofOffChain(circuit, proof, publicInputs, options = {}) {
4868
4014
  if (proofBytes.length === 0) {
4869
4015
  return { valid: false, error: 'Proof is empty' };
4870
4016
  }
4871
- const publicInputBytes = packPublicInputs(publicInputs);
4017
+ // Normalised to 32-byte big-endian fields before handing them over: the
4018
+ // relay can deliver a short value (`0x01`), and the check below must see
4019
+ // exactly what the circuit committed to.
4020
+ const fields = normalisePublicInputs(publicInputs);
4872
4021
  const vk = options.verificationKey ?? (await fetchVerificationKey(circuit, timeoutMs));
4873
- // Public inputs first, then the proof — the order Barretenberg wrote them in
4874
- // before the mobile app split them apart for the Solidity verifier.
4875
- const full = bb.reconstructHonkProof(publicInputBytes, proofBytes);
4876
- api = (await bb.Barretenberg.new({ threads: 1 }));
4877
- // THE VERIFY MUST MATCH HOW THE PROOF WAS MADE, AND THERE ARE FOUR OF THEM.
4878
- //
4879
- // `scripts/build.sh` in the circuits repo proves with `--oracle_hash keccak`
4880
- // and ZK on — so the only entry point that answers correctly is the one that
4881
- // is BOTH. Measured against a real proof of the Korea Mobile ID age circuit,
4882
- // freshly generated against its own key:
4883
- //
4884
- // acirVerifyUltraKeccakZkHonk true ← the right one
4885
- // acirVerifyUltraKeccakHonk TRAPS with a bare "unreachable"
4886
- // acirVerifyUltraZKHonk false (poseidon: wrong oracle hash)
4887
- //
4888
- // The middle row is why this took a while: picking keccak-without-ZK does
4889
- // not return false, it kills the WebAssembly instance with a message that
4890
- // names nothing. It looks identical to a malformed proof, to a mismatched
4891
- // key, and to a missing SRS.
4892
- //
4893
- // Two more things that produce that same trap, both required:
4894
- // initSRSForCircuitSize must be called before the first verify.
4895
- // The key must be a RawBuffer — a plain Uint8Array is marshalled as a
4896
- // length-prefixed vector, so its length is read as key material.
4897
- // Both are what bb.js's own BarretenbergVerifier does before verifying.
4898
- await api.initSRSForCircuitSize(0);
4899
- const valid = await api.acirVerifyUltraKeccakZkHonk(full, new bb.RawBuffer(vk));
4022
+ const barretenberg = await bb.Barretenberg.new({ threads: 1 });
4023
+ api = barretenberg;
4024
+ const verifier = new bb.UltraHonkVerifierBackend(barretenberg);
4025
+ const valid = await verifier.verifyProof({ proof: proofBytes, publicInputs: fields, verificationKey: vk }, { verifierTarget: 'evm' });
4900
4026
  return { valid };
4901
4027
  }
4902
4028
  catch (error) {
@@ -5989,6 +5115,13 @@ class ProofportSDK {
5989
5115
  inputs,
5990
5116
  challenge,
5991
5117
  };
5118
+ // The relay never reads `inputs`, so the scope is also sent on the envelope,
5119
+ // where the relay counts requests per scope. The app proves the scope inside
5120
+ // `inputs` and uses the envelope copy only when `inputs` has none; both are
5121
+ // the same string here.
5122
+ const scope = inputs.scope;
5123
+ if (typeof scope === 'string' && scope.length > 0)
5124
+ body.scope = scope;
5992
5125
  // Sign challenge for circuits that require wallet signature
5993
5126
  if (needsSignature && this.signer) {
5994
5127
  body.signature = await this.signer.signMessage(challenge);
@@ -6303,5 +5436,5 @@ class ProofportSDK {
6303
5436
  }
6304
5437
  }
6305
5438
 
6306
- export { ALL_CIRCUIT_IDS, CIRCUIT_ACTION_BINDING, CIRCUIT_IDS, CIRCUIT_SUPPORT_STATUS, CIRCUIT_VK_PATHS, COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, EXPERIMENTAL_CIRCUIT_IDS, MDL_KR_PUBLIC_INPUT_LAYOUT, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, PLANNED_CIRCUIT_IDS, ProofportSDK, RELAY_TERMINAL_STATUSES, SUPPORTED_CIRCUIT_IDS, circuitActionBinding, ProofportSDK as default, extractDomainFromPublicInputs, extractNullifierFromPublicInputs, extractScopeFromPublicInputs, getCircuitSupportStatus, isCircuitId, isSupportedCircuitId, isTerminalRelayStatus, parseTypedAction, validateTypedAction, verifyProofOffChain };
5439
+ export { ProofportSDK, RELAY_TERMINAL_STATUSES, ProofportSDK as default, isTerminalRelayStatus, verifyProofOffChain };
6307
5440
  //# sourceMappingURL=index.esm.js.map