forked from pepae/ShutterAPIHongbao
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathapp.js
1257 lines (1051 loc) · 46.8 KB
/
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import { registerPasskey } from "./wallet.js";
import { authenticateWallet } from "./wallet.js";
const GNOSIS_CHAIN_PARAMS = {
chainId: '0x64', // Chain ID 100 in hexadecimal
chainName: 'Gnosis Chain',
rpcUrls: ['https://rpc.gnosis.gateway.fm'],
nativeCurrency: {
name: 'xDai',
symbol: 'XDAI',
decimals: 18,
},
blockExplorerUrls: ['https://gnosisscan.io/'],
};
const fallbackWeb3 = new Web3(GNOSIS_CHAIN_PARAMS.rpcUrls[0]);
if (typeof window.ethereum === 'undefined') {
console.warn('MetaMask is not available. Using fallback provider for redemption.');
}
const web3 = new Web3(window.ethereum);
async function ensureGnosisChain() {
if (typeof window.ethereum === 'undefined') {
throw new Error('MetaMask is required to create a Hongbao.');
}
try {
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
if (chainId !== GNOSIS_CHAIN_PARAMS.chainId) {
try {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: GNOSIS_CHAIN_PARAMS.chainId }],
});
} catch (switchError) {
if (switchError.code === 4902) {
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [GNOSIS_CHAIN_PARAMS],
});
} else {
throw switchError;
}
}
}
} catch (error) {
console.error('Failed to switch to Gnosis Chain:', error);
throw error;
}
}
// Connect MetaMask wallet
async function connectMetaMask() {
try {
await ensureGnosisChain();
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
console.log('MetaMask connected:', accounts[0]);
return accounts[0];
} catch (error) {
console.error('MetaMask connection failed:', error);
alert('Failed to connect MetaMask. Please try again.');
throw error;
}
}
// Copy to clipboard functionality
function copyToClipboard(text) {
const tempInput = document.createElement('textarea');
tempInput.style.position = 'absolute';
tempInput.style.left = '-9999px';
tempInput.value = text;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand('copy');
document.body.removeChild(tempInput);
alert('Message copied to clipboard!');
}
document.getElementById('hongbao-link').addEventListener('click', (event) => {
const linkElement = event.target.closest('#hongbao-link');
if (linkElement) {
const link = linkElement.querySelector('a')?.href;
if (link) {
const message = `
Here's a threshold encrypted Hongbao with some xDAI on Gnosis Chain! Open this link in a real browser:
${link}
`;
copyToClipboard(message.trim());
} else {
alert('No link found to copy.');
}
}
});
// AES Encryption using Web Crypto API
async function encryptWithPassword(data, password) {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(password),
"PBKDF2",
false,
["deriveKey"]
);
const saltSeed = new Uint8Array(16);
crypto.getRandomValues(saltSeed);
const salt = "shutter_hongbao_" + btoa(String.fromCharCode(...saltSeed));
const derivedKey = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: encoder.encode(salt),
iterations: 100000,
hash: "SHA-256",
},
key,
{ name: "AES-GCM", length: 256 },
false,
["encrypt"]
);
const iv = new Uint8Array(12);
crypto.getRandomValues(iv);
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
derivedKey,
encoder.encode(data)
);
return {
encrypted: btoa(String.fromCharCode(...new Uint8Array(encrypted))),
iv: btoa(String.fromCharCode(...iv)),
salt: salt,
};
}
// AES Decryption using Web Crypto API
async function decryptWithPassword(encryptedData, password, iv, salt) {
const decoder = new TextDecoder();
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(password),
"PBKDF2",
false,
["deriveKey"]
);
const derivedKey = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: encoder.encode(salt),
iterations: 100000,
hash: "SHA-256",
},
key,
{ name: "AES-GCM", length: 256 },
false,
["decrypt"]
);
const decrypted = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: Uint8Array.from(atob(iv), (c) => c.charCodeAt(0)) },
derivedKey,
Uint8Array.from(atob(encryptedData), (c) => c.charCodeAt(0))
);
return decoder.decode(decrypted);
}
function calculateReleaseTimestamp() {
const unlockTimeSelect = document.getElementById("unlock-time");
const selectedOption = unlockTimeSelect.value;
if (selectedOption === "custom") {
const customTimestampInput = document.getElementById("custom-timestamp").value;
if (!customTimestampInput) {
alert("Please select a valid custom timestamp.");
throw new Error("Invalid custom timestamp.");
}
return Math.floor(new Date(customTimestampInput).getTime() / 1000);
}
if (selectedOption === "lunar-new-year") {
// Lunar New Year timestamp for 2025 in UTC
return Math.floor(new Date("2025-01-29T12:36:00Z").getTime() / 1000);
}
// Predefined time options in seconds
return Math.floor(Date.now() / 1000) + parseInt(selectedOption, 10);
}
async function sendHongbao(amount) {
try {
const senderAccount = await connectMetaMask();
const releaseTimestamp = calculateReleaseTimestamp();
const newAccount = web3.eth.accounts.create();
const privateKey = newAccount.privateKey;
const recipientAddress = newAccount.address;
const password = document.getElementById("hongbao-password").value.trim();
const detailsElement = document.getElementById("hongbao-details");
const linkElement = document.getElementById("hongbao-link");
const hongbaoVisual = document.getElementById("hongbao-visual");
detailsElement.textContent = "Requesting encryption key from Shutter...";
detailsElement.classList.remove("hidden");
// 1) Register or ensure your identity on Shutter
const identityPrefixHex = "0x" + crypto
.getRandomValues(new Uint8Array(32))
.reduce((acc, byte) => acc + byte.toString(16).padStart(2, "0"), "");
const registrationData = await registerShutterIdentity(releaseTimestamp, identityPrefixHex);
// Unwrap the real identity
const finalIdentity = registrationData.message.identity;
// 2) Get encryption data (eon_key, identity, etc.)
const encryptionData = await getShutterEncryptionData(senderAccount, identityPrefixHex);
// Unwrap the actual encryption fields
const actualEncryptionData = encryptionData.message;
// 3) Encrypt the privateKey with BLST
let shutterEncryptedKey = await shutterEncryptPrivateKey(privateKey, actualEncryptionData);
// 4) Optionally password-encrypt the BLST ciphertext
if (password) {
const passwordEncrypted = await encryptWithPassword(shutterEncryptedKey, password);
shutterEncryptedKey = JSON.stringify(passwordEncrypted);
}
// 5) Construct the link
const link = `${window.location.origin}/#redeem?key=${encodeURIComponent(
shutterEncryptedKey
)}×tamp=${releaseTimestamp}&amount=${amount}&protected=${!!password}&identity=${finalIdentity}`;
detailsElement.innerHTML = `
Identity registered successfully with Shutter!<br>
One-time-use private key was created and funded.<br>
Shutter Keypers provided the encryption key for the Hongbao.<br>
Funds are locked until: <strong>${new Date(releaseTimestamp * 1000).toLocaleString()}</strong>
`;
linkElement.innerHTML = `
<strong>Share this message (click/touch to copy to clipboard):</strong><br>
Someone gifted you a Hongbao with some xDAI on Gnosis Chain! This Hongbao is locked until ${new Date(releaseTimestamp * 1000).toLocaleString()}! It was encrypted with Shutter, and will unlock exactly on time thanks to encryption and decryption magic!<br><br>
Open this link in a real browser (not e.g., embedded webkit): <a href="${link}" target="_blank">${link}</a>
`;
linkElement.classList.remove("hidden");
// 6) Fund the ephemeral address
const hongbaoAmountWei = web3.utils.toWei(amount.toString(), "ether");
await web3.eth.sendTransaction({
from: senderAccount,
to: recipientAddress,
value: hongbaoAmountWei,
});
hongbaoVisual.classList.remove("hidden");
hongbaoVisual.classList.add("sealed");
alert("Hongbao created successfully! Share the link with the recipient.");
} catch (error) {
console.error("Error creating Hongbao:", error);
alert("Failed to create Hongbao.");
}
}
async function fundHongbaoWithPasskey(amount) {
try {
const wallet = await authenticateWallet(); // Passkey-based wallet
console.log("Passkey Wallet Address:", wallet.address);
const provider = new ethers.JsonRpcProvider(GNOSIS_CHAIN_PARAMS.rpcUrls[0]);
const walletWithProvider = wallet.connect(provider);
const releaseTimestamp = calculateReleaseTimestamp();
const newAccount = ethers.Wallet.createRandom();
const privateKey = newAccount.privateKey;
const recipientAddress = newAccount.address;
const password = document.getElementById("hongbao-password").value.trim();
const detailsElement = document.getElementById("hongbao-details");
const linkElement = document.getElementById("hongbao-link");
const hongbaoVisual = document.getElementById("hongbao-visual");
detailsElement.textContent = "Requesting encryption key from Shutter...";
detailsElement.classList.remove("hidden");
// 1) Register identity with Shutter
const identityPrefixHex = "0x" + crypto
.getRandomValues(new Uint8Array(32))
.reduce((acc, byte) => acc + byte.toString(16).padStart(2, "0"), "");
const registrationData = await registerShutterIdentity(releaseTimestamp, identityPrefixHex);
const finalIdentity = registrationData.message.identity;
// 2) Get encryption data
const encryptionData = await getShutterEncryptionData(wallet.address, identityPrefixHex);
const actualEncryptionData = encryptionData.message;
// 3) Encrypt privateKey with BLST
let shutterEncryptedKey = await shutterEncryptPrivateKey(privateKey, actualEncryptionData);
// 4) Optionally AES-encrypt with user password
if (password) {
const passwordEncrypted = await encryptWithPassword(shutterEncryptedKey, password);
shutterEncryptedKey = JSON.stringify(passwordEncrypted);
}
const link = `${window.location.origin}/#redeem?key=${encodeURIComponent(
shutterEncryptedKey
)}×tamp=${releaseTimestamp}&amount=${amount}&protected=${!!password}&identity=${finalIdentity}`;
detailsElement.innerHTML = `
Identity registered successfully with Shutter!<br>
One-time-use private key was created and funded.<br>
Shutter Keypers provided the encryption key for the Hongbao.<br>
Funds are locked until: <strong>${new Date(releaseTimestamp * 1000).toLocaleString()}</strong>
`;
linkElement.innerHTML = `
<strong>Share this message (click/touch to copy to clipboard):</strong><br>
Someone gifted you a Hongbao with some xDAI on Gnosis Chain! This Hongbao is locked until ${new Date(releaseTimestamp * 1000).toLocaleString()}! It was encrypted with Shutter, and will unlock exactly on time thanks to encryption and decryption magic!<br><br>
Open this link in a real browser (not e.g., embedded webkit): <a href="${link}" target="_blank">${link}</a>
`;
linkElement.classList.remove("hidden");
// 5) Estimate gas and send transaction
const hongbaoAmountWei = ethers.parseEther(amount.toString());
const gasPrice = await provider.send("eth_gasPrice", []);
const gasLimitEstimate = await provider.estimateGas({
from: wallet.address,
to: recipientAddress,
value: hongbaoAmountWei,
});
const gasLimit = BigInt(gasLimitEstimate);
const gasCost = BigInt(gasPrice) * gasLimit;
const walletBalance = BigInt(await provider.getBalance(wallet.address));
if (walletBalance < hongbaoAmountWei + gasCost) {
const formattedGasCost = ethers.formatEther(gasCost);
const formattedRequired = ethers.formatEther(hongbaoAmountWei + gasCost);
const formattedBalance = ethers.formatEther(walletBalance);
alert(`Insufficient funds to fund the Hongbao.
Required: ${formattedRequired} xDAI (includes ${formattedGasCost} xDAI for gas).
Available: ${formattedBalance} xDAI.`);
return;
}
const tx = await walletWithProvider.sendTransaction({
to: recipientAddress,
value: hongbaoAmountWei,
gasLimit: gasLimit,
gasPrice: BigInt(gasPrice),
});
console.log("Transaction sent:", tx.hash);
hongbaoVisual.classList.remove("hidden");
hongbaoVisual.classList.add("sealed");
alert("Hongbao funded successfully! Share the link with the recipient.");
} catch (error) {
console.error("Error funding Hongbao with Passkey Wallet:", error);
alert("Failed to fund Hongbao with Passkey Wallet.");
}
}async function redeemHongbaoAndSweep(encryptedKey, timestamp, amount) {
try {
await ensureGnosisChain();
const currentTime = Math.floor(Date.now() / 1000);
if (currentTime < timestamp) {
alert(`Hongbao is locked until ${new Date(timestamp * 1000).toLocaleString()}`);
return;
}
const detailsElement = document.getElementById("redemption-details");
const hongbaoVisual = document.getElementById("hongbao-visual-redeem");
const resultElement = document.getElementById("redeem-result");
detailsElement.innerHTML = "Checking for decrypted key or password protection...<br>";
detailsElement.classList.remove("hidden");
let decryptedKey;
// Step 1: Check for a previously decrypted key
const decryptedKeyField = document.getElementById("decrypted-hongbao-key");
if (decryptedKeyField && decryptedKeyField.value) {
decryptedKey = decryptedKeyField.value;
console.log("Using previously decrypted key from field.");
} else if (window.decryptedHongbaoKey) {
decryptedKey = window.decryptedHongbaoKey;
console.log("Using previously decrypted key from memory.");
} else {
console.log("No previously decrypted key found. Proceeding to decrypt.");
const isProtected = new URLSearchParams(window.location.search).get("protected") === "true";
if (isProtected) {
const password = document.getElementById("redeem-password").value.trim();
if (!password) {
alert("Password is required to decrypt this Hongbao.");
return;
}
try {
const encryptedObject = JSON.parse(encryptedKey);
decryptedKey = await decryptWithPassword(
encryptedObject.encrypted,
password,
encryptedObject.iv,
encryptedObject.salt,
);
} catch (error) {
alert("Invalid password. Unable to decrypt the Hongbao.");
return;
}
} else {
decryptedKey = encryptedKey; // Not password-protected
}
// Step 2: Check if still encrypted with Shutter
if (
decryptedKey.startsWith("0x03") && // BLST ciphertext
decryptedKey.length > 66
) {
const urlParams = new URLSearchParams(window.location.hash.split("?")[1]);
const identityParam = urlParams.get("identity");
if (!identityParam) {
alert("Missing Shutter identity. Cannot complete final decryption.");
return;
}
const finalKey = await getShutterDecryptionKey(identityParam);
// Perform Shutter decryption
decryptedKey = await shutterDecryptPrivateKey(decryptedKey, finalKey);
}
// Store the decrypted key for reuse
if (decryptedKeyField) {
decryptedKeyField.value = decryptedKey;
} else {
window.decryptedHongbaoKey = decryptedKey;
}
}
// Step 3: Validate the final key
if (!decryptedKey.startsWith("0x") || decryptedKey.length !== 66) {
throw new Error("Invalid private key after decryption.");
}
// Step 4: Use the fully decrypted key to sweep the funds
const hongbaoAccount = fallbackWeb3.eth.accounts.privateKeyToAccount(decryptedKey);
fallbackWeb3.eth.accounts.wallet.add(hongbaoAccount);
const balance = BigInt(await fallbackWeb3.eth.getBalance(hongbaoAccount.address));
if (balance === BigInt(0)) {
alert("No funds available to sweep.");
detailsElement.innerHTML += "No funds available to sweep.";
return;
}
const gasPrice = BigInt(await fallbackWeb3.eth.getGasPrice());
const gasLimit = BigInt(21000);
const gasCost = gasPrice * gasLimit;
if (balance <= gasCost) {
alert("Insufficient funds to cover gas fees.");
detailsElement.innerHTML += "Insufficient funds to cover gas fees.";
return;
}
const receiverAccount = await connectMetaMask();
const tx = {
from: hongbaoAccount.address,
to: receiverAccount,
value: (balance - gasCost).toString(),
gas: 21000,
gasPrice: gasPrice.toString(),
};
detailsElement.innerHTML += `
Amount gifted: <strong>${amount} XDAI</strong><br>
Signing transaction and sending funds...<br>
Pending transaction confirmation...<br>
`;
const signedTx = await hongbaoAccount.signTransaction(tx);
await fallbackWeb3.eth.sendSignedTransaction(signedTx.rawTransaction);
resultElement.innerHTML = `
Funds swept to your wallet: <strong>${receiverAccount}</strong><br>
<a href="wallet.html" target="_blank">Manage Wallet</a>
`;
resultElement.classList.remove("hidden");
hongbaoVisual.classList.add("opened");
detailsElement.innerHTML += "Transaction confirmed! Funds successfully transferred.";
alert(`Hongbao redeemed! Funds have been transferred to your wallet: ${receiverAccount}`);
} catch (error) {
console.error("Error redeeming and sweeping Hongbao:", error);
alert("Failed to redeem or sweep Hongbao.");
}
}
async function claimToNewWallet(encryptedKey, timestamp, amount) {
try {
const wallet = await registerPasskey("My New Hongbao Wallet"); // Create a new passkey wallet
console.log("New Passkey Wallet Address:", wallet.address);
// Proceed to redeem and sweep with the new wallet
await redeemHongbaoWithWallet(encryptedKey, timestamp, amount, wallet);
alert(`A new wallet was created successfully, and funds were claimed to: ${wallet.address}`);
} catch (error) {
console.error("Error claiming to a new wallet:", error);
alert("Failed to claim Hongbao to a new wallet.");
}
}
async function claimToExistingWallet(encryptedKey, timestamp, amount) {
try {
const wallet = await authenticateWallet(); // Authenticate an existing wallet
console.log("Existing Passkey Wallet Address:", wallet.address);
// Proceed to redeem and sweep with the existing wallet
await redeemHongbaoWithWallet(encryptedKey, timestamp, amount, wallet);
} catch (error) {
console.error("Error claiming to an existing wallet:", error);
alert("Failed to claim Hongbao to an existing wallet.");
}
}
async function redeemHongbaoWithWallet(encryptedKey, timestamp, amount, wallet) {
try {
const currentTime = Math.floor(Date.now() / 1000);
if (currentTime < timestamp) {
alert(`Hongbao is locked until ${new Date(timestamp * 1000).toLocaleString()}`);
return;
}
const detailsElement = document.getElementById("redemption-details");
const hongbaoVisual = document.getElementById("hongbao-visual-redeem");
const resultElement = document.getElementById("redeem-result");
detailsElement.innerHTML = "Checking for decrypted key or password protection...<br>";
detailsElement.classList.remove("hidden");
let decryptedPrivateKey;
// Check for a previously decrypted key
const decryptedKeyField = document.getElementById("decrypted-hongbao-key");
if (decryptedKeyField && decryptedKeyField.value) {
decryptedPrivateKey = decryptedKeyField.value;
console.log("Using previously decrypted key from field.");
} else if (window.decryptedHongbaoKey) {
decryptedPrivateKey = window.decryptedHongbaoKey;
console.log("Using previously decrypted key from memory.");
} else {
console.log("No previously decrypted key found. Proceeding to decrypt.");
const isProtected = new URLSearchParams(window.location.search).get("protected") === "true";
if (isProtected) {
const password = document.getElementById("redeem-password").value.trim();
if (!password) {
alert("Password is required to decrypt this Hongbao.");
return;
}
try {
const encryptedObject = JSON.parse(encryptedKey);
decryptedPrivateKey = await decryptWithPassword(
encryptedObject.encrypted,
password,
encryptedObject.iv,
encryptedObject.salt,
);
} catch (error) {
alert("Invalid password. Unable to decrypt the Hongbao.");
return;
}
} else {
decryptedPrivateKey = encryptedKey; // Not password-protected
}
// Check if still encrypted with Shutter
if (
decryptedPrivateKey.startsWith("0x03") && // BLST ciphertext
decryptedPrivateKey.length > 66
) {
const urlParams = new URLSearchParams(window.location.hash.split("?")[1]);
const identityParam = urlParams.get("identity");
if (!identityParam) {
alert("Missing Shutter identity. Cannot complete final decryption.");
return;
}
const finalKey = await getShutterDecryptionKey(identityParam);
// Perform Shutter decryption
decryptedPrivateKey = await shutterDecryptPrivateKey(decryptedPrivateKey, finalKey);
}
// Store the decrypted key for reuse
if (decryptedKeyField) {
decryptedKeyField.value = decryptedPrivateKey;
} else {
window.decryptedHongbaoKey = decryptedPrivateKey;
}
}
// Validate the final key
if (!decryptedPrivateKey.startsWith("0x") || decryptedPrivateKey.length !== 66) {
throw new Error("Invalid private key after decryption.");
}
// Sweep funds to the wallet
const hongbaoAccount = fallbackWeb3.eth.accounts.privateKeyToAccount(decryptedPrivateKey);
fallbackWeb3.eth.accounts.wallet.add(hongbaoAccount);
const balance = BigInt(await fallbackWeb3.eth.getBalance(hongbaoAccount.address));
if (balance === BigInt(0)) {
alert("No funds available to sweep.");
detailsElement.innerHTML += "No funds available to sweep.";
return;
}
const gasPrice = BigInt(await fallbackWeb3.eth.getGasPrice());
const gasLimit = BigInt(21000);
const gasCost = gasPrice * gasLimit;
if (balance <= gasCost) {
alert("Insufficient funds to cover gas fees.");
detailsElement.innerHTML += "Insufficient funds to cover gas fees.";
return;
}
const tx = {
from: hongbaoAccount.address,
to: wallet.address,
value: (balance - gasCost).toString(),
gas: 21000,
gasPrice: gasPrice.toString(),
chainId: parseInt(GNOSIS_CHAIN_PARAMS.chainId, 16),
};
const signedTx = await hongbaoAccount.signTransaction(tx);
await fallbackWeb3.eth.sendSignedTransaction(signedTx.rawTransaction);
resultElement.innerHTML = `
Funds swept to your wallet: <strong>${wallet.address}</strong><br>
<a href="wallet.html" target="_blank">Manage Wallet</a>
`;
resultElement.classList.remove("hidden");
hongbaoVisual.classList.add("opened");
detailsElement.innerHTML += "Transaction confirmed! Funds successfully transferred.";
alert(`Hongbao redeemed! Funds have been transferred to your wallet: ${wallet.address}`);
} catch (error) {
console.error("Error redeeming Hongbao with Passkey Wallet:", error);
alert("Failed to redeem Hongbao with the specified wallet.");
}
}
async function populateFieldsFromHash() {
console.log("=== populateFieldsFromHash: Start ===");
handlePasswordVisibility();
const hash = window.location.hash.substring(1);
console.log("hash:", hash);
const [_, queryString] = hash.split("?");
const params = new URLSearchParams(queryString);
const encryptedKey = params.get("key");
const timestamp = params.get("timestamp");
const amount = params.get("amount");
const identityParam = params.get("identity");
const isProtected = params.get("protected") === "true";
console.log("Params found:", {
encryptedKey,
timestamp,
amount,
identityParam,
isProtected,
});
const senderSection = document.getElementById("sender-section");
const receiverSection = document.getElementById("receiver-section");
const hongbaoVisual = document.getElementById("hongbao-visual-redeem");
const detailsElement = document.getElementById("redemption-details");
const claimNewWalletButton = document.getElementById("redeem-new-wallet");
const toggleOtherOptionsButton = document.getElementById("toggle-other-options");
// Hide both sections by default
senderSection.classList.add("hidden");
receiverSection.classList.add("hidden");
// If no required params, show sender
if (!encryptedKey || !timestamp || !amount) {
console.log("No valid ShutterHongbao link found. Showing sender section.");
senderSection.classList.remove("hidden");
return;
}
// We have a valid link, so show receiver section
receiverSection.classList.remove("hidden");
document.getElementById("hongbao-key").value = encryptedKey;
document.getElementById("hongbao-timestamp").value = timestamp;
document.getElementById("redeem-hongbao").setAttribute("data-amount", amount);
hongbaoVisual.classList.remove("hidden");
// If time is already up
const currentTime = Math.floor(Date.now() / 1000);
if (currentTime >= parseInt(timestamp, 10)) {
console.log("Time is up, Hongbao is available immediately.");
document.getElementById("countdown").textContent = "Hongbao is now available!";
if (claimNewWalletButton) claimNewWalletButton.classList.remove("hidden");
if (toggleOtherOptionsButton) toggleOtherOptionsButton.classList.remove("hidden");
} else {
// Otherwise start a countdown
console.log("Time not up yet. Starting countdown...");
startCountdown(parseInt(timestamp, 10));
}
detailsElement.textContent = "Checking Hongbao status...";
detailsElement.classList.remove("hidden");
// Proceed to attempt final or partial decryption
try {
console.log("Checking identityParam:", identityParam);
if (!identityParam) {
console.error("No identity param found. Can't fetch final key.");
throw new Error("No identity found in URL. Cannot check or decrypt Hongbao.");
}
// Step A: If it's password-protected, do local AES decryption
let possiblyShutterCipher = encryptedKey;
if (isProtected) {
const passwordField = document.getElementById("redeem-password");
if (!passwordField) {
console.warn("No password field in DOM. Can't proceed to password decrypt.");
detailsElement.innerHTML = "This Hongbao is password-protected. Please enter a password.";
return;
}
const userPassword = passwordField.value.trim();
console.log("User typed password:", userPassword ? "[REDACTED]" : "EMPTY");
if (!userPassword) {
console.warn("No password typed yet. Asking user to type it first.");
detailsElement.innerHTML = "This Hongbao is password-protected. Enter a password to decrypt.";
return;
}
try {
console.log("Attempting password-based AES decryption...");
const encryptedObject = JSON.parse(possiblyShutterCipher);
possiblyShutterCipher = await decryptWithPassword(
encryptedObject.encrypted,
userPassword,
encryptedObject.iv,
encryptedObject.salt,
);
console.log("Post-password data (possibly Shutter ciphertext):", possiblyShutterCipher);
} catch (aesErr) {
console.error("Password AES decryption failed:", aesErr);
detailsElement.innerHTML = "Invalid password. Unable to decrypt the Hongbao.";
return;
}
} else {
console.log("Not password-protected. Data is presumably raw Shutter ciphertext or locked.");
}
// Step B: If it looks like Shutter ciphertext, do Shutter BLST decrypt
if (possiblyShutterCipher.startsWith("0x03") && possiblyShutterCipher.length > 66) {
console.log("Looks like Shutter ciphertext. Attempting final BLST decrypt...");
const finalKey = await getShutterDecryptionKey(identityParam);
console.log("Final Key retrieved from Shutter:", finalKey);
console.log("Calling shutterDecryptPrivateKey with ciphertext:", possiblyShutterCipher);
const ephemeralPrivateKey = await shutterDecryptPrivateKey(possiblyShutterCipher, finalKey);
console.log("BLST decrypted ephemeralPrivateKey:", ephemeralPrivateKey);
// Step C: Check ephemeral account balance
const hongbaoAccount = fallbackWeb3.eth.accounts.privateKeyToAccount(ephemeralPrivateKey);
fallbackWeb3.eth.accounts.wallet.add(hongbaoAccount);
console.log("Ephemeral account address:", hongbaoAccount.address);
await checkHongbaoBalance(hongbaoAccount.address, amount);
} else {
console.warn("Not pure Shutter ciphertext or too short. Possibly locked or needs password.");
detailsElement.innerHTML =
"Shutter ciphertext might be password-protected, or not yet available. Enter password if needed.";
}
} catch (error) {
console.error("Error retrieving or decrypting key with Shutter API:", error);
detailsElement.textContent = "The Hongbao might still be locked or password-protected.";
}
// Always handle optional password field
console.log("=== populateFieldsFromHash: End ===");
}
function handlePasswordVisibility() {
const hash = window.location.hash;
const passwordContainer = document.getElementById("password-container");
if (!passwordContainer) {
console.warn("No 'password-container' element found. Cannot toggle password visibility.");
return;
}
// A simple check: if the hash string includes "protected=true", show container
if (hash.includes("protected=true")) {
console.log("Password container visible (protected=true in hash).");
passwordContainer.classList.remove("hidden");
} else {
console.log("Password container hidden (no protected=true in hash).");
passwordContainer.classList.add("hidden");
}
}
async function checkHongbaoBalance(hongbaoAccountAddress, expectedAmount) {
const detailsElement = document.getElementById("redemption-details");
try {
// Use fallbackWeb3 for the balance check
const balance = BigInt(await fallbackWeb3.eth.getBalance(hongbaoAccountAddress));
if (balance === BigInt(0)) {
detailsElement.innerHTML = "<strong>Status:</strong> This Hongbao has already been claimed.";
} else {
const formattedBalance = fallbackWeb3.utils.fromWei(balance.toString(), "ether");
detailsElement.innerHTML = `<strong>Status:</strong> Hongbao available! Current balance: ${formattedBalance} XDAI (Expected: ${expectedAmount} XDAI)`;
}
} catch (error) {
console.error("Error checking Hongbao balance:", error);
detailsElement.textContent = "Error retrieving balance. Please try again later.";
}
}
function isWeChatBrowser() {
const ua = navigator.userAgent.toLowerCase();
// Detect WeChat in-app browser
const isWeChat = ua.includes("micromessenger");
// Detect Telegram in-app browser
const isTelegram = typeof window.TelegramWebview !== 'undefined' ||
typeof window.TelegramWebviewProxy !== 'undefined' ||
typeof window.TelegramWebviewProxyProto !== 'undefined';
// Detect Twitter in-app browser
const isTwitter = ua.includes("twitter");
return isWeChat || isTelegram || isTwitter;
}
// Countdown timer
function startCountdown(timestamp) {
const countdownElement = document.getElementById("countdown");
const claimNewWalletButton = document.getElementById("redeem-new-wallet");
const toggleOtherOptionsButton = document.getElementById("toggle-other-options");
const otherClaimOptionsDiv = document.getElementById("other-claim-options");
// Initially hide claim buttons
if (claimNewWalletButton) claimNewWalletButton.classList.add("hidden");
if (toggleOtherOptionsButton) toggleOtherOptionsButton.classList.add("hidden");
if (otherClaimOptionsDiv) otherClaimOptionsDiv.classList.add("hidden");
const interval = setInterval(() => {
const now = Math.floor(Date.now() / 1000);
const secondsLeft = timestamp - now;
if (secondsLeft <= 0) {
clearInterval(interval);
// Update the countdown text
countdownElement.textContent = "Hongbao is now available!";
// Show claim buttons
if (claimNewWalletButton) claimNewWalletButton.classList.remove("hidden");
if (toggleOtherOptionsButton) toggleOtherOptionsButton.classList.remove("hidden");
return;
}
// Calculate time remaining
const hours = Math.floor(secondsLeft / 3600);
const minutes = Math.floor((secondsLeft % 3600) / 60);
const seconds = secondsLeft % 60;
countdownElement.textContent = `${hours}h ${minutes}m ${seconds}s remaining.`;
}, 1000);
}
document.addEventListener('DOMContentLoaded', () => {
// Event listeners for sender section
if (isWeChatBrowser()) {
document.body.innerHTML = `
<div style="text-align: center; padding: 20px;">
<h2>Shutterized Hongbao - Unsupported Browser</h2>
<p>Please press the 3 dots in the upper right and open it in your browser. Someone has gifted you a shutterized, threshold encrypted Hongbao! But this page works best in a real browser like Chrome or Safari.</p>
</div>
`;
}
const createOwnHongbaoButton = document.getElementById('create-own-hongbao');
if (createOwnHongbaoButton) {
createOwnHongbaoButton.addEventListener('click', () => {
// Reset URL to the base without query/hash
window.history.replaceState({}, document.title, window.location.pathname);
// Reset the page sections
document.getElementById('sender-section').classList.remove('hidden');
document.getElementById('receiver-section').classList.add('hidden');
document.querySelector('.title').textContent = '🎁 Shutterized Hongbao Gifting DApp - 红包赠送应用';
// Clear any fields
document.getElementById('hongbao-amount').value = '';
document.getElementById('unlock-time').value = '60';
document.getElementById('custom-timestamp-container').classList.add('hidden');
document.getElementById('hongbao-password').value = '';
document.getElementById('hongbao-details').textContent = '';
document.getElementById('hongbao-details').classList.add('hidden');
document.getElementById('hongbao-link').textContent = '';
document.getElementById('hongbao-link').classList.add('hidden');
document.getElementById('hongbao-visual').classList.add('hidden');
});
}
const createHongbaoButton = document.getElementById('create-hongbao');
if (createHongbaoButton) {
createHongbaoButton.addEventListener('click', async () => {
const amount = parseFloat(document.getElementById('hongbao-amount').value);
await sendHongbao(amount);
});
}
const createHongbaoWithPasskeyButton = document.getElementById('create-hongbao-with-passkey');
if (createHongbaoWithPasskeyButton) {
createHongbaoWithPasskeyButton.addEventListener('click', async () => {
const amount = parseFloat(document.getElementById('hongbao-amount').value);
if (!amount || amount <= 0) {
alert("Please enter a valid amount.");
return;
}
await fundHongbaoWithPasskey(amount);
});
}
// Event listeners for receiver section
const redeemHongbaoButton = document.getElementById('redeem-hongbao');
if (redeemHongbaoButton) {
redeemHongbaoButton.addEventListener('click', () => {
const encryptedKey = document.getElementById('hongbao-key').value;
const timestamp = parseInt(document.getElementById('hongbao-timestamp').value, 10);
const amount = document.getElementById('redeem-hongbao').getAttribute('data-amount');
redeemHongbaoAndSweep(encryptedKey, timestamp, amount);
});
}
const redeemNewWalletButton = document.getElementById('redeem-new-wallet');
if (redeemNewWalletButton) {
redeemNewWalletButton.addEventListener('click', () => {
const encryptedKey = document.getElementById('hongbao-key').value;
const timestamp = parseInt(document.getElementById('hongbao-timestamp').value, 10);
const amount = document.getElementById('redeem-hongbao').getAttribute('data-amount');
claimToNewWallet(encryptedKey, timestamp, amount);
});
}
const redeemExistingWalletButton = document.getElementById('redeem-existing-wallet');
if (redeemExistingWalletButton) {
redeemExistingWalletButton.addEventListener('click', () => {
const encryptedKey = document.getElementById('hongbao-key').value;