├── .github ├── ISSUE_TEMPLATES │ ├── bug_report.md │ ├── ci.md │ ├── documentation.md │ └── feature-request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── run-test-build-deploy.yaml ├── .gitignore ├── README.md ├── commitlint.config.cjs ├── example ├── .gitignore ├── README.md ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── next.svg │ └── vercel.svg ├── src │ └── app │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx ├── tailwind.config.ts └── tsconfig.json ├── package-lock.json ├── package.json ├── readme ├── depemdency-diagram.svg ├── usage-flow-2.svg ├── usage-flow-3.svg └── usage-flow.svg ├── scripts └── build.sh ├── src ├── Reclaim.ts ├── contract-types │ ├── config.json │ ├── contracts │ │ ├── factories │ │ │ ├── Reclaim__factory.ts │ │ │ └── index.ts │ │ └── index.ts │ └── index.ts ├── index.ts ├── smart-contract.ts ├── utils │ ├── __tests__ │ │ └── helper.test.ts │ ├── constants.ts │ ├── device.ts │ ├── errors.ts │ ├── helper.ts │ ├── interfaces.ts │ ├── logger.ts │ ├── modalUtils.ts │ ├── proofUtils.ts │ ├── sessionUtils.ts │ ├── types.ts │ └── validationUtils.ts └── witness.ts └── tsconfig.json /.github/ISSUE_TEMPLATES/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 General Bug Report 3 | about: Report an issue to help improve the project. 4 | title: '' 5 | labels: 'kind/bug' 6 | assignees: '' 7 | --- 8 | 9 | ### Current Behavior 10 | 11 | 12 | ### Expected Behavior 13 | 14 | 15 | ### Screenshots/Logs 16 | 17 | 18 | ### To Reproduce 19 | 1. Go to '...' 20 | 2. Click on '....' 21 | 3. Scroll down to '....' 22 | 4. See error 23 | 24 | --- 25 | ## Before You Begin 26 | 27 | Before submitting an issue, please ensure you have completed the following steps: 28 | 29 | - [ ] I have read and agree to follow the [Code of Conduct](https://github.com/reclaimprotocol/.github/blob/main/Code-of-Conduct.md). 30 | - [ ] I have checked that this is not a security-related issue. For security concerns, please refer to our [Security Policy](https://github.com/reclaimprotocol/.github/blob/main/SECURITY.md). 31 | - [ ] I have read and signed the [Contributor License Agreement (CLA)](https://github.com/reclaimprotocol/.github/blob/main/CLA.md). 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATES/ci.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🛠 Continuous Integration / DevOps 3 | about: Improve or update workflows or other automation 4 | title: '[CI] ' 5 | labels: 'area/ci' 6 | assignees: '' 7 | --- 8 | 9 | ## Issue Description 10 | 11 | ### Current Situation 12 | 13 | 14 | ### Desired Outcome 15 | 16 | 17 | ### Proposed Implementation 18 | 19 | 20 | ### Additional Context 21 | 22 | 23 | --- 24 | ## Before You Begin 25 | 26 | Before submitting an issue, please ensure you have completed the following steps: 27 | 28 | - [ ] I have read and agree to follow the [Code of Conduct](https://github.com/reclaimprotocol/.github/blob/main/Code-of-Conduct.md). 29 | - [ ] I have checked that this is not a security-related issue. For security concerns, please refer to our [Security Policy](https://github.com/reclaimprotocol/.github/blob/main/SECURITY.md). 30 | - [ ] I have read and signed the [Contributor License Agreement (CLA)](https://github.com/reclaimprotocol/.github/blob/main/CLA.md). 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATES/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 📄 Documentation issue 3 | about: Issues related to documentation. 4 | title: '[Docs]' 5 | labels: 'area/docs, language/markdown' 6 | assignees: '' 7 | --- 8 | 9 | ### Current State 10 | 11 | 12 | ### Desired State 13 | 14 | 15 | --- 16 | ## Before You Begin 17 | 18 | Before submitting an issue, please ensure you have completed the following steps: 19 | 20 | - [ ] I have read and agree to follow the [Code of Conduct](https://github.com/reclaimprotocol/.github/blob/main/Code-of-Conduct.md). 21 | - [ ] I have checked that this is not a security-related issue. For security concerns, please refer to our [Security Policy](https://github.com/reclaimprotocol/.github/blob/main/SECURITY.md). 22 | - [ ] I have read and signed the [Contributor License Agreement (CLA)](https://github.com/reclaimprotocol/.github/blob/main/CLA.md). 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATES/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💡 Feature Request 3 | about: Suggest an enhancement for this project 4 | title: '[FEATURE] ' 5 | labels: 'enhancement' 6 | assignees: '' 7 | --- 8 | 9 | ## Feature Description 10 | 11 | ### Current Situation 12 | 13 | 14 | ### Proposed Enhancement 15 | 16 | 17 | ### Benefits (Optional) 18 | 19 | 20 | ### Possible Implementation (Optional) 21 | 22 | 23 | 24 | ### Additional Context 25 | 26 | 27 | --- 28 | ## Before You Begin 29 | 30 | Before submitting an issue, please ensure you have completed the following steps: 31 | 32 | - [ ] I have read and agree to follow the [Code of Conduct](https://github.com/reclaimprotocol/.github/blob/main/Code-of-Conduct.md). 33 | - [ ] I have checked that this is not a security-related issue. For security concerns, please refer to our [Security Policy](https://github.com/reclaimprotocol/.github/blob/main/SECURITY.md). 34 | - [ ] I have read and signed the [Contributor License Agreement (CLA)](https://github.com/reclaimprotocol/.github/blob/main/CLA.md). 35 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 4 | ### Testing (ignore for documentation update) 5 | 6 | 7 | ### Type of change 8 | - [ ] Bug fix 9 | - [ ] New feature 10 | - [ ] Breaking change 11 | - [ ] Documentation update 12 | 13 | ### Checklist: 14 | - [ ] I have read and agree to the [Code of Conduct](https://github.com/reclaimprotocol/.github/blob/main/Code-of-Conduct.md). 15 | - [ ] I have signed the [Contributor License Agreement (CLA)](https://github.com/reclaimprotocol/.github/blob/main/CLA.md). 16 | - [ ] I have considered the [Security Policy](https://github.com/reclaimprotocol/.github/blob/main/SECURITY.md). 17 | - [ ] I have self-reviewed and tested my code. 18 | - [ ] I have updated documentation as needed. 19 | - [ ] I agree to the [project's custom license](https://github.com/reclaimprotocol/.github/blob/main/LICENSE). 20 | 21 | ### Additional Notes: 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/run-test-build-deploy.yaml: -------------------------------------------------------------------------------- 1 | name: build-deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | inputs: 9 | release_type: 10 | description: "Type of release" 11 | required: true 12 | default: "patch" 13 | type: choice 14 | options: 15 | - patch 16 | - minor 17 | - major 18 | - bugfix 19 | - hotfix 20 | 21 | permissions: 22 | contents: write 23 | packages: write 24 | 25 | jobs: 26 | test_and_build: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v3 30 | 31 | - name: Set up Node.js 32 | uses: actions/setup-node@v3 33 | with: 34 | node-version: "16" 35 | 36 | - name: Clean install dependencies 37 | run: | 38 | rm -rf node_modules 39 | rm -rf package-lock.json 40 | npm i 41 | 42 | - name: Build package 43 | run: npm run build 44 | 45 | - name: Upload build artifacts 46 | uses: actions/upload-artifact@v4 47 | with: 48 | name: dist 49 | path: dist/ 50 | 51 | publish: 52 | needs: test_and_build 53 | runs-on: ubuntu-latest 54 | if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' 55 | steps: 56 | - uses: actions/checkout@v3 57 | with: 58 | persist-credentials: false # Disable automatic token authentication 59 | 60 | - name: Set up Node.js 61 | uses: actions/setup-node@v3 62 | with: 63 | node-version: "16" 64 | registry-url: "https://registry.npmjs.org" 65 | 66 | - name: Download build artifacts 67 | uses: actions/download-artifact@v4 68 | with: 69 | name: dist 70 | path: dist/ 71 | 72 | - name: Version and publish 73 | env: 74 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 75 | run: | 76 | npm publish 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | dist 4 | 5 | .env* 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 | # Reclaim Protocol JavaScript SDK Integration Guide 8 | 9 | This guide will walk you through integrating the Reclaim Protocol JavaScript SDK into your application. We'll create a simple React application that demonstrates how to use the SDK to generate proofs and verify claims. 10 | 11 | ## Prerequisites 12 | 13 | Before we begin, make sure you have: 14 | 15 | 1. An application ID from Reclaim Protocol. 16 | 2. An application secret from Reclaim Protocol. 17 | 3. A provider ID for the specific service you want to verify. 18 | 19 | You can obtain these details from the [Reclaim Developer Portal](https://dev.reclaimprotocol.org/). 20 | 21 | ## Step 1: Create a new React application 22 | 23 | Let's start by creating a new React application: 24 | 25 | ```bash 26 | npx create-react-app reclaim-app 27 | cd reclaim-app 28 | ``` 29 | 30 | ## Step 2: Install necessary dependencies 31 | 32 | Install the Reclaim Protocol SDK and a QR code generator: 33 | 34 | ```bash 35 | npm install @reclaimprotocol/js-sdk react-qr-code 36 | ``` 37 | 38 | ## Step 3: Set up your React component 39 | 40 | Replace the contents of `src/App.js` with the following code: 41 | 42 | ```javascript 43 | import React, { useState, useEffect } from 'react' 44 | import { ReclaimProofRequest } from '@reclaimprotocol/js-sdk' 45 | import QRCode from 'react-qr-code' 46 | 47 | function App() { 48 | const [reclaimProofRequest, setReclaimProofRequest] = useState(null) 49 | const [requestUrl, setRequestUrl] = useState('') 50 | const [statusUrl, setStatusUrl] = useState('') 51 | const [proofs, setProofs] = useState(null) 52 | 53 | useEffect(() => { 54 | async function initializeReclaim() { 55 | const APP_ID = 'YOUR_APPLICATION_ID_HERE' 56 | const APP_SECRET = 'YOUR_APPLICATION_SECRET_HERE' 57 | const PROVIDER_ID = 'YOUR_PROVIDER_ID_HERE' 58 | 59 | const proofRequest = await ReclaimProofRequest.init( 60 | APP_ID, 61 | APP_SECRET, 62 | PROVIDER_ID 63 | ) 64 | setReclaimProofRequest(proofRequest) 65 | } 66 | 67 | initializeReclaim() 68 | }, []) 69 | 70 | async function handleCreateClaim() { 71 | if (!reclaimProofRequest) { 72 | console.error('Reclaim Proof Request not initialized') 73 | return 74 | } 75 | 76 | const url = await reclaimProofRequest.getRequestUrl() 77 | setRequestUrl(url) 78 | 79 | const status = reclaimProofRequest.getStatusUrl() 80 | setStatusUrl(status) 81 | console.log('Status URL:', status) 82 | 83 | await reclaimProofRequest.startSession({ 84 | onSuccess: (proofs) => { 85 | if (proofs && typeof proofs === 'string') { 86 | // When using a custom callback url, the proof is returned to the callback url and we get a message instead of a proof 87 | console.log('SDK Message:', proofs) 88 | setProofs(proofs) 89 | } else if (proofs && typeof proofs !== 'string') { 90 | // When using the default callback url, we get a proof 91 | if (Array.isArray(proofs)) { 92 | // when using the cascading providers, providers having more than one proof will return an array of proofs 93 | console.log(JSON.stringify(proofs.map(p => p.claimData.context))) 94 | } else { 95 | console.log('Proof received:', proofs?.claimData.context) 96 | } 97 | setProofs(proofs) 98 | } 99 | }, 100 | onFailure: (error) => { 101 | console.error('Verification failed', error) 102 | } 103 | }) 104 | } 105 | 106 | return ( 107 |
108 |

Reclaim Protocol Demo

