@zkproofport-app/sdk 0.2.8 → 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/README.md +250 -95
- package/dist/ProofportSDK.d.ts +24 -3
- package/dist/deeplink.d.ts +16 -0
- package/dist/index.esm.js +97 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +97 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +97 -3
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
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
|
|-------|------|----------|-------------|
|
|
@@ -165,12 +180,12 @@ const relay = await sdk.createRelayRequest('oidc_domain_attestation', {
|
|
|
165
180
|
|
|
166
181
|
### `mdl_kr_ownership`
|
|
167
182
|
|
|
168
|
-
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
|
|
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.
|
|
169
184
|
|
|
170
185
|
| Field | Type | Required | Description |
|
|
171
186
|
|-------|------|----------|-------------|
|
|
172
|
-
| `scope` | `string` | Yes | Application-specific identifier for proof uniqueness |
|
|
173
|
-
| `discloseFlags` | `number` | No | Attribute disclosure bitmask
|
|
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. |
|
|
174
189
|
|
|
175
190
|
```typescript
|
|
176
191
|
// Anonymous "holds a valid Korean mobile ID" proof
|
|
@@ -181,12 +196,12 @@ const relay = await sdk.createRelayRequest('mdl_kr_ownership', {
|
|
|
181
196
|
|
|
182
197
|
### `mdl_kr_age`
|
|
183
198
|
|
|
184
|
-
Proves the user is at least `ageThreshold` years old according to their Korean mobile driver's license, without revealing the birth date.
|
|
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.
|
|
185
200
|
|
|
186
201
|
| Field | Type | Required | Description |
|
|
187
202
|
|-------|------|----------|-------------|
|
|
188
|
-
| `scope` | `string` | Yes | Application-specific identifier |
|
|
189
|
-
| `ageThreshold` | `number` | Yes | Minimum age to prove
|
|
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) |
|
|
190
205
|
|
|
191
206
|
```typescript
|
|
192
207
|
const relay = await sdk.createRelayRequest('mdl_kr_age', {
|
|
@@ -197,12 +212,12 @@ const relay = await sdk.createRelayRequest('mdl_kr_age', {
|
|
|
197
212
|
|
|
198
213
|
### `mdl_kr_region`
|
|
199
214
|
|
|
200
|
-
Proves the user's registered address is in the specified si/do region without revealing the full address.
|
|
215
|
+
Proves the user's registered address is in the specified si/do region without revealing the full address. No wallet signer is required.
|
|
201
216
|
|
|
202
217
|
| Field | Type | Required | Description |
|
|
203
218
|
|-------|------|----------|-------------|
|
|
204
|
-
| `scope` | `string` | Yes | Application-specific identifier |
|
|
205
|
-
| `targetRegion` | `string` | Yes | Region (si/do) to prove residency in, in Korean (e.g. `'경기도'`, `'서울특별시'`) |
|
|
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. |
|
|
206
221
|
|
|
207
222
|
```typescript
|
|
208
223
|
const relay = await sdk.createRelayRequest('mdl_kr_region', {
|
|
@@ -211,7 +226,9 @@ const relay = await sdk.createRelayRequest('mdl_kr_region', {
|
|
|
211
226
|
});
|
|
212
227
|
```
|
|
213
228
|
|
|
214
|
-
>
|
|
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.
|
|
215
232
|
|
|
216
233
|
## Integration Guide
|
|
217
234
|
|
|
@@ -223,11 +240,11 @@ import { ProofportSDK } from '@zkproofport-app/sdk';
|
|
|
223
240
|
const sdk = ProofportSDK.create();
|
|
224
241
|
```
|
|
225
242
|
|
|
226
|
-
`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.
|
|
227
244
|
|
|
228
245
|
### Step 2: Set Wallet Signer
|
|
229
246
|
|
|
230
|
-
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:
|
|
231
248
|
|
|
232
249
|
```typescript
|
|
233
250
|
import { BrowserProvider } from 'ethers';
|
|
@@ -241,14 +258,14 @@ The `WalletSigner` interface requires two methods:
|
|
|
241
258
|
|
|
242
259
|
```typescript
|
|
243
260
|
interface WalletSigner {
|
|
244
|
-
signMessage(message: string): Promise<string>;
|
|
261
|
+
signMessage(message: string | Uint8Array): Promise<string>;
|
|
245
262
|
getAddress(): Promise<string>;
|
|
246
263
|
}
|
|
247
264
|
```
|
|
248
265
|
|
|
249
266
|
Any ethers v5/v6 `Signer` is compatible.
|
|
250
267
|
|
|
251
|
-
> **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.`
|
|
252
269
|
|
|
253
270
|
#### About challenge-signature
|
|
254
271
|
|
|
@@ -265,7 +282,7 @@ sdk.setSigner(Wallet.createRandom());
|
|
|
265
282
|
|
|
266
283
|
### Step 3: Create Request (via Relay)
|
|
267
284
|
|
|
268
|
-
`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.
|
|
269
286
|
|
|
270
287
|
```typescript
|
|
271
288
|
const relay = await sdk.createRelayRequest('coinbase_attestation', {
|
|
@@ -275,6 +292,7 @@ const relay = await sdk.createRelayRequest('coinbase_attestation', {
|
|
|
275
292
|
dappIcon: 'https://myapp.com/icon.png',
|
|
276
293
|
message: 'Verify your identity to continue',
|
|
277
294
|
nonce: 'unique-nonce-123', // Optional: replay prevention
|
|
295
|
+
returnScheme: 'mydapp://', // Optional: mobile-only, see below
|
|
278
296
|
});
|
|
279
297
|
|
|
280
298
|
// relay.requestId — Relay-issued UUID
|
|
@@ -283,6 +301,51 @@ const relay = await sdk.createRelayRequest('coinbase_attestation', {
|
|
|
283
301
|
// relay.pollUrl — Relative URL for HTTP polling
|
|
284
302
|
```
|
|
285
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
|
+
|
|
286
349
|
**OIDC Domain Attestation:**
|
|
287
350
|
|
|
288
351
|
```typescript
|
|
@@ -303,7 +366,6 @@ const relay = await sdk.createRelayRequest('oidc_domain_attestation', {
|
|
|
303
366
|
provider: 'google',
|
|
304
367
|
}, {
|
|
305
368
|
dappName: 'My DApp',
|
|
306
|
-
dappIcon: 'https://myapp.com/icon.png',
|
|
307
369
|
message: 'Verify your organization membership',
|
|
308
370
|
});
|
|
309
371
|
|
|
@@ -314,7 +376,6 @@ const relay = await sdk.createRelayRequest('oidc_domain_attestation', {
|
|
|
314
376
|
provider: 'microsoft',
|
|
315
377
|
}, {
|
|
316
378
|
dappName: 'My DApp',
|
|
317
|
-
dappIcon: 'https://myapp.com/icon.png',
|
|
318
379
|
message: 'Verify your organization membership',
|
|
319
380
|
});
|
|
320
381
|
```
|
|
@@ -327,13 +388,15 @@ Generate a QR code from the relay deep link for the user to scan with the ZKProo
|
|
|
327
388
|
|
|
328
389
|
```typescript
|
|
329
390
|
const qrDataUrl = await sdk.generateQRCode(relay.deepLink, {
|
|
330
|
-
width: 400,
|
|
391
|
+
width: 400, // pixels (default: 300)
|
|
331
392
|
darkColor: '#1a1a1a',
|
|
332
393
|
margin: 4,
|
|
333
394
|
});
|
|
334
|
-
document.getElementById('qr').src = qrDataUrl;
|
|
395
|
+
(document.getElementById('qr') as HTMLImageElement).src = qrDataUrl;
|
|
335
396
|
```
|
|
336
397
|
|
|
398
|
+
`QRCodeOptions` accepts `width`, `margin`, `darkColor`, `lightColor` and `errorCorrectionLevel` (`'L' | 'M' | 'Q' | 'H'`, default `'M'`).
|
|
399
|
+
|
|
337
400
|
**Other QR formats:**
|
|
338
401
|
|
|
339
402
|
```typescript
|
|
@@ -343,7 +406,7 @@ const svg = await sdk.generateQRCodeSVG(relay.deepLink);
|
|
|
343
406
|
// Render to canvas
|
|
344
407
|
await sdk.renderQRCodeToCanvas(canvasElement, relay.deepLink, { width: 400 });
|
|
345
408
|
|
|
346
|
-
// Check if data fits QR limits
|
|
409
|
+
// Check if data fits QR limits (2953 bytes)
|
|
347
410
|
const { size, withinLimit } = sdk.checkQRCodeSize(relay.deepLink);
|
|
348
411
|
```
|
|
349
412
|
|
|
@@ -357,7 +420,7 @@ if (ProofportSDK.isMobile()) {
|
|
|
357
420
|
|
|
358
421
|
### Step 5: Wait for Proof
|
|
359
422
|
|
|
360
|
-
**`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.
|
|
361
424
|
|
|
362
425
|
```typescript
|
|
363
426
|
const result = await sdk.waitForProof(relay.requestId, {
|
|
@@ -368,6 +431,8 @@ const result = await sdk.waitForProof(relay.requestId, {
|
|
|
368
431
|
});
|
|
369
432
|
```
|
|
370
433
|
|
|
434
|
+
`result.status` is `'pending'`, `'completed'` or `'failed'`. On `'failed'`, `result.error` explains why.
|
|
435
|
+
|
|
371
436
|
**Alternative: Subscribe to real-time updates directly:**
|
|
372
437
|
|
|
373
438
|
```typescript
|
|
@@ -387,7 +452,7 @@ const unsubscribe = await sdk.subscribe(relay.requestId, {
|
|
|
387
452
|
|
|
388
453
|
```typescript
|
|
389
454
|
// Single poll
|
|
390
|
-
const
|
|
455
|
+
const once = await sdk.pollResult(relay.requestId);
|
|
391
456
|
|
|
392
457
|
// Poll until terminal state
|
|
393
458
|
const result = await sdk.waitForResult(relay.requestId, {
|
|
@@ -397,17 +462,29 @@ const result = await sdk.waitForResult(relay.requestId, {
|
|
|
397
462
|
});
|
|
398
463
|
```
|
|
399
464
|
|
|
465
|
+
When you are done, `sdk.disconnect()` closes any open socket.
|
|
466
|
+
|
|
400
467
|
### Step 6: Verify On-Chain
|
|
401
468
|
|
|
402
469
|
Verify the proof cryptographically by calling the deployed Solidity verifier contract.
|
|
403
470
|
|
|
471
|
+
The relay result tells you which verifier contract to call (`verifierAddress`, `chainId`), so verification goes through `verifyResponseOnChain`:
|
|
472
|
+
|
|
404
473
|
```typescript
|
|
474
|
+
import type { CircuitType, ProofResponse } from '@zkproofport-app/sdk';
|
|
475
|
+
|
|
405
476
|
if (result.status === 'completed') {
|
|
406
|
-
const
|
|
407
|
-
result.
|
|
408
|
-
result.
|
|
409
|
-
|
|
410
|
-
|
|
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);
|
|
411
488
|
|
|
412
489
|
if (verification.valid) {
|
|
413
490
|
console.log('Proof verified on-chain!');
|
|
@@ -417,47 +494,57 @@ if (result.status === 'completed') {
|
|
|
417
494
|
}
|
|
418
495
|
```
|
|
419
496
|
|
|
420
|
-
|
|
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:
|
|
421
498
|
|
|
422
499
|
```typescript
|
|
423
|
-
const verification = await sdk.verifyResponseOnChain(response);
|
|
500
|
+
const verification = await sdk.verifyResponseOnChain(response, myProvider);
|
|
424
501
|
```
|
|
425
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
|
+
|
|
426
507
|
### Step 7: Extract Scope, Nullifier, and Domain
|
|
427
508
|
|
|
428
509
|
After verification, extract data from the public inputs:
|
|
429
510
|
|
|
430
511
|
```typescript
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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);
|
|
434
519
|
|
|
435
520
|
// Extract nullifier — a unique, deterministic hash per user + scope
|
|
436
521
|
// Same user with the same scope always produces the same nullifier
|
|
437
|
-
const nullifier = sdk.extractNullifier(result.publicInputs,
|
|
522
|
+
const nullifier = sdk.extractNullifier(result.publicInputs, circuit);
|
|
438
523
|
|
|
439
|
-
console.log('Scope:', scope);
|
|
524
|
+
console.log('Scope:', scope); // '0x7a6b70726f...'
|
|
440
525
|
console.log('Nullifier:', nullifier); // '0xabc123...'
|
|
441
526
|
|
|
442
527
|
// Extract domain — only for OIDC Domain Attestation
|
|
443
|
-
if (
|
|
444
|
-
const domain = sdk.extractDomain(result.publicInputs,
|
|
528
|
+
if (circuit === 'oidc_domain_attestation') {
|
|
529
|
+
const domain = sdk.extractDomain(result.publicInputs, circuit);
|
|
445
530
|
console.log('Domain:', domain); // 'example.com'
|
|
446
531
|
}
|
|
447
532
|
}
|
|
448
533
|
```
|
|
449
534
|
|
|
535
|
+
All three return `null` rather than throwing when the public inputs are too short for the circuit's layout.
|
|
536
|
+
|
|
450
537
|
The **nullifier** serves as a privacy-preserving user identifier:
|
|
451
538
|
- Deterministic: same user + same scope = same nullifier (enables duplicate detection)
|
|
452
|
-
- Privacy-preserving: the wallet address (Coinbase)
|
|
539
|
+
- Privacy-preserving: the wallet address (Coinbase), the email (OIDC) or the license (mDL) is never revealed
|
|
453
540
|
- Scope-bound: different scopes produce different nullifiers for the same user
|
|
454
541
|
|
|
455
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.
|
|
456
543
|
|
|
457
544
|
The **domain** (OIDC Domain Attestation only) is the email domain the user proved:
|
|
458
|
-
-
|
|
545
|
+
- Decoded from the circuit's public inputs, up to 64 ASCII characters
|
|
459
546
|
- Matches the domain parameter provided during proof request
|
|
460
|
-
-
|
|
547
|
+
- `extractDomain` returns `null` for every other circuit
|
|
461
548
|
|
|
462
549
|
**Standalone utility functions** are also available for use outside the SDK class:
|
|
463
550
|
|
|
@@ -468,7 +555,8 @@ import {
|
|
|
468
555
|
extractDomainFromPublicInputs,
|
|
469
556
|
} from '@zkproofport-app/sdk';
|
|
470
557
|
|
|
471
|
-
//
|
|
558
|
+
// Scope and nullifier work for all six circuits — the layout is picked from the
|
|
559
|
+
// circuit id, so always pass it:
|
|
472
560
|
const scope = extractScopeFromPublicInputs(publicInputs, 'coinbase_attestation');
|
|
473
561
|
const nullifier = extractNullifierFromPublicInputs(publicInputs, 'coinbase_attestation');
|
|
474
562
|
|
|
@@ -476,17 +564,23 @@ const nullifier = extractNullifierFromPublicInputs(publicInputs, 'coinbase_attes
|
|
|
476
564
|
const oidcScope = extractScopeFromPublicInputs(publicInputs, 'oidc_domain_attestation');
|
|
477
565
|
const oidcNullifier = extractNullifierFromPublicInputs(publicInputs, 'oidc_domain_attestation');
|
|
478
566
|
|
|
567
|
+
// The three mDL circuits share one layout
|
|
568
|
+
const mdlNullifier = extractNullifierFromPublicInputs(publicInputs, 'mdl_kr_age');
|
|
569
|
+
|
|
479
570
|
// Extract domain from OIDC Domain Attestation
|
|
480
571
|
const domain = extractDomainFromPublicInputs(publicInputs, 'oidc_domain_attestation');
|
|
481
|
-
// 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
|
|
482
573
|
```
|
|
483
574
|
|
|
575
|
+
Omitting the circuit argument falls back to the `coinbase_attestation` layout, which is why the examples above always pass it explicitly.
|
|
576
|
+
|
|
484
577
|
## Complete Example
|
|
485
578
|
|
|
486
579
|
End-to-end integration using the relay flow:
|
|
487
580
|
|
|
488
581
|
```typescript
|
|
489
582
|
import { ProofportSDK } from '@zkproofport-app/sdk';
|
|
583
|
+
import type { CircuitType, ProofResponse } from '@zkproofport-app/sdk';
|
|
490
584
|
import { BrowserProvider } from 'ethers';
|
|
491
585
|
|
|
492
586
|
async function verifyUser() {
|
|
@@ -508,30 +602,38 @@ async function verifyUser() {
|
|
|
508
602
|
|
|
509
603
|
// Display QR code
|
|
510
604
|
const qrDataUrl = await sdk.generateQRCode(relay.deepLink, { width: 400 });
|
|
511
|
-
document.getElementById('qr-image').src = qrDataUrl;
|
|
512
|
-
document.getElementById('status')
|
|
605
|
+
(document.getElementById('qr-image') as HTMLImageElement).src = qrDataUrl;
|
|
606
|
+
document.getElementById('status')!.textContent = 'Scan the QR code with ZKProofport';
|
|
513
607
|
|
|
514
608
|
// Wait for proof result
|
|
515
609
|
const result = await sdk.waitForProof(relay.requestId, {
|
|
516
610
|
onStatusChange: (update) => {
|
|
517
|
-
document.getElementById('status')
|
|
611
|
+
document.getElementById('status')!.textContent = `Status: ${update.status}`;
|
|
518
612
|
},
|
|
519
613
|
});
|
|
520
614
|
|
|
521
615
|
if (result.status === 'completed') {
|
|
522
616
|
// Verify on-chain
|
|
523
|
-
const
|
|
524
|
-
result.
|
|
525
|
-
result.
|
|
526
|
-
|
|
527
|
-
|
|
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);
|
|
528
628
|
|
|
529
629
|
if (verification.valid) {
|
|
530
|
-
document.getElementById('status')
|
|
630
|
+
document.getElementById('status')!.textContent = 'Identity verified!';
|
|
531
631
|
// Grant access to your application
|
|
632
|
+
} else {
|
|
633
|
+
document.getElementById('status')!.textContent = `Invalid proof: ${verification.error}`;
|
|
532
634
|
}
|
|
533
635
|
} else {
|
|
534
|
-
document.getElementById('status')
|
|
636
|
+
document.getElementById('status')!.textContent = `Failed: ${result.error}`;
|
|
535
637
|
}
|
|
536
638
|
|
|
537
639
|
// Cleanup
|
|
@@ -541,11 +643,11 @@ async function verifyUser() {
|
|
|
541
643
|
|
|
542
644
|
## Configuration
|
|
543
645
|
|
|
544
|
-
`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.
|
|
545
647
|
|
|
546
648
|
## Types Reference
|
|
547
649
|
|
|
548
|
-
|
|
650
|
+
Types you can import:
|
|
549
651
|
|
|
550
652
|
```typescript
|
|
551
653
|
import type {
|
|
@@ -554,6 +656,9 @@ import type {
|
|
|
554
656
|
CoinbaseKycInputs,
|
|
555
657
|
CoinbaseCountryInputs,
|
|
556
658
|
OidcDomainInputs,
|
|
659
|
+
MdlKrOwnershipInputs,
|
|
660
|
+
MdlKrAgeInputs,
|
|
661
|
+
MdlKrRegionInputs,
|
|
557
662
|
CircuitInputs,
|
|
558
663
|
ProofRequest,
|
|
559
664
|
ProofResponse,
|
|
@@ -569,21 +674,24 @@ import type {
|
|
|
569
674
|
|
|
570
675
|
| Type | Description |
|
|
571
676
|
|------|-------------|
|
|
572
|
-
| `CircuitType` | `'coinbase_attestation' \| 'coinbase_country_attestation' \| 'oidc_domain_attestation'` |
|
|
573
|
-
| `ProofRequestStatus` | `'pending' \| 'completed' \| 'error' \| 'cancelled'` |
|
|
574
|
-
| `CoinbaseKycInputs` | Inputs for `coinbase_attestation
|
|
575
|
-
| `CoinbaseCountryInputs` | Inputs for `coinbase_country_attestation
|
|
576
|
-
| `OidcDomainInputs` | Inputs for `oidc_domain_attestation
|
|
577
|
-
| `
|
|
578
|
-
| `
|
|
579
|
-
| `
|
|
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 |
|
|
580
688
|
| `QRCodeOptions` | QR customization: `width`, `margin`, `darkColor`, `lightColor`, `errorCorrectionLevel` |
|
|
581
689
|
| `VerifierContract` | Verifier contract info: `{ address, chainId, abi }` |
|
|
582
|
-
| `ProofportConfig` |
|
|
583
|
-
| `ChallengeResponse` | Challenge from relay: `{ challenge, expiresAt }` |
|
|
584
|
-
| `WalletSigner` | Signer interface: `{ signMessage(
|
|
585
|
-
| `RelayProofRequest` |
|
|
586
|
-
| `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? }` |
|
|
587
695
|
|
|
588
696
|
The `OidcDomainInputs` interface:
|
|
589
697
|
|
|
@@ -595,6 +703,8 @@ interface OidcDomainInputs {
|
|
|
595
703
|
}
|
|
596
704
|
```
|
|
597
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
|
+
|
|
598
708
|
## Public Input Layout Constants
|
|
599
709
|
|
|
600
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.
|
|
@@ -604,24 +714,25 @@ import {
|
|
|
604
714
|
COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT,
|
|
605
715
|
COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT,
|
|
606
716
|
OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT,
|
|
717
|
+
MDL_KR_PUBLIC_INPUT_LAYOUT,
|
|
607
718
|
} from '@zkproofport-app/sdk';
|
|
608
719
|
```
|
|
609
720
|
|
|
610
|
-
**Coinbase KYC Attestation** (128 fields
|
|
721
|
+
**Coinbase KYC Attestation** (128 fields):
|
|
611
722
|
```typescript
|
|
612
723
|
COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT = {
|
|
613
|
-
SIGNAL_HASH_START: 0, //
|
|
724
|
+
SIGNAL_HASH_START: 0, // Signal hash
|
|
614
725
|
SIGNAL_HASH_END: 31,
|
|
615
|
-
MERKLE_ROOT_START: 32, // Merkle root of
|
|
726
|
+
MERKLE_ROOT_START: 32, // Merkle root of the attestation signer list
|
|
616
727
|
MERKLE_ROOT_END: 63,
|
|
617
|
-
SCOPE_START: 64, //
|
|
728
|
+
SCOPE_START: 64, // Scope value
|
|
618
729
|
SCOPE_END: 95,
|
|
619
730
|
NULLIFIER_START: 96, // Unique identifier per user+scope
|
|
620
731
|
NULLIFIER_END: 127,
|
|
621
732
|
}
|
|
622
733
|
```
|
|
623
734
|
|
|
624
|
-
**Coinbase Country Attestation** (150 fields
|
|
735
|
+
**Coinbase Country Attestation** (150 fields):
|
|
625
736
|
```typescript
|
|
626
737
|
COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT = {
|
|
627
738
|
SIGNAL_HASH_START: 0,
|
|
@@ -639,7 +750,7 @@ COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT = {
|
|
|
639
750
|
}
|
|
640
751
|
```
|
|
641
752
|
|
|
642
|
-
**OIDC Domain Attestation** (148 fields
|
|
753
|
+
**OIDC Domain Attestation** (148 fields):
|
|
643
754
|
```typescript
|
|
644
755
|
OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT = {
|
|
645
756
|
PUBKEY_MODULUS_START: 0, // RSA modulus limbs (JWT issuer key)
|
|
@@ -647,29 +758,69 @@ OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT = {
|
|
|
647
758
|
DOMAIN_STORAGE_START: 18, // Domain bytes (up to 64 ASCII characters)
|
|
648
759
|
DOMAIN_STORAGE_END: 81,
|
|
649
760
|
DOMAIN_LEN: 82, // Domain string length
|
|
650
|
-
SCOPE_START: 83, //
|
|
761
|
+
SCOPE_START: 83, // Scope value
|
|
651
762
|
SCOPE_END: 114,
|
|
652
763
|
NULLIFIER_START: 115, // Unique identifier per user+scope
|
|
653
764
|
NULLIFIER_END: 146,
|
|
654
765
|
PROVIDER: 147, // OIDC provider code (0=none, 1=Google, 2=Microsoft)
|
|
655
766
|
|
|
656
|
-
// Deprecated aliases (use
|
|
767
|
+
// Deprecated aliases (use the names above)
|
|
657
768
|
DOMAIN_START: 18, // @deprecated Use DOMAIN_STORAGE_START
|
|
658
769
|
DOMAIN_END: 82, // @deprecated Use DOMAIN_LEN
|
|
659
770
|
}
|
|
660
771
|
```
|
|
661
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
|
+
|
|
662
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.
|
|
663
797
|
|
|
664
798
|
## Error Handling
|
|
665
799
|
|
|
666
|
-
|
|
800
|
+
Async SDK methods throw standard `Error` objects. The messages are stable enough to log, not to switch on:
|
|
667
801
|
|
|
668
802
|
```typescript
|
|
669
803
|
try {
|
|
804
|
+
// Coinbase circuit without a signer
|
|
670
805
|
await sdk.createRelayRequest('coinbase_attestation', { scope: 'app.com' });
|
|
671
806
|
} catch (err) {
|
|
672
|
-
// "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)
|
|
673
824
|
}
|
|
674
825
|
|
|
675
826
|
try {
|
|
@@ -679,6 +830,10 @@ try {
|
|
|
679
830
|
}
|
|
680
831
|
```
|
|
681
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
|
+
|
|
682
837
|
## Networks
|
|
683
838
|
|
|
684
839
|
Proofs are verified on **Base** (Ethereum L2). The SDK handles network configuration automatically — no manual setup required.
|