@unicitylabs/sphere-sdk 0.5.3 → 0.5.5
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 +2 -0
- package/dist/connect/index.cjs +145 -23
- package/dist/connect/index.cjs.map +1 -1
- package/dist/connect/index.d.cts +15 -2
- package/dist/connect/index.d.ts +15 -2
- package/dist/connect/index.js +145 -23
- package/dist/connect/index.js.map +1 -1
- package/dist/core/index.cjs +670 -473
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +123 -2
- package/dist/core/index.d.ts +123 -2
- package/dist/core/index.js +667 -473
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/connect/index.cjs +119 -1
- package/dist/impl/browser/connect/index.cjs.map +1 -1
- package/dist/impl/browser/connect/index.d.cts +53 -1
- package/dist/impl/browser/connect/index.d.ts +53 -1
- package/dist/impl/browser/connect/index.js +119 -1
- package/dist/impl/browser/connect/index.js.map +1 -1
- package/dist/impl/browser/index.cjs +306 -193
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +306 -193
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/browser/ipfs.cjs +134 -19
- package/dist/impl/browser/ipfs.cjs.map +1 -1
- package/dist/impl/browser/ipfs.js +134 -19
- package/dist/impl/browser/ipfs.js.map +1 -1
- package/dist/impl/nodejs/connect/index.cjs +101 -6
- package/dist/impl/nodejs/connect/index.cjs.map +1 -1
- package/dist/impl/nodejs/connect/index.d.cts +2 -0
- package/dist/impl/nodejs/connect/index.d.ts +2 -0
- package/dist/impl/nodejs/connect/index.js +101 -6
- package/dist/impl/nodejs/connect/index.js.map +1 -1
- package/dist/impl/nodejs/index.cjs +267 -152
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +2 -1
- package/dist/impl/nodejs/index.d.ts +2 -1
- package/dist/impl/nodejs/index.js +267 -152
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +682 -493
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +124 -8
- package/dist/index.d.ts +124 -8
- package/dist/index.js +680 -493
- package/dist/index.js.map +1 -1
- package/dist/l1/index.cjs +139 -32
- package/dist/l1/index.cjs.map +1 -1
- package/dist/l1/index.js +139 -32
- package/dist/l1/index.js.map +1 -1
- package/package.json +1 -16
package/dist/l1/index.cjs
CHANGED
|
@@ -112,6 +112,18 @@ function domIdToPath(encoded) {
|
|
|
112
112
|
}).join("/");
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// core/errors.ts
|
|
116
|
+
var SphereError = class extends Error {
|
|
117
|
+
code;
|
|
118
|
+
cause;
|
|
119
|
+
constructor(message, code, cause) {
|
|
120
|
+
super(message);
|
|
121
|
+
this.name = "SphereError";
|
|
122
|
+
this.code = code;
|
|
123
|
+
this.cause = cause;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
115
127
|
// core/bech32.ts
|
|
116
128
|
var CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
117
129
|
var GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
|
|
@@ -168,11 +180,11 @@ function bech32Checksum(hrp, data) {
|
|
|
168
180
|
}
|
|
169
181
|
function encodeBech32(hrp, version, program) {
|
|
170
182
|
if (version < 0 || version > 16) {
|
|
171
|
-
throw new
|
|
183
|
+
throw new SphereError("Invalid witness version", "VALIDATION_ERROR");
|
|
172
184
|
}
|
|
173
185
|
const converted = convertBits(Array.from(program), 8, 5, true);
|
|
174
186
|
if (!converted) {
|
|
175
|
-
throw new
|
|
187
|
+
throw new SphereError("Failed to convert bits", "VALIDATION_ERROR");
|
|
176
188
|
}
|
|
177
189
|
const data = [version].concat(converted);
|
|
178
190
|
const checksum = bech32Checksum(hrp, data);
|
|
@@ -219,7 +231,7 @@ function bytesToHex(buf) {
|
|
|
219
231
|
}
|
|
220
232
|
function addressToScriptHash(address) {
|
|
221
233
|
const decoded = decodeBech32(address);
|
|
222
|
-
if (!decoded) throw new
|
|
234
|
+
if (!decoded) throw new SphereError("Invalid bech32 address: " + address, "VALIDATION_ERROR");
|
|
223
235
|
const scriptHex = "0014" + bytesToHex(decoded.data);
|
|
224
236
|
const sha = import_crypto_js.default.SHA256(import_crypto_js.default.enc.Hex.parse(scriptHex)).toString();
|
|
225
237
|
return sha.match(/../g).reverse().join("");
|
|
@@ -242,7 +254,7 @@ function generateMasterKey(seedHex) {
|
|
|
242
254
|
const IR = I.substring(64);
|
|
243
255
|
const masterKeyBigInt = BigInt("0x" + IL);
|
|
244
256
|
if (masterKeyBigInt === 0n || masterKeyBigInt >= CURVE_ORDER) {
|
|
245
|
-
throw new
|
|
257
|
+
throw new SphereError("Invalid master key generated", "VALIDATION_ERROR");
|
|
246
258
|
}
|
|
247
259
|
return {
|
|
248
260
|
privateKey: IL,
|
|
@@ -270,11 +282,11 @@ function deriveChildKey(parentPrivKey, parentChainCode, index) {
|
|
|
270
282
|
const ilBigInt = BigInt("0x" + IL);
|
|
271
283
|
const parentKeyBigInt = BigInt("0x" + parentPrivKey);
|
|
272
284
|
if (ilBigInt >= CURVE_ORDER) {
|
|
273
|
-
throw new
|
|
285
|
+
throw new SphereError("Invalid key: IL >= curve order", "VALIDATION_ERROR");
|
|
274
286
|
}
|
|
275
287
|
const childKeyBigInt = (ilBigInt + parentKeyBigInt) % CURVE_ORDER;
|
|
276
288
|
if (childKeyBigInt === 0n) {
|
|
277
|
-
throw new
|
|
289
|
+
throw new SphereError("Invalid key: child key is zero", "VALIDATION_ERROR");
|
|
278
290
|
}
|
|
279
291
|
const childPrivKey = childKeyBigInt.toString(16).padStart(64, "0");
|
|
280
292
|
return {
|
|
@@ -444,6 +456,98 @@ function generateHDAddress(masterPriv, chainCode, index) {
|
|
|
444
456
|
return generateAddressInfo(child.privateKey, index, path);
|
|
445
457
|
}
|
|
446
458
|
|
|
459
|
+
// core/logger.ts
|
|
460
|
+
var LOGGER_KEY = "__sphere_sdk_logger__";
|
|
461
|
+
function getState() {
|
|
462
|
+
const g = globalThis;
|
|
463
|
+
if (!g[LOGGER_KEY]) {
|
|
464
|
+
g[LOGGER_KEY] = { debug: false, tags: {}, handler: null };
|
|
465
|
+
}
|
|
466
|
+
return g[LOGGER_KEY];
|
|
467
|
+
}
|
|
468
|
+
function isEnabled(tag) {
|
|
469
|
+
const state = getState();
|
|
470
|
+
if (tag in state.tags) return state.tags[tag];
|
|
471
|
+
return state.debug;
|
|
472
|
+
}
|
|
473
|
+
var logger = {
|
|
474
|
+
/**
|
|
475
|
+
* Configure the logger. Can be called multiple times (last write wins).
|
|
476
|
+
* Typically called by createBrowserProviders(), createNodeProviders(), or Sphere.init().
|
|
477
|
+
*/
|
|
478
|
+
configure(config) {
|
|
479
|
+
const state = getState();
|
|
480
|
+
if (config.debug !== void 0) state.debug = config.debug;
|
|
481
|
+
if (config.handler !== void 0) state.handler = config.handler;
|
|
482
|
+
},
|
|
483
|
+
/**
|
|
484
|
+
* Enable/disable debug logging for a specific tag.
|
|
485
|
+
* Per-tag setting overrides the global debug flag.
|
|
486
|
+
*
|
|
487
|
+
* @example
|
|
488
|
+
* ```ts
|
|
489
|
+
* logger.setTagDebug('Nostr', true); // enable only Nostr logs
|
|
490
|
+
* logger.setTagDebug('Nostr', false); // disable Nostr logs even if global debug=true
|
|
491
|
+
* ```
|
|
492
|
+
*/
|
|
493
|
+
setTagDebug(tag, enabled) {
|
|
494
|
+
getState().tags[tag] = enabled;
|
|
495
|
+
},
|
|
496
|
+
/**
|
|
497
|
+
* Clear per-tag override, falling back to global debug flag.
|
|
498
|
+
*/
|
|
499
|
+
clearTagDebug(tag) {
|
|
500
|
+
delete getState().tags[tag];
|
|
501
|
+
},
|
|
502
|
+
/** Returns true if debug mode is enabled for the given tag (or globally). */
|
|
503
|
+
isDebugEnabled(tag) {
|
|
504
|
+
if (tag) return isEnabled(tag);
|
|
505
|
+
return getState().debug;
|
|
506
|
+
},
|
|
507
|
+
/**
|
|
508
|
+
* Debug-level log. Only shown when debug is enabled (globally or for this tag).
|
|
509
|
+
* Use for detailed operational information.
|
|
510
|
+
*/
|
|
511
|
+
debug(tag, message, ...args) {
|
|
512
|
+
if (!isEnabled(tag)) return;
|
|
513
|
+
const state = getState();
|
|
514
|
+
if (state.handler) {
|
|
515
|
+
state.handler("debug", tag, message, ...args);
|
|
516
|
+
} else {
|
|
517
|
+
console.log(`[${tag}]`, message, ...args);
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
/**
|
|
521
|
+
* Warning-level log. ALWAYS shown regardless of debug flag.
|
|
522
|
+
* Use for important but non-critical issues (timeouts, retries, degraded state).
|
|
523
|
+
*/
|
|
524
|
+
warn(tag, message, ...args) {
|
|
525
|
+
const state = getState();
|
|
526
|
+
if (state.handler) {
|
|
527
|
+
state.handler("warn", tag, message, ...args);
|
|
528
|
+
} else {
|
|
529
|
+
console.warn(`[${tag}]`, message, ...args);
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
/**
|
|
533
|
+
* Error-level log. ALWAYS shown regardless of debug flag.
|
|
534
|
+
* Use for critical failures that should never be silenced.
|
|
535
|
+
*/
|
|
536
|
+
error(tag, message, ...args) {
|
|
537
|
+
const state = getState();
|
|
538
|
+
if (state.handler) {
|
|
539
|
+
state.handler("error", tag, message, ...args);
|
|
540
|
+
} else {
|
|
541
|
+
console.error(`[${tag}]`, message, ...args);
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
/** Reset all logger state (debug flag, tags, handler). Primarily for tests. */
|
|
545
|
+
reset() {
|
|
546
|
+
const g = globalThis;
|
|
547
|
+
delete g[LOGGER_KEY];
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
|
|
447
551
|
// constants.ts
|
|
448
552
|
var STORAGE_KEYS_GLOBAL = {
|
|
449
553
|
/** Encrypted BIP39 mnemonic */
|
|
@@ -572,7 +676,7 @@ function connect(endpoint = DEFAULT_ENDPOINT) {
|
|
|
572
676
|
try {
|
|
573
677
|
ws = new WebSocket(endpoint);
|
|
574
678
|
} catch (err) {
|
|
575
|
-
|
|
679
|
+
logger.error("L1", "WebSocket constructor threw exception:", err);
|
|
576
680
|
isConnecting = false;
|
|
577
681
|
reject(err);
|
|
578
682
|
return;
|
|
@@ -608,7 +712,7 @@ function connect(endpoint = DEFAULT_ENDPOINT) {
|
|
|
608
712
|
return;
|
|
609
713
|
}
|
|
610
714
|
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
611
|
-
|
|
715
|
+
logger.error("L1", "Max reconnect attempts reached. Giving up.");
|
|
612
716
|
isConnecting = false;
|
|
613
717
|
const error = new Error("Max reconnect attempts reached");
|
|
614
718
|
connectionCallbacks.forEach((cb) => {
|
|
@@ -624,8 +728,9 @@ function connect(endpoint = DEFAULT_ENDPOINT) {
|
|
|
624
728
|
}
|
|
625
729
|
const delay = Math.min(BASE_DELAY * Math.pow(2, reconnectAttempts), MAX_DELAY);
|
|
626
730
|
reconnectAttempts++;
|
|
627
|
-
|
|
628
|
-
|
|
731
|
+
logger.warn(
|
|
732
|
+
"L1",
|
|
733
|
+
`WebSocket closed unexpectedly. Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`
|
|
629
734
|
);
|
|
630
735
|
setTimeout(() => {
|
|
631
736
|
connect(endpoint).then(() => {
|
|
@@ -642,7 +747,7 @@ function connect(endpoint = DEFAULT_ENDPOINT) {
|
|
|
642
747
|
}, delay);
|
|
643
748
|
};
|
|
644
749
|
ws.onerror = (err) => {
|
|
645
|
-
|
|
750
|
+
logger.error("L1", "WebSocket error:", err);
|
|
646
751
|
};
|
|
647
752
|
ws.onmessage = (msg) => handleMessage(msg);
|
|
648
753
|
});
|
|
@@ -700,7 +805,7 @@ async function getUtxo(address) {
|
|
|
700
805
|
const scripthash = addressToScriptHash(address);
|
|
701
806
|
const result = await rpc("blockchain.scripthash.listunspent", [scripthash]);
|
|
702
807
|
if (!Array.isArray(result)) {
|
|
703
|
-
|
|
808
|
+
logger.warn("L1", "listunspent returned non-array:", result);
|
|
704
809
|
return [];
|
|
705
810
|
}
|
|
706
811
|
return result.map((u) => ({
|
|
@@ -752,7 +857,7 @@ async function getTransactionHistory(address) {
|
|
|
752
857
|
const scriptHash = addressToScriptHash(address);
|
|
753
858
|
const result = await rpc("blockchain.scripthash.get_history", [scriptHash]);
|
|
754
859
|
if (!Array.isArray(result)) {
|
|
755
|
-
|
|
860
|
+
logger.warn("L1", "get_history returned non-array:", result);
|
|
756
861
|
return [];
|
|
757
862
|
}
|
|
758
863
|
return result;
|
|
@@ -768,7 +873,7 @@ async function getCurrentBlockHeight() {
|
|
|
768
873
|
const header = await rpc("blockchain.headers.subscribe", []);
|
|
769
874
|
return header?.height || 0;
|
|
770
875
|
} catch (err) {
|
|
771
|
-
|
|
876
|
+
logger.error("L1", "Error getting current block height:", err);
|
|
772
877
|
return 0;
|
|
773
878
|
}
|
|
774
879
|
}
|
|
@@ -1040,7 +1145,7 @@ var VestingClassifier = class {
|
|
|
1040
1145
|
await new Promise((resolve) => {
|
|
1041
1146
|
const req = indexedDB.deleteDatabase(this.dbName);
|
|
1042
1147
|
const timer = setTimeout(() => {
|
|
1043
|
-
|
|
1148
|
+
logger.warn("L1", ` destroy: deleteDatabase timed out for ${this.dbName}`);
|
|
1044
1149
|
resolve();
|
|
1045
1150
|
}, 3e3);
|
|
1046
1151
|
req.onsuccess = () => {
|
|
@@ -1052,7 +1157,7 @@ var VestingClassifier = class {
|
|
|
1052
1157
|
resolve();
|
|
1053
1158
|
};
|
|
1054
1159
|
req.onblocked = () => {
|
|
1055
|
-
|
|
1160
|
+
logger.warn("L1", ` destroy: deleteDatabase blocked for ${this.dbName}, waiting...`);
|
|
1056
1161
|
};
|
|
1057
1162
|
});
|
|
1058
1163
|
}
|
|
@@ -1070,7 +1175,7 @@ var VestingStateManager = class {
|
|
|
1070
1175
|
*/
|
|
1071
1176
|
setMode(mode) {
|
|
1072
1177
|
if (!["all", "vested", "unvested"].includes(mode)) {
|
|
1073
|
-
throw new
|
|
1178
|
+
throw new SphereError(`Invalid vesting mode: ${mode}`, "VALIDATION_ERROR");
|
|
1074
1179
|
}
|
|
1075
1180
|
this.currentMode = mode;
|
|
1076
1181
|
}
|
|
@@ -1107,10 +1212,10 @@ var VestingStateManager = class {
|
|
|
1107
1212
|
}
|
|
1108
1213
|
});
|
|
1109
1214
|
if (result.errors.length > 0) {
|
|
1110
|
-
|
|
1215
|
+
logger.warn("L1", `Vesting classification errors: ${result.errors.length}`);
|
|
1111
1216
|
result.errors.slice(0, 5).forEach((err) => {
|
|
1112
1217
|
const txHash = err.utxo.tx_hash || err.utxo.txid;
|
|
1113
|
-
|
|
1218
|
+
logger.warn("L1", ` ${txHash}: ${err.error}`);
|
|
1114
1219
|
});
|
|
1115
1220
|
}
|
|
1116
1221
|
} finally {
|
|
@@ -1232,16 +1337,17 @@ var WalletAddressHelper = class {
|
|
|
1232
1337
|
*/
|
|
1233
1338
|
static add(wallet, newAddress) {
|
|
1234
1339
|
if (!newAddress.path) {
|
|
1235
|
-
throw new
|
|
1340
|
+
throw new SphereError("Cannot add address without a path", "INVALID_CONFIG");
|
|
1236
1341
|
}
|
|
1237
1342
|
const existing = this.findByPath(wallet, newAddress.path);
|
|
1238
1343
|
if (existing) {
|
|
1239
1344
|
if (existing.address !== newAddress.address) {
|
|
1240
|
-
throw new
|
|
1345
|
+
throw new SphereError(
|
|
1241
1346
|
`CRITICAL: Attempted to overwrite address for path ${newAddress.path}
|
|
1242
1347
|
Existing: ${existing.address}
|
|
1243
1348
|
New: ${newAddress.address}
|
|
1244
|
-
This indicates master key corruption or derivation logic error
|
|
1349
|
+
This indicates master key corruption or derivation logic error.`,
|
|
1350
|
+
"INVALID_CONFIG"
|
|
1245
1351
|
);
|
|
1246
1352
|
}
|
|
1247
1353
|
return wallet;
|
|
@@ -1300,9 +1406,10 @@ This indicates master key corruption or derivation logic error.`
|
|
|
1300
1406
|
const uniquePaths = new Set(paths);
|
|
1301
1407
|
if (paths.length !== uniquePaths.size) {
|
|
1302
1408
|
const duplicates = paths.filter((p, i) => paths.indexOf(p) !== i);
|
|
1303
|
-
throw new
|
|
1409
|
+
throw new SphereError(
|
|
1304
1410
|
`CRITICAL: Wallet has duplicate paths: ${duplicates.join(", ")}
|
|
1305
|
-
This indicates data corruption. Please restore from backup
|
|
1411
|
+
This indicates data corruption. Please restore from backup.`,
|
|
1412
|
+
"INVALID_CONFIG"
|
|
1306
1413
|
);
|
|
1307
1414
|
}
|
|
1308
1415
|
}
|
|
@@ -1334,11 +1441,11 @@ var DUST = 546;
|
|
|
1334
1441
|
var SAT = 1e8;
|
|
1335
1442
|
function createScriptPubKey(address) {
|
|
1336
1443
|
if (!address || typeof address !== "string") {
|
|
1337
|
-
throw new
|
|
1444
|
+
throw new SphereError("Invalid address: must be a string", "VALIDATION_ERROR");
|
|
1338
1445
|
}
|
|
1339
1446
|
const decoded = decodeBech32(address);
|
|
1340
1447
|
if (!decoded) {
|
|
1341
|
-
throw new
|
|
1448
|
+
throw new SphereError("Invalid bech32 address: " + address, "VALIDATION_ERROR");
|
|
1342
1449
|
}
|
|
1343
1450
|
const dataHex = Array.from(decoded.data).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1344
1451
|
return "0014" + dataHex;
|
|
@@ -1462,7 +1569,7 @@ function createAndSignTransaction(wallet, txPlan) {
|
|
|
1462
1569
|
privateKeyHex = wallet.masterPrivateKey;
|
|
1463
1570
|
}
|
|
1464
1571
|
if (!privateKeyHex) {
|
|
1465
|
-
throw new
|
|
1572
|
+
throw new SphereError("No private key available for address: " + fromAddress, "INVALID_CONFIG");
|
|
1466
1573
|
}
|
|
1467
1574
|
const keyPair = ec2.keyFromPrivate(privateKeyHex, "hex");
|
|
1468
1575
|
const publicKey = keyPair.getPublic(true, "hex");
|
|
@@ -1561,7 +1668,7 @@ function collectUtxosForAmount(utxoList, amountSats, recipientAddress, senderAdd
|
|
|
1561
1668
|
}
|
|
1562
1669
|
async function createTransactionPlan(wallet, toAddress, amountAlpha, fromAddress) {
|
|
1563
1670
|
if (!decodeBech32(toAddress)) {
|
|
1564
|
-
throw new
|
|
1671
|
+
throw new SphereError("Invalid recipient address", "INVALID_RECIPIENT");
|
|
1565
1672
|
}
|
|
1566
1673
|
const defaultAddr = WalletAddressHelper.getDefault(wallet);
|
|
1567
1674
|
const senderAddress = fromAddress || defaultAddr.address;
|
|
@@ -1570,21 +1677,21 @@ async function createTransactionPlan(wallet, toAddress, amountAlpha, fromAddress
|
|
|
1570
1677
|
const currentMode = vestingState.getMode();
|
|
1571
1678
|
if (vestingState.hasClassifiedData(senderAddress)) {
|
|
1572
1679
|
utxos = vestingState.getFilteredUtxos(senderAddress);
|
|
1573
|
-
|
|
1680
|
+
logger.debug("L1", `Using ${utxos.length} ${currentMode} UTXOs`);
|
|
1574
1681
|
} else {
|
|
1575
1682
|
utxos = await getUtxo(senderAddress);
|
|
1576
|
-
|
|
1683
|
+
logger.debug("L1", `Using ${utxos.length} UTXOs (vesting not classified yet)`);
|
|
1577
1684
|
}
|
|
1578
1685
|
if (!Array.isArray(utxos) || utxos.length === 0) {
|
|
1579
1686
|
const modeText = currentMode !== "all" ? ` (${currentMode} coins)` : "";
|
|
1580
|
-
throw new
|
|
1687
|
+
throw new SphereError(`No UTXOs available${modeText} for address: ` + senderAddress, "INSUFFICIENT_BALANCE");
|
|
1581
1688
|
}
|
|
1582
1689
|
return collectUtxosForAmount(utxos, amountSats, toAddress, senderAddress);
|
|
1583
1690
|
}
|
|
1584
1691
|
async function sendAlpha(wallet, toAddress, amountAlpha, fromAddress) {
|
|
1585
1692
|
const plan = await createTransactionPlan(wallet, toAddress, amountAlpha, fromAddress);
|
|
1586
1693
|
if (!plan.success) {
|
|
1587
|
-
throw new
|
|
1694
|
+
throw new SphereError(plan.error || "Transaction planning failed", "TRANSFER_FAILED");
|
|
1588
1695
|
}
|
|
1589
1696
|
const results = [];
|
|
1590
1697
|
for (const tx of plan.transactions) {
|