Skip to content

Commit b5fc86d

Browse files
feat: transactions count on token table (#273)
* chore: empty commit (that will be rebased) * feat: added transactions column to tokens table and tests to the method * feat: adding hathor token to tokens table * docs: added docs to getTokenListFromInputsAndOutputs * refactor: using inner join instead of left outer join * refactor: changed method name * feat: added index on voided on address_tx_history * feat: handling token transactions on reorg * fix: wrong type * tests: testing for hathor increment * feat: add voided address_tx_history index to models * refactor: improved checkTokenTable
1 parent 8aafe4c commit b5fc86d

13 files changed

+301
-15
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
module.exports = {
4+
up: async (queryInterface, Sequelize) => {
5+
await queryInterface.addColumn('token', 'transactions', {
6+
type: Sequelize.INTEGER.UNSIGNED,
7+
allowNull: false,
8+
defaultValue: 0,
9+
});
10+
},
11+
12+
down: async (queryInterface) => {
13+
await queryInterface.removeColumn('token', 'transactions');
14+
},
15+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use strict';
2+
3+
module.exports = {
4+
up: async (queryInterface, Sequelize) => {
5+
await queryInterface.bulkInsert('token', [{
6+
id: '00',
7+
name: 'Hathor',
8+
symbol: 'HTR',
9+
transactions: 0,
10+
}]);
11+
},
12+
13+
down: async (queryInterface) => {
14+
await queryInterface.bulkDelete('token', [{
15+
id: '00',
16+
name: 'Hathor',
17+
symbol: 'HTR',
18+
}]);
19+
},
20+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
module.exports = {
4+
up: async (queryInterface) => queryInterface.addIndex(
5+
'address_tx_history',
6+
['voided'], {
7+
name: 'address_tx_history_voided_idx',
8+
fields: 'voided',
9+
},
10+
),
11+
down: async (queryInterface) => queryInterface.removeIndex(
12+
'address_tx_history',
13+
'address_tx_history_voided_idx',
14+
),
15+
};

db/models/addresstxhistory.js

+3
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ module.exports = (sequelize, DataTypes) => {
5656
}, {
5757
name: 'address_tx_history_timestamp_idx',
5858
fields: ['timestamp'],
59+
}, {
60+
name: 'address_tx_history_voided_idx',
61+
fields: ['voided'],
5962
}],
6063
});
6164
return AddressTxHistory;

