Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Services API endpoints to support Monero / multiple cryptocurrencies #54

Open
monicanagent opened this issue Mar 21, 2019 · 1 comment
Assignees
Labels
enhancement New feature or request help wanted Extra attention is needed
Milestone

Comments

@monicanagent
Copy link
Owner

monicanagent commented Mar 21, 2019

This task is preceded by #53

Overview

With the adapter in place, the CP_Account API endpoint must be updated to use it while maintaining existing functionality. Functions should be updated in place whenever possible -- that is, names and existing parameters should not change.

Functional Branching

To enable branching based on cryptocurrency, if not already supported, additional parameters may be added that should default to existing functionality.

For example, if the following function needs to be updated to support Monero and other cryptocurrencies:

function makeHDWallet(privKey) {
try {
if (privKey.indexOf("xprv") == 0) {
//mainnet
var wallet = bitcoin.bip32.fromBase58(privKey);
} else {
//testnett
wallet = bitcoin.bip32.fromBase58(privKey, bitcoin.networks.testnet);
}
} catch (err) {
console.error(err.stack);
return (null);
}
if (wallet == undefined) {
return (null);
}
return (wallet);

... an additional parameter should be added that will default to the current functionality if not supplied:

function makeHDWallet(privKey, currencyType="bitcoin") {
if (currencyType == "bitcoin") {
      try {
         if (privKey.indexOf("xprv") == 0) {
            //mainnet
            var wallet = bitcoin.bip32.fromBase58(privKey);
         } else {
            //testnett
            wallet = bitcoin.bip32.fromBase58(privKey, bitcoin.networks.testnet);
         }
      } catch (err) {
         console.error(err.stack);
         return (null);
      }
     if (wallet == undefined) {
         return (null);
      }
      return (wallet);
   } else if (currencyType == "monero") {
     //make a Monero HD wallet
   }
}

Since any existing calls are providing only one parameter and the new, second parameter defaults to "bitcoin", any current functionality will continue to work while any new functionality can incorporate the new parameter.

That being said, anywhere that makeHDWallet is called will probably still need to be updated since the HD wallet for the new cryptocurrency will need to be generated somewhere.

Luckily, on the server side there are only two API endpoints that include cryptocurrency transactions; primarily CP_Account, and sometimes CP_SmartContract.

Additionally, startup and configuration information for the newly-added cryptocurrency will need to be processed at server startup:

/**
* Adjusts various settings and properties based on the detected runtime environment. For example,
* Zeit (NOW) deployments require only a single WebSocket connection that must be on port 80 (this
* may change).
*/
function adjustEnvironment() {
//NOW_DC and NOW_REGION are defined for Zeit's Data Centre settings
if ((typeof(process.env["NOW_DC"]) == "string") && (typeof(process.env["NOW_REGION"]) == "string")) {
console.log ("Detected Zeit (NOW) runtime environment.")
rpc_options.http_port = -1; //disable HTTP server altogether
rpc_options.ws_port = 80; //WebSocket server can only listen on port 80 (forwarded from a secure connection)
rpc_options.http_only_handshake = false; //enable WebSockets for handshakes (since HTTP server is disabled)
}
}
/**
* Creates and initializes the account system using either environmental settings,
* command line parameters, or configuration options (in that order).
*
* @param {Function} [onCreateCB=null] An optional callback function to invoke when the
* account system has been successfully created. This callback will not be invoked
* if an error occurs during creation.
*
* @async
* @private
*/
async function createAccountSystem(onCreateCB=null) {
if (typeof(process.env["BLOCKCYPHER_TOKEN"]) == "string") {
config.CP.API.tokens.blockcypher = process.env["BLOCKCYPHER_TOKEN"];
}
if (typeof(process.env["DB_URL"]) == "string") {
config.CP.API.database.url = process.env["DB_URL"];
}
if (typeof(process.env["DB_HOST"]) == "string") {
config.CP.API.database.host = process.env["DB_HOST"];
}
if (typeof(process.env["DB_ACCESS_KEY"]) == "string") {
config.CP.API.database.accessKey = process.env["DB_ACCESS_KEY"];
}
if (typeof(process.env["WALLET_XPRV"]) == "string") {
config.CP.API.wallets.bitcoin.xprv = process.env["WALLET_XPRV"];
}
if (typeof(process.env["WALLET_TPRV"]) == "string") {
config.CP.API.wallets.test3.tprv = process.env["WALLET_TPRV"];
}
//try updating via command line arguments:
for (var count = 2; count < process.argv.length; count++) {
var currentArg = process.argv[count];
var splitArg = currentArg.split("=");
if (splitArg.length < 2) {
throw (new Error("Malformed command line argument: "+currentArg));
}
var argName = new String(splitArg[0]);
var joinedArr = new Array();
joinedArr.push (splitArg[1]);
//there may have been more than one "=" in the argument
for (count2 = 2; count2 < splitArg.length; count2++) {
joinedArr.push(splitArg[count2]);
}
var argValue = joinedArr.join("=");
switch (argName.toLowerCase()) {
case "blockcypher_token":
config.CP.API.tokens.blockcypher = argValue;
break;
case "db_url":
config.CP.API.database.url = argValue;
break;
case "db_host":
config.CP.API.database.host = argValue;
break;
case "db_access_key":
config.CP.API.database.accessKey = argValue;
break;
case "wallet_xprv":
onfig.CP.API.wallets.bitcoin.xprv = argValue;
break;
case "wallet_tprv":
onfig.CP.API.wallets.test3.tprv = argValue;
break;
default:
//unrecognized command line parameter
break;
}
}
//create HD wallets if possible
namespace.cp.bitcoinWallet = namespace.cp.makeHDWallet(config.CP.API.wallets.bitcoin.xprv);
if (namespace.cp.bitcoinWallet != null) {
var walletPath = config.CP.API.bitcoin.default.main.cashOutAddrPath;
var cashoutWallet = namespace.cp.bitcoinWallet.derivePath(walletPath);
console.log ("Bitcoin HD wallet (\""+walletPath+"\") configured @ "+namespace.cp.getAddress(cashoutWallet));
} else {
console.log ("Could not configure Bitcoin wallet.");
}
namespace.cp.bitcoinTest3Wallet = namespace.cp.makeHDWallet(config.CP.API.wallets.test3.tprv);
if (namespace.cp.bitcoinTest3Wallet != null) {
walletPath = config.CP.API.bitcoin.default.test3.cashOutAddrPath;
cashoutWallet = namespace.cp.bitcoinTest3Wallet.derivePath(walletPath);
console.log ("Bitcoin testnet HD wallet (\""+walletPath+"\") configured @ "+namespace.cp.getAddress(cashoutWallet, bitcoin.networks.testnet));
} else {
console.log ("Could not configure Bitcoin testnet wallet.");
}
var wallets = config.CP.API.wallets;
if (config.CP.API.database.enabled == true) {
console.log ("Database functionality is ENABLED.");
//the second parameter is there to provide a value for the HMAC
try {
var walletStatusObj = await namespace.cp.callAccountDatabase("walletstatus", {"random":String(Math.random())});
} catch (err) {
console.error ("Could not get current wallet status.");
console.error (err);
console.error ("Trying again in 5 seconds...");
setTimeout(5000, createAccountSystem, onCreateCB);
return (false);
}
var resultObj = walletStatusObj.result;
//force-convert values in case the database returned them as strings
var btcStartChain = Number(String(resultObj.bitcoin.main.startChain));
var btcStartIndex = Number(String(resultObj.bitcoin.main.startIndex));
var test3StartChain = Number(String(resultObj.bitcoin.test3.startChain));
var test3StartIndex = Number(String(resultObj.bitcoin.test3.startIndex));
if (btcStartChain > wallets.bitcoin.startChain) {
wallets.bitcoin.startChain = Number(String(resultObj.bitcoin.main.startChain));
}
if (btcStartIndex > wallets.bitcoin.startIndex) {
wallets.bitcoin.startIndex = Number(String(resultObj.bitcoin.main.startIndex));
}
if (test3StartChain > wallets.test3.startChain) {
wallets.test3.startChain = Number(String(resultObj.bitcoin.test3.startChain));
}
if (test3StartIndex > wallets.test3.startIndex) {
wallets.test3.startIndex = Number(String(resultObj.bitcoin.test3.startIndex));
}
} else {
console.log ("Database functionality is DISABLED.");
}
console.log ("Initial Bitcoin account derivation path: m/"+wallets.bitcoin.startChain+"/"+(wallets.bitcoin.startIndex+1));
console.log ("Initial Bitcoin testnet account derivation path: m/"+wallets.test3.startChain+"/"+(wallets.test3.startIndex+1));
if (config.CP.API.database.enabled == true) {
if (hostEnv.embedded == true) {
console.log ("Local database size: "+resultObj.db.sizeMB+" megabytes");
console.log ("Local database limit: "+resultObj.db.maxMB+" megabytes");
} else {
console.log ("Remote database size: "+resultObj.db.sizeMB+" megabytes");
console.log ("Remote database limit: "+resultObj.db.maxMB+" megabytes");
}
console.log ("Database last updated "+resultObj.db.elapsedUpdateSeconds+" seconds ago");
} else {
console.log ("Using in-memory storage instead of database.");
}
onCreateCB();
return (true);
}
/**
* Invokes the "onInit" function in the host desktop (Electron) environment when
* all initialization routines have completed. If the server is not running as
* an embedded component this function does nothing.
*
* @private
*/
function invokeHostOnInit() {
if (hostEnv.embedded == true) {
try {
hostEnv.server.onInit();
} catch (err) {
console.error("Couldn't invoke host environment \"onInit\" function: "+err.stack);
}
}
}
/**
* Function invoked after the main configuration data is loaded and parsed but
* before API functions are loaded, the account system initialized, and the
* server endpoints started.
*
* @async
* @private
*/
async function postLoadConfig() {
if ((hostEnv.embedded == true) && (config.CP.API.database.enabled == true)) {
var url = config.CP.API.database.url;
var transport = url.split("://")[0];
if (transport == "https") {
transport = "http";
}
if (transport == "sqlite3") {
//using local SQLite 3 database
var dbFilePath = url.split(transport+"://").join("");
try {
//console.dir (hostEnv.host);
//startDatabase function exposed by host (Electron) environment
var started = await startDatabase(transport);
if (started == true) {
var opened = await hostEnv.database.sqlite3.adapter.openDBFile(dbFilePath);
if (opened == false) {
console.log ("Couldn't open SQLite 3 database: "+dbFilePath);
}
} else {
console.error ("Could not start SQLite 3 database.");
}
} catch (err) {
//couldn't open database
return(false);
}
} else {
//using rdb or other account storage
}
}
loadAPIFunctions(startHTTPServer, startWSServer); //load available API functions and then start servers
try {
var result = createAccountSystem(invokeHostOnInit);
} catch(error) {
return (false);
}
return (true);
}
//Application entry point:
if ((this["electronEnv"] != undefined) && (this["electronEnv"] != null)) {
hostEnv = this.electronEnv;
hostEnv.embedded = true;
console.log ("Launching in desktop embedded (Electron) mode.");
console.log ("Server path prefix: "+hostEnv.dir.server);
console.log ("Client path prefix: "+hostEnv.dir.client);
} else {
console.log ("Launching in standalone mode.");
}
//load external configuration data from default location
loadConfig().then (configObj => {
console.log ("Configuration data successfully loaded and parsed.");
rpc_options.exposed_objects.config = config;
if (config.CP.API.RPC.http.enabled == true) {
rpc_options.http_port = config.CP.API.RPC.http.port;
} else {
rpc_options.http_port = -1;
}
if (config.CP.API.RPC.wss.enabled == true) {
rpc_options.ws_port = config.CP.API.RPC.wss.port;
} else {
rpc_options.ws_port = -1;
}
adjustEnvironment(); //adjust for local runtime environment
if (postLoadConfig() == true) {
//account system successfully created immediately
} else {
//account system not yet successfully created
}
}).catch (err => {
console.error ("Couldn't load or parse configuration data.");
console.error (err);
})

As with the adapter, it's best to keep any newly-added functionality in modular form. That is, as async (where required) functions that are passed references rather than relying on static locations of objects such as the main configuration.

Function Updates

The functions that can be may need to be updated or re-created include:

function makeHDWallet(privKey) {

function getAddress(walletObj, network=null) {

function getNewAddress(APIType="bitcoin", network=null) {

function getBlockchainBalance(address, APIType="bitcoin", network=null) {

async function cashoutToAddress(toAddress, amount, fees=null, APIType="bitcoin", network=null) {

async function sendTransaction(fromWallet, toAddress, amount, fees=null, APIType="bitcoin", network=null) {

function newBTCTx (fromAddress, toAddress, satAmount, satFees, network=null) {

function signBTCTx (txObject, WIF, network=null) {

function signToDER (toSignHex, privateKeyBuffer) {

function sendBTCTx(txObject, network=null) {

async function estimateTxFee (txData=null, priority=1, APIType="bitcoin", network=null) {

function updateTxFees(APIType="bitcoin", network=null, forceUpdate=false) {

Obviously some of these functions are fairly specific to Bitcoin so the creation of new functions in their place may be good idea.

When it comes to generic functions such as cashOutToAddress or sendTransaction, however, functional branching (if..else, switch..case, etc.), within the functions makes sense since they can be applied more broadly than just for a single cryptocurrency (e.g. Bitcoin). If in doubt, branch within the function rather than creating a new one.

Additional information, including code documentation and formatting, can be found here: #48

@monicanagent monicanagent added enhancement New feature or request help wanted Extra attention is needed labels Mar 21, 2019
@monicanagent monicanagent self-assigned this Mar 21, 2019
@monicanagent monicanagent added this to To do in CypherPoker.JS via automation Mar 21, 2019
@monicanagent monicanagent changed the title Update CP_Account API endpoint to support Monero / multiple cryptocurrencies Update Services API endpoints to support Monero / multiple cryptocurrencies Mar 21, 2019
@monicanagent monicanagent added this to the v0.5.1 milestone Mar 23, 2019
@monicanagent monicanagent modified the milestones: v0.5.1, v0.5.2 Jun 27, 2019
@monicanagent
Copy link
Owner Author

Server functions have been updated to be generic and routed to appropriate cryptocurrency handlers as of Bitcoin Cash integration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
CypherPoker.JS
  
To do
Development

No branches or pull requests

1 participant