@oracle-agent/oracle 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +37 -10
  2. package/SECURITY.md +11 -11
  3. package/SETUP.md +76 -16
  4. package/bin/desk-server.mjs +3 -1
  5. package/docs/architecture.md +10 -9
  6. package/package.json +13 -7
  7. package/protocols/templates/safe-erc20/SECURITY.md +16 -0
  8. package/protocols/templates/safe-erc20/foundry.toml +12 -0
  9. package/protocols/templates/safe-erc20/remappings.txt +2 -0
  10. package/protocols/templates/safe-erc20/src/SafeERC20.sol +53 -0
  11. package/protocols/templates/safe-erc20/test/SafeERC20.t.sol +72 -0
  12. package/public/oracle-splash/index.html +648 -598
  13. package/public/oracle-splash/mesh-graph.png +0 -0
  14. package/public/oracle-splash/mesh-graph.svg +216 -0
  15. package/scripts/protocol-template-gate.mjs +15 -0
  16. package/skills/oracle-protocol-builder/SKILL.md +45 -24
  17. package/src/data/catalog.mjs +9 -0
  18. package/src/data/desk-data.mjs +24 -0
  19. package/src/data/providers/bitcoin-esplora.mjs +2 -16
  20. package/src/data/providers/cowswap.mjs +9 -96
  21. package/src/data/providers/evm-rpc.mjs +19 -0
  22. package/src/data/providers/hl-assets.mjs +165 -0
  23. package/src/data/providers/hl-info.mjs +7 -1
  24. package/src/data/providers/hl-outcome.mjs +61 -0
  25. package/src/data/providers/hl-perps.mjs +40 -35
  26. package/src/data/providers/magiceden-sol.mjs +7 -6
  27. package/src/data/providers/poly-clob.mjs +460 -0
  28. package/src/data/providers/satflow.mjs +5 -4
  29. package/src/data/providers/solana-rpc.mjs +9 -0
  30. package/src/data/providers/uniswap-v3.mjs +3 -2
  31. package/src/index.mjs +1 -0
  32. package/src/onboarding/agent-keys.mjs +4 -0
  33. package/src/prepare-envelope.mjs +108 -0
  34. package/src/protocol-templates/gate.mjs +179 -0
  35. package/src/protocol-templates/prepare-deploy.mjs +81 -0
  36. package/src/public-control/bundler-client.mjs +3 -18
