Monorepo for Aesthetic.Computer aesthetic.computer
at main 1137 lines 29 kB view raw
1// Keeps Wallet - Background Service Worker 2// Handles key management, signing, Tezos operations, and Beacon protocol 3 4import { 5 initCrypto, 6 generateMnemonic, 7 validateMnemonic, 8 deriveKeypair, 9 importFromPrivateKey, 10 encrypt, 11 decrypt, 12 signOperation as cryptoSignOperation, 13 sign as cryptoSign, 14 clearSensitiveData, 15} from './lib/crypto.mjs'; 16 17import { 18 getOrCreateBeaconKeypair, 19 getSenderId, 20 encryptBeaconMessage, 21 decryptBeaconMessage, 22 bytesToHex, 23 hexToBytes, 24} from './lib/beacon-crypto.mjs'; 25 26// State (in-memory, encrypted keys in storage) 27let unlockedWallet = null; // { address, publicKey, secretKeyBytes } 28let lockTimeout = null; 29const LOCK_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes 30 31// Beacon pending requests — requestId -> { request, peerPublicKey, senderTabId, resolve } 32const pendingBeaconRequests = new Map(); 33 34// Initialize crypto on load 35initCrypto().then(() => { 36 console.log('Keeps Wallet crypto initialized'); 37}); 38 39// Prefer opening the UI in the side panel (new extension UX) 40async function enableSidePanel() { 41 if (!chrome.sidePanel?.setPanelBehavior) return; 42 try { 43 await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }); 44 await chrome.sidePanel.setOptions({ 45 path: chrome.runtime.getURL('popup/popup.html'), 46 enabled: true, 47 }); 48 } catch (err) { 49 console.warn('Side panel setup failed', err); 50 } 51} 52 53// Open UI via side panel when available; otherwise fall back to popup window. 54async function openUiForClick(tab) { 55 if (chrome.sidePanel?.open) { 56 try { 57 await chrome.sidePanel.open({ windowId: tab?.windowId }); 58 return; 59 } catch (err) { 60 console.warn('Side panel open failed, falling back to popup window', err); 61 } 62 } 63 64 // Fallback: open small popup window with the same UI 65 chrome.windows.create({ 66 url: chrome.runtime.getURL('popup/popup.html'), 67 type: 'popup', 68 width: 420, 69 height: 640, 70 }); 71} 72 73chrome.runtime.onInstalled.addListener(() => { 74 enableSidePanel(); 75}); 76 77chrome.runtime.onStartup?.addListener(() => { 78 enableSidePanel(); 79}); 80 81// Open UI on browser action click 82chrome.action.onClicked.addListener((tab) => { 83 openUiForClick(tab); 84}); 85 86// Message handler 87chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 88 handleMessage(message, sender).then(sendResponse); 89 return true; // Keep channel open for async response 90}); 91 92async function handleMessage(message, sender) { 93 const { type, payload } = message; 94 95 try { 96 switch (type) { 97 case 'KEEPS_PING': 98 return { success: true, version: '0.1.0' }; 99 100 case 'KEEPS_GET_STATE': 101 return await getWalletState(); 102 103 case 'KEEPS_CREATE_WALLET': 104 return await createWallet(payload.password); 105 106 case 'KEEPS_IMPORT_WALLET': 107 return await importWallet(payload.mnemonic, payload.password); 108 109 case 'KEEPS_IMPORT_PRIVATE_KEY': 110 return await importPrivateKey(payload.privateKey, payload.password); 111 112 case 'KEEPS_UNLOCK': 113 return await unlockWallet(payload.password); 114 115 case 'KEEPS_LOCK': 116 return lockWallet(); 117 118 case 'KEEPS_GET_ADDRESS': 119 return getAddress(); 120 121 case 'KEEPS_SIGN_OPERATION': 122 return await handleSignRequest(payload.operation, sender); 123 124 case 'KEEPS_GET_BALANCE': 125 return await getBalance(payload.network); 126 127 case 'KEEPS_GET_KEEPS': 128 return await getKeeps(payload.network); 129 130 case 'KEEPS_SET_NETWORK': 131 return await setNetwork(payload.network); 132 133 // --- Beacon protocol --- 134 case 'BEACON_MESSAGE': 135 return await handleBeaconMessage(payload, sender); 136 137 case 'BEACON_CONFIRM': 138 return await handleBeaconConfirm(payload); 139 140 case 'BEACON_GET_PENDING': 141 return await handleBeaconGetPending(payload); 142 143 default: 144 return { error: 'Unknown message type' }; 145 } 146 } catch (error) { 147 console.error('Message handler error:', error); 148 return { error: error.message }; 149 } 150} 151 152// ============================================================ 153// Beacon Protocol 154// ============================================================ 155 156async function handleBeaconMessage(msgFromContent, sender) { 157 // msgFromContent is the raw postMessage data from the page 158 // It has: { target: "toExtension", payload: ..., encryptedPayload?: ..., targetId?: ... } 159 const data = msgFromContent; 160 161 // --- Pairing request (unencrypted, has publicKey + name + type fields in payload) --- 162 if ( 163 data.payload && 164 typeof data.payload === 'object' && 165 data.payload.publicKey && 166 data.payload.name 167 ) { 168 return await handleBeaconPairing(data.payload, sender); 169 } 170 171 // --- Encrypted message --- 172 if (data.encryptedPayload) { 173 return await handleBeaconEncryptedMessage( 174 data.encryptedPayload, 175 sender, 176 ); 177 } 178 179 // Unknown Beacon message shape — ignore 180 return null; 181} 182 183async function handleBeaconPairing(pairingRequest, sender) { 184 // pairingRequest: { id, name, publicKey (hex), version, icon?, appUrl?, type? } 185 const beaconKp = await getOrCreateBeaconKeypair(); 186 const ourPublicKeyHex = bytesToHex(beaconKp.publicKey); 187 const ourSenderId = await getSenderId(beaconKp.publicKey); 188 189 // Store dApp as a peer 190 const peerPublicKeyBytes = hexToBytes(pairingRequest.publicKey); 191 const peerSenderId = await getSenderId(peerPublicKeyBytes); 192 193 const peerData = { 194 name: pairingRequest.name, 195 publicKey: pairingRequest.publicKey, 196 senderId: peerSenderId, 197 version: pairingRequest.version || '2', 198 icon: pairingRequest.icon || null, 199 appUrl: pairingRequest.appUrl || null, 200 connectedAt: Date.now(), 201 }; 202 203 // Save peer 204 const stored = await chrome.storage.local.get('beacon_peers'); 205 const peers = stored.beacon_peers || {}; 206 peers[peerSenderId] = peerData; 207 await chrome.storage.local.set({ beacon_peers: peers }); 208 209 // Build pairing response 210 const pairingResponse = { 211 target: 'toPage', 212 payload: { 213 type: 'postmessage-pairing-response', 214 id: pairingRequest.id, 215 name: 'Keeps Wallet', 216 publicKey: ourPublicKeyHex, 217 version: pairingRequest.version || '2', 218 extensionId: 'keeps-wallet', 219 senderId: ourSenderId, 220 }, 221 }; 222 223 return { postToPage: pairingResponse }; 224} 225 226async function handleBeaconEncryptedMessage(encryptedHex, sender) { 227 const beaconKp = await getOrCreateBeaconKeypair(); 228 const ourSenderId = await getSenderId(beaconKp.publicKey); 229 230 // We need to find which peer sent this. Try all known peers. 231 const stored = await chrome.storage.local.get('beacon_peers'); 232 const peers = stored.beacon_peers || {}; 233 234 let decryptedJson = null; 235 let matchedPeerSenderId = null; 236 let matchedPeerPublicKey = null; 237 238 for (const [peerId, peer] of Object.entries(peers)) { 239 try { 240 const peerPkBytes = hexToBytes(peer.publicKey); 241 const plaintext = await decryptBeaconMessage( 242 encryptedHex, 243 peerPkBytes, 244 beaconKp.secretKey, 245 ); 246 decryptedJson = plaintext; 247 matchedPeerSenderId = peerId; 248 matchedPeerPublicKey = peer.publicKey; 249 break; 250 } catch { 251 // Wrong peer, try next 252 } 253 } 254 255 if (!decryptedJson) { 256 console.warn('Beacon: could not decrypt message from any known peer'); 257 return null; 258 } 259 260 let beaconMsg; 261 try { 262 beaconMsg = JSON.parse(decryptedJson); 263 } catch { 264 console.error('Beacon: decrypted payload is not valid JSON'); 265 return null; 266 } 267 268 const peerPkBytes = hexToBytes(matchedPeerPublicKey); 269 270 // Route based on Beacon message type 271 switch (beaconMsg.type) { 272 case 'permission_request': 273 return await handleBeaconPermissionRequest( 274 beaconMsg, 275 peerPkBytes, 276 matchedPeerSenderId, 277 beaconKp, 278 ourSenderId, 279 sender, 280 ); 281 282 case 'operation_request': 283 return await handleBeaconOperationRequest( 284 beaconMsg, 285 peerPkBytes, 286 matchedPeerSenderId, 287 beaconKp, 288 ourSenderId, 289 sender, 290 ); 291 292 case 'sign_payload_request': 293 return await handleBeaconSignPayloadRequest( 294 beaconMsg, 295 peerPkBytes, 296 matchedPeerSenderId, 297 beaconKp, 298 ourSenderId, 299 sender, 300 ); 301 302 case 'disconnect': 303 return await handleBeaconDisconnect(matchedPeerSenderId); 304 305 default: 306 console.warn('Beacon: unhandled message type', beaconMsg.type); 307 return null; 308 } 309} 310 311// --- Send an encrypted Beacon response back to the page via content script --- 312 313async function sendBeaconResponseToTab(tabId, responseObj, peerPkBytes, beaconKp) { 314 const responseJson = JSON.stringify(responseObj); 315 const encryptedHex = await encryptBeaconMessage( 316 responseJson, 317 peerPkBytes, 318 beaconKp.secretKey, 319 ); 320 321 chrome.tabs.sendMessage(tabId, { 322 type: 'BEACON_RESPONSE', 323 data: { 324 target: 'toPage', 325 encryptedPayload: encryptedHex, 326 }, 327 }); 328} 329 330// --- Send an unencrypted acknowledge immediately --- 331 332async function sendBeaconAcknowledge(tabId, requestId, ourSenderId) { 333 // Acknowledge is sent unencrypted as a postMessage 334 // Actually, per Beacon protocol, acknowledge is encrypted too. 335 // But for simplicity and compatibility, we send it encrypted. 336 // We'll handle it in the request handlers. 337} 338 339// --- Build a Beacon error response --- 340 341function buildBeaconError(requestId, errorType, ourSenderId) { 342 return { 343 type: 'error', 344 id: requestId, 345 version: '2', 346 senderId: ourSenderId, 347 errorType: errorType, 348 }; 349} 350 351// --- Permission Request --- 352 353async function handleBeaconPermissionRequest( 354 request, 355 peerPkBytes, 356 peerSenderId, 357 beaconKp, 358 ourSenderId, 359 sender, 360) { 361 const tabId = sender.tab?.id; 362 363 // Send encrypted acknowledge 364 if (tabId) { 365 const ack = { 366 type: 'acknowledge', 367 id: request.id, 368 version: '2', 369 senderId: ourSenderId, 370 }; 371 await sendBeaconResponseToTab(tabId, ack, peerPkBytes, beaconKp); 372 } 373 374 // Check if wallet is unlocked 375 if (!unlockedWallet) { 376 if (tabId) { 377 const err = buildBeaconError(request.id, 'NOT_GRANTED_ERROR', ourSenderId); 378 await sendBeaconResponseToTab(tabId, err, peerPkBytes, beaconKp); 379 } 380 return null; 381 } 382 383 // Store pending request and open confirmation popup 384 const requestId = request.id; 385 const stored = await chrome.storage.local.get('beacon_peers'); 386 const peers = stored.beacon_peers || {}; 387 const peerInfo = peers[peerSenderId] || {}; 388 389 return new Promise((resolve) => { 390 pendingBeaconRequests.set(requestId, { 391 requestType: 'permission_request', 392 request, 393 peerPublicKey: bytesToHex(peerPkBytes), 394 peerSenderId, 395 peerName: peerInfo.name || 'Unknown dApp', 396 peerIcon: peerInfo.icon || null, 397 beaconKpPublicKey: bytesToHex(beaconKp.publicKey), 398 tabId, 399 ourSenderId, 400 resolve, 401 }); 402 403 // Open confirmation popup 404 chrome.windows.create({ 405 url: chrome.runtime.getURL( 406 `confirm.html?requestId=${encodeURIComponent(requestId)}`, 407 ), 408 type: 'popup', 409 width: 400, 410 height: 500, 411 focused: true, 412 }); 413 }); 414} 415 416// --- Operation Request --- 417 418async function handleBeaconOperationRequest( 419 request, 420 peerPkBytes, 421 peerSenderId, 422 beaconKp, 423 ourSenderId, 424 sender, 425) { 426 const tabId = sender.tab?.id; 427 428 // Send encrypted acknowledge 429 if (tabId) { 430 const ack = { 431 type: 'acknowledge', 432 id: request.id, 433 version: '2', 434 senderId: ourSenderId, 435 }; 436 await sendBeaconResponseToTab(tabId, ack, peerPkBytes, beaconKp); 437 } 438 439 // Check if wallet is unlocked 440 if (!unlockedWallet) { 441 if (tabId) { 442 const err = buildBeaconError(request.id, 'NOT_GRANTED_ERROR', ourSenderId); 443 await sendBeaconResponseToTab(tabId, err, peerPkBytes, beaconKp); 444 } 445 return null; 446 } 447 448 const requestId = request.id; 449 const stored = await chrome.storage.local.get('beacon_peers'); 450 const peers = stored.beacon_peers || {}; 451 const peerInfo = peers[peerSenderId] || {}; 452 453 return new Promise((resolve) => { 454 pendingBeaconRequests.set(requestId, { 455 requestType: 'operation_request', 456 request, 457 peerPublicKey: bytesToHex(peerPkBytes), 458 peerSenderId, 459 peerName: peerInfo.name || 'Unknown dApp', 460 peerIcon: peerInfo.icon || null, 461 beaconKpPublicKey: bytesToHex(beaconKp.publicKey), 462 tabId, 463 ourSenderId, 464 resolve, 465 }); 466 467 chrome.windows.create({ 468 url: chrome.runtime.getURL( 469 `confirm.html?requestId=${encodeURIComponent(requestId)}`, 470 ), 471 type: 'popup', 472 width: 400, 473 height: 600, 474 focused: true, 475 }); 476 }); 477} 478 479// --- Sign Payload Request --- 480 481async function handleBeaconSignPayloadRequest( 482 request, 483 peerPkBytes, 484 peerSenderId, 485 beaconKp, 486 ourSenderId, 487 sender, 488) { 489 const tabId = sender.tab?.id; 490 491 // Send encrypted acknowledge 492 if (tabId) { 493 const ack = { 494 type: 'acknowledge', 495 id: request.id, 496 version: '2', 497 senderId: ourSenderId, 498 }; 499 await sendBeaconResponseToTab(tabId, ack, peerPkBytes, beaconKp); 500 } 501 502 // Check if wallet is unlocked 503 if (!unlockedWallet) { 504 if (tabId) { 505 const err = buildBeaconError(request.id, 'NOT_GRANTED_ERROR', ourSenderId); 506 await sendBeaconResponseToTab(tabId, err, peerPkBytes, beaconKp); 507 } 508 return null; 509 } 510 511 const requestId = request.id; 512 const stored = await chrome.storage.local.get('beacon_peers'); 513 const peers = stored.beacon_peers || {}; 514 const peerInfo = peers[peerSenderId] || {}; 515 516 return new Promise((resolve) => { 517 pendingBeaconRequests.set(requestId, { 518 requestType: 'sign_payload_request', 519 request, 520 peerPublicKey: bytesToHex(peerPkBytes), 521 peerSenderId, 522 peerName: peerInfo.name || 'Unknown dApp', 523 peerIcon: peerInfo.icon || null, 524 beaconKpPublicKey: bytesToHex(beaconKp.publicKey), 525 tabId, 526 ourSenderId, 527 resolve, 528 }); 529 530 chrome.windows.create({ 531 url: chrome.runtime.getURL( 532 `confirm.html?requestId=${encodeURIComponent(requestId)}`, 533 ), 534 type: 'popup', 535 width: 400, 536 height: 500, 537 focused: true, 538 }); 539 }); 540} 541 542// --- Disconnect --- 543 544async function handleBeaconDisconnect(peerSenderId) { 545 const stored = await chrome.storage.local.get('beacon_peers'); 546 const peers = stored.beacon_peers || {}; 547 delete peers[peerSenderId]; 548 await chrome.storage.local.set({ beacon_peers: peers }); 549 return null; 550} 551 552// --- Get pending request details (for confirm.html) --- 553 554async function handleBeaconGetPending(payload) { 555 const { requestId } = payload; 556 const pending = pendingBeaconRequests.get(requestId); 557 if (!pending) { 558 return { error: 'No pending request found' }; 559 } 560 561 return { 562 requestType: pending.requestType, 563 peerName: pending.peerName, 564 peerIcon: pending.peerIcon, 565 request: pending.request, 566 walletAddress: unlockedWallet?.address || null, 567 }; 568} 569 570// --- Handle confirmation from confirm.html --- 571 572async function handleBeaconConfirm(payload) { 573 const { requestId, approved } = payload; 574 const pending = pendingBeaconRequests.get(requestId); 575 if (!pending) { 576 return { error: 'No pending request found' }; 577 } 578 579 pendingBeaconRequests.delete(requestId); 580 581 const beaconKp = await getOrCreateBeaconKeypair(); 582 const peerPkBytes = hexToBytes(pending.peerPublicKey); 583 584 if (!approved) { 585 // Send error response: user rejected 586 if (pending.tabId) { 587 const err = buildBeaconError( 588 pending.request.id, 589 'NOT_GRANTED_ERROR', 590 pending.ourSenderId, 591 ); 592 await sendBeaconResponseToTab( 593 pending.tabId, 594 err, 595 peerPkBytes, 596 beaconKp, 597 ); 598 } 599 if (pending.resolve) pending.resolve(null); 600 return { success: true }; 601 } 602 603 // --- Approved: build and send appropriate response --- 604 let response; 605 606 switch (pending.requestType) { 607 case 'permission_request': 608 response = await buildPermissionResponse(pending); 609 break; 610 case 'operation_request': 611 response = await buildOperationResponse(pending); 612 break; 613 case 'sign_payload_request': 614 response = await buildSignPayloadResponse(pending); 615 break; 616 default: 617 if (pending.resolve) pending.resolve(null); 618 return { error: 'Unknown request type' }; 619 } 620 621 // If the response is an error (e.g., operation failed), send as error 622 if (response && response.type === 'error') { 623 if (pending.tabId) { 624 await sendBeaconResponseToTab( 625 pending.tabId, 626 response, 627 peerPkBytes, 628 beaconKp, 629 ); 630 } 631 if (pending.resolve) pending.resolve(null); 632 return { success: true }; 633 } 634 635 // Send encrypted response back to the dApp 636 if (pending.tabId && response) { 637 await sendBeaconResponseToTab( 638 pending.tabId, 639 response, 640 peerPkBytes, 641 beaconKp, 642 ); 643 } 644 645 if (pending.resolve) pending.resolve(null); 646 return { success: true }; 647} 648 649// --- Build permission response --- 650 651async function buildPermissionResponse(pending) { 652 if (!unlockedWallet) { 653 return buildBeaconError( 654 pending.request.id, 655 'NOT_GRANTED_ERROR', 656 pending.ourSenderId, 657 ); 658 } 659 660 const storedState = await chrome.storage.local.get('network'); 661 const network = storedState.network || 'ghostnet'; 662 663 return { 664 type: 'permission_response', 665 id: pending.request.id, 666 publicKey: unlockedWallet.publicKey, // edpk... base58 667 network: { type: network }, 668 scopes: ['sign', 'operation_request'], 669 appMetadata: { 670 senderId: pending.peerSenderId, 671 name: pending.peerName, 672 }, 673 version: '2', 674 senderId: pending.ourSenderId, 675 }; 676} 677 678// --- Build operation response --- 679 680async function buildOperationResponse(pending) { 681 if (!unlockedWallet) { 682 return buildBeaconError( 683 pending.request.id, 684 'NOT_GRANTED_ERROR', 685 pending.ourSenderId, 686 ); 687 } 688 689 try { 690 const request = pending.request; 691 const storedState = await chrome.storage.local.get('network'); 692 const network = storedState.network || 'ghostnet'; 693 694 // Get the RPC node URL 695 const rpcUrl = 696 network === 'mainnet' 697 ? 'https://mainnet.api.tez.ie' 698 : 'https://ghostnet.teztnets.com'; 699 700 // Forge the operations via RPC 701 const operationContents = request.operationDetails; 702 const sourceAddress = unlockedWallet.address; 703 704 // Get counter and branch 705 const [headRes, counterRes] = await Promise.all([ 706 fetch(`${rpcUrl}/chains/main/blocks/head/header`), 707 fetch( 708 `${rpcUrl}/chains/main/blocks/head/context/contracts/${sourceAddress}/counter`, 709 ), 710 ]); 711 712 const head = await headRes.json(); 713 const currentCounter = parseInt(await counterRes.json()); 714 const branch = head.hash; 715 716 // Build the operations array with proper counter, source, etc. 717 let counter = currentCounter; 718 const contents = operationContents.map((op) => { 719 counter++; 720 const content = { 721 kind: op.kind, 722 source: sourceAddress, 723 counter: String(counter), 724 fee: op.fee || '0', 725 gas_limit: op.gas_limit || '10600', 726 storage_limit: op.storage_limit || '300', 727 }; 728 729 if (op.kind === 'transaction') { 730 content.destination = op.destination; 731 content.amount = op.amount || '0'; 732 if (op.parameters) { 733 content.parameters = op.parameters; 734 } 735 } else if (op.kind === 'origination') { 736 content.balance = op.balance || '0'; 737 content.script = op.script; 738 } else if (op.kind === 'delegation') { 739 if (op.delegate) content.delegate = op.delegate; 740 } 741 742 return content; 743 }); 744 745 // Forge via RPC 746 const forgeRes = await fetch( 747 `${rpcUrl}/chains/main/blocks/head/helpers/forge/operations`, 748 { 749 method: 'POST', 750 headers: { 'Content-Type': 'application/json' }, 751 body: JSON.stringify({ branch, contents }), 752 }, 753 ); 754 755 const forgedBytes = (await forgeRes.json()).replace(/^"|"$/g, ''); 756 757 // Sign the forged operation 758 const signature = await cryptoSignOperation( 759 forgedBytes, 760 unlockedWallet.secretKeyBytes, 761 ); 762 763 // Inject the signed operation 764 const signedOp = signature.signedBytes; 765 const injectRes = await fetch( 766 `${rpcUrl}/injection/operation`, 767 { 768 method: 'POST', 769 headers: { 'Content-Type': 'application/json' }, 770 body: JSON.stringify(signedOp), 771 }, 772 ); 773 774 const opHash = (await injectRes.json()).replace(/^"|"$/g, ''); 775 776 resetLockTimeout(); 777 778 return { 779 type: 'operation_response', 780 id: pending.request.id, 781 transactionHash: opHash, 782 version: '2', 783 senderId: pending.ourSenderId, 784 }; 785 } catch (error) { 786 console.error('Beacon operation error:', error); 787 return { 788 type: 'error', 789 id: pending.request.id, 790 version: '2', 791 senderId: pending.ourSenderId, 792 errorType: 'TRANSACTION_INVALID_ERROR', 793 errorData: [{ kind: 'temporary', id: error.message }], 794 }; 795 } 796} 797 798// --- Build sign payload response --- 799 800async function buildSignPayloadResponse(pending) { 801 if (!unlockedWallet) { 802 return buildBeaconError( 803 pending.request.id, 804 'NOT_GRANTED_ERROR', 805 pending.ourSenderId, 806 ); 807 } 808 809 try { 810 const request = pending.request; 811 const payloadToSign = request.payload; 812 813 // Sign the payload (it comes as a hex string from the dApp) 814 const signature = await cryptoSign( 815 payloadToSign, 816 unlockedWallet.secretKeyBytes, 817 ); 818 819 resetLockTimeout(); 820 821 return { 822 type: 'sign_payload_response', 823 id: pending.request.id, 824 signingType: request.signingType || 'raw', 825 signature: signature.edsig, 826 version: '2', 827 senderId: pending.ourSenderId, 828 }; 829 } catch (error) { 830 console.error('Beacon sign payload error:', error); 831 return { 832 type: 'error', 833 id: pending.request.id, 834 version: '2', 835 senderId: pending.ourSenderId, 836 errorType: 'SIGNATURE_TYPE_NOT_SUPPORTED', 837 }; 838 } 839} 840 841// ============================================================ 842// Existing Wallet Operations (unchanged) 843// ============================================================ 844 845// Wallet State 846async function getWalletState() { 847 const stored = await chrome.storage.local.get([ 848 'encryptedSeed', 849 'address', 850 'publicKey', 851 'network', 852 ]); 853 return { 854 exists: !!stored.encryptedSeed, 855 unlocked: !!unlockedWallet, 856 address: stored.address || null, 857 publicKey: stored.publicKey || null, 858 network: stored.network || 'ghostnet', 859 }; 860} 861 862// Create new wallet 863async function createWallet(password) { 864 if (!password || password.length < 8) { 865 return { error: 'Password must be at least 8 characters' }; 866 } 867 868 await initCrypto(); 869 870 const mnemonic = generateMnemonic(); 871 const keypair = await deriveKeypair(mnemonic); 872 const encryptedSeed = await encrypt(mnemonic, password); 873 874 await chrome.storage.local.set({ 875 encryptedSeed, 876 address: keypair.address, 877 publicKey: keypair.publicKey, 878 network: 'ghostnet', 879 }); 880 881 unlockedWallet = { 882 address: keypair.address, 883 publicKey: keypair.publicKey, 884 secretKeyBytes: keypair.secretKeyBytes, 885 }; 886 resetLockTimeout(); 887 888 return { 889 success: true, 890 mnemonic, 891 address: keypair.address, 892 }; 893} 894 895// Import existing wallet 896async function importWallet(mnemonic, password) { 897 if (!password || password.length < 8) { 898 return { error: 'Password must be at least 8 characters' }; 899 } 900 901 const cleanMnemonic = mnemonic.trim().toLowerCase().replace(/\s+/g, ' '); 902 if (!validateMnemonic(cleanMnemonic)) { 903 return { error: 'Invalid seed phrase' }; 904 } 905 906 await initCrypto(); 907 908 const keypair = await deriveKeypair(cleanMnemonic); 909 const encryptedSeed = await encrypt(cleanMnemonic, password); 910 911 await chrome.storage.local.set({ 912 encryptedSeed, 913 address: keypair.address, 914 publicKey: keypair.publicKey, 915 network: 'ghostnet', 916 }); 917 918 unlockedWallet = { 919 address: keypair.address, 920 publicKey: keypair.publicKey, 921 secretKeyBytes: keypair.secretKeyBytes, 922 }; 923 resetLockTimeout(); 924 925 return { 926 success: true, 927 address: keypair.address, 928 }; 929} 930 931// Import wallet from private key 932async function importPrivateKey(privateKey, password) { 933 if (!password || password.length < 8) { 934 return { error: 'Password must be at least 8 characters' }; 935 } 936 937 if (!privateKey || !privateKey.startsWith('edsk')) { 938 return { error: 'Invalid private key format' }; 939 } 940 941 try { 942 await initCrypto(); 943 944 const keypair = await importFromPrivateKey(privateKey.trim()); 945 const encryptedSeed = await encrypt(privateKey.trim(), password); 946 947 await chrome.storage.local.set({ 948 encryptedSeed, 949 importType: 'privateKey', 950 address: keypair.address, 951 publicKey: keypair.publicKey, 952 network: 'ghostnet', 953 }); 954 955 unlockedWallet = { 956 address: keypair.address, 957 publicKey: keypair.publicKey, 958 secretKeyBytes: keypair.secretKeyBytes, 959 }; 960 resetLockTimeout(); 961 962 return { 963 success: true, 964 address: keypair.address, 965 }; 966 } catch (error) { 967 console.error('Import private key error:', error); 968 return { error: error.message || 'Failed to import private key' }; 969 } 970} 971 972// Unlock wallet 973async function unlockWallet(password) { 974 const { encryptedSeed, importType, address, publicKey } = 975 await chrome.storage.local.get([ 976 'encryptedSeed', 977 'importType', 978 'address', 979 'publicKey', 980 ]); 981 982 if (!encryptedSeed) { 983 return { error: 'No wallet found' }; 984 } 985 986 try { 987 await initCrypto(); 988 989 const decrypted = await decrypt(encryptedSeed, password); 990 991 let keypair; 992 if (importType === 'privateKey') { 993 keypair = await importFromPrivateKey(decrypted); 994 } else { 995 keypair = await deriveKeypair(decrypted); 996 } 997 998 if (keypair.address !== address) { 999 return { error: 'Key derivation mismatch' }; 1000 } 1001 1002 unlockedWallet = { 1003 address: keypair.address, 1004 publicKey: keypair.publicKey, 1005 secretKeyBytes: keypair.secretKeyBytes, 1006 }; 1007 1008 clearSensitiveData(decrypted); 1009 resetLockTimeout(); 1010 1011 return { success: true, address }; 1012 } catch (error) { 1013 return { error: 'Invalid password' }; 1014 } 1015} 1016 1017// Lock wallet 1018function lockWallet() { 1019 if (unlockedWallet?.secretKeyBytes) { 1020 clearSensitiveData(unlockedWallet.secretKeyBytes); 1021 } 1022 unlockedWallet = null; 1023 1024 if (lockTimeout) { 1025 clearTimeout(lockTimeout); 1026 lockTimeout = null; 1027 } 1028 1029 // Notify popup and content scripts 1030 chrome.runtime.sendMessage({ type: 'KEEPS_LOCKED' }).catch(() => {}); 1031 1032 return { success: true }; 1033} 1034 1035// Reset auto-lock timeout 1036function resetLockTimeout() { 1037 if (lockTimeout) clearTimeout(lockTimeout); 1038 lockTimeout = setTimeout(() => { 1039 lockWallet(); 1040 }, LOCK_TIMEOUT_MS); 1041} 1042 1043// Get address 1044function getAddress() { 1045 return { address: unlockedWallet?.address || null }; 1046} 1047 1048// Set network 1049async function setNetwork(network) { 1050 if (network !== 'mainnet' && network !== 'ghostnet') { 1051 return { error: 'Invalid network' }; 1052 } 1053 await chrome.storage.local.set({ network }); 1054 return { success: true, network }; 1055} 1056 1057// Handle signing request (with confirmation) 1058async function handleSignRequest(operation, sender) { 1059 if (!unlockedWallet) { 1060 return { error: 'Wallet is locked' }; 1061 } 1062 1063 console.log('Signing operation:', operation); 1064 1065 try { 1066 const signature = await cryptoSignOperation( 1067 operation.forgedBytes, 1068 unlockedWallet.secretKeyBytes, 1069 ); 1070 resetLockTimeout(); 1071 return { 1072 success: true, 1073 signature: signature.edsig, 1074 signedBytes: signature.signedBytes, 1075 }; 1076 } catch (error) { 1077 return { error: 'Signing failed: ' + error.message }; 1078 } 1079} 1080 1081// Get balance from TzKT 1082async function getBalance(network) { 1083 const state = await getWalletState(); 1084 const address = state.address; 1085 if (!address) return { balance: null }; 1086 1087 const net = network || state.network || 'ghostnet'; 1088 const apiBase = 1089 net === 'mainnet' 1090 ? 'https://api.tzkt.io' 1091 : 'https://api.ghostnet.tzkt.io'; 1092 1093 try { 1094 const res = await fetch(`${apiBase}/v1/accounts/${address}/balance`); 1095 const mutez = await res.json(); 1096 return { balance: mutez / 1000000 }; 1097 } catch (error) { 1098 return { error: error.message }; 1099 } 1100} 1101 1102// Get keeps (NFTs) from TzKT 1103async function getKeeps(network) { 1104 const state = await getWalletState(); 1105 const address = state.address; 1106 if (!address) return { keeps: [] }; 1107 1108 const net = network || state.network || 'ghostnet'; 1109 const apiBase = 1110 net === 'mainnet' 1111 ? 'https://api.tzkt.io' 1112 : 'https://api.ghostnet.tzkt.io'; 1113 1114 try { 1115 const res = await fetch( 1116 `${apiBase}/v1/tokens/balances?account=${address}&balance.gt=0&token.standard=fa2&limit=100`, 1117 ); 1118 const tokens = await res.json(); 1119 1120 const keeps = tokens.map((t) => ({ 1121 id: t.token.tokenId, 1122 contract: t.token.contract.address, 1123 balance: parseInt(t.balance), 1124 name: t.token.metadata?.name, 1125 description: t.token.metadata?.description, 1126 thumbnailUri: t.token.metadata?.thumbnailUri, 1127 displayUri: t.token.metadata?.displayUri, 1128 artifactUri: t.token.metadata?.artifactUri, 1129 })); 1130 1131 return { keeps }; 1132 } catch (error) { 1133 return { error: error.message, keeps: [] }; 1134 } 1135} 1136 1137console.log('Keeps Wallet background service worker loaded');