109 | 110 | {requestUrl && ( 111 |
112 |

Scan this QR code to start the verification process:

113 | 114 |
115 | )} 116 | {proofs && ( 117 |
118 |

Verification Successful!

119 |
{JSON.stringify(proofs, null, 2)}
120 |
121 | )} 122 |
123 | ) 124 | } 125 | 126 | export default App 127 | ``` 128 | 129 | ## Step 4: Understanding the code 130 | 131 | Let's break down what's happening in this code: 132 | 133 | 1. We initialize the Reclaim SDK with your application ID, secret, and provider ID. This happens once when the component mounts. 134 | 135 | 2. When the user clicks the "Create Claim" button, we: 136 | - Generate a request URL using `getRequestUrl()`. This URL is used to create the QR code. 137 | - Get the status URL using `getStatusUrl()`. This URL can be used to check the status of the claim process. 138 | - Start a session with `startSession()`, which sets up callbacks for successful and failed verifications. 139 | 140 | 3. We display a QR code using the request URL. When a user scans this code, it starts the verification process. 141 | 142 | 4. The status URL is logged to the console. You can use this URL to check the status of the claim process programmatically. 143 | 144 | 5. When the verification is successful, we display the proof data on the page. 145 | 146 | ## Step 5: New Streamlined Flow with Browser Extension Support 147 | 148 | The Reclaim SDK now provides a simplified `triggerReclaimFlow()` method that automatically handles the verification process across different platforms and devices. This method intelligently chooses the best verification method based on the user's environment. 149 | 150 | ### Using triggerReclaimFlow() 151 | 152 | Replace the `handleCreateClaim` function in your React component with this simpler approach: 153 | 154 | ```javascript 155 | async function handleCreateClaim() { 156 | if (!reclaimProofRequest) { 157 | console.error('Reclaim Proof Request not initialized') 158 | return 159 | } 160 | 161 | try { 162 | // Start the verification process automatically 163 | await reclaimProofRequest.triggerReclaimFlow() 164 | 165 | // Listen for the verification results 166 | await reclaimProofRequest.startSession({ 167 | onSuccess: (proofs) => { 168 | if (proofs && typeof proofs === 'string') { 169 | console.log('SDK Message:', proofs) 170 | setProofs(proofs) 171 | } else if (proofs && typeof proofs !== 'string') { 172 | if (Array.isArray(proofs)) { 173 | console.log(JSON.stringify(proofs.map(p => p.claimData.context))) 174 | } else { 175 | console.log('Proof received:', proofs?.claimData.context) 176 | } 177 | setProofs(proofs) 178 | } 179 | }, 180 | onFailure: (error) => { 181 | console.error('Verification failed', error) 182 | } 183 | }) 184 | } catch (error) { 185 | console.error('Error triggering Reclaim flow:', error) 186 | } 187 | } 188 | ``` 189 | 190 | ### How triggerReclaimFlow() Works 191 | 192 | The `triggerReclaimFlow()` method automatically detects the user's environment and chooses the optimal verification method: 193 | 194 | #### On Desktop Browsers: 195 | 1. **Browser Extension First**: If the Reclaim browser extension is installed, it will use the extension for a seamless in-browser verification experience. 196 | 2. **QR Code Fallback**: If the extension is not available, it automatically displays a QR code modal for mobile scanning. 197 | 198 | #### On Mobile Devices: 199 | 1. **iOS Devices**: Automatically redirects to the Reclaim App Clip for native iOS verification. 200 | 2. **Android Devices**: Automatically redirects to the Reclaim Instant App for native Android verification. 201 | 202 | ### Browser Extension Support 203 | 204 | The SDK now includes built-in support for the Reclaim browser extension, providing users with a seamless verification experience without leaving their current browser tab. 205 | 206 | #### Features: 207 | - **Automatic Detection**: The SDK automatically detects if the Reclaim browser extension is installed 208 | - **Seamless Integration**: No additional setup required - the extension integration works out of the box 209 | - **Fallback Support**: If the extension is not available, the SDK gracefully falls back to QR code or mobile app flows 210 | 211 | #### Manual Extension Detection: 212 | 213 | You can also manually check if the browser extension is available: 214 | 215 | ```javascript 216 | const isExtensionAvailable = await reclaimProofRequest.isBrowserExtensionAvailable() 217 | if (isExtensionAvailable) { 218 | console.log('Reclaim browser extension is installed') 219 | } else { 220 | console.log('Browser extension not available, will use alternative flow') 221 | } 222 | ``` 223 | 224 | #### Configuring Browser Extension Options: 225 | 226 | You can customize the browser extension behavior during SDK initialization: 227 | 228 | ```javascript 229 | const proofRequest = await ReclaimProofRequest.init( 230 | APP_ID, 231 | APP_SECRET, 232 | PROVIDER_ID, 233 | { 234 | useBrowserExtension: true, // Enable/disable browser extension (default: true) 235 | extensionID: 'custom-extension-id', // Use custom extension ID if needed 236 | // ... other options 237 | } 238 | ) 239 | ``` 240 | 241 | ### Modal Customization 242 | 243 | When the QR code modal is displayed (fallback on desktop), you can customize its appearance: 244 | 245 | ```javascript 246 | // Set modal options before triggering the flow 247 | reclaimProofRequest.setModalOptions({ 248 | title: 'Custom Verification Title', 249 | description: 'Scan this QR code with your mobile device to verify your account', 250 | darkTheme: true, // Enable dark theme 251 | extensionUrl: 'https://custom-extension-url.com' // Custom extension download URL 252 | }) 253 | 254 | await reclaimProofRequest.triggerReclaimFlow() 255 | ``` 256 | 257 | ### Benefits of the New Flow: 258 | 259 | 1. **Platform Adaptive**: Automatically chooses the best verification method for each platform 260 | 2. **User-Friendly**: Provides the most seamless experience possible for each user 261 | 3. **Simplified Integration**: Single method call handles all verification scenarios 262 | 4. **Extension Support**: Leverages browser extension for desktop users when available 263 | 5. **Mobile Optimized**: Native app experiences on mobile devices 264 | 265 | ## Step 6: Run your application 266 | 267 | Start your development server: 268 | 269 | ```bash 270 | npm start 271 | ``` 272 | 273 | Your Reclaim SDK demo should now be running. Click the "Create Claim" button to generate a QR code. Scan this code to start the verification process. 274 | 275 | ## Understanding the Claim Process 276 | 277 | 1. **Creating a Claim**: When you click "Create Claim", the SDK generates a unique request for verification. 278 | 279 | 2. **QR Code**: The QR code contains the request URL. When scanned, it initiates the verification process. 280 | 281 | 3. **Status URL**: This URL (logged to the console) can be used to check the status of the claim process. It's useful for tracking the progress of verification. 282 | 283 | 4. **Verification**: The `onSuccess` is called when verification is successful, providing the proof data. When using a custom callback url, the proof is returned to the callback url and we get a message instead of a proof. 284 | 285 | 5. **Handling Failures**: The `onFailure` is called if verification fails, allowing you to handle errors gracefully. 286 | 287 | ## Advanced Configuration 288 | 289 | The Reclaim SDK offers several advanced options to customize your integration: 290 | 291 | 1. **Adding Context**: 292 | You can add context to your proof request, which can be useful for providing additional information: 293 | ```javascript 294 | reclaimProofRequest.addContext('0x00000000000', 'Example context message') 295 | ``` 296 | 297 | 2. **Setting Parameters**: 298 | If your provider requires specific parameters, you can set them like this: 299 | ```javascript 300 | reclaimProofRequest.setParams({ email: "test@example.com", userName: "testUser" }) 301 | ``` 302 | 303 | 3. **Custom Redirect URL**: 304 | Set a custom URL to redirect users after the verification process: 305 | ```javascript 306 | reclaimProofRequest.setRedirectUrl('https://example.com/redirect') 307 | ``` 308 | 309 | 4. **Custom Callback URL**: 310 | Set a custom callback URL for your app which allows you to receive proofs and status updates on your callback URL: 311 | Pass in `jsonProofResponse: true` to receive the proof in JSON format: By default, the proof is returned as a url encoded string. 312 | ```javascript 313 | reclaimProofRequest.setAppCallbackUrl('https://example.com/callback', true) 314 | ``` 315 | 316 | 5. **Modal Customization for Desktop Users**: 317 | Customize the appearance and behavior of the QR code modal shown to desktop users: 318 | ```javascript 319 | reclaimProofRequest.setModalOptions({ 320 | title: 'Verify Your Account', 321 | description: 'Scan the QR code with your mobile device or install our browser extension', 322 | darkTheme: false, // Enable dark theme (default: false) 323 | extensionUrl: 'https://chrome.google.com/webstore/detail/reclaim' // Custom extension URL 324 | }) 325 | ``` 326 | 327 | 6. **Browser Extension Configuration**: 328 | Configure browser extension behavior and detection: 329 | ```javascript 330 | // Check if browser extension is available 331 | const isExtensionAvailable = await reclaimProofRequest.isBrowserExtensionAvailable() 332 | 333 | // Trigger the verification flow with automatic platform detection 334 | await reclaimProofRequest.triggerReclaimFlow() 335 | 336 | // Initialize with browser extension options 337 | const proofRequest = await ReclaimProofRequest.init( 338 | APP_ID, 339 | APP_SECRET, 340 | PROVIDER_ID, 341 | { 342 | useBrowserExtension: true, // Enable browser extension support (default: true) 343 | extensionID: 'custom-extension-id', // Custom extension identifier 344 | useAppClip: true, // Enable mobile app clips (default: true) 345 | log: true // Enable logging for debugging 346 | } 347 | ) 348 | ``` 349 | 350 | 7. **Platform-Specific Flow Control**: 351 | The `triggerReclaimFlow()` method provides intelligent platform detection, but you can still use traditional methods for custom flows: 352 | ```javascript 353 | // Traditional approach with manual QR code handling 354 | const requestUrl = await reclaimProofRequest.getRequestUrl() 355 | // Display your own QR code implementation 356 | 357 | // Or use the new streamlined approach 358 | await reclaimProofRequest.triggerReclaimFlow() 359 | // Automatically handles platform detection and optimal user experience 360 | ``` 361 | 362 | 8. **Exporting and Importing SDK Configuration**: 363 | You can export the entire Reclaim SDK configuration as a JSON string and use it to initialize the SDK with the same configuration on a different service or backend: 364 | ```javascript 365 | // On the client-side or initial service 366 | const configJson = reclaimProofRequest.toJsonString() 367 | console.log('Exportable config:', configJson) 368 | 369 | // Send this configJson to your backend or another service 370 | 371 | // On the backend or different service 372 | const importedRequest = ReclaimProofRequest.fromJsonString(configJson) 373 | const requestUrl = await importedRequest.getRequestUrl() 374 | ``` 375 | This allows you to generate request URLs and other details from your backend or a different service while maintaining the same configuration. 376 | 377 | ## Handling Proofs on Your Backend 378 | 379 | For production applications, it's recommended to handle proofs on your backend: 380 | 381 | 1. Set a callback URL: 382 | ```javascript 383 | reclaimProofRequest.setCallbackUrl('https://your-backend.com/receive-proofs') 384 | ``` 385 | 386 | These options allow you to securely process proofs and status updates on your server. 387 | 388 | ## Next Steps 389 | 390 | Explore the [Reclaim Protocol documentation](https://docs.reclaimprotocol.org/) for more advanced features and best practices for integrating the SDK into your production applications. 391 | 392 | Happy coding with Reclaim Protocol! 393 | 394 | ## Contributing to Our Project 395 | 396 | We welcome contributions to our project! If you find any issues or have suggestions for improvements, please open an issue or submit a pull request. 397 | 398 | ## Security Note 399 | 400 | Always keep your Application Secret secure. Never expose it in client-side code or public repositories. 401 | 402 | ## Code of Conduct 403 | 404 | Please read and follow our [Code of Conduct](https://github.com/reclaimprotocol/.github/blob/main/Code-of-Conduct.md) to ensure a positive and inclusive environment for all contributors. 405 | 406 | ## Security 407 | 408 | If you discover any security-related issues, please refer to our [Security Policy](https://github.com/reclaimprotocol/.github/blob/main/SECURITY.md) for information on how to responsibly disclose vulnerabilities. 409 | 410 | ## Contributor License Agreement 411 | 412 | Before contributing to this project, please read and sign our [Contributor License Agreement (CLA)](https://github.com/reclaimprotocol/.github/blob/main/CLA.md). 413 | 414 | ## Indie Hackers 415 | 416 | For Indie Hackers: [Check out our guidelines and potential grant opportunities](https://github.com/reclaimprotocol/.github/blob/main/Indie-Hackers.md) 417 | 418 | ## License 419 | 420 | This project is licensed under a [custom license](https://github.com/reclaimprotocol/.github/blob/main/LICENSE). By contributing to this project, you agree that your contributions will be licensed under its terms. 421 | 422 | Thank you for your contributions! -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /example/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@reclaimprotocol/js-sdk": "file:..", 13 | "next": "14.1.0", 14 | "next-qrcode": "^2.5.1", 15 | "qrcode": "^1.5.3", 16 | "qrcode-next": "^1.4.5", 17 | "react": "^18", 18 | "react-dom": "^18", 19 | "uuid": "^9.0.1" 20 | }, 21 | "devDependencies": { 22 | "@types/node": "^20", 23 | "@types/qrcode": "^1.5.5", 24 | "@types/react": "^18", 25 | "@types/react-dom": "^18", 26 | "autoprefixer": "^10.0.1", 27 | "postcss": "^8", 28 | "tailwindcss": "^3.3.0", 29 | "typescript": "^5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /example/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reclaimprotocol/reclaim-js-sdk/eff578902cfbd48b6193b0c0ca6eeeaf93c3b99d/example/src/app/favicon.ico -------------------------------------------------------------------------------- /example/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | /* @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } */ 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | /* background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); */ 27 | } 28 | 29 | @layer utilities { 30 | .text-balance { 31 | text-wrap: balance; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Create Next App", 9 | description: "Generated by create next app", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /example/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React, { useEffect, useState } from 'react' 3 | import { ReclaimProofRequest } from '@reclaimprotocol/js-sdk' 4 | import { Proof } from '@reclaimprotocol/js-sdk' 5 | 6 | export default function Home() { 7 | const [isLoading, setIsLoading] = useState(false) 8 | const [proofData, setProofData] = useState(null) 9 | const [error, setError] = useState(null) 10 | const [reclaimProofRequest, setReclaimProofRequest] = useState(null) 11 | 12 | useEffect(() => { 13 | // Initialize the ReclaimProofRequest when the component mounts 14 | initializeReclaimProofRequest() 15 | // verifyProofData() 16 | }, []) 17 | 18 | async function initializeReclaimProofRequest() { 19 | try { 20 | // ReclaimProofRequest Fields: 21 | // - applicationId: Unique identifier for your application 22 | // - providerId: Identifier for the specific provider you're using 23 | // - sessionId: Unique identifier for the current proof request session 24 | // - context: Additional context information for the proof request 25 | // - requestedProof: Details of the proof being requested 26 | // - signature: Cryptographic signature for request authentication 27 | // - redirectUrl: URL to redirect after proof generation (optional) 28 | // - appCallbackUrl: URL for receiving proof generation updates (optional) 29 | // - timeStamp: Timestamp of the proof request 30 | // - options: Additional configuration options 31 | 32 | 33 | const proofRequest = await ReclaimProofRequest.init( 34 | process.env.NEXT_PUBLIC_RECLAIM_APP_ID!, 35 | process.env.NEXT_PUBLIC_RECLAIM_APP_SECRET!, 36 | process.env.NEXT_PUBLIC_RECLAIM_PROVIDER_ID!, 37 | // Uncomment the following line to enable logging and AI providers 38 | { 39 | log: true, 40 | } 41 | ) 42 | setReclaimProofRequest(proofRequest) 43 | 44 | // // // Add context to the proof request (optional) 45 | // proofRequest.addContext("0x48796C654F7574707574", "test") 46 | 47 | // Set parameters for the proof request (if needed) 48 | // proofRequest.setParams({ email: "test@example.com", userName: "testUser" }) 49 | 50 | // Set a redirect URL (if needed) 51 | // proofRequest.setRedirectUrl('https://example.com/redirect') 52 | 53 | // Set a custom app callback URL (if needed) 54 | // proofRequest.setAppCallbackUrl('https://webhook.site/fd6cf442-0ea7-4427-8cb8-cb4dbe8884d2') 55 | 56 | // Uncomment the following line to log the proof request and to get the Json String 57 | // console.log('Proof request initialized:', proofRequest.toJsonString()) 58 | } catch (error) { 59 | console.error('Error initializing ReclaimProofRequest:', error) 60 | setError('Failed to initialize Reclaim. Please try again.') 61 | } 62 | } 63 | 64 | async function startClaimProcess() { 65 | if (!reclaimProofRequest) { 66 | setError('Reclaim not initialized. Please refresh the page.') 67 | return 68 | } 69 | 70 | setIsLoading(true) 71 | setError(null) 72 | 73 | try { 74 | // Start the verification session 75 | await reclaimProofRequest.triggerReclaimFlow() 76 | 77 | await reclaimProofRequest.startSession({ 78 | onSuccess: async (proof: Proof | Proof[] | string | undefined) => { 79 | setIsLoading(false) 80 | 81 | if (proof && typeof proof === 'string') { 82 | console.log('SDK Message:', proof) 83 | setError('Received string response instead of proof object.') 84 | } else if (proof && typeof proof !== 'string') { 85 | console.log('Proof received:', proof) 86 | if (Array.isArray(proof)) { 87 | setProofData(proof) 88 | } else { 89 | setProofData([proof]) 90 | } 91 | } 92 | }, 93 | onError: (error: Error) => { 94 | console.error('Error in proof generation:', error) 95 | setIsLoading(false) 96 | setError(`Error: ${error.message}`) 97 | } 98 | }) 99 | } catch (error) { 100 | console.error('Error starting verification session:', error) 101 | setIsLoading(false) 102 | setError('Failed to start verification. Please try again.') 103 | } 104 | } 105 | 106 | // Function to extract provider URL from parameters 107 | const getProviderUrl = (proof: Proof) => { 108 | try { 109 | const parameters = JSON.parse(proof.claimData.parameters); 110 | return parameters.url || "Unknown Provider"; 111 | } catch (e) { 112 | return proof.claimData.provider || "Unknown Provider"; 113 | } 114 | } 115 | 116 | // Function to beautify and display extracted parameters 117 | const renderExtractedParameters = (proof: Proof) => { 118 | try { 119 | const context = JSON.parse(proof.claimData.context) 120 | const extractedParams = context.extractedParameters || {} 121 | 122 | return ( 123 | <> 124 |

Extracted Parameters

125 | {Object.entries(extractedParams).length > 0 ? ( 126 |
127 | {Object.entries(extractedParams).map(([key, value]) => ( 128 |
129 |
130 | {key}: 131 | {String(value)} 132 |
133 |
134 | ))} 135 |
136 | ) : ( 137 |

No parameters extracted

138 | )} 139 | 140 | ) 141 | } catch (e) { 142 | return

Failed to parse parameters

143 | } 144 | } 145 | 146 | return ( 147 |
148 |
149 |

Reclaim SDK

150 | 151 | {!proofData && !isLoading && ( 152 |
153 |

154 | Click the button below to start the claim process. 155 |

156 | 163 |
164 | )} 165 | 166 | {isLoading && ( 167 |
168 |
169 |

Processing your claim...

170 |
171 | )} 172 | 173 | {error && ( 174 |
175 | {error} 176 |
177 | )} 178 | 179 | {proofData && proofData.length > 0 && ( 180 |
181 |

Verification Successful

182 | 183 | {proofData.map((proof, index) => ( 184 |
185 |
186 |

Proof #{index + 1}

187 | Verified 188 |
189 | 190 |
191 |
192 |

Provider

193 |

{getProviderUrl(proof)}

194 |
195 |
196 |

Timestamp

197 |

{new Date(proof.claimData.timestampS * 1000).toLocaleString()}

198 |
199 |
200 | 201 | {/* Extracted parameters section */} 202 |
203 | {renderExtractedParameters(proof)} 204 |
205 | 206 | {/* Witnesses section */} 207 | {proof.witnesses && proof.witnesses.length > 0 && ( 208 |
209 |

Attested by

210 |
211 | {proof.witnesses.map((witness, widx) => ( 212 |
213 |

{witness.id}

214 |
215 | ))} 216 |
217 |
218 | )} 219 | 220 | {/* Signatures section */} 221 | {proof.signatures && proof.signatures.length > 0 && ( 222 |
223 |

Signatures

224 |
225 | {proof.signatures.map((signature, sidx) => ( 226 |
227 |

{signature}

228 |
229 | ))} 230 |
231 |
232 | )} 233 | 234 | {/* Identifier (full) */} 235 |
236 |

Proof Identifier

237 |

238 | {proof.identifier} 239 |

240 |
241 |
242 | ))} 243 | 244 | 250 |
251 | )} 252 |
253 |
254 | ) 255 | } 256 | -------------------------------------------------------------------------------- /example/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reclaimprotocol/js-sdk", 3 | "version": "4.2.0", 4 | "description": "Designed to request proofs from the Reclaim protocol and manage the flow of claims and witness interactions.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "keywords": [ 8 | "reclaim", 9 | "protocol", 10 | "blockchain", 11 | "proof", 12 | "verification", 13 | "identity", 14 | "claims", 15 | "witness", 16 | "sdk", 17 | "javascript", 18 | "typescript", 19 | "decentralized", 20 | "web3" 21 | ], 22 | "files": [ 23 | "dist" 24 | ], 25 | "tsup": { 26 | "entry": [ 27 | "src/index.ts" 28 | ], 29 | "splitting": false, 30 | "sourcemap": true, 31 | "clean": true 32 | }, 33 | "scripts": { 34 | "build": "sh scripts/build.sh", 35 | "release": "release-it", 36 | "test": "echo \"Error: no test specified\" && exit 1", 37 | "commitlint": "commitlint --edit" 38 | }, 39 | "repository": { 40 | "type": "git", 41 | "url": "https://github.com/reclaimprotocol/reclaim-js-sdk" 42 | }, 43 | "author": "ali ", 44 | "license": "See License in ", 45 | "bugs": { 46 | "url": "https://github.com/reclaimprotocol/reclaim-js-sdk/issues" 47 | }, 48 | "homepage": "https://github.com/reclaimprotocol/reclaim-js-sdk/", 49 | "publishConfig": { 50 | "registry": "https://registry.npmjs.org/", 51 | "access": "public" 52 | }, 53 | "release-it": { 54 | "git": { 55 | "commitMessage": "chore: release ${version}", 56 | "tagName": "v${version}" 57 | }, 58 | "npm": { 59 | "publish": true, 60 | "tag": "latest" 61 | }, 62 | "github": { 63 | "release": true 64 | }, 65 | "plugins": { 66 | "@release-it/conventional-changelog": { 67 | "preset": "angular" 68 | } 69 | } 70 | }, 71 | "devDependencies": { 72 | "@bconnorwhite/bob": "^2.9.5", 73 | "@commitlint/cli": "^17.7.1", 74 | "@commitlint/config-conventional": "^17.7.0", 75 | "@release-it/conventional-changelog": "^5.0.0", 76 | "@types/qs": "^6.9.11", 77 | "@types/url-parse": "^1.4.11", 78 | "@types/uuid": "^9.0.7", 79 | "release-it": "^15.0.0", 80 | "tsup": "^8.0.1", 81 | "typescript": "^5.3.3" 82 | }, 83 | "dependencies": { 84 | "canonicalize": "^2.0.0", 85 | "ethers": "^6.9.1", 86 | "qs": "^6.11.2", 87 | "url-parse": "^1.5.10", 88 | "uuid": "^9.0.1" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /readme/depemdency-diagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
Reclaim SDK
Reclaim SDK
uuid
uuid
canonicalize
canonicalize
ethers
ethers
Text is not SVG - cannot display
-------------------------------------------------------------------------------- /readme/usage-flow-2.svg: -------------------------------------------------------------------------------- 1 | participant%20AppClip%2FInstanApp%0Aparticipant%20User%0Aparticipant%20SDK%20Sample%20Web%20App%0Aparticipant%20Reclaim%20SDK%0A%0AUser-%3ESDK%20Sample%20Web%20App%3A%20ask%20to%20provide%20proof%0A%0ASDK%20Sample%20Web%20App-%3EReclaim%20SDK%3A%20requestProof(request%2C%20AppCallbackUrl)%0Aactivate%20Reclaim%20SDK%0AReclaim%20SDK-%3EReclaim%20SDK%3A%20Validate%20request%0AReclaim%20SDK-%3EReclaim%20SDK%3A%20Generate%20Template%20Instance%0AReclaim%20SDK--%3ESDK%20Sample%20Web%20App%3A%20TemplateInstance%0Adeactivate%20Reclaim%20SDK%0ASDK%20Sample%20Web%20App-%3ESDK%20Sample%20Web%20App%3A%20Generate%20QrCode%20from%20template%0A%0AUser--%3ESDK%20Sample%20Web%20App%3A%20Scan%20QrCode%0A%0ASDK%20Sample%20Web%20App--%3EAppClip%2FInstanApp%3A%20Open%20AppClip%2FInstanApp%0A%0AAppClip%2FInstanApp-%3EAppClip%2FInstanApp%3A%20complete%20verification%20task%0A%0AAppClip%2FInstanApp-%3ESDK%20Sample%20Web%20App%3A%20Submit%20Proof%20using%20AppCallbackUrl%0A%0ASDK%20Sample%20Web%20App--%3EUser%3A%20notify%20user%20(proof%20received)%0AAppClip/InstanAppUserSDK Sample Web AppReclaim SDKask to provide proofrequestProof(request, AppCallbackUrl)Validate requestGenerate Template InstanceTemplateInstanceGenerate QrCode from templateScan QrCodeOpen AppClip/InstanAppcomplete verification taskSubmit Proof using AppCallbackUrlnotify user (proof received) -------------------------------------------------------------------------------- /readme/usage-flow-3.svg: -------------------------------------------------------------------------------- 1 | participant%20AppClip%2FInstanApp%0Aparticipant%20User%0Aparticipant%20SDK%20Sample%20Web%20App%0Aparticipant%20Reclaim%20SDK%0A%0AUser-%3ESDK%20Sample%20Web%20App%3A%20ask%20to%20provide%20proof%0A%0ASDK%20Sample%20Web%20App-%3EReclaim%20SDK%3A%20requestProof(request%2C%20AppCallbackUrl)%0Aactivate%20Reclaim%20SDK%0AReclaim%20SDK-%3EReclaim%20SDK%3A%20Generate%20Template%20Instance%0AReclaim%20SDK--%3ESDK%20Sample%20Web%20App%3A%20TemplateInstance%0Adeactivate%20Reclaim%20SDK%0A%0ASDK%20Sample%20Web%20App-%3ESDK%20Sample%20Web%20App%3A%20Generate%20QrCode%20from%20template%0A%0AUser--%3ESDK%20Sample%20Web%20App%3A%20Scan%20QrCode%0A%0ASDK%20Sample%20Web%20App--%3EAppClip%2FInstanApp%3A%20Open%20AppClip%2FInstanApp%0A%0AAppClip%2FInstanApp-%3EAppClip%2FInstanApp%3A%20complete%20verification%20task%0A%0AAppClip%2FInstanApp-%3ESDK%20Sample%20Web%20App%3A%20Submit%20Proof%20using%20AppCallbackUrl%0A%0ASDK%20Sample%20Web%20App--%3EUser%3A%20notify%20user%20(proof%20received)%0AAppClip/InstanAppUserSDK Sample Web AppReclaim SDKask to provide proofrequestProof(request, AppCallbackUrl)Generate Template InstanceTemplateInstanceGenerate QrCode from templateScan QrCodeOpen AppClip/InstanAppcomplete verification taskSubmit Proof using AppCallbackUrlnotify user (proof received) -------------------------------------------------------------------------------- /readme/usage-flow.svg: -------------------------------------------------------------------------------- 1 | %0A%0Aparticipant%20AppClip%2FInstanApp%0Aparticipant%20User%0Aparticipant%Reclaim%0Aparticipant%20Reclaim%20SDK%0A%0AUser-%3EApp%3A%20ask%20to%20provide%20proof%0A%0AApp-%3EReclaim%20SDK%3A%20requestProof(request%2C%20AppCallbackUrl)%0Aactivate%20Reclaim%20SDK%0AReclaim%20SDK-%3EReclaim%20SDK%3A%20Validate%20request%0AReclaim%20SDK-%3EReclaim%20SDK%3A%20Generate%20Template%20Instance%0AReclaim%20SDK--%3EApp%3A%20TemplateInstance%0Adeactivate%20Reclaim%20SDK%0AApp-%3EApp%3A%20Generate%20QrCode%20from%20template%0A%0A%0AUser--%3EApp%3A%20Scan%20QrCode%0A%0AApp--%3EAppClip%2FInstanApp%3A%20Open%20AppClip%2FInstanApp%0A%0AAppClip%2FInstanApp-%3EAppClip%2FInstanApp%3A%20complete%20verification%20task%0A%0A%0AAppClip%2FInstanApp-%3EApp%3A%20Submit%20Proof%20using%20AppCallbackUrl%0A%0A%0AApp--%3EUser%3A%20notify%20user%20(proof%20received)%0AAppClip/InstanAppUserAppReclaim SDKask to provide proofrequestProof(request, AppCallbackUrl)Validate requestGenerate Template InstanceTemplateInstanceGenerate QrCode from templateScan QrCodeOpen AppClip/InstanAppcomplete verification taskSubmit Proof using AppCallbackUrlnotify user (proof received) -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Delete existing dist folder 4 | echo "Deleting existing dist folder" 5 | rm -rf dist 6 | 7 | # Build 8 | echo "Building the project" 9 | tsup --dts 10 | -------------------------------------------------------------------------------- /src/Reclaim.ts: -------------------------------------------------------------------------------- 1 | import { type Proof, type Context, RECLAIM_EXTENSION_ACTIONS, ExtensionMessage } from './utils/interfaces' 2 | import { getIdentifierFromClaimInfo } from './witness' 3 | import { 4 | SignedClaim, 5 | ProofRequestOptions, 6 | StartSessionParams, 7 | ProofPropertiesJSON, 8 | TemplateData, 9 | InitSessionResponse, 10 | ClaimCreationType, 11 | ModalOptions, 12 | } from './utils/types' 13 | import { SessionStatus, DeviceType } from './utils/types' 14 | import { ethers } from 'ethers' 15 | import canonicalize from 'canonicalize' 16 | import { 17 | replaceAll, 18 | scheduleIntervalEndingTask 19 | } from './utils/helper' 20 | import { constants, setBackendBaseUrl } from './utils/constants' 21 | import { 22 | AddContextError, 23 | GetAppCallbackUrlError, 24 | GetStatusUrlError, 25 | InitError, 26 | InvalidParamError, 27 | ProofNotVerifiedError, 28 | ProofSubmissionFailedError, 29 | ProviderFailedError, 30 | SessionNotStartedError, 31 | SetParamsError, 32 | SetSignatureError, 33 | SignatureGeneratingError, 34 | SignatureNotFoundError 35 | } from './utils/errors' 36 | import { validateContext, validateFunctionParams, validateParameters, validateSignature, validateURL } from './utils/validationUtils' 37 | import { fetchStatusUrl, initSession, updateSession } from './utils/sessionUtils' 38 | import { assertValidSignedClaim, createLinkWithTemplateData, getWitnessesForClaim } from './utils/proofUtils' 39 | import { QRCodeModal } from './utils/modalUtils' 40 | import loggerModule from './utils/logger'; 41 | import { getDeviceType, getMobileDeviceType, isMobileDevice } from './utils/device' 42 | const logger = loggerModule.logger 43 | 44 | const sdkVersion = require('../package.json').version; 45 | 46 | // Implementation 47 | export async function verifyProof(proofOrProofs: Proof | Proof[]): Promise { 48 | // If input is an array of proofs 49 | if (Array.isArray(proofOrProofs)) { 50 | for (const proof of proofOrProofs) { 51 | const isVerified = await verifyProof(proof); 52 | if (!isVerified) { 53 | return false; 54 | } 55 | } 56 | return true; 57 | } 58 | 59 | // Single proof verification logic 60 | const proof = proofOrProofs; 61 | if (!proof.signatures.length) { 62 | throw new SignatureNotFoundError('No signatures') 63 | } 64 | 65 | try { 66 | // check if witness array exist and first element is manual-verify 67 | let witnesses = [] 68 | if (proof.witnesses.length && proof.witnesses[0]?.url === 'manual-verify') { 69 | witnesses.push(proof.witnesses[0].id) 70 | } else { 71 | witnesses = await getWitnessesForClaim( 72 | proof.claimData.epoch, 73 | proof.identifier, 74 | proof.claimData.timestampS 75 | ) 76 | } 77 | // then hash the claim info with the encoded ctx to get the identifier 78 | const calculatedIdentifier = getIdentifierFromClaimInfo({ 79 | parameters: JSON.parse( 80 | canonicalize(proof.claimData.parameters) as string 81 | ), 82 | provider: proof.claimData.provider, 83 | context: proof.claimData.context 84 | }) 85 | proof.identifier = replaceAll(proof.identifier, '"', '') 86 | // check if the identifier matches the one in the proof 87 | if (calculatedIdentifier !== proof.identifier) { 88 | throw new ProofNotVerifiedError('Identifier Mismatch') 89 | } 90 | 91 | const signedClaim: SignedClaim = { 92 | claim: { 93 | ...proof.claimData 94 | }, 95 | signatures: proof.signatures.map(signature => { 96 | return ethers.getBytes(signature) 97 | }) 98 | } 99 | 100 | assertValidSignedClaim(signedClaim, witnesses) 101 | } catch (e: Error | unknown) { 102 | logger.info(`Error verifying proof: ${e instanceof Error ? e.message : String(e)}`) 103 | return false 104 | } 105 | 106 | return true 107 | } 108 | 109 | export function transformForOnchain(proof: Proof): { claimInfo: any, signedClaim: any } { 110 | const claimInfoBuilder = new Map([ 111 | ['context', proof.claimData.context], 112 | ['parameters', proof.claimData.parameters], 113 | ['provider', proof.claimData.provider], 114 | ]); 115 | const claimInfo = Object.fromEntries(claimInfoBuilder); 116 | const claimBuilder = new Map([ 117 | ['epoch', proof.claimData.epoch], 118 | ['identifier', proof.claimData.identifier], 119 | ['owner', proof.claimData.owner], 120 | ['timestampS', proof.claimData.timestampS], 121 | ]); 122 | const signedClaim = { 123 | claim: Object.fromEntries(claimBuilder), 124 | signatures: proof.signatures, 125 | }; 126 | return { claimInfo, signedClaim }; 127 | } 128 | 129 | // create a empty template data object to assign to templateData 130 | const emptyTemplateData: TemplateData = { 131 | sessionId: '', 132 | providerId: '', 133 | applicationId: '', 134 | signature: '', 135 | timestamp: '', 136 | callbackUrl: '', 137 | context: '', 138 | parameters: {}, 139 | redirectUrl: '', 140 | acceptAiProviders: false, 141 | sdkVersion: '', 142 | providerVersion: '', 143 | resolvedProviderVersion: '', 144 | jsonProofResponse: false 145 | } 146 | export class ReclaimProofRequest { 147 | // Private class properties 148 | private applicationId: string; 149 | private signature?: string; 150 | private appCallbackUrl?: string; 151 | private sessionId: string; 152 | private options?: ProofRequestOptions; 153 | private context: Context = { contextAddress: '0x0', contextMessage: 'sample context' }; 154 | private claimCreationType?: ClaimCreationType = ClaimCreationType.STANDALONE; 155 | private providerId: string; 156 | private resolvedProviderVersion?: string; 157 | private parameters: { [key: string]: string }; 158 | private redirectUrl?: string; 159 | private intervals: Map = new Map(); 160 | private timeStamp: string; 161 | private sdkVersion: string; 162 | private jsonProofResponse: boolean = false; 163 | private lastFailureTime?: number; 164 | private templateData: TemplateData; 165 | private extensionID: string = "reclaim-extension"; 166 | private modalOptions?: ModalOptions; 167 | private readonly FAILURE_TIMEOUT = 30000; // 30 seconds timeout, can be adjusted 168 | 169 | // Private constructor 170 | private constructor(applicationId: string, providerId: string, options?: ProofRequestOptions) { 171 | this.providerId = providerId; 172 | this.timeStamp = Date.now().toString(); 173 | this.applicationId = applicationId; 174 | this.sessionId = ""; 175 | // keep template data as empty object 176 | this.templateData = emptyTemplateData; 177 | this.parameters = {}; 178 | 179 | if (!options) { 180 | options = {}; 181 | } 182 | 183 | options.useBrowserExtension = options.useBrowserExtension ?? true; 184 | 185 | if (options?.log) { 186 | loggerModule.setLogLevel('info'); 187 | } else { 188 | loggerModule.setLogLevel('silent'); 189 | } 190 | 191 | if (options.useAppClip === undefined) { 192 | options.useAppClip = true; 193 | } 194 | 195 | if (options?.envUrl) { 196 | setBackendBaseUrl(options.envUrl); 197 | } 198 | 199 | if (options.extensionID) { 200 | this.extensionID = options.extensionID; 201 | } 202 | 203 | this.options = options; 204 | // Fetch sdk version from package.json 205 | this.sdkVersion = 'js-' + sdkVersion; 206 | logger.info(`Initializing client with applicationId: ${this.applicationId}`); 207 | } 208 | 209 | // Static initialization methods 210 | static async init(applicationId: string, appSecret: string, providerId: string, options?: ProofRequestOptions): Promise { 211 | try { 212 | validateFunctionParams([ 213 | { paramName: 'applicationId', input: applicationId, isString: true }, 214 | { paramName: 'providerId', input: providerId, isString: true }, 215 | { paramName: 'appSecret', input: appSecret, isString: true } 216 | ], 'the constructor') 217 | 218 | // check if options is provided and validate each property of options 219 | if (options) { 220 | if (options.acceptAiProviders) { 221 | validateFunctionParams([ 222 | { paramName: 'acceptAiProviders', input: options.acceptAiProviders } 223 | ], 'the constructor') 224 | } 225 | if (options.providerVersion) { 226 | validateFunctionParams([ 227 | { paramName: 'providerVersion', input: options.providerVersion, isString: true } 228 | ], 'the constructor') 229 | } 230 | if (options.log) { 231 | validateFunctionParams([ 232 | { paramName: 'log', input: options.log } 233 | ], 'the constructor') 234 | } 235 | if (options.useAppClip) { 236 | validateFunctionParams([ 237 | { paramName: 'useAppClip', input: options.useAppClip } 238 | ], 'the constructor') 239 | } 240 | if (options.device) { 241 | validateFunctionParams([ 242 | { paramName: 'device', input: options.device, isString: true } 243 | ], 'the constructor') 244 | } 245 | if (options.useBrowserExtension) { 246 | validateFunctionParams([ 247 | { paramName: 'useBrowserExtension', input: options.useBrowserExtension } 248 | ], 'the constructor') 249 | } 250 | if (options.extensionID) { 251 | validateFunctionParams([ 252 | { paramName: 'extensionID', input: options.extensionID, isString: true } 253 | ], 'the constructor') 254 | } 255 | if (options.envUrl) { 256 | validateFunctionParams([ 257 | { paramName: 'envUrl', input: options.envUrl, isString: true } 258 | ], 'the constructor') 259 | } 260 | 261 | } 262 | 263 | const proofRequestInstance = new ReclaimProofRequest(applicationId, providerId, options) 264 | 265 | const signature = await proofRequestInstance.generateSignature(appSecret) 266 | proofRequestInstance.setSignature(signature) 267 | 268 | const data: InitSessionResponse = await initSession(providerId, applicationId, proofRequestInstance.timeStamp, signature, options?.providerVersion); 269 | proofRequestInstance.sessionId = data.sessionId 270 | proofRequestInstance.resolvedProviderVersion = data.resolvedProviderVersion 271 | 272 | return proofRequestInstance 273 | } catch (error) { 274 | logger.info('Failed to initialize ReclaimProofRequest', error as Error); 275 | throw new InitError('Failed to initialize ReclaimProofRequest', error as Error) 276 | } 277 | } 278 | 279 | static async fromJsonString(jsonString: string): Promise { 280 | try { 281 | const { 282 | applicationId, 283 | providerId, 284 | sessionId, 285 | context, 286 | parameters, 287 | signature, 288 | redirectUrl, 289 | timeStamp, 290 | appCallbackUrl, 291 | claimCreationType, 292 | options, 293 | sdkVersion, 294 | jsonProofResponse, 295 | resolvedProviderVersion 296 | }: ProofPropertiesJSON = JSON.parse(jsonString) 297 | 298 | validateFunctionParams([ 299 | { input: applicationId, paramName: 'applicationId', isString: true }, 300 | { input: providerId, paramName: 'providerId', isString: true }, 301 | { input: signature, paramName: 'signature', isString: true }, 302 | { input: sessionId, paramName: 'sessionId', isString: true }, 303 | { input: timeStamp, paramName: 'timeStamp', isString: true }, 304 | { input: sdkVersion, paramName: 'sdkVersion', isString: true }, 305 | ], 'fromJsonString'); 306 | 307 | 308 | if (redirectUrl) { 309 | validateURL(redirectUrl, 'fromJsonString'); 310 | } 311 | 312 | if (appCallbackUrl) { 313 | validateURL(appCallbackUrl, 'fromJsonString'); 314 | } 315 | 316 | if (context) { 317 | validateContext(context); 318 | } 319 | 320 | if (parameters) { 321 | validateParameters(parameters); 322 | } 323 | 324 | if (claimCreationType) { 325 | validateFunctionParams([ 326 | { input: claimCreationType, paramName: 'claimCreationType' } 327 | ], 'fromJsonString'); 328 | } 329 | 330 | if (jsonProofResponse !== undefined) { 331 | validateFunctionParams([ 332 | { input: jsonProofResponse, paramName: 'jsonProofResponse' } 333 | ], 'fromJsonString'); 334 | } 335 | 336 | 337 | if (options?.providerVersion) { 338 | validateFunctionParams([ 339 | { input: options.providerVersion, paramName: 'providerVersion', isString: true } 340 | ], 'fromJsonString'); 341 | } 342 | 343 | if (resolvedProviderVersion) { 344 | validateFunctionParams([ 345 | { input: resolvedProviderVersion, paramName: 'resolvedProviderVersion', isString: true } 346 | ], 'fromJsonString'); 347 | } 348 | 349 | const proofRequestInstance = new ReclaimProofRequest(applicationId, providerId, options); 350 | proofRequestInstance.sessionId = sessionId; 351 | proofRequestInstance.context = context; 352 | proofRequestInstance.parameters = parameters; 353 | proofRequestInstance.appCallbackUrl = appCallbackUrl 354 | proofRequestInstance.redirectUrl = redirectUrl 355 | proofRequestInstance.timeStamp = timeStamp 356 | proofRequestInstance.signature = signature 357 | proofRequestInstance.sdkVersion = sdkVersion; 358 | proofRequestInstance.resolvedProviderVersion = resolvedProviderVersion; 359 | return proofRequestInstance 360 | } catch (error) { 361 | logger.info('Failed to parse JSON string in fromJsonString:', error); 362 | throw new InvalidParamError('Invalid JSON string provided to fromJsonString'); 363 | } 364 | } 365 | 366 | // Setter methods 367 | setAppCallbackUrl(url: string, jsonProofResponse?: boolean): void { 368 | validateURL(url, 'setAppCallbackUrl') 369 | this.appCallbackUrl = url 370 | this.jsonProofResponse = jsonProofResponse ?? false 371 | } 372 | 373 | setRedirectUrl(url: string): void { 374 | validateURL(url, 'setRedirectUrl'); 375 | this.redirectUrl = url; 376 | } 377 | 378 | setClaimCreationType(claimCreationType: ClaimCreationType): void { 379 | this.claimCreationType = claimCreationType; 380 | } 381 | 382 | setModalOptions(options: ModalOptions): void { 383 | try { 384 | // Validate modal options 385 | if (options.title !== undefined) { 386 | validateFunctionParams([ 387 | { input: options.title, paramName: 'title', isString: true } 388 | ], 'setModalOptions'); 389 | } 390 | 391 | if (options.description !== undefined) { 392 | validateFunctionParams([ 393 | { input: options.description, paramName: 'description', isString: true } 394 | ], 'setModalOptions'); 395 | } 396 | 397 | if (options.extensionUrl !== undefined) { 398 | validateURL(options.extensionUrl, 'setModalOptions'); 399 | } 400 | 401 | if (options.darkTheme !== undefined) { 402 | validateFunctionParams([ 403 | { input: options.darkTheme, paramName: 'darkTheme' } 404 | ], 'setModalOptions'); 405 | } 406 | 407 | this.modalOptions = { ...this.modalOptions, ...options }; 408 | logger.info('Modal options set successfully'); 409 | } catch (error) { 410 | logger.info('Error setting modal options:', error); 411 | throw new SetParamsError('Error setting modal options', error as Error); 412 | } 413 | } 414 | 415 | addContext(address: string, message: string): void { 416 | try { 417 | validateFunctionParams([ 418 | { input: address, paramName: 'address', isString: true }, 419 | { input: message, paramName: 'message', isString: true } 420 | ], 'addContext'); 421 | this.context = { contextAddress: address, contextMessage: message }; 422 | } catch (error) { 423 | logger.info("Error adding context", error) 424 | throw new AddContextError("Error adding context", error as Error) 425 | } 426 | } 427 | 428 | setParams(params: { [key: string]: string }): void { 429 | try { 430 | validateParameters(params); 431 | this.parameters = { ...this.parameters, ...params } 432 | } catch (error) { 433 | logger.info('Error Setting Params:', error); 434 | throw new SetParamsError("Error setting params", error as Error) 435 | } 436 | } 437 | 438 | // Getter methods 439 | getAppCallbackUrl(): string { 440 | try { 441 | validateFunctionParams([{ input: this.sessionId, paramName: 'sessionId', isString: true }], 'getAppCallbackUrl'); 442 | return this.appCallbackUrl || `${constants.DEFAULT_RECLAIM_CALLBACK_URL}${this.sessionId}` 443 | } catch (error) { 444 | logger.info("Error getting app callback url", error) 445 | throw new GetAppCallbackUrlError("Error getting app callback url", error as Error) 446 | } 447 | } 448 | 449 | getStatusUrl(): string { 450 | try { 451 | validateFunctionParams([{ input: this.sessionId, paramName: 'sessionId', isString: true }], 'getStatusUrl'); 452 | return `${constants.DEFAULT_RECLAIM_STATUS_URL}${this.sessionId}` 453 | } catch (error) { 454 | logger.info("Error fetching Status Url", error) 455 | throw new GetStatusUrlError("Error fetching status url", error as Error) 456 | } 457 | } 458 | 459 | // getter for SessionId 460 | getSessionId(): string { 461 | if (!this.sessionId) { 462 | throw new SessionNotStartedError("SessionId is not set"); 463 | } 464 | return this.sessionId; 465 | } 466 | 467 | // Private helper methods 468 | private setSignature(signature: string): void { 469 | try { 470 | validateFunctionParams([{ input: signature, paramName: 'signature', isString: true }], 'setSignature'); 471 | this.signature = signature; 472 | logger.info(`Signature set successfully for applicationId: ${this.applicationId}`); 473 | } catch (error) { 474 | logger.info("Error setting signature", error) 475 | throw new SetSignatureError("Error setting signature", error as Error) 476 | } 477 | } 478 | 479 | private async generateSignature(applicationSecret: string): Promise { 480 | try { 481 | const wallet = new ethers.Wallet(applicationSecret) 482 | const canonicalData = canonicalize({ providerId: this.providerId, timestamp: this.timeStamp }); 483 | 484 | 485 | if (!canonicalData) { 486 | throw new SignatureGeneratingError('Failed to canonicalize data for signing.'); 487 | } 488 | 489 | const messageHash = ethers.keccak256(new TextEncoder().encode(canonicalData)); 490 | 491 | return await wallet.signMessage(ethers.getBytes(messageHash)); 492 | } catch (err) { 493 | logger.info(`Error generating proof request for applicationId: ${this.applicationId}, providerId: ${this.providerId}, signature: ${this.signature}, timeStamp: ${this.timeStamp}`, err); 494 | throw new SignatureGeneratingError(`Error generating signature for applicationSecret: ${applicationSecret}`) 495 | } 496 | } 497 | 498 | private clearInterval(): void { 499 | if (this.sessionId && this.intervals.has(this.sessionId)) { 500 | clearInterval(this.intervals.get(this.sessionId) as NodeJS.Timeout) 501 | this.intervals.delete(this.sessionId) 502 | } 503 | } 504 | 505 | // Public methods 506 | toJsonString(options?: ProofRequestOptions): string { 507 | return JSON.stringify({ 508 | applicationId: this.applicationId, 509 | providerId: this.providerId, 510 | sessionId: this.sessionId, 511 | context: this.context, 512 | appCallbackUrl: this.appCallbackUrl, 513 | claimCreationType: this.claimCreationType, 514 | parameters: this.parameters, 515 | signature: this.signature, 516 | redirectUrl: this.redirectUrl, 517 | timeStamp: this.timeStamp, 518 | options: this.options, 519 | sdkVersion: this.sdkVersion, 520 | jsonProofResponse: this.jsonProofResponse, 521 | resolvedProviderVersion: this.resolvedProviderVersion ?? '' 522 | }) 523 | } 524 | 525 | async getRequestUrl(): Promise { 526 | logger.info('Creating Request Url') 527 | if (!this.signature) { 528 | throw new SignatureNotFoundError('Signature is not set.') 529 | } 530 | 531 | try { 532 | validateSignature(this.providerId, this.signature, this.applicationId, this.timeStamp) 533 | 534 | const templateData: TemplateData = { 535 | sessionId: this.sessionId, 536 | providerId: this.providerId, 537 | providerVersion: this.options?.providerVersion ?? '', 538 | resolvedProviderVersion: this.resolvedProviderVersion ?? '', 539 | applicationId: this.applicationId, 540 | signature: this.signature, 541 | timestamp: this.timeStamp, 542 | callbackUrl: this.getAppCallbackUrl(), 543 | context: JSON.stringify(this.context), 544 | parameters: this.parameters, 545 | redirectUrl: this.redirectUrl ?? '', 546 | acceptAiProviders: this.options?.acceptAiProviders ?? false, 547 | sdkVersion: this.sdkVersion, 548 | jsonProofResponse: this.jsonProofResponse 549 | 550 | } 551 | await updateSession(this.sessionId, SessionStatus.SESSION_STARTED) 552 | if (this.options?.useAppClip) { 553 | let template = encodeURIComponent(JSON.stringify(templateData)); 554 | template = replaceAll(template, '(', '%28'); 555 | template = replaceAll(template, ')', '%29'); 556 | 557 | // check if the app is running on iOS or Android 558 | const isIos = getMobileDeviceType() === DeviceType.IOS; 559 | if (!isIos) { 560 | const instantAppUrl = `https://share.reclaimprotocol.org/verify/?template=${template}`; 561 | logger.info('Instant App Url created successfully: ' + instantAppUrl); 562 | return instantAppUrl; 563 | } else { 564 | const appClipUrl = `https://appclip.apple.com/id?p=org.reclaimprotocol.app.clip&template=${template}`; 565 | logger.info('App Clip Url created successfully: ' + appClipUrl); 566 | return appClipUrl; 567 | } 568 | } else { 569 | const link = await createLinkWithTemplateData(templateData) 570 | logger.info('Request Url created successfully: ' + link); 571 | return link; 572 | } 573 | } catch (error) { 574 | logger.info('Error creating Request Url:', error) 575 | throw error 576 | } 577 | } 578 | 579 | async triggerReclaimFlow(): Promise { 580 | if (!this.signature) { 581 | throw new SignatureNotFoundError('Signature is not set.') 582 | } 583 | 584 | try { 585 | validateSignature(this.providerId, this.signature, this.applicationId, this.timeStamp) 586 | const templateData: TemplateData = { 587 | sessionId: this.sessionId, 588 | providerId: this.providerId, 589 | applicationId: this.applicationId, 590 | signature: this.signature, 591 | timestamp: this.timeStamp, 592 | callbackUrl: this.getAppCallbackUrl(), 593 | context: JSON.stringify(this.context), 594 | providerVersion: this.options?.providerVersion ?? '', 595 | resolvedProviderVersion: this.resolvedProviderVersion ?? '', 596 | parameters: this.parameters, 597 | redirectUrl: this.redirectUrl ?? '', 598 | acceptAiProviders: this.options?.acceptAiProviders ?? false, 599 | sdkVersion: this.sdkVersion, 600 | jsonProofResponse: this.jsonProofResponse 601 | } 602 | 603 | this.templateData = templateData; 604 | 605 | logger.info('Triggering Reclaim flow'); 606 | 607 | // Get device type 608 | const deviceType = getDeviceType(); 609 | await updateSession(this.sessionId, SessionStatus.SESSION_STARTED) 610 | 611 | if (deviceType === DeviceType.DESKTOP) { 612 | const extensionAvailable = await this.isBrowserExtensionAvailable(); 613 | // Desktop flow 614 | if (this.options?.useBrowserExtension && extensionAvailable) { 615 | logger.info('Triggering browser extension flow'); 616 | this.triggerBrowserExtensionFlow(); 617 | return; 618 | } else { 619 | // Show QR code popup modal 620 | logger.info('Browser extension not available, showing QR code modal'); 621 | await this.showQRCodeModal(); 622 | } 623 | } else if (deviceType === DeviceType.MOBILE) { 624 | // Mobile flow 625 | const mobileDeviceType = getMobileDeviceType(); 626 | 627 | if (mobileDeviceType === DeviceType.ANDROID) { 628 | // Redirect to instant app URL 629 | logger.info('Redirecting to Android instant app'); 630 | await this.redirectToInstantApp(); 631 | } else if (mobileDeviceType === DeviceType.IOS) { 632 | // Redirect to app clip URL 633 | logger.info('Redirecting to iOS app clip'); 634 | await this.redirectToAppClip(); 635 | } 636 | } 637 | } catch (error) { 638 | logger.info('Error triggering Reclaim flow:', error); 639 | throw error; 640 | } 641 | } 642 | 643 | 644 | async isBrowserExtensionAvailable(timeout = 200): Promise { 645 | try { 646 | return new Promise((resolve) => { 647 | const messageId = `reclaim-check-${Date.now()}`; 648 | 649 | const timeoutId = setTimeout(() => { 650 | window.removeEventListener('message', messageListener); 651 | resolve(false); 652 | }, timeout); 653 | 654 | const messageListener = (event: MessageEvent) => { 655 | if (event.data?.action === RECLAIM_EXTENSION_ACTIONS.EXTENSION_RESPONSE && 656 | event.data?.messageId === messageId) { 657 | clearTimeout(timeoutId); 658 | window.removeEventListener('message', messageListener); 659 | resolve(!!event.data.installed); 660 | } 661 | }; 662 | 663 | window.addEventListener('message', messageListener); 664 | const message: ExtensionMessage = { 665 | action: RECLAIM_EXTENSION_ACTIONS.CHECK_EXTENSION, 666 | extensionID: this.extensionID, 667 | messageId: messageId 668 | } 669 | window.postMessage(message, '*'); 670 | }); 671 | } catch (error) { 672 | logger.info('Error checking Reclaim extension installed:', error); 673 | return false; 674 | } 675 | } 676 | 677 | private triggerBrowserExtensionFlow(): void { 678 | const message: ExtensionMessage = { 679 | action: RECLAIM_EXTENSION_ACTIONS.START_VERIFICATION, 680 | messageId: this.sessionId, 681 | data: this.templateData, 682 | extensionID: this.extensionID 683 | } 684 | window.postMessage(message, '*'); 685 | logger.info('Browser extension flow triggered'); 686 | } 687 | 688 | private async showQRCodeModal(): Promise { 689 | try { 690 | const requestUrl = await createLinkWithTemplateData(this.templateData); 691 | const modal = new QRCodeModal(this.modalOptions); 692 | await modal.show(requestUrl); 693 | } catch (error) { 694 | logger.info('Error showing QR code modal:', error); 695 | throw error; 696 | } 697 | } 698 | 699 | private async redirectToInstantApp(): Promise { 700 | try { 701 | let template = encodeURIComponent(JSON.stringify(this.templateData)); 702 | template = replaceAll(template, '(', '%28'); 703 | template = replaceAll(template, ')', '%29'); 704 | 705 | const instantAppUrl = `https://share.reclaimprotocol.org/verify/?template=${template}`; 706 | logger.info('Redirecting to Android instant app: ' + instantAppUrl); 707 | 708 | // Redirect to instant app 709 | window.location.href = instantAppUrl; 710 | } catch (error) { 711 | logger.info('Error redirecting to instant app:', error); 712 | throw error; 713 | } 714 | } 715 | 716 | private async redirectToAppClip(): Promise { 717 | try { 718 | let template = encodeURIComponent(JSON.stringify(this.templateData)); 719 | template = replaceAll(template, '(', '%28'); 720 | template = replaceAll(template, ')', '%29'); 721 | 722 | const appClipUrl = `https://appclip.apple.com/id?p=org.reclaimprotocol.app.clip&template=${template}`; 723 | logger.info('Redirecting to iOS app clip: ' + appClipUrl); 724 | 725 | // Redirect to app clip 726 | window.location.href = appClipUrl; 727 | } catch (error) { 728 | logger.info('Error redirecting to app clip:', error); 729 | throw error; 730 | } 731 | } 732 | 733 | async startSession({ onSuccess, onError }: StartSessionParams): Promise { 734 | if (!this.sessionId) { 735 | const message = "Session can't be started due to undefined value of sessionId"; 736 | logger.info(message); 737 | throw new SessionNotStartedError(message); 738 | } 739 | 740 | logger.info('Starting session'); 741 | const interval = setInterval(async () => { 742 | try { 743 | const statusUrlResponse = await fetchStatusUrl(this.sessionId); 744 | 745 | if (!statusUrlResponse.session) return; 746 | 747 | // Reset failure time if status is not PROOF_GENERATION_FAILED 748 | if (statusUrlResponse.session.statusV2 !== SessionStatus.PROOF_GENERATION_FAILED) { 749 | this.lastFailureTime = undefined; 750 | } 751 | 752 | // Check for failure timeout 753 | if (statusUrlResponse.session.statusV2 === SessionStatus.PROOF_GENERATION_FAILED) { 754 | const currentTime = Date.now(); 755 | if (!this.lastFailureTime) { 756 | this.lastFailureTime = currentTime; 757 | } else if (currentTime - this.lastFailureTime >= this.FAILURE_TIMEOUT) { 758 | throw new ProviderFailedError('Proof generation failed - timeout reached'); 759 | } 760 | return; // Continue monitoring if under timeout 761 | } 762 | 763 | const isDefaultCallbackUrl = this.getAppCallbackUrl() === `${constants.DEFAULT_RECLAIM_CALLBACK_URL}${this.sessionId}`; 764 | 765 | if (isDefaultCallbackUrl) { 766 | if (statusUrlResponse.session.proofs && statusUrlResponse.session.proofs.length > 0) { 767 | const proofs = statusUrlResponse.session.proofs; 768 | if (this.claimCreationType === ClaimCreationType.STANDALONE) { 769 | const verified = await verifyProof(proofs); 770 | if (!verified) { 771 | logger.info(`Proofs not verified: ${JSON.stringify(proofs)}`); 772 | throw new ProofNotVerifiedError(); 773 | } 774 | } 775 | // check if the proofs array has only one proof then send the proofs in onSuccess 776 | if (proofs.length === 1) { 777 | onSuccess(proofs[0]); 778 | } else { 779 | onSuccess(proofs); 780 | } 781 | this.clearInterval(); 782 | } 783 | } else { 784 | if (statusUrlResponse.session.statusV2 === SessionStatus.PROOF_SUBMISSION_FAILED) { 785 | throw new ProofSubmissionFailedError(); 786 | } 787 | if (statusUrlResponse.session.statusV2 === SessionStatus.PROOF_SUBMITTED) { 788 | if (onSuccess) { 789 | onSuccess('Proof submitted successfully to the custom callback url'); 790 | } 791 | this.clearInterval(); 792 | } 793 | } 794 | } catch (e) { 795 | if (onError) { 796 | onError(e as Error); 797 | } 798 | this.clearInterval(); 799 | } 800 | }, 3000); 801 | 802 | this.intervals.set(this.sessionId, interval); 803 | scheduleIntervalEndingTask(this.sessionId, this.intervals, onError); 804 | } 805 | } 806 | 807 | -------------------------------------------------------------------------------- /src/contract-types/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "0x1a4": { 3 | "chainName": "opt-goerli", 4 | "address": "0xF93F605142Fb1Efad7Aa58253dDffF67775b4520", 5 | "rpcUrl": "https://opt-goerli.g.alchemy.com/v2/rksDkSUXd2dyk2ANy_zzODknx_AAokui" 6 | }, 7 | "0xaa37dc": { 8 | "chainName": "opt-sepolia", 9 | "address": "0x6D0f81BDA11995f25921aAd5B43359630E65Ca96", 10 | "rpcUrl": "https://opt-sepolia.g.alchemy.com/v2/aO1-SfG4oFRLyAiLREqzyAUu0HTCwHgs" 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/contract-types/contracts/factories/Reclaim__factory.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | 4 | import { Contract } from 'ethers'; 5 | 6 | const _abi = [ 7 | { 8 | anonymous: false, 9 | inputs: [ 10 | { 11 | indexed: false, 12 | internalType: 'address', 13 | name: 'previousAdmin', 14 | type: 'address', 15 | }, 16 | { 17 | indexed: false, 18 | internalType: 'address', 19 | name: 'newAdmin', 20 | type: 'address', 21 | }, 22 | ], 23 | name: 'AdminChanged', 24 | type: 'event', 25 | }, 26 | { 27 | anonymous: false, 28 | inputs: [ 29 | { 30 | indexed: true, 31 | internalType: 'address', 32 | name: 'beacon', 33 | type: 'address', 34 | }, 35 | ], 36 | name: 'BeaconUpgraded', 37 | type: 'event', 38 | }, 39 | { 40 | anonymous: false, 41 | inputs: [ 42 | { 43 | components: [ 44 | { 45 | internalType: 'uint32', 46 | name: 'id', 47 | type: 'uint32', 48 | }, 49 | { 50 | internalType: 'uint32', 51 | name: 'timestampStart', 52 | type: 'uint32', 53 | }, 54 | { 55 | internalType: 'uint32', 56 | name: 'timestampEnd', 57 | type: 'uint32', 58 | }, 59 | { 60 | components: [ 61 | { 62 | internalType: 'address', 63 | name: 'addr', 64 | type: 'address', 65 | }, 66 | { 67 | internalType: 'string', 68 | name: 'host', 69 | type: 'string', 70 | }, 71 | ], 72 | internalType: 'struct Reclaim.Witness[]', 73 | name: 'witnesses', 74 | type: 'tuple[]', 75 | }, 76 | { 77 | internalType: 'uint8', 78 | name: 'minimumWitnessesForClaimCreation', 79 | type: 'uint8', 80 | }, 81 | ], 82 | indexed: false, 83 | internalType: 'struct Reclaim.Epoch', 84 | name: 'epoch', 85 | type: 'tuple', 86 | }, 87 | ], 88 | name: 'EpochAdded', 89 | type: 'event', 90 | }, 91 | { 92 | anonymous: false, 93 | inputs: [ 94 | { 95 | indexed: false, 96 | internalType: 'uint8', 97 | name: 'version', 98 | type: 'uint8', 99 | }, 100 | ], 101 | name: 'Initialized', 102 | type: 'event', 103 | }, 104 | { 105 | anonymous: false, 106 | inputs: [ 107 | { 108 | indexed: true, 109 | internalType: 'address', 110 | name: 'previousOwner', 111 | type: 'address', 112 | }, 113 | { 114 | indexed: true, 115 | internalType: 'address', 116 | name: 'newOwner', 117 | type: 'address', 118 | }, 119 | ], 120 | name: 'OwnershipTransferred', 121 | type: 'event', 122 | }, 123 | { 124 | anonymous: false, 125 | inputs: [ 126 | { 127 | indexed: true, 128 | internalType: 'address', 129 | name: 'implementation', 130 | type: 'address', 131 | }, 132 | ], 133 | name: 'Upgraded', 134 | type: 'event', 135 | }, 136 | { 137 | inputs: [ 138 | { 139 | internalType: 'address', 140 | name: 'witnessAddress', 141 | type: 'address', 142 | }, 143 | { 144 | internalType: 'string', 145 | name: 'host', 146 | type: 'string', 147 | }, 148 | ], 149 | name: 'addAsWitness', 150 | outputs: [], 151 | stateMutability: 'nonpayable', 152 | type: 'function', 153 | }, 154 | { 155 | inputs: [], 156 | name: 'addNewEpoch', 157 | outputs: [], 158 | stateMutability: 'nonpayable', 159 | type: 'function', 160 | }, 161 | { 162 | inputs: [ 163 | { 164 | internalType: 'uint32', 165 | name: 'epochNum', 166 | type: 'uint32', 167 | }, 168 | { 169 | components: [ 170 | { 171 | internalType: 'string', 172 | name: 'provider', 173 | type: 'string', 174 | }, 175 | { 176 | internalType: 'string', 177 | name: 'parameters', 178 | type: 'string', 179 | }, 180 | { 181 | internalType: 'string', 182 | name: 'context', 183 | type: 'string', 184 | }, 185 | ], 186 | internalType: 'struct Claims.ClaimInfo', 187 | name: 'claimInfo', 188 | type: 'tuple', 189 | }, 190 | { 191 | components: [ 192 | { 193 | internalType: 'bytes32', 194 | name: 'identifier', 195 | type: 'bytes32', 196 | }, 197 | { 198 | internalType: 'address', 199 | name: 'owner', 200 | type: 'address', 201 | }, 202 | { 203 | internalType: 'uint32', 204 | name: 'timestampS', 205 | type: 'uint32', 206 | }, 207 | { 208 | internalType: 'uint256', 209 | name: 'epoch', 210 | type: 'uint256', 211 | }, 212 | ], 213 | internalType: 'struct Claims.CompleteClaimData', 214 | name: 'claimData', 215 | type: 'tuple', 216 | }, 217 | { 218 | internalType: 'bytes[]', 219 | name: 'signatures', 220 | type: 'bytes[]', 221 | }, 222 | ], 223 | name: 'assertValidEpochAndSignedClaim', 224 | outputs: [], 225 | stateMutability: 'view', 226 | type: 'function', 227 | }, 228 | { 229 | inputs: [], 230 | name: 'currentEpoch', 231 | outputs: [ 232 | { 233 | internalType: 'uint32', 234 | name: '', 235 | type: 'uint32', 236 | }, 237 | ], 238 | stateMutability: 'view', 239 | type: 'function', 240 | }, 241 | { 242 | inputs: [], 243 | name: 'epochDurationS', 244 | outputs: [ 245 | { 246 | internalType: 'uint32', 247 | name: '', 248 | type: 'uint32', 249 | }, 250 | ], 251 | stateMutability: 'view', 252 | type: 'function', 253 | }, 254 | { 255 | inputs: [ 256 | { 257 | internalType: 'uint256', 258 | name: '', 259 | type: 'uint256', 260 | }, 261 | ], 262 | name: 'epochs', 263 | outputs: [ 264 | { 265 | internalType: 'uint32', 266 | name: 'id', 267 | type: 'uint32', 268 | }, 269 | { 270 | internalType: 'uint32', 271 | name: 'timestampStart', 272 | type: 'uint32', 273 | }, 274 | { 275 | internalType: 'uint32', 276 | name: 'timestampEnd', 277 | type: 'uint32', 278 | }, 279 | { 280 | internalType: 'uint8', 281 | name: 'minimumWitnessesForClaimCreation', 282 | type: 'uint8', 283 | }, 284 | ], 285 | stateMutability: 'view', 286 | type: 'function', 287 | }, 288 | { 289 | inputs: [ 290 | { 291 | internalType: 'uint32', 292 | name: 'epoch', 293 | type: 'uint32', 294 | }, 295 | ], 296 | name: 'fetchEpoch', 297 | outputs: [ 298 | { 299 | components: [ 300 | { 301 | internalType: 'uint32', 302 | name: 'id', 303 | type: 'uint32', 304 | }, 305 | { 306 | internalType: 'uint32', 307 | name: 'timestampStart', 308 | type: 'uint32', 309 | }, 310 | { 311 | internalType: 'uint32', 312 | name: 'timestampEnd', 313 | type: 'uint32', 314 | }, 315 | { 316 | components: [ 317 | { 318 | internalType: 'address', 319 | name: 'addr', 320 | type: 'address', 321 | }, 322 | { 323 | internalType: 'string', 324 | name: 'host', 325 | type: 'string', 326 | }, 327 | ], 328 | internalType: 'struct Reclaim.Witness[]', 329 | name: 'witnesses', 330 | type: 'tuple[]', 331 | }, 332 | { 333 | internalType: 'uint8', 334 | name: 'minimumWitnessesForClaimCreation', 335 | type: 'uint8', 336 | }, 337 | ], 338 | internalType: 'struct Reclaim.Epoch', 339 | name: '', 340 | type: 'tuple', 341 | }, 342 | ], 343 | stateMutability: 'view', 344 | type: 'function', 345 | }, 346 | { 347 | inputs: [ 348 | { 349 | internalType: 'uint32', 350 | name: 'epoch', 351 | type: 'uint32', 352 | }, 353 | { 354 | internalType: 'bytes32', 355 | name: 'identifier', 356 | type: 'bytes32', 357 | }, 358 | { 359 | internalType: 'uint32', 360 | name: 'timestampS', 361 | type: 'uint32', 362 | }, 363 | ], 364 | name: 'fetchWitnessesForClaim', 365 | outputs: [ 366 | { 367 | components: [ 368 | { 369 | internalType: 'address', 370 | name: 'addr', 371 | type: 'address', 372 | }, 373 | { 374 | internalType: 'string', 375 | name: 'host', 376 | type: 'string', 377 | }, 378 | ], 379 | internalType: 'struct Reclaim.Witness[]', 380 | name: '', 381 | type: 'tuple[]', 382 | }, 383 | ], 384 | stateMutability: 'view', 385 | type: 'function', 386 | }, 387 | { 388 | inputs: [], 389 | name: 'initialize', 390 | outputs: [], 391 | stateMutability: 'nonpayable', 392 | type: 'function', 393 | }, 394 | { 395 | inputs: [], 396 | name: 'minimumWitnessesForClaimCreation', 397 | outputs: [ 398 | { 399 | internalType: 'uint8', 400 | name: '', 401 | type: 'uint8', 402 | }, 403 | ], 404 | stateMutability: 'view', 405 | type: 'function', 406 | }, 407 | { 408 | inputs: [], 409 | name: 'owner', 410 | outputs: [ 411 | { 412 | internalType: 'address', 413 | name: '', 414 | type: 'address', 415 | }, 416 | ], 417 | stateMutability: 'view', 418 | type: 'function', 419 | }, 420 | { 421 | inputs: [], 422 | name: 'proxiableUUID', 423 | outputs: [ 424 | { 425 | internalType: 'bytes32', 426 | name: '', 427 | type: 'bytes32', 428 | }, 429 | ], 430 | stateMutability: 'view', 431 | type: 'function', 432 | }, 433 | { 434 | inputs: [ 435 | { 436 | internalType: 'address', 437 | name: 'witnessAddress', 438 | type: 'address', 439 | }, 440 | ], 441 | name: 'removeAsWitness', 442 | outputs: [], 443 | stateMutability: 'nonpayable', 444 | type: 'function', 445 | }, 446 | { 447 | inputs: [], 448 | name: 'renounceOwnership', 449 | outputs: [], 450 | stateMutability: 'nonpayable', 451 | type: 'function', 452 | }, 453 | { 454 | inputs: [ 455 | { 456 | internalType: 'address', 457 | name: 'newOwner', 458 | type: 'address', 459 | }, 460 | ], 461 | name: 'transferOwnership', 462 | outputs: [], 463 | stateMutability: 'nonpayable', 464 | type: 'function', 465 | }, 466 | { 467 | inputs: [ 468 | { 469 | internalType: 'address', 470 | name: 'addr', 471 | type: 'address', 472 | }, 473 | { 474 | internalType: 'bool', 475 | name: 'isWhitelisted', 476 | type: 'bool', 477 | }, 478 | ], 479 | name: 'updateWitnessWhitelist', 480 | outputs: [], 481 | stateMutability: 'nonpayable', 482 | type: 'function', 483 | }, 484 | { 485 | inputs: [ 486 | { 487 | internalType: 'address', 488 | name: 'newImplementation', 489 | type: 'address', 490 | }, 491 | ], 492 | name: 'upgradeTo', 493 | outputs: [], 494 | stateMutability: 'nonpayable', 495 | type: 'function', 496 | }, 497 | { 498 | inputs: [ 499 | { 500 | internalType: 'address', 501 | name: 'newImplementation', 502 | type: 'address', 503 | }, 504 | { 505 | internalType: 'bytes', 506 | name: 'data', 507 | type: 'bytes', 508 | }, 509 | ], 510 | name: 'upgradeToAndCall', 511 | outputs: [], 512 | stateMutability: 'payable', 513 | type: 'function', 514 | }, 515 | { 516 | inputs: [ 517 | { 518 | internalType: 'uint256', 519 | name: '', 520 | type: 'uint256', 521 | }, 522 | ], 523 | name: 'witnesses', 524 | outputs: [ 525 | { 526 | internalType: 'address', 527 | name: 'addr', 528 | type: 'address', 529 | }, 530 | { 531 | internalType: 'string', 532 | name: 'host', 533 | type: 'string', 534 | }, 535 | ], 536 | stateMutability: 'view', 537 | type: 'function', 538 | }, 539 | ] as const; 540 | 541 | export class Reclaim__factory { 542 | static readonly abi = _abi; 543 | 544 | static connect(address: string, signerOrProvider: any): Contract { 545 | return new Contract(address, _abi, signerOrProvider); 546 | } 547 | } 548 | -------------------------------------------------------------------------------- /src/contract-types/contracts/factories/index.ts: -------------------------------------------------------------------------------- 1 | export { Reclaim__factory } from './Reclaim__factory'; 2 | -------------------------------------------------------------------------------- /src/contract-types/contracts/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | export { Reclaim__factory } from './factories/Reclaim__factory'; 4 | -------------------------------------------------------------------------------- /src/contract-types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './contracts'; 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Reclaim'; 2 | export * from './utils/interfaces'; 3 | export { ClaimCreationType, ModalOptions } from './utils/types'; -------------------------------------------------------------------------------- /src/smart-contract.ts: -------------------------------------------------------------------------------- 1 | import type { Beacon, BeaconState } from './utils/interfaces'; 2 | import { Reclaim__factory as ReclaimFactory } from './contract-types'; 3 | import CONTRACTS_CONFIG from './contract-types/config.json'; 4 | import { Contract, ethers } from 'ethers'; 5 | 6 | const DEFAULT_CHAIN_ID = 11155420; 7 | 8 | export function makeBeacon(chainId?: number): Beacon | undefined { 9 | chainId = chainId || DEFAULT_CHAIN_ID; 10 | const contract = getContract(chainId); 11 | if (contract) { 12 | return makeBeaconCacheable({ 13 | async getState(epochId: number | undefined): Promise { 14 | //@ts-ignore 15 | const epoch = await contract.fetchEpoch(epochId || 0); 16 | if (!epoch.id) { 17 | throw new Error(`Invalid epoch ID: ${epochId}`); 18 | } 19 | 20 | return { 21 | epoch: epoch.id, 22 | witnesses: epoch.witnesses.map((w: any) => ({ 23 | id: w.addr.toLowerCase(), 24 | url: w.host, 25 | })), 26 | witnessesRequiredForClaim: epoch.minimumWitnessesForClaimCreation, 27 | nextEpochTimestampS: epoch.timestampEnd, 28 | }; 29 | }, 30 | }); 31 | } else { 32 | return undefined; 33 | } 34 | } 35 | 36 | export function makeBeaconCacheable(beacon: Beacon): Beacon { 37 | const cache: { [epochId: number]: Promise } = {}; 38 | 39 | return { 40 | ...beacon, 41 | async getState(epochId: number | undefined): Promise { 42 | if (!epochId) { 43 | // TODO: add cache here 44 | const state = await beacon.getState(); 45 | return state; 46 | } 47 | 48 | const key = epochId; 49 | 50 | if (!cache[key]) { 51 | cache[key] = beacon.getState(epochId); 52 | } 53 | 54 | return cache[key] as unknown as BeaconState; 55 | }, 56 | }; 57 | } 58 | 59 | const existingContractsMap: { [chain: string]: Contract } = {}; 60 | 61 | function getContract(chainId: number): Contract { 62 | const chainKey = `0x${chainId.toString(16)}`; 63 | if (!existingContractsMap[chainKey]) { 64 | const contractData = 65 | CONTRACTS_CONFIG[chainKey as keyof typeof CONTRACTS_CONFIG]; 66 | if (!contractData) { 67 | throw new Error(`Unsupported chain: "${chainKey}"`); 68 | } 69 | 70 | const rpcProvider = new ethers.JsonRpcProvider(contractData.rpcUrl); 71 | existingContractsMap[chainKey] = ReclaimFactory.connect( 72 | contractData.address, 73 | rpcProvider 74 | ); 75 | } 76 | 77 | return existingContractsMap[chainKey] as Contract; 78 | } 79 | -------------------------------------------------------------------------------- /src/utils/__tests__/helper.test.ts: -------------------------------------------------------------------------------- 1 | import { escapeRegExp, replaceAll } from '../helper'; 2 | 3 | describe('escapeRegExp', () => { 4 | it('should escape special characters in a string', () => { 5 | const input = '.*+?^${}()|[]\\'; 6 | const expectedOutput = '\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\'; 7 | expect(escapeRegExp(input)).toBe(expectedOutput); 8 | }); 9 | 10 | it('should return the same string if there are no special characters', () => { 11 | const input = 'abc123'; 12 | const expectedOutput = 'abc123'; 13 | expect(escapeRegExp(input)).toBe(expectedOutput); 14 | }); 15 | 16 | it('should return an empty string if the input is empty', () => { 17 | const input = ''; 18 | const expectedOutput = ''; 19 | expect(escapeRegExp(input)).toBe(expectedOutput); 20 | }); 21 | }); 22 | 23 | describe('replaceAll', () => { 24 | it('should replace all occurrences of a substring in a string', () => { 25 | const str = 'hello world, hello universe'; 26 | const find = 'hello'; 27 | const replace = 'hi'; 28 | const expectedOutput = 'hi world, hi universe'; 29 | expect(replaceAll(str, find, replace)).toBe(expectedOutput); 30 | }); 31 | 32 | it('should return the same string if the substring to find is not present', () => { 33 | const str = 'hello world'; 34 | const find = 'bye'; 35 | const replace = 'hi'; 36 | const expectedOutput = 'hello world'; 37 | expect(replaceAll(str, find, replace)).toBe(expectedOutput); 38 | }); 39 | 40 | it('should return the same string if the substring to find is empty', () => { 41 | const str = 'hello world'; 42 | const find = ''; 43 | const replace = 'hi'; 44 | const expectedOutput = 'hello world'; 45 | expect(replaceAll(str, find, replace)).toBe(expectedOutput); 46 | }); 47 | 48 | it('should replace all occurrences of a substring containing special regex characters', () => { 49 | const str = 'hello.world.hello.world'; 50 | const find = '.'; 51 | const replace = '!'; 52 | const expectedOutput = 'hello!world!hello!world'; 53 | expect(replaceAll(str, find, replace)).toBe(expectedOutput); 54 | }); 55 | 56 | it('should return an empty string if the input string is empty', () => { 57 | const str = ''; 58 | const find = 'hello'; 59 | const replace = 'hi'; 60 | const expectedOutput = ''; 61 | expect(replaceAll(str, find, replace)).toBe(expectedOutput); 62 | }); 63 | 64 | it('should handle overlapping substrings correctly', () => { 65 | const str = 'aaa'; 66 | const find = 'aa'; 67 | const replace = 'b'; 68 | const expectedOutput = 'ba'; 69 | expect(replaceAll(str, find, replace)).toBe(expectedOutput); 70 | }); 71 | 72 | it('should replace the substring with an empty string', () => { 73 | const str = 'hello world'; 74 | const find = 'world'; 75 | const replace = ''; 76 | const expectedOutput = 'hello '; 77 | expect(replaceAll(str, find, replace)).toBe(expectedOutput); 78 | }); 79 | }); -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | // Base URL for the backend API 2 | export let BACKEND_BASE_URL = "https://api.reclaimprotocol.org"; 3 | 4 | export function setBackendBaseUrl(url: string) { 5 | BACKEND_BASE_URL = url; 6 | } 7 | 8 | // Constant values used throughout the application 9 | export const constants = { 10 | // Default callback URL for Reclaim protocol 11 | get DEFAULT_RECLAIM_CALLBACK_URL() { 12 | return `${BACKEND_BASE_URL}/api/sdk/callback?callbackId=`; 13 | }, 14 | 15 | // Default status URL for Reclaim sessions 16 | get DEFAULT_RECLAIM_STATUS_URL() { 17 | return `${BACKEND_BASE_URL}/api/sdk/session/`; 18 | }, 19 | 20 | // URL for sharing Reclaim templates 21 | RECLAIM_SHARE_URL: 'https://share.reclaimprotocol.org/verifier/?template=', 22 | 23 | // Chrome extension URL for Reclaim Protocol 24 | CHROME_EXTENSION_URL: 'https://chromewebstore.google.com/', 25 | 26 | // QR Code API base URL 27 | QR_CODE_API_URL: 'https://api.qrserver.com/v1/create-qr-code/' 28 | }; 29 | -------------------------------------------------------------------------------- /src/utils/device.ts: -------------------------------------------------------------------------------- 1 | import { DeviceType } from "./types"; 2 | 3 | const navigatorDefined = typeof navigator !== 'undefined'; 4 | const windowDefined = typeof window !== 'undefined'; 5 | 6 | const userAgent = navigatorDefined ? navigator.userAgent.toLowerCase() : ''; 7 | const userAgentData = navigatorDefined ? (navigator as Navigator & { 8 | userAgentData?: { 9 | platform: string; 10 | brands?: { brand: string; version: string }[]; 11 | } 12 | }).userAgentData : undefined; 13 | 14 | /** 15 | * Highly accurate device type detection - returns only 'desktop' or 'mobile' 16 | * Uses multiple detection methods and scoring system for maximum accuracy 17 | * @returns {DeviceType.DESKTOP | DeviceType.MOBILE} The detected device type 18 | */ 19 | export function getDeviceType(): DeviceType.DESKTOP | DeviceType.MOBILE { 20 | // Early return for server-side rendering - assume desktop 21 | if (!navigatorDefined || !windowDefined) { 22 | return DeviceType.DESKTOP; 23 | } 24 | 25 | let mobileScore = 0; 26 | const CONFIDENCE_THRESHOLD = 3; // Need at least 3 indicators for mobile 27 | 28 | // Method 1: Touch capability detection (weight: 2) 29 | const isTouchDevice = 'ontouchstart' in window || 30 | (navigatorDefined && navigator.maxTouchPoints > 0); 31 | if (isTouchDevice) { 32 | mobileScore += 2; 33 | } 34 | 35 | // Method 2: Screen size analysis (weight: 2) 36 | const screenWidth = window.innerWidth || window.screen?.width || 0; 37 | const screenHeight = window.innerHeight || window.screen?.height || 0; 38 | const hasSmallScreen = screenWidth <= 768 || screenHeight <= 768; 39 | if (hasSmallScreen) { 40 | mobileScore += 2; 41 | } 42 | 43 | // Method 3: User agent detection (weight: 3) 44 | const mobileUserAgentPattern = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile/i; 45 | if (mobileUserAgentPattern.test(userAgent)) { 46 | mobileScore += 3; 47 | } 48 | 49 | // Method 4: Mobile-specific APIs (weight: 2) 50 | const hasMobileAPIs = 'orientation' in window || 51 | 'DeviceMotionEvent' in window || 52 | 'DeviceOrientationEvent' in window; 53 | if (hasMobileAPIs) { 54 | mobileScore += 2; 55 | } 56 | 57 | // Method 5: Device pixel ratio (weight: 1) 58 | const hasHighDPI = windowDefined && window.devicePixelRatio > 1.5; 59 | if (hasHighDPI && hasSmallScreen) { 60 | mobileScore += 1; 61 | } 62 | 63 | // Method 6: Viewport meta tag presence (weight: 1) 64 | const hasViewportMeta = document.querySelector('meta[name="viewport"]') !== null; 65 | if (hasViewportMeta && hasSmallScreen) { 66 | mobileScore += 1; 67 | } 68 | 69 | // Method 7: Check for desktop-specific indicators (negative weight) 70 | const hasLargeScreen = screenWidth > 1024 && screenHeight > 768; 71 | const hasKeyboard = 'keyboard' in navigator; 72 | const hasPointer = window.matchMedia && window.matchMedia('(pointer: fine)').matches; 73 | 74 | if (hasLargeScreen && !isTouchDevice) { 75 | mobileScore -= 2; 76 | } 77 | if (hasPointer && !isTouchDevice) { 78 | mobileScore -= 1; 79 | } 80 | 81 | // Method 8: Special case for iPad Pro and similar devices 82 | const isPadWithKeyboard = userAgent.includes('macintosh') && isTouchDevice; 83 | if (isPadWithKeyboard) { 84 | mobileScore += 2; 85 | } 86 | 87 | return mobileScore >= CONFIDENCE_THRESHOLD ? DeviceType.MOBILE : DeviceType.DESKTOP; 88 | } 89 | 90 | /** 91 | * Highly accurate mobile device type detection - returns only 'android' or 'ios' 92 | * Should only be called when getDeviceType() returns 'mobile' 93 | * @returns {DeviceType.ANDROID | DeviceType.IOS} The detected mobile device type 94 | */ 95 | export function getMobileDeviceType(): DeviceType.ANDROID | DeviceType.IOS { 96 | // Early return for server-side rendering - default to Android 97 | if (!navigatorDefined || !windowDefined) { 98 | return DeviceType.ANDROID; 99 | } 100 | 101 | const ua = navigator.userAgent; 102 | 103 | // Strategy 1: Direct iOS detection using comprehensive regex 104 | const iosPattern = /iPad|iPhone|iPod/i; 105 | if (iosPattern.test(ua)) { 106 | return DeviceType.IOS; 107 | } 108 | 109 | // Strategy 2: Direct Android detection 110 | const androidPattern = /Android/i; 111 | if (androidPattern.test(ua)) { 112 | return DeviceType.ANDROID; 113 | } 114 | 115 | // Strategy 3: iPad Pro masquerading as Mac detection 116 | const isMacWithTouch = /Macintosh|MacIntel/i.test(ua) && 'ontouchstart' in window; 117 | if (isMacWithTouch) { 118 | return DeviceType.IOS; 119 | } 120 | 121 | // Strategy 4: Modern iPad detection using userAgentData 122 | if (userAgentData?.platform === 'macOS' && 'ontouchstart' in window) { 123 | return DeviceType.IOS; 124 | } 125 | 126 | // Strategy 5: iOS-specific API detection 127 | if (typeof (window as any).DeviceMotionEvent?.requestPermission === 'function') { 128 | return DeviceType.IOS; 129 | } 130 | 131 | // Strategy 6: CSS property detection for iOS 132 | if (typeof CSS !== 'undefined' && CSS.supports?.('-webkit-touch-callout', 'none')) { 133 | return DeviceType.IOS; 134 | } 135 | 136 | // Strategy 7: WebKit without Chrome/Android indicates iOS Safari 137 | const isIOSWebKit = /WebKit/i.test(ua) && !/Chrome|CriOS|Android/i.test(ua); 138 | if (isIOSWebKit) { 139 | return DeviceType.IOS; 140 | } 141 | 142 | // Strategy 8: Chrome detection for Android (when Android not explicitly found) 143 | const isChromeOnMobile = (window as any).chrome && /Mobile/i.test(ua); 144 | if (isChromeOnMobile && !iosPattern.test(ua)) { 145 | return DeviceType.ANDROID; 146 | } 147 | 148 | // Strategy 9: Check for mobile-specific patterns that indicate Android 149 | const androidMobilePattern = /Mobile.*Android|Android.*Mobile/i; 150 | if (androidMobilePattern.test(ua)) { 151 | return DeviceType.ANDROID; 152 | } 153 | 154 | // Strategy 10: Fallback using existing regex patterns 155 | const mobilePattern = /webos|blackberry|iemobile|opera mini/i; 156 | if (mobilePattern.test(ua)) { 157 | // These are typically Android-based or Android-like 158 | return DeviceType.ANDROID; 159 | } 160 | 161 | // Default fallback - Android is more common globally 162 | return DeviceType.ANDROID; 163 | } 164 | 165 | /** 166 | * Convenience method to check if current device is mobile 167 | * @returns {boolean} True if device is mobile 168 | */ 169 | export function isMobileDevice(): boolean { 170 | return getDeviceType() === DeviceType.MOBILE; 171 | } 172 | 173 | /** 174 | * Convenience method to check if current device is desktop 175 | * @returns {boolean} True if device is desktop 176 | */ 177 | export function isDesktopDevice(): boolean { 178 | return getDeviceType() === DeviceType.DESKTOP; 179 | } -------------------------------------------------------------------------------- /src/utils/errors.ts: -------------------------------------------------------------------------------- 1 | function createErrorClass(name: string) { 2 | return class extends Error { 3 | constructor(message?: string, public innerError?: Error) { 4 | // Include inner error message in the main message if available 5 | const fullMessage = innerError 6 | ? `${message || ''} caused by ${innerError.name}: ${innerError.message}` 7 | : message; 8 | 9 | super(fullMessage); 10 | this.name = name; 11 | if (innerError) { 12 | this.stack += `\nCaused by: ${innerError.stack}`; 13 | } 14 | } 15 | }; 16 | } 17 | 18 | export const TimeoutError = createErrorClass('TimeoutError'); 19 | export const ProofNotVerifiedError = createErrorClass('ProofNotVerifiedError'); 20 | export const SessionNotStartedError = createErrorClass('SessionNotStartedError'); 21 | export const ProviderNotFoundError = createErrorClass('ProviderNotFoundError'); 22 | export const SignatureGeneratingError = createErrorClass('SignatureGeneratingError'); 23 | export const SignatureNotFoundError = createErrorClass('SignatureNotFoundError'); 24 | export const InvalidSignatureError = createErrorClass('InvalidSignatureError'); 25 | export const UpdateSessionError = createErrorClass('UpdateSessionError'); 26 | export const InitSessionError = createErrorClass('InitSessionError'); 27 | export const ProviderFailedError = createErrorClass('ProviderFailedError'); 28 | export const InvalidParamError = createErrorClass('InvalidParamError'); 29 | export const ApplicationError = createErrorClass('ApplicationError'); 30 | export const InitError = createErrorClass('InitError'); 31 | export const BackendServerError = createErrorClass('BackendServerError'); 32 | export const GetStatusUrlError = createErrorClass('GetStatusUrlError'); 33 | export const NoProviderParamsError = createErrorClass('NoProviderParamsError'); 34 | export const SetParamsError = createErrorClass('SetParamsError'); 35 | export const AddContextError = createErrorClass('AddContextError'); 36 | export const SetSignatureError = createErrorClass('SetSignatureError'); 37 | export const GetAppCallbackUrlError = createErrorClass("GetAppCallbackUrlError"); 38 | export const StatusUrlError = createErrorClass('StatusUrlError'); 39 | export const InavlidParametersError = createErrorClass('InavlidParametersError'); 40 | export const ProofSubmissionFailedError = createErrorClass('ProofSubmissionFailedError'); -------------------------------------------------------------------------------- /src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | import { OnError } from './types' 2 | import { TimeoutError } from './errors' 3 | import loggerModule from './logger' 4 | const logger = loggerModule.logger 5 | 6 | /** 7 | * Escapes special characters in a string for use in a regular expression 8 | * @param string - The input string to escape 9 | * @returns The input string with special regex characters escaped 10 | */ 11 | export function escapeRegExp(string: string): string { 12 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string 13 | } 14 | 15 | /** 16 | * Replaces all occurrences of a substring in a string 17 | * @param str - The original string 18 | * @param find - The substring to find 19 | * @param replace - The string to replace the found substrings with 20 | * @returns A new string with all occurrences of 'find' replaced by 'replace' 21 | */ 22 | export function replaceAll(str: string, find: string, replace: string): string { 23 | if (find === '') return str; 24 | return str.replace(new RegExp(escapeRegExp(find), 'g'), replace); 25 | } 26 | 27 | /** 28 | * Schedules a task to end an interval after a specified timeout 29 | * @param sessionId - The ID of the current session 30 | * @param intervals - A Map containing the intervals 31 | * @param onFailureCallback - Callback function to be called on failure 32 | * @param timeout - Timeout in milliseconds (default: 10 minutes) 33 | */ 34 | export function scheduleIntervalEndingTask( 35 | sessionId: string, 36 | intervals: Map, 37 | onFailureCallback: OnError, 38 | timeout: number = 1000 * 60 * 10 39 | ): void { 40 | setTimeout(() => { 41 | if (intervals.has(sessionId)) { 42 | const message = 'Interval ended without receiving proofs' 43 | onFailureCallback(new TimeoutError(message)) 44 | logger.info(message) 45 | clearInterval(intervals.get(sessionId) as NodeJS.Timeout) 46 | intervals.delete(sessionId) 47 | } 48 | }, timeout) 49 | } 50 | -------------------------------------------------------------------------------- /src/utils/interfaces.ts: -------------------------------------------------------------------------------- 1 | // Proof-related interfaces 2 | export interface Proof { 3 | identifier: string; 4 | claimData: ProviderClaimData; 5 | signatures: string[]; 6 | witnesses: WitnessData[]; 7 | extractedParameterValues: any; 8 | publicData?: { [key: string]: string }; 9 | taskId?: number; 10 | } 11 | 12 | // Extension Interactions 13 | export const RECLAIM_EXTENSION_ACTIONS = { 14 | CHECK_EXTENSION: 'RECLAIM_EXTENSION_CHECK', 15 | EXTENSION_RESPONSE: 'RECLAIM_EXTENSION_RESPONSE', 16 | START_VERIFICATION: 'RECLAIM_START_VERIFICATION', 17 | STATUS_UPDATE: 'RECLAIM_STATUS_UPDATE', 18 | }; 19 | 20 | export interface ExtensionMessage { 21 | action: string; 22 | messageId: string; 23 | data?: any; 24 | extensionID?: string; 25 | } 26 | 27 | export interface WitnessData { 28 | id: string; 29 | url: string; 30 | } 31 | 32 | export interface ProviderClaimData { 33 | provider: string; 34 | parameters: string; 35 | owner: string; 36 | timestampS: number; 37 | context: string; 38 | identifier: string; 39 | epoch: number; 40 | } 41 | 42 | // Context and Beacon interfaces 43 | export interface Context { 44 | contextAddress: string; 45 | contextMessage: string; 46 | } 47 | 48 | export interface Beacon { 49 | getState(epoch?: number): Promise; 50 | close?(): Promise; 51 | } 52 | 53 | export type BeaconState = { 54 | witnesses: WitnessData[]; 55 | epoch: number; 56 | witnessesRequiredForClaim: number; 57 | nextEpochTimestampS: number; 58 | }; 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | // Define the possible log levels 2 | export type LogLevel = 'info' | 'warn' | 'error' | 'silent'; 3 | 4 | // Define a simple logger class 5 | class SimpleLogger { 6 | private level: LogLevel = 'info'; 7 | 8 | setLevel(level: LogLevel) { 9 | this.level = level; 10 | } 11 | 12 | private shouldLog(messageLevel: LogLevel): boolean { 13 | const levels: LogLevel[] = ['error', 'warn', 'info', 'silent']; 14 | return levels.indexOf(this.level) >= levels.indexOf(messageLevel); 15 | } 16 | 17 | private log(level: LogLevel, message: string, ...args: any[]) { 18 | if (this.shouldLog(level) && this.level !== 'silent') { 19 | const logFunction = this.getLogFunction(level); 20 | console.log('current level', this.level); 21 | logFunction(`[${level.toUpperCase()}]`, message, ...args); 22 | } 23 | } 24 | 25 | private getLogFunction(level: LogLevel): (message?: any, ...optionalParams: any[]) => void { 26 | switch (level) { 27 | case 'error': 28 | return console.error; 29 | case 'warn': 30 | return console.warn; 31 | case 'info': 32 | return console.info; 33 | default: 34 | return () => {}; // No-op for 'silent' 35 | } 36 | } 37 | 38 | info(message: string, ...args: any[]) { 39 | this.log('info', message, ...args); 40 | } 41 | 42 | warn(message: string, ...args: any[]) { 43 | this.log('warn', message, ...args); 44 | } 45 | 46 | error(message: string, ...args: any[]) { 47 | this.log('error', message, ...args); 48 | } 49 | } 50 | 51 | // Create the logger instance 52 | const logger = new SimpleLogger(); 53 | 54 | // Function to set the log level 55 | export function setLogLevel(level: LogLevel) { 56 | logger.setLevel(level); 57 | } 58 | 59 | // Export the logger instance and the setLogLevel function 60 | export default { 61 | logger, 62 | setLogLevel 63 | }; 64 | -------------------------------------------------------------------------------- /src/utils/modalUtils.ts: -------------------------------------------------------------------------------- 1 | import loggerModule from './logger'; 2 | import { ModalOptions } from './types'; 3 | import { constants } from './constants'; 4 | const logger = loggerModule.logger; 5 | 6 | export class QRCodeModal { 7 | private modalId: string; 8 | private options: ModalOptions; 9 | private autoCloseTimer?: NodeJS.Timeout; 10 | private countdownTimer?: NodeJS.Timeout; 11 | private countdownSeconds: number = 60; 12 | 13 | constructor(options: ModalOptions = {}) { 14 | this.modalId = 'reclaim-qr-modal'; 15 | this.options = { 16 | title: 'Verify with Reclaim', 17 | description: 'Scan the QR code with your mobile device to complete verification', 18 | extensionUrl: constants.CHROME_EXTENSION_URL, 19 | darkTheme: false, 20 | ...options 21 | }; 22 | } 23 | 24 | async show(requestUrl: string): Promise { 25 | try { 26 | // Remove existing modal if present 27 | this.close(); 28 | 29 | // Create modal HTML 30 | const modalHTML = this.createModalHTML(); 31 | 32 | // Add modal to DOM 33 | document.body.insertAdjacentHTML('beforeend', modalHTML); 34 | 35 | // Generate QR code 36 | await this.generateQRCode(requestUrl, 'reclaim-qr-code'); 37 | 38 | // Add event listeners 39 | this.addEventListeners(); 40 | 41 | // Start auto-close timer 42 | this.startAutoCloseTimer(); 43 | 44 | } catch (error) { 45 | logger.info('Error showing QR code modal:', error); 46 | throw error; 47 | } 48 | } 49 | 50 | close(): void { 51 | // Clear timers 52 | if (this.autoCloseTimer) { 53 | clearTimeout(this.autoCloseTimer); 54 | this.autoCloseTimer = undefined; 55 | } 56 | if (this.countdownTimer) { 57 | clearInterval(this.countdownTimer); 58 | this.countdownTimer = undefined; 59 | } 60 | 61 | const modal = document.getElementById(this.modalId); 62 | if (modal) { 63 | modal.remove(); 64 | } 65 | if (this.options.onClose) { 66 | this.options.onClose(); 67 | } 68 | } 69 | 70 | private getThemeStyles() { 71 | const isDark = this.options.darkTheme; 72 | 73 | return { 74 | modalBackground: isDark ? 'rgba(0, 0, 0, 0.8)' : 'rgba(0, 0, 0, 0.5)', 75 | cardBackground: isDark ? '#1f2937' : 'white', 76 | titleColor: isDark ? '#f9fafb' : '#1f2937', 77 | textColor: isDark ? '#d1d5db' : '#6b7280', 78 | qrBackground: isDark ? '#374151' : '#f9fafb', 79 | tipBackground: isDark ? '#1e40af' : '#f0f9ff', 80 | tipBorder: isDark ? '#1e40af' : '#e0f2fe', 81 | tipTextColor: isDark ? '#dbeafe' : '#0369a1', 82 | buttonBackground: isDark ? '#374151' : '#f3f4f6', 83 | buttonColor: isDark ? '#f9fafb' : '#374151', 84 | buttonHoverBackground: isDark ? '#4b5563' : '#e5e7eb', 85 | countdownColor: isDark ? '#6b7280' : '#9ca3af', 86 | progressBackground: isDark ? '#4b5563' : '#e5e7eb', 87 | progressGradient: isDark 88 | ? 'linear-gradient(90deg, #3b82f6 0%, #2563eb 50%, #1d4ed8 100%)' 89 | : 'linear-gradient(90deg, #2563eb 0%, #1d4ed8 50%, #1e40af 100%)', 90 | linkColor: isDark ? '#60a5fa' : '#2563eb', 91 | extensionButtonBackground: isDark ? '#1e40af' : '#2563eb', 92 | extensionButtonHover: isDark ? '#1d4ed8' : '#1d4ed8' 93 | }; 94 | } 95 | 96 | private createModalHTML(): string { 97 | const styles = this.getThemeStyles(); 98 | 99 | return ` 100 |
113 |
123 | 146 | 147 |

${this.options.title}

153 | 154 |

${this.options.description}

160 | 161 |
167 | 168 |
175 |

💡 For a better experience

181 |

Install our browser extension for seamless verification without QR codes

187 | 202 | Install Extension 203 | 204 |
205 | 206 |
207 |
Auto-close in 1:00
213 | 214 |
221 |
228 |
229 |
230 |
231 |
232 | ` 233 | } 234 | 235 | private async generateQRCode(text: string, containerId: string): Promise { 236 | try { 237 | // Simple QR code generation using a public API 238 | // In production, you might want to use a proper QR code library 239 | const qrCodeUrl = `${constants.QR_CODE_API_URL}?size=200x200&data=${encodeURIComponent(text)}`; 240 | 241 | const container = document.getElementById(containerId); 242 | const styles = this.getThemeStyles(); 243 | 244 | if (container) { 245 | container.innerHTML = ` 246 | QR Code for Reclaim verification 250 |
251 | QR code could not be loaded.
252 | 253 | Click here to open verification link 254 | 255 |
256 | `; 257 | } 258 | } catch (error) { 259 | logger.info('Error generating QR code:', error); 260 | // Fallback to text link 261 | const container = document.getElementById(containerId); 262 | const styles = this.getThemeStyles(); 263 | 264 | if (container) { 265 | container.innerHTML = ` 266 | 271 | `; 272 | } 273 | } 274 | } 275 | 276 | private addEventListeners(): void { 277 | const closeButton = document.getElementById('reclaim-close-modal'); 278 | const modal = document.getElementById(this.modalId); 279 | 280 | const closeModal = () => { 281 | this.close(); 282 | }; 283 | 284 | if (closeButton) { 285 | closeButton.addEventListener('click', closeModal); 286 | } 287 | 288 | // Close on backdrop click 289 | if (modal) { 290 | modal.addEventListener('click', (e) => { 291 | if (e.target === modal) { 292 | closeModal(); 293 | } 294 | }); 295 | } 296 | 297 | // Close on escape key 298 | const handleEscape = (e: KeyboardEvent) => { 299 | if (e.key === 'Escape') { 300 | closeModal(); 301 | document.removeEventListener('keydown', handleEscape); 302 | } 303 | }; 304 | document.addEventListener('keydown', handleEscape); 305 | } 306 | 307 | private startAutoCloseTimer(): void { 308 | this.countdownSeconds = 60; 309 | 310 | // Update countdown display immediately 311 | this.updateCountdownDisplay(); 312 | 313 | // Start countdown timer (updates every second) 314 | this.countdownTimer = setInterval(() => { 315 | this.countdownSeconds--; 316 | this.updateCountdownDisplay(); 317 | 318 | if (this.countdownSeconds <= 0) { 319 | this.close(); 320 | } 321 | }, 1000); 322 | 323 | // Set auto-close timer for 1 minute 324 | this.autoCloseTimer = setTimeout(() => { 325 | this.close(); 326 | }, 60000); 327 | } 328 | 329 | private updateCountdownDisplay(): void { 330 | const countdownElement = document.getElementById('reclaim-countdown'); 331 | const progressBar = document.getElementById('reclaim-progress-bar'); 332 | 333 | if (countdownElement) { 334 | const minutes = Math.floor(this.countdownSeconds / 60); 335 | const seconds = this.countdownSeconds % 60; 336 | const timeString = `${minutes}:${seconds.toString().padStart(2, '0')}`; 337 | countdownElement.textContent = `Auto-close in ${timeString}`; 338 | } 339 | 340 | if (progressBar) { 341 | // Calculate progress percentage (reverse: starts at 100%, goes to 0%) 342 | const progressPercentage = (this.countdownSeconds / 60) * 100; 343 | progressBar.style.width = `${progressPercentage}%`; 344 | } 345 | } 346 | } -------------------------------------------------------------------------------- /src/utils/proofUtils.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { WitnessData } from "./interfaces"; 3 | import { SignedClaim, TemplateData } from "./types"; 4 | import { createSignDataForClaim, fetchWitnessListForClaim } from "../witness"; 5 | import { BACKEND_BASE_URL, constants } from "./constants"; 6 | import { replaceAll } from "./helper"; 7 | import { validateURL } from "./validationUtils"; 8 | import { makeBeacon } from "../smart-contract"; 9 | import { ProofNotVerifiedError } from "./errors"; 10 | import loggerModule from './logger'; 11 | const logger = loggerModule.logger; 12 | 13 | 14 | /** 15 | * Retrieves a shortened URL for the given URL 16 | * @param url - The URL to be shortened 17 | * @returns A promise that resolves to the shortened URL, or the original URL if shortening fails 18 | */ 19 | export async function getShortenedUrl(url: string): Promise { 20 | logger.info(`Attempting to shorten URL: ${url}`); 21 | try { 22 | validateURL(url, 'getShortenedUrl') 23 | const response = await fetch(`${BACKEND_BASE_URL}/api/sdk/shortener`, { 24 | method: 'POST', 25 | headers: { 'Content-Type': 'application/json' }, 26 | body: JSON.stringify({ fullUrl: url }) 27 | }) 28 | const res = await response.json() 29 | if (!response.ok) { 30 | logger.info(`Failed to shorten URL: ${url}, Response: ${JSON.stringify(res)}`); 31 | return url; 32 | } 33 | const shortenedVerificationUrl = res.result.shortUrl 34 | return shortenedVerificationUrl 35 | } catch (err) { 36 | logger.info(`Error shortening URL: ${url}, Error: ${err}`); 37 | return url 38 | } 39 | } 40 | 41 | /** 42 | * Creates a link with embedded template data 43 | * @param templateData - The data to be embedded in the link 44 | * @returns A promise that resolves to the created link (shortened if possible) 45 | */ 46 | export async function createLinkWithTemplateData(templateData: TemplateData): Promise { 47 | let template = encodeURIComponent(JSON.stringify(templateData)) 48 | template = replaceAll(template, '(', '%28') 49 | template = replaceAll(template, ')', '%29') 50 | 51 | const fullLink = `${constants.RECLAIM_SHARE_URL}${template}` 52 | try { 53 | const shortenedLink = await getShortenedUrl(fullLink) 54 | return shortenedLink; 55 | } catch (err) { 56 | logger.info(`Error creating link for sessionId: ${templateData.sessionId}, Error: ${err}`); 57 | return fullLink; 58 | } 59 | } 60 | 61 | /** 62 | * Retrieves the list of witnesses for a given claim 63 | * @param epoch - The epoch number 64 | * @param identifier - The claim identifier 65 | * @param timestampS - The timestamp in seconds 66 | * @returns A promise that resolves to an array of witness addresses 67 | * @throws Error if no beacon is available 68 | */ 69 | export async function getWitnessesForClaim( 70 | epoch: number, 71 | identifier: string, 72 | timestampS: number 73 | ): Promise { 74 | const beacon = makeBeacon() 75 | if (!beacon) { 76 | logger.info('No beacon available for getting witnesses'); 77 | throw new Error('No beacon available'); 78 | } 79 | const state = await beacon.getState(epoch) 80 | const witnessList = fetchWitnessListForClaim(state, identifier, timestampS) 81 | const witnesses = witnessList.map((w: WitnessData) => w.id.toLowerCase()) 82 | return witnesses; 83 | } 84 | 85 | /** 86 | * Recovers the signers' addresses from a signed claim 87 | * @param claim - The signed claim object 88 | * @param signatures - The signatures associated with the claim 89 | * @returns An array of recovered signer addresses 90 | */ 91 | export function recoverSignersOfSignedClaim({ 92 | claim, 93 | signatures 94 | }: SignedClaim): string[] { 95 | const dataStr = createSignDataForClaim({ ...claim }) 96 | const signers = signatures.map(signature => 97 | ethers.verifyMessage(dataStr, ethers.hexlify(signature)).toLowerCase() 98 | ) 99 | return signers; 100 | } 101 | 102 | /** 103 | * Asserts that a signed claim is valid by checking if all expected witnesses have signed 104 | * @param claim - The signed claim to validate 105 | * @param expectedWitnessAddresses - An array of expected witness addresses 106 | * @throws ProofNotVerifiedError if any expected witness signature is missing 107 | */ 108 | export function assertValidSignedClaim( 109 | claim: SignedClaim, 110 | expectedWitnessAddresses: string[] 111 | ): void { 112 | const witnessAddresses = recoverSignersOfSignedClaim(claim) 113 | const witnessesNotSeen = new Set(expectedWitnessAddresses) 114 | for (const witness of witnessAddresses) { 115 | if (witnessesNotSeen.has(witness)) { 116 | witnessesNotSeen.delete(witness) 117 | } 118 | } 119 | 120 | if (witnessesNotSeen.size > 0) { 121 | const missingWitnesses = Array.from(witnessesNotSeen).join(', '); 122 | logger.info(`Claim validation failed. Missing signatures from: ${missingWitnesses}`); 123 | throw new ProofNotVerifiedError( 124 | `Missing signatures from ${missingWitnesses}` 125 | ) 126 | } 127 | } -------------------------------------------------------------------------------- /src/utils/sessionUtils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InitSessionError, 3 | UpdateSessionError, 4 | StatusUrlError 5 | } from "./errors"; 6 | import { InitSessionResponse, SessionStatus, StatusUrlResponse } from "./types"; 7 | import { validateFunctionParams } from "./validationUtils"; 8 | import { BACKEND_BASE_URL, constants } from './constants'; 9 | import loggerModule from './logger'; 10 | const logger = loggerModule.logger; 11 | 12 | /** 13 | * Initializes a session with the provided parameters 14 | * @param providerId - The ID of the provider 15 | * @param appId - The ID of the application 16 | * @param timestamp - The timestamp of the request 17 | * @param signature - The signature for authentication 18 | * @returns A promise that resolves to an InitSessionResponse 19 | * @throws InitSessionError if the session initialization fails 20 | */ 21 | export async function initSession( 22 | providerId: string, 23 | appId: string, 24 | timestamp: string, 25 | signature: string, 26 | versionNumber?: string 27 | ): Promise { 28 | logger.info(`Initializing session for providerId: ${providerId}, appId: ${appId}`); 29 | try { 30 | const response = await fetch(`${BACKEND_BASE_URL}/api/sdk/init/session/`, { 31 | method: 'POST', 32 | headers: { 'Content-Type': 'application/json' }, 33 | body: JSON.stringify({ providerId, appId, timestamp, signature, versionNumber }) 34 | }); 35 | 36 | const res = await response.json(); 37 | 38 | if (!response.ok) { 39 | logger.info(`Session initialization failed: ${res.message || 'Unknown error'}`); 40 | throw new InitSessionError(res.message || `Error initializing session with providerId: ${providerId}`); 41 | } 42 | 43 | return res as InitSessionResponse; 44 | } catch (err) { 45 | logger.info(`Failed to initialize session for providerId: ${providerId}, appId: ${appId}`, err); 46 | throw err; 47 | } 48 | } 49 | 50 | /** 51 | * Updates the status of an existing session 52 | * @param sessionId - The ID of the session to update 53 | * @param status - The new status of the session 54 | * @returns A promise that resolves to the update response 55 | * @throws UpdateSessionError if the session update fails 56 | */ 57 | export async function updateSession(sessionId: string, status: SessionStatus) { 58 | logger.info(`Updating session status for sessionId: ${sessionId}, new status: ${status}`); 59 | validateFunctionParams( 60 | [{ input: sessionId, paramName: 'sessionId', isString: true }], 61 | 'updateSession' 62 | ); 63 | 64 | try { 65 | const response = await fetch(`${BACKEND_BASE_URL}/api/sdk/update/session/`, { 66 | method: 'POST', 67 | headers: { 'Content-Type': 'application/json' }, 68 | body: JSON.stringify({ sessionId, status }) 69 | }); 70 | 71 | const res = await response.json(); 72 | 73 | if (!response.ok) { 74 | const errorMessage = `Error updating session with sessionId: ${sessionId}. Status Code: ${response.status}`; 75 | logger.info(errorMessage, res); 76 | throw new UpdateSessionError(errorMessage); 77 | } 78 | 79 | logger.info(`Session status updated successfully for sessionId: ${sessionId}`); 80 | return res; 81 | } catch (err) { 82 | const errorMessage = `Failed to update session with sessionId: ${sessionId}`; 83 | logger.info(errorMessage, err); 84 | throw new UpdateSessionError(`Error updating session with sessionId: ${sessionId}`); 85 | } 86 | } 87 | 88 | /** 89 | * Fetches the status URL for a given session ID 90 | * @param sessionId - The ID of the session to fetch the status URL for 91 | * @returns A promise that resolves to a StatusUrlResponse 92 | * @throws StatusUrlError if the status URL fetch fails 93 | */ 94 | export async function fetchStatusUrl(sessionId: string): Promise { 95 | validateFunctionParams( 96 | [{ input: sessionId, paramName: 'sessionId', isString: true }], 97 | 'fetchStatusUrl' 98 | ); 99 | 100 | try { 101 | const response = await fetch(`${constants.DEFAULT_RECLAIM_STATUS_URL}${sessionId}`, { 102 | method: 'GET', 103 | headers: { 'Content-Type': 'application/json' } 104 | }); 105 | 106 | const res = await response.json(); 107 | 108 | if (!response.ok) { 109 | const errorMessage = `Error fetching status URL for sessionId: ${sessionId}. Status Code: ${response.status}`; 110 | logger.info(errorMessage, res); 111 | throw new StatusUrlError(errorMessage); 112 | } 113 | 114 | return res as StatusUrlResponse; 115 | } catch (err) { 116 | const errorMessage = `Failed to fetch status URL for sessionId: ${sessionId}`; 117 | logger.info(errorMessage, err); 118 | throw new StatusUrlError(`Error fetching status URL for sessionId: ${sessionId}`); 119 | } 120 | } 121 | 122 | 123 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | import type { Context, Proof, ProviderClaimData } from './interfaces'; 2 | import type { ParsedQs } from 'qs'; 3 | 4 | // Claim-related types 5 | export type ClaimID = ProviderClaimData['identifier']; 6 | 7 | export type ClaimInfo = Pick; 8 | 9 | export type AnyClaimInfo = ClaimInfo | { identifier: ClaimID }; 10 | 11 | export type CompleteClaimData = Pick & AnyClaimInfo; 12 | 13 | export type SignedClaim = { 14 | claim: CompleteClaimData; 15 | signatures: Uint8Array[]; 16 | }; 17 | 18 | // Request and session-related types 19 | export type CreateVerificationRequest = { 20 | providerIds: string[]; 21 | applicationSecret?: string; 22 | }; 23 | 24 | export type StartSessionParams = { 25 | onSuccess: OnSuccess; 26 | onError: OnError; 27 | }; 28 | 29 | export type OnSuccess = (proof?: Proof | Proof[] | string) => void; 30 | export type OnError = (error: Error) => void; 31 | 32 | export type ProofRequestOptions = { 33 | log?: boolean; 34 | acceptAiProviders?: boolean; 35 | useAppClip?: boolean; 36 | device?: string; 37 | envUrl?: string; 38 | useBrowserExtension?: boolean; 39 | extensionID?: string; 40 | providerVersion?: string; 41 | }; 42 | 43 | // Modal customization options 44 | export type ModalOptions = { 45 | title?: string; 46 | description?: string; 47 | extensionUrl?: string; 48 | darkTheme?: boolean; 49 | onClose?: () => void; 50 | }; 51 | 52 | // Claim creation type enum 53 | export enum ClaimCreationType { 54 | STANDALONE = 'createClaim', 55 | ON_ME_CHAIN = 'createClaimOnMechain' 56 | } 57 | 58 | // Device type enum 59 | export enum DeviceType { 60 | ANDROID = 'android', 61 | IOS = 'ios', 62 | DESKTOP = 'desktop', 63 | MOBILE = 'mobile' 64 | } 65 | 66 | 67 | // Session and response types 68 | export type InitSessionResponse = { 69 | sessionId: string; 70 | resolvedProviderVersion: string; 71 | }; 72 | 73 | export interface UpdateSessionResponse { 74 | success: boolean; 75 | message?: string; 76 | }; 77 | 78 | export enum SessionStatus { 79 | SESSION_INIT = 'SESSION_INIT', 80 | SESSION_STARTED = 'SESSION_STARTED', 81 | USER_INIT_VERIFICATION = 'USER_INIT_VERIFICATION', 82 | USER_STARTED_VERIFICATION = 'USER_STARTED_VERIFICATION', 83 | PROOF_GENERATION_STARTED = 'PROOF_GENERATION_STARTED', 84 | PROOF_GENERATION_SUCCESS = 'PROOF_GENERATION_SUCCESS', 85 | PROOF_GENERATION_FAILED = 'PROOF_GENERATION_FAILED', 86 | PROOF_SUBMITTED = 'PROOF_SUBMITTED', 87 | PROOF_SUBMISSION_FAILED = 'PROOF_SUBMISSION_FAILED', 88 | PROOF_MANUAL_VERIFICATION_SUBMITED = 'PROOF_MANUAL_VERIFICATION_SUBMITED', 89 | }; 90 | 91 | // JSON and template-related types 92 | export type ProofPropertiesJSON = { 93 | applicationId: string; 94 | providerId: string; 95 | sessionId: string; 96 | context: Context; 97 | signature: string; 98 | redirectUrl?: string; 99 | parameters: { [key: string]: string }; 100 | timeStamp: string; 101 | appCallbackUrl?: string; 102 | claimCreationType?: ClaimCreationType; 103 | options?: ProofRequestOptions; 104 | sdkVersion: string; 105 | jsonProofResponse?: boolean; 106 | resolvedProviderVersion: string; 107 | }; 108 | 109 | export type TemplateData = { 110 | sessionId: string; 111 | providerId: string; 112 | applicationId: string; 113 | signature: string; 114 | timestamp: string; 115 | callbackUrl: string; 116 | context: string; 117 | parameters: { [key: string]: string }; 118 | redirectUrl: string; 119 | acceptAiProviders: boolean; 120 | sdkVersion: string; 121 | jsonProofResponse?: boolean; 122 | providerVersion?: string; 123 | resolvedProviderVersion: string; 124 | }; 125 | 126 | // Add the new StatusUrlResponse type 127 | export type StatusUrlResponse = { 128 | message: string; 129 | session?: { 130 | id: string; 131 | appId: string; 132 | httpProviderId: string[]; 133 | sessionId: string; 134 | proofs?: Proof[]; 135 | statusV2: string; 136 | }; 137 | providerId?: string; 138 | }; -------------------------------------------------------------------------------- /src/utils/validationUtils.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { InavlidParametersError, InvalidParamError, InvalidSignatureError } from "./errors"; 3 | import canonicalize from 'canonicalize' 4 | import { Context } from "./interfaces"; 5 | import loggerModule from './logger'; 6 | import { ProofRequestOptions } from "./types"; 7 | const logger = loggerModule.logger; 8 | 9 | /** 10 | * Validates function parameters based on specified criteria 11 | * @param params - An array of objects containing input, paramName, and optional isString flag 12 | * @param functionName - The name of the function being validated 13 | * @throws InvalidParamError if any parameter fails validation 14 | */ 15 | export function validateFunctionParams(params: { input: any, paramName: string, isString?: boolean }[], functionName: string): void { 16 | params.forEach(({ input, paramName, isString }) => { 17 | if (input == null) { 18 | logger.info(`Validation failed: ${paramName} in ${functionName} is null or undefined`); 19 | throw new InvalidParamError(`${paramName} passed to ${functionName} must not be null or undefined.`); 20 | } 21 | if (isString && typeof input !== 'string') { 22 | logger.info(`Validation failed: ${paramName} in ${functionName} is not a string`); 23 | throw new InvalidParamError(`${paramName} passed to ${functionName} must be a string.`); 24 | } 25 | if (isString && input.trim() === '') { 26 | logger.info(`Validation failed: ${paramName} in ${functionName} is an empty string`); 27 | throw new InvalidParamError(`${paramName} passed to ${functionName} must not be an empty string.`); 28 | } 29 | }); 30 | } 31 | 32 | 33 | // validate the parameters 34 | /** 35 | * Validates the parameters object 36 | * @param parameters - The parameters object to validate 37 | * @throws InavlidParametersError if the parameters object is not valid 38 | */ 39 | export function validateParameters(parameters: { [key: string]: string }): void { 40 | try { 41 | // check if the parameters is an object of key value pairs of string and string 42 | if (typeof parameters !== 'object' || parameters === null) { 43 | logger.info(`Parameters validation failed: Provided parameters is not an object`); 44 | throw new InavlidParametersError(`The provided parameters is not an object`); 45 | } 46 | // check each key and value in the parameters object 47 | for (const [key, value] of Object.entries(parameters)) { 48 | if (typeof key !== 'string' || typeof value !== 'string') { 49 | logger.info(`Parameters validation failed: Provided parameters is not an object of key value pairs of string and string`); 50 | throw new InavlidParametersError(`The provided parameters is not an object of key value pairs of string and string`); 51 | } 52 | } 53 | } catch (e) { 54 | logger.info(`Parameters validation failed: ${(e as Error).message}`); 55 | throw new InavlidParametersError(`Invalid parameters passed to validateParameters.`, e as Error); 56 | } 57 | } 58 | 59 | 60 | /** 61 | * Validates a URL string 62 | * @param url - The URL to validate 63 | * @param functionName - The name of the function calling this validation 64 | * @throws InvalidParamError if the URL is invalid or empty 65 | */ 66 | export function validateURL(url: string, functionName: string): void { 67 | try { 68 | new URL(url); 69 | } catch (e) { 70 | logger.info(`URL validation failed for ${url} in ${functionName}: ${(e as Error).message}`); 71 | throw new InvalidParamError(`Invalid URL format ${url} passed to ${functionName}.`, e as Error); 72 | } 73 | } 74 | 75 | /** 76 | * Validates a signature against the provided application ID 77 | * @param providerId - The ID of the provider 78 | * @param signature - The signature to validate 79 | * @param applicationId - The expected application ID 80 | * @param timestamp - The timestamp of the signature 81 | * @throws InvalidSignatureError if the signature is invalid or doesn't match the application ID 82 | */ 83 | export function validateSignature(providerId: string, signature: string, applicationId: string, timestamp: string): void { 84 | try { 85 | logger.info(`Starting signature validation for providerId: ${providerId}, applicationId: ${applicationId}, timestamp: ${timestamp}`); 86 | 87 | const message = canonicalize({ providerId, timestamp }); 88 | if (!message) { 89 | logger.info('Failed to canonicalize message for signature validation'); 90 | throw new Error('Failed to canonicalize message'); 91 | } 92 | const messageHash = ethers.keccak256(new TextEncoder().encode(message)); 93 | let appId = ethers.verifyMessage( 94 | ethers.getBytes(messageHash), 95 | ethers.hexlify(signature) 96 | ).toLowerCase(); 97 | 98 | if (ethers.getAddress(appId) !== ethers.getAddress(applicationId)) { 99 | logger.info(`Signature validation failed: Mismatch between derived appId (${appId}) and provided applicationId (${applicationId})`); 100 | throw new InvalidSignatureError(`Signature does not match the application id: ${appId}`); 101 | } 102 | 103 | logger.info(`Signature validated successfully for applicationId: ${applicationId}`); 104 | } catch (err) { 105 | logger.info(`Signature validation failed: ${(err as Error).message}`); 106 | if (err instanceof InvalidSignatureError) { 107 | throw err; 108 | } 109 | throw new InvalidSignatureError(`Failed to validate signature: ${(err as Error).message}`); 110 | } 111 | } 112 | 113 | 114 | /** 115 | * Validates the context object 116 | * @param context - The context object to validate 117 | * @throws InvalidParamError if the context object is not valid 118 | */ 119 | export function validateContext(context: Context): void { 120 | if (!context.contextAddress) { 121 | logger.info(`Context validation failed: Provided context address in context is not valid`); 122 | throw new InvalidParamError(`The provided context address in context is not valid`); 123 | } 124 | 125 | if (!context.contextMessage) { 126 | logger.info(`Context validation failed: Provided context message in context is not valid`); 127 | throw new InvalidParamError(`The provided context message in context is not valid`); 128 | } 129 | 130 | validateFunctionParams([ 131 | { input: context.contextAddress, paramName: 'contextAddress', isString: true }, 132 | { input: context.contextMessage, paramName: 'contextMessage', isString: true } 133 | ], 'validateContext'); 134 | } 135 | 136 | /** 137 | * Validates the options object 138 | * @param options - The options object to validate 139 | * @throws InvalidParamError if the options object is not valid 140 | */ 141 | export function validateOptions(options: ProofRequestOptions): void { 142 | if (options.acceptAiProviders && typeof options.acceptAiProviders !== 'boolean') { 143 | logger.info(`Options validation failed: Provided acceptAiProviders in options is not valid`); 144 | throw new InvalidParamError(`The provided acceptAiProviders in options is not valid`); 145 | } 146 | 147 | if (options.log && typeof options.log !== 'boolean') { 148 | logger.info(`Options validation failed: Provided log in options is not valid`); 149 | throw new InvalidParamError(`The provided log in options is not valid`); 150 | } 151 | 152 | if (options.providerVersion && typeof options.providerVersion !== 'string') { 153 | logger.info(`Options validation failed: Provided providerVersion in options is not valid`); 154 | throw new InvalidParamError(`The provided providerVersion in options is not valid`); 155 | } 156 | } 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /src/witness.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | import type { WitnessData } from './utils/interfaces'; 3 | import type { ClaimID, ClaimInfo, CompleteClaimData } from './utils/types'; 4 | 5 | type BeaconState = { 6 | witnesses: WitnessData[]; 7 | epoch: number; 8 | witnessesRequiredForClaim: number; 9 | nextEpochTimestampS: number; 10 | }; 11 | 12 | export function fetchWitnessListForClaim( 13 | { witnesses, witnessesRequiredForClaim, epoch }: BeaconState, 14 | params: string | ClaimInfo, 15 | timestampS: number 16 | ): WitnessData[] { 17 | const identifier: ClaimID = 18 | typeof params === 'string' ? params : getIdentifierFromClaimInfo(params); 19 | const completeInput: string = [ 20 | identifier, 21 | epoch.toString(), 22 | witnessesRequiredForClaim.toString(), 23 | timestampS.toString(), 24 | ].join('\n'); 25 | const completeHashStr: string = ethers.keccak256(strToUint8Array(completeInput)); 26 | const completeHash: Uint8Array = ethers.getBytes(completeHashStr); 27 | const completeHashView: DataView = uint8ArrayToDataView(completeHash); 28 | const witnessesLeft: WitnessData[] = [...witnesses]; 29 | const selectedWitnesses: WitnessData[] = []; 30 | let byteOffset: number = 0; 31 | for (let i = 0; i < witnessesRequiredForClaim; i++) { 32 | const randomSeed: number = completeHashView.getUint32(byteOffset); 33 | const witnessIndex: number = randomSeed % witnessesLeft.length; 34 | const witness: WitnessData = witnessesLeft[witnessIndex]; 35 | selectedWitnesses.push(witness); 36 | 37 | witnessesLeft[witnessIndex] = witnessesLeft[witnessesLeft.length - 1]; 38 | witnessesLeft.pop(); 39 | byteOffset = (byteOffset + 4) % completeHash.length; 40 | } 41 | 42 | return selectedWitnesses; 43 | } 44 | 45 | export function getIdentifierFromClaimInfo(info: ClaimInfo): ClaimID { 46 | const str: string = `${info.provider}\n${info.parameters}\n${info.context || ''}`; 47 | return ethers.keccak256(strToUint8Array(str)).toLowerCase(); 48 | } 49 | 50 | export function strToUint8Array(str: string): Uint8Array { 51 | return new TextEncoder().encode(str); 52 | } 53 | 54 | export function uint8ArrayToDataView(arr: Uint8Array): DataView { 55 | return new DataView(arr.buffer, arr.byteOffset, arr.byteLength); 56 | } 57 | 58 | export function createSignDataForClaim(data: CompleteClaimData): string { 59 | const identifier: ClaimID = 60 | 'identifier' in data ? data.identifier : getIdentifierFromClaimInfo(data); 61 | const lines: string[] = [ 62 | identifier, 63 | data.owner.toLowerCase(), 64 | data.timestampS.toString(), 65 | data.epoch.toString(), 66 | ]; 67 | 68 | return lines.join('\n'); 69 | } 70 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | "outDir": "./dist", 5 | "declaration": true, 6 | "emitDeclarationOnly": false, 7 | "declarationMap": true, 8 | 9 | /* Language and Environment */ 10 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 11 | 12 | /* Modules */ 13 | "module": "commonjs" /* Specify what module code is generated. */, 14 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 15 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 16 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 17 | "resolveJsonModule": true, 18 | /* Type Checking */ 19 | "strict": true /* Enable all strict type-checking options. */, 20 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 21 | }, 22 | "exclude": ["example"] 23 | } 24 | --------------------------------------------------------------------------------