@@ -0,0 +1,179 @@
1
+ // Protocol template security gate.
2
+ // Refuse prepare-deploy unless Foundry tests are green.
3
+ // Static analysis (slither) is required when available; otherwise documented skip.
4
+ // This is NOT a substitute for a paid Solidity firm audit.
5
+
6
+ import { spawnSync } from "node:child_process";
7
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
8
+ import { join, resolve } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { createHash } from "node:crypto";
11
+
12
+ const HERE = fileURLToPath(new URL(".", import.meta.url));
13
+ const PKG_ROOT = resolve(HERE, "../..");
14
+ const TEMPLATES_ROOT = join(PKG_ROOT, "protocols", "templates");
15
+
16
+ export const TEMPLATE_AUDIT_DISCLAIMER =
17
+ "Template + Foundry tests + optional static analysis. NOT a paid security-firm audit. Do not put mainnet TVL on forks without an independent Solidity audit.";
18
+
19
+ export function listProtocolTemplates() {
20
+ if (!existsSync(TEMPLATES_ROOT)) return [];
21
+ return readdirSync(TEMPLATES_ROOT)
22
+ .filter((name) => statSync(join(TEMPLATES_ROOT, name)).isDirectory())
23
+ .map((id) => {
24
+ const dir = join(TEMPLATES_ROOT, id);
25
+ const security = join(dir, "SECURITY.md");
26
+ return {
27
+ id,
28
+ path: dir,
29
+ securityNotice: existsSync(security) ? readFileSync(security, "utf8").slice(0, 500) : null,
30
+ disclaimer: TEMPLATE_AUDIT_DISCLAIMER,
31
+ };
32
+ });
33
+ }
34
+
35
+ function forgeBin() {
36
+ if (process.env.FORGE_BIN) return process.env.FORGE_BIN;
37
+ const home = process.env.HOME || "";
38
+ const candidates = ["forge", join(home, ".foundry/bin/forge")];
39
+ for (const c of candidates) {
40
+ const r = spawnSync(c, ["--version"], { encoding: "utf8" });
41
+ if (r.status === 0) return c;
42
+ }
43
+ return null;
44
+ }
45
+
46
+ function slitherBin() {
47
+ if (process.env.SLITHER_BIN) return process.env.SLITHER_BIN;
48
+ const r = spawnSync("slither", ["--version"], { encoding: "utf8" });
49
+ return r.status === 0 ? "slither" : null;
50
+ }
51
+
52
+ function run(cmd, args, cwd) {
53
+ const r = spawnSync(cmd, args, {
54
+ cwd,
55
+ encoding: "utf8",
56
+ env: { ...process.env, PATH: `${process.env.HOME || ""}/.foundry/bin:${process.env.PATH || ""}` },
57
+ timeout: 120_000,
58
+ });
59
+ return {
60
+ ok: r.status === 0,
61
+ status: r.status,
62
+ stdout: r.stdout || "",
63
+ stderr: r.stderr || "",
64
+ };
65
+ }
66
+
67
+ function hashTree(dir) {
68
+ const h = createHash("sha256");
69
+ function walk(p) {
70
+ const st = statSync(p);
71
+ if (st.isDirectory()) {
72
+ if (["lib", "out", "cache", ".git"].includes(p.split("/").pop())) return;
73
+ for (const name of readdirSync(p).sort()) walk(join(p, name));
74
+ } else if (st.isFile() && (p.endsWith(".sol") || p.endsWith(".toml") || p.endsWith(".md"))) {
75
+ h.update(p);
76
+ h.update(readFileSync(p));
77
+ }
78
+ }
79
+ walk(dir);
80
+ return h.digest("hex");
81
+ }
82
+
83
+ /**
84
+ * Run security gate on a template id.
85
+ * @param {string} templateId e.g. "safe-erc20"
86
+ * @param {{ allowSkipStatic?: boolean }} opts
87
+ */
88
+ export function runProtocolTemplateGate(templateId, opts = {}) {
89
+ const allowSkipStatic = opts.allowSkipStatic !== false; // default allow skip if no slither
90
+ const dir = join(TEMPLATES_ROOT, templateId);
91
+ if (!existsSync(dir)) {
92
+ throw new Error(`protocol-templates: unknown template "${templateId}"`);
93
+ }
94
+
95
+ const forge = forgeBin();
96
+ if (!forge) {
97
+ throw new Error(
98
+ "protocol-templates: forge not found. Install Foundry (https://getfoundry.sh) then: cd protocols/templates/" +
99
+ templateId +
100
+ " && forge install && forge test"
101
+ );
102
+ }
103
+
104
+ // ensure libs (not vendored in git)
105
+ if (!existsSync(join(dir, "lib", "forge-std"))) {
106
+ const a = run(forge, ["install", "foundry-rs/forge-std", "--no-git"], dir);
107
+ if (!a.ok) throw new Error(`protocol-templates: forge install forge-std failed\n${a.stderr}`);
108
+ }
109
+ if (!existsSync(join(dir, "lib", "openzeppelin-contracts"))) {
110
+ const b = run(forge, ["install", "OpenZeppelin/openzeppelin-contracts@v5.0.2", "--no-git"], dir);
111
+ if (!b.ok) throw new Error(`protocol-templates: forge install openzeppelin failed\n${b.stderr}`);
112
+ }
113
+
114
+ const tests = run(forge, ["test", "-q"], dir);
115
+ if (!tests.ok) {
116
+ throw new Error(
117
+ `protocol-templates: forge test FAILED for ${templateId}\n${tests.stdout}\n${tests.stderr}`.slice(0, 2000)
118
+ );
119
+ }
120
+
121
+ let staticAnalysis = { tool: null, ok: true, skipped: true, reason: "slither not installed" };
122
+ const slither = slitherBin();
123
+ if (slither) {
124
+ const s = run(slither, [".", "--filter-paths", "lib", "--exclude-dependencies"], dir);
125
+ staticAnalysis = {
126
+ tool: "slither",
127
+ ok: s.ok,
128
+ skipped: false,
129
+ stdout: (s.stdout || "").slice(0, 1500),
130
+ stderr: (s.stderr || "").slice(0, 500),
131
+ };
132
+ if (!s.ok && !opts.allowStaticFail) {
133
+ throw new Error(`protocol-templates: slither FAILED for ${templateId}\n${s.stdout}\n${s.stderr}`.slice(0, 2000));
134
+ }
135
+ } else if (!allowSkipStatic) {
136
+ throw new Error("protocol-templates: slither required (allowSkipStatic=false) but not installed");
137
+ }
138
+
139
+ const sourceHash = hashTree(dir);
140
+ return {
141
+ ok: true,
142
+ templateId,
143
+ path: dir,
144
+ forgeTests: { ok: true, tool: "forge", summary: "forge test passed" },
145
+ staticAnalysis,
146
+ sourceHash,
147
+ disclaimer: TEMPLATE_AUDIT_DISCLAIMER,
148
+ firmAudit: false,
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Build artifact after gate. Returns bytecode for unsigned deploy prepare.
154
+ */
155
+ export function buildProtocolTemplate(templateId, opts = {}) {
156
+ const gate = runProtocolTemplateGate(templateId, opts);
157
+ const forge = forgeBin();
158
+ const dir = gate.path;
159
+ const build = run(forge, ["build", "-q"], dir);
160
+ if (!build.ok) {
161
+ throw new Error(`protocol-templates: forge build failed\n${build.stderr}`.slice(0, 1500));
162
+ }
163
+ // default contract name heuristic
164
+ const contractName = opts.contractName || (templateId === "safe-erc20" ? "SafeERC20" : null);
165
+ if (!contractName) throw new Error("protocol-templates: contractName required");
166
+ const artifactPath = join(dir, "out", `${contractName}.sol`, `${contractName}.json`);
167
+ if (!existsSync(artifactPath)) {
168
+ throw new Error(`protocol-templates: artifact missing at ${artifactPath}`);
169
+ }
170
+ const artifact = JSON.parse(readFileSync(artifactPath, "utf8"));
171
+ return {
172
+ gate,
173
+ contractName,
174
+ abi: artifact.abi,
175
+ bytecode: artifact.bytecode?.object || artifact.bytecode,
176
+ deployedBytecode: artifact.deployedBytecode?.object || artifact.deployedBytecode,
177
+ artifactPath,
178
+ };
179
+ }
@@ -0,0 +1,81 @@
1
+ // Prepare unsigned deploy for a gated protocol template.
2
+ // Never signs. Never broadcasts. Requires green forge tests first.
3
+
4
+ import { ContractFactory, isAddress } from "ethers";
5
+ import { stampPrepared } from "../prepare-envelope.mjs";
6
+ import { buildProtocolTemplate, listProtocolTemplates, TEMPLATE_AUDIT_DISCLAIMER } from "./gate.mjs";
7
+
8
+ export { listProtocolTemplates, TEMPLATE_AUDIT_DISCLAIMER };
9
+
10
+ /**
11
+ * @param {object} args
12
+ * @param {string} args.templateId - e.g. safe-erc20
13
+ * @param {number} args.chainId
14
+ * @param {string[]} [args.constructorArgs] - ABI-encoded via ethers from values
15
+ * @param {any[]} [args.args] - constructor JS values (preferred)
16
+ * @param {string} [args.contractName]
17
+ */
18
+ export async function prepareTemplateDeploy(args = {}, opts = {}) {
19
+ const templateId = String(args.templateId || "").trim();
20
+ if (!templateId) throw new Error("prepareTemplateDeploy: templateId required");
21
+ const chainId = Number(args.chainId);
22
+ if (!Number.isInteger(chainId) || chainId <= 0) {
23
+ throw new Error("prepareTemplateDeploy: chainId required");
24
+ }
25
+
26
+ const built = buildProtocolTemplate(templateId, {
27
+ contractName: args.contractName,
28
+ allowSkipStatic: opts.allowSkipStatic !== false,
29
+ allowStaticFail: opts.allowStaticFail === true,
30
+ });
31
+
32
+ if (!built.bytecode || built.bytecode === "0x") {
33
+ throw new Error("prepareTemplateDeploy: empty bytecode");
34
+ }
35
+
36
+ const bc = built.bytecode.startsWith("0x") ? built.bytecode : `0x${built.bytecode}`;
37
+ const ctorArgs = args.args ?? args.constructorArgs ?? [];
38
+ const factory = new ContractFactory(built.abi, bc);
39
+ const deployTx = await factory.getDeployTransaction(...(Array.isArray(ctorArgs) ? ctorArgs : []));
40
+ const data = deployTx.data;
41
+
42
+ // validate SafeERC20 constructor shape if applicable
43
+ if (templateId === "safe-erc20" && Array.isArray(ctorArgs)) {
44
+ const [name, symbol, supply, holder, owner] = ctorArgs;
45
+ if (!name || !symbol) throw new Error("safe-erc20: name/symbol required");
46
+ if (supply == null || BigInt(supply) <= 0n) throw new Error("safe-erc20: supply must be > 0");
47
+ if (!isAddress(String(holder))) throw new Error("safe-erc20: initialHolder must be address");
48
+ if (!isAddress(String(owner))) throw new Error("safe-erc20: initialOwner must be address");
49
+ }
50
+
51
+ return stampPrepared(
52
+ {
53
+ provider: "protocol-templates",
54
+ kind: "template-deploy",
55
+ templateId,
56
+ contractName: built.contractName,
57
+ chainId,
58
+ transaction: {
59
+ chainId,
60
+ to: null, // creation
61
+ data,
62
+ value: "0x0",
63
+ },
64
+ abi: built.abi,
65
+ bytecodeHash: built.gate.sourceHash,
66
+ gate: {
67
+ forgeTests: built.gate.forgeTests,
68
+ staticAnalysis: {
69
+ tool: built.gate.staticAnalysis.tool,
70
+ ok: built.gate.staticAnalysis.ok,
71
+ skipped: built.gate.staticAnalysis.skipped,
72
+ },
73
+ firmAudit: false,
74
+ disclaimer: TEMPLATE_AUDIT_DISCLAIMER,
75
+ },
76
+ requiresUserSignature: true,
77
+ note: "Unsigned deploy. User wallet signs. Template tests passed; not a firm audit.",
78
+ },
79
+ { provider: "protocol-templates", kind: "template-deploy" }
80
+ );
81
+ }
@@ -211,26 +211,11 @@ function toHexIfBigIntLike(value) {
211
211
  * if it does not resolve to ENTRYPOINT_V07
212
212
  * @returns {Promise<string>} userOpHash returned by the bundler client
213
213
  */
214
- export async function submitUserOperation(params = {}) {
215
- const { userOp, bundlerClient, entryPoint } = params;
216
- requireSignedUserOp(userOp);
217
- const resolvedEntryPoint = resolveEntryPoint(entryPoint);
218
- requireClientMethod(bundlerClient, "sendUserOperation", "bundlerClient");
219
-
220
- const payload = canonicalizeUserOpForSubmit(userOp);
221
- assertNoSecretPayload(payload);
222
-
223
- const userOpHash = await bundlerClient.sendUserOperation({
224
- userOperation: payload,
225
- entryPoint: resolvedEntryPoint,
226
- });
227
-
228
- if (typeof userOpHash !== "string" || !isHex(userOpHash)) {
229
- fail("bundlerClient.sendUserOperation must resolve to a 0x-hex userOpHash");
230
- }
231
- return userOpHash;
214
+ export async function submitUserOperation(_params = {}) {
215
+ throw new Error("bundler-client.submitUserOperation refused: @oracle-agent/oracle is prepare-only. Submit UserOps from the user wallet or local operator.");
232
216
  }
233
217
 
218
+
234
219
  /**
235
220
  * Fill gas fields on an UNSIGNED (pre-signature) UserOperation via an
236
221
  * injected bundler client's estimateUserOperationGas. Never submits