@claw-network/node 0.2.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/README.md +49 -0
- package/dist/api/api-key-store.d.ts +74 -0
- package/dist/api/api-key-store.d.ts.map +1 -0
- package/dist/api/api-key-store.js +170 -0
- package/dist/api/api-key-store.js.map +1 -0
- package/dist/api/auth.d.ts +30 -0
- package/dist/api/auth.d.ts.map +1 -0
- package/dist/api/auth.js +115 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/legacy.d.ts +26 -0
- package/dist/api/legacy.d.ts.map +1 -0
- package/dist/api/legacy.js +281 -0
- package/dist/api/legacy.js.map +1 -0
- package/dist/api/middleware.d.ts +35 -0
- package/dist/api/middleware.d.ts.map +1 -0
- package/dist/api/middleware.js +75 -0
- package/dist/api/middleware.js.map +1 -0
- package/dist/api/response.d.ts +85 -0
- package/dist/api/response.d.ts.map +1 -0
- package/dist/api/response.js +185 -0
- package/dist/api/response.js.map +1 -0
- package/dist/api/router.d.ts +45 -0
- package/dist/api/router.d.ts.map +1 -0
- package/dist/api/router.js +183 -0
- package/dist/api/router.js.map +1 -0
- package/dist/api/routes/admin.d.ts +11 -0
- package/dist/api/routes/admin.d.ts.map +1 -0
- package/dist/api/routes/admin.js +124 -0
- package/dist/api/routes/admin.js.map +1 -0
- package/dist/api/routes/contracts.d.ts +7 -0
- package/dist/api/routes/contracts.d.ts.map +1 -0
- package/dist/api/routes/contracts.js +665 -0
- package/dist/api/routes/contracts.js.map +1 -0
- package/dist/api/routes/dao.d.ts +7 -0
- package/dist/api/routes/dao.d.ts.map +1 -0
- package/dist/api/routes/dao.js +549 -0
- package/dist/api/routes/dao.js.map +1 -0
- package/dist/api/routes/dev.d.ts +9 -0
- package/dist/api/routes/dev.d.ts.map +1 -0
- package/dist/api/routes/dev.js +273 -0
- package/dist/api/routes/dev.js.map +1 -0
- package/dist/api/routes/escrows.d.ts +7 -0
- package/dist/api/routes/escrows.d.ts.map +1 -0
- package/dist/api/routes/escrows.js +454 -0
- package/dist/api/routes/escrows.js.map +1 -0
- package/dist/api/routes/identities.d.ts +7 -0
- package/dist/api/routes/identities.d.ts.map +1 -0
- package/dist/api/routes/identities.js +245 -0
- package/dist/api/routes/identities.js.map +1 -0
- package/dist/api/routes/markets-capabilities.d.ts +7 -0
- package/dist/api/routes/markets-capabilities.d.ts.map +1 -0
- package/dist/api/routes/markets-capabilities.js +477 -0
- package/dist/api/routes/markets-capabilities.js.map +1 -0
- package/dist/api/routes/markets-disputes.d.ts +7 -0
- package/dist/api/routes/markets-disputes.d.ts.map +1 -0
- package/dist/api/routes/markets-disputes.js +102 -0
- package/dist/api/routes/markets-disputes.js.map +1 -0
- package/dist/api/routes/markets-info.d.ts +7 -0
- package/dist/api/routes/markets-info.d.ts.map +1 -0
- package/dist/api/routes/markets-info.js +523 -0
- package/dist/api/routes/markets-info.js.map +1 -0
- package/dist/api/routes/markets-search.d.ts +7 -0
- package/dist/api/routes/markets-search.d.ts.map +1 -0
- package/dist/api/routes/markets-search.js +38 -0
- package/dist/api/routes/markets-search.js.map +1 -0
- package/dist/api/routes/markets-tasks.d.ts +7 -0
- package/dist/api/routes/markets-tasks.d.ts.map +1 -0
- package/dist/api/routes/markets-tasks.js +539 -0
- package/dist/api/routes/markets-tasks.js.map +1 -0
- package/dist/api/routes/node.d.ts +7 -0
- package/dist/api/routes/node.d.ts.map +1 -0
- package/dist/api/routes/node.js +53 -0
- package/dist/api/routes/node.js.map +1 -0
- package/dist/api/routes/nonce.d.ts +10 -0
- package/dist/api/routes/nonce.d.ts.map +1 -0
- package/dist/api/routes/nonce.js +65 -0
- package/dist/api/routes/nonce.js.map +1 -0
- package/dist/api/routes/reputations.d.ts +7 -0
- package/dist/api/routes/reputations.d.ts.map +1 -0
- package/dist/api/routes/reputations.js +243 -0
- package/dist/api/routes/reputations.js.map +1 -0
- package/dist/api/routes/transfers.d.ts +7 -0
- package/dist/api/routes/transfers.d.ts.map +1 -0
- package/dist/api/routes/transfers.js +88 -0
- package/dist/api/routes/transfers.js.map +1 -0
- package/dist/api/routes/wallets.d.ts +7 -0
- package/dist/api/routes/wallets.d.ts.map +1 -0
- package/dist/api/routes/wallets.js +132 -0
- package/dist/api/routes/wallets.js.map +1 -0
- package/dist/api/schemas/common.d.ts +45 -0
- package/dist/api/schemas/common.d.ts.map +1 -0
- package/dist/api/schemas/common.js +30 -0
- package/dist/api/schemas/common.js.map +1 -0
- package/dist/api/schemas/contracts.d.ts +284 -0
- package/dist/api/schemas/contracts.d.ts.map +1 -0
- package/dist/api/schemas/contracts.js +79 -0
- package/dist/api/schemas/contracts.js.map +1 -0
- package/dist/api/schemas/dao.d.ts +271 -0
- package/dist/api/schemas/dao.d.ts.map +1 -0
- package/dist/api/schemas/dao.js +78 -0
- package/dist/api/schemas/dao.js.map +1 -0
- package/dist/api/schemas/identity.d.ts +75 -0
- package/dist/api/schemas/identity.d.ts.map +1 -0
- package/dist/api/schemas/identity.js +32 -0
- package/dist/api/schemas/identity.js.map +1 -0
- package/dist/api/schemas/markets.d.ts +822 -0
- package/dist/api/schemas/markets.d.ts.map +1 -0
- package/dist/api/schemas/markets.js +246 -0
- package/dist/api/schemas/markets.js.map +1 -0
- package/dist/api/schemas/wallet.d.ts +163 -0
- package/dist/api/schemas/wallet.d.ts.map +1 -0
- package/dist/api/schemas/wallet.js +54 -0
- package/dist/api/schemas/wallet.js.map +1 -0
- package/dist/api/server.d.ts +45 -0
- package/dist/api/server.d.ts.map +1 -0
- package/dist/api/server.js +131 -0
- package/dist/api/server.js.map +1 -0
- package/dist/api/types.d.ts +69 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +196 -0
- package/dist/api/types.js.map +1 -0
- package/dist/daemon.d.ts +11 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +248 -0
- package/dist/daemon.js.map +1 -0
- package/dist/index.d.ts +137 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +795 -0
- package/dist/index.js.map +1 -0
- package/dist/indexer/index.d.ts +10 -0
- package/dist/indexer/index.d.ts.map +1 -0
- package/dist/indexer/index.js +7 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/indexer.d.ts +60 -0
- package/dist/indexer/indexer.d.ts.map +1 -0
- package/dist/indexer/indexer.js +408 -0
- package/dist/indexer/indexer.js.map +1 -0
- package/dist/indexer/query.d.ts +141 -0
- package/dist/indexer/query.d.ts.map +1 -0
- package/dist/indexer/query.js +244 -0
- package/dist/indexer/query.js.map +1 -0
- package/dist/indexer/store.d.ts +95 -0
- package/dist/indexer/store.d.ts.map +1 -0
- package/dist/indexer/store.js +250 -0
- package/dist/indexer/store.js.map +1 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +37 -0
- package/dist/logger.js.map +1 -0
- package/dist/p2p/sync.d.ts +105 -0
- package/dist/p2p/sync.d.ts.map +1 -0
- package/dist/p2p/sync.js +875 -0
- package/dist/p2p/sync.js.map +1 -0
- package/dist/policy/liquidity-policy.d.ts +17 -0
- package/dist/policy/liquidity-policy.d.ts.map +1 -0
- package/dist/policy/liquidity-policy.js +112 -0
- package/dist/policy/liquidity-policy.js.map +1 -0
- package/dist/services/chain-config.d.ts +226 -0
- package/dist/services/chain-config.d.ts.map +1 -0
- package/dist/services/chain-config.js +105 -0
- package/dist/services/chain-config.js.map +1 -0
- package/dist/services/contract-provider.d.ts +44 -0
- package/dist/services/contract-provider.d.ts.map +1 -0
- package/dist/services/contract-provider.js +167 -0
- package/dist/services/contract-provider.js.map +1 -0
- package/dist/services/contracts-service.d.ts +192 -0
- package/dist/services/contracts-service.d.ts.map +1 -0
- package/dist/services/contracts-service.js +336 -0
- package/dist/services/contracts-service.js.map +1 -0
- package/dist/services/dao-service.d.ts +245 -0
- package/dist/services/dao-service.d.ts.map +1 -0
- package/dist/services/dao-service.js +389 -0
- package/dist/services/dao-service.js.map +1 -0
- package/dist/services/identity-service.d.ts +150 -0
- package/dist/services/identity-service.d.ts.map +1 -0
- package/dist/services/identity-service.js +286 -0
- package/dist/services/identity-service.js.map +1 -0
- package/dist/services/index.d.ts +20 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +15 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/reputation-service.d.ts +128 -0
- package/dist/services/reputation-service.d.ts.map +1 -0
- package/dist/services/reputation-service.js +204 -0
- package/dist/services/reputation-service.js.map +1 -0
- package/dist/services/wallet-service.d.ts +201 -0
- package/dist/services/wallet-service.d.ts.map +1 -0
- package/dist/services/wallet-service.js +402 -0
- package/dist/services/wallet-service.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service contract routes — /api/v1/contracts
|
|
3
|
+
*/
|
|
4
|
+
import { Router } from '../router.js';
|
|
5
|
+
import { ok, created, badRequest, notFound, internalError, paginated, parsePagination, } from '../response.js';
|
|
6
|
+
import { validate } from '../schemas/common.js';
|
|
7
|
+
import { ContractCreateSchema, ContractSignSchema, ContractFundSchema, ContractCompleteSchema, ContractMilestoneSubmitSchema, ContractMilestoneReviewSchema, ContractDisputeSchema, ContractDisputeResolveSchema, ContractSettlementSchema, } from '../schemas/contracts.js';
|
|
8
|
+
import { resolveAddress, resolvePrivateKey, addressFromDid, buildContractView } from '../types.js';
|
|
9
|
+
import { createContractCreateEnvelope, createContractSignEnvelope, createContractActivateEnvelope, createContractCompleteEnvelope, createContractDisputeOpenEnvelope, createContractDisputeResolveEnvelope, createContractMilestoneSubmitEnvelope, createContractMilestoneApproveEnvelope, createContractMilestoneRejectEnvelope, createContractSettlementExecuteEnvelope, applyContractEvent, createContractState, createWalletEscrowCreateEnvelope, createWalletEscrowFundEnvelope, } from '@claw-network/protocol';
|
|
10
|
+
import { keccak256, toUtf8Bytes } from 'ethers';
|
|
11
|
+
/** Legacy helper: keccak256(utf8(value)) — for backward-compat when no envelopeDigest is provided. */
|
|
12
|
+
function legacyHash(value) {
|
|
13
|
+
return keccak256(toUtf8Bytes(value));
|
|
14
|
+
}
|
|
15
|
+
export function contractRoutes(ctx) {
|
|
16
|
+
const r = new Router();
|
|
17
|
+
// ── Helper: rebuild contract state from eventStore ────────────
|
|
18
|
+
async function getContractFromStore(contractId) {
|
|
19
|
+
if (!ctx.contractStore)
|
|
20
|
+
return null;
|
|
21
|
+
return (await ctx.contractStore.getContract(contractId)) ?? null;
|
|
22
|
+
}
|
|
23
|
+
/** Resolve the latest event hash for a contract (for resourcePrev). */
|
|
24
|
+
async function contractPrev(contractId) {
|
|
25
|
+
if (!ctx.contractStore)
|
|
26
|
+
return '';
|
|
27
|
+
const state = await ctx.contractStore.getState();
|
|
28
|
+
return state.contractEvents[contractId] ?? '';
|
|
29
|
+
}
|
|
30
|
+
// ── POST / — create contract ──────────────────────────────────
|
|
31
|
+
r.post('/', async (_req, res, route) => {
|
|
32
|
+
const v = validate(ContractCreateSchema, route.body);
|
|
33
|
+
if (!v.success) {
|
|
34
|
+
badRequest(res, v.error, route.url.pathname);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const body = v.data;
|
|
38
|
+
const contractId = body.contractId ?? `contract-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
39
|
+
const providerAddr = resolveAddress(body.provider);
|
|
40
|
+
if (!providerAddr) {
|
|
41
|
+
badRequest(res, 'Invalid provider', route.url.pathname);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// On-chain
|
|
45
|
+
if (ctx.contractsService) {
|
|
46
|
+
try {
|
|
47
|
+
const terms = (body.terms ?? {});
|
|
48
|
+
const payment = (body.payment ?? {});
|
|
49
|
+
const milestones = (body.milestones ?? []);
|
|
50
|
+
const result = await ctx.contractsService.createContract({
|
|
51
|
+
contractId,
|
|
52
|
+
provider: providerAddr,
|
|
53
|
+
arbiter: resolveAddress(body.did) ?? '',
|
|
54
|
+
totalAmount: Number(payment.total ?? terms.total ?? 0),
|
|
55
|
+
termsHash: JSON.stringify(terms),
|
|
56
|
+
deadline: body.timeline?.deadline ?? 0,
|
|
57
|
+
milestoneAmounts: milestones.map((m) => Number(m.amount ?? 0)),
|
|
58
|
+
milestoneDeadlines: milestones.map((m) => Number(m.deadline ?? 0)),
|
|
59
|
+
});
|
|
60
|
+
created(res, result, { self: `/api/v1/contracts/${contractId}` });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
internalError(res, err.message);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Legacy
|
|
69
|
+
const privateKey = await resolvePrivateKey(ctx.config.dataDir, body.did, body.passphrase);
|
|
70
|
+
if (!privateKey) {
|
|
71
|
+
badRequest(res, 'Key unavailable', route.url.pathname);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const parties = (body.parties ?? {
|
|
76
|
+
client: { did: body.did },
|
|
77
|
+
provider: { did: body.provider },
|
|
78
|
+
});
|
|
79
|
+
const envelope = await createContractCreateEnvelope({
|
|
80
|
+
issuer: body.did,
|
|
81
|
+
privateKey,
|
|
82
|
+
contractId,
|
|
83
|
+
parties,
|
|
84
|
+
service: (body.service ?? {}),
|
|
85
|
+
terms: (body.terms ?? {}),
|
|
86
|
+
payment: (body.payment ?? {}),
|
|
87
|
+
timeline: (body.timeline ?? {}),
|
|
88
|
+
milestones: body.milestones,
|
|
89
|
+
attachments: body.attachments,
|
|
90
|
+
metadata: body.metadata,
|
|
91
|
+
ts: body.ts ?? Date.now(),
|
|
92
|
+
nonce: body.nonce,
|
|
93
|
+
prev: body.prev,
|
|
94
|
+
});
|
|
95
|
+
const hash = await ctx.publishEvent(envelope);
|
|
96
|
+
const state = applyContractEvent(createContractState(), envelope);
|
|
97
|
+
const resultContract = state.contracts[contractId];
|
|
98
|
+
const view = resultContract
|
|
99
|
+
? buildContractView(resultContract)
|
|
100
|
+
: { contractId, txHash: hash };
|
|
101
|
+
created(res, view, { self: `/api/v1/contracts/${contractId}` });
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
internalError(res, err.message || 'Contract creation failed');
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
// ── GET / — list contracts ────────────────────────────────────
|
|
108
|
+
r.get('/', async (_req, res, route) => {
|
|
109
|
+
const { page, perPage, offset } = parsePagination(route.query);
|
|
110
|
+
const party = route.query.get('party') ?? route.query.get('did');
|
|
111
|
+
const status = route.query.get('status');
|
|
112
|
+
if (ctx.contractStore) {
|
|
113
|
+
try {
|
|
114
|
+
const all = await ctx.contractStore.listContracts();
|
|
115
|
+
let filtered = all;
|
|
116
|
+
if (party)
|
|
117
|
+
filtered = filtered.filter((c) => c.parties.client.did === party || c.parties.provider.did === party);
|
|
118
|
+
if (status)
|
|
119
|
+
filtered = filtered.filter((c) => c.status === status);
|
|
120
|
+
const total = filtered.length;
|
|
121
|
+
const sliced = filtered.slice(offset, offset + perPage);
|
|
122
|
+
paginated(res, sliced.map(buildContractView), {
|
|
123
|
+
page,
|
|
124
|
+
perPage,
|
|
125
|
+
total,
|
|
126
|
+
basePath: '/api/v1/contracts',
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* fallthrough */
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// If no store, return empty
|
|
135
|
+
paginated(res, [], { page, perPage, total: 0, basePath: '/api/v1/contracts' });
|
|
136
|
+
});
|
|
137
|
+
// ── GET /:id — single contract ────────────────────────────────
|
|
138
|
+
r.get('/:id', async (_req, res, route) => {
|
|
139
|
+
const { id } = route.params;
|
|
140
|
+
const contract = await getContractFromStore(id);
|
|
141
|
+
if (!contract) {
|
|
142
|
+
notFound(res, `Contract ${id} not found`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
ok(res, buildContractView(contract), { self: `/api/v1/contracts/${id}` });
|
|
146
|
+
});
|
|
147
|
+
// ── POST /:id/actions/sign — sign contract ────────────────────
|
|
148
|
+
r.post('/:id/actions/sign', async (_req, res, route) => {
|
|
149
|
+
const { id } = route.params;
|
|
150
|
+
const v = validate(ContractSignSchema, route.body);
|
|
151
|
+
if (!v.success) {
|
|
152
|
+
badRequest(res, v.error, route.url.pathname);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const body = v.data;
|
|
156
|
+
if (ctx.contractsService) {
|
|
157
|
+
try {
|
|
158
|
+
const result = await ctx.contractsService.signContract(id);
|
|
159
|
+
ok(res, result, { self: `/api/v1/contracts/${id}` });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
internalError(res, err.message);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const privateKey = await resolvePrivateKey(ctx.config.dataDir, body.did, body.passphrase);
|
|
168
|
+
if (!privateKey) {
|
|
169
|
+
badRequest(res, 'Key unavailable', route.url.pathname);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const prev = await contractPrev(id);
|
|
174
|
+
const envelope = await createContractSignEnvelope({
|
|
175
|
+
issuer: body.did,
|
|
176
|
+
privateKey,
|
|
177
|
+
contractId: id,
|
|
178
|
+
resourcePrev: prev,
|
|
179
|
+
signer: body.did,
|
|
180
|
+
ts: body.ts ?? Date.now(),
|
|
181
|
+
nonce: body.nonce,
|
|
182
|
+
prev: body.prev,
|
|
183
|
+
});
|
|
184
|
+
const hash = await ctx.publishEvent(envelope);
|
|
185
|
+
ok(res, { txHash: hash, contractId: id, status: 'signed', timestamp: body.ts ?? Date.now() }, { self: `/api/v1/contracts/${id}` });
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
internalError(res, err.message || 'Contract sign failed');
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
// ── POST /:id/actions/activate — fund & activate ──────────────
|
|
192
|
+
r.post('/:id/actions/activate', async (_req, res, route) => {
|
|
193
|
+
const { id } = route.params;
|
|
194
|
+
const v = validate(ContractFundSchema, route.body);
|
|
195
|
+
if (!v.success) {
|
|
196
|
+
badRequest(res, v.error, route.url.pathname);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const body = v.data;
|
|
200
|
+
if (ctx.contractsService) {
|
|
201
|
+
try {
|
|
202
|
+
const result = await ctx.contractsService.activateContract(id);
|
|
203
|
+
ok(res, result, { self: `/api/v1/contracts/${id}` });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
internalError(res, err.message);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const privateKey = await resolvePrivateKey(ctx.config.dataDir, body.did, body.passphrase);
|
|
212
|
+
if (!privateKey) {
|
|
213
|
+
badRequest(res, 'Key unavailable', route.url.pathname);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
// 3 envelopes: escrow create → escrow fund → activate
|
|
218
|
+
const escrowId = body.escrowId ?? `escrow-${id}-${Date.now()}`;
|
|
219
|
+
const ts = body.ts ?? Date.now();
|
|
220
|
+
const nonce = body.nonce ?? 0;
|
|
221
|
+
const e1 = await createWalletEscrowCreateEnvelope({
|
|
222
|
+
issuer: body.did,
|
|
223
|
+
privateKey,
|
|
224
|
+
escrowId,
|
|
225
|
+
depositor: addressFromDid(body.did),
|
|
226
|
+
beneficiary: addressFromDid(body.did),
|
|
227
|
+
amount: String(body.amount),
|
|
228
|
+
releaseRules: body.releaseRules ?? [{ type: 'manual' }],
|
|
229
|
+
ts,
|
|
230
|
+
nonce,
|
|
231
|
+
prev: body.prev,
|
|
232
|
+
});
|
|
233
|
+
const h1 = await ctx.publishEvent(e1);
|
|
234
|
+
const e2 = await createWalletEscrowFundEnvelope({
|
|
235
|
+
issuer: body.did,
|
|
236
|
+
privateKey,
|
|
237
|
+
escrowId,
|
|
238
|
+
amount: String(body.amount),
|
|
239
|
+
resourcePrev: h1,
|
|
240
|
+
ts: ts + 1,
|
|
241
|
+
nonce: nonce + 1,
|
|
242
|
+
prev: h1,
|
|
243
|
+
});
|
|
244
|
+
const h2 = await ctx.publishEvent(e2);
|
|
245
|
+
const cPrev = await contractPrev(id);
|
|
246
|
+
const e3 = await createContractActivateEnvelope({
|
|
247
|
+
issuer: body.did,
|
|
248
|
+
privateKey,
|
|
249
|
+
contractId: id,
|
|
250
|
+
escrowId,
|
|
251
|
+
resourcePrev: cPrev,
|
|
252
|
+
ts: ts + 2,
|
|
253
|
+
nonce: nonce + 2,
|
|
254
|
+
prev: h2,
|
|
255
|
+
});
|
|
256
|
+
const h3 = await ctx.publishEvent(e3);
|
|
257
|
+
ok(res, {
|
|
258
|
+
txHash: h3,
|
|
259
|
+
contractId: id,
|
|
260
|
+
escrowId,
|
|
261
|
+
status: 'activated',
|
|
262
|
+
timestamp: ts,
|
|
263
|
+
}, { self: `/api/v1/contracts/${id}` });
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
internalError(res, err.message || 'Contract activation failed');
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
// ── POST /:id/actions/complete ────────────────────────────────
|
|
270
|
+
r.post('/:id/actions/complete', async (_req, res, route) => {
|
|
271
|
+
const { id } = route.params;
|
|
272
|
+
const v = validate(ContractCompleteSchema, route.body);
|
|
273
|
+
if (!v.success) {
|
|
274
|
+
badRequest(res, v.error, route.url.pathname);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const body = v.data;
|
|
278
|
+
if (ctx.contractsService) {
|
|
279
|
+
try {
|
|
280
|
+
const result = await ctx.contractsService.completeContract(id);
|
|
281
|
+
ok(res, result, { self: `/api/v1/contracts/${id}` });
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
internalError(res, err.message);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const privateKey = await resolvePrivateKey(ctx.config.dataDir, body.did, body.passphrase);
|
|
290
|
+
if (!privateKey) {
|
|
291
|
+
badRequest(res, 'Key unavailable', route.url.pathname);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const prev = await contractPrev(id);
|
|
296
|
+
const envelope = await createContractCompleteEnvelope({
|
|
297
|
+
issuer: body.did,
|
|
298
|
+
privateKey,
|
|
299
|
+
contractId: id,
|
|
300
|
+
resourcePrev: prev,
|
|
301
|
+
ts: body.ts ?? Date.now(),
|
|
302
|
+
nonce: body.nonce,
|
|
303
|
+
prev: body.prev,
|
|
304
|
+
});
|
|
305
|
+
const hash = await ctx.publishEvent(envelope);
|
|
306
|
+
ok(res, { txHash: hash, contractId: id, status: 'completed', timestamp: body.ts ?? Date.now() }, { self: `/api/v1/contracts/${id}` });
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
internalError(res, err.message || 'Contract complete failed');
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
// ── POST /:id/actions/terminate ───────────────────────────────
|
|
313
|
+
r.post('/:id/actions/terminate', async (_req, res, route) => {
|
|
314
|
+
const { id } = route.params;
|
|
315
|
+
const v = validate(ContractSettlementSchema, route.body);
|
|
316
|
+
if (!v.success) {
|
|
317
|
+
badRequest(res, v.error, route.url.pathname);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const body = v.data;
|
|
321
|
+
if (ctx.contractsService) {
|
|
322
|
+
try {
|
|
323
|
+
const result = await ctx.contractsService.terminateContract(id, body.notes ?? '');
|
|
324
|
+
ok(res, result, { self: `/api/v1/contracts/${id}` });
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
internalError(res, err.message);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const privateKey = await resolvePrivateKey(ctx.config.dataDir, body.did, body.passphrase);
|
|
333
|
+
if (!privateKey) {
|
|
334
|
+
badRequest(res, 'Key unavailable', route.url.pathname);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
const prev = await contractPrev(id);
|
|
339
|
+
const envelope = await createContractSettlementExecuteEnvelope({
|
|
340
|
+
issuer: body.did,
|
|
341
|
+
privateKey,
|
|
342
|
+
contractId: id,
|
|
343
|
+
settlement: body.settlement,
|
|
344
|
+
notes: body.notes,
|
|
345
|
+
resourcePrev: prev,
|
|
346
|
+
ts: body.ts ?? Date.now(),
|
|
347
|
+
nonce: body.nonce,
|
|
348
|
+
prev: body.prev,
|
|
349
|
+
});
|
|
350
|
+
const hash = await ctx.publishEvent(envelope);
|
|
351
|
+
ok(res, { txHash: hash, contractId: id, status: 'terminated', timestamp: body.ts ?? Date.now() }, { self: `/api/v1/contracts/${id}` });
|
|
352
|
+
}
|
|
353
|
+
catch (err) {
|
|
354
|
+
internalError(res, err.message || 'Contract terminate failed');
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
// ── POST /:id/actions/dispute ─────────────────────────────────
|
|
358
|
+
r.post('/:id/actions/dispute', async (_req, res, route) => {
|
|
359
|
+
const { id } = route.params;
|
|
360
|
+
const v = validate(ContractDisputeSchema, route.body);
|
|
361
|
+
if (!v.success) {
|
|
362
|
+
badRequest(res, v.error, route.url.pathname);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const body = v.data;
|
|
366
|
+
if (ctx.contractsService) {
|
|
367
|
+
try {
|
|
368
|
+
const result = await ctx.contractsService.disputeContract(id, body.evidence ? JSON.stringify(body.evidence) : (body.reason ?? ''));
|
|
369
|
+
ok(res, result, { self: `/api/v1/contracts/${id}` });
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
internalError(res, err.message);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const privateKey = await resolvePrivateKey(ctx.config.dataDir, body.did, body.passphrase);
|
|
378
|
+
if (!privateKey) {
|
|
379
|
+
badRequest(res, 'Key unavailable', route.url.pathname);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
const prev = await contractPrev(id);
|
|
384
|
+
const envelope = await createContractDisputeOpenEnvelope({
|
|
385
|
+
issuer: body.did,
|
|
386
|
+
privateKey,
|
|
387
|
+
contractId: id,
|
|
388
|
+
reason: body.reason,
|
|
389
|
+
description: body.description,
|
|
390
|
+
evidence: body.evidence,
|
|
391
|
+
resourcePrev: prev,
|
|
392
|
+
ts: body.ts ?? Date.now(),
|
|
393
|
+
nonce: body.nonce,
|
|
394
|
+
prev: body.prev,
|
|
395
|
+
});
|
|
396
|
+
const hash = await ctx.publishEvent(envelope);
|
|
397
|
+
ok(res, {
|
|
398
|
+
id: `dispute-${id}`,
|
|
399
|
+
contractId: id,
|
|
400
|
+
initiator: body.did,
|
|
401
|
+
reason: body.reason,
|
|
402
|
+
status: 'open',
|
|
403
|
+
txHash: hash,
|
|
404
|
+
createdAt: body.ts ?? Date.now(),
|
|
405
|
+
}, { self: `/api/v1/contracts/${id}` });
|
|
406
|
+
}
|
|
407
|
+
catch (err) {
|
|
408
|
+
internalError(res, err.message || 'Contract dispute failed');
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
// ── POST /:id/actions/resolve ─────────────────────────────────
|
|
412
|
+
r.post('/:id/actions/resolve', async (_req, res, route) => {
|
|
413
|
+
const { id } = route.params;
|
|
414
|
+
const v = validate(ContractDisputeResolveSchema, route.body);
|
|
415
|
+
if (!v.success) {
|
|
416
|
+
badRequest(res, v.error, route.url.pathname);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const body = v.data;
|
|
420
|
+
if (ctx.contractsService) {
|
|
421
|
+
try {
|
|
422
|
+
const resolutionMap = {
|
|
423
|
+
favor_provider: 0,
|
|
424
|
+
favor_client: 1,
|
|
425
|
+
resume: 2,
|
|
426
|
+
};
|
|
427
|
+
const result = await ctx.contractsService.resolveDispute(id, resolutionMap[body.resolution] ?? 0);
|
|
428
|
+
ok(res, result, { self: `/api/v1/contracts/${id}` });
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
internalError(res, err.message);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const privateKey = await resolvePrivateKey(ctx.config.dataDir, body.did, body.passphrase);
|
|
437
|
+
if (!privateKey) {
|
|
438
|
+
badRequest(res, 'Key unavailable', route.url.pathname);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
const prev = await contractPrev(id);
|
|
443
|
+
const envelope = await createContractDisputeResolveEnvelope({
|
|
444
|
+
issuer: body.did,
|
|
445
|
+
privateKey,
|
|
446
|
+
contractId: id,
|
|
447
|
+
resolution: body.resolution,
|
|
448
|
+
notes: body.notes,
|
|
449
|
+
resourcePrev: prev,
|
|
450
|
+
ts: body.ts ?? Date.now(),
|
|
451
|
+
nonce: body.nonce,
|
|
452
|
+
prev: body.prev,
|
|
453
|
+
});
|
|
454
|
+
const hash = await ctx.publishEvent(envelope);
|
|
455
|
+
ok(res, {
|
|
456
|
+
txHash: hash,
|
|
457
|
+
contractId: id,
|
|
458
|
+
resolution: body.resolution,
|
|
459
|
+
status: 'resolved',
|
|
460
|
+
timestamp: body.ts ?? Date.now(),
|
|
461
|
+
}, { self: `/api/v1/contracts/${id}` });
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
internalError(res, err.message || 'Contract dispute resolve failed');
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
// ── GET /:id/milestones — list milestones ─────────────────────
|
|
468
|
+
r.get('/:id/milestones', async (_req, res, route) => {
|
|
469
|
+
const { id } = route.params;
|
|
470
|
+
const contract = await getContractFromStore(id);
|
|
471
|
+
if (!contract) {
|
|
472
|
+
notFound(res, `Contract ${id} not found`);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
ok(res, contract.milestones ?? [], { self: `/api/v1/contracts/${id}/milestones` });
|
|
476
|
+
});
|
|
477
|
+
// ── GET /:id/milestones/:idx ──────────────────────────────────
|
|
478
|
+
r.get('/:id/milestones/:idx', async (_req, res, route) => {
|
|
479
|
+
const { id, idx } = route.params;
|
|
480
|
+
const index = Number(idx);
|
|
481
|
+
const contract = await getContractFromStore(id);
|
|
482
|
+
if (!contract) {
|
|
483
|
+
notFound(res, `Contract ${id} not found`);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const ms = contract.milestones?.[index];
|
|
487
|
+
if (!ms) {
|
|
488
|
+
notFound(res, `Milestone ${idx} not found`);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
ok(res, ms, { self: `/api/v1/contracts/${id}/milestones/${idx}` });
|
|
492
|
+
});
|
|
493
|
+
// ── POST /:id/milestones/:idx/actions/submit ──────────────────
|
|
494
|
+
r.post('/:id/milestones/:idx/actions/submit', async (_req, res, route) => {
|
|
495
|
+
const { id, idx } = route.params;
|
|
496
|
+
const index = Number(idx);
|
|
497
|
+
const v = validate(ContractMilestoneSubmitSchema, route.body);
|
|
498
|
+
if (!v.success) {
|
|
499
|
+
badRequest(res, v.error, route.url.pathname);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const body = v.data;
|
|
503
|
+
if (ctx.contractsService) {
|
|
504
|
+
try {
|
|
505
|
+
let digest;
|
|
506
|
+
if (body.envelopeDigest) {
|
|
507
|
+
// New path: caller provides pre-computed BLAKE3(JCS(envelope)) hex
|
|
508
|
+
digest = body.envelopeDigest;
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
// Legacy path: hash JSON-stringified deliverables via keccak256
|
|
512
|
+
const raw = body.deliverables
|
|
513
|
+
? JSON.stringify(body.deliverables)
|
|
514
|
+
: (body.notes ?? '');
|
|
515
|
+
digest = legacyHash(raw);
|
|
516
|
+
}
|
|
517
|
+
const result = await ctx.contractsService.submitMilestone(id, index, digest);
|
|
518
|
+
ok(res, result, { self: `/api/v1/contracts/${id}/milestones/${idx}` });
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
catch (err) {
|
|
522
|
+
internalError(res, err.message);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const privateKey = await resolvePrivateKey(ctx.config.dataDir, body.did, body.passphrase);
|
|
527
|
+
if (!privateKey) {
|
|
528
|
+
badRequest(res, 'Key unavailable', route.url.pathname);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
const prev = await contractPrev(id);
|
|
533
|
+
const envelope = await createContractMilestoneSubmitEnvelope({
|
|
534
|
+
issuer: body.did,
|
|
535
|
+
privateKey,
|
|
536
|
+
contractId: id,
|
|
537
|
+
milestoneId: String(index),
|
|
538
|
+
submissionId: body.submissionId ?? `sub-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
539
|
+
notes: body.notes,
|
|
540
|
+
resourcePrev: prev,
|
|
541
|
+
ts: body.ts ?? Date.now(),
|
|
542
|
+
nonce: body.nonce,
|
|
543
|
+
prev: body.prev,
|
|
544
|
+
});
|
|
545
|
+
const hash = await ctx.publishEvent(envelope);
|
|
546
|
+
ok(res, {
|
|
547
|
+
txHash: hash,
|
|
548
|
+
contractId: id,
|
|
549
|
+
milestoneIndex: index,
|
|
550
|
+
status: 'submitted',
|
|
551
|
+
timestamp: body.ts ?? Date.now(),
|
|
552
|
+
}, { self: `/api/v1/contracts/${id}/milestones/${idx}` });
|
|
553
|
+
}
|
|
554
|
+
catch (err) {
|
|
555
|
+
internalError(res, err.message || 'Milestone submit failed');
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
// ── POST /:id/milestones/:idx/actions/approve ─────────────────
|
|
559
|
+
r.post('/:id/milestones/:idx/actions/approve', async (_req, res, route) => {
|
|
560
|
+
const { id, idx } = route.params;
|
|
561
|
+
const index = Number(idx);
|
|
562
|
+
const v = validate(ContractMilestoneReviewSchema, route.body);
|
|
563
|
+
if (!v.success) {
|
|
564
|
+
badRequest(res, v.error, route.url.pathname);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
const body = v.data;
|
|
568
|
+
if (ctx.contractsService) {
|
|
569
|
+
try {
|
|
570
|
+
const result = await ctx.contractsService.approveMilestone(id, index);
|
|
571
|
+
ok(res, result, { self: `/api/v1/contracts/${id}/milestones/${idx}` });
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
catch (err) {
|
|
575
|
+
internalError(res, err.message);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const privateKey = await resolvePrivateKey(ctx.config.dataDir, body.did, body.passphrase);
|
|
580
|
+
if (!privateKey) {
|
|
581
|
+
badRequest(res, 'Key unavailable', route.url.pathname);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
try {
|
|
585
|
+
const prev = await contractPrev(id);
|
|
586
|
+
const envelope = await createContractMilestoneApproveEnvelope({
|
|
587
|
+
issuer: body.did,
|
|
588
|
+
privateKey,
|
|
589
|
+
contractId: id,
|
|
590
|
+
milestoneId: String(index),
|
|
591
|
+
notes: body.notes,
|
|
592
|
+
resourcePrev: prev,
|
|
593
|
+
ts: body.ts ?? Date.now(),
|
|
594
|
+
nonce: body.nonce,
|
|
595
|
+
prev: body.prev,
|
|
596
|
+
});
|
|
597
|
+
const hash = await ctx.publishEvent(envelope);
|
|
598
|
+
ok(res, {
|
|
599
|
+
txHash: hash,
|
|
600
|
+
contractId: id,
|
|
601
|
+
milestoneIndex: index,
|
|
602
|
+
status: 'approved',
|
|
603
|
+
timestamp: body.ts ?? Date.now(),
|
|
604
|
+
}, { self: `/api/v1/contracts/${id}/milestones/${idx}` });
|
|
605
|
+
}
|
|
606
|
+
catch (err) {
|
|
607
|
+
internalError(res, err.message || 'Milestone approve failed');
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
// ── POST /:id/milestones/:idx/actions/reject ──────────────────
|
|
611
|
+
r.post('/:id/milestones/:idx/actions/reject', async (_req, res, route) => {
|
|
612
|
+
const { id, idx } = route.params;
|
|
613
|
+
const index = Number(idx);
|
|
614
|
+
const v = validate(ContractMilestoneReviewSchema, route.body);
|
|
615
|
+
if (!v.success) {
|
|
616
|
+
badRequest(res, v.error, route.url.pathname);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const body = v.data;
|
|
620
|
+
if (ctx.contractsService) {
|
|
621
|
+
try {
|
|
622
|
+
const reason = body.feedback ?? body.notes ?? '';
|
|
623
|
+
const result = await ctx.contractsService.rejectMilestone(id, index, reason);
|
|
624
|
+
ok(res, result, { self: `/api/v1/contracts/${id}/milestones/${idx}` });
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
catch (err) {
|
|
628
|
+
internalError(res, err.message);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const privateKey = await resolvePrivateKey(ctx.config.dataDir, body.did, body.passphrase);
|
|
633
|
+
if (!privateKey) {
|
|
634
|
+
badRequest(res, 'Key unavailable', route.url.pathname);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
const prev = await contractPrev(id);
|
|
639
|
+
const envelope = await createContractMilestoneRejectEnvelope({
|
|
640
|
+
issuer: body.did,
|
|
641
|
+
privateKey,
|
|
642
|
+
contractId: id,
|
|
643
|
+
milestoneId: String(index),
|
|
644
|
+
notes: body.feedback ?? body.notes,
|
|
645
|
+
resourcePrev: prev,
|
|
646
|
+
ts: body.ts ?? Date.now(),
|
|
647
|
+
nonce: body.nonce,
|
|
648
|
+
prev: body.prev,
|
|
649
|
+
});
|
|
650
|
+
const hash = await ctx.publishEvent(envelope);
|
|
651
|
+
ok(res, {
|
|
652
|
+
txHash: hash,
|
|
653
|
+
contractId: id,
|
|
654
|
+
milestoneIndex: index,
|
|
655
|
+
status: 'rejected',
|
|
656
|
+
timestamp: body.ts ?? Date.now(),
|
|
657
|
+
}, { self: `/api/v1/contracts/${id}/milestones/${idx}` });
|
|
658
|
+
}
|
|
659
|
+
catch (err) {
|
|
660
|
+
internalError(res, err.message || 'Milestone reject failed');
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
return r;
|
|
664
|
+
}
|
|
665
|
+
//# sourceMappingURL=contracts.js.map
|