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/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — unreleased (owner publishes)
4
+
5
+ - First version. A local signer for cascrow's MilestoneEscrowV3 escrows: `init` (key and
6
+ encrypted keystore on this machine), `policy` (limits in XRP, counterparty and arbiter
7
+ allowlists, minimum review period — terminal only, MAC-protected), `link`, `accept`,
8
+ `fund`, `submit-claim`, `claim`, `release`, `object`, `refund`, `extend-deadline`,
9
+ `return-funds`, `withdraw`, `claims`, `watch`, `speed-up`, and an MCP server (`mcp`)
10
+ with the `cascrow_signer_*` tools.
11
+ - Chain, public RPC, contract address and runtime code hash are pinned in the package; no
12
+ deployment is pinned yet, so every network is refused until the contract is deployed
13
+ (testnet accepts an explicit, announced override).
14
+ - One runtime dependency, `ethers` 6.17.0, pinned exactly, with `npm-shrinkwrap.json`.
15
+ - `submit-claim` takes `--proof-hash` (MCP: `proofHash`) — the `manifestHash` the evidence
16
+ door returned — and signs only after cascrow confirms it is a sealed submission of the
17
+ milestone; a hash computed from a local file must be confirmed the same way
18
+ (`CLAIM_PROOF_UNKNOWN` otherwise, no override).
19
+ - A cascrow `prepare` that answers with a refusal (4xx and its code) is surfaced as that
20
+ refusal, with its remedy; only an unreachable cascrow, a 5xx, a non-JSON answer, 401, 408
21
+ or 429 count as "not available" and let a post-funding action proceed from the chain.
22
+ - `object` is gated on the chain alone (the on-chain payer is this key); only `release` and
23
+ `extend-deadline` need the local book, and a damaged book is a loud note, not an abort,
24
+ for every other action.
25
+ - The CLI removes `CASCROW_KEYSTORE_PASSPHRASE` and `_FILE` from its environment at start
26
+ for every command; a `watch --exec` command gets an allowlisted environment.
27
+ - `accept` refuses an offer that leaves the payee less than max(review period, 72 h, 25 %
28
+ of the term) between `acceptBy` and the deadline.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cascrow
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/MIGRATION.md ADDED
@@ -0,0 +1,35 @@
1
+ # Migrating to cascrow-signer
2
+
3
+ ## From `cascrow_escrow_fund` / `escrowFund()` / `cascrow escrow-fund --private-key`
4
+
5
+ Those paths sent your private key to cascrow, which signed the deposit on its servers.
6
+ They were removed in cascrow-mcp 2.0.0, cascrow-sdk 0.5.0 and cascrow-cli 2.0.0.
7
+ **If you ever used one, treat that key as exposed**: move its funds to a new wallet, and
8
+ do not link the old address again (cascrow refuses it anyway).
9
+
10
+ | Before | Now |
11
+ |---|---|
12
+ | `cascrow_escrow_fund { agentPrivateKey, … }` | `cascrow_signer_fund { milestoneId }` in this package's MCP server |
13
+ | `client.escrowFund({ privateKey, … })` | `createSigner(configFromEnv(process.env, io)).fund({ milestoneId })` |
14
+ | `cascrow escrow-fund --private-key …` | `npx cascrow-signer@0.1.0 fund --milestone <id>` |
15
+ | the wallet cascrow generated at registration | `cascrow-signer init` + `cascrow-signer link` |
16
+
17
+ ## v2 (RLUSD) escrows
18
+
19
+ Unchanged: `GET /api/agent/escrow-prepare` returns the two unsigned transactions (approve,
20
+ fund); sign and broadcast them with your own wallet, then `POST /api/escrow/sync`. This
21
+ package signs MilestoneEscrowV3 only.
22
+
23
+ ## MilestoneEscrowV3, step by step
24
+
25
+ 1. Payer: `cascrow_v3_offer` (remote, cascrow-mcp / SDK `v3.offer` / `cascrow v3 offer`).
26
+ 2. Payee: `cascrow-signer accept --milestone <id>`.
27
+ 3. Payer: `cascrow-signer fund --milestone <id>`.
28
+ 4. Payee: work, submit evidence to cascrow for the AI recommendation, then
29
+ `cascrow-signer submit-claim --milestone <id> --file …`.
30
+ 5. Payer: within the review period, `release` — or `object` with a reason. `watch` makes
31
+ sure you see the claim; cascrow's notifications are a convenience, not the guarantee.
32
+
33
+ A cascrow verification of `YES` on a V3 milestone is a **recommendation** and moves no
34
+ money: only the payer's `release`, an unobjected claim running out its review period
35
+ (`claim`), or the arbiter pays the payee.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # cascrow-signer
2
+
3
+ Your key for cascrow's **MilestoneEscrowV3** escrows — generated, encrypted and used on
4
+ your machine only. cascrow verifies work, keeps the audit trail and prepares transactions;
5
+ it never holds, receives or uses your key, and it never sends, relays or pays for a
6
+ transaction on your behalf. This package is the part that signs, and it checks everything
7
+ it signs against what it knows independently of cascrow.
8
+
9
+ - One runtime dependency (`ethers`, pinned exactly) and a shrinkwrap, so the code that
10
+ touches your key can be read end to end.
11
+ - The chain, the public RPC and the escrow contract's address and code hash are **pinned in
12
+ the package**. A network without a pinned deployment is refused.
13
+ - Transactions go to the network's **public RPC**, never to cascrow.
14
+
15
+ Install a pinned version — never `@latest` for a tool that holds money:
16
+
17
+ ```sh
18
+ npx cascrow-signer@0.1.0 help
19
+ ```
20
+
21
+ > **Status: unreleased.** The MilestoneEscrowV3 contract is not deployed yet, so every
22
+ > network is refused until a version with a pinned deployment ships. On testnet a
23
+ > deployment can be named explicitly (see *Testnet override*).
24
+
25
+ ## Setup
26
+
27
+ ```sh
28
+ export CASCROW_SIGNER_NETWORK=testnet # or mainnet — never guessed
29
+ cascrow-signer init # new key, encrypted keystore in ~/.cascrow/signer
30
+ cascrow-signer policy set --per-escrow-xrp 500 --daily-xrp 2000 --min-review-hours 72
31
+ export CASCROW_API_KEY=csk_… # your cascrow account
32
+ cascrow-signer link # proves this address is yours to cascrow
33
+ ```
34
+
35
+ `~/.cascrow/signer` is created `0700`, every file in it `0600`; a file other users can
36
+ read is refused, not repaired. Set `CASCROW_SIGNER_HOME` to use another directory.
37
+
38
+ **Passphrase**: a terminal prompt, `CASCROW_KEYSTORE_PASSPHRASE_FILE` (a `0600` file), or
39
+ `CASCROW_KEYSTORE_PASSPHRASE`. Either variable is removed from the process environment once
40
+ read. There is no OS-keychain integration on purpose (native dependency, supply chain).
41
+
42
+ ## As payer
43
+
44
+ ```sh
45
+ cascrow-signer fund --milestone <id> # one transaction, value = amount, no approve
46
+ cascrow-signer claims # live claims on your escrows, read from the chain
47
+ cascrow-signer watch --exec ./page-me.sh # alarm for every claim whose window is open
48
+ cascrow-signer object --escrow 0x… --reason "Criterion 2 is not met: …"
49
+ cascrow-signer release --escrow 0x…
50
+ cascrow-signer extend-deadline --escrow 0x… --new-deadline 1767225600
51
+ ```
52
+
53
+ Before `fund` signs, it checks — against values it knows itself, not the server's word:
54
+ the escrow ID is `escrowIdFor(your address, ref, salt)`; the token is native XRP; `value`
55
+ equals the amount; the amount is within your per-escrow limit (a lower one for a payee not
56
+ in your counterparty book) and your rolling 24-hour limit; the arbiter is absent or one you
57
+ listed; the review period is at least your minimum; the deadline is within the contract's
58
+ maximum and `acceptBy` still open **by chain time**; and the payee's signature verifies
59
+ under the pinned chain and contract. The escrow is then written to your local book.
60
+
61
+ After funding, `object`, `release`, `refund`, `extend-deadline`, `claim`, `withdraw` and
62
+ `return-funds` are built from the escrow ID and the chain alone — **cascrow does not need
63
+ to be reachable** for you to object. `object` is signed whenever the chain names you as
64
+ the escrow's payer — it does not need the local book, so a lost or damaged book never
65
+ costs you an objection window. `release` and `extend-deadline` are signed only for
66
+ escrows in your local book whose on-chain payer is you. When cascrow is reachable and
67
+ **refuses** to prepare an action (an answer with a code, e.g. `WRONG_PHASE` or
68
+ `COMPLIANCE_BLOCK`), the signer stops and shows that refusal instead of sending anyway;
69
+ only an unreachable cascrow, a 5xx, a rate limit or a bad API key count as "not available".
70
+
71
+ A pending objection that is not being mined: `cascrow-signer speed-up --tx <hash>`
72
+ (same nonce, fee +25%). Never send the same action twice.
73
+
74
+ ## As payee
75
+
76
+ ```sh
77
+ cascrow-signer accept --milestone <id> # checks and signs the payer's offer
78
+ cascrow-signer submit-claim --milestone <id> --proof-hash 0x… # the manifestHash the upload returned
79
+ cascrow-signer return-funds --escrow 0x…
80
+ ```
81
+
82
+ `accept` refuses an offer whose payee is not your address, whose token is not native XRP,
83
+ whose arbiter you have not listed, or whose `acceptBy` has passed. **Your acceptance binds
84
+ you until `acceptBy`** — the payer can fund these exact terms until then, and nothing in
85
+ cascrow revokes it.
86
+
87
+ `submit-claim` claims with ONE sealed submission. Every evidence upload (one request —
88
+ today one file, one text or one GitHub item) is its own submission, and its answer carries
89
+ the submission's `manifestHash`: pass that as `--proof-hash`. Instead you can pass the one
90
+ file of that submission (`--file report.pdf`) and the hash is computed here with the same
91
+ manifest encoding cascrow uses. Either way cascrow must confirm the hash is a sealed
92
+ submission of the milestone (`CLAIM_PROOF_UNKNOWN` otherwise) before anything is signed — a
93
+ claim naming evidence cascrow never assessed has no recommendation and invites an
94
+ objection. Claim with a submission whose verdict is YES (`claimReady`); with NO or PENDING
95
+ the payer is likely to object.
96
+
97
+ ## As an MCP server
98
+
99
+ ```json
100
+ { "mcpServers": { "cascrow-signer": {
101
+ "command": "npx", "args": ["cascrow-signer@0.1.0", "mcp"],
102
+ "env": { "CASCROW_SIGNER_NETWORK": "testnet", "CASCROW_API_KEY": "csk_…",
103
+ "CASCROW_KEYSTORE_PASSPHRASE_FILE": "/home/me/.cascrow/pass" } } } }
104
+ ```
105
+
106
+ Tools: `cascrow_signer_status`, `_link`, `_accept`, `_fund`, `_submit_claim`, `_object`,
107
+ `_release`, `_extend_deadline`, `_claim`, `_refund`, `_return_funds`, `_withdraw`,
108
+ `_onchain_claims`, `_speed_up`. They are all local; cascrow's hosted MCP server and the
109
+ `cascrow-mcp` package offer none of them, and no tool takes a key or a passphrase. The
110
+ policy cannot be changed through MCP — only on a terminal.
111
+
112
+ Use it next to `cascrow-mcp` (or the hosted server), which carries the remote
113
+ `cascrow_v3_*` tools: offers, acceptance data, prepared transactions, attestations.
114
+
115
+ ## Testnet override
116
+
117
+ For a testnet deployment the package does not pin yet — only one you deployed or verified
118
+ yourself:
119
+
120
+ ```sh
121
+ CASCROW_SIGNER_TESTNET_CONTRACT=0x… CASCROW_SIGNER_TESTNET_DEPLOY_BLOCK=123 \
122
+ CASCROW_SIGNER_TESTNET_CODE_HASH=0x… [CASCROW_SIGNER_TESTNET_RPC_URL=…] cascrow-signer status
123
+ ```
124
+
125
+ Every use prints a warning. There is no override for mainnet.
126
+
127
+ ## Windows
128
+
129
+ POSIX permission checks are skipped on Windows; protect `%USERPROFILE%\.cascrow\signer`
130
+ yourself. See [SECURITY.md](SECURITY.md) for what this package does and does not protect.
package/SECURITY.md ADDED
@@ -0,0 +1,71 @@
1
+ # Security — what cascrow-signer protects, and what it does not
2
+
3
+ ## The boundary
4
+
5
+ cascrow (the service) prepares, reads and recommends. It never creates, holds, receives
6
+ or uses a party's key, and never sends, relays or pays for a transaction to the escrow
7
+ contract (rule I9). This package is where the key lives. The line between the two:
8
+
9
+ | Leaves this machine for cascrow | Never leaves this machine |
10
+ |---|---|
11
+ | the wallet-link signature over cascrow's fixed challenge sentence | the private key |
12
+ | the payee's acceptance signature (EIP-712, pinned domain) | the passphrase |
13
+ | a transaction **hash**, as a hint to re-read the chain | the keystore file |
14
+ | your API key, as the `Authorization` header | any signed transaction |
15
+
16
+ Signed transactions go to the network's public RPC only (pinned in the package). The
17
+ complete list of calls to cascrow is in `src/remote.ts`. A test in the cascrow repository
18
+ (`signer-canary.test.ts`) runs every action with a random key and passphrase against a fake
19
+ cascrow and a fake RPC and fails if any URL, header, body, stdout/stderr line or tool
20
+ result contains the key, the passphrase, the keystore ciphertext or a raw signed
21
+ transaction.
22
+
23
+ ## What the checks protect against
24
+
25
+ - **A wrong or compromised cascrow API.** Chain ID, contract address and runtime code hash
26
+ come from the package, not from the server; every term of a `fund` is checked against
27
+ your address, your policy, the chain's clock and the payee's signature under the pinned
28
+ domain; `data` is rebuilt from the ABI. A server can refuse to help you, but it cannot
29
+ make this signer fund different terms, pay a different contract or sign a message that is
30
+ not the wallet challenge.
31
+ - **A confused or prompt-injected agent driving the MCP tools.** Limits per escrow and per
32
+ 24 hours, a lower limit for unknown payees, the arbiter allowlist and the minimum review
33
+ period are enforced locally; the policy can only be changed on a terminal, and its file
34
+ carries a MAC under a key derived from your private key.
35
+ - **Losing cascrow.** Every action after funding but `submitClaim` works from the chain
36
+ alone; the watcher reads your escrows from the chain. `object` needs neither cascrow nor
37
+ the local book — only that the chain names this key as the payer. `submitClaim` needs
38
+ cascrow's confirmation that the proof hash is a sealed submission of the milestone.
39
+ - **Cheating the payee on time.** `accept` refuses an offer whose `acceptBy` leaves less
40
+ than max(review period, 72 h, 25 % of the remaining term) before the deadline: the payer
41
+ can fund at any moment up to `acceptBy`, so what follows `acceptBy` is the payee's
42
+ guaranteed time to deliver.
43
+ - **A deleted journal.** Today's spending is rebuilt from `Funded(payer = you)` events; if
44
+ neither is readable, `fund` is refused.
45
+
46
+ ## What it does NOT protect against
47
+
48
+ - **A compromised cascrow server, on the payee's side, within your own limits.** A payee's
49
+ acceptance signature can be funded by the payer until `acceptBy`; the server only relays
50
+ it. A compromised server can therefore get a fund to a payee who signed an acceptance —
51
+ never different terms, never above your local limits. In MCP mode (no terminal) a payee
52
+ not in your counterparty book is funded only up to `unknownPayeePerEscrowMaxWei`, so that
53
+ is the most a compromised server plus a driven agent can place with an unknown payee per
54
+ escrow (and your 24-hour limit across escrows). Set it low, or add payees on a terminal.
55
+ And until `acceptBy` the payer holds a free option to fund or let the offer lapse; the
56
+ payee's minimum work window (above) bounds what that option can cost the payee.
57
+ - **A process that can read your passphrase.** It can unlock the key, recompute the policy
58
+ MAC and write any limits it likes. If an agent runs with `CASCROW_KEYSTORE_PASSPHRASE` in
59
+ its environment or can read the passphrase file, the policy stops honest mistakes, not
60
+ that agent. Run the signer as a separate user or on a separate machine if that matters.
61
+ - **Malware on the machine**, a compromised Node.js, or a compromised `ethers` (pinned
62
+ exactly and shrinkwrapped, so an upgrade is a visible change — not a guarantee).
63
+ - **A compromised release of this package.** Install a pinned version and check it.
64
+ - **The public RPC lying about chain state** (it cannot forge your signature, but it could
65
+ hide a claim from `watch`). Point `watch` at a second RPC you trust if the review window
66
+ is short.
67
+ - **Windows file permissions** — not checked there.
68
+
69
+ ## Reporting
70
+
71
+ security@cascrow.com. Please do not open a public issue for a vulnerability.
@@ -0,0 +1,70 @@
1
+ import type { SignerNetwork } from "./network.js";
2
+ import type { SignerContext } from "./context.js";
3
+ export type AcceptanceOffer = {
4
+ offerId: string;
5
+ escrowId: string;
6
+ payer: string;
7
+ ref: string;
8
+ salt: string;
9
+ amountWei: string;
10
+ acceptBy: string | number;
11
+ typedData: {
12
+ domain: Record<string, unknown>;
13
+ types: Record<string, {
14
+ name: string;
15
+ type: string;
16
+ }[]>;
17
+ primaryType: string;
18
+ message: Record<string, unknown>;
19
+ };
20
+ };
21
+ export type AcceptanceMessage = {
22
+ escrowId: string;
23
+ payee: string;
24
+ token: string;
25
+ amount: bigint;
26
+ deadline: bigint;
27
+ reviewPeriod: number;
28
+ arbiter: string;
29
+ arbitrationPeriod: number;
30
+ acceptBy: bigint;
31
+ };
32
+ /** The floor of the payee's work window, whatever the review period and term (the app's MIN_WORK_FLOOR_SEC). */
33
+ export declare const MIN_WORK_FLOOR_SECONDS: number;
34
+ /**
35
+ * The least time the payee must have between the last moment the payer can fund
36
+ * (`acceptBy`) and the deadline: max(review period, 72 h, 25 % of the remaining
37
+ * term). The same formula cascrow applies when it issues the offer
38
+ * (`minWorkWindowSec` in the app's v3-terms.ts). Checked HERE as well, because the
39
+ * payer holds a free fund-or-lapse option until `acceptBy`: a compromised server
40
+ * could otherwise hand the payee an offer whose acceptBy sits right before the
41
+ * deadline, and a funding at the last moment would leave no time to deliver.
42
+ */
43
+ export declare function minWorkWindowSeconds(reviewPeriodSec: number, deadlineSec: bigint | number, nowSec: bigint | number): number;
44
+ /** Every check on an offer before the payee signs it. Pure. Returns the message to sign, rebuilt from checked values. */
45
+ export declare function checkAcceptance(o: AcceptanceOffer, ctx: {
46
+ me: string;
47
+ net: Pick<SignerNetwork, "chainId" | "address">;
48
+ chainNowSec: number;
49
+ arbiters: readonly string[];
50
+ minAmountWei?: bigint | null;
51
+ }): AcceptanceMessage;
52
+ export type AcceptInput = {
53
+ milestoneId: string;
54
+ minAmountXrp?: string;
55
+ };
56
+ export declare function accept(ctx: SignerContext, input: AcceptInput): Promise<{
57
+ accepted: boolean;
58
+ escrowId: string;
59
+ payer: string;
60
+ amountWei: string;
61
+ acceptBy: string;
62
+ deadline: string;
63
+ reviewPeriod: number;
64
+ arbiter: string;
65
+ server: {
66
+ escrowId: {} | null;
67
+ acceptBy: {} | null;
68
+ };
69
+ note: string;
70
+ }>;
@@ -0,0 +1,169 @@
1
+ /**
2
+ * The payee's acceptance, signed on this machine after checking it against what
3
+ * the payee knows independently of cascrow.
4
+ *
5
+ * The signature is irrevocable until `acceptBy`: whoever holds it can fund the
6
+ * escrow with exactly these terms, and deleting anything in cascrow does not take
7
+ * it back. So nothing from the server's typed data is signed as-is — the message
8
+ * is rebuilt here from checked values, under the PINNED domain (this package's
9
+ * chain ID and contract), with the contract's own types.
10
+ */
11
+ import { ZeroAddress, getAddress, isAddress, parseEther } from "ethers";
12
+ import { refuse } from "./errors.js";
13
+ import { ACCEPTANCE_PRIMARY_TYPE, ACCEPTANCE_TYPES, V3_LIMITS, acceptanceDomain, escrowIdFor } from "./calldata.js";
14
+ import { latestBlock } from "./chain.js";
15
+ import { ACCEPT_BY_MARGIN_SECONDS } from "./checks.js";
16
+ import { Book } from "./book.js";
17
+ import { loadPolicy } from "./policy.js";
18
+ import { verifyEndpoint } from "./network.js";
19
+ const lc = (s) => String(s ?? "").toLowerCase();
20
+ /** The floor of the payee's work window, whatever the review period and term (the app's MIN_WORK_FLOOR_SEC). */
21
+ export const MIN_WORK_FLOOR_SECONDS = 72 * 3600;
22
+ /**
23
+ * The least time the payee must have between the last moment the payer can fund
24
+ * (`acceptBy`) and the deadline: max(review period, 72 h, 25 % of the remaining
25
+ * term). The same formula cascrow applies when it issues the offer
26
+ * (`minWorkWindowSec` in the app's v3-terms.ts). Checked HERE as well, because the
27
+ * payer holds a free fund-or-lapse option until `acceptBy`: a compromised server
28
+ * could otherwise hand the payee an offer whose acceptBy sits right before the
29
+ * deadline, and a funding at the last moment would leave no time to deliver.
30
+ */
31
+ export function minWorkWindowSeconds(reviewPeriodSec, deadlineSec, nowSec) {
32
+ const term = Number(BigInt(deadlineSec) - BigInt(nowSec));
33
+ return Math.max(reviewPeriodSec, MIN_WORK_FLOOR_SECONDS, Math.floor(Math.max(0, term) * 0.25));
34
+ }
35
+ /** Every check on an offer before the payee signs it. Pure. Returns the message to sign, rebuilt from checked values. */
36
+ export function checkAcceptance(o, ctx) {
37
+ const r = [];
38
+ if (!o || typeof o !== "object" || !o.typedData || typeof o.typedData !== "object")
39
+ refuse("ACCEPTANCE_CHECK_FAILED", "refused before signing: no typed data");
40
+ const d = o.typedData.domain ?? {};
41
+ const pinned = acceptanceDomain(ctx.net.chainId, ctx.net.address);
42
+ if (d.name !== pinned.name || d.version !== pinned.version)
43
+ r.push("the domain name/version is not the contract's");
44
+ if (String(d.chainId) !== String(pinned.chainId))
45
+ r.push("the domain chainId is not the pinned chain");
46
+ if (lc(d.verifyingContract) !== lc(pinned.verifyingContract))
47
+ r.push("the domain verifyingContract is not the pinned contract");
48
+ const types = { ...(o.typedData.types ?? {}) };
49
+ delete types.EIP712Domain;
50
+ if (JSON.stringify(types) !== JSON.stringify(ACCEPTANCE_TYPES))
51
+ r.push("the types are not the contract's PayeeAcceptance");
52
+ if (o.typedData.primaryType !== ACCEPTANCE_PRIMARY_TYPE)
53
+ r.push("primaryType is not PayeeAcceptance");
54
+ const m = o.typedData.message ?? {};
55
+ let msg = null;
56
+ try {
57
+ msg = {
58
+ escrowId: lc(m.escrowId),
59
+ payee: getAddress(String(m.payee)),
60
+ token: getAddress(String(m.token)),
61
+ amount: BigInt(String(m.amount)),
62
+ deadline: BigInt(String(m.deadline)),
63
+ reviewPeriod: Number(m.reviewPeriod),
64
+ arbiter: getAddress(String(m.arbiter)),
65
+ arbitrationPeriod: Number(m.arbitrationPeriod),
66
+ acceptBy: BigInt(String(m.acceptBy)),
67
+ };
68
+ }
69
+ catch {
70
+ refuse("ACCEPTANCE_CHECK_FAILED", "refused before signing: the message does not parse");
71
+ }
72
+ if (msg.payee !== getAddress(ctx.me))
73
+ r.push("the payee is not this key");
74
+ if (!isAddress(o.payer) || getAddress(o.payer) === getAddress(ctx.me))
75
+ r.push("the payer is missing or is this key");
76
+ else if (lc(escrowIdFor(o.payer, o.ref, o.salt)) !== msg.escrowId)
77
+ r.push("escrowId is not escrowIdFor(payer, ref, salt)");
78
+ if (lc(o.escrowId) !== msg.escrowId)
79
+ r.push("the offer's escrowId and the message's differ");
80
+ if (msg.token !== ZeroAddress)
81
+ r.push("the token is not native XRP");
82
+ if (msg.amount <= BigInt(0) || String(o.amountWei) !== msg.amount.toString())
83
+ r.push("the amount is zero or differs from the offer's amountWei");
84
+ if (ctx.minAmountWei && msg.amount < ctx.minAmountWei)
85
+ r.push("the amount is below the minimum you named");
86
+ if (String(o.acceptBy) !== msg.acceptBy.toString() && String(Math.floor(Date.parse(String(o.acceptBy)) / 1000)) !== msg.acceptBy.toString()) {
87
+ r.push("the offer's acceptBy and the message's differ");
88
+ }
89
+ const now = BigInt(ctx.chainNowSec);
90
+ if (msg.acceptBy < now + BigInt(ACCEPT_BY_MARGIN_SECONDS))
91
+ r.push("acceptBy has passed on chain (or is within two minutes)");
92
+ if (msg.deadline <= msg.acceptBy)
93
+ r.push("the deadline is not after acceptBy");
94
+ else {
95
+ const minWork = minWorkWindowSeconds(msg.reviewPeriod, msg.deadline, now);
96
+ if (msg.acceptBy > msg.deadline - BigInt(minWork)) {
97
+ r.push(`a funding at acceptBy would leave less than ${Math.ceil(minWork / 3600)} h before the deadline to do the work (max of the review period, 72 h and 25 % of the term)`);
98
+ }
99
+ }
100
+ if (msg.deadline > now + BigInt(V3_LIMITS.MAX_DEADLINE_SECONDS))
101
+ r.push("the deadline is beyond the contract's maximum duration");
102
+ if (msg.reviewPeriod < V3_LIMITS.MIN_REVIEW_SECONDS || msg.reviewPeriod > V3_LIMITS.MAX_REVIEW_SECONDS)
103
+ r.push("the review period is outside the contract's bounds");
104
+ if (msg.arbiter === ZeroAddress) {
105
+ if (msg.arbitrationPeriod !== 0)
106
+ r.push("arbitrationPeriod without an arbiter");
107
+ }
108
+ else {
109
+ if (!ctx.arbiters.includes(msg.arbiter.toLowerCase()))
110
+ r.push("the arbiter is not one your local policy accepts");
111
+ if (msg.arbiter === msg.payee || msg.arbiter === getAddress(o.payer))
112
+ r.push("the arbiter is a party");
113
+ }
114
+ if (typeof o.offerId !== "string" || !o.offerId)
115
+ r.push("no offerId");
116
+ if (r.length)
117
+ refuse("ACCEPTANCE_CHECK_FAILED", `refused before signing: ${r.join("; ")}`);
118
+ return msg;
119
+ }
120
+ export async function accept(ctx, input) {
121
+ if (!input.milestoneId)
122
+ refuse("INPUT", "milestoneId is required");
123
+ const key = await ctx.unlock();
124
+ const me = key.address;
125
+ let arbiters = [];
126
+ try {
127
+ arbiters = loadPolicy(ctx.home, key.macKey, me).arbiters;
128
+ }
129
+ catch (e) {
130
+ // A payee without a policy accepts only escrows without an arbiter; a TAMPERED policy is never read as empty.
131
+ if (!(e instanceof Error && /POLICY_MISSING/.test(e.message)))
132
+ throw e;
133
+ }
134
+ const offer = (await ctx.remote.acceptance(input.milestoneId));
135
+ await verifyEndpoint(ctx.rpc, ctx.net);
136
+ const head = await latestBlock({ rpc: ctx.rpc, net: ctx.net });
137
+ const msg = checkAcceptance(offer, {
138
+ me,
139
+ net: ctx.net,
140
+ chainNowSec: head.timestamp,
141
+ arbiters,
142
+ minAmountWei: input.minAmountXrp ? parseEther(input.minAmountXrp) : null,
143
+ });
144
+ const signature = await key.signTypedData(acceptanceDomain(ctx.net.chainId, ctx.net.address), ACCEPTANCE_TYPES, { ...msg });
145
+ const answer = await ctx.remote.submitAcceptance(offer.offerId, signature);
146
+ new Book(ctx.home, key.macKey).add({
147
+ escrowId: msg.escrowId,
148
+ role: "payee",
149
+ milestoneId: input.milestoneId,
150
+ payer: offer.payer,
151
+ payee: me,
152
+ amountWei: msg.amount.toString(),
153
+ chainId: ctx.net.chainId,
154
+ contract: ctx.net.address,
155
+ at: head.timestamp,
156
+ });
157
+ return {
158
+ accepted: true,
159
+ escrowId: msg.escrowId,
160
+ payer: getAddress(offer.payer),
161
+ amountWei: msg.amount.toString(),
162
+ acceptBy: msg.acceptBy.toString(),
163
+ deadline: msg.deadline.toString(),
164
+ reviewPeriod: msg.reviewPeriod,
165
+ arbiter: msg.arbiter,
166
+ server: { escrowId: answer.escrowId ?? null, acceptBy: answer.acceptBy ?? null },
167
+ note: "Your signature binds you until acceptBy: the payer can fund these exact terms until then, and nothing in cascrow revokes it.",
168
+ };
169
+ }
@@ -0,0 +1,60 @@
1
+ import { PER_ESCROW_ACTIONS } from "./calldata.js";
2
+ import type { SignerAction } from "./calldata.js";
3
+ import { RemoteError } from "./remote.js";
4
+ import type { V3EscrowAction } from "./generated/v3-actions.js";
5
+ import type { SignerContext } from "./context.js";
6
+ export type ActionResult = {
7
+ action: SignerAction;
8
+ escrowId: string;
9
+ txHash: string;
10
+ sent: boolean;
11
+ reported: boolean;
12
+ from: string;
13
+ notes: string[];
14
+ [k: string]: unknown;
15
+ };
16
+ export type FundInput = {
17
+ milestoneId: string;
18
+ maxAmountXrp?: string;
19
+ };
20
+ export declare function fund(ctx: SignerContext, input: FundInput): Promise<ActionResult>;
21
+ export type EvidenceInput = {
22
+ files?: string[];
23
+ texts?: string[];
24
+ github?: {
25
+ commitSha: string;
26
+ diffSha256: string;
27
+ }[];
28
+ };
29
+ /**
30
+ * The manifest hash of evidence the payee holds locally, computed HERE with the
31
+ * one manifest encoding. A submission is what ONE evidence-door request sealed —
32
+ * today one file, one text or one GitHub item — so the hash of several items only
33
+ * matches a submission that sealed exactly those items. It is never trusted on
34
+ * its own: `submitClaim` sends it only after cascrow's prepare confirms it is a
35
+ * sealed submission of the milestone (post-review G2).
36
+ */
37
+ export declare function localProofHash(milestoneId: string, ev: EvidenceInput): string;
38
+ export type EscrowActionInput = {
39
+ escrowId?: string;
40
+ milestoneId?: string;
41
+ /** object: the objection reason; its keccak256 goes on chain. */
42
+ reason?: string;
43
+ /** extendDeadline: unix seconds. */
44
+ newDeadline?: string | number;
45
+ /** submitClaim: the manifestHash the evidence door returned for the sealed submission. */
46
+ proofHash?: string;
47
+ /** submitClaim: evidence to hash locally instead (must equal a sealed submission). */
48
+ evidence?: EvidenceInput;
49
+ };
50
+ /**
51
+ * The fields of a refusal body a recipient can act on, as one line. The same
52
+ * fields every cascrow client appends (post-review G5).
53
+ */
54
+ export declare function refusalDetail(body: Record<string, unknown>): string;
55
+ /** True when prepare's failure is cascrow saying no (a verdict), not cascrow being unavailable. */
56
+ export declare function isPrepareRefusal(e: unknown): e is RemoteError;
57
+ export declare function escrowAction(ctx: SignerContext, action: V3EscrowAction, input: EscrowActionInput): Promise<ActionResult>;
58
+ /** withdraw(own account, native XRP): pays this key's credit to this key. No escrow, no cascrow. */
59
+ export declare function withdraw(ctx: SignerContext): Promise<ActionResult>;
60
+ export { PER_ESCROW_ACTIONS };