Page MenuHomePhabricator

No OneTemporary

diff --git a/apps/alias-server/constants/alias.js b/apps/alias-server/constants/alias.js
index af886ae20..d83837c80 100644
--- a/apps/alias-server/constants/alias.js
+++ b/apps/alias-server/constants/alias.js
@@ -1,36 +1,48 @@
// Copyright (c) 2023 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
'use strict';
module.exports = {
// Per spec at https://github.com/Bitcoin-ABC/bitcoin-abc/blob/master/doc/standards/ecash-alias.md
// A valid alias registration outputScript must have protocol identifier pushed by '04'
outputScriptStartsWith: '6a042e786563',
registrationAddress: 'ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07',
minLength: 1,
maxLength: 21,
- registrationFeesSats: {
- 1: 558,
- 2: 557,
- 3: 556,
- 4: 555,
- 5: 554,
- 6: 553,
- 7: 552,
- 8: 551,
- 9: 551,
- 10: 551,
- 11: 551,
- 12: 551,
- 13: 551,
- 14: 551,
- 15: 551,
- 16: 551,
- 17: 551,
- 18: 551,
- 19: 551,
- 20: 551,
- 21: 551,
- },
+ /**
+ * prices
+ * alias registration fee for aliases of each valid length
+ * alias prices come into effect at a "startHeight", the blockheight at which these prices are valid
+ * The prices corresponding to the highest startHeight in prices are the current active alias prices
+ * prices is sorted by prices[i].startHeight, highest to lowest
+ */
+ prices: [
+ {
+ startHeight: 785000, // Beta, will be set at launch blockheight when determined
+ fees: {
+ 1: 558,
+ 2: 557,
+ 3: 556,
+ 4: 555,
+ 5: 554,
+ 6: 553,
+ 7: 552,
+ 8: 551,
+ 9: 551,
+ 10: 551,
+ 11: 551,
+ 12: 551,
+ 13: 551,
+ 14: 551,
+ 15: 551,
+ 16: 551,
+ 17: 551,
+ 18: 551,
+ 19: 551,
+ 20: 551,
+ 21: 551,
+ },
+ },
+ ],
};
diff --git a/apps/alias-server/src/alias.js b/apps/alias-server/src/alias.js
index 5f1638df5..fd7afc62b 100644
--- a/apps/alias-server/src/alias.js
+++ b/apps/alias-server/src/alias.js
@@ -1,286 +1,295 @@
// Copyright (c) 2023 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
'use strict';
const cashaddr = require('ecashaddrjs');
const config = require('../config');
-const { getAliasFromHex, isValidAliasString } = require('./utils');
+const {
+ getAliasFromHex,
+ isValidAliasString,
+ getAliasPrice,
+} = require('./utils');
const { addOneAliasToDb } = require('./db');
const { consumeNextPush } = require('ecash-script');
module.exports = {
getAliasTxs: function (aliasTxHistory, aliasConstants) {
const aliasTxCount = aliasTxHistory.length;
// Get expected output script match for parseAliasTx
// Do it here and not in parseAliasTx so that you don't do it for every single tx
// Will all be the same for a given set of tx history
const registrationOutputScript = cashaddr.getOutputScriptFromAddress(
aliasConstants.registrationAddress,
);
// initialize array for all valid aliases
let aliasTxs = [];
// iterate over history to get all alias:address pairs
for (let i = 0; i < aliasTxCount; i += 1) {
const thisAliasTx = aliasTxHistory[i];
const parsedAliasTx = module.exports.parseAliasTx(
thisAliasTx,
aliasConstants,
registrationOutputScript,
);
if (parsedAliasTx) {
aliasTxs = aliasTxs.concat(parsedAliasTx);
}
}
return aliasTxs;
},
/**
* Parse a single transaction as returned by chronik tx history endpoint
* for valid alias registration(s)
* @param {object} aliasTx Object returned by chronik's tx history endpoint; must be a tx sent to the alias registration address per the spec
* @param {object} aliasConstants Object used to determine alias pricing and registration address
* @param {string} registrationOutputScript the hash160 of the alias registration address
* @returns {array} array of valid aliases registered in this tx if any
* Might always just be one. But need to handle edge case of multiple OP_RETURNs being mined.
* @returns {bool} false if the tx is not a valid alias registration
*/
parseAliasTx: function (aliasTx, aliasConstants, registrationOutputScript) {
// Initialize aliasHexStrings, an array to hold registered alias hex strings
let aliasHexStrings = [];
// Initialize validAliases, an array to hold valid registered alias tx objects
let validAliases = [];
// Initialize fee paid and fee required
let aliasFeePaidSats = BigInt(0);
let aliasFeeRequiredSats = BigInt(0);
// Iterate over outputs
const outputs = aliasTx.outputs;
for (let i = 0; i < outputs.length; i += 1) {
const { value, outputScript } = outputs[i];
if (
outputScript.startsWith(aliasConstants.outputScriptStartsWith)
) {
// If this is an OP_RETURN tx that pushes the alias registration
// protocol identifier with the required '04' push operator,
// Parse the rest of this OP_RETURN stack
let stack = {
remainingHex: outputScript.slice(
aliasConstants.outputScriptStartsWith.length,
),
};
let stackArray = [];
while (stack.remainingHex.length > 0) {
stackArray.push(consumeNextPush(stack));
}
if (stackArray.length !== 3) {
// If we don't have exactly 3 pushes after the protocol identifier
// Then it is not a valid alias registration per spec
// This invalidates registrations that include "empty" pushes e.g. "4c00"
continue;
}
// stackArray is now
// [
// {data: version_number, pushedWith: <pushOp>},
// {data: aliasHex, pushedWith: <pushOp>},
// {data: address_type_and_hash, pushedWith: <pushOp>}
// ]
// Validate alias tx version
const aliasTxVersion = stackArray[0];
if (
aliasTxVersion.data !== '00' ||
aliasTxVersion.pushedWith !== '00'
) {
// If this is not a version 0 alias tx pushed with OP_0,
// Then it is not a valid alias registration per spec
continue;
}
// Validate alias length
const aliasHex = stackArray[1];
// Alias length in characters is aliasHex / 2, each hex byte is 1 character
const aliasLength = aliasHex.data.length / 2;
if (
aliasLength === 0 ||
aliasLength > aliasConstants.maxLength ||
parseInt(aliasHex.pushedWith, 16) !== aliasLength
) {
// If the alias has 0 length OR
// If the alias has length greater than 21 OR
// If the alias was not pushed with the minimum push operator
// Then it is not a valid alias registration per spec
continue;
}
const alias = getAliasFromHex(aliasHex.data);
if (!isValidAliasString(alias)) {
// If the registered alias contains characters other than a-z or 0-9
// Then it is not a valid alias registration per spec
continue;
}
if (aliasHexStrings.includes(aliasHex.data)) {
// If this tx has already tried to register this same alias in an OP_RETURN output
// Then no alias registered in this tx may be valid, per spec
return false;
}
aliasHexStrings.push(aliasHex.data);
// Validate alias assigned address
const addressTypeAndHash = stackArray[2];
if (
addressTypeAndHash.data.length / 2 !== 21 ||
addressTypeAndHash.pushedWith !== '15'
) {
// If we don't have one byte address type and twenty bytes address hash
// pushed with the minimum push operator i.e. '15' === hex for 21
// Then it is not a valid alias registration per spec
continue;
}
let addressType;
switch (addressTypeAndHash.data.slice(0, 2)) {
case '00': {
addressType = 'p2pkh';
break;
}
case '08': {
addressType = 'p2sh';
break;
}
default: {
// If the address type byte is not '00' or '08'
// Then it is not a valid alias registration per spec
continue;
}
}
let address;
try {
address = cashaddr.encode(
'ecash',
addressType,
addressTypeAndHash.data.slice(2),
);
} catch (err) {
// If the type and hash do not constitute a valid cashaddr,
// Then it is not a valid alias registration per spec
continue;
}
// If you get here, the construction of the registration in the OP_RETURN field is valid
// However you still must compare against fee paid and registration history to finalize
+ const registrationBlockheight =
+ aliasTx && aliasTx.block
+ ? aliasTx.block.height
+ : config.unconfirmedBlockheight;
validAliases.push({
alias,
address,
txid: aliasTx.txid,
- blockheight:
- aliasTx && aliasTx.block
- ? aliasTx.block.height
- : config.unconfirmedBlockheight,
+ blockheight: registrationBlockheight,
});
// Increment the required fee based on this valid alias
aliasFeeRequiredSats += BigInt(
- aliasConstants.registrationFeesSats[aliasLength],
+ getAliasPrice(
+ aliasConstants.prices,
+ aliasLength,
+ registrationBlockheight,
+ ),
);
} else {
// Check if outputScript matches alias registration address
if (outputScript === registrationOutputScript)
// If so, then the value here is part of the alias registration fee, aliasFeePaidSats
aliasFeePaidSats += BigInt(value);
}
}
if (validAliases.length === 0) {
// If you have no valid OP_RETURN alias registrations, this is not a valid alias registration tx
return false;
}
if (aliasFeePaidSats < aliasFeeRequiredSats) {
// If this tx does not pay the required fee for all aliases registered in the tx
// Then no alias registered in this tx may be valid, per spec
return false;
}
return validAliases;
},
sortAliasTxsByTxidAndBlockheight: function (unsortedAliasTxs) {
// First, sort the aliases array by alphabetical txid
// (alphabetical first to last, 0 comes before a comes before b comes before c, etc)
const aliasesTxsSortedByTxid = unsortedAliasTxs.sort((a, b) => {
return a.txid.localeCompare(b.txid);
});
// Next, sort the aliases array by blockheight. This will preserve the alphabetical txid sort
// 735,625 comes before 735,626 comes before 100,000,000 etc
const aliasTxsSortedByTxidAndBlockheight = aliasesTxsSortedByTxid.sort(
(a, b) => {
return a.blockheight - b.blockheight;
},
);
return aliasTxsSortedByTxidAndBlockheight;
},
registerAliases: async function (db, unsortedConfirmedAliasTxs) {
/* Add new valid aliases registration txs to the database. Return an array of what was added.
*
* Input parameters
* db - the app database
* unsortedConfirmedAliasTxs - array, arbitrary collection of confirmed alias-prefixed txs
* at the alias registration address
*
* Outputs
* - The function adds new valid alias txs to the database
* - An array of objects, each one a new valid alias registration tx that was added to the database
*
* Will get all valid alias registrations if given the full tx history and
* the database is empty
*/
// Sort aliases such that the earliest aliases are the valid ones
const aliasesSortedByTxidAndBlockheight =
module.exports.sortAliasTxsByTxidAndBlockheight(
unsortedConfirmedAliasTxs,
);
// Initialize arrays to store alias registration info
let validAliasRegistrations = [];
// Iterate over sorted aliases starting from oldest registrations to newest
// (and alphabetically first txids to last)
for (let i = 0; i < aliasesSortedByTxidAndBlockheight.length; i += 1) {
const thisAliasTx = aliasesSortedByTxidAndBlockheight[i];
/* If the alias isn't in the database, it's valid and should be added
*/
// If database add is successful,
// add thisAliasObject to the validAliasObjects array
if (await addOneAliasToDb(db, thisAliasTx)) {
// Because thisAliasTx receives an "_id" key on being added to the db,
// clone it without this field to return
const { address, alias, blockheight, txid } = thisAliasTx;
validAliasRegistrations.push({
address,
alias,
blockheight,
txid,
});
}
}
return validAliasRegistrations;
},
};
diff --git a/apps/alias-server/src/app.js b/apps/alias-server/src/app.js
index 423a9c737..69a72abad 100644
--- a/apps/alias-server/src/app.js
+++ b/apps/alias-server/src/app.js
@@ -1,119 +1,119 @@
// Copyright (c) 2023 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
'use strict';
const config = require('../config');
const express = require('express');
var cors = require('cors');
const requestIp = require('request-ip');
const { getAliasInfoFromAlias, getAliasInfoFromAddress } = require('./db');
const aliasConstants = require('../constants/alias');
module.exports = {
startServer: function (db, port) {
// Set up your API endpoints
const app = express();
app.use(express.json());
app.use(requestIp.mw());
app.use(cors());
app.get('/prices', async function (req, res) {
// Get IP address from before cloudflare proxy
const ip = req.clientIp;
console.log(`/prices from IP: ${ip}, host ${req.headers.host}`);
// Add a note about prices
let pricesResponse = {
note: 'alias-server is in beta and these prices are not finalized.',
- prices: aliasConstants.registrationFeesSats,
+ prices: aliasConstants.prices,
};
try {
return res.status(200).json(pricesResponse);
} catch (err) {
return res.status(500).json({
error:
err && err.message
? err.message
: 'Error fetching /prices',
});
}
});
app.get('/aliases', async function (req, res) {
// Get IP address from before cloudflare proxy
const ip = req.clientIp;
console.log(`/aliases from IP: ${ip}, host ${req.headers.host}`);
let aliases;
try {
aliases = await db
.collection(config.database.collections.validAliases)
.find()
.project({ _id: 0 })
.toArray();
return res.status(200).json(aliases);
} catch (error) {
return res.status(500).json({
error:
error && error.message
? error.message
: 'Error fetching /aliases',
});
}
});
app.get('/alias/:alias', async function (req, res) {
// Get the requested alias
const alias = req.params.alias;
// Log the request
console.log(
`/alias/${alias} from IP: ${req.clientIp}, host ${req.headers.host}`,
);
// Lookup the alias
let response;
try {
response = await getAliasInfoFromAlias(db, alias);
// Custom msg if alias has not yet been registered
if (response === null) {
response = { alias, isRegistered: false };
}
// Return successful response
return res.status(200).json(response);
} catch (err) {
// Return error response
return res.status(500).json({
error: `Error fetching /alias/${alias}${
err && err.message ? `: ${err.message}` : ''
}`,
});
}
});
app.get('/address/:address', async function (req, res) {
// Get the requested alias
const address = req.params.address;
// Log the request
console.log(
`/address/${address} from IP: ${req.clientIp}, host ${req.headers.host}`,
);
// Lookup the aliases at given address
try {
return res
.status(200)
.json(await getAliasInfoFromAddress(db, address));
} catch (err) {
// Return error response
return res.status(500).json({
error: `Error fetching /address/${address}${
err && err.message ? `: ${err.message}` : ''
}`,
});
}
});
return app.listen(port);
},
};
diff --git a/apps/alias-server/src/telegram.js b/apps/alias-server/src/telegram.js
index 3be76f5a8..a69884f0e 100644
--- a/apps/alias-server/src/telegram.js
+++ b/apps/alias-server/src/telegram.js
@@ -1,98 +1,98 @@
// Copyright (c) 2023 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
'use strict';
const aliasConstants = require('../constants/alias');
-const { getXecPrice, satsToFormattedValue } = require('./utils');
+const { getXecPrice, satsToFormattedValue, getAliasPrice } = require('./utils');
module.exports = {
/**
*
* @param {object} aliasObject {address, alias, blockheight, txid}
* @param {number or bool} xecPrice number if API call was successful, otherwise false
* @returns {string} a string formatted for HTML-parsed Telegram message
*/
buildAliasTgMsg: function (aliasObject, xecPrice) {
- const { address, alias, txid } = aliasObject;
+ const { address, alias, txid, blockheight } = aliasObject;
const displayedAliasPrice = satsToFormattedValue(
- aliasConstants.registrationFeesSats[alias.length],
+ getAliasPrice(aliasConstants.prices, alias.length, blockheight),
xecPrice,
);
// Define block explorer
const blockExplorer = 'https://explorer.e.cash';
// Return the msg string
const startSliceBeginning = 6; // 'ecash:'.length
const displayedCharacters = 3;
return `alias "${alias}" <a href="${blockExplorer}/tx/${txid}">registered</a> to <a href="${blockExplorer}/address/${address}">${address.slice(
startSliceBeginning,
startSliceBeginning + displayedCharacters,
)}...${address.slice(
-displayedCharacters,
)}</a> for ${displayedAliasPrice}`;
},
/**
* Use Promise.all() to async send telegram msg of all alias announcements
* @param {object or null} telegramBot an active, polling telegram bot
* @param {string} channelId telegram channel or chat where bot is an admin
* @param {array} newAliasRegistrations Array of alias objects [{alias, address, txid, blockheight}...]
* @returns {promise} Promise.all() for async sending of all registered aliases in this block
* @returns {undefined} If telegramBot is null, the function does nothing
*/
sendAliasAnnouncements: async function (
telegramBot,
channelId,
newAliasRegistrations,
) {
// If telegramBot is null, do nothing
if (telegramBot === null) {
return;
}
// Get eCash price
const xecPrice = await getXecPrice();
// Build an array of promises to send each registration announcement
const aliasAnnouncementPromises = [];
for (let i in newAliasRegistrations) {
// Build the tg msg
const aliasTgMsg = module.exports.buildAliasTgMsg(
newAliasRegistrations[i],
xecPrice,
);
// Add a promise to send the msg to aliasAnnouncementPromises array
aliasAnnouncementPromises.push(
new Promise(resolve => {
telegramBot
.sendMessage(channelId, aliasTgMsg, {
parse_mode: 'HTML',
// disable_web_page_preview: true prevents link preview for the block explorer, which dominates the msg
disable_web_page_preview: true,
})
.then(
result => {
resolve(result);
},
err => {
// Don't log the actual error as telegram bot errors are very long and the issue
// is typically deducible in testing
console.log(
`Error sending alias announcement for ${newAliasRegistrations[i].alias}`,
);
// You don't want to throw this error, so resolve() instead of reject()
resolve(err);
},
);
}),
);
}
// Do not await as
// (1) you don't really care if any of these fail
// (2) the next block can't process until this function completes, which would be
// delayed for no important reason by an await
return Promise.all(aliasAnnouncementPromises);
},
};
diff --git a/apps/alias-server/src/utils.js b/apps/alias-server/src/utils.js
index 9f6580698..5b36b3b74 100644
--- a/apps/alias-server/src/utils.js
+++ b/apps/alias-server/src/utils.js
@@ -1,103 +1,155 @@
// Copyright (c) 2023 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
'use strict';
const axios = require('axios');
+const config = require('../config');
module.exports = {
getXecPrice: async function () {
let coingeckoApiResponse;
try {
coingeckoApiResponse = await axios.get(
'https://api.coingecko.com/api/v3/simple/price?ids=ecash&vs_currencies=usd',
);
return coingeckoApiResponse.data.ecash.usd;
} catch (err) {
console.log(err);
// If API error or error getting price, return false
return false;
}
},
/**
* Return a formatted string in USD if xecPrice is available
* Otherwise return formatted XEC amount
* @param {integer} satoshis
* @param {number or false} xecPrice
* @returns {string} formatted price string e.g. '150 XEC' or "$150"
*/
satsToFormattedValue: function (satoshis, xecPrice) {
// Convert satoshis to XEC
const xecAmount = satoshis / 100;
// Get fiat price
let formattedValue = xecPrice ? xecAmount * xecPrice : xecAmount;
// Format fiatAmount for different tiers
let displayedAmount;
let localeOptions = { maximumFractionDigits: 0 };
let descriptor = '';
if (formattedValue < 0.01) {
// enough decimal places to show one significant digit
localeOptions = {
minimumFractionDigits: -Math.floor(Math.log10(formattedValue)),
};
} else if (formattedValue < 10) {
// Two decimal places
localeOptions = { minimumFractionDigits: 2 };
}
if (formattedValue < 1000) {
displayedAmount = formattedValue;
} else if (formattedValue < 1000000) {
// thousands
displayedAmount = formattedValue / 1000;
descriptor = 'k';
} else if (formattedValue < 1000000000) {
// millions
displayedAmount = formattedValue / 1000000;
descriptor = 'M';
} else if (formattedValue >= 1000000000) {
// billions or more
displayedAmount = formattedValue / 1000000000;
descriptor = 'B';
}
return `${xecPrice ? '$' : ''}${displayedAmount.toLocaleString(
'en-US',
localeOptions,
)}${descriptor}${xecPrice ? '' : ' XEC'}`;
},
getAliasFromHex: function (aliasHex) {
return Buffer.from(aliasHex, 'hex').toString('utf8');
},
getHexFromAlias: function (alias) {
return Buffer.from(alias, 'utf8').toString('hex');
},
getAliasBytecount: function (alias) {
const aliasHex = module.exports.getHexFromAlias(alias);
const aliasByteCount = aliasHex.length / 2;
return aliasByteCount;
},
isValidAliasString: function (alias) {
/*
Initial launch will support only lower case roman alphabet and numbers 0 through 9
*/
return /^[a-z0-9]+$/.test(alias);
},
removeUnconfirmedTxsFromTxHistory: function (txHistory) {
// Remove unconfirmed txs from an array of chronik tx objects
const confirmedTxHistory = [];
for (let i = 0; i < txHistory.length; i += 1) {
const thisTx = txHistory[i];
if (typeof thisTx.block !== 'undefined') {
confirmedTxHistory.push(thisTx);
}
}
return confirmedTxHistory;
},
wait: async function (msecs) {
await new Promise(resolve => setTimeout(resolve, msecs));
},
+ /**
+ * Get alias registration price for an alias of a given length and blockheight
+ * Note that for alias-server, you want the price for a given block and NOT the next block,
+ * as you need to verify txs in this block
+ * @param {object} prices
+ * an array of alias registration prices and the blockheight at which they become valid
+ * although prices is a constant, it is used as a parameter here to allow unit testing a range of possible options
+ * @param {number} aliasLength bytecount of alias hex string, or alias.length of the utf8 alias. 1-21.
+ * @param {number} registrationBlockheight blockheight of confirmed alias registration tx
+ * @returns {number} price in satoshis
+ * @throws {error} if blockheight precedes alias launch
+ * @throws {error} if the entries of prices are not sorted highest to lowest by prices[i].startHeight
+ */
+ getAliasPrice: function (prices, aliasLength, registrationBlockheight) {
+ // Initialize registrationFeeSats
+ let registrationFeeSats;
+ // Initialize lastStartHeight as arbitrarily high
+ let lastStartHeight = config.unconfirmedBlockheight;
+
+ for (let i = 0; i < prices.length; i += 1) {
+ const { startHeight, fees } = prices[i];
+
+ // Confirm this startHeight is greater than lastStartHeight
+ if (startHeight >= lastStartHeight) {
+ throw new Error(
+ 'alias price epochs must be sorted by startHeight, highest to lowest',
+ );
+ }
+
+ // If your tested blockheight is higher than this blockheight, these are your prices
+ if (registrationBlockheight >= startHeight) {
+ registrationFeeSats = fees[aliasLength];
+ if (typeof registrationFeeSats !== 'number') {
+ throw new Error(
+ `fees[${aliasLength}] is undefined for ${registrationBlockheight}`,
+ );
+ }
+ }
+ // If not, check the next price epoch
+ // Update lastStartHeight before incrementing i
+ lastStartHeight = startHeight;
+ }
+ // Return registrationFeeSats if you found it
+ if (typeof registrationFeeSats === 'number') {
+ return registrationFeeSats;
+ }
+ // If you get to the earliest defined block and haven't found anything, throw an error
+ throw new Error(
+ `${registrationBlockheight} precedes alias protocol activation height`,
+ );
+ },
};
diff --git a/apps/alias-server/test/app.test.js b/apps/alias-server/test/app.test.js
index 8316422a3..1f007aa4b 100644
--- a/apps/alias-server/test/app.test.js
+++ b/apps/alias-server/test/app.test.js
@@ -1,240 +1,240 @@
// Copyright (c) 2023 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
'use strict';
const request = require('supertest');
const { startServer } = require('../src/app');
const { initializeDb, addOneAliasToDb, addAliasesToDb } = require('../src/db');
// Mock mongodb
const { MongoClient } = require('mongodb');
const { MongoMemoryServer } = require('mongodb-memory-server');
const { generated } = require('./mocks/aliasMocks');
const aliasConstants = require('../constants/alias');
describe('alias-server app.js', function () {
let mongoServer, testMongoClient;
before(async () => {
mongoServer = await MongoMemoryServer.create();
const mongoUri = mongoServer.getUri();
testMongoClient = new MongoClient(mongoUri);
});
after(async () => {
await testMongoClient.close();
await mongoServer.stop();
});
let testDb, app, dbErrorApp;
beforeEach(async () => {
// Initialize db before each unit test
testDb = await initializeDb(testMongoClient);
// Start the express server
app = startServer(testDb, 5000);
// Start an express server with a bad db to mock errors
dbErrorApp = startServer('not_a_database', 5001);
});
afterEach(async () => {
// Wipe the database after each unit test
await testDb.dropDatabase();
// Stop express server
app.close();
dbErrorApp.close();
});
- it('/prices returns aliasConstants.registrationFeesSats', function () {
+ it('/prices returns aliasConstants.prices', function () {
let pricesResponse = {
note: 'alias-server is in beta and these prices are not finalized.',
- prices: aliasConstants.registrationFeesSats,
+ prices: aliasConstants.prices,
};
return request(app)
.get('/prices')
.expect(200)
.expect('Content-Type', /json/)
.expect(pricesResponse);
});
it('/aliases returns an empty array if no aliases are indexed', function () {
return request(app)
.get('/aliases')
.expect(200)
.expect('Content-Type', /json/)
.expect('[]');
});
it('/aliases returns an error on database error', function () {
return request(dbErrorApp)
.get('/aliases')
.expect(500)
.expect('Content-Type', /json/)
.expect({ error: 'db.collection is not a function' });
});
it('/aliases returns array of all indexed alias objects', async function () {
// newValidAliases needs to be a clone of the mock because
// each object gets an _id field when added to the database
const newValidAliases = JSON.parse(
JSON.stringify(generated.validAliasRegistrations),
);
await addAliasesToDb(testDb, newValidAliases);
return request(app)
.get('/aliases')
.expect(200)
.expect('Content-Type', /json/)
.expect(generated.validAliasRegistrations);
});
it('/alias/<alias> returns object with isRegistered:false for an alias not in the database', async function () {
const testedAlias = 'test';
return request(app)
.get(`/alias/${testedAlias}`)
.expect(200)
.expect('Content-Type', /json/)
.expect({ alias: testedAlias, isRegistered: false });
});
it('/alias/<alias> returns object for an alias in the database', async function () {
// newValidAliases needs to be a clone of the mock because
// each object gets an _id field when added to the database
const newValidAliases = JSON.parse(
JSON.stringify(generated.validAliasRegistrations),
);
await addOneAliasToDb(testDb, newValidAliases[0]);
const { alias, address, blockheight, txid } = newValidAliases[0];
return request(app)
.get(`/alias/${alias}`)
.expect(200)
.expect('Content-Type', /json/)
.expect({ alias, address, blockheight, txid });
});
it('/alias/<alias> returns an error on database error', function () {
const testAlias = 'test';
return request(dbErrorApp)
.get(`/alias/${testAlias}`)
.expect(500)
.expect('Content-Type', /json/)
.expect({
error: `Error fetching /alias/${testAlias}: Error finding alias "${testAlias}" in database`,
});
});
it('/address/:address returns an empty array if there are no registered aliases for the given address', function () {
const validAddress = 'ecash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxav9up3h67g';
return request(app)
.get(`/address/${validAddress}`)
.expect(200)
.expect('Content-Type', /json/)
.expect([]);
});
it('/address/:address returns an empty array if there are no registered aliases for the given address and input is prefixless but has valid checksum', function () {
const validAddress = 'qphpmfj0qn7znklqhrfn5dq7qh36l3vxav9up3h67g';
return request(app)
.get(`/address/${validAddress}`)
.expect(200)
.expect('Content-Type', /json/)
.expect([]);
});
it('/address/:address returns an array of length 1 if there is one registered alias for the given address', async function () {
// newValidAliases needs to be a clone of the mock because
// each object gets an _id field when added to the database
const newValidAliases = JSON.parse(
JSON.stringify(generated.validAliasRegistrations),
);
await addOneAliasToDb(testDb, newValidAliases[0]);
const { address } = newValidAliases[0];
return request(app)
.get(`/address/${address}`)
.expect(200)
.expect('Content-Type', /json/)
.expect([generated.validAliasRegistrations[0]]);
});
it('/address/:address returns an array of length 1 if there is one registered alias for the given address and given address is prefixless but valid checksum', async function () {
// newValidAliases needs to be a clone of the mock because
// each object gets an _id field when added to the database
const newValidAliases = JSON.parse(
JSON.stringify(generated.validAliasRegistrations),
);
await addOneAliasToDb(testDb, newValidAliases[0]);
const { address } = newValidAliases[0];
return request(app)
.get(`/address/${address.slice('ecash:'.length)}`)
.expect(200)
.expect('Content-Type', /json/)
.expect([generated.validAliasRegistrations[0]]);
});
it('/address/:address returns an array of multiple alias registrations if there are multiple registered aliases for the given address', async function () {
// Add aliases
// newValidAliases needs to be a clone of the mock because
// each object gets an _id field when added to the database
const newValidAliases = JSON.parse(
JSON.stringify(generated.validAliasRegistrations),
);
const { address } = newValidAliases[0];
// Pre-populate the aliases collection
await addAliasesToDb(testDb, newValidAliases);
// Get the expected array using array filtering
// This way, if the mocks change, the expected result updates appropriately
const expectedResult = generated.validAliasRegistrations.filter(
aliasObj => {
if (aliasObj.address === address) {
return aliasObj;
}
},
);
return request(app)
.get(`/address/${address}`)
.expect(200)
.expect('Content-Type', /json/)
.expect(expectedResult);
});
it('/address/:address returns an array of multiple alias registrations if there are multiple registered aliases for the given address and input is prefixless with valid checksum', async function () {
// Add aliases
// newValidAliases needs to be a clone of the mock because
// each object gets an _id field when added to the database
const newValidAliases = JSON.parse(
JSON.stringify(generated.validAliasRegistrations),
);
const { address } = newValidAliases[0];
// Pre-populate the aliases collection
await addAliasesToDb(testDb, newValidAliases);
// Get the expected array using array filtering
// This way, if the mocks change, the expected result updates appropriately
const expectedResult = generated.validAliasRegistrations.filter(
aliasObj => {
if (aliasObj.address === address) {
return aliasObj;
}
},
);
return request(app)
.get(`/address/${address.slice('ecash:'.length)}`)
.expect(200)
.expect('Content-Type', /json/)
.expect(expectedResult);
});
it('/address/:address returns an error on valid address that is not ecash: prefixed', function () {
const etokenAddress =
'etoken:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavtzgnpa6l';
return request(app)
.get(`/address/${etokenAddress}`)
.expect(500)
.expect('Content-Type', /json/)
.expect({
error: `Error fetching /address/${etokenAddress}: Input must be a valid eCash address`,
});
});
it('/address/:address returns an error on a string that is not a valid ecash address', function () {
const invalidAddress = 'justSomeString';
return request(app)
.get(`/address/${invalidAddress}`)
.expect(500)
.expect('Content-Type', /json/)
.expect({
error: `Error fetching /address/${invalidAddress}: Input must be a valid eCash address`,
});
});
it('/address/:address returns an error on database error', function () {
const validAddress = 'ecash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxav9up3h67g';
return request(dbErrorApp)
.get(`/address/${validAddress}`)
.expect(500)
.expect('Content-Type', /json/)
.expect({
error: `Error fetching /address/${validAddress}: Error finding aliases for address ${validAddress} in database`,
});
});
});
diff --git a/apps/alias-server/test/utilsTests.js b/apps/alias-server/test/utilsTests.js
index 344113a7e..a7f96e8f4 100644
--- a/apps/alias-server/test/utilsTests.js
+++ b/apps/alias-server/test/utilsTests.js
@@ -1,145 +1,476 @@
// Copyright (c) 2023 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
'use strict';
const assert = require('assert');
const {
getXecPrice,
getAliasFromHex,
getHexFromAlias,
getAliasBytecount,
isValidAliasString,
removeUnconfirmedTxsFromTxHistory,
satsToFormattedValue,
+ getAliasPrice,
} = require('../src/utils');
const {
aliasHexConversions,
validAliasStrings,
invalidAliasStrings,
} = require('./mocks/utilsMocks');
const { generated } = require('./mocks/aliasMocks');
const axios = require('axios');
const MockAdapter = require('axios-mock-adapter');
const mockXecPrice = 0.000033;
+const config = require('../config');
describe('alias-server utils.js', function () {
it('getXecPrice returns price as a number', async function () {
// Mock a successful API request
const mock = new MockAdapter(axios, { onNoMatch: 'throwException' });
const mockResult = { ecash: { usd: 3.331e-5 } };
mock.onGet().reply(200, mockResult);
assert.strictEqual(await getXecPrice(), 3.331e-5);
});
it('getXecPrice returns false on API error', async function () {
// Mock a successful API request
const mock = new MockAdapter(axios, { onNoMatch: 'throwException' });
const mockResult = { error: 'API is down' };
mock.onGet().reply(500, mockResult);
assert.strictEqual(await getXecPrice(), false);
});
it('Hexadecimal to utf8 encoding functions work forward and backward. Byte counts match hexadecimal bytes.', function () {
for (let i = 0; i < aliasHexConversions.length; i += 1) {
const { alias, aliasHex, aliasByteCount } = aliasHexConversions[i];
assert.deepEqual(getHexFromAlias(alias), aliasHex);
assert.deepEqual(getAliasFromHex(aliasHex), alias);
assert.deepEqual(getAliasBytecount(alias), aliasByteCount);
}
});
it('Recognizes lower case alphanumeric strings as valid alias strings', function () {
for (let i = 0; i < validAliasStrings.length; i += 1) {
const validAliasString = validAliasStrings[i];
assert.deepEqual(isValidAliasString(validAliasString), true);
}
});
it('Recognizes strings with characters other than lower case a-z or numbers 0-9 as invalid alias strings', function () {
for (let i = 0; i < invalidAliasStrings.length; i += 1) {
const invalidAliasString = invalidAliasStrings[i];
assert.deepEqual(isValidAliasString(invalidAliasString), false);
}
});
it('removeUnconfirmedTxsFromTxHistory removes unconfirmed txs from an array of chronik tx history', function () {
// First, clone the mock so that you are not modifying it in place
const txHistoryWithSomeUnconfirmedTxs = JSON.parse(
JSON.stringify(generated.txHistory),
);
// Then, delete the 'block' key of the most recent 3 txs
// NB these do not include valid alias registrations
delete txHistoryWithSomeUnconfirmedTxs[0].block; // db09c578d38f37bd9f2bb69eeb8ecb2e24c5be01aa2914f17d94759aadf71386
delete txHistoryWithSomeUnconfirmedTxs[1].block; // c040ccdc46df2951b2ab0cd6d48cf9db7c518068d1f871e60379ee8ccd1caa0e
delete txHistoryWithSomeUnconfirmedTxs[2].block; // 828201e4680e6617636193d3f2a319daab80a8cc5772b9a5b6e068de639f2d9c
// Manually delete these txs from your expected result
let expectedResult = JSON.parse(JSON.stringify(generated.txHistory));
expectedResult.splice(0, 3);
assert.deepEqual(
removeUnconfirmedTxsFromTxHistory(txHistoryWithSomeUnconfirmedTxs),
expectedResult,
);
});
it('satsToFormattedValue returns a 6-decimal formatted fiat amount if total fiat value is less than $0.00001', function () {
assert.strictEqual(satsToFormattedValue(10, mockXecPrice), `$0.000003`);
});
it('satsToFormattedValue returns a 5-decimal formatted fiat amount if total fiat value is less than $0.0001', function () {
assert.strictEqual(satsToFormattedValue(100, mockXecPrice), `$0.00003`);
});
it('satsToFormattedValue returns a 4-decimal formatted fiat amount if total fiat value is less than $0.001', function () {
assert.strictEqual(satsToFormattedValue(1000, mockXecPrice), `$0.0003`);
});
it('satsToFormattedValue returns a 3-decimal formatted fiat amount if total fiat value is less than $0.01', function () {
assert.strictEqual(satsToFormattedValue(10000, mockXecPrice), `$0.003`);
});
it('satsToFormattedValue returns a 2-decimal formatted fiat amount if total fiat value is less than $1', function () {
assert.strictEqual(
satsToFormattedValue(1000000, mockXecPrice),
`$0.33`,
);
});
it('satsToFormattedValue returns a formatted fiat amount if total fiat value is less than $10', function () {
assert.strictEqual(
satsToFormattedValue(10000000, mockXecPrice),
'$3.30',
);
});
it('satsToFormattedValue returns a formatted fiat amount if $100 < total fiat value < $1k', function () {
assert.strictEqual(
satsToFormattedValue(1234567890, mockXecPrice),
'$407',
);
});
it('satsToFormattedValue returns a formatted fiat amount if $1k < total fiat value < $1M', function () {
assert.strictEqual(
satsToFormattedValue(55555555555, mockXecPrice),
'$18k',
);
});
it('satsToFormattedValue returns a formatted fiat amount of $1M if $1M < total fiat value < $1B', function () {
assert.strictEqual(
satsToFormattedValue(3367973856209, mockXecPrice),
'$1M',
);
});
it('satsToFormattedValue returns a formatted fiat amount if $1M < total fiat value < $1B', function () {
assert.strictEqual(
satsToFormattedValue(55555555555555, mockXecPrice),
'$18M',
);
});
it('satsToFormattedValue returns a formatted fiat amount if total fiat value > $1B', function () {
assert.strictEqual(
satsToFormattedValue(21000000000000000, mockXecPrice),
'$7B',
);
});
it('satsToFormattedValue returns a formatted XEC amount if coingeckoPrices is false', function () {
assert.strictEqual(
satsToFormattedValue(55555555555555, false),
'556B XEC',
);
});
it('satsToFormattedValue returns a USD amount with 7 decimal places if fiat qty is less than 0.000001', function () {
assert.strictEqual(satsToFormattedValue(1, mockXecPrice), '$0.0000003');
});
+ it('getAliasPrice returns expected price for an alias registered in the most recent price epoch', function () {
+ const registrationBlockheight = 785000;
+ const aliasLength = 15;
+ const mockPrices = [
+ {
+ startHeight: registrationBlockheight,
+ fees: {
+ 1: 571,
+ 2: 570,
+ 3: 569,
+ 4: 568,
+ 5: 567,
+ 6: 566,
+ 7: 565,
+ 8: 564,
+ 9: 563,
+ 10: 562,
+ 11: 561,
+ 12: 560,
+ 13: 559,
+ 14: 558,
+ 15: 557,
+ 16: 556,
+ 17: 555,
+ 18: 554,
+ 19: 553,
+ 20: 552,
+ 21: 551,
+ },
+ },
+ ];
+ assert.strictEqual(
+ getAliasPrice(mockPrices, aliasLength, registrationBlockheight),
+ 557,
+ );
+
+ // Also works for an unconfirmed tx
+ assert.strictEqual(
+ getAliasPrice(
+ mockPrices,
+ aliasLength,
+ config.unconfirmedBlockheight,
+ ),
+ 557,
+ );
+ });
+ it('getAliasPrice throws an error if asked for a price of an undefined epoch', function () {
+ const aliasLength = 15;
+ const mockPrices = [
+ {
+ startHeight: 800000,
+ fees: {
+ 1: 571,
+ 2: 570,
+ 3: 569,
+ 4: 568,
+ 5: 567,
+ 6: 566,
+ 7: 565,
+ 8: 564,
+ 9: 563,
+ 10: 562,
+ 11: 561,
+ 12: 560,
+ 13: 559,
+ 14: 558,
+ 15: 557,
+ 16: 556,
+ 17: 555,
+ 18: 554,
+ 19: 553,
+ 20: 552,
+ 21: 551,
+ },
+ },
+ ];
+ const registrationBlockheight = 799999;
+
+ assert.throws(() => {
+ getAliasPrice(mockPrices, aliasLength, registrationBlockheight);
+ }, new Error(`${registrationBlockheight} precedes alias protocol activation height`));
+ });
+ it('getAliasPrice throws an error if called with a prices object that does not cover the alias length', function () {
+ const registrationBlockheight = 785000;
+ const aliasLength = 15;
+ const mockPrices = [
+ {
+ startHeight: registrationBlockheight,
+ fees: {
+ 1: 571,
+ 2: 570,
+ 3: 569,
+ 4: 568,
+ 5: 567,
+ 6: 566,
+ 7: 565,
+ 8: 564,
+ 9: 563,
+ 10: 562,
+ 11: 561,
+ 12: 560,
+ 13: 559,
+ 14: 558,
+ 16: 556,
+ 17: 555,
+ 18: 554,
+ 19: 553,
+ 20: 552,
+ 21: 551,
+ },
+ },
+ ];
+
+ assert.throws(() => {
+ getAliasPrice(mockPrices, aliasLength, registrationBlockheight);
+ }, new Error(`fees[${aliasLength}] is undefined for ${registrationBlockheight}`));
+ });
+ it('getAliasPrice returns expected price for an alias registered in a price epoch older than the most recent price epoch', function () {
+ const registrationBlockheight = 750000;
+ const aliasLength = 21;
+ const mockPrices = [
+ {
+ startHeight: 785000,
+ fees: {
+ 1: 571,
+ 2: 570,
+ 3: 569,
+ 4: 568,
+ 5: 567,
+ 6: 566,
+ 7: 565,
+ 8: 564,
+ 9: 563,
+ 10: 562,
+ 11: 561,
+ 12: 560,
+ 13: 559,
+ 14: 558,
+ 15: 557,
+ 16: 556,
+ 17: 555,
+ 18: 554,
+ 19: 553,
+ 20: 552,
+ 21: 551,
+ },
+ },
+ {
+ startHeight: registrationBlockheight,
+ fees: {
+ 1: 1001,
+ 2: 1002,
+ 3: 1003,
+ 4: 1004,
+ 5: 1005,
+ 6: 1006,
+ 7: 1007,
+ 8: 1008,
+ 9: 1009,
+ 10: 1010,
+ 11: 1011,
+ 12: 1012,
+ 13: 1013,
+ 14: 1014,
+ 15: 1015,
+ 16: 1016,
+ 17: 1017,
+ 18: 1018,
+ 19: 1019,
+ 20: 1020,
+ 21: 1021,
+ },
+ },
+ ];
+ assert.strictEqual(
+ getAliasPrice(mockPrices, aliasLength, registrationBlockheight),
+ 1021,
+ );
+ });
+ it('getAliasPrice throws error if prices object is not properly sorted', function () {
+ const registrationBlockheight = 786000;
+ const aliasLength = 21;
+ const mockPrices = [
+ {
+ startHeight: 750000,
+ fees: {
+ 1: 1001,
+ 2: 1002,
+ 3: 1003,
+ 4: 1004,
+ 5: 1005,
+ 6: 1006,
+ 7: 1007,
+ 8: 1008,
+ 9: 1009,
+ 10: 1010,
+ 11: 1011,
+ 12: 1012,
+ 13: 1013,
+ 14: 1014,
+ 15: 1015,
+ 16: 1016,
+ 17: 1017,
+ 18: 1018,
+ 19: 1019,
+ 20: 1020,
+ 21: 1021,
+ },
+ },
+ {
+ startHeight: 785000,
+ fees: {
+ 1: 571,
+ 2: 570,
+ 3: 569,
+ 4: 568,
+ 5: 567,
+ 6: 566,
+ 7: 565,
+ 8: 564,
+ 9: 563,
+ 10: 562,
+ 11: 561,
+ 12: 560,
+ 13: 559,
+ 14: 558,
+ 15: 557,
+ 16: 556,
+ 17: 555,
+ 18: 554,
+ 19: 553,
+ 20: 552,
+ 21: 551,
+ },
+ },
+ ];
+
+ assert.throws(() => {
+ getAliasPrice(mockPrices, aliasLength, registrationBlockheight);
+ }, new Error('alias price epochs must be sorted by startHeight, highest to lowest'));
+ });
+ it('getAliasPrice throws error if prices object is not properly sorted, even if the first two epochs are', function () {
+ const registrationBlockheight = 790000;
+ const aliasLength = 21;
+ const mockPrices = [
+ {
+ startHeight: 785000,
+ fees: {
+ 1: 1001,
+ 2: 1002,
+ 3: 1003,
+ 4: 1004,
+ 5: 1005,
+ 6: 1006,
+ 7: 1007,
+ 8: 1008,
+ 9: 1009,
+ 10: 1010,
+ 11: 1011,
+ 12: 1012,
+ 13: 1013,
+ 14: 1014,
+ 15: 1015,
+ 16: 1016,
+ 17: 1017,
+ 18: 1018,
+ 19: 1019,
+ 20: 1020,
+ 21: 1021,
+ },
+ },
+ {
+ startHeight: 750000,
+ fees: {
+ 1: 571,
+ 2: 570,
+ 3: 569,
+ 4: 568,
+ 5: 567,
+ 6: 566,
+ 7: 565,
+ 8: 564,
+ 9: 563,
+ 10: 562,
+ 11: 561,
+ 12: 560,
+ 13: 559,
+ 14: 558,
+ 15: 557,
+ 16: 556,
+ 17: 555,
+ 18: 554,
+ 19: 553,
+ 20: 552,
+ 21: 551,
+ },
+ },
+ {
+ startHeight: 786000,
+ fees: {
+ 1: 2001,
+ 2: 2002,
+ 3: 2003,
+ 4: 2004,
+ 5: 2005,
+ 6: 2006,
+ 7: 2007,
+ 8: 2008,
+ 9: 2009,
+ 10: 2010,
+ 11: 2011,
+ 12: 2012,
+ 13: 2013,
+ 14: 2014,
+ 15: 2015,
+ 16: 2016,
+ 17: 2017,
+ 18: 2018,
+ 19: 2019,
+ 20: 2020,
+ 21: 2021,
+ },
+ },
+ ];
+
+ assert.throws(() => {
+ getAliasPrice(mockPrices, aliasLength, registrationBlockheight);
+ }, new Error('alias price epochs must be sorted by startHeight, highest to lowest'));
+ });
});

File Metadata

Mime Type
text/x-diff
Expires
Fri, Feb 7, 16:11 (1 d, 17 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5082668
Default Alt Text
(58 KB)

Event Timeline