db/models/token.js

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ module.exports = (sequelize, DataTypes) => {
4040
allowNull: false,
4141
defaultValue: DataTypes.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
4242
},
43+
transactions: {
44+
type: DataTypes.INTEGER.UNSIGNED,
45+
allowNull: false,
46+
},
4347
}, {
4448
sequelize,
4549
modelName: 'Token',

src/commons.ts

+21
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,27 @@ export const getAddressBalanceMap = (
191191
return addressBalanceMap;
192192
};
193193

194+
/**
195+
* Gets a list of tokens from a list of inputs and outputs
196+
*
197+
* @param inputs - The transaction inputs
198+
* @param outputs - The transaction outputs
199+
* @returns A list of tokens present in the inputs and outputs
200+
*/
201+
export const getTokenListFromInputsAndOutputs = (inputs: TxInput[], outputs: TxOutputWithIndex[]): string[] => {
202+
const tokenIds = new Set<string>([]);
203+
204+
for (const input of inputs) {
205+
tokenIds.add(input.token);
206+
}
207+
208+
for (const output of outputs) {
209+
tokenIds.add(output.token);
210+
}
211+
212+
return [...tokenIds];
213+
};
214+
194215
/**
195216
* Get the map of token balances for each wallet.
196217
*

src/db/index.ts

+78-5
Original file line numberDiff line numberDiff line change
@@ -1255,8 +1255,20 @@ export const getWalletBalances = async (mysql: ServerlessMysql, walletId: string
12551255
params.push(tokenIds);
12561256
}
12571257

1258-
// use LEFT JOIN as HTR token ('00') won't be on the token table, so INNER JOIN would never match it
1259-
const query = `SELECT * FROM (${subquery}) w LEFT JOIN token ON w.token_id = token.id;`;
1258+
const query = `
1259+
SELECT NULL AS total_received,
1260+
w.unlocked_balance AS unlocked_balance,
1261+
w.locked_balance AS locked_balance,
1262+
w.unlocked_authorities AS unlocked_authorities,
1263+
w.locked_authorities AS locked_authorities,
1264+
w.timelock_expires AS timelock_expires,
1265+
w.transactions AS transactions,
1266+
w.token_id AS token_id,
1267+
token.name AS name,
1268+
token.symbol AS symbol
1269+
FROM (${subquery}) w
1270+
INNER JOIN token ON w.token_id = token.id
1271+
`;
12601272

12611273
const results: DbSelectResult = await mysql.query(query, params);
12621274
for (const result of results) {
@@ -2109,6 +2121,7 @@ export const rebuildAddressBalancesFromUtxos = async (
21092121

21102122
const addressTransactionCount: StringMap<number> = await getAffectedAddressTxCountFromTxList(mysql, txList);
21112123
const addressTotalReceived: StringMap<number> = await getAffectedAddressTotalReceivedFromTxList(mysql, txList);
2124+
const tokenTransactionCount: StringMap<number> = await getAffectedTokenTxCountFromTxList(mysql, txList);
21122125

21132126
const finalValues = oldAddressTokenTransactions.map(({ address, tokenId, transactions, totalReceived }) => {
21142127
const diffTransactions = addressTransactionCount[`${address}_${tokenId}`] || 0;
@@ -2129,6 +2142,15 @@ export const rebuildAddressBalancesFromUtxos = async (
21292142
AND \`token_id\` = ?
21302143
`, item);
21312144
}
2145+
2146+
// update token table with the correct amount of transactions
2147+
for (const token of Object.keys(tokenTransactionCount)) {
2148+
await mysql.query(`
2149+
UPDATE \`token\`
2150+
SET \`transactions\` = \`transactions\` - ?
2151+
WHERE \`id\` = ?
2152+
`, [tokenTransactionCount[token], token]);
2153+
}
21322154
};
21332155

21342156
/**
@@ -2546,6 +2568,39 @@ export const getAffectedAddressTxCountFromTxList = async (
25462568
return addressTransactions as StringMap<number>;
25472569
};
25482570

2571+
/**
2572+
* Get the number of affected transactions for each token from the address_tx_history table
2573+
* given a list of transactions
2574+
*
2575+
* @param mysql - Database connection
2576+
* @param txList - A list of affected transactions to get the token tx count
2577+
2578+
* @returns A Map with tokenId as key and the transaction count as values
2579+
*/
2580+
export const getAffectedTokenTxCountFromTxList = async (
2581+
mysql: ServerlessMysql,
2582+
txList: string[],
2583+
): Promise<StringMap<number>> => {
2584+
const results: DbSelectResult = await mysql.query(`
2585+
SELECT token_id AS tokenId, COUNT(DISTINCT(tx_id)) AS txCount
2586+
FROM address_tx_history
2587+
WHERE tx_id IN (?)
2588+
AND voided = TRUE
2589+
GROUP BY token_id
2590+
`, [txList]);
2591+
2592+
const tokenTransactions = results.reduce((acc, result) => {
2593+
const tokenId = result.tokenId as string;
2594+
const txCount = result.txCount as number;
2595+
2596+
acc[tokenId] = txCount;
2597+
2598+
return acc;
2599+
}, {});
2600+
2601+
return tokenTransactions as StringMap<number>;
2602+
};
2603+
25492604
/**
25502605
* Get the affected total_received for each address/token pair given a list of transactions
25512606
*
@@ -2560,9 +2615,10 @@ export const getAffectedAddressTotalReceivedFromTxList = async (
25602615
): Promise<StringMap<number>> => {
25612616
const results: DbSelectResult = await mysql.query(`
25622617
SELECT address, token_id as tokenId, SUM(value) as total
2563-
FROM tx_output
2564-
WHERE tx_id IN (?) AND voided = TRUE
2565-
GROUP BY address, token_id
2618+
FROM tx_output
2619+
WHERE tx_id IN (?)
2620+
AND voided = TRUE
2621+
GROUP BY address, token_id
25662622
`, [txList]);
25672623

25682624
const addressTotalReceivedMap = results.reduce((acc, result) => {
@@ -2577,3 +2633,20 @@ export const getAffectedAddressTotalReceivedFromTxList = async (
25772633

25782634
return addressTotalReceivedMap as StringMap<number>;
25792635
};
2636+
2637+
/**
2638+
* Increment a list of tokens transactions count
2639+
*
2640+
* @param mysql - Database connection
2641+
* @param tokenList - The list of tokens to increment
2642+
*/
2643+
export const incrementTokensTxCount = async (
2644+
mysql: ServerlessMysql,
2645+
tokenList: string[],
2646+
): Promise<void> => {
2647+
await mysql.query(`
2648+
UPDATE \`token\`
2649+
SET \`transactions\` = \`transactions\` + 1
2650+
WHERE \`id\` IN (?)
2651+
`, [tokenList]);
2652+
};

src/txProcessor.ts

+7
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
unlockUtxos,
1717
unlockTimelockedUtxos,
1818
searchForLatestValidBlock,
19+
getTokenListFromInputsAndOutputs,
1920
handleReorg,
2021
handleVoided,
2122
prepareOutputs,
@@ -34,6 +35,7 @@ import {
3435
storeTokenInformation,
3536
updateAddressTablesWithTx,
3637
updateWalletTablesWithTx,
38+
incrementTokensTxCount,
3739
fetchTx,
3840
addMiner,
3941
} from '@src/db';
@@ -377,6 +379,11 @@ const _unsafeAddNewTx = async (_logger: Logger, tx: Transaction, now: number, bl
377379
addressBalanceMap,
378380
});
379381

