@allorc/a423-sdk 0.4.0 → 0.6.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/CANCELLATION.md +70 -0
- package/CHANGELOG.md +17 -0
- package/ESCROW.md +4 -2
- package/INTEGRATION.md +9 -1
- package/OPERATIONS.md +5 -1
- package/README.md +6 -2
- package/SECURITY.md +4 -0
- package/SETTLEMENT.md +142 -0
- package/contracts/A423Escrow.sol +68 -6
- package/contracts/standard-input.json +1 -1
- package/examples/operations-dashboard.html +3 -3
- package/package.json +6 -2
- package/src/cancellation.cjs +74 -0
- package/src/closing.cjs +17 -0
- package/src/escrow/artifact.json +292 -1
- package/src/escrow/cancellation.cjs +34 -0
- package/src/escrow/index.cjs +70 -5
- package/src/escrow/index.d.mts +8 -1
- package/src/escrow/index.d.ts +8 -1
- package/src/escrow/index.mjs +1 -1
- package/src/escrow/negotiation.cjs +13 -0
- package/src/escrow/services.cjs +4 -1
- package/src/operations/application.cjs +8 -3
- package/src/operations/factory.cjs +3 -3
- package/src/operations/index.cjs +18 -7
- package/src/operations/index.d.ts +5 -3
- package/src/operations/index.mjs +1 -1
- package/src/workflow.cjs +79 -9
- package/src/workflow.d.ts +36 -2
- package/src/workflow.mjs +1 -1
package/CANCELLATION.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Cancellation subflow — 0.6.0
|
|
2
|
+
|
|
3
|
+
A job has one parent lifecycle and an optional cancellation subflow. Cancellation can negotiate compensation for completed work and agreed expenses, then close the job. It does not permit continuing work after a partial release.
|
|
4
|
+
|
|
5
|
+
The flow is:
|
|
6
|
+
|
|
7
|
+
1. Publish `propose_settlement`, or alternate `counter_settlement`, with the worker's proposed amount, reason, claims, original deadline and expected revision. The payer receives the remainder. Funds remain in the escrow contract, never the worker's wallet. Work pauses.
|
|
8
|
+
2. Both parties review the exact current proposal and sign the `ClosingSettlement` typed data returned by `settlementConsent`. Signature authorization is bound to chain, contract, agreement, job, revision, amounts, proposal hash and expiry.
|
|
9
|
+
3. Accept using `workflow.acceptCancellation`. The adapter verifies both EOA/ERC-1271 signatures and current on-chain terms/proposal. The workflow atomically saves an immutable JSON instruction and marks the child flow `accepted` before any closing transaction is signed or broadcast. The instruction says `funds_status: "not_executed"`.
|
|
10
|
+
4. Forward the saved instruction. The adapter validates the JSON against the job, encodes `settleByConsent`, applies the signing/broadcast policy and journals the transaction. The contract verifies signatures and distributes the exact split atomically. It does not parse or trust JSON.
|
|
11
|
+
5. After the configured confirmations and matching settlement evidence, save the final cancellation receipt. It records actual worker/payer allocations, zero remaining locked amount, transaction/block evidence and the accepted instruction ID.
|
|
12
|
+
|
|
13
|
+
An acceptance record is an authorization receipt, not proof of payment. Pending, rejected, expired or unavailable execution must not display a completed payment receipt.
|
|
14
|
+
|
|
15
|
+
## Server API
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
const proposed = await workflow.act(id, 'propose_settlement', {
|
|
19
|
+
worker_amount: '30', // raw token units; payer gets total minus 30
|
|
20
|
+
expected_revision: 0,
|
|
21
|
+
expires_at: proposalExpiry, // no later than original resolution deadline
|
|
22
|
+
reason: 'Research completed; remaining work cancelled',
|
|
23
|
+
claims: [{ kind: 'work', description: 'Checked source list', evidence: ['artifact:sources'] }]
|
|
24
|
+
}, { actor: workerId, idempotencyKey: 'cancel-proposal-1' });
|
|
25
|
+
|
|
26
|
+
const job = await workflow.acceptCancellation(id, {
|
|
27
|
+
expected_revision: proposed.negotiation.current.escrow_version,
|
|
28
|
+
proposal_hash: proposed.negotiation.current.proposal_hash,
|
|
29
|
+
payer_signature: payerSignature,
|
|
30
|
+
worker_signature: workerSignature
|
|
31
|
+
}, { actor: payerId, idempotencyKey: 'cancel-accept-1' });
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The default forwards immediately AFTER saving acceptance. To separate acceptance and execution, pass `forward: false`, then:
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
const instruction = await workflow.cancellationInstruction(id, payerId);
|
|
38
|
+
await workflow.act(id, 'execute_cancellation', {
|
|
39
|
+
instruction_id: instruction.id
|
|
40
|
+
}, { actor: payerId, idempotencyKey: 'cancel-execute-1' });
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`createEscrowApplication({forwardCancellations:false,...})` stages HTTP acceptance. An explicitly enabled keeper (`allowExecute:true` / factory `keeperEnabled:true`) forwards accepted instructions. Observe-only reconciliation never creates a new payment transaction.
|
|
44
|
+
|
|
45
|
+
| Route, relative to `/api/a423/jobs/:id` | Meaning |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `GET /subflows` | Child records and active child ID |
|
|
48
|
+
| `GET /cancellation` | Cancellation child or null |
|
|
49
|
+
| `POST /cancellation/accept` | Body: revision/hash and both signatures; save then optionally forward |
|
|
50
|
+
| `POST /cancellation/execute` | Body: `instruction_id`, optionally external `transaction_hash` |
|
|
51
|
+
| `GET /cancellation/instruction` | Accepted instruction; unavailable until acceptance |
|
|
52
|
+
| `GET /cancellation/receipt` | Final receipt; HTTP 409 before confirmation |
|
|
53
|
+
|
|
54
|
+
POST requests require the usual credential and `idempotency-key`. The typed workflow client exposes the corresponding methods. The application handler freshly revalidates final receipt evidence; unavailable/changed evidence returns 503. The lower-level workflow getter validates record integrity only; use `freshCancellationReceipt` with an evidence revalidator for current chain proof.
|
|
55
|
+
|
|
56
|
+
## State and recovery
|
|
57
|
+
|
|
58
|
+
Child statuses are `proposal_pending`, `negotiating`, `acceptance_pending`, `accepted`, `execution_pending`, `execution_failed`, `disputed`, `proposal_rejected`, and `completed`. Its ID and `parent_job_id` link it to the parent job; events retain proposal revisions and receipt/instruction IDs. The job remains `settlement_negotiation` while acceptance/execution is pending. The separate escrow ledger changes only with confirmed chain evidence.
|
|
59
|
+
|
|
60
|
+
Keep the same actor, request and idempotency key on retries. Accepted JSON survives storage restarts. Recovery observes the durable transaction journal; it must not send a duplicate payout. If approval is saved but execution was never started, call acceptance again or use an authorized keeper. A rejected execution preserves the accepted instruction and locked ledger; retry deliberately using a new execution key after resolving its cause. Pending execution must be reconciled/resumed first.
|
|
61
|
+
|
|
62
|
+
The SDK blocks counters after instruction acceptance. External transactions can still change the chain state; forwarding checks fresh state and the contract rejects stale signatures. Original expiry refunds, arbitration and timeout allocations remain available under their original conditions. Those exits create a final receipt with their actual outcome and `instruction_id:null`; `prior_instruction_id` preserves the unexecuted acceptance reference. A failed acceptance or proposal produces no payment receipt.
|
|
63
|
+
|
|
64
|
+
## Compatibility and limits
|
|
65
|
+
|
|
66
|
+
0.6 reuses the 0.5 `A423Escrow` contract. No new deployment is needed for a verified 0.5 deployment. Existing 0.4/older contracts do not support these methods; retain their original adapter until custody is closed. Existing records without subflows remain readable; a cancellation child is added when negotiation starts.
|
|
67
|
+
|
|
68
|
+
The legacy `accept_settlement` action remains available for a direct counterparty transaction or dual-signature relay. It can produce a final receipt without a separately saved accepted instruction. Use the new staged flow when the UI needs acceptance before execution.
|
|
69
|
+
|
|
70
|
+
Amounts use raw integer token units, never floating point. Signatures and claims are public authorization material, not private keys. Persist records securely and avoid exposing participant-specific endpoints without authentication. Pure JSON constructors validate structure/binding; they do not independently prove cryptographic consent or chain finality. The reference adapters perform those checks. No live-chain deployment, external app migration or independent security audit is implied.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
- Add a persisted cancellation child flow with proposal, acceptance, escrow execution and confirmed receipt stages.
|
|
6
|
+
- Verify both wallet signatures before saving the immutable JSON instruction; encode it into the existing `settleByConsent` call.
|
|
7
|
+
- Add typed client/HTTP/chat actions, accepted-instruction and fresh final-receipt routes, keeper forwarding and dashboard controls.
|
|
8
|
+
- Preserve locked balances through rejected/expired execution; retain arbitration and original deadline exits.
|
|
9
|
+
- Reuse the 0.5 contract without Solidity changes. Earlier contracts still require the 0.5 deployment upgrade.
|
|
10
|
+
|
|
11
|
+
## 0.5.0
|
|
12
|
+
|
|
13
|
+
- Add reference-contract closing proposals, counterproposals, exact counterpart acceptance and dual-signature permissionless settlement.
|
|
14
|
+
- Add Negotiating state (8), stale-revision protection, expiry/amount/signature binding, ordinary release blocking and original deadline exits.
|
|
15
|
+
- Support pre-submission compensation disputes; retain the original unsubmitted expiry refund.
|
|
16
|
+
- Track work status, immutable claim/proposal history and a separate escrow ledger.
|
|
17
|
+
- Add typed proposal/consent helpers, chat/HTTP/wallet/dashboard integration, cooperative execution abort and fresh proposal/settlement receipts.
|
|
18
|
+
- Add local-EVM negotiation, replay, fallback and recovery tests. Requires a new contract deployment; no milestone/continuing-work payments or audit claim.
|
|
19
|
+
|
|
3
20
|
## 0.4.0
|
|
4
21
|
|
|
5
22
|
- Add durable single-host SQLite workflow/transaction/nonce/queue/event/spending storage and local-only reset.
|
package/ESCROW.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
# Reference on-chain escrow (0.
|
|
1
|
+
# Reference on-chain escrow (0.5.0)
|
|
2
|
+
|
|
3
|
+
For negotiated cancellation and partial compensation, see [SETTLEMENT.md](./SETTLEMENT.md). The 0.5 contract adds proposal/counter/accept operations and a Negotiating state; a new deployment is required.
|
|
2
4
|
|
|
3
5
|
This opt-in module implements a real ERC-20 escrow path. The generic workflow remains provider-independent. It includes Solidity source, reproducibly compiled ABI/bytecode, an ethers v6 adapter and local-EVM tests. It is experimental and has not received an independent security audit; deploy to a local chain/testnet for evaluation. No contract is deployed automatically and no funds move merely by installing the package.
|
|
4
6
|
|
|
5
7
|
## Installation and deployment
|
|
6
8
|
|
|
7
9
|
```sh
|
|
8
|
-
npm install @allorc/a423-sdk@0.
|
|
10
|
+
npm install @allorc/a423-sdk@0.5.0 ethers@^6.15.0
|
|
9
11
|
```
|
|
10
12
|
|
|
11
13
|
```ts
|
package/INTEGRATION.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
# Cancellation subflows in 0.6
|
|
2
|
+
|
|
3
|
+
Use `workflow.acceptCancellation` or `POST /api/a423/jobs/:id/cancellation/accept` with both wallet signatures. Acceptance is persisted before forwarding; show the subflow status and issue the final receipt only after confirmation. See [CANCELLATION.md](./CANCELLATION.md) for routes, retry behavior and migration. The actual remote application is not modified by installing this package.
|
|
4
|
+
|
|
5
|
+
# Closing settlements in 0.5
|
|
6
|
+
|
|
7
|
+
Read SETTLEMENT.md. Use a new local/testnet reference deployment, preserve older funded contracts with their original adapters, wire proposal/counter/accept actions and the settlement-consent endpoint, and display work status/escrow ledger/negotiation separately. Test partial compensation, stale and expired approvals, arbitration, original deadlines and crash recovery. No real-chain deployment is implicit.
|
|
8
|
+
|
|
1
9
|
# Integration update for 0.4
|
|
2
10
|
|
|
3
11
|
Read OPERATIONS.md before following the older integration checklist below. Persistence, authority credentials, renewal, spending authorization, application projections/chat tools, keeper/resume, execution queue, watcher/indexer, PDF receipts and a local UI shell are now provided as concrete modules. Choose the supplied implementation or adapt deliberately.
|
|
@@ -9,7 +17,7 @@ The remote MVP was not available in the SDK workspace; these utilities do not im
|
|
|
9
17
|
Install after the maintainer publishes:
|
|
10
18
|
|
|
11
19
|
```sh
|
|
12
|
-
npm install @allorc/a423-sdk@0.
|
|
20
|
+
npm install @allorc/a423-sdk@0.6.0
|
|
13
21
|
```
|
|
14
22
|
|
|
15
23
|
For testing before publication, install the maintainer's `npm pack` tarball instead. Do not silently install 0.1 and assume the workflow entry point exists.
|
package/OPERATIONS.md
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
# A423
|
|
1
|
+
# A423 operational integration
|
|
2
|
+
|
|
3
|
+
For the 0.6 accepted cancellation JSON → escrow execution → confirmed receipt subflow, see [CANCELLATION.md](./CANCELLATION.md). The 0.5 contract is unchanged; an accepted instruction is not proof of payment.
|
|
4
|
+
|
|
5
|
+
Version 0.5 adds closing negotiation, a paused-work state, a separate escrow ledger and proposal history. See SETTLEMENT.md before deploying the new contract. Execution callbacks receive an AbortSignal and must honor it for cooperative cancellation.
|
|
2
6
|
|
|
3
7
|
This guide covers concrete SDK modules and their deployment boundaries. The remote MVP's source was unavailable during this SDK change, so its chat tools, dashboard, environment, reset route and testnet deployment have not been modified. Use the supplied bridge in that repository, then run the actual app's integration tests. Existing contract-level constraints remain explicit.
|
|
4
8
|
|
package/README.md
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
# A423 SDK — 0.
|
|
1
|
+
# A423 SDK — 0.6.0
|
|
2
|
+
|
|
3
|
+
Version 0.6 adds a durable **cancellation subflow**: accepted signed JSON → escrow execution → confirmed cancellation receipt. See [CANCELLATION.md](./CANCELLATION.md). Acceptance records never claim funds have moved. It reuses the 0.5 reference contract.
|
|
4
|
+
|
|
5
|
+
Version 0.5 adds **negotiated closing settlements**: partial-work/expense claims, counterproposals, exact acceptance, on-chain release blocking during negotiation, arbitration and unchanged deadline fallbacks. See [SETTLEMENT.md](./SETTLEMENT.md) for the complete flow, signatures, ledger and upgrade requirements. It closes the job; it does not add milestone payments.
|
|
2
6
|
|
|
3
7
|
Experimental Node.js/Next.js middleware for agent tasks, agreements and escrow. Version 0.4 adds working persistence, authority credentials and operational services around the configurable workflow and reference contract. Installing the SDK never publishes endpoints or deploys a contract automatically.
|
|
4
8
|
|
|
5
9
|
```sh
|
|
6
|
-
npm install @allorc/a423-sdk@0.
|
|
10
|
+
npm install @allorc/a423-sdk@0.6.0 ethers@^6.15.0 pdf-lib@^1.17.1
|
|
7
11
|
```
|
|
8
12
|
|
|
9
13
|
Node.js 22.13+ is required for the SQLite module. Node 24 is the tested runtime. `ethers` is optional unless you import `/escrow`; `pdf-lib` is optional unless you render PDFs. Core/workflow remain framework-independent.
|
package/SECURITY.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Security
|
|
2
2
|
|
|
3
|
+
For the 0.6 accepted cancellation JSON → escrow execution → confirmed receipt subflow, see [CANCELLATION.md](./CANCELLATION.md). The 0.5 contract is unchanged; an accepted instruction is not proof of payment.
|
|
4
|
+
|
|
3
5
|
Experimental SDK and independently unaudited reference escrow. Use local/testnet evaluation until the deployment and operational model receive independent review.
|
|
4
6
|
|
|
5
7
|
Reference-contract custody is distinct from the legacy root API, which has no funded escrow. Authority credentials identify keys approved by your operator registry; they are not proof of legal-company status. Administrative enrollment/revocation capabilities must not be exposed to untrusted callers. Use explicit trust roots, TLS, short credential lifetimes, revocation checks and rate-limited proof endpoints.
|
|
@@ -13,3 +15,5 @@ The reference token must support exact transfers. Unsupported fee/rebase/blackli
|
|
|
13
15
|
Reorg watchers quarantine stale/unavailable evidence; they cannot guarantee finality or undo already executed external work. Continuous operation, monitoring, deliberate recovery and chain availability are still necessary. Do not clear holds without current matching evidence.
|
|
14
16
|
|
|
15
17
|
Report vulnerabilities privately through the repository's configured security reporting channel. Do not post credentials, customer records or live exploit details publicly. No completed independent audit is claimed.
|
|
18
|
+
|
|
19
|
+
Closing settlements in 0.5 require a new deployment and exact current proposal acceptance. Publishing freezes ordinary release, but cannot reverse a release already mined. Proposals never extend original refund/resolution deadlines. Unsubmitted partial-work claims do not remove the original expiry refund right; arbitrary expenses are not automatically owed. First valid closing settlement/refund/arbitration/timeout wins; no later operation can settle the same job again. See SETTLEMENT.md.
|
package/SETTLEMENT.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Negotiated closing settlements — 0.5.0
|
|
2
|
+
|
|
3
|
+
For the 0.6 accepted cancellation JSON → escrow execution → confirmed receipt subflow, see [CANCELLATION.md](./CANCELLATION.md). The 0.5 contract is unchanged; an accepted instruction is not proof of payment.
|
|
4
|
+
|
|
5
|
+
This release adds a closing negotiation for cancellation and partial-work compensation. A party proposes a worker payment, the other can counter or accept, and the contract pays the agreed amount and refunds the remainder atomically. It closes the whole job. **It does not introduce partial releases while continuing work or milestone payments.**
|
|
6
|
+
|
|
7
|
+
## Separate facts
|
|
8
|
+
|
|
9
|
+
- `job.state` drives allowed workflow operations. Published negotiation uses `settlement_negotiation`.
|
|
10
|
+
- `job.work_status` records not_started/running/submitted/verified/paused/completed/stopped.
|
|
11
|
+
- `job.escrow_ledger` records funded amount, worker paid, payer refunded, locked amount and proposal version, all as raw units except version.
|
|
12
|
+
- `job.negotiation` records origin state, current proposal, complete proposal history, status and closing observation.
|
|
13
|
+
|
|
14
|
+
No money leaves custody when a proposal is made or countered. Reported effort, token consumption and expenses are claims, not automatically payable debts. Both parties decide the closing allocation, or the agreed arbitrator/deadline rules decide it.
|
|
15
|
+
|
|
16
|
+
## Lifecycle
|
|
17
|
+
|
|
18
|
+
| Situation | Action | Result |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| Before funding | Existing `cancel` | No token refund needed |
|
|
21
|
+
| Funded/running/submitted/verified | `propose_settlement` | Publish allocation commitment; work pauses and contract enters Negotiating |
|
|
22
|
+
| Open negotiation | `counter_settlement` | Other party publishes next revision; previous proposal/signatures become stale |
|
|
23
|
+
| Open negotiation | `accept_settlement` | Counterparty accepts exact current revision/hash; worker paid, payer refunded, job settled |
|
|
24
|
+
| Open negotiation | `dispute` | Escalate to arbitrator; ordinary mutual acceptance is no longer available |
|
|
25
|
+
| Disputed | `resolve` | Arbitrator splits and closes escrow under original deadline |
|
|
26
|
+
| Unsubmitted job, including negotiation/dispute | `refund` / `timeout` after original work deadline | Full payer refund remains available |
|
|
27
|
+
| Submitted job in negotiation/dispute | `timeout` after original resolution deadline | Original signed timeout allocation applies |
|
|
28
|
+
| Already paid/refunded/split | Any new proposal/payment attempt | Rejected; settled money cannot be recovered by this protocol |
|
|
29
|
+
|
|
30
|
+
Negotiation is reusable across the eligible work stages, but remains state-aware. Work must stop while it is pending. Once arbitration is engaged, parties cannot unilaterally reopen negotiations and obstruct the arbitrator. Proposed payment never exceeds the locked balance. There is one closing settlement, not a remaining-work agreement.
|
|
31
|
+
|
|
32
|
+
## Example: request 30, agree on 20 out of 100
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
const offered = await client.act(jobId, 'propose_settlement', {
|
|
36
|
+
expected_revision: 0,
|
|
37
|
+
worker_amount: '30',
|
|
38
|
+
expires_at: proposalExpiryUnixSeconds,
|
|
39
|
+
reason: 'Research complete; analysis is unfinished. I propose closing the job.',
|
|
40
|
+
claims: [
|
|
41
|
+
{kind: 'work', description: 'Collected and checked sources', amount: '20', evidence: ['artifact:source-list']},
|
|
42
|
+
{kind: 'expense', description: 'Agreed research cost', amount: '10', evidence: ['receipt:research-cost']},
|
|
43
|
+
],
|
|
44
|
+
}, {idempotencyKey: 'closing_offer_1'});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The payer uses its own authenticated client:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
const counter = await payerClient.act(jobId, 'counter_settlement', {
|
|
51
|
+
expected_revision: offered.negotiation!.current.escrow_version,
|
|
52
|
+
worker_amount: '20',
|
|
53
|
+
expires_at: proposalExpiryUnixSeconds,
|
|
54
|
+
reason: 'The source list is useful; I propose 20 for the completed portion.',
|
|
55
|
+
claims: [],
|
|
56
|
+
}, {idempotencyKey: 'closing_counter_1'});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The worker accepts the exact counter:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
await workerClient.act(jobId, 'accept_settlement', {
|
|
63
|
+
expected_revision: counter.negotiation!.current.escrow_version,
|
|
64
|
+
proposal_hash: counter.negotiation!.current.proposal_hash,
|
|
65
|
+
}, {idempotencyKey: 'closing_accept_1'});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
After confirmation: work stopped, workflow settled, funded 100, worker paid 20, payer refunded 80, locked 0. A payment transaction pending confirmation does not establish these final amounts. Raw amount `100` is not necessarily 100 displayed tokens; apply the configured token decimals only for presentation.
|
|
69
|
+
|
|
70
|
+
Use the same action/key to progress a pending transaction; `workflow.reconcile` observes known transactions and `workflow.resume` authorizes continued execution. A stale, expired, unauthorized or failed proposal does not move tokens. Check operation results: a definitively rejected action leaves the last confirmed job state unchanged. Use a new action key only for a deliberate new request after rejection.
|
|
71
|
+
|
|
72
|
+
## Exact proposal recipe
|
|
73
|
+
|
|
74
|
+
`settlementProposal(job, input)` derives the payer refund and returns a canonical `a423.closing.v1` record/hash. Input fields:
|
|
75
|
+
|
|
76
|
+
- `expected_revision`: current on-chain proposal revision; 0 for the first proposal.
|
|
77
|
+
- `worker_amount`: nonnegative raw-token integer string. `payer_amount` is optional but must equal funded amount minus worker amount.
|
|
78
|
+
- `expires_at`: absolute Unix seconds, no later than original resolution deadline. The contract also requires it to be in the future when published.
|
|
79
|
+
- `reason`: nonempty explanation, up to 4000 characters.
|
|
80
|
+
- `claims`: optional list, up to 64 work/expense/other claims, each with description, optional raw amount and up to 32 evidence references.
|
|
81
|
+
|
|
82
|
+
The canonical commitment includes job ID, original agreement hash, next escrow/proposal version, both allocations, expiry, explanation, claims and `closes_job: true`. The proposal's claim amounts do not automatically sum to the payout; they support the negotiation and can be disputed. Evidence references are not fetched or certified by the helper. Do not place secrets or private customer information in proposal data.
|
|
83
|
+
|
|
84
|
+
Publish/counter goes on-chain with expected revision, worker amount, proposal hash and expiry. Explanations/claims remain in the app's durable record and are bound by the hash. A transaction per published revision is deliberate: it freezes ordinary release and invalidates old revisions on-chain. Informal chat discussion can remain off-chain until a party actually publishes a proposal.
|
|
85
|
+
|
|
86
|
+
Counterproposals must alternate parties. A proposer cannot self-accept through `acceptSettlement`, or unilaterally renew its own current proposal. A published offer is actionable until it expires, is superseded by the counterparty's proposal, enters dispute, or the job is otherwise settled. Choose expiry carefully. Expiration does not resume work or release money automatically; the other party may counter, either party may escalate, or the original fallback remains available.
|
|
87
|
+
|
|
88
|
+
## Signatures and relayers
|
|
89
|
+
|
|
90
|
+
The counterparty's direct `acceptSettlement(jobId, revision, proposalHash)` transaction is on-chain acceptance of the proposer's published terms. Alternatively, obtain both signatures:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { settlementConsent } from '@allorc/a423-sdk/escrow';
|
|
94
|
+
const typed = settlementConsent(job, escrowConfiguration);
|
|
95
|
+
const payerSignature = await payerSigner.signTypedData(typed.domain, typed.types, typed.value);
|
|
96
|
+
const workerSignature = await workerSigner.signTypedData(typed.domain, typed.types, typed.value);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Then a participant may relay through the adapter:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
await client.act(jobId, 'accept_settlement', {
|
|
103
|
+
expected_revision: typed.value.escrowVersion,
|
|
104
|
+
proposal_hash: typed.value.proposalHash,
|
|
105
|
+
payer_signature: payerSignature,
|
|
106
|
+
worker_signature: workerSignature,
|
|
107
|
+
}, {idempotencyKey: 'relay_closing_1'});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`settleByConsent` is permissionless on-chain: any relayer can submit both approvals. It verifies the EIP-712 domain (chain and contract), job, agreement hash, exact current proposal revision/hash, both allocations and expiry. EOA and ERC-1271 signatures are supported. Old revisions, altered allocations, wrong chains/contracts, expired signatures and already-settled jobs are rejected. A proposal commitment without these approvals is not a payout authorization.
|
|
111
|
+
|
|
112
|
+
`GET /api/a423-settlement-consent/:id` returns typed data only to the payer/worker. It never generates private keys or signs for them. The reference dashboard can sign the current closing terms with an EIP-1193 EOA wallet and display/share the public signature. For ERC-1271, obtain the account's own signature format through its wallet. A proposer relaying through the SDK must supply both signatures; it cannot self-accept merely by submitting its own transaction. Externally submitted settlement transactions can be adopted with verified `transaction_hash` alongside the expected revision/hash.
|
|
113
|
+
|
|
114
|
+
## Contract enforcement and deadline behaviour
|
|
115
|
+
|
|
116
|
+
New reference contract state `Negotiating` is enum value 8; previous state values 0..7 remain unchanged. `proposeSettlement` accepts funded/submitted/verified/current-negotiating jobs. It sets Negotiating before acceptance is possible. `submit`, `verify` and ordinary `release` are blocked in that state. New event `SettlementProposed` records revision, proposer, allocation, hash and expiry. Acceptance emits `SettlementAccepted` and `Settled` with reason **4 (mutual closing settlement)**. Transfers use the same exact-token/nonreentrant settlement path.
|
|
117
|
+
|
|
118
|
+
Crucially, proposing does not extend `workDeadline`, `reviewDeadline` or `resolutionDeadline`:
|
|
119
|
+
|
|
120
|
+
- **No committed submission:** after the original work deadline, anyone can still trigger full payer refund—even if a partial-work claim or an unexpired proposal exists. A claim alone does not remove that agreed refund right. Agree or seek arbitration in time; do not imply unpaid effort itself guarantees compensation.
|
|
121
|
+
- **Committed submission:** original refund restrictions remain. After the original resolution deadline, anyone may trigger the original `timeoutWorkerBps` split, not the last unaccepted offer.
|
|
122
|
+
- **Arbitration:** an arbitrator may resolve an eligible dispute before the original resolution deadline, including pre-submission compensation disputes. That competes with any independently eligible original fallback. The first valid settlement mined closes the job.
|
|
123
|
+
|
|
124
|
+
If release was mined before cancellation was published, cancellation fails; there is no guaranteed challenge window before verified release. Likewise, acceptance and an eligible timeout/refund can race: whichever valid transaction executes first settles once, and later attempts fail. A chat message or a pending transaction does not establish an on-chain lock on release.
|
|
125
|
+
|
|
126
|
+
## Worker and UI integration
|
|
127
|
+
|
|
128
|
+
The SDK marks `work_status: paused` when a proposal is confirmed and blocks queued work through `assertSafe`. Pending proposal/dispute intent also blocks fresh execution immediately. The execution runner supplies `execute({job,idempotencyKey,signal})` and aborts that signal when it observes a pause or lost lease. Your AI/tools must honor cancellation signals to stop ongoing computation. External processes cannot be physically stopped by a Solidity contract. A checkpointed partial artifact is retained, but cannot be automatically submitted while negotiation blocks the job.
|
|
129
|
+
|
|
130
|
+
Chat tools now expose `request_cancellation`, `propose_settlement`, `counter_settlement`, `accept_settlement`; cancellation maps to a closing proposal after funding. Ordinary `cancel_job` remains pre-funding. The application factory wires the new adapters and settlement-consent endpoint. Existing generic workflow users must use the updated `escrowTransitions` and reference adapters deliberately.
|
|
131
|
+
|
|
132
|
+
The dashboard shell shows work status, negotiation/history and escrow ledger. It provides proposal/counter/accept actions, raw allocation inputs, claim data and a wallet signing control. Display the two sides distinctly:
|
|
133
|
+
|
|
134
|
+
> Cancellation / partial-work settlement proposed. Work paused. 100 remains locked. Proposed worker payment 30; payer refund 70. Awaiting the counterparty.
|
|
135
|
+
|
|
136
|
+
Display completed allocations only after matching confirmed `SettlementAccepted`/`Settled` evidence. `freshReceipts` checks claim commitments and revalidates proposal/settlement events. PDFs/JSON include history and the ledger, without claiming expenses were independently proven.
|
|
137
|
+
|
|
138
|
+
## Upgrade and limits
|
|
139
|
+
|
|
140
|
+
Version **0.5.0 requires a newly deployed reference contract** for this functionality. Existing 0.4 and earlier contracts are immutable and cannot acquire these methods by changing the npm version or RPC URL. Keep their adapter/ABI pinned for existing custody; do not relabel their records or move their tokens without the original release/refund process. Node SDK package files include the new compiled artifact and standard compiler input. No public deployment is performed automatically.
|
|
141
|
+
|
|
142
|
+
The upgrade does not add milestones, payment while continuing work, scope revisions, resubmission after a commitment, a quality/cost oracle, token recovery, admin withdrawals, pause/upgrades or support for broken/fee/rebasing tokens. Verifier/arbitrator trust and reviewed-token assumptions remain. Deployment and funding on a public chain spend real assets; the new contract remains experimental and unaudited.
|
package/contracts/A423Escrow.sol
CHANGED
|
@@ -12,7 +12,7 @@ import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
|
|
|
12
12
|
/// No admin withdrawals/upgrades. Arbitration and timeout allocation are agreed before funding.
|
|
13
13
|
contract A423Escrow is ReentrancyGuard, EIP712 {
|
|
14
14
|
using SafeERC20 for IERC20;
|
|
15
|
-
enum State { None, Funded, Submitted, Verified, Disputed, Paid, Refunded, Split }
|
|
15
|
+
enum State { None, Funded, Submitted, Verified, Disputed, Paid, Refunded, Split, Negotiating }
|
|
16
16
|
struct Terms {
|
|
17
17
|
bytes32 jobId;
|
|
18
18
|
address payer;
|
|
@@ -26,10 +26,29 @@ contract A423Escrow is ReentrancyGuard, EIP712 {
|
|
|
26
26
|
uint16 timeoutWorkerBps;
|
|
27
27
|
bytes32 agreementHash;
|
|
28
28
|
}
|
|
29
|
-
struct
|
|
29
|
+
struct Proposal {
|
|
30
|
+
uint64 revision;
|
|
31
|
+
uint64 expiresAt;
|
|
32
|
+
address proposer;
|
|
33
|
+
uint256 workerAmount;
|
|
34
|
+
bytes32 proposalHash;
|
|
35
|
+
}
|
|
36
|
+
struct ClosingSettlement {
|
|
37
|
+
bytes32 jobId;
|
|
38
|
+
bytes32 agreementHash;
|
|
39
|
+
uint64 escrowVersion;
|
|
40
|
+
uint256 workerAmount;
|
|
41
|
+
uint256 payerAmount;
|
|
42
|
+
bytes32 proposalHash;
|
|
43
|
+
uint64 expiresAt;
|
|
44
|
+
}
|
|
45
|
+
struct Job { Terms terms; State state; bytes32 resultHash; uint256 workerPaid; Proposal proposal; }
|
|
30
46
|
IERC20 public immutable token;
|
|
31
47
|
mapping(bytes32 => Job) private jobs;
|
|
32
48
|
bytes32 public constant TERMS_TYPEHASH = keccak256("Terms(bytes32 jobId,address payer,address worker,address verifier,address arbitrator,uint256 amount,uint64 workDeadline,uint64 reviewDeadline,uint64 resolutionDeadline,uint16 timeoutWorkerBps,bytes32 agreementHash)");
|
|
49
|
+
bytes32 public constant SETTLEMENT_TYPEHASH = keccak256("ClosingSettlement(bytes32 jobId,bytes32 agreementHash,uint64 escrowVersion,uint256 workerAmount,uint256 payerAmount,bytes32 proposalHash,uint64 expiresAt)");
|
|
50
|
+
event SettlementProposed(bytes32 indexed jobId, uint64 revision, address indexed proposer, uint256 workerAmount, uint256 payerAmount, bytes32 proposalHash, uint64 expiresAt);
|
|
51
|
+
event SettlementAccepted(bytes32 indexed jobId, uint64 revision, bytes32 proposalHash);
|
|
33
52
|
event Funded(bytes32 indexed jobId, bytes32 indexed agreementHash, address indexed payer, address worker, uint256 amount);
|
|
34
53
|
event Submitted(bytes32 indexed jobId, bytes32 resultHash);
|
|
35
54
|
event Verified(bytes32 indexed jobId, bytes32 resultHash);
|
|
@@ -59,7 +78,7 @@ contract A423Escrow is ReentrancyGuard, EIP712 {
|
|
|
59
78
|
if (t.worker.code.length == 0) { valid = ECDSA.recover(termsDigest(t), workerSignature) == t.worker; }
|
|
60
79
|
else { try IERC1271(t.worker).isValidSignature(termsDigest(t), workerSignature) returns (bytes4 magic) { valid = magic == IERC1271.isValidSignature.selector; } catch { valid = false; } }
|
|
61
80
|
if (!valid) revert Unauthorized();
|
|
62
|
-
jobs[t.jobId] = Job(t, State.Funded, bytes32(0), 0);
|
|
81
|
+
jobs[t.jobId] = Job(t, State.Funded, bytes32(0), 0, Proposal(0, 0, address(0), 0, bytes32(0)));
|
|
63
82
|
uint256 beforeBalance = token.balanceOf(address(this));
|
|
64
83
|
token.safeTransferFrom(t.payer, address(this), t.amount);
|
|
65
84
|
if (token.balanceOf(address(this)) != beforeBalance + t.amount) revert TokenAmountMismatch();
|
|
@@ -91,14 +110,14 @@ contract A423Escrow is ReentrancyGuard, EIP712 {
|
|
|
91
110
|
}
|
|
92
111
|
function refundExpired(bytes32 id) external nonReentrant {
|
|
93
112
|
Job storage j = jobs[id];
|
|
94
|
-
if (j.state != State.Funded) revert WrongState();
|
|
113
|
+
if (j.state != State.Funded && !((j.state == State.Negotiating || j.state == State.Disputed) && j.resultHash == bytes32(0))) revert WrongState();
|
|
95
114
|
if (block.timestamp <= j.terms.workDeadline) revert Deadline();
|
|
96
115
|
_settle(id, j, 0, 1);
|
|
97
116
|
}
|
|
98
117
|
function dispute(bytes32 id, bytes32 reasonHash) external {
|
|
99
118
|
Job storage j = jobs[id];
|
|
100
119
|
if (msg.sender != j.terms.payer && msg.sender != j.terms.worker && msg.sender != j.terms.verifier) revert Unauthorized();
|
|
101
|
-
if (j.state != State.Submitted && j.state != State.Verified) revert WrongState();
|
|
120
|
+
if (j.state != State.Submitted && j.state != State.Verified && j.state != State.Negotiating && j.state != State.Funded) revert WrongState();
|
|
102
121
|
if (block.timestamp > j.terms.resolutionDeadline) revert Deadline();
|
|
103
122
|
j.state = State.Disputed; emit Disputed(id, reasonHash);
|
|
104
123
|
}
|
|
@@ -112,12 +131,55 @@ contract A423Escrow is ReentrancyGuard, EIP712 {
|
|
|
112
131
|
/// Permissionless liveness fallback; requires an actual transaction after the deadline.
|
|
113
132
|
function timeout(bytes32 id) external nonReentrant {
|
|
114
133
|
Job storage j = jobs[id];
|
|
115
|
-
if (j.state != State.Submitted && j.state != State.Verified && j.state != State.Disputed) revert WrongState();
|
|
134
|
+
if ((j.state != State.Submitted && j.state != State.Verified && j.state != State.Disputed && j.state != State.Negotiating) || j.resultHash == bytes32(0)) revert WrongState();
|
|
116
135
|
if (block.timestamp <= j.terms.resolutionDeadline) revert Deadline();
|
|
117
136
|
// Quotient/remainder avoids overflowing amount * bps.
|
|
118
137
|
uint256 amount = (j.terms.amount / 10000) * j.terms.timeoutWorkerBps + ((j.terms.amount % 10000) * j.terms.timeoutWorkerBps) / 10000;
|
|
119
138
|
_settle(id, j, amount, 3);
|
|
120
139
|
}
|
|
140
|
+
/// A published proposal freezes submit/verify/release, but never extends a deadline.
|
|
141
|
+
/// Off-chain claim/evidence bytes are committed through proposalHash.
|
|
142
|
+
function proposeSettlement(bytes32 id, uint64 expectedRevision, uint256 workerAmount, bytes32 proposalHash, uint64 expiresAt) external {
|
|
143
|
+
Job storage j = jobs[id];
|
|
144
|
+
if (msg.sender != j.terms.payer && msg.sender != j.terms.worker) revert Unauthorized();
|
|
145
|
+
if (j.state != State.Funded && j.state != State.Submitted && j.state != State.Verified && j.state != State.Negotiating) revert WrongState();
|
|
146
|
+
if (j.proposal.revision != expectedRevision || workerAmount > j.terms.amount || proposalHash == bytes32(0)) revert InvalidTerms();
|
|
147
|
+
if (j.state == State.Negotiating && j.proposal.proposer == msg.sender) revert Unauthorized();
|
|
148
|
+
if (expiresAt <= block.timestamp || expiresAt > j.terms.resolutionDeadline || block.timestamp > j.terms.resolutionDeadline) revert Deadline();
|
|
149
|
+
j.proposal = Proposal(expectedRevision + 1, expiresAt, msg.sender, workerAmount, proposalHash);
|
|
150
|
+
j.state = State.Negotiating;
|
|
151
|
+
emit SettlementProposed(id, j.proposal.revision, msg.sender, workerAmount, j.terms.amount - workerAmount, proposalHash, expiresAt);
|
|
152
|
+
}
|
|
153
|
+
/// The counterparty's transaction accepts the exact currently published closing allocation.
|
|
154
|
+
function acceptSettlement(bytes32 id, uint64 revision, bytes32 proposalHash) external nonReentrant {
|
|
155
|
+
Job storage j = jobs[id];
|
|
156
|
+
if ((msg.sender != j.terms.payer && msg.sender != j.terms.worker) || msg.sender == j.proposal.proposer) revert Unauthorized();
|
|
157
|
+
_checkProposal(j, revision, proposalHash);
|
|
158
|
+
emit SettlementAccepted(id, revision, proposalHash);
|
|
159
|
+
_settle(id, j, j.proposal.workerAmount, 4);
|
|
160
|
+
}
|
|
161
|
+
function settlementDigest(ClosingSettlement calldata s) public view returns (bytes32) {
|
|
162
|
+
return _hashTypedDataV4(keccak256(abi.encode(SETTLEMENT_TYPEHASH, s)));
|
|
163
|
+
}
|
|
164
|
+
/// Anyone can relay both parties' exact, current, chain/domain-bound signatures.
|
|
165
|
+
function settleByConsent(ClosingSettlement calldata s, bytes calldata payerSignature, bytes calldata workerSignature) external nonReentrant {
|
|
166
|
+
Job storage j = jobs[s.jobId];
|
|
167
|
+
_checkProposal(j, s.escrowVersion, s.proposalHash);
|
|
168
|
+
if (s.agreementHash != j.terms.agreementHash || s.workerAmount != j.proposal.workerAmount || s.payerAmount != j.terms.amount - s.workerAmount || s.expiresAt != j.proposal.expiresAt) revert InvalidTerms();
|
|
169
|
+
bytes32 digest = settlementDigest(s);
|
|
170
|
+
if (!_validSignature(j.terms.payer, digest, payerSignature) || !_validSignature(j.terms.worker, digest, workerSignature)) revert Unauthorized();
|
|
171
|
+
emit SettlementAccepted(s.jobId, s.escrowVersion, s.proposalHash);
|
|
172
|
+
_settle(s.jobId, j, s.workerAmount, 4);
|
|
173
|
+
}
|
|
174
|
+
function _validSignature(address account, bytes32 digest, bytes calldata signature) private view returns (bool) {
|
|
175
|
+
if (account.code.length == 0) return ECDSA.recover(digest, signature) == account;
|
|
176
|
+
try IERC1271(account).isValidSignature(digest, signature) returns (bytes4 magic) { return magic == IERC1271.isValidSignature.selector; } catch { return false; }
|
|
177
|
+
}
|
|
178
|
+
function _checkProposal(Job storage j, uint64 revision, bytes32 proposalHash) private view {
|
|
179
|
+
if (j.state != State.Negotiating) revert WrongState();
|
|
180
|
+
if (revision != j.proposal.revision || proposalHash != j.proposal.proposalHash) revert InvalidTerms();
|
|
181
|
+
if (block.timestamp > j.proposal.expiresAt || block.timestamp > j.terms.resolutionDeadline) revert Deadline();
|
|
182
|
+
}
|
|
121
183
|
function _settle(bytes32 id, Job storage j, uint256 workerAmount, uint8 reason) private {
|
|
122
184
|
uint256 payerAmount = j.terms.amount - workerAmount;
|
|
123
185
|
j.state = workerAmount == 0 ? State.Refunded : payerAmount == 0 ? State.Paid : State.Split;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"language": "Solidity",
|
|
3
3
|
"sources": {
|
|
4
4
|
"A423Escrow.sol": {
|
|
5
|
-
"content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.24;\n\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\nimport {ReentrancyGuard} from \"@openzeppelin/contracts/utils/ReentrancyGuard.sol\";\nimport {EIP712} from \"@openzeppelin/contracts/utils/cryptography/EIP712.sol\";\nimport {ECDSA} from \"@openzeppelin/contracts/utils/cryptography/ECDSA.sol\";\nimport {IERC1271} from \"@openzeppelin/contracts/interfaces/IERC1271.sol\";\n\n/// @notice Experimental reference escrow for standard, non-rebasing ERC20 tokens.\n/// No admin withdrawals/upgrades. Arbitration and timeout allocation are agreed before funding.\ncontract A423Escrow is ReentrancyGuard, EIP712 {\n using SafeERC20 for IERC20;\n enum State { None, Funded, Submitted, Verified, Disputed, Paid, Refunded, Split }\n struct Terms {\n bytes32 jobId;\n address payer;\n address worker;\n address verifier;\n address arbitrator;\n uint256 amount;\n uint64 workDeadline;\n uint64 reviewDeadline;\n uint64 resolutionDeadline;\n uint16 timeoutWorkerBps;\n bytes32 agreementHash;\n }\n struct Job { Terms terms; State state; bytes32 resultHash; uint256 workerPaid; }\n IERC20 public immutable token;\n mapping(bytes32 => Job) private jobs;\n bytes32 public constant TERMS_TYPEHASH = keccak256(\"Terms(bytes32 jobId,address payer,address worker,address verifier,address arbitrator,uint256 amount,uint64 workDeadline,uint64 reviewDeadline,uint64 resolutionDeadline,uint16 timeoutWorkerBps,bytes32 agreementHash)\");\n event Funded(bytes32 indexed jobId, bytes32 indexed agreementHash, address indexed payer, address worker, uint256 amount);\n event Submitted(bytes32 indexed jobId, bytes32 resultHash);\n event Verified(bytes32 indexed jobId, bytes32 resultHash);\n event Disputed(bytes32 indexed jobId, bytes32 reasonHash);\n event Settled(bytes32 indexed jobId, uint256 workerAmount, uint256 payerAmount, uint8 reason);\n error InvalidTerms();\n error WrongState();\n error Unauthorized();\n error Deadline();\n error TokenAmountMismatch();\n\n constructor(address token_) EIP712(\"A423Escrow\", \"1\") {\n if (token_.code.length == 0) revert InvalidTerms();\n token = IERC20(token_);\n }\n function termsDigest(Terms calldata t) public view returns (bytes32) {\n return _hashTypedDataV4(keccak256(abi.encode(TERMS_TYPEHASH, t)));\n }\n function getJob(bytes32 id) external view returns (Job memory) { return jobs[id]; }\n /// Payer transaction plus worker EIP712 signature establish bilateral on-chain consent.\n function fund(Terms calldata t, bytes calldata workerSignature) external nonReentrant {\n if (msg.sender != t.payer) revert Unauthorized();\n if (jobs[t.jobId].state != State.None) revert WrongState();\n if (t.jobId == bytes32(0) || t.agreementHash == bytes32(0) || t.amount == 0 || t.worker == address(0) || t.worker == t.payer || t.verifier == address(0) || t.arbitrator == address(0) || t.arbitrator == t.payer || t.arbitrator == t.worker || t.timeoutWorkerBps > 10000) revert InvalidTerms();\n if (t.workDeadline <= block.timestamp || t.reviewDeadline <= t.workDeadline || t.resolutionDeadline <= t.reviewDeadline) revert InvalidTerms();\n bool valid;\n if (t.worker.code.length == 0) { valid = ECDSA.recover(termsDigest(t), workerSignature) == t.worker; }\n else { try IERC1271(t.worker).isValidSignature(termsDigest(t), workerSignature) returns (bytes4 magic) { valid = magic == IERC1271.isValidSignature.selector; } catch { valid = false; } }\n if (!valid) revert Unauthorized();\n jobs[t.jobId] = Job(t, State.Funded, bytes32(0), 0);\n uint256 beforeBalance = token.balanceOf(address(this));\n token.safeTransferFrom(t.payer, address(this), t.amount);\n if (token.balanceOf(address(this)) != beforeBalance + t.amount) revert TokenAmountMismatch();\n emit Funded(t.jobId, t.agreementHash, t.payer, t.worker, t.amount);\n }\n function submit(bytes32 id, bytes32 resultHash) external {\n Job storage j = jobs[id];\n if (msg.sender != j.terms.worker) revert Unauthorized();\n if (j.state != State.Funded) revert WrongState();\n if (block.timestamp > j.terms.workDeadline) revert Deadline();\n if (resultHash == bytes32(0)) revert InvalidTerms();\n j.resultHash = resultHash; j.state = State.Submitted;\n emit Submitted(id, resultHash);\n }\n function verify(bytes32 id, bytes32 resultHash) external {\n Job storage j = jobs[id];\n if (msg.sender != j.terms.verifier) revert Unauthorized();\n if (j.state != State.Submitted) revert WrongState();\n if (block.timestamp > j.terms.reviewDeadline) revert Deadline();\n if (resultHash != j.resultHash) revert InvalidTerms();\n j.state = State.Verified; emit Verified(id, resultHash);\n }\n function release(bytes32 id) external nonReentrant {\n Job storage j = jobs[id];\n if (msg.sender != j.terms.verifier) revert Unauthorized();\n if (j.state != State.Verified) revert WrongState();\n if (block.timestamp > j.terms.resolutionDeadline) revert Deadline();\n _settle(id, j, j.terms.amount, 0);\n }\n function refundExpired(bytes32 id) external nonReentrant {\n Job storage j = jobs[id];\n if (j.state != State.Funded) revert WrongState();\n if (block.timestamp <= j.terms.workDeadline) revert Deadline();\n _settle(id, j, 0, 1);\n }\n function dispute(bytes32 id, bytes32 reasonHash) external {\n Job storage j = jobs[id];\n if (msg.sender != j.terms.payer && msg.sender != j.terms.worker && msg.sender != j.terms.verifier) revert Unauthorized();\n if (j.state != State.Submitted && j.state != State.Verified) revert WrongState();\n if (block.timestamp > j.terms.resolutionDeadline) revert Deadline();\n j.state = State.Disputed; emit Disputed(id, reasonHash);\n }\n function resolve(bytes32 id, uint256 workerAmount) external nonReentrant {\n Job storage j = jobs[id];\n if (msg.sender != j.terms.arbitrator) revert Unauthorized();\n if (j.state != State.Disputed) revert WrongState();\n if (block.timestamp > j.terms.resolutionDeadline || workerAmount > j.terms.amount) revert InvalidTerms();\n _settle(id, j, workerAmount, 2);\n }\n /// Permissionless liveness fallback; requires an actual transaction after the deadline.\n function timeout(bytes32 id) external nonReentrant {\n Job storage j = jobs[id];\n if (j.state != State.Submitted && j.state != State.Verified && j.state != State.Disputed) revert WrongState();\n if (block.timestamp <= j.terms.resolutionDeadline) revert Deadline();\n // Quotient/remainder avoids overflowing amount * bps.\n uint256 amount = (j.terms.amount / 10000) * j.terms.timeoutWorkerBps + ((j.terms.amount % 10000) * j.terms.timeoutWorkerBps) / 10000;\n _settle(id, j, amount, 3);\n }\n function _settle(bytes32 id, Job storage j, uint256 workerAmount, uint8 reason) private {\n uint256 payerAmount = j.terms.amount - workerAmount;\n j.state = workerAmount == 0 ? State.Refunded : payerAmount == 0 ? State.Paid : State.Split;\n j.workerPaid = workerAmount;\n if (workerAmount > 0) _transferExact(j.terms.worker, workerAmount);\n if (payerAmount > 0) _transferExact(j.terms.payer, payerAmount);\n emit Settled(id, workerAmount, payerAmount, reason);\n }\n function _transferExact(address to, uint256 amount) private {\n uint256 beforeBalance = token.balanceOf(address(this));\n uint256 beforeRecipient = token.balanceOf(to);\n token.safeTransfer(to, amount);\n if (token.balanceOf(address(this)) != beforeBalance - amount || token.balanceOf(to) != beforeRecipient + amount) revert TokenAmountMismatch();\n }\n}\n"
|
|
5
|
+
"content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.24;\n\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\nimport {ReentrancyGuard} from \"@openzeppelin/contracts/utils/ReentrancyGuard.sol\";\nimport {EIP712} from \"@openzeppelin/contracts/utils/cryptography/EIP712.sol\";\nimport {ECDSA} from \"@openzeppelin/contracts/utils/cryptography/ECDSA.sol\";\nimport {IERC1271} from \"@openzeppelin/contracts/interfaces/IERC1271.sol\";\n\n/// @notice Experimental reference escrow for standard, non-rebasing ERC20 tokens.\n/// No admin withdrawals/upgrades. Arbitration and timeout allocation are agreed before funding.\ncontract A423Escrow is ReentrancyGuard, EIP712 {\n using SafeERC20 for IERC20;\n enum State { None, Funded, Submitted, Verified, Disputed, Paid, Refunded, Split, Negotiating }\n struct Terms {\n bytes32 jobId;\n address payer;\n address worker;\n address verifier;\n address arbitrator;\n uint256 amount;\n uint64 workDeadline;\n uint64 reviewDeadline;\n uint64 resolutionDeadline;\n uint16 timeoutWorkerBps;\n bytes32 agreementHash;\n }\n struct Proposal {\n uint64 revision;\n uint64 expiresAt;\n address proposer;\n uint256 workerAmount;\n bytes32 proposalHash;\n }\n struct ClosingSettlement {\n bytes32 jobId;\n bytes32 agreementHash;\n uint64 escrowVersion;\n uint256 workerAmount;\n uint256 payerAmount;\n bytes32 proposalHash;\n uint64 expiresAt;\n }\n struct Job { Terms terms; State state; bytes32 resultHash; uint256 workerPaid; Proposal proposal; }\n IERC20 public immutable token;\n mapping(bytes32 => Job) private jobs;\n bytes32 public constant TERMS_TYPEHASH = keccak256(\"Terms(bytes32 jobId,address payer,address worker,address verifier,address arbitrator,uint256 amount,uint64 workDeadline,uint64 reviewDeadline,uint64 resolutionDeadline,uint16 timeoutWorkerBps,bytes32 agreementHash)\");\n bytes32 public constant SETTLEMENT_TYPEHASH = keccak256(\"ClosingSettlement(bytes32 jobId,bytes32 agreementHash,uint64 escrowVersion,uint256 workerAmount,uint256 payerAmount,bytes32 proposalHash,uint64 expiresAt)\");\n event SettlementProposed(bytes32 indexed jobId, uint64 revision, address indexed proposer, uint256 workerAmount, uint256 payerAmount, bytes32 proposalHash, uint64 expiresAt);\n event SettlementAccepted(bytes32 indexed jobId, uint64 revision, bytes32 proposalHash);\n event Funded(bytes32 indexed jobId, bytes32 indexed agreementHash, address indexed payer, address worker, uint256 amount);\n event Submitted(bytes32 indexed jobId, bytes32 resultHash);\n event Verified(bytes32 indexed jobId, bytes32 resultHash);\n event Disputed(bytes32 indexed jobId, bytes32 reasonHash);\n event Settled(bytes32 indexed jobId, uint256 workerAmount, uint256 payerAmount, uint8 reason);\n error InvalidTerms();\n error WrongState();\n error Unauthorized();\n error Deadline();\n error TokenAmountMismatch();\n\n constructor(address token_) EIP712(\"A423Escrow\", \"1\") {\n if (token_.code.length == 0) revert InvalidTerms();\n token = IERC20(token_);\n }\n function termsDigest(Terms calldata t) public view returns (bytes32) {\n return _hashTypedDataV4(keccak256(abi.encode(TERMS_TYPEHASH, t)));\n }\n function getJob(bytes32 id) external view returns (Job memory) { return jobs[id]; }\n /// Payer transaction plus worker EIP712 signature establish bilateral on-chain consent.\n function fund(Terms calldata t, bytes calldata workerSignature) external nonReentrant {\n if (msg.sender != t.payer) revert Unauthorized();\n if (jobs[t.jobId].state != State.None) revert WrongState();\n if (t.jobId == bytes32(0) || t.agreementHash == bytes32(0) || t.amount == 0 || t.worker == address(0) || t.worker == t.payer || t.verifier == address(0) || t.arbitrator == address(0) || t.arbitrator == t.payer || t.arbitrator == t.worker || t.timeoutWorkerBps > 10000) revert InvalidTerms();\n if (t.workDeadline <= block.timestamp || t.reviewDeadline <= t.workDeadline || t.resolutionDeadline <= t.reviewDeadline) revert InvalidTerms();\n bool valid;\n if (t.worker.code.length == 0) { valid = ECDSA.recover(termsDigest(t), workerSignature) == t.worker; }\n else { try IERC1271(t.worker).isValidSignature(termsDigest(t), workerSignature) returns (bytes4 magic) { valid = magic == IERC1271.isValidSignature.selector; } catch { valid = false; } }\n if (!valid) revert Unauthorized();\n jobs[t.jobId] = Job(t, State.Funded, bytes32(0), 0, Proposal(0, 0, address(0), 0, bytes32(0)));\n uint256 beforeBalance = token.balanceOf(address(this));\n token.safeTransferFrom(t.payer, address(this), t.amount);\n if (token.balanceOf(address(this)) != beforeBalance + t.amount) revert TokenAmountMismatch();\n emit Funded(t.jobId, t.agreementHash, t.payer, t.worker, t.amount);\n }\n function submit(bytes32 id, bytes32 resultHash) external {\n Job storage j = jobs[id];\n if (msg.sender != j.terms.worker) revert Unauthorized();\n if (j.state != State.Funded) revert WrongState();\n if (block.timestamp > j.terms.workDeadline) revert Deadline();\n if (resultHash == bytes32(0)) revert InvalidTerms();\n j.resultHash = resultHash; j.state = State.Submitted;\n emit Submitted(id, resultHash);\n }\n function verify(bytes32 id, bytes32 resultHash) external {\n Job storage j = jobs[id];\n if (msg.sender != j.terms.verifier) revert Unauthorized();\n if (j.state != State.Submitted) revert WrongState();\n if (block.timestamp > j.terms.reviewDeadline) revert Deadline();\n if (resultHash != j.resultHash) revert InvalidTerms();\n j.state = State.Verified; emit Verified(id, resultHash);\n }\n function release(bytes32 id) external nonReentrant {\n Job storage j = jobs[id];\n if (msg.sender != j.terms.verifier) revert Unauthorized();\n if (j.state != State.Verified) revert WrongState();\n if (block.timestamp > j.terms.resolutionDeadline) revert Deadline();\n _settle(id, j, j.terms.amount, 0);\n }\n function refundExpired(bytes32 id) external nonReentrant {\n Job storage j = jobs[id];\n if (j.state != State.Funded && !((j.state == State.Negotiating || j.state == State.Disputed) && j.resultHash == bytes32(0))) revert WrongState();\n if (block.timestamp <= j.terms.workDeadline) revert Deadline();\n _settle(id, j, 0, 1);\n }\n function dispute(bytes32 id, bytes32 reasonHash) external {\n Job storage j = jobs[id];\n if (msg.sender != j.terms.payer && msg.sender != j.terms.worker && msg.sender != j.terms.verifier) revert Unauthorized();\n if (j.state != State.Submitted && j.state != State.Verified && j.state != State.Negotiating && j.state != State.Funded) revert WrongState();\n if (block.timestamp > j.terms.resolutionDeadline) revert Deadline();\n j.state = State.Disputed; emit Disputed(id, reasonHash);\n }\n function resolve(bytes32 id, uint256 workerAmount) external nonReentrant {\n Job storage j = jobs[id];\n if (msg.sender != j.terms.arbitrator) revert Unauthorized();\n if (j.state != State.Disputed) revert WrongState();\n if (block.timestamp > j.terms.resolutionDeadline || workerAmount > j.terms.amount) revert InvalidTerms();\n _settle(id, j, workerAmount, 2);\n }\n /// Permissionless liveness fallback; requires an actual transaction after the deadline.\n function timeout(bytes32 id) external nonReentrant {\n Job storage j = jobs[id];\n if ((j.state != State.Submitted && j.state != State.Verified && j.state != State.Disputed && j.state != State.Negotiating) || j.resultHash == bytes32(0)) revert WrongState();\n if (block.timestamp <= j.terms.resolutionDeadline) revert Deadline();\n // Quotient/remainder avoids overflowing amount * bps.\n uint256 amount = (j.terms.amount / 10000) * j.terms.timeoutWorkerBps + ((j.terms.amount % 10000) * j.terms.timeoutWorkerBps) / 10000;\n _settle(id, j, amount, 3);\n }\n /// A published proposal freezes submit/verify/release, but never extends a deadline.\n /// Off-chain claim/evidence bytes are committed through proposalHash.\n function proposeSettlement(bytes32 id, uint64 expectedRevision, uint256 workerAmount, bytes32 proposalHash, uint64 expiresAt) external {\n Job storage j = jobs[id];\n if (msg.sender != j.terms.payer && msg.sender != j.terms.worker) revert Unauthorized();\n if (j.state != State.Funded && j.state != State.Submitted && j.state != State.Verified && j.state != State.Negotiating) revert WrongState();\n if (j.proposal.revision != expectedRevision || workerAmount > j.terms.amount || proposalHash == bytes32(0)) revert InvalidTerms();\n if (j.state == State.Negotiating && j.proposal.proposer == msg.sender) revert Unauthorized();\n if (expiresAt <= block.timestamp || expiresAt > j.terms.resolutionDeadline || block.timestamp > j.terms.resolutionDeadline) revert Deadline();\n j.proposal = Proposal(expectedRevision + 1, expiresAt, msg.sender, workerAmount, proposalHash);\n j.state = State.Negotiating;\n emit SettlementProposed(id, j.proposal.revision, msg.sender, workerAmount, j.terms.amount - workerAmount, proposalHash, expiresAt);\n }\n /// The counterparty's transaction accepts the exact currently published closing allocation.\n function acceptSettlement(bytes32 id, uint64 revision, bytes32 proposalHash) external nonReentrant {\n Job storage j = jobs[id];\n if ((msg.sender != j.terms.payer && msg.sender != j.terms.worker) || msg.sender == j.proposal.proposer) revert Unauthorized();\n _checkProposal(j, revision, proposalHash);\n emit SettlementAccepted(id, revision, proposalHash);\n _settle(id, j, j.proposal.workerAmount, 4);\n }\n function settlementDigest(ClosingSettlement calldata s) public view returns (bytes32) {\n return _hashTypedDataV4(keccak256(abi.encode(SETTLEMENT_TYPEHASH, s)));\n }\n /// Anyone can relay both parties' exact, current, chain/domain-bound signatures.\n function settleByConsent(ClosingSettlement calldata s, bytes calldata payerSignature, bytes calldata workerSignature) external nonReentrant {\n Job storage j = jobs[s.jobId];\n _checkProposal(j, s.escrowVersion, s.proposalHash);\n if (s.agreementHash != j.terms.agreementHash || s.workerAmount != j.proposal.workerAmount || s.payerAmount != j.terms.amount - s.workerAmount || s.expiresAt != j.proposal.expiresAt) revert InvalidTerms();\n bytes32 digest = settlementDigest(s);\n if (!_validSignature(j.terms.payer, digest, payerSignature) || !_validSignature(j.terms.worker, digest, workerSignature)) revert Unauthorized();\n emit SettlementAccepted(s.jobId, s.escrowVersion, s.proposalHash);\n _settle(s.jobId, j, s.workerAmount, 4);\n }\n function _validSignature(address account, bytes32 digest, bytes calldata signature) private view returns (bool) {\n if (account.code.length == 0) return ECDSA.recover(digest, signature) == account;\n try IERC1271(account).isValidSignature(digest, signature) returns (bytes4 magic) { return magic == IERC1271.isValidSignature.selector; } catch { return false; }\n }\n function _checkProposal(Job storage j, uint64 revision, bytes32 proposalHash) private view {\n if (j.state != State.Negotiating) revert WrongState();\n if (revision != j.proposal.revision || proposalHash != j.proposal.proposalHash) revert InvalidTerms();\n if (block.timestamp > j.proposal.expiresAt || block.timestamp > j.terms.resolutionDeadline) revert Deadline();\n }\n function _settle(bytes32 id, Job storage j, uint256 workerAmount, uint8 reason) private {\n uint256 payerAmount = j.terms.amount - workerAmount;\n j.state = workerAmount == 0 ? State.Refunded : payerAmount == 0 ? State.Paid : State.Split;\n j.workerPaid = workerAmount;\n if (workerAmount > 0) _transferExact(j.terms.worker, workerAmount);\n if (payerAmount > 0) _transferExact(j.terms.payer, payerAmount);\n emit Settled(id, workerAmount, payerAmount, reason);\n }\n function _transferExact(address to, uint256 amount) private {\n uint256 beforeBalance = token.balanceOf(address(this));\n uint256 beforeRecipient = token.balanceOf(to);\n token.safeTransfer(to, amount);\n if (token.balanceOf(address(this)) != beforeBalance - amount || token.balanceOf(to) != beforeRecipient + amount) revert TokenAmountMismatch();\n }\n}\n"
|
|
6
6
|
},
|
|
7
7
|
"@openzeppelin/contracts/token/ERC20/IERC20.sol": {
|
|
8
8
|
"content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/IERC20.sol)\n\npragma solidity >=0.4.16;\n\n/**\n * @dev Interface of the ERC-20 standard as defined in the ERC.\n */\ninterface IERC20 {\n /**\n * @dev Emitted when `value` tokens are moved from one account (`from`) to\n * another (`to`).\n *\n * Note that `value` may be zero.\n */\n event Transfer(address indexed from, address indexed to, uint256 value);\n\n /**\n * @dev Emitted when the allowance of a `spender` for an `owner` is set by\n * a call to {approve}. `value` is the new allowance.\n */\n event Approval(address indexed owner, address indexed spender, uint256 value);\n\n /**\n * @dev Returns the value of tokens in existence.\n */\n function totalSupply() external view returns (uint256);\n\n /**\n * @dev Returns the value of tokens owned by `account`.\n */\n function balanceOf(address account) external view returns (uint256);\n\n /**\n * @dev Moves a `value` amount of tokens from the caller's account to `to`.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * Emits a {Transfer} event.\n */\n function transfer(address to, uint256 value) external returns (bool);\n\n /**\n * @dev Returns the remaining number of tokens that `spender` will be\n * allowed to spend on behalf of `owner` through {transferFrom}. This is\n * zero by default.\n *\n * This value changes when {approve} or {transferFrom} are called.\n */\n function allowance(address owner, address spender) external view returns (uint256);\n\n /**\n * @dev Sets a `value` amount of tokens as the allowance of `spender` over the\n * caller's tokens.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * IMPORTANT: Beware that changing an allowance with this method brings the risk\n * that someone may use both the old and the new allowance by unfortunate\n * transaction ordering. One possible solution to mitigate this race\n * condition is to first reduce the spender's allowance to 0 and set the\n * desired value afterwards:\n * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729\n *\n * Emits an {Approval} event.\n */\n function approve(address spender, uint256 value) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from `from` to `to` using the\n * allowance mechanism. `value` is then deducted from the caller's\n * allowance.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * Emits a {Transfer} event.\n */\n function transferFrom(address from, address to, uint256 value) external returns (bool);\n}\n"
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
const $=id=>document.getElementById(id);let credential='';
|
|
8
8
|
async function api(path,body,key){const response=await fetch(path,{method:body===undefined?'GET':'POST',headers:{authorization:'Bearer '+credential,...(body===undefined?{}:{'content-type':'application/json'}),...(key?{'idempotency-key':key}:{})},body:body===undefined?undefined:JSON.stringify(body)});const data=await response.json();if(!response.ok)throw Error(JSON.stringify(data));return data;}
|
|
9
9
|
function show(error){$('status').textContent=error.message||String(error);}
|
|
10
|
-
async function load(){credential=$('token').value;const data=await api('/api/state');$('jobs').replaceChildren();for(const job of data.a423.jobs){const article=document.createElement('article');const title=document.createElement('h2');title.textContent=job.id+' · '+job.state+(job.safety?.blocked?' · SAFETY HOLD':'')+(job.pending?' · '+job.pending.action+' pending':'');article.append(title);const info=document.createElement('pre');info.textContent=JSON.stringify({safety:job.safety,participants:job.participants,agreement:job.agreement,submission:job.submission,evidence:job.evidence},null,2);article.append(info);
|
|
11
|
-
const action=document.createElement('select');for(const value of ['quote','accept','fund','start','submit','verify','release','refund','dispute','resolve','timeout','cancel','expire']){const o=document.createElement('option');o.textContent=value;action.append(o);}const input=document.createElement('textarea');input.value='{}';const key=document.createElement('input');key.value=crypto.randomUUID();key.title='Keep this key when retrying the same action';const run=document.createElement('button');run.textContent='Run action';run.onclick=async()=>{try{await api('/api/a423/jobs/'+job.id+'/actions',{action:action.value,input:JSON.parse(input.value)},key.value);await load();}catch(e){show(e);}};
|
|
10
|
+
async function load(){credential=$('token').value;const data=await api('/api/state');$('jobs').replaceChildren();for(const job of data.a423.jobs){const article=document.createElement('article');const title=document.createElement('h2');title.textContent=job.id+' · '+job.state+(job.safety?.blocked?' · SAFETY HOLD':'')+(job.pending?' · '+job.pending.action+' pending':'');article.append(title);const info=document.createElement('pre');info.textContent=JSON.stringify({work_status:job.work_status,escrow_ledger:job.escrow_ledger,negotiation:job.negotiation,subflows:job.subflows,active_subflow_id:job.active_subflow_id,safety:job.safety,participants:job.participants,agreement:job.agreement,submission:job.submission,evidence:job.evidence},null,2);article.append(info);
|
|
11
|
+
const action=document.createElement('select');for(const value of ['quote','accept','fund','start','submit','verify','release','refund','dispute','resolve','timeout','propose_settlement','counter_settlement','approve_cancellation','execute_cancellation','accept_settlement','cancel','expire']){const o=document.createElement('option');o.textContent=value;action.append(o);}const input=document.createElement('textarea');input.value='{}';action.onchange=()=>{const p=job.negotiation?.current;if(['propose_settlement','counter_settlement'].includes(action.value))input.value=JSON.stringify({expected_revision:p?.escrow_version||0,worker_amount:p?.worker_amount||'0',expires_at:Math.min(Math.floor(Date.now()/1000)+3600,job.agreement?.terms.escrow.resolution_deadline||0),reason:'Explain completed work and proposed compensation',claims:[]},null,2);if(['accept_settlement','approve_cancellation'].includes(action.value)&&p)input.value=JSON.stringify({expected_revision:p.escrow_version,proposal_hash:p.proposal_hash},null,2);if(action.value==='execute_cancellation')input.value=JSON.stringify({instruction_id:job.subflows?.find(s=>s.kind==='cancellation')?.instruction?.id||''},null,2);};const key=document.createElement('input');key.value=crypto.randomUUID();key.title='Keep this key when retrying the same action';const run=document.createElement('button');run.textContent='Run action';run.onclick=async()=>{try{await api('/api/a423/jobs/'+job.id+(action.value==='approve_cancellation'?'/cancellation/accept':'/actions'),action.value==='approve_cancellation'?JSON.parse(input.value):{action:action.value,input:JSON.parse(input.value)},key.value);await load();}catch(e){show(e);}};
|
|
12
12
|
const consent=document.createElement('button');consent.textContent='Sign worker consent with wallet';consent.onclick=async()=>{try{if(!window.ethereum)throw Error('EIP-1193 wallet required');const typed=await api('/api/a423-worker-consent/'+job.id);const accounts=await window.ethereum.request({method:'eth_requestAccounts'});if(accounts[0].toLowerCase()!==typed.value.worker.toLowerCase())throw Error('Choose the agreed worker account');const signature=await window.ethereum.request({method:'eth_signTypedData_v4',params:[accounts[0],JSON.stringify({domain:typed.domain,primaryType:'Terms',types:{EIP712Domain:[{name:'name',type:'string'},{name:'version',type:'string'},{name:'chainId',type:'uint256'},{name:'verifyingContract',type:'address'}],...typed.types},message:typed.value})]});input.value=JSON.stringify({worker_signature:signature},null,2);action.value='fund';show('Worker signed. Share the public signature with the authenticated payer to fund.');}catch(e){show(e);}};
|
|
13
13
|
const receipts=document.createElement('button');receipts.textContent='Fresh JSON receipt';receipts.onclick=async()=>{try{info.textContent=JSON.stringify(await api('/api/a423-receipts/'+job.id),null,2);}catch(e){show(e);}};const pdf=document.createElement('button');pdf.textContent='Fresh PDF';pdf.onclick=async()=>{try{const r=await fetch('/api/a423-receipts/'+job.id+'.pdf',{headers:{authorization:'Bearer '+credential}});if(!r.ok)throw Error('Evidence unavailable');const url=URL.createObjectURL(await r.blob());const a=document.createElement('a');a.href=url;a.download=job.id+'.pdf';a.click();setTimeout(()=>URL.revokeObjectURL(url),10000);}catch(e){show(e);}};
|
|
14
|
-
article.append(action,input,key,run,consent,receipts,pdf);$('jobs').append(article);}show('Loaded '+data.a423.jobs.length+' workflows');}
|
|
14
|
+
const closing=document.createElement('button');closing.textContent='Sign current closing split';closing.onclick=async()=>{try{if(!window.ethereum)throw Error('EIP-1193 wallet required');const typed=await api('/api/a423-settlement-consent/'+job.id);const accounts=await window.ethereum.request({method:'eth_requestAccounts'});const address=accounts[0].toLowerCase(),terms=job.agreement.terms.escrow;const field=address===terms.payer.toLowerCase()?'payer_signature':address===terms.worker.toLowerCase()?'worker_signature':null;if(!field)throw Error('Choose the agreed payer or worker wallet');if(!confirm('Close this job: '+typed.value.workerAmount+' raw units to worker, '+typed.value.payerAmount+' refunded. Review the claim and wallet prompt.'))return;const signature=await window.ethereum.request({method:'eth_signTypedData_v4',params:[accounts[0],JSON.stringify({domain:typed.domain,primaryType:'ClosingSettlement',types:{EIP712Domain:[{name:'name',type:'string'},{name:'version',type:'string'},{name:'chainId',type:'uint256'},{name:'verifyingContract',type:'address'}],...typed.types},message:typed.value})]});let previous={};try{previous=JSON.parse(input.value);}catch{}input.value=JSON.stringify({...previous,expected_revision:typed.value.escrowVersion,proposal_hash:typed.value.proposalHash,[field]:signature},null,2);action.value='approve_cancellation';show('Signed closing split. Combine both wallet signatures, then Run action to save acceptance and forward to escrow.');}catch(e){show(e);}};const cancellation=document.createElement('button');cancellation.textContent='Confirmed cancellation receipt';cancellation.onclick=async()=>{try{info.textContent=JSON.stringify(await api('/api/a423/jobs/'+job.id+'/cancellation/receipt'),null,2);}catch(e){show(e);}};const instruction=document.createElement('button');instruction.textContent='Accepted cancellation JSON';instruction.onclick=async()=>{try{info.textContent=JSON.stringify(await api('/api/a423/jobs/'+job.id+'/cancellation/instruction'),null,2);}catch(e){show(e);}};article.append(action,input,key,run,consent,closing,instruction,cancellation,receipts,pdf);$('jobs').append(article);}show('Loaded '+data.a423.jobs.length+' workflows');}
|
|
15
15
|
$('refresh').onclick=()=>load().catch(show);$('createButton').onclick=async()=>{try{credential=$('token').value;await api('/api/a423/jobs',JSON.parse($('create').value));await load();}catch(e){show(e);}};
|
|
16
16
|
</script></html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allorc/a423-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Experimental Next.js-friendly agent job SDK with typed HTTP client and pluggable storage, authentication and settlement",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -71,7 +71,11 @@
|
|
|
71
71
|
"contracts/standard-input.json",
|
|
72
72
|
"examples/operations-dashboard.html",
|
|
73
73
|
"LEGACY.md",
|
|
74
|
-
"scripts/operations-worker.mjs"
|
|
74
|
+
"scripts/operations-worker.mjs",
|
|
75
|
+
"src/closing.cjs",
|
|
76
|
+
"SETTLEMENT.md",
|
|
77
|
+
"src/cancellation.cjs",
|
|
78
|
+
"CANCELLATION.md"
|
|
75
79
|
],
|
|
76
80
|
"engines": {
|
|
77
81
|
"node": ">=22.13"
|