site-agent-pro 1.0.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.
- package/README.md +689 -0
- package/dist/auth/credentialStore.js +62 -0
- package/dist/auth/inbox.js +193 -0
- package/dist/auth/profile.js +379 -0
- package/dist/auth/runner.js +1124 -0
- package/dist/backend/dashboardData.js +194 -0
- package/dist/backend/runArtifacts.js +48 -0
- package/dist/backend/runRepository.js +93 -0
- package/dist/bin.js +2 -0
- package/dist/cli/backfillSiteChecks.js +143 -0
- package/dist/cli/run.js +309 -0
- package/dist/cli/trade.js +69 -0
- package/dist/config.js +199 -0
- package/dist/core/agentProfiles.js +55 -0
- package/dist/core/aggregateReport.js +382 -0
- package/dist/core/audit.js +30 -0
- package/dist/core/customTaskSuite.js +148 -0
- package/dist/core/evaluator.js +217 -0
- package/dist/core/executor.js +788 -0
- package/dist/core/fallbackReport.js +335 -0
- package/dist/core/formHeuristics.js +411 -0
- package/dist/core/gameplaySummary.js +164 -0
- package/dist/core/interaction.js +202 -0
- package/dist/core/pageState.js +201 -0
- package/dist/core/planner.js +1669 -0
- package/dist/core/processSubmissionBatch.js +204 -0
- package/dist/core/runAuditJob.js +170 -0
- package/dist/core/runner.js +2352 -0
- package/dist/core/siteBrief.js +107 -0
- package/dist/core/siteChecks.js +1526 -0
- package/dist/core/taskDirectives.js +279 -0
- package/dist/core/taskHeuristics.js +263 -0
- package/dist/dashboard/client.js +1256 -0
- package/dist/dashboard/contracts.js +95 -0
- package/dist/dashboard/narrative.js +277 -0
- package/dist/dashboard/server.js +458 -0
- package/dist/dashboard/theme.js +888 -0
- package/dist/index.js +84 -0
- package/dist/llm/client.js +188 -0
- package/dist/paystack/account.js +123 -0
- package/dist/paystack/client.js +100 -0
- package/dist/paystack/index.js +13 -0
- package/dist/paystack/test-paystack.js +83 -0
- package/dist/paystack/transfer.js +138 -0
- package/dist/paystack/types.js +74 -0
- package/dist/paystack/webhook.js +121 -0
- package/dist/prompts/browserAgent.js +124 -0
- package/dist/prompts/reviewer.js +71 -0
- package/dist/reporting/clickReplay.js +290 -0
- package/dist/reporting/html.js +930 -0
- package/dist/reporting/markdown.js +238 -0
- package/dist/reporting/template.js +1141 -0
- package/dist/schemas/types.js +361 -0
- package/dist/submissions/customTasks.js +196 -0
- package/dist/submissions/html.js +770 -0
- package/dist/submissions/model.js +56 -0
- package/dist/submissions/publicUrl.js +76 -0
- package/dist/submissions/service.js +74 -0
- package/dist/submissions/store.js +37 -0
- package/dist/submissions/types.js +65 -0
- package/dist/trade/engine.js +241 -0
- package/dist/trade/evm/erc20.js +44 -0
- package/dist/trade/extractor.js +148 -0
- package/dist/trade/policy.js +35 -0
- package/dist/trade/session.js +31 -0
- package/dist/trade/types.js +107 -0
- package/dist/trade/validator.js +148 -0
- package/dist/utils/files.js +59 -0
- package/dist/utils/log.js +24 -0
- package/dist/utils/playwrightCompat.js +14 -0
- package/dist/utils/time.js +3 -0
- package/dist/wallet/provider.js +345 -0
- package/dist/wallet/relay.js +129 -0
- package/dist/wallet/wallet.js +178 -0
- package/docs/01-installation.md +134 -0
- package/docs/02-running-your-first-audit.md +136 -0
- package/docs/03-configuration.md +233 -0
- package/docs/04-how-the-agent-thinks.md +41 -0
- package/docs/05-extending-personas-and-tasks.md +42 -0
- package/docs/06-hardening-for-production.md +92 -0
- package/package.json +60 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a JavaScript source string that, when evaluated in a browser page via
|
|
3
|
+
* `context.addInitScript()`, installs a minimal EIP-1193-compatible
|
|
4
|
+
* `window.ethereum` provider backed by the agent's server-side signing relay.
|
|
5
|
+
*
|
|
6
|
+
* The injected provider:
|
|
7
|
+
* - Auto-connects (returns the agent address for `eth_requestAccounts`)
|
|
8
|
+
* - Proxies read-only RPC calls to the configured JSON-RPC endpoint
|
|
9
|
+
* - Delegates signing operations to the local signing relay running on `relayPort`
|
|
10
|
+
* - Emits EIP-1193 events (`connect`, `accountsChanged`, `chainChanged`)
|
|
11
|
+
* - Reports `isMetaMask = true` for dApp compatibility
|
|
12
|
+
*/
|
|
13
|
+
export function buildWeb3InjectionScript(args) {
|
|
14
|
+
const { walletConfig, relayPort } = args;
|
|
15
|
+
const address = walletConfig.address.toLowerCase();
|
|
16
|
+
const chainIdHex = `0x${walletConfig.chainId.toString(16)}`;
|
|
17
|
+
const rpcUrl = walletConfig.rpcUrl;
|
|
18
|
+
const relayOrigin = `http://127.0.0.1:${relayPort}`;
|
|
19
|
+
// The entire script is a self-executing IIFE injected before any page JS runs.
|
|
20
|
+
return `(function() {
|
|
21
|
+
"use strict";
|
|
22
|
+
|
|
23
|
+
if (window.ethereum && window.ethereum.__siteAgentInjected) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
var AGENT_ADDRESS = ${JSON.stringify(address)};
|
|
28
|
+
var CHAIN_ID_HEX = ${JSON.stringify(chainIdHex)};
|
|
29
|
+
var CHAIN_ID_DEC = ${walletConfig.chainId};
|
|
30
|
+
var RPC_URL = ${JSON.stringify(rpcUrl)};
|
|
31
|
+
var RELAY_ORIGIN = ${JSON.stringify(relayOrigin)};
|
|
32
|
+
var connected = true;
|
|
33
|
+
var upstreamEthereum = window.ethereum && !window.ethereum.__siteAgentInjected ? window.ethereum : null;
|
|
34
|
+
|
|
35
|
+
/* ----- Event emitter ----- */
|
|
36
|
+
var listeners = {};
|
|
37
|
+
|
|
38
|
+
function on(event, fn) {
|
|
39
|
+
if (!listeners[event]) listeners[event] = [];
|
|
40
|
+
listeners[event].push(fn);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function removeListener(event, fn) {
|
|
44
|
+
if (!listeners[event]) return;
|
|
45
|
+
listeners[event] = listeners[event].filter(function(f) { return f !== fn; });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function emit(event) {
|
|
49
|
+
var args = Array.prototype.slice.call(arguments, 1);
|
|
50
|
+
(listeners[event] || []).forEach(function(fn) {
|
|
51
|
+
try { fn.apply(null, args); } catch(e) { console.warn("[site-agent-wallet] listener error:", e); }
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ----- JSON-RPC helpers ----- */
|
|
56
|
+
var rpcId = 1;
|
|
57
|
+
|
|
58
|
+
function rpcCall(method, params) {
|
|
59
|
+
return fetch(RPC_URL, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: { "Content-Type": "application/json" },
|
|
62
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: rpcId++, method: method, params: params || [] })
|
|
63
|
+
})
|
|
64
|
+
.then(function(res) { return res.json(); })
|
|
65
|
+
.then(function(json) {
|
|
66
|
+
if (json.error) throw new Error(json.error.message || "RPC error");
|
|
67
|
+
return json.result;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function relayCall(endpoint, body) {
|
|
72
|
+
return fetch(RELAY_ORIGIN + endpoint, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: { "Content-Type": "application/json" },
|
|
75
|
+
body: JSON.stringify(body)
|
|
76
|
+
})
|
|
77
|
+
.then(function(res) { return res.json(); })
|
|
78
|
+
.then(function(json) {
|
|
79
|
+
if (json.error) throw new Error(json.error);
|
|
80
|
+
return json.result;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function maybeCaptureUpstreamEthereum(candidate) {
|
|
85
|
+
if (!candidate || candidate === provider || candidate.__siteAgentInjected) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (
|
|
90
|
+
!upstreamEthereum ||
|
|
91
|
+
candidate.isMetaMask ||
|
|
92
|
+
(candidate._metamask && typeof candidate._metamask.isUnlocked === "function")
|
|
93
|
+
) {
|
|
94
|
+
upstreamEthereum = candidate;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getUpstreamEthereum() {
|
|
99
|
+
if (!upstreamEthereum && window.ethereum && window.ethereum !== provider && !window.ethereum.__siteAgentInjected) {
|
|
100
|
+
maybeCaptureUpstreamEthereum(window.ethereum);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!upstreamEthereum && window.ethereum && Array.isArray(window.ethereum.providers)) {
|
|
104
|
+
var announcedMetaMaskProvider = window.ethereum.providers.find(function(candidate) {
|
|
105
|
+
return candidate && candidate !== provider && (candidate.isMetaMask || candidate._metamask);
|
|
106
|
+
});
|
|
107
|
+
maybeCaptureUpstreamEthereum(announcedMetaMaskProvider);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return upstreamEthereum && upstreamEthereum !== provider ? upstreamEthereum : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* ----- EIP-1193 request handler ----- */
|
|
114
|
+
function request(args) {
|
|
115
|
+
var method = args.method;
|
|
116
|
+
var params = args.params || [];
|
|
117
|
+
var originalEthereum = getUpstreamEthereum();
|
|
118
|
+
|
|
119
|
+
switch (method) {
|
|
120
|
+
case "eth_requestAccounts":
|
|
121
|
+
case "eth_accounts":
|
|
122
|
+
if (originalEthereum && originalEthereum.request) {
|
|
123
|
+
return originalEthereum.request(args);
|
|
124
|
+
}
|
|
125
|
+
return Promise.resolve([AGENT_ADDRESS]);
|
|
126
|
+
|
|
127
|
+
case "eth_chainId":
|
|
128
|
+
if (originalEthereum && originalEthereum.request) {
|
|
129
|
+
return originalEthereum.request(args).catch(function() { return CHAIN_ID_HEX; });
|
|
130
|
+
}
|
|
131
|
+
return Promise.resolve(CHAIN_ID_HEX);
|
|
132
|
+
|
|
133
|
+
case "net_version":
|
|
134
|
+
if (originalEthereum && originalEthereum.request) {
|
|
135
|
+
return originalEthereum.request(args).catch(function() { return String(CHAIN_ID_DEC); });
|
|
136
|
+
}
|
|
137
|
+
return Promise.resolve(String(CHAIN_ID_DEC));
|
|
138
|
+
|
|
139
|
+
case "wallet_switchEthereumChain":
|
|
140
|
+
if (originalEthereum && originalEthereum.request) {
|
|
141
|
+
return originalEthereum.request(args);
|
|
142
|
+
}
|
|
143
|
+
/* Accept any chain switch silently — the relay always uses the configured chain */
|
|
144
|
+
emit("chainChanged", CHAIN_ID_HEX);
|
|
145
|
+
return Promise.resolve(null);
|
|
146
|
+
|
|
147
|
+
case "wallet_addEthereumChain":
|
|
148
|
+
if (originalEthereum && originalEthereum.request) {
|
|
149
|
+
return originalEthereum.request(args);
|
|
150
|
+
}
|
|
151
|
+
emit("chainChanged", CHAIN_ID_HEX);
|
|
152
|
+
return Promise.resolve(null);
|
|
153
|
+
|
|
154
|
+
case "wallet_requestPermissions":
|
|
155
|
+
if (originalEthereum && originalEthereum.request) {
|
|
156
|
+
return originalEthereum.request(args);
|
|
157
|
+
}
|
|
158
|
+
return Promise.resolve([{ parentCapability: "eth_accounts" }]);
|
|
159
|
+
|
|
160
|
+
case "wallet_watchAsset":
|
|
161
|
+
if (originalEthereum && originalEthereum.request) {
|
|
162
|
+
return originalEthereum.request(args);
|
|
163
|
+
}
|
|
164
|
+
return Promise.resolve(true);
|
|
165
|
+
|
|
166
|
+
case "eth_sendTransaction":
|
|
167
|
+
if (originalEthereum && originalEthereum.request) {
|
|
168
|
+
return originalEthereum.request(args);
|
|
169
|
+
}
|
|
170
|
+
return relayCall("/send-transaction", { tx: params[0] });
|
|
171
|
+
|
|
172
|
+
case "personal_sign":
|
|
173
|
+
if (originalEthereum && originalEthereum.request) {
|
|
174
|
+
return originalEthereum.request(args);
|
|
175
|
+
}
|
|
176
|
+
return relayCall("/sign-message", { message: params[0], address: params[1] });
|
|
177
|
+
|
|
178
|
+
case "eth_sign":
|
|
179
|
+
if (originalEthereum && originalEthereum.request) {
|
|
180
|
+
return originalEthereum.request(args);
|
|
181
|
+
}
|
|
182
|
+
return relayCall("/sign-message", { message: params[1], address: params[0] });
|
|
183
|
+
|
|
184
|
+
case "eth_signTypedData":
|
|
185
|
+
case "eth_signTypedData_v3":
|
|
186
|
+
case "eth_signTypedData_v4":
|
|
187
|
+
if (originalEthereum && originalEthereum.request) {
|
|
188
|
+
return originalEthereum.request(args);
|
|
189
|
+
}
|
|
190
|
+
var typedDataParam = typeof params[1] === "string" ? JSON.parse(params[1]) : params[1];
|
|
191
|
+
return relayCall("/sign-typed-data", { data: typedDataParam });
|
|
192
|
+
|
|
193
|
+
/* Read-only calls — proxy directly to the RPC endpoint */
|
|
194
|
+
case "eth_getBalance":
|
|
195
|
+
case "eth_blockNumber":
|
|
196
|
+
case "eth_call":
|
|
197
|
+
case "eth_estimateGas":
|
|
198
|
+
case "eth_gasPrice":
|
|
199
|
+
case "eth_getTransactionCount":
|
|
200
|
+
case "eth_getTransactionReceipt":
|
|
201
|
+
case "eth_getTransactionByHash":
|
|
202
|
+
case "eth_getCode":
|
|
203
|
+
case "eth_getStorageAt":
|
|
204
|
+
case "eth_getLogs":
|
|
205
|
+
case "eth_getBlockByNumber":
|
|
206
|
+
case "eth_getBlockByHash":
|
|
207
|
+
case "eth_feeHistory":
|
|
208
|
+
case "eth_maxPriorityFeePerGas":
|
|
209
|
+
return rpcCall(method, params);
|
|
210
|
+
|
|
211
|
+
default:
|
|
212
|
+
/* Attempt to proxy unknown methods to RPC — some dApps use non-standard calls */
|
|
213
|
+
return rpcCall(method, params);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* ----- Legacy send / sendAsync ----- */
|
|
218
|
+
function send(methodOrPayload, callbackOrParams) {
|
|
219
|
+
if (typeof methodOrPayload === "string") {
|
|
220
|
+
return request({ method: methodOrPayload, params: callbackOrParams || [] });
|
|
221
|
+
}
|
|
222
|
+
/* JSON-RPC payload object with callback */
|
|
223
|
+
if (typeof callbackOrParams === "function") {
|
|
224
|
+
request({ method: methodOrPayload.method, params: methodOrPayload.params || [] })
|
|
225
|
+
.then(function(result) { callbackOrParams(null, { id: methodOrPayload.id, jsonrpc: "2.0", result: result }); })
|
|
226
|
+
.catch(function(err) { callbackOrParams(err, null); });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
return request({ method: methodOrPayload.method, params: methodOrPayload.params || [] });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function sendAsync(payload, callback) {
|
|
233
|
+
request({ method: payload.method, params: payload.params || [] })
|
|
234
|
+
.then(function(result) { callback(null, { id: payload.id, jsonrpc: "2.0", result: result }); })
|
|
235
|
+
.catch(function(err) { callback(err, null); });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* ----- Provider object ----- */
|
|
239
|
+
var provider = {
|
|
240
|
+
isMetaMask: true,
|
|
241
|
+
__siteAgentInjected: true,
|
|
242
|
+
get chainId() {
|
|
243
|
+
var originalEthereum = getUpstreamEthereum();
|
|
244
|
+
return originalEthereum && originalEthereum.chainId ? originalEthereum.chainId : CHAIN_ID_HEX;
|
|
245
|
+
},
|
|
246
|
+
get networkVersion() {
|
|
247
|
+
var originalEthereum = getUpstreamEthereum();
|
|
248
|
+
return originalEthereum && originalEthereum.networkVersion ? originalEthereum.networkVersion : String(CHAIN_ID_DEC);
|
|
249
|
+
},
|
|
250
|
+
get selectedAddress() {
|
|
251
|
+
var originalEthereum = getUpstreamEthereum();
|
|
252
|
+
return originalEthereum && originalEthereum.selectedAddress ? originalEthereum.selectedAddress : AGENT_ADDRESS;
|
|
253
|
+
},
|
|
254
|
+
isConnected: function() {
|
|
255
|
+
var originalEthereum = getUpstreamEthereum();
|
|
256
|
+
return originalEthereum && typeof originalEthereum.isConnected === "function"
|
|
257
|
+
? originalEthereum.isConnected()
|
|
258
|
+
: connected;
|
|
259
|
+
},
|
|
260
|
+
request: request,
|
|
261
|
+
send: send,
|
|
262
|
+
sendAsync: sendAsync,
|
|
263
|
+
on: on,
|
|
264
|
+
removeListener: removeListener,
|
|
265
|
+
removeAllListeners: function(event) {
|
|
266
|
+
if (event) { listeners[event] = []; }
|
|
267
|
+
else { listeners = {}; }
|
|
268
|
+
},
|
|
269
|
+
/* Some dApps access these */
|
|
270
|
+
enable: function() { return request({ method: "eth_requestAccounts" }); },
|
|
271
|
+
_metamask: {
|
|
272
|
+
isUnlocked: function() {
|
|
273
|
+
var originalEthereum = getUpstreamEthereum();
|
|
274
|
+
if (originalEthereum && originalEthereum._metamask && typeof originalEthereum._metamask.isUnlocked === "function") {
|
|
275
|
+
return originalEthereum._metamask.isUnlocked();
|
|
276
|
+
}
|
|
277
|
+
return Promise.resolve(true);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
window.addEventListener("eip6963:announceProvider", function(event) {
|
|
284
|
+
var detail = event && event.detail ? event.detail : null;
|
|
285
|
+
if (!detail || detail.provider === provider) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
var info = detail.info || {};
|
|
290
|
+
if (info.rdns === "com.siteagent.wallet") {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (info.rdns === "io.metamask" || /metamask/i.test(info.name || "") || detail.provider.isMetaMask) {
|
|
295
|
+
maybeCaptureUpstreamEthereum(detail.provider);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
} catch(e) { /* Provider discovery listener is optional */ }
|
|
299
|
+
|
|
300
|
+
/* Announce the provider using EIP-6963 for modern dApps */
|
|
301
|
+
try {
|
|
302
|
+
window.dispatchEvent(new CustomEvent("eip6963:announceProvider", {
|
|
303
|
+
detail: Object.freeze({
|
|
304
|
+
info: {
|
|
305
|
+
uuid: "site-agent-wallet-00000000",
|
|
306
|
+
name: "Site Agent Wallet",
|
|
307
|
+
icon: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>",
|
|
308
|
+
rdns: "com.siteagent.wallet"
|
|
309
|
+
},
|
|
310
|
+
provider: provider
|
|
311
|
+
})
|
|
312
|
+
}));
|
|
313
|
+
/* Listen for discovery requests */
|
|
314
|
+
window.addEventListener("eip6963:requestProvider", function() {
|
|
315
|
+
window.dispatchEvent(new CustomEvent("eip6963:announceProvider", {
|
|
316
|
+
detail: Object.freeze({
|
|
317
|
+
info: {
|
|
318
|
+
uuid: "site-agent-wallet-00000000",
|
|
319
|
+
name: "Site Agent Wallet",
|
|
320
|
+
icon: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>",
|
|
321
|
+
rdns: "com.siteagent.wallet"
|
|
322
|
+
},
|
|
323
|
+
provider: provider
|
|
324
|
+
})
|
|
325
|
+
}));
|
|
326
|
+
});
|
|
327
|
+
} catch(e) { /* EIP-6963 not critical */ }
|
|
328
|
+
|
|
329
|
+
/* Install as window.ethereum */
|
|
330
|
+
Object.defineProperty(window, "ethereum", {
|
|
331
|
+
configurable: true,
|
|
332
|
+
enumerable: true,
|
|
333
|
+
get: function() { return provider; },
|
|
334
|
+
set: function(candidate) { maybeCaptureUpstreamEthereum(candidate); }
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
/* Fire initial connect event on next tick */
|
|
338
|
+
setTimeout(function() {
|
|
339
|
+
emit("connect", { chainId: CHAIN_ID_HEX });
|
|
340
|
+
emit("accountsChanged", [AGENT_ADDRESS]);
|
|
341
|
+
}, 0);
|
|
342
|
+
|
|
343
|
+
console.log("[site-agent-wallet] Injected Web3 provider — address:", AGENT_ADDRESS, "chain:", CHAIN_ID_DEC);
|
|
344
|
+
})();`;
|
|
345
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { signMessage, signTypedData, sendTransaction } from "./wallet.js";
|
|
3
|
+
import { debug } from "../utils/log.js";
|
|
4
|
+
/* ------------------------------------------------------------------ */
|
|
5
|
+
/* Request parsing helpers */
|
|
6
|
+
/* ------------------------------------------------------------------ */
|
|
7
|
+
function readBody(req) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const chunks = [];
|
|
10
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
11
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
12
|
+
req.on("error", reject);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function jsonResponse(res, statusCode, body) {
|
|
16
|
+
const payload = JSON.stringify(body);
|
|
17
|
+
res.writeHead(statusCode, {
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
20
|
+
"Access-Control-Allow-Origin": "*",
|
|
21
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
22
|
+
"Access-Control-Allow-Headers": "Content-Type"
|
|
23
|
+
});
|
|
24
|
+
res.end(payload);
|
|
25
|
+
}
|
|
26
|
+
/* ------------------------------------------------------------------ */
|
|
27
|
+
/* Route handlers */
|
|
28
|
+
/* ------------------------------------------------------------------ */
|
|
29
|
+
async function handleSignMessage(body) {
|
|
30
|
+
const message = body.message;
|
|
31
|
+
if (typeof message !== "string") {
|
|
32
|
+
throw new Error("Missing or invalid 'message' field.");
|
|
33
|
+
}
|
|
34
|
+
debug("signing relay: sign-message request");
|
|
35
|
+
return signMessage(message);
|
|
36
|
+
}
|
|
37
|
+
async function handleSignTypedData(body) {
|
|
38
|
+
const data = body.data;
|
|
39
|
+
if (!data || typeof data !== "object") {
|
|
40
|
+
throw new Error("Missing or invalid 'data' field.");
|
|
41
|
+
}
|
|
42
|
+
debug("signing relay: sign-typed-data request");
|
|
43
|
+
// EIP-712 typed data comes in { domain, types, message, primaryType }
|
|
44
|
+
const domain = data.domain ?? {};
|
|
45
|
+
const types = { ...data.types };
|
|
46
|
+
// Remove EIP712Domain from types — ethers adds it automatically
|
|
47
|
+
delete types["EIP712Domain"];
|
|
48
|
+
const value = data.message ?? {};
|
|
49
|
+
return signTypedData(domain, types, value);
|
|
50
|
+
}
|
|
51
|
+
async function handleSendTransaction(body) {
|
|
52
|
+
const tx = body.tx;
|
|
53
|
+
if (!tx || typeof tx !== "object") {
|
|
54
|
+
throw new Error("Missing or invalid 'tx' field.");
|
|
55
|
+
}
|
|
56
|
+
debug("signing relay: send-transaction request", { to: tx.to, value: tx.value });
|
|
57
|
+
return sendTransaction(tx);
|
|
58
|
+
}
|
|
59
|
+
/* ------------------------------------------------------------------ */
|
|
60
|
+
/* Server */
|
|
61
|
+
/* ------------------------------------------------------------------ */
|
|
62
|
+
export function startSigningRelay() {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const server = http.createServer(async (req, res) => {
|
|
65
|
+
// CORS preflight
|
|
66
|
+
if (req.method === "OPTIONS") {
|
|
67
|
+
res.writeHead(204, {
|
|
68
|
+
"Access-Control-Allow-Origin": "*",
|
|
69
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
70
|
+
"Access-Control-Allow-Headers": "Content-Type"
|
|
71
|
+
});
|
|
72
|
+
res.end();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (req.method !== "POST") {
|
|
76
|
+
jsonResponse(res, 405, { error: "Method not allowed" });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const rawBody = await readBody(req);
|
|
81
|
+
const body = JSON.parse(rawBody);
|
|
82
|
+
let result;
|
|
83
|
+
switch (req.url) {
|
|
84
|
+
case "/sign-message":
|
|
85
|
+
result = await handleSignMessage(body);
|
|
86
|
+
break;
|
|
87
|
+
case "/sign-typed-data":
|
|
88
|
+
result = await handleSignTypedData(body);
|
|
89
|
+
break;
|
|
90
|
+
case "/send-transaction":
|
|
91
|
+
result = await handleSendTransaction(body);
|
|
92
|
+
break;
|
|
93
|
+
default:
|
|
94
|
+
jsonResponse(res, 404, { error: `Unknown endpoint: ${req.url}` });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
jsonResponse(res, 200, { result });
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
101
|
+
debug("signing relay: request error", { url: req.url, error: message });
|
|
102
|
+
jsonResponse(res, 500, { error: message });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// Bind to a random available port on localhost only
|
|
106
|
+
server.listen(0, "127.0.0.1", () => {
|
|
107
|
+
const addr = server.address();
|
|
108
|
+
if (!addr || typeof addr === "string") {
|
|
109
|
+
server.close();
|
|
110
|
+
reject(new Error("Failed to resolve signing relay address."));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const port = addr.port;
|
|
114
|
+
debug("signing relay: started on 127.0.0.1:" + port);
|
|
115
|
+
resolve({
|
|
116
|
+
port,
|
|
117
|
+
close: () => new Promise((resolveClose) => {
|
|
118
|
+
server.close(() => {
|
|
119
|
+
debug("signing relay: stopped");
|
|
120
|
+
resolveClose();
|
|
121
|
+
});
|
|
122
|
+
})
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
server.on("error", (error) => {
|
|
126
|
+
reject(new Error(`Signing relay failed to start: ${error.message}`));
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import dotenv from "dotenv";
|
|
3
|
+
dotenv.config();
|
|
4
|
+
/* ------------------------------------------------------------------ */
|
|
5
|
+
/* Environment schema */
|
|
6
|
+
/* ------------------------------------------------------------------ */
|
|
7
|
+
function normalizeOptionalString(value) {
|
|
8
|
+
const trimmed = value?.trim();
|
|
9
|
+
if (!trimmed)
|
|
10
|
+
return undefined;
|
|
11
|
+
// Strip surrounding quotes if they exist
|
|
12
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
13
|
+
return trimmed.slice(1, -1).trim() || undefined;
|
|
14
|
+
}
|
|
15
|
+
return trimmed;
|
|
16
|
+
}
|
|
17
|
+
const WalletEnvSchema = z.object({
|
|
18
|
+
WALLET_PRIVATE_KEY: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.transform((value) => normalizeOptionalString(value)),
|
|
22
|
+
WALLET_MNEMONIC: z
|
|
23
|
+
.string()
|
|
24
|
+
.optional()
|
|
25
|
+
.transform((value) => normalizeOptionalString(value)),
|
|
26
|
+
WALLET_RPC_URL: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.transform((value) => normalizeOptionalString(value)),
|
|
30
|
+
WALLET_CHAIN_ID: z.coerce.number().int().positive().default(11155111),
|
|
31
|
+
WALLET_METAMASK_EXTENSION_PATH: z
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.transform((value) => normalizeOptionalString(value)),
|
|
35
|
+
WALLET_METAMASK_USER_DATA_DIR: z
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.transform((value) => normalizeOptionalString(value))
|
|
39
|
+
});
|
|
40
|
+
const walletOverrides = {};
|
|
41
|
+
function getWalletEnv() {
|
|
42
|
+
const base = WalletEnvSchema.parse(process.env);
|
|
43
|
+
return {
|
|
44
|
+
...base,
|
|
45
|
+
...walletOverrides
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function updateWalletSettings(updates) {
|
|
49
|
+
Object.assign(walletOverrides, updates);
|
|
50
|
+
}
|
|
51
|
+
/* ------------------------------------------------------------------ */
|
|
52
|
+
/* Lazy singleton */
|
|
53
|
+
/* ------------------------------------------------------------------ */
|
|
54
|
+
let cachedEthers = null;
|
|
55
|
+
let cachedWallet = null;
|
|
56
|
+
async function loadEthers() {
|
|
57
|
+
if (cachedEthers) {
|
|
58
|
+
return cachedEthers;
|
|
59
|
+
}
|
|
60
|
+
const mod = (await import("ethers"));
|
|
61
|
+
cachedEthers = mod.default ?? mod;
|
|
62
|
+
return cachedEthers;
|
|
63
|
+
}
|
|
64
|
+
async function resolveWallet() {
|
|
65
|
+
if (cachedWallet) {
|
|
66
|
+
return cachedWallet;
|
|
67
|
+
}
|
|
68
|
+
const ethers = await loadEthers();
|
|
69
|
+
const env = getWalletEnv();
|
|
70
|
+
const rpcUrl = env.WALLET_RPC_URL;
|
|
71
|
+
if (!rpcUrl) {
|
|
72
|
+
throw new Error("WALLET_RPC_URL is required when a wallet private key or mnemonic is configured.");
|
|
73
|
+
}
|
|
74
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
75
|
+
if (env.WALLET_PRIVATE_KEY) {
|
|
76
|
+
const key = env.WALLET_PRIVATE_KEY.startsWith("0x")
|
|
77
|
+
? env.WALLET_PRIVATE_KEY
|
|
78
|
+
: `0x${env.WALLET_PRIVATE_KEY}`;
|
|
79
|
+
cachedWallet = new ethers.Wallet(key, provider);
|
|
80
|
+
return cachedWallet;
|
|
81
|
+
}
|
|
82
|
+
if (env.WALLET_MNEMONIC) {
|
|
83
|
+
const mnemonic = ethers.Mnemonic.fromPhrase(env.WALLET_MNEMONIC);
|
|
84
|
+
const hdWallet = ethers.HDNodeWallet.fromMnemonic(mnemonic);
|
|
85
|
+
// HDNodeWallet needs to be reconnected with the provider
|
|
86
|
+
cachedWallet = new ethers.Wallet(hdWallet.privateKey, provider);
|
|
87
|
+
return cachedWallet;
|
|
88
|
+
}
|
|
89
|
+
throw new Error("Either WALLET_PRIVATE_KEY or WALLET_MNEMONIC must be set.");
|
|
90
|
+
}
|
|
91
|
+
/* ------------------------------------------------------------------ */
|
|
92
|
+
/* Public API */
|
|
93
|
+
/* ------------------------------------------------------------------ */
|
|
94
|
+
export function isWalletConfigured() {
|
|
95
|
+
return Boolean((getWalletEnv().WALLET_PRIVATE_KEY || getWalletEnv().WALLET_MNEMONIC) && getWalletEnv().WALLET_RPC_URL);
|
|
96
|
+
}
|
|
97
|
+
export function getWalletChainId() {
|
|
98
|
+
return getWalletEnv().WALLET_CHAIN_ID;
|
|
99
|
+
}
|
|
100
|
+
export function getMetaMaskExtensionPath() {
|
|
101
|
+
return getWalletEnv().WALLET_METAMASK_EXTENSION_PATH;
|
|
102
|
+
}
|
|
103
|
+
export function getMetaMaskUserDataDir() {
|
|
104
|
+
return getWalletEnv().WALLET_METAMASK_USER_DATA_DIR;
|
|
105
|
+
}
|
|
106
|
+
export async function getWalletConfig() {
|
|
107
|
+
if (!isWalletConfigured()) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const wallet = await resolveWallet();
|
|
111
|
+
return {
|
|
112
|
+
address: wallet.address,
|
|
113
|
+
chainId: getWalletEnv().WALLET_CHAIN_ID,
|
|
114
|
+
rpcUrl: getWalletEnv().WALLET_RPC_URL,
|
|
115
|
+
metamaskExtensionPath: getWalletEnv().WALLET_METAMASK_EXTENSION_PATH,
|
|
116
|
+
metamaskUserDataDir: getWalletEnv().WALLET_METAMASK_USER_DATA_DIR
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export async function getWalletAddress() {
|
|
120
|
+
const wallet = await resolveWallet();
|
|
121
|
+
return wallet.address;
|
|
122
|
+
}
|
|
123
|
+
export async function getWalletProvider() {
|
|
124
|
+
const wallet = await resolveWallet();
|
|
125
|
+
return wallet.provider;
|
|
126
|
+
}
|
|
127
|
+
export async function signTransaction(tx) {
|
|
128
|
+
const wallet = await resolveWallet();
|
|
129
|
+
return wallet.signTransaction(tx);
|
|
130
|
+
}
|
|
131
|
+
export async function signMessage(message) {
|
|
132
|
+
const wallet = await resolveWallet();
|
|
133
|
+
return wallet.signMessage(message);
|
|
134
|
+
}
|
|
135
|
+
export async function signTypedData(domain, types, value) {
|
|
136
|
+
const wallet = await resolveWallet();
|
|
137
|
+
return wallet.signTypedData(domain, types, value);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Send a raw transaction via the wallet's connected provider.
|
|
141
|
+
* Returns the transaction hash.
|
|
142
|
+
*/
|
|
143
|
+
export async function sendTransaction(tx) {
|
|
144
|
+
const wallet = await resolveWallet();
|
|
145
|
+
const populated = await wallet.populateTransaction(tx);
|
|
146
|
+
const signed = await wallet.signTransaction(populated);
|
|
147
|
+
const provider = wallet.provider;
|
|
148
|
+
return provider.send("eth_sendRawTransaction", [signed]);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Returns the native balance of the configured wallet in human-readable Ether.
|
|
152
|
+
*/
|
|
153
|
+
export async function getWalletBalance() {
|
|
154
|
+
if (!isWalletConfigured())
|
|
155
|
+
return "0";
|
|
156
|
+
const wallet = await resolveWallet();
|
|
157
|
+
const balance = await wallet.provider.getBalance(wallet.address);
|
|
158
|
+
// Convert BigInt Wei to a decimal string (Ether)
|
|
159
|
+
const balanceStr = balance.toString();
|
|
160
|
+
if (balanceStr === "0")
|
|
161
|
+
return "0";
|
|
162
|
+
// Simple formatting: insert decimal 18 places from the right
|
|
163
|
+
const padded = balanceStr.padStart(19, '0');
|
|
164
|
+
const integerPart = padded.slice(0, -18);
|
|
165
|
+
const fractionalPart = padded.slice(-18).replace(/0+$/, '');
|
|
166
|
+
return fractionalPart ? `${integerPart}.${fractionalPart}` : integerPart;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Returns a summary of wallet balances for the planner.
|
|
170
|
+
*/
|
|
171
|
+
export async function getWalletBalances() {
|
|
172
|
+
if (!isWalletConfigured())
|
|
173
|
+
return {};
|
|
174
|
+
const balance = await getWalletBalance();
|
|
175
|
+
return {
|
|
176
|
+
native: `${balance} ETH`,
|
|
177
|
+
};
|
|
178
|
+
}
|