Skip to content

Commit bcdadc8

Browse files
Merge pull request #279 from HathorNetwork/dev
Release v1.18.0-alpha
2 parents b5b97ca + 5b9fde7 commit bcdadc8

15 files changed

+303
-17
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',

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.17.0-alpha",
3+
"version": "1.18.0-alpha",
44
"description": "",
55
"scripts": {
66
"postinstall": "npm dedupe",

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, [

0 commit comments

Comments
 (0)