@lumiapassport/ui-kit 1.6.3 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -340,6 +340,79 @@ function DirectTransactionExample() {
340
340
  - ✅ Use `useSendTransaction()` hook for React components (automatic session management)
341
341
  - ✅ Use `sendUserOperation()` function for custom logic, utility functions, or non-React code
342
342
 
343
+ ### signTypedData - Sign EIP712 Structured Messages
344
+
345
+ Sign structured data according to [EIP-712](https://eips.ethereum.org/EIPS/eip-712) standard. This is commonly used for off-chain signatures in dApps (e.g., NFT marketplace orders, gasless transactions, permit signatures).
346
+
347
+ ```tsx
348
+ import { signTypedData, useLumiaPassportSession } from '@lumiapassport/ui-kit';
349
+
350
+ function SignatureExample() {
351
+ const { session } = useLumiaPassportSession();
352
+
353
+ const handleSign = async () => {
354
+ if (!session) return;
355
+
356
+ try {
357
+ // Define EIP712 typed data
358
+ const signature = await signTypedData(session, {
359
+ domain: {
360
+ name: 'MyDApp', // Your dApp name (must match contract)
361
+ version: '1', // Contract version
362
+ chainId: 994, // Lumia Prism Testnet
363
+ verifyingContract: '0x...', // Your contract address (REQUIRED in production!)
364
+ },
365
+ types: {
366
+ Order: [
367
+ { name: 'tokenIds', type: 'uint256[]' },
368
+ { name: 'price', type: 'uint256' },
369
+ { name: 'deadline', type: 'uint256' },
370
+ ],
371
+ },
372
+ primaryType: 'Order',
373
+ message: {
374
+ tokenIds: [1n, 2n, 3n],
375
+ price: 1000000000000000000n, // 1 token in wei
376
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 3600), // 1 hour from now
377
+ },
378
+ });
379
+
380
+ console.log('Signature:', signature);
381
+
382
+ // Verify signature (optional)
383
+ const { recoverTypedDataAddress } = await import('viem');
384
+ const recoveredAddress = await recoverTypedDataAddress({
385
+ domain: { /* same domain */ },
386
+ types: { /* same types */ },
387
+ primaryType: 'Order',
388
+ message: { /* same message */ },
389
+ signature,
390
+ });
391
+
392
+ console.log('Signer:', recoveredAddress); // Should match session.ownerAddress
393
+ } catch (error) {
394
+ console.error('Signing failed:', error);
395
+ }
396
+ };
397
+
398
+ return <button onClick={handleSign}>Sign Message</button>;
399
+ }
400
+ ```
401
+
402
+ **Important Notes:**
403
+ - The signature is created by the **owner address** (EOA), not the smart account address
404
+ - In production, always use your actual `verifyingContract` address (not zero address!)
405
+ - The `domain` parameters must match exactly between frontend and smart contract
406
+ - Shows a MetaMask-like confirmation modal with structured message preview
407
+ - All BigInt values are supported in the message
408
+
409
+ **Common Use Cases:**
410
+ - NFT marketplace orders (OpenSea-style)
411
+ - ERC-20 Permit signatures (gasless approvals)
412
+ - Meta-transactions and gasless operations
413
+ - DAO voting signatures
414
+ - Any off-chain signature verification
415
+
343
416
  ### prepareUserOperation - Prepare for Backend Submission
344
417
 
345
418
  ```tsx
@@ -15,7 +15,7 @@
15
15
  <meta http-equiv="X-Content-Type-Options" content="nosniff" />
16
16
  <meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin" />
17
17
 
18
- <title>Lumia Passport Secure Wallet - iframe version 1.6.3</title>
18
+ <title>Lumia Passport Secure Wallet - iframe version 1.7.0</title>
19
19
 
20
20
  <!-- Styles will be injected by build process -->
21
21
  <style>
@@ -28,6 +28,10 @@
28
28
  --iframe-modal-bg: white;
29
29
  --iframe-button-bg: #667eea;
30
30
  --iframe-button-text: white;
31
+ --iframe-section-bg: #f9fafb;
32
+ --iframe-section-border: #e5e7eb;
33
+ --iframe-section-text: #374151;
34
+ --iframe-field-label: #6b7280;
31
35
  }
32
36
 
33
37
  * {
@@ -542,6 +546,221 @@
542
546
  .trust-app-label:hover {
543
547
  color: #111827;
544
548
  }
549
+
550
+ /* EIP712 Signature Request Modal Styles */
551
+ .eip712-confirmation-modal {
552
+ position: fixed;
553
+ top: 0;
554
+ left: 0;
555
+ right: 0;
556
+ bottom: 0;
557
+ z-index: 10000;
558
+ }
559
+
560
+ .eip712-modal {
561
+ max-width: 480px;
562
+ }
563
+
564
+ .eip712-content {
565
+ padding: 1.5rem 2rem;
566
+ }
567
+
568
+ .section-title {
569
+ font-size: 0.875rem;
570
+ font-weight: 600;
571
+ color: var(--iframe-section-text);
572
+ margin-bottom: 0.75rem;
573
+ text-transform: uppercase;
574
+ letter-spacing: 0.05em;
575
+ }
576
+
577
+ .eip712-section {
578
+ background: var(--iframe-section-bg);
579
+ border: 1px solid var(--iframe-section-border);
580
+ border-radius: 8px;
581
+ padding: 1rem;
582
+ margin-bottom: 1rem;
583
+ }
584
+
585
+ .section-subtitle {
586
+ font-size: 1rem;
587
+ font-weight: 700;
588
+ color: var(--iframe-section-text);
589
+ margin-bottom: 0.75rem;
590
+ padding-bottom: 0.5rem;
591
+ border-bottom: 1px solid var(--iframe-section-border);
592
+ }
593
+
594
+ .eip712-field {
595
+ display: grid;
596
+ grid-template-columns: 120px 1fr;
597
+ gap: 0.75rem;
598
+ padding: 0.5rem 0;
599
+ align-items: start;
600
+ }
601
+
602
+ .eip712-field:not(:last-child) {
603
+ border-bottom: 1px solid var(--iframe-section-border);
604
+ }
605
+
606
+ .field-name {
607
+ font-size: 0.8125rem;
608
+ font-weight: 500;
609
+ color: var(--iframe-field-label);
610
+ }
611
+
612
+ .field-value {
613
+ font-size: 0.8125rem;
614
+ color: var(--iframe-section-text);
615
+ word-break: break-word;
616
+ font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
617
+ }
618
+
619
+ .eip712-details {
620
+ margin-bottom: 1rem;
621
+ border: 1px solid var(--iframe-section-border);
622
+ border-radius: 6px;
623
+ overflow: hidden;
624
+ }
625
+
626
+ .eip712-details summary {
627
+ padding: 0.75rem 1rem;
628
+ cursor: pointer;
629
+ background: var(--iframe-section-bg);
630
+ font-size: 0.875rem;
631
+ font-weight: 600;
632
+ color: var(--iframe-section-text);
633
+ user-select: none;
634
+ list-style: none;
635
+ display: flex;
636
+ align-items: center;
637
+ justify-content: space-between;
638
+ }
639
+
640
+ .eip712-details summary::-webkit-details-marker {
641
+ display: none;
642
+ }
643
+
644
+ .eip712-details summary:after {
645
+ content: '▼';
646
+ font-size: 0.7rem;
647
+ transition: transform 0.2s;
648
+ color: var(--iframe-section-text);
649
+ }
650
+
651
+ .eip712-details[open] summary:after {
652
+ transform: rotate(180deg);
653
+ }
654
+
655
+ .eip712-details summary:hover {
656
+ background: var(--iframe-modal-bg);
657
+ opacity: 0.9;
658
+ }
659
+
660
+ .eip712-raw {
661
+ margin: 0;
662
+ padding: 1rem;
663
+ background: var(--iframe-modal-bg);
664
+ border-top: 1px solid var(--iframe-section-border);
665
+ max-height: 200px;
666
+ overflow-y: auto;
667
+ }
668
+
669
+ .eip712-raw code {
670
+ font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
671
+ font-size: 0.75rem;
672
+ color: var(--iframe-section-text);
673
+ line-height: 1.5;
674
+ white-space: pre-wrap;
675
+ word-break: break-all;
676
+ }
677
+
678
+ .security-info {
679
+ background: var(--iframe-section-bg);
680
+ border-radius: 6px;
681
+ padding: 0.75rem 1rem;
682
+ margin-bottom: 1rem;
683
+ }
684
+
685
+ .info-item {
686
+ display: flex;
687
+ justify-content: space-between;
688
+ align-items: center;
689
+ font-size: 0.8125rem;
690
+ }
691
+
692
+ .info-label {
693
+ font-weight: 500;
694
+ color: var(--iframe-field-label);
695
+ }
696
+
697
+ .info-value {
698
+ font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
699
+ color: var(--iframe-section-text);
700
+ }
701
+
702
+ .footer-notice {
703
+ padding: 0 2rem 2rem;
704
+ text-align: center;
705
+ }
706
+
707
+ .modal-title {
708
+ font-size: 1.25rem;
709
+ font-weight: 600;
710
+ color: var(--iframe-text);
711
+ margin: 0.5rem 0;
712
+ }
713
+
714
+ .origin-text {
715
+ font-size: 0.875rem;
716
+ color: var(--iframe-text-secondary);
717
+ margin: 0;
718
+ display: flex;
719
+ align-items: center;
720
+ justify-content: center;
721
+ gap: 0.5rem;
722
+ }
723
+
724
+ .verified-badge {
725
+ color: #10b981;
726
+ font-weight: 600;
727
+ }
728
+
729
+ .project-logo {
730
+ width: 48px;
731
+ height: 48px;
732
+ border-radius: 8px;
733
+ object-fit: cover;
734
+ }
735
+
736
+ .project-logo-placeholder {
737
+ width: 48px;
738
+ height: 48px;
739
+ border-radius: 8px;
740
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
741
+ display: flex;
742
+ align-items: center;
743
+ justify-content: center;
744
+ font-size: 1.5rem;
745
+ }
746
+
747
+ .arrow-icon {
748
+ font-size: 1.25rem;
749
+ color: #9ca3af;
750
+ }
751
+
752
+ .lumia-logo {
753
+ width: 40px;
754
+ height: 40px;
755
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
756
+ border-radius: 8px;
757
+ display: flex;
758
+ align-items: center;
759
+ justify-content: center;
760
+ color: white;
761
+ font-weight: 700;
762
+ font-size: 1.25rem;
763
+ }
545
764
  </style>
546
765
  </head>
547
766
  <body>
@@ -1379,6 +1379,7 @@ var SecureMessenger = class {
1379
1379
  "AUTHENTICATE",
1380
1380
  "START_DKG",
1381
1381
  "SIGN_TRANSACTION",
1382
+ "SIGN_TYPED_DATA",
1382
1383
  "GET_ADDRESS",
1383
1384
  "CHECK_KEYSHARE",
1384
1385
  "GET_TRUSTED_APPS",
@@ -2911,14 +2912,22 @@ var SigningManager = class extends TokenRefreshApiClient {
2911
2912
  }
2912
2913
  /**
2913
2914
  * Assess transaction risk
2915
+ *
2916
+ * TODO: Implement backend-based risk assessment with real-time price conversion
2917
+ * - Get current LUMIA/USD price from API
2918
+ * - Calculate transaction value in USD
2919
+ * - Use dynamic thresholds based on USD value (e.g., >$100 = HIGH)
2920
+ * - Consider additional factors: recipient reputation, contract verification, etc.
2914
2921
  */
2915
2922
  async assessRisk(transaction) {
2916
2923
  const reasons = [];
2917
2924
  let score = 0;
2918
- const valueEth = parseFloat(transaction.value);
2919
- if (valueEth > 1) {
2925
+ const valueWei = BigInt(transaction.value || "0");
2926
+ const valueLumia = Number(valueWei) / 1e18;
2927
+ if (valueLumia > 10) {
2920
2928
  score += 20;
2921
- reasons.push(`High value transaction (${valueEth} ETH)`);
2929
+ const formattedValue = valueLumia >= 1 ? valueLumia.toFixed(4).replace(/\.?0+$/, "") : valueLumia.toFixed(8).replace(/\.?0+$/, "");
2930
+ reasons.push(`High value transaction (${formattedValue} LUMIA)`);
2922
2931
  }
2923
2932
  if (transaction.data && transaction.data !== "0x" && transaction.data.length > 2) {
2924
2933
  score += 10;
@@ -2975,6 +2984,170 @@ var SigningManager = class extends TokenRefreshApiClient {
2975
2984
  extractServerMessage(response) {
2976
2985
  return response?.msgNext || response?.msg_next || response?.msg || null;
2977
2986
  }
2987
+ /**
2988
+ * Sign EIP712 typed data with user confirmation
2989
+ */
2990
+ async signTypedData(userId, projectId, origin, typedData, digest32, accessToken) {
2991
+ console.log("[iframe][Sign] EIP712 signing request:", { userId, projectId, origin, primaryType: typedData.primaryType });
2992
+ await this.initialize();
2993
+ const projectInfo = {
2994
+ id: projectId,
2995
+ name: "Application",
2996
+ logoUrl: "",
2997
+ website: origin,
2998
+ domains: [origin]
2999
+ };
3000
+ const isTrusted = this.trustedApps.isTrusted(userId, projectId, origin);
3001
+ if (!isTrusted) {
3002
+ const confirmResult = await this.showEIP712ConfirmationDialog(
3003
+ userId,
3004
+ projectId,
3005
+ projectInfo,
3006
+ origin,
3007
+ typedData
3008
+ );
3009
+ if (!confirmResult.confirmed) {
3010
+ throw new Error("User rejected signature request");
3011
+ }
3012
+ if (confirmResult.trustApp) {
3013
+ this.trustedApps.addTrustedApp(userId, projectId, origin);
3014
+ }
3015
+ }
3016
+ const keyshareData = this.storage.loadKeyshare(userId);
3017
+ if (!keyshareData) {
3018
+ throw new Error("No keyshare found. Please complete DKG first.");
3019
+ }
3020
+ const signature = await this.performMPCSigning(userId, keyshareData, digest32, projectId, accessToken);
3021
+ console.log("[iframe][Sign] EIP712 signature generated");
3022
+ return signature;
3023
+ }
3024
+ /**
3025
+ * Show EIP712 confirmation dialog
3026
+ */
3027
+ async showEIP712ConfirmationDialog(userId, projectId, project, origin, typedData) {
3028
+ this.showIframe();
3029
+ const metadata = await this.fetchProjectMetadata(projectId);
3030
+ return new Promise((resolve) => {
3031
+ const modal = this.createEIP712ConfirmationModal(userId, projectId, project, origin, typedData, metadata);
3032
+ const confirmBtn = modal.querySelector(".confirm-btn");
3033
+ const cancelBtn = modal.querySelector(".cancel-btn");
3034
+ const trustCheckbox = modal.querySelector(".trust-app-checkbox");
3035
+ confirmBtn?.addEventListener("click", (e) => {
3036
+ if (e.isTrusted) {
3037
+ const trustApp = trustCheckbox?.checked || false;
3038
+ modal.remove();
3039
+ this.hideIframe();
3040
+ resolve({ confirmed: true, trustApp });
3041
+ }
3042
+ });
3043
+ cancelBtn?.addEventListener("click", () => {
3044
+ modal.remove();
3045
+ this.hideIframe();
3046
+ resolve({ confirmed: false, trustApp: false });
3047
+ });
3048
+ document.body.appendChild(modal);
3049
+ });
3050
+ }
3051
+ /**
3052
+ * Create EIP712 confirmation modal UI (similar to MetaMask)
3053
+ */
3054
+ createEIP712ConfirmationModal(userId, projectId, project, origin, typedData, metadata) {
3055
+ const modal = document.createElement("div");
3056
+ modal.className = "eip712-confirmation-modal";
3057
+ const isVerifiedOrigin = project.domains.includes(origin);
3058
+ const formatFieldValue = (value) => {
3059
+ if (Array.isArray(value)) {
3060
+ return `[${value.map((v) => formatFieldValue(v)).join(", ")}]`;
3061
+ }
3062
+ if (typeof value === "bigint") {
3063
+ return value.toString();
3064
+ }
3065
+ if (typeof value === "object" && value !== null) {
3066
+ return JSON.stringify(value, null, 2);
3067
+ }
3068
+ return String(value);
3069
+ };
3070
+ const messageFields = Object.entries(typedData.message).map(([key, value]) => `
3071
+ <div class="eip712-field">
3072
+ <div class="field-name">${key}:</div>
3073
+ <div class="field-value">${formatFieldValue(value)}</div>
3074
+ </div>
3075
+ `).join("");
3076
+ const domainFields = Object.entries(typedData.domain).filter(([_, value]) => value !== void 0).map(([key, value]) => `
3077
+ <div class="eip712-field">
3078
+ <div class="field-name">${key}:</div>
3079
+ <div class="field-value">${formatFieldValue(value)}</div>
3080
+ </div>
3081
+ `).join("");
3082
+ modal.innerHTML = `
3083
+ <div class="modal-overlay">
3084
+ <div class="modal-content eip712-modal">
3085
+ <!-- Header -->
3086
+ <div class="auth-header">
3087
+ <div class="logo-container">
3088
+ ${metadata?.logo ? `<img src="${metadata.logo}" alt="${metadata.name}" class="project-logo" />` : '<div class="project-logo-placeholder">\u{1F510}</div>'}
3089
+ <span class="arrow-icon">\u2192</span>
3090
+ <div class="lumia-logo">L</div>
3091
+ </div>
3092
+ <h2 class="modal-title">Signature Request</h2>
3093
+ <p class="origin-text">
3094
+ ${isVerifiedOrigin ? '<span class="verified-badge">\u2713</span>' : ""}
3095
+ ${origin}
3096
+ </p>
3097
+ </div>
3098
+
3099
+ <!-- EIP712 Message Content -->
3100
+ <div class="eip712-content">
3101
+ <div class="section-title">\u{1F4DD} Message</div>
3102
+ <div class="eip712-section">
3103
+ <div class="section-subtitle">${typedData.primaryType}</div>
3104
+ ${messageFields}
3105
+ </div>
3106
+
3107
+ <details class="eip712-details">
3108
+ <summary>\u{1F50D} Domain</summary>
3109
+ <div class="eip712-section">
3110
+ ${domainFields}
3111
+ </div>
3112
+ </details>
3113
+
3114
+ <details class="eip712-details">
3115
+ <summary>\u{1F4CB} Full Message</summary>
3116
+ <pre class="eip712-raw"><code>${JSON.stringify(typedData.message, null, 2)}</code></pre>
3117
+ </details>
3118
+ </div>
3119
+
3120
+ <!-- Security Info -->
3121
+ <div class="security-info">
3122
+ <div class="info-item">
3123
+ <span class="info-label">Signing with:</span>
3124
+ <span class="info-value">${this.storage.getOwnerAddress(userId) || userId.substring(0, 20) + "..."}</span>
3125
+ </div>
3126
+ </div>
3127
+
3128
+ <!-- Trust App Option -->
3129
+ <div class="trust-app-section">
3130
+ <label class="trust-app-label">
3131
+ <input type="checkbox" class="trust-app-checkbox" />
3132
+ <span>Trust this application and skip confirmation for future signatures</span>
3133
+ </label>
3134
+ </div>
3135
+
3136
+ <!-- Action Buttons -->
3137
+ <div class="actions">
3138
+ <button class="cancel-btn">Reject</button>
3139
+ <button class="confirm-btn">Sign</button>
3140
+ </div>
3141
+
3142
+ <!-- Footer Notice -->
3143
+ <div class="footer-notice">
3144
+ <p class="footer-note">Only sign messages from applications you trust.</p>
3145
+ </div>
3146
+ </div>
3147
+ </div>
3148
+ `;
3149
+ return modal;
3150
+ }
2978
3151
  /**
2979
3152
  * Show iframe (notify parent)
2980
3153
  */
@@ -3748,7 +3921,7 @@ var BackupManager = class {
3748
3921
  };
3749
3922
 
3750
3923
  // src/iframe/main.ts
3751
- var IFRAME_VERSION = "1.6.3";
3924
+ var IFRAME_VERSION = "1.7.0";
3752
3925
  var IframeWallet = class {
3753
3926
  constructor() {
3754
3927
  console.log("=".repeat(60));
@@ -3831,6 +4004,9 @@ var IframeWallet = class {
3831
4004
  case "SIGN_TRANSACTION":
3832
4005
  await this.handleSignTransaction(message, origin);
3833
4006
  break;
4007
+ case "SIGN_TYPED_DATA":
4008
+ await this.handleSignTypedData(message, origin);
4009
+ break;
3834
4010
  case "GET_ADDRESS":
3835
4011
  await this.handleGetAddress(message, origin);
3836
4012
  break;
@@ -4005,6 +4181,40 @@ var IframeWallet = class {
4005
4181
  throw error;
4006
4182
  }
4007
4183
  }
4184
+ async handleSignTypedData(message, origin) {
4185
+ const { sessionToken, userId, projectId, typedData, digest32, accessToken } = message.data;
4186
+ const { messageId } = message;
4187
+ if (!this.sessionManager.validateSession(sessionToken, origin)) {
4188
+ throw new Error("Invalid session");
4189
+ }
4190
+ const isAuthorized = await this.authManager.checkAuthorization(userId, projectId);
4191
+ if (!isAuthorized) {
4192
+ throw new Error("User has not authorized this application");
4193
+ }
4194
+ console.log(`[iframe] SIGN_TYPED_DATA: userId=${userId}, primaryType=${typedData?.primaryType}`);
4195
+ try {
4196
+ const signature = await this.signingManager.signTypedData(
4197
+ userId,
4198
+ projectId,
4199
+ origin,
4200
+ typedData,
4201
+ digest32,
4202
+ accessToken
4203
+ );
4204
+ this.messenger.sendResponse(
4205
+ messageId,
4206
+ {
4207
+ type: "LUMIA_PASSPORT_EIP712_SIGNATURE",
4208
+ signature
4209
+ },
4210
+ origin
4211
+ );
4212
+ console.log(`[iframe] \u2705 EIP712 message signed`);
4213
+ } catch (error) {
4214
+ console.error("[iframe] EIP712 signing failed:", error);
4215
+ throw error;
4216
+ }
4217
+ }
4008
4218
  async handleGetAddress(message, origin) {
4009
4219
  const { sessionToken, userId } = message.data;
4010
4220
  const { messageId } = message;