arc402-cli 0.2.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 +245 -0
- package/dist/abis.d.ts +19 -0
- package/dist/abis.d.ts.map +1 -0
- package/dist/abis.js +177 -0
- package/dist/abis.js.map +1 -0
- package/dist/bundler.d.ts +65 -0
- package/dist/bundler.d.ts.map +1 -0
- package/dist/bundler.js +181 -0
- package/dist/bundler.js.map +1 -0
- package/dist/client.d.ts +14 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +24 -0
- package/dist/client.js.map +1 -0
- package/dist/coinbase-smart-wallet.d.ts +28 -0
- package/dist/coinbase-smart-wallet.d.ts.map +1 -0
- package/dist/coinbase-smart-wallet.js +38 -0
- package/dist/coinbase-smart-wallet.js.map +1 -0
- package/dist/commands/accept.d.ts +3 -0
- package/dist/commands/accept.d.ts.map +1 -0
- package/dist/commands/accept.js +26 -0
- package/dist/commands/accept.js.map +1 -0
- package/dist/commands/agent-handshake.d.ts +3 -0
- package/dist/commands/agent-handshake.d.ts.map +1 -0
- package/dist/commands/agent-handshake.js +61 -0
- package/dist/commands/agent-handshake.js.map +1 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +417 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/agreements.d.ts +3 -0
- package/dist/commands/agreements.d.ts.map +1 -0
- package/dist/commands/agreements.js +344 -0
- package/dist/commands/agreements.js.map +1 -0
- package/dist/commands/arbitrator.d.ts +3 -0
- package/dist/commands/arbitrator.d.ts.map +1 -0
- package/dist/commands/arbitrator.js +157 -0
- package/dist/commands/arbitrator.js.map +1 -0
- package/dist/commands/arena-handshake.d.ts +3 -0
- package/dist/commands/arena-handshake.d.ts.map +1 -0
- package/dist/commands/arena-handshake.js +187 -0
- package/dist/commands/arena-handshake.js.map +1 -0
- package/dist/commands/cancel.d.ts +3 -0
- package/dist/commands/cancel.d.ts.map +1 -0
- package/dist/commands/cancel.js +30 -0
- package/dist/commands/cancel.js.map +1 -0
- package/dist/commands/channel.d.ts +3 -0
- package/dist/commands/channel.d.ts.map +1 -0
- package/dist/commands/channel.js +238 -0
- package/dist/commands/channel.js.map +1 -0
- package/dist/commands/coldstart.d.ts +3 -0
- package/dist/commands/coldstart.d.ts.map +1 -0
- package/dist/commands/coldstart.js +148 -0
- package/dist/commands/coldstart.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +40 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/contract-interaction.d.ts +3 -0
- package/dist/commands/contract-interaction.d.ts.map +1 -0
- package/dist/commands/contract-interaction.js +165 -0
- package/dist/commands/contract-interaction.js.map +1 -0
- package/dist/commands/daemon.d.ts +3 -0
- package/dist/commands/daemon.d.ts.map +1 -0
- package/dist/commands/daemon.js +891 -0
- package/dist/commands/daemon.js.map +1 -0
- package/dist/commands/deliver.d.ts +3 -0
- package/dist/commands/deliver.d.ts.map +1 -0
- package/dist/commands/deliver.js +156 -0
- package/dist/commands/deliver.js.map +1 -0
- package/dist/commands/discover.d.ts +3 -0
- package/dist/commands/discover.d.ts.map +1 -0
- package/dist/commands/discover.js +224 -0
- package/dist/commands/discover.js.map +1 -0
- package/dist/commands/dispute.d.ts +3 -0
- package/dist/commands/dispute.d.ts.map +1 -0
- package/dist/commands/dispute.js +348 -0
- package/dist/commands/dispute.js.map +1 -0
- package/dist/commands/endpoint.d.ts +3 -0
- package/dist/commands/endpoint.d.ts.map +1 -0
- package/dist/commands/endpoint.js +604 -0
- package/dist/commands/endpoint.js.map +1 -0
- package/dist/commands/hire.d.ts +3 -0
- package/dist/commands/hire.d.ts.map +1 -0
- package/dist/commands/hire.js +189 -0
- package/dist/commands/hire.js.map +1 -0
- package/dist/commands/migrate.d.ts +3 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +163 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/negotiate.d.ts +3 -0
- package/dist/commands/negotiate.d.ts.map +1 -0
- package/dist/commands/negotiate.js +247 -0
- package/dist/commands/negotiate.js.map +1 -0
- package/dist/commands/openshell.d.ts +3 -0
- package/dist/commands/openshell.d.ts.map +1 -0
- package/dist/commands/openshell.js +952 -0
- package/dist/commands/openshell.js.map +1 -0
- package/dist/commands/owner.d.ts +3 -0
- package/dist/commands/owner.d.ts.map +1 -0
- package/dist/commands/owner.js +32 -0
- package/dist/commands/owner.js.map +1 -0
- package/dist/commands/policy.d.ts +4 -0
- package/dist/commands/policy.d.ts.map +1 -0
- package/dist/commands/policy.js +248 -0
- package/dist/commands/policy.js.map +1 -0
- package/dist/commands/relay.d.ts +3 -0
- package/dist/commands/relay.d.ts.map +1 -0
- package/dist/commands/relay.js +279 -0
- package/dist/commands/relay.js.map +1 -0
- package/dist/commands/remediate.d.ts +3 -0
- package/dist/commands/remediate.d.ts.map +1 -0
- package/dist/commands/remediate.js +42 -0
- package/dist/commands/remediate.js.map +1 -0
- package/dist/commands/reputation.d.ts +4 -0
- package/dist/commands/reputation.d.ts.map +1 -0
- package/dist/commands/reputation.js +72 -0
- package/dist/commands/reputation.js.map +1 -0
- package/dist/commands/setup.d.ts +3 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +332 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/trust.d.ts +3 -0
- package/dist/commands/trust.d.ts.map +1 -0
- package/dist/commands/trust.js +23 -0
- package/dist/commands/trust.js.map +1 -0
- package/dist/commands/verify.d.ts +3 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +88 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/wallet.d.ts +3 -0
- package/dist/commands/wallet.d.ts.map +1 -0
- package/dist/commands/wallet.js +2520 -0
- package/dist/commands/wallet.js.map +1 -0
- package/dist/commands/watchtower.d.ts +3 -0
- package/dist/commands/watchtower.d.ts.map +1 -0
- package/dist/commands/watchtower.js +238 -0
- package/dist/commands/watchtower.js.map +1 -0
- package/dist/commands/workroom.d.ts +3 -0
- package/dist/commands/workroom.d.ts.map +1 -0
- package/dist/commands/workroom.js +855 -0
- package/dist/commands/workroom.js.map +1 -0
- package/dist/config.d.ts +62 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +141 -0
- package/dist/config.js.map +1 -0
- package/dist/daemon/config.d.ts +74 -0
- package/dist/daemon/config.d.ts.map +1 -0
- package/dist/daemon/config.js +271 -0
- package/dist/daemon/config.js.map +1 -0
- package/dist/daemon/hire-listener.d.ts +31 -0
- package/dist/daemon/hire-listener.d.ts.map +1 -0
- package/dist/daemon/hire-listener.js +207 -0
- package/dist/daemon/hire-listener.js.map +1 -0
- package/dist/daemon/index.d.ts +29 -0
- package/dist/daemon/index.d.ts.map +1 -0
- package/dist/daemon/index.js +535 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/daemon/job-lifecycle.d.ts +62 -0
- package/dist/daemon/job-lifecycle.d.ts.map +1 -0
- package/dist/daemon/job-lifecycle.js +201 -0
- package/dist/daemon/job-lifecycle.js.map +1 -0
- package/dist/daemon/notify.d.ts +22 -0
- package/dist/daemon/notify.d.ts.map +1 -0
- package/dist/daemon/notify.js +148 -0
- package/dist/daemon/notify.js.map +1 -0
- package/dist/daemon/token-metering.d.ts +42 -0
- package/dist/daemon/token-metering.d.ts.map +1 -0
- package/dist/daemon/token-metering.js +178 -0
- package/dist/daemon/token-metering.js.map +1 -0
- package/dist/daemon/userops.d.ts +21 -0
- package/dist/daemon/userops.d.ts.map +1 -0
- package/dist/daemon/userops.js +88 -0
- package/dist/daemon/userops.js.map +1 -0
- package/dist/daemon/wallet-monitor.d.ts +16 -0
- package/dist/daemon/wallet-monitor.d.ts.map +1 -0
- package/dist/daemon/wallet-monitor.js +57 -0
- package/dist/daemon/wallet-monitor.js.map +1 -0
- package/dist/drain-v4.d.ts +2 -0
- package/dist/drain-v4.d.ts.map +1 -0
- package/dist/drain-v4.js +167 -0
- package/dist/drain-v4.js.map +1 -0
- package/dist/endpoint-config.d.ts +36 -0
- package/dist/endpoint-config.d.ts.map +1 -0
- package/dist/endpoint-config.js +96 -0
- package/dist/endpoint-config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +79 -0
- package/dist/index.js.map +1 -0
- package/dist/openshell-runtime.d.ts +55 -0
- package/dist/openshell-runtime.d.ts.map +1 -0
- package/dist/openshell-runtime.js +268 -0
- package/dist/openshell-runtime.js.map +1 -0
- package/dist/signing.d.ts +2 -0
- package/dist/signing.d.ts.map +1 -0
- package/dist/signing.js +23 -0
- package/dist/signing.js.map +1 -0
- package/dist/telegram-notify.d.ts +23 -0
- package/dist/telegram-notify.d.ts.map +1 -0
- package/dist/telegram-notify.js +106 -0
- package/dist/telegram-notify.js.map +1 -0
- package/dist/ui/banner.d.ts +7 -0
- package/dist/ui/banner.d.ts.map +1 -0
- package/dist/ui/banner.js +37 -0
- package/dist/ui/banner.js.map +1 -0
- package/dist/ui/colors.d.ts +14 -0
- package/dist/ui/colors.d.ts.map +1 -0
- package/dist/ui/colors.js +29 -0
- package/dist/ui/colors.js.map +1 -0
- package/dist/ui/format.d.ts +26 -0
- package/dist/ui/format.d.ts.map +1 -0
- package/dist/ui/format.js +77 -0
- package/dist/ui/format.js.map +1 -0
- package/dist/ui/spinner.d.ts +8 -0
- package/dist/ui/spinner.d.ts.map +1 -0
- package/dist/ui/spinner.js +43 -0
- package/dist/ui/spinner.js.map +1 -0
- package/dist/utils/format.d.ts +10 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +61 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/hash.d.ts +3 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +43 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/time.d.ts +3 -0
- package/dist/utils/time.d.ts.map +1 -0
- package/dist/utils/time.js +21 -0
- package/dist/utils/time.js.map +1 -0
- package/dist/wallet-router.d.ts +25 -0
- package/dist/wallet-router.d.ts.map +1 -0
- package/dist/wallet-router.js +153 -0
- package/dist/wallet-router.js.map +1 -0
- package/dist/walletconnect-session.d.ts +12 -0
- package/dist/walletconnect-session.d.ts.map +1 -0
- package/dist/walletconnect-session.js +26 -0
- package/dist/walletconnect-session.js.map +1 -0
- package/dist/walletconnect.d.ts +46 -0
- package/dist/walletconnect.d.ts.map +1 -0
- package/dist/walletconnect.js +267 -0
- package/dist/walletconnect.js.map +1 -0
- package/package.json +38 -0
- package/scripts/authorize-machine-key.ts +43 -0
- package/scripts/drain-wallet.ts +149 -0
- package/scripts/execute-spend-only.ts +81 -0
- package/scripts/register-agent-userop.ts +186 -0
- package/src/abis.ts +187 -0
- package/src/bundler.ts +235 -0
- package/src/client.ts +34 -0
- package/src/coinbase-smart-wallet.ts +51 -0
- package/src/commands/accept.ts +25 -0
- package/src/commands/agent-handshake.ts +67 -0
- package/src/commands/agent.ts +458 -0
- package/src/commands/agreements.ts +324 -0
- package/src/commands/arbitrator.ts +129 -0
- package/src/commands/arena-handshake.ts +217 -0
- package/src/commands/cancel.ts +26 -0
- package/src/commands/channel.ts +208 -0
- package/src/commands/coldstart.ts +156 -0
- package/src/commands/config.ts +35 -0
- package/src/commands/contract-interaction.ts +166 -0
- package/src/commands/daemon.ts +971 -0
- package/src/commands/deliver.ts +116 -0
- package/src/commands/discover.ts +295 -0
- package/src/commands/dispute.ts +373 -0
- package/src/commands/endpoint.ts +619 -0
- package/src/commands/hire.ts +200 -0
- package/src/commands/migrate.ts +175 -0
- package/src/commands/negotiate.ts +270 -0
- package/src/commands/openshell.ts +1053 -0
- package/src/commands/owner.ts +30 -0
- package/src/commands/policy.ts +252 -0
- package/src/commands/relay.ts +272 -0
- package/src/commands/remediate.ts +22 -0
- package/src/commands/reputation.ts +71 -0
- package/src/commands/setup.ts +343 -0
- package/src/commands/trust.ts +15 -0
- package/src/commands/verify.ts +88 -0
- package/src/commands/wallet.ts +2892 -0
- package/src/commands/watchtower.ts +232 -0
- package/src/commands/workroom.ts +889 -0
- package/src/config.ts +153 -0
- package/src/daemon/config.ts +308 -0
- package/src/daemon/hire-listener.ts +226 -0
- package/src/daemon/index.ts +609 -0
- package/src/daemon/job-lifecycle.ts +215 -0
- package/src/daemon/notify.ts +157 -0
- package/src/daemon/token-metering.ts +183 -0
- package/src/daemon/userops.ts +119 -0
- package/src/daemon/wallet-monitor.ts +90 -0
- package/src/drain-v4.ts +159 -0
- package/src/endpoint-config.ts +83 -0
- package/src/index.ts +75 -0
- package/src/openshell-runtime.ts +277 -0
- package/src/signing.ts +28 -0
- package/src/telegram-notify.ts +88 -0
- package/src/ui/banner.ts +41 -0
- package/src/ui/colors.ts +30 -0
- package/src/ui/format.ts +77 -0
- package/src/ui/spinner.ts +46 -0
- package/src/utils/format.ts +48 -0
- package/src/utils/hash.ts +5 -0
- package/src/utils/time.ts +15 -0
- package/src/wallet-router.ts +178 -0
- package/src/walletconnect-session.ts +27 -0
- package/src/walletconnect.ts +294 -0
- package/test/time.test.js +11 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARC-402 Job Lifecycle — Post-delivery processing.
|
|
3
|
+
*
|
|
4
|
+
* Handles everything that happens after a worker completes a job:
|
|
5
|
+
* 1. Execution receipt generation
|
|
6
|
+
* 2. Learning extraction from completed work
|
|
7
|
+
* 3. Worker memory update
|
|
8
|
+
* 4. Per-agreement job directory management
|
|
9
|
+
*
|
|
10
|
+
* These functions are called by the daemon after a successful delivery.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import * as os from "os";
|
|
15
|
+
import * as crypto from "crypto";
|
|
16
|
+
import { readUsageReport, type AggregatedTokenUsage } from "./token-metering";
|
|
17
|
+
|
|
18
|
+
const ARC402_DIR = path.join(os.homedir(), ".arc402");
|
|
19
|
+
const RECEIPTS_DIR = path.join(ARC402_DIR, "receipts");
|
|
20
|
+
const WORKER_DIR = path.join(ARC402_DIR, "worker");
|
|
21
|
+
const WORKER_MEMORY_DIR = path.join(WORKER_DIR, "memory");
|
|
22
|
+
const WORKER_CONFIG = path.join(WORKER_DIR, "config.json");
|
|
23
|
+
const JOBS_DIR = path.join(ARC402_DIR, "jobs");
|
|
24
|
+
|
|
25
|
+
// ─── Execution Receipt ────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
export interface ExecutionReceipt {
|
|
28
|
+
schema: "arc402.execution-receipt.v1";
|
|
29
|
+
agreement_id: string;
|
|
30
|
+
workroom_policy_hash: string;
|
|
31
|
+
started_at: string;
|
|
32
|
+
completed_at: string;
|
|
33
|
+
deliverable_hash: string;
|
|
34
|
+
worker_address: string;
|
|
35
|
+
metrics: {
|
|
36
|
+
wall_clock_seconds: number;
|
|
37
|
+
network_hosts_contacted: string[];
|
|
38
|
+
policy_violations_attempted: number;
|
|
39
|
+
};
|
|
40
|
+
token_usage: AggregatedTokenUsage | null;
|
|
41
|
+
receipt_hash: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Generate and persist an execution receipt after job delivery.
|
|
46
|
+
*/
|
|
47
|
+
export function generateReceipt(params: {
|
|
48
|
+
agreementId: string;
|
|
49
|
+
deliverableHash: string;
|
|
50
|
+
walletAddress: string;
|
|
51
|
+
startedAt: string;
|
|
52
|
+
completedAt: string;
|
|
53
|
+
policyFilePath?: string;
|
|
54
|
+
networkHosts?: string[];
|
|
55
|
+
}): ExecutionReceipt {
|
|
56
|
+
fs.mkdirSync(RECEIPTS_DIR, { recursive: true });
|
|
57
|
+
|
|
58
|
+
// Compute policy hash
|
|
59
|
+
let policyHash = "0x0";
|
|
60
|
+
const policyPath = params.policyFilePath ?? path.join(ARC402_DIR, "openshell-policy.yaml");
|
|
61
|
+
if (fs.existsSync(policyPath)) {
|
|
62
|
+
const content = fs.readFileSync(policyPath, "utf-8");
|
|
63
|
+
policyHash = "0x" + crypto.createHash("sha256").update(content).digest("hex");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Calculate wall clock time
|
|
67
|
+
const started = new Date(params.startedAt).getTime();
|
|
68
|
+
const completed = new Date(params.completedAt).getTime();
|
|
69
|
+
const wallClockSeconds = Math.max(0, Math.floor((completed - started) / 1000));
|
|
70
|
+
|
|
71
|
+
// Read token usage report (if the worker wrote one)
|
|
72
|
+
const tokenUsage = readUsageReport(params.agreementId);
|
|
73
|
+
|
|
74
|
+
const receipt: ExecutionReceipt = {
|
|
75
|
+
schema: "arc402.execution-receipt.v1",
|
|
76
|
+
agreement_id: params.agreementId,
|
|
77
|
+
workroom_policy_hash: policyHash,
|
|
78
|
+
started_at: params.startedAt,
|
|
79
|
+
completed_at: params.completedAt,
|
|
80
|
+
deliverable_hash: params.deliverableHash,
|
|
81
|
+
worker_address: params.walletAddress,
|
|
82
|
+
metrics: {
|
|
83
|
+
wall_clock_seconds: wallClockSeconds,
|
|
84
|
+
network_hosts_contacted: params.networkHosts ?? [],
|
|
85
|
+
policy_violations_attempted: 0,
|
|
86
|
+
},
|
|
87
|
+
token_usage: tokenUsage,
|
|
88
|
+
receipt_hash: "", // filled below
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Receipt hash = SHA-256 of the receipt content (without the hash field)
|
|
92
|
+
const hashInput = JSON.stringify({ ...receipt, receipt_hash: undefined });
|
|
93
|
+
receipt.receipt_hash = "0x" + crypto.createHash("sha256").update(hashInput).digest("hex");
|
|
94
|
+
|
|
95
|
+
// Persist
|
|
96
|
+
const receiptPath = path.join(RECEIPTS_DIR, `${params.agreementId}.json`);
|
|
97
|
+
fs.writeFileSync(receiptPath, JSON.stringify(receipt, null, 2));
|
|
98
|
+
|
|
99
|
+
return receipt;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── Learning Extraction ──────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Extract learnings from a completed job and update worker memory.
|
|
106
|
+
*
|
|
107
|
+
* This creates:
|
|
108
|
+
* 1. A per-job memory file: memory/job-<id>.md
|
|
109
|
+
* 2. Updates the cumulative learnings.md
|
|
110
|
+
* 3. Updates the worker config (job count, earnings)
|
|
111
|
+
*/
|
|
112
|
+
export function extractLearnings(params: {
|
|
113
|
+
agreementId: string;
|
|
114
|
+
taskDescription: string;
|
|
115
|
+
deliverableHash: string;
|
|
116
|
+
priceEth: string;
|
|
117
|
+
capability: string;
|
|
118
|
+
wallClockSeconds: number;
|
|
119
|
+
success: boolean;
|
|
120
|
+
}): void {
|
|
121
|
+
fs.mkdirSync(WORKER_MEMORY_DIR, { recursive: true });
|
|
122
|
+
|
|
123
|
+
const now = new Date().toISOString();
|
|
124
|
+
const dateStr = now.split("T")[0];
|
|
125
|
+
|
|
126
|
+
// 1. Per-job memory file
|
|
127
|
+
const jobMemory = `# Job: ${params.agreementId}
|
|
128
|
+
*Completed: ${now}*
|
|
129
|
+
*Capability: ${params.capability}*
|
|
130
|
+
*Price: ${params.priceEth} ETH*
|
|
131
|
+
*Duration: ${params.wallClockSeconds}s*
|
|
132
|
+
*Outcome: ${params.success ? "delivered" : "failed"}*
|
|
133
|
+
|
|
134
|
+
## Task
|
|
135
|
+
${params.taskDescription}
|
|
136
|
+
|
|
137
|
+
## Deliverable
|
|
138
|
+
Hash: ${params.deliverableHash}
|
|
139
|
+
|
|
140
|
+
## Learnings
|
|
141
|
+
- Completed ${params.capability} task in ${params.wallClockSeconds}s
|
|
142
|
+
- ${params.success ? "Delivered successfully" : "Delivery failed — review required"}
|
|
143
|
+
`;
|
|
144
|
+
|
|
145
|
+
const jobFile = path.join(WORKER_MEMORY_DIR, `job-${params.agreementId}.md`);
|
|
146
|
+
fs.writeFileSync(jobFile, jobMemory);
|
|
147
|
+
|
|
148
|
+
// 2. Update cumulative learnings.md
|
|
149
|
+
const learningsPath = path.join(WORKER_MEMORY_DIR, "learnings.md");
|
|
150
|
+
let learnings = "";
|
|
151
|
+
if (fs.existsSync(learningsPath)) {
|
|
152
|
+
learnings = fs.readFileSync(learningsPath, "utf-8");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const newEntry = `\n### ${dateStr} — ${params.capability} (${params.agreementId.slice(0, 8)}...)
|
|
156
|
+
- Duration: ${params.wallClockSeconds}s | Price: ${params.priceEth} ETH | Outcome: ${params.success ? "✓" : "✗"}
|
|
157
|
+
`;
|
|
158
|
+
|
|
159
|
+
// Append to learnings
|
|
160
|
+
if (learnings.includes("No learnings yet")) {
|
|
161
|
+
learnings = `# Accumulated Learnings\n\n*Auto-updated after each completed job.*\n${newEntry}`;
|
|
162
|
+
} else {
|
|
163
|
+
learnings += newEntry;
|
|
164
|
+
}
|
|
165
|
+
fs.writeFileSync(learningsPath, learnings);
|
|
166
|
+
|
|
167
|
+
// 3. Update worker config
|
|
168
|
+
if (fs.existsSync(WORKER_CONFIG)) {
|
|
169
|
+
try {
|
|
170
|
+
const config = JSON.parse(fs.readFileSync(WORKER_CONFIG, "utf-8"));
|
|
171
|
+
config.job_count = (config.job_count || 0) + 1;
|
|
172
|
+
const currentEarnings = parseFloat(config.total_earned_eth || "0");
|
|
173
|
+
const jobEarnings = parseFloat(params.priceEth || "0");
|
|
174
|
+
config.total_earned_eth = (currentEarnings + jobEarnings).toFixed(6);
|
|
175
|
+
config.last_job_at = now;
|
|
176
|
+
fs.writeFileSync(WORKER_CONFIG, JSON.stringify(config, null, 2));
|
|
177
|
+
} catch { /* non-fatal — config may be malformed */ }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ─── Per-Agreement Job Directory ──────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Create an isolated job directory for a specific agreement.
|
|
185
|
+
* Returns the path to the job workspace.
|
|
186
|
+
*/
|
|
187
|
+
export function createJobDirectory(agreementId: string): string {
|
|
188
|
+
const jobDir = path.join(JOBS_DIR, `agreement-${agreementId}`);
|
|
189
|
+
fs.mkdirSync(jobDir, { recursive: true });
|
|
190
|
+
return jobDir;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Clean up a job directory after settlement.
|
|
195
|
+
* Preserves the receipt and job memory — only removes working files.
|
|
196
|
+
*/
|
|
197
|
+
export function cleanJobDirectory(agreementId: string): void {
|
|
198
|
+
const jobDir = path.join(JOBS_DIR, `agreement-${agreementId}`);
|
|
199
|
+
if (fs.existsSync(jobDir)) {
|
|
200
|
+
fs.rmSync(jobDir, { recursive: true, force: true });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ─── Policy Hash ──────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Compute the SHA-256 hash of the current workroom policy.
|
|
208
|
+
* This hash can be registered in AgentRegistry for verifiability.
|
|
209
|
+
*/
|
|
210
|
+
export function computePolicyHash(policyFilePath?: string): string {
|
|
211
|
+
const policyPath = policyFilePath ?? path.join(ARC402_DIR, "openshell-policy.yaml");
|
|
212
|
+
if (!fs.existsSync(policyPath)) return "0x0";
|
|
213
|
+
const content = fs.readFileSync(policyPath, "utf-8");
|
|
214
|
+
return "0x" + crypto.createHash("sha256").update(content).digest("hex");
|
|
215
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram notification module for daemon events.
|
|
3
|
+
* Uses Telegram Bot API — no external dependencies.
|
|
4
|
+
*/
|
|
5
|
+
import * as https from "https";
|
|
6
|
+
import * as http from "http";
|
|
7
|
+
|
|
8
|
+
export type NotifyEvent =
|
|
9
|
+
| "hire_request"
|
|
10
|
+
| "hire_accepted"
|
|
11
|
+
| "hire_rejected"
|
|
12
|
+
| "delivery"
|
|
13
|
+
| "dispute"
|
|
14
|
+
| "channel_challenge"
|
|
15
|
+
| "low_balance"
|
|
16
|
+
| "daemon_started"
|
|
17
|
+
| "daemon_stopped";
|
|
18
|
+
|
|
19
|
+
export interface TelegramConfig {
|
|
20
|
+
botToken: string;
|
|
21
|
+
chatId: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function telegramPost(botToken: string, method: string, body: object): Promise<void> {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const payload = JSON.stringify(body);
|
|
27
|
+
const options: https.RequestOptions = {
|
|
28
|
+
hostname: "api.telegram.org",
|
|
29
|
+
port: 443,
|
|
30
|
+
path: `/bot${botToken}/${method}`,
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: {
|
|
33
|
+
"Content-Type": "application/json",
|
|
34
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
const req = https.request(options, (res) => {
|
|
38
|
+
let data = "";
|
|
39
|
+
res.on("data", (c: Buffer) => { data += c.toString(); });
|
|
40
|
+
res.on("end", () => {
|
|
41
|
+
const parsed = JSON.parse(data) as { ok: boolean; description?: string };
|
|
42
|
+
if (!parsed.ok) {
|
|
43
|
+
reject(new Error(`Telegram API error: ${parsed.description}`));
|
|
44
|
+
} else {
|
|
45
|
+
resolve();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
req.on("error", reject);
|
|
50
|
+
req.write(payload);
|
|
51
|
+
req.end();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class Notifier {
|
|
56
|
+
private config: TelegramConfig | null;
|
|
57
|
+
private notifyFlags: Record<NotifyEvent, boolean>;
|
|
58
|
+
|
|
59
|
+
constructor(
|
|
60
|
+
botToken: string,
|
|
61
|
+
chatId: string,
|
|
62
|
+
flags: Partial<Record<NotifyEvent, boolean>> = {}
|
|
63
|
+
) {
|
|
64
|
+
this.config = botToken && chatId ? { botToken, chatId } : null;
|
|
65
|
+
this.notifyFlags = {
|
|
66
|
+
hire_request: flags.hire_request ?? true,
|
|
67
|
+
hire_accepted: flags.hire_accepted ?? true,
|
|
68
|
+
hire_rejected: flags.hire_rejected ?? true,
|
|
69
|
+
delivery: flags.delivery ?? true,
|
|
70
|
+
dispute: flags.dispute ?? true,
|
|
71
|
+
channel_challenge: flags.channel_challenge ?? true,
|
|
72
|
+
low_balance: flags.low_balance ?? true,
|
|
73
|
+
daemon_started: true,
|
|
74
|
+
daemon_stopped: true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
isEnabled(): boolean {
|
|
79
|
+
return this.config !== null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async send(event: NotifyEvent, message: string): Promise<void> {
|
|
83
|
+
if (!this.config) return;
|
|
84
|
+
if (!this.notifyFlags[event]) return;
|
|
85
|
+
try {
|
|
86
|
+
await telegramPost(this.config.botToken, "sendMessage", {
|
|
87
|
+
chat_id: this.config.chatId,
|
|
88
|
+
text: message,
|
|
89
|
+
parse_mode: "HTML",
|
|
90
|
+
});
|
|
91
|
+
} catch (err) {
|
|
92
|
+
// Non-fatal — log and continue
|
|
93
|
+
process.stderr.write(`[notify] Telegram send failed: ${err}\n`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async notifyHireRequest(hireId: string, hirerAddress: string, priceEth: string, capability: string): Promise<void> {
|
|
98
|
+
const short = hirerAddress.slice(0, 10);
|
|
99
|
+
const msg = [
|
|
100
|
+
`⚡ <b>Hire Request</b>`,
|
|
101
|
+
`ID: <code>${hireId}</code>`,
|
|
102
|
+
`From: <code>${short}...</code>`,
|
|
103
|
+
`Capability: ${capability || "unspecified"}`,
|
|
104
|
+
`Price: ${priceEth} ETH`,
|
|
105
|
+
``,
|
|
106
|
+
`Approve: <code>arc402 daemon approve ${hireId}</code>`,
|
|
107
|
+
`Reject: <code>arc402 daemon reject ${hireId}</code>`,
|
|
108
|
+
].join("\n");
|
|
109
|
+
await this.send("hire_request", msg);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async notifyHireAccepted(hireId: string, agreementId: string): Promise<void> {
|
|
113
|
+
await this.send("hire_accepted",
|
|
114
|
+
`✅ <b>Hire Accepted</b>\nID: <code>${hireId}</code>\nAgreement: <code>${agreementId}</code>`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async notifyHireRejected(hireId: string, reason: string): Promise<void> {
|
|
119
|
+
await this.send("hire_rejected",
|
|
120
|
+
`❌ <b>Hire Rejected</b>\nID: <code>${hireId}</code>\nReason: ${reason}`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async notifyDelivery(agreementId: string, deliveryHash: string, userOpHash: string): Promise<void> {
|
|
125
|
+
await this.send("delivery",
|
|
126
|
+
`📦 <b>Delivery Submitted</b>\nAgreement: <code>${agreementId}</code>\nDelivery hash: <code>${deliveryHash.slice(0, 16)}...</code>\nUserOp: <code>${userOpHash.slice(0, 16)}...</code>`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async notifyDispute(agreementId: string, raisedBy: string): Promise<void> {
|
|
131
|
+
await this.send("dispute",
|
|
132
|
+
`⚠️ <b>Dispute Raised</b>\nAgreement: <code>${agreementId}</code>\nBy: <code>${raisedBy}</code>`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async notifyChannelChallenge(channelId: string, txHash: string): Promise<void> {
|
|
137
|
+
await this.send("channel_challenge",
|
|
138
|
+
`🔔 <b>Channel Challenged</b>\nChannel: <code>${channelId.slice(0, 16)}...</code>\nTx: <code>${txHash.slice(0, 16)}...</code>`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async notifyLowBalance(balanceEth: string, thresholdEth: string): Promise<void> {
|
|
143
|
+
await this.send("low_balance",
|
|
144
|
+
`💸 <b>Low Balance Alert</b>\nCurrent: ${balanceEth} ETH\nThreshold: ${thresholdEth} ETH`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async notifyStarted(walletAddress: string, subsystems: string[]): Promise<void> {
|
|
149
|
+
await this.send("daemon_started",
|
|
150
|
+
`🟢 <b>ARC-402 Daemon Started</b>\nWallet: <code>${walletAddress}</code>\nSubsystems: ${subsystems.join(", ")}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async notifyStopped(): Promise<void> {
|
|
155
|
+
await this.send("daemon_stopped", `🔴 <b>ARC-402 Daemon Stopped</b>`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARC-402 Token Usage Metering
|
|
3
|
+
*
|
|
4
|
+
* Tracks LLM token consumption per agreement. The worker writes a usage report
|
|
5
|
+
* file during execution; the daemon reads it after delivery and includes the
|
|
6
|
+
* data in the execution receipt.
|
|
7
|
+
*
|
|
8
|
+
* Architecture:
|
|
9
|
+
* - Before execution: daemon creates a usage report path for the agreement
|
|
10
|
+
* - During execution: worker appends usage entries as it calls LLM APIs
|
|
11
|
+
* - After execution: daemon reads the report, aggregates totals, includes in receipt
|
|
12
|
+
*
|
|
13
|
+
* The worker writes entries in JSON Lines format to:
|
|
14
|
+
* /workroom/jobs/agreement-<id>/token-usage.jsonl
|
|
15
|
+
*
|
|
16
|
+
* Each line:
|
|
17
|
+
* {"model":"claude-sonnet-4-6","provider":"anthropic","input":1200,"output":450,"ts":"..."}
|
|
18
|
+
*
|
|
19
|
+
* This is non-invasive — no proxy, no network interception. The worker
|
|
20
|
+
* is responsible for reporting its own usage. OpenClaw agents can do this
|
|
21
|
+
* natively via session metadata.
|
|
22
|
+
*/
|
|
23
|
+
import * as fs from "fs";
|
|
24
|
+
import * as path from "path";
|
|
25
|
+
import * as os from "os";
|
|
26
|
+
|
|
27
|
+
const ARC402_DIR = path.join(os.homedir(), ".arc402");
|
|
28
|
+
const JOBS_DIR = path.join(ARC402_DIR, "jobs");
|
|
29
|
+
|
|
30
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
export interface TokenUsageEntry {
|
|
33
|
+
model: string;
|
|
34
|
+
provider: string;
|
|
35
|
+
input: number;
|
|
36
|
+
output: number;
|
|
37
|
+
ts: string;
|
|
38
|
+
cost_usd?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AggregatedTokenUsage {
|
|
42
|
+
total_input: number;
|
|
43
|
+
total_output: number;
|
|
44
|
+
total_tokens: number;
|
|
45
|
+
estimated_cost_usd: number;
|
|
46
|
+
models_used: string[];
|
|
47
|
+
entries: number;
|
|
48
|
+
by_model: Record<string, {
|
|
49
|
+
input: number;
|
|
50
|
+
output: number;
|
|
51
|
+
calls: number;
|
|
52
|
+
cost_usd: number;
|
|
53
|
+
}>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Known model pricing (USD per 1M tokens, as of 2026-03) ──────────────────
|
|
57
|
+
|
|
58
|
+
const MODEL_PRICING: Record<string, { input: number; output: number }> = {
|
|
59
|
+
// Anthropic
|
|
60
|
+
"claude-opus-4-6": { input: 15.0, output: 75.0 },
|
|
61
|
+
"claude-sonnet-4-6": { input: 3.0, output: 15.0 },
|
|
62
|
+
"claude-haiku-4-5": { input: 0.8, output: 4.0 },
|
|
63
|
+
// OpenAI
|
|
64
|
+
"gpt-5.4": { input: 2.5, output: 10.0 },
|
|
65
|
+
"gpt-5.3-codex": { input: 2.5, output: 10.0 },
|
|
66
|
+
"gpt-4o": { input: 2.5, output: 10.0 },
|
|
67
|
+
"gpt-4o-mini": { input: 0.15, output: 0.6 },
|
|
68
|
+
// Google
|
|
69
|
+
"gemini-2.5-pro": { input: 1.25, output: 10.0 },
|
|
70
|
+
"gemini-2.5-flash": { input: 0.15, output: 0.6 },
|
|
71
|
+
// Defaults
|
|
72
|
+
"default": { input: 2.0, output: 8.0 },
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function estimateCost(model: string, inputTokens: number, outputTokens: number): number {
|
|
76
|
+
const pricing = MODEL_PRICING[model] ?? MODEL_PRICING["default"];
|
|
77
|
+
return (inputTokens * pricing.input + outputTokens * pricing.output) / 1_000_000;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── Usage report path ────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get the path where the worker should write token usage for an agreement.
|
|
84
|
+
* The daemon sets this as an env var before calling exec_command.
|
|
85
|
+
*/
|
|
86
|
+
export function getUsageReportPath(agreementId: string): string {
|
|
87
|
+
return path.join(JOBS_DIR, `agreement-${agreementId}`, "token-usage.jsonl");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create the parent directory and return the path.
|
|
92
|
+
* Called by the daemon before spawning the worker.
|
|
93
|
+
*/
|
|
94
|
+
export function prepareUsageReport(agreementId: string): string {
|
|
95
|
+
const reportPath = getUsageReportPath(agreementId);
|
|
96
|
+
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
|
97
|
+
return reportPath;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── Read and aggregate ───────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Read the token usage report for an agreement and aggregate totals.
|
|
104
|
+
* Returns null if no report exists.
|
|
105
|
+
*/
|
|
106
|
+
export function readUsageReport(agreementId: string): AggregatedTokenUsage | null {
|
|
107
|
+
const reportPath = getUsageReportPath(agreementId);
|
|
108
|
+
|
|
109
|
+
if (!fs.existsSync(reportPath)) return null;
|
|
110
|
+
|
|
111
|
+
const content = fs.readFileSync(reportPath, "utf-8").trim();
|
|
112
|
+
if (!content) return null;
|
|
113
|
+
|
|
114
|
+
const lines = content.split("\n").filter(Boolean);
|
|
115
|
+
const entries: TokenUsageEntry[] = [];
|
|
116
|
+
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
try {
|
|
119
|
+
entries.push(JSON.parse(line) as TokenUsageEntry);
|
|
120
|
+
} catch {
|
|
121
|
+
// Skip malformed lines
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (entries.length === 0) return null;
|
|
126
|
+
|
|
127
|
+
// Aggregate
|
|
128
|
+
const byModel: Record<string, { input: number; output: number; calls: number; cost_usd: number }> = {};
|
|
129
|
+
let totalInput = 0;
|
|
130
|
+
let totalOutput = 0;
|
|
131
|
+
let totalCost = 0;
|
|
132
|
+
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
totalInput += entry.input;
|
|
135
|
+
totalOutput += entry.output;
|
|
136
|
+
|
|
137
|
+
const model = entry.model || "unknown";
|
|
138
|
+
if (!byModel[model]) {
|
|
139
|
+
byModel[model] = { input: 0, output: 0, calls: 0, cost_usd: 0 };
|
|
140
|
+
}
|
|
141
|
+
byModel[model].input += entry.input;
|
|
142
|
+
byModel[model].output += entry.output;
|
|
143
|
+
byModel[model].calls += 1;
|
|
144
|
+
|
|
145
|
+
const cost = entry.cost_usd ?? estimateCost(model, entry.input, entry.output);
|
|
146
|
+
byModel[model].cost_usd += cost;
|
|
147
|
+
totalCost += cost;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
total_input: totalInput,
|
|
152
|
+
total_output: totalOutput,
|
|
153
|
+
total_tokens: totalInput + totalOutput,
|
|
154
|
+
estimated_cost_usd: Math.round(totalCost * 10000) / 10000, // 4 decimal places
|
|
155
|
+
models_used: Object.keys(byModel),
|
|
156
|
+
entries: entries.length,
|
|
157
|
+
by_model: byModel,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ─── CLI display helper ───────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Format token usage for CLI display.
|
|
165
|
+
*/
|
|
166
|
+
export function formatUsageReport(usage: AggregatedTokenUsage): string {
|
|
167
|
+
const lines: string[] = [];
|
|
168
|
+
lines.push(`Token Usage`);
|
|
169
|
+
lines.push(`───────────`);
|
|
170
|
+
lines.push(`Total tokens: ${usage.total_tokens.toLocaleString()} (${usage.total_input.toLocaleString()} in / ${usage.total_output.toLocaleString()} out)`);
|
|
171
|
+
lines.push(`Est. cost: $${usage.estimated_cost_usd.toFixed(4)}`);
|
|
172
|
+
lines.push(`LLM calls: ${usage.entries}`);
|
|
173
|
+
lines.push(``);
|
|
174
|
+
|
|
175
|
+
if (Object.keys(usage.by_model).length > 1) {
|
|
176
|
+
lines.push(`By model:`);
|
|
177
|
+
for (const [model, data] of Object.entries(usage.by_model)) {
|
|
178
|
+
lines.push(` ${model.padEnd(24)} ${data.calls} calls ${(data.input + data.output).toLocaleString()} tokens $${data.cost_usd.toFixed(4)}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return lines.join("\n");
|
|
183
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UserOperation builder and submitter for the ARC-402 daemon.
|
|
3
|
+
* Wraps protocol calls (accept, fulfill) into ERC-4337 UserOperations.
|
|
4
|
+
*/
|
|
5
|
+
import { ethers } from "ethers";
|
|
6
|
+
import { BundlerClient } from "../bundler";
|
|
7
|
+
import type { UserOperation } from "../bundler";
|
|
8
|
+
import type { DaemonConfig } from "./config";
|
|
9
|
+
import { ARC402_WALLET_EXECUTE_ABI } from "../abis";
|
|
10
|
+
|
|
11
|
+
// ServiceAgreement calldata encoders
|
|
12
|
+
const SA_IFACE = new ethers.Interface([
|
|
13
|
+
"function accept(uint256 agreementId) external",
|
|
14
|
+
"function fulfill(uint256 agreementId, bytes32 actualDeliverablesHash) external",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
// ARC402Wallet.executeContractCall param type
|
|
18
|
+
const WALLET_EXEC_IFACE = new ethers.Interface(ARC402_WALLET_EXECUTE_ABI as unknown as string[]);
|
|
19
|
+
|
|
20
|
+
export function encodeWalletCall(
|
|
21
|
+
target: string,
|
|
22
|
+
innerCalldata: string,
|
|
23
|
+
value = BigInt(0)
|
|
24
|
+
): string {
|
|
25
|
+
return WALLET_EXEC_IFACE.encodeFunctionData("executeContractCall", [{
|
|
26
|
+
target,
|
|
27
|
+
data: innerCalldata,
|
|
28
|
+
value,
|
|
29
|
+
minReturnValue: BigInt(0),
|
|
30
|
+
maxApprovalAmount: BigInt(0),
|
|
31
|
+
approvalToken: ethers.ZeroAddress,
|
|
32
|
+
}]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildAcceptCalldata(
|
|
36
|
+
serviceAgreementAddress: string,
|
|
37
|
+
agreementId: string,
|
|
38
|
+
walletAddress: string
|
|
39
|
+
): string {
|
|
40
|
+
const inner = SA_IFACE.encodeFunctionData("accept", [BigInt(agreementId)]);
|
|
41
|
+
return encodeWalletCall(serviceAgreementAddress, inner);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function buildFulfillCalldata(
|
|
45
|
+
serviceAgreementAddress: string,
|
|
46
|
+
agreementId: string,
|
|
47
|
+
deliveryHash: string,
|
|
48
|
+
walletAddress: string
|
|
49
|
+
): string {
|
|
50
|
+
const inner = SA_IFACE.encodeFunctionData("fulfill", [BigInt(agreementId), deliveryHash]);
|
|
51
|
+
return encodeWalletCall(serviceAgreementAddress, inner);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class UserOpsManager {
|
|
55
|
+
private bundlerClient: BundlerClient;
|
|
56
|
+
private provider: ethers.Provider;
|
|
57
|
+
private config: DaemonConfig;
|
|
58
|
+
|
|
59
|
+
constructor(config: DaemonConfig, provider: ethers.Provider) {
|
|
60
|
+
this.config = config;
|
|
61
|
+
this.provider = provider;
|
|
62
|
+
|
|
63
|
+
const bundlerUrl =
|
|
64
|
+
config.bundler.endpoint || "https://api.pimlico.io/v2/base/rpc";
|
|
65
|
+
this.bundlerClient = new BundlerClient(
|
|
66
|
+
bundlerUrl,
|
|
67
|
+
config.network.entry_point,
|
|
68
|
+
config.network.chain_id
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async buildUserOp(callData: string, senderWallet: string): Promise<UserOperation> {
|
|
73
|
+
const entryPointContract = new ethers.Contract(
|
|
74
|
+
this.config.network.entry_point,
|
|
75
|
+
["function getNonce(address sender, uint192 key) external view returns (uint256)"],
|
|
76
|
+
this.provider
|
|
77
|
+
);
|
|
78
|
+
const nonce: bigint = await entryPointContract.getNonce(senderWallet, 0);
|
|
79
|
+
const feeData = await this.provider.getFeeData();
|
|
80
|
+
const maxFeePerGas = feeData.maxFeePerGas ?? BigInt(1_000_000_000);
|
|
81
|
+
const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas ?? BigInt(100_000_000);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
sender: senderWallet,
|
|
85
|
+
nonce: ethers.toBeHex(nonce),
|
|
86
|
+
callData,
|
|
87
|
+
callGasLimit: ethers.toBeHex(300_000),
|
|
88
|
+
verificationGasLimit: ethers.toBeHex(150_000),
|
|
89
|
+
preVerificationGas: ethers.toBeHex(50_000),
|
|
90
|
+
maxFeePerGas: ethers.toBeHex(maxFeePerGas),
|
|
91
|
+
maxPriorityFeePerGas: ethers.toBeHex(maxPriorityFeePerGas),
|
|
92
|
+
signature: "0x",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async submit(callData: string, senderWallet: string): Promise<string> {
|
|
97
|
+
const userOp = await this.buildUserOp(callData, senderWallet);
|
|
98
|
+
return this.bundlerClient.sendUserOperation(userOp);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async waitForInclusion(userOpHash: string): Promise<void> {
|
|
102
|
+
await this.bundlerClient.getUserOperationReceipt(userOpHash);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async pingBundler(): Promise<boolean> {
|
|
106
|
+
try {
|
|
107
|
+
const bundlerUrl = this.config.bundler.endpoint || "https://api.pimlico.io/v2/base/rpc";
|
|
108
|
+
const response = await fetch(bundlerUrl, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: { "Content-Type": "application/json" },
|
|
111
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_chainId", params: [] }),
|
|
112
|
+
signal: AbortSignal.timeout(5000),
|
|
113
|
+
});
|
|
114
|
+
return response.ok;
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|