forked from Creditra/Creditra-Frontend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwallet.ts
More file actions
270 lines (240 loc) · 9.37 KB
/
Copy pathwallet.ts
File metadata and controls
270 lines (240 loc) · 9.37 KB
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
import { WalletType, WalletInfo, WalletError } from '../types/wallet';
import { readJson, writeJson, removeKey } from './storage';
// ─── Storage keys ──────────────────────────────────────────────────────────────
//
// We namespace wallet-related state under `creditra-wallet-*` to avoid clashes
// with other apps sharing the same origin. All values are JSON-encodable so
// they can flow through the safe wrappers in `./storage` (private-mode safe,
// quota-safe).
//
// creditra-wallet-info — full `WalletInfo` of the active session
// (unchanged, kept for backward compat).
// creditra-wallet-recent — `WalletType[]` ordered MRU list.
// creditra-wallet-remember — `boolean` opt-in flag for auto-reconnect.
//
// Only wallet **type** strings are ever persisted — never addresses, keys, or
// signatures. This keeps the surface area limited to non-PII data.
const STORAGE_KEY_INFO = 'creditra-wallet-info';
const STORAGE_KEY_RECENT = 'creditra-wallet-recent';
const STORAGE_KEY_REMEMBER = 'creditra-wallet-remember';
const STORAGE_KEY_PREFERENCE_LEGACY = 'wallet_preference';
const STORAGE_KEY_INFO_LEGACY = 'wallet_info';
// Hard cap on MRU length — covers every supported wallet and then some, so the
// array stays small and the rewrite cost is trivial.
const MRU_MAX_LENGTH = 8;
const SUPPORTED_WALLET_TYPES: ReadonlySet<WalletType> = new Set([
'freighter',
'albedo',
'xbull',
'rabet',
]);
declare global {
interface Window {
freighter?: any;
albedo?: any;
xBullSDK?: any;
rabet?: any;
}
}
/**
* Detects whether a given Stellar wallet browser extension is currently
* installed and exposed on the window object.
*
* This is a synchronous, side-effect-free probe — it does not request user
* permission or trigger any UI from the wallet.
*/
export const isWalletInstalled = (type: WalletType): boolean => {
if (type === 'freighter') return !!window.freighter;
if (type === 'albedo') return !!window.albedo;
if (type === 'xbull') return !!window.xBullSDK;
if (type === 'rabet') return !!window.rabet;
return false;
};
/**
* Opens a connection to the requested Stellar wallet and returns the
* public key plus the network the wallet is reporting.
*
* Each supported wallet exposes a slightly different surface area, so this
* function normalises them into a single `WalletInfo` shape and throws a
* `WalletError` with a stable `type` discriminator on failure.
*/
export const connectWallet = async (type: WalletType): Promise<WalletInfo> => {
if (!isWalletInstalled(type)) {
const walletNames: Record<WalletType, string> = {
freighter: 'Freighter',
albedo: 'Albedo',
xbull: 'xBull',
rabet: 'Rabet'
};
throw {
type: 'not_found',
message: `${walletNames[type]} wallet not found. Please install the extension.`
} as WalletError;
}
try {
if (type === 'freighter') {
const publicKey = await window.freighter.getPublicKey();
const network = await window.freighter.getNetwork();
if (network !== 'PUBLIC' && network !== 'TESTNET') {
throw {
type: 'wrong_network',
message: 'Please switch to Stellar network in your wallet.'
} as WalletError;
}
return { type, publicKey, network };
}
if (type === 'albedo') {
const result = await window.albedo.publicKey({});
return {
type,
publicKey: result.pubkey,
network: 'PUBLIC'
};
}
if (type === 'xbull') {
const publicKey = await window.xBullSDK.getPublicKey();
return {
type,
publicKey,
network: 'PUBLIC'
};
}
if (type === 'rabet') {
const result = await window.rabet.connect();
return {
type,
publicKey: result.publicKey,
network: 'PUBLIC'
};
}
throw new Error('Unsupported wallet type');
} catch (error: any) {
if (error.type) throw error;
throw {
type: 'connection_failed',
message: error.message || 'Failed to connect wallet. Please try again.'
} as WalletError;
}
};
// ─── Connection lifecycle helpers ────────────────────────────────────────────
/**
* Drop the active session from localStorage. Also clears the opt-in
* "remember my choice" flag so a future return visit will not auto-connect.
*
* IMPORTANT: this call does **not** clear the MRU list (`creditra-wallet-recent`).
* The MRU ordering reflects the user's activity history, which we want to keep
* so the modal still surfaces preferred wallets in their natural order after a
* full disconnect. MRU entries are only dropped implicitly when the active
* session no longer makes sense — clearMRU() is the explicit-only escape hatch.
*
* Safe to call when nothing is stored (no-ops silently).
*/
export const disconnectWallet = () => {
removeKey(STORAGE_KEY_INFO);
removeKey(STORAGE_KEY_INFO_LEGACY);
removeKey(STORAGE_KEY_PREFERENCE_LEGACY);
removeKey(STORAGE_KEY_REMEMBER);
};
/**
* Explicitly wipe the MRU list. Not currently called by any user-facing
* control — exposed so future integrity tools or test helpers can reset
* recency state without affecting the active session.
*/
export const clearMRU = (): void => {
removeKey(STORAGE_KEY_RECENT);
};
/**
* Persist the connected wallet's full session record. Only the wallet type —
* never addresses, secrets, or signatures — ever leaves this module.
*/
export const saveWalletPreference = (walletInfo: WalletInfo) => {
writeJson(STORAGE_KEY_INFO, walletInfo);
// Mirror to the legacy key so anything still reading `wallet_info` keeps
// working through the upgrade window. Will be removed in a later release.
writeJson(STORAGE_KEY_INFO_LEGACY, walletInfo);
};
/**
* Read the active session. Falls back to the legacy `wallet_info` key for
* users who installed the application before `creditra-wallet-info` shipped.
*/
export const getStoredWallet = (): WalletInfo | null => {
const fromNew = readJson<WalletInfo | null>(STORAGE_KEY_INFO, null);
if (fromNew) return fromNew;
const fromLegacy = readJson<WalletInfo | null>(STORAGE_KEY_INFO_LEGACY, null);
return fromLegacy;
};
// ─── MRU (most-recently-used) bookkeeping ────────────────────────────────────
/**
* Validate a single entry of a (possibly corrupt) stored MRU list.
* Drops anything that doesn't match a supported wallet type. This is
* important because `readJson` will happily surface any JSON we wrote
* historically, including out-of-date values when the wallet set changes.
*/
function sanitizeMru(value: unknown): WalletType[] {
if (!Array.isArray(value)) return [];
const result: WalletType[] = [];
// Preserve insertion order (the array already encodes recency).
for (const entry of value) {
if (
typeof entry === 'string' &&
SUPPORTED_WALLET_TYPES.has(entry as WalletType) &&
!result.includes(entry as WalletType)
) {
result.push(entry as WalletType);
}
if (result.length >= MRU_MAX_LENGTH) break;
}
return result;
}
/**
* Read the MRU list. Last entry is the most-recently-used.
* Returns `[]` when nothing is stored or the stored value is malformed.
*/
export const getRecentWalletOrder = (): WalletType[] => {
return sanitizeMru(readJson<unknown>(STORAGE_KEY_RECENT, []));
};
/**
* Promote `type` to the end (most-recent) of the MRU list.
* - Removes any prior occurrence so the wallet never appears twice.
* - Caps the list at `MRU_MAX_LENGTH` entries.
* - Silent no-op on storage failure (uses the safe wrappers).
*/
export const recordRecentWallet = (type: WalletType): void => {
if (!SUPPORTED_WALLET_TYPES.has(type)) return;
const current = getRecentWalletOrder().filter((t) => t !== type);
current.push(type);
const trimmed = current.slice(-MRU_MAX_LENGTH);
writeJson(STORAGE_KEY_RECENT, trimmed);
};
// ─── "Remember my choice" opt-in flag ────────────────────────────────────────
/**
* `true` when the user explicitly opted in to next-visit auto-connect for
* their most-recently used wallet. Defaults to `false` so storage absence
* is indistinguishable from a fresh "no".
*/
export const isWalletRemembered = (): boolean => {
return readJson<boolean>(STORAGE_KEY_REMEMBER, false) === true;
};
/**
* Persist (or clear) the opt-in flag. When `false`, the key is removed so we
* do not pollute localStorage with a redundant "false".
*/
export const setWalletRemembered = (remember: boolean): void => {
if (remember) {
writeJson(STORAGE_KEY_REMEMBER, true);
} else {
removeKey(STORAGE_KEY_REMEMBER);
}
};
export const EXPECTED_NETWORK = 'TESTNET';
export const isSwitchSupported = (type: WalletType): boolean => {
if (type === 'freighter' && window.freighter?.switchNetwork) return true;
return false;
};
export const switchNetwork = async (type: WalletType, network: string): Promise<void> => {
if (type === 'freighter' && window.freighter?.switchNetwork) {
await window.freighter.switchNetwork(network);
return;
}
throw new Error(`Please open your ${type} wallet and switch to ${network}.`);
};