@totemsdk/core 1.2.9 → 1.2.11

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.
@@ -16,6 +16,9 @@ exports.parseDecimalToMiniNumber = parseDecimalToMiniNumber;
16
16
  exports.serializeCoin = serializeCoin;
17
17
  exports.serializeTransaction = serializeTransaction;
18
18
  exports.computeTransactionDigest = computeTransactionDigest;
19
+ exports.computeTokenId = computeTokenId;
20
+ exports.serializeTokenDescriptor = serializeTokenDescriptor;
21
+ exports.deserializeTransaction = deserializeTransaction;
19
22
  exports.precomputeTransactionCoinID = precomputeTransactionCoinID;
20
23
  exports.createDefaultTransaction = createDefaultTransaction;
21
24
  exports.buildMinimaCoin = buildMinimaCoin;
@@ -125,11 +128,11 @@ function serializeStateVariable(sv) {
125
128
  case 'string':
126
129
  typeByte = new Uint8Array([STATETYPE_STRING]);
127
130
  if (typeof sv.value === 'string') {
128
- let bracketedValue = sv.value;
129
- if (!sv.value.startsWith('[') || !sv.value.endsWith(']')) {
130
- bracketedValue = `[${sv.value}]`;
131
- }
132
- const utf8Bytes = new TextEncoder().encode(bracketedValue);
131
+ // Minima stores a string state variable as MiniString: MiniData of the
132
+ // raw UTF-8 bytes. The `[ … ]` form is KISSVM literal syntax, NOT part
133
+ // of the stored value (the C++ StateVariable::writeDataStream writes
134
+ // mData directly; the deserializer likewise strips no brackets).
135
+ const utf8Bytes = new TextEncoder().encode(sv.value);
133
136
  dataBytes = (0, Streamable_js_1.writeMiniData)(utf8Bytes);
134
137
  }
135
138
  else {
@@ -147,7 +150,10 @@ function serializeToken(token) {
147
150
  parts.push((0, Streamable_js_1.writeHashToStream)(token.coinId));
148
151
  parts.push((0, Streamable_js_1.writeMiniData)(token.script));
149
152
  parts.push((0, Streamable_js_1.writeMiniNumber)(BigInt(token.scale), 0));
150
- parts.push((0, Streamable_js_1.writeMiniNumber)(token.totalAmount, 0));
153
+ // The minima amount is a MiniNumber carrying its own scale (Java
154
+ // Token.writeDataStream → mTokenMinimaAmount.writeDataStream). Minima's
155
+ // tokencreate always builds it from MINI_UNIT (scale 44).
156
+ parts.push((0, Streamable_js_1.writeMiniNumber)(token.totalAmount, token.totalAmountScale ?? 44));
151
157
  parts.push((0, Streamable_js_1.writeMiniData)(token.name));
152
158
  parts.push((0, Streamable_js_1.writeMiniNumber)(token.created ?? 0n, 0));
153
159
  return (0, Streamable_js_1.concat)(...parts);
@@ -233,6 +239,151 @@ function computeTransactionDigest(tx) {
233
239
  const serialized = serializeTransaction(tx);
234
240
  return (0, wasm_sync_js_1.sha3_256)(serialized);
235
241
  }
242
+ /**
243
+ * Compute a Minima **token id** — SHA3-256 of the MiniData-wrapped serialized
244
+ * Token, matching Java `Token.calculateTokenID()`. A newly created token's coins
245
+ * carry this value as their `tokenId`; token creation returns it.
246
+ */
247
+ function computeTokenId(token) {
248
+ return (0, wasm_sync_js_1.sha3_256)((0, Streamable_js_1.writeMiniData)(serializeToken(token)));
249
+ }
250
+ /** Serialize a token descriptor (Java `Token.writeDataStream()` order). */
251
+ function serializeTokenDescriptor(token) {
252
+ return serializeToken(token);
253
+ }
254
+ function txReadBytes(data, c, n) {
255
+ if (n < 0 || c.offset + n > data.length) {
256
+ throw new Error(`deserializeTransaction: unexpected end of input at offset ${c.offset} (need ${n})`);
257
+ }
258
+ const out = data.slice(c.offset, c.offset + n);
259
+ c.offset += n;
260
+ return out;
261
+ }
262
+ function txReadMiniByte(data, c) {
263
+ return txReadBytes(data, c, 1)[0];
264
+ }
265
+ /** MiniData / hash: 4-byte big-endian length + bytes. */
266
+ function txReadMiniData(data, c) {
267
+ const lenBytes = txReadBytes(data, c, 4);
268
+ const len = ((lenBytes[0] << 24) | (lenBytes[1] << 16) | (lenBytes[2] << 8) | lenBytes[3]) >>> 0;
269
+ return txReadBytes(data, c, len);
270
+ }
271
+ /** MiniNumber: 1-byte scale + 1-byte length + two's-complement unscaled bytes. */
272
+ function txReadMiniNumber(data, c) {
273
+ const scale = txReadBytes(data, c, 1)[0];
274
+ const len = txReadBytes(data, c, 1)[0];
275
+ const bytes = txReadBytes(data, c, len);
276
+ let value = 0n;
277
+ for (const b of bytes)
278
+ value = (value << 8n) | BigInt(b);
279
+ // RFC-020 P2-10 (TXB-SERIAL-003): Java's BigInteger.toByteArray() (and the
280
+ // canonical writers here) use two's-complement big-endian; a set high bit
281
+ // means the value is negative. Previously this was read as unsigned.
282
+ if (bytes.length > 0 && (bytes[0] & 0x80) !== 0) {
283
+ value -= 1n << BigInt(8 * bytes.length);
284
+ }
285
+ return { value, scale };
286
+ }
287
+ function txBytesToBigInt(bytes) {
288
+ let value = 0n;
289
+ for (const b of bytes)
290
+ value = (value << 8n) | BigInt(b);
291
+ return value;
292
+ }
293
+ /** MMREntryNumber: MiniNumber(scale) followed by MiniData(unscaled). */
294
+ function txReadMMREntryNumber(data, c) {
295
+ txReadMiniNumber(data, c); // scale (always 0 for integer entry numbers)
296
+ return txBytesToBigInt(txReadMiniData(data, c));
297
+ }
298
+ function miniNumberToDecimal(value, scale) {
299
+ if (scale === 0)
300
+ return value.toString();
301
+ const digits = value.toString().padStart(scale + 1, '0');
302
+ const whole = digits.slice(0, digits.length - scale);
303
+ const frac = digits.slice(digits.length - scale);
304
+ return `${whole}.${frac}`;
305
+ }
306
+ function txReadStateVariable(data, c) {
307
+ const port = txReadBytes(data, c, 1)[0];
308
+ const type = txReadBytes(data, c, 1)[0];
309
+ switch (type) {
310
+ case STATETYPE_HEX:
311
+ return { port, type: 'hex', value: txReadMiniData(data, c) };
312
+ case STATETYPE_NUMBER:
313
+ return { port, type: 'number', value: txReadMiniNumber(data, c).value };
314
+ case STATETYPE_STRING:
315
+ return { port, type: 'string', value: new TextDecoder().decode(txReadMiniData(data, c)) };
316
+ case STATETYPE_BOOL:
317
+ return { port, type: 'bool', value: txReadMiniByte(data, c) === 1 };
318
+ default:
319
+ throw new Error(`deserializeTransaction: unknown StateVariable type ${type}`);
320
+ }
321
+ }
322
+ function txReadToken(data, c) {
323
+ const coinId = txReadMiniData(data, c);
324
+ const script = txReadMiniData(data, c);
325
+ const { value: scale } = txReadMiniNumber(data, c);
326
+ // RFC-020 P2-10 (TXB-SERIAL-002): carry the totalAmount's own scale so a
327
+ // serialize→deserialize→serialize round-trip is byte-exact (previously the
328
+ // scale was dropped and re-defaulted to 44).
329
+ const { value: totalAmount, scale: totalAmountScale } = txReadMiniNumber(data, c);
330
+ const name = txReadMiniData(data, c);
331
+ const { value: created } = txReadMiniNumber(data, c);
332
+ return { coinId, script, scale: Number(scale), totalAmount, totalAmountScale, name, created };
333
+ }
334
+ function txReadCoin(data, c) {
335
+ const coinId = txReadMiniData(data, c);
336
+ const address = txReadMiniData(data, c);
337
+ const { value: amountUnscaled, scale } = txReadMiniNumber(data, c);
338
+ const tokenId = txReadMiniData(data, c);
339
+ const storeState = txReadMiniByte(data, c) === 1;
340
+ const mmrEntryNumber = txReadMMREntryNumber(data, c);
341
+ const spent = txReadMiniByte(data, c) === 1;
342
+ const { value: created } = txReadMiniNumber(data, c);
343
+ const stateCount = Number(txReadMiniNumber(data, c).value);
344
+ const state = [];
345
+ for (let i = 0; i < stateCount; i++)
346
+ state.push(txReadStateVariable(data, c));
347
+ const hasToken = txReadMiniByte(data, c) === 1;
348
+ const token = hasToken ? txReadToken(data, c) : null;
349
+ return {
350
+ coinId,
351
+ address,
352
+ amount: miniNumberToDecimal(amountUnscaled, scale),
353
+ tokenId,
354
+ token,
355
+ storeState,
356
+ state,
357
+ mmrEntryNumber,
358
+ spent,
359
+ created,
360
+ };
361
+ }
362
+ /**
363
+ * Deserialize a transaction produced by {@link serializeTransaction}. The
364
+ * inverse is byte-exact for canonically-encoded transactions (outputs/inputs
365
+ * written from decimal amounts, MMR entry numbers and tokens).
366
+ */
367
+ function deserializeTransaction(bytes) {
368
+ const c = { offset: 0 };
369
+ const inputCount = Number(txReadMiniNumber(bytes, c).value);
370
+ const inputs = [];
371
+ for (let i = 0; i < inputCount; i++)
372
+ inputs.push(txReadCoin(bytes, c));
373
+ const outputCount = Number(txReadMiniNumber(bytes, c).value);
374
+ const outputs = [];
375
+ for (let i = 0; i < outputCount; i++)
376
+ outputs.push(txReadCoin(bytes, c));
377
+ const stateCount = Number(txReadMiniNumber(bytes, c).value);
378
+ const state = [];
379
+ for (let i = 0; i < stateCount; i++)
380
+ state.push(txReadStateVariable(bytes, c));
381
+ const linkHash = txReadMiniData(bytes, c);
382
+ if (c.offset !== bytes.length) {
383
+ throw new Error(`deserializeTransaction: ${bytes.length - c.offset} trailing byte(s) after transaction`);
384
+ }
385
+ return { linkHash, inputs, outputs, state };
386
+ }
236
387
  function precomputeTransactionCoinID(tx) {
237
388
  if (tx.inputs.length === 0)
238
389
  return;
@@ -37,33 +37,28 @@ class TransactionReceiptStore {
37
37
  return this._initialized;
38
38
  }
39
39
  async load() {
40
- try {
41
- const receipts = await this.storage.get(this.storageKey);
42
- if (receipts) {
43
- this.receipts = receipts;
44
- this.logger.debug(`Loaded ${this.receipts.length} transaction receipts`);
45
- }
46
- }
47
- catch (error) {
48
- this.logger.error('Failed to load receipts:', error);
40
+ const receipts = await this.storage.get(this.storageKey);
41
+ if (receipts) {
42
+ this.receipts = receipts;
43
+ this.logger.debug(`Loaded ${this.receipts.length} transaction receipts`);
49
44
  }
50
45
  }
51
46
  async persist() {
47
+ const trimmedReceipts = this.receipts.slice(-this.maxReceipts);
48
+ await this.storage.set(this.storageKey, trimmedReceipts);
49
+ this.receipts = trimmedReceipts;
50
+ }
51
+ async add(receipt) {
52
+ this.receipts.push(receipt);
53
+ this.logger.debug(`Added receipt: ${receipt.txpowid}`);
52
54
  try {
53
- const trimmedReceipts = this.receipts.slice(-this.maxReceipts);
54
- await this.storage.set(this.storageKey, trimmedReceipts);
55
- this.receipts = trimmedReceipts;
55
+ await this.persist();
56
56
  }
57
57
  catch (error) {
58
- this.logger.error('Failed to persist receipts:', error);
58
+ this.receipts.pop();
59
59
  throw error;
60
60
  }
61
61
  }
62
- async add(receipt) {
63
- this.receipts.push(receipt);
64
- this.logger.debug(`Added receipt: ${receipt.txpowid}`);
65
- await this.persist();
66
- }
67
62
  getAll() {
68
63
  return [...this.receipts].reverse();
69
64
  }
@@ -76,15 +71,29 @@ class TransactionReceiptStore {
76
71
  async updateStatus(txpowid, status) {
77
72
  const receipt = this.receipts.find(r => r.txpowid === txpowid);
78
73
  if (receipt) {
74
+ const previousStatus = receipt.status;
79
75
  receipt.status = status;
80
76
  this.logger.debug(`Updated receipt status: ${txpowid} → ${status}`);
81
- await this.persist();
77
+ try {
78
+ await this.persist();
79
+ }
80
+ catch (error) {
81
+ receipt.status = previousStatus;
82
+ throw error;
83
+ }
82
84
  }
83
85
  }
84
86
  async clear() {
87
+ const previous = this.receipts;
85
88
  this.receipts = [];
86
- await this.storage.remove(this.storageKey);
87
- this.logger.debug('Cleared all receipts');
89
+ try {
90
+ await this.storage.remove(this.storageKey);
91
+ this.logger.debug('Cleared all receipts');
92
+ }
93
+ catch (error) {
94
+ this.receipts = previous;
95
+ throw error;
96
+ }
88
97
  }
89
98
  count() {
90
99
  return this.receipts.length;
package/dist/index.d.ts CHANGED
@@ -20,6 +20,7 @@ export { mmrRootFromPublicKeys, verifyMMRProof, } from './wasm-sync.js';
20
20
  export { phraseToSeed, generateWordList, validatePhrase, cleanSeedPhrase, } from './bip39.js';
21
21
  export { makeMxAddress, parseMxAddress, wotsAddressFromKeypair, } from './wasm-sync.js';
22
22
  export { serializeTransaction, computeTransactionDigest, } from './wasm-sync.js';
23
+ export { deserializeTransaction } from './transaction.js';
23
24
  export { timingSafeEqual, createChallenge, validateChallenge, } from './wasm-sync.js';
24
25
  export { scriptFromWotsPk } from './script.js';
25
26
  export { makeMxAddress as encodeMx, parseMxAddress as decodeMx } from './wasm-sync.js';
@@ -30,13 +31,13 @@ export * from './scripts/index.js';
30
31
  export { scriptToAddress, addressToRoot } from './derive.js';
31
32
  export { WORD_LIST, convertStringToSeed, convertWordListToSeed, generateSeedPhrase, } from './bip39.js';
32
33
  export { MiniNumber } from './MiniNumber.js';
33
- export { type MinimaTransaction, type MinimaCoin, type ParsedMiniNumber, type MinimaToken, type StateVariable, type RawStateVariable, type CoinProofData, type SpendableCoinInput, type TransactionBuildResult, parseDecimalToMiniNumber, serializeCoin, createDefaultTransaction, buildMinimaCoin, } from './transaction.js';
34
+ export { type MinimaTransaction, type MinimaCoin, type ParsedMiniNumber, type MinimaToken, type StateVariable, type RawStateVariable, type CoinProofData, type SpendableCoinInput, type TransactionBuildResult, parseDecimalToMiniNumber, serializeCoin, serializeTransaction as serializeTransactionObject, computeTokenId, serializeTokenDescriptor, createDefaultTransaction, buildMinimaCoin, } from './transaction.js';
34
35
  export { verifySignature, verifySignatureDetailed, verifyTreeSignatureDetailed, deriveAddressFromPublicKey, normalizeHex, type VerificationResult, } from './verify.js';
35
- export { writeMiniByte, writeMMREntryNumber, concat, bigIntToByteArray, type Bytes, } from './Streamable.js';
36
+ export { writeMiniByte, writeMMREntryNumber, writeSignature, writeSignatureProof, concat, bigIntToByteArray, type Bytes, } from './Streamable.js';
36
37
  export { serializeMiniNumber, serializeMiniData, hashAllObjects, deriveChildTreeSeedJava, hashObject, serializeMiniNumberZERO, serializeMiniNumberONE, writeHashToStream, javaHashAllObjects, indexToMiniDataBytes, deriveUnifiedChildSeed, createMMREntryNumber, serializeMMREntryNumber, serializeMMRData, serializeMMREntry, type MMREntryNumber as JavaMMREntryNumber, type MMRData as JavaMMRData, type MMREntry as JavaMMREntry, } from './javaStreamables.js';
37
- export { TreeKey, TreeKeyNode, serializeTreeSignature, deserializeTreeSignature, getRootPublicKey, DEFAULT_KEYS_PER_LEVEL, DEFAULT_LEVELS, createUnifiedChildTreeKeyAsync, createPerAddressTreeKey, createPerAddressTreeKeyAsync, type KeyGenProgress, type ProgressCallback, type SignatureProof, type TreeSignature, } from './treekey.js';
38
+ export { TreeKey, TreeKeyNode, serializeTreeSignature, deserializeTreeSignature, getRootPublicKey, DEFAULT_KEYS_PER_LEVEL, DEFAULT_LEVELS, createUnifiedChildTreeKeyAsync, createPerAddressTreeKey, createPerAddressTreeKeyAsync, setTreeKeyLogger, disableTreeKeyLogger, type KeyGenProgress, type ProgressCallback, type SignatureProof, type TreeSignature, } from './treekey.js';
38
39
  export type { WotsKeypair, WotsSignature } from './wots.js';
39
- export { F, hex, fromHex, u16be, u32be, assert32, h, prfChainSeed, toWinternitzDigits, baseWWithChecksum, wotsSignLegacy, } from './wots.js';
40
+ export { F, hex, fromHex, setWotsLogger, disableWotsLogger, u16be, u32be, assert32, h, prfChainSeed, toWinternitzDigits, baseWWithChecksum, wotsSignLegacy, } from './wots.js';
40
41
  export { toHex, canonicalJson, hashCanonical } from './canonical.js';
41
42
  export { utf8ToBytes, bytesToUtf8 } from './utils.js';
42
43
  export declare const MINIMA_CONSTANTS: {
package/dist/index.js CHANGED
@@ -35,16 +35,18 @@ export { deriveChainSeedJava, derivePerAddressSeed, deriveRootPrivSeed, writeMin
35
35
  export { createUnifiedChildTreeKey, createUnifiedRootTreeKey, deriveUnifiedAddressPublicKey, verifyTreeSignature, } from './treekey.js';
36
36
  export { mmrRootFromPublicKeys, verifyMMRProof, } from './wasm-sync.js';
37
37
  // BIP39 Seed Phrase Handling
38
- // NOTE: the TS implementation (bip39.ts) uses the complete 2048-word BIP39
39
- // word list and Minima-compatible uppercase canonicalization. The WASM
40
- // binding (wasm-sync.ts) ships a corrupted/partial word list (missing
41
- // "hungry", contains spurious words) and lowercases instead of uppercasing,
42
- // so the public API routes through the TS implementation.
38
+ // The WASM binding (wasm-sync.ts) shares the canonical 2048-word BIP39 list
39
+ // and Minima-compatible uppercase canonicalization with the TS implementation
40
+ // (packages/core-wasm/src/bip39.rs -- regenerated from bip39.ts). We still
41
+ // route the public API through the TS implementation (bip39.ts): it is the
42
+ // canonical source of truth and avoids double-entering the list.
43
43
  export { phraseToSeed, generateWordList, validatePhrase, cleanSeedPhrase, } from './bip39.js';
44
44
  // Address Derivation
45
45
  export { makeMxAddress, parseMxAddress, wotsAddressFromKeypair, } from './wasm-sync.js';
46
46
  // Transaction Serialization & Digest
47
47
  export { serializeTransaction, computeTransactionDigest, } from './wasm-sync.js';
48
+ // Transaction deserialization (byte-exact inverse of the TS serializer)
49
+ export { deserializeTransaction } from './transaction.js';
48
50
  // Verification
49
51
  export { timingSafeEqual, createChallenge, validateChallenge, } from './wasm-sync.js';
50
52
  // ---------------------------------------------------------------------------
@@ -72,16 +74,16 @@ export { WORD_LIST, convertStringToSeed, convertWordListToSeed, generateSeedPhra
72
74
  // MiniNumber class (matches Java MiniNumber/BigDecimal semantics)
73
75
  export { MiniNumber } from './MiniNumber.js';
74
76
  // Transaction types (from JS)
75
- export { parseDecimalToMiniNumber, serializeCoin, createDefaultTransaction, buildMinimaCoin, } from './transaction.js';
77
+ export { parseDecimalToMiniNumber, serializeCoin, serializeTransaction as serializeTransactionObject, computeTokenId, serializeTokenDescriptor, createDefaultTransaction, buildMinimaCoin, } from './transaction.js';
76
78
  // High-level Verification API (convenience functions for integrators)
77
79
  export { verifySignature, verifySignatureDetailed, verifyTreeSignatureDetailed, deriveAddressFromPublicKey, normalizeHex, } from './verify.js';
78
80
  // Streamable primitives (low-level serialization building blocks)
79
- export { writeMiniByte, writeMMREntryNumber, concat, bigIntToByteArray, } from './Streamable.js';
81
+ export { writeMiniByte, writeMMREntryNumber, writeSignature, writeSignatureProof, concat, bigIntToByteArray, } from './Streamable.js';
80
82
  // Java-Compatible Serialization Helpers (non-crypto)
81
83
  export { serializeMiniNumber, serializeMiniData, hashAllObjects, deriveChildTreeSeedJava, hashObject, serializeMiniNumberZERO, serializeMiniNumberONE, writeHashToStream, javaHashAllObjects, indexToMiniDataBytes, deriveUnifiedChildSeed, createMMREntryNumber, serializeMMREntryNumber, serializeMMRData, serializeMMREntry, } from './javaStreamables.js';
82
84
  // TreeKey/TreeKeyNode — JS types and helpers
83
- export { TreeKey, TreeKeyNode, serializeTreeSignature, deserializeTreeSignature, getRootPublicKey, DEFAULT_KEYS_PER_LEVEL, DEFAULT_LEVELS, createUnifiedChildTreeKeyAsync, createPerAddressTreeKey, createPerAddressTreeKeyAsync, } from './treekey.js';
84
- export { F, hex, fromHex, u16be, u32be, assert32, h, prfChainSeed, toWinternitzDigits, baseWWithChecksum, wotsSignLegacy, } from './wots.js';
85
+ export { TreeKey, TreeKeyNode, serializeTreeSignature, deserializeTreeSignature, getRootPublicKey, DEFAULT_KEYS_PER_LEVEL, DEFAULT_LEVELS, createUnifiedChildTreeKeyAsync, createPerAddressTreeKey, createPerAddressTreeKeyAsync, setTreeKeyLogger, disableTreeKeyLogger, } from './treekey.js';
86
+ export { F, hex, fromHex, setWotsLogger, disableWotsLogger, u16be, u32be, assert32, h, prfChainSeed, toWinternitzDigits, baseWWithChecksum, wotsSignLegacy, } from './wots.js';
85
87
  // Canonical helpers (deterministic JSON + hashing)
86
88
  export { toHex, canonicalJson, hashCanonical } from './canonical.js';
87
89
  // Byte/string utilities
@@ -33,6 +33,7 @@ export interface LeaseStoreConfig {
33
33
  export declare class LeaseStore {
34
34
  private leases;
35
35
  private _initialized;
36
+ private mutationTail;
36
37
  private readonly storageKey;
37
38
  private readonly storage;
38
39
  private readonly logger;
@@ -42,16 +43,22 @@ export declare class LeaseStore {
42
43
  private load;
43
44
  private persist;
44
45
  save(lease: StoredLease): Promise<void>;
46
+ private saveUnlocked;
47
+ delete(leaseId: string): Promise<boolean>;
48
+ private deleteUnlocked;
45
49
  get(leaseId: string): StoredLease | undefined;
46
50
  getByToken(leaseToken: string): StoredLease | undefined;
47
51
  getAll(): StoredLease[];
48
52
  getActive(): StoredLease[];
49
53
  getExpiringSoon(thresholdMs?: number): StoredLease[];
50
- delete(leaseId: string): Promise<boolean>;
51
54
  deleteByToken(leaseToken: string): Promise<boolean>;
52
55
  updateStatus(leaseId: string, status: LeaseStatus): Promise<void>;
56
+ private updateStatusUnlocked;
53
57
  cleanupExpired(): Promise<number>;
58
+ private cleanupExpiredUnlocked;
54
59
  clear(): Promise<void>;
60
+ private clearUnlocked;
61
+ private mutate;
55
62
  getMinimumTTL(): number | null;
56
63
  calculateMonitoringInterval(): number;
57
64
  private flattenIndex;
@@ -11,9 +11,19 @@
11
11
  */
12
12
  import { NoopLogger } from '../adapters/index.js';
13
13
  const DEFAULT_STORAGE_KEY = 'totem_wots_leases';
14
+ /** Drop fields whose value is `undefined` (strict adapters reject `undefined`). */
15
+ function stripUndefined(value) {
16
+ const out = {};
17
+ for (const [k, v] of Object.entries(value)) {
18
+ if (v !== undefined)
19
+ out[k] = v;
20
+ }
21
+ return out;
22
+ }
14
23
  export class LeaseStore {
15
24
  leases = new Map();
16
25
  _initialized = false;
26
+ mutationTail = Promise.resolve();
17
27
  storageKey;
18
28
  storage;
19
29
  logger;
@@ -42,11 +52,12 @@ export class LeaseStore {
42
52
  }
43
53
  catch (error) {
44
54
  this.logger.error('Failed to load leases:', error);
55
+ throw error;
45
56
  }
46
57
  }
47
58
  async persist() {
48
59
  try {
49
- const leasesArray = Array.from(this.leases.values());
60
+ const leasesArray = Array.from(this.leases.values()).map((lease) => stripUndefined(lease));
50
61
  await this.storage.set(this.storageKey, leasesArray);
51
62
  }
52
63
  catch (error) {
@@ -55,12 +66,45 @@ export class LeaseStore {
55
66
  }
56
67
  }
57
68
  async save(lease) {
69
+ return this.mutate(() => this.saveUnlocked(lease));
70
+ }
71
+ async saveUnlocked(lease) {
58
72
  const flatIndex = this.flattenIndex(lease.indices);
59
73
  this.logger.debug(`Storing lease: ${lease.leaseId}`);
60
74
  this.logger.debug(` indices: (addressIndex=${lease.indices.addressIndex}, l1=${lease.indices.l1}, l2=${lease.indices.l2}) [${flatIndex}/262,144]`);
61
75
  this.logger.debug(` status: ${lease.status}, TTL: ${lease.leaseTTL}ms`);
76
+ const previous = this.leases.get(lease.leaseId);
62
77
  this.leases.set(lease.leaseId, lease);
63
- await this.persist();
78
+ try {
79
+ await this.persist();
80
+ }
81
+ catch (error) {
82
+ if (previous)
83
+ this.leases.set(lease.leaseId, previous);
84
+ else
85
+ this.leases.delete(lease.leaseId);
86
+ throw error;
87
+ }
88
+ }
89
+ async delete(leaseId) {
90
+ return this.mutate(() => this.deleteUnlocked(leaseId));
91
+ }
92
+ async deleteUnlocked(leaseId) {
93
+ const lease = this.leases.get(leaseId);
94
+ if (lease) {
95
+ this.logger.debug(`Deleting lease: ${leaseId}, final status: ${lease.status}`);
96
+ }
97
+ const existed = this.leases.delete(leaseId);
98
+ if (existed) {
99
+ try {
100
+ await this.persist();
101
+ }
102
+ catch (error) {
103
+ this.leases.set(leaseId, lease);
104
+ throw error;
105
+ }
106
+ }
107
+ return existed;
64
108
  }
65
109
  get(leaseId) {
66
110
  return this.leases.get(leaseId);
@@ -81,17 +125,6 @@ export class LeaseStore {
81
125
  lease.expiresAt > now &&
82
126
  lease.expiresAt <= now + thresholdMs);
83
127
  }
84
- async delete(leaseId) {
85
- const lease = this.leases.get(leaseId);
86
- if (lease) {
87
- this.logger.debug(`Deleting lease: ${leaseId}, final status: ${lease.status}`);
88
- }
89
- const existed = this.leases.delete(leaseId);
90
- if (existed) {
91
- await this.persist();
92
- }
93
- return existed;
94
- }
95
128
  async deleteByToken(leaseToken) {
96
129
  const lease = this.getByToken(leaseToken);
97
130
  if (lease) {
@@ -100,34 +133,71 @@ export class LeaseStore {
100
133
  return false;
101
134
  }
102
135
  async updateStatus(leaseId, status) {
136
+ return this.mutate(() => this.updateStatusUnlocked(leaseId, status));
137
+ }
138
+ async updateStatusUnlocked(leaseId, status) {
103
139
  const lease = this.leases.get(leaseId);
104
140
  if (lease) {
105
141
  const oldStatus = lease.status;
106
142
  this.logger.debug(`Lease status change: ${leaseId} ${oldStatus} → ${status}`);
107
143
  lease.status = status;
108
- await this.persist();
144
+ try {
145
+ await this.persist();
146
+ }
147
+ catch (error) {
148
+ lease.status = oldStatus;
149
+ throw error;
150
+ }
109
151
  }
110
152
  }
111
153
  async cleanupExpired() {
154
+ return this.mutate(() => this.cleanupExpiredUnlocked());
155
+ }
156
+ async cleanupExpiredUnlocked() {
112
157
  const now = Date.now();
113
- const expiredLeaseIds = [];
158
+ const expiredLeases = [];
114
159
  for (const [leaseId, lease] of this.leases.entries()) {
115
- if (lease.expiresAt <= now && lease.status !== 'finalized') {
160
+ if (lease.expiresAt <= now && (lease.status === 'active' || lease.status === 'pending')) {
161
+ expiredLeases.push({ leaseId, status: lease.status });
116
162
  lease.status = 'expired';
117
- expiredLeaseIds.push(leaseId);
118
163
  }
119
164
  }
120
- if (expiredLeaseIds.length > 0) {
121
- this.logger.debug(`Marked ${expiredLeaseIds.length} leases as expired`);
122
- await this.persist();
165
+ if (expiredLeases.length > 0) {
166
+ this.logger.debug(`Marked ${expiredLeases.length} leases as expired`);
167
+ try {
168
+ await this.persist();
169
+ }
170
+ catch (error) {
171
+ for (const { leaseId, status } of expiredLeases)
172
+ this.leases.get(leaseId).status = status;
173
+ throw error;
174
+ }
123
175
  }
124
- return expiredLeaseIds.length;
176
+ return expiredLeases.length;
125
177
  }
126
178
  async clear() {
179
+ await this.mutate(() => this.clearUnlocked());
180
+ }
181
+ async clearUnlocked() {
182
+ const previous = new Map(this.leases);
127
183
  this.leases.clear();
128
- await this.storage.remove(this.storageKey);
184
+ try {
185
+ await this.storage.remove(this.storageKey);
186
+ }
187
+ catch (error) {
188
+ this.leases = previous;
189
+ throw error;
190
+ }
129
191
  this.logger.debug('Cleared all leases');
130
192
  }
193
+ mutate(operation) {
194
+ const prior = this.mutationTail;
195
+ let release;
196
+ const gate = new Promise((resolve) => { release = resolve; });
197
+ const run = prior.then(() => operation()).finally(() => release());
198
+ this.mutationTail = run.then(() => undefined, () => undefined);
199
+ return run;
200
+ }
131
201
  getMinimumTTL() {
132
202
  const activeLeases = this.getActive();
133
203
  if (activeLeases.length === 0)
@@ -47,19 +47,13 @@ export class WatermarkStore {
47
47
  return this._initialized;
48
48
  }
49
49
  async load() {
50
- try {
51
- const watermark = await this.storage.get(this.storageKey);
52
- if (watermark) {
53
- this.state = watermark;
54
- this.logger.debug('Loaded watermark from storage');
55
- return this.state;
56
- }
57
- return null;
58
- }
59
- catch (error) {
60
- this.logger.error('Failed to load watermark:', error);
61
- return null;
50
+ const watermark = await this.storage.get(this.storageKey);
51
+ if (watermark) {
52
+ this.state = watermark;
53
+ this.logger.debug('Loaded watermark from storage');
54
+ return this.state;
62
55
  }
56
+ return null;
63
57
  }
64
58
  async save(watermark) {
65
59
  try {
package/dist/script.js CHANGED
@@ -3,7 +3,7 @@ import { scriptToAddress } from "./derive.js";
3
3
  /** Produce KISSVM script that authorizes with a WOTS PK digest (32 bytes). */
4
4
  export function scriptFromWotsPk(pkDigest32) {
5
5
  // Minima uses SHA3-256 in SIGNEDBY paths; here we embed the digest as hex.
6
- return `RETURN SIGNEDBY(${bytesToHex(pkDigest32)})`;
6
+ return `RETURN SIGNEDBY(0x${bytesToHex(pkDigest32)})`;
7
7
  }
8
8
  /** Address from WOTS keypair (client-only): script -> MMR leaf -> Mx */
9
9
  export function wotsAddressFromKeypair(kp) {
@@ -23,7 +23,7 @@ export declare function serializeExtraScripts(extraScripts: Map<string, string>)
23
23
  export declare function aggregateSignatures(totemSignature: {
24
24
  publicKey: Uint8Array;
25
25
  signature: Uint8Array;
26
- }, externalSignatures: ExternalSignature[]): Uint8Array[];
26
+ }, externalSignatures: ExternalSignature[], transactionDigest: Uint8Array): Uint8Array[];
27
27
  export declare function validateExternalSignature(signature: ExternalSignature, transactionDigest: Uint8Array): boolean;
28
28
  export declare function computeScriptAddress(script: string): string;
29
29
  export { concat };
@@ -1,4 +1,5 @@
1
1
  import { sha3_256 } from '../wasm-sync.js';
2
+ import { wotsVerifyDigest } from '../wots.js';
2
3
  import { serializeMMRProof } from '../mmr.js';
3
4
  import { mxToHex } from '../minima32.js';
4
5
  function hexToBytes(hex) {
@@ -252,12 +253,26 @@ export function serializeExtraScripts(extraScripts) {
252
253
  }
253
254
  return concat(...parts);
254
255
  }
255
- export function aggregateSignatures(totemSignature, externalSignatures) {
256
+ export function aggregateSignatures(totemSignature, externalSignatures, transactionDigest) {
257
+ // RFC-020 C5: the aggregate must never include an unverified signature.
258
+ // The totem signature is verified against the transaction digest; external
259
+ // signatures are verified here as well as gated on their `validated` flag.
260
+ const totemValid = (() => {
261
+ try {
262
+ return wotsVerifyDigest(totemSignature.signature, transactionDigest, totemSignature.publicKey);
263
+ }
264
+ catch {
265
+ return false;
266
+ }
267
+ })();
268
+ if (!totemValid) {
269
+ throw new Error('aggregateSignatures: the totem signature is invalid over the transaction digest');
270
+ }
256
271
  const allSigProofs = [];
257
272
  allSigProofs.push(serializeSignatureProof(totemSignature.publicKey, totemSignature.signature, { chunks: [] }));
258
273
  for (const extSig of externalSignatures) {
259
- if (!extSig.validated) {
260
- console.warn(`[AggregateSignatures] Skipping unvalidated signature from ${extSig.publicKey}`);
274
+ if (!extSig.validated || !validateExternalSignature(extSig, transactionDigest)) {
275
+ console.warn(`[AggregateSignatures] Skipping unverified signature from ${extSig.publicKey}`);
261
276
  continue;
262
277
  }
263
278
  const proof = extSig.proof || { chunks: [] };
@@ -276,8 +291,16 @@ export function validateExternalSignature(signature, transactionDigest) {
276
291
  if (!signature.publicKey || !signature.signature) {
277
292
  return false;
278
293
  }
279
- console.log(`[ValidateSignature] Validating signature from ${signature.publicKey.substring(0, 16)}...`);
280
- return true;
294
+ // RFC-020 C5: real WOTS verification (was an unconditional `true` stub).
295
+ if (signature.signatureType && signature.signatureType !== 'wots') {
296
+ return false;
297
+ }
298
+ try {
299
+ return wotsVerifyDigest(hexToBytes(signature.signature), transactionDigest, hexToBytes(signature.publicKey));
300
+ }
301
+ catch {
302
+ return false;
303
+ }
281
304
  }
282
305
  export function computeScriptAddress(script) {
283
306
  const cleanScript = script.trim().toUpperCase();