Compare commits
3 Commits
master
...
perf-hd-fe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf1517582d | ||
|
|
55842b6f7f | ||
|
|
bb11ba87f4 |
@ -424,137 +424,96 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||
// now, we need to put transactions in all relevant `cells` of internal hashmaps:
|
||||
// this._txs_by_internal_index, this._txs_by_external_index & this._txs_by_payment_code_index
|
||||
|
||||
// instead of re-scanning every transaction for every address index (quadratic),
|
||||
// we build address -> index lookup maps once and do a single pass over transactions
|
||||
const externalIndexByAddress = new Map<string, number>();
|
||||
for (let c = 0; c < next_free_address_index + this.gap_limit; c++) {
|
||||
for (const tx of Object.values(txdatas)) {
|
||||
for (const vin of tx.vin) {
|
||||
if (vin.addresses && vin.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) {
|
||||
// this TX is related to our address
|
||||
this._txs_by_external_index[c] = this._txs_by_external_index[c] || [];
|
||||
const { vin: txVin, vout: txVout, ...txRest } = tx;
|
||||
const clonedTx = {
|
||||
...txRest,
|
||||
inputs: txVin.slice(0),
|
||||
outputs: txVout.slice(0),
|
||||
timestamp: tx.blocktime || tx.time || Math.floor(+new Date() / 1000) - 30 /* unconfirmed */,
|
||||
};
|
||||
|
||||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
||||
let replaced = false;
|
||||
for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) {
|
||||
if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) {
|
||||
replaced = true;
|
||||
this._txs_by_external_index[c][cc] = clonedTx;
|
||||
}
|
||||
}
|
||||
if (!replaced) this._txs_by_external_index[c].push(clonedTx);
|
||||
}
|
||||
}
|
||||
for (const vout of tx.vout) {
|
||||
if (vout.scriptPubKey.addresses && vout.scriptPubKey.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) {
|
||||
// this TX is related to our address
|
||||
this._txs_by_external_index[c] = this._txs_by_external_index[c] || [];
|
||||
const { vin: txVin, vout: txVout, ...txRest } = tx;
|
||||
const clonedTx = {
|
||||
...txRest,
|
||||
inputs: txVin.slice(0),
|
||||
outputs: txVout.slice(0),
|
||||
timestamp: tx.blocktime || tx.time || Math.floor(+new Date() / 1000) - 30 /* unconfirmed */,
|
||||
};
|
||||
|
||||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
||||
let replaced = false;
|
||||
for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) {
|
||||
if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) {
|
||||
replaced = true;
|
||||
this._txs_by_external_index[c][cc] = clonedTx;
|
||||
}
|
||||
}
|
||||
if (!replaced) this._txs_by_external_index[c].push(clonedTx);
|
||||
}
|
||||
}
|
||||
}
|
||||
externalIndexByAddress.set(this._getExternalAddressByIndex(c), c);
|
||||
}
|
||||
|
||||
const internalIndexByAddress = new Map<string, number>();
|
||||
for (let c = 0; c < next_free_change_address_index + this.gap_limit; c++) {
|
||||
for (const tx of Object.values(txdatas)) {
|
||||
for (const vin of tx.vin) {
|
||||
if (vin.addresses && vin.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) {
|
||||
// this TX is related to our address
|
||||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || [];
|
||||
const { vin: txVin, vout: txVout, ...txRest } = tx;
|
||||
const clonedTx = {
|
||||
...txRest,
|
||||
inputs: txVin.slice(0),
|
||||
outputs: txVout.slice(0),
|
||||
timestamp: tx.blocktime || tx.time || Math.floor(+new Date() / 1000) - 30 /* unconfirmed */,
|
||||
};
|
||||
|
||||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
||||
let replaced = false;
|
||||
for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) {
|
||||
if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) {
|
||||
replaced = true;
|
||||
this._txs_by_internal_index[c][cc] = clonedTx;
|
||||
}
|
||||
}
|
||||
if (!replaced) this._txs_by_internal_index[c].push(clonedTx);
|
||||
}
|
||||
}
|
||||
for (const vout of tx.vout) {
|
||||
if (vout.scriptPubKey.addresses && vout.scriptPubKey.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) {
|
||||
// this TX is related to our address
|
||||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || [];
|
||||
const { vin: txVin, vout: txVout, ...txRest } = tx;
|
||||
const clonedTx = {
|
||||
...txRest,
|
||||
inputs: txVin.slice(0),
|
||||
outputs: txVout.slice(0),
|
||||
timestamp: tx.blocktime || tx.time || Math.floor(+new Date() / 1000) - 30 /* unconfirmed */,
|
||||
};
|
||||
|
||||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
||||
let replaced = false;
|
||||
for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) {
|
||||
if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) {
|
||||
replaced = true;
|
||||
this._txs_by_internal_index[c][cc] = clonedTx;
|
||||
}
|
||||
}
|
||||
if (!replaced) this._txs_by_internal_index[c].push(clonedTx);
|
||||
}
|
||||
}
|
||||
}
|
||||
internalIndexByAddress.set(this._getInternalAddressByIndex(c), c);
|
||||
}
|
||||
|
||||
const paymentCodeIndexByAddress = new Map<string, { pc: string; c: number }>();
|
||||
for (const pc of this._receive_payment_codes) {
|
||||
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + this.gap_limit; c++) {
|
||||
for (const tx of Object.values(txdatas)) {
|
||||
// since we are iterating PCs who can pay us, we can completely ignore `tx.vin` and only iterate `tx.vout`
|
||||
for (const vout of tx.vout) {
|
||||
if (vout.scriptPubKey.addresses && vout.scriptPubKey.addresses.indexOf(this._getBIP47AddressReceive(pc, c)) !== -1) {
|
||||
// this TX is related to our address
|
||||
this._txs_by_payment_code_index[pc] = this._txs_by_payment_code_index[pc] || {};
|
||||
this._txs_by_payment_code_index[pc][c] = this._txs_by_payment_code_index[pc][c] || [];
|
||||
const { vin: txVin, vout: txVout, ...txRest } = tx;
|
||||
const clonedTx = {
|
||||
...txRest,
|
||||
inputs: txVin.slice(0),
|
||||
outputs: txVout.slice(0),
|
||||
timestamp: tx.blocktime || tx.time || Math.floor(+new Date() / 1000) - 30 /* unconfirmed */,
|
||||
};
|
||||
paymentCodeIndexByAddress.set(this._getBIP47AddressReceive(pc, c), { pc, c });
|
||||
}
|
||||
}
|
||||
|
||||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
||||
let replaced = false;
|
||||
for (let cc = 0; cc < this._txs_by_payment_code_index[pc][c].length; cc++) {
|
||||
if (this._txs_by_payment_code_index[pc][c][cc].txid === clonedTx.txid) {
|
||||
replaced = true;
|
||||
this._txs_by_payment_code_index[pc][c][cc] = clonedTx;
|
||||
}
|
||||
}
|
||||
if (!replaced) this._txs_by_payment_code_index[pc][c].push(clonedTx);
|
||||
}
|
||||
}
|
||||
// per-cell txid -> position lookup, so upserting does not linearly re-scan the cell
|
||||
// for every transaction (which is quadratic when many transactions share one address)
|
||||
const cellPositionsByTxid = new Map<Transaction[], Map<string, number>>();
|
||||
const getCellPositions = (cell: Transaction[]): Map<string, number> => {
|
||||
let positions = cellPositionsByTxid.get(cell);
|
||||
if (!positions) {
|
||||
positions = new Map();
|
||||
for (let cc = 0; cc < cell.length; cc++) positions.set(cell[cc].txid, cc);
|
||||
cellPositionsByTxid.set(cell, positions);
|
||||
}
|
||||
return positions;
|
||||
};
|
||||
|
||||
for (const tx of Object.values(txdatas)) {
|
||||
// collecting which of our address `cells` this transaction touches:
|
||||
const externalCells = new Set<number>();
|
||||
const internalCells = new Set<number>();
|
||||
const paymentCodeCells = new Map<string, { pc: string; c: number }>();
|
||||
|
||||
const matchAddress = (address: string, isVout: boolean) => {
|
||||
const externalIndex = externalIndexByAddress.get(address);
|
||||
if (externalIndex !== undefined) externalCells.add(externalIndex);
|
||||
const internalIndex = internalIndexByAddress.get(address);
|
||||
if (internalIndex !== undefined) internalCells.add(internalIndex);
|
||||
if (isVout) {
|
||||
// since we are iterating PCs who can pay us, we can completely ignore `tx.vin` and only check `tx.vout`
|
||||
const paymentCodeIndex = paymentCodeIndexByAddress.get(address);
|
||||
if (paymentCodeIndex) paymentCodeCells.set(address, paymentCodeIndex);
|
||||
}
|
||||
};
|
||||
|
||||
for (const vin of tx.vin) {
|
||||
for (const address of vin.addresses ?? []) matchAddress(address, false);
|
||||
}
|
||||
for (const vout of tx.vout) {
|
||||
for (const address of vout.scriptPubKey.addresses ?? []) matchAddress(address, true);
|
||||
}
|
||||
|
||||
if (externalCells.size === 0 && internalCells.size === 0 && paymentCodeCells.size === 0) continue;
|
||||
|
||||
// this TX is related to our address(es)
|
||||
const upsertClone = (cell: Transaction[]) => {
|
||||
const { vin: txVin, vout: txVout, ...txRest } = tx;
|
||||
const clonedTx = {
|
||||
...txRest,
|
||||
inputs: txVin.slice(0),
|
||||
outputs: txVout.slice(0),
|
||||
timestamp: tx.blocktime || tx.time || Math.floor(+new Date() / 1000) - 30 /* unconfirmed */,
|
||||
};
|
||||
|
||||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
||||
const positions = getCellPositions(cell);
|
||||
const existingPosition = positions.get(clonedTx.txid);
|
||||
if (existingPosition !== undefined) {
|
||||
cell[existingPosition] = clonedTx;
|
||||
} else {
|
||||
positions.set(clonedTx.txid, cell.length);
|
||||
cell.push(clonedTx);
|
||||
}
|
||||
};
|
||||
|
||||
for (const c of externalCells) {
|
||||
this._txs_by_external_index[c] = this._txs_by_external_index[c] || [];
|
||||
upsertClone(this._txs_by_external_index[c]);
|
||||
}
|
||||
for (const c of internalCells) {
|
||||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || [];
|
||||
upsertClone(this._txs_by_internal_index[c]);
|
||||
}
|
||||
for (const { pc, c } of paymentCodeCells.values()) {
|
||||
this._txs_by_payment_code_index[pc] = this._txs_by_payment_code_index[pc] || {};
|
||||
this._txs_by_payment_code_index[pc][c] = this._txs_by_payment_code_index[pc][c] || [];
|
||||
upsertClone(this._txs_by_payment_code_index[pc][c]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
47
package-lock.json
generated
47
package-lock.json
generated
@ -115,7 +115,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@jest/reporters": "^27.5.1",
|
||||
"@react-native/eslint-config": "^0.85.3",
|
||||
@ -584,7 +583,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
|
||||
"version": "7.28.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
@ -599,7 +597,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -613,7 +610,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -629,7 +625,6 @@
|
||||
"version": "7.29.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.3.tgz",
|
||||
"integrity": "sha512-SRS46DFR4HqzUzCVgi90/xMoL+zeBDBvWdKYXSEzh79kXswNFEglUpMKxR04//dPqwYXWUBJ3mpUd933ru9Kmg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
@ -644,7 +639,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
@ -660,7 +654,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
@ -688,7 +681,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-proposal-private-property-in-object": {
|
||||
"version": "7.21.0-placeholder-for-preset-env.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@ -782,7 +774,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-import-assertions": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
@ -796,7 +787,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-import-attributes": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
@ -950,7 +940,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-unicode-sets-regex": {
|
||||
"version": "7.18.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
|
||||
@ -1008,7 +997,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-block-scoped-functions": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1049,7 +1037,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-class-static-block": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.28.6",
|
||||
@ -1082,7 +1069,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-computed-properties": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
@ -1111,7 +1097,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-dotall-regex": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||
@ -1126,7 +1111,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-duplicate-keys": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1140,7 +1124,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": {
|
||||
"version": "7.29.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||
@ -1155,7 +1138,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-dynamic-import": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1169,7 +1151,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-explicit-resource-management": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
@ -1184,7 +1165,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-exponentiation-operator": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
@ -1198,7 +1178,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-export-namespace-from": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1240,7 +1219,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-function-name": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-compilation-targets": "^7.27.1",
|
||||
@ -1256,7 +1234,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-json-strings": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
@ -1270,7 +1247,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-literals": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1284,7 +1260,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-logical-assignment-operators": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
@ -1298,7 +1273,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-member-expression-literals": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1312,7 +1286,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-modules-amd": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-transforms": "^7.27.1",
|
||||
@ -1343,7 +1316,6 @@
|
||||
"version": "7.29.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz",
|
||||
"integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-transforms": "^7.28.6",
|
||||
@ -1360,7 +1332,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-modules-umd": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-transforms": "^7.27.1",
|
||||
@ -1389,7 +1360,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-new-target": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1416,7 +1386,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-numeric-separator": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
@ -1430,7 +1399,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-object-rest-spread": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-compilation-targets": "^7.28.6",
|
||||
@ -1448,7 +1416,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-object-super": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
@ -1490,7 +1457,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-parameters": {
|
||||
"version": "7.27.7",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1533,7 +1499,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-property-literals": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1616,7 +1581,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-regexp-modifiers": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||
@ -1631,7 +1595,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-reserved-words": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1676,7 +1639,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-spread": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
@ -1691,7 +1653,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-sticky-regex": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1718,7 +1679,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-typeof-symbol": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1749,7 +1709,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-unicode-escapes": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1763,7 +1722,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-unicode-property-regex": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||
@ -1792,7 +1750,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-unicode-sets-regex": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||
@ -1809,7 +1766,6 @@
|
||||
"version": "7.29.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.5.tgz",
|
||||
"integrity": "sha512-/69t2aEzGKHD76DyLbHysF/QH2LJOB8iFnYO37unDTKBTubzcMRv0f3H5EiN1Q6ajOd/eB7dAInF0qdFVS06kA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.29.3",
|
||||
@ -1895,7 +1851,6 @@
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz",
|
||||
"integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-define-polyfill-provider": "^0.6.8",
|
||||
@ -1907,7 +1862,6 @@
|
||||
},
|
||||
"node_modules/@babel/preset-modules": {
|
||||
"version": "0.1.6-no-external-plugins",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.0.0",
|
||||
@ -9833,7 +9787,6 @@
|
||||
},
|
||||
"node_modules/esutils": {
|
||||
"version": "2.0.3",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
||||
@ -16,7 +16,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@jest/reporters": "^27.5.1",
|
||||
"@react-native/eslint-config": "^0.85.3",
|
||||
|
||||
115
tests/unit/hd-fetch-transactions-mapping.test.ts
Normal file
115
tests/unit/hd-fetch-transactions-mapping.test.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import assert from 'assert';
|
||||
|
||||
import { HDSegwitBech32Wallet } from '../../class/wallets/hd-segwit-bech32-wallet';
|
||||
|
||||
const mockState: { histories: Record<string, any>; txdata: Record<string, any> } = { histories: {}, txdata: {} };
|
||||
|
||||
jest.mock('../../blue_modules/BlueElectrum', () => {
|
||||
return {
|
||||
multiGetHistoryByAddress: jest.fn(async (addresses: string[]) => {
|
||||
const ret: Record<string, any> = {};
|
||||
for (const addr of addresses) {
|
||||
if (mockState.histories[addr]) ret[addr] = mockState.histories[addr];
|
||||
}
|
||||
return ret;
|
||||
}),
|
||||
multiGetTransactionByTxid: jest.fn(async (txids: string[]) => {
|
||||
const ret: Record<string, any> = {};
|
||||
for (const txid of txids) {
|
||||
if (mockState.txdata[txid]) ret[txid] = JSON.parse(JSON.stringify(mockState.txdata[txid]));
|
||||
}
|
||||
return ret;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('AbstractHDElectrumWallet.fetchTransactions() tx-to-cell mapping', () => {
|
||||
it('puts transactions into correct external & internal cells', async () => {
|
||||
const hd = new HDSegwitBech32Wallet();
|
||||
hd.setSecret('abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about');
|
||||
assert.ok(hd.validateMnemonic());
|
||||
|
||||
const e0 = hd._getExternalAddressByIndex(0);
|
||||
const e1 = hd._getExternalAddressByIndex(1);
|
||||
const i0 = hd._getInternalAddressByIndex(0);
|
||||
|
||||
// fetchTransactions assumes fetchBalance already populated balance cells
|
||||
for (let c = 0; c < hd.next_free_address_index + hd.gap_limit + 1; c++) {
|
||||
// @ts-ignore accessing internals for test setup
|
||||
hd._balances_by_external_index[c] = { c: 0, u: 0 };
|
||||
// @ts-ignore accessing internals for test setup
|
||||
hd._balances_by_internal_index[c] = { c: 0, u: 0 };
|
||||
}
|
||||
|
||||
// tx1: receive 100000 sats to e0 (funded by a foreign tx we don't have data for)
|
||||
// tx2: spend from e0, sending to a foreign address with change to i0 and a small amount to e1
|
||||
mockState.histories = {
|
||||
[e0]: [
|
||||
{ tx_hash: 'tx1', height: 100 },
|
||||
{ tx_hash: 'tx2', height: 101 },
|
||||
],
|
||||
[e1]: [{ tx_hash: 'tx2', height: 101 }],
|
||||
[i0]: [{ tx_hash: 'tx2', height: 101 }],
|
||||
};
|
||||
mockState.txdata = {
|
||||
tx1: {
|
||||
txid: 'tx1',
|
||||
confirmations: 10,
|
||||
blocktime: 1700000000,
|
||||
vin: [{ txid: 'aa'.repeat(32), vout: 0 }],
|
||||
vout: [{ value: 0.001, n: 0, scriptPubKey: { addresses: [e0] } }],
|
||||
},
|
||||
tx2: {
|
||||
txid: 'tx2',
|
||||
confirmations: 9,
|
||||
blocktime: 1700000100,
|
||||
vin: [{ txid: 'tx1', vout: 0 }],
|
||||
vout: [
|
||||
{ value: 0.0005, n: 0, scriptPubKey: { addresses: ['bc1qforeignforeignforeignforeignforeign00'] } },
|
||||
{ value: 0.0003, n: 1, scriptPubKey: { addresses: [i0] } },
|
||||
{ value: 0.0001, n: 2, scriptPubKey: { addresses: [e1] } },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
await hd.fetchTransactions();
|
||||
|
||||
// external cell 0: tx1 (vout match) and tx2 (vin spends e0's utxo), exactly one entry each
|
||||
const ext0 = hd._txs_by_external_index[0];
|
||||
assert.deepStrictEqual(ext0.map(tx => tx.txid).sort(), ['tx1', 'tx2']);
|
||||
|
||||
// external cell 1: only tx2 (vout match)
|
||||
const ext1 = hd._txs_by_external_index[1];
|
||||
assert.deepStrictEqual(
|
||||
ext1.map(tx => tx.txid),
|
||||
['tx2'],
|
||||
);
|
||||
|
||||
// internal cell 0: only tx2 (change output)
|
||||
const int0 = hd._txs_by_internal_index[0];
|
||||
assert.deepStrictEqual(
|
||||
int0.map(tx => tx.txid),
|
||||
['tx2'],
|
||||
);
|
||||
|
||||
// untouched cells stay empty
|
||||
assert.strictEqual((hd._txs_by_external_index[2] || []).length, 0);
|
||||
assert.strictEqual((hd._txs_by_internal_index[1] || []).length, 0);
|
||||
|
||||
// clones must carry inputs/outputs/timestamp, with vin enriched from the prev tx (tx1 vout0 -> e0)
|
||||
const tx2clone = ext0.find(tx => tx.txid === 'tx2') as any;
|
||||
assert.ok(Array.isArray(tx2clone.inputs) && Array.isArray(tx2clone.outputs));
|
||||
assert.strictEqual(tx2clone.timestamp, 1700000100);
|
||||
assert.deepStrictEqual(tx2clone.inputs[0].addresses, [e0]);
|
||||
assert.strictEqual(tx2clone.inputs[0].value, 0.001);
|
||||
|
||||
// re-fetching must not duplicate entries (upsert by txid)
|
||||
await hd.fetchTransactions();
|
||||
assert.deepStrictEqual(hd._txs_by_external_index[0].map(tx => tx.txid).sort(), ['tx1', 'tx2']);
|
||||
assert.strictEqual(hd._txs_by_internal_index[0].length, 1);
|
||||
|
||||
// getTransactions() must dedupe across cells: tx2 lives in 3 cells but is one tx
|
||||
const all = hd.getTransactions();
|
||||
assert.deepStrictEqual(all.map(tx => tx.txid).sort(), ['tx1', 'tx2']);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user