@@ -2008,15 +2008,35 @@ export const markWalletTxHistoryAsVoided = async (
2008
2008
2009
2009
* @param mysql - Database connection
2010
2010
* @param addresses - The list of addresses to rebuild
2011
+ * @param txList - The list of affected transactions, to rebuild the transaction count
2011
2012
*/
2012
2013
export const rebuildAddressBalancesFromUtxos = async (
2013
2014
mysql : ServerlessMysql ,
2014
2015
addresses : string [ ] ,
2016
+ txList : string [ ] ,
2015
2017
) : 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
+
2016
2031
// delete affected address_balances
2017
2032
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
2020
2040
WHERE \`address\` IN (?)` ,
2021
2041
[ addresses ] ,
2022
2042
) ;
@@ -2033,20 +2053,23 @@ export const rebuildAddressBalancesFromUtxos = async (
2033
2053
\`timelock_expires\`,
2034
2054
\`transactions\`
2035
2055
)
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)
2050
2073
` , [ addresses ] ) ;
2051
2074
2052
2075
// update address balances with locked utxos
@@ -2066,7 +2089,7 @@ export const rebuildAddressBalancesFromUtxos = async (
2066
2089
SUM(\`value\`) AS locked_balance,
2067
2090
BIT_OR(\`authorities\`) AS locked_authorities,
2068
2091
MIN(\`timelock\`) AS timelock_expires,
2069
- COUNT(DISTINCT \`tx_id\`) -- transactions
2092
+ 0 -- transactions
2070
2093
FROM \`tx_output\`
2071
2094
WHERE spent_by IS NULL
2072
2095
AND voided = FALSE
@@ -2076,9 +2099,31 @@ export const rebuildAddressBalancesFromUtxos = async (
2076
2099
ON DUPLICATE KEY UPDATE
2077
2100
locked_balance = VALUES(locked_balance),
2078
2101
locked_authorities = VALUES(locked_authorities),
2079
- timelock_expires = VALUES(timelock_expires),
2080
- transactions = transactions + VALUES(\`transactions\`)
2102
+ timelock_expires = VALUES(timelock_expires)
2081
2103
` , [ 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
+ }
2082
2127
} ;
2083
2128
2084
2129
/**
@@ -2461,3 +2506,37 @@ export const getAvailableAuthorities = async (
2461
2506
2462
2507
return utxos ;
2463
2508
} ;
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
+ } ;
0 commit comments