@fernolab/wallet-adapter-svelte 0.1.0 → 0.1.1
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/dist/components/WalletModal.svelte +323 -8
- package/dist/index.d.ts +1 -1
- package/dist/wallet/wallet.svelte.d.ts +52 -2
- package/dist/wallet/wallet.svelte.js +415 -15
- package/package.json +10 -10
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
let { show = $bindable(false), onModalClosed }: Props = $props();
|
|
12
12
|
|
|
13
13
|
let connectError = $state<string | null>(null);
|
|
14
|
+
let hotWalletNotice = $state<string | null>(null);
|
|
15
|
+
let topUpAmount = $state(wallet.hotWalletDefaultTopUpSol);
|
|
16
|
+
let withdrawAmount = $state(wallet.hotWalletDefaultWithdrawSol);
|
|
14
17
|
|
|
15
18
|
const standardWallets = $derived(
|
|
16
19
|
wallet.availableWallets.filter(
|
|
@@ -21,6 +24,9 @@
|
|
|
21
24
|
Boolean(wallet.mobileWalletAdapterWallet || wallet.detectedWalletBrowserName)
|
|
22
25
|
);
|
|
23
26
|
const hasWallets = $derived(standardWallets.length > 0);
|
|
27
|
+
const showMobileWalletLinks = $derived(
|
|
28
|
+
wallet.mobileRuntime.isMobile && !hasWallets && wallet.mobileWalletLinks.length > 0
|
|
29
|
+
);
|
|
24
30
|
|
|
25
31
|
function getWalletDisplayName(name: string): string {
|
|
26
32
|
return name === wallet.mobileWalletAdapterName ? wallet.mobileWalletAdapterDisplayName : name;
|
|
@@ -80,12 +86,99 @@
|
|
|
80
86
|
}
|
|
81
87
|
}
|
|
82
88
|
|
|
89
|
+
async function handleCreateHotWallet(): Promise<void> {
|
|
90
|
+
hotWalletNotice = null;
|
|
91
|
+
try {
|
|
92
|
+
const address = await wallet.createHotWalletVault();
|
|
93
|
+
hotWalletNotice = `Hot wallet ${shortAddress(address)} ready.`;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
hotWalletNotice =
|
|
96
|
+
error instanceof Error ? error.message : "Could not create hot wallet.";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function handleUnlockHotWallet(): Promise<void> {
|
|
101
|
+
hotWalletNotice = null;
|
|
102
|
+
try {
|
|
103
|
+
const address = await wallet.unlockHotWalletVault();
|
|
104
|
+
hotWalletNotice = `Hot wallet ${shortAddress(address)} unlocked.`;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
hotWalletNotice =
|
|
107
|
+
error instanceof Error ? error.message : "Could not unlock hot wallet.";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function handleRefreshHotWalletBalance(): Promise<void> {
|
|
112
|
+
hotWalletNotice = null;
|
|
113
|
+
try {
|
|
114
|
+
await wallet.refreshHotWalletBalance();
|
|
115
|
+
} catch (error) {
|
|
116
|
+
hotWalletNotice =
|
|
117
|
+
error instanceof Error ? error.message : "Could not refresh hot wallet.";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function handleTopUpHotWallet(): Promise<void> {
|
|
122
|
+
hotWalletNotice = null;
|
|
123
|
+
try {
|
|
124
|
+
const result = await wallet.topUpHotWallet(topUpAmount);
|
|
125
|
+
hotWalletNotice = `Top up sent: ${shortAddress(result.signature)}.`;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
hotWalletNotice =
|
|
128
|
+
error instanceof Error ? error.message : "Could not top up hot wallet.";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function handleWithdrawHotWallet(): Promise<void> {
|
|
133
|
+
hotWalletNotice = null;
|
|
134
|
+
try {
|
|
135
|
+
const result = await wallet.withdrawHotWallet(withdrawAmount);
|
|
136
|
+
hotWalletNotice = `Withdraw sent: ${shortAddress(result.signature)}.`;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
hotWalletNotice =
|
|
139
|
+
error instanceof Error ? error.message : "Could not withdraw from hot wallet.";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function handleCopyHotWalletAddress(): Promise<void> {
|
|
144
|
+
hotWalletNotice = null;
|
|
145
|
+
|
|
146
|
+
if (!wallet.hotWalletAddress) {
|
|
147
|
+
hotWalletNotice = "Create a hot wallet first.";
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
await navigator.clipboard.writeText(wallet.hotWalletAddress);
|
|
153
|
+
hotWalletNotice = "Hot wallet address copied.";
|
|
154
|
+
} catch {
|
|
155
|
+
hotWalletNotice = wallet.hotWalletAddress;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function handleFundingOption(optionId: string): Promise<void> {
|
|
160
|
+
hotWalletNotice = null;
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const url = await wallet.createHotWalletFundingUrl(optionId);
|
|
164
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
165
|
+
} catch (error) {
|
|
166
|
+
hotWalletNotice =
|
|
167
|
+
error instanceof Error ? error.message : "Could not open funding option.";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
83
171
|
function handleClose(): void {
|
|
84
172
|
show = false;
|
|
85
173
|
connectError = null;
|
|
174
|
+
hotWalletNotice = null;
|
|
86
175
|
onModalClosed && onModalClosed();
|
|
87
176
|
}
|
|
88
177
|
|
|
178
|
+
function shortAddress(address: string): string {
|
|
179
|
+
return address.length > 18 ? `${address.slice(0, 8)}...${address.slice(-8)}` : address;
|
|
180
|
+
}
|
|
181
|
+
|
|
89
182
|
function handleOverlayKeydown(event: KeyboardEvent): void {
|
|
90
183
|
if (event.key === "Escape" || event.key === "Enter" || event.key === " ") {
|
|
91
184
|
event.preventDefault();
|
|
@@ -149,14 +242,6 @@
|
|
|
149
242
|
</div>
|
|
150
243
|
{/if}
|
|
151
244
|
|
|
152
|
-
{#if wallet.mobileRuntime.isMobile && wallet.mobileWalletLinks.length > 0}
|
|
153
|
-
<div class="mobile-links" aria-label="Open in wallet browser">
|
|
154
|
-
{#each wallet.mobileWalletLinks as link}
|
|
155
|
-
<a href={link.url} rel="noreferrer">{link.name}</a>
|
|
156
|
-
{/each}
|
|
157
|
-
</div>
|
|
158
|
-
{/if}
|
|
159
|
-
|
|
160
245
|
<div class="wallet-list">
|
|
161
246
|
{#if standardWallets.length > 0}
|
|
162
247
|
<div class="section-title">Wallets</div>
|
|
@@ -175,6 +260,126 @@
|
|
|
175
260
|
</div>
|
|
176
261
|
{/if}
|
|
177
262
|
|
|
263
|
+
{#if showMobileWalletLinks}
|
|
264
|
+
<div class="section-title">Mobile wallets</div>
|
|
265
|
+
<div class="mobile-links" aria-label="Open in mobile wallet">
|
|
266
|
+
{#each wallet.mobileWalletLinks as link (link.name)}
|
|
267
|
+
<a href={link.url} rel="noreferrer">{link.name}</a>
|
|
268
|
+
{/each}
|
|
269
|
+
</div>
|
|
270
|
+
{/if}
|
|
271
|
+
|
|
272
|
+
{#if wallet.connected}
|
|
273
|
+
<div class="hot-wallet-panel">
|
|
274
|
+
<div class="section-title">Hot wallet</div>
|
|
275
|
+
<div class="hot-wallet-state">
|
|
276
|
+
<div>
|
|
277
|
+
<span>Owner</span>
|
|
278
|
+
<strong>{wallet.shortAddress}</strong>
|
|
279
|
+
</div>
|
|
280
|
+
<div>
|
|
281
|
+
<span>Address</span>
|
|
282
|
+
<strong>
|
|
283
|
+
{wallet.hotWalletAddress
|
|
284
|
+
? shortAddress(wallet.hotWalletAddress)
|
|
285
|
+
: "Not created"}
|
|
286
|
+
</strong>
|
|
287
|
+
</div>
|
|
288
|
+
<div>
|
|
289
|
+
<span>Balance</span>
|
|
290
|
+
<strong>{wallet.hotWalletBalance}</strong>
|
|
291
|
+
</div>
|
|
292
|
+
<div>
|
|
293
|
+
<span>Key</span>
|
|
294
|
+
<strong>{wallet.hotWalletUnlocked ? "Unlocked" : "Locked"}</strong>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
<div class="hot-wallet-actions">
|
|
299
|
+
<button
|
|
300
|
+
class="action-button"
|
|
301
|
+
type="button"
|
|
302
|
+
disabled={wallet.hotWalletBusy || wallet.signing}
|
|
303
|
+
onclick={handleCreateHotWallet}
|
|
304
|
+
>
|
|
305
|
+
Create
|
|
306
|
+
</button>
|
|
307
|
+
<button
|
|
308
|
+
class="action-button"
|
|
309
|
+
type="button"
|
|
310
|
+
disabled={wallet.hotWalletBusy || wallet.signing || !wallet.hotWalletAddress}
|
|
311
|
+
onclick={handleUnlockHotWallet}
|
|
312
|
+
>
|
|
313
|
+
Unlock
|
|
314
|
+
</button>
|
|
315
|
+
<button
|
|
316
|
+
class="action-button"
|
|
317
|
+
type="button"
|
|
318
|
+
disabled={wallet.hotWalletBusy || !wallet.hotWalletAddress}
|
|
319
|
+
onclick={handleRefreshHotWalletBalance}
|
|
320
|
+
>
|
|
321
|
+
Balance
|
|
322
|
+
</button>
|
|
323
|
+
<button
|
|
324
|
+
class="action-button"
|
|
325
|
+
type="button"
|
|
326
|
+
disabled={!wallet.hotWalletAddress}
|
|
327
|
+
onclick={handleCopyHotWalletAddress}
|
|
328
|
+
>
|
|
329
|
+
Copy
|
|
330
|
+
</button>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
<div class="amount-row">
|
|
334
|
+
<label for="hot-wallet-top-up">Top up</label>
|
|
335
|
+
<input id="hot-wallet-top-up" bind:value={topUpAmount} inputmode="decimal" />
|
|
336
|
+
<button
|
|
337
|
+
class="action-button primary"
|
|
338
|
+
type="button"
|
|
339
|
+
disabled={wallet.hotWalletBusy || wallet.signing}
|
|
340
|
+
onclick={handleTopUpHotWallet}
|
|
341
|
+
>
|
|
342
|
+
Send
|
|
343
|
+
</button>
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
<div class="amount-row">
|
|
347
|
+
<label for="hot-wallet-withdraw">Withdraw</label>
|
|
348
|
+
<input id="hot-wallet-withdraw" bind:value={withdrawAmount} inputmode="decimal" />
|
|
349
|
+
<button
|
|
350
|
+
class="action-button"
|
|
351
|
+
type="button"
|
|
352
|
+
disabled={wallet.hotWalletBusy || wallet.signing || !wallet.hotWalletAddress}
|
|
353
|
+
onclick={handleWithdrawHotWallet}
|
|
354
|
+
>
|
|
355
|
+
Send
|
|
356
|
+
</button>
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
{#if wallet.hotWalletFundingOptions.length > 0}
|
|
360
|
+
<div class="funding-grid" aria-label="External funding options">
|
|
361
|
+
{#each wallet.hotWalletFundingOptions as option (option.id)}
|
|
362
|
+
<button
|
|
363
|
+
class="funding-button"
|
|
364
|
+
type="button"
|
|
365
|
+
disabled={wallet.hotWalletBusy}
|
|
366
|
+
onclick={() => handleFundingOption(option.id)}
|
|
367
|
+
>
|
|
368
|
+
<span>{option.label}</span>
|
|
369
|
+
<small>{option.description}</small>
|
|
370
|
+
</button>
|
|
371
|
+
{/each}
|
|
372
|
+
</div>
|
|
373
|
+
{/if}
|
|
374
|
+
|
|
375
|
+
{#if hotWalletNotice || wallet.hotWalletError}
|
|
376
|
+
<p class="hot-wallet-note" role="status">
|
|
377
|
+
{hotWalletNotice ?? wallet.hotWalletError?.message}
|
|
378
|
+
</p>
|
|
379
|
+
{/if}
|
|
380
|
+
</div>
|
|
381
|
+
{/if}
|
|
382
|
+
|
|
178
383
|
<button class="refresh-button" type="button" onclick={handleRefresh}>
|
|
179
384
|
Refresh wallets
|
|
180
385
|
</button>
|
|
@@ -345,6 +550,116 @@
|
|
|
345
550
|
margin: 0;
|
|
346
551
|
}
|
|
347
552
|
|
|
553
|
+
.hot-wallet-panel {
|
|
554
|
+
display: grid;
|
|
555
|
+
gap: 10px;
|
|
556
|
+
margin-top: 6px;
|
|
557
|
+
padding-top: 4px;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.hot-wallet-state {
|
|
561
|
+
display: grid;
|
|
562
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
563
|
+
gap: 8px;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.hot-wallet-state div {
|
|
567
|
+
border: 1px solid #d8d8d8;
|
|
568
|
+
border-radius: 8px;
|
|
569
|
+
padding: 9px 10px;
|
|
570
|
+
min-width: 0;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.hot-wallet-state span,
|
|
574
|
+
.amount-row label {
|
|
575
|
+
display: block;
|
|
576
|
+
color: #666666;
|
|
577
|
+
font-size: 0.74rem;
|
|
578
|
+
font-weight: 700;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.hot-wallet-state strong {
|
|
582
|
+
display: block;
|
|
583
|
+
color: #111111;
|
|
584
|
+
font-size: 0.86rem;
|
|
585
|
+
margin-top: 2px;
|
|
586
|
+
overflow-wrap: anywhere;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.hot-wallet-actions {
|
|
590
|
+
display: grid;
|
|
591
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
592
|
+
gap: 8px;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.amount-row {
|
|
596
|
+
display: grid;
|
|
597
|
+
grid-template-columns: 72px minmax(0, 1fr) 78px;
|
|
598
|
+
align-items: center;
|
|
599
|
+
gap: 8px;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.amount-row input {
|
|
603
|
+
width: 100%;
|
|
604
|
+
min-width: 0;
|
|
605
|
+
border: 1px solid #d8d8d8;
|
|
606
|
+
border-radius: 8px;
|
|
607
|
+
color: #111111;
|
|
608
|
+
font: inherit;
|
|
609
|
+
padding: 9px 10px;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
.hot-wallet-note {
|
|
613
|
+
margin: 0;
|
|
614
|
+
color: #111111;
|
|
615
|
+
font-size: 0.85rem;
|
|
616
|
+
font-weight: 600;
|
|
617
|
+
overflow-wrap: anywhere;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.funding-grid {
|
|
621
|
+
display: grid;
|
|
622
|
+
gap: 8px;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.funding-button {
|
|
626
|
+
width: 100%;
|
|
627
|
+
border: 1px solid #d8d8d8;
|
|
628
|
+
border-radius: 8px;
|
|
629
|
+
background: #ffffff;
|
|
630
|
+
color: #111111;
|
|
631
|
+
cursor: pointer;
|
|
632
|
+
padding: 9px 10px;
|
|
633
|
+
text-align: left;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.funding-button:hover:not(:disabled) {
|
|
637
|
+
background: #f7f7f7;
|
|
638
|
+
border-color: #b8b8b8;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.funding-button:disabled {
|
|
642
|
+
cursor: not-allowed;
|
|
643
|
+
opacity: 0.5;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.funding-button span,
|
|
647
|
+
.funding-button small {
|
|
648
|
+
display: block;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.funding-button span {
|
|
652
|
+
font-size: 0.88rem;
|
|
653
|
+
font-weight: 750;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.funding-button small {
|
|
657
|
+
color: #666666;
|
|
658
|
+
font-size: 0.76rem;
|
|
659
|
+
line-height: 1.35;
|
|
660
|
+
margin-top: 2px;
|
|
661
|
+
}
|
|
662
|
+
|
|
348
663
|
.refresh-button {
|
|
349
664
|
margin-top: 4px;
|
|
350
665
|
padding: 9px 12px;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * from './components/index.js';
|
|
2
|
-
export { createWalletStore, wallet, type WalletStore } from './wallet/wallet.svelte.js';
|
|
2
|
+
export { createWalletStore, wallet, type WalletStore, type WalletStoreOptions } from './wallet/wallet.svelte.js';
|
|
3
3
|
export * from './wallet/mobile-detection.js';
|
|
4
4
|
export * from '@fernolab/wallet-adapter-core';
|
|
5
5
|
export type { WalletWithStandardCapabilities } from './wallet/types.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { StandardWallet } from './types.js';
|
|
2
|
-
import { adaptWallet, dedupeStandardWallets, isSolanaWallet, type IdentifierString, type InjectedWallet, type WalletAccount } from '@fernolab/wallet-adapter-core';
|
|
2
|
+
import { adaptWallet, dedupeStandardWallets, isSolanaWallet, type IdentifierString, type InjectedWallet, type WalletAccount, type WalletStorage } from '@fernolab/wallet-adapter-core';
|
|
3
3
|
export { adaptWallet, dedupeStandardWallets, isSolanaWallet };
|
|
4
4
|
export declare const RUNTIME_DEBUG_MODES: readonly ["auto", "desktop", "ios", "android", "phantom", "solflare", "jupiter", "backpack"];
|
|
5
5
|
export type RuntimeDebugMode = (typeof RUNTIME_DEBUG_MODES)[number];
|
|
@@ -10,11 +10,43 @@ interface SignMessageResult {
|
|
|
10
10
|
}
|
|
11
11
|
type ConnectionStatus = 'disconnected' | 'connecting' | 'connected';
|
|
12
12
|
type WalletEventCallback = (wallet: StandardWallet | null) => void;
|
|
13
|
+
export interface WalletStoreOptions {
|
|
14
|
+
fundingProviders?: readonly HotWalletFundingProvider[];
|
|
15
|
+
storage?: WalletStorage | null;
|
|
16
|
+
}
|
|
17
|
+
export interface HotWalletTransferResult {
|
|
18
|
+
signature: string;
|
|
19
|
+
amountLamports: bigint;
|
|
20
|
+
address: string;
|
|
21
|
+
}
|
|
22
|
+
export type HotWalletFundingKind = 'onramp' | 'bridge' | 'exchange' | 'copy';
|
|
23
|
+
export interface HotWalletFundingOption {
|
|
24
|
+
description: string;
|
|
25
|
+
id: string;
|
|
26
|
+
kind: HotWalletFundingKind;
|
|
27
|
+
label: string;
|
|
28
|
+
requiresSignedUrl?: boolean;
|
|
29
|
+
url?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface HotWalletFundingUrlInput {
|
|
32
|
+
chain: IdentifierString;
|
|
33
|
+
hotWalletAddress: string;
|
|
34
|
+
ownerAddress: string;
|
|
35
|
+
}
|
|
36
|
+
export type HotWalletFundingProvider = {
|
|
37
|
+
enabled?: boolean;
|
|
38
|
+
type: 'jupiter-hosted';
|
|
39
|
+
} | {
|
|
40
|
+
createSignedUrl(input: HotWalletFundingUrlInput): Promise<string>;
|
|
41
|
+
id?: string;
|
|
42
|
+
label?: string;
|
|
43
|
+
type: 'onramper';
|
|
44
|
+
};
|
|
13
45
|
/**
|
|
14
46
|
* Determine Solana chain identifier from RPC endpoint
|
|
15
47
|
*/
|
|
16
48
|
export declare function resolveChainFromEndpoint(endpoint: string): IdentifierString;
|
|
17
|
-
export declare function createWalletStore(): {
|
|
49
|
+
export declare function createWalletStore(options?: WalletStoreOptions): {
|
|
18
50
|
readonly rpcEndpoint: string;
|
|
19
51
|
readonly availableWallets: StandardWallet[];
|
|
20
52
|
readonly standardWallet: StandardWallet | null;
|
|
@@ -41,6 +73,17 @@ export declare function createWalletStore(): {
|
|
|
41
73
|
readonly mobileWalletAdapterWallet: StandardWallet | null;
|
|
42
74
|
readonly mobileWalletAdapterName: string;
|
|
43
75
|
readonly mobileWalletAdapterDisplayName: string;
|
|
76
|
+
readonly lastUsedWalletName: string | null;
|
|
77
|
+
readonly hotWalletAddress: string | null;
|
|
78
|
+
readonly hotWalletBalanceLamports: bigint | null;
|
|
79
|
+
readonly hotWalletBalance: string;
|
|
80
|
+
readonly hotWalletBusy: boolean;
|
|
81
|
+
readonly hotWalletError: Error | null;
|
|
82
|
+
readonly hotWalletUnlocked: boolean;
|
|
83
|
+
readonly hotWalletDefaultTopUpSol: string;
|
|
84
|
+
readonly hotWalletDefaultWithdrawSol: string;
|
|
85
|
+
readonly walletAccounts: readonly unknown[];
|
|
86
|
+
readonly hotWalletFundingOptions: HotWalletFundingOption[];
|
|
44
87
|
initialize: (endpoint?: string) => Promise<void>;
|
|
45
88
|
refreshAvailableWallets: () => Promise<void>;
|
|
46
89
|
setRuntimeDebugMode: (mode: RuntimeDebugMode | string) => Promise<void>;
|
|
@@ -49,8 +92,15 @@ export declare function createWalletStore(): {
|
|
|
49
92
|
connectWalletBrowser: () => Promise<void>;
|
|
50
93
|
disconnect: () => Promise<void>;
|
|
51
94
|
signMessage: (message: string | Uint8Array) => Promise<SignMessageResult>;
|
|
95
|
+
createHotWalletVault: () => Promise<string>;
|
|
96
|
+
unlockHotWalletVault: () => Promise<string>;
|
|
97
|
+
refreshHotWalletBalance: () => Promise<bigint>;
|
|
98
|
+
topUpHotWallet: (amountSol?: string) => Promise<HotWalletTransferResult>;
|
|
99
|
+
withdrawHotWallet: (amountSol?: string) => Promise<HotWalletTransferResult>;
|
|
100
|
+
createHotWalletFundingUrl: (optionId: string) => Promise<string>;
|
|
52
101
|
onWalletChange: (callback: WalletEventCallback) => () => void;
|
|
53
102
|
clearError: () => void;
|
|
103
|
+
clearHotWalletError: () => void;
|
|
54
104
|
};
|
|
55
105
|
export type WalletStore = ReturnType<typeof createWalletStore>;
|
|
56
106
|
export declare const wallet: WalletStore;
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { getWallets } from '@wallet-standard/app';
|
|
2
2
|
import { PUBLIC_SOLANA_RPC_ENDPOINT } from '../config.js';
|
|
3
3
|
import { browser } from '../environment.js';
|
|
4
|
-
import { MOBILE_DEEPLINK_WALLETS, MOBILE_WALLETS, SolanaSignMessage, StandardConnect, StandardDisconnect, StandardEvents, adaptStandardWallets, adaptWallet, createWalletController, createMobileWalletBrowseUrl, dedupeStandardWallets, detectInjectedWallets, detectMobileRuntime, isSolanaWallet, resolveSolanaChainFromEndpoint } from '@fernolab/wallet-adapter-core';
|
|
4
|
+
import { MOBILE_DEEPLINK_WALLETS, MOBILE_WALLETS, SolanaSignMessage, SolanaSignAndSendTransaction, SolanaSignTransaction, StandardConnect, StandardDisconnect, StandardEvents, adaptStandardWallets, adaptWallet, applySolanaTransactionSignature, base58Encode, createWalletController, createMobileWalletBrowseUrl, createHotWalletUnlockMessage, createSystemTransferTransaction, createWrappedHotWalletFromSignature, dedupeStandardWallets, getHotWalletAddress, getLatestSolanaBlockhash, getSolanaBalance, detectInjectedWallets, detectMobileRuntime, isSolanaWallet, lamportsToSol, normalizeSignatureBytes, readWrappedHotWallet, resolveSolanaChainFromEndpoint, saveWrappedHotWallet, sendSerializedSolanaTransaction, signWithHotWallet, solToLamports, unlockHotWalletFromSignature } from '@fernolab/wallet-adapter-core';
|
|
5
5
|
export { adaptWallet, dedupeStandardWallets, isSolanaWallet };
|
|
6
6
|
// ============================================================================
|
|
7
7
|
// Constants
|
|
8
8
|
// ============================================================================
|
|
9
|
-
const LAST_WALLET_KEY = 'lastConnectedWallet';
|
|
10
9
|
const SolanaMobileWalletAdapterWalletName = 'Mobile Wallet Adapter';
|
|
11
10
|
const MOBILE_WALLET_ADAPTER_DISPLAY_NAME = 'Use Installed Wallet';
|
|
12
11
|
const INJECTED_WALLET_ICON = 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2248%22%20height%3D%2248%22%20viewBox%3D%220%200%2048%2048%22%20fill%3D%22none%22%3E%3Crect%20width%3D%2248%22%20height%3D%2248%22%20rx%3D%2210%22%20fill%3D%22%23111827%22%2F%3E%3Cpath%20d%3D%22M14%2018h20M14%2024h20M14%2030h20%22%20stroke%3D%22white%22%20stroke-width%3D%222.5%22%20stroke-linecap%3D%22round%22%2F%3E%3C%2Fsvg%3E';
|
|
@@ -38,6 +37,9 @@ const DESKTOP_RUNTIME = {
|
|
|
38
37
|
supportsMobileWalletAdapter: false,
|
|
39
38
|
walletBrowser: null
|
|
40
39
|
};
|
|
40
|
+
const HOT_WALLET_STORAGE_PREFIX = 'ferno:svelte-adapter:hot-wallet:';
|
|
41
|
+
const DEFAULT_HOT_WALLET_TOP_UP_SOL = '0.001';
|
|
42
|
+
const DEFAULT_HOT_WALLET_WITHDRAW_SOL = '0.0005';
|
|
41
43
|
// ============================================================================
|
|
42
44
|
// Wallet Detection & Adaptation
|
|
43
45
|
// ============================================================================
|
|
@@ -355,10 +357,56 @@ function getWalletFeature(wallet, featureName) {
|
|
|
355
357
|
const features = wallet.features;
|
|
356
358
|
return features[featureName] ?? null;
|
|
357
359
|
}
|
|
360
|
+
function getDefaultWalletStorage() {
|
|
361
|
+
if (!browser || typeof localStorage === 'undefined') {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
return localStorage;
|
|
365
|
+
}
|
|
366
|
+
function getWalletStorage(options) {
|
|
367
|
+
return 'storage' in options ? options.storage ?? null : getDefaultWalletStorage();
|
|
368
|
+
}
|
|
369
|
+
function getFundingProviders(options) {
|
|
370
|
+
return options.fundingProviders ?? [{ type: 'jupiter-hosted' }];
|
|
371
|
+
}
|
|
372
|
+
function createJupiterFundingOptions() {
|
|
373
|
+
return [
|
|
374
|
+
{
|
|
375
|
+
description: 'Buy SOL or USDC through Jupiter hosted onboarding.',
|
|
376
|
+
id: 'jupiter-onramp',
|
|
377
|
+
kind: 'onramp',
|
|
378
|
+
label: 'Buy with Jupiter',
|
|
379
|
+
url: 'https://jup.ag/onboard/onramp'
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
description: 'Move funds from an exchange into your Solana wallet.',
|
|
383
|
+
id: 'jupiter-exchange',
|
|
384
|
+
kind: 'exchange',
|
|
385
|
+
label: 'Transfer from exchange',
|
|
386
|
+
url: 'https://jup.ag/onboard'
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
description: 'Bridge assets from other chains via deBridge.',
|
|
390
|
+
id: 'jupiter-debridge',
|
|
391
|
+
kind: 'bridge',
|
|
392
|
+
label: 'Bridge with deBridge',
|
|
393
|
+
url: 'https://jup.ag/onboard/debridge'
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
description: 'Bridge native USDC to Solana through CCTP.',
|
|
397
|
+
id: 'jupiter-cctp',
|
|
398
|
+
kind: 'bridge',
|
|
399
|
+
label: 'Bridge USDC',
|
|
400
|
+
url: 'https://jup.ag/onboard/cctp'
|
|
401
|
+
}
|
|
402
|
+
];
|
|
403
|
+
}
|
|
358
404
|
// ============================================================================
|
|
359
405
|
// Wallet Store
|
|
360
406
|
// ============================================================================
|
|
361
|
-
export function createWalletStore() {
|
|
407
|
+
export function createWalletStore(options = {}) {
|
|
408
|
+
const walletStorage = getWalletStorage(options);
|
|
409
|
+
const fundingProviders = getFundingProviders(options);
|
|
362
410
|
// State
|
|
363
411
|
let rpcEndpoint = $state(PUBLIC_SOLANA_RPC_ENDPOINT);
|
|
364
412
|
let availableWallets = $state([]);
|
|
@@ -375,15 +423,44 @@ export function createWalletStore() {
|
|
|
375
423
|
let runtimeDebugMode = $state(readRuntimeDebugMode());
|
|
376
424
|
let injectedWallets = $state([]);
|
|
377
425
|
let mobileWalletLinks = $state([]);
|
|
426
|
+
let lastUsedWalletName = $state(null);
|
|
427
|
+
let hotWalletAddress = $state(null);
|
|
428
|
+
let hotWalletBalanceLamports = $state(null);
|
|
429
|
+
let hotWalletBusy = $state(false);
|
|
430
|
+
let hotWalletError = $state(null);
|
|
431
|
+
let hotWalletUnlocked = $state(false);
|
|
378
432
|
let walletChangeCallbacks = new Set();
|
|
379
433
|
let walletRegistrationOff = null;
|
|
380
|
-
|
|
434
|
+
let unlockedHotWallet = null;
|
|
435
|
+
const controller = createWalletController({ storage: walletStorage });
|
|
381
436
|
// Derived state
|
|
382
437
|
const shortAddress = $derived(publicKey ? `${publicKey.slice(0, 4)}...${publicKey.slice(-4)}` : '');
|
|
383
438
|
const connectionStatus = $derived(connecting ? 'connecting' : connected ? 'connected' : 'disconnected');
|
|
384
439
|
const detectedWalletBrowserName = $derived(getDetectedWalletBrowserName(mobileRuntime, injectedWallets));
|
|
385
440
|
const runtimeLabel = $derived(getRuntimeLabel(mobileRuntime, injectedWallets));
|
|
386
441
|
const mobileWalletAdapterWallet = $derived(availableWallets.find((wallet) => wallet.name === SolanaMobileWalletAdapterWalletName) ?? null);
|
|
442
|
+
const hotWalletFundingOptions = $derived.by(() => {
|
|
443
|
+
if (!hotWalletAddress) {
|
|
444
|
+
return [];
|
|
445
|
+
}
|
|
446
|
+
return fundingProviders.flatMap((provider) => {
|
|
447
|
+
if (provider.type === 'jupiter-hosted') {
|
|
448
|
+
return provider.enabled === false ? [] : createJupiterFundingOptions();
|
|
449
|
+
}
|
|
450
|
+
if (provider.type === 'onramper') {
|
|
451
|
+
return [
|
|
452
|
+
{
|
|
453
|
+
description: 'Open an embedded-ready Onramper flow with your hot wallet prefilled.',
|
|
454
|
+
id: provider.id ?? 'onramper',
|
|
455
|
+
kind: 'onramp',
|
|
456
|
+
label: provider.label ?? 'Buy with card',
|
|
457
|
+
requiresSignedUrl: true
|
|
458
|
+
}
|
|
459
|
+
];
|
|
460
|
+
}
|
|
461
|
+
return [];
|
|
462
|
+
});
|
|
463
|
+
});
|
|
387
464
|
// ============================================================================
|
|
388
465
|
// Event System
|
|
389
466
|
// ============================================================================
|
|
@@ -404,6 +481,7 @@ export function createWalletStore() {
|
|
|
404
481
|
}
|
|
405
482
|
controller.subscribe((state) => {
|
|
406
483
|
const previousWallet = standardWallet;
|
|
484
|
+
const previousPublicKey = publicKey;
|
|
407
485
|
availableWallets = [...state.availableWallets];
|
|
408
486
|
standardWallet = state.standardWallet;
|
|
409
487
|
standardAccount = state.standardAccount;
|
|
@@ -412,17 +490,15 @@ export function createWalletStore() {
|
|
|
412
490
|
connecting = state.connecting;
|
|
413
491
|
signing = state.signing;
|
|
414
492
|
error = state.error;
|
|
493
|
+
lastUsedWalletName = state.lastUsedWalletName;
|
|
415
494
|
if (previousWallet !== state.standardWallet) {
|
|
416
|
-
if (browser) {
|
|
417
|
-
if (state.standardWallet) {
|
|
418
|
-
localStorage.setItem(LAST_WALLET_KEY, state.standardWallet.name);
|
|
419
|
-
}
|
|
420
|
-
else {
|
|
421
|
-
localStorage.removeItem(LAST_WALLET_KEY);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
495
|
notifyWalletChange(state.standardWallet);
|
|
425
496
|
}
|
|
497
|
+
if (previousPublicKey !== state.publicKey) {
|
|
498
|
+
unlockedHotWallet = null;
|
|
499
|
+
hotWalletUnlocked = false;
|
|
500
|
+
loadStoredHotWalletAddress(state.publicKey);
|
|
501
|
+
}
|
|
426
502
|
});
|
|
427
503
|
// ============================================================================
|
|
428
504
|
// Internal Helper Functions
|
|
@@ -430,6 +506,102 @@ export function createWalletStore() {
|
|
|
430
506
|
function setError(err) {
|
|
431
507
|
error = err;
|
|
432
508
|
}
|
|
509
|
+
function setHotWalletError(err) {
|
|
510
|
+
hotWalletError = err;
|
|
511
|
+
}
|
|
512
|
+
function getCurrentChain() {
|
|
513
|
+
return resolveChainFromEndpoint(rpcEndpoint);
|
|
514
|
+
}
|
|
515
|
+
function getHotWalletDomain() {
|
|
516
|
+
if (!browser || typeof location === 'undefined') {
|
|
517
|
+
return { domain: 'localhost', uri: 'http://localhost' };
|
|
518
|
+
}
|
|
519
|
+
return { domain: location.host, uri: location.origin };
|
|
520
|
+
}
|
|
521
|
+
function getStoredHotWallet(ownerAddress) {
|
|
522
|
+
return readWrappedHotWallet({
|
|
523
|
+
chainId: getCurrentChain(),
|
|
524
|
+
ownerAddress,
|
|
525
|
+
storage: walletStorage,
|
|
526
|
+
storagePrefix: HOT_WALLET_STORAGE_PREFIX
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
function loadStoredHotWalletAddress(ownerAddress = publicKey) {
|
|
530
|
+
const wrappedWallet = ownerAddress ? getStoredHotWallet(ownerAddress) : null;
|
|
531
|
+
hotWalletAddress = wrappedWallet ? getHotWalletAddress(wrappedWallet) : null;
|
|
532
|
+
hotWalletBalanceLamports = null;
|
|
533
|
+
hotWalletUnlocked = Boolean(unlockedHotWallet && hotWalletAddress);
|
|
534
|
+
}
|
|
535
|
+
function resetHotWalletSession() {
|
|
536
|
+
unlockedHotWallet = null;
|
|
537
|
+
hotWalletAddress = null;
|
|
538
|
+
hotWalletBalanceLamports = null;
|
|
539
|
+
hotWalletBusy = false;
|
|
540
|
+
hotWalletError = null;
|
|
541
|
+
hotWalletUnlocked = false;
|
|
542
|
+
}
|
|
543
|
+
function getHotWalletUnlockPayload(ownerAddress) {
|
|
544
|
+
const { domain, uri } = getHotWalletDomain();
|
|
545
|
+
return createHotWalletUnlockMessage({
|
|
546
|
+
chainId: getCurrentChain(),
|
|
547
|
+
domain,
|
|
548
|
+
uri,
|
|
549
|
+
walletAddress: ownerAddress
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
async function signHotWalletUnlockPayload(ownerAddress) {
|
|
553
|
+
const result = await signMessage(getHotWalletUnlockPayload(ownerAddress));
|
|
554
|
+
return normalizeSignatureBytes(result.signature);
|
|
555
|
+
}
|
|
556
|
+
function getConnectedOwnerAddress() {
|
|
557
|
+
if (!publicKey) {
|
|
558
|
+
throw new Error('Connect a wallet first');
|
|
559
|
+
}
|
|
560
|
+
return publicKey;
|
|
561
|
+
}
|
|
562
|
+
function getConnectedWalletForTransactions() {
|
|
563
|
+
if (!standardWallet || !standardAccount) {
|
|
564
|
+
throw new Error('Connect a wallet first');
|
|
565
|
+
}
|
|
566
|
+
return { account: standardAccount, wallet: standardWallet };
|
|
567
|
+
}
|
|
568
|
+
async function signOwnerTransfer(transaction) {
|
|
569
|
+
const { account, wallet } = getConnectedWalletForTransactions();
|
|
570
|
+
const signTransactionFeature = getWalletFeature(wallet.wallet, SolanaSignTransaction);
|
|
571
|
+
if (signTransactionFeature) {
|
|
572
|
+
const [result] = await signTransactionFeature.signTransaction({
|
|
573
|
+
account,
|
|
574
|
+
chain: getCurrentChain(),
|
|
575
|
+
transaction
|
|
576
|
+
});
|
|
577
|
+
if (!result) {
|
|
578
|
+
throw new Error(`${wallet.name} returned no signed transaction`);
|
|
579
|
+
}
|
|
580
|
+
return result.signedTransaction;
|
|
581
|
+
}
|
|
582
|
+
const signAndSendFeature = getWalletFeature(wallet.wallet, SolanaSignAndSendTransaction);
|
|
583
|
+
if (signAndSendFeature) {
|
|
584
|
+
const result = await signAndSendFeature.signAndSendTransaction({
|
|
585
|
+
account,
|
|
586
|
+
chain: getCurrentChain(),
|
|
587
|
+
transaction
|
|
588
|
+
});
|
|
589
|
+
const normalizedResult = Array.isArray(result) ? result[0] : result;
|
|
590
|
+
if (!normalizedResult) {
|
|
591
|
+
throw new Error(`${wallet.name} returned no transaction signature`);
|
|
592
|
+
}
|
|
593
|
+
return typeof normalizedResult.signature === 'string'
|
|
594
|
+
? normalizedResult.signature
|
|
595
|
+
: base58Encode(normalizedResult.signature);
|
|
596
|
+
}
|
|
597
|
+
throw new Error(`${wallet.name} does not support transaction signing`);
|
|
598
|
+
}
|
|
599
|
+
function findFundingProvider(optionId) {
|
|
600
|
+
if (optionId.startsWith('jupiter-')) {
|
|
601
|
+
return fundingProviders.find((provider) => provider.type === 'jupiter-hosted') ?? null;
|
|
602
|
+
}
|
|
603
|
+
return (fundingProviders.find((provider) => provider.type === 'onramper' && (provider.id ?? 'onramper') === optionId) ?? null);
|
|
604
|
+
}
|
|
433
605
|
function getDetectedInjectedWallets() {
|
|
434
606
|
const debugWalletId = getRuntimeDebugWalletId(runtimeDebugMode);
|
|
435
607
|
if (debugWalletId) {
|
|
@@ -489,7 +661,7 @@ export function createWalletStore() {
|
|
|
489
661
|
await refreshAvailableWallets();
|
|
490
662
|
const wallets = availableWallets;
|
|
491
663
|
// Attempt to restore last connected wallet
|
|
492
|
-
const lastWallet =
|
|
664
|
+
const lastWallet = controller.state.lastUsedWalletName;
|
|
493
665
|
if (lastWallet) {
|
|
494
666
|
const existing = wallets.find((wallet) => wallet.name === lastWallet);
|
|
495
667
|
if (existing) {
|
|
@@ -497,7 +669,6 @@ export function createWalletStore() {
|
|
|
497
669
|
await connectStandard(existing);
|
|
498
670
|
}
|
|
499
671
|
catch (err) {
|
|
500
|
-
localStorage.removeItem(LAST_WALLET_KEY);
|
|
501
672
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
502
673
|
}
|
|
503
674
|
}
|
|
@@ -559,6 +730,7 @@ export function createWalletStore() {
|
|
|
559
730
|
async function disconnect() {
|
|
560
731
|
try {
|
|
561
732
|
await controller.disconnect();
|
|
733
|
+
resetHotWalletSession();
|
|
562
734
|
setError(null);
|
|
563
735
|
}
|
|
564
736
|
catch (err) {
|
|
@@ -582,6 +754,194 @@ export function createWalletStore() {
|
|
|
582
754
|
throw error;
|
|
583
755
|
}
|
|
584
756
|
}
|
|
757
|
+
async function createHotWalletVault() {
|
|
758
|
+
const ownerAddress = getConnectedOwnerAddress();
|
|
759
|
+
const signature = await signHotWalletUnlockPayload(ownerAddress);
|
|
760
|
+
hotWalletBusy = true;
|
|
761
|
+
setHotWalletError(null);
|
|
762
|
+
try {
|
|
763
|
+
const { domain } = getHotWalletDomain();
|
|
764
|
+
const wrappedWallet = await createWrappedHotWalletFromSignature({
|
|
765
|
+
chainId: getCurrentChain(),
|
|
766
|
+
domain,
|
|
767
|
+
walletAddress: ownerAddress,
|
|
768
|
+
walletSignature: signature
|
|
769
|
+
});
|
|
770
|
+
unlockedHotWallet = await unlockHotWalletFromSignature({
|
|
771
|
+
chainId: getCurrentChain(),
|
|
772
|
+
domain,
|
|
773
|
+
walletAddress: ownerAddress,
|
|
774
|
+
walletSignature: signature,
|
|
775
|
+
wrappedWallet
|
|
776
|
+
});
|
|
777
|
+
saveWrappedHotWallet({
|
|
778
|
+
chainId: getCurrentChain(),
|
|
779
|
+
ownerAddress,
|
|
780
|
+
storage: walletStorage,
|
|
781
|
+
storagePrefix: HOT_WALLET_STORAGE_PREFIX,
|
|
782
|
+
wallet: wrappedWallet
|
|
783
|
+
});
|
|
784
|
+
hotWalletAddress = getHotWalletAddress(wrappedWallet);
|
|
785
|
+
hotWalletUnlocked = true;
|
|
786
|
+
hotWalletBalanceLamports = null;
|
|
787
|
+
return hotWalletAddress;
|
|
788
|
+
}
|
|
789
|
+
catch (err) {
|
|
790
|
+
const nextError = err instanceof Error ? err : new Error('Failed to create hot wallet');
|
|
791
|
+
setHotWalletError(nextError);
|
|
792
|
+
throw nextError;
|
|
793
|
+
}
|
|
794
|
+
finally {
|
|
795
|
+
signature.fill(0);
|
|
796
|
+
hotWalletBusy = false;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
async function unlockHotWalletVault() {
|
|
800
|
+
const ownerAddress = getConnectedOwnerAddress();
|
|
801
|
+
const signature = await signHotWalletUnlockPayload(ownerAddress);
|
|
802
|
+
hotWalletBusy = true;
|
|
803
|
+
setHotWalletError(null);
|
|
804
|
+
try {
|
|
805
|
+
const { domain } = getHotWalletDomain();
|
|
806
|
+
const wrappedWallet = getStoredHotWallet(ownerAddress);
|
|
807
|
+
if (!wrappedWallet) {
|
|
808
|
+
throw new Error('No hot wallet vault stored for this wallet');
|
|
809
|
+
}
|
|
810
|
+
unlockedHotWallet = await unlockHotWalletFromSignature({
|
|
811
|
+
chainId: getCurrentChain(),
|
|
812
|
+
domain,
|
|
813
|
+
walletAddress: ownerAddress,
|
|
814
|
+
walletSignature: signature,
|
|
815
|
+
wrappedWallet
|
|
816
|
+
});
|
|
817
|
+
hotWalletAddress = getHotWalletAddress(wrappedWallet);
|
|
818
|
+
hotWalletUnlocked = true;
|
|
819
|
+
return hotWalletAddress;
|
|
820
|
+
}
|
|
821
|
+
catch (err) {
|
|
822
|
+
const nextError = err instanceof Error ? err : new Error('Failed to unlock hot wallet');
|
|
823
|
+
setHotWalletError(nextError);
|
|
824
|
+
throw nextError;
|
|
825
|
+
}
|
|
826
|
+
finally {
|
|
827
|
+
signature.fill(0);
|
|
828
|
+
hotWalletBusy = false;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
async function refreshHotWalletBalance() {
|
|
832
|
+
if (!hotWalletAddress) {
|
|
833
|
+
throw new Error('Create or unlock a hot wallet first');
|
|
834
|
+
}
|
|
835
|
+
hotWalletBusy = true;
|
|
836
|
+
setHotWalletError(null);
|
|
837
|
+
try {
|
|
838
|
+
hotWalletBalanceLamports = await getSolanaBalance(rpcEndpoint, hotWalletAddress);
|
|
839
|
+
return hotWalletBalanceLamports;
|
|
840
|
+
}
|
|
841
|
+
catch (err) {
|
|
842
|
+
const nextError = err instanceof Error ? err : new Error('Failed to refresh hot wallet balance');
|
|
843
|
+
setHotWalletError(nextError);
|
|
844
|
+
throw nextError;
|
|
845
|
+
}
|
|
846
|
+
finally {
|
|
847
|
+
hotWalletBusy = false;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
async function topUpHotWallet(amountSol = DEFAULT_HOT_WALLET_TOP_UP_SOL) {
|
|
851
|
+
const ownerAddress = getConnectedOwnerAddress();
|
|
852
|
+
if (!hotWalletAddress) {
|
|
853
|
+
await createHotWalletVault();
|
|
854
|
+
}
|
|
855
|
+
if (!hotWalletAddress) {
|
|
856
|
+
throw new Error('Create a hot wallet first');
|
|
857
|
+
}
|
|
858
|
+
hotWalletBusy = true;
|
|
859
|
+
setHotWalletError(null);
|
|
860
|
+
try {
|
|
861
|
+
const amountLamports = solToLamports(amountSol);
|
|
862
|
+
const { transaction } = createSystemTransferTransaction({
|
|
863
|
+
blockhash: await getLatestSolanaBlockhash(rpcEndpoint),
|
|
864
|
+
fromAddress: ownerAddress,
|
|
865
|
+
lamports: amountLamports,
|
|
866
|
+
toAddress: hotWalletAddress
|
|
867
|
+
});
|
|
868
|
+
const signedOrSignature = await signOwnerTransfer(transaction);
|
|
869
|
+
const signature = typeof signedOrSignature === 'string'
|
|
870
|
+
? signedOrSignature
|
|
871
|
+
: await sendSerializedSolanaTransaction(rpcEndpoint, signedOrSignature);
|
|
872
|
+
await refreshHotWalletBalance();
|
|
873
|
+
return { address: hotWalletAddress, amountLamports, signature };
|
|
874
|
+
}
|
|
875
|
+
catch (err) {
|
|
876
|
+
const nextError = err instanceof Error ? err : new Error('Failed to top up hot wallet');
|
|
877
|
+
setHotWalletError(nextError);
|
|
878
|
+
throw nextError;
|
|
879
|
+
}
|
|
880
|
+
finally {
|
|
881
|
+
hotWalletBusy = false;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
async function withdrawHotWallet(amountSol = DEFAULT_HOT_WALLET_WITHDRAW_SOL) {
|
|
885
|
+
const ownerAddress = getConnectedOwnerAddress();
|
|
886
|
+
if (!unlockedHotWallet) {
|
|
887
|
+
await unlockHotWalletVault();
|
|
888
|
+
}
|
|
889
|
+
if (!unlockedHotWallet) {
|
|
890
|
+
throw new Error('Unlock the hot wallet first');
|
|
891
|
+
}
|
|
892
|
+
hotWalletBusy = true;
|
|
893
|
+
setHotWalletError(null);
|
|
894
|
+
try {
|
|
895
|
+
const amountLamports = solToLamports(amountSol);
|
|
896
|
+
const address = getHotWalletAddress(unlockedHotWallet);
|
|
897
|
+
const { messageBytes, transaction } = createSystemTransferTransaction({
|
|
898
|
+
blockhash: await getLatestSolanaBlockhash(rpcEndpoint),
|
|
899
|
+
fromAddress: address,
|
|
900
|
+
lamports: amountLamports,
|
|
901
|
+
toAddress: ownerAddress
|
|
902
|
+
});
|
|
903
|
+
const signature = await signWithHotWallet({
|
|
904
|
+
message: messageBytes,
|
|
905
|
+
privateKey: unlockedHotWallet.privateKey
|
|
906
|
+
});
|
|
907
|
+
const tx = await sendSerializedSolanaTransaction(rpcEndpoint, applySolanaTransactionSignature(transaction, signature));
|
|
908
|
+
await refreshHotWalletBalance();
|
|
909
|
+
return { address, amountLamports, signature: tx };
|
|
910
|
+
}
|
|
911
|
+
catch (err) {
|
|
912
|
+
const nextError = err instanceof Error ? err : new Error('Failed to withdraw hot wallet funds');
|
|
913
|
+
setHotWalletError(nextError);
|
|
914
|
+
throw nextError;
|
|
915
|
+
}
|
|
916
|
+
finally {
|
|
917
|
+
hotWalletBusy = false;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
async function createHotWalletFundingUrl(optionId) {
|
|
921
|
+
const ownerAddress = getConnectedOwnerAddress();
|
|
922
|
+
if (!hotWalletAddress) {
|
|
923
|
+
await createHotWalletVault();
|
|
924
|
+
}
|
|
925
|
+
if (!hotWalletAddress) {
|
|
926
|
+
throw new Error('Create a hot wallet first');
|
|
927
|
+
}
|
|
928
|
+
const option = hotWalletFundingOptions.find((candidate) => candidate.id === optionId);
|
|
929
|
+
if (!option) {
|
|
930
|
+
throw new Error('Funding option is not available');
|
|
931
|
+
}
|
|
932
|
+
if (option.url) {
|
|
933
|
+
return option.url;
|
|
934
|
+
}
|
|
935
|
+
const provider = findFundingProvider(optionId);
|
|
936
|
+
if (provider?.type !== 'onramper') {
|
|
937
|
+
throw new Error('Funding option has no URL');
|
|
938
|
+
}
|
|
939
|
+
return await provider.createSignedUrl({
|
|
940
|
+
chain: getCurrentChain(),
|
|
941
|
+
hotWalletAddress,
|
|
942
|
+
ownerAddress
|
|
943
|
+
});
|
|
944
|
+
}
|
|
585
945
|
// ============================================================================
|
|
586
946
|
// Return Public Interface
|
|
587
947
|
// ============================================================================
|
|
@@ -656,6 +1016,39 @@ export function createWalletStore() {
|
|
|
656
1016
|
get mobileWalletAdapterDisplayName() {
|
|
657
1017
|
return MOBILE_WALLET_ADAPTER_DISPLAY_NAME;
|
|
658
1018
|
},
|
|
1019
|
+
get lastUsedWalletName() {
|
|
1020
|
+
return lastUsedWalletName;
|
|
1021
|
+
},
|
|
1022
|
+
get hotWalletAddress() {
|
|
1023
|
+
return hotWalletAddress;
|
|
1024
|
+
},
|
|
1025
|
+
get hotWalletBalanceLamports() {
|
|
1026
|
+
return hotWalletBalanceLamports;
|
|
1027
|
+
},
|
|
1028
|
+
get hotWalletBalance() {
|
|
1029
|
+
return lamportsToSol(hotWalletBalanceLamports);
|
|
1030
|
+
},
|
|
1031
|
+
get hotWalletBusy() {
|
|
1032
|
+
return hotWalletBusy;
|
|
1033
|
+
},
|
|
1034
|
+
get hotWalletError() {
|
|
1035
|
+
return hotWalletError;
|
|
1036
|
+
},
|
|
1037
|
+
get hotWalletUnlocked() {
|
|
1038
|
+
return hotWalletUnlocked;
|
|
1039
|
+
},
|
|
1040
|
+
get hotWalletDefaultTopUpSol() {
|
|
1041
|
+
return DEFAULT_HOT_WALLET_TOP_UP_SOL;
|
|
1042
|
+
},
|
|
1043
|
+
get hotWalletDefaultWithdrawSol() {
|
|
1044
|
+
return DEFAULT_HOT_WALLET_WITHDRAW_SOL;
|
|
1045
|
+
},
|
|
1046
|
+
get walletAccounts() {
|
|
1047
|
+
return standardWallet?.wallet.accounts ?? [];
|
|
1048
|
+
},
|
|
1049
|
+
get hotWalletFundingOptions() {
|
|
1050
|
+
return hotWalletFundingOptions;
|
|
1051
|
+
},
|
|
659
1052
|
// Methods
|
|
660
1053
|
initialize,
|
|
661
1054
|
refreshAvailableWallets,
|
|
@@ -665,8 +1058,15 @@ export function createWalletStore() {
|
|
|
665
1058
|
connectWalletBrowser,
|
|
666
1059
|
disconnect,
|
|
667
1060
|
signMessage,
|
|
1061
|
+
createHotWalletVault,
|
|
1062
|
+
unlockHotWalletVault,
|
|
1063
|
+
refreshHotWalletBalance,
|
|
1064
|
+
topUpHotWallet,
|
|
1065
|
+
withdrawHotWallet,
|
|
1066
|
+
createHotWalletFundingUrl,
|
|
668
1067
|
onWalletChange,
|
|
669
|
-
clearError: () => setError(null)
|
|
1068
|
+
clearError: () => setError(null),
|
|
1069
|
+
clearHotWalletError: () => setHotWalletError(null)
|
|
670
1070
|
};
|
|
671
1071
|
}
|
|
672
1072
|
export const wallet = createWalletStore();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fernolab/wallet-adapter-svelte",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Lean Svelte wallet adapter UI for Solana Wallet Standard providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -51,19 +51,19 @@
|
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@sveltejs/adapter-auto": "^7.0.1",
|
|
54
|
-
"@sveltejs/kit": "^2.
|
|
55
|
-
"@sveltejs/package": "^2.5.
|
|
54
|
+
"@sveltejs/kit": "^2.66.0",
|
|
55
|
+
"@sveltejs/package": "^2.5.8",
|
|
56
56
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
57
|
-
"@types/node": "^
|
|
58
|
-
"svelte": "^5.
|
|
59
|
-
"svelte-check": "^4.
|
|
57
|
+
"@types/node": "^26.0.0",
|
|
58
|
+
"svelte": "^5.56.3",
|
|
59
|
+
"svelte-check": "^4.6.0",
|
|
60
60
|
"typescript": "^6.0.3",
|
|
61
|
-
"vite": "^8.0.
|
|
62
|
-
"vitest": "^4.1.
|
|
61
|
+
"vite": "^8.0.16",
|
|
62
|
+
"vitest": "^4.1.9"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@fernolab/wallet-adapter-core": "^0.1.
|
|
66
|
-
"@wallet-standard/app": "^1.1.
|
|
65
|
+
"@fernolab/wallet-adapter-core": "^0.1.1",
|
|
66
|
+
"@wallet-standard/app": "^1.1.1"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
69
|
"svelte": "^5.0.0"
|