382+
const tokenList: string[] = getTokenListFromInputsAndOutputs(tx.inputs, outputs);
383+
384+
// Update transaction count with the new tx
385+
await incrementTokensTxCount(mysql, tokenList);
386+
380387
// update address tables (address, address_balance, address_tx_history)
381388
await updateAddressTablesWithTx(mysql, txId, tx.timestamp, addressBalanceMap);
382389

src/types.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,13 @@ export class TokenInfo {
102102

103103
symbol: string;
104104

105-
constructor(id: string, name: string, symbol: string) {
105+
transactions: number;
106+
107+
constructor(id: string, name: string, symbol: string, transactions?: number) {
106108
this.id = id;
107109
this.name = name;
108110
this.symbol = symbol;
111+
this.transactions = transactions || 0;
109112

110113
const hathorConfig = hathorLib.constants.HATHOR_TOKEN_CONFIG;
111114

tests/api.test.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -261,16 +261,19 @@ test('GET /balances', async () => {
261261
readyAt: 10001,
262262
}]);
263263

264+
// add the hathor token as it will be deleted by the beforeAll
265+
const htrToken = { id: '00', name: 'Hathor', symbol: 'HTR' };
264266
// add tokens
265267
const token1 = { id: 'token1', name: 'MyToken1', symbol: 'MT1' };
266268
const token2 = { id: 'token2', name: 'MyToken2', symbol: 'MT2' };
267269
const token3 = { id: 'token3', name: 'MyToken3', symbol: 'MT3' };
268270
const token4 = { id: 'token4', name: 'MyToken4', symbol: 'MT4' };
269271
await addToTokenTable(mysql, [
270-
{ id: token1.id, name: token1.name, symbol: token1.symbol },
271-
{ id: token2.id, name: token2.name, symbol: token2.symbol },
272-
{ id: token3.id, name: token3.name, symbol: token3.symbol },
273-
{ id: token4.id, name: token4.name, symbol: token4.symbol },
272+
{ ...htrToken, transactions: 0 },
273+
{ id: token1.id, name: token1.name, symbol: token1.symbol, transactions: 0 },
274+
{ id: token2.id, name: token2.name, symbol: token2.symbol, transactions: 0 },
275+
{ id: token3.id, name: token3.name, symbol: token3.symbol, transactions: 0 },
276+
{ id: token4.id, name: token4.name, symbol: token4.symbol, transactions: 0 },
274277
]);
275278

276279
// missing wallet
@@ -1249,8 +1252,8 @@ test('GET /wallet/tokens/token_id/details', async () => {
12491252
const token2 = { id: 'token2', name: 'MyToken2', symbol: 'MT2' };
12501253

12511254
await addToTokenTable(mysql, [
1252-
{ id: token1.id, name: token1.name, symbol: token1.symbol },
1253-
{ id: token2.id, name: token2.name, symbol: token2.symbol },
1255+
{ id: token1.id, name: token1.name, symbol: token1.symbol, transactions: 0 },
1256+
{ id: token2.id, name: token2.name, symbol: token2.symbol, transactions: 0 },
12541257
]);
12551258

12561259
await addToUtxoTable(mysql, [

tests/db.test.ts

+56-2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import {
6363
getTotalTransactions,
6464
getAvailableAuthorities,
6565
getAffectedAddressTxCountFromTxList,
66+
incrementTokensTxCount,
6667
} from '@src/db';
6768
import {
6869
beginTransaction,
@@ -109,6 +110,7 @@ import {
109110
createOutput,
110111
createInput,
111112
countTxOutputTable,
113+
checkTokenTable,
112114
} from '@tests/utils';
113115
import { AddressTxHistoryTableEntry } from '@tests/types';
114116

@@ -917,8 +919,8 @@ test('getWalletBalances', async () => {
917919
}]);
918920

919921
await addToTokenTable(mysql, [
920-
{ id: token1.id, name: token1.name, symbol: token1.symbol },
921-
{ id: token2.id, name: token2.name, symbol: token2.symbol },
922+
{ id: token1.id, name: token1.name, symbol: token1.symbol, transactions: 0 },
923+
{ id: token2.id, name: token2.name, symbol: token2.symbol, transactions: 0 },
922924
]);
923925

924926
// first test fetching all tokens
@@ -1633,6 +1635,18 @@ test('rebuildAddressBalancesFromUtxos', async () => {
16331635

16341636
await addToAddressTxHistoryTable(mysql, txHistory);
16351637

1638+
// add to the token table
1639+
await addToTokenTable(mysql, [
1640+
{ id: token1, name: 'token1', symbol: 'TKN1', transactions: 2 },
1641+
]);
1642+
1643+
await expect(checkTokenTable(mysql, 1, [{
1644+
tokenId: token1,
1645+
tokenSymbol: 'TKN1',
1646+
tokenName: 'token1',
1647+
transactions: 2,
1648+
}])).resolves.toBe(true);
1649+
16361650
// We are only using the txList parameter on `transactions` recalculation, so our balance
16371651
// checks should include txId3 and txId4, but the transaction count should not.
16381652
await rebuildAddressBalancesFromUtxos(mysql, [addr1, addr2], [txId3, txId4]);
@@ -1655,6 +1669,13 @@ test('rebuildAddressBalancesFromUtxos', async () => {
16551669
expect(addressBalances[2].address).toStrictEqual(addr2);
16561670
expect(addressBalances[2].transactions).toStrictEqual(1);
16571671
expect(addressBalances[2].tokenId).toStrictEqual('token2');
1672+
1673+
await expect(checkTokenTable(mysql, 1, [{
1674+
tokenId: token1,
1675+
tokenSymbol: 'TKN1',
1676+
tokenName: 'token1',
1677+
transactions: 0,
1678+
}])).resolves.toBe(true);
16581679
});
16591680

16601681
test('markAddressTxHistoryAsVoided', async () => {
@@ -2121,3 +2142,36 @@ test('getAffectedAddressTxCountFromTxList', async () => {
21212142
// We should get an empty object if no addresses have been affected:
21222143
expect(await getAffectedAddressTxCountFromTxList(mysql, [txId2])).toStrictEqual({});
21232144
});
2145+
2146+
test('incrementTokensTxCount', async () => {
2147+
expect.hasAssertions();
2148+
2149+
const htr = new TokenInfo('00', 'Hathor', 'HTR', 5);
2150+
const token1 = new TokenInfo('token1', 'MyToken1', 'MT1', 10);
2151+
const token2 = new TokenInfo('token2', 'MyToken2', 'MT2', 15);
2152+
2153+
await addToTokenTable(mysql, [
2154+
{ id: htr.id, name: htr.name, symbol: htr.symbol, transactions: htr.transactions },
2155+
{ id: token1.id, name: token1.name, symbol: token1.symbol, transactions: token1.transactions },
2156+
{ id: token2.id, name: token2.name, symbol: token2.symbol, transactions: token2.transactions },
2157+
]);
2158+
2159+
await incrementTokensTxCount(mysql, ['token1', '00', 'token2']);
2160+
2161+
await expect(checkTokenTable(mysql, 3, [{
2162+
tokenId: token1.id,
2163+
tokenSymbol: token1.symbol,
2164+
tokenName: token1.name,
2165+
transactions: token1.transactions + 1,
2166+
}, {
2167+
tokenId: token2.id,
2168+
tokenSymbol: token2.symbol,
2169+
tokenName: token2.name,
2170+
transactions: token2.transactions + 1,
2171+
}, {
2172+
tokenId: htr.id,
2173+
tokenSymbol: htr.symbol,
2174+
tokenName: htr.name,
2175+
transactions: htr.transactions + 1,
2176+
}])).resolves.toBe(true);
2177+
});

0 commit comments

Comments
 (0)