Merge pull request #1 from status-im/relayer-network

Relayer Network
This commit is contained in:
Richard Ramos 2018-08-31 20:30:47 -04:00 committed by GitHub
commit 52cad4a04e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 775 additions and 159 deletions

108
README.md
View File

@ -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);
});
```
```
#### Valid operations
TODO

View File

@ -275,7 +275,7 @@
"error",
"never"
],
"valid-jsdoc": "error",
"valid-jsdoc": "off",
"vars-on-top": "off",
"wrap-iife": "error",
"wrap-regex": "error",

View File

@ -21,11 +21,6 @@ module.exports = {
"powTime": 1000
}
},
"heartbeat": {
"enabled": true,
"symKey": "0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b"
},
"tokens": {
"0x0000000000000000000000000000000000000000": {

View File

@ -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]);
}
}

View File

@ -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?

View File

@ -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");
}
});
});

View File

@ -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;

View File

@ -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({

View File

@ -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);

View File

@ -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);

View File

@ -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 {
</Card>
{ this.state.messagingError && <MySnackbarContentWrapper variant="error" message={this.state.messagingError} /> }
<Card className={classes.card}>
<CardHeader title="2. Message" />
<CardHeader title="2. Find Available Relayers" />
<CardContent>
<TextField
id="signature"
label="Signed Message"
value={this.state.signature}
margin="normal"
fullWidth
InputProps={{
readOnly: true
}}
/>
<TextField
id="symKey"
label="Symmetric Key"
@ -317,8 +354,48 @@ class ApproveAndCallGasRelayed extends Component {
/>
</CardContent>
<CardActions>
<Button size="small" color="primary" onClick={this.sendMessage} disabled={this.state.submitting}>
Send Message
<Button size="small" color="primary" onClick={this.obtainRelayers} disabled={this.state.submitting}>
Send &quot;availability&quot; Message
</Button>
</CardActions>
</Card>
<Card className={classes.card}>
<CardHeader title="3. Generate Transaction" />
<CardContent>
<TextField
id="relayer"
label="Relayer"
value={this.state.relayer}
onChange={this.handleChange('relayer')}
margin="normal"
fullWidth
select
SelectProps={{
native: true
}}
>
{
this.props.relayers.length > 0 ?
this.props.relayers.map((r, i) => <option key={i} value={r}>Relayer #{i+1}: {r}</option>)
:
<option></option>
}
</TextField>
<TextField
id="signature"
label="Signed Message"
value={this.state.signature}
margin="normal"
fullWidth
InputProps={{
readOnly: true
}}
/>
</CardContent>
<CardActions>
<Button size="small" color="primary" onClick={this.sendTransaction} disabled={this.state.submitting}>
Send &quot;transaction&quot; Message
</Button>
</CardActions>
</Card>
@ -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);

View File

@ -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 <Fragment>
<Tabs value={tab} onChange={this.handleChange}>
@ -112,8 +126,8 @@ class Body extends Component {
<Tab label="Approve and Call" />
<Tab label="Deploy" />
</Tabs>
{tab === 0 && <Container><CallGasRelayed clearMessages={this.clearMessages} web3={web3js} kid={kid} skid={skid} nonce={nonce} identityAddress={identityAddress} /></Container>}
{tab === 1 && <Container><ApproveAndCallGasRelayed clearMessages={this.clearMessages} web3={web3js} kid={kid} skid={skid} nonce={nonce} identityAddress={identityAddress} /></Container>}
{tab === 0 && <Container><CallGasRelayed clearMessages={this.clearMessages} web3={web3js} kid={kid} skid={skid} nonce={nonce} identityAddress={identityAddress} relayers={relayers} /></Container>}
{tab === 1 && <Container><ApproveAndCallGasRelayed clearMessages={this.clearMessages} web3={web3js} kid={kid} skid={skid} nonce={nonce} identityAddress={identityAddress} relayers={relayers} /></Container>}
{tab === 2 && <Container>Item Three</Container>}
<Divider />
<Container>

View File

@ -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 <Fragment>
<Tabs value={tab} onChange={this.handleChange}>
<Tab label="Transfer SNT" />
<Tab label="Execute" />
</Tabs>
{tab === 0 && <Container><TransferSNT clearMessages={this.clearMessages} web3={web3js} kid={kid} skid={skid} nonce={nonce} /></Container>}
{tab === 1 && <Container><Execute clearMessage={this.clearMessages} web3={web3js} kid={kid} skid={skid} nonce={nonce} /></Container>}
{tab === 0 && <Container><TransferSNT clearMessages={this.clearMessages} web3={web3js} kid={kid} skid={skid} nonce={nonce} relayers={relayers} /></Container>}
{tab === 1 && <Container><Execute clearMessages={this.clearMessages} web3={web3js} kid={kid} skid={skid} nonce={nonce} relayers={relayers} /></Container>}
<Divider />
<Container>
<Status message={message} nonceUpdateFunction={this.updateNonce} nonce={nonce} walletAddress={walletAddress} />

View File

@ -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 {
</Card>
{ this.state.messagingError && <MySnackbarContentWrapper variant="error" message={this.state.messagingError} /> }
<Card className={classes.card}>
<CardHeader title="2. Message" />
<CardHeader title="2. Find Available Relayers" />
<CardContent>
<TextField
id="signature"
label="Signed Message"
value={this.state.signature}
margin="normal"
fullWidth
InputProps={{
readOnly: true
}}
/>
<TextField
id="symKey"
label="Symmetric Key"
@ -300,8 +337,49 @@ class CallGasRelayed extends Component {
/>
</CardContent>
<CardActions>
<Button size="small" color="primary" onClick={this.sendMessage} disabled={this.state.submitting}>
Send Message
<Button size="small" color="primary" onClick={this.obtainRelayers} disabled={this.state.submitting}>
Send &quot;availability&quot; Message
</Button>
</CardActions>
</Card>
<Card className={classes.card}>
<CardHeader title="3. Generate Transaction" />
<CardContent>
<TextField
id="relayer"
label="Relayer"
value={this.state.relayer}
onChange={this.handleChange('relayer')}
margin="normal"
fullWidth
select
SelectProps={{
native: true
}}
>
{
this.props.relayers.length > 0 ?
this.props.relayers.map((r, i) => <option key={i} value={r}>Relayer #{i+1}: {r}</option>)
:
<option></option>
}
</TextField>
<TextField
id="signature"
label="Signed Message"
value={this.state.signature}
margin="normal"
fullWidth
InputProps={{
readOnly: true
}}
/>
</CardContent>
<CardActions>
<Button size="small" color="primary" onClick={this.sendTransaction} disabled={this.state.submitting}>
Send &quot;transaction&quot; Message
</Button>
</CardActions>
</Card>
@ -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);

View File

@ -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 {
</Card>
{ this.state.messagingError && <MySnackbarContentWrapper variant="error" message={this.state.messagingError} /> }
<Card className={classes.card}>
<CardHeader title="2. Message" />
<CardHeader title="2. Find Available Relayers" />
<CardContent>
<TextField
id="signature"
label="Signed Message"
value={this.state.signature}
margin="normal"
fullWidth
InputProps={{
readOnly: true
}}
/>
<TextField
id="symKey"
label="Symmetric Key"
@ -262,8 +304,47 @@ class Execute extends Component {
/>
</CardContent>
<CardActions>
<Button size="small" color="primary" onClick={this.sendMessage} disabled={this.state.submitting}>
Send Message
<Button size="small" color="primary" onClick={this.obtainRelayers} disabled={this.state.submitting}>
Send &quot;availability&quot; Message
</Button>
</CardActions>
</Card>
<Card className={classes.card}>
<CardHeader title="3. Generate Transaction" />
<CardContent>
<TextField
id="relayer"
label="Relayer"
value={this.state.relayer}
onChange={this.handleChange('relayer')}
margin="normal"
fullWidth
select
SelectProps={{
native: true
}}
>
{
this.props.relayers.length > 0 ?
this.props.relayers.map((r, i) => <option key={i} value={r}>Relayer #{i+1}: {r}</option>)
:
<option></option>
}
</TextField>
<TextField
id="signature"
label="Signed Message"
value={this.state.signature}
margin="normal"
fullWidth
InputProps={{
readOnly: true
}}
/>
</CardContent>
<CardActions>
<Button size="small" color="primary" onClick={this.sendTransaction} disabled={this.state.submitting}>
Send &quot;transaction&quot; Message
</Button>
</CardActions>
</Card>
@ -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);

View File

@ -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 {
</Card>
{ this.state.messagingError && <MySnackbarContentWrapper variant="error" message={this.state.messagingError} /> }
<Card className={classes.card}>
<CardHeader title="2. Message" />
<CardHeader title="2. Find Available Relayers" />
<CardContent>
<TextField
id="signature"
label="Signed Message"
value={this.state.signature}
margin="normal"
fullWidth
InputProps={{
readOnly: true
}}
/>
<TextField
id="symKey"
label="Symmetric Key"
@ -248,8 +287,49 @@ class TransferSNT extends Component {
/>
</CardContent>
<CardActions>
<Button size="small" color="primary" onClick={this.sendMessage} disabled={this.state.submitting}>
Send Message
<Button size="small" color="primary" onClick={this.obtainRelayers} disabled={this.state.submitting}>
Send &quot;availability&quot; Message
</Button>
</CardActions>
</Card>
<Card className={classes.card}>
<CardHeader title="3. Generate Transaction" />
<CardContent>
<TextField
id="relayer"
label="Relayer"
value={this.state.relayer}
onChange={this.handleChange('relayer')}
margin="normal"
fullWidth
select
SelectProps={{
native: true
}}
>
{
this.props.relayers.length > 0 ?
this.props.relayers.map((r, i) => <option key={i} value={r}>Relayer #{i+1}: {r}</option>)
:
<option></option>
}
</TextField>
<TextField
id="signature"
label="Signed Message"
value={this.state.signature}
margin="normal"
fullWidth
InputProps={{
readOnly: true
}}
/>
</CardContent>
<CardActions>
<Button size="small" color="primary" onClick={this.sendTransaction} disabled={this.state.submitting}>
Send &quot;transaction&quot; Message
</Button>
</CardActions>
</Card>
@ -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);