Skip to content
This repository has been archived by the owner on Jun 10, 2022. It is now read-only.

Commit

Permalink
bug: fix CMC routes
Browse files Browse the repository at this point in the history
- total: all minted funds (initial + inflation)

- circulating: total - burned (returned to nemesis)

- max: maximum minted funds
  • Loading branch information
Jaguar0625 authored and Fernando committed Nov 16, 2021
1 parent 19ca1c7 commit 8e126e8
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 144 deletions.
4 changes: 4 additions & 0 deletions rest/resources/rest.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"allowedHosts": ["*"],
"allowedMethods": ["GET", "POST", "PUT", "OPTIONS"]
},
"uncirculatingAccountPublicKeys": [
"A4739036FD7EFED2750A51EE9D1D3113BA3F9849E0889213CED7F221B2AA1A20",
"2BF1E1F3072E3BE0CD851E4741E101E33DB19C163895F69AA890E7CF177C878C"
],
"extensions": [
"accountLink",
"aggregate",
Expand Down
4 changes: 3 additions & 1 deletion rest/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ const registerRoutes = (server, db, services) => {
apiNode: services.config.apiNode,
websocket: services.config.websocket,
numBlocksTransactionFeeStats: services.config.numBlocksTransactionFeeStats,
deployment: services.config.deployment
deployment: services.config.deployment,

uncirculatingAccountPublicKeys: services.config.uncirculatingAccountPublicKeys
},
connections: services.connectionService
};
Expand Down
61 changes: 32 additions & 29 deletions rest/src/plugins/cmc/cmcRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,66 +20,69 @@
*/

const cmcUtils = require('./cmcUtils');
const uncirculatedAddresses = require('./unCirculatedAccounts');
const { longToUint64 } = require('../../db/dbUtils');
const routeUtils = require('../../routes/routeUtils');
const errors = require('../../server/errors');
const AccountType = require('../AccountType');
const catapult = require('catapult-sdk');
const ini = require('ini');
const fs = require('fs');
const util = require('util');

const { convert, uint64 } = catapult.utils;

