@zkproofport-app/sdk 0.2.7 → 0.2.9
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/LICENSE +1 -1
- package/README.md +290 -85
- package/dist/ProofportSDK.d.ts +40 -3
- package/dist/constants.d.ts +30 -0
- package/dist/deeplink.d.ts +30 -13
- package/dist/index.d.ts +2 -2
- package/dist/index.esm.js +292 -5
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +292 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +292 -5
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.ts +69 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@zkproofport-app/sdk)
|
|
4
4
|
[](./LICENSE)
|
|
5
5
|
|
|
6
|
-
TypeScript SDK for requesting zero-knowledge proofs from the [ZKProofport](https://zkproofport.com) mobile app and verifying them on-chain.
|
|
6
|
+
TypeScript SDK for requesting zero-knowledge proofs from the [ZKProofport](https://zkproofport.com) mobile app and verifying them on-chain. Six circuits are supported: Coinbase KYC and country attestations, OIDC email-domain attestations (Google, Microsoft 365), and the three Korean mobile ID (mDL) circuits.
|
|
7
7
|
|
|
8
8
|
## How It Works
|
|
9
9
|
|
|
@@ -21,22 +21,22 @@ TypeScript SDK for requesting zero-knowledge proofs from the [ZKProofport](https
|
|
|
21
21
|
│ │ relay callback
|
|
22
22
|
│ v
|
|
23
23
|
│ ┌──────────────────────────────────────────────────┐
|
|
24
|
-
│ │ SDK receives result (WebSocket / polling)
|
|
24
|
+
│ │ SDK receives result (WebSocket / polling) │
|
|
25
25
|
│ │ (proof, publicInputs, status) │
|
|
26
26
|
│ └─────────────────────┬────────────────────────────┘
|
|
27
27
|
│ │
|
|
28
28
|
v v
|
|
29
29
|
┌──────────────┐ ┌──────────────────┐ ┌───────────────────┐
|
|
30
30
|
│ Verify │────>│ On-chain verify │────>│ Access granted │
|
|
31
|
-
│ on-chain │ │ (Base
|
|
31
|
+
│ on-chain │ │ (Base) │ │ or denied │
|
|
32
32
|
└──────────────┘ └──────────────────┘ └───────────────────┘
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
1. Your app
|
|
36
|
-
2. The SDK
|
|
37
|
-
3.
|
|
38
|
-
4. The user opens the ZKProofport app, which generates the ZK proof
|
|
39
|
-
5. The proof result flows back through the relay to your app
|
|
35
|
+
1. Your app creates a proof request through the SDK
|
|
36
|
+
2. The SDK asks the relay for a one-time challenge and a tracked request ID. For the Coinbase circuits it also signs that challenge with your wallet signer (EIP-191); the OIDC and mDL circuits need no signature
|
|
37
|
+
3. Your app shows the returned deep link as a QR code (desktop) or navigates to it (mobile)
|
|
38
|
+
4. The user opens the ZKProofport app, which generates the ZK proof on-device
|
|
39
|
+
5. The proof result flows back through the relay to your app over WebSocket (HTTP polling as fallback)
|
|
40
40
|
6. Your app verifies the proof on-chain
|
|
41
41
|
|
|
42
42
|
## Installation
|
|
@@ -51,16 +51,19 @@ npm install @zkproofport-app/sdk
|
|
|
51
51
|
npm install ethers
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
`ethers` v6 is recommended; v5 also works. Real-time delivery uses `socket.io-client`, which ships as a dependency of this package — nothing extra to install.
|
|
55
|
+
|
|
54
56
|
## Quick Start
|
|
55
57
|
|
|
56
58
|
```typescript
|
|
57
59
|
import { ProofportSDK } from '@zkproofport-app/sdk';
|
|
60
|
+
import type { CircuitType, ProofResponse } from '@zkproofport-app/sdk';
|
|
58
61
|
import { BrowserProvider } from 'ethers';
|
|
59
62
|
|
|
60
63
|
// 1. Initialize
|
|
61
64
|
const sdk = ProofportSDK.create();
|
|
62
65
|
|
|
63
|
-
// 2. Set wallet signer (ethers v6 Signer)
|
|
66
|
+
// 2. Set wallet signer (ethers v6 Signer) — required for the Coinbase circuits
|
|
64
67
|
const provider = new BrowserProvider(window.ethereum);
|
|
65
68
|
const signer = await provider.getSigner();
|
|
66
69
|
sdk.setSigner(signer);
|
|
@@ -72,31 +75,41 @@ const relay = await sdk.createRelayRequest('coinbase_attestation', {
|
|
|
72
75
|
|
|
73
76
|
// 4. Show QR code to user
|
|
74
77
|
const qrDataUrl = await sdk.generateQRCode(relay.deepLink);
|
|
75
|
-
document.getElementById('qr').src = qrDataUrl;
|
|
78
|
+
(document.getElementById('qr') as HTMLImageElement).src = qrDataUrl;
|
|
76
79
|
|
|
77
80
|
// 5. Wait for proof (WebSocket primary, HTTP polling fallback)
|
|
78
81
|
const result = await sdk.waitForProof(relay.requestId);
|
|
79
82
|
|
|
80
83
|
if (result.status === 'completed') {
|
|
81
|
-
// 6. Verify on-chain
|
|
82
|
-
const
|
|
83
|
-
result.
|
|
84
|
-
result.
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
// 6. Verify on-chain — the relay result carries the verifier to call
|
|
85
|
+
const response: ProofResponse = {
|
|
86
|
+
requestId: result.requestId,
|
|
87
|
+
circuit: result.circuit as CircuitType,
|
|
88
|
+
status: 'completed',
|
|
89
|
+
proof: result.proof,
|
|
90
|
+
publicInputs: result.publicInputs,
|
|
91
|
+
verifierAddress: result.verifierAddress,
|
|
92
|
+
chainId: result.chainId,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const verification = await sdk.verifyResponseOnChain(response);
|
|
87
96
|
console.log('Valid:', verification.valid);
|
|
88
97
|
}
|
|
89
98
|
```
|
|
90
99
|
|
|
91
100
|
## Supported Circuits
|
|
92
101
|
|
|
102
|
+
Circuit IDs are canonical and case-sensitive — pass them exactly as written here. `sdk.getSupportedCircuits()` returns the same list at runtime.
|
|
103
|
+
|
|
93
104
|
### `coinbase_attestation`
|
|
94
105
|
|
|
95
|
-
Proves that a user has completed Coinbase KYC identity verification without revealing any personal information.
|
|
106
|
+
Proves that a user has completed Coinbase KYC identity verification without revealing any personal information. Requires a wallet signer.
|
|
96
107
|
|
|
97
108
|
| Field | Type | Required | Description |
|
|
98
109
|
|-------|------|----------|-------------|
|
|
99
110
|
| `scope` | `string` | Yes | Application-specific identifier (e.g., your domain). Ensures proof uniqueness per app. |
|
|
111
|
+
| `userAddress` | `string` | No | Address to prove the attestation for. Omit it and the mobile app asks the user to connect a wallet. |
|
|
112
|
+
| `rawTransaction` | `string` | No | Pre-fetched attestation transaction data. Omit it and the app fetches the attestation itself. |
|
|
100
113
|
|
|
101
114
|
```typescript
|
|
102
115
|
const relay = await sdk.createRelayRequest('coinbase_attestation', {
|
|
@@ -106,13 +119,15 @@ const relay = await sdk.createRelayRequest('coinbase_attestation', {
|
|
|
106
119
|
|
|
107
120
|
### `coinbase_country_attestation`
|
|
108
121
|
|
|
109
|
-
Proves a user's country based on Coinbase verification, supporting inclusion and exclusion checks, without revealing the actual country.
|
|
122
|
+
Proves a user's country based on Coinbase verification, supporting inclusion and exclusion checks, without revealing the actual country. Requires a wallet signer.
|
|
110
123
|
|
|
111
124
|
| Field | Type | Required | Description |
|
|
112
125
|
|-------|------|----------|-------------|
|
|
113
126
|
| `scope` | `string` | Yes | Application-specific identifier |
|
|
114
127
|
| `countryList` | `string[]` | Yes | ISO 3166-1 alpha-2 country codes (e.g., `['US', 'KR']`) |
|
|
115
128
|
| `isIncluded` | `boolean` | Yes | `true` = prove user IS from listed countries; `false` = prove user is NOT |
|
|
129
|
+
| `userAddress` | `string` | No | Address to prove the attestation for (optional, as above) |
|
|
130
|
+
| `rawTransaction` | `string` | No | Pre-fetched attestation transaction data (optional, as above) |
|
|
116
131
|
|
|
117
132
|
```typescript
|
|
118
133
|
const relay = await sdk.createRelayRequest('coinbase_country_attestation', {
|
|
@@ -124,7 +139,7 @@ const relay = await sdk.createRelayRequest('coinbase_country_attestation', {
|
|
|
124
139
|
|
|
125
140
|
### `oidc_domain_attestation`
|
|
126
141
|
|
|
127
|
-
Prove email domain affiliation via OIDC Sign-In. The mobile app handles authentication and proof generation entirely on-device — the user's email is never revealed.
|
|
142
|
+
Prove email domain affiliation via OIDC Sign-In. The mobile app handles authentication and proof generation entirely on-device — the user's email is never revealed. No wallet signer is required.
|
|
128
143
|
|
|
129
144
|
| Field | Type | Required | Description |
|
|
130
145
|
|-------|------|----------|-------------|
|
|
@@ -163,6 +178,58 @@ const relay = await sdk.createRelayRequest('oidc_domain_attestation', {
|
|
|
163
178
|
|
|
164
179
|
> When `provider` is set, the mobile app verifies the user's account is managed by the specified workspace provider (e.g., Google Workspace `hd` claim, Microsoft 365 `tid` claim). Without `provider`, only the email domain is verified.
|
|
165
180
|
|
|
181
|
+
### `mdl_kr_ownership`
|
|
182
|
+
|
|
183
|
+
Proves the user holds a valid Korean mobile driver's license (모바일 신분증). The license data stays on-device; only the attributes selected by `discloseFlags` are revealed. No wallet signer is required.
|
|
184
|
+
|
|
185
|
+
| Field | Type | Required | Description |
|
|
186
|
+
|-------|------|----------|-------------|
|
|
187
|
+
| `scope` | `string` | Yes | Application-specific identifier for proof uniqueness. Non-empty, 256 characters or fewer, no control characters. |
|
|
188
|
+
| `discloseFlags` | `number` | No | Attribute disclosure bitmask, integer 0–15 (`0x01` name, `0x02` birth, `0x04` sex, `0x08` phone). Omit or `0` for a fully anonymous ownership proof. |
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
// Anonymous "holds a valid Korean mobile ID" proof
|
|
192
|
+
const relay = await sdk.createRelayRequest('mdl_kr_ownership', {
|
|
193
|
+
scope: 'myapp.com',
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### `mdl_kr_age`
|
|
198
|
+
|
|
199
|
+
Proves the user is at least `ageThreshold` years old according to their Korean mobile driver's license, without revealing the birth date. No wallet signer is required.
|
|
200
|
+
|
|
201
|
+
| Field | Type | Required | Description |
|
|
202
|
+
|-------|------|----------|-------------|
|
|
203
|
+
| `scope` | `string` | Yes | Application-specific identifier (same rules as above) |
|
|
204
|
+
| `ageThreshold` | `number` | Yes | Minimum age to prove — integer between 1 and 150 (e.g. `19` for Korean adult verification) |
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
const relay = await sdk.createRelayRequest('mdl_kr_age', {
|
|
208
|
+
scope: 'myapp.com',
|
|
209
|
+
ageThreshold: 19,
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### `mdl_kr_region`
|
|
214
|
+
|
|
215
|
+
Proves the user's registered address is in the specified si/do region without revealing the full address. No wallet signer is required.
|
|
216
|
+
|
|
217
|
+
| Field | Type | Required | Description |
|
|
218
|
+
|-------|------|----------|-------------|
|
|
219
|
+
| `scope` | `string` | Yes | Application-specific identifier (same rules as above) |
|
|
220
|
+
| `targetRegion` | `string` | Yes | Region (si/do) to prove residency in, in Korean (e.g. `'경기도'`, `'서울특별시'`). Non-empty, 256 characters or fewer, no control characters. |
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
const relay = await sdk.createRelayRequest('mdl_kr_region', {
|
|
224
|
+
scope: 'myapp.com',
|
|
225
|
+
targetRegion: '경기도',
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
> mDL inputs are checked before the request leaves your process: a missing `scope`, an out-of-range `discloseFlags`, a non-integer `ageThreshold` or an empty `targetRegion` throws locally instead of coming back as a relay error.
|
|
230
|
+
|
|
231
|
+
> The mDL circuits do not require `setSigner()` — the proof is bound to the license via an on-device nullifier (`nullifier_value` public input), not a wallet address. Use `sdk.extractNullifier(publicInputs, circuit)` for sybil-resistant user identification.
|
|
232
|
+
|
|
166
233
|
## Integration Guide
|
|
167
234
|
|
|
168
235
|
### Step 1: Initialize
|
|
@@ -173,11 +240,11 @@ import { ProofportSDK } from '@zkproofport-app/sdk';
|
|
|
173
240
|
const sdk = ProofportSDK.create();
|
|
174
241
|
```
|
|
175
242
|
|
|
176
|
-
`ProofportSDK.create()` returns an SDK instance pre-configured with the relay server
|
|
243
|
+
`ProofportSDK.create()` returns an SDK instance pre-configured with the relay server. No configuration needed.
|
|
177
244
|
|
|
178
245
|
### Step 2: Set Wallet Signer
|
|
179
246
|
|
|
180
|
-
The
|
|
247
|
+
The Coinbase circuits (`coinbase_attestation`, `coinbase_country_attestation`) authenticate with a signed challenge (EIP-191). Set a wallet signer that can sign messages:
|
|
181
248
|
|
|
182
249
|
```typescript
|
|
183
250
|
import { BrowserProvider } from 'ethers';
|
|
@@ -191,14 +258,14 @@ The `WalletSigner` interface requires two methods:
|
|
|
191
258
|
|
|
192
259
|
```typescript
|
|
193
260
|
interface WalletSigner {
|
|
194
|
-
signMessage(message: string): Promise<string>;
|
|
261
|
+
signMessage(message: string | Uint8Array): Promise<string>;
|
|
195
262
|
getAddress(): Promise<string>;
|
|
196
263
|
}
|
|
197
264
|
```
|
|
198
265
|
|
|
199
266
|
Any ethers v5/v6 `Signer` is compatible.
|
|
200
267
|
|
|
201
|
-
> **OIDC Domain
|
|
268
|
+
> **When you can skip this step:** the OIDC Domain and the three mDL circuits never sign a challenge. Calling `createRelayRequest` for those circuits without a signer works. Calling it for a Coinbase circuit without one throws `Signer not set. Call setSigner() first. Wallet signature is required for this circuit.`
|
|
202
269
|
|
|
203
270
|
#### About challenge-signature
|
|
204
271
|
|
|
@@ -215,7 +282,7 @@ sdk.setSigner(Wallet.createRandom());
|
|
|
215
282
|
|
|
216
283
|
### Step 3: Create Request (via Relay)
|
|
217
284
|
|
|
218
|
-
`createRelayRequest`
|
|
285
|
+
`createRelayRequest` fetches a challenge and request ID from the relay, signs the challenge when the circuit needs it, creates a tracked proof request, and returns a deep link.
|
|
219
286
|
|
|
220
287
|
```typescript
|
|
221
288
|
const relay = await sdk.createRelayRequest('coinbase_attestation', {
|
|
@@ -225,6 +292,7 @@ const relay = await sdk.createRelayRequest('coinbase_attestation', {
|
|
|
225
292
|
dappIcon: 'https://myapp.com/icon.png',
|
|
226
293
|
message: 'Verify your identity to continue',
|
|
227
294
|
nonce: 'unique-nonce-123', // Optional: replay prevention
|
|
295
|
+
returnScheme: 'mydapp://', // Optional: mobile-only, see below
|
|
228
296
|
});
|
|
229
297
|
|
|
230
298
|
// relay.requestId — Relay-issued UUID
|
|
@@ -233,6 +301,51 @@ const relay = await sdk.createRelayRequest('coinbase_attestation', {
|
|
|
233
301
|
// relay.pollUrl — Relative URL for HTTP polling
|
|
234
302
|
```
|
|
235
303
|
|
|
304
|
+
Every field of the third argument is optional: `message`, `dappName` and `dappIcon` are shown to the user in the ZKProofport app, `nonce` is a value of your own choosing that the relay refuses to accept twice, and `returnScheme` is described next.
|
|
305
|
+
|
|
306
|
+
#### Switching back to your app: `returnScheme`
|
|
307
|
+
|
|
308
|
+
By default the user stays in the ZKProofport app after generating a proof and switches back to your app themselves. Pass `returnScheme` and the ZKProofport app brings your app to the foreground once the proof has been delivered — the same handoff a wallet performs after a signature.
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
// Native app: your own registered scheme
|
|
312
|
+
await sdk.createRelayRequest('coinbase_attestation', { scope: 'myapp.com' }, {
|
|
313
|
+
returnScheme: 'mydapp://',
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Web app: your own origin
|
|
317
|
+
await sdk.createRelayRequest('coinbase_attestation', { scope: 'myapp.com' }, {
|
|
318
|
+
returnScheme: 'https://myapp.com',
|
|
319
|
+
});
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**Only set it in the mobile deep-link flow.** The switch happens on the phone that generated the proof, so it makes sense only when that phone is also where the user started — i.e. when you navigated the browser to `relay.deepLink` on the same device. In the desktop QR flow the user is watching the desktop screen while the proof happens on their phone; opening a page or an app on the phone at that moment surfaces something nobody asked for. Gate it:
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
const relay = await sdk.createRelayRequest('coinbase_attestation', { scope: 'myapp.com' },
|
|
326
|
+
ProofportSDK.isMobile() ? { returnScheme: 'https://myapp.com' } : {});
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
`returnScheme` is **not** a return address. It says which app to open, nothing more — the proof still reaches you the usual way, through `waitForProof()` / `waitForResult()`, and the ZKProofport app never posts anything to it.
|
|
330
|
+
|
|
331
|
+
Two shapes are accepted, and nothing else:
|
|
332
|
+
|
|
333
|
+
| Form | Example | Notes |
|
|
334
|
+
|------|---------|-------|
|
|
335
|
+
| Bare custom scheme | `'mydapp://'` | An RFC 3986 scheme followed by exactly `://` |
|
|
336
|
+
| https origin | `'https://myapp.com'`, `'https://myapp.com:8443'` | Host (and optional port) only |
|
|
337
|
+
|
|
338
|
+
Anything else throws before the SDK makes a single network call — no challenge is consumed, so nothing is wasted on a typo:
|
|
339
|
+
|
|
340
|
+
- a path, query string or fragment (`'mydapp://transfer?to=0x...'`, `'https://myapp.com/done'`), which is what keeps a request from driving your app to a specific action
|
|
341
|
+
- an empty or whitespace-containing value, or one longer than 128 characters
|
|
342
|
+
- userinfo (`'https://user:pass@myapp.com'`) and non-ASCII hosts (use punycode)
|
|
343
|
+
- `http://` and other schemes the OS treats specially: `about`, `blob`, `content`, `data`, `facetime`, `facetime-audio`, `file`, `ftp`, `intent`, `javascript`, `jar`, `mailto`, `sms`, `tel`, `vbscript`
|
|
344
|
+
|
|
345
|
+
The value is matched case-insensitively and stored lowercased by the relay.
|
|
346
|
+
|
|
347
|
+
Omit `returnScheme` and nothing changes: the request is created without it and the user stays in the ZKProofport app when the proof is done. There is no auto-switch when proof generation fails either — the error has to be read in the ZKProofport app first.
|
|
348
|
+
|
|
236
349
|
**OIDC Domain Attestation:**
|
|
237
350
|
|
|
238
351
|
```typescript
|
|
@@ -253,7 +366,6 @@ const relay = await sdk.createRelayRequest('oidc_domain_attestation', {
|
|
|
253
366
|
provider: 'google',
|
|
254
367
|
}, {
|
|
255
368
|
dappName: 'My DApp',
|
|
256
|
-
dappIcon: 'https://myapp.com/icon.png',
|
|
257
369
|
message: 'Verify your organization membership',
|
|
258
370
|
});
|
|
259
371
|
|
|
@@ -264,7 +376,6 @@ const relay = await sdk.createRelayRequest('oidc_domain_attestation', {
|
|
|
264
376
|
provider: 'microsoft',
|
|
265
377
|
}, {
|
|
266
378
|
dappName: 'My DApp',
|
|
267
|
-
dappIcon: 'https://myapp.com/icon.png',
|
|
268
379
|
message: 'Verify your organization membership',
|
|
269
380
|
});
|
|
270
381
|
```
|
|
@@ -277,13 +388,15 @@ Generate a QR code from the relay deep link for the user to scan with the ZKProo
|
|
|
277
388
|
|
|
278
389
|
```typescript
|
|
279
390
|
const qrDataUrl = await sdk.generateQRCode(relay.deepLink, {
|
|
280
|
-
width: 400,
|
|
391
|
+
width: 400, // pixels (default: 300)
|
|
281
392
|
darkColor: '#1a1a1a',
|
|
282
393
|
margin: 4,
|
|
283
394
|
});
|
|
284
|
-
document.getElementById('qr').src = qrDataUrl;
|
|
395
|
+
(document.getElementById('qr') as HTMLImageElement).src = qrDataUrl;
|
|
285
396
|
```
|
|
286
397
|
|
|
398
|
+
`QRCodeOptions` accepts `width`, `margin`, `darkColor`, `lightColor` and `errorCorrectionLevel` (`'L' | 'M' | 'Q' | 'H'`, default `'M'`).
|
|
399
|
+
|
|
287
400
|
**Other QR formats:**
|
|
288
401
|
|
|
289
402
|
```typescript
|
|
@@ -293,7 +406,7 @@ const svg = await sdk.generateQRCodeSVG(relay.deepLink);
|
|
|
293
406
|
// Render to canvas
|
|
294
407
|
await sdk.renderQRCodeToCanvas(canvasElement, relay.deepLink, { width: 400 });
|
|
295
408
|
|
|
296
|
-
// Check if data fits QR limits
|
|
409
|
+
// Check if data fits QR limits (2953 bytes)
|
|
297
410
|
const { size, withinLimit } = sdk.checkQRCodeSize(relay.deepLink);
|
|
298
411
|
```
|
|
299
412
|
|
|
@@ -307,7 +420,7 @@ if (ProofportSDK.isMobile()) {
|
|
|
307
420
|
|
|
308
421
|
### Step 5: Wait for Proof
|
|
309
422
|
|
|
310
|
-
**`waitForProof` (recommended)** — Uses WebSocket (Socket.IO) for instant delivery
|
|
423
|
+
**`waitForProof` (recommended)** — Uses WebSocket (Socket.IO) for instant delivery and falls back to HTTP polling automatically if the socket connection fails.
|
|
311
424
|
|
|
312
425
|
```typescript
|
|
313
426
|
const result = await sdk.waitForProof(relay.requestId, {
|
|
@@ -318,6 +431,8 @@ const result = await sdk.waitForProof(relay.requestId, {
|
|
|
318
431
|
});
|
|
319
432
|
```
|
|
320
433
|
|
|
434
|
+
`result.status` is `'pending'`, `'completed'` or `'failed'`. On `'failed'`, `result.error` explains why.
|
|
435
|
+
|
|
321
436
|
**Alternative: Subscribe to real-time updates directly:**
|
|
322
437
|
|
|
323
438
|
```typescript
|
|
@@ -337,7 +452,7 @@ const unsubscribe = await sdk.subscribe(relay.requestId, {
|
|
|
337
452
|
|
|
338
453
|
```typescript
|
|
339
454
|
// Single poll
|
|
340
|
-
const
|
|
455
|
+
const once = await sdk.pollResult(relay.requestId);
|
|
341
456
|
|
|
342
457
|
// Poll until terminal state
|
|
343
458
|
const result = await sdk.waitForResult(relay.requestId, {
|
|
@@ -347,17 +462,29 @@ const result = await sdk.waitForResult(relay.requestId, {
|
|
|
347
462
|
});
|
|
348
463
|
```
|
|
349
464
|
|
|
465
|
+
When you are done, `sdk.disconnect()` closes any open socket.
|
|
466
|
+
|
|
350
467
|
### Step 6: Verify On-Chain
|
|
351
468
|
|
|
352
469
|
Verify the proof cryptographically by calling the deployed Solidity verifier contract.
|
|
353
470
|
|
|
471
|
+
The relay result tells you which verifier contract to call (`verifierAddress`, `chainId`), so verification goes through `verifyResponseOnChain`:
|
|
472
|
+
|
|
354
473
|
```typescript
|
|
474
|
+
import type { CircuitType, ProofResponse } from '@zkproofport-app/sdk';
|
|
475
|
+
|
|
355
476
|
if (result.status === 'completed') {
|
|
356
|
-
const
|
|
357
|
-
result.
|
|
358
|
-
result.
|
|
359
|
-
|
|
360
|
-
|
|
477
|
+
const response: ProofResponse = {
|
|
478
|
+
requestId: result.requestId,
|
|
479
|
+
circuit: result.circuit as CircuitType,
|
|
480
|
+
status: 'completed',
|
|
481
|
+
proof: result.proof,
|
|
482
|
+
publicInputs: result.publicInputs,
|
|
483
|
+
verifierAddress: result.verifierAddress,
|
|
484
|
+
chainId: result.chainId,
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const verification = await sdk.verifyResponseOnChain(response);
|
|
361
488
|
|
|
362
489
|
if (verification.valid) {
|
|
363
490
|
console.log('Proof verified on-chain!');
|
|
@@ -367,47 +494,57 @@ if (result.status === 'completed') {
|
|
|
367
494
|
}
|
|
368
495
|
```
|
|
369
496
|
|
|
370
|
-
|
|
497
|
+
The SDK connects to the right network on its own. Pass your own ethers `Provider` or `Signer` as the second argument if you would rather use your existing connection:
|
|
371
498
|
|
|
372
499
|
```typescript
|
|
373
|
-
const verification = await sdk.verifyResponseOnChain(response);
|
|
500
|
+
const verification = await sdk.verifyResponseOnChain(response, myProvider);
|
|
374
501
|
```
|
|
375
502
|
|
|
503
|
+
`verifyResponseOnChain` never throws for a bad response: an incomplete one (`status` other than `'completed'`, missing `proof` or `publicInputs`) returns `{ valid: false, error: 'Invalid or incomplete response' }`, and a reverting contract call returns `{ valid: false, error }` with the revert message.
|
|
504
|
+
|
|
505
|
+
> `sdk.verifyOnChain(circuit, proof, publicInputs, providerOrSigner?)` takes the same proof in raw pieces, but those pieces carry no verifier address — it returns `{ valid: false, error: 'No verifier address provided...' }` unless the SDK was constructed with a verifier for that circuit. Prefer `verifyResponseOnChain`.
|
|
506
|
+
|
|
376
507
|
### Step 7: Extract Scope, Nullifier, and Domain
|
|
377
508
|
|
|
378
509
|
After verification, extract data from the public inputs:
|
|
379
510
|
|
|
380
511
|
```typescript
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
512
|
+
import type { CircuitType } from '@zkproofport-app/sdk';
|
|
513
|
+
|
|
514
|
+
if (result.status === 'completed' && result.publicInputs && result.circuit) {
|
|
515
|
+
const circuit = result.circuit as CircuitType;
|
|
516
|
+
|
|
517
|
+
// Extract scope — the bytes32 scope value carried in the public inputs
|
|
518
|
+
const scope = sdk.extractScope(result.publicInputs, circuit);
|
|
384
519
|
|
|
385
520
|
// Extract nullifier — a unique, deterministic hash per user + scope
|
|
386
521
|
// Same user with the same scope always produces the same nullifier
|
|
387
|
-
const nullifier = sdk.extractNullifier(result.publicInputs,
|
|
522
|
+
const nullifier = sdk.extractNullifier(result.publicInputs, circuit);
|
|
388
523
|
|
|
389
|
-
console.log('Scope:', scope);
|
|
524
|
+
console.log('Scope:', scope); // '0x7a6b70726f...'
|
|
390
525
|
console.log('Nullifier:', nullifier); // '0xabc123...'
|
|
391
526
|
|
|
392
527
|
// Extract domain — only for OIDC Domain Attestation
|
|
393
|
-
if (
|
|
394
|
-
const domain = sdk.extractDomain(result.publicInputs,
|
|
528
|
+
if (circuit === 'oidc_domain_attestation') {
|
|
529
|
+
const domain = sdk.extractDomain(result.publicInputs, circuit);
|
|
395
530
|
console.log('Domain:', domain); // 'example.com'
|
|
396
531
|
}
|
|
397
532
|
}
|
|
398
533
|
```
|
|
399
534
|
|
|
535
|
+
All three return `null` rather than throwing when the public inputs are too short for the circuit's layout.
|
|
536
|
+
|
|
400
537
|
The **nullifier** serves as a privacy-preserving user identifier:
|
|
401
538
|
- Deterministic: same user + same scope = same nullifier (enables duplicate detection)
|
|
402
|
-
- Privacy-preserving: the wallet address (Coinbase)
|
|
539
|
+
- Privacy-preserving: the wallet address (Coinbase), the email (OIDC) or the license (mDL) is never revealed
|
|
403
540
|
- Scope-bound: different scopes produce different nullifiers for the same user
|
|
404
541
|
|
|
405
542
|
> **OIDC Domain:** The nullifier is a hash of the user's email and scope. The same email + scope always produces the same nullifier, enabling Sybil resistance without revealing the email address.
|
|
406
543
|
|
|
407
544
|
The **domain** (OIDC Domain Attestation only) is the email domain the user proved:
|
|
408
|
-
-
|
|
545
|
+
- Decoded from the circuit's public inputs, up to 64 ASCII characters
|
|
409
546
|
- Matches the domain parameter provided during proof request
|
|
410
|
-
-
|
|
547
|
+
- `extractDomain` returns `null` for every other circuit
|
|
411
548
|
|
|
412
549
|
**Standalone utility functions** are also available for use outside the SDK class:
|
|
413
550
|
|
|
@@ -418,7 +555,8 @@ import {
|
|
|
418
555
|
extractDomainFromPublicInputs,
|
|
419
556
|
} from '@zkproofport-app/sdk';
|
|
420
557
|
|
|
421
|
-
//
|
|
558
|
+
// Scope and nullifier work for all six circuits — the layout is picked from the
|
|
559
|
+
// circuit id, so always pass it:
|
|
422
560
|
const scope = extractScopeFromPublicInputs(publicInputs, 'coinbase_attestation');
|
|
423
561
|
const nullifier = extractNullifierFromPublicInputs(publicInputs, 'coinbase_attestation');
|
|
424
562
|
|
|
@@ -426,17 +564,23 @@ const nullifier = extractNullifierFromPublicInputs(publicInputs, 'coinbase_attes
|
|
|
426
564
|
const oidcScope = extractScopeFromPublicInputs(publicInputs, 'oidc_domain_attestation');
|
|
427
565
|
const oidcNullifier = extractNullifierFromPublicInputs(publicInputs, 'oidc_domain_attestation');
|
|
428
566
|
|
|
567
|
+
// The three mDL circuits share one layout
|
|
568
|
+
const mdlNullifier = extractNullifierFromPublicInputs(publicInputs, 'mdl_kr_age');
|
|
569
|
+
|
|
429
570
|
// Extract domain from OIDC Domain Attestation
|
|
430
571
|
const domain = extractDomainFromPublicInputs(publicInputs, 'oidc_domain_attestation');
|
|
431
|
-
// domain: 'example.com' or null if circuit doesn't match or inputs insufficient
|
|
572
|
+
// domain: 'example.com', or null if the circuit doesn't match or inputs are insufficient
|
|
432
573
|
```
|
|
433
574
|
|
|
575
|
+
Omitting the circuit argument falls back to the `coinbase_attestation` layout, which is why the examples above always pass it explicitly.
|
|
576
|
+
|
|
434
577
|
## Complete Example
|
|
435
578
|
|
|
436
579
|
End-to-end integration using the relay flow:
|
|
437
580
|
|
|
438
581
|
```typescript
|
|
439
582
|
import { ProofportSDK } from '@zkproofport-app/sdk';
|
|
583
|
+
import type { CircuitType, ProofResponse } from '@zkproofport-app/sdk';
|
|
440
584
|
import { BrowserProvider } from 'ethers';
|
|
441
585
|
|
|
442
586
|
async function verifyUser() {
|
|
@@ -458,30 +602,38 @@ async function verifyUser() {
|
|
|
458
602
|
|
|
459
603
|
// Display QR code
|
|
460
604
|
const qrDataUrl = await sdk.generateQRCode(relay.deepLink, { width: 400 });
|
|
461
|
-
document.getElementById('qr-image').src = qrDataUrl;
|
|
462
|
-
document.getElementById('status')
|
|
605
|
+
(document.getElementById('qr-image') as HTMLImageElement).src = qrDataUrl;
|
|
606
|
+
document.getElementById('status')!.textContent = 'Scan the QR code with ZKProofport';
|
|
463
607
|
|
|
464
608
|
// Wait for proof result
|
|
465
609
|
const result = await sdk.waitForProof(relay.requestId, {
|
|
466
610
|
onStatusChange: (update) => {
|
|
467
|
-
document.getElementById('status')
|
|
611
|
+
document.getElementById('status')!.textContent = `Status: ${update.status}`;
|
|
468
612
|
},
|
|
469
613
|
});
|
|
470
614
|
|
|
471
615
|
if (result.status === 'completed') {
|
|
472
616
|
// Verify on-chain
|
|
473
|
-
const
|
|
474
|
-
result.
|
|
475
|
-
result.
|
|
476
|
-
|
|
477
|
-
|
|
617
|
+
const response: ProofResponse = {
|
|
618
|
+
requestId: result.requestId,
|
|
619
|
+
circuit: result.circuit as CircuitType,
|
|
620
|
+
status: 'completed',
|
|
621
|
+
proof: result.proof,
|
|
622
|
+
publicInputs: result.publicInputs,
|
|
623
|
+
verifierAddress: result.verifierAddress,
|
|
624
|
+
chainId: result.chainId,
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
const verification = await sdk.verifyResponseOnChain(response);
|
|
478
628
|
|
|
479
629
|
if (verification.valid) {
|
|
480
|
-
document.getElementById('status')
|
|
630
|
+
document.getElementById('status')!.textContent = 'Identity verified!';
|
|
481
631
|
// Grant access to your application
|
|
632
|
+
} else {
|
|
633
|
+
document.getElementById('status')!.textContent = `Invalid proof: ${verification.error}`;
|
|
482
634
|
}
|
|
483
635
|
} else {
|
|
484
|
-
document.getElementById('status')
|
|
636
|
+
document.getElementById('status')!.textContent = `Failed: ${result.error}`;
|
|
485
637
|
}
|
|
486
638
|
|
|
487
639
|
// Cleanup
|
|
@@ -491,11 +643,11 @@ async function verifyUser() {
|
|
|
491
643
|
|
|
492
644
|
## Configuration
|
|
493
645
|
|
|
494
|
-
`ProofportSDK.create()` returns a fully configured SDK instance. No manual configuration is needed — relay
|
|
646
|
+
`ProofportSDK.create()` returns a fully configured SDK instance. No manual configuration is needed — the relay endpoint and the network used for verification are built in.
|
|
495
647
|
|
|
496
648
|
## Types Reference
|
|
497
649
|
|
|
498
|
-
|
|
650
|
+
Types you can import:
|
|
499
651
|
|
|
500
652
|
```typescript
|
|
501
653
|
import type {
|
|
@@ -504,6 +656,9 @@ import type {
|
|
|
504
656
|
CoinbaseKycInputs,
|
|
505
657
|
CoinbaseCountryInputs,
|
|
506
658
|
OidcDomainInputs,
|
|
659
|
+
MdlKrOwnershipInputs,
|
|
660
|
+
MdlKrAgeInputs,
|
|
661
|
+
MdlKrRegionInputs,
|
|
507
662
|
CircuitInputs,
|
|
508
663
|
ProofRequest,
|
|
509
664
|
ProofResponse,
|
|
@@ -519,21 +674,24 @@ import type {
|
|
|
519
674
|
|
|
520
675
|
| Type | Description |
|
|
521
676
|
|------|-------------|
|
|
522
|
-
| `CircuitType` | `'coinbase_attestation' \| 'coinbase_country_attestation' \| 'oidc_domain_attestation'` |
|
|
523
|
-
| `ProofRequestStatus` | `'pending' \| 'completed' \| 'error' \| 'cancelled'` |
|
|
524
|
-
| `CoinbaseKycInputs` | Inputs for `coinbase_attestation
|
|
525
|
-
| `CoinbaseCountryInputs` | Inputs for `coinbase_country_attestation
|
|
526
|
-
| `OidcDomainInputs` | Inputs for `oidc_domain_attestation
|
|
527
|
-
| `
|
|
528
|
-
| `
|
|
529
|
-
| `
|
|
677
|
+
| `CircuitType` | `'coinbase_attestation' \| 'coinbase_country_attestation' \| 'oidc_domain_attestation' \| 'mdl_kr_ownership' \| 'mdl_kr_age' \| 'mdl_kr_region'` |
|
|
678
|
+
| `ProofRequestStatus` | `'pending' \| 'completed' \| 'error' \| 'cancelled'` — the status on a `ProofResponse` |
|
|
679
|
+
| `CoinbaseKycInputs` | Inputs for `coinbase_attestation`: `{ scope, userAddress?, rawTransaction? }` |
|
|
680
|
+
| `CoinbaseCountryInputs` | Inputs for `coinbase_country_attestation`: `{ scope, countryList, isIncluded, userAddress?, rawTransaction? }` |
|
|
681
|
+
| `OidcDomainInputs` | Inputs for `oidc_domain_attestation`: `{ domain, scope, provider? }` |
|
|
682
|
+
| `MdlKrOwnershipInputs` | Inputs for `mdl_kr_ownership`: `{ scope, discloseFlags? }` |
|
|
683
|
+
| `MdlKrAgeInputs` | Inputs for `mdl_kr_age`: `{ scope, ageThreshold }` |
|
|
684
|
+
| `MdlKrRegionInputs` | Inputs for `mdl_kr_region`: `{ scope, targetRegion }` |
|
|
685
|
+
| `CircuitInputs` | Union of every input type above (plus an empty-input form for circuits that need nothing from the dApp) |
|
|
686
|
+
| `ProofRequest` | Request object: `requestId`, `circuit`, `inputs`, `createdAt`, plus optional `message`, `dappName`, `dappIcon`, `returnScheme`, `expiresAt` |
|
|
687
|
+
| `ProofResponse` | Proof response: `requestId`, `circuit`, `status`, and — when completed — `proof`, `publicInputs`, `numPublicInputs`, `verifierAddress`, `chainId`, `timestamp`; `error` when it failed |
|
|
530
688
|
| `QRCodeOptions` | QR customization: `width`, `margin`, `darkColor`, `lightColor`, `errorCorrectionLevel` |
|
|
531
689
|
| `VerifierContract` | Verifier contract info: `{ address, chainId, abi }` |
|
|
532
|
-
| `ProofportConfig` |
|
|
533
|
-
| `ChallengeResponse` | Challenge from relay: `{ challenge, expiresAt }` |
|
|
534
|
-
| `WalletSigner` | Signer interface: `{ signMessage(
|
|
535
|
-
| `RelayProofRequest` |
|
|
536
|
-
| `RelayProofResult` |
|
|
690
|
+
| `ProofportConfig` | Constructor configuration — `ProofportSDK.create()` fills it in for you |
|
|
691
|
+
| `ChallengeResponse` | Challenge from the relay: `{ requestId, challenge, expiresAt }` |
|
|
692
|
+
| `WalletSigner` | Signer interface: `{ signMessage(message), getAddress() }` |
|
|
693
|
+
| `RelayProofRequest` | Result of `createRelayRequest()`: `{ requestId, deepLink, status, pollUrl }` |
|
|
694
|
+
| `RelayProofResult` | Result of `waitForProof()` / `waitForResult()` / `pollResult()`: `{ requestId, status: 'pending' \| 'completed' \| 'failed', deepLink?, createdAt?, updatedAt?, proof?, publicInputs?, verifierAddress?, chainId?, circuit?, error? }` |
|
|
537
695
|
|
|
538
696
|
The `OidcDomainInputs` interface:
|
|
539
697
|
|
|
@@ -545,6 +703,8 @@ interface OidcDomainInputs {
|
|
|
545
703
|
}
|
|
546
704
|
```
|
|
547
705
|
|
|
706
|
+
Note that `RelayProofResult` (what you get back from the relay) and `ProofResponse` (what `verifyResponseOnChain` takes) are different types: the relay result's `status` uses `'failed'`, and its `circuit` is a plain `string`. Step 6 shows the conversion.
|
|
707
|
+
|
|
548
708
|
## Public Input Layout Constants
|
|
549
709
|
|
|
550
710
|
The SDK exports constants defining the field positions in each circuit's public inputs array. These are useful when working with standalone extraction functions or building custom verification logic.
|
|
@@ -554,24 +714,25 @@ import {
|
|
|
554
714
|
COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT,
|
|
555
715
|
COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT,
|
|
556
716
|
OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT,
|
|
717
|
+
MDL_KR_PUBLIC_INPUT_LAYOUT,
|
|
557
718
|
} from '@zkproofport-app/sdk';
|
|
558
719
|
```
|
|
559
720
|
|
|
560
|
-
**Coinbase KYC Attestation** (128 fields
|
|
721
|
+
**Coinbase KYC Attestation** (128 fields):
|
|
561
722
|
```typescript
|
|
562
723
|
COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT = {
|
|
563
|
-
SIGNAL_HASH_START: 0, //
|
|
724
|
+
SIGNAL_HASH_START: 0, // Signal hash
|
|
564
725
|
SIGNAL_HASH_END: 31,
|
|
565
|
-
MERKLE_ROOT_START: 32, // Merkle root of
|
|
726
|
+
MERKLE_ROOT_START: 32, // Merkle root of the attestation signer list
|
|
566
727
|
MERKLE_ROOT_END: 63,
|
|
567
|
-
SCOPE_START: 64, //
|
|
728
|
+
SCOPE_START: 64, // Scope value
|
|
568
729
|
SCOPE_END: 95,
|
|
569
730
|
NULLIFIER_START: 96, // Unique identifier per user+scope
|
|
570
731
|
NULLIFIER_END: 127,
|
|
571
732
|
}
|
|
572
733
|
```
|
|
573
734
|
|
|
574
|
-
**Coinbase Country Attestation** (150 fields
|
|
735
|
+
**Coinbase Country Attestation** (150 fields):
|
|
575
736
|
```typescript
|
|
576
737
|
COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT = {
|
|
577
738
|
SIGNAL_HASH_START: 0,
|
|
@@ -589,7 +750,7 @@ COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT = {
|
|
|
589
750
|
}
|
|
590
751
|
```
|
|
591
752
|
|
|
592
|
-
**OIDC Domain Attestation** (148 fields
|
|
753
|
+
**OIDC Domain Attestation** (148 fields):
|
|
593
754
|
```typescript
|
|
594
755
|
OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT = {
|
|
595
756
|
PUBKEY_MODULUS_START: 0, // RSA modulus limbs (JWT issuer key)
|
|
@@ -597,29 +758,69 @@ OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT = {
|
|
|
597
758
|
DOMAIN_STORAGE_START: 18, // Domain bytes (up to 64 ASCII characters)
|
|
598
759
|
DOMAIN_STORAGE_END: 81,
|
|
599
760
|
DOMAIN_LEN: 82, // Domain string length
|
|
600
|
-
SCOPE_START: 83, //
|
|
761
|
+
SCOPE_START: 83, // Scope value
|
|
601
762
|
SCOPE_END: 114,
|
|
602
763
|
NULLIFIER_START: 115, // Unique identifier per user+scope
|
|
603
764
|
NULLIFIER_END: 146,
|
|
604
765
|
PROVIDER: 147, // OIDC provider code (0=none, 1=Google, 2=Microsoft)
|
|
605
766
|
|
|
606
|
-
// Deprecated aliases (use
|
|
767
|
+
// Deprecated aliases (use the names above)
|
|
607
768
|
DOMAIN_START: 18, // @deprecated Use DOMAIN_STORAGE_START
|
|
608
769
|
DOMAIN_END: 82, // @deprecated Use DOMAIN_LEN
|
|
609
770
|
}
|
|
610
771
|
```
|
|
611
772
|
|
|
773
|
+
**Korea Mobile ID (mDL)** — one layout shared by the three mDL circuits. The prefix is identical everywhere; the suffix depends on which predicate the circuit proves (`mdl_kr_ownership` 97 fields, `mdl_kr_age` 66, `mdl_kr_region` 96):
|
|
774
|
+
```typescript
|
|
775
|
+
MDL_KR_PUBLIC_INPUT_LAYOUT = {
|
|
776
|
+
SCOPE_START: 0, // Scope value
|
|
777
|
+
SCOPE_END: 31,
|
|
778
|
+
NULLIFIER_START: 32, // nullifier_value
|
|
779
|
+
NULLIFIER_END: 63,
|
|
780
|
+
|
|
781
|
+
// mdl_kr_ownership only
|
|
782
|
+
OWNERSHIP_DISCLOSE_FLAGS: 64,
|
|
783
|
+
OWNERSHIP_OWNER_COMMIT_START: 65,
|
|
784
|
+
OWNERSHIP_OWNER_COMMIT_END: 96,
|
|
785
|
+
|
|
786
|
+
// mdl_kr_age only
|
|
787
|
+
AGE_THRESHOLD: 64,
|
|
788
|
+
AGE_CURRENT_YEAR: 65,
|
|
789
|
+
|
|
790
|
+
// mdl_kr_region only
|
|
791
|
+
REGION_CODE_START: 64,
|
|
792
|
+
REGION_CODE_END: 95,
|
|
793
|
+
}
|
|
794
|
+
```
|
|
795
|
+
|
|
612
796
|
> **Note on field positions:** Each position in the public inputs array corresponds to a field element in the circuit. For bytes32 values (scope, nullifier, signal hash), 32 consecutive fields are concatenated to form the final value.
|
|
613
797
|
|
|
614
798
|
## Error Handling
|
|
615
799
|
|
|
616
|
-
|
|
800
|
+
Async SDK methods throw standard `Error` objects. The messages are stable enough to log, not to switch on:
|
|
617
801
|
|
|
618
802
|
```typescript
|
|
619
803
|
try {
|
|
804
|
+
// Coinbase circuit without a signer
|
|
620
805
|
await sdk.createRelayRequest('coinbase_attestation', { scope: 'app.com' });
|
|
621
806
|
} catch (err) {
|
|
622
|
-
// "Signer not set. Call setSigner() first."
|
|
807
|
+
// "Signer not set. Call setSigner() first. Wallet signature is required for this circuit."
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
try {
|
|
811
|
+
await sdk.createRelayRequest('mdl_kr_age', { scope: 'app.com', ageThreshold: 0 });
|
|
812
|
+
} catch (err) {
|
|
813
|
+
// "ageThreshold is required and must be an integer between 1 and 150 (circuit: mdl_kr_age)"
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
try {
|
|
817
|
+
await sdk.createRelayRequest('coinbase_attestation', { scope: 'app.com' }, {
|
|
818
|
+
returnScheme: 'https://myapp.com/done',
|
|
819
|
+
});
|
|
820
|
+
} catch (err) {
|
|
821
|
+
// "returnScheme must be a bare custom scheme such as "mydapp://" or an https
|
|
822
|
+
// origin such as "https://myapp.com" — paths, query strings and fragments are
|
|
823
|
+
// not accepted" (thrown before any network call)
|
|
623
824
|
}
|
|
624
825
|
|
|
625
826
|
try {
|
|
@@ -629,6 +830,10 @@ try {
|
|
|
629
830
|
}
|
|
630
831
|
```
|
|
631
832
|
|
|
833
|
+
A relay rejection surfaces as its message, e.g. `Duplicate nonce (replay detected)` for a reused `nonce`, or `Request not found or expired` from `pollResult()`.
|
|
834
|
+
|
|
835
|
+
Verification is the exception: `verifyResponseOnChain` and `verifyOnChain` report failures as `{ valid: false, error }` instead of throwing.
|
|
836
|
+
|
|
632
837
|
## Networks
|
|
633
838
|
|
|
634
839
|
Proofs are verified on **Base** (Ethereum L2). The SDK handles network configuration automatically — no manual setup required.
|