Skip to content

Commit c3e558c

Browse files
Merge pull request #272 from HathorNetwork/dev
Release v1.16.0-alpha
2 parents 9ba7f08 + 63c9465 commit c3e558c

11 files changed

+358
-97
lines changed

package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hathor-wallet-service",
3-
"version": "1.15.2-alpha",
3+
"version": "1.16.0-alpha",
44
"description": "",
55
"scripts": {
66
"postinstall": "npm dedupe",

serverless.yml

+1
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ functions:
132132
enabled: false
133133
onNewTxRequest:
134134
handler: src/txProcessor.onNewTxRequest
135+
timeout: 12 # seconds
135136
warmup:
136137
walletWarmer:
137138
enabled: false

src/commons.ts

+29-9
Original file line numberDiff line numberDiff line change
@@ -429,15 +429,27 @@ export const handleVoided = async (mysql: ServerlessMysql, logger: Logger, tx: T
429429
affectedUtxoList = [...affectedUtxoList, ...newAffectedUtxoList];
430430
}
431431

432-
// fetch all addresses affected by the voided tx
433-
const affectedAddresses = affectedUtxoList.reduce((acc: Set<string>, utxo: DbTxOutput) => acc.add(utxo.address), new Set<string>());
432+
// fetch all addresses and transactions affected by the voided transaction
433+
const [affectedAddresses, affectedTxIds] = affectedUtxoList.reduce(
434+
(acc: [Set<string>, Set<string>], utxo: DbTxOutput) => {
435+
acc[0].add(utxo.address);
436+
acc[1].add(utxo.txId);
437+
438+
return acc;
439+
},
440+
[new Set<string>(), new Set<string>()],
441+
);
434442

435443
if (affectedAddresses.size > 0) {
436444
const addresses = [...affectedAddresses];
445+
437446
logger.debug(`Rebuilding balances for ${addresses.length} addresses.`, {
438447
addresses,
439448
});
440-
await rebuildAddressBalancesFromUtxos(mysql, addresses);
449+
logger.debug(`Rebuilding tx count from ${affectedTxIds.size} transactions`, {
450+
affectedTxIds,
451+
});
452+
await rebuildAddressBalancesFromUtxos(mysql, addresses, [...affectedTxIds]);
441453
await validateAddressBalances(mysql, addresses);
442454
}
443455

@@ -448,9 +460,6 @@ export const validateAddressBalances = async (mysql: ServerlessMysql, addresses:
448460
const addressBalances: AddressBalance[] = await fetchAddressBalance(mysql, addresses);
449461
const addressTxHistorySums: AddressTotalBalance[] = await fetchAddressTxHistorySum(mysql, addresses);
450462

451-
// if we have the full history of transactions, the number of rows must match:
452-
assert.strictEqual(addressBalances.length, addressTxHistorySums.length);
453-
454463
for (let i = 0; i < addressTxHistorySums.length; i++) {
455464
const addressBalance: AddressBalance = addressBalances[i];
456465
const addressTxHistorySum: AddressTotalBalance = addressTxHistorySums[i];
@@ -520,15 +529,26 @@ export const handleReorg = async (mysql: ServerlessMysql, logger: Logger): Promi
520529
await removeTxsHeight(mysql, remainingTxs);
521530
}
522531

523-
// fetch all addresses affected by the reorg
524-
const affectedAddresses = affectedUtxoList.reduce((acc: Set<string>, utxo: DbTxOutput) => acc.add(utxo.address), new Set<string>());
532+
// fetch all addresses and transactions affected by the reorg
533+
const [affectedAddresses, affectedTxIds] = affectedUtxoList.reduce(
534+
(acc: [Set<string>, Set<string>], utxo: DbTxOutput) => {
535+
acc[0].add(utxo.address);
536+
acc[1].add(utxo.txId);
537+
538+
return acc;
539+
},
540+
[new Set<string>(), new Set<string>()],
541+
);
525542

526543
if (affectedAddresses.size > 0) {
527544
const addresses = [...affectedAddresses];
528545
logger.debug(`Rebuilding balances for ${addresses.length} addresses.`, {
529546
addresses,
530547
});
531-
await rebuildAddressBalancesFromUtxos(mysql, addresses);
548+
logger.debug(`Rebuilding tx count from ${affectedTxIds.size} transactions`, {
549+
affectedTxIds,
550+
});
551+
await rebuildAddressBalancesFromUtxos(mysql, addresses, [...affectedTxIds]);
532552
await validateAddressBalances(mysql, addresses);
533553
}
534554

src/db/index.ts

+98-19
Original file line numberDiff line numberDiff line change
@@ -2008,15 +2008,35 @@ export const markWalletTxHistoryAsVoided = async (
20082008
20092009
* @param mysql - Database connection
20102010
* @param addresses - The list of addresses to rebuild
2011+
* @param txList - The list of affected transactions, to rebuild the transaction count
20112012
*/
20122013
export const rebuildAddressBalancesFromUtxos = async (
20132014
mysql: ServerlessMysql,
20142015
addresses: string[],
2016+
txList: string[],
20152017
): Promise<void> => {
2018+
if (txList.length === 0) {
2019+
// This should never happen, we should throw so the re-org is rolled back
2020+
// and an error is triggered for manual inspection
2021+
throw new Error('Attempted to rebuild address balances but no transactions were affected');
2022+
}
2023+
// first we need to store the transactions count before deleting
2024+
const oldAddressTokenTransactions: DbSelectResult = await mysql.query(
2025+
`SELECT \`address\`, \`token_id\` AS tokenId, \`transactions\`
2026+
FROM \`address_balance\`
2027+
WHERE \`address\` IN (?)`,
2028+
[addresses],
2029+
);
2030+
20162031
// delete affected address_balances
20172032
await mysql.query(
2018-
`DELETE
2019-
FROM \`address_balance\`
2033+
`UPDATE \`address_balance\`
2034+
SET \`unlocked_balance\` = 0,
2035+
\`locked_balance\` = 0,
2036+
\`locked_authorities\` = 0,
2037+
\`unlocked_authorities\` = 0,
2038+
\`timelock_expires\` = NULL,
2039+
\`transactions\` = 0
20202040
WHERE \`address\` IN (?)`,
20212041
[addresses],
20222042
);
@@ -2033,20 +2053,23 @@ export const rebuildAddressBalancesFromUtxos = async (
20332053
\`timelock_expires\`,
20342054
\`transactions\`
20352055
)
2036-
SELECT address,
2037-
token_id,
2038-
SUM(\`value\`), -- unlocked_balance
2039-
0,
2040-
BIT_OR(\`authorities\`), -- unlocked_authorities
2041-
0, -- locked_authorities
2042-
0, -- timelock_expires
2043-
COUNT(DISTINCT \`tx_id\`) -- transactions
2044-
FROM \`tx_output\`
2045-
WHERE spent_by IS NULL
2046-
AND voided = FALSE
2047-
AND locked = FALSE
2048-
AND address IN (?)
2049-
GROUP BY address, token_id
2056+
SELECT address,
2057+
token_id,
2058+
SUM(\`value\`), -- unlocked_balance
2059+
0,
2060+
BIT_OR(\`authorities\`), -- unlocked_authorities
2061+
0, -- locked_authorities
2062+
NULL, -- timelock_expires
2063+
0 -- transactions
2064+
FROM \`tx_output\`
2065+
WHERE spent_by IS NULL
2066+
AND voided = FALSE
2067+
AND locked = FALSE
2068+
AND address IN (?)
2069+
GROUP BY address, token_id
2070+
ON DUPLICATE KEY UPDATE
2071+
unlocked_balance = VALUES(unlocked_balance),
2072+
unlocked_authorities = VALUES(unlocked_authorities)
20502073
`, [addresses]);
20512074

20522075
// update address balances with locked utxos
@@ -2066,7 +2089,7 @@ export const rebuildAddressBalancesFromUtxos = async (
20662089
SUM(\`value\`) AS locked_balance,
20672090
BIT_OR(\`authorities\`) AS locked_authorities,
20682091
MIN(\`timelock\`) AS timelock_expires,
2069-
COUNT(DISTINCT \`tx_id\`) -- transactions
2092+
0 -- transactions
20702093
FROM \`tx_output\`
20712094
WHERE spent_by IS NULL
20722095
AND voided = FALSE
@@ -2076,9 +2099,31 @@ export const rebuildAddressBalancesFromUtxos = async (
20762099
ON DUPLICATE KEY UPDATE
20772100
locked_balance = VALUES(locked_balance),
20782101
locked_authorities = VALUES(locked_authorities),
2079-
timelock_expires = VALUES(timelock_expires),
2080-
transactions = transactions + VALUES(\`transactions\`)
2102+
timelock_expires = VALUES(timelock_expires)
20812103
`, [addresses]);
2104+
2105+
const addressTransactionCount: StringMap<number> = await getAffectedAddressTxCountFromTxList(mysql, txList);
2106+
const finalTxCount = oldAddressTokenTransactions.map(({ address, tokenId, transactions }) => {
2107+
const diff = addressTransactionCount[`${address}_${tokenId}`] || 0;
2108+
2109+
return [address, tokenId, transactions as number - diff];
2110+
});
2111+
2112+
// update address balances with the correct amount of transactions
2113+
// We have to run multiple updates because we don't want to insert new rows to the table (which would be done
2114+
// if we used the INSERT ... ON CONFLICT syntax)
2115+
for (const addressTokenTx of finalTxCount) {
2116+
await mysql.query(`
2117+
UPDATE \`address_balance\`
2118+
SET \`transactions\` = ?
2119+
WHERE \`address\` = ?
2120+
AND \`token_id\` = ?
2121+
`, [
2122+
addressTokenTx[2],
2123+
addressTokenTx[0],
2124+
addressTokenTx[1],
2125+
]);
2126+
}
20822127
};
20832128

20842129
/**
@@ -2461,3 +2506,37 @@ export const getAvailableAuthorities = async (
24612506

24622507
return utxos;
24632508
};
2509+
2510+
/**
2511+
* Get the number of transactions for each token from the address_tx_history table
2512+
* given a list of transactions
2513+
*
2514+
* @param mysql - Database connection
2515+
* @param txList - A list of affected transactions to get the addresses token transaction count
2516+
2517+
* @returns A Map with address_tokenId as key and the transaction count as values
2518+
*/
2519+
export const getAffectedAddressTxCountFromTxList = async (
2520+
mysql: ServerlessMysql,
2521+
txList: string[],
2522+
): Promise<StringMap<number>> => {
2523+
const results: DbSelectResult = await mysql.query(`
2524+
SELECT address, COUNT(DISTINCT(tx_id)) AS txCount, token_id as tokenId
2525+
FROM address_tx_history
2526+
WHERE tx_id IN (?)
2527+
AND voided = TRUE
2528+
GROUP BY address, token_id
2529+
`, [txList]);
2530+
2531+
const addressTransactions = results.reduce((acc, result) => {
2532+
const address = result.address as string;
2533+
const txCount = result.txCount as number;
2534+
const tokenId = result.tokenId as string;
2535+
2536+
acc[`${address}_${tokenId}`] = txCount;
2537+
2538+
return acc;
2539+
}, {});
2540+
2541+
return addressTransactions as StringMap<number>;
2542+
};

tests/api.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1265,9 +1265,9 @@ test('GET /wallet/tokens/token_id/details', async () => {
12651265
]);
12661266

12671267
await addToAddressTxHistoryTable(mysql, [
1268-
[ADDRESSES[0], 'txId', token1.id, 100, 0],
1269-
[ADDRESSES[0], 'txId2', token2.id, 250, 0],
1270-
[ADDRESSES[0], 'txId3', token2.id, 0, 0],
1268+
{ address: ADDRESSES[0], txId: 'txId', tokenId: token1.id, balance: 100, timestamp: 0 },
1269+
{ address: ADDRESSES[0], txId: 'txId2', tokenId: token2.id, balance: 250, timestamp: 0 },
1270+
{ address: ADDRESSES[0], txId: 'txId3', tokenId: token2.id, balance: 0, timestamp: 0 },
12711271
]);
12721272

12731273
event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: token1.id });

0 commit comments

Comments
 (0)