diff --git a/README.md b/README.md index 77c7309..93eea5f 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,9 @@ Before executing this program, `config/config.json` must be setup and `npm insta - Host, port and protocol Ganache will use when forking the blockchain for gas estimations and other operations - Wallet account used for processing the transactions - Symmetric key used to receive the Whisper messages -- Symmetric key used to send the heartbeats that notify the tokens and prices accepted - Accepted tokens information - Contract configuration + This program is configured with the default values for a embark installation run from 0 A `geth` node running whisper (via `-shh` option) is required. To execute the gas-relayer, you may use any of the following three methods. @@ -45,27 +45,103 @@ The gas relayer service needs to be running, and configured correctly to process -## Additional notes -How to send a message to this service (all accounts and privatekeys should be replaced by your own test data) +## Using the gas relayer + +### The relayer +A node that wants to act as a relayer only needs to have a geth node with whisper enabled, and an account with ether to process the transactions. This account and node need to be configured in `./config/config.js`. + +The relayer will be subscribed to receive messages in a specific symkey (this will change in the future to use ENS), and will reply back to both availability and transaction requests + +### The user + +#### Sending a message to the gas relayer network (all accounts and privatekeys should be replaced by your own test data) ``` -shh.post({symKeyID: SYM_KEY, sig: WHISPER_KEY_ID, ttl: 1000, powTarget: 1, powTime: 20, topic: TOPIC_NAME, payload: PAYLOAD_BYTES}); -``` -- `SYM_KEY` must contain the whisper symmetric key used. It is shown on the console when running the service with `node`. With the provided configuration you can use the value: -``` -0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b` +shh.post({ + symKeyID: SYM_KEY, // If requesting availability + pubKey: PUBLIC_KEY_ID, // If sending a transaction + sig: WHISPER_KEY_ID, + ttl: 1000, + powTarget: 1, + powTime: 20, + topic: TOPIC_NAME, + payload: PAYLOAD_BYTES +}).then(......) ``` +- `symKeyID: SYM_KEY` must contain the whisper symmetric key used. It is shown on the console when running the service with `node`. With the provided configuration you can use the symmetric key `0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b`. Only used when asking for relayer availability. +- `pubKey: PUBLIC_KEY_ID`. After asking for availability, once the user decides on a relayer, it needs to set the `pubKey` attribute with the relayer public key (received in the availability reply in the `sig` attribute of the message). - `WHISPER_KEY_ID` represents a keypair registered on your node, that will be used to sign the message. Can be generated with `web3W.shh.newKeyPair()` - `TOPIC_NAME` must contain one of the topic names generated based on converting the contract name to hex, and taking the first 8 bytes. For the provided configuration the following topics are available: - - IdentityGasRelay: `0x4964656e` - - SNTController: `0x534e5443` -- `PAYLOAD_BYTES` a hex string that contains the identity/contract address to invoke and the web3 encoded abi function invocation plus parameters. If we were to execute `callGasRelayed(address,uint256,bytes,uint256,uint256,uint256,address,bytes)` (part of the IdentityGasRelay) in contract `0x692a70d2e424a56d2c6c27aa97d1a86395877b3a`, with these values: `"0x11223344556677889900998877665544332211",100,"0x00",1,10,20,"0x1122334455112233445511223344551122334455"`, "0x1122334455", `PAYLOAD_BYTES` can be prepared as follows: +- `PAYLOAD_BYTES` a hex string that contains details on the operation to perform. +#### Polling for gas relayers +The first step is asking the relayers for their availability. The message payload needs to be the hex string representation of a JSON object with a specific structure: ``` +const payload = web3.utils.toHex({ + 'contract': "0xContractToInvoke", + 'address': web3.eth.defaultAccount, + 'action': 'availability', + 'token': "0xGasTokenAddress", + 'gasPrice': 1234 + }); +``` +- `contract` is the address of the contract that will perform the operation, in this case it can be an Identity, or the SNTController. +- `address` The address that will sign the transactions. Normally it's `web3.eth.defaultAccount` +- `gasToken`: token used for paying the gas cost +- `gasPrice`: The gas price used for the transaction + +This is a example code of how to send an 'availability' message: + +``` const whisperKeyPairID = await web3W.shh.newKeyPair(); -const jsonAbi = ABIOfIdentityGasRelay.filter(x => x.name == "callGasRelayed")[0]; + +const msgObj = { + symKeyId: "0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b", + sig: whisperKeyPairID, + ttl: 1000, + powTarget: 1, + powTime: 20, + topic: "0x4964656e", + payload: web3.utils.toHex({ + 'contract': "0x692a70d2e424a56d2c6c27aa97d1a86395877b3a", + 'address': web3.eth.defaultAccount + 'action': 'availability', + 'gasToken': "0x744d70fdbe2ba4cf95131626614a1763df805b9e", + 'gasPrice': 40000000000 // 40 gwei equivalent in SNT + }) +}; + + +web3.shh.post(msgObj) +.then((err, result) => { + console.log(result); + console.log(err); +}); +``` + +When it replies, you need to extract the `sig` attribute to obtain the relayer's public key + +#### Sending transaction details + +Sending a transaction is similar to the previous operation, except that we send the message to an specific node, we use the action `transaction`, and also we send a `encodedFunctionCall` with the details of the transaction to execute. + +From the list of relayers received via whisper messages, you need to extract the `message.sig` to obtain the `pubKey`. This value is used to send the transaction to that specific relayer. + +`encodedFunCall` is the hex data used obtained from `web3.eth.abi.encodeFunctionCall` for the specific function we want to invoke. + +If we were to execute `callGasRelayed(address,uint256,bytes,uint256,uint256,uint256,address,bytes)` (part of the IdentityGasRelay) in contract `0x692a70d2e424a56d2c6c27aa97d1a86395877b3a`, with these values: `"0x11223344556677889900998877665544332211",100,"0x00",1,10,20,"0x1122334455112233445511223344551122334455"`, "0x1122334455", `PAYLOAD_BYTES` can be prepared as follows: + +``` +// The following values are created obtained when polling for relayers +const whisperKeyPairID = await web3W.shh.newKeyPair(); +const relayerPubKey = "0xRELAYER_PUBLIC_KEY_HERE"; +// ... +// ... +const jsonAbi = ABIOfIdentityGasRelay.find(x => x.name == "callGasRelayed"); const funCall = web3.eth.abi.encodeFunctionCall(jsonAbi, [ @@ -80,7 +156,7 @@ const funCall = web3.eth.abi.encodeFunctionCall(jsonAbi, ]); const msgObj = { - symKeyID: "0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b", + pubKey: relayerPubKey, sig: whisperKeyPairID, ttl: 1000, powTarget: 1, @@ -88,8 +164,9 @@ const msgObj = { topic: "0x4964656e", payload: web3.utils.toHex({ 'contract': "0x692a70d2e424a56d2c6c27aa97d1a86395877b3a", - 'encodedFunctionCall': funCall, 'address': web3.eth.defaultAccount + 'action': 'transaction', + 'encodedFunctionCall': funCall, }) }; @@ -100,4 +177,9 @@ web3.shh.post(msgObj) console.log(err); }); -``` \ No newline at end of file +``` + + +#### Valid operations +TODO + diff --git a/gas-relayer/.eslintrc b/gas-relayer/.eslintrc index 776e80e..13108e4 100644 --- a/gas-relayer/.eslintrc +++ b/gas-relayer/.eslintrc @@ -275,7 +275,7 @@ "error", "never" ], - "valid-jsdoc": "error", + "valid-jsdoc": "off", "vars-on-top": "off", "wrap-iife": "error", "wrap-regex": "error", diff --git a/gas-relayer/config/config.js b/gas-relayer/config/config.js index 60d1703..6ca7d91 100644 --- a/gas-relayer/config/config.js +++ b/gas-relayer/config/config.js @@ -21,11 +21,6 @@ module.exports = { "powTime": 1000 } }, - - "heartbeat": { - "enabled": true, - "symKey": "0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b" - }, "tokens": { "0x0000000000000000000000000000000000000000": { diff --git a/gas-relayer/src/contract-settings.js b/gas-relayer/src/contract-settings.js index 09bcad1..6bb0744 100644 --- a/gas-relayer/src/contract-settings.js +++ b/gas-relayer/src/contract-settings.js @@ -1,5 +1,13 @@ +/** + * Configuration Settings related to contracts + */ class ContractSettings { + /** + * @param {object} config - Configuration object obtained from `./config/config.js` + * @param {object} web3 - Web3 object already configured + * @param {object} eventEmitter - Event Emitter + */ constructor(config, web3, eventEmitter){ this.tokens = config.tokens; this.topics = []; @@ -12,11 +20,17 @@ class ContractSettings { this.pendingToLoad = 0; } + /** + * Process configuration file + */ process(){ this._setTokenPricePlugin(); this._processContracts(); } + /** + * Set price plugin for token + */ _setTokenPricePlugin(){ for(let token in this.tokens){ if(this.tokens[token].pricePlugin !== undefined){ @@ -26,16 +40,30 @@ class ContractSettings { } } + /** + * Get allowed tokens + * @return {object} - Dictionary with allowed tokens (address as key) + */ getTokens(){ return this.tokens; } + /** + * Get token by address + * @param {string} - Token address + * @return {object} - Token details + */ getToken(token){ const tokenObj = this.tokens[token]; tokenObj.address = token; return tokenObj; } + /** + * Get token by symbol + * @param {string} - Token symbol + * @return {object} - Token details + */ getTokenBySymbol(symbol){ for(let token in this.tokens){ if(this.tokens[token].symbol == symbol){ @@ -46,14 +74,28 @@ class ContractSettings { } } + /** + * Get contract by topicName + * @param {string} topicName - Topic name that represents a contract + * @return {object} - Contract details + */ getContractByTopic(topicName){ return this.contracts[topicName]; } + /** + * Calculate the topic based on the contract's name + * @param {string} contractName - Name of the contract as it appears in the configuration + * @return {string} - Topic + */ getTopicName(contractName){ return this.web3.utils.toHex(contractName).slice(0, 10); } + /** + * Set contract's bytecode in the configuration + * @param {string} topicName - Topic name that represents a contract + */ async _obtainContractBytecode(topicName){ if(this.contracts[topicName].isIdentity) return; @@ -71,6 +113,10 @@ class ContractSettings { } } + /** + * Extract function details based on topicName + * @param {string} topicName - Topic name that represents a contract + */ _extractFunctions(topicName){ const contract = this.getContractByTopic(topicName); @@ -90,6 +136,9 @@ class ContractSettings { this.contracts[topicName] = contract; } + /** + * Process contracts and setup the settings object + */ _processContracts(){ for(let contractName in this.contracts){ // Obtaining the abis @@ -105,8 +154,7 @@ class ContractSettings { // Obtaining strategy if(this.contracts[topicName].strategy){ - const strategy = require(this.contracts[topicName].strategy); - this.contracts[topicName].strategy = new strategy(this.web3, this.config, this, this.contracts[topicName]); + this.contracts[topicName].strategy = this.buildStrategy(this.contracts[topicName].strategy, topicName); } this._obtainContractBytecode(topicName); @@ -114,6 +162,16 @@ class ContractSettings { this._extractFunctions(topicName); } } + + /** + * Create strategy object based on source code and topicName + * @param {string} strategyFile - Souce code path of strategy to build + * @param {string} topicName - Hex string that represents a contract's topic + */ + buildStrategy(strategyFile, topicName){ + const strategy = require(strategyFile); + return new strategy(this.web3, this.config, this, this.contracts[topicName]); + } } diff --git a/gas-relayer/src/message-processor.js b/gas-relayer/src/message-processor.js index 1e03ae8..78292ad 100644 --- a/gas-relayer/src/message-processor.js +++ b/gas-relayer/src/message-processor.js @@ -1,5 +1,14 @@ +/** + * Message Processor to analyze and execute strategies based on input objects + */ class MessageProcessor { + /** + * @param {object} config - Configuration object obtained from `./config/config.js` + * @param {object} settings - Settings obtained from parsing the configuration object + * @param {object} web3 - Web3 object already configured + * @param {object} events - Event emitter + */ constructor(config, settings, web3, events){ this.config = config; this.settings = settings; @@ -7,14 +16,20 @@ class MessageProcessor { this.events = events; } + /** + * Validate input message content + * @param {object} contract - Object obtained from the settings based on the message topic + * @param {object} input - Object obtained from a message. + * @returns {object} State of validation + */ async _validateInput(contract, input){ - console.info("Processing request to: %s, %s", input.contract, input.functionName); + console.info("Processing '%s' request to contract: %s", input.action, input.contract); if(contract == undefined){ return {success: false, message: 'Unknown contract'}; } - if(!contract.functionSignatures.includes(input.functionName)){ + if(input.functionName && !contract.functionSignatures.includes(input.functionName)){ return {success: false, message: 'Function not allowed'}; } @@ -37,7 +52,15 @@ class MessageProcessor { return {success: true}; } - async process(contract, input, reply){ + /** + * Process strategy and return validation result + * @param {object} contract - Object obtained from the settings based on the message topic + * @param {object} input - Object obtained from a message. + * @param {function} reply - Function to reply a message + * @param {object} strategy - Strategy to apply. If undefined, it will use a strategy based on the contract + * @returns {object} State of validation + */ + async processStrategy(contract, input, reply, strategy){ const inputValidation = await this._validateInput(contract, input); if(!inputValidation.success){ // TODO Log? @@ -45,16 +68,33 @@ class MessageProcessor { return; } - let validationResult; + if(strategy || contract.strategy){ + let validationResult; + if(strategy){ + validationResult = await strategy.execute(input, reply); + } else { + validationResult = await contract.strategy.execute(input, reply); + } - if(contract.strategy){ - validationResult = await contract.strategy.execute(input, reply); if(!validationResult.success){ reply(validationResult.message); return; } - } + return validationResult; + } + } + + /** + * Process strategy and based on its result, send a transaction to the blockchain + * @param {object} contract - Object obtained from the settings based on the message topic + * @param {object} input - Object obtained from a message. + * @param {function} reply - function to reply a message + * @returns {undefined} + */ + async processTransaction(contract, input, reply){ + const validationResult = await this.processStrategy(contract, input, reply); + let p = { from: this.config.node.blockchain.account, to: input.contract, @@ -79,7 +119,7 @@ class MessageProcessor { try { const receipt = await this.web3.eth.sendTransaction(p); // TODO: parse events - return reply("Transaction mined", receipt); + reply("Transaction mined", receipt); } catch(err){ reply("Couldn't mine transaction: " + err.message); // TODO log this? diff --git a/gas-relayer/src/service.js b/gas-relayer/src/service.js index 4c7d981..ff521f9 100644 --- a/gas-relayer/src/service.js +++ b/gas-relayer/src/service.js @@ -4,9 +4,6 @@ const config = require('../config/config.js'); const ContractSettings = require('./contract-settings'); const MessageProcessor = require('./message-processor'); -// IDEA: A node should call an API (probably from a status node) to register itself as a -// token gas relayer. - console.info("Starting..."); const events = new EventEmitter(); @@ -59,52 +56,39 @@ events.on('setup:complete', async (settings) => { // Verifying relayer balance await verifyBalance(); - shhOptions.symKeyId = await web3.shh.addSymKey(config.node.whisper.symKey); shhOptions.kId = await web3.shh.newKeyPair(); + const symKeyID = await web3.shh.addSymKey(config.node.whisper.symKey); + const pubKey = await web3.shh.getPublicKey(shhOptions.kId); + // Listening to whisper + // Individual subscriptions due to https://github.com/ethereum/web3.js/issues/1361 + // once this is fixed, we'll be able to use an array of topics and a single subs for symkey and a single subs for privKey console.info(`Sym Key: ${config.node.whisper.symKey}`); + console.info(`Relayer Public Key: ${pubKey}`); console.info("Topics Available:"); for(let contract in settings.contracts) { console.info("- %s: %s [%s]", settings.getContractByTopic(contract).name, contract, Object.keys(settings.getContractByTopic(contract).allowedFunctions).join(', ')); shhOptions.topics = [contract]; - events.emit('server:listen', shhOptions, settings); + + // Listen to public channel - Used for reporting availability + events.emit('server:listen', Object.assign({symKeyID}, shhOptions), settings); + + // Listen to private channel - Individual transactions + events.emit('server:listen', Object.assign({privateKeyID: shhOptions.kId}, shhOptions), settings); } - - /* - if(config.heartbeat.enabled){ - - web3.shh.addSymKey(config.heartbeat.symKey) - .then(heartbeatSymKeyId => { - - for(let tokenAddress in settings.getTokens()){ - - let heartbeatPayload = settings.getToken(tokenAddress); - heartbeatPayload.address = tokenAddress; - - setInterval(() => { - web3.shh.post({ - symKeyID: heartbeatSymKeyId, - sig: keyId, - ttl: config.node.whisper.ttl, - powTarget:config.node.whisper.minPow, - powTime: config.node.whisper.powTime, - topic: web3.utils.toHex("relay-heartbeat-" + heartbeatPayload.symbol).slice(0, 10), - payload: web3.utils.toHex(JSON.stringify(heartbeatPayload)) - }).catch((err) => { - console.error(err); - process.exit(-1); - }); - }, 60000); - - } - }); - }*/ }); -const reply = (message) => (text, receipt) => { +const replyFunction = (message) => (text, receipt) => { if(message.sig !== undefined){ - console.log(text); + + let payloadContent; + if(typeof text === 'object'){ + payloadContent = {...text, receipt}; + } else { + payloadContent = {text, receipt}; + } + web3.shh.post({ pubKey: message.sig, sig: shhOptions.kId, @@ -112,7 +96,7 @@ const reply = (message) => (text, receipt) => { powTarget:config.node.whisper.minPow, powTime: config.node.whisper.powTime, topic: message.topic, - payload: web3.utils.fromAscii(JSON.stringify({message:text, receipt}, null, " ")) + payload: web3.utils.fromAscii(JSON.stringify(payloadContent, null, " ")) }).catch(console.error); } }; @@ -121,9 +105,7 @@ const extractInput = (message) => { let obj = { contract: null, address: null, - functionName: null, - functionParameters: null, - payload: null + action: null }; try { @@ -131,9 +113,15 @@ const extractInput = (message) => { let parsedObj = JSON.parse(msg); obj.contract = parsedObj.contract; obj.address = parsedObj.address; - obj.functionName = parsedObj.encodedFunctionCall.slice(0, 10); - obj.functionParameters = "0x" + parsedObj.encodedFunctionCall.slice(10); - obj.payload = parsedObj.encodedFunctionCall; + obj.action = parsedObj.action; + if(obj.action == 'transaction'){ + obj.functionName = parsedObj.encodedFunctionCall.slice(0, 10); + obj.functionParameters = "0x" + parsedObj.encodedFunctionCall.slice(10); + obj.payload = parsedObj.encodedFunctionCall; + } else if(obj.action == 'availability') { + obj.gasToken = parsedObj.gasToken; + obj.gasPrice = parsedObj.gasPrice; + } } catch(err){ console.error("Couldn't parse " + message); } @@ -144,7 +132,7 @@ const extractInput = (message) => { events.on('server:listen', (shhOptions, settings) => { let processor = new MessageProcessor(config, settings, web3, events); - web3.shh.subscribe('messages', shhOptions, (error, message) => { + web3.shh.subscribe('messages', shhOptions, async (error, message) => { if(error){ console.error(error); return; @@ -152,9 +140,30 @@ events.on('server:listen', (shhOptions, settings) => { verifyBalance(true); - processor.process(settings.getContractByTopic(message.topic), - extractInput(message), - reply(message)); + const input = extractInput(message); + const reply = replyFunction(message); + let validationResult; + + switch(input.action){ + case 'transaction': + processor.processTransaction(settings.getContractByTopic(message.topic), + input, + reply); + break; + case 'availability': + validationResult = await processor.processStrategy(settings.getContractByTopic(message.topic), + input, + reply, + settings.buildStrategy("./strategy/AvailabilityStrategy", message.topic) + ); + if(validationResult.success) reply(validationResult.message); + + break; + default: + reply("unknown-action"); + } + + }); }); diff --git a/gas-relayer/src/strategy/AvailabilityStrategy.js b/gas-relayer/src/strategy/AvailabilityStrategy.js new file mode 100644 index 0000000..3f7e557 --- /dev/null +++ b/gas-relayer/src/strategy/AvailabilityStrategy.js @@ -0,0 +1,33 @@ +const Strategy = require('./BaseStrategy'); + +/** + * Class representing a strategy to validate an 'availability' request. + * @extends Strategy + */ +class AvailabilityStrategy extends Strategy { + + /** + * Process availability strategy + * @param {object} input - Object obtained from an 'availability' request. It expects an object with this structure `{contract, address, action, gasToken, gasPrice}` + * @returns {object} Status of validation, and minimum price + */ + execute(input){ + // Verifying if token is allowed + const token = this.settings.getToken(input.gasToken); + if(token == undefined) return {success: false, message: "Token not allowed"}; + + // TODO: Validate gasPrice, and return the minPrice accepted + const minPrice = 0.00; + + return { + success: true, + message: { + message: "Available", + minPrice: minPrice + } + }; + } + +} + +module.exports = AvailabilityStrategy; diff --git a/gas-relayer/src/strategy/BaseStrategy.js b/gas-relayer/src/strategy/BaseStrategy.js index bdd0ec7..6ab5d26 100644 --- a/gas-relayer/src/strategy/BaseStrategy.js +++ b/gas-relayer/src/strategy/BaseStrategy.js @@ -2,7 +2,17 @@ const ganache = require("ganache-cli"); const Web3 = require('web3'); const erc20ABI = require('../../abi/ERC20Token.json'); +/** + * Abstract class used for validation strategies + */ class BaseStrategy { + + /** + * @param {object} web3 - Web3 object already configured + * @param {object} config - Configuration object obtained from `./config/config.js` + * @param {object} settings - Settings obtained from parsing the configuration object + * @param {object} contract - Object obtained from the settings based on the message topic + */ constructor(web3, config, settings, contract){ this.web3 = web3; this.settings = settings; @@ -10,6 +20,12 @@ class BaseStrategy { this.config = config; } + /** + * Obtain the balance in tokens or ETH from an address + * @param {string} address - ETH address to obtain the balance from + * @param {object} token - Obtained from `settings.getToken(tokenSymbol)` + * @returns {web3.utils.BN} Balance + */ async getBalance(address, token){ // Determining balances of token used if(token.symbol == "ETH"){ @@ -21,6 +37,11 @@ class BaseStrategy { } } + /** + * Build Parameters Function + * @param {object} input - Object obtained from an `transaction` request. + * @returns {function} Function that simplifies accessing contract functions' parameters + */ _obtainParametersFunc(input){ const parameterList = this.web3.eth.abi.decodeParameters(this.contract.allowedFunctions[input.functionName].inputs, input.functionParameters); return function(parameterName){ @@ -28,6 +49,11 @@ class BaseStrategy { }; } + /** + * Estimate gas using web3 + * @param {object} input - Object obtained from an `transaction` request. + * @returns {web3.utils.toBN} Estimated gas fees + */ async _estimateGas(input){ let p = { from: this.config.node.blockchain.account, @@ -40,6 +66,8 @@ class BaseStrategy { /** * Simulate transaction using ganache. Useful for obtaining events + * @param {object} input - Object obtained from an `transaction` request. + * @returns {object} Simulated transaction receipt */ async _simulateTransaction(input){ let web3Sim = new Web3(ganache.provider({ diff --git a/gas-relayer/src/strategy/IdentityStrategy.js b/gas-relayer/src/strategy/IdentityStrategy.js index f718e9c..a2c86d0 100644 --- a/gas-relayer/src/strategy/IdentityStrategy.js +++ b/gas-relayer/src/strategy/IdentityStrategy.js @@ -1,8 +1,17 @@ const Strategy = require('./BaseStrategy'); const erc20ABI = require('../../abi/ERC20Token.json'); +/** + * Class representing a strategy to validate a `transaction` request when the topic is related to Identities. + * @extends Strategy + */ class IdentityStrategy extends Strategy { + /** + * Validates if the contract being invoked represents an Identity instance + * @param {object} input - Object obtained from a `transaction` request. + * @returns {bool} Valid instance or not + */ async _validateInstance(input){ const instanceCodeHash = this.web3.utils.soliditySha3(await this.web3.eth.getCode(input.contract)); const kernelVerifSignature = this.web3.utils.soliditySha3(this.contract.kernelVerification).slice(0, 10); @@ -15,6 +24,11 @@ class IdentityStrategy extends Strategy { return this.web3.eth.abi.decodeParameter('bool', verificationResult); } + /** + * Process Identity strategy + * @param {object} input - Object obtained from an 'transaction' request. It expects an object with this structure `{contract, address, action, functionName, functionParameters, payload}` + * @returns {object} Status of validation and estimated gas + */ async execute(input){ if(this.contract.isIdentity){ let validInstance = await this._validateInstance(input); diff --git a/gas-relayer/src/strategy/SNTStrategy.js b/gas-relayer/src/strategy/SNTStrategy.js index 6bcc7b6..80476d1 100644 --- a/gas-relayer/src/strategy/SNTStrategy.js +++ b/gas-relayer/src/strategy/SNTStrategy.js @@ -3,9 +3,17 @@ const Strategy = require('./BaseStrategy'); const TransferSNT = "0x916b6511"; const ExecuteGasRelayed = "0x754e6ab0"; - +/** + * Class representing a strategy to validate a `transaction` request when the topic is related to SNTController. + * @extends Strategy + */ class SNTStrategy extends Strategy { + /** + * Process SNTController strategy + * @param {object} input - Object obtained from an 'transaction' request. It expects an object with this structure `{contract, address, action, functionName, functionParameters, payload}` + * @returns {object} Status of validation and estimated gas + */ async execute(input){ const params = this._obtainParametersFunc(input); diff --git a/gas-relayer/src/utils.js b/gas-relayer/src/utils.js deleted file mode 100644 index e69de29..0000000 diff --git a/test-dapp/app/components/approveandcallgasrelayed.js b/test-dapp/app/components/approveandcallgasrelayed.js index 0ef01f2..6fec346 100644 --- a/test-dapp/app/components/approveandcallgasrelayed.js +++ b/test-dapp/app/components/approveandcallgasrelayed.js @@ -41,6 +41,7 @@ class ApproveAndCallGasRelayed extends Component { gasLimit: 0, gasToken: "0x0000000000000000000000000000000000000000", signature: '', + relayer: '', transactionError: '', messagingError: '', submitting: false @@ -93,7 +94,7 @@ class ApproveAndCallGasRelayed extends Component { } } - sendMessage = event => { + obtainRelayers = event => { event.preventDefault(); const {web3, kid, skid} = this.props; @@ -103,6 +104,50 @@ class ApproveAndCallGasRelayed extends Component { submitting: true }); this.props.clearMessages(); + + try { + const sendOptions = { + ttl: 1000, + sig: kid, + powTarget: 1, + powTime: 20, + topic: this.state.topic, + symKeyID: skid, + payload: web3.utils.toHex({ + 'contract': this.props.identityAddress, + 'address': web3.eth.defaultAccount, + 'action': 'availability', + 'gasToken': this.state.gasToken, + 'gasPrice': this.state.gasPrice + }) + }; + + web3.shh.post(sendOptions) + .then(() => { + this.setState({submitting: false}); + console.log("Message sent"); + return true; + }); + } catch(error){ + this.setState({messagingError: error.message, submitting: false}); + } + } + + sendTransaction = event => { + event.preventDefault(); + + const {web3, kid} = this.props; + + let relayer = this.state.relayer; + if(relayer == '' && this.props.relayers.length == 1){ + relayer = this.props.relayers[0]; + } + + this.setState({ + messagingError: '', + submitting: true + }); + this.props.clearMessages(); try { @@ -124,11 +169,12 @@ class ApproveAndCallGasRelayed extends Component { powTarget: 1, powTime: 20, topic: this.state.topic, - symKeyID: skid, + pubKey: relayer, payload: web3.utils.toHex({ 'contract': this.props.identityAddress, - 'encodedFunctionCall': funCall, - 'address': web3.eth.defaultAccount + 'address': web3.eth.defaultAccount, + 'action': 'transaction', + 'encodedFunctionCall': funCall }) }; @@ -283,19 +329,10 @@ class ApproveAndCallGasRelayed extends Component { { this.state.messagingError && } + - + - - + + + + + + + + { + this.props.relayers.length > 0 ? + this.props.relayers.map((r, i) => ) + : + + } + + + + + @@ -334,7 +411,8 @@ ApproveAndCallGasRelayed.propTypes = { web3: PropTypes.object, kid: PropTypes.string, skid: PropTypes.string, - clearMessages: PropTypes.func + clearMessages: PropTypes.func, + relayers: PropTypes.array.isRequired }; export default withStyles(styles)(ApproveAndCallGasRelayed); diff --git a/test-dapp/app/components/body-identity.js b/test-dapp/app/components/body-identity.js index 077fe7b..710ba1c 100644 --- a/test-dapp/app/components/body-identity.js +++ b/test-dapp/app/components/body-identity.js @@ -27,7 +27,8 @@ class Body extends Component { nonce: '0', kid: null, skid: null, - message: '' + message: '', + relayers: [] }; } @@ -45,18 +46,31 @@ class Body extends Component { web3js.shh.addSymKey(config.relaySymKey) .then((skid) => { this.setState({kid, skid}); - web3js.shh.subscribe('messages', { "privateKeyID": kid, "ttl": 1000, "minPow": 0.1, "powTime": 1000 }, (error, message) => { - console.log(message); + console.log(message); + + const msg = web3js.utils.toAscii(message.payload); + const msgObj = JSON.parse(msg); + + if(msgObj.message == 'Available'){ + // found a relayer + console.log("Relayer available: " + message.sig); + + let relayers = this.state.relayers; + relayers.push(message.sig); + relayers = relayers.filter((value, index, self) => self.indexOf(value) === index); + this.setState({relayers}); + } + if(error){ console.error(error); } else { - this.setState({message: web3js.utils.toAscii(message.payload)}); + this.setState({message: msg}); } }); @@ -104,7 +118,7 @@ class Body extends Component { } render(){ - const {tab, identityAddress, nonce, web3js, message, kid, skid} = this.state; + const {tab, identityAddress, nonce, web3js, message, kid, skid, relayers} = this.state; return @@ -112,8 +126,8 @@ class Body extends Component { - {tab === 0 && } - {tab === 1 && } + {tab === 0 && } + {tab === 1 && } {tab === 2 && Item Three} diff --git a/test-dapp/app/components/body-sntcontroller.js b/test-dapp/app/components/body-sntcontroller.js index 6bd0903..28c1220 100644 --- a/test-dapp/app/components/body-sntcontroller.js +++ b/test-dapp/app/components/body-sntcontroller.js @@ -28,7 +28,8 @@ class Body extends Component { nonce: '0', kid: null, skid: null, - message: '' + message: '', + relayers: [] }; } @@ -53,7 +54,21 @@ class Body extends Component { "minPow": 0.1, "powTime": 1000 }, (error, message) => { - console.log(message); + console.log(message); + + const msg = web3js.utils.toAscii(message.payload); + const msgObj = JSON.parse(msg); + + if(msgObj.message == 'Available'){ + // found a relayer + console.log("Relayer available: " + message.sig); + + let relayers = this.state.relayers; + relayers.push(message.sig); + relayers = relayers.filter((value, index, self) => self.indexOf(value) === index); + this.setState({relayers}); + } + if(error){ console.error(error); } else { @@ -90,15 +105,15 @@ class Body extends Component { } render(){ - const {tab, walletAddress, nonce, web3js, message, kid, skid} = this.state; + const {tab, walletAddress, nonce, web3js, message, kid, skid, relayers} = this.state; return - {tab === 0 && } - {tab === 1 && } + {tab === 0 && } + {tab === 1 && } diff --git a/test-dapp/app/components/callgasrelayed.js b/test-dapp/app/components/callgasrelayed.js index efd11ba..c443cce 100644 --- a/test-dapp/app/components/callgasrelayed.js +++ b/test-dapp/app/components/callgasrelayed.js @@ -45,6 +45,7 @@ class CallGasRelayed extends Component { payload: '', message: '', web3js: null, + relayer: '', transactionError: '', messagingError: '', submitting: false @@ -97,7 +98,7 @@ class CallGasRelayed extends Component { } } - sendMessage = event => { + obtainRelayers = event => { event.preventDefault(); const {web3, kid, skid} = this.props; @@ -108,6 +109,50 @@ class CallGasRelayed extends Component { }); this.props.clearMessages(); + try { + const sendOptions = { + ttl: 1000, + sig: kid, + powTarget: 1, + powTime: 20, + topic: this.state.topic, + symKeyID: skid, + payload: web3.utils.toHex({ + 'contract': this.props.identityAddress, + 'address': web3.eth.defaultAccount, + 'action': 'availability', + 'gasToken': this.state.gasToken, + 'gasPrice': this.state.gasPrice + }) + }; + + web3.shh.post(sendOptions) + .then(() => { + this.setState({submitting: false}); + console.log("Message sent"); + return true; + }); + } catch(error){ + this.setState({messagingError: error.message, submitting: false}); + } + } + + sendTransaction = event => { + event.preventDefault(); + + const {web3, kid} = this.props; + + let relayer = this.state.relayer; + if(relayer == '' && this.props.relayers.length == 1){ + relayer = this.props.relayers[0]; + } + + this.setState({ + messagingError: '', + submitting: true + }); + this.props.clearMessages(); + try { let jsonAbi = IdentityGasRelay._jsonInterface.filter(x => x.name == "callGasRelayed")[0]; let funCall = web3.eth.abi.encodeFunctionCall(jsonAbi, [ @@ -126,14 +171,15 @@ class CallGasRelayed extends Component { powTarget: 1, powTime: 20, topic: this.state.topic, - symKeyID: skid, + pubKey: relayer, payload: web3.utils.toHex({ 'contract': this.props.identityAddress, - 'encodedFunctionCall': funCall, - 'address': web3.eth.defaultAccount + 'address': web3.eth.defaultAccount, + 'action': 'transaction', + 'encodedFunctionCall': funCall }) }; - + web3.shh.post(sendOptions) .then(() => { this.setState({submitting: false}); @@ -266,19 +312,10 @@ class CallGasRelayed extends Component { { this.state.messagingError && } + - + - - + + + + + + + + + { + this.props.relayers.length > 0 ? + this.props.relayers.map((r, i) => ) + : + + } + + + + + @@ -316,7 +394,8 @@ CallGasRelayed.propTypes = { web3: PropTypes.object, kid: PropTypes.string, skid: PropTypes.string, - clearMessages: PropTypes.func + clearMessages: PropTypes.func, + relayers: PropTypes.array.isRequired }; export default withStyles(styles)(CallGasRelayed); diff --git a/test-dapp/app/components/execute.js b/test-dapp/app/components/execute.js index 87ca0b5..cdf5ff5 100644 --- a/test-dapp/app/components/execute.js +++ b/test-dapp/app/components/execute.js @@ -41,6 +41,7 @@ class Execute extends Component { msgSent: '', payload: '', message: '', + relayer: '', web3js: null, transactionError: '', messagingError: '', @@ -99,11 +100,60 @@ class Execute extends Component { TestContract.methods.val().call().then(value => console.log({message: "TestContract.val(): " + value})); } - sendMessage = async event => { + obtainRelayers = async event => { event.preventDefault(); const {web3, kid, skid} = this.props; + this.setState({ + messagingError: '', + submitting: true + }); + + this.props.clearMessages(); + + const accounts = await web3.eth.getAccounts(); + + + try { + const sendOptions = { + ttl: 1000, + sig: kid, + powTarget: 1, + powTime: 20, + topic: this.state.topic, + symKeyID: skid, + payload: web3.utils.toHex({ + 'contract': SNTController.options.address, + 'address': accounts[2], + 'action': 'availability', + 'gasToken': STT.options.address, + 'gasPrice': this.state.gasPrice + }) + }; + + web3.shh.post(sendOptions) + .then(() => { + this.setState({submitting: false}); + console.log("Message sent"); + return true; + }); + } catch(error){ + this.setState({messagingError: error.message, submitting: false}); + } + } + + sendTransaction = async event => { + event.preventDefault(); + + const {web3, kid} = this.props; + + let relayer = this.state.relayer; + if(relayer == '' && this.props.relayers.length == 1){ + relayer = this.props.relayers[0]; + } + + this.setState({ messagingError: '', submitting: true @@ -129,10 +179,11 @@ class Execute extends Component { powTarget: 1, powTime: 20, topic: this.state.topic, - symKeyID: skid, + pubKey: relayer, payload: web3.utils.toHex({ 'contract': SNTController.options.address, 'address': accounts[2], + 'action': 'transaction', 'encodedFunctionCall': funCall }) }; @@ -228,19 +279,10 @@ class Execute extends Component { { this.state.messagingError && } + - + - - + + + + + + + { + this.props.relayers.length > 0 ? + this.props.relayers.map((r, i) => ) + : + + } + + + + + @@ -278,7 +359,8 @@ Execute.propTypes = { web3: PropTypes.object, kid: PropTypes.string, skid: PropTypes.string, - clearMessages: PropTypes.func + clearMessages: PropTypes.func, + relayers: PropTypes.array.isRequired }; export default withStyles(styles)(Execute); diff --git a/test-dapp/app/components/transfersnt.js b/test-dapp/app/components/transfersnt.js index 9c91aa1..e3d6abc 100644 --- a/test-dapp/app/components/transfersnt.js +++ b/test-dapp/app/components/transfersnt.js @@ -41,6 +41,7 @@ class TransferSNT extends Component { msgSent: '', payload: '', message: '', + relayer: '', web3js: null, transactionError: '', messagingError: '', @@ -97,11 +98,57 @@ class TransferSNT extends Component { } } - sendMessage = async event => { + obtainRelayers = async event => { event.preventDefault(); const {web3, kid, skid} = this.props; + this.setState({ + messagingError: '', + submitting: true + }); + this.props.clearMessages(); + + const accounts = await web3.eth.getAccounts(); + + try { + const sendOptions = { + ttl: 1000, + sig: kid, + powTarget: 1, + powTime: 20, + topic: this.state.topic, + symKeyID: skid, + payload: web3.utils.toHex({ + 'contract': SNTController.options.address, + 'address': accounts[2], + 'action': 'availability', + 'gasToken': this.state.gasToken, + 'gasPrice': this.state.gasPrice + }) + }; + + web3.shh.post(sendOptions) + .then(() => { + this.setState({submitting: false}); + console.log("Message sent"); + return true; + }); + } catch(error){ + this.setState({messagingError: error.message, submitting: false}); + } + } + + sendTransaction = async event => { + event.preventDefault(); + + const {web3, kid} = this.props; + + let relayer = this.state.relayer; + if(relayer == '' && this.props.relayers.length == 1){ + relayer = this.props.relayers[0]; + } + this.setState({ messagingError: '', submitting: true @@ -126,10 +173,11 @@ class TransferSNT extends Component { powTarget: 1, powTime: 20, topic: this.state.topic, - symKeyID: skid, + pubKey: relayer, payload: web3.utils.toHex({ 'contract': SNTController.options.address, 'address': accounts[2], + 'action': 'transaction', 'encodedFunctionCall': funCall }) }; @@ -214,19 +262,10 @@ class TransferSNT extends Component { { this.state.messagingError && } + - + - - + + + + + + + + + { + this.props.relayers.length > 0 ? + this.props.relayers.map((r, i) => ) + : + + } + + + + + @@ -264,7 +344,8 @@ TransferSNT.propTypes = { web3: PropTypes.object, kid: PropTypes.string, skid: PropTypes.string, - clearMessages: PropTypes.func + clearMessages: PropTypes.func, + relayers: PropTypes.array.isRequired }; export default withStyles(styles)(TransferSNT);