@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/src/escrow/index.cjs
CHANGED
|
@@ -6,9 +6,15 @@ 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
|
-
|
|
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},
|
|
11
|
+
propose_settlement: {from:['funded','running','submitted','verified'],roles:['requester','worker'],to:'settlement_negotiation',effect:true},
|
|
12
|
+
counter_settlement: {from:['settlement_negotiation'],roles:['requester','worker'],to:'settlement_negotiation',effect:true},
|
|
13
|
+
accept_settlement: {from:['settlement_negotiation'],roles:['requester','worker'],to:'settled',effect:true},
|
|
14
|
+
refund: {from:['funded','running','settlement_negotiation','disputed'],roles:['requester'],to:'refunded',effect:true},
|
|
15
|
+
dispute: {from:['funded','running','submitted','verified','settlement_negotiation'],roles:['requester','worker','verifier'],to:'disputed',effect:true},
|
|
10
16
|
resolve: {from:['disputed'],roles:['arbitrator'],to:'settled',effect:true},
|
|
11
|
-
timeout: {from:['funded','running','submitted','verified','disputed'],roles:['requester','worker','verifier','arbitrator'],to:'settled',effect:true}
|
|
17
|
+
timeout: {from:['funded','running','submitted','verified','disputed','settlement_negotiation'],roles:['requester','worker','verifier','arbitrator'],to:'settled',effect:true}
|
|
12
18
|
};
|
|
13
19
|
function check(condition,message) { if(!condition) throw new Error('Escrow: '+message); }
|
|
14
20
|
function positive(value,name) { check(typeof value==='string' && /^[1-9][0-9]*$/.test(value),name+' must be a positive decimal string');const n=BigInt(value);check(n<(1n<<256n),name+' exceeds uint256');return n; }
|
|
@@ -108,12 +114,71 @@ function createEscrowAdapters({provider,chainId,contractAddress,tokenAddress,sig
|
|
|
108
114
|
const event=logs.find(log=>log?.name===eventName && log.args.jobId===t.jobId);
|
|
109
115
|
check(event,'matching '+eventName+' event missing');
|
|
110
116
|
const snapshot=await contract.getJob(t.jobId,{blockTag:r.blockNumber});match(snapshot,t);validate(snapshot,event);
|
|
111
|
-
return {status:'confirmed',evidence:require('../operations/index.cjs').validateEscrowEvidence({chain_id:String(chainId),contract:config.contractAddress,token:config.tokenAddress,job_id:t.jobId,agreement_hash:t.agreementHash,transaction_hash:r.hash,block_number:r.blockNumber,block_hash:r.blockHash,confirmations,event:eventName,settlement_reason:eventName==='Settled'?Number(event.args.reason):null,reason_hash:eventName==='Disputed'?event.args.reasonHash:null,amount:t.amount.toString(),payer:t.payer,worker:t.worker,verifier:t.verifier,arbitrator:t.arbitrator,result_hash:snapshot.resultHash,worker_paid:snapshot.workerPaid.toString(),state:Number(snapshot.state),observed_at:new Date().toISOString()})};
|
|
117
|
+
return {status:'confirmed',evidence:require('../operations/index.cjs').validateEscrowEvidence({chain_id:String(chainId),contract:config.contractAddress,token:config.tokenAddress,job_id:t.jobId,agreement_hash:t.agreementHash,transaction_hash:r.hash,block_number:r.blockNumber,block_hash:r.blockHash,confirmations,event:eventName,escrow_version:Number(snapshot.proposal.revision),proposal_hash:snapshot.proposal.proposalHash,proposal_worker_amount:snapshot.proposal.workerAmount.toString(),proposal_expires_at:Number(snapshot.proposal.expiresAt),proposal_proposer:snapshot.proposal.proposer,settlement_reason:eventName==='Settled'?Number(event.args.reason):null,reason_hash:eventName==='Disputed'?event.args.reasonHash:null,amount:t.amount.toString(),payer:t.payer,worker:t.worker,verifier:t.verifier,arbitrator:t.arbitrator,result_hash:snapshot.resultHash,worker_paid:snapshot.workerPaid.toString(),state:Number(snapshot.state),observed_at:new Date().toISOString()})};
|
|
112
118
|
}
|
|
113
119
|
function adapter(run){return {execute:ctx=>run(ctx,false),reconcile:ctx=>run(ctx,true)};}
|
|
114
120
|
const state=(s,n)=>check(Number(s.state)===n,'unexpected escrow state');
|
|
115
121
|
const settled=(s,e,t)=>{check([5,6,7].includes(Number(s.state)),'not settled');check(e.args.workerAmount+e.args.payerAmount===t.amount && e.args.workerAmount===s.workerPaid,'settlement amount mismatch');};
|
|
122
|
+
async function propose(ctx,reconcile){
|
|
123
|
+
const {settlementProposal}=require('./negotiation.cjs');
|
|
124
|
+
const t=await context(ctx.job);let proposal;
|
|
125
|
+
try {proposal=settlementProposal(ctx.job,ctx.input);}catch(error){return {status:'rejected',reason:error.message};}
|
|
126
|
+
return observe(ctx,t,'propose_settlement','proposeSettlement',[t.jobId,ctx.input.expected_revision,BigInt(proposal.worker_amount),proposal.proposal_hash,proposal.expires_at],'SettlementProposed',(s,e)=>{
|
|
127
|
+
check(Number(s.state)===8,'negotiation is not active');
|
|
128
|
+
check(Number(e.args.revision)===proposal.escrow_version && e.args.proposalHash===proposal.proposal_hash && e.args.workerAmount===BigInt(proposal.worker_amount) && e.args.payerAmount===BigInt(proposal.payer_amount) && Number(e.args.expiresAt)===proposal.expires_at && getAddress(e.args.proposer)===getAddress(ctx.settlementActorAddress), 'proposal event mismatch');
|
|
129
|
+
},reconcile);
|
|
130
|
+
}
|
|
131
|
+
const proposeAdapter=adapter(async(ctx,r)=>{ctx.settlementActorAddress=getAddress(await addressFor(ctx.operation.actor));return propose(ctx,r);});
|
|
116
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
|
+
}),
|
|
163
|
+
propose_settlement:proposeAdapter,
|
|
164
|
+
counter_settlement:proposeAdapter,
|
|
165
|
+
accept_settlement:adapter(async(ctx,r)=>{
|
|
166
|
+
const {settlementConsent}=require('./negotiation.cjs');const t=await context(ctx.job);const p=ctx.job.negotiation?.current;
|
|
167
|
+
if(!p || ctx.input.expected_revision!==p.escrow_version || ctx.input.proposal_hash!==p.proposal_hash)return {status:'rejected',reason:'Accept the exact current proposal hash and revision'};
|
|
168
|
+
let consent;try{consent=settlementConsent(ctx.job,config);}catch(error){return {status:'rejected',reason:error.message};}
|
|
169
|
+
let method='acceptSettlement',args=[t.jobId,p.escrow_version,p.proposal_hash];
|
|
170
|
+
if(ctx.input.payer_signature!==undefined || ctx.input.worker_signature!==undefined){
|
|
171
|
+
if(typeof ctx.input.payer_signature!=='string'||typeof ctx.input.worker_signature!=='string')return {status:'rejected',reason:'Both party signatures required for relayed settlement'};
|
|
172
|
+
method='settleByConsent';args=[consent.value,ctx.input.payer_signature,ctx.input.worker_signature];
|
|
173
|
+
}else if(ctx.job.negotiation.current.proposer_id===ctx.operation.actor)return {status:'rejected',reason:'Counterparty must accept; proposer cannot self-accept'};
|
|
174
|
+
const result=await observe(ctx,t,'accept_settlement',method,args,'Settled',(s,e)=>{settled(s,e,t);check(Number(e.args.reason)===4 && e.args.workerAmount===BigInt(p.worker_amount) && e.args.payerAmount===BigInt(p.payer_amount) && Number(s.proposal.revision)===p.escrow_version && s.proposal.proposalHash===p.proposal_hash,'closing allocation mismatch');},r);
|
|
175
|
+
if(result.status==='confirmed'){
|
|
176
|
+
const receipt=await provider.getTransactionReceipt(result.evidence.transaction_hash);
|
|
177
|
+
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);
|
|
178
|
+
check(accepted&&Number(accepted.args.revision)===p.escrow_version&&accepted.args.proposalHash===p.proposal_hash,'matching settlement acceptance missing');
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}),
|
|
117
182
|
fund:adapter(async(ctx,reconcile)=>{
|
|
118
183
|
const t=await context(ctx.job);
|
|
119
184
|
check([ctx.job.participants.requester,ctx.job.participants.worker].every(p=>ctx.job.agreement.accepted_by.includes(p)),'both acceptances required');
|
|
@@ -142,9 +207,9 @@ function createEscrowAdapters({provider,chainId,contractAddress,tokenAddress,sig
|
|
|
142
207
|
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);}),
|
|
143
208
|
dispute:adapter(async(ctx,r)=>{const t=await context(ctx.job);const reason=bytes32(ctx.input.reason_hash);return observe(ctx,t,'dispute','dispute',[t.jobId,reason],'Disputed',(s,e)=>{check(Number(s.state)>=4,'missing dispute');check(e.args.reasonHash===reason,'reason mismatch');},r);}),
|
|
144
209
|
resolve:adapter(async(ctx,r)=>{const t=await context(ctx.job);check(typeof ctx.input.worker_amount==='string' && /^(0|[1-9][0-9]*)$/.test(ctx.input.worker_amount),'worker_amount must be decimal string');const amount=BigInt(ctx.input.worker_amount);check(amount<=t.amount,'resolution exceeds escrow');return observe(ctx,t,'resolve','resolve',[t.jobId,amount],'Settled',(s,e)=>{settled(s,e,t);check(e.args.workerAmount===amount && Number(e.args.reason)===2,'resolution mismatch');},r);}),
|
|
145
|
-
timeout:adapter(async(ctx,r)=>{const t=await context(ctx.job);const
|
|
210
|
+
timeout:adapter(async(ctx,r)=>{const t=await context(ctx.job);const snapshot=await contract.getJob(t.jobId);const method=snapshot.resultHash===ZeroHash?'refundExpired':'timeout';return observe(ctx,t,'timeout',method,[t.jobId],'Settled',(s,e)=>{settled(s,e,t);const expected=method==='refundExpired'?0n:(t.amount*BigInt(t.timeoutWorkerBps))/10000n;check(e.args.workerAmount===expected && Number(e.args.reason)===(method==='refundExpired'?1:3),'timeout allocation mismatch');},r);})
|
|
146
211
|
};
|
|
147
212
|
}
|
|
148
213
|
module.exports={createEscrowAdapters,createMemoryTransactionStore,workerConsent,escrowTerms,escrowTransitions:transitions,ESCROW_ABI:ABI,ESCROW_BYTECODE:artifact.bytecode};
|
|
149
214
|
|
|
150
|
-
Object.assign(module.exports,require('./services.cjs'));
|
|
215
|
+
Object.assign(module.exports,require('./services.cjs'),require('./negotiation.cjs'),require('./cancellation.cjs'));
|
package/src/escrow/index.d.mts
CHANGED
|
@@ -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', 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): {
|
|
@@ -46,3 +46,10 @@ export function replacePendingTransaction(options: {id: string;transactions: Tra
|
|
|
46
46
|
export function createFeeReplacementWorker(options: {transactions: TransactionStore;provider: Provider;signerForRecord(record: TransactionRecord): Promise<Signer>;authorize(context: any): Promise<boolean>;minAgeMs?: number;maxAttempts?: number;maxFeePerGas: bigint;maxPriorityFeePerGas: bigint}): () => Promise<any[]>;
|
|
47
47
|
|
|
48
48
|
export function createEscrowIndexer(options: {provider: Provider;chainId: number;contractAddress: string;confirmations: number;fromBlock: number;kv: import('../storage/index.js').KeyValueStore;onEvent(event: any): Promise<void>;onReorg(event: any): Promise<void>;batchSize?: number}): () => Promise<{indexed: number;through?: number}>;
|
|
49
|
+
|
|
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
|
+
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
|
@@ -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', 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): {
|
|
@@ -46,3 +46,10 @@ export function replacePendingTransaction(options: {id: string;transactions: Tra
|
|
|
46
46
|
export function createFeeReplacementWorker(options: {transactions: TransactionStore;provider: Provider;signerForRecord(record: TransactionRecord): Promise<Signer>;authorize(context: any): Promise<boolean>;minAgeMs?: number;maxAttempts?: number;maxFeePerGas: bigint;maxPriorityFeePerGas: bigint}): () => Promise<any[]>;
|
|
47
47
|
|
|
48
48
|
export function createEscrowIndexer(options: {provider: Provider;chainId: number;contractAddress: string;confirmations: number;fromBlock: number;kv: import('../storage/index.js').KeyValueStore;onEvent(event: any): Promise<void>;onReorg(event: any): Promise<void>;batchSize?: number}): () => Promise<{indexed: number;through?: number}>;
|
|
49
|
+
|
|
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
|
+
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 } = 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;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const {getAddress}=require('ethers');
|
|
3
|
+
const TYPES={ClosingSettlement:[['jobId','bytes32'],['agreementHash','bytes32'],['escrowVersion','uint64'],['workerAmount','uint256'],['payerAmount','uint256'],['proposalHash','bytes32'],['expiresAt','uint64']].map(([name,type])=>({name,type}))};
|
|
4
|
+
function check(ok,message){if(!ok)throw new TypeError(message);}
|
|
5
|
+
const {settlementProposal}=require('../closing.cjs');
|
|
6
|
+
function settlementConsent(job,configuration){
|
|
7
|
+
const {escrowTerms}=require('./index.cjs');const t=escrowTerms(job,configuration),p=job.negotiation?.current;
|
|
8
|
+
check(p&&['open','accepted'].includes(job.negotiation.status),'Open settlement proposal required');
|
|
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
|
+
check(canonical.proposal_hash===p.proposal_hash,'Stored proposal claims or allocation changed');
|
|
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}};
|
|
12
|
+
}
|
|
13
|
+
module.exports={settlementProposal,settlementConsent};
|
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'];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'])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;
|
|
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}={}){
|
|
@@ -31,6 +31,9 @@ function createEvidenceRevalidator({provider,chainId,contractAddress,tokenAddres
|
|
|
31
31
|
if(event.name==='Funded'&&(event.args.amount!==t.amount||getAddress(event.args.payer)!==t.payer||getAddress(event.args.worker)!==t.worker||event.args.agreementHash!==t.agreementHash))return invalid('Funding fields mismatch');
|
|
32
32
|
if(['Submitted','Verified'].includes(event.name)&&event.args.resultHash!==snapshot.resultHash)return invalid('Artifact mismatch');
|
|
33
33
|
if(event.name==='Disputed'&&event.args.reasonHash!==evidence.reason_hash)return invalid('Dispute reason mismatch');
|
|
34
|
+
if(event.name==='SettlementProposed'&&(Number(event.args.revision)!==evidence.escrow_version||event.args.proposalHash!==evidence.proposal_hash||event.args.workerAmount.toString()!==evidence.proposal_worker_amount||Number(event.args.expiresAt)!==evidence.proposal_expires_at||getAddress(event.args.proposer)!==getAddress(evidence.proposal_proposer)))return invalid('Proposal fields mismatch');
|
|
35
|
+
if(evidence.escrow_version!==undefined && (Number(snapshot.proposal.revision)!==evidence.escrow_version||snapshot.proposal.proposalHash!==evidence.proposal_hash||snapshot.proposal.workerAmount.toString()!==evidence.proposal_worker_amount||Number(snapshot.proposal.expiresAt)!==evidence.proposal_expires_at||getAddress(snapshot.proposal.proposer)!==getAddress(evidence.proposal_proposer)))return invalid('Cached proposal mismatch');
|
|
36
|
+
if(event.name==='Settled'&&Number(event.args.reason)===4){const acceptance=logs.find(l=>l?.name==='SettlementAccepted'&&l.args.jobId===t.jobId);if(!acceptance||Number(acceptance.args.revision)!==evidence.escrow_version||acceptance.args.proposalHash!==evidence.proposal_hash)return invalid('Mutual acceptance missing');}
|
|
34
37
|
if(event.name==='Settled'&&(event.args.workerAmount!==snapshot.workerPaid||event.args.payerAmount+event.args.workerAmount!==t.amount||Number(event.args.reason)!==evidence.settlement_reason))return invalid('Settlement mismatch');
|
|
35
38
|
return {status:'valid',transaction_hash:receipt.hash,block_hash:block.hash,confirmations:head.number-block.number+1};
|
|
36
39
|
};
|
|
@@ -8,13 +8,16 @@ function createChatTools({workflow,store,queue,actor}={}){
|
|
|
8
8
|
let job=await workflow.create({id,participants:{requester:actor,worker:worker_id,verifier:verifier_id,arbitrator:arbitrator_id},metadata},actor);
|
|
9
9
|
if(terms)job=await workflow.act(job.id,'quote',{terms},{actor,idempotencyKey:'initial_quote'});return job;
|
|
10
10
|
},
|
|
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'),cancel_job:act('cancel'),
|
|
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'),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'),
|
|
12
12
|
work_on_job:async(input)=>{const job=await act('start')(input);if(!job.pending&&job.state==='running')await enqueueRunningJobs({store,queue});return job;}
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
|
-
function createApplicationHandler({workflow,store,queue,authenticate,revalidate,consentFor,legacyState=async()=>({}),authorityHandler,basePath='/api/a423'}={}){
|
|
15
|
+
function createApplicationHandler({workflow,store,queue,authenticate,revalidate,consentFor,settlementConsentFor,legacyState=async()=>({}),authorityHandler,forwardCancellations=true,basePath='/api/a423'}={}){
|
|
16
16
|
const {createEventStream,freshReceipts,renderReceiptPdf}=require('./index.cjs');
|
|
17
|
-
const protocol=createWorkflowHandler({workflow,authenticate,basePath
|
|
17
|
+
const protocol=createWorkflowHandler({workflow,authenticate,basePath,forwardCancellations,revalidateCancellationReceipt:async({job,receipt})=>{
|
|
18
|
+
const {validateCancellationReceipt}=require('../cancellation.cjs');validateCancellationReceipt(job,receipt);
|
|
19
|
+
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);}
|
|
20
|
+
}});const events=createEventStream({store,authenticate});
|
|
18
21
|
return async request=>{
|
|
19
22
|
const path=new URL(request.url).pathname;
|
|
20
23
|
if(path.startsWith('/api/a423-auth/')&&authorityHandler)return authorityHandler(request);
|
|
@@ -29,6 +32,8 @@ function createApplicationHandler({workflow,store,queue,authenticate,revalidate,
|
|
|
29
32
|
}
|
|
30
33
|
const receipt=/^\/api\/a423-receipts\/([A-Za-z0-9_-]{1,128})(\.pdf)?$/.exec(path);
|
|
31
34
|
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'}});}
|
|
35
|
+
const closing=/^\/api\/a423-settlement-consent\/([A-Za-z0-9_-]{1,128})$/.exec(path);
|
|
36
|
+
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'}});}
|
|
32
37
|
const consent=/^\/api\/a423-worker-consent\/([A-Za-z0-9_-]{1,128})$/.exec(path);
|
|
33
38
|
if(consent){const job=await workflow.get(consent[1],actor);if(job.participants.worker!==actor)return Response.json({error:'worker required'},{status:403});if(!consentFor)throw Error('Wallet consent not configured');const typed=await consentFor(job);return Response.json(JSON.parse(JSON.stringify(typed,(_,v)=>typeof v==='bigint'?v.toString():v)),{headers:{'cache-control':'no-store'}});}
|
|
34
39
|
return Response.json({error:'not found'},{status:404});
|
|
@@ -1,8 +1,8 @@
|
|
|
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}={}){
|
|
5
|
-
const {createEscrowRegistry,createEvidenceRevalidator,escrowTerms,workerConsent,escrowTransitions}=require('../escrow/index.cjs');
|
|
4
|
+
function createEscrowApplication({storage,authority,escrows,signerFor,addressFor,limitsFor,approveSigning,validateArtifact,execute,keeperEnabled=false,forwardCancellations=true,allowAuthorityRequest=async()=>false,requireIndependentArbitrator=true}={}){
|
|
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
8
|
const registry=new Map(escrows.map(c=>[String(c.chainId)+':'+c.contractAddress.toLowerCase(),c]));
|
|
@@ -24,7 +24,7 @@ function createEscrowApplication({storage,authority,escrows,signerFor,addressFor
|
|
|
24
24
|
const validators=new Map(escrows.map(c=>[String(c.chainId)+':'+c.contractAddress.toLowerCase(),createEvidenceRevalidator(c)]));
|
|
25
25
|
const revalidate=(job,evidence)=>validators.get(String(configFor(job).chainId)+':'+configFor(job).contractAddress.toLowerCase())(job,evidence);
|
|
26
26
|
const authenticate=createCredentialAuthenticator({authority});
|
|
27
|
-
const applicationOptions={workflow,store:storage.jobs,queue:storage.queue,authenticate,revalidate,consentFor:async job=>workerConsent(job,configFor(job)),authorityHandler:createAuthorityHandler({authority,allowRequest:allowAuthorityRequest})};
|
|
27
|
+
const applicationOptions={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
28
|
const workerOptions={authority,actorFor:async job=>job.pending?.actor||job.participants.requester,allowExecute:keeperEnabled,
|
|
29
29
|
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
30
|
};
|
package/src/operations/index.cjs
CHANGED
|
@@ -16,8 +16,8 @@ function validateEscrowEvidence(e){
|
|
|
16
16
|
for(const key of ['contract','token','payer','worker','verifier','arbitrator'])assert(/^0x[0-9a-fA-F]{40}$/.test(e[key]),'Invalid evidence '+key);
|
|
17
17
|
for(const key of ['job_id','agreement_hash','transaction_hash','block_hash','result_hash'])assert(/^0x[0-9a-fA-F]{64}$/.test(e[key]),'Invalid evidence '+key);
|
|
18
18
|
assert(Number.isSafeInteger(e.block_number)&&e.block_number>=0&&Number.isSafeInteger(e.confirmations)&&e.confirmations>0,'Invalid evidence block');
|
|
19
|
-
assert(['Funded','Submitted','Verified','Disputed','Settled'].includes(e.event),'Invalid evidence event');
|
|
20
|
-
assert(Number.isInteger(e.state)&&e.state>=1&&e.state<=
|
|
19
|
+
assert(['Funded','Submitted','Verified','Disputed','Settled','SettlementProposed'].includes(e.event),'Invalid evidence event');
|
|
20
|
+
assert(Number.isInteger(e.state)&&e.state>=1&&e.state<=8&&BigInt(e.worker_paid)<=BigInt(e.amount),'Invalid evidence allocation/state');
|
|
21
21
|
return structuredClone(e);
|
|
22
22
|
}
|
|
23
23
|
function createSpendingAuthorizer({spending,limitsFor,authorize}={}){
|
|
@@ -39,9 +39,12 @@ 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
|
-
if(e&&['funded','running'].includes(job.state)&&seconds>e.work_deadline)action='timeout';
|
|
44
|
-
if(e&&['submitted','verified','disputed'].includes(job.state)&&seconds>e.resolution_deadline)action='timeout';
|
|
46
|
+
if(e&&(['funded','running'].includes(job.state)||(['settlement_negotiation','disputed'].includes(job.state)&&!job.submission))&&seconds>e.work_deadline)action='timeout';
|
|
47
|
+
if(e&&['submitted','verified','disputed','settlement_negotiation'].includes(job.state)&&seconds>e.resolution_deadline)action='timeout';
|
|
45
48
|
const expiry=job.agreement.terms.expires_at;
|
|
46
49
|
if(['quoted','accepted'].includes(job.state)&&Number.isSafeInteger(expiry)&&seconds>expiry)action='expire';
|
|
47
50
|
if(action&&allowExecute)result=await workflow.act(job.id,action,{}, {actor,idempotencyKey:'keeper_'+hash(job.id+':'+action)});
|
|
@@ -58,10 +61,11 @@ function createExpiryAdapter({now=()=>Date.now()}={}){const run=async({job})=>{
|
|
|
58
61
|
};return {execute:run,reconcile:run};}
|
|
59
62
|
async function runExecutionOnce({workflow,queue,execute,beforeExecute,leaseMs=30000}={}){
|
|
60
63
|
const item=await queue.claim({kind:'execution',leaseMs});if(!item)return null;
|
|
61
|
-
const
|
|
64
|
+
const controller=new AbortController();
|
|
65
|
+
const heartbeat=setInterval(async()=>{try{if(!await queue.heartbeat(item.id,item.lease,{leaseMs}))throw Error('Execution lease lost');const current=await workflow.assertSafe(item.payload.job_id,item.payload.actor);if(current.state!=='running')throw Error('Work paused');}catch(error){controller.abort(error);}},Math.max(1,Math.min(1000,Math.floor(leaseMs/3))));
|
|
62
66
|
try{
|
|
63
67
|
let result=item.result;
|
|
64
|
-
if(!result){const job=await workflow.assertSafe(item.payload.job_id,item.payload.actor);assert(job.state==='running','Work requires running state');validateTask(job.agreement.terms.task);assert(typeof beforeExecute==='function'&&await beforeExecute(job)===true,'Execution eligibility check required');result=await execute({job,idempotencyKey:item.id});assert(await queue.checkpoint(item.id,item.lease,result),'Execution lease lost');}
|
|
68
|
+
if(!result){const job=await workflow.assertSafe(item.payload.job_id,item.payload.actor);assert(job.state==='running','Work requires running state');validateTask(job.agreement.terms.task);assert(typeof beforeExecute==='function'&&await beforeExecute(job)===true,'Execution eligibility check required');result=await execute({job,idempotencyKey:item.id,signal:controller.signal});assert(await queue.checkpoint(item.id,item.lease,result),'Execution lease lost');}
|
|
65
69
|
assert(typeof result.result_hash==='string','Execution must return artifact commitment');
|
|
66
70
|
const job=await workflow.act(item.payload.job_id,'submit',result,{actor:item.payload.actor,idempotencyKey:'execution_'+hash(item.id)});
|
|
67
71
|
assert(await queue.complete(item.id,item.lease,{job_id:job.id,state:job.state}),'Execution lease lost');return job;
|
|
@@ -91,7 +95,7 @@ function createWebhookDispatcher({queue,kv,subscriptions,secretFor,fetch:fetchIm
|
|
|
91
95
|
}
|
|
92
96
|
async function watchEvidenceOnce({store,revalidate}){const results=[];let after='';while(true){const jobs=await store.list({after,limit:100});if(!jobs.length)break;after=jobs.at(-1).id;for(const job of jobs){if(!job.evidence.length)continue;const checks=[];for(const item of job.evidence)if(item.evidence.transaction_hash){try{checks.push(await revalidate(job,item.evidence));}catch(error){checks.push({status:'unavailable',message:'Evidence provider unavailable'});}}
|
|
93
97
|
const blocked=checks.some(c=>c.status!=='valid');const safety={blocked,checks,checked_at:new Date().toISOString()};await store.setSafety(job.id,safety);results.push({id:job.id,...safety});}}return results;}
|
|
94
|
-
async function freshReceipts({workflow,id,actor,revalidate}){const job=await workflow.get(id,actor);const checks=[];for(const item of job.evidence)if(item.evidence.transaction_hash){const result=await revalidate(job,item.evidence);assert(result.status==='valid','Receipt evidence is not currently valid: '+result.status);checks.push(result);}return {...await workflow.receipts(id,actor),revalidated_at:new Date().toISOString(),checks};}
|
|
98
|
+
async function freshReceipts({workflow,id,actor,revalidate}){const job=await workflow.get(id,actor);for(const p of job.negotiation?.history||[]){const canonical=require('../closing.cjs').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});assert(canonical.proposal_hash===p.proposal_hash,'Stored settlement claims changed');}const checks=[];for(const item of job.evidence)if(item.evidence.transaction_hash){const result=await revalidate(job,item.evidence);assert(result.status==='valid','Receipt evidence is not currently valid: '+result.status);checks.push(result);}return {...await workflow.receipts(id,actor),revalidated_at:new Date().toISOString(),checks};}
|
|
95
99
|
async function renderReceiptPdf(receipt){const {PDFDocument,StandardFonts}=require('pdf-lib');const pdf=await PDFDocument.create(),font=await pdf.embedFont(StandardFonts.Courier);let page,y;const newPage=()=>{page=pdf.addPage([595,842]);y=800;};newPage();const text=JSON.stringify(receipt,null,2).replace(/[^\x20-\x7e\n]/g,'?');for(const line of ['A423 Evidence Receipt',...text.split('\n')])for(let i=0;i<Math.max(1,line.length);i+=88){if(y<40)newPage();page.drawText(line.slice(i,i+88),{x:32,y,size:9,font});y-=12;}return pdf.save();}
|
|
96
100
|
function planLegacyMigration(records){return records.map(record=>({id:record.id,state:record.state,automatic:false,reason:['funded','running','submitted','verified','paid','settlement_pending','completed'].includes(String(record.state).toLowerCase())?'Existing custody/evidence must stay with original contract; do not relabel':'Requires reviewed participant and accepted-terms mapping; create a new job with provenance'}));}
|
|
97
101
|
module.exports={validateTask,validateEscrowEvidence,createSpendingAuthorizer,runMaintenanceOnce,createExpiryAdapter,runExecutionOnce,enqueueRunningJobs,createEventStream,createWebhookDispatcher,watchEvidenceOnce,freshReceipts,renderReceiptPdf,planLegacyMigration};
|
|
@@ -103,3 +107,10 @@ module.exports.migrateUnfundedLegacy=migrateUnfundedLegacy;
|
|
|
103
107
|
Object.assign(module.exports,require('./application.cjs'));
|
|
104
108
|
|
|
105
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;
|
|
@@ -15,7 +15,7 @@ export function createSpendingAuthorizer(options: {spending: SpendingStore;limit
|
|
|
15
15
|
export function runMaintenanceOnce(options: {workflow: Workflow;store: SqliteJobStore;actorFor(job: WorkflowJob): Promise<string | null>;allowExecute?: boolean;now?: number}): Promise<Array<{id: string;state?: string;pending?: boolean;error?: string}>>;
|
|
16
16
|
export function createExpiryAdapter(options?: {now?(): number}): {execute(context: {job: WorkflowJob}): Promise<any>;reconcile(context: {job: WorkflowJob}): Promise<any>};
|
|
17
17
|
export function enqueueRunningJobs(options: {store: SqliteJobStore;queue: DurableQueue}): Promise<number>;
|
|
18
|
-
export function runExecutionOnce(options: {workflow: Workflow;queue: DurableQueue;execute(context: {job: WorkflowJob;idempotencyKey: string}): Promise<{result_hash: string;[key: string]: Json}>;beforeExecute(job: WorkflowJob): Promise<boolean>;leaseMs?: number}): Promise<WorkflowJob | null>;
|
|
18
|
+
export function runExecutionOnce(options: {workflow: Workflow;queue: DurableQueue;execute(context: {job: WorkflowJob;idempotencyKey: string;signal: AbortSignal}): Promise<{result_hash: string;[key: string]: Json}>;beforeExecute(job: WorkflowJob): Promise<boolean>;leaseMs?: number}): Promise<WorkflowJob | null>;
|
|
19
19
|
export function createEventStream(options: {store: SqliteJobStore;authenticate(request: Request): Promise<string | null>;pollMs?: number;maxDurationMs?: number}): (request: Request) => Promise<Response>;
|
|
20
20
|
export function createWebhookDispatcher(options: {queue: DurableQueue;kv: KeyValueStore;subscriptions: Array<{id: string;actor: string;url: string;after?: number}>;secretFor(id: string): Promise<string>;fetch?: typeof fetch;timeoutMs?: number}): {project(store: SqliteJobStore): Promise<void>;deliverOne(): Promise<any>};
|
|
21
21
|
export function watchEvidenceOnce(options: {store: SqliteJobStore;revalidate: Revalidator}): Promise<any[]>;
|
|
@@ -24,6 +24,8 @@ 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
26
|
export function createChatTools(options: {workflow: Workflow;store: SqliteJobStore;queue: DurableQueue;actor: string}): Record<string,(input: any) => Promise<WorkflowJob>>;
|
|
27
|
-
export function createApplicationHandler(options: {workflow: Workflow;store: SqliteJobStore;queue: DurableQueue;authenticate(request: Request): Promise<string | null>;revalidate: Revalidator;consentFor?(job: WorkflowJob): Promise<object>;legacyState?(actor: string): Promise<object>;authorityHandler?(request: Request): Promise<Response>;basePath?: string}): (request: Request) => Promise<Response>;
|
|
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>;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}): 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}>;keeperEnabled?: boolean;forwardCancellations?: 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>};
|
|
30
|
+
|
|
31
|
+
export function freshCancellationReceipt(options: {workflow: Workflow;id: string;actor: string;revalidate: Revalidator}): Promise<import('../workflow.js').CancellationReceipt>;
|
package/src/operations/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import api from './index.cjs';
|
|
2
|
-
export const { validateTask, validateEscrowEvidence, createSpendingAuthorizer, runMaintenanceOnce, createExpiryAdapter, runExecutionOnce, enqueueRunningJobs, createEventStream, createWebhookDispatcher, watchEvidenceOnce, freshReceipts, renderReceiptPdf, planLegacyMigration, migrateUnfundedLegacy, createChatTools, createApplicationHandler, createEscrowApplication } = api;
|
|
2
|
+
export const { validateTask, validateEscrowEvidence, createSpendingAuthorizer, runMaintenanceOnce, createExpiryAdapter, runExecutionOnce, enqueueRunningJobs, createEventStream, createWebhookDispatcher, watchEvidenceOnce, freshReceipts, renderReceiptPdf, planLegacyMigration, migrateUnfundedLegacy, createChatTools, createApplicationHandler, createEscrowApplication, freshCancellationReceipt } = api;
|
package/src/workflow.cjs
CHANGED
|
@@ -62,11 +62,13 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
|
|
|
62
62
|
canonical(result);
|
|
63
63
|
if (result.status === 'pending') {
|
|
64
64
|
const next = copy(job); next.pending.observation = copy(result);
|
|
65
|
+
require('./cancellation.cjs').updateCancellationSubflow(job,next,{action:pending.action,actor:pending.actor,phase:'pending',evidence:result,at:stamp()});
|
|
65
66
|
return save(job, next, pending.action + '.pending', pending.actor, result);
|
|
66
67
|
}
|
|
67
68
|
if (result.status === 'rejected') {
|
|
68
69
|
const next = copy(job); next.pending = null;
|
|
69
70
|
next.operations[pending.key] = { request_hash: pending.request_hash, status: 'rejected', result: copy(result) };
|
|
71
|
+
require('./cancellation.cjs').updateCancellationSubflow(job,next,{action:pending.action,actor:pending.actor,phase:'rejected',error:result.reason,at:stamp()});
|
|
70
72
|
return save(job, next, pending.action + '.rejected', pending.actor, result);
|
|
71
73
|
}
|
|
72
74
|
if (!result.evidence || typeof result.evidence !== 'object' || Array.isArray(result.evidence)) error('INVALID_ADAPTER_RESULT', 'Confirmed effects require structured evidence', 502);
|
|
@@ -75,7 +77,37 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
|
|
|
75
77
|
next.operations[pending.key] = { request_hash: pending.request_hash, status: 'confirmed', result: copy(result) };
|
|
76
78
|
next.evidence.push({ action: pending.action, operation_id: pending.id, evidence: copy(result.evidence) });
|
|
77
79
|
if (pending.action === 'submit') next.submission = copy(pending.input);
|
|
78
|
-
|
|
80
|
+
let eventData=copy(result.evidence);
|
|
81
|
+
if(['propose_settlement','counter_settlement'].includes(pending.action)){
|
|
82
|
+
const proposal=require('./closing.cjs').settlementProposal(job,pending.input);
|
|
83
|
+
if(result.evidence.proposal_hash!==proposal.proposal_hash || result.evidence.escrow_version!==proposal.escrow_version)error('INVALID_ADAPTER_RESULT','Confirmed proposal must match exact claimed terms',502);
|
|
84
|
+
const record={...proposal,proposer_id:pending.actor,at:stamp()};
|
|
85
|
+
next.negotiation={status:'open',origin_state:job.negotiation?.origin_state||job.state,current:record,history:[...(job.negotiation?.history||[]),record]};
|
|
86
|
+
next.work_status='paused';eventData={...eventData,proposal:record};
|
|
87
|
+
}
|
|
88
|
+
if(pending.action==='approve_cancellation'){
|
|
89
|
+
const instruction=require('./cancellation.cjs').validateCancellationInstruction(job,result.evidence.instruction);
|
|
90
|
+
next.negotiation={...copy(job.negotiation),status:'accepted',acceptance_basis:'dual_wallet_signatures'};next.work_status='paused';
|
|
91
|
+
eventData={...eventData,instruction_id:instruction.id};
|
|
92
|
+
}
|
|
93
|
+
if(['accept_settlement','execute_cancellation'].includes(pending.action)){
|
|
94
|
+
const p=job.negotiation?.current;
|
|
95
|
+
if(!p || result.evidence.settlement_reason!==4 || result.evidence.worker_paid!==p.worker_amount || result.evidence.proposal_hash!==p.proposal_hash || result.evidence.escrow_version!==p.escrow_version)error('INVALID_ADAPTER_RESULT','Closing settlement evidence mismatch',502);
|
|
96
|
+
next.negotiation={...copy(job.negotiation),status:'settled',accepted_by:pending.actor,settled_at:stamp()};next.work_status='stopped';
|
|
97
|
+
}
|
|
98
|
+
if(pending.action==='dispute'){next.work_status='paused';if(next.negotiation)next.negotiation.status='disputed';}
|
|
99
|
+
if(['release','refund','resolve','timeout'].includes(pending.action)){
|
|
100
|
+
next.work_status=pending.action==='release'?'completed':'stopped';
|
|
101
|
+
if(next.negotiation)next.negotiation={...next.negotiation,status:'settled',settled_at:stamp()};
|
|
102
|
+
}
|
|
103
|
+
if(['start','submit','verify'].includes(pending.action))next.work_status={start:'running',submit:'submitted',verify:'verified'}[pending.action];
|
|
104
|
+
const e=result.evidence;
|
|
105
|
+
if(typeof e.amount==='string'&&typeof e.worker_paid==='string'&&Number.isInteger(e.state)){
|
|
106
|
+
const closed=[5,6,7].includes(e.state);
|
|
107
|
+
next.escrow_ledger={funded_amount:e.amount,worker_paid:e.worker_paid,payer_refunded:closed?(BigInt(e.amount)-BigInt(e.worker_paid)).toString():'0',locked_amount:closed?'0':e.amount,escrow_version:e.escrow_version??0};
|
|
108
|
+
}
|
|
109
|
+
require('./cancellation.cjs').updateCancellationSubflow(job,next,{action:pending.action,actor:pending.actor,phase:'confirmed',evidence:result.evidence,instruction:result.evidence.instruction,at:stamp()});
|
|
110
|
+
return save(job, next, pending.action + '.confirmed', pending.actor, eventData);
|
|
79
111
|
}
|
|
80
112
|
async function execute(job, reconcile = false) {
|
|
81
113
|
const pending = job.pending;
|
|
@@ -86,7 +118,7 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
|
|
|
86
118
|
const result = await fn({ job: copy(job), input: copy(pending.input), operation: copy(pending) });
|
|
87
119
|
return complete(job, result);
|
|
88
120
|
}
|
|
89
|
-
|
|
121
|
+
const workflow={
|
|
90
122
|
async create({ id = randomUUID(), participants, metadata = {} }, actor) {
|
|
91
123
|
identifier(id); identifier(actor);
|
|
92
124
|
if (!participants || !participants.requester || !participants.worker || !participants.verifier) throw new TypeError('Supply requester, worker and verifier participants');
|
|
@@ -94,13 +126,13 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
|
|
|
94
126
|
if (participants.requester === participants.worker || actor !== participants.requester) error('FORBIDDEN', 'Requester must create the job and differ from worker', 403);
|
|
95
127
|
canonical(metadata); canonical(participants);
|
|
96
128
|
const at = stamp();
|
|
97
|
-
const job = { id, protocol: 'a423.workflow.v1', version: 1, state: 'draft', participants: copy(participants), metadata: copy(metadata), agreement: null, submission: null, pending: null, operations: {}, evidence: [], events: [{ sequence: 1, type: 'created', actor_id: actor, at, data: {} }], created_at: at, updated_at: at };
|
|
129
|
+
const job = { subflows:[],active_subflow_id:null,work_status:'not_started',negotiation:null,escrow_ledger:null,id, protocol: 'a423.workflow.v1', version: 1, state: 'draft', participants: copy(participants), metadata: copy(metadata), agreement: null, submission: null, pending: null, operations: {}, evidence: [], events: [{ sequence: 1, type: 'created', actor_id: actor, at, data: {} }], created_at: at, updated_at: at };
|
|
98
130
|
if (authorize && await authorize({ job: copy(job), action: 'create', actor, input: {} }) !== true) error('FORBIDDEN', 'Application policy rejected creation', 403);
|
|
99
131
|
if (!await store.create(job)) error('CONFLICT', 'Job ID already exists');
|
|
100
132
|
return copy(job);
|
|
101
133
|
},
|
|
102
134
|
get,
|
|
103
|
-
async assertSafe(id,actor){const job=await get(id,actor);const safety=store.getSafety?await store.getSafety(id):null;if(safety?.blocked)error('SAFETY_HOLD','Evidence requires reconciliation',409);return job;},
|
|
135
|
+
async assertSafe(id,actor){const job=await get(id,actor);const safety=store.getSafety?await store.getSafety(id):null;if(safety?.blocked)error('SAFETY_HOLD','Evidence requires reconciliation',409);if(['propose_settlement','counter_settlement','dispute'].includes(job.pending?.action)||job.state==='settlement_negotiation')error('WORK_PAUSED','Settlement negotiation pauses work',409);return job;},
|
|
104
136
|
async act(id, action, input, { actor, idempotencyKey } = {}) {
|
|
105
137
|
identifier(actor); identifier(idempotencyKey); identifier(action); canonical(input);
|
|
106
138
|
let job = await get(id, actor);
|
|
@@ -119,6 +151,9 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
|
|
|
119
151
|
if (job.pending.key !== key || job.pending.request_hash !== requestHash) error('OPERATION_PENDING', 'Reconcile or retry the pending operation first');
|
|
120
152
|
return execute(job);
|
|
121
153
|
}
|
|
154
|
+
const cancellation=require('./cancellation.cjs').getCancellationSubflow(job);
|
|
155
|
+
if(['propose_settlement','counter_settlement'].includes(action)&&cancellation?.instruction)error('CANCELLATION_ACCEPTED','An accepted instruction must be executed or resolved before renegotiating',409);
|
|
156
|
+
if(action==='approve_cancellation'&&cancellation?.instruction){const candidate=require('./cancellation.cjs').createCancellationInstruction(job,input);if(input.expected_revision!==job.negotiation.current.escrow_version||input.proposal_hash!==job.negotiation.current.proposal_hash||hashAgreement(candidate)!==hashAgreement(cancellation.instruction))error('CANCELLATION_CONFLICT','Another instruction is already accepted',409);return job;}
|
|
122
157
|
if (!rule.from.includes(job.state)) error('INVALID_STATE', `Cannot ${action} from ${job.state}`);
|
|
123
158
|
let next = copy(job);
|
|
124
159
|
if (action === 'quote') {
|
|
@@ -133,6 +168,7 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
|
|
|
133
168
|
if (rule.effect) {
|
|
134
169
|
if (!adapters[action]) error('ADAPTER_REQUIRED', `Configure ${action} adapter`, 501);
|
|
135
170
|
next.pending = { id: `a423:${id}:${key}`, key, action, actor, input: copy(input), request_hash: requestHash, target: rule.to, created_at: stamp(), observation: null };
|
|
171
|
+
require('./cancellation.cjs').updateCancellationSubflow(job,next,{action,actor,phase:'requested',input,at:stamp()});
|
|
136
172
|
job = await save(job, next, action + '.requested', actor);
|
|
137
173
|
return execute(job);
|
|
138
174
|
}
|
|
@@ -154,13 +190,25 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
|
|
|
154
190
|
await allowed(job, job.pending.action, actor, job.pending.input, rules[job.pending.action]);
|
|
155
191
|
return execute(job, true);
|
|
156
192
|
},
|
|
193
|
+
async cancellation(id,actor){return require('./cancellation.cjs').getCancellationSubflow(await get(id,actor));},
|
|
194
|
+
async cancellationInstruction(id,actor){const job=await get(id,actor),subflow=require('./cancellation.cjs').getCancellationSubflow(job);if(!subflow?.instruction)error('INSTRUCTION_UNAVAILABLE','No accepted cancellation instruction',409);return require('./cancellation.cjs').validateCancellationInstruction(job,subflow.instruction);},
|
|
195
|
+
async cancellationReceipt(id,actor){const job=await get(id,actor),subflow=require('./cancellation.cjs').getCancellationSubflow(job);if(subflow?.status!=='completed'||!subflow.receipt)error('RECEIPT_UNAVAILABLE','Cancellation payment is not confirmed',409);return require('./cancellation.cjs').validateCancellationReceipt(job,subflow.receipt);},
|
|
196
|
+
async acceptCancellation(id,input,{actor,idempotencyKey,forward=true}={}){
|
|
197
|
+
if(typeof forward!=='boolean')throw new TypeError('forward must be a boolean');
|
|
198
|
+
let job=await workflow.act(id,'approve_cancellation',input,{actor,idempotencyKey});
|
|
199
|
+
const subflow=require('./cancellation.cjs').getCancellationSubflow(job);
|
|
200
|
+
if(!forward||job.pending||!subflow?.instruction||subflow.status!=='accepted')return job;
|
|
201
|
+
return workflow.act(id,'execute_cancellation',{instruction_id:subflow.instruction.id},{actor,idempotencyKey:'cancel_forward_'+digest(subflow.instruction.id)});
|
|
202
|
+
},
|
|
157
203
|
async receipts(id, actor) {
|
|
158
204
|
const job = await get(id, actor);
|
|
159
|
-
return { protocol: job.protocol, job_id: id, state: job.state, agreement: job.agreement, submission: job.submission, evidence: job.evidence, events: job.events, pending: job.pending };
|
|
205
|
+
return { protocol: job.protocol, job_id: id, state: job.state, agreement: job.agreement, submission: job.submission, subflows:job.subflows||[],active_subflow_id:job.active_subflow_id||null,work_status:job.work_status,negotiation:job.negotiation,escrow_ledger:job.escrow_ledger,evidence: job.evidence, events: job.events, pending: job.pending };
|
|
160
206
|
}
|
|
161
207
|
};
|
|
208
|
+
return workflow;
|
|
162
209
|
}
|
|
163
|
-
function createWorkflowHandler({ workflow, authenticate, basePath = '/api/a423', maxBodyBytes = 65536, onError } = {}) {
|
|
210
|
+
function createWorkflowHandler({ workflow, authenticate, basePath = '/api/a423', maxBodyBytes = 65536, onError, forwardCancellations = true, revalidateCancellationReceipt } = {}) {
|
|
211
|
+
if(typeof forwardCancellations!=='boolean')throw new TypeError('forwardCancellations must be a boolean');
|
|
164
212
|
if (!workflow || typeof authenticate !== 'function' || !/^\/(?:[A-Za-z0-9_-]+\/)*[A-Za-z0-9_-]+$/.test(basePath)) throw new TypeError('Supply workflow, authenticate and valid basePath');
|
|
165
213
|
if (!Number.isSafeInteger(maxBodyBytes) || maxBodyBytes < 1 || maxBodyBytes > 1048576) throw new TypeError('Invalid maxBodyBytes');
|
|
166
214
|
const respond = (value, status = 200) => Response.json(value, { status, headers: { 'cache-control': 'no-store' } });
|
|
@@ -168,10 +216,10 @@ function createWorkflowHandler({ workflow, authenticate, basePath = '/api/a423',
|
|
|
168
216
|
try {
|
|
169
217
|
const path = new URL(request.url).pathname;
|
|
170
218
|
if (!path.startsWith(basePath + '/')) error('NOT_FOUND', 'Route not found', 404);
|
|
171
|
-
const match = /^\/jobs(?:\/([A-Za-z0-9_-]{1,128})(?:\/(actions|reconcile|receipts))?)?$/.exec(path.slice(basePath.length));
|
|
219
|
+
const match = /^\/jobs(?:\/([A-Za-z0-9_-]{1,128})(?:\/(actions|reconcile|receipts|subflows|cancellation(?:\/(?:accept|execute|instruction|receipt))?))?)?$/.exec(path.slice(basePath.length));
|
|
172
220
|
if (!match) error('NOT_FOUND', 'Route not found', 404);
|
|
173
221
|
const [, id, action] = match;
|
|
174
|
-
const method = !id ||
|
|
222
|
+
const method = !id || ['actions','reconcile','cancellation/accept','cancellation/execute'].includes(action) ? 'POST' : 'GET';
|
|
175
223
|
if (request.method !== method) { const r = respond({ error: { code: 'METHOD_NOT_ALLOWED', message: `Use ${method}` } }, 405); r.headers.set('allow', method); return r; }
|
|
176
224
|
const actor = await authenticate(request);
|
|
177
225
|
if (!actor) error('UNAUTHENTICATED', 'Authentication required', 401);
|
|
@@ -184,6 +232,16 @@ function createWorkflowHandler({ workflow, authenticate, basePath = '/api/a423',
|
|
|
184
232
|
try { body = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { error('INVALID_INPUT', 'Invalid JSON', 400); }
|
|
185
233
|
}
|
|
186
234
|
if (!id) return respond({ job: await workflow.create(body, actor) }, 201);
|
|
235
|
+
if(action==='subflows'){const job=await workflow.get(id,actor);return respond({job_id:id,subflows:job.subflows||[],active_subflow_id:job.active_subflow_id||null});}
|
|
236
|
+
if(action==='cancellation')return respond({job_id:id,subflow:await workflow.cancellation(id,actor)});
|
|
237
|
+
if(action==='cancellation/instruction')return respond({instruction:await workflow.cancellationInstruction(id,actor)});
|
|
238
|
+
if(action==='cancellation/receipt'){
|
|
239
|
+
const receipt=await workflow.cancellationReceipt(id,actor);
|
|
240
|
+
if(revalidateCancellationReceipt)await revalidateCancellationReceipt({job:await workflow.get(id,actor),receipt,actor});
|
|
241
|
+
return respond({receipt});
|
|
242
|
+
}
|
|
243
|
+
if(action==='cancellation/accept')return respond({job:await workflow.acceptCancellation(id,body,{actor,idempotencyKey:request.headers.get('idempotency-key'),forward:forwardCancellations})});
|
|
244
|
+
if(action==='cancellation/execute')return respond({job:await workflow.act(id,'execute_cancellation',body,{actor,idempotencyKey:request.headers.get('idempotency-key')})});
|
|
187
245
|
if (action === 'actions') return respond({ job: await workflow.act(id, body.action, body.input ?? {}, { actor, idempotencyKey: request.headers.get('idempotency-key') }) });
|
|
188
246
|
if (action === 'reconcile') return respond({ job: await workflow.reconcile(id, actor) });
|
|
189
247
|
if (action === 'receipts') return respond(await workflow.receipts(id, actor));
|
|
@@ -216,12 +274,24 @@ function createWorkflowClient({ baseUrl, headers, fetch: fetchImpl = globalThis.
|
|
|
216
274
|
return data;
|
|
217
275
|
}
|
|
218
276
|
const path = id => '/jobs/' + identifier(id);
|
|
219
|
-
|
|
277
|
+
const workflow={
|
|
220
278
|
async create(input) { return (await request('/jobs',input)).job; },
|
|
221
279
|
async get(id) { return (await request(path(id))).job; },
|
|
222
280
|
async act(id,action,input,{idempotencyKey}={}) { identifier(action);identifier(idempotencyKey);return (await request(path(id)+'/actions',{action,input},idempotencyKey)).job; },
|
|
223
281
|
async reconcile(id) { return (await request(path(id)+'/reconcile',{})).job; },
|
|
282
|
+
async cancellation(id){return (await request(path(id)+'/cancellation')).subflow;},
|
|
283
|
+
async cancellationInstruction(id){return (await request(path(id)+'/cancellation/instruction')).instruction;},
|
|
284
|
+
async cancellationReceipt(id){return (await request(path(id)+'/cancellation/receipt')).receipt;},
|
|
285
|
+
async subflows(id){return (await request(path(id)+'/subflows')).subflows;},
|
|
286
|
+
async acceptCancellation(id,input,{idempotencyKey}={}){identifier(idempotencyKey);return (await request(path(id)+'/cancellation/accept',input,idempotencyKey)).job;},
|
|
287
|
+
async executeCancellation(id,input,{idempotencyKey}={}){identifier(idempotencyKey);return (await request(path(id)+'/cancellation/execute',input,idempotencyKey)).job;},
|
|
224
288
|
async receipts(id) { return request(path(id)+'/receipts'); }
|
|
225
289
|
};
|
|
290
|
+
return workflow;
|
|
226
291
|
}
|
|
227
292
|
module.exports.createWorkflowClient = createWorkflowClient;
|
|
293
|
+
|
|
294
|
+
module.exports.getCancellationSubflow=(job)=>require('./cancellation.cjs').getCancellationSubflow(job);
|
|
295
|
+
module.exports.createCancellationInstruction=(job,proofs)=>require('./cancellation.cjs').createCancellationInstruction(job,proofs);
|
|
296
|
+
module.exports.validateCancellationInstruction=(job,instruction)=>require('./cancellation.cjs').validateCancellationInstruction(job,instruction);
|
|
297
|
+
module.exports.validateCancellationReceipt=(job,receipt)=>require('./cancellation.cjs').validateCancellationReceipt(job,receipt);
|