cascrow-signer 0.1.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/MIGRATION.md +35 -0
  4. package/README.md +130 -0
  5. package/SECURITY.md +71 -0
  6. package/dist/acceptance.d.ts +70 -0
  7. package/dist/acceptance.js +169 -0
  8. package/dist/actions.d.ts +60 -0
  9. package/dist/actions.js +319 -0
  10. package/dist/api.d.ts +83 -0
  11. package/dist/api.js +105 -0
  12. package/dist/book.d.ts +26 -0
  13. package/dist/book.js +62 -0
  14. package/dist/calldata.d.ts +62 -0
  15. package/dist/calldata.js +121 -0
  16. package/dist/chain.d.ts +59 -0
  17. package/dist/chain.js +233 -0
  18. package/dist/checks.d.ts +101 -0
  19. package/dist/checks.js +236 -0
  20. package/dist/cli.d.ts +2 -0
  21. package/dist/cli.js +163 -0
  22. package/dist/context.d.ts +47 -0
  23. package/dist/context.js +50 -0
  24. package/dist/errors.d.ts +11 -0
  25. package/dist/errors.js +17 -0
  26. package/dist/files.d.ts +13 -0
  27. package/dist/files.js +78 -0
  28. package/dist/generated/milestone-escrow-v3.deployments.json +15 -0
  29. package/dist/generated/milestone-escrow-v3.json +1179 -0
  30. package/dist/generated/v3-actions.d.ts +49 -0
  31. package/dist/generated/v3-actions.js +102 -0
  32. package/dist/generated/v3-proof-manifest.d.ts +19 -0
  33. package/dist/generated/v3-proof-manifest.js +79 -0
  34. package/dist/journal.d.ts +41 -0
  35. package/dist/journal.js +186 -0
  36. package/dist/keystore.d.ts +30 -0
  37. package/dist/keystore.js +119 -0
  38. package/dist/link.d.ts +17 -0
  39. package/dist/link.js +65 -0
  40. package/dist/mcp.d.ts +10 -0
  41. package/dist/mcp.js +81 -0
  42. package/dist/network.d.ts +53 -0
  43. package/dist/network.js +122 -0
  44. package/dist/policy.d.ts +31 -0
  45. package/dist/policy.js +92 -0
  46. package/dist/remote.d.ts +31 -0
  47. package/dist/remote.js +104 -0
  48. package/dist/tools.d.ts +20 -0
  49. package/dist/tools.js +112 -0
  50. package/dist/watch.d.ts +35 -0
  51. package/dist/watch.js +129 -0
  52. package/package.json +64 -0
