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) => )
+ :
+
+ }
+
+
+
+
+
+ Send "transaction" Message
@@ -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 && }
+
-
+
-
-
- Send Message
+
+ Send "availability" Message
+
+
+
+
+
+
+
+
+
+ {
+ this.props.relayers.length > 0 ?
+ this.props.relayers.map((r, i) => )
+ :
+
+ }
+
+
+
+
+
+ Send "transaction" Message
@@ -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 && }
+
-
+
-
-
- Send Message
+
+ Send "availability" Message
+
+
+
+
+
+
+
+ {
+ this.props.relayers.length > 0 ?
+ this.props.relayers.map((r, i) => )
+ :
+
+ }
+
+
+
+
+
+ Send "transaction" Message
@@ -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 && }
+
-
+
-
-
- Send Message
+
+ Send "availability" Message
+
+
+
+
+
+
+
+
+
+ {
+ this.props.relayers.length > 0 ?
+ this.props.relayers.map((r, i) => )
+ :
+
+ }
+
+
+
+
+
+ Send "transaction" Message
@@ -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);