Reorganizing code

This commit is contained in:
Richard Ramos 2018-04-20 20:48:09 -04:00
parent 9432694fa3
commit 56879acc00
5 changed files with 347 additions and 240 deletions

View File

@ -1,19 +1,17 @@
{ {
"blockchain": { "node": {
"account": "0x9e14016ba37b23498885864053fded5226161a3a",
"protocol": "ws",
"host": "localhost",
"port": 8545
},
"whisper": {
"symKey": "0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b",
"protocol": "ws", "protocol": "ws",
"host": "localhost", "host": "localhost",
"port": 8546, "port": 8546,
"ttl": 20, "blockchain": {
"minPow": 0.8, "account": "0x9e14016ba37b23498885864053fded5226161a3a"
"powTime": 1000 },
"whisper": {
"symKey": "0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b",
"ttl": 20,
"minPow": 0.8,
"powTime": 1000
}
}, },
"tokens": { "tokens": {

View File

@ -0,0 +1,111 @@
const md5 = require('md5');
class ContractSettings {
constructor(config, web3, eventEmitter){
this.tokens = config.tokens;
this.topics = [];
this.contracts = config.contracts;
this.web3 = web3;
this.events = eventEmitter;
this.pendingToLoad = 0;
this.events.on('setup:bytecode-address', this._obtainContractBytecode.bind(this))
}
process(){
this._setTokenPricePlugin();
this._processContracts();
}
getToken(token){
return this.tokens[token];
}
getContractByTopic(topicName){
return this.contracts[topicName];
}
getTopicName(contractName){
return this.web3.utils.toHex(contractName).slice(0, 10);
}
_setTokenPricePlugin(){
for(let token in this.tokens){
if(this.tokens[token].pricePlugin !== undefined){
let PricePlugin = require(this.tokens[token].pricePlugin);
this.tokens[token].pricePlugin = new PricePlugin(this.tokens[token]);
}
}
}
_determineBytecodeAddress(topicName, i){
let contractAddress = this.contracts[topicName].address;
if(this.contracts[topicName].isIdentity){
this.pendingToLoad++;
const lastKernelSignature = "0x4ac99424"; // REFACTOR
this.web3.eth.call({to: this.contracts[topicName].factoryAddress, data: lastKernelSignature})
.then(kernel => {
contractAddress = '0x' + kernel.slice(26);
this.events.emit('setup:bytecode-address', topicName, contractAddress);
})
}
}
_obtainContractBytecode(topicName, contractAddress){
this.web3.eth.getCode(contractAddress)
.then(code => {
this.contracts[topicName].code = md5(code);
this.pendingToLoad--;
if(this.pendingToLoad == 0) this.events.emit("setup:complete", this);
})
.catch(function(err){
console.error("Invalid contract for " + contractName);
console.error(err);
process.exit();
});
}
_extractFunctions(topicName){
const contract = this.getContractByTopic(topicName);
for(let i = 0; i < contract.allowedFunctions.length; i++){
contract.allowedFunctions[i].functionName = contract.allowedFunctions[i].function.slice(0, contract.allowedFunctions[i].function.indexOf('('));
// Extracting input
contract.allowedFunctions[i].inputs = contract.abi.filter(x => x.name == contract.allowedFunctions[i].functionName && x.type == "function")[0].inputs;
// Obtaining function signatures
let functionSignature = this.web3.utils.sha3(contract.allowedFunctions[i].function).slice(0, 10);
contract.allowedFunctions[functionSignature] = contract.allowedFunctions[i];
delete this.contracts[topicName].allowedFunctions[i];
}
contract.functionSignatures = Object.keys(contract.allowedFunctions);
this.contracts[topicName] = contract;
}
_processContracts(){
for(let contractName in this.contracts){
// Obtaining the abis
this.contracts[contractName].abi = require(this.contracts[contractName].abiFile);
const topicName = this.getTopicName(contractName);
// Extracting topic
this.topics.push(topicName);
this.contracts[topicName] = this.contracts[contractName];
this.contracts[topicName].name = contractName;
delete this.contracts[contractName];
this._determineBytecodeAddress(topicName);
this._extractFunctions(topicName);
}
}
}
module.exports = ContractSettings;

View File

@ -0,0 +1,182 @@
const md5 = require('md5');
const erc20ABI = require('../abi/ERC20.json');
const ganache = require("ganache-cli");
class MessageProcessor {
constructor(config, settings, web3, kId){
this.config = config;
this.settings = settings;
this.web3 = web3;
this.kId = kId;
}
_reply(text, message){
if(message.sig !== undefined){
this.web3.shh.post({
pubKey: message.sig,
sig: this.kId,
ttl: this.config.node.whisper.ttl,
powTarget:this.config.node.whisper.minPow,
powTime: this.config.node.whisper.powTime,
topic: message.topic,
payload: this.web3.utils.fromAscii(text)
}).catch(console.error);
}
}
async _validateInput(message, input){
const contract = this.settings.getContractByTopic(message.topic);
if(!/^0x[0-9a-f]{40}$/i.test(input.address)){
this._reply('Invalid address', message);
return false;
}
if(contract == undefined){
this._reply('Invalid topic', message);
return false;
}
if(!contract.functionSignatures.includes(input.functionName)){
this._reply('Function not allowed', message);
return false;
}
// Get code from address and compare it against the contract code
const code = md5(await this.web3.eth.getCode(input.address));
if(code != contract.code){
this._reply('Invalid contract code', message);
return false;
}
return true;
}
_extractInput(message){
return {
address: message.payload.slice(0, 42),
functionName: '0x' + message.payload.slice(42, 50),
functionParameters: '0x' + message.payload.slice(50),
payload: '0x' + message.payload.slice(42)
}
}
_obtainParametersFunc(contract, input){
const parameterList = this.web3.eth.abi.decodeParameters(contract.allowedFunctions[input.functionName].inputs, input.functionParameters);
return function(parameterName){
return parameterList[contract.allowedFunctions[input.functionName][parameterName]];
}
}
_getFactor(input, contract, gasToken){
if(contract.allowedFunctions[input.functionName].isToken){
return this.web3.utils.toBN(this.settings.getToken(gasToken).pricePlugin.getFactor());
} else {
return this.web3.utils.toBN(1);
}
}
async getBalance(token, input){
// Determining balances of token used
if(token.symbol == "ETH")
return new this.web3.utils.BN(await this.web3.eth.getBalance(input.address));
else {
const Token = new this.web3.eth.Contract(erc20ABI);
Token.options.address = params('gasToken');
return new this.web3.utils.BN(await Token.methods.balanceOf(input.address).call());
}
}
async process(error, message){
if(error){
console.error(error);
} else {
let input = this._extractInput(message);
const contract = this.settings.getContractByTopic(message.topic);
console.info("Processing request to: %s, %s", input.address, input.functionName);
if(!this._validateInput(message, input)) return; // TODO Log
const params = this._obtainParametersFunc(contract, input);
const token = this.settings.getToken(params('gasToken'));
if(token == undefined)
return reply("Token not allowed", message);
const gasPrice = this.web3.utils.toBN(params('gasPrice'));
const gasLimit = this.web3.utils.toBN(params('gasLimit'));
// Determine if enough balance for baseToken
if(contract.allowedFunctions[input.functionName].isToken){
const Token = new this.web3.eth.Contract(erc20ABI);
Token.options.address = params('token');
const baseToken = new this.web3.utils.BN(await Token.methods.balanceOf(input.address).call());
if(balance.lt(this.web3.utils.BN(params('value')))){
this._reply("Not enough balance", message);
return;
}
}
const balance = await this.getBalance(token, input);
const gasToken = params('gasToken');
const factor = this._getFactor(input, contract, gasToken);
const balanceInETH = balance.div(factor);
const gasLimitInETH = gasLimit.div(factor);
if(balanceInETH.lt(this.web3.utils.toBN(gasPrice.mul(gasLimit)))) {
this._reply("Not enough balance", message);
return;
}
// Estimate costs
const web3Sim = new Web3(ganache.provider({fork: `${config.node.protocol}://${config.node.host}:${config.node.port}`}));
const simAccounts = await web3Sim.eth.getAccounts();
let simulatedReceipt = await web3Sim.eth.sendTransaction({
from: simAccounts[0],
to: input.address,
value: 0,
data: input.payload
});
const estimatedGas = web3.utils.toBN(simulatedReceipt.gasUsed);
if(gasLimit.lt(estimatedGas)) {
return this._reply("Gas limit below estimated gas", message);
}
this.web3.eth.sendTransaction({
from: config.node.blockchain.account,
to: address,
value: 0,
data: input.payload,
gasLimit: gasLimitInETH
})
.then(function(receipt){
return this._reply("Transaction mined;" + receipt.transactionHash, message);
}).catch(function(err){
this._reply("Couldn't mine transaction", message);
// TODO log this?
console.error(err);
});
}
}
}
module.exports = MessageProcessor;

View File

@ -1,245 +1,71 @@
const md5 = require('md5'); const EventEmitter = require('events');
const Web3 = require('web3'); const Web3 = require('web3');
const config = require('../config/config.json'); const config = require('../config/config.json');
const web3 = new Web3(`${config.whisper.protocol}://${config.whisper.host}:${config.whisper.port}`);
var ganache = require("ganache-cli");
const erc20ABI = require('../abi/ERC20.json'); const ContractSettings = require('./contract-settings');
const MessageProcessor = require('./message-processor');
console.info("Starting...")
// TODO A node should call an API (probably from a status node) to register itself as a // TODO A node should call an API (probably from a status node) to register itself as a
// token gas relayer. // token gas relayer.
async function start(){ console.info("Starting...");
for(token in config.tokens){ const events = new EventEmitter();
if(config.tokens[token].pricePlugin !== undefined){
let PricePlugin = require(config.tokens[token].pricePlugin);
config.tokens[token].pricePlugin = new PricePlugin(config.tokens)
}
}
config.topics = []; // Web3 Connection
for(let contractName in config.contracts){ let connectionURL = `${config.node.protocol}://${config.node.host}:${config.node.port}`;
const web3 = new Web3(connectionURL);
// Obtaining the abis web3.eth.net.isListening()
config.contracts[contractName].abi = require(config.contracts[contractName].abiFile); .then(listening => events.emit('web3:connected', connectionURL))
.catch(error => {
const lngt = config.contracts[contractName].allowedFunctions.length; console.error(error);
for(i = 0; i < lngt; i++){ process.exit();
config.contracts[contractName].allowedFunctions[i].functionName = config.contracts[contractName].allowedFunctions[i].function.slice(0, config.contracts[contractName].allowedFunctions[i].function.indexOf('(')); });
// Extracting input
config.contracts[contractName].allowedFunctions[i].inputs = config.contracts[contractName].abi.filter(x => x.name == config.contracts[contractName].allowedFunctions[i].functionName && x.type == "function")[0].inputs;
// Obtaining function signatures
let functionSignature = web3.utils.sha3(config.contracts[contractName].allowedFunctions[i].function).slice(0, 10);
config.contracts[contractName].allowedFunctions[functionSignature] = config.contracts[contractName].allowedFunctions[i];
delete config.contracts[contractName].allowedFunctions[i];
}
config.contracts[contractName].functionSignatures = Object.keys(config.contracts[contractName].allowedFunctions);
// Extracting topics and available functions
let topicName = web3.utils.toHex(contractName).slice(0, 10);
config.topics.push(topicName);
config.contracts[topicName] = config.contracts[contractName];
config.contracts[topicName].name = contractName;
delete config.contracts[contractName];
// Get Contract Bytecode
let contractAddress = config.contracts[topicName].address;
if(config.contracts[topicName].isIdentity){
const lastKernelSignature = "0x4ac99424";
let kernel = await web3.eth.call({to: config.contracts[topicName].factoryAddress, data: lastKernelSignature});
contractAddress = '0x' + kernel.slice(26);
}
try { events.on('web3:connected', connURL => {
config.contracts[topicName].code = md5(await web3.eth.getCode(contractAddress)); console.info("Connected to '%s'", connURL);
} catch(err){ let settings = new ContractSettings(config, web3, events);
console.error("Invalid contract for " + contractName); settings.process();
console.error(err); });
process.exit();
}
}
// Setting up Whisper options
const shhOptions = {
ttl: config.whisper.ttl,
minPow: config.whisper.minPow,
};
let kId; events.on('setup:complete', (settings) => {
let symKId; // Setting up Whisper options
// Listening to whisper const shhOptions = {
ttl: config.node.whisper.ttl,
minPow: config.node.whisper.minPow,
};
web3.shh.addSymKey(config.whisper.symKey) let kId;
let symKId;
// Listening to whisper
web3.shh.addSymKey(config.node.whisper.symKey)
.then(symKeyId => { .then(symKeyId => {
symKId = symKeyId; symKId = symKeyId;
return web3.shh.newKeyPair(); return web3.shh.newKeyPair();
}) })
.then(keyId => { .then(keyId => {
shhOptions.symKeyId = symKId; shhOptions.symKeyId = symKId;
shhOptions.kId = keyId;
kId = keyId; console.info(`Sym Key: ${config.node.whisper.symKey}`);
console.info(`Sym Key: ${config.whisper.symKey}`);
console.info("Topics Available:"); console.info("Topics Available:");
for(let contract in settings.contracts) {
config.topics = []; console.info("- %s: %s [%s]", settings.getContractByTopic(contract).name, contract, Object.keys(settings.getContractByTopic(contract).allowedFunctions).join(', '));
for(let contractName in config.contracts) { shhOptions.topics = [contract];
console.info("- %s: %s [%s]", config.contracts[contractName].name, contractName, Object.keys(config.contracts[contractName].allowedFunctions).join(', ')); events.emit('server:listen', shhOptions, settings);
shhOptions.topics = [contractName];
web3.shh.subscribe('messages', shhOptions, processMessages);
} }
console.info("Started.");
console.info("Listening for messages...")
}); });
});
events.on('server:listen', (shhOptions, settings) => {
const reply = async function(text, message){ let processor = new MessageProcessor(config, settings, web3, shhOptions.kId);
try { web3.shh.subscribe('messages', shhOptions, (error, message, subscription) => processor.process(error, message));
if(message.sig !== undefined){ });
let shhOptions = {
pubKey: message.sig,
sig: kId,
ttl: config.whisper.ttl,
powTarget:config.whisper.minPow,
powTime: config.whisper.powTime,
topic: message.topic,
payload: web3.utils.fromAscii(text)
};
await web3.shh.post(shhOptions);
}
} catch(Err){
// TODO
console.error(Err);
}
}
// Process individual whisper message
const processMessages = async function(error, message, subscription){
if(error){
// TODO log
console.error(error);
} else {
const address = message.payload.slice(0, 42);
const functionName = '0x' + message.payload.slice(42, 50);
const functionParameters = '0x' + message.payload.slice(50);
const payload = '0x' + message.payload.slice(42);
console.info("Processing request to: %s, %s", address, functionName);
if(!/^0x[0-9a-f]{40}$/i.test(address))
return reply('Invalid address', message);
if(config.contracts[message.topic] == undefined)
return reply('Invalid topic', message);
const contract = config.contracts[message.topic];
if(!contract.functionSignatures.includes(functionName))
return reply('Function not allowed', message) // TODO Log this
// Get code from address and compare it against the contract code
const code = md5(await web3.eth.getCode(address));
if(code != contract.code){
return reply('Invalid contract code', message); // TODO Log this
}
const params = web3.eth.abi.decodeParameters(contract.allowedFunctions[functionName].inputs, functionParameters);
const token = config.tokens[params[contract.allowedFunctions[functionName].gasToken]];
if(token == undefined){
return reply("Token not allowed", message);
}
const gasPrice = web3.utils.toBN(params[contract.allowedFunctions[functionName].gasPrice]);
const gasLimit = web3.utils.toBN(params[contract.allowedFunctions[functionName].gasLimit]);
// Determining balances of token used
let balance;
if(token.symbol == "ETH")
balance = new web3.utils.BN(await web3.eth.getBalance(address));
else {
const Token = new web3.eth.Contract(erc20ABI);
Token.options.address = params[contract.allowedFunctions[functionName].gasToken];
balance = new web3.utils.BN(await Token.methods.balanceOf(address).call());
}
// Determine if enough balance for baseToken
if(contract.allowedFunctions[functionName].isToken){
const Token = new web3.eth.Contract(erc20ABI);
Token.options.address = params[contract.allowedFunctions[functionName].token];
balance = new web3.utils.BN(await Token.methods.balanceOf(address).call());
if(balance.lt(web3.utils.BN(params[contract.allowedFunctions[functionName].value]))){
return reply("Not enough balance", message);
}
}
// Obtain factor
let factor;
if(contract.allowedFunctions[functionName].isToken){
factor =web3.utils.toBN(config.tokens[tokenAddress].pricePlugin.getFactor());
} else {
factor = web3.utils.toBN(1);
}
const balanceInETH = balance.div(factor);
const gasLimitInETH = gasLimit.div(factor);
if(balanceInETH.lt(web3.utils.toBN(gasPrice.mul(gasLimit)))) {
return reply("Not enough balance", message);
}
// Estimate costs
const web3Sim = new Web3(ganache.provider({fork: `http://localhost:8545`}));
const simAccounts = await web3Sim.eth.getAccounts();
let simulatedReceipt = await web3Sim.eth.sendTransaction({
from: simAccounts[0],
to: address,
value: 0,
data: payload
});
const estimatedGas = web3.utils.toBN(simulatedReceipt.gasUsed);
console.log(simulatedReceipt);
if(gasLimit.lt(estimatedGas)) {
return reply("Gas limit below estimated gas", message);
}
web3.eth.sendTransaction({
from: config.blockchain.account,
to: address,
value: 0,
data: payload,
gasLimit: gasLimitInETH
})
.then(function(receipt){
return reply("Transaction mined;" + receipt.transactionHash, message);
}).catch(function(err){
reply("Couldn't mine transaction", message);
// TODO log this?
//console.error(err);
});
}
}
}
start();
// Daemon helper functions // Daemon helper functions
@ -248,14 +74,6 @@ process.on("uncaughtException", function(err) {
}); });
process.on("SIGUSR1", function() {
log("Reloading...");
log("Reloaded.");
});
process.once("SIGTERM", function() { process.once("SIGTERM", function() {
log("Stopping..."); log("Stopping...");
}); });

View File

@ -40,8 +40,6 @@ contract TestIdentityFactory {
function TestIdentityFactory(){ function TestIdentityFactory(){
latestKernel = address(new TestIdentityGasRelay()); latestKernel = address(new TestIdentityGasRelay());
} }
} }
contract TestSNTController { contract TestSNTController {