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.
Files changed (81) hide show
  1. package/README.md +689 -0
  2. package/dist/auth/credentialStore.js +62 -0
  3. package/dist/auth/inbox.js +193 -0
  4. package/dist/auth/profile.js +379 -0
  5. package/dist/auth/runner.js +1124 -0
  6. package/dist/backend/dashboardData.js +194 -0
  7. package/dist/backend/runArtifacts.js +48 -0
  8. package/dist/backend/runRepository.js +93 -0
  9. package/dist/bin.js +2 -0
  10. package/dist/cli/backfillSiteChecks.js +143 -0
  11. package/dist/cli/run.js +309 -0
  12. package/dist/cli/trade.js +69 -0
  13. package/dist/config.js +199 -0
  14. package/dist/core/agentProfiles.js +55 -0
  15. package/dist/core/aggregateReport.js +382 -0
  16. package/dist/core/audit.js +30 -0
  17. package/dist/core/customTaskSuite.js +148 -0
  18. package/dist/core/evaluator.js +217 -0
  19. package/dist/core/executor.js +788 -0
  20. package/dist/core/fallbackReport.js +335 -0
  21. package/dist/core/formHeuristics.js +411 -0
  22. package/dist/core/gameplaySummary.js +164 -0
  23. package/dist/core/interaction.js +202 -0
  24. package/dist/core/pageState.js +201 -0
  25. package/dist/core/planner.js +1669 -0
  26. package/dist/core/processSubmissionBatch.js +204 -0
  27. package/dist/core/runAuditJob.js +170 -0
  28. package/dist/core/runner.js +2352 -0
  29. package/dist/core/siteBrief.js +107 -0
  30. package/dist/core/siteChecks.js +1526 -0
  31. package/dist/core/taskDirectives.js +279 -0
  32. package/dist/core/taskHeuristics.js +263 -0
  33. package/dist/dashboard/client.js +1256 -0
  34. package/dist/dashboard/contracts.js +95 -0
  35. package/dist/dashboard/narrative.js +277 -0
  36. package/dist/dashboard/server.js +458 -0
  37. package/dist/dashboard/theme.js +888 -0
  38. package/dist/index.js +84 -0
  39. package/dist/llm/client.js +188 -0
  40. package/dist/paystack/account.js +123 -0
  41. package/dist/paystack/client.js +100 -0
  42. package/dist/paystack/index.js +13 -0
  43. package/dist/paystack/test-paystack.js +83 -0
  44. package/dist/paystack/transfer.js +138 -0
  45. package/dist/paystack/types.js +74 -0
  46. package/dist/paystack/webhook.js +121 -0
  47. package/dist/prompts/browserAgent.js +124 -0
  48. package/dist/prompts/reviewer.js +71 -0
  49. package/dist/reporting/clickReplay.js +290 -0
  50. package/dist/reporting/html.js +930 -0
  51. package/dist/reporting/markdown.js +238 -0
  52. package/dist/reporting/template.js +1141 -0
  53. package/dist/schemas/types.js +361 -0
  54. package/dist/submissions/customTasks.js +196 -0
  55. package/dist/submissions/html.js +770 -0
  56. package/dist/submissions/model.js +56 -0
  57. package/dist/submissions/publicUrl.js +76 -0
  58. package/dist/submissions/service.js +74 -0
  59. package/dist/submissions/store.js +37 -0
  60. package/dist/submissions/types.js +65 -0
  61. package/dist/trade/engine.js +241 -0
  62. package/dist/trade/evm/erc20.js +44 -0
  63. package/dist/trade/extractor.js +148 -0
  64. package/dist/trade/policy.js +35 -0
  65. package/dist/trade/session.js +31 -0
  66. package/dist/trade/types.js +107 -0
  67. package/dist/trade/validator.js +148 -0
  68. package/dist/utils/files.js +59 -0
  69. package/dist/utils/log.js +24 -0
  70. package/dist/utils/playwrightCompat.js +14 -0
  71. package/dist/utils/time.js +3 -0
  72. package/dist/wallet/provider.js +345 -0
  73. package/dist/wallet/relay.js +129 -0
  74. package/dist/wallet/wallet.js +178 -0
  75. package/docs/01-installation.md +134 -0
  76. package/docs/02-running-your-first-audit.md +136 -0
  77. package/docs/03-configuration.md +233 -0
  78. package/docs/04-how-the-agent-thinks.md +41 -0
  79. package/docs/05-extending-personas-and-tasks.md +42 -0
  80. package/docs/06-hardening-for-production.md +92 -0
  81. package/package.json +60 -0
@@ -0,0 +1,56 @@
1
+ import crypto from "node:crypto";
2
+ import { config } from "../config.js";
3
+ import { buildDefaultTradeRunOptions } from "../trade/policy.js";
4
+ import { SubmissionSchema } from "./types.js";
5
+ import { buildInitialAgentRuns } from "../core/agentProfiles.js";
6
+ import { buildCustomTaskSuite } from "../core/customTaskSuite.js";
7
+ import { normalizeCustomTasks, SUBMISSION_TASKS_REQUIRED_MESSAGE } from "./customTasks.js";
8
+ export function computeExpiresAt(createdAt) {
9
+ const createdMs = new Date(createdAt).getTime();
10
+ return new Date(createdMs + config.reportTtlDays * 24 * 60 * 60 * 1000).toISOString();
11
+ }
12
+ export function isExpired(expiresAt, now = Date.now()) {
13
+ return new Date(expiresAt).getTime() <= now;
14
+ }
15
+ export function createSubmissionRecord(args) {
16
+ const createdAt = new Date().toISOString();
17
+ const id = crypto.randomUUID();
18
+ const reportToken = crypto.randomBytes(18).toString("base64url");
19
+ const agentCount = Math.min(5, Math.max(1, Math.round(args.agentCount ?? args.agentRuns?.length ?? 1)));
20
+ const customTasks = normalizeCustomTasks(args.customTasks ?? []);
21
+ if (customTasks.length === 0) {
22
+ throw new Error(SUBMISSION_TASKS_REQUIRED_MESSAGE);
23
+ }
24
+ const customSuite = buildCustomTaskSuite(customTasks);
25
+ const agentRuns = args.agentRuns ?? buildInitialAgentRuns(agentCount, customSuite);
26
+ return SubmissionSchema.parse({
27
+ id,
28
+ url: args.url,
29
+ createdAt,
30
+ startedAt: null,
31
+ completedAt: null,
32
+ expiresAt: computeExpiresAt(createdAt),
33
+ status: "queued",
34
+ reportToken,
35
+ publicReportPath: `/r/${reportToken}`,
36
+ headed: Boolean(args.headed),
37
+ mobile: Boolean(args.mobile),
38
+ ignoreHttpsErrors: Boolean(args.ignoreHttpsErrors),
39
+ tradeOptions: {
40
+ ...buildDefaultTradeRunOptions(),
41
+ ...(args.tradeOptions ?? {})
42
+ },
43
+ customTasks,
44
+ instructionText: args.instructionText?.trim() || customTasks.join("\n"),
45
+ instructionFileName: args.instructionFileName?.trim() || null,
46
+ agentCount,
47
+ completedAgentCount: 0,
48
+ failedAgentCount: 0,
49
+ agentRuns,
50
+ runId: null,
51
+ runDir: null,
52
+ error: null,
53
+ reportSummary: null,
54
+ overallScore: null
55
+ });
56
+ }
@@ -0,0 +1,76 @@
1
+ function isPrivateIpv4(hostname) {
2
+ const parts = hostname.split(".").map((part) => Number(part));
3
+ if (parts.length !== 4 || parts.some((part) => Number.isNaN(part))) {
4
+ return false;
5
+ }
6
+ const [a, b] = parts;
7
+ if (a === 10 || a === 127) {
8
+ return true;
9
+ }
10
+ if (a === 169 && b === 254) {
11
+ return true;
12
+ }
13
+ if (a === 192 && b === 168) {
14
+ return true;
15
+ }
16
+ if (a === 172 && b !== undefined && b >= 16 && b <= 31) {
17
+ return true;
18
+ }
19
+ if (a === 0) {
20
+ return true;
21
+ }
22
+ return false;
23
+ }
24
+ function isPrivateIpv6(hostname) {
25
+ const normalized = hostname.toLowerCase();
26
+ return normalized === "::1" || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe80:");
27
+ }
28
+ function isLocalHostname(hostname) {
29
+ const normalized = hostname.toLowerCase();
30
+ return normalized === "localhost" || normalized.endsWith(".localhost") || normalized.endsWith(".local");
31
+ }
32
+ export const DEFAULT_SUBMISSION_TARGET_MODE = "public";
33
+ export function parseSubmissionTargetMode(value) {
34
+ return value === "localhost" ? "localhost" : DEFAULT_SUBMISSION_TARGET_MODE;
35
+ }
36
+ export function validateSubmissionUrl(rawUrl, args = {}) {
37
+ let parsed;
38
+ try {
39
+ parsed = new URL(rawUrl.trim());
40
+ }
41
+ catch {
42
+ return { valid: false, reason: "Enter a valid http or https URL." };
43
+ }
44
+ if (!["http:", "https:"].includes(parsed.protocol)) {
45
+ return { valid: false, reason: "Only http and https URLs are allowed." };
46
+ }
47
+ const hostname = parsed.hostname.toLowerCase();
48
+ const canUsePrivateHosts = Boolean(args.allowPrivateHosts) && args.targetMode === "localhost";
49
+ if (!canUsePrivateHosts && isLocalHostname(hostname)) {
50
+ return {
51
+ valid: false,
52
+ reason: args.allowPrivateHosts
53
+ ? "Switch target to Localhost/private dev site to use localhost, .localhost, or .local addresses."
54
+ : "Localhost, .localhost, and .local addresses are not allowed in V1. Use a public URL."
55
+ };
56
+ }
57
+ if (!canUsePrivateHosts && (isPrivateIpv4(hostname) || isPrivateIpv6(hostname))) {
58
+ return {
59
+ valid: false,
60
+ reason: args.allowPrivateHosts
61
+ ? "Switch target to Localhost/private dev site to use 127.0.0.1 or private network addresses."
62
+ : "Private network addresses are not allowed in V1. Use a public URL."
63
+ };
64
+ }
65
+ parsed.hash = "";
66
+ return {
67
+ valid: true,
68
+ normalizedUrl: parsed.toString()
69
+ };
70
+ }
71
+ export function validatePublicUrl(rawUrl) {
72
+ return validateSubmissionUrl(rawUrl, {
73
+ allowPrivateHosts: false,
74
+ targetMode: DEFAULT_SUBMISSION_TARGET_MODE
75
+ });
76
+ }
@@ -0,0 +1,74 @@
1
+ import { processSubmissionBatch } from "../core/processSubmissionBatch.js";
2
+ import { createSubmissionRecord, listSubmissions, readSubmission, writeSubmission } from "./store.js";
3
+ export class SubmissionService {
4
+ activeSubmissionId = null;
5
+ queue = [];
6
+ async createSubmission(args) {
7
+ const submission = createSubmissionRecord(args);
8
+ writeSubmission(submission);
9
+ this.enqueue(submission.id);
10
+ return submission;
11
+ }
12
+ async getSubmission(id) {
13
+ return readSubmission(id);
14
+ }
15
+ resumePendingSubmissions() {
16
+ for (const submission of listSubmissions()) {
17
+ if (submission.status === "queued" || submission.status === "running") {
18
+ const resetSubmission = {
19
+ ...submission,
20
+ status: "queued",
21
+ startedAt: submission.status === "running" ? null : submission.startedAt,
22
+ completedAt: null,
23
+ error: null
24
+ };
25
+ writeSubmission(resetSubmission);
26
+ this.enqueue(resetSubmission.id);
27
+ }
28
+ }
29
+ }
30
+ enqueue(id) {
31
+ if (!this.queue.includes(id) && this.activeSubmissionId !== id) {
32
+ this.queue.push(id);
33
+ }
34
+ void this.processQueue();
35
+ }
36
+ async processQueue() {
37
+ if (this.activeSubmissionId || this.queue.length === 0) {
38
+ return;
39
+ }
40
+ const nextId = this.queue.shift();
41
+ if (!nextId) {
42
+ return;
43
+ }
44
+ this.activeSubmissionId = nextId;
45
+ try {
46
+ await this.runSubmission(nextId);
47
+ }
48
+ finally {
49
+ this.activeSubmissionId = null;
50
+ if (this.queue.length > 0) {
51
+ void this.processQueue();
52
+ }
53
+ }
54
+ }
55
+ async runSubmission(id) {
56
+ const submission = readSubmission(id);
57
+ if (!submission) {
58
+ return;
59
+ }
60
+ await processSubmissionBatch({
61
+ submission: {
62
+ ...submission,
63
+ startedAt: new Date().toISOString()
64
+ },
65
+ writeSubmission: async (nextSubmission) => {
66
+ writeSubmission(nextSubmission);
67
+ },
68
+ uploadRunArtifacts: async () => {
69
+ return;
70
+ },
71
+ source: "submission_form"
72
+ });
73
+ }
74
+ }
@@ -0,0 +1,37 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { ensureDir, readUtf8, resolveSubmissionsDir, writeJson } from "../utils/files.js";
4
+ import { SubmissionSchema } from "./types.js";
5
+ const SUBMISSIONS_DIR = resolveSubmissionsDir();
6
+ function submissionPath(id) {
7
+ return path.join(SUBMISSIONS_DIR, `${id}.json`);
8
+ }
9
+ export function getSubmissionsDir() {
10
+ ensureDir(SUBMISSIONS_DIR);
11
+ return SUBMISSIONS_DIR;
12
+ }
13
+ export function writeSubmission(submission) {
14
+ writeJson(submissionPath(submission.id), SubmissionSchema.parse(submission));
15
+ }
16
+ export function readSubmission(id) {
17
+ const filePath = submissionPath(id);
18
+ if (!fs.existsSync(filePath)) {
19
+ return null;
20
+ }
21
+ return SubmissionSchema.parse(JSON.parse(readUtf8(filePath)));
22
+ }
23
+ export function listSubmissions() {
24
+ getSubmissionsDir();
25
+ return fs
26
+ .readdirSync(SUBMISSIONS_DIR, { withFileTypes: true })
27
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
28
+ .map((entry) => SubmissionSchema.parse(JSON.parse(readUtf8(path.join(SUBMISSIONS_DIR, entry.name)))))
29
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt));
30
+ }
31
+ export function findSubmissionByReportToken(reportToken) {
32
+ return listSubmissions().find((submission) => submission.reportToken === reportToken) ?? null;
33
+ }
34
+ export function findSubmissionByRunId(runId) {
35
+ return listSubmissions().find((submission) => submission.runId === runId) ?? null;
36
+ }
37
+ export { createSubmissionRecord } from "./model.js";
@@ -0,0 +1,65 @@
1
+ import { z } from "zod";
2
+ import { TradeRunOptionsSchema } from "../trade/types.js";
3
+ export const SubmissionStatusSchema = z.enum(["queued", "running", "completed", "failed"]);
4
+ const TenPointNullableScoreSchema = z
5
+ .number()
6
+ .finite()
7
+ .min(0)
8
+ .max(100)
9
+ .transform((value) => {
10
+ const scaled = value > 10 ? value / 10 : value;
11
+ return Math.min(10, Math.max(1, Math.round(scaled)));
12
+ })
13
+ .pipe(z.number().int().min(1).max(10))
14
+ .nullable();
15
+ export const SubmissionAgentRunSchema = z.object({
16
+ id: z.string(),
17
+ index: z.number().int().min(1).max(5),
18
+ label: z.string(),
19
+ profileLabel: z.string(),
20
+ personaName: z.string(),
21
+ personaVariantKey: z.string(),
22
+ status: SubmissionStatusSchema,
23
+ startedAt: z.string().nullable(),
24
+ completedAt: z.string().nullable(),
25
+ runId: z.string().nullable(),
26
+ runDir: z.string().nullable(),
27
+ clickReplayAvailable: z.boolean().optional(),
28
+ clickReplayArtifact: z.string().nullable().optional(),
29
+ videoArtifact: z.string().nullable().optional(),
30
+ error: z.string().nullable(),
31
+ reportSummary: z.string().nullable(),
32
+ overallScore: TenPointNullableScoreSchema
33
+ });
34
+ export const SubmissionSchema = z.object({
35
+ id: z.string(),
36
+ url: z.string().url(),
37
+ createdAt: z.string(),
38
+ startedAt: z.string().nullable(),
39
+ completedAt: z.string().nullable(),
40
+ expiresAt: z.string(),
41
+ status: SubmissionStatusSchema,
42
+ reportToken: z.string(),
43
+ publicReportPath: z.string(),
44
+ headed: z.boolean(),
45
+ mobile: z.boolean(),
46
+ ignoreHttpsErrors: z.boolean(),
47
+ tradeOptions: TradeRunOptionsSchema.default({
48
+ enabled: false,
49
+ dryRun: false,
50
+ strategy: "auto",
51
+ confirmations: 1
52
+ }),
53
+ customTasks: z.array(z.string().min(1)).max(12).default([]),
54
+ instructionText: z.string().default(""),
55
+ instructionFileName: z.string().nullable().default(null),
56
+ agentCount: z.number().int().min(1).max(5).default(1),
57
+ completedAgentCount: z.number().int().nonnegative().default(0),
58
+ failedAgentCount: z.number().int().nonnegative().default(0),
59
+ agentRuns: z.array(SubmissionAgentRunSchema).max(5).default([]),
60
+ runId: z.string().nullable(),
61
+ runDir: z.string().nullable(),
62
+ error: z.string().nullable(),
63
+ reportSummary: z.string().nullable(),
64
+ overallScore: TenPointNullableScoreSchema
65
+ });
@@ -0,0 +1,241 @@
1
+ import crypto from "node:crypto";
2
+ import { sendTransaction } from "../wallet/wallet.js";
3
+ import { encodeErc20Transfer, parseTokenAmount, waitForEvmReceipt } from "./evm/erc20.js";
4
+ import { appendTradeExecutionRecord, computeInstructionFingerprint, readTradeExecutionRecords } from "./session.js";
5
+ import { validateSellInstruction } from "./validator.js";
6
+ function cleanErrorMessage(error) {
7
+ const message = error instanceof Error ? error.message : String(error);
8
+ return message.replace(/\u001b\[[0-9;]*m/g, "").replace(/\s+/g, " ").trim() || "Unknown trade execution error";
9
+ }
10
+ function serializeJsonSafe(value) {
11
+ if (typeof value === "bigint") {
12
+ return value.toString();
13
+ }
14
+ if (Array.isArray(value)) {
15
+ return value.map((item) => serializeJsonSafe(item));
16
+ }
17
+ if (value && typeof value === "object") {
18
+ return Object.fromEntries(Object.entries(value).map(([key, nestedValue]) => [key, serializeJsonSafe(nestedValue)]));
19
+ }
20
+ return value;
21
+ }
22
+ function createExecutionRecord(args) {
23
+ return {
24
+ id: crypto.randomUUID(),
25
+ fingerprint: args.fingerprint,
26
+ time: new Date().toISOString(),
27
+ source: args.source,
28
+ strategy: args.requestedStrategy,
29
+ selectedMode: args.selectedMode,
30
+ dryRun: args.dryRun,
31
+ status: args.status,
32
+ instruction: args.instruction,
33
+ validation: args.validation,
34
+ txHash: args.txHash ?? null,
35
+ confirmationsRequested: args.confirmationsRequested,
36
+ confirmationsReached: args.confirmationsReached ?? 0,
37
+ receipt: args.receipt ?? null,
38
+ error: args.error ?? null,
39
+ note: args.note
40
+ };
41
+ }
42
+ function resolveSelectedMode(args) {
43
+ if (args.instruction.mode === "dapp_managed") {
44
+ return "dapp_managed";
45
+ }
46
+ if (args.instruction.mode === "deposit_address_transfer") {
47
+ return "deposit_address_transfer";
48
+ }
49
+ return "unsupported";
50
+ }
51
+ function strategyAllowsMode(args) {
52
+ if (args.runOptions.strategy === "auto") {
53
+ return true;
54
+ }
55
+ if (args.runOptions.strategy === "dapp_only") {
56
+ return args.selectedMode === "dapp_managed";
57
+ }
58
+ if (args.runOptions.strategy === "deposit_only") {
59
+ return args.selectedMode === "deposit_address_transfer";
60
+ }
61
+ return false;
62
+ }
63
+ export async function executeTradeInstruction(args) {
64
+ const fingerprint = computeInstructionFingerprint(args.instruction);
65
+ const selectedMode = resolveSelectedMode({
66
+ instruction: args.instruction,
67
+ runOptions: args.runOptions
68
+ });
69
+ const { validation, normalizedInstruction } = await validateSellInstruction({
70
+ instruction: args.instruction,
71
+ policy: args.policy,
72
+ runOptions: args.runOptions
73
+ });
74
+ if (!strategyAllowsMode({ selectedMode, runOptions: args.runOptions })) {
75
+ const blockedRecord = createExecutionRecord({
76
+ fingerprint,
77
+ source: args.source,
78
+ requestedStrategy: args.runOptions.strategy,
79
+ selectedMode,
80
+ dryRun: args.runOptions.dryRun,
81
+ status: "blocked",
82
+ instruction: normalizedInstruction,
83
+ validation: {
84
+ ok: false,
85
+ reasons: [
86
+ `Requested trade strategy '${args.runOptions.strategy}' does not allow the extracted mode '${selectedMode}'.`,
87
+ ...validation.reasons
88
+ ]
89
+ },
90
+ confirmationsRequested: args.runOptions.confirmations,
91
+ note: `Blocked trade execution because strategy '${args.runOptions.strategy}' does not permit '${selectedMode}'.`
92
+ });
93
+ appendTradeExecutionRecord(args.runDir, blockedRecord);
94
+ return blockedRecord;
95
+ }
96
+ if (!validation.ok) {
97
+ const blockedRecord = createExecutionRecord({
98
+ fingerprint,
99
+ source: args.source,
100
+ requestedStrategy: args.runOptions.strategy,
101
+ selectedMode,
102
+ dryRun: args.runOptions.dryRun,
103
+ status: "blocked",
104
+ instruction: normalizedInstruction,
105
+ validation,
106
+ confirmationsRequested: args.runOptions.confirmations,
107
+ note: `Blocked trade execution after validation: ${validation.reasons.join(" ")}`
108
+ });
109
+ appendTradeExecutionRecord(args.runDir, blockedRecord);
110
+ return blockedRecord;
111
+ }
112
+ const previousExecution = readTradeExecutionRecords(args.runDir).find((record) => record.fingerprint === fingerprint &&
113
+ !record.dryRun &&
114
+ (record.status === "broadcast" || record.status === "confirmed"));
115
+ if (previousExecution) {
116
+ const blockedRecord = createExecutionRecord({
117
+ fingerprint,
118
+ source: args.source,
119
+ requestedStrategy: args.runOptions.strategy,
120
+ selectedMode,
121
+ dryRun: args.runOptions.dryRun,
122
+ status: "blocked",
123
+ instruction: normalizedInstruction,
124
+ validation,
125
+ confirmationsRequested: args.runOptions.confirmations,
126
+ txHash: previousExecution.txHash,
127
+ note: "Blocked duplicate trade execution because the same instruction was already broadcast in this run."
128
+ });
129
+ appendTradeExecutionRecord(args.runDir, blockedRecord);
130
+ return blockedRecord;
131
+ }
132
+ if (args.runOptions.dryRun) {
133
+ const dryRunRecord = createExecutionRecord({
134
+ fingerprint,
135
+ source: args.source,
136
+ requestedStrategy: args.runOptions.strategy,
137
+ selectedMode,
138
+ dryRun: true,
139
+ status: "dry_run",
140
+ instruction: normalizedInstruction,
141
+ validation,
142
+ confirmationsRequested: args.runOptions.confirmations,
143
+ note: `Dry run validated a ${selectedMode} trade without broadcasting a transaction.`
144
+ });
145
+ appendTradeExecutionRecord(args.runDir, dryRunRecord);
146
+ return dryRunRecord;
147
+ }
148
+ if (selectedMode === "dapp_managed") {
149
+ const blockedRecord = createExecutionRecord({
150
+ fingerprint,
151
+ source: args.source,
152
+ requestedStrategy: args.runOptions.strategy,
153
+ selectedMode,
154
+ dryRun: false,
155
+ status: "blocked",
156
+ instruction: normalizedInstruction,
157
+ validation,
158
+ confirmationsRequested: args.runOptions.confirmations,
159
+ note: "Dapp-managed trades must be initiated by the page through the injected wallet provider; the direct trade engine only broadcasts deposit-address transfers."
160
+ });
161
+ appendTradeExecutionRecord(args.runDir, blockedRecord);
162
+ return blockedRecord;
163
+ }
164
+ if (selectedMode !== "deposit_address_transfer") {
165
+ const blockedRecord = createExecutionRecord({
166
+ fingerprint,
167
+ source: args.source,
168
+ requestedStrategy: args.runOptions.strategy,
169
+ selectedMode: "unsupported",
170
+ dryRun: false,
171
+ status: "blocked",
172
+ instruction: normalizedInstruction,
173
+ validation,
174
+ confirmationsRequested: args.runOptions.confirmations,
175
+ note: "The extracted trade mode is not supported by the direct trade engine."
176
+ });
177
+ appendTradeExecutionRecord(args.runDir, blockedRecord);
178
+ return blockedRecord;
179
+ }
180
+ try {
181
+ const tx = normalizedInstruction.assetKind === "native"
182
+ ? {
183
+ to: normalizedInstruction.recipientAddress,
184
+ value: await parseTokenAmount(normalizedInstruction.amount, normalizedInstruction.tokenDecimals ?? 18),
185
+ chainId: normalizedInstruction.chainId
186
+ }
187
+ : {
188
+ to: normalizedInstruction.tokenContract,
189
+ data: await encodeErc20Transfer({
190
+ recipientAddress: normalizedInstruction.recipientAddress,
191
+ amountBaseUnits: await parseTokenAmount(normalizedInstruction.amount, normalizedInstruction.tokenDecimals ?? 0)
192
+ }),
193
+ value: 0n,
194
+ chainId: normalizedInstruction.chainId
195
+ };
196
+ const txHash = await sendTransaction(tx);
197
+ const receipt = await waitForEvmReceipt({
198
+ txHash,
199
+ confirmations: Math.max(0, args.runOptions.confirmations),
200
+ timeoutMs: args.policy.receiptTimeoutMs
201
+ });
202
+ const confirmed = Boolean(receipt);
203
+ const record = createExecutionRecord({
204
+ fingerprint,
205
+ source: args.source,
206
+ requestedStrategy: args.runOptions.strategy,
207
+ selectedMode,
208
+ dryRun: false,
209
+ status: confirmed ? "confirmed" : "broadcast",
210
+ instruction: normalizedInstruction,
211
+ validation,
212
+ txHash,
213
+ confirmationsRequested: args.runOptions.confirmations,
214
+ confirmationsReached: confirmed ? Math.max(1, args.runOptions.confirmations) : 0,
215
+ receipt: serializeJsonSafe(receipt),
216
+ note: confirmed
217
+ ? `Broadcast and confirmed ${normalizedInstruction.assetKind} transfer ${txHash}.`
218
+ : `Broadcast ${normalizedInstruction.assetKind} transfer ${txHash}, but no receipt was observed before timeout.`
219
+ });
220
+ appendTradeExecutionRecord(args.runDir, record);
221
+ return record;
222
+ }
223
+ catch (error) {
224
+ const message = cleanErrorMessage(error);
225
+ const failedRecord = createExecutionRecord({
226
+ fingerprint,
227
+ source: args.source,
228
+ requestedStrategy: args.runOptions.strategy,
229
+ selectedMode,
230
+ dryRun: false,
231
+ status: "failed",
232
+ instruction: normalizedInstruction,
233
+ validation,
234
+ confirmationsRequested: args.runOptions.confirmations,
235
+ error: message,
236
+ note: `Trade execution failed: ${message}`
237
+ });
238
+ appendTradeExecutionRecord(args.runDir, failedRecord);
239
+ return failedRecord;
240
+ }
241
+ }
@@ -0,0 +1,44 @@
1
+ import { getWalletConfig, getWalletProvider } from "../../wallet/wallet.js";
2
+ async function loadEthers() {
3
+ const mod = (await import("ethers"));
4
+ return mod.default ?? mod;
5
+ }
6
+ async function resolveRpcProvider() {
7
+ const walletConfig = await getWalletConfig();
8
+ if (!walletConfig?.rpcUrl) {
9
+ throw new Error("Wallet RPC configuration is required for EVM trade execution.");
10
+ }
11
+ const ethers = await loadEthers();
12
+ return new ethers.JsonRpcProvider(walletConfig.rpcUrl);
13
+ }
14
+ export async function parseTokenAmount(value, decimals) {
15
+ const ethers = await loadEthers();
16
+ return ethers.parseUnits(value, decimals);
17
+ }
18
+ export async function encodeErc20Transfer(args) {
19
+ const ethers = await loadEthers();
20
+ const iface = new ethers.Interface(["function transfer(address to, uint256 amount) returns (bool)"]);
21
+ return iface.encodeFunctionData("transfer", [args.recipientAddress, args.amountBaseUnits]);
22
+ }
23
+ export async function readNativeBalance(address) {
24
+ const provider = await resolveRpcProvider();
25
+ return provider.getBalance(address);
26
+ }
27
+ export async function readErc20Balance(args) {
28
+ const ethers = await loadEthers();
29
+ const provider = await resolveRpcProvider();
30
+ const iface = new ethers.Interface(["function balanceOf(address owner) view returns (uint256)"]);
31
+ const data = iface.encodeFunctionData("balanceOf", [args.owner]);
32
+ const result = await provider.call({ to: args.contract, data });
33
+ return BigInt(result);
34
+ }
35
+ export async function waitForEvmReceipt(args) {
36
+ const provider = (await getWalletProvider());
37
+ if (provider?.waitForTransaction) {
38
+ const receipt = await provider.waitForTransaction(args.txHash, args.confirmations, args.timeoutMs);
39
+ return receipt ?? null;
40
+ }
41
+ const rpcProvider = await resolveRpcProvider();
42
+ const receipt = await rpcProvider.waitForTransaction(args.txHash, args.confirmations, args.timeoutMs);
43
+ return receipt ?? null;
44
+ }