forked from CredenceOrg/Credence-Frontend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfreighterClient.ts
More file actions
219 lines (191 loc) · 7.41 KB
/
Copy pathfreighterClient.ts
File metadata and controls
219 lines (191 loc) · 7.41 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
/**
* @file freighterClient.ts
* @description Thin, SSR-safe wrapper around `@stellar/freighter-api`.
*
* Every export guards against a non-browser environment (`typeof window === 'undefined'`)
* and lazy-loads the Freighter SDK the first time it is needed, so importing this module
* has no side effects and is safe in SSR/test contexts. The wallet React state machine in
* `src/hooks/useWallet.ts` builds on these primitives; prefer that hook in components.
*/
/** Official Freighter browser extension install page. */
export const FREIGHTER_INSTALL_URL = 'https://www.freighter.app/'
import type { CredenceNetwork } from './networkLabels'
export type { CredenceNetwork } from './networkLabels'
type FreighterModule = typeof import('@stellar/freighter-api')
let freighterModule: FreighterModule | null = null
/**
* Lazily imports `@stellar/freighter-api`, caching the module after first load.
* Returns `null` outside a browser (SSR), so callers can short-circuit safely.
*/
async function loadFreighter(): Promise<FreighterModule | null> {
if (typeof window === 'undefined') return null
if (!freighterModule) {
freighterModule = await import('@stellar/freighter-api')
}
return freighterModule
}
export function mapFreighterNetwork(freighterNetwork: string): CredenceNetwork | null {
const normalized = freighterNetwork.trim().toUpperCase()
if (normalized.includes('TEST')) return 'test'
if (normalized.includes('PUBLIC') || normalized.includes('MAIN')) return 'public'
return null
}
/**
* Reports whether the Freighter extension is present and reachable.
*
* @returns `true` only when the SDK loads (browser) and reports a healthy connection.
*/
export async function checkFreighterInstalled(): Promise<boolean> {
try {
const freighter = await loadFreighter()
if (!freighter) return false
const result = await freighter.isConnected()
return result.isConnected === true && !result.error
} catch {
return false
}
}
/**
* Prompts the user (via Freighter) to grant access and returns the connected address.
*
* Never throws — failures are returned as a discriminated result so callers can map them
* to UI state. The `code` distinguishes a missing extension, a user-rejected prompt, and
* any other failure (best-effort detected by inspecting Freighter's error message).
*
* @returns `{ ok: true, address }` on success, otherwise `{ ok: false, code, message }`.
*/
export async function requestFreighterAccess(): Promise<
| { ok: true; address: string }
| { ok: false; code: 'not_installed' | 'rejected' | 'unknown'; message: string }
> {
const freighter = await loadFreighter()
if (!freighter) {
return {
ok: false,
code: 'not_installed',
message: 'Freighter is not available in this environment.',
}
}
const installed = await freighter.isConnected()
if (!installed.isConnected || installed.error) {
return { ok: false, code: 'not_installed', message: 'Freighter extension was not detected.' }
}
const access = await freighter.requestAccess()
if (access.error) {
const message = access.error.message || 'Connection request was rejected.'
const rejected =
access.error.message?.toLowerCase().includes('reject') ||
access.error.message?.toLowerCase().includes('denied') ||
access.error.message?.toLowerCase().includes('cancel')
return { ok: false, code: rejected ? 'rejected' : 'unknown', message }
}
if (!access.address) {
return { ok: false, code: 'unknown', message: 'Freighter did not return a wallet address.' }
}
return { ok: true, address: access.address }
}
/**
* Reads the already-authorized address without prompting, for silent session restore.
*
* @returns The public key when Freighter has previously granted access, else `null`.
*/
export async function fetchFreighterAddress(): Promise<string | null> {
const freighter = await loadFreighter()
if (!freighter) return null
const allowed = await freighter.isAllowed()
if (!allowed.isAllowed || allowed.error) return null
const addressResult = await freighter.getAddress()
if (addressResult.error || !addressResult.address) return null
return addressResult.address
}
/**
* Reads Freighter's currently selected network, mapped to a {@link CredenceNetwork}.
*
* @returns The mapped network, or `null` if unavailable or unrecognized.
*/
export async function fetchFreighterNetwork(): Promise<CredenceNetwork | null> {
const freighter = await loadFreighter()
if (!freighter) return null
const networkResult = await freighter.getNetwork()
if (networkResult.error || !networkResult.network) return null
return mapFreighterNetwork(networkResult.network)
}
/**
* Subscribes to Freighter account/network changes (e.g. the user switches accounts).
*
* The caller **must** invoke the returned `stop()` to remove the watcher and avoid leaks
* (`useWallet` does this on disconnect and unmount). Errors emitted by the watcher are
* swallowed; only successful change events reach `onChange`.
*
* @param onChange - Invoked with the new address and mapped network on each change.
* @returns A `{ stop }` handle, or `null` outside a browser.
*/
export async function createWalletWatcher(
onChange: (params: { address: string; network: CredenceNetwork | null }) => void
): Promise<{ stop: () => void } | null> {
const freighter = await loadFreighter()
if (!freighter) return null
const watcher = new freighter.WatchWalletChanges()
watcher.watch((params) => {
if (params.error) return
onChange({
address: params.address,
network: mapFreighterNetwork(params.network),
})
})
return {
stop: () => watcher.stop(),
}
}
/**
* Prompts the user (via Freighter) to sign a Stellar transaction XDR.
*
* Never throws — failures are returned as a discriminated result so callers can map them
* to UI state. The `code` distinguishes a missing extension from a user rejection and
* any other failure.
*
* @param xdr - The transaction envelope XDR to sign.
* @param opts - Optional network passphrase or address hint.
* @returns `{ ok: true, signedTxXdr }` on success, otherwise `{ ok: false, code, message }`.
*/
export async function signFreighterTransaction(
xdr: string,
opts?: { networkPassphrase?: string; address?: string }
): Promise<
| { ok: true; signedTxXdr: string }
| { ok: false; code: 'not_installed' | 'rejected' | 'unknown'; message: string }
> {
const freighter = await loadFreighter()
if (!freighter) {
return {
ok: false,
code: 'not_installed',
message: 'Freighter is not available in this environment.',
}
}
try {
const result = await freighter.signTransaction(xdr, opts)
if (result.error) {
const message = result.error || 'Signing request was rejected.'
const rejected =
result.error.toLowerCase().includes('reject') ||
result.error.toLowerCase().includes('denied') ||
result.error.toLowerCase().includes('cancel')
return { ok: false, code: rejected ? 'rejected' : 'unknown', message }
}
if (!result.signedTxXdr) {
return { ok: false, code: 'unknown', message: 'Freighter did not return a signed transaction.' }
}
return { ok: true, signedTxXdr: result.signedTxXdr }
} catch {
return {
ok: false,
code: 'unknown',
message: 'Unable to sign the transaction. Please try again.',
}
}
}
/** Resets the cached module — for tests only. */
export function resetFreighterModuleCache(): void {
freighterModule = null
}