@@ -6,6 +6,57 @@ export interface FreighterSignResponse {
66 publicKey : string ;
77}
88
9+ export type SignerErrorCode =
10+ | "WALLET_UNAVAILABLE"
11+ | "USER_REJECTED"
12+ | "INVALID_XDR"
13+ | "NETWORK_MISMATCH"
14+ | "SUBMISSION_FAILED"
15+ | "PUBLIC_KEY_FETCH_FAILED"
16+ | "UNKNOWN" ;
17+
18+ export class TransactionSignerError extends Error {
19+ constructor (
20+ message : string ,
21+ public readonly code : SignerErrorCode ,
22+ public readonly cause ?: unknown ,
23+ ) {
24+ super ( message ) ;
25+ this . name = "TransactionSignerError" ;
26+ }
27+ }
28+
29+ function classifyFreighterError ( err : unknown ) : TransactionSignerError {
30+ const msg = err instanceof Error ? err . message : String ( err ) ;
31+
32+ if ( / u s e r d e c l i n e d | u s e r r e j e c t e d | c a n c e l l e d / i. test ( msg ) ) {
33+ return new TransactionSignerError (
34+ "User rejected the signing request in Freighter." ,
35+ "USER_REJECTED" ,
36+ err ,
37+ ) ;
38+ }
39+ if ( / n e t w o r k | p a s s p h r a s e / i. test ( msg ) ) {
40+ return new TransactionSignerError (
41+ "Network passphrase mismatch between app and Freighter wallet." ,
42+ "NETWORK_MISMATCH" ,
43+ err ,
44+ ) ;
45+ }
46+ if ( / x d r | t r a n s a c t i o n / i. test ( msg ) ) {
47+ return new TransactionSignerError (
48+ `Invalid transaction XDR: ${ msg } ` ,
49+ "INVALID_XDR" ,
50+ err ,
51+ ) ;
52+ }
53+ return new TransactionSignerError (
54+ `Freighter error: ${ msg } ` ,
55+ "UNKNOWN" ,
56+ err ,
57+ ) ;
58+ }
59+
960/**
1061 * Check if Freighter wallet is installed (not just allowed).
1162 * We check for installation separately from permission so the button
@@ -23,7 +74,7 @@ export async function isFreighterInstalled(): Promise<boolean> {
2374}
2475
2576/**
26- * Check if Freighter wallet is available and allowed
77+ * Check if Freighter wallet is available and allowed.
2778 */
2879export async function isFreighterAvailable ( ) : Promise < boolean > {
2980 return isFreighterInstalled ( ) ;
@@ -32,13 +83,25 @@ export async function isFreighterAvailable(): Promise<boolean> {
3283/**
3384 * Get the public key from Freighter wallet.
3485 * Calls setAllowed() first which triggers the Freighter permission popup.
86+ * Throws a typed TransactionSignerError on any failure.
3587 */
3688export async function getFreighterPublicKey ( ) : Promise < string > {
89+ const available = await isFreighterAvailable ( ) ;
90+ if ( ! available ) {
91+ throw new TransactionSignerError (
92+ "Freighter wallet is not installed or has not granted access." ,
93+ "WALLET_UNAVAILABLE" ,
94+ ) ;
95+ }
96+
3797 try {
3898 // setAllowed() triggers the Freighter popup asking user to approve the site
3999 const allowed = await freighter . setAllowed ( ) ;
40100 if ( ! allowed ) {
41- throw new Error ( "User denied Freighter access" ) ;
101+ throw new TransactionSignerError (
102+ "User denied Freighter access." ,
103+ "USER_REJECTED" ,
104+ ) ;
42105 }
43106
44107 const result = await freighter . getPublicKey ( ) ;
@@ -52,25 +115,44 @@ export async function getFreighterPublicKey(): Promise<string> {
52115 if ( ! obj . publicKey ) throw new Error ( "No public key returned from Freighter" ) ;
53116 return obj . publicKey ;
54117 } catch ( err ) {
55- throw new Error (
56- `Freighter: ${ err instanceof Error ? err . message : String ( err ) } `
118+ if ( err instanceof TransactionSignerError ) throw err ;
119+ throw new TransactionSignerError (
120+ `Failed to retrieve public key from Freighter wallet: ${ err instanceof Error ? err . message : String ( err ) } ` ,
121+ "PUBLIC_KEY_FETCH_FAILED" ,
122+ err ,
57123 ) ;
58124 }
59125}
60126
61127/**
62- * Sign a transaction with Freighter wallet
128+ * Sign a transaction XDR with Freighter, surfacing typed errors so callers
129+ * can handle user-rejected vs network-mismatch vs unknown failures distinctly.
63130 */
64131export async function signWithFreighter (
65132 transactionXDR : string ,
66- networkPassphrase : string
133+ networkPassphrase : string ,
67134) : Promise < FreighterSignResponse > {
135+ if ( ! transactionXDR ) {
136+ throw new TransactionSignerError (
137+ "transactionXDR must not be empty." ,
138+ "INVALID_XDR" ,
139+ ) ;
140+ }
141+
142+ const available = await isFreighterAvailable ( ) ;
143+ if ( ! available ) {
144+ throw new TransactionSignerError (
145+ "Freighter wallet is not installed or has not granted access." ,
146+ "WALLET_UNAVAILABLE" ,
147+ ) ;
148+ }
149+
68150 try {
69151 const result = await freighter . signTransaction ( transactionXDR , {
70152 networkPassphrase,
71153 } ) ;
72154
73- // Handle both string return (old) and object return (new)
155+ // Handle both string return (old API ) and object return (new API )
74156 let signedXDR : string ;
75157 if ( typeof result === "string" ) {
76158 signedXDR = result ;
@@ -82,44 +164,61 @@ export async function signWithFreighter(
82164
83165 if ( ! signedXDR ) throw new Error ( "No signed XDR returned from Freighter" ) ;
84166
85- const pkResult = await freighter . getPublicKey ( ) ;
86- const publicKey = typeof pkResult === "string" ? pkResult : ( pkResult as { publicKey ?: string } ) ?. publicKey ?? "" ;
87-
167+ const publicKey = await getFreighterPublicKey ( ) ;
88168 return { signedXDR, publicKey } ;
89169 } catch ( err ) {
90- throw new Error (
91- `Freighter sign failed: ${ err instanceof Error ? err . message : String ( err ) } `
92- ) ;
170+ if ( err instanceof TransactionSignerError ) throw err ;
171+ throw classifyFreighterError ( err ) ;
93172 }
94173}
95174
96175/**
97- * Submit a signed transaction to Stellar network
176+ * Submit a signed transaction to the Stellar network with structured error
177+ * reporting on Horizon failures.
98178 */
99179export async function submitTransaction (
100180 signedXDR : string ,
101181 horizonUrl : string ,
102- networkPassphrase : string
182+ networkPassphrase : string ,
103183) : Promise < { hash : string } > {
184+ if ( ! signedXDR ) {
185+ throw new TransactionSignerError (
186+ "signedXDR must not be empty." ,
187+ "INVALID_XDR" ,
188+ ) ;
189+ }
190+
191+ let signedTx : StellarSdk . Transaction | StellarSdk . FeeBumpTransaction ;
104192 try {
105- const server = new StellarSdk . Horizon . Server ( horizonUrl ) ;
106- const signedTx = StellarSdk . TransactionBuilder . fromXDR (
107- signedXDR ,
108- networkPassphrase
193+ signedTx = StellarSdk . TransactionBuilder . fromXDR ( signedXDR , networkPassphrase ) ;
194+ } catch ( err ) {
195+ throw new TransactionSignerError (
196+ `Cannot parse signed XDR: ${ err instanceof Error ? err . message : String ( err ) } ` ,
197+ "INVALID_XDR" ,
198+ err ,
109199 ) ;
200+ }
110201
202+ try {
203+ const server = new StellarSdk . Horizon . Server ( horizonUrl ) ;
111204 const result = await server . submitTransaction ( signedTx ) ;
112-
205+
113206 if ( ! result . hash ) {
114- throw new Error ( "No transaction hash returned" ) ;
207+ throw new TransactionSignerError (
208+ "Horizon returned a response without a transaction hash." ,
209+ "SUBMISSION_FAILED" ,
210+ ) ;
115211 }
116212
117- return {
118- hash : result . hash ,
119- } ;
120- } catch ( error ) {
121- throw new Error (
122- `Failed to submit transaction: ${ error instanceof Error ? error . message : "Unknown error" } `
213+ return { hash : result . hash } ;
214+ } catch ( err ) {
215+ if ( err instanceof TransactionSignerError ) throw err ;
216+
217+ const msg = err instanceof Error ? err . message : String ( err ) ;
218+ throw new TransactionSignerError (
219+ `Transaction submission failed: ${ msg } ` ,
220+ "SUBMISSION_FAILED" ,
221+ err ,
123222 ) ;
124223 }
125224}
0 commit comments