safehands-pharos 1.3.0 → 1.4.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/.env.example +64 -26
- package/README.md +333 -445
- package/dist/cli.d.ts +5 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +124 -98
- package/dist/cli.js.map +1 -1
- package/dist/demo.d.ts +1 -1
- package/dist/demo.js +171 -171
- package/dist/index.d.ts +2 -2
- package/dist/index.js +138 -85
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +1 -1
- package/dist/init.js +65 -65
- package/dist/lib/auditLog.d.ts +9 -0
- package/dist/lib/auditLog.d.ts.map +1 -0
- package/dist/lib/auditLog.js +30 -0
- package/dist/lib/auditLog.js.map +1 -0
- package/dist/lib/constants.d.ts +291 -291
- package/dist/lib/constants.js +292 -292
- package/dist/lib/dodoApi.d.ts +78 -70
- package/dist/lib/dodoApi.d.ts.map +1 -1
- package/dist/lib/dodoApi.js +196 -178
- package/dist/lib/dodoApi.js.map +1 -1
- package/dist/lib/http.d.ts +14 -14
- package/dist/lib/http.js +118 -118
- package/dist/lib/pharosClient.d.ts +58 -58
- package/dist/lib/pharosClient.d.ts.map +1 -1
- package/dist/lib/pharosClient.js +63 -53
- package/dist/lib/pharosClient.js.map +1 -1
- package/dist/lib/policy/actionPolicyEngine.d.ts +53 -53
- package/dist/lib/policy/actionPolicyEngine.js +212 -212
- package/dist/lib/riskEngine.d.ts +26 -26
- package/dist/lib/riskEngine.js +283 -283
- package/dist/lib/signer/index.d.ts +24 -24
- package/dist/lib/signer/index.d.ts.map +1 -1
- package/dist/lib/signer/index.js +88 -89
- package/dist/lib/signer/index.js.map +1 -1
- package/dist/lib/spendAccumulator.d.ts +10 -0
- package/dist/lib/spendAccumulator.d.ts.map +1 -0
- package/dist/lib/spendAccumulator.js +54 -0
- package/dist/lib/spendAccumulator.js.map +1 -0
- package/dist/lib/testDodoLive.d.ts +1 -1
- package/dist/lib/testDodoLive.js +104 -104
- package/dist/lib/testLiveSafehands.d.ts +1 -1
- package/dist/lib/testLiveSafehands.js +92 -92
- package/dist/lib/testRpc.d.ts +1 -1
- package/dist/lib/testRpc.js +29 -29
- package/dist/lib/testRpcLive.d.ts +1 -1
- package/dist/lib/testRpcLive.js +88 -88
- package/dist/lib/testTools.d.ts +1 -1
- package/dist/lib/testTools.js +397 -397
- package/dist/lib/testX402Live.d.ts +1 -1
- package/dist/lib/testX402Live.js +159 -159
- package/dist/lib/toolResponse.d.ts +25 -25
- package/dist/lib/toolResponse.js +53 -53
- package/dist/lib/wallet/index.d.ts +37 -18
- package/dist/lib/wallet/index.d.ts.map +1 -1
- package/dist/lib/wallet/index.js +128 -70
- package/dist/lib/wallet/index.js.map +1 -1
- package/dist/scripts/checkDeploy.d.ts +1 -1
- package/dist/scripts/checkDeploy.js +24 -24
- package/dist/scripts/deployRegistry.d.ts +1 -1
- package/dist/scripts/deployRegistry.js +100 -100
- package/dist/scripts/testRegistry.d.ts +1 -1
- package/dist/scripts/testRegistry.js +43 -43
- package/dist/tools/approveToken.d.ts +45 -46
- package/dist/tools/approveToken.d.ts.map +1 -1
- package/dist/tools/approveToken.js +85 -83
- package/dist/tools/approveToken.js.map +1 -1
- package/dist/tools/assessRisk.d.ts +79 -79
- package/dist/tools/assessRisk.d.ts.map +1 -1
- package/dist/tools/assessRisk.js +104 -93
- package/dist/tools/assessRisk.js.map +1 -1
- package/dist/tools/checkAllowance.d.ts +43 -36
- package/dist/tools/checkAllowance.d.ts.map +1 -1
- package/dist/tools/checkAllowance.js +56 -42
- package/dist/tools/checkAllowance.js.map +1 -1
- package/dist/tools/checkTokenSecurity.d.ts +46 -46
- package/dist/tools/checkTokenSecurity.d.ts.map +1 -1
- package/dist/tools/checkTokenSecurity.js +95 -88
- package/dist/tools/checkTokenSecurity.js.map +1 -1
- package/dist/tools/createAgentWallet.d.ts +26 -26
- package/dist/tools/createAgentWallet.d.ts.map +1 -1
- package/dist/tools/createAgentWallet.js +58 -59
- package/dist/tools/createAgentWallet.js.map +1 -1
- package/dist/tools/estimateGas.d.ts +79 -79
- package/dist/tools/estimateGas.js +124 -124
- package/dist/tools/executeSwap.d.ts +61 -59
- package/dist/tools/executeSwap.d.ts.map +1 -1
- package/dist/tools/executeSwap.js +141 -129
- package/dist/tools/executeSwap.js.map +1 -1
- package/dist/tools/explainRisk.d.ts +29 -29
- package/dist/tools/explainRisk.js +32 -32
- package/dist/tools/getAgentWallet.d.ts +21 -21
- package/dist/tools/getAgentWallet.js +27 -27
- package/dist/tools/getAgentWalletBalance.d.ts +11 -11
- package/dist/tools/getAgentWalletBalance.js +70 -70
- package/dist/tools/getExecutionHistory.d.ts +49 -51
- package/dist/tools/getExecutionHistory.d.ts.map +1 -1
- package/dist/tools/getExecutionHistory.js +154 -93
- package/dist/tools/getExecutionHistory.js.map +1 -1
- package/dist/tools/getGasPrice.d.ts +43 -43
- package/dist/tools/getGasPrice.js +59 -59
- package/dist/tools/getPoolInfo.d.ts +75 -75
- package/dist/tools/getPoolInfo.js +137 -137
- package/dist/tools/getTokenPrice.d.ts +113 -113
- package/dist/tools/getTokenPrice.js +117 -117
- package/dist/tools/getTransactionStatus.d.ts +43 -57
- package/dist/tools/getTransactionStatus.d.ts.map +1 -1
- package/dist/tools/getTransactionStatus.js +59 -67
- package/dist/tools/getTransactionStatus.js.map +1 -1
- package/dist/tools/getWalletBalance.d.ts +68 -68
- package/dist/tools/getWalletBalance.js +87 -87
- package/dist/tools/publishRiskScore.d.ts +63 -63
- package/dist/tools/publishRiskScore.d.ts.map +1 -1
- package/dist/tools/publishRiskScore.js +88 -85
- package/dist/tools/publishRiskScore.js.map +1 -1
- package/dist/tools/queryRiskRegistry.d.ts +38 -48
- package/dist/tools/queryRiskRegistry.d.ts.map +1 -1
- package/dist/tools/queryRiskRegistry.js +55 -60
- package/dist/tools/queryRiskRegistry.js.map +1 -1
- package/dist/tools/safehandsPreflightCheck.d.ts +77 -77
- package/dist/tools/safehandsPreflightCheck.js +47 -47
- package/dist/tools/safehandsRiskReport.d.ts +81 -81
- package/dist/tools/safehandsRiskReport.js +28 -28
- package/dist/tools/safehandsSafeExecute.d.ts +20 -20
- package/dist/tools/safehandsSafeExecute.d.ts.map +1 -1
- package/dist/tools/safehandsSafeExecute.js +81 -75
- package/dist/tools/safehandsSafeExecute.js.map +1 -1
- package/dist/tools/safehandsWalletHealth.d.ts +14 -14
- package/dist/tools/safehandsWalletHealth.js +103 -103
- package/dist/tools/safehandsX402Preflight.d.ts +26 -26
- package/dist/tools/safehandsX402Preflight.js +65 -65
- package/dist/tools/sendPayment.d.ts +57 -58
- package/dist/tools/sendPayment.d.ts.map +1 -1
- package/dist/tools/sendPayment.js +117 -108
- package/dist/tools/sendPayment.js.map +1 -1
- package/dist/tools/simulateTransaction.d.ts +60 -81
- package/dist/tools/simulateTransaction.d.ts.map +1 -1
- package/dist/tools/simulateTransaction.js +83 -88
- package/dist/tools/simulateTransaction.js.map +1 -1
- package/dist/tools/tokenRegistryStatus.d.ts +26 -26
- package/dist/tools/tokenRegistryStatus.js +96 -96
- package/dist/tools/x402PayAndFetch.d.ts +81 -81
- package/dist/tools/x402PayAndFetch.d.ts.map +1 -1
- package/dist/tools/x402PayAndFetch.js +152 -149
- package/dist/tools/x402PayAndFetch.js.map +1 -1
- package/dist/x402Server.d.ts +1 -1
- package/dist/x402Server.js +252 -252
- package/examples/dashboard/index.html +337 -0
- package/package.json +83 -84
- package/.agents/skill/safehands/SKILL.md +0 -212
- package/.agents/skill/safehands/assets/networks.json +0 -24
- package/.agents/skill/safehands/assets/tokens.json +0 -66
- package/.agents/wallets.json +0 -20
- package/docs/reports/OFFICIAL_DOCS_ALIGNMENT_REPORT.md +0 -137
- package/docs/reports/final_audit_report.md +0 -307
- package/docs/reports/live_verification_report.md +0 -147
- package/docs/reports/pharos_skill_engine_alignment_report.md +0 -85
package/dist/lib/riskEngine.js
CHANGED
|
@@ -1,284 +1,284 @@
|
|
|
1
|
-
// ─── Risk Scoring Engine ───────────────────────────────────────────────
|
|
2
|
-
// 5-dimension risk assessment engine for SafeHands.
|
|
3
|
-
// Weights: liquidity 25%, slippage 25%, counterparty 20%,
|
|
4
|
-
// balance 15%, market conditions 15%
|
|
5
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
6
|
-
import { publicClient } from "./pharosClient.js";
|
|
7
|
-
import { getDodoRoute, isNativeToken, resolveTokenAddress, resolveTokenDecimals, toWei, } from "./dodoApi.js";
|
|
8
|
-
import { RISK_WEIGHTS, RISK_BLOCK_THRESHOLD, MAX_SLIPPAGE_PCT, MAX_BALANCE_USAGE_PCT, ERC20_ABI, } from "./constants.js";
|
|
9
|
-
import { formatEther, isAddress, parseEther } from "viem";
|
|
10
|
-
// ─── Helpers ───────────────────────────────────────────────────────────
|
|
11
|
-
function clamp(value, min = 0, max = 100) {
|
|
12
|
-
return Math.max(min, Math.min(max, value));
|
|
13
|
-
}
|
|
14
|
-
function computeRiskLevel(score) {
|
|
15
|
-
if (score <= 30)
|
|
16
|
-
return "low";
|
|
17
|
-
if (score <= 60)
|
|
18
|
-
return "medium";
|
|
19
|
-
if (score <= 80)
|
|
20
|
-
return "high";
|
|
21
|
-
return "critical";
|
|
22
|
-
}
|
|
23
|
-
function computeRecommendation(score) {
|
|
24
|
-
if (score <= 30)
|
|
25
|
-
return "proceed";
|
|
26
|
-
if (score <= RISK_BLOCK_THRESHOLD)
|
|
27
|
-
return "caution";
|
|
28
|
-
return "block";
|
|
29
|
-
}
|
|
30
|
-
function weightedAverage(breakdown) {
|
|
31
|
-
const w = RISK_WEIGHTS;
|
|
32
|
-
return Math.round(breakdown.liquidityRisk * w.liquidityRisk +
|
|
33
|
-
breakdown.slippageRisk * w.slippageRisk +
|
|
34
|
-
breakdown.counterpartyRisk * w.counterpartyRisk +
|
|
35
|
-
breakdown.balanceRisk * w.balanceRisk +
|
|
36
|
-
breakdown.marketConditionRisk * w.marketConditionRisk);
|
|
37
|
-
}
|
|
38
|
-
// ─── Individual Risk Scorers ───────────────────────────────────────────
|
|
39
|
-
async function scoreLiquidity(input) {
|
|
40
|
-
const reasons = [];
|
|
41
|
-
if (input.action !== "swap")
|
|
42
|
-
return { score: 0, reasons };
|
|
43
|
-
try {
|
|
44
|
-
const quote = await getDodoRoute({
|
|
45
|
-
fromToken: input.tokenIn,
|
|
46
|
-
toToken: input.tokenOut,
|
|
47
|
-
amountHuman: input.amount,
|
|
48
|
-
walletAddress: input.walletAddress,
|
|
49
|
-
});
|
|
50
|
-
if (!quote.routeAvailable) {
|
|
51
|
-
reasons.push("No swap route available — liquidity may be exhausted");
|
|
52
|
-
return { score: 100, reasons };
|
|
53
|
-
}
|
|
54
|
-
const impact = Math.abs(quote.priceImpact);
|
|
55
|
-
if (impact > 5) {
|
|
56
|
-
reasons.push(`High price impact: ${impact.toFixed(2)}%`);
|
|
57
|
-
return { score: clamp(80 + impact), reasons };
|
|
58
|
-
}
|
|
59
|
-
if (impact > 2) {
|
|
60
|
-
reasons.push(`Moderate price impact: ${impact.toFixed(2)}%`);
|
|
61
|
-
return { score: clamp(40 + impact * 10), reasons };
|
|
62
|
-
}
|
|
63
|
-
reasons.push(`Price impact acceptable: ${impact.toFixed(2)}%`);
|
|
64
|
-
return { score: clamp(impact * 15), reasons };
|
|
65
|
-
}
|
|
66
|
-
catch (err) {
|
|
67
|
-
reasons.push(`Failed to fetch liquidity data: ${err.message}`);
|
|
68
|
-
return { score: 70, reasons };
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
async function scoreSlippage(input) {
|
|
72
|
-
const reasons = [];
|
|
73
|
-
if (input.action !== "swap")
|
|
74
|
-
return { score: 0, reasons };
|
|
75
|
-
try {
|
|
76
|
-
const quote = await getDodoRoute({
|
|
77
|
-
fromToken: input.tokenIn,
|
|
78
|
-
toToken: input.tokenOut,
|
|
79
|
-
amountHuman: input.amount,
|
|
80
|
-
walletAddress: input.walletAddress,
|
|
81
|
-
});
|
|
82
|
-
if (!quote.routeAvailable) {
|
|
83
|
-
reasons.push("Cannot estimate slippage — no route");
|
|
84
|
-
return { score: 90, reasons };
|
|
85
|
-
}
|
|
86
|
-
const impact = Math.abs(quote.priceImpact);
|
|
87
|
-
if (impact > MAX_SLIPPAGE_PCT) {
|
|
88
|
-
reasons.push(`Slippage exceeds max ${MAX_SLIPPAGE_PCT}%: estimated ${impact.toFixed(2)}%`);
|
|
89
|
-
return { score: clamp(80 + (impact - MAX_SLIPPAGE_PCT) * 5), reasons };
|
|
90
|
-
}
|
|
91
|
-
reasons.push(`Estimated slippage: ${impact.toFixed(2)}%`);
|
|
92
|
-
return { score: clamp(impact * 16), reasons };
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
reasons.push("Slippage estimation failed");
|
|
96
|
-
return { score: 60, reasons };
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
async function scoreCounterparty(input) {
|
|
100
|
-
const reasons = [];
|
|
101
|
-
if (input.action === "swap") {
|
|
102
|
-
// For swaps, counterparty is the DODO protocol — trusted
|
|
103
|
-
reasons.push("Swap routed through DODO protocol (known)");
|
|
104
|
-
return { score: 5, reasons };
|
|
105
|
-
}
|
|
106
|
-
// For transfers, check the recipient address
|
|
107
|
-
const to = input.toAddress;
|
|
108
|
-
if (!to) {
|
|
109
|
-
reasons.push("No recipient address provided");
|
|
110
|
-
return { score: 100, reasons };
|
|
111
|
-
}
|
|
112
|
-
if (!isAddress(to)) {
|
|
113
|
-
reasons.push("Invalid EVM address format");
|
|
114
|
-
return { score: 100, reasons };
|
|
115
|
-
}
|
|
116
|
-
// Zero address check
|
|
117
|
-
if (to === "0x0000000000000000000000000000000000000000") {
|
|
118
|
-
reasons.push("Sending to zero address — tokens will be burned");
|
|
119
|
-
return { score: 95, reasons };
|
|
120
|
-
}
|
|
121
|
-
// Self-send check
|
|
122
|
-
if (to.toLowerCase() === input.walletAddress.toLowerCase()) {
|
|
123
|
-
reasons.push("Sending to own address — likely unintended");
|
|
124
|
-
return { score: 50, reasons };
|
|
125
|
-
}
|
|
126
|
-
// Check if address has code (is a contract)
|
|
127
|
-
try {
|
|
128
|
-
const code = await publicClient.getCode({ address: to });
|
|
129
|
-
if (code && code !== "0x") {
|
|
130
|
-
reasons.push("Recipient is a contract address — verify intent");
|
|
131
|
-
return { score: 40, reasons };
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
// RPC error, non-critical
|
|
136
|
-
}
|
|
137
|
-
// Check if address has any transaction history (has balance)
|
|
138
|
-
try {
|
|
139
|
-
const balance = await publicClient.getBalance({ address: to });
|
|
140
|
-
if (balance === 0n) {
|
|
141
|
-
reasons.push("Recipient has zero balance — new or unused address");
|
|
142
|
-
return { score: 30, reasons };
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
// Non-critical
|
|
147
|
-
}
|
|
148
|
-
reasons.push("Recipient address looks valid");
|
|
149
|
-
return { score: 10, reasons };
|
|
150
|
-
}
|
|
151
|
-
async function scoreBalance(input) {
|
|
152
|
-
const reasons = [];
|
|
153
|
-
try {
|
|
154
|
-
const walletAddr = input.walletAddress;
|
|
155
|
-
if (input.action === "transfer" || (input.action === "swap" && isNativeToken(input.tokenIn || "PHRS"))) {
|
|
156
|
-
// Check native PHRS balance
|
|
157
|
-
const balance = await publicClient.getBalance({ address: walletAddr });
|
|
158
|
-
const amountWei = parseEther(input.amount);
|
|
159
|
-
const gasBuffer = parseEther("0.01"); // ~0.01 PHRS for gas
|
|
160
|
-
if (balance < amountWei + gasBuffer) {
|
|
161
|
-
const balanceHuman = formatEther(balance);
|
|
162
|
-
reasons.push(`Insufficient PHRS balance: have ${balanceHuman}, need ${input.amount} + gas`);
|
|
163
|
-
return { score: 100, reasons };
|
|
164
|
-
}
|
|
165
|
-
const usagePct = Number((amountWei * 100n) / balance);
|
|
166
|
-
if (usagePct > MAX_BALANCE_USAGE_PCT) {
|
|
167
|
-
reasons.push(`Using ${usagePct}% of wallet balance — high exposure`);
|
|
168
|
-
return { score: clamp(60 + (usagePct - MAX_BALANCE_USAGE_PCT)), reasons };
|
|
169
|
-
}
|
|
170
|
-
reasons.push(`Balance sufficient, using ${usagePct}% of wallet`);
|
|
171
|
-
return { score: clamp(usagePct / 3), reasons };
|
|
172
|
-
}
|
|
173
|
-
// ERC-20 token balance check
|
|
174
|
-
if (input.tokenIn) {
|
|
175
|
-
const tokenAddress = resolveTokenAddress(input.tokenIn);
|
|
176
|
-
const decimals = resolveTokenDecimals(input.tokenIn);
|
|
177
|
-
const amountWei = BigInt(toWei(input.amount, decimals));
|
|
178
|
-
const balance = (await publicClient.readContract({
|
|
179
|
-
address: tokenAddress,
|
|
180
|
-
abi: ERC20_ABI,
|
|
181
|
-
functionName: "balanceOf",
|
|
182
|
-
args: [walletAddr],
|
|
183
|
-
}));
|
|
184
|
-
if (balance < amountWei) {
|
|
185
|
-
reasons.push(`Insufficient ${input.tokenIn} balance`);
|
|
186
|
-
return { score: 100, reasons };
|
|
187
|
-
}
|
|
188
|
-
// Also check gas balance
|
|
189
|
-
const ethBalance = await publicClient.getBalance({ address: walletAddr });
|
|
190
|
-
if (ethBalance < parseEther("0.005")) {
|
|
191
|
-
reasons.push("Very low PHRS balance for gas fees");
|
|
192
|
-
return { score: 80, reasons };
|
|
193
|
-
}
|
|
194
|
-
reasons.push("Token and gas balance sufficient");
|
|
195
|
-
return { score: 5, reasons };
|
|
196
|
-
}
|
|
197
|
-
return { score: 0, reasons: ["Balance check skipped"] };
|
|
198
|
-
}
|
|
199
|
-
catch (err) {
|
|
200
|
-
reasons.push(`Balance check failed: ${err.message}`);
|
|
201
|
-
return { score: 50, reasons };
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
async function scoreMarketCondition(_input) {
|
|
205
|
-
const reasons = [];
|
|
206
|
-
try {
|
|
207
|
-
// Check recent block production rate as a proxy for chain health
|
|
208
|
-
const latestBlock = await publicClient.getBlock({ blockTag: "latest" });
|
|
209
|
-
const prevBlock = await publicClient.getBlock({
|
|
210
|
-
blockNumber: latestBlock.number - 10n,
|
|
211
|
-
});
|
|
212
|
-
const timeDiff = Number(latestBlock.timestamp - prevBlock.timestamp);
|
|
213
|
-
const avgBlockTime = timeDiff / 10;
|
|
214
|
-
if (avgBlockTime > 30) {
|
|
215
|
-
reasons.push(`Slow block production: ~${avgBlockTime.toFixed(1)}s/block — chain may be congested`);
|
|
216
|
-
return { score: 60, reasons };
|
|
217
|
-
}
|
|
218
|
-
if (avgBlockTime > 15) {
|
|
219
|
-
reasons.push(`Slightly elevated block times: ~${avgBlockTime.toFixed(1)}s/block`);
|
|
220
|
-
return { score: 30, reasons };
|
|
221
|
-
}
|
|
222
|
-
// Check gas price
|
|
223
|
-
const gasPrice = await publicClient.getGasPrice();
|
|
224
|
-
const gasPriceGwei = Number(gasPrice) / 1e9;
|
|
225
|
-
if (gasPriceGwei > 100) {
|
|
226
|
-
reasons.push(`High gas price: ${gasPriceGwei.toFixed(1)} Gwei`);
|
|
227
|
-
return { score: 50, reasons };
|
|
228
|
-
}
|
|
229
|
-
reasons.push(`Chain healthy: ~${avgBlockTime.toFixed(1)}s/block, ${gasPriceGwei.toFixed(1)} Gwei gas`);
|
|
230
|
-
return { score: 5, reasons };
|
|
231
|
-
}
|
|
232
|
-
catch {
|
|
233
|
-
reasons.push("Could not assess market conditions");
|
|
234
|
-
return { score: 20, reasons };
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
// ─── Main Risk Assessment ──────────────────────────────────────────────
|
|
238
|
-
export async function assessRisk(input) {
|
|
239
|
-
const [liquidity, slippage, counterparty, balance, market] = await Promise.all([
|
|
240
|
-
scoreLiquidity(input),
|
|
241
|
-
scoreSlippage(input),
|
|
242
|
-
scoreCounterparty(input),
|
|
243
|
-
scoreBalance(input),
|
|
244
|
-
scoreMarketCondition(input),
|
|
245
|
-
]);
|
|
246
|
-
const breakdown = {
|
|
247
|
-
liquidityRisk: liquidity.score,
|
|
248
|
-
slippageRisk: slippage.score,
|
|
249
|
-
counterpartyRisk: counterparty.score,
|
|
250
|
-
balanceRisk: balance.score,
|
|
251
|
-
marketConditionRisk: market.score,
|
|
252
|
-
};
|
|
253
|
-
const riskScore = clamp(weightedAverage(breakdown));
|
|
254
|
-
const riskLevel = computeRiskLevel(riskScore);
|
|
255
|
-
const recommendation = computeRecommendation(riskScore);
|
|
256
|
-
const reasons = [
|
|
257
|
-
...liquidity.reasons,
|
|
258
|
-
...slippage.reasons,
|
|
259
|
-
...counterparty.reasons,
|
|
260
|
-
...balance.reasons,
|
|
261
|
-
...market.reasons,
|
|
262
|
-
];
|
|
263
|
-
let suggestion;
|
|
264
|
-
if (recommendation === "proceed") {
|
|
265
|
-
suggestion = "Risk is within acceptable range. Proceed with the transaction.";
|
|
266
|
-
}
|
|
267
|
-
else if (recommendation === "caution") {
|
|
268
|
-
suggestion =
|
|
269
|
-
"Moderate risk detected. Review the risk breakdown carefully before proceeding. Consider reducing the amount or adjusting parameters.";
|
|
270
|
-
}
|
|
271
|
-
else {
|
|
272
|
-
suggestion =
|
|
273
|
-
"High risk detected — execution blocked for safety. Review the reasons and adjust your transaction parameters significantly before retrying.";
|
|
274
|
-
}
|
|
275
|
-
return {
|
|
276
|
-
riskScore,
|
|
277
|
-
riskLevel,
|
|
278
|
-
recommendation,
|
|
279
|
-
breakdown,
|
|
280
|
-
reasons,
|
|
281
|
-
suggestion,
|
|
282
|
-
};
|
|
283
|
-
}
|
|
1
|
+
// ─── Risk Scoring Engine ───────────────────────────────────────────────
|
|
2
|
+
// 5-dimension risk assessment engine for SafeHands.
|
|
3
|
+
// Weights: liquidity 25%, slippage 25%, counterparty 20%,
|
|
4
|
+
// balance 15%, market conditions 15%
|
|
5
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
6
|
+
import { publicClient } from "./pharosClient.js";
|
|
7
|
+
import { getDodoRoute, isNativeToken, resolveTokenAddress, resolveTokenDecimals, toWei, } from "./dodoApi.js";
|
|
8
|
+
import { RISK_WEIGHTS, RISK_BLOCK_THRESHOLD, MAX_SLIPPAGE_PCT, MAX_BALANCE_USAGE_PCT, ERC20_ABI, } from "./constants.js";
|
|
9
|
+
import { formatEther, isAddress, parseEther } from "viem";
|
|
10
|
+
// ─── Helpers ───────────────────────────────────────────────────────────
|
|
11
|
+
function clamp(value, min = 0, max = 100) {
|
|
12
|
+
return Math.max(min, Math.min(max, value));
|
|
13
|
+
}
|
|
14
|
+
function computeRiskLevel(score) {
|
|
15
|
+
if (score <= 30)
|
|
16
|
+
return "low";
|
|
17
|
+
if (score <= 60)
|
|
18
|
+
return "medium";
|
|
19
|
+
if (score <= 80)
|
|
20
|
+
return "high";
|
|
21
|
+
return "critical";
|
|
22
|
+
}
|
|
23
|
+
function computeRecommendation(score) {
|
|
24
|
+
if (score <= 30)
|
|
25
|
+
return "proceed";
|
|
26
|
+
if (score <= RISK_BLOCK_THRESHOLD)
|
|
27
|
+
return "caution";
|
|
28
|
+
return "block";
|
|
29
|
+
}
|
|
30
|
+
function weightedAverage(breakdown) {
|
|
31
|
+
const w = RISK_WEIGHTS;
|
|
32
|
+
return Math.round(breakdown.liquidityRisk * w.liquidityRisk +
|
|
33
|
+
breakdown.slippageRisk * w.slippageRisk +
|
|
34
|
+
breakdown.counterpartyRisk * w.counterpartyRisk +
|
|
35
|
+
breakdown.balanceRisk * w.balanceRisk +
|
|
36
|
+
breakdown.marketConditionRisk * w.marketConditionRisk);
|
|
37
|
+
}
|
|
38
|
+
// ─── Individual Risk Scorers ───────────────────────────────────────────
|
|
39
|
+
async function scoreLiquidity(input) {
|
|
40
|
+
const reasons = [];
|
|
41
|
+
if (input.action !== "swap")
|
|
42
|
+
return { score: 0, reasons };
|
|
43
|
+
try {
|
|
44
|
+
const quote = await getDodoRoute({
|
|
45
|
+
fromToken: input.tokenIn,
|
|
46
|
+
toToken: input.tokenOut,
|
|
47
|
+
amountHuman: input.amount,
|
|
48
|
+
walletAddress: input.walletAddress,
|
|
49
|
+
});
|
|
50
|
+
if (!quote.routeAvailable) {
|
|
51
|
+
reasons.push("No swap route available — liquidity may be exhausted");
|
|
52
|
+
return { score: 100, reasons };
|
|
53
|
+
}
|
|
54
|
+
const impact = Math.abs(quote.priceImpact);
|
|
55
|
+
if (impact > 5) {
|
|
56
|
+
reasons.push(`High price impact: ${impact.toFixed(2)}%`);
|
|
57
|
+
return { score: clamp(80 + impact), reasons };
|
|
58
|
+
}
|
|
59
|
+
if (impact > 2) {
|
|
60
|
+
reasons.push(`Moderate price impact: ${impact.toFixed(2)}%`);
|
|
61
|
+
return { score: clamp(40 + impact * 10), reasons };
|
|
62
|
+
}
|
|
63
|
+
reasons.push(`Price impact acceptable: ${impact.toFixed(2)}%`);
|
|
64
|
+
return { score: clamp(impact * 15), reasons };
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
reasons.push(`Failed to fetch liquidity data: ${err.message}`);
|
|
68
|
+
return { score: 70, reasons };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function scoreSlippage(input) {
|
|
72
|
+
const reasons = [];
|
|
73
|
+
if (input.action !== "swap")
|
|
74
|
+
return { score: 0, reasons };
|
|
75
|
+
try {
|
|
76
|
+
const quote = await getDodoRoute({
|
|
77
|
+
fromToken: input.tokenIn,
|
|
78
|
+
toToken: input.tokenOut,
|
|
79
|
+
amountHuman: input.amount,
|
|
80
|
+
walletAddress: input.walletAddress,
|
|
81
|
+
});
|
|
82
|
+
if (!quote.routeAvailable) {
|
|
83
|
+
reasons.push("Cannot estimate slippage — no route");
|
|
84
|
+
return { score: 90, reasons };
|
|
85
|
+
}
|
|
86
|
+
const impact = Math.abs(quote.priceImpact);
|
|
87
|
+
if (impact > MAX_SLIPPAGE_PCT) {
|
|
88
|
+
reasons.push(`Slippage exceeds max ${MAX_SLIPPAGE_PCT}%: estimated ${impact.toFixed(2)}%`);
|
|
89
|
+
return { score: clamp(80 + (impact - MAX_SLIPPAGE_PCT) * 5), reasons };
|
|
90
|
+
}
|
|
91
|
+
reasons.push(`Estimated slippage: ${impact.toFixed(2)}%`);
|
|
92
|
+
return { score: clamp(impact * 16), reasons };
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
reasons.push("Slippage estimation failed");
|
|
96
|
+
return { score: 60, reasons };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function scoreCounterparty(input) {
|
|
100
|
+
const reasons = [];
|
|
101
|
+
if (input.action === "swap") {
|
|
102
|
+
// For swaps, counterparty is the DODO protocol — trusted
|
|
103
|
+
reasons.push("Swap routed through DODO protocol (known)");
|
|
104
|
+
return { score: 5, reasons };
|
|
105
|
+
}
|
|
106
|
+
// For transfers, check the recipient address
|
|
107
|
+
const to = input.toAddress;
|
|
108
|
+
if (!to) {
|
|
109
|
+
reasons.push("No recipient address provided");
|
|
110
|
+
return { score: 100, reasons };
|
|
111
|
+
}
|
|
112
|
+
if (!isAddress(to)) {
|
|
113
|
+
reasons.push("Invalid EVM address format");
|
|
114
|
+
return { score: 100, reasons };
|
|
115
|
+
}
|
|
116
|
+
// Zero address check
|
|
117
|
+
if (to === "0x0000000000000000000000000000000000000000") {
|
|
118
|
+
reasons.push("Sending to zero address — tokens will be burned");
|
|
119
|
+
return { score: 95, reasons };
|
|
120
|
+
}
|
|
121
|
+
// Self-send check
|
|
122
|
+
if (to.toLowerCase() === input.walletAddress.toLowerCase()) {
|
|
123
|
+
reasons.push("Sending to own address — likely unintended");
|
|
124
|
+
return { score: 50, reasons };
|
|
125
|
+
}
|
|
126
|
+
// Check if address has code (is a contract)
|
|
127
|
+
try {
|
|
128
|
+
const code = await publicClient.getCode({ address: to });
|
|
129
|
+
if (code && code !== "0x") {
|
|
130
|
+
reasons.push("Recipient is a contract address — verify intent");
|
|
131
|
+
return { score: 40, reasons };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// RPC error, non-critical
|
|
136
|
+
}
|
|
137
|
+
// Check if address has any transaction history (has balance)
|
|
138
|
+
try {
|
|
139
|
+
const balance = await publicClient.getBalance({ address: to });
|
|
140
|
+
if (balance === 0n) {
|
|
141
|
+
reasons.push("Recipient has zero balance — new or unused address");
|
|
142
|
+
return { score: 30, reasons };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Non-critical
|
|
147
|
+
}
|
|
148
|
+
reasons.push("Recipient address looks valid");
|
|
149
|
+
return { score: 10, reasons };
|
|
150
|
+
}
|
|
151
|
+
async function scoreBalance(input) {
|
|
152
|
+
const reasons = [];
|
|
153
|
+
try {
|
|
154
|
+
const walletAddr = input.walletAddress;
|
|
155
|
+
if (input.action === "transfer" || (input.action === "swap" && isNativeToken(input.tokenIn || "PHRS"))) {
|
|
156
|
+
// Check native PHRS balance
|
|
157
|
+
const balance = await publicClient.getBalance({ address: walletAddr });
|
|
158
|
+
const amountWei = parseEther(input.amount);
|
|
159
|
+
const gasBuffer = parseEther("0.01"); // ~0.01 PHRS for gas
|
|
160
|
+
if (balance < amountWei + gasBuffer) {
|
|
161
|
+
const balanceHuman = formatEther(balance);
|
|
162
|
+
reasons.push(`Insufficient PHRS balance: have ${balanceHuman}, need ${input.amount} + gas`);
|
|
163
|
+
return { score: 100, reasons };
|
|
164
|
+
}
|
|
165
|
+
const usagePct = Number((amountWei * 100n) / balance);
|
|
166
|
+
if (usagePct > MAX_BALANCE_USAGE_PCT) {
|
|
167
|
+
reasons.push(`Using ${usagePct}% of wallet balance — high exposure`);
|
|
168
|
+
return { score: clamp(60 + (usagePct - MAX_BALANCE_USAGE_PCT)), reasons };
|
|
169
|
+
}
|
|
170
|
+
reasons.push(`Balance sufficient, using ${usagePct}% of wallet`);
|
|
171
|
+
return { score: clamp(usagePct / 3), reasons };
|
|
172
|
+
}
|
|
173
|
+
// ERC-20 token balance check
|
|
174
|
+
if (input.tokenIn) {
|
|
175
|
+
const tokenAddress = resolveTokenAddress(input.tokenIn);
|
|
176
|
+
const decimals = resolveTokenDecimals(input.tokenIn);
|
|
177
|
+
const amountWei = BigInt(toWei(input.amount, decimals));
|
|
178
|
+
const balance = (await publicClient.readContract({
|
|
179
|
+
address: tokenAddress,
|
|
180
|
+
abi: ERC20_ABI,
|
|
181
|
+
functionName: "balanceOf",
|
|
182
|
+
args: [walletAddr],
|
|
183
|
+
}));
|
|
184
|
+
if (balance < amountWei) {
|
|
185
|
+
reasons.push(`Insufficient ${input.tokenIn} balance`);
|
|
186
|
+
return { score: 100, reasons };
|
|
187
|
+
}
|
|
188
|
+
// Also check gas balance
|
|
189
|
+
const ethBalance = await publicClient.getBalance({ address: walletAddr });
|
|
190
|
+
if (ethBalance < parseEther("0.005")) {
|
|
191
|
+
reasons.push("Very low PHRS balance for gas fees");
|
|
192
|
+
return { score: 80, reasons };
|
|
193
|
+
}
|
|
194
|
+
reasons.push("Token and gas balance sufficient");
|
|
195
|
+
return { score: 5, reasons };
|
|
196
|
+
}
|
|
197
|
+
return { score: 0, reasons: ["Balance check skipped"] };
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
reasons.push(`Balance check failed: ${err.message}`);
|
|
201
|
+
return { score: 50, reasons };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function scoreMarketCondition(_input) {
|
|
205
|
+
const reasons = [];
|
|
206
|
+
try {
|
|
207
|
+
// Check recent block production rate as a proxy for chain health
|
|
208
|
+
const latestBlock = await publicClient.getBlock({ blockTag: "latest" });
|
|
209
|
+
const prevBlock = await publicClient.getBlock({
|
|
210
|
+
blockNumber: latestBlock.number - 10n,
|
|
211
|
+
});
|
|
212
|
+
const timeDiff = Number(latestBlock.timestamp - prevBlock.timestamp);
|
|
213
|
+
const avgBlockTime = timeDiff / 10;
|
|
214
|
+
if (avgBlockTime > 30) {
|
|
215
|
+
reasons.push(`Slow block production: ~${avgBlockTime.toFixed(1)}s/block — chain may be congested`);
|
|
216
|
+
return { score: 60, reasons };
|
|
217
|
+
}
|
|
218
|
+
if (avgBlockTime > 15) {
|
|
219
|
+
reasons.push(`Slightly elevated block times: ~${avgBlockTime.toFixed(1)}s/block`);
|
|
220
|
+
return { score: 30, reasons };
|
|
221
|
+
}
|
|
222
|
+
// Check gas price
|
|
223
|
+
const gasPrice = await publicClient.getGasPrice();
|
|
224
|
+
const gasPriceGwei = Number(gasPrice) / 1e9;
|
|
225
|
+
if (gasPriceGwei > 100) {
|
|
226
|
+
reasons.push(`High gas price: ${gasPriceGwei.toFixed(1)} Gwei`);
|
|
227
|
+
return { score: 50, reasons };
|
|
228
|
+
}
|
|
229
|
+
reasons.push(`Chain healthy: ~${avgBlockTime.toFixed(1)}s/block, ${gasPriceGwei.toFixed(1)} Gwei gas`);
|
|
230
|
+
return { score: 5, reasons };
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
reasons.push("Could not assess market conditions");
|
|
234
|
+
return { score: 20, reasons };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// ─── Main Risk Assessment ──────────────────────────────────────────────
|
|
238
|
+
export async function assessRisk(input) {
|
|
239
|
+
const [liquidity, slippage, counterparty, balance, market] = await Promise.all([
|
|
240
|
+
scoreLiquidity(input),
|
|
241
|
+
scoreSlippage(input),
|
|
242
|
+
scoreCounterparty(input),
|
|
243
|
+
scoreBalance(input),
|
|
244
|
+
scoreMarketCondition(input),
|
|
245
|
+
]);
|
|
246
|
+
const breakdown = {
|
|
247
|
+
liquidityRisk: liquidity.score,
|
|
248
|
+
slippageRisk: slippage.score,
|
|
249
|
+
counterpartyRisk: counterparty.score,
|
|
250
|
+
balanceRisk: balance.score,
|
|
251
|
+
marketConditionRisk: market.score,
|
|
252
|
+
};
|
|
253
|
+
const riskScore = clamp(weightedAverage(breakdown));
|
|
254
|
+
const riskLevel = computeRiskLevel(riskScore);
|
|
255
|
+
const recommendation = computeRecommendation(riskScore);
|
|
256
|
+
const reasons = [
|
|
257
|
+
...liquidity.reasons,
|
|
258
|
+
...slippage.reasons,
|
|
259
|
+
...counterparty.reasons,
|
|
260
|
+
...balance.reasons,
|
|
261
|
+
...market.reasons,
|
|
262
|
+
];
|
|
263
|
+
let suggestion;
|
|
264
|
+
if (recommendation === "proceed") {
|
|
265
|
+
suggestion = "Risk is within acceptable range. Proceed with the transaction.";
|
|
266
|
+
}
|
|
267
|
+
else if (recommendation === "caution") {
|
|
268
|
+
suggestion =
|
|
269
|
+
"Moderate risk detected. Review the risk breakdown carefully before proceeding. Consider reducing the amount or adjusting parameters.";
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
suggestion =
|
|
273
|
+
"High risk detected — execution blocked for safety. Review the reasons and adjust your transaction parameters significantly before retrying.";
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
riskScore,
|
|
277
|
+
riskLevel,
|
|
278
|
+
recommendation,
|
|
279
|
+
breakdown,
|
|
280
|
+
reasons,
|
|
281
|
+
suggestion,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
284
|
//# sourceMappingURL=riskEngine.js.map
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import type { Account } from "viem";
|
|
2
|
-
export type SignerMode = "none" | "env" | "managed-testnet" | "external-signer" | "x402-env";
|
|
3
|
-
export type SignerPurpose = "write" | "x402";
|
|
4
|
-
export interface SignerResult {
|
|
5
|
-
account: Account;
|
|
6
|
-
address: `0x${string}`;
|
|
7
|
-
mode: SignerMode;
|
|
8
|
-
}
|
|
9
|
-
export interface SignerFailure {
|
|
10
|
-
error: {
|
|
11
|
-
code: string;
|
|
12
|
-
message: string;
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
export type GetSignerResult = SignerResult | SignerFailure;
|
|
16
|
-
export declare function isSignerFailure(r: GetSignerResult): r is SignerFailure;
|
|
17
|
-
/**
|
|
18
|
-
* Get a signer for write/payment operations.
|
|
19
|
-
* Priority for x402: managed wallet > X402_SIGNER_PRIVATE_KEY > PRIVATE_KEY fallback.
|
|
20
|
-
* Priority for writes: managed wallet > PRIVATE_KEY fallback.
|
|
21
|
-
*/
|
|
22
|
-
export declare function getSigner(agentId?: string, options?: {
|
|
23
|
-
purpose?: SignerPurpose;
|
|
24
|
-
}): Promise<GetSignerResult>;
|
|
1
|
+
import type { Account } from "viem";
|
|
2
|
+
export type SignerMode = "none" | "env" | "managed-testnet" | "external-signer" | "x402-env";
|
|
3
|
+
export type SignerPurpose = "write" | "x402";
|
|
4
|
+
export interface SignerResult {
|
|
5
|
+
account: Account;
|
|
6
|
+
address: `0x${string}`;
|
|
7
|
+
mode: SignerMode;
|
|
8
|
+
}
|
|
9
|
+
export interface SignerFailure {
|
|
10
|
+
error: {
|
|
11
|
+
code: string;
|
|
12
|
+
message: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export type GetSignerResult = SignerResult | SignerFailure;
|
|
16
|
+
export declare function isSignerFailure(r: GetSignerResult): r is SignerFailure;
|
|
17
|
+
/**
|
|
18
|
+
* Get a signer for write/payment operations.
|
|
19
|
+
* Priority for x402: managed wallet > X402_SIGNER_PRIVATE_KEY > PRIVATE_KEY fallback.
|
|
20
|
+
* Priority for writes: managed wallet > PRIVATE_KEY fallback.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getSigner(agentId?: string, options?: {
|
|
23
|
+
purpose?: SignerPurpose;
|
|
24
|
+
}): Promise<GetSignerResult>;
|
|
25
25
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/signer/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAGpC,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,KAAK,GAAG,iBAAiB,GAAG,iBAAiB,GAAG,UAAU,CAAC;AAC7F,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,MAAM,CAAC;AAE7C,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,KAAK,MAAM,EAAE,CAAC;IACvB,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AAED,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,aAAa,CAAC;AAE3D,wBAAgB,eAAe,CAAC,CAAC,EAAE,eAAe,GAAG,CAAC,IAAI,aAAa,CAEtE;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/signer/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAGpC,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,KAAK,GAAG,iBAAiB,GAAG,iBAAiB,GAAG,UAAU,CAAC;AAC7F,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,MAAM,CAAC;AAE7C,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,KAAK,MAAM,EAAE,CAAC;IACvB,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AAED,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,aAAa,CAAC;AAE3D,wBAAgB,eAAe,CAAC,CAAC,EAAE,eAAe,GAAG,CAAC,IAAI,aAAa,CAEtE;AAqDD;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,aAAa,CAAA;CAAO,GACxC,OAAO,CAAC,eAAe,CAAC,CA0B1B"}
|