module.exports = {
register: (server, db, services) => {
const sender = routeUtils.createSender('cmc');

const propertyValueToMosaicId = value => uint64.fromHex(value.replace(/'/g, '').replace('0x', ''));

const readAndParseNetworkPropertiesFile = () => {
const readFile = util.promisify(fs.readFile);
return readFile(services.config.apiNode.networkPropertyFilePath, 'utf8')
.then(fileData => ini.parse(fileData));
};

server.get('/network/currency/supply/circulating', (req, res, next) => readAndParseNetworkPropertiesFile()
.then(async propertiesObject => {
/* eslint-disable global-require */
const accountIds = routeUtils.parseArgumentAsArray({ addresses: uncirculatedAddresses }, 'addresses', 'address');
const currencyId = propertiesObject.chain.currencyMosaicId.replace(/'/g, '').replace('0x', '');
const mosaicId = routeUtils.parseArgument({ mosaicId: currencyId }, 'mosaicId', 'uint64hex');
const getUncirculatingAccountIds = propertiesObject => {
const publicKeys = [propertiesObject.network.nemesisSignerPublicKey].concat(services.config.uncirculatingAccountPublicKeys);
return publicKeys.map(publicKey => ({ [AccountType.publicKey]: convert.hexToUint8(publicKey) }));
};

const mosaics = await db.mosaicsByIds([mosaicId]);
const accounts = await db.catapultDb.accountsByIds(accountIds.map(accountId => ({ [AccountType.address]: accountId })));
const lookupMosaicAmount = (mosaics, currencyMosaicId) => {
const matchingMosaic = mosaics.find(mosaic => {
const mosaicId = longToUint64(mosaic.id); // convert Long to uint64
return 0 === uint64.compare(currencyMosaicId, mosaicId);
});

const totalSupply = parseInt(mosaics[0].mosaic.supply.toString(), 10);
const totalUncirculated = accounts.reduce((a, b) => a + parseInt(b.account.mosaics[0].amount.toString(), 10), 0);
return undefined === matchingMosaic ? 0 : matchingMosaic.amount.toNumber();
};

const circulatingSupply = (totalSupply - totalUncirculated).toString();
server.get('/network/currency/supply/circulating', (req, res, next) => readAndParseNetworkPropertiesFile()
.then(async propertiesObject => {
const currencyMosaicId = propertyValueToMosaicId(propertiesObject.chain.currencyMosaicId);
const mosaics = await db.mosaicsByIds([currencyMosaicId]);
const accounts = await db.catapultDb.accountsByIds(getUncirculatingAccountIds(propertiesObject));

sender.sendPlainText(res, next)(cmcUtils.convertToRelative(circulatingSupply));
}).catch(() => {
res.send(errors.createInvalidArgumentError('there was an error reading the network properties file'));
next();
const totalSupply = mosaics[0].mosaic.supply.toNumber();
const burnedSupply = accounts.reduce(
(sum, account) => sum + lookupMosaicAmount(account.account.mosaics, currencyMosaicId),
0
);
sender.sendPlainText(res, next)(cmcUtils.convertToRelative(totalSupply - burnedSupply));
}));

server.get('/network/currency/supply/total', (req, res, next) => readAndParseNetworkPropertiesFile()
.then(propertiesObject => {
const currencyId = propertiesObject.chain.currencyMosaicId.replace(/'/g, '').replace('0x', '');
const mosaicId = routeUtils.parseArgument({ mosaicId: currencyId }, 'mosaicId', 'uint64hex');
return db.mosaicsByIds([mosaicId]).then(response => {
const supply = response[0].mosaic.supply.toString();

const currencyMosaicId = propertyValueToMosaicId(propertiesObject.chain.currencyMosaicId);
return db.mosaicsByIds([currencyMosaicId]).then(response => {
const supply = response[0].mosaic.supply.toNumber();
sender.sendPlainText(res, next)(cmcUtils.convertToRelative(supply));
}).catch(() => {
res.send(errors.createInvalidArgumentError('there was an error reading the network properties file'));
next();
});
}));

server.get('/network/currency/supply/max', (req, res, next) => readAndParseNetworkPropertiesFile()
.then(propertiesObject => {
const supply = propertiesObject.chain.maxMosaicAtomicUnits.replace(/'/g, '').replace('0x', '');
const supply = parseInt(propertiesObject.chain.maxMosaicAtomicUnits.replace(/'/g, ''), 10);
sender.sendPlainText(res, next)(cmcUtils.convertToRelative(supply));
}).catch(() => {
res.send(errors.createInvalidArgumentError('there was an error reading the network properties file'));
next();
}));
}
};
65 changes: 0 additions & 65 deletions rest/src/plugins/cmc/unCirculatedAccounts.js

This file was deleted.

133 changes: 84 additions & 49 deletions rest/test/plugins/cmc/cmcRoutes_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* along with Catapult. If not, see <http://www.gnu.org/licenses/>.
*/

const { convertToLong } = require('../../../src/db/dbUtils');
const cmcRoutes = require('../../../src/plugins/cmc/cmcRoutes');
const cmcUtils = require('../../../src/plugins/cmc/cmcUtils');
const { MockServer } = require('../../routes/utils/routeTestUtils');
Expand All @@ -27,18 +28,22 @@ const { expect } = require('chai');
const sinon = require('sinon');
const fs = require('fs');

const { uint64 } = catapult.utils;

describe('cmc routes', () => {
describe('network currency supply', () => {
const maxSupply = 9000000000000000;
const XYMSupply = 8998999998000000;
const xymSupply = 8998999998000000;

const currencyMosaicId = '0x1234\'5678\'ABCD\'EF01';
const nemesisSignerPublicKey = 'AF1B9DCF4FAD2CDC2C04B4F9CBDF3C9C884A9F05B40A59E233681E282DC824D9';
const uncirculatingAccountPublicKey1 = 'D4912C4CA33F608E95B9C3ABAE59263B99E2DF6E87252D61F8DEFADF7DFFC455';
const uncirculatingAccountPublicKey2 = '3BF1E1F3072E3BE0CD851E4741E101E33DB19C163895F69AA890E7CF177C878C';
const circulatingAccountPublicKey1 = '346AA758B2ED98923204D7361A5A47C7B569C594C7904461C67459703D7B5874';

const mosaicsSample = [{
id: '',
mosaic: {
id: '1234567890ABCDEF',
supply: XYMSupply,
id: convertToLong([0xABCDEF01, 0x12345678]),
supply: convertToLong(xymSupply),
startHeight: '',
ownerAddress: '',
revision: 1,
Expand All @@ -48,25 +53,34 @@ describe('cmc routes', () => {
}
}];

const accountsSample = [{
id: 'random1',
account: {
address: '',
addressHeight: '',
publicKey: '',
publicKeyHeight: '',
supplementalPublicKeys: {},
importance: '',
importanceHeight: '',
activityBuckets: [],
mosaics: [
{ id: 0, amount: uint64.fromUint((1000000)) }
]
}
}];
const createAccountSample = (publicKey, currencyAmount, otherAmount) => ({
address: '',
addressHeight: '',
publicKey: catapult.utils.convert.hexToUint8(publicKey),
publicKeyHeight: '',
supplementalPublicKeys: {},
importance: '',
importanceHeight: '',
activityBuckets: [],
mosaics: [
{ id: convertToLong([0xABCDEF01, 0x22222222]), amount: convertToLong(otherAmount) },
{ id: convertToLong([0xABCDEF01, 0x12345678]), amount: convertToLong(currencyAmount) }
]
});

const accountsSample = [
{ id: 'random1', account: createAccountSample(nemesisSignerPublicKey, 1000000, 9000000) },
{ id: 'random2', account: createAccountSample(uncirculatingAccountPublicKey1, 2000000, 9000000) },
{ id: 'random3', account: createAccountSample(circulatingAccountPublicKey1, 4000000, 9000000) },
{ id: 'random4', account: createAccountSample(uncirculatingAccountPublicKey2, 8000000, 9000000) }
];

const dbMosaicsFake = sinon.fake(() => Promise.resolve(mosaicsSample));
const dbAccountsFake = sinon.fake(() => Promise.resolve(accountsSample));
const dbAccountsFake = sinon.fake(accountIds => {
const filteredAccountsSample = accountsSample.filter(accountSample =>
accountIds.some(accountId => catapult.utils.array.deepEqual(accountId.publicKey, accountSample.account.publicKey)));
return Promise.resolve(filteredAccountsSample);
});

const mockServer = new MockServer();

Expand All @@ -77,69 +91,90 @@ describe('cmc routes', () => {
}
};

const services = { config: { apiNode: {} } };
const services = {
config: {
apiNode: {},
uncirculatingAccountPublicKeys: [uncirculatingAccountPublicKey1, uncirculatingAccountPublicKey2]
}
};
cmcRoutes.register(mockServer.server, db, services);

const req = { params: {} };

beforeEach(() => {
afterEach(() => {
mockServer.resetStats();
dbMosaicsFake.resetHistory();
fs.readFile.restore();
});

describe('GET', () => {
it('network currency supply circulating', () => {
const readFileStub = sinon.stub(fs, 'readFile').callsFake((path, data, callback) =>
callback(null, `[chain]\nmaxMosaicAtomicUnits = ${maxSupply}\ncurrencyMosaicId = "0x1234567890ABCDEF"`));

it('network currency supply circulating (without burns)', () => {
sinon.stub(fs, 'readFile').callsFake((path, data, callback) =>
callback(null, [
'[network]',
`nemesisSignerPublicKey=${nemesisSignerPublicKey}`,
'',
'[chain]',
'currencyMosaicId = 0x1234\'5678\'ABCD\'EF02'
].join('\n')));
const route = mockServer.getRoute('/network/currency/supply/circulating').get();

// Arrange:
const totalUncirculated = accountsSample.reduce((a, b) => a + parseInt(b.account.mosaics[0].amount.toString(), 10), 0);
const circulatingSupply = XYMSupply - totalUncirculated;
// Act:
return mockServer.callRoute(route, req).then(() => {
// Assert:
expect(mockServer.next.calledOnce).to.equal(true);

const expectedSupply = mosaicsSample[0].mosaic.supply - 0;
expect(mockServer.send.firstCall.args[0]).to.equal(cmcUtils.convertToRelative(expectedSupply));
});
});

it('network currency supply circulating (with burns)', () => {
sinon.stub(fs, 'readFile').callsFake((path, data, callback) =>
callback(null, [
'[network]',
`nemesisSignerPublicKey=${nemesisSignerPublicKey}`,
'',
'[chain]',
`currencyMosaicId = ${currencyMosaicId}`
].join('\n')));
const route = mockServer.getRoute('/network/currency/supply/circulating').get();

// Act:
return mockServer.callRoute(route, req).then(() => {
// Assert
// Assert:
expect(mockServer.next.calledOnce).to.equal(true);
expect(mockServer.send.firstCall.args[0]).to.equal(cmcUtils.convertToRelative(circulatingSupply));
readFileStub.restore();

const expectedSupply = mosaicsSample[0].mosaic.supply - 11000000;
expect(mockServer.send.firstCall.args[0]).to.equal(cmcUtils.convertToRelative(expectedSupply));
});
});

it('network currency supply total', () => {
const readFileStub = sinon.stub(fs, 'readFile').callsFake((path, data, callback) =>
callback(null, '[chain]\ncurrencyMosaicId = 0x1234567890ABCDEF'));
sinon.stub(fs, 'readFile').callsFake((path, data, callback) =>
callback(null, `[chain]\ncurrencyMosaicId = ${currencyMosaicId}`));

const route = mockServer.getRoute('/network/currency/supply/total').get();

// Arrange:
const xymSupply = cmcUtils.convertToRelative(mosaicsSample[0].mosaic.supply);

// Act:
return mockServer.callRoute(route, req).then(() => {
// Assert
// Assert:
expect(mockServer.next.calledOnce).to.equal(true);
expect(mockServer.send.firstCall.args[0]).to.equal(xymSupply);
readFileStub.restore();
expect(mockServer.send.firstCall.args[0]).to.equal(cmcUtils.convertToRelative(mosaicsSample[0].mosaic.supply));
});
});

it('network currency supply max', () => {
const readFileStub = sinon.stub(fs, 'readFile').callsFake((path, data, callback) =>
sinon.stub(fs, 'readFile').callsFake((path, data, callback) =>
callback(null, `[chain]\nmaxMosaicAtomicUnits = ${maxSupply}`));

const route = mockServer.getRoute('/network/currency/supply/max').get();

// Arrange:
const mosaicMaxSupply = cmcUtils.convertToRelative(maxSupply);

// Act:
return mockServer.callRoute(route, req).then(() => {
// Assert
// Assert:
expect(mockServer.next.calledOnce).to.equal(true);
expect(mockServer.send.firstCall.args[0]).to.equal(mosaicMaxSupply);
readFileStub.restore();
expect(mockServer.send.firstCall.args[0]).to.equal(cmcUtils.convertToRelative(maxSupply));
});
});
});
Expand Down

0 comments on commit 8e126e8

Please sign in to comment.