@financedistrict/fdx 0.1.0

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.
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const LEVELS = { debug: 0, info: 1, warn: 2, error: 3, off: 4 };
8
+ const MAX_BYTES = 5 * 1024 * 1024; // rotate at 5 MB, keep one backup
9
+
10
+ class Logger {
11
+ constructor(opts = {}) {
12
+ this._logPathOverride = opts.logPath || null;
13
+ this._levelOverride = opts.level || null;
14
+ this._configured = false;
15
+ this._logPath = null;
16
+ this._level = LEVELS.info;
17
+ }
18
+
19
+ _configure() {
20
+ if (this._configured) return;
21
+ this._configured = true;
22
+ this._logPath =
23
+ this._logPathOverride ||
24
+ process.env.FDX_LOG_PATH ||
25
+ path.join(os.homedir(), '.fdx', 'fdx.log');
26
+ const levelStr = (this._levelOverride || process.env.FDX_LOG_LEVEL || 'info').toLowerCase();
27
+ this._level = LEVELS[levelStr] ?? LEVELS.info;
28
+ }
29
+
30
+ _write(level, message, data) {
31
+ this._configure();
32
+ if (LEVELS[level] < this._level) return;
33
+
34
+ const line = JSON.stringify({
35
+ ts: new Date().toISOString(),
36
+ level,
37
+ msg: message,
38
+ ...(data != null ? { data } : {}),
39
+ });
40
+
41
+ try {
42
+ this._maybeRotate();
43
+ fs.mkdirSync(path.dirname(this._logPath), { recursive: true });
44
+ fs.appendFileSync(this._logPath, line + '\n', { encoding: 'utf8' });
45
+ } catch {
46
+ // Logging must never crash the application
47
+ }
48
+ }
49
+
50
+ _maybeRotate() {
51
+ try {
52
+ const stat = fs.statSync(this._logPath);
53
+ if (stat.size >= MAX_BYTES) {
54
+ const backup = this._logPath + '.1';
55
+ try {
56
+ fs.unlinkSync(backup);
57
+ } catch {
58
+ // no existing backup — ignore
59
+ }
60
+ fs.renameSync(this._logPath, backup);
61
+ }
62
+ } catch {
63
+ // file may not exist yet — that is fine
64
+ }
65
+ }
66
+
67
+ debug(message, data) {
68
+ this._write('debug', message, data);
69
+ }
70
+ info(message, data) {
71
+ this._write('info', message, data);
72
+ }
73
+ warn(message, data) {
74
+ this._write('warn', message, data);
75
+ }
76
+ error(message, data) {
77
+ this._write('error', message, data);
78
+ }
79
+ }
80
+
81
+ // Singleton for production use; Logger class exported for testing
82
+ const logger = new Logger();
83
+ logger.Logger = Logger;
84
+
85
+ module.exports = logger;
@@ -0,0 +1,27 @@
1
+ const crypto = require('crypto');
2
+
3
+ function base64UrlEncode(buffer) {
4
+ return buffer.toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
5
+ }
6
+
7
+ function generateCodeVerifier(length) {
8
+ const size = length || 64;
9
+ const bounded = Math.max(43, Math.min(size, 128));
10
+ return base64UrlEncode(crypto.randomBytes(bounded));
11
+ }
12
+
13
+ function generateState(length) {
14
+ const size = length || 24;
15
+ return base64UrlEncode(crypto.randomBytes(size));
16
+ }
17
+
18
+ async function generateCodeChallenge(codeVerifier) {
19
+ const hash = crypto.createHash('sha256').update(codeVerifier).digest();
20
+ return base64UrlEncode(hash);
21
+ }
22
+
23
+ module.exports = {
24
+ generateCodeChallenge,
25
+ generateCodeVerifier,
26
+ generateState,
27
+ };
@@ -0,0 +1,275 @@
1
+ const { MCPAuthClient } = require('./mcp-auth');
2
+ const { MCPClient } = require('./mcp-client');
3
+
4
+ class WalletClient {
5
+ constructor({ mcpServerUrl, redirectUri, storePath, httpClient }) {
6
+ if (!mcpServerUrl) throw new Error('mcpServerUrl is required');
7
+ if (!redirectUri) throw new Error('redirectUri is required');
8
+ if (!storePath) throw new Error('storePath is required');
9
+
10
+ this.authClient = new MCPAuthClient({
11
+ mcpServerUrl,
12
+ redirectUri,
13
+ storePath,
14
+ httpClient,
15
+ });
16
+
17
+ this.mcpClient = new MCPClient({
18
+ mcpServerUrl,
19
+ authClient: this.authClient,
20
+ });
21
+ }
22
+
23
+ async initialize() {
24
+ return this.authClient.initialize();
25
+ }
26
+
27
+ async initializeForDevice() {
28
+ return this.authClient.initializeForDevice();
29
+ }
30
+
31
+ async getAuthorizationUrl() {
32
+ return this.authClient.getAuthorizationUrl();
33
+ }
34
+
35
+ async startDeviceFlow() {
36
+ return this.authClient.startDeviceFlow();
37
+ }
38
+
39
+ async pollDeviceToken({ deviceCode, interval }) {
40
+ return this.authClient.pollDeviceToken({ deviceCode, interval });
41
+ }
42
+
43
+ async exchangeCodeForToken({ code, state, codeVerifier }) {
44
+ return this.authClient.exchangeCodeForToken({ code, state, codeVerifier });
45
+ }
46
+
47
+ async getTokenState() {
48
+ return this.authClient.getTokenState();
49
+ }
50
+
51
+ async logout() {
52
+ return this.authClient.logout();
53
+ }
54
+
55
+ async listTools() {
56
+ return this.mcpClient.listTools();
57
+ }
58
+
59
+ async close() {
60
+ return this.mcpClient.close();
61
+ }
62
+
63
+ async getMyInfo() {
64
+ return this.mcpClient.callTool('getMyInfo', {});
65
+ }
66
+
67
+ async getAppVersion() {
68
+ return this.mcpClient.callTool('getAppVersion', {});
69
+ }
70
+
71
+ async helpNarrative({ question, locale, tone }) {
72
+ if (!question) throw new Error('question is required');
73
+
74
+ return this.mcpClient.callTool('helpNarrative', {
75
+ question,
76
+ locale,
77
+ tone,
78
+ });
79
+ }
80
+
81
+ async onboardingAssistant({ question, context, locale, tone }) {
82
+ if (!question) throw new Error('question is required');
83
+
84
+ return this.mcpClient.callTool('onboardingAssistant', {
85
+ question,
86
+ context,
87
+ locale,
88
+ tone,
89
+ });
90
+ }
91
+
92
+ async reportIssue({ title, description, severity, category }) {
93
+ if (!title) throw new Error('title is required');
94
+ if (!description) throw new Error('description is required');
95
+
96
+ return this.mcpClient.callTool('reportIssue', {
97
+ title,
98
+ description,
99
+ severity,
100
+ category,
101
+ });
102
+ }
103
+
104
+ async getWalletOverview({ chainKey, accountAddress }) {
105
+ return this.mcpClient.callTool('getWalletOverview', {
106
+ chainKey,
107
+ accountAddress,
108
+ });
109
+ }
110
+
111
+ async getAccountActivity({ chainKey, accountAddress, limit, offset }) {
112
+ return this.mcpClient.callTool('getAccountActivity', {
113
+ chainKey,
114
+ accountAddress,
115
+ limit,
116
+ offset,
117
+ });
118
+ }
119
+
120
+ async deploySmartAccount({ chainKey, initialOwners, threshold }) {
121
+ if (!chainKey) throw new Error('chainKey is required');
122
+
123
+ return this.mcpClient.callTool('deploySmartAccount', {
124
+ chainKey,
125
+ initialOwners,
126
+ threshold,
127
+ });
128
+ }
129
+
130
+ async manageSmartAccountOwnership({
131
+ chainKey,
132
+ accountAddress,
133
+ action,
134
+ ownerAddress,
135
+ newThreshold,
136
+ }) {
137
+ if (!chainKey) throw new Error('chainKey is required');
138
+ if (!accountAddress) throw new Error('accountAddress is required');
139
+ if (!action) throw new Error('action is required');
140
+
141
+ return this.mcpClient.callTool('manageSmartAccountOwnership', {
142
+ chainKey,
143
+ accountAddress,
144
+ action,
145
+ ownerAddress,
146
+ newThreshold,
147
+ });
148
+ }
149
+
150
+ async transferTokens({
151
+ chainKey,
152
+ fromAccountAddress,
153
+ recipientAddress,
154
+ amount,
155
+ tokenAddress,
156
+ memo,
157
+ maxPriorityFeePerGas,
158
+ maxFeePerGas,
159
+ }) {
160
+ if (!chainKey) throw new Error('chainKey is required');
161
+ if (!recipientAddress) throw new Error('recipientAddress is required');
162
+ if (!amount) throw new Error('amount is required');
163
+
164
+ return this.mcpClient.callTool('transferTokens', {
165
+ chainKey,
166
+ fromAccountAddress,
167
+ recipientAddress,
168
+ amount,
169
+ tokenAddress,
170
+ memo,
171
+ maxPriorityFeePerGas,
172
+ maxFeePerGas,
173
+ });
174
+ }
175
+
176
+ async swapTokens({
177
+ chainKey,
178
+ tokenIn,
179
+ tokenOut,
180
+ amount,
181
+ mode,
182
+ objective,
183
+ maxSlippageBps,
184
+ deadlineSeconds,
185
+ }) {
186
+ if (!chainKey) throw new Error('chainKey is required');
187
+ if (!tokenIn) throw new Error('tokenIn is required');
188
+ if (!tokenOut) throw new Error('tokenOut is required');
189
+ if (!amount) throw new Error('amount is required');
190
+
191
+ return this.mcpClient.callTool('swapTokens', {
192
+ chainKey,
193
+ tokenIn,
194
+ tokenOut,
195
+ amount,
196
+ mode,
197
+ objective,
198
+ maxSlippageBps,
199
+ deadlineSeconds,
200
+ });
201
+ }
202
+
203
+ async discoverYieldStrategies({ chainKey, tokenAddress, minApy, maxRisk, sortBy }) {
204
+ return this.mcpClient.callTool('discoverYieldStrategies', {
205
+ chainKey,
206
+ tokenAddress,
207
+ minApy,
208
+ maxRisk,
209
+ sortBy,
210
+ });
211
+ }
212
+
213
+ async depositForYield({ chainKey, strategyId, amount, tokenAddress }) {
214
+ if (!chainKey) throw new Error('chainKey is required');
215
+ if (!strategyId) throw new Error('strategyId is required');
216
+ if (!amount) throw new Error('amount is required');
217
+
218
+ return this.mcpClient.callTool('depositForYield', {
219
+ chainKey,
220
+ strategyId,
221
+ amount,
222
+ tokenAddress,
223
+ });
224
+ }
225
+
226
+ async withdrawFromYield({ chainKey, positionId, amount, recipient }) {
227
+ if (!chainKey) throw new Error('chainKey is required');
228
+ if (!positionId) throw new Error('positionId is required');
229
+
230
+ return this.mcpClient.callTool('withdrawFromYield', {
231
+ chainKey,
232
+ positionId,
233
+ amount,
234
+ recipient,
235
+ });
236
+ }
237
+
238
+ async authorizePayment({
239
+ url,
240
+ preferredNetwork,
241
+ preferredNetworkName,
242
+ preferredAsset,
243
+ maxPaymentAmount,
244
+ }) {
245
+ if (!url) throw new Error('url is required');
246
+
247
+ return this.mcpClient.callTool('authorizePayment', {
248
+ url,
249
+ preferredNetwork,
250
+ preferredNetworkName,
251
+ preferredAsset,
252
+ maxPaymentAmount,
253
+ });
254
+ }
255
+
256
+ async getX402Content({
257
+ url,
258
+ preferredNetwork,
259
+ preferredNetworkName,
260
+ preferredAsset,
261
+ maxPaymentAmount,
262
+ }) {
263
+ if (!url) throw new Error('url is required');
264
+
265
+ return this.mcpClient.callTool('getX402Content', {
266
+ url,
267
+ preferredNetwork,
268
+ preferredNetworkName,
269
+ preferredAsset,
270
+ maxPaymentAmount,
271
+ });
272
+ }
273
+ }
274
+
275
+ module.exports = { WalletClient };