@dogfood-lab/ingest 1.3.2 → 1.5.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 +42 -22
- package/anchor/cli.js +185 -0
- package/anchor/compute-root.js +302 -0
- package/anchor/config.js +44 -0
- package/anchor/merkle.js +123 -0
- package/anchor/post-anchor.js +299 -0
- package/anchor/verify-anchor.js +358 -0
- package/lib/chain-manifest.js +106 -0
- package/lib/integrity.js +98 -0
- package/package.json +6 -2
- package/persist.js +41 -1
- package/rebuild-indexes.js +262 -20
- package/run.js +239 -25
- package/verify-chain.js +178 -0
package/anchor/merkle.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merkle tree over chain entry digests — the leaf computation the XRPL anchor
|
|
3
|
+
* commits to on-chain.
|
|
4
|
+
*
|
|
5
|
+
* Ported from @mcptoolshop/repomesh's anchor/xrpl/scripts/merkle.mjs (the
|
|
6
|
+
* proven, tested design). The RFC-6962 (Certificate Transparency) v2 algorithm
|
|
7
|
+
* is the default: it is second-preimage-safe (closes CVE-2012-2459) via domain
|
|
8
|
+
* separation —
|
|
9
|
+
* leaf hash = sha256(0x00 || leafBytes)
|
|
10
|
+
* internal node = sha256(0x01 || left || right)
|
|
11
|
+
* lone odd node = CARRIED UP UNCHANGED (no duplicate-last)
|
|
12
|
+
*
|
|
13
|
+
* Pure module: no I/O, no deps beyond node:crypto. The leaves are the chain
|
|
14
|
+
* entries' `submission_digest` values (64-char hex sha256) for the partition
|
|
15
|
+
* being anchored — see compute-root.js.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
19
|
+
|
|
20
|
+
function sha256(buf) {
|
|
21
|
+
return createHash('sha256').update(buf).digest();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Domain-separation prefixes for the RFC-6962 (Certificate Transparency) tree.
|
|
25
|
+
const LEAF_PREFIX = Buffer.from([0x00]);
|
|
26
|
+
const NODE_PREFIX = Buffer.from([0x01]);
|
|
27
|
+
|
|
28
|
+
function validateLeaves(leavesHex) {
|
|
29
|
+
if (!Array.isArray(leavesHex) || leavesHex.length === 0) {
|
|
30
|
+
throw new Error('merkle: need at least 1 leaf');
|
|
31
|
+
}
|
|
32
|
+
return leavesHex.map((h, i) => {
|
|
33
|
+
if (typeof h !== 'string' || !/^[0-9a-fA-F]{64}$/.test(h)) {
|
|
34
|
+
throw new Error(`Invalid leaf[${i}] (expected 64 hex chars): ${h}`);
|
|
35
|
+
}
|
|
36
|
+
return Buffer.from(h, 'hex');
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* v1 — historical algorithm. NO domain separation; the lone odd node is
|
|
42
|
+
* DUPLICATED (the CVE-2012-2459 dup-last shape). Ported byte-identical so that
|
|
43
|
+
* any partition anchored under v1 in a prior system still verifies. New
|
|
44
|
+
* partitions should use v2. Do NOT change this function.
|
|
45
|
+
*
|
|
46
|
+
* @param {string[]} leavesHex - 64-char hex leaves (one per chain entry digest).
|
|
47
|
+
* @returns {string} Root as lowercase hex.
|
|
48
|
+
*/
|
|
49
|
+
export function merkleRootHex(leavesHex) {
|
|
50
|
+
let level = validateLeaves(leavesHex);
|
|
51
|
+
while (level.length > 1) {
|
|
52
|
+
const next = [];
|
|
53
|
+
for (let i = 0; i < level.length; i += 2) {
|
|
54
|
+
const left = level[i];
|
|
55
|
+
const right = (i + 1 < level.length) ? level[i + 1] : level[i];
|
|
56
|
+
next.push(sha256(Buffer.concat([left, right])));
|
|
57
|
+
}
|
|
58
|
+
level = next;
|
|
59
|
+
}
|
|
60
|
+
return level[0].toString('hex');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* v2 — RFC-6962 (Certificate Transparency). Domain separation closes the
|
|
65
|
+
* leaf/node ambiguity AND the dup-last second-preimage (CVE-2012-2459):
|
|
66
|
+
* leaf hash = sha256(0x00 || leafBytes)
|
|
67
|
+
* internal node = sha256(0x01 || left || right)
|
|
68
|
+
* lone odd node = carried up unchanged
|
|
69
|
+
*
|
|
70
|
+
* @param {string[]} leavesHex - 64-char hex leaves.
|
|
71
|
+
* @returns {string} Root as lowercase hex.
|
|
72
|
+
*/
|
|
73
|
+
export function merkleRootHexV2(leavesHex) {
|
|
74
|
+
const raw = validateLeaves(leavesHex);
|
|
75
|
+
// First hash every leaf with the leaf-domain prefix.
|
|
76
|
+
let level = raw.map((b) => sha256(Buffer.concat([LEAF_PREFIX, b])));
|
|
77
|
+
while (level.length > 1) {
|
|
78
|
+
const next = [];
|
|
79
|
+
for (let i = 0; i < level.length; i += 2) {
|
|
80
|
+
if (i + 1 < level.length) {
|
|
81
|
+
next.push(sha256(Buffer.concat([NODE_PREFIX, level[i], level[i + 1]])));
|
|
82
|
+
} else {
|
|
83
|
+
next.push(level[i]); // carry the lone odd node up unchanged
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
level = next;
|
|
87
|
+
}
|
|
88
|
+
return level[0].toString('hex');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Compute a root for the requested algo. Defaults to RFC-6962 v2 — the
|
|
93
|
+
* second-preimage-safe variant is what new anchors commit to. Pass
|
|
94
|
+
* `sha256-merkle-v1` only to reproduce a historical v1 partition.
|
|
95
|
+
*
|
|
96
|
+
* @param {string[]} leavesHex
|
|
97
|
+
* @param {string} [algo='sha256-merkle-v2']
|
|
98
|
+
* @returns {string}
|
|
99
|
+
*/
|
|
100
|
+
export function merkleRootForAlgo(leavesHex, algo = 'sha256-merkle-v2') {
|
|
101
|
+
if (algo === 'sha256-merkle-v2') return merkleRootHexV2(leavesHex);
|
|
102
|
+
if (algo === 'sha256-merkle-v1') return merkleRootHex(leavesHex);
|
|
103
|
+
throw new Error(`Unknown merkle algo: ${algo}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Build a self-describing manifest fragment: the algo, leaf encoding, count,
|
|
108
|
+
* and root. The leaf encoding label documents that leaves are the chain
|
|
109
|
+
* entries' submission_digest hex values.
|
|
110
|
+
*
|
|
111
|
+
* @param {string[]} leavesHex
|
|
112
|
+
* @param {string} [algo='sha256-merkle-v2']
|
|
113
|
+
* @returns {{ algo: string, leafEncoding: string, leafCount: number, root: string }}
|
|
114
|
+
*/
|
|
115
|
+
export function merkleManifest(leavesHex, algo = 'sha256-merkle-v2') {
|
|
116
|
+
const root = merkleRootForAlgo(leavesHex, algo);
|
|
117
|
+
return {
|
|
118
|
+
algo,
|
|
119
|
+
leafEncoding: 'submissionDigest:hex(32)',
|
|
120
|
+
leafCount: leavesHex.length,
|
|
121
|
+
root,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* post-anchor — submit an anchor manifest's Merkle root to the XRP Ledger as a
|
|
3
|
+
* memo on an AccountSet transaction.
|
|
4
|
+
*
|
|
5
|
+
* Adapted from repomesh's post-anchor.mjs. This is the ONLY module in the anchor
|
|
6
|
+
* subsystem that touches the network, and it LAZILY imports `xrpl` so the rest
|
|
7
|
+
* of the subsystem (compute-root, verify-anchor, the truncation check, the memo
|
|
8
|
+
* builder, and ALL tests) runs with xrpl NOT installed. xrpl is intentionally
|
|
9
|
+
* NOT a declared dependency anywhere; only `postAnchor()` reaches for it, and it
|
|
10
|
+
* throws a structured install hint when it is absent.
|
|
11
|
+
*
|
|
12
|
+
* ── Network (default TESTNET) ───────────────────────────────────────────────
|
|
13
|
+
* Default endpoint is the XRPL testnet (wss://s.altnet.rippletest.net:51233).
|
|
14
|
+
* Testnet ledgers are periodically RESET, which purges historical transactions —
|
|
15
|
+
* fine for proving the mechanism, but for DURABLE, citable anchors migrate to
|
|
16
|
+
* mainnet by passing `network: 'mainnet'` (or XRPL_WS_URL) and funding a real
|
|
17
|
+
* wallet. Each AccountSet anchor costs the standard XRP tx fee.
|
|
18
|
+
*
|
|
19
|
+
* ── Authorization root ──────────────────────────────────────────────────────
|
|
20
|
+
* Any funded wallet can post a memo; the trust comes from the verifier's
|
|
21
|
+
* trusted-anchor-account allowlist (see verify-anchor.js). Posting requires the
|
|
22
|
+
* XRPL_SEED env var (the funded wallet's family seed); post-anchor REFUSES to
|
|
23
|
+
* submit without it. The on-chain memo self-describes its Merkle algo so the
|
|
24
|
+
* standalone verifier recomputes the root with the same algorithm.
|
|
25
|
+
*
|
|
26
|
+
* Honest framing: a confirmed on-chain XRPL transaction is the EXTERNAL
|
|
27
|
+
* head/count witness the chain writer cannot forge or remove. That is what
|
|
28
|
+
* upgrades the chain from tamper-EVIDENT to tamper-PROOF below an anchored point.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { readFileSync } from 'node:fs';
|
|
32
|
+
|
|
33
|
+
import { readLastAnchor, computeAnchor } from './compute-root.js';
|
|
34
|
+
|
|
35
|
+
/** Default XRPL endpoints per network. */
|
|
36
|
+
export const DEFAULT_WS_URL = {
|
|
37
|
+
testnet: 'wss://s.altnet.rippletest.net:51233',
|
|
38
|
+
mainnet: 'wss://xrplcluster.com',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Ripple epoch offset from the Unix epoch (seconds): 2000-01-01T00:00:00Z. */
|
|
42
|
+
const RIPPLE_EPOCH_OFFSET_SECONDS = 946684800;
|
|
43
|
+
|
|
44
|
+
/** Memo type that self-identifies an anchor memo so a verifier can locate it. */
|
|
45
|
+
export const ANCHOR_MEMO_TYPE = 'testing-os-anchor-v1';
|
|
46
|
+
|
|
47
|
+
function stringToHex(s) {
|
|
48
|
+
return Buffer.from(s, 'utf8').toString('hex').toUpperCase();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Validate the XRPL_SEED SHAPE before opening a socket, so a typo'd/empty seed
|
|
53
|
+
* gives a one-line actionable message instead of a raw crypto stack after the
|
|
54
|
+
* connect. XRPL family seeds are base58 (no 0/O/I/l), begin with 's', and are
|
|
55
|
+
* ~29-31 chars. This is a SHAPE pre-check, not a full base58-check — the xrpl
|
|
56
|
+
* Wallet.fromSeed remains the authority. Pure / testable.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} seed
|
|
59
|
+
* @returns {{ ok: boolean, reason?: string }}
|
|
60
|
+
*/
|
|
61
|
+
export function validateSeedShape(seed) {
|
|
62
|
+
if (typeof seed !== 'string' || seed.length === 0) {
|
|
63
|
+
return { ok: false, reason: "XRPL_SEED is empty or unset — set the funded wallet's seed (generate with: xrpl wallet create)." };
|
|
64
|
+
}
|
|
65
|
+
if (seed[0] !== 's') {
|
|
66
|
+
return { ok: false, reason: `XRPL_SEED does not look like an XRPL family seed — it must begin with 's' (got "${seed[0]}…"). Did you paste the classic ADDRESS (r…) instead of the SEED?` };
|
|
67
|
+
}
|
|
68
|
+
if (seed.length < 16 || seed.length > 40) {
|
|
69
|
+
return { ok: false, reason: `XRPL_SEED has an unexpected length (${seed.length}); an XRPL seed is ~29-31 base58 chars. Check for truncation or extra whitespace.` };
|
|
70
|
+
}
|
|
71
|
+
if (/[0OIl]/.test(seed) || !/^[1-9A-HJ-NP-Za-km-z]+$/.test(seed)) {
|
|
72
|
+
return { ok: false, reason: 'XRPL_SEED contains characters outside the base58 alphabet (no 0, O, I, l) — it is not a valid seed.' };
|
|
73
|
+
}
|
|
74
|
+
return { ok: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Convert an XRPL Ripple-epoch close-time (seconds since 2000-01-01) into an
|
|
79
|
+
* ISO-8601 string. `submitAndWait` returns the validated tx, whose
|
|
80
|
+
* `result.date` is the ledger close-time — the only trustworthy clock for an
|
|
81
|
+
* anchor. Returns null (never throws) when the date is absent.
|
|
82
|
+
*
|
|
83
|
+
* @param {object} submitResult
|
|
84
|
+
* @returns {string|null}
|
|
85
|
+
*/
|
|
86
|
+
export function extractCloseTime(submitResult) {
|
|
87
|
+
const date = submitResult?.result?.date;
|
|
88
|
+
if (typeof date !== 'number' || !Number.isFinite(date)) return null;
|
|
89
|
+
const d = new Date((date + RIPPLE_EPOCH_OFFSET_SECONDS) * 1000);
|
|
90
|
+
return Number.isNaN(d.getTime()) ? null : d.toISOString();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Extract the validating ledger index from a submitAndWait result. Returns null
|
|
95
|
+
* (never throws) when absent.
|
|
96
|
+
*
|
|
97
|
+
* @param {object} submitResult
|
|
98
|
+
* @returns {number|null}
|
|
99
|
+
*/
|
|
100
|
+
export function extractLedgerIndex(submitResult) {
|
|
101
|
+
const li = submitResult?.result?.ledger_index;
|
|
102
|
+
return typeof li === 'number' && Number.isFinite(li) ? li : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Build the XRPL memo object binding an anchor to the chain. Pure — returns the
|
|
107
|
+
* `{ Memo: { MemoType, MemoFormat, MemoData } }` envelope (hex-encoded fields).
|
|
108
|
+
*
|
|
109
|
+
* The memo self-describes the Merkle algo so the standalone verifier recomputes
|
|
110
|
+
* the root with the SAME algorithm that produced it. MemoType is a stable
|
|
111
|
+
* self-identifying tag (ANCHOR_MEMO_TYPE) so a verifier can locate the memo.
|
|
112
|
+
*
|
|
113
|
+
* @param {object} params
|
|
114
|
+
* @param {number} params.anchorSeq
|
|
115
|
+
* @param {string} params.network - 'testnet' | 'mainnet'
|
|
116
|
+
* @param {string} params.rootHex - The Merkle root.
|
|
117
|
+
* @param {string} params.manifestHash - sha256 of the canonical manifest base.
|
|
118
|
+
* @param {number} params.count - leaf_count.
|
|
119
|
+
* @param {string|null} [params.prev] - prev_anchor_root, or null/"0" for genesis.
|
|
120
|
+
* @param {[number,number]|null} [params.range] - seq_range [from, to].
|
|
121
|
+
* @param {string} [params.algo] - Merkle algo (omitted from memo when undefined).
|
|
122
|
+
* @returns {{ Memo: { MemoType: string, MemoFormat: string, MemoData: string } }}
|
|
123
|
+
*/
|
|
124
|
+
export function buildAnchorMemo({ anchorSeq, network, rootHex, manifestHash, count, prev, range, algo }) {
|
|
125
|
+
const dataObj = {
|
|
126
|
+
v: 1,
|
|
127
|
+
s: anchorSeq,
|
|
128
|
+
n: network,
|
|
129
|
+
r: rootHex,
|
|
130
|
+
h: manifestHash,
|
|
131
|
+
c: count,
|
|
132
|
+
pv: prev && prev !== null ? prev : '0',
|
|
133
|
+
rg: range ? `${range[0]}..${range[1]}` : '0',
|
|
134
|
+
};
|
|
135
|
+
// Self-describe the algorithm so v2 anchors verify as v2 (and any legacy v1
|
|
136
|
+
// memo without an algo field falls back to v1 in the verifier).
|
|
137
|
+
if (algo) dataObj.algo = algo;
|
|
138
|
+
const memoData = JSON.stringify(dataObj);
|
|
139
|
+
if (Buffer.byteLength(memoData, 'utf8') > 700) {
|
|
140
|
+
throw new Error(`MemoData too large: ${Buffer.byteLength(memoData)} bytes (XRPL memo cap is ~1KB; keep anchors small)`);
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
Memo: {
|
|
144
|
+
MemoType: stringToHex(ANCHOR_MEMO_TYPE),
|
|
145
|
+
MemoFormat: stringToHex('application/json'),
|
|
146
|
+
MemoData: stringToHex(memoData),
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Structured error for the missing optional dependency. Thrown by postAnchor()
|
|
153
|
+
* (the only network path) when `import('xrpl')` fails — so the floor + verify +
|
|
154
|
+
* all tests run without xrpl, and only an actual post demands it.
|
|
155
|
+
*/
|
|
156
|
+
export class XrplDependencyError extends Error {
|
|
157
|
+
constructor() {
|
|
158
|
+
super('XRPL anchoring requires the optional xrpl package — run: npm install xrpl');
|
|
159
|
+
this.name = 'XrplDependencyError';
|
|
160
|
+
this.code = 'XRPL_NOT_INSTALLED';
|
|
161
|
+
this.hint = 'npm install xrpl';
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Lazily import the optional `xrpl` package. Isolated so it can be stubbed in
|
|
167
|
+
* tests and so the structured install hint is produced in exactly one place.
|
|
168
|
+
*
|
|
169
|
+
* @returns {Promise<object>} The xrpl module.
|
|
170
|
+
* @throws {XrplDependencyError} when xrpl is not installed.
|
|
171
|
+
*/
|
|
172
|
+
export async function loadXrpl() {
|
|
173
|
+
try {
|
|
174
|
+
return await import('xrpl');
|
|
175
|
+
} catch {
|
|
176
|
+
throw new XrplDependencyError();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Compute (if needed) and post the next anchor to the XRP Ledger.
|
|
182
|
+
*
|
|
183
|
+
* Steps:
|
|
184
|
+
* 1. Compute the anchor manifest if one is not already pending — uses the same
|
|
185
|
+
* append-only compute-root path. (Operators usually run anchor-compute
|
|
186
|
+
* first; this re-runs it idempotently so a post is never against a stale or
|
|
187
|
+
* missing manifest.)
|
|
188
|
+
* 2. Validate XRPL_SEED shape — REFUSE to post without it (structured error).
|
|
189
|
+
* 3. Lazily import xrpl, connect, submitAndWait an AccountSet carrying the
|
|
190
|
+
* anchor memo.
|
|
191
|
+
* 4. On tesSUCCESS, record the on-chain close-time + ledger index back into the
|
|
192
|
+
* anchor manifest (via the provided manifest-update callback) and return the
|
|
193
|
+
* receipt.
|
|
194
|
+
*
|
|
195
|
+
* The actual xrpl client + wallet are injected via `deps` so tests can drive the
|
|
196
|
+
* whole post path WITHOUT xrpl installed and WITHOUT a network. In production
|
|
197
|
+
* `deps` is omitted and the real xrpl is lazily loaded.
|
|
198
|
+
*
|
|
199
|
+
* @param {string} repoRoot
|
|
200
|
+
* @param {object} [opts]
|
|
201
|
+
* @param {'testnet'|'mainnet'} [opts.network='testnet']
|
|
202
|
+
* @param {string} [opts.wsUrl] - Override endpoint (else DEFAULT_WS_URL[network]).
|
|
203
|
+
* @param {string} [opts.seed] - Override XRPL_SEED (else process.env.XRPL_SEED).
|
|
204
|
+
* @param {'since-last'|'all'} [opts.mode='since-last']
|
|
205
|
+
* @param {object} [opts.deps] - Test injection: { xrpl, recordPost }.
|
|
206
|
+
* @returns {Promise<object>} A receipt: { ok, anchor_seq, root, tx_hash, ... }.
|
|
207
|
+
*/
|
|
208
|
+
export async function postAnchor(repoRoot, opts = {}) {
|
|
209
|
+
const network = opts.network || 'testnet';
|
|
210
|
+
const wsUrl = opts.wsUrl || DEFAULT_WS_URL[network] || DEFAULT_WS_URL.testnet;
|
|
211
|
+
const seed = opts.seed !== undefined ? opts.seed : process.env.XRPL_SEED;
|
|
212
|
+
const mode = opts.mode || 'since-last';
|
|
213
|
+
|
|
214
|
+
// (2) Refuse without a valid-shaped seed — BEFORE any network work.
|
|
215
|
+
const seedCheck = validateSeedShape(seed);
|
|
216
|
+
if (!seedCheck.ok) {
|
|
217
|
+
const e = new Error(seedCheck.reason);
|
|
218
|
+
e.code = 'XRPL_SEED_INVALID';
|
|
219
|
+
throw e;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// (1) Ensure there is a manifest to post. Re-running compute is idempotent
|
|
223
|
+
// (append-only). If nothing new is pending, post the LAST anchor manifest
|
|
224
|
+
// so a re-post after a transient network failure still binds the same root.
|
|
225
|
+
const computed = computeAnchor(repoRoot, { mode, network });
|
|
226
|
+
let manifest;
|
|
227
|
+
if (computed.empty) {
|
|
228
|
+
manifest = readLastAnchor(repoRoot);
|
|
229
|
+
if (!manifest) {
|
|
230
|
+
const e = new Error(`nothing to anchor: ${computed.reason}`);
|
|
231
|
+
e.code = 'ANCHOR_NOTHING_TO_POST';
|
|
232
|
+
throw e;
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
manifest = computed.manifest;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const memo = buildAnchorMemo({
|
|
239
|
+
anchorSeq: manifest.anchor_seq,
|
|
240
|
+
network,
|
|
241
|
+
rootHex: manifest.root,
|
|
242
|
+
manifestHash: manifest.manifest_hash,
|
|
243
|
+
count: manifest.leaf_count,
|
|
244
|
+
prev: manifest.prev_anchor_root,
|
|
245
|
+
range: manifest.seq_range,
|
|
246
|
+
algo: manifest.algo,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// (3) Lazily load xrpl (or use the injected stub) and submit.
|
|
250
|
+
const xrpl = opts.deps?.xrpl || (await loadXrpl());
|
|
251
|
+
const client = new xrpl.Client(wsUrl);
|
|
252
|
+
await client.connect();
|
|
253
|
+
let receipt;
|
|
254
|
+
try {
|
|
255
|
+
const wallet = xrpl.Wallet.fromSeed(seed);
|
|
256
|
+
const tx = {
|
|
257
|
+
TransactionType: 'AccountSet',
|
|
258
|
+
Account: wallet.address,
|
|
259
|
+
Memos: [memo],
|
|
260
|
+
};
|
|
261
|
+
const result = await client.submitAndWait(tx, { wallet });
|
|
262
|
+
const txHash = result?.result?.hash || result?.result?.tx_json?.hash || null;
|
|
263
|
+
const engineResult = result?.result?.meta?.TransactionResult || result?.result?.engine_result;
|
|
264
|
+
const closeTimeIso = extractCloseTime(result);
|
|
265
|
+
const ledgerIndex = extractLedgerIndex(result);
|
|
266
|
+
|
|
267
|
+
receipt = {
|
|
268
|
+
ok: engineResult === 'tesSUCCESS',
|
|
269
|
+
network,
|
|
270
|
+
anchor_seq: manifest.anchor_seq,
|
|
271
|
+
root: manifest.root,
|
|
272
|
+
manifest_hash: manifest.manifest_hash,
|
|
273
|
+
leaf_count: manifest.leaf_count,
|
|
274
|
+
seq_range: manifest.seq_range,
|
|
275
|
+
head_digest: manifest.head_digest,
|
|
276
|
+
tx_hash: txHash,
|
|
277
|
+
wallet_address: wallet.address,
|
|
278
|
+
ledger_index: ledgerIndex,
|
|
279
|
+
close_time_iso: closeTimeIso,
|
|
280
|
+
engine_result: engineResult,
|
|
281
|
+
};
|
|
282
|
+
} finally {
|
|
283
|
+
await client.disconnect();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// (4) Record the on-chain facts back into the anchor manifest. Injected in
|
|
287
|
+
// tests; in production it patches the manifest file in place.
|
|
288
|
+
if (receipt.ok && typeof opts.deps?.recordPost === 'function') {
|
|
289
|
+
opts.deps.recordPost(manifest.anchor_seq, {
|
|
290
|
+
tx_hash: receipt.tx_hash,
|
|
291
|
+
ledger_index: receipt.ledger_index,
|
|
292
|
+
close_time_iso: receipt.close_time_iso,
|
|
293
|
+
wallet_address: receipt.wallet_address,
|
|
294
|
+
network,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return receipt;
|
|
299
|
+
}
|