@allorc/a423-sdk 0.5.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CANCELLATION.md +106 -0
- package/CHANGELOG.md +36 -0
- package/ESCROW.md +2 -0
- package/INTEGRATION.md +15 -1
- package/OPERATIONS.md +4 -0
- package/PROFIT-POLICY.md +154 -0
- package/README.md +606 -63
- package/SECURITY.md +6 -0
- package/SETTLEMENT.md +4 -0
- package/examples/README.md +29 -0
- package/examples/operations-dashboard.html +3 -3
- package/examples/quickstart.mjs +33 -0
- package/package.json +14 -3
- package/src/cancellation.cjs +74 -0
- package/src/escrow/cancellation.cjs +34 -0
- package/src/escrow/index.cjs +41 -2
- package/src/escrow/index.d.mts +6 -2
- package/src/escrow/index.d.ts +6 -2
- package/src/escrow/index.mjs +1 -1
- package/src/escrow/negotiation.cjs +1 -1
- package/src/escrow/services.cjs +1 -1
- package/src/operations/application.cjs +14 -6
- package/src/operations/factory.cjs +6 -4
- package/src/operations/index.cjs +10 -0
- package/src/operations/index.d.ts +5 -3
- package/src/operations/index.mjs +1 -1
- package/src/policy/index.cjs +94 -0
- package/src/policy/index.d.ts +23 -0
- package/src/policy/index.mjs +2 -0
- package/src/workflow.cjs +97 -13
- package/src/workflow.d.ts +38 -5
- package/src/workflow.mjs +1 -1
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.
|
|
@@ -15,3 +17,7 @@ Reorg watchers quarantine stale/unavailable evidence; they cannot guarantee fina
|
|
|
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.
|
|
16
18
|
|
|
17
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.
|
|
20
|
+
|
|
21
|
+
## Off-chain profit policy (0.8)
|
|
22
|
+
|
|
23
|
+
Private cost snapshots and exception decisions live in backend KV, not agreement terms/events. Configure role-aware projectors and never share worker-private responses through public caches. The policy is opt-in and cannot constrain direct external contract calls or prove costs are real. Owner exceptions must be checked by a trusted server approval system, never client flags. Malformed-pending recovery requires an adapter impossibility proof and refuses any known signed/external intent; do not add permissive recovery hooks. See PROFIT-POLICY.md.
|
package/SETTLEMENT.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Negotiated closing settlements — 0.5.0
|
|
2
2
|
|
|
3
|
+
[Start here: installation, arguments and lifecycle maps](./README.md) · [All examples](./examples/README.md)
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
3
7
|
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.**
|
|
4
8
|
|
|
5
9
|
## Separate facts
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# A423 examples
|
|
2
|
+
|
|
3
|
+
Start with the [main README](../README.md) for installation, configuration arguments and lifecycle diagrams. These examples target the source version in `package.json`; check the installed npm version before copying newer APIs.
|
|
4
|
+
|
|
5
|
+
| Example | Purpose | Chain activity |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| [quickstart.mjs](./quickstart.mjs) | Runnable in-memory agreement and pure profit calculation | None; no wallet or funding adapter |
|
|
8
|
+
| [operations-server.mjs](./operations-server.mjs) | Localhost HTTP shell around a configured application handler | Only through your explicitly configured adapters/signers |
|
|
9
|
+
| [operations-dashboard.html](./operations-dashboard.html) | Reference UI for states, wallet consent, cancellation JSON, receipts and private economics | Actions use the authenticated backend |
|
|
10
|
+
| [nextjs/lib/a423.ts](./nextjs/lib/a423.ts) | Older root API example | No settlement adapter; records approval only |
|
|
11
|
+
| [client.mjs](./client.mjs) | Older root API client example | Depends on the configured server; not the reference workflow client |
|
|
12
|
+
|
|
13
|
+
Run the no-chain example from the source repository:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
node examples/quickstart.mjs
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
After installing the package in another app, copy the quickstart file and run it there; it imports public package entry points.
|
|
20
|
+
|
|
21
|
+
For the modern escrow path, construct `createEscrowApplication` with your authority, durable storage, deployment registry, signing/spending approvals and artifact validator. Mount its handler in Next.js with the Node runtime or use the local shell. The old `examples/nextjs` code intentionally demonstrates the preserved root API; do not mistake it for the new escrow workflow.
|
|
22
|
+
|
|
23
|
+
The shell requires your own configuration module exporting `applicationOptions`:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
A423_CONFIG_MODULE=/absolute/path/config.mjs node examples/operations-server.mjs
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The operational worker separately requires `workerOptions`; see [OPERATIONS.md](../OPERATIONS.md). Installing a package, mounting an HTTP handler and running a keeper are separate steps. No example supplies default credentials, signing keys or a deployed contract.
|
|
@@ -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({work_status:job.work_status,escrow_ledger:job.escrow_ledger,negotiation:job.negotiation,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','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(action.value
|
|
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);if(job.profit){const economics=document.createElement('p');const p=job.profit;economics.textContent=p.cost_basis_raw!==undefined?'Raw token units · Cost: '+p.cost_basis_raw+' · Target payout: '+p.target_payout+' · Minimum: '+p.min_payout+' · Current: '+p.projected_payout_raw+' · '+(p.receipt_confirmed?'Realized':'Projected')+' profit: '+(p.receipt_confirmed?p.realized_profit_raw:p.projected_profit_raw)+' · Margin bps: '+(p.receipt_confirmed?p.realized_margin_bps:p.projected_margin_bps)+' · '+p.target_status:'Proposed payout: '+(p.proposed_payout_raw??'—')+' · Confirmed payout: '+(p.realized_payout_raw??'pending');article.append(economics);}const info=document.createElement('pre');info.textContent=JSON.stringify({epoch:job.epoch,pre_funding_cancellations:job.pre_funding_cancellations,profit:job.profit,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','decline','reopen','expire']){const o=document.createElement('option');o.textContent=value;action.append(o);}const input=document.createElement('textarea');input.value='{}';action.onchange=()=>{input.value=JSON.stringify({expected_epoch:job.epoch||0},null,2);if(action.value==='reopen')input.value=JSON.stringify({cancellation_id:job.pre_funding_cancellations?.at(-1)?.id},null,2);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'?{expected_epoch:job.epoch||0,...JSON.parse(input.value)}:{action:action.value,input:{expected_epoch:job.epoch||0,...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
|
-
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='
|
|
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={expected_epoch:job.epoch||0,...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>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// In-memory workflow and policy example. No on-chain transactions or keys.
|
|
2
|
+
import {createMemoryStore} from '@allorc/a423-sdk';
|
|
3
|
+
import {createWorkflow} from '@allorc/a423-sdk/workflow';
|
|
4
|
+
import {createSettlementPolicy} from '@allorc/a423-sdk/policy';
|
|
5
|
+
|
|
6
|
+
const workflow = createWorkflow({store: createMemoryStore()});
|
|
7
|
+
const id = 'research-001';
|
|
8
|
+
await workflow.create({
|
|
9
|
+
id,
|
|
10
|
+
participants: {requester: 'buyer', worker: 'researcher', verifier: 'reviewer'},
|
|
11
|
+
}, 'buyer');
|
|
12
|
+
|
|
13
|
+
let job = await workflow.act(id, 'quote', {
|
|
14
|
+
terms: {description: 'Prepare a research brief', amount_raw: '200'},
|
|
15
|
+
}, {actor: 'researcher', idempotencyKey: 'quote-1'});
|
|
16
|
+
|
|
17
|
+
const acceptance = {
|
|
18
|
+
revision: job.agreement.revision,
|
|
19
|
+
agreement_hash: job.agreement.hash,
|
|
20
|
+
};
|
|
21
|
+
await workflow.act(id, 'accept', acceptance, {
|
|
22
|
+
actor: 'buyer', idempotencyKey: 'buyer-accept-1',
|
|
23
|
+
});
|
|
24
|
+
job = await workflow.act(id, 'accept', acceptance, {
|
|
25
|
+
actor: 'researcher', idempotencyKey: 'worker-accept-1',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const plan = createSettlementPolicy().evaluate({
|
|
29
|
+
cost_basis_raw: '100', budget_raw: '300', escrow_raw: '300',
|
|
30
|
+
});
|
|
31
|
+
console.log(job.state); // accepted; no funding has occurred
|
|
32
|
+
console.log(plan.target_payout); // 200: 50% gross-margin target
|
|
33
|
+
console.log(plan.recommended_payout);// 200: fits budget and escrow caps
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allorc/a423-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Experimental
|
|
3
|
+
"version": "0.8.1",
|
|
4
|
+
"description": "Experimental backend SDK for agent job agreements, durable workflows, ERC-20 escrow, negotiated settlement and configurable profit policies",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
7
7
|
"main": "./src/index.cjs",
|
|
@@ -41,6 +41,11 @@
|
|
|
41
41
|
"types": "./src/operations/index.d.ts",
|
|
42
42
|
"import": "./src/operations/index.mjs",
|
|
43
43
|
"require": "./src/operations/index.cjs"
|
|
44
|
+
},
|
|
45
|
+
"./policy": {
|
|
46
|
+
"types": "./src/policy/index.d.ts",
|
|
47
|
+
"import": "./src/policy/index.mjs",
|
|
48
|
+
"require": "./src/policy/index.cjs"
|
|
44
49
|
}
|
|
45
50
|
},
|
|
46
51
|
"files": [
|
|
@@ -73,7 +78,13 @@
|
|
|
73
78
|
"LEGACY.md",
|
|
74
79
|
"scripts/operations-worker.mjs",
|
|
75
80
|
"src/closing.cjs",
|
|
76
|
-
"SETTLEMENT.md"
|
|
81
|
+
"SETTLEMENT.md",
|
|
82
|
+
"src/cancellation.cjs",
|
|
83
|
+
"CANCELLATION.md",
|
|
84
|
+
"src/policy",
|
|
85
|
+
"PROFIT-POLICY.md",
|
|
86
|
+
"examples/quickstart.mjs",
|
|
87
|
+
"examples/README.md"
|
|
77
88
|
],
|
|
78
89
|
"engines": {
|
|
79
90
|
"node": ">=22.13"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const {createHash}=require('node:crypto');
|
|
3
|
+
const {hashAgreement}=require('./workflow.cjs');
|
|
4
|
+
const {settlementProposal}=require('./closing.cjs');
|
|
5
|
+
const clone=structuredClone;
|
|
6
|
+
function check(ok,message){if(!ok)throw new TypeError(message);}
|
|
7
|
+
function cancellationSubflowId(job){check(job.agreement,'Agreement required');return 'cancellation_'+hashAgreement([job.id,job.agreement.hash]);}
|
|
8
|
+
function getCancellationSubflow(job){return clone((job.subflows||[]).find(s=>s.kind==='cancellation')||null);}
|
|
9
|
+
function canonicalProposal(job){
|
|
10
|
+
const p=job.negotiation?.current;check(p,'Closing proposal required');
|
|
11
|
+
const canonical=settlementProposal(job,{worker_amount:p.worker_amount,payer_amount:p.payer_amount,expected_revision:p.escrow_version-1,expires_at:p.expires_at,reason:p.reason,claims:p.claims});
|
|
12
|
+
check(canonical.proposal_hash===p.proposal_hash,'Proposal claims or allocation changed');return canonical;
|
|
13
|
+
}
|
|
14
|
+
/** Constructs a JSON instruction. Cryptographic approval is checked separately by the escrow adapter. */
|
|
15
|
+
function createCancellationInstruction(job,{payer_signature,worker_signature}={}){
|
|
16
|
+
const p=canonicalProposal(job),e=job.agreement.terms.escrow;
|
|
17
|
+
const address=x=>{check(typeof x==='string'&&/^0x[0-9a-fA-F]{40}$/.test(x),'Invalid escrow address');return x.toLowerCase();};
|
|
18
|
+
const signature=x=>check(typeof x==='string'&&x.length<=32768&&/^0x(?:[0-9a-fA-F]{2})*$/.test(x),'Both wallet signature proofs required');
|
|
19
|
+
signature(payer_signature);signature(worker_signature);
|
|
20
|
+
const body={version:'a423.cancellation_instruction.v1',purpose:'close_job',funds_status:'not_executed',job_id:job.id,subflow_id:cancellationSubflowId(job),agreement_hash:'0x'+job.agreement.hash,chain_id:e.chain_id,escrow_contract:address(e.contract),token:address(e.token),onchain_job_id:'0x'+createHash('sha256').update('a423:escrow:v1:'+job.id).digest('hex'),escrow_version:p.escrow_version,proposal_hash:p.proposal_hash,expires_at:p.expires_at,payer:{agent_id:job.participants.requester,address:address(e.payer),amount:p.payer_amount},worker:{agent_id:job.participants.worker,address:address(e.worker),amount:p.worker_amount},proposal:p};
|
|
21
|
+
return {id:'cancel_instruction_'+hashAgreement(body),...body,authorization:{scheme:'eip712_closing_settlement_v1',payer_signature,worker_signature}};
|
|
22
|
+
}
|
|
23
|
+
function validateCancellationInstruction(job,instruction){
|
|
24
|
+
check(instruction&&instruction.authorization?.scheme==='eip712_closing_settlement_v1','Cancellation authorization required');
|
|
25
|
+
const expected=createCancellationInstruction(job,instruction.authorization);
|
|
26
|
+
check(hashAgreement(expected)===hashAgreement(instruction),'Cancellation instruction changed or does not match this proposal');
|
|
27
|
+
return clone(instruction);
|
|
28
|
+
}
|
|
29
|
+
function createCancellationReceipt(job,evidence,instruction=null){
|
|
30
|
+
const e=job.agreement?.terms.escrow;
|
|
31
|
+
check(e&&evidence?.event==='Settled'&&[5,6,7].includes(evidence.state)&&/^0x[0-9a-fA-F]{64}$/.test(evidence.transaction_hash),'Confirmed closing transaction required');
|
|
32
|
+
check(evidence.chain_id===e.chain_id&&evidence.contract.toLowerCase()===e.contract.toLowerCase()&&evidence.token.toLowerCase()===e.token.toLowerCase()&&evidence.agreement_hash==='0x'+job.agreement.hash,'Closing evidence destination mismatch');
|
|
33
|
+
check(evidence.job_id==='0x'+createHash('sha256').update('a423:escrow:v1:'+job.id).digest('hex'),'Closing evidence job mismatch');
|
|
34
|
+
const amount=BigInt(e.amount),paid=BigInt(evidence.worker_paid);
|
|
35
|
+
check(evidence.amount===e.amount&&paid>=0n&&paid<=amount&&((evidence.state===5&&paid===amount)||(evidence.state===6&&paid===0n)||(evidence.state===7&&paid>0n&&paid<amount)),'Invalid actual allocation');
|
|
36
|
+
check(evidence.payer.toLowerCase()===e.payer.toLowerCase()&&evidence.worker.toLowerCase()===e.worker.toLowerCase(),'Closing evidence parties mismatch');
|
|
37
|
+
const reason=evidence.settlement_reason;check([0,1,2,3,4].includes(reason),'Unknown closing outcome');
|
|
38
|
+
const mutual=reason===4;
|
|
39
|
+
if(mutual&&instruction){validateCancellationInstruction(job,instruction);check(instruction.worker.amount===paid.toString()&&instruction.proposal_hash===evidence.proposal_hash&&instruction.escrow_version===evidence.escrow_version,'Instruction was not the executed allocation');}
|
|
40
|
+
const proof=Object.fromEntries(Object.entries(evidence).filter(([key])=>!['observed_at','instruction_id'].includes(key)));
|
|
41
|
+
const body={version:'a423.cancellation_receipt.v1',status:'confirmed',job_id:job.id,subflow_id:cancellationSubflowId(job),agreement_hash:'0x'+job.agreement.hash,outcome:['verified_payment','expiry_refund','arbitrated_split','timeout_allocation','mutual_cancellation'][reason],instruction_id:mutual&&instruction?instruction.id:null,prior_instruction_id:instruction?.id||null,allocations:{worker:{address:e.worker.toLowerCase(),amount:paid.toString()},payer:{address:e.payer.toLowerCase(),amount:(amount-paid).toString()}},remaining_locked:'0',evidence:proof};
|
|
42
|
+
return {id:'cancel_receipt_'+hashAgreement(body),...body};
|
|
43
|
+
}
|
|
44
|
+
function validateCancellationReceipt(job,receipt){
|
|
45
|
+
check(receipt?.version==='a423.cancellation_receipt.v1','Final cancellation receipt required');
|
|
46
|
+
const instruction=getCancellationSubflow(job)?.instruction||null;
|
|
47
|
+
const expected=createCancellationReceipt(job,receipt.evidence,instruction);
|
|
48
|
+
check(hashAgreement(expected)===hashAgreement(receipt),'Final cancellation receipt changed');return clone(receipt);
|
|
49
|
+
}
|
|
50
|
+
function updateCancellationSubflow(previous,next,{action,actor,phase,input={},evidence={},instruction,error,at}){
|
|
51
|
+
const old=getCancellationSubflow(previous);
|
|
52
|
+
if(!old&&!['propose_settlement','counter_settlement','approve_cancellation','execute_cancellation','accept_settlement'].includes(action))return;
|
|
53
|
+
const s=old||{id:cancellationSubflowId(previous),kind:'cancellation',parent_job_id:previous.id,status:'negotiating',started_at:at,updated_at:at,proposal_revision:previous.negotiation?.current?.escrow_version||0,instruction:null,receipt:null,events:[]};
|
|
54
|
+
if(phase==='requested'){
|
|
55
|
+
if(['propose_settlement','counter_settlement'].includes(action))s.status='proposal_pending';
|
|
56
|
+
if(action==='approve_cancellation')s.status='acceptance_pending';
|
|
57
|
+
if(action==='execute_cancellation')s.status='execution_pending';
|
|
58
|
+
}
|
|
59
|
+
if(phase==='pending'&&action==='execute_cancellation')s.execution_reference=evidence.reference||null;
|
|
60
|
+
if(phase==='rejected'){
|
|
61
|
+
s.status=action==='execute_cancellation'?'execution_failed':s.instruction?'accepted':previous.negotiation?.current?'negotiating':'proposal_rejected';s.last_error=error||'Operation rejected';
|
|
62
|
+
}
|
|
63
|
+
if(phase==='confirmed'){
|
|
64
|
+
if(['propose_settlement','counter_settlement'].includes(action)){s.status='negotiating';s.proposal_revision=evidence.escrow_version;s.instruction=null;s.last_error=null;}
|
|
65
|
+
if(action==='approve_cancellation'){s.status='accepted';s.instruction=clone(instruction);s.accepted_at=at;s.approved_by=[previous.participants.requester,previous.participants.worker];s.last_error=null;}
|
|
66
|
+
if(action==='dispute')s.status='disputed';
|
|
67
|
+
if(['execute_cancellation','accept_settlement','refund','resolve','timeout','release'].includes(action)&&evidence.event==='Settled'){
|
|
68
|
+
s.receipt=createCancellationReceipt(previous,evidence,s.instruction);s.status='completed';s.completed_at=at;s.execution_reference=evidence.transaction_hash;s.closing_action=action;s.last_error=null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
s.updated_at=at;s.events.push({sequence:s.events.length+1,type:action+'.'+phase,actor_id:actor,at,data:{proposal_hash:evidence.proposal_hash||previous.negotiation?.current?.proposal_hash||null,instruction_id:s.instruction?.id||null,receipt_id:s.receipt?.id||null,error:error||null}});
|
|
72
|
+
next.subflows=[...(next.subflows||previous.subflows||[]).filter(flow=>flow.id!==s.id),s];next.active_subflow_id=['completed','proposal_rejected'].includes(s.status)?null:s.id;
|
|
73
|
+
}
|
|
74
|
+
module.exports={cancellationSubflowId,getCancellationSubflow,createCancellationInstruction,validateCancellationInstruction,createCancellationReceipt,validateCancellationReceipt,updateCancellationSubflow};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const {Contract,TypedDataEncoder,verifyTypedData,getAddress}=require('ethers');
|
|
3
|
+
const {createCancellationInstruction,validateCancellationInstruction}=require('../cancellation.cjs');
|
|
4
|
+
function check(ok,message){if(!ok)throw new TypeError(message);}
|
|
5
|
+
function cancellationCall(instruction){
|
|
6
|
+
check(instruction?.version==='a423.cancellation_instruction.v1'&&instruction.authorization?.scheme==='eip712_closing_settlement_v1','Signed cancellation instruction required');
|
|
7
|
+
return {functionName:'settleByConsent',args:[{jobId:instruction.onchain_job_id,agreementHash:instruction.agreement_hash,escrowVersion:instruction.escrow_version,workerAmount:BigInt(instruction.worker.amount),payerAmount:BigInt(instruction.payer.amount),proposalHash:instruction.proposal_hash,expiresAt:instruction.expires_at},instruction.authorization.payer_signature,instruction.authorization.worker_signature]};
|
|
8
|
+
}
|
|
9
|
+
async function verifyCancellationAuthorization(job,instruction,{provider,chainId,contractAddress,tokenAddress}={}){
|
|
10
|
+
validateCancellationInstruction(job,instruction);
|
|
11
|
+
const {escrowTerms,ESCROW_ABI,settlementConsent}=require('./index.cjs');
|
|
12
|
+
const config={chainId,contractAddress,tokenAddress},terms=escrowTerms(job,config),typed=settlementConsent(job,config);
|
|
13
|
+
if(Number((await provider.getNetwork()).chainId)!==chainId)throw Error('RPC chain mismatch');
|
|
14
|
+
const escrow=new Contract(contractAddress,ESCROW_ABI,provider),block=await provider.getBlock('latest');
|
|
15
|
+
if(getAddress(await escrow.token())!==getAddress(tokenAddress))throw Error('Escrow token mismatch');
|
|
16
|
+
const snapshot=await escrow.getJob(terms.jobId,{blockTag:block.number});
|
|
17
|
+
const invalid=message=>({valid:false,reason:message});
|
|
18
|
+
for(const [key,value] of Object.entries(terms))if(String(snapshot.terms[key]).toLowerCase()!==String(value).toLowerCase())return invalid('On-chain agreement mismatch');
|
|
19
|
+
if(Number(snapshot.state)!==8)return invalid('Escrow is not negotiating');
|
|
20
|
+
const p=snapshot.proposal;
|
|
21
|
+
if(Number(p.revision)!==instruction.escrow_version||p.proposalHash!==instruction.proposal_hash||p.workerAmount.toString()!==instruction.worker.amount||Number(p.expiresAt)!==instruction.expires_at)return invalid('On-chain cancellation proposal changed');
|
|
22
|
+
const proposer=job.negotiation.current.proposer_id===job.participants.requester?terms.payer:job.negotiation.current.proposer_id===job.participants.worker?terms.worker:null;
|
|
23
|
+
if(!proposer||getAddress(p.proposer)!==getAddress(proposer))return invalid('Proposal owner mismatch');
|
|
24
|
+
if(block.timestamp>instruction.expires_at||block.timestamp>terms.resolutionDeadline)return invalid('Cancellation approval expired');
|
|
25
|
+
const digest=TypedDataEncoder.hash(typed.domain,typed.types,typed.value);
|
|
26
|
+
for(const [account,signature] of [[terms.payer,instruction.authorization.payer_signature],[terms.worker,instruction.authorization.worker_signature]]){
|
|
27
|
+
if(await provider.getCode(account,block.number)!=='0x'){
|
|
28
|
+
const wallet=new Contract(account,['function isValidSignature(bytes32,bytes) view returns(bytes4)'],provider);
|
|
29
|
+
try{if(await wallet.isValidSignature(digest,signature,{blockTag:block.number})!=='0x1626ba7e')return invalid('Invalid smart-wallet approval');}catch(error){if(error.code==='CALL_EXCEPTION')return invalid('Smart-wallet approval rejected');throw error;}
|
|
30
|
+
}else{try{if(getAddress(verifyTypedData(typed.domain,typed.types,typed.value,signature))!==getAddress(account))return invalid('Invalid party approval');}catch{return invalid('Invalid party approval');}}
|
|
31
|
+
}
|
|
32
|
+
return {valid:true,checked_block:block.number};
|
|
33
|
+
}
|
|
34
|
+
module.exports={createCancellationInstruction,validateCancellationInstruction,cancellationCall,verifyCancellationAuthorization};
|
package/src/escrow/index.cjs
CHANGED
|
@@ -6,6 +6,8 @@ const ABI = artifact.abi;
|
|
|
6
6
|
const TOKEN_ABI = ['function allowance(address,address) view returns(uint256)','function approve(address,uint256) returns(bool)'];
|
|
7
7
|
const TERMS_TYPES = { Terms: [ ['jobId','bytes32'],['payer','address'],['worker','address'],['verifier','address'],['arbitrator','address'],['amount','uint256'],['workDeadline','uint64'],['reviewDeadline','uint64'],['resolutionDeadline','uint64'],['timeoutWorkerBps','uint16'],['agreementHash','bytes32'] ].map(([name,type])=>({name,type})) };
|
|
8
8
|
const transitions = {
|
|
9
|
+
approve_cancellation: {from:['settlement_negotiation'],roles:['requester','worker'],to:'settlement_negotiation',effect:true},
|
|
10
|
+
execute_cancellation: {from:['settlement_negotiation'],roles:['requester','worker'],to:'settled',effect:true},
|
|
9
11
|
propose_settlement: {from:['funded','running','submitted','verified'],roles:['requester','worker'],to:'settlement_negotiation',effect:true},
|
|
10
12
|
counter_settlement: {from:['settlement_negotiation'],roles:['requester','worker'],to:'settlement_negotiation',effect:true},
|
|
11
13
|
accept_settlement: {from:['settlement_negotiation'],roles:['requester','worker'],to:'settled',effect:true},
|
|
@@ -128,6 +130,36 @@ function createEscrowAdapters({provider,chainId,contractAddress,tokenAddress,sig
|
|
|
128
130
|
}
|
|
129
131
|
const proposeAdapter=adapter(async(ctx,r)=>{ctx.settlementActorAddress=getAddress(await addressFor(ctx.operation.actor));return propose(ctx,r);});
|
|
130
132
|
return {
|
|
133
|
+
approve_cancellation:adapter(async ctx=>{
|
|
134
|
+
await context(ctx.job);const p=ctx.job.negotiation?.current;
|
|
135
|
+
if(!p||ctx.input.expected_revision!==p.escrow_version||ctx.input.proposal_hash!==p.proposal_hash)return {status:'rejected',reason:'Approve the exact current proposal'};
|
|
136
|
+
const {createCancellationInstruction,verifyCancellationAuthorization}=require('./cancellation.cjs');let instruction;
|
|
137
|
+
try{instruction=createCancellationInstruction(ctx.job,ctx.input);}catch(error){return {status:'rejected',reason:error.message};}
|
|
138
|
+
const verified=await verifyCancellationAuthorization(ctx.job,instruction,{provider,...config});
|
|
139
|
+
if(!verified.valid)return {status:'rejected',reason:verified.reason};
|
|
140
|
+
return {status:'confirmed',evidence:{kind:'a423.cancellation_acceptance.v1',instruction,checked_block:verified.checked_block,funds_moved:false}};
|
|
141
|
+
}),
|
|
142
|
+
execute_cancellation:adapter(async(ctx,r)=>{
|
|
143
|
+
const t=await context(ctx.job);const {getCancellationSubflow,validateCancellationInstruction}=require('../cancellation.cjs');
|
|
144
|
+
const instruction=getCancellationSubflow(ctx.job)?.instruction;
|
|
145
|
+
if(!instruction||ctx.input.instruction_id!==instruction.id)return {status:'rejected',reason:'Accepted cancellation instruction required'};
|
|
146
|
+
try{validateCancellationInstruction(ctx.job,instruction);}catch(error){return {status:'rejected',reason:error.message};}
|
|
147
|
+
const {verifyCancellationAuthorization,cancellationCall}=require('./cancellation.cjs');
|
|
148
|
+
if(!ctx.input.transaction_hash&&!await transactions.get(ctx.operation.id+':execute_cancellation')){
|
|
149
|
+
const verified=await verifyCancellationAuthorization(ctx.job,instruction,{provider,...config});if(!verified.valid)return {status:'rejected',reason:verified.reason};
|
|
150
|
+
}
|
|
151
|
+
const call=cancellationCall(instruction);
|
|
152
|
+
const result=await observe(ctx,t,'execute_cancellation',call.functionName,call.args,'Settled',(s,e)=>{
|
|
153
|
+
settled(s,e,t);check(Number(e.args.reason)===4&&e.args.workerAmount.toString()===instruction.worker.amount&&e.args.payerAmount.toString()===instruction.payer.amount&&Number(s.proposal.revision)===instruction.escrow_version&&s.proposal.proposalHash===instruction.proposal_hash,'Instruction allocation mismatch');
|
|
154
|
+
},r);
|
|
155
|
+
if(result.status==='confirmed'){
|
|
156
|
+
const receipt=await provider.getTransactionReceipt(result.evidence.transaction_hash);
|
|
157
|
+
const accepted=receipt.logs.filter(l=>getAddress(l.address)===config.contractAddress).map(l=>{try{return iface.parseLog(l);}catch{return null;}}).find(e=>e?.name==='SettlementAccepted'&&e.args.jobId===t.jobId);
|
|
158
|
+
check(accepted&&Number(accepted.args.revision)===instruction.escrow_version&&accepted.args.proposalHash===instruction.proposal_hash,'Instruction acceptance event missing');
|
|
159
|
+
result.evidence.instruction_id=instruction.id;
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}),
|
|
131
163
|
propose_settlement:proposeAdapter,
|
|
132
164
|
counter_settlement:proposeAdapter,
|
|
133
165
|
accept_settlement:adapter(async(ctx,r)=>{
|
|
@@ -169,7 +201,14 @@ function createEscrowAdapters({provider,chainId,contractAddress,tokenAddress,sig
|
|
|
169
201
|
return observe(ctx,t,'fund','fund',[t,ctx.input.worker_signature],'Funded',(s,e)=>{check(Number(s.state)>0,'missing funded job');check(e.args.amount===t.amount && getAddress(e.args.payer)===t.payer && getAddress(e.args.worker)===t.worker && e.args.agreementHash===t.agreementHash,'funding event mismatch');},reconcile);
|
|
170
202
|
}),
|
|
171
203
|
start:adapter(async ctx=>{const t=await context(ctx.job);const block=await provider.getBlock('latest');const safeNumber=block.number-confirmations+1;check(safeNumber>=0,'insufficient confirmation depth');const safe=await provider.getBlock(safeNumber);const confirmed=await contract.getJob(t.jobId,{blockTag:safeNumber});match(confirmed,t);state(confirmed,1);const s=await contract.getJob(t.jobId,{blockTag:block.number});match(s,t);state(s,1);check(block.timestamp<=t.workDeadline,'work deadline passed');return {status:'confirmed',evidence:{chain_id:String(chainId),contract:config.contractAddress,job_id:t.jobId,block_number:safe.number,block_hash:safe.hash,confirmations,amount:t.amount.toString(),agreement_hash:t.agreementHash}};}),
|
|
172
|
-
submit:adapter(async(ctx,r)=>{const t=await context(ctx.job);const hash=bytes32(ctx.input.result_hash);return observe(ctx,t,'submit','submit',[t.jobId,hash],'Submitted',(s,e)=>{check(Number(s.state)>=2,'missing submission');check(s.resultHash===hash && e.args.resultHash===hash,'artifact commitment mismatch');},r);}),
|
|
204
|
+
submit:Object.assign(adapter(async(ctx,r)=>{const t=await context(ctx.job);const hash=bytes32(ctx.input.result_hash);return observe(ctx,t,'submit','submit',[t.jobId,hash],'Submitted',(s,e)=>{check(Number(s.state)>=2,'missing submission');check(s.resultHash===hash && e.args.resultHash===hash,'artifact commitment mismatch');},r);}),{rejectPending:async ctx=>{
|
|
205
|
+
const t=await context(ctx.job);let malformed=false;try{bytes32(ctx.input.result_hash);}catch{malformed=true;}
|
|
206
|
+
check(malformed&&!ctx.input.transaction_hash&&!ctx.operation.observation?.reference&&!ctx.operation.observation?.transaction_hash&&!ctx.operation.observation?.evidence?.transaction_hash,'Only syntactically impossible submissions without external transaction references can be rejected');
|
|
207
|
+
check(typeof transactions.list==='function','Transaction journal inventory required');
|
|
208
|
+
check(!(await transactions.list()).some(record=>record.id===ctx.operation.id||record.id.startsWith(ctx.operation.id+':')),'Journaled intent exists; reconcile instead');
|
|
209
|
+
const snapshot=await contract.getJob(t.jobId);match(snapshot,t);check(snapshot.resultHash===ZeroHash,'On-chain submission exists; reconcile instead');
|
|
210
|
+
return {status:'rejected',never_executable:true,reason:'Malformed result_hash cannot encode a submit transaction',checked_chain_id:String(chainId)};
|
|
211
|
+
}}),
|
|
173
212
|
verify:adapter(async(ctx,r)=>{const t=await context(ctx.job);const hash=bytes32(ctx.job.submission?.result_hash);if(!r && !await transactions.get(ctx.operation.id+':verify')){check(typeof validateArtifact==='function','validateArtifact callback required');if(await validateArtifact({job:structuredClone(ctx.job),resultHash:hash})!==true)return {status:'rejected',reason:'Artifact verification failed'};}return observe(ctx,t,'verify','verify',[t.jobId,hash],'Verified',(s,e)=>{check(Number(s.state)>=3,'missing verification');check(s.resultHash===hash && e.args.resultHash===hash,'verified artifact mismatch');},r);}),
|
|
174
213
|
release:adapter(async(ctx,r)=>{const t=await context(ctx.job);return observe(ctx,t,'release','release',[t.jobId],'Settled',(s,e)=>{state(s,5);settled(s,e,t);check(e.args.workerAmount===t.amount && Number(e.args.reason)===0,'not a full release');},r);}),
|
|
175
214
|
refund:adapter(async(ctx,r)=>{const t=await context(ctx.job);return observe(ctx,t,'refund','refundExpired',[t.jobId],'Settled',(s,e)=>{state(s,6);settled(s,e,t);check(e.args.payerAmount===t.amount && Number(e.args.reason)===1,'not an expiry refund');},r);}),
|
|
@@ -180,4 +219,4 @@ function createEscrowAdapters({provider,chainId,contractAddress,tokenAddress,sig
|
|
|
180
219
|
}
|
|
181
220
|
module.exports={createEscrowAdapters,createMemoryTransactionStore,workerConsent,escrowTerms,escrowTransitions:transitions,ESCROW_ABI:ABI,ESCROW_BYTECODE:artifact.bytecode};
|
|
182
221
|
|
|
183
|
-
Object.assign(module.exports,require('./services.cjs'),require('./negotiation.cjs'));
|
|
222
|
+
Object.assign(module.exports,require('./services.cjs'),require('./negotiation.cjs'),require('./cancellation.cjs'));
|
package/src/escrow/index.d.mts
CHANGED
|
@@ -16,7 +16,7 @@ export interface TransactionStore {
|
|
|
16
16
|
/** Atomically reserve a nonce per chain/address; same operationId always returns same nonce. */
|
|
17
17
|
reserveNonce(scope: string, minimum: number, operationId: string): Promise<number>;
|
|
18
18
|
}
|
|
19
|
-
export interface EscrowAdapter { execute(context: AdapterContext): Promise<AdapterResult>; reconcile(context: AdapterContext): Promise<AdapterResult> }
|
|
19
|
+
export interface EscrowAdapter { execute(context: AdapterContext): Promise<AdapterResult>; reconcile(context: AdapterContext): Promise<AdapterResult>;rejectPending?(context: AdapterContext): Promise<AdapterResult> }
|
|
20
20
|
export interface EscrowAdapterOptions extends EscrowConfiguration {
|
|
21
21
|
provider: Provider;
|
|
22
22
|
/** Requires populateTransaction + offline signTransaction support; no silent wallet approval. */
|
|
@@ -27,7 +27,7 @@ export interface EscrowAdapterOptions extends EscrowConfiguration {
|
|
|
27
27
|
confirmations: number;
|
|
28
28
|
validateArtifact?(context: { job: WorkflowJob; resultHash: string }): Promise<boolean>;
|
|
29
29
|
}
|
|
30
|
-
export function createEscrowAdapters(options: EscrowAdapterOptions): Record<'fund'|'start'|'submit'|'verify'|'release'|'refund'|'dispute'|'resolve'|'timeout'|'propose_settlement'|'counter_settlement'|'accept_settlement', EscrowAdapter>;
|
|
30
|
+
export function createEscrowAdapters(options: EscrowAdapterOptions): Record<'fund'|'start'|'submit'|'verify'|'release'|'refund'|'dispute'|'resolve'|'timeout'|'propose_settlement'|'counter_settlement'|'accept_settlement'|'approve_cancellation'|'execute_cancellation', EscrowAdapter>;
|
|
31
31
|
export function createMemoryTransactionStore(): TransactionStore;
|
|
32
32
|
export function escrowTerms(job: WorkflowJob, config: EscrowConfiguration): ContractTerms;
|
|
33
33
|
export function workerConsent(job: WorkflowJob, config: EscrowConfiguration): {
|
|
@@ -49,3 +49,7 @@ export function createEscrowIndexer(options: {provider: Provider;chainId: number
|
|
|
49
49
|
|
|
50
50
|
export function settlementProposal(job: WorkflowJob,input: {worker_amount: string;payer_amount?: string;expected_revision: number;expires_at: number;reason: string;claims?: import('../workflow.js').SettlementClaim[]}): import('../workflow.js').SettlementProposal;
|
|
51
51
|
export function settlementConsent(job: WorkflowJob,configuration: EscrowConfiguration): {domain: {name: string;version: string;chainId: number;verifyingContract: string};types: Record<string,TypedDataField[]>;value: {jobId: string;agreementHash: string;escrowVersion: number;workerAmount: bigint;payerAmount: bigint;proposalHash: string;expiresAt: number}};
|
|
52
|
+
|
|
53
|
+
export {createCancellationInstruction,validateCancellationInstruction} from '../workflow.js';
|
|
54
|
+
export function cancellationCall(instruction: import('../workflow.js').CancellationInstruction): {functionName: 'settleByConsent';args: [ReturnType<typeof settlementConsent>['value'],string,string]};
|
|
55
|
+
export function verifyCancellationAuthorization(job: WorkflowJob,instruction: import('../workflow.js').CancellationInstruction,options: EscrowConfiguration & {provider: Provider}): Promise<{valid: boolean;checked_block?: number;reason?: string}>;
|
package/src/escrow/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface TransactionStore {
|
|
|
16
16
|
/** Atomically reserve a nonce per chain/address; same operationId always returns same nonce. */
|
|
17
17
|
reserveNonce(scope: string, minimum: number, operationId: string): Promise<number>;
|
|
18
18
|
}
|
|
19
|
-
export interface EscrowAdapter { execute(context: AdapterContext): Promise<AdapterResult>; reconcile(context: AdapterContext): Promise<AdapterResult> }
|
|
19
|
+
export interface EscrowAdapter { execute(context: AdapterContext): Promise<AdapterResult>; reconcile(context: AdapterContext): Promise<AdapterResult>;rejectPending?(context: AdapterContext): Promise<AdapterResult> }
|
|
20
20
|
export interface EscrowAdapterOptions extends EscrowConfiguration {
|
|
21
21
|
provider: Provider;
|
|
22
22
|
/** Requires populateTransaction + offline signTransaction support; no silent wallet approval. */
|
|
@@ -27,7 +27,7 @@ export interface EscrowAdapterOptions extends EscrowConfiguration {
|
|
|
27
27
|
confirmations: number;
|
|
28
28
|
validateArtifact?(context: { job: WorkflowJob; resultHash: string }): Promise<boolean>;
|
|
29
29
|
}
|
|
30
|
-
export function createEscrowAdapters(options: EscrowAdapterOptions): Record<'fund'|'start'|'submit'|'verify'|'release'|'refund'|'dispute'|'resolve'|'timeout'|'propose_settlement'|'counter_settlement'|'accept_settlement', EscrowAdapter>;
|
|
30
|
+
export function createEscrowAdapters(options: EscrowAdapterOptions): Record<'fund'|'start'|'submit'|'verify'|'release'|'refund'|'dispute'|'resolve'|'timeout'|'propose_settlement'|'counter_settlement'|'accept_settlement'|'approve_cancellation'|'execute_cancellation', EscrowAdapter>;
|
|
31
31
|
export function createMemoryTransactionStore(): TransactionStore;
|
|
32
32
|
export function escrowTerms(job: WorkflowJob, config: EscrowConfiguration): ContractTerms;
|
|
33
33
|
export function workerConsent(job: WorkflowJob, config: EscrowConfiguration): {
|
|
@@ -49,3 +49,7 @@ export function createEscrowIndexer(options: {provider: Provider;chainId: number
|
|
|
49
49
|
|
|
50
50
|
export function settlementProposal(job: WorkflowJob,input: {worker_amount: string;payer_amount?: string;expected_revision: number;expires_at: number;reason: string;claims?: import('../workflow.js').SettlementClaim[]}): import('../workflow.js').SettlementProposal;
|
|
51
51
|
export function settlementConsent(job: WorkflowJob,configuration: EscrowConfiguration): {domain: {name: string;version: string;chainId: number;verifyingContract: string};types: Record<string,TypedDataField[]>;value: {jobId: string;agreementHash: string;escrowVersion: number;workerAmount: bigint;payerAmount: bigint;proposalHash: string;expiresAt: number}};
|
|
52
|
+
|
|
53
|
+
export {createCancellationInstruction,validateCancellationInstruction} from '../workflow.js';
|
|
54
|
+
export function cancellationCall(instruction: import('../workflow.js').CancellationInstruction): {functionName: 'settleByConsent';args: [ReturnType<typeof settlementConsent>['value'],string,string]};
|
|
55
|
+
export function verifyCancellationAuthorization(job: WorkflowJob,instruction: import('../workflow.js').CancellationInstruction,options: EscrowConfiguration & {provider: Provider}): Promise<{valid: boolean;checked_block?: number;reason?: string}>;
|
package/src/escrow/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import escrow from './index.cjs';
|
|
2
|
-
export const { createEscrowAdapters, createMemoryTransactionStore, workerConsent, escrowTerms, escrowTransitions, ESCROW_ABI, ESCROW_BYTECODE, createEscrowRegistry, createEvidenceRevalidator, createManagedSigner, createDelegationAuthorizer, replacePendingTransaction, createFeeReplacementWorker, createEscrowIndexer, settlementProposal, settlementConsent } = escrow;
|
|
2
|
+
export const { createEscrowAdapters, createMemoryTransactionStore, workerConsent, escrowTerms, escrowTransitions, ESCROW_ABI, ESCROW_BYTECODE, createEscrowRegistry, createEvidenceRevalidator, createManagedSigner, createDelegationAuthorizer, replacePendingTransaction, createFeeReplacementWorker, createEscrowIndexer, settlementProposal, settlementConsent, createCancellationInstruction, validateCancellationInstruction, cancellationCall, verifyCancellationAuthorization } = escrow;
|
|
@@ -5,7 +5,7 @@ function check(ok,message){if(!ok)throw new TypeError(message);}
|
|
|
5
5
|
const {settlementProposal}=require('../closing.cjs');
|
|
6
6
|
function settlementConsent(job,configuration){
|
|
7
7
|
const {escrowTerms}=require('./index.cjs');const t=escrowTerms(job,configuration),p=job.negotiation?.current;
|
|
8
|
-
check(p&&job.negotiation.status
|
|
8
|
+
check(p&&['open','accepted'].includes(job.negotiation.status),'Open settlement proposal required');
|
|
9
9
|
const canonical=settlementProposal(job,{worker_amount:p.worker_amount,payer_amount:p.payer_amount,expected_revision:p.escrow_version-1,expires_at:p.expires_at,reason:p.reason,claims:p.claims});
|
|
10
10
|
check(canonical.proposal_hash===p.proposal_hash,'Stored proposal claims or allocation changed');
|
|
11
11
|
return {domain:{name:'A423Escrow',version:'1',chainId:configuration.chainId,verifyingContract:getAddress(configuration.contractAddress)},types:TYPES,value:{jobId:t.jobId,agreementHash:t.agreementHash,escrowVersion:p.escrow_version,workerAmount:BigInt(p.worker_amount),payerAmount:BigInt(p.payer_amount),proposalHash:p.proposal_hash,expiresAt:p.expires_at}};
|
package/src/escrow/services.cjs
CHANGED
|
@@ -5,7 +5,7 @@ const {validateEscrowEvidence}=require('../operations/index.cjs');
|
|
|
5
5
|
function check(ok,message){if(!ok)throw Error(message);}
|
|
6
6
|
function createEscrowRegistry(configurations){
|
|
7
7
|
const map=new Map();for(const c of configurations){const key=c.chainId+':'+getAddress(c.contractAddress).toLowerCase();check(!map.has(key),'Duplicate escrow configuration');map.set(key,createEscrowAdapters(c));}
|
|
8
|
-
const actions=['fund','start','submit','verify','release','refund','dispute','resolve','timeout','propose_settlement','counter_settlement','accept_settlement'];const result={};for(const action of actions){result[action]={};for(const method of ['execute','reconcile'])result[action][method]=ctx=>{const e=ctx.job.agreement?.terms.escrow;const adapter=map.get(e?.chain_id+':'+getAddress(e?.contract).toLowerCase());check(adapter,'Escrow not registered');return adapter[action][method](ctx);};}return result;
|
|
8
|
+
const actions=['fund','start','submit','verify','release','refund','dispute','resolve','timeout','propose_settlement','counter_settlement','accept_settlement','approve_cancellation','execute_cancellation'];const result={};for(const action of actions){result[action]={};for(const method of ['execute','reconcile','rejectPending'])result[action][method]=ctx=>{const e=ctx.job.agreement?.terms.escrow;const adapter=map.get(e?.chain_id+':'+getAddress(e?.contract).toLowerCase());check(adapter,'Escrow not registered');check(typeof adapter[action][method]==='function','Recovery unsupported for this action');return adapter[action][method](ctx);};}return result;
|
|
9
9
|
}
|
|
10
10
|
/** Fresh historical evidence verification. Returns invalid, insufficient_confirmations, or valid; RPC failures throw. */
|
|
11
11
|
function createEvidenceRevalidator({provider,chainId,contractAddress,tokenAddress,confirmations}={}){
|
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const {createWorkflowHandler}=require('../workflow.cjs');
|
|
3
|
-
function createChatTools({workflow,store,queue,actor}={}){
|
|
3
|
+
function createChatTools({workflow,store,queue,actor,profitFor,policyPromptFor}={}){
|
|
4
4
|
const {enqueueRunningJobs}=require('./index.cjs');
|
|
5
5
|
const act=(action)=>async({job_id,input={},idempotency_key})=>workflow.act(job_id,action,input,{actor,idempotencyKey:idempotency_key});
|
|
6
6
|
return {
|
|
7
|
+
list_jobs:async()=>Promise.all((await store.list({actor,limit:1000})).map(async job=>profitFor?{...job,profit:await profitFor(job,actor)}:job)),
|
|
8
|
+
inspect_settlement_policy:async({job_id})=>{const job=await workflow.get(job_id,actor);if(!profitFor)throw Error('Settlement policy not configured');return {profit:await profitFor(job,actor),prompt:policyPromptFor?policyPromptFor(job,actor):null};},
|
|
9
|
+
reconcile_job:async({job_id})=>workflow.reconcile(job_id,actor),
|
|
7
10
|
send_job_offer:async({id,worker_id,verifier_id,arbitrator_id,terms,metadata={}})=>{
|
|
8
11
|
let job=await workflow.create({id,participants:{requester:actor,worker:worker_id,verifier:verifier_id,arbitrator:arbitrator_id},metadata},actor);
|
|
9
12
|
if(terms)job=await workflow.act(job.id,'quote',{terms},{actor,idempotencyKey:'initial_quote'});return job;
|
|
10
13
|
},
|
|
11
|
-
propose_quote:act('quote'),accept_quote:act('accept'),fund_escrow:act('fund'),submit_job:act('submit'),verify_job:act('verify'),release_escrow:act('release'),refund_escrow:act('refund'),dispute_job:act('dispute'),resolve_job:act('resolve'),timeout_job:act('timeout'),propose_settlement:act('propose_settlement'),counter_settlement:act('counter_settlement'),accept_settlement:act('accept_settlement'),request_cancellation:act('propose_settlement'),cancel_job:act('cancel'),
|
|
14
|
+
propose_quote:act('quote'),accept_quote:act('accept'),fund_escrow:act('fund'),submit_job:act('submit'),verify_job:act('verify'),release_escrow:act('release'),refund_escrow:act('refund'),dispute_job:act('dispute'),resolve_job:act('resolve'),timeout_job:act('timeout'),propose_settlement:act('propose_settlement'),counter_settlement:act('counter_settlement'),accept_settlement:act('accept_settlement'),accept_cancellation:async({job_id,input={},idempotency_key})=>workflow.acceptCancellation(job_id,input,{actor,idempotencyKey:idempotency_key}),execute_cancellation:act('execute_cancellation'),request_cancellation:act('propose_settlement'),cancel_job:act('cancel'),decline_job:act('decline'),reopen_job:act('reopen'),
|
|
12
15
|
work_on_job:async(input)=>{const job=await act('start')(input);if(!job.pending&&job.state==='running')await enqueueRunningJobs({store,queue});return job;}
|
|
13
16
|
};
|
|
14
17
|
}
|
|
15
|
-
function createApplicationHandler({workflow,store,queue,authenticate,revalidate,consentFor,settlementConsentFor,legacyState=async()=>({}),authorityHandler,basePath='/api/a423'}={}){
|
|
18
|
+
function createApplicationHandler({workflow,store,queue,authenticate,revalidate,consentFor,settlementConsentFor,profitFor,legacyState=async()=>({}),authorityHandler,forwardCancellations=true,basePath='/api/a423'}={}){
|
|
16
19
|
const {createEventStream,freshReceipts,renderReceiptPdf}=require('./index.cjs');
|
|
17
|
-
const protocol=createWorkflowHandler({workflow,authenticate,basePath
|
|
20
|
+
const protocol=createWorkflowHandler({workflow,authenticate,basePath,forwardCancellations,revalidateCancellationReceipt:async({job,receipt})=>{
|
|
21
|
+
const {validateCancellationReceipt}=require('../cancellation.cjs');validateCancellationReceipt(job,receipt);
|
|
22
|
+
try{const check=await revalidate(job,receipt.evidence);if(check.status!=='valid')throw Error('Evidence is not valid');}catch{const {A423Error}=require('../index.cjs');throw new A423Error('EVIDENCE_UNAVAILABLE','Cancellation receipt evidence is unavailable or changed',503);}
|
|
23
|
+
}});const events=createEventStream({store,authenticate});
|
|
18
24
|
return async request=>{
|
|
19
25
|
const path=new URL(request.url).pathname;
|
|
20
26
|
if(path.startsWith('/api/a423-auth/')&&authorityHandler)return authorityHandler(request);
|
|
@@ -24,11 +30,13 @@ function createApplicationHandler({workflow,store,queue,authenticate,revalidate,
|
|
|
24
30
|
const actor=await authenticate(request);if(!actor)return Response.json({error:'unauthenticated'},{status:401});
|
|
25
31
|
try{
|
|
26
32
|
if(path==='/api/state'){
|
|
27
|
-
const jobs=await Promise.all((await store.list({actor,limit:1000})).map(async job=>({...job,safety:await store.getSafety(job.id)})));const wallet_transactions=jobs.flatMap(j=>j.evidence.filter(e=>e.evidence.transaction_hash).map(e=>({job_id:j.id,action:e.action,...e.evidence})));
|
|
33
|
+
const jobs=await Promise.all((await store.list({actor,limit:1000})).map(async job=>({...job,profit:profitFor?await profitFor(job,actor):null,safety:await store.getSafety(job.id)})));const wallet_transactions=jobs.flatMap(j=>j.evidence.filter(e=>e.evidence.transaction_hash).map(e=>({job_id:j.id,action:e.action,...e.evidence})));
|
|
28
34
|
return Response.json({...await legacyState(actor),a423:{jobs,wallet_transactions}},{headers:{'cache-control':'no-store'}});
|
|
29
35
|
}
|
|
36
|
+
const profit=/^\/api\/a423-profit\/([A-Za-z0-9_-]{1,128})$/.exec(path);
|
|
37
|
+
if(profit){const job=await workflow.get(profit[1],actor);if(!profitFor)return Response.json({error:'POLICY_NOT_CONFIGURED'},{status:404});return Response.json(await profitFor(job,actor),{headers:{'cache-control':'no-store'}});}
|
|
30
38
|
const receipt=/^\/api\/a423-receipts\/([A-Za-z0-9_-]{1,128})(\.pdf)?$/.exec(path);
|
|
31
|
-
if(receipt){const data=await freshReceipts({workflow,id:receipt[1],actor,revalidate});if(receipt[2])return new Response(Buffer.from(await renderReceiptPdf(data)),{headers:{'content-type':'application/pdf','cache-control':'no-store'}});return Response.json(data,{headers:{'cache-control':'no-store'}});}
|
|
39
|
+
if(receipt){const data=await freshReceipts({workflow,id:receipt[1],actor,revalidate});if(profitFor)data.profit=await profitFor(await workflow.get(receipt[1],actor),actor);if(receipt[2])return new Response(Buffer.from(await renderReceiptPdf(data)),{headers:{'content-type':'application/pdf','cache-control':'no-store'}});return Response.json(data,{headers:{'cache-control':'no-store'}});}
|
|
32
40
|
const closing=/^\/api\/a423-settlement-consent\/([A-Za-z0-9_-]{1,128})$/.exec(path);
|
|
33
41
|
if(closing){const job=await workflow.get(closing[1],actor);if(![job.participants.requester,job.participants.worker].includes(actor))return Response.json({error:'settlement party required'},{status:403});if(!settlementConsentFor)throw Error('Settlement consent not configured');const typed=await settlementConsentFor(job);return Response.json(JSON.parse(JSON.stringify(typed,(_,v)=>typeof v==='bigint'?v.toString():v)),{headers:{'cache-control':'no-store'}});}
|
|
34
42
|
const consent=/^\/api\/a423-worker-consent\/([A-Za-z0-9_-]{1,128})$/.exec(path);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const {createWorkflow,hashAgreement}=require('../workflow.cjs');
|
|
3
3
|
const {createCredentialAuthenticator,createAuthorityHandler}=require('../authority/index.cjs');
|
|
4
|
-
function createEscrowApplication({storage,authority,escrows,signerFor,addressFor,limitsFor,approveSigning,validateArtifact,execute,keeperEnabled=false,allowAuthorityRequest=async()=>false,requireIndependentArbitrator=true}={}){
|
|
4
|
+
function createEscrowApplication({storage,authority,escrows,signerFor,addressFor,limitsFor,approveSigning,validateArtifact,execute,settlementPolicy,keeperEnabled=false,forwardCancellations=true,allowAuthorityRequest=async()=>false,requireIndependentArbitrator=true}={}){
|
|
5
5
|
const {createEscrowRegistry,createEvidenceRevalidator,escrowTerms,workerConsent,settlementConsent,escrowTransitions}=require('../escrow/index.cjs');
|
|
6
6
|
const {validateTask,validateEscrowEvidence,createSpendingAuthorizer,createExpiryAdapter,createApplicationHandler,createChatTools}=require('./index.cjs');
|
|
7
7
|
if(!storage||!authority||!Array.isArray(escrows)||!escrows.length||typeof approveSigning!=='function'||typeof limitsFor!=='function')throw new TypeError('Explicit storage, authority, escrow configurations and spending/signing approvals required');
|
|
8
|
+
const profit=settlementPolicy?require('../policy/index.cjs').createProfitSettlementService({...settlementPolicy,kv:storage.kv}):null;
|
|
8
9
|
const registry=new Map(escrows.map(c=>[String(c.chainId)+':'+c.contractAddress.toLowerCase(),c]));
|
|
9
10
|
const configFor=job=>{const e=job.agreement?.terms.escrow;const c=registry.get(e?.chain_id+':'+e?.contract?.toLowerCase());if(!c)throw Error('Unregistered escrow');return c;};
|
|
10
11
|
const checkPolicy=async context=>{
|
|
@@ -17,17 +18,18 @@ function createEscrowApplication({storage,authority,escrows,signerFor,addressFor
|
|
|
17
18
|
const authorizeTransaction=createSpendingAuthorizer({spending:storage.spending,limitsFor,authorize:checkPolicy});
|
|
18
19
|
const adapters=createEscrowRegistry(escrows.map(c=>({...c,signerFor,addressFor,transactions:storage.transactions,authorizeTransaction,validateArtifact})));
|
|
19
20
|
const workflow=createWorkflow({store:storage.jobs,adapters:{...adapters,expire:createExpiryAdapter()},transitions:escrowTransitions,
|
|
20
|
-
|
|
21
|
+
prepareInput:profit?async context=>{if(await authority.isActive(context.actor)!==true){const {A423Error}=require('../index.cjs');throw new A423Error('FORBIDDEN','Active identity required',403);}return profit.prepare(context);}:undefined,
|
|
22
|
+
authorize:async context=>await authority.isActive(context.actor)===true&&(context.phase==='reconcile'||!profit||await profit.authorize(context)),
|
|
21
23
|
validateTerms:terms=>{validateTask(terms.task);const job={id:'terms_validation',agreement:{terms,hash:hashAgreement(terms)}};escrowTerms(job,configFor(job));},
|
|
22
24
|
validateEvidence:({evidence})=>{if(evidence.transaction_hash)validateEscrowEvidence(evidence);}
|
|
23
25
|
});
|
|
24
26
|
const validators=new Map(escrows.map(c=>[String(c.chainId)+':'+c.contractAddress.toLowerCase(),createEvidenceRevalidator(c)]));
|
|
25
27
|
const revalidate=(job,evidence)=>validators.get(String(configFor(job).chainId)+':'+configFor(job).contractAddress.toLowerCase())(job,evidence);
|
|
26
28
|
const authenticate=createCredentialAuthenticator({authority});
|
|
27
|
-
const applicationOptions={workflow,store:storage.jobs,queue:storage.queue,authenticate,revalidate,consentFor:async job=>workerConsent(job,configFor(job)),settlementConsentFor:async job=>settlementConsent(job,configFor(job)),authorityHandler:createAuthorityHandler({authority,allowRequest:allowAuthorityRequest})};
|
|
29
|
+
const applicationOptions={profitFor:profit?(job,actor)=>profit.project(job,actor,{revalidate}):undefined,forwardCancellations,workflow,store:storage.jobs,queue:storage.queue,authenticate,revalidate,consentFor:async job=>workerConsent(job,configFor(job)),settlementConsentFor:async job=>settlementConsent(job,configFor(job)),authorityHandler:createAuthorityHandler({authority,allowRequest:allowAuthorityRequest})};
|
|
28
30
|
const workerOptions={authority,actorFor:async job=>job.pending?.actor||job.participants.requester,allowExecute:keeperEnabled,
|
|
29
31
|
execute,beforeExecute:async job=>{await workflow.assertSafe(job.id,job.participants.worker);if(!await authority.isActive(job.participants.worker))return false;const result=await adapters.start.execute({job,input:{},operation:{actor:job.participants.worker}});return result.status==='confirmed';}
|
|
30
32
|
};
|
|
31
|
-
return {workflow,applicationOptions,workerOptions,handler:createApplicationHandler(applicationOptions),chatTools:actor=>createChatTools({workflow,store:storage.jobs,queue:storage.queue,actor})};
|
|
33
|
+
return {profit,workflow,applicationOptions,workerOptions,handler:createApplicationHandler(applicationOptions),chatTools:actor=>createChatTools({workflow,store:storage.jobs,queue:storage.queue,actor,profitFor:applicationOptions.profitFor,policyPromptFor:profit?(job,actor)=>profit.prompt({role:actor===job.participants.worker?'worker':'requester'}):undefined})};
|
|
32
34
|
}
|
|
33
35
|
module.exports={createEscrowApplication};
|
package/src/operations/index.cjs
CHANGED
|
@@ -39,6 +39,9 @@ async function runMaintenanceOnce({workflow,store,actorFor,allowExecute=false,no
|
|
|
39
39
|
const actor=await actorFor(job);if(!actor)continue;
|
|
40
40
|
let result=job;
|
|
41
41
|
if(job.pending){result=await workflow.reconcile(job.id,actor);if(result.pending&&allowExecute)result=await workflow.resume(job.id,actor);}
|
|
42
|
+
else if((job.subflows||[]).some(s=>s.kind==='cancellation'&&s.status==='accepted')&&allowExecute){
|
|
43
|
+
const subflow=job.subflows.find(s=>s.kind==='cancellation');result=await workflow.act(job.id,'execute_cancellation',{instruction_id:subflow.instruction.id},{actor,idempotencyKey:'cancel_forward_'+hash(JSON.stringify(subflow.instruction.id))});
|
|
44
|
+
}
|
|
42
45
|
else if(job.agreement){const e=job.agreement.terms.escrow;const seconds=Math.floor(now/1000);let action;
|
|
43
46
|
if(e&&(['funded','running'].includes(job.state)||(['settlement_negotiation','disputed'].includes(job.state)&&!job.submission))&&seconds>e.work_deadline)action='timeout';
|
|
44
47
|
if(e&&['submitted','verified','disputed','settlement_negotiation'].includes(job.state)&&seconds>e.resolution_deadline)action='timeout';
|
|
@@ -104,3 +107,10 @@ module.exports.migrateUnfundedLegacy=migrateUnfundedLegacy;
|
|
|
104
107
|
Object.assign(module.exports,require('./application.cjs'));
|
|
105
108
|
|
|
106
109
|
Object.assign(module.exports,require('./factory.cjs'));
|
|
110
|
+
|
|
111
|
+
async function freshCancellationReceipt({workflow,id,actor,revalidate}){
|
|
112
|
+
const job=await workflow.get(id,actor),receipt=await workflow.cancellationReceipt(id,actor);
|
|
113
|
+
require('../cancellation.cjs').validateCancellationReceipt(job,receipt);
|
|
114
|
+
const check=await revalidate(job,receipt.evidence);assert(check.status==='valid','Cancellation receipt evidence is not currently valid');return receipt;
|
|
115
|
+
}
|
|
116
|
+
module.exports.freshCancellationReceipt=freshCancellationReceipt;
|
|
@@ -23,7 +23,9 @@ export function freshReceipts(options: {workflow: Workflow;id: string;actor: str
|
|
|
23
23
|
export function renderReceiptPdf(receipt: object): Promise<Uint8Array>;
|
|
24
24
|
export function planLegacyMigration(records: Array<{id: string;state: string}>): Array<{id: string;state: string;automatic: false;reason: string}>;
|
|
25
25
|
export function migrateUnfundedLegacy(options: {records: Array<{id: string;state: string}>;workflow: Workflow;actor: string;map(source: any): Promise<Parameters<Workflow['create']>[0]>}): Promise<WorkflowJob[]>;
|
|
26
|
-
export function createChatTools(options: {workflow: Workflow;store: SqliteJobStore;queue: DurableQueue;actor: string}): Record<string,(input: any) => Promise<
|
|
27
|
-
export function createApplicationHandler(options: {workflow: Workflow;store: SqliteJobStore;queue: DurableQueue;authenticate(request: Request): Promise<string | null>;revalidate: Revalidator;consentFor?(job: WorkflowJob): Promise<object>;settlementConsentFor?(job: WorkflowJob): Promise<object>;legacyState?(actor: string): Promise<object>;authorityHandler?(request: Request): Promise<Response>;basePath?: string}): (request: Request) => Promise<Response>;
|
|
26
|
+
export function createChatTools(options: {workflow: Workflow;store: SqliteJobStore;queue: DurableQueue;actor: string;profitFor?(job: WorkflowJob,actor: string): Promise<object | null>;policyPromptFor?(job: WorkflowJob,actor: string): string}): Record<string,(input: any) => Promise<any>>;
|
|
27
|
+
export function createApplicationHandler(options: {workflow: Workflow;store: SqliteJobStore;queue: DurableQueue;authenticate(request: Request): Promise<string | null>;revalidate: Revalidator;consentFor?(job: WorkflowJob): Promise<object>;settlementConsentFor?(job: WorkflowJob): Promise<object>;profitFor?(job: WorkflowJob,actor: string): Promise<object | null>;legacyState?(actor: string): Promise<object>;authorityHandler?(request: Request): Promise<Response>;forwardCancellations?: boolean;basePath?: string}): (request: Request) => Promise<Response>;
|
|
28
28
|
|
|
29
|
-
export function createEscrowApplication(options: {storage: import('../storage/index.js').SqliteStorage;authority: import('../authority/index.js').Authority;escrows: Array<any>;signerFor(actor: string): Promise<any>;addressFor(actor: string): Promise<string>;limitsFor(context: SigningContext): Promise<{perTransaction: string;daily: string;total: string}>;approveSigning(context: SigningContext): Promise<boolean>;validateArtifact(context: {job: WorkflowJob;resultHash: string}): Promise<boolean>;execute?(context: {job: WorkflowJob;idempotencyKey: string;signal: AbortSignal}): Promise<{result_hash: string;[key: string]: Json}>;keeperEnabled?: boolean;allowAuthorityRequest?(request: Request): Promise<boolean>;requireIndependentArbitrator?: boolean}): {workflow: Workflow;applicationOptions: Parameters<typeof createApplicationHandler>[0];workerOptions: any;handler: (request: Request) => Promise<Response>;chatTools(actor: string): ReturnType<typeof createChatTools>};
|
|
29
|
+
export function createEscrowApplication(options: {storage: import('../storage/index.js').SqliteStorage;authority: import('../authority/index.js').Authority;escrows: Array<any>;signerFor(actor: string): Promise<any>;addressFor(actor: string): Promise<string>;limitsFor(context: SigningContext): Promise<{perTransaction: string;daily: string;total: string}>;approveSigning(context: SigningContext): Promise<boolean>;validateArtifact(context: {job: WorkflowJob;resultHash: string}): Promise<boolean>;execute?(context: {job: WorkflowJob;idempotencyKey: string;signal: AbortSignal}): Promise<{result_hash: string;[key: string]: Json}>;settlementPolicy?: Omit<import('../policy/index.js').ProfitServiceOptions, 'kv'>;keeperEnabled?: boolean;forwardCancellations?: boolean;allowAuthorityRequest?(request: Request): Promise<boolean>;requireIndependentArbitrator?: boolean}): {profit: ReturnType<typeof import('../policy/index.js').createProfitSettlementService> | null;workflow: Workflow;applicationOptions: Parameters<typeof createApplicationHandler>[0];workerOptions: any;handler: (request: Request) => Promise<Response>;chatTools(actor: string): ReturnType<typeof createChatTools>};
|
|
30
|
+
|
|
31
|
+
export function freshCancellationReceipt(options: {workflow: Workflow;id: string;actor: string;revalidate: Revalidator}): Promise<import('../workflow.js').CancellationReceipt>;
|