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,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
|
+
}
|