package/dist/tools.js ADDED
@@ -0,0 +1,112 @@
1
+ const escrowRef = {
2
+ escrowId: { type: "string", description: "The escrow ID (0x…). Optional when milestoneId names an escrow in this signer's local book." },
3
+ milestoneId: { type: "string", description: "The cascrow milestone. Used to find the escrow in the local book and to report the transaction hash." },
4
+ };
5
+ export const SIGNER_TOOLS = Object.freeze([
6
+ {
7
+ name: "cascrow_signer_status",
8
+ description: "Show this local signer's address, network, pinned escrow contract and RPC. Signs nothing.",
9
+ inputSchema: { type: "object", properties: {} },
10
+ },
11
+ {
12
+ name: "cascrow_signer_link",
13
+ description: "Link this signer's address to your cascrow account by signing cascrow's wallet-ownership challenge (only that exact sentence is ever signed). " +
14
+ "Refuses to replace a different linked wallet — that needs a person on a terminal: cascrow-signer link.",
15
+ inputSchema: { type: "object", properties: {} },
16
+ },
17
+ {
18
+ name: "cascrow_signer_accept",
19
+ description: "As the PAYEE, check the payer's MilestoneEscrowV3 offer against this signer's pinned chain and contract and sign the acceptance locally. " +
20
+ "The signature is binding until acceptBy: the payer can fund these exact terms until then. Refuses an offer whose payee is not this key, whose token is not native XRP, " +
21
+ "whose arbiter your local policy does not accept, or whose acceptBy has passed on chain.",
22
+ inputSchema: {
23
+ type: "object",
24
+ required: ["milestoneId"],
25
+ properties: {
26
+ milestoneId: { type: "string" },
27
+ minAmountXrp: { type: "string", description: "Refuse if the escrowed amount is below this many XRP." },
28
+ },
29
+ },
30
+ },
31
+ {
32
+ name: "cascrow_signer_fund",
33
+ description: "As the PAYER, fund an accepted MilestoneEscrowV3 escrow with ONE transaction (value = amount, no approve). The signer rebuilds and checks every term " +
34
+ "(escrow ID from your own address, native XRP, your per-escrow and 24-hour limits, arbiter, review period, the payee's signature under the pinned domain, acceptBy on chain time) " +
35
+ "and sends to the public RPC itself. Limits are set only on a terminal: cascrow-signer policy set.",
36
+ inputSchema: {
37
+ type: "object",
38
+ required: ["milestoneId"],
39
+ properties: {
40
+ milestoneId: { type: "string" },
41
+ maxAmountXrp: { type: "string", description: "Refuse if the amount exceeds this many XRP (on top of the local limits)." },
42
+ },
43
+ },
44
+ },
45
+ {
46
+ name: "cascrow_signer_submit_claim",
47
+ description: "As the PAYEE, claim the escrow on chain with ONE sealed submission. Pass proofHash = the manifestHash the evidence door returned " +
48
+ "(cascrow_mcp_submit, proof upload, cascrow_verify_work) — one submission is one evidence request (today one file, text or GitHub item). " +
49
+ "Alternatively pass the one file (or text) of that submission and the hash is computed HERE. Either way cascrow must confirm the hash is a " +
50
+ "sealed submission of this milestone before anything is signed (CLAIM_PROOF_UNKNOWN otherwise). Claim with a submission whose verdict is YES " +
51
+ "(claimReady) — with NO or PENDING the payer is likely to object. The payer then has the review period to object.",
52
+ inputSchema: {
53
+ type: "object",
54
+ required: ["milestoneId"],
55
+ properties: {
56
+ ...escrowRef,
57
+ proofHash: { type: "string", description: "The manifestHash the evidence door returned for the sealed submission (0x + 64 hex)." },
58
+ files: { type: "array", items: { type: "string" }, description: "Instead of proofHash: the path of the submission's file." },
59
+ texts: { type: "array", items: { type: "string" }, description: "Instead of proofHash: the submission's text, byte for byte as the evidence door received it." },
60
+ },
61
+ },
62
+ },
63
+ {
64
+ name: "cascrow_signer_object",
65
+ description: "As the PAYER, object to the live claim before its objection deadline — this stops the release. Only for escrows whose on-chain payer is this key " +
66
+ "(read from the chain; the local book is not needed). cascrow's server is not needed; the transaction is built from the chain. If the objection is pending too long, use cascrow_signer_speed_up.",
67
+ inputSchema: { type: "object", required: ["reason"], properties: { ...escrowRef, reason: { type: "string", description: "Why; its keccak256 goes on chain." } } },
68
+ },
69
+ {
70
+ name: "cascrow_signer_release",
71
+ description: "As the PAYER, pay the escrow to the payee now. Only for escrows this signer funded, whose on-chain payee is the one in the local book.",
72
+ inputSchema: { type: "object", properties: { ...escrowRef } },
73
+ },
74
+ {
75
+ name: "cascrow_signer_extend_deadline",
76
+ description: "As the PAYER, move the escrow's deadline later (never earlier). Only for escrows this signer funded.",
77
+ inputSchema: { type: "object", required: ["newDeadline"], properties: { ...escrowRef, newDeadline: { type: "string", description: "Unix seconds." } } },
78
+ },
79
+ {
80
+ name: "cascrow_signer_claim",
81
+ description: "Pay out a claim whose objection window has closed without an objection (either party may call it).",
82
+ inputSchema: { type: "object", properties: { ...escrowRef } },
83
+ },
84
+ {
85
+ name: "cascrow_signer_refund",
86
+ description: "Return the escrow to the payer once the contract allows it (deadline passed with no live claim, or an arbitration window lapsed).",
87
+ inputSchema: { type: "object", properties: { ...escrowRef } },
88
+ },
89
+ {
90
+ name: "cascrow_signer_return_funds",
91
+ description: "As the PAYEE, give the escrowed money back to the payer.",
92
+ inputSchema: { type: "object", properties: { ...escrowRef } },
93
+ },
94
+ {
95
+ name: "cascrow_signer_withdraw",
96
+ description: "Withdraw this address's own credit from the escrow contract (a payment that could not be pushed). Pays only this address.",
97
+ inputSchema: { type: "object", properties: {} },
98
+ },
99
+ {
100
+ name: "cascrow_signer_onchain_claims",
101
+ description: "As the PAYER, list the live claims on your escrows read from the CHAIN (your Funded events and local journal) — not from cascrow. " +
102
+ "Every entry with objectable=true releases your money at objectionDeadline unless you object.",
103
+ inputSchema: { type: "object", properties: {} },
104
+ },
105
+ {
106
+ name: "cascrow_signer_speed_up",
107
+ description: "Replace one of this signer's pending transactions with the same nonce and a 25% higher fee.",
108
+ inputSchema: { type: "object", required: ["txHash"], properties: { txHash: { type: "string" } } },
109
+ },
110
+ ]);
111
+ /** The tools only this package offers — disjoint from the hosted cascrow MCP tools. */
112
+ export const LOCAL_ONLY_TOOLS = Object.freeze(SIGNER_TOOLS.map((t) => t.name));
@@ -0,0 +1,35 @@
1
+ import type { ChainCtx } from "./chain.js";
2
+ import type { SignerContext } from "./context.js";
3
+ export type OnchainClaim = {
4
+ escrowId: string;
5
+ role: "payer";
6
+ claimIndex: number;
7
+ claimedAt: number;
8
+ objectionDeadline: number;
9
+ secondsLeft: number;
10
+ objectable: boolean;
11
+ };
12
+ /** Every escrow this address funded, from Funded(payer = me) since the deploy block. */
13
+ export declare function escrowsFundedBy(c: ChainCtx, me: string, toBlock: number): Promise<string[]>;
14
+ /** Open claims on this address's escrows, read from the chain alone. */
15
+ export declare function onchainClaims(ctx: SignerContext): Promise<{
16
+ address: string;
17
+ claims: OnchainClaim[];
18
+ escrowsChecked: number;
19
+ }>;
20
+ /**
21
+ * The environment an `--exec` alarm command gets: an allowlist of what a program
22
+ * needs to start (PATH, HOME, locale, temp dir, and the Windows basics) plus the
23
+ * claim — never the signer's own environment, which may carry the API key or,
24
+ * in a caller that set it, the keystore passphrase (post-review H3).
25
+ */
26
+ export declare const EXEC_ENV_ALLOWLIST: readonly string[];
27
+ export declare function execEnv(env: Record<string, string | undefined>, cl: Pick<OnchainClaim, "escrowId" | "claimIndex" | "objectionDeadline">): Record<string, string>;
28
+ export type WatchOptions = {
29
+ intervalSec?: number;
30
+ exec?: string;
31
+ webhook?: string;
32
+ once?: boolean;
33
+ signal?: AbortSignal;
34
+ };
35
+ export declare function watch(ctx: SignerContext, o?: WatchOptions): Promise<void>;
package/dist/watch.js ADDED
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The payer's watcher: finds this key's escrows ON CHAIN (every `Funded` event
3
+ * with payer = this address, plus the escrows in the local journal) and raises
4
+ * an alarm for every live claim whose objection window is still open.
5
+ *
6
+ * Never from cascrow. The objection window can be one hour long, and a veto with
7
+ * a clock must not depend on the index, the notifications or the reachability of
8
+ * the party that is not the payer. It needs no passphrase: it reads the address
9
+ * from the keystore file and signs nothing.
10
+ *
11
+ * Alarms go to stderr, to an optional `--exec` command (run without a shell,
12
+ * with an allowlisted environment plus the claim — `execEnv`) and to an optional webhook of the
13
+ * user's own. The remedy it names is the payer's own `object`; if an objection is
14
+ * already pending, raise its fee (`cascrow-signer speed-up`) instead of waiting.
15
+ */
16
+ import { execFile } from "node:child_process";
17
+ import { toBeHex, zeroPadValue, getAddress } from "ethers";
18
+ import { ESCROW_V3_INTERFACE } from "./calldata.js";
19
+ import { latestBlock, readEscrow } from "./chain.js";
20
+ import { chainFacts } from "./checks.js";
21
+ import { v3ClaimLive, v3ObjectionDeadline } from "./generated/v3-actions.js";
22
+ import { verifyEndpoint } from "./network.js";
23
+ const LOG_CHUNK = 10_000;
24
+ /** Every escrow this address funded, from Funded(payer = me) since the deploy block. */
25
+ export async function escrowsFundedBy(c, me, toBlock) {
26
+ const topic0 = ESCROW_V3_INTERFACE.getEvent("Funded").topicHash;
27
+ const payerTopic = zeroPadValue(getAddress(me), 32).toLowerCase();
28
+ const ids = new Set();
29
+ for (let start = c.net.deployBlock; start <= toBlock; start += LOG_CHUNK) {
30
+ const end = Math.min(toBlock, start + LOG_CHUNK - 1);
31
+ const logs = await c.rpc.call("eth_getLogs", [
32
+ { address: c.net.address, fromBlock: toBeHex(start), toBlock: toBeHex(end), topics: [topic0, null, payerTopic] },
33
+ ]);
34
+ for (const l of logs ?? []) {
35
+ const p = ESCROW_V3_INTERFACE.parseLog(l);
36
+ if (p && p.name === "Funded")
37
+ ids.add(String(p.args.escrowId).toLowerCase());
38
+ }
39
+ }
40
+ return [...ids];
41
+ }
42
+ /** Open claims on this address's escrows, read from the chain alone. */
43
+ export async function onchainClaims(ctx) {
44
+ const me = getAddress(ctx.address());
45
+ const c = { rpc: ctx.rpc, net: ctx.net };
46
+ await verifyEndpoint(ctx.rpc, ctx.net);
47
+ const head = await latestBlock(c);
48
+ const ids = new Set(await escrowsFundedBy(c, me, head.number));
49
+ for (const r of ctx.journal.exists() ? ctx.journal.read() : [])
50
+ if (r.action === "fund" && r.escrowId)
51
+ ids.add(r.escrowId.toLowerCase());
52
+ const claims = [];
53
+ for (const id of ids) {
54
+ const e = await readEscrow(c, id);
55
+ if (e.status !== 1 || e.payer !== me)
56
+ continue;
57
+ const f = chainFacts(e);
58
+ if (!v3ClaimLive(f))
59
+ continue;
60
+ const deadline = v3ObjectionDeadline(f);
61
+ claims.push({
62
+ escrowId: id,
63
+ role: "payer",
64
+ claimIndex: e.claimCount,
65
+ claimedAt: e.claimedAt,
66
+ objectionDeadline: deadline,
67
+ secondsLeft: Math.max(0, deadline - head.timestamp),
68
+ objectable: head.timestamp < deadline,
69
+ });
70
+ }
71
+ return { address: me, claims, escrowsChecked: ids.size };
72
+ }
73
+ function fmtLeft(s) {
74
+ const h = Math.floor(s / 3600);
75
+ const m = Math.floor((s % 3600) / 60);
76
+ return `${h}h ${m}m`;
77
+ }
78
+ /**
79
+ * The environment an `--exec` alarm command gets: an allowlist of what a program
80
+ * needs to start (PATH, HOME, locale, temp dir, and the Windows basics) plus the
81
+ * claim — never the signer's own environment, which may carry the API key or,
82
+ * in a caller that set it, the keystore passphrase (post-review H3).
83
+ */
84
+ export const EXEC_ENV_ALLOWLIST = Object.freeze([
85
+ "PATH", "HOME", "USER", "LOGNAME", "SHELL", "LANG", "LC_ALL", "LC_CTYPE", "TZ", "TMPDIR", "TEMP", "TMP",
86
+ "SYSTEMROOT", "SYSTEMDRIVE", "WINDIR", "COMSPEC", "PATHEXT", "USERPROFILE",
87
+ ]);
88
+ export function execEnv(env, cl) {
89
+ const out = {};
90
+ for (const k of EXEC_ENV_ALLOWLIST)
91
+ if (typeof env[k] === "string")
92
+ out[k] = env[k];
93
+ out.CASCROW_ESCROW_ID = cl.escrowId;
94
+ out.CASCROW_CLAIM_INDEX = String(cl.claimIndex);
95
+ out.CASCROW_OBJECTION_DEADLINE = String(cl.objectionDeadline);
96
+ return out;
97
+ }
98
+ export async function watch(ctx, o = {}) {
99
+ const seen = new Set();
100
+ const interval = Math.max(15, o.intervalSec ?? 60) * 1000;
101
+ for (;;) {
102
+ try {
103
+ const { claims } = await onchainClaims(ctx);
104
+ for (const cl of claims) {
105
+ if (!cl.objectable)
106
+ continue;
107
+ const k = `${cl.escrowId}:${cl.claimedAt}`;
108
+ if (seen.has(k))
109
+ continue;
110
+ seen.add(k);
111
+ ctx.io.err(`ALARM: claim #${cl.claimIndex} on escrow ${cl.escrowId} releases your money in ${fmtLeft(cl.secondsLeft)} ` +
112
+ `(at ${new Date(cl.objectionDeadline * 1000).toISOString()}) unless you object: cascrow-signer object --escrow ${cl.escrowId} --reason "…". ` +
113
+ "If an objection is already pending, raise its fee: cascrow-signer speed-up --tx <hash>.");
114
+ if (o.exec) {
115
+ execFile(o.exec, [], { env: execEnv(process.env, cl) }, () => undefined);
116
+ }
117
+ if (o.webhook) {
118
+ fetch(o.webhook, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ type: "claim.open", ...cl }) }).catch(() => undefined);
119
+ }
120
+ }
121
+ }
122
+ catch (e) {
123
+ ctx.io.err(`watch: could not read the chain (${e instanceof Error ? e.message : "error"}) — retrying. Your objection window runs regardless.`);
124
+ }
125
+ if (o.once || o.signal?.aborted)
126
+ return;
127
+ await new Promise((r) => setTimeout(r, interval));
128
+ }
129
+ }
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "cascrow-signer",
3
+ "version": "0.1.0",
4
+ "description": "Local signer for cascrow MilestoneEscrowV3 escrows — your key is generated, encrypted and used only on your machine; cascrow prepares, you check and sign",
5
+ "type": "module",
6
+ "main": "./dist/api.js",
7
+ "types": "./dist/api.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/api.d.ts",
11
+ "import": "./dist/api.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "bin": {
16
+ "cascrow-signer": "dist/cli.js"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md",
21
+ "SECURITY.md",
22
+ "MIGRATION.md",
23
+ "CHANGELOG.md",
24
+ "LICENSE"
25
+ ],
26
+ "engines": {
27
+ "node": ">=20"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc -p tsconfig.json && node build-copy.mjs",
31
+ "prepublishOnly": "npm run build"
32
+ },
33
+ "dependencies": {
34
+ "ethers": "6.17.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "20.19.37",
38
+ "typescript": "5.9.3"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "keywords": [
44
+ "cascrow",
45
+ "escrow",
46
+ "signer",
47
+ "xrpl-evm",
48
+ "milestone",
49
+ "agent",
50
+ "verification",
51
+ "trust"
52
+ ],
53
+ "author": "Cascrow <dev@cascrow.com>",
54
+ "license": "MIT",
55
+ "homepage": "https://cascrow.com",
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "https://github.com/Jnm04/Milestone-Based-Funding.git",
59
+ "directory": "cascrow-signer"
60
+ },
61
+ "bugs": {
62
+ "url": "https://github.com/Jnm04/Milestone-Based-Funding/issues"
63
+ }
64
+ }