Skip to content

Commit cef6bbc

Browse files
authored
Merge pull request #87 from olekon/fordefi-wallet-module
Feat: Fordefi wallet module
2 parents 5a0786f + f7f7da0 commit cef6bbc

4 files changed

Lines changed: 212 additions & 0 deletions

File tree

src/build_npm.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ await build({
3232
name: "./modules/bitget",
3333
path: "./sdk/modules/bitget.module.ts",
3434
},
35+
{
36+
name: "./modules/fordefi",
37+
path: "./sdk/modules/fordefi.module.ts",
38+
},
3539
{
3640
name: "./modules/freighter",
3741
path: "./sdk/modules/freighter.module.ts",

src/deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"./sdk": "./sdk/mod.ts",
4141
"./modules/albedo": "./sdk/modules/albedo.module.ts",
4242
"./modules/bitget": "./sdk/modules/bitget.module.ts",
43+
"./modules/fordefi": "./sdk/modules/fordefi.module.ts",
4344
"./modules/freighter": "./sdk/modules/freighter.module.ts",
4445
"./modules/hana": "./sdk/modules/hana.module.ts",
4546
"./modules/hotwallet": "./sdk/modules/hotwallet.module.ts",

src/sdk/modules/fordefi.module.ts

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import { type ModuleInterface, ModuleType } from "../../types/mod.ts";
2+
import { parseError } from "../utils.ts";
3+
4+
/**
5+
* Fordefi Wallet module for Stellar Wallets Kit.
6+
*
7+
* The Fordefi browser extension can optionally impersonate the Freighter wallet
8+
* (controlled by a user-facing toggle in extension settings). When impersonation
9+
* is enabled, the extension listens for the standard Freighter postMessage
10+
* protocol (FREIGHTER_EXTERNAL_MSG_REQUEST / FREIGHTER_EXTERNAL_MSG_RESPONSE)
11+
* so that dapps using @stellar/freighter-api work transparently.
12+
*
13+
* This module reuses that same postMessage protocol to communicate with the
14+
* extension, while detecting Fordefi specifically via window.FordefiProviders
15+
* (which the extension always injects regardless of impersonation settings).
16+
* This allows the kit to show "Fordefi" as a distinct wallet option even when
17+
* Freighter impersonation is active.
18+
*/
19+
20+
const FREIGHTER_EXTERNAL_MSG_REQUEST = "FREIGHTER_EXTERNAL_MSG_REQUEST";
21+
const FREIGHTER_EXTERNAL_MSG_RESPONSE = "FREIGHTER_EXTERNAL_MSG_RESPONSE";
22+
23+
declare const window: Window &
24+
typeof globalThis & {
25+
isFordefi?: boolean;
26+
FordefiProviders?: {
27+
StellarProvider?: unknown;
28+
};
29+
};
30+
31+
let messageCounter = 0;
32+
33+
function sendFreighterMessage<T>(
34+
type: string,
35+
params?: Record<string, unknown>
36+
): Promise<T> {
37+
return new Promise((resolve, reject) => {
38+
const messageId = ++messageCounter;
39+
40+
const handler = (event: MessageEvent) => {
41+
if (event.source !== window) return;
42+
if (event.data?.source !== FREIGHTER_EXTERNAL_MSG_RESPONSE) return;
43+
// Freighter uses "messagedId" (typo with extra 'd') in responses
44+
if (event.data?.messagedId !== messageId) return;
45+
46+
window.removeEventListener("message", handler);
47+
48+
if (event.data.apiError) {
49+
reject(event.data.apiError);
50+
} else {
51+
resolve(event.data as T);
52+
}
53+
};
54+
55+
window.addEventListener("message", handler);
56+
57+
window.postMessage(
58+
{
59+
source: FREIGHTER_EXTERNAL_MSG_REQUEST,
60+
messageId,
61+
type,
62+
...params,
63+
},
64+
window.location.origin
65+
);
66+
});
67+
}
68+
69+
export const FORDEFI_ID = "fordefi";
70+
71+
export class FordefiModule implements ModuleInterface {
72+
moduleType: ModuleType = ModuleType.HOT_WALLET;
73+
74+
productId: string = FORDEFI_ID;
75+
productName: string = "Fordefi";
76+
productUrl: string = "https://www.fordefi.com";
77+
productIcon: string = "https://stellar.creit.tech/wallet-icons/fordefi.png";
78+
79+
async runChecks(): Promise<void> {
80+
if (!(await this.isAvailable())) {
81+
throw new Error("Fordefi is not installed");
82+
}
83+
}
84+
85+
async isAvailable(): Promise<boolean> {
86+
return (
87+
typeof window !== "undefined" &&
88+
!!window.FordefiProviders?.StellarProvider
89+
);
90+
}
91+
92+
async getAddress(): Promise<{ address: string }> {
93+
try {
94+
await this.runChecks();
95+
96+
const { publicKey } = await sendFreighterMessage<{ publicKey: string }>(
97+
"REQUEST_ACCESS"
98+
);
99+
100+
if (!publicKey) {
101+
return Promise.reject({
102+
code: -3,
103+
message: "Failed to get address from Fordefi.",
104+
});
105+
}
106+
107+
return { address: publicKey };
108+
} catch (e) {
109+
throw parseError(e);
110+
}
111+
}
112+
113+
async signTransaction(
114+
xdr: string,
115+
opts?: {
116+
networkPassphrase?: string;
117+
address?: string;
118+
path?: string;
119+
}
120+
): Promise<{ signedTxXdr: string; signerAddress?: string }> {
121+
try {
122+
await this.runChecks();
123+
124+
const { signedTransaction, signerAddress } = await sendFreighterMessage<{
125+
signedTransaction: string;
126+
signerAddress: string;
127+
}>("SUBMIT_TRANSACTION", {
128+
transactionXdr: xdr,
129+
networkPassphrase: opts?.networkPassphrase,
130+
accountToSign: opts?.address,
131+
});
132+
133+
if (!signedTransaction) {
134+
return Promise.reject({
135+
code: -3,
136+
message: "Failed to sign transaction with Fordefi.",
137+
});
138+
}
139+
140+
return { signedTxXdr: signedTransaction, signerAddress };
141+
} catch (e) {
142+
throw parseError(e);
143+
}
144+
}
145+
146+
signAuthEntry(): Promise<{
147+
signedAuthEntry: string;
148+
signerAddress?: string;
149+
}> {
150+
return Promise.reject({
151+
code: -3,
152+
message: 'Fordefi does not support the "signAuthEntry" function',
153+
});
154+
}
155+
156+
async signMessage(
157+
message: string,
158+
opts?: {
159+
networkPassphrase?: string;
160+
address?: string;
161+
path?: string;
162+
}
163+
): Promise<{ signedMessage: string; signerAddress?: string }> {
164+
try {
165+
await this.runChecks();
166+
167+
const { signedBlob, signerAddress } = await sendFreighterMessage<{
168+
signedBlob: string | null;
169+
signerAddress: string;
170+
}>("SUBMIT_BLOB", {
171+
blob: message,
172+
networkPassphrase: opts?.networkPassphrase,
173+
accountToSign: opts?.address,
174+
});
175+
176+
if (!signedBlob) {
177+
return Promise.reject({
178+
code: -3,
179+
message: "Failed to sign message with Fordefi.",
180+
});
181+
}
182+
183+
return { signedMessage: signedBlob, signerAddress };
184+
} catch (e) {
185+
throw parseError(e);
186+
}
187+
}
188+
189+
async getNetwork(): Promise<{ network: string; networkPassphrase: string }> {
190+
try {
191+
await this.runChecks();
192+
193+
const { networkDetails } = await sendFreighterMessage<{
194+
networkDetails: { network: string; networkPassphrase: string };
195+
}>("REQUEST_NETWORK_DETAILS");
196+
197+
return {
198+
network: networkDetails.network,
199+
networkPassphrase: networkDetails.networkPassphrase,
200+
};
201+
} catch (e) {
202+
throw parseError(e);
203+
}
204+
}
205+
}

src/sdk/modules/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { HanaModule } from "./hana.module.ts";
88
import { KleverModule } from "./klever.module.ts";
99
import { OneKeyModule } from "./onekey.module.ts";
1010
import { BitgetModule } from "./bitget.module.ts";
11+
import { FordefiModule } from "./fordefi.module.ts";
1112
import { CactusLinkModule } from "./cactuslink.module.ts";
1213

1314
/**
@@ -23,6 +24,7 @@ export function defaultModules(opts?: {
2324
const modules: ModuleInterface[] = [
2425
new AlbedoModule(),
2526
new FreighterModule(),
27+
new FordefiModule(),
2628
new RabetModule(),
2729
new xBullModule(),
2830
new LobstrModule(),

0 commit comments

Comments
 (0)