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,148 @@
|
|
|
1
|
+
const EVM_ADDRESS_PATTERN = /\b0x[a-fA-F0-9]{40}\b/g;
|
|
2
|
+
const DEPOSIT_ADDRESS_HINT_PATTERN = /\b(?:send(?:\s+your)?|deposit|transfer|recipient|wallet address|sale(?:s)? address|sell address|copy address)\b/i;
|
|
3
|
+
const QUOTE_ID_PATTERN = /\b(?:quote(?:\s+id)?|reference|ref)\s*[:#-]?\s*([a-z0-9_-]{6,})\b/i;
|
|
4
|
+
const EXPIRY_PATTERN = /\b(?:expires?|valid until)\s*[:#-]?\s*([^\n]+)$/i;
|
|
5
|
+
const CHAIN_NAME_MAP = {
|
|
6
|
+
ethereum: 1,
|
|
7
|
+
mainnet: 1,
|
|
8
|
+
sepolia: 11155111,
|
|
9
|
+
polygon: 137,
|
|
10
|
+
mumbai: 80001,
|
|
11
|
+
arbitrum: 42161,
|
|
12
|
+
optimism: 10,
|
|
13
|
+
base: 8453,
|
|
14
|
+
avalanche: 43114,
|
|
15
|
+
bsc: 56
|
|
16
|
+
};
|
|
17
|
+
function inferAssetKind(tokenSymbol) {
|
|
18
|
+
const normalized = tokenSymbol.trim().toUpperCase();
|
|
19
|
+
if (["ETH", "MATIC", "POL", "BNB", "AVAX"].includes(normalized)) {
|
|
20
|
+
return "native";
|
|
21
|
+
}
|
|
22
|
+
return "erc20";
|
|
23
|
+
}
|
|
24
|
+
function normalizeText(value) {
|
|
25
|
+
return value.replace(/\s+/g, " ").trim();
|
|
26
|
+
}
|
|
27
|
+
function uniqueStrings(values) {
|
|
28
|
+
return [...new Set(values.map((value) => normalizeText(value)).filter(Boolean))];
|
|
29
|
+
}
|
|
30
|
+
function findDepositAddressCandidate(lines) {
|
|
31
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
32
|
+
const line = normalizeText(lines[index] || "");
|
|
33
|
+
if (!line) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const addressMatches = line.match(EVM_ADDRESS_PATTERN) ?? [];
|
|
37
|
+
if (addressMatches.length > 0 && DEPOSIT_ADDRESS_HINT_PATTERN.test(line)) {
|
|
38
|
+
return {
|
|
39
|
+
address: addressMatches[0],
|
|
40
|
+
evidence: uniqueStrings([line, lines[index - 1] || "", lines[index + 1] || ""]).slice(0, 4)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (DEPOSIT_ADDRESS_HINT_PATTERN.test(line)) {
|
|
44
|
+
const nextWindow = [lines[index + 1] || "", lines[index + 2] || ""];
|
|
45
|
+
for (const candidateLine of nextWindow) {
|
|
46
|
+
const candidateAddress = normalizeText(candidateLine).match(EVM_ADDRESS_PATTERN)?.[0];
|
|
47
|
+
if (candidateAddress) {
|
|
48
|
+
return {
|
|
49
|
+
address: candidateAddress,
|
|
50
|
+
evidence: uniqueStrings([line, candidateLine, lines[index + 2] || ""]).slice(0, 4)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function parseAmountAndSymbol(taskGoal, lines) {
|
|
59
|
+
const textSources = [taskGoal, ...lines];
|
|
60
|
+
const patterns = [
|
|
61
|
+
/\b(?:sell|send|transfer)\s+([0-9]+(?:\.[0-9]+)?)\s+([A-Za-z][A-Za-z0-9]{1,11})\b/i,
|
|
62
|
+
/\b([0-9]+(?:\.[0-9]+)?)\s+([A-Za-z][A-Za-z0-9]{1,11})\b.*\b(?:sell|send|transfer)\b/i
|
|
63
|
+
];
|
|
64
|
+
for (const source of textSources) {
|
|
65
|
+
const normalized = normalizeText(source);
|
|
66
|
+
for (const pattern of patterns) {
|
|
67
|
+
const match = normalized.match(pattern);
|
|
68
|
+
if (match?.[1] && match?.[2]) {
|
|
69
|
+
return {
|
|
70
|
+
amount: match[1],
|
|
71
|
+
tokenSymbol: match[2].toUpperCase()
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
function inferChainId(lines, defaultChainId) {
|
|
79
|
+
const blob = normalizeText(lines.join(" ")).toLowerCase();
|
|
80
|
+
for (const [name, chainId] of Object.entries(CHAIN_NAME_MAP)) {
|
|
81
|
+
if (blob.includes(name)) {
|
|
82
|
+
return chainId;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return defaultChainId ?? null;
|
|
86
|
+
}
|
|
87
|
+
function findQuoteId(lines) {
|
|
88
|
+
for (const line of lines) {
|
|
89
|
+
const match = normalizeText(line).match(QUOTE_ID_PATTERN);
|
|
90
|
+
if (match?.[1]) {
|
|
91
|
+
return match[1];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
function findExpiryLine(lines) {
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
const match = normalizeText(line).match(EXPIRY_PATTERN);
|
|
99
|
+
if (match?.[1]) {
|
|
100
|
+
return match[1];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
export function taskLooksLikeTrade(taskGoal) {
|
|
106
|
+
return /\b(?:sell|cash\s*out|offramp|send(?:\s+crypto)?|transfer(?:\s+crypto)?|deposit(?:\s+crypto)?)\b/i.test(taskGoal);
|
|
107
|
+
}
|
|
108
|
+
export function pageLooksTradeReady(pageState) {
|
|
109
|
+
const lines = [
|
|
110
|
+
...pageState.visibleLines,
|
|
111
|
+
...pageState.headings,
|
|
112
|
+
...pageState.interactive.map((item) => item.text)
|
|
113
|
+
];
|
|
114
|
+
return Boolean(findDepositAddressCandidate(lines));
|
|
115
|
+
}
|
|
116
|
+
export function extractSellInstruction(args) {
|
|
117
|
+
const evidenceLines = uniqueStrings([
|
|
118
|
+
...args.pageState.visibleLines,
|
|
119
|
+
...args.pageState.headings,
|
|
120
|
+
...args.pageState.interactive.map((item) => item.text)
|
|
121
|
+
]).slice(0, 80);
|
|
122
|
+
const depositAddress = findDepositAddressCandidate(evidenceLines);
|
|
123
|
+
if (!depositAddress) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const amountAndSymbol = parseAmountAndSymbol(args.taskGoal, evidenceLines);
|
|
127
|
+
const chainId = inferChainId(depositAddress.evidence.concat(evidenceLines), args.defaultChainId);
|
|
128
|
+
if (!amountAndSymbol || !chainId) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
mode: "deposit_address_transfer",
|
|
133
|
+
chainFamily: "evm",
|
|
134
|
+
chainId,
|
|
135
|
+
assetKind: inferAssetKind(amountAndSymbol.tokenSymbol),
|
|
136
|
+
tokenSymbol: amountAndSymbol.tokenSymbol,
|
|
137
|
+
tokenContract: undefined,
|
|
138
|
+
tokenDecimals: undefined,
|
|
139
|
+
amount: amountAndSymbol.amount,
|
|
140
|
+
recipientAddress: depositAddress.address,
|
|
141
|
+
recipientMemo: undefined,
|
|
142
|
+
quoteId: findQuoteId(evidenceLines),
|
|
143
|
+
expiresAt: findExpiryLine(evidenceLines),
|
|
144
|
+
sourceUrl: args.pageState.url,
|
|
145
|
+
sourceTitle: args.pageState.title,
|
|
146
|
+
evidenceText: uniqueStrings([...depositAddress.evidence, args.taskGoal]).slice(0, 8)
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { config } from "../config.js";
|
|
2
|
+
export function getTradePolicy() {
|
|
3
|
+
return config.tradePolicy;
|
|
4
|
+
}
|
|
5
|
+
export function buildDefaultTradeRunOptions(overrides) {
|
|
6
|
+
return {
|
|
7
|
+
enabled: overrides?.enabled ?? config.tradePolicy.enabledByDefault,
|
|
8
|
+
dryRun: overrides?.dryRun ?? false,
|
|
9
|
+
strategy: overrides?.strategy ?? "auto",
|
|
10
|
+
confirmations: overrides?.confirmations ?? config.tradePolicy.confirmationsRequired
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function resolveTokenRegistryEntry(args) {
|
|
14
|
+
const normalizedSymbol = args.symbol.trim().toUpperCase();
|
|
15
|
+
const normalizedContract = args.contract?.trim().toLowerCase();
|
|
16
|
+
const matches = args.policy.tokenRegistry.filter((entry) => {
|
|
17
|
+
if (entry.chainId !== args.chainId) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (entry.symbol !== normalizedSymbol) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (!normalizedContract) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return (entry.contract || "").trim().toLowerCase() === normalizedContract;
|
|
27
|
+
});
|
|
28
|
+
if (matches.length === 0) {
|
|
29
|
+
return { entry: null, ambiguous: false };
|
|
30
|
+
}
|
|
31
|
+
if (matches.length > 1 && !normalizedContract) {
|
|
32
|
+
return { entry: null, ambiguous: true };
|
|
33
|
+
}
|
|
34
|
+
return { entry: matches[0] ?? null, ambiguous: false };
|
|
35
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { writeJson } from "../utils/files.js";
|
|
5
|
+
import { SellInstructionSchema, TradeExecutionRecordsSchema } from "./types.js";
|
|
6
|
+
const TRADE_EXECUTIONS_FILE = "trade-executions.json";
|
|
7
|
+
export function resolveTradeExecutionsPath(runDir) {
|
|
8
|
+
return path.join(runDir, TRADE_EXECUTIONS_FILE);
|
|
9
|
+
}
|
|
10
|
+
export function computeInstructionFingerprint(instruction) {
|
|
11
|
+
const normalized = SellInstructionSchema.parse(instruction);
|
|
12
|
+
return crypto.createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
|
|
13
|
+
}
|
|
14
|
+
export function readTradeExecutionRecords(runDir) {
|
|
15
|
+
const artifactPath = resolveTradeExecutionsPath(runDir);
|
|
16
|
+
if (!fs.existsSync(artifactPath)) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(fs.readFileSync(artifactPath, "utf8"));
|
|
21
|
+
return TradeExecutionRecordsSchema.parse(parsed);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function appendTradeExecutionRecord(runDir, record) {
|
|
28
|
+
const nextRecords = [...readTradeExecutionRecords(runDir), record];
|
|
29
|
+
writeJson(resolveTradeExecutionsPath(runDir), nextRecords);
|
|
30
|
+
return nextRecords;
|
|
31
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const TradeStrategySchema = z.enum(["auto", "dapp_only", "deposit_only"]);
|
|
3
|
+
export const TradeRunOptionsSchema = z.object({
|
|
4
|
+
enabled: z.boolean().default(false),
|
|
5
|
+
dryRun: z.boolean().default(false),
|
|
6
|
+
strategy: TradeStrategySchema.default("auto"),
|
|
7
|
+
confirmations: z.number().int().min(0).max(12).default(1)
|
|
8
|
+
});
|
|
9
|
+
export const TradeTokenRegistryEntrySchema = z.object({
|
|
10
|
+
chainId: z.number().int().positive(),
|
|
11
|
+
symbol: z.string().min(1).transform((value) => value.trim().toUpperCase()),
|
|
12
|
+
assetKind: z.enum(["native", "erc20"]),
|
|
13
|
+
contract: z
|
|
14
|
+
.string()
|
|
15
|
+
.optional()
|
|
16
|
+
.transform((value) => {
|
|
17
|
+
const trimmed = value?.trim();
|
|
18
|
+
return trimmed ? trimmed : undefined;
|
|
19
|
+
}),
|
|
20
|
+
decimals: z.number().int().min(0).max(36)
|
|
21
|
+
});
|
|
22
|
+
export const TradePolicySchema = z.object({
|
|
23
|
+
enabledByDefault: z.boolean().default(false),
|
|
24
|
+
allowlistedChainIds: z.array(z.number().int().positive()).default([]),
|
|
25
|
+
tokenRegistry: z.array(TradeTokenRegistryEntrySchema).default([]),
|
|
26
|
+
maxTokenAmount: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.transform((value) => {
|
|
30
|
+
const trimmed = value?.trim();
|
|
31
|
+
return trimmed ? trimmed : undefined;
|
|
32
|
+
}),
|
|
33
|
+
requireExactTokenContract: z.boolean().default(true),
|
|
34
|
+
receiptTimeoutMs: z.number().int().positive().default(120000),
|
|
35
|
+
confirmationsRequired: z.number().int().min(0).max(12).default(1)
|
|
36
|
+
});
|
|
37
|
+
export const SellInstructionSchema = z.object({
|
|
38
|
+
mode: z.enum(["dapp_managed", "deposit_address_transfer"]),
|
|
39
|
+
chainFamily: z.literal("evm"),
|
|
40
|
+
chainId: z.number().int().positive(),
|
|
41
|
+
assetKind: z.enum(["native", "erc20"]),
|
|
42
|
+
tokenSymbol: z.string().min(1).transform((value) => value.trim().toUpperCase()),
|
|
43
|
+
tokenContract: z
|
|
44
|
+
.string()
|
|
45
|
+
.optional()
|
|
46
|
+
.transform((value) => {
|
|
47
|
+
const trimmed = value?.trim();
|
|
48
|
+
return trimmed ? trimmed : undefined;
|
|
49
|
+
}),
|
|
50
|
+
tokenDecimals: z.number().int().min(0).max(36).optional(),
|
|
51
|
+
amount: z.string().min(1),
|
|
52
|
+
recipientAddress: z.string().min(1),
|
|
53
|
+
recipientMemo: z
|
|
54
|
+
.string()
|
|
55
|
+
.optional()
|
|
56
|
+
.transform((value) => {
|
|
57
|
+
const trimmed = value?.trim();
|
|
58
|
+
return trimmed ? trimmed : undefined;
|
|
59
|
+
}),
|
|
60
|
+
quoteId: z
|
|
61
|
+
.string()
|
|
62
|
+
.optional()
|
|
63
|
+
.transform((value) => {
|
|
64
|
+
const trimmed = value?.trim();
|
|
65
|
+
return trimmed ? trimmed : undefined;
|
|
66
|
+
}),
|
|
67
|
+
expiresAt: z
|
|
68
|
+
.string()
|
|
69
|
+
.optional()
|
|
70
|
+
.transform((value) => {
|
|
71
|
+
const trimmed = value?.trim();
|
|
72
|
+
return trimmed ? trimmed : undefined;
|
|
73
|
+
}),
|
|
74
|
+
sourceUrl: z.string().url(),
|
|
75
|
+
sourceTitle: z.string(),
|
|
76
|
+
evidenceText: z.array(z.string()).default([])
|
|
77
|
+
});
|
|
78
|
+
export const TradeExecutionStatusSchema = z.enum([
|
|
79
|
+
"dry_run",
|
|
80
|
+
"broadcast",
|
|
81
|
+
"confirmed",
|
|
82
|
+
"blocked",
|
|
83
|
+
"failed"
|
|
84
|
+
]);
|
|
85
|
+
export const TradeValidationResultSchema = z.object({
|
|
86
|
+
ok: z.boolean(),
|
|
87
|
+
reasons: z.array(z.string()).default([])
|
|
88
|
+
});
|
|
89
|
+
export const TradeExecutionRecordSchema = z.object({
|
|
90
|
+
id: z.string(),
|
|
91
|
+
fingerprint: z.string(),
|
|
92
|
+
time: z.string(),
|
|
93
|
+
source: z.enum(["browser", "cli"]),
|
|
94
|
+
strategy: TradeStrategySchema,
|
|
95
|
+
selectedMode: z.enum(["dapp_managed", "deposit_address_transfer", "unsupported"]),
|
|
96
|
+
dryRun: z.boolean(),
|
|
97
|
+
status: TradeExecutionStatusSchema,
|
|
98
|
+
instruction: SellInstructionSchema.optional(),
|
|
99
|
+
validation: TradeValidationResultSchema,
|
|
100
|
+
txHash: z.string().nullable().default(null),
|
|
101
|
+
confirmationsRequested: z.number().int().min(0).max(12).default(1),
|
|
102
|
+
confirmationsReached: z.number().int().min(0).max(12).default(0),
|
|
103
|
+
receipt: z.record(z.string(), z.unknown()).nullable().default(null),
|
|
104
|
+
error: z.string().nullable().default(null),
|
|
105
|
+
note: z.string()
|
|
106
|
+
});
|
|
107
|
+
export const TradeExecutionRecordsSchema = z.array(TradeExecutionRecordSchema);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { getWalletAddress, getWalletChainId, isWalletConfigured } from "../wallet/wallet.js";
|
|
2
|
+
import { parseTokenAmount, readErc20Balance, readNativeBalance } from "./evm/erc20.js";
|
|
3
|
+
import { resolveTokenRegistryEntry } from "./policy.js";
|
|
4
|
+
function normalizeAddress(value) {
|
|
5
|
+
return value.trim().toLowerCase();
|
|
6
|
+
}
|
|
7
|
+
function isEvmAddress(value) {
|
|
8
|
+
return /^0x[a-fA-F0-9]{40}$/.test(value.trim());
|
|
9
|
+
}
|
|
10
|
+
function isNumberishDecimal(value) {
|
|
11
|
+
return /^[0-9]+(?:\.[0-9]+)?$/.test(value.trim());
|
|
12
|
+
}
|
|
13
|
+
function compareDecimalStrings(left, right) {
|
|
14
|
+
const [leftWhole = "0", leftFraction = ""] = left.trim().split(".");
|
|
15
|
+
const [rightWhole = "0", rightFraction = ""] = right.trim().split(".");
|
|
16
|
+
const normalizedLeftWhole = leftWhole.replace(/^0+/, "") || "0";
|
|
17
|
+
const normalizedRightWhole = rightWhole.replace(/^0+/, "") || "0";
|
|
18
|
+
if (normalizedLeftWhole.length !== normalizedRightWhole.length) {
|
|
19
|
+
return normalizedLeftWhole.length > normalizedRightWhole.length ? 1 : -1;
|
|
20
|
+
}
|
|
21
|
+
if (normalizedLeftWhole !== normalizedRightWhole) {
|
|
22
|
+
return normalizedLeftWhole > normalizedRightWhole ? 1 : -1;
|
|
23
|
+
}
|
|
24
|
+
const maxFractionLength = Math.max(leftFraction.length, rightFraction.length);
|
|
25
|
+
const normalizedLeftFraction = leftFraction.padEnd(maxFractionLength, "0");
|
|
26
|
+
const normalizedRightFraction = rightFraction.padEnd(maxFractionLength, "0");
|
|
27
|
+
if (normalizedLeftFraction === normalizedRightFraction) {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
return normalizedLeftFraction > normalizedRightFraction ? 1 : -1;
|
|
31
|
+
}
|
|
32
|
+
export async function validateSellInstruction(args) {
|
|
33
|
+
const reasons = [];
|
|
34
|
+
const walletChainId = getWalletChainId();
|
|
35
|
+
const normalizedInstruction = {
|
|
36
|
+
...args.instruction,
|
|
37
|
+
tokenSymbol: args.instruction.tokenSymbol.trim().toUpperCase(),
|
|
38
|
+
recipientAddress: args.instruction.recipientAddress.trim()
|
|
39
|
+
};
|
|
40
|
+
if (!args.runOptions.enabled) {
|
|
41
|
+
reasons.push("Trade execution is disabled for this run.");
|
|
42
|
+
}
|
|
43
|
+
if (!isWalletConfigured()) {
|
|
44
|
+
reasons.push("Wallet execution is not configured.");
|
|
45
|
+
}
|
|
46
|
+
if (normalizedInstruction.chainFamily !== "evm") {
|
|
47
|
+
reasons.push("Only EVM trade execution is supported in this build.");
|
|
48
|
+
}
|
|
49
|
+
if (!isEvmAddress(normalizedInstruction.recipientAddress)) {
|
|
50
|
+
reasons.push("Recipient address is not a valid EVM address.");
|
|
51
|
+
}
|
|
52
|
+
if (!isNumberishDecimal(normalizedInstruction.amount)) {
|
|
53
|
+
reasons.push("Amount must be a numeric decimal string.");
|
|
54
|
+
}
|
|
55
|
+
if (normalizedInstruction.chainId !== walletChainId) {
|
|
56
|
+
reasons.push(`Instruction chain ${normalizedInstruction.chainId} does not match the configured wallet chain ${walletChainId}.`);
|
|
57
|
+
}
|
|
58
|
+
if (args.policy.allowlistedChainIds.length > 0 &&
|
|
59
|
+
!args.policy.allowlistedChainIds.includes(normalizedInstruction.chainId)) {
|
|
60
|
+
reasons.push(`Chain ${normalizedInstruction.chainId} is not in the allowed trade chain list.`);
|
|
61
|
+
}
|
|
62
|
+
if (normalizedInstruction.recipientMemo) {
|
|
63
|
+
reasons.push("Recipient memo or tag flows are not supported for EVM transfers in this build.");
|
|
64
|
+
}
|
|
65
|
+
if (normalizedInstruction.expiresAt) {
|
|
66
|
+
const expiresAtMs = Date.parse(normalizedInstruction.expiresAt);
|
|
67
|
+
if (!Number.isFinite(expiresAtMs)) {
|
|
68
|
+
reasons.push("Instruction expiry could not be parsed.");
|
|
69
|
+
}
|
|
70
|
+
else if (expiresAtMs <= Date.now()) {
|
|
71
|
+
reasons.push("Instruction quote or deposit window has already expired.");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const registryResolution = resolveTokenRegistryEntry({
|
|
75
|
+
policy: args.policy,
|
|
76
|
+
chainId: normalizedInstruction.chainId,
|
|
77
|
+
symbol: normalizedInstruction.tokenSymbol,
|
|
78
|
+
contract: normalizedInstruction.tokenContract
|
|
79
|
+
});
|
|
80
|
+
if (registryResolution.ambiguous) {
|
|
81
|
+
reasons.push(`Token symbol '${normalizedInstruction.tokenSymbol}' maps to multiple registry entries on chain ${normalizedInstruction.chainId}; a contract address is required.`);
|
|
82
|
+
}
|
|
83
|
+
const registryEntry = registryResolution.entry;
|
|
84
|
+
if (registryEntry) {
|
|
85
|
+
normalizedInstruction.assetKind = registryEntry.assetKind;
|
|
86
|
+
normalizedInstruction.tokenDecimals = registryEntry.decimals;
|
|
87
|
+
if (registryEntry.contract) {
|
|
88
|
+
normalizedInstruction.tokenContract = registryEntry.contract;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (normalizedInstruction.assetKind === "native" && normalizedInstruction.tokenDecimals === undefined) {
|
|
92
|
+
normalizedInstruction.tokenDecimals = 18;
|
|
93
|
+
}
|
|
94
|
+
if (normalizedInstruction.assetKind === "erc20") {
|
|
95
|
+
if (!normalizedInstruction.tokenContract) {
|
|
96
|
+
reasons.push(`No ERC-20 contract address is known for token '${normalizedInstruction.tokenSymbol}'.`);
|
|
97
|
+
}
|
|
98
|
+
if (args.policy.requireExactTokenContract && !normalizedInstruction.tokenContract) {
|
|
99
|
+
reasons.push("Trade policy requires an exact token contract for ERC-20 transfers.");
|
|
100
|
+
}
|
|
101
|
+
if (normalizedInstruction.tokenDecimals === undefined) {
|
|
102
|
+
reasons.push(`No decimals are known for token '${normalizedInstruction.tokenSymbol}'.`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (args.policy.maxTokenAmount &&
|
|
106
|
+
isNumberishDecimal(normalizedInstruction.amount) &&
|
|
107
|
+
compareDecimalStrings(normalizedInstruction.amount, args.policy.maxTokenAmount) > 0) {
|
|
108
|
+
reasons.push(`Amount ${normalizedInstruction.amount} exceeds the configured max token amount ${args.policy.maxTokenAmount}.`);
|
|
109
|
+
}
|
|
110
|
+
if (reasons.length === 0) {
|
|
111
|
+
try {
|
|
112
|
+
const walletAddress = await getWalletAddress();
|
|
113
|
+
if (normalizedInstruction.assetKind === "native") {
|
|
114
|
+
const balance = await readNativeBalance(walletAddress);
|
|
115
|
+
const amountBaseUnits = await parseTokenAmount(normalizedInstruction.amount, normalizedInstruction.tokenDecimals ?? 18);
|
|
116
|
+
if (balance <= 0n) {
|
|
117
|
+
reasons.push("The wallet does not have a native balance available for this transfer.");
|
|
118
|
+
}
|
|
119
|
+
else if (balance < amountBaseUnits) {
|
|
120
|
+
reasons.push("The wallet does not have enough native balance for this transfer amount.");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else if (normalizedInstruction.tokenContract) {
|
|
124
|
+
const balance = await readErc20Balance({
|
|
125
|
+
contract: normalizedInstruction.tokenContract,
|
|
126
|
+
owner: walletAddress
|
|
127
|
+
});
|
|
128
|
+
const amountBaseUnits = await parseTokenAmount(normalizedInstruction.amount, normalizedInstruction.tokenDecimals ?? 0);
|
|
129
|
+
if (balance <= 0n) {
|
|
130
|
+
reasons.push(`The wallet does not hold a positive ${normalizedInstruction.tokenSymbol} balance.`);
|
|
131
|
+
}
|
|
132
|
+
else if (balance < amountBaseUnits) {
|
|
133
|
+
reasons.push(`The wallet does not hold enough ${normalizedInstruction.tokenSymbol} for this transfer amount.`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
reasons.push(`Unable to verify wallet balance before execution: ${error instanceof Error ? error.message : String(error)}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
validation: {
|
|
143
|
+
ok: reasons.length === 0,
|
|
144
|
+
reasons
|
|
145
|
+
},
|
|
146
|
+
normalizedInstruction
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const DEFAULT_RENDER_DATA_ROOT = "/opt/render/project/src/data";
|
|
5
|
+
export function ensureDir(dirPath) {
|
|
6
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
7
|
+
}
|
|
8
|
+
export function readUtf8(filePath) {
|
|
9
|
+
return fs.readFileSync(filePath, "utf8");
|
|
10
|
+
}
|
|
11
|
+
export function writeJson(filePath, data) {
|
|
12
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
|
|
13
|
+
}
|
|
14
|
+
export function writeText(filePath, content) {
|
|
15
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
16
|
+
}
|
|
17
|
+
function resolveDataRoot() {
|
|
18
|
+
const configuredRoot = process.env.SITE_AGENT_DATA_DIR?.trim();
|
|
19
|
+
if (configuredRoot) {
|
|
20
|
+
ensureDir(configuredRoot);
|
|
21
|
+
return configuredRoot;
|
|
22
|
+
}
|
|
23
|
+
if (process.env.RENDER === "true") {
|
|
24
|
+
ensureDir(DEFAULT_RENDER_DATA_ROOT);
|
|
25
|
+
return DEFAULT_RENDER_DATA_ROOT;
|
|
26
|
+
}
|
|
27
|
+
// Some serverless runtimes expose a read-only working tree.
|
|
28
|
+
if (process.cwd().startsWith('/var/task')) {
|
|
29
|
+
const tempRoot = path.join(os.tmpdir(), "site-agent-pro");
|
|
30
|
+
ensureDir(tempRoot);
|
|
31
|
+
return tempRoot;
|
|
32
|
+
}
|
|
33
|
+
return process.cwd();
|
|
34
|
+
}
|
|
35
|
+
export function timestampSlug() {
|
|
36
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
37
|
+
}
|
|
38
|
+
export function safeSlug(input) {
|
|
39
|
+
return input
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
42
|
+
.replace(/(^-|-$)/g, "");
|
|
43
|
+
}
|
|
44
|
+
export function resolveRunsDir() {
|
|
45
|
+
const dir = path.join(resolveDataRoot(), "runs");
|
|
46
|
+
ensureDir(dir);
|
|
47
|
+
return dir;
|
|
48
|
+
}
|
|
49
|
+
export function resolveRunDir(baseUrl) {
|
|
50
|
+
const host = new URL(baseUrl).hostname.replace(/^www\./, "");
|
|
51
|
+
const dir = path.join(resolveRunsDir(), `${timestampSlug()}-${safeSlug(host)}`);
|
|
52
|
+
ensureDir(dir);
|
|
53
|
+
return dir;
|
|
54
|
+
}
|
|
55
|
+
export function resolveSubmissionsDir() {
|
|
56
|
+
const dir = path.join(resolveDataRoot(), "submissions");
|
|
57
|
+
ensureDir(dir);
|
|
58
|
+
return dir;
|
|
59
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { inspect } from "node:util";
|
|
2
|
+
function isDebugEnabled() {
|
|
3
|
+
const value = process.env.SITE_AGENT_DEBUG?.trim().toLowerCase();
|
|
4
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
5
|
+
}
|
|
6
|
+
export function info(message) {
|
|
7
|
+
process.stdout.write(`[INFO] ${message}\n`);
|
|
8
|
+
}
|
|
9
|
+
export function warn(message) {
|
|
10
|
+
process.stderr.write(`[WARN] ${message}\n`);
|
|
11
|
+
}
|
|
12
|
+
export function error(message) {
|
|
13
|
+
process.stderr.write(`[ERROR] ${message}\n`);
|
|
14
|
+
}
|
|
15
|
+
export function debug(message, details) {
|
|
16
|
+
if (!isDebugEnabled()) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (details === undefined) {
|
|
20
|
+
process.stdout.write(`[DEBUG] ${message}\n`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
process.stdout.write(`[DEBUG] ${message} ${inspect(details, { depth: 4, breakLength: 120 })}\n`);
|
|
24
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const PLAYWRIGHT_PAGE_COMPAT_INIT_SCRIPT = `
|
|
2
|
+
(() => {
|
|
3
|
+
if (typeof globalThis.__name !== "function") {
|
|
4
|
+
Object.defineProperty(globalThis, "__name", {
|
|
5
|
+
configurable: true,
|
|
6
|
+
writable: true,
|
|
7
|
+
value: (target) => target
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
})();
|
|
11
|
+
`;
|
|
12
|
+
export async function installPlaywrightPageCompat(context) {
|
|
13
|
+
await context.addInitScript({ content: PLAYWRIGHT_PAGE_COMPAT_INIT_SCRIPT });
|
|
14
|
+
}
|