@ainize/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +20 -0
- package/dist/ain-ledger.d.ts +191 -0
- package/dist/ain-ledger.d.ts.map +1 -0
- package/dist/ain-ledger.js +714 -0
- package/dist/ain-ledger.js.map +1 -0
- package/dist/browser.d.ts +15 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +16 -0
- package/dist/browser.js.map +1 -0
- package/dist/bytes.d.ts +3 -0
- package/dist/bytes.d.ts.map +1 -0
- package/dist/bytes.js +14 -0
- package/dist/bytes.js.map +1 -0
- package/dist/canonical.d.ts +5 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +70 -0
- package/dist/canonical.js.map +1 -0
- package/dist/catalog.d.ts +181 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +496 -0
- package/dist/catalog.js.map +1 -0
- package/dist/config-schema.d.ts +222 -0
- package/dist/config-schema.d.ts.map +1 -0
- package/dist/config-schema.js +302 -0
- package/dist/config-schema.js.map +1 -0
- package/dist/config.d.ts +128 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +330 -0
- package/dist/config.js.map +1 -0
- package/dist/events.d.ts +14 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +20 -0
- package/dist/events.js.map +1 -0
- package/dist/identity.d.ts +15 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +59 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/ledger.d.ts +134 -0
- package/dist/ledger.d.ts.map +1 -0
- package/dist/ledger.js +2 -0
- package/dist/ledger.js.map +1 -0
- package/dist/lineage.d.ts +63 -0
- package/dist/lineage.d.ts.map +1 -0
- package/dist/lineage.js +156 -0
- package/dist/lineage.js.map +1 -0
- package/dist/local-ledger.d.ts +74 -0
- package/dist/local-ledger.d.ts.map +1 -0
- package/dist/local-ledger.js +229 -0
- package/dist/local-ledger.js.map +1 -0
- package/dist/npz.d.ts +107 -0
- package/dist/npz.d.ts.map +1 -0
- package/dist/npz.js +502 -0
- package/dist/npz.js.map +1 -0
- package/dist/ops.d.ts +65 -0
- package/dist/ops.d.ts.map +1 -0
- package/dist/ops.js +9 -0
- package/dist/ops.js.map +1 -0
- package/dist/teach-auth.d.ts +19 -0
- package/dist/teach-auth.d.ts.map +1 -0
- package/dist/teach-auth.js +33 -0
- package/dist/teach-auth.js.map +1 -0
- package/dist/types.d.ts +1096 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +98 -0
- package/dist/types.js.map +1 -0
- package/dist/x402.d.ts +35 -0
- package/dist/x402.d.ts.map +1 -0
- package/dist/x402.js +84 -0
- package/dist/x402.js.map +1 -0
- package/package.json +49 -0
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AinLedger — AI Network blockchain backend built on @ainblockchain/ain-js.
|
|
3
|
+
*
|
|
4
|
+
* Mapping (see 10-출원명세서 §4 원장·거래부, 청구항 11-1..11-3, 19):
|
|
5
|
+
* - anchor → knowledge.explore(): gated exploration (price + gateway_url = this node's x402 endpoint),
|
|
6
|
+
* content = patch manifest JSON → content_hash on-chain; parentEntry → `extends` graph edge
|
|
7
|
+
* (lineage / royalty), relatedEntries → `related` edges (branch siblings, conflicts).
|
|
8
|
+
* Public patch metadata mirrors to /apps/knowledge/market/patches/$id (author-only write rule).
|
|
9
|
+
* - attest → /apps/knowledge/market/attestations/$id/$verifier/$created_at (rule: auth.addr === $verifier && data === null — write-once, item 331)
|
|
10
|
+
* - settle → /apps/knowledge/market/settlements/$id/$tx (rule: buyer or seller)
|
|
11
|
+
* + ain-js access receipt (/apps/knowledge/access/$buyer/…) written by the buyer after download (recordAccess).
|
|
12
|
+
* - challenge→ /apps/knowledge/market/challenges/$id/$challenger
|
|
13
|
+
* - branch → /apps/knowledge/market/branches/$name (rule: owner)
|
|
14
|
+
* - node → /apps/knowledge/market/nodes/$addr (rule: auth.addr === $addr)
|
|
15
|
+
* - supersede→ /apps/knowledge/market/supersedes/$old/$new
|
|
16
|
+
* - subscribe→ /apps/knowledge/market/subscriptions/$node/$branch
|
|
17
|
+
*
|
|
18
|
+
* Permissions are enforced by the chain's rule engine, not by this process. Every write is a
|
|
19
|
+
* signed transaction from the node identity, so records carry the chain tx hash as `sig`.
|
|
20
|
+
*/
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
import { canonicalJson, sha256Hex } from './canonical.js';
|
|
23
|
+
import { MAX_CONTRIBUTORS } from './types.js';
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
// ain-js is CommonJS with `exports.default`
|
|
26
|
+
const AinCtor = require('@ainblockchain/ain-js').default;
|
|
27
|
+
/** Genesis account of the 1-node local dev chain (blockchain-configs/1-node) — public test key, dev only. */
|
|
28
|
+
export const LOCAL_GENESIS = {
|
|
29
|
+
address: '0x00ADEc28B6a845a085e03591bE7550dd68673C1C',
|
|
30
|
+
privateKey: 'b22c95ffc4a5c096f7d7d0487ba963ce6ac945bdc91c79b64ce209de289bec96',
|
|
31
|
+
};
|
|
32
|
+
/** Fund an address from the local dev chain's genesis account. Refuses non-local providers. */
|
|
33
|
+
export async function fundFromGenesis(providerUrl, to, amount) {
|
|
34
|
+
const u = new URL(providerUrl);
|
|
35
|
+
if (!['localhost', '127.0.0.1', '0.0.0.0', '::1'].includes(u.hostname))
|
|
36
|
+
throw new Error('fundFromGenesis only works against a local dev chain');
|
|
37
|
+
const ain = new AinCtor(providerUrl, null, 0);
|
|
38
|
+
ain.wallet.addAndSetDefaultAccount(LOCAL_GENESIS.privateKey);
|
|
39
|
+
const res = await ain.wallet.transfer({ to, value: amount, nonce: -1 });
|
|
40
|
+
if (!res?.tx_hash)
|
|
41
|
+
throw new Error(`transfer failed: ${JSON.stringify(res).slice(0, 200)}`);
|
|
42
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
43
|
+
return { tx_hash: res.tx_hash, balance: await ain.wallet.getBalance(to) };
|
|
44
|
+
}
|
|
45
|
+
/** Quick reachability probe for an AIN node. */
|
|
46
|
+
export async function ainReachable(providerUrl, timeoutMs = 3000) {
|
|
47
|
+
try {
|
|
48
|
+
const r = await fetch(`${providerUrl.replace(/\/$/, '')}/node_status`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
49
|
+
if (!r.ok)
|
|
50
|
+
return false;
|
|
51
|
+
const j = (await r.json());
|
|
52
|
+
return !!j.result?.health && j.result?.state === 'SERVING';
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const APP = '/apps/knowledge';
|
|
59
|
+
const MARKET = `${APP}/market`;
|
|
60
|
+
function keyOf(path) {
|
|
61
|
+
return path.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* AIN state values cannot hold JS arrays (only objects/primitives) and empty objects are pruned.
|
|
65
|
+
* toAin(): arrays → {"0":…,"1":…}; empty arrays/objects → null (key omitted); undefined → omitted.
|
|
66
|
+
* fromAin(): objects whose keys are exactly 0..n-1 → arrays. Our record schemas never use such keys otherwise.
|
|
67
|
+
*/
|
|
68
|
+
export function toAin(v) {
|
|
69
|
+
if (v === undefined)
|
|
70
|
+
return undefined;
|
|
71
|
+
if (v === null)
|
|
72
|
+
return null;
|
|
73
|
+
if (Array.isArray(v)) {
|
|
74
|
+
if (!v.length)
|
|
75
|
+
return null;
|
|
76
|
+
const o = {};
|
|
77
|
+
v.forEach((x, i) => { const y = toAin(x); if (y !== undefined)
|
|
78
|
+
o[String(i)] = y; });
|
|
79
|
+
return Object.keys(o).length ? o : null;
|
|
80
|
+
}
|
|
81
|
+
if (typeof v === 'object') {
|
|
82
|
+
const o = {};
|
|
83
|
+
for (const [k, x] of Object.entries(v)) {
|
|
84
|
+
const y = toAin(x);
|
|
85
|
+
if (y !== undefined && y !== null)
|
|
86
|
+
o[k] = y;
|
|
87
|
+
}
|
|
88
|
+
return Object.keys(o).length ? o : null;
|
|
89
|
+
}
|
|
90
|
+
return v;
|
|
91
|
+
}
|
|
92
|
+
export function fromAin(v) {
|
|
93
|
+
if (v === null || v === undefined)
|
|
94
|
+
return v;
|
|
95
|
+
if (typeof v !== 'object')
|
|
96
|
+
return v;
|
|
97
|
+
const o = v;
|
|
98
|
+
const keys = Object.keys(o);
|
|
99
|
+
if (keys.length && keys.every((k, i) => k === String(i)))
|
|
100
|
+
return keys.map((k) => fromAin(o[k]));
|
|
101
|
+
const out = {};
|
|
102
|
+
for (const k of keys)
|
|
103
|
+
out[k] = fromAin(o[k]);
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Restore array fields that `toAin()` dropped because they were empty. Every record kind is handled with an explicit
|
|
108
|
+
* per-field line below (a generic field list cannot tell an anchor's `parents` from a branch's `patch_ids`).
|
|
109
|
+
*/
|
|
110
|
+
export function withEmptyArrays(body) {
|
|
111
|
+
if (!body || typeof body !== 'object')
|
|
112
|
+
return body;
|
|
113
|
+
const b = body;
|
|
114
|
+
if ('author' in b && 'patch_sha256' in b) {
|
|
115
|
+
// anchor
|
|
116
|
+
b.parents = b.parents ?? [];
|
|
117
|
+
b.parent_authors = b.parent_authors ?? [];
|
|
118
|
+
b.contributors = Array.isArray(b.contributors) ? b.contributors.slice(0, MAX_CONTRIBUTORS) : [];
|
|
119
|
+
// lineage fields (design §5.1): the three arrays inside them are restored only when the parent object exists,
|
|
120
|
+
// so a pre-lineage anchor reads back exactly as it was written
|
|
121
|
+
const dv = b.derivation;
|
|
122
|
+
if (dv && typeof dv === 'object')
|
|
123
|
+
dv.bases = dv.bases ?? [];
|
|
124
|
+
const bs = b.base;
|
|
125
|
+
if (bs && typeof bs === 'object')
|
|
126
|
+
bs.stack = bs.stack ?? [];
|
|
127
|
+
const ds = b.dataset;
|
|
128
|
+
if (ds && typeof ds === 'object' && ds.access !== undefined)
|
|
129
|
+
ds.parents = ds.parents ?? [];
|
|
130
|
+
}
|
|
131
|
+
if ('benchmark' in b && b.benchmark && typeof b.benchmark === 'object') {
|
|
132
|
+
const bm = b.benchmark;
|
|
133
|
+
bm.format = bm.format ?? [];
|
|
134
|
+
bm.samples = bm.samples ?? [];
|
|
135
|
+
}
|
|
136
|
+
if ('roles' in b || 'endpoint' in b) {
|
|
137
|
+
b.roles = b.roles ?? [];
|
|
138
|
+
b.branches = b.branches ?? [];
|
|
139
|
+
b.blobs = b.blobs ?? [];
|
|
140
|
+
b.datasets = b.datasets ?? [];
|
|
141
|
+
}
|
|
142
|
+
if ('context' in b && 'owner' in b) {
|
|
143
|
+
b.patch_ids = b.patch_ids ?? [];
|
|
144
|
+
b.context = b.context ?? {};
|
|
145
|
+
}
|
|
146
|
+
if ('action' in b && 'branch' in b) {
|
|
147
|
+
b.patch_ids = b.patch_ids ?? [];
|
|
148
|
+
}
|
|
149
|
+
if ('royalty' in b) {
|
|
150
|
+
b.royalty = b.royalty ?? {};
|
|
151
|
+
}
|
|
152
|
+
if ('score' in b) {
|
|
153
|
+
b.score = b.score ?? {};
|
|
154
|
+
}
|
|
155
|
+
return body;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Rebuild ledger records from the decoded /apps/knowledge/market subtree (pure; used by refresh() and unit tests).
|
|
159
|
+
* Records are sorted oldest → newest by `ts`. Supersede and subscription values written before `created_at`
|
|
160
|
+
* existed carry no timestamp of their own: a supersede is dated right after the newest attestation (or the
|
|
161
|
+
* anchor) of the *new* patch — it is written the moment that patch reaches quorum — and authored by that
|
|
162
|
+
* patch's author; a subscription is dated right after its branch was created.
|
|
163
|
+
*/
|
|
164
|
+
export function recordsFromMarketState(market) {
|
|
165
|
+
const recs = [];
|
|
166
|
+
const push = (kind, ref, rawBody, author, ts) => {
|
|
167
|
+
const body = withEmptyArrays(rawBody);
|
|
168
|
+
recs.push({ hash: sha256Hex(`${ref}:${canonicalJson(body)}`), kind, body, author, ts, parents: [], sig: '' });
|
|
169
|
+
};
|
|
170
|
+
const anchorTs = new Map();
|
|
171
|
+
const anchorAuthor = new Map();
|
|
172
|
+
const attestTs = new Map();
|
|
173
|
+
const branchTs = new Map();
|
|
174
|
+
for (const [id, a] of Object.entries(market?.patches ?? {}))
|
|
175
|
+
if (a && typeof a.patch_sha256 === 'string' && a.author) {
|
|
176
|
+
push('anchor', `${MARKET}/patches/${id}`, a, a.author, a.created_at ?? 0);
|
|
177
|
+
anchorTs.set(id, a.created_at ?? 0);
|
|
178
|
+
anchorAuthor.set(id, a.author);
|
|
179
|
+
}
|
|
180
|
+
for (const [id, m] of Object.entries(market?.attestations ?? {}))
|
|
181
|
+
for (const [v, slot] of Object.entries(m)) {
|
|
182
|
+
// Two shapes live under one verifier (item 331). Records written before 2026-09 are a single value at
|
|
183
|
+
// `…/$id/$verifier`, overwritten in place by every re-verification. Records written since are one write-once
|
|
184
|
+
// child per `created_at`, so a PASS that was challenged and re-passed leaves both on the chain — which is what
|
|
185
|
+
// `effectiveAttestation` needs to choose among, and the only history a verifier can be held to.
|
|
186
|
+
const legacy = slot && typeof slot.passed === 'boolean';
|
|
187
|
+
const records = legacy ? [['', slot]] : Object.entries(slot ?? {});
|
|
188
|
+
for (const [key, at] of records) {
|
|
189
|
+
if (!at || at.patch_id !== id || at.verifier !== v || typeof at.passed !== 'boolean')
|
|
190
|
+
continue;
|
|
191
|
+
// the legacy ref is kept exactly as it was: the record hash is derived from it
|
|
192
|
+
push('attest', legacy ? `${MARKET}/attestations/${id}/${v}` : `${MARKET}/attestations/${id}/${v}/${key}`, at, v, at.created_at ?? 0);
|
|
193
|
+
attestTs.set(id, Math.max(attestTs.get(id) ?? 0, at.created_at ?? 0));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (const [id, m] of Object.entries(market?.settlements ?? {}))
|
|
197
|
+
for (const [k, s] of Object.entries(m))
|
|
198
|
+
push('settle', `${MARKET}/settlements/${id}/${k}`, s, s.seller, s.created_at ?? 0);
|
|
199
|
+
for (const [id, m] of Object.entries(market?.challenges ?? {}))
|
|
200
|
+
for (const [c, ch] of Object.entries(m))
|
|
201
|
+
push('challenge', `${MARKET}/challenges/${id}/${c}`, ch, c, ch.created_at ?? 0);
|
|
202
|
+
for (const [k, b] of Object.entries(market?.branches ?? {})) {
|
|
203
|
+
push('branch', `${MARKET}/branches/${k}`, b, b.owner, b.created_at ?? 0);
|
|
204
|
+
if (typeof b.name === 'string')
|
|
205
|
+
branchTs.set(b.name, b.created_at ?? 0);
|
|
206
|
+
}
|
|
207
|
+
for (const [addr, n] of Object.entries(market?.nodes ?? {}))
|
|
208
|
+
push('node', `${MARKET}/nodes/${addr}`, n, addr, n.last_seen ?? 0);
|
|
209
|
+
for (const [o, m] of Object.entries(market?.supersedes ?? {}))
|
|
210
|
+
for (const [nw, slot] of Object.entries(m)) {
|
|
211
|
+
// Two shapes, as with attestations: the legacy slot IS the record (nobody's address in the path, so the
|
|
212
|
+
// best `author` available is the new anchor's — which is exactly what a reader has to check it against),
|
|
213
|
+
// and the current one is keyed by the address that wrote it, which the rule engine enforces.
|
|
214
|
+
const legacy = slot && typeof slot.old_patch_id === 'string';
|
|
215
|
+
const entries = legacy ? [['', slot]] : Object.entries(slot ?? {});
|
|
216
|
+
for (const [writer, s] of entries) {
|
|
217
|
+
if (!s || typeof s !== 'object')
|
|
218
|
+
continue;
|
|
219
|
+
const related = Math.max(attestTs.get(nw) ?? 0, anchorTs.get(nw) ?? 0);
|
|
220
|
+
const ts = typeof s.created_at === 'number' && s.created_at > 0 ? s.created_at : related ? related + 1 : 0;
|
|
221
|
+
push('supersede', legacy ? `${MARKET}/supersedes/${o}/${nw}` : `${MARKET}/supersedes/${o}/${nw}/${writer}`, s, legacy ? anchorAuthor.get(nw) ?? '' : writer, ts);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
for (const [id, per] of Object.entries(market?.prices ?? {}))
|
|
225
|
+
for (const [author, slots] of Object.entries(per ?? {}))
|
|
226
|
+
for (const [key, p] of Object.entries(slots ?? {}))
|
|
227
|
+
if (p && typeof p.price === 'string')
|
|
228
|
+
push('price', `${MARKET}/prices/${id}/${author}/${key}`, p, author, p.created_at ?? 0);
|
|
229
|
+
for (const [hash, per] of Object.entries(market?.payouts ?? {}))
|
|
230
|
+
for (const [to, p] of Object.entries(per ?? {}))
|
|
231
|
+
if (p && typeof p.tx_hash === 'string')
|
|
232
|
+
push('payout', `${MARKET}/payouts/${hash}/${to}`, p, p.seller ?? '', p.created_at ?? 0);
|
|
233
|
+
for (const [id, m] of Object.entries(market?.retires ?? {}))
|
|
234
|
+
for (const [author, r] of Object.entries(m))
|
|
235
|
+
if (r && typeof r.patch_id === 'string')
|
|
236
|
+
push('retire', `${MARKET}/retires/${id}/${author}`, r, author, r.created_at ?? 0);
|
|
237
|
+
for (const [id, m] of Object.entries(market?.disputes ?? {}))
|
|
238
|
+
for (const [settleHash, per] of Object.entries(m ?? {}))
|
|
239
|
+
for (const [author, d] of Object.entries(per ?? {}))
|
|
240
|
+
if (d && typeof d.reason === 'string')
|
|
241
|
+
push('dispute', `${MARKET}/disputes/${id}/${settleHash}/${author}`, d, author, d.created_at ?? 0);
|
|
242
|
+
for (const [node, m] of Object.entries(market?.subscriptions ?? {}))
|
|
243
|
+
for (const [b, s] of Object.entries(m)) {
|
|
244
|
+
const related = branchTs.get(s?.branch) ?? 0;
|
|
245
|
+
const ts = typeof s.created_at === 'number' && s.created_at > 0 ? s.created_at : related ? related + 1 : 0;
|
|
246
|
+
push('subscribe', `${MARKET}/subscriptions/${node}/${b}`, s, node, ts);
|
|
247
|
+
}
|
|
248
|
+
recs.sort((x, y) => x.ts - y.ts);
|
|
249
|
+
return recs;
|
|
250
|
+
}
|
|
251
|
+
export class AinLedger {
|
|
252
|
+
opts;
|
|
253
|
+
identity;
|
|
254
|
+
events;
|
|
255
|
+
pollMs;
|
|
256
|
+
kind = 'ain';
|
|
257
|
+
ain;
|
|
258
|
+
cache = [];
|
|
259
|
+
lastPoll = 0;
|
|
260
|
+
pollTimer = null;
|
|
261
|
+
constructor(opts, identity, events = {}, pollMs = 8000) {
|
|
262
|
+
this.opts = opts;
|
|
263
|
+
this.identity = identity;
|
|
264
|
+
this.events = events;
|
|
265
|
+
this.pollMs = pollMs;
|
|
266
|
+
this.ain = new AinCtor(opts.providerUrl, opts.eventHandlerUrl ?? null, opts.chainId);
|
|
267
|
+
this.ain.wallet.addAndSetDefaultAccount(identity.privateKey);
|
|
268
|
+
}
|
|
269
|
+
get address() { return this.identity.address; }
|
|
270
|
+
async init() {
|
|
271
|
+
await this.refresh();
|
|
272
|
+
this.pollTimer = setInterval(() => { this.refresh().catch(() => undefined); }, this.pollMs);
|
|
273
|
+
this.pollTimer.unref?.();
|
|
274
|
+
}
|
|
275
|
+
async close() {
|
|
276
|
+
if (this.pollTimer)
|
|
277
|
+
clearInterval(this.pollTimer);
|
|
278
|
+
}
|
|
279
|
+
async info() {
|
|
280
|
+
let height;
|
|
281
|
+
try {
|
|
282
|
+
height = Number(await this.ain.getLastBlockNumber());
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
height = undefined;
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
kind: 'ain', network: `ain:${this.opts.chainId === 0 ? 'local' : 'mainnet'}`, provider: this.opts.providerUrl,
|
|
289
|
+
app: APP, height, records: this.cache.length, valid: true,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
// ---------------------------------------------------------------- chain helpers
|
|
293
|
+
tx(extra = {}) {
|
|
294
|
+
return { nonce: -1, gas_price: this.opts.gasPrice ?? 0, ...extra };
|
|
295
|
+
}
|
|
296
|
+
async set(ref, value) {
|
|
297
|
+
const encoded = toAin(value);
|
|
298
|
+
const res = await this.ain.db.ref(ref).setValue({ value: encoded, ...this.tx() });
|
|
299
|
+
this.noteGas(res); // what this write cost, measured (finding 366)
|
|
300
|
+
return AinLedger.assertOk(res, ref);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* What this node's chain writes have actually cost (finding 366).
|
|
304
|
+
*
|
|
305
|
+
* The publish form gives no pricing guidance and the default price is 0.1, while the product's own estimate is
|
|
306
|
+
* ~0.19 AIN of gas around a 0.1 AIN purchase at `min_gas_price 500` — so on a real network the default price
|
|
307
|
+
* makes every sale a loss. Nothing here is estimated: `gas_cost_total` comes back on every write this node
|
|
308
|
+
* makes, and this is the average of the ones it has seen. The dev chain sends `gas_price 0`, so it measures 0
|
|
309
|
+
* and the warning correctly says nothing.
|
|
310
|
+
*/
|
|
311
|
+
gas = { writes: 0, total: 0 };
|
|
312
|
+
gasStats() {
|
|
313
|
+
return this.gas.writes ? { writes: this.gas.writes, total: Math.round(this.gas.total * 1e6) / 1e6, avg: Math.round((this.gas.total / this.gas.writes) * 1e6) / 1e6 } : null;
|
|
314
|
+
}
|
|
315
|
+
noteGas(res) {
|
|
316
|
+
const cost = Number(res?.result?.gas_cost_total ?? res?.result?.result_list?.['0']?.gas_cost_total ?? NaN);
|
|
317
|
+
if (Number.isFinite(cost)) {
|
|
318
|
+
this.gas.writes++;
|
|
319
|
+
this.gas.total += cost;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
static assertOk(res, what) {
|
|
323
|
+
const code = res?.result?.code ?? res?.result?.result_list?.['0']?.code;
|
|
324
|
+
if (res?.result?.message && code !== 0 && code !== undefined) {
|
|
325
|
+
throw new Error(`AIN write rejected (${what}): ${res.result.message}`);
|
|
326
|
+
}
|
|
327
|
+
if (res?.result?.code && res.result.code !== 0)
|
|
328
|
+
throw new Error(`AIN write rejected (${what}): code ${res.result.code}`);
|
|
329
|
+
return res?.tx_hash ?? '';
|
|
330
|
+
}
|
|
331
|
+
async getValue(ref) {
|
|
332
|
+
return fromAin(await this.ain.db.ref(ref).getValue());
|
|
333
|
+
}
|
|
334
|
+
/** AIN balance; an address the chain has never seen has no account yet (getBalance → null) and counts as 0. */
|
|
335
|
+
async balance(address = this.identity.address) {
|
|
336
|
+
const raw = (await this.ain.wallet.getBalance(address));
|
|
337
|
+
return Number(raw ?? 0) || 0;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Send AIN. With `key`, the transfer is written at /transfer/$from/$to/$key instead of a random push id, which is
|
|
341
|
+
* how an x402 payment is bound to the quote it answers: the seller recomputes the key from (resource, nonce) and
|
|
342
|
+
* refuses a transfer that carries any other one (finding 344). Without `key` this is ain-js's own push behaviour,
|
|
343
|
+
* used by the royalty payouts, which answer no quote.
|
|
344
|
+
*/
|
|
345
|
+
async transfer(to, value, key) {
|
|
346
|
+
if (!key) {
|
|
347
|
+
const res = await this.ain.wallet.transfer({ to, value, nonce: -1 });
|
|
348
|
+
return { tx_hash: AinLedger.assertOk(res, `transfer→${to}`), key: '' };
|
|
349
|
+
}
|
|
350
|
+
if (!/^[A-Za-z0-9_-]{1,120}$/.test(key))
|
|
351
|
+
throw new Error(`invalid transfer key ${JSON.stringify(key)}`);
|
|
352
|
+
if (!(value > 0))
|
|
353
|
+
throw new Error(`non-positive transfer value ${value}`);
|
|
354
|
+
const from = this.identity.address;
|
|
355
|
+
const res = await this.ain.db.ref(`/transfer/${from}/${to}/${key}/value`).setValue({ value, ...this.tx() });
|
|
356
|
+
this.noteGas(res);
|
|
357
|
+
return { tx_hash: AinLedger.assertOk(res, `transfer→${to}`), key };
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Several transfers in ONE transaction (finding 366).
|
|
361
|
+
*
|
|
362
|
+
* A sale with three ancestors wrote three separate transfers, each paying its own gas — the product's own
|
|
363
|
+
* estimate is ~0.19 AIN of gas around a 0.1 AIN purchase at `min_gas_price 500`, so a family of three
|
|
364
|
+
* multiplied the loss by three. One `SET` with an op_list per payee costs one write.
|
|
365
|
+
*/
|
|
366
|
+
async transferMany(items) {
|
|
367
|
+
if (!items.length)
|
|
368
|
+
throw new Error('no transfers to make');
|
|
369
|
+
const from = this.identity.address;
|
|
370
|
+
for (const it of items) {
|
|
371
|
+
if (!/^[A-Za-z0-9_-]{1,120}$/.test(it.key))
|
|
372
|
+
throw new Error(`invalid transfer key ${JSON.stringify(it.key)}`);
|
|
373
|
+
if (!(it.value > 0))
|
|
374
|
+
throw new Error(`non-positive transfer value ${it.value}`);
|
|
375
|
+
}
|
|
376
|
+
const op_list = items.map((it) => ({ type: 'SET_VALUE', ref: `/transfer/${from}/${it.to}/${it.key}/value`, value: it.value }));
|
|
377
|
+
const res = await this.ain.sendTransaction({ operation: { type: 'SET', op_list }, ...this.tx() });
|
|
378
|
+
this.noteGas(res);
|
|
379
|
+
return { tx_hash: AinLedger.assertOk(res, `transfer x${items.length}`) };
|
|
380
|
+
}
|
|
381
|
+
/** The chain's own minimum gas price, straight from `/blockchain_params` — measured, never assumed (finding 366). */
|
|
382
|
+
async minGasPrice() {
|
|
383
|
+
try {
|
|
384
|
+
const v = await this.getValue('/blockchain_params/resource/min_gas_price');
|
|
385
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : null;
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/** Verify an AIN transfer by tx hash: returns {from,to,value} if finalized/executed. */
|
|
392
|
+
async verifyTransfer(txHash) {
|
|
393
|
+
const info = await this.ain.getTransactionByHash(txHash);
|
|
394
|
+
const op = info?.transaction?.tx_body?.operation;
|
|
395
|
+
if (!op || op.type !== 'SET_VALUE')
|
|
396
|
+
return null;
|
|
397
|
+
const m = /^\/transfer\/([^/]+)\/([^/]+)\/([^/]+)\/value$/.exec(op.ref ?? '');
|
|
398
|
+
if (!m)
|
|
399
|
+
return null;
|
|
400
|
+
if (!(info.is_executed || info.is_finalized))
|
|
401
|
+
return null;
|
|
402
|
+
if (info.exec_result?.code && info.exec_result.code !== 0)
|
|
403
|
+
return null;
|
|
404
|
+
return { from: m[1], to: m[2], key: m[3], value: Number(op.value) };
|
|
405
|
+
}
|
|
406
|
+
/** Total AIN staked on the knowledge app (state/bandwidth budget is proportional to app stake). */
|
|
407
|
+
async appStake() {
|
|
408
|
+
const v = await this.ain.db.ref('/staking/knowledge/balance_total').getValue();
|
|
409
|
+
return Number(v ?? 0);
|
|
410
|
+
}
|
|
411
|
+
/** Stake AIN on the knowledge app from this identity (apps on the free tier are capped at ~100 KB of state). */
|
|
412
|
+
async stakeApp(amount) {
|
|
413
|
+
const ref = `/staking/knowledge/${this.identity.address}/0/stake/${Date.now()}/value`;
|
|
414
|
+
const res = await this.ain.db.ref(ref).setValue({ value: amount, ...this.tx() });
|
|
415
|
+
return AinLedger.assertOk(res, 'stake knowledge app');
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* One-time app setup (run by the first node = app admin): ain-js setupApp() + market rules + app stake.
|
|
419
|
+
* Idempotent: if the app exists and we are not admin, only the stake top-up (if we can afford it) is attempted.
|
|
420
|
+
*/
|
|
421
|
+
async setupApp(opts = {}) {
|
|
422
|
+
const cfg = await this.ain.db.ref('/manage_app/knowledge/config').getValue();
|
|
423
|
+
let created = false;
|
|
424
|
+
let tx;
|
|
425
|
+
let admin;
|
|
426
|
+
if (!cfg) {
|
|
427
|
+
const res = await this.ain.knowledge.setupApp({ nonce: -1 });
|
|
428
|
+
tx = AinLedger.assertOk(res, 'setupApp');
|
|
429
|
+
await this.setMarketRules();
|
|
430
|
+
created = true;
|
|
431
|
+
admin = this.identity.address;
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
admin = Object.keys(cfg.admin ?? {})[0];
|
|
435
|
+
if (cfg.admin?.[this.identity.address])
|
|
436
|
+
await this.setMarketRules().catch(() => undefined);
|
|
437
|
+
}
|
|
438
|
+
let staked;
|
|
439
|
+
const want = opts.stake ?? 100;
|
|
440
|
+
try {
|
|
441
|
+
const current = await this.appStake();
|
|
442
|
+
if (current < want) {
|
|
443
|
+
const bal = await this.balance();
|
|
444
|
+
if (bal > want + 10) {
|
|
445
|
+
await this.stakeApp(want);
|
|
446
|
+
staked = want;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
catch { /* staking is best effort */ }
|
|
451
|
+
return { created, tx, admin, staked };
|
|
452
|
+
}
|
|
453
|
+
async setMarketRules() {
|
|
454
|
+
const rules = AinLedger.marketRules();
|
|
455
|
+
const op_list = rules.map(([ref, write]) => ({ type: 'SET_RULE', ref, value: { '.rule': { write } } }));
|
|
456
|
+
const res = await this.ain.sendTransaction({ operation: { type: 'SET', op_list }, ...this.tx() });
|
|
457
|
+
AinLedger.assertOk(res, 'market rules');
|
|
458
|
+
}
|
|
459
|
+
/** The write rules this build sets, and the ones `verify()` holds the chain to. One table, so they cannot drift. */
|
|
460
|
+
static marketRules() {
|
|
461
|
+
return [
|
|
462
|
+
[`${MARKET}/patches/$patch_id`, "auth.addr === newData.author && (data === null || data.author === auth.addr)"],
|
|
463
|
+
[`${MARKET}/attestations/$patch_id/$verifier`, 'auth.addr === $verifier'],
|
|
464
|
+
// item 331 — one slot per measurement, and a written slot is immutable. Without `data === null` a verifier
|
|
465
|
+
// could silently rewrite what it had signed: a FAIL that became a PASS left no trace of either.
|
|
466
|
+
[`${MARKET}/attestations/$patch_id/$verifier/$created_at`, 'auth.addr === $verifier && data === null'],
|
|
467
|
+
[`${MARKET}/settlements/$patch_id/$tx_hash`, 'auth.addr === newData.seller || auth.addr === newData.buyer'],
|
|
468
|
+
[`${MARKET}/challenges/$patch_id/$challenger`, 'auth.addr === $challenger'],
|
|
469
|
+
[`${MARKET}/branches/$branch`, "auth.addr === newData.owner && (data === null || data.owner === auth.addr)"],
|
|
470
|
+
[`${MARKET}/nodes/$addr`, 'auth.addr === $addr'],
|
|
471
|
+
// A supersede said `auth.addr !== ''` — any address at all could mark ANYBODY's anchor superseded, and the
|
|
472
|
+
// record then sat on the permanent public record where every node applied it. The writer goes in the path,
|
|
473
|
+
// the way `retires` and `disputes` already do it, so the rule engine can name them; readers then check that
|
|
474
|
+
// writer against the two anchors' author, which is the rule a supersede actually has (items 151, 363).
|
|
475
|
+
[`${MARKET}/supersedes/$old_id/$new_id`, "auth.addr !== ''"],
|
|
476
|
+
[`${MARKET}/supersedes/$old_id/$new_id/$author`, 'auth.addr === $author && data === null'],
|
|
477
|
+
[`${MARKET}/subscriptions/$node/$branch`, 'auth.addr === $node'],
|
|
478
|
+
[`${MARKET}/retires/$patch_id/$author`, 'auth.addr === $author'],
|
|
479
|
+
// item 347 — a contested sale and the seller's answer to it, one write-once slot per party.
|
|
480
|
+
[`${MARKET}/disputes/$patch_id/$settle_hash/$author`, 'auth.addr === $author && data === null'],
|
|
481
|
+
// item 278 — only the anchor's own author re-prices it, and every price ever set stays on the record.
|
|
482
|
+
[`${MARKET}/prices/$patch_id/$author/$created_at`, 'auth.addr === $author && data === null'],
|
|
483
|
+
// item 314 — the seller's own record of the royalty transfer that honoured a settlement, keyed by settle hash.
|
|
484
|
+
// `|| data === null` let ANY address create the first payout record for any settlement — a fabricated proof
|
|
485
|
+
// that a seller had paid royalties it never paid — and the first clause with no `data === null` let the
|
|
486
|
+
// seller rewrite one afterwards. Every sibling write-once rule here uses `&&`; this one meant to.
|
|
487
|
+
[`${MARKET}/payouts/$settle_hash/$to`, 'auth.addr === newData.seller && data === null'],
|
|
488
|
+
];
|
|
489
|
+
}
|
|
490
|
+
// ---------------------------------------------------------------- Ledger API
|
|
491
|
+
async append(kind, body) {
|
|
492
|
+
const ts = Date.now();
|
|
493
|
+
let txHash = '';
|
|
494
|
+
let ref = '';
|
|
495
|
+
switch (kind) {
|
|
496
|
+
case 'anchor': {
|
|
497
|
+
const a = body;
|
|
498
|
+
ref = `${MARKET}/patches/${a.id}`;
|
|
499
|
+
if ((a.contributors?.length ?? 0) > MAX_CONTRIBUTORS)
|
|
500
|
+
throw new Error(`anchor carries more than ${MAX_CONTRIBUTORS} contributors`);
|
|
501
|
+
// 1) knowledge graph entry with lineage edges (parentEntry = first parent, others related)
|
|
502
|
+
const parentAnchors = await this.anchors();
|
|
503
|
+
const findEntry = (pid) => parentAnchors.find((r) => r.body.id === pid)?.body;
|
|
504
|
+
const parentEntry = a.parents[0] ? findEntry(a.parents[0]) : undefined;
|
|
505
|
+
const related = a.parents.slice(1).map(findEntry).filter(Boolean);
|
|
506
|
+
const manifest = canonicalJson({ id: a.id, patch_sha256: a.patch_sha256, model: a.model, benchmark_hash: a.benchmark_hash, rows: a.rows, size_bytes: a.size_bytes });
|
|
507
|
+
const gateway = a.gateway_url ?? '';
|
|
508
|
+
const ex = await this.ain.knowledge.explore({
|
|
509
|
+
topicPath: a.topic_path || 'patches',
|
|
510
|
+
title: a.name,
|
|
511
|
+
content: manifest,
|
|
512
|
+
summary: a.description.slice(0, 500),
|
|
513
|
+
depth: Math.min(5, Math.max(1, a.parents.length + 1)),
|
|
514
|
+
tags: `patch,${a.model.id_M},${a.benchmark.schema}${a.branch ? ',' + a.branch : ''}`,
|
|
515
|
+
price: a.price,
|
|
516
|
+
gatewayUrl: gateway || null,
|
|
517
|
+
parentEntry: parentEntry?.entry_id ? { ownerAddress: parentEntry.author, topicPath: parentEntry.topic_path, entryId: parentEntry.entry_id } : null,
|
|
518
|
+
relatedEntries: related.filter((r) => r.entry_id).map((r) => ({ ownerAddress: r.author, topicPath: r.topic_path, entryId: r.entry_id, type: 'related' })),
|
|
519
|
+
}, { nonce: -1 });
|
|
520
|
+
AinLedger.assertOk(ex.txResult, 'knowledge.explore');
|
|
521
|
+
a.entry_id = ex.entryId;
|
|
522
|
+
a.node_id = ex.nodeId;
|
|
523
|
+
// 2) public metadata mirror (rule: author only, write-once) — includes the entry id so buyers can knowledge.access() it
|
|
524
|
+
txHash = await this.set(ref, a);
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
case 'attest': {
|
|
528
|
+
const at = body;
|
|
529
|
+
// One write-once slot per measurement (item 331). The old ref held ONE value per verifier and the rule let its
|
|
530
|
+
// author overwrite it, so a re-verification erased what this node had signed before the challenge — the only
|
|
531
|
+
// asset a verifier accumulates is a history it can be held to, and it was silently rewritable by itself.
|
|
532
|
+
ref = `${MARKET}/attestations/${at.patch_id}/${this.identity.address}/${keyOf(String(at.created_at || ts))}`;
|
|
533
|
+
txHash = await this.set(ref, at);
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
case 'settle': {
|
|
537
|
+
const s = body;
|
|
538
|
+
ref = `${MARKET}/settlements/${s.patch_id}/${keyOf(s.tx_hash || String(ts))}`;
|
|
539
|
+
txHash = await this.set(ref, s);
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
case 'dispute': {
|
|
543
|
+
const d = body;
|
|
544
|
+
ref = `${MARKET}/disputes/${d.patch_id}/${keyOf(d.settle_hash)}/${this.identity.address}`;
|
|
545
|
+
txHash = await this.set(ref, d);
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
case 'challenge': {
|
|
549
|
+
const c = body;
|
|
550
|
+
ref = `${MARKET}/challenges/${c.patch_id}/${this.identity.address}`;
|
|
551
|
+
txHash = await this.set(ref, c);
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
case 'branch': {
|
|
555
|
+
const b = body;
|
|
556
|
+
ref = `${MARKET}/branches/${keyOf(b.name)}`;
|
|
557
|
+
txHash = await this.set(ref, b);
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
case 'node': {
|
|
561
|
+
ref = `${MARKET}/nodes/${this.identity.address}`;
|
|
562
|
+
const info = body;
|
|
563
|
+
txHash = await this.set(ref, { ...info, blobs: info.blobs.slice(0, 40) });
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
case 'supersede': {
|
|
567
|
+
const s = body;
|
|
568
|
+
ref = `${MARKET}/supersedes/${s.old_patch_id}/${s.new_patch_id}/${this.identity.address}`;
|
|
569
|
+
txHash = await this.set(ref, s);
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
case 'subscribe': {
|
|
573
|
+
const s = body;
|
|
574
|
+
ref = `${MARKET}/subscriptions/${this.identity.address}/${keyOf(s.branch)}`;
|
|
575
|
+
txHash = await this.set(ref, s);
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
// A takedown of one's own knowledge: the write rule keys it by the retiring address, and only a record whose
|
|
579
|
+
// author is the anchor's author is honoured when the catalogue is derived (node/market.ts).
|
|
580
|
+
case 'retire': {
|
|
581
|
+
const r = body;
|
|
582
|
+
ref = `${MARKET}/retires/${keyOf(r.patch_id)}/${this.identity.address}`;
|
|
583
|
+
txHash = await this.set(ref, r);
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
// A re-pricing of one's own knowledge (item 278). One write-once child per `created_at`, so the whole price
|
|
587
|
+
// history is on the chain and a discount can be checked against the price it was discounted from.
|
|
588
|
+
case 'price': {
|
|
589
|
+
const p = body;
|
|
590
|
+
ref = `${MARKET}/prices/${keyOf(p.patch_id)}/${this.identity.address}/${keyOf(String(p.created_at || ts))}`;
|
|
591
|
+
txHash = await this.set(ref, p);
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
// What the seller actually transferred for one settlement (item 314) — the join between a promise and money.
|
|
595
|
+
case 'payout': {
|
|
596
|
+
const p = body;
|
|
597
|
+
ref = `${MARKET}/payouts/${keyOf(p.settle_hash)}/${keyOf(p.to)}`;
|
|
598
|
+
txHash = await this.set(ref, { ...p, seller: this.identity.address });
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const rec = {
|
|
603
|
+
hash: sha256Hex(`${ref}:${txHash}`), kind, body, author: this.identity.address, ts, parents: [], sig: txHash,
|
|
604
|
+
};
|
|
605
|
+
this.cache.push(rec);
|
|
606
|
+
this.events.onRecord?.(rec);
|
|
607
|
+
return rec;
|
|
608
|
+
}
|
|
609
|
+
/** Records are authoritative on-chain; peers don't push them to us. */
|
|
610
|
+
async ingest() { return false; }
|
|
611
|
+
/** Re-read the market subtree and rebuild the record cache. */
|
|
612
|
+
async refresh() {
|
|
613
|
+
const market = fromAin(await this.ain.db.ref(MARKET).getValue()) ?? {};
|
|
614
|
+
const recs = recordsFromMarketState(market);
|
|
615
|
+
const known = new Set(this.cache.map((r) => r.hash));
|
|
616
|
+
for (const r of recs)
|
|
617
|
+
if (!known.has(r.hash))
|
|
618
|
+
this.events.onRecord?.(r);
|
|
619
|
+
this.cache = recs;
|
|
620
|
+
this.lastPoll = Date.now();
|
|
621
|
+
}
|
|
622
|
+
async list(opts = {}) {
|
|
623
|
+
if (Date.now() - this.lastPoll > this.pollMs)
|
|
624
|
+
await this.refresh().catch(() => undefined);
|
|
625
|
+
let out = this.cache;
|
|
626
|
+
if (opts.kind)
|
|
627
|
+
out = out.filter((r) => r.kind === opts.kind);
|
|
628
|
+
if (opts.since !== undefined)
|
|
629
|
+
out = out.filter((r) => r.ts > opts.since);
|
|
630
|
+
if (opts.limit)
|
|
631
|
+
out = out.slice(-opts.limit);
|
|
632
|
+
return out;
|
|
633
|
+
}
|
|
634
|
+
async get(hash) { return this.cache.find((r) => r.hash === hash) ?? null; }
|
|
635
|
+
async hashes(since = 0) { return (await this.list({ since })).map((r) => r.hash); }
|
|
636
|
+
async byKind(kind, f) {
|
|
637
|
+
const recs = (await this.list({ kind }));
|
|
638
|
+
return f ? recs.filter((r) => f(r.body)) : recs;
|
|
639
|
+
}
|
|
640
|
+
anchors() { return this.byKind('anchor'); }
|
|
641
|
+
attestations(patchId) { return this.byKind('attest', patchId ? (b) => b.patch_id === patchId : undefined); }
|
|
642
|
+
settlements(patchId) { return this.byKind('settle', patchId ? (b) => b.patch_id === patchId : undefined); }
|
|
643
|
+
challenges(patchId) { return this.byKind('challenge', patchId ? (b) => b.patch_id === patchId : undefined); }
|
|
644
|
+
disputes(patchId) { return this.byKind('dispute', patchId ? (b) => b.patch_id === patchId : undefined); }
|
|
645
|
+
branches() { return this.byKind('branch'); }
|
|
646
|
+
nodes() { return this.byKind('node'); }
|
|
647
|
+
supersedes() { return this.byKind('supersede'); }
|
|
648
|
+
subscriptions() { return this.byKind('subscribe'); }
|
|
649
|
+
/**
|
|
650
|
+
* What this node can actually check about the chain it trusts.
|
|
651
|
+
*
|
|
652
|
+
* Consensus guarantees that the records are the ones that were written. It guarantees nothing about WHO was
|
|
653
|
+
* allowed to write them: that is the rule engine, and the rules can only be set by the app admin — the address
|
|
654
|
+
* that happened to run `chain setup` first. This used to read one rule and only ask whether it existed, so an
|
|
655
|
+
* admin who relaxed `attestations` to let anyone sign anyone's verdict, or `patches` to let anyone overwrite
|
|
656
|
+
* anyone's anchor, changed the meaning of every record on the chain and no node said a word.
|
|
657
|
+
*
|
|
658
|
+
* So the rules are compared with the ones this build expects, and a difference is an error naming both. It is
|
|
659
|
+
* not a defence — an admin can still change them — but it is the difference between a silent change and a
|
|
660
|
+
* visible one, which is the most a participant can have while one address owns the rule engine.
|
|
661
|
+
*/
|
|
662
|
+
async verify() {
|
|
663
|
+
const errors = [];
|
|
664
|
+
try {
|
|
665
|
+
const expected = AinLedger.marketRules();
|
|
666
|
+
let seen = 0;
|
|
667
|
+
for (const [ref, want] of expected) {
|
|
668
|
+
const got = await this.ain.db.ref(ref).getRule();
|
|
669
|
+
const write = got?.['.rule']?.write;
|
|
670
|
+
if (write === undefined || write === null) {
|
|
671
|
+
errors.push(`no write rule on ${ref} (run \`ainize chain setup\`)`);
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
seen++;
|
|
675
|
+
if (String(write) !== want)
|
|
676
|
+
errors.push(`the write rule on ${ref} is not the one this build expects — on chain: ${String(write).slice(0, 160)} / expected: ${want.slice(0, 160)}`);
|
|
677
|
+
}
|
|
678
|
+
if (!seen)
|
|
679
|
+
errors.push('market rules not set (run `ainize chain setup`)');
|
|
680
|
+
}
|
|
681
|
+
catch (e) {
|
|
682
|
+
errors.push(`chain unreachable: ${e.message}`);
|
|
683
|
+
}
|
|
684
|
+
return { valid: errors.length === 0, checked: this.cache.length, errors };
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Buyer-side access receipt, exactly where ain-js `knowledge.access()` / `hasAccess()` look:
|
|
688
|
+
* /apps/knowledge/access/$buyer/{owner}_{topicKey}_{entryId} (write rule: auth.addr === $buyer).
|
|
689
|
+
*/
|
|
690
|
+
async recordAccess(anchor, amount, currency, txHash) {
|
|
691
|
+
if (!anchor.entry_id)
|
|
692
|
+
return null;
|
|
693
|
+
const topicKey = (anchor.topic_path || 'patches').replace(/\//g, '|');
|
|
694
|
+
const entryKey = `${anchor.author}_${topicKey}_${anchor.entry_id}`;
|
|
695
|
+
const receipt = { seller: anchor.author, topic_path: anchor.topic_path, entry_id: anchor.entry_id, amount, currency, tx_hash: txHash, accessed_at: Date.now() };
|
|
696
|
+
return this.set(`${APP}/access/${this.identity.address}/${entryKey}`, receipt);
|
|
697
|
+
}
|
|
698
|
+
/** Has `buyer` an on-chain access receipt for this patch (ain-js hasAccess semantics)? */
|
|
699
|
+
async hasAccess(buyer, anchor) {
|
|
700
|
+
if (!anchor.entry_id)
|
|
701
|
+
return false;
|
|
702
|
+
const topicKey = (anchor.topic_path || 'patches').replace(/\//g, '|');
|
|
703
|
+
return this.ain.knowledge.hasAccess(buyer, `${anchor.author}_${topicKey}_${anchor.entry_id}`);
|
|
704
|
+
}
|
|
705
|
+
/** Knowledge-graph view (nodes/edges) straight from ain-js. */
|
|
706
|
+
async graph() {
|
|
707
|
+
return this.ain.knowledge.getGraph();
|
|
708
|
+
}
|
|
709
|
+
/** Access receipts of a buyer (ain-js). */
|
|
710
|
+
async receipts(buyer) {
|
|
711
|
+
return this.ain.knowledge.getAccessReceipts(buyer);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
//# sourceMappingURL=ain-ledger.js.map
|