Merge pull request #8639 from BlueWallet/perf-hd-fetch-transactions

REF: single-pass tx-to-address mapping in HD fetchTransactions
This commit is contained in:
GLaDOS 2026-06-11 14:37:51 +01:00 committed by GitHub
commit 770a7a4075
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 81 additions and 171 deletions

View File

@ -424,137 +424,95 @@ 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
// address -> index lookup maps; the single pass over transactions below uses them
// to find which cells a transaction belongs to
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, used to replace-or-push a transaction into a cell in constant time
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
View File

@ -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"

View File

@ -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",