diff --git a/gas-relayer/src/message-processor.js b/gas-relayer/src/message-processor.js index 78292ad..054952f 100644 --- a/gas-relayer/src/message-processor.js +++ b/gas-relayer/src/message-processor.js @@ -78,7 +78,6 @@ class MessageProcessor { if(!validationResult.success){ reply(validationResult.message); - return; } return validationResult; @@ -95,6 +94,8 @@ class MessageProcessor { async processTransaction(contract, input, reply){ const validationResult = await this.processStrategy(contract, input, reply); + if(!validationResult.success) return; + let p = { from: this.config.node.blockchain.account, to: input.contract, @@ -107,10 +108,10 @@ class MessageProcessor { validationResult.estimatedGas = await this.web3.eth.estimateGas(p); } - p.gas = parseInt(validationResult.estimatedGas * 1.05, 10); // Tune this - + p.gas = Math.floor(parseInt(validationResult.estimatedGas, 10)); // Tune this + const nodeBalance = await this.web3.eth.getBalance(this.config.node.blockchain.account); - + if(nodeBalance < p.gas){ reply("Relayer unavailable"); console.error("Relayer doesn't have enough gas to process trx: %s, required %s", nodeBalance, p.gas); diff --git a/gas-relayer/src/strategy/BaseStrategy.js b/gas-relayer/src/strategy/BaseStrategy.js index 6ab5d26..32259ca 100644 --- a/gas-relayer/src/strategy/BaseStrategy.js +++ b/gas-relayer/src/strategy/BaseStrategy.js @@ -77,16 +77,15 @@ class BaseStrategy { })); let simAccounts = await web3Sim.eth.getAccounts(); - + let simulatedReceipt = await web3Sim.eth.sendTransaction({ from: simAccounts[0], - to: input.address, + to: input.contract, value: 0, data: input.payload, gasLimit: 9500000 // 95% of current chain latest gas block limit }); - return simulatedReceipt; } diff --git a/gas-relayer/src/strategy/IdentityStrategy.js b/gas-relayer/src/strategy/IdentityStrategy.js index a2c86d0..5871747 100644 --- a/gas-relayer/src/strategy/IdentityStrategy.js +++ b/gas-relayer/src/strategy/IdentityStrategy.js @@ -1,6 +1,9 @@ const Strategy = require('./BaseStrategy'); const erc20ABI = require('../../abi/ERC20Token.json'); +const CallGasRelayed = "0xfd0dded5"; +const ApproveAndCallGasRelayed = "0x59f4ac61"; + /** * Class representing a strategy to validate a `transaction` request when the topic is related to Identities. * @extends Strategy @@ -64,13 +67,23 @@ class IdentityStrategy extends Strategy { let estimatedGas = 0; try { - // Geth tends to fail estimation with proxies, so we simulate it with ganache - estimatedGas = await this._simulateTransaction(input); - if(gasLimit.mul(this.web3.utils.toBN(1.05)).lt(estimatedGas)) { + // TODO: Investigate why sometimes geth fails estimations with proxies + if(input.functionName == CallGasRelayed){ + estimatedGas = await this._estimateGas(input); + } else { + const tmp = Math.floor(parseInt((await this._estimateGas(input)).toString(10), 10) * 1.05); + estimatedGas = this.web3.utils.toBN(tmp); // TODO: tune this + } + + if(gasLimit.lt(estimatedGas)) { return {success: false, message: "Gas limit below estimated gas (" + estimatedGas + ")"}; } } catch(exc){ if(exc.message.indexOf("revert") > -1) return {success: false, message: "Transaction will revert"}; + else { + console.error(exc); + return {success: false, message: "Couldn't process transaction"}; + } } return { diff --git a/gas-relayer/src/strategy/SNTStrategy.js b/gas-relayer/src/strategy/SNTStrategy.js index 80476d1..85cf449 100644 --- a/gas-relayer/src/strategy/SNTStrategy.js +++ b/gas-relayer/src/strategy/SNTStrategy.js @@ -41,7 +41,8 @@ class SNTStrategy extends Strategy { const latestBlock = await this.web3.eth.getBlock("latest"); let estimatedGas = 0; try { - estimatedGas = await this._estimateGas(input, latestBlock.gasLimit); + const simulatedReceipt = await this._simulateTransaction(input, latestBlock.gasLimit); + estimatedGas = this.web3.utils.toBN(simulatedReceipt.gasUsed); } catch(exc){ if(exc.message.indexOf("revert") > -1) return {success: false, message: "Transaction will revert"}; } diff --git a/test-dapp/app/components/approveandcallgasrelayed.js b/test-dapp/app/components/approveandcallgasrelayed.js index 6fec346..63a0b5c 100644 --- a/test-dapp/app/components/approveandcallgasrelayed.js +++ b/test-dapp/app/components/approveandcallgasrelayed.js @@ -1,4 +1,5 @@ import React, {Component} from 'react'; +import StatusGasRelayer, {Contracts, Functions} from '../status-gas-relayer'; import Button from '@material-ui/core/Button'; import Card from '@material-ui/core/Card'; import CardActions from '@material-ui/core/CardActions'; @@ -15,7 +16,7 @@ import TextField from '@material-ui/core/TextField'; import config from '../config'; import web3 from 'Embark/web3'; import {withStyles} from '@material-ui/core/styles'; -import { Typography } from '@material-ui/core'; + const styles = theme => ({ root: { width: '100%', @@ -63,7 +64,7 @@ class ApproveAndCallGasRelayed extends Component { }); }; - sign = (event) => { + sign = async (event) => { if(event) event.preventDefault(); this.setState({ @@ -74,27 +75,21 @@ class ApproveAndCallGasRelayed extends Component { IdentityGasRelay.options.address = this.props.identityAddress; try { - IdentityGasRelay.methods.deployGasRelayHash( - this.state.value, - web3.utils.soliditySha3({t: 'bytes', v: this.state.data}), - this.props.nonce, - this.state.gasPrice, - this.state.gasLimit, - this.state.gasToken - ) - .call() - .then(message => { - return web3.eth.sign(message, web3.eth.defaultAccount); - }) - .then(signature => { - this.setState({signature}); - }); + const s = new StatusGasRelayer.Identity(this.props.identityAddress, web3.eth.defaultAccount) + .setContractFunction(Functions.Identity.approveAndCall) + .setTransaction(this.state.to, this.state.value, this.state.data) + .setBaseToken(this.state.baseToken) + .setGas(this.state.gasToken, this.state.gasPrice, this.state.gasLimit); + + const signature = await s.sign(web3); + + this.setState({signature}); } catch(error){ this.setState({transactionError: error.message}); } } - obtainRelayers = event => { + obtainRelayers = async event => { event.preventDefault(); const {web3, kid, skid} = this.props; @@ -106,34 +101,20 @@ class ApproveAndCallGasRelayed 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; - }); + const s = new StatusGasRelayer.AvailableRelayers(Contracts.Identity, this.props.identityAddress, web3.eth.defaultAccount) + .setRelayersSymKeyID(skid) + .setAsymmetricKeyID(kid) + .setGas(this.state.gasToken, this.state.gasPrice); + await s.post(web3); + + console.log("Message sent"); + this.setState({submitting: false}); } catch(error){ this.setState({messagingError: error.message, submitting: false}); } } - sendTransaction = event => { + sendTransaction = async event => { event.preventDefault(); const {web3, kid} = this.props; @@ -151,39 +132,18 @@ class ApproveAndCallGasRelayed extends Component { try { - let jsonAbi = IdentityGasRelay._jsonInterface.filter(x => x.name == "approveAndCallGasRelayed")[0]; - let funCall = web3.eth.abi.encodeFunctionCall(jsonAbi, [ - this.state.baseToken, - this.state.to, - this.state.value, - this.state.data, - this.props.nonce, - this.state.gasPrice, - this.state.gasLimit, - this.state.gasToken, - this.state.signature - ]); - const sendOptions = { - ttl: 1000, - sig: kid, - powTarget: 1, - powTime: 20, - topic: this.state.topic, - pubKey: relayer, - payload: web3.utils.toHex({ - 'contract': this.props.identityAddress, - 'address': web3.eth.defaultAccount, - 'action': 'transaction', - 'encodedFunctionCall': funCall - }) - }; + const s = new StatusGasRelayer.Identity(this.props.identityAddress, web3.eth.defaultAccount) + .setContractFunction(Functions.Identity.approveAndCall) + .setTransaction(this.state.to, this.state.value, this.state.data) + .setBaseToken(this.state.baseToken) + .setGas(this.state.gasToken, this.state.gasPrice, this.state.gasLimit) + .setRelayer(relayer) + .setAsymmetricKeyID(kid); - web3.shh.post(sendOptions) - .then(() => { - this.setState({submitting: false}); - console.log("Message sent"); - return true; - }); + await s.post(this.state.signature, web3); + + this.setState({submitting: false}); + console.log("Message sent"); } catch(error){ this.setState({messagingError: error.message, submitting: false}); } diff --git a/test-dapp/app/components/callgasrelayed.js b/test-dapp/app/components/callgasrelayed.js index c443cce..70c6475 100644 --- a/test-dapp/app/components/callgasrelayed.js +++ b/test-dapp/app/components/callgasrelayed.js @@ -1,4 +1,5 @@ import React, {Component} from 'react'; +import StatusGasRelayer, {Contracts, Functions} from '../status-gas-relayer'; import Button from '@material-ui/core/Button'; import Card from '@material-ui/core/Card'; import CardActions from '@material-ui/core/CardActions'; @@ -6,7 +7,6 @@ import CardContent from '@material-ui/core/CardContent'; import CardHeader from '@material-ui/core/CardHeader'; import EmbarkJS from 'Embark/EmbarkJS'; import Grid from '@material-ui/core/Grid'; -import IdentityGasRelay from 'Embark/contracts/IdentityGasRelay'; import MySnackbarContentWrapper from './snackbar'; import PropTypes from 'prop-types'; import STT from 'Embark/contracts/STT'; @@ -15,6 +15,7 @@ import TextField from '@material-ui/core/TextField'; import config from '../config'; import web3 from 'Embark/web3'; import {withStyles} from '@material-ui/core/styles'; + const styles = theme => ({ root: { width: '100%', @@ -66,7 +67,7 @@ class CallGasRelayed extends Component { }); }; - sign = (event) => { + sign = async (event) => { if(event) event.preventDefault(); this.setState({ @@ -74,31 +75,22 @@ class CallGasRelayed extends Component { transactionError: '' }); - IdentityGasRelay.options.address = this.props.identityAddress; - try { - IdentityGasRelay.methods.callGasRelayHash( - this.state.to, - this.state.value, - web3.utils.soliditySha3({t: 'bytes', v: this.state.data}), - this.props.nonce, - this.state.gasPrice, - this.state.gasLimit, - this.state.gasToken - ) - .call() - .then(message => { - return web3.eth.sign(message, web3.eth.defaultAccount); - }) - .then(signature => { - this.setState({signature}); - }); + + const s = new StatusGasRelayer.Identity(this.props.identityAddress, web3.eth.defaultAccount) + .setContractFunction(Functions.Identity.call) + .setTransaction(this.state.to, this.state.value, this.state.data) + .setGas(this.state.gasToken, this.state.gasPrice, this.state.gasLimit); + + const signature = await s.sign(web3); + + this.setState({signature}); } catch(error){ this.setState({transactionError: error.message}); } } - obtainRelayers = event => { + obtainRelayers = async event => { event.preventDefault(); const {web3, kid, skid} = this.props; @@ -110,34 +102,21 @@ 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 - }) - }; + const s = new StatusGasRelayer.AvailableRelayers(Contracts.Identity, this.props.identityAddress, web3.eth.defaultAccount) + .setRelayersSymKeyID(skid) + .setAsymmetricKeyID(kid) + .setGas(this.state.gasToken, this.state.gasPrice); + await s.post(web3); + + console.log("Message sent"); + this.setState({submitting: false}); - 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 => { + sendTransaction = async event => { event.preventDefault(); const {web3, kid} = this.props; @@ -154,38 +133,17 @@ class CallGasRelayed extends Component { this.props.clearMessages(); try { - let jsonAbi = IdentityGasRelay._jsonInterface.filter(x => x.name == "callGasRelayed")[0]; - let funCall = web3.eth.abi.encodeFunctionCall(jsonAbi, [ - this.state.to, - this.state.value, - this.state.data, - this.props.nonce, - this.state.gasPrice, - this.state.gasLimit, - this.state.gasToken, - this.state.signature - ]); - const sendOptions = { - ttl: 1000, - sig: kid, - powTarget: 1, - powTime: 20, - topic: this.state.topic, - pubKey: relayer, - payload: web3.utils.toHex({ - 'contract': this.props.identityAddress, - 'address': web3.eth.defaultAccount, - 'action': 'transaction', - 'encodedFunctionCall': funCall - }) - }; - - web3.shh.post(sendOptions) - .then(() => { - this.setState({submitting: false}); - console.log("Message sent"); - return true; - }); + const s = new StatusGasRelayer.Identity(this.props.identityAddress, web3.eth.defaultAccount) + .setContractFunction(Functions.Identity.call) + .setTransaction(this.state.to, this.state.value, this.state.data) + .setGas(this.state.gasToken, this.state.gasPrice, this.state.gasLimit) + .setRelayer(relayer) + .setAsymmetricKeyID(kid); + + await s.post(this.state.signature, web3); + + this.setState({submitting: false}); + console.log("Message sent"); } catch(error){ this.setState({messagingError: error.message, submitting: false}); } diff --git a/test-dapp/app/components/execute.js b/test-dapp/app/components/execute.js index cdf5ff5..7e335ed 100644 --- a/test-dapp/app/components/execute.js +++ b/test-dapp/app/components/execute.js @@ -1,4 +1,5 @@ import React, {Component} from 'react'; +import StatusGasRelayer, {Contracts} from '../status-gas-relayer'; import Button from '@material-ui/core/Button'; import Card from '@material-ui/core/Card'; import CardActions from '@material-ui/core/CardActions'; @@ -9,11 +10,11 @@ import MySnackbarContentWrapper from './snackbar'; import PropTypes from 'prop-types'; import SNTController from 'Embark/contracts/SNTController'; import STT from 'Embark/contracts/STT'; +import TestContract from 'Embark/contracts/TestContract'; import TextField from '@material-ui/core/TextField'; import config from '../config'; import web3 from 'Embark/web3'; import {withStyles} from '@material-ui/core/styles'; -import TestContract from 'Embark/contracts/TestContract'; const styles = theme => ({ root: { @@ -55,7 +56,7 @@ class Execute extends Component { }); }; - sign = (event) => { + sign = async (event) => { if(event) event.preventDefault(); this.setState({ @@ -63,28 +64,16 @@ class Execute extends Component { transactionError: '' }); - SNTController.options.address = this.props.identityAddress; - try { - let message = ""; - SNTController.methods.getExecuteGasRelayedHash( - this.state.allowedContract, - this.state.data, - this.props.nonce, - this.state.gasPrice, - this.state.gasMinimal - ) - .call() - .then(result => { - message = result; - return web3.eth.getAccounts(); - }) - .then(accounts => { - return web3.eth.sign(message, accounts[2]); - }) - .then(signature => { - this.setState({signature}); - }); + const accounts = await web3.eth.getAccounts(); + + const s = new StatusGasRelayer.SNTController(SNTController.options.address, accounts[2]) + .execute(this.state.allowedContract, this.state.data) + .setGas(this.state.gasPrice, this.state.gasMinimal); + + const signature = await s.sign(web3); + + this.setState({signature}); } catch(error){ this.setState({transactionError: error.message}); } @@ -112,32 +101,18 @@ class Execute extends Component { 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 - }) - }; + const accounts = await web3.eth.getAccounts(); + + const s = new StatusGasRelayer.AvailableRelayers(Contracts.SNT, SNTController.options.address, accounts[2]) + .setRelayersSymKeyID(skid) + .setAsymmetricKeyID(kid) + .setGas(STT.options.address, this.state.gasPrice); + await s.post(web3); + + console.log("Message sent"); + this.setState({submitting: false}); - web3.shh.post(sendOptions) - .then(() => { - this.setState({submitting: false}); - console.log("Message sent"); - return true; - }); } catch(error){ this.setState({messagingError: error.message, submitting: false}); } @@ -161,39 +136,19 @@ class Execute extends Component { this.props.clearMessages(); try { - let jsonAbi = SNTController._jsonInterface.filter(x => x.name == "executeGasRelayed")[0]; - let funCall = web3.eth.abi.encodeFunctionCall(jsonAbi, [ - this.state.allowedContract, - this.state.data, - this.props.nonce, - this.state.gasPrice, - this.state.gasMinimal, - this.state.signature - ]); - const accounts = await web3.eth.getAccounts(); - const sendOptions = { - ttl: 1000, - sig: kid, - powTarget: 1, - powTime: 20, - topic: this.state.topic, - pubKey: relayer, - payload: web3.utils.toHex({ - 'contract': SNTController.options.address, - 'address': accounts[2], - 'action': 'transaction', - 'encodedFunctionCall': funCall - }) - }; + const s = new StatusGasRelayer.SNTController(SNTController.options.address, accounts[2]) + .execute(this.state.allowedContract, this.state.data) + .setGas(this.state.gasPrice, this.state.gasMinimal) + .setRelayer(relayer) + .setAsymmetricKeyID(kid); + + await s.post(this.state.signature, web3); + + this.setState({submitting: false}); + console.log("Message sent"); - web3.shh.post(sendOptions) - .then(() => { - this.setState({submitting: false}); - console.log("Message sent"); - return true; - }); } catch(error){ this.setState({messagingError: error.message, submitting: false}); } diff --git a/test-dapp/app/components/transfersnt.js b/test-dapp/app/components/transfersnt.js index e3d6abc..d9129c1 100644 --- a/test-dapp/app/components/transfersnt.js +++ b/test-dapp/app/components/transfersnt.js @@ -1,4 +1,5 @@ import React, {Component} from 'react'; +import StatusGasRelayer, {Contracts} from '../status-gas-relayer'; import Button from '@material-ui/core/Button'; import Card from '@material-ui/core/Card'; import CardActions from '@material-ui/core/CardActions'; @@ -9,12 +10,12 @@ import MySnackbarContentWrapper from './snackbar'; import PropTypes from 'prop-types'; import SNTController from 'Embark/contracts/SNTController'; import STT from 'Embark/contracts/STT'; - import TestContract from 'Embark/contracts/TestContract'; import TextField from '@material-ui/core/TextField'; import config from '../config'; import web3 from 'Embark/web3'; import {withStyles} from '@material-ui/core/styles'; + const styles = theme => ({ root: { width: '100%', @@ -64,7 +65,7 @@ class TransferSNT extends Component { }); } - sign = (event) => { + sign = async (event) => { if(event) event.preventDefault(); this.setState({ @@ -72,27 +73,18 @@ class TransferSNT extends Component { transactionError: '' }); - SNTController.options.address = this.props.identityAddress; try { - let message = ""; - SNTController.methods.getTransferSNTHash( - this.state.to, - this.state.amount, - this.props.nonce, - this.state.gasPrice - ) - .call() - .then(result => { - message = result; - return web3.eth.getAccounts(); - }) - .then(accounts => { - return web3.eth.sign(message, accounts[2]); - }) - .then(signature => { - this.setState({signature}); - }); + const accounts = await web3.eth.getAccounts(); + + const s = new StatusGasRelayer.SNTController(SNTController.options.address, accounts[2]) + .transferSNT(this.state.to, this.state.amount) + .setGas(this.state.gasPrice); + + const signature = await s.sign(web3); + + this.setState({signature}); + } catch(error){ this.setState({transactionError: error.message}); } @@ -109,31 +101,19 @@ class TransferSNT extends Component { }); 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 - }) - }; + const accounts = await web3.eth.getAccounts(); + + const s = new StatusGasRelayer.AvailableRelayers(Contracts.SNT, SNTController.options.address, accounts[2]) + .setRelayersSymKeyID(skid) + .setAsymmetricKeyID(kid) + .setGas(STT.options.address, this.state.gasPrice); + await s.post(web3); + + console.log("Message sent"); + this.setState({submitting: false}); - web3.shh.post(sendOptions) - .then(() => { - this.setState({submitting: false}); - console.log("Message sent"); - return true; - }); } catch(error){ this.setState({messagingError: error.message, submitting: false}); } @@ -156,38 +136,19 @@ class TransferSNT extends Component { this.props.clearMessages(); try { - let jsonAbi = SNTController._jsonInterface.filter(x => x.name == "transferSNT")[0]; - let funCall = web3.eth.abi.encodeFunctionCall(jsonAbi, [ - this.state.to, - this.state.amount, - this.props.nonce, - this.state.gasPrice, - this.state.signature - ]); - const accounts = await web3.eth.getAccounts(); - const sendOptions = { - ttl: 1000, - sig: kid, - powTarget: 1, - powTime: 20, - topic: this.state.topic, - pubKey: relayer, - payload: web3.utils.toHex({ - 'contract': SNTController.options.address, - 'address': accounts[2], - 'action': 'transaction', - 'encodedFunctionCall': funCall - }) - }; + const s = new StatusGasRelayer.SNTController(SNTController.options.address, accounts[2]) + .transferSNT(this.state.to, this.state.amount) + .setGas(this.state.gasPrice) + .setRelayer(relayer) + .setAsymmetricKeyID(kid); + + await s.post(this.state.signature, web3); + + this.setState({submitting: false}); + console.log("Message sent"); - web3.shh.post(sendOptions) - .then(() => { - this.setState({submitting: false}); - console.log("Message sent"); - return true; - }); } catch(error){ this.setState({messagingError: error.message, submitting: false}); } diff --git a/test-dapp/app/status-gas-relayer.js b/test-dapp/app/status-gas-relayer.js new file mode 100644 index 0000000..934ebbd --- /dev/null +++ b/test-dapp/app/status-gas-relayer.js @@ -0,0 +1,777 @@ +export const Contracts = { + 'Identity': 'IdentityGasRelay', + 'SNT': 'SNTController' +}; + +export const Functions = { + 'Identity': { + 'call': 'callGasRelayed', + 'approveAndCall': 'approveAndCallGasRelayed' + }, + 'SNT': { + 'transfer': 'transferSNT', + 'execute': 'executeGasRelayed' + } +}; + +export const Actions = { + 'Availability': 'availability', + 'Transaction': 'transaction' +}; + +const relayerSymmmetricKeyID = "0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b"; + +class StatusGasRelayer { + constructor(build, web3) { + if (arguments.length !== 2 || !this.validateBuild(build)) throw new Error("Invalid build"); + + this.web3 = web3; + + Object.defineProperties(this, { + message: { + value: build._getMessage(web3), + writable: false + }, + topic: { + value: web3.utils.toHex(build.contractName).slice(0, 10), + writable: false + }, + kid: { + value: build.kid, + writable: false + } + }); + + if(build.operation === Actions.Transaction){ + Object.defineProperties(this, { + pubKey: { + value: build.relayer, + writable: false + } + }); + } else { + Object.defineProperties(this, { + skid: { + value: build.skid, + writable: false + } + }); + } + } + + post = async (options) => { + options = options || {}; + + let pubKey = options.pubKey || this.pubKey; + let skid = options.skid || this.skid; + + let kid = options.kid || this.kid; + if(!kid){ + kid = await this.web3.shh.newKeyPair(); + } + + const sendOptions = { + ttl: options.ttl || 1000, + sig: kid, + powTarget: options.powTarget || 1, + powTime: options.powTime || 20, + topic: this.topic, + payload: this.web3.utils.toHex(this.message) + }; + + if(pubKey){ + sendOptions.pubKey = pubKey; + } else { + if(!skid){ + skid = await this.web3.shh.addSymKey(relayerSymmmetricKeyID); + } + sendOptions.symKeyID = skid; + } + + const msgId = await this.web3.shh.post(sendOptions); + return msgId; + } + + validateBuild = (build) => { + return (String(build.constructor) === String(StatusGasRelayer.AvailableRelayers) || + String(build.constructor) === String(StatusGasRelayer.Identity) || + String(build.constructor) === String(StatusGasRelayer.SNTController) + ); + } + + static get AvailableRelayers() { + return AvailableRelayersAction; + } + + static get Identity() { + return IdentityGasRelayedAction; + } + + static get SNTController() { + return SNTControllerAction; + } +} + +class Action { + setGas(token, price, limit){ + this.gasToken = token; + this.gasPrice = price; + this.gasLimit = limit; + return this; + } + + setOperation(operation){ + this.operation = operation; + return this; + } + + setRelayer(relayer){ + this.relayer = relayer; + return this; + } + + setRelayersSymKeyID = (skid) => { + this.skid = skid; + return this; + } + + setAsymmetricKeyID = (kid) => { + this.kid = kid; + return this; + } +} + +class IdentityGasRelayedAction extends Action { + constructor(contractAddress, accountAddress) { + super(); + this.contractName = Contracts.Identity; + this.contractAddress = contractAddress; + this.accountAddress = accountAddress; + + this.operation = Actions.Transaction; + return this; + } + + setContractFunction = contractFunction => { + this.contractFunction = contractFunction; + return this; + } + + setTransaction = (to, value, data) => { + this.to = to; + this.value = value; + this.data = data; + + return this; + } + + setBaseToken = baseToken => { + this.baseToken = baseToken; + return this; + } + + _nonce = async(contract) => { + const nonce = await contract.methods.nonce().call(); + return nonce; + } + + getNonce = async (web3) => { + const contract = new web3.eth.Contract(identityGasRelayABI, this.contractAddress); + const nonce = await this._nonce(contract); + return nonce; + } + + sign = async web3 => { + const contract = new web3.eth.Contract(identityGasRelayABI, this.contractAddress); + const nonce = await this._nonce(contract); + let hashedMessage; + + switch(this.contractFunction){ + case Functions.Identity.call: + hashedMessage = await contract.methods.callGasRelayHash( + this.to, + this.value, + web3.utils.soliditySha3({t: 'bytes', v: this.data}), + nonce, + this.gasPrice, + this.gasLimit, + this.gasToken + ).call(); + break; + case Functions.Identity.approveAndCall: + hashedMessage = await contract.methods.deployGasRelayHash( + this.value, + web3.utils.soliditySha3({t: 'bytes', v: this.data}), + nonce, + this.gasPrice, + this.gasLimit, + this.gasToken + ).call(); + break; + default: + throw new Error("Function not allowed"); + } + + const signedMessage = await web3.eth.sign(hashedMessage, this.accountAddress); + + return signedMessage; + } + + post = async (signature, web3, options) => { + this.nonce = await this.getNonce(web3); + this.signature = signature; + + const s = new StatusGasRelayer(this, web3); + return s.post(options); + } + + _getMessage = web3 => { + let jsonAbi = identityGasRelayABI.find(x => x.name == this.contractFunction); + let funCall; + + switch(this.contractFunction){ + case Functions.Identity.call: + funCall = web3.eth.abi.encodeFunctionCall(jsonAbi, [ + this.to, + this.value, + this.data, + this.nonce, + this.gasPrice, + this.gasLimit, + this.gasToken, + this.signature + ]); + break; + case Functions.Identity.approveAndCall: + funCall = web3.eth.abi.encodeFunctionCall(jsonAbi, [ + this.baseToken, + this.to, + this.value, + this.data, + this.nonce, + this.gasPrice, + this.gasLimit, + this.gasToken, + this.signature + ]); + break; + default: + throw new Error("Function not allowed"); + } + + return { + 'contract': this.contractAddress, + 'address': this.accountAddress, + 'action': Actions.Transaction, + 'encodedFunctionCall': funCall + }; + } +} + +class SNTControllerAction extends Action { + constructor(contractAddress, accountAddress) { + super(); + this.contractName = Contracts.SNT; + this.contractAddress = contractAddress; + this.accountAddress = accountAddress; + + this.operation = Actions.Transaction; + return this; + } + + transferSNT = (to, value) => { + this.to = to; + this.value = value; + this.contractFunction = Functions.SNT.transfer; + return this; + } + + execute = (contract, data) => { + this.allowedContract = contract; + this.data = data; + this.contractFunction = Functions.SNT.execute; + return this; + } + + _nonce = async(contract) => { + const nonce = await contract.methods.signNonce(this.accountAddress).call(); + return nonce; + } + + getNonce = async (web3) => { + const contract = new web3.eth.Contract(sntControllerABI, this.contractAddress); + const nonce = await this._nonce(contract); + return nonce; + } + + setGas(price, minimal){ + this.gasPrice = price; + this.gasMinimal = minimal; + return this; + } + + sign = async web3 => { + const contract = new web3.eth.Contract(sntControllerABI, this.contractAddress); + const nonce = await this._nonce(contract); + let hashedMessage; + + switch(this.contractFunction){ + case Functions.SNT.execute: + hashedMessage = await contract.methods.getExecuteGasRelayedHash( + this.allowedContract, + this.data, + nonce, + this.gasPrice, + this.gasMinimal, + ).call(); + break; + case Functions.SNT.transfer: + hashedMessage = await contract.methods.getTransferSNTHash( + this.to, + this.value, + nonce, + this.gasPrice + ).call(); + break; + default: + throw new Error("Function not allowed"); + } + + const signedMessage = await web3.eth.sign(hashedMessage, this.accountAddress); + + return signedMessage; + } + + post = async (signature, web3, options) => { + this.nonce = await this.getNonce(web3); + this.signature = signature; + const s = new StatusGasRelayer(this, web3); + return s.post(options); + } + + _getMessage = web3 => { + let jsonAbi = sntControllerABI.find(x => x.name == this.contractFunction); + let funCall; + + switch(this.contractFunction){ + case Functions.SNT.execute: + funCall = web3.eth.abi.encodeFunctionCall(jsonAbi, [ + this.allowedContract, + this.data, + this.nonce, + this.gasPrice, + this.gasMinimal, + this.signature + ]); + break; + case Functions.SNT.transfer: + funCall = web3.eth.abi.encodeFunctionCall(jsonAbi, [ + this.to, + this.value, + this.nonce, + this.gasPrice, + this.signature + ]); + break; + default: + throw new Error("Function not allowed"); + } + + return { + 'contract': this.contractAddress, + 'address': this.accountAddress, + 'action': Actions.Transaction, + 'encodedFunctionCall': funCall + }; + } +} + + +class AvailableRelayersAction extends Action { + constructor(contractName, contractAddress, accountAddress) { + super(); + this.contractName = contractName; + this.contractAddress = contractAddress; + this.accountAddress = accountAddress; + + this.operation = Actions.Availability; + + return this; + } + + _getMessage = web3 => { + return { + contract: this.contractAddress, + address: this.accountAddress || web3.eth.defaultAccount, + action: Actions.Availability, + gasToken: this.gasToken, + gasPrice: this.gasPrice + }; + } + + post(web3, options) { + const s = new StatusGasRelayer(this, web3); + return s.post(options); + } +} + +const sntControllerABI = [ + { + "constant": true, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_amount", + "type": "uint256" + }, + { + "name": "_nonce", + "type": "uint256" + }, + { + "name": "_gasPrice", + "type": "uint256" + } + ], + "name": "getTransferSNTHash", + "outputs": [ + { + "name": "txHash", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function", + "signature": "0x0c1f1f25" + }, + { + "constant": true, + "inputs": [ + { + "name": "_allowedContract", + "type": "address" + }, + { + "name": "_data", + "type": "bytes" + }, + { + "name": "_nonce", + "type": "uint256" + }, + { + "name": "_gasPrice", + "type": "uint256" + }, + { + "name": "_gasMinimal", + "type": "uint256" + } + ], + "name": "getExecuteGasRelayedHash", + "outputs": [ + { + "name": "execHash", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function", + "signature": "0x31c128b1" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_amount", + "type": "uint256" + }, + { + "name": "_nonce", + "type": "uint256" + }, + { + "name": "_gasPrice", + "type": "uint256" + }, + { + "name": "_signature", + "type": "bytes" + } + ], + "name": "transferSNT", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function", + "signature": "0x916b6511" + }, + { + "constant": false, + "inputs": [ + { + "name": "_allowedContract", + "type": "address" + }, + { + "name": "_data", + "type": "bytes" + }, + { + "name": "_nonce", + "type": "uint256" + }, + { + "name": "_gasPrice", + "type": "uint256" + }, + { + "name": "_gasMinimal", + "type": "uint256" + }, + { + "name": "_signature", + "type": "bytes" + } + ], + "name": "executeGasRelayed", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function", + "signature": "0x754e6ab0" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address" + } + ], + "name": "signNonce", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function", + "signature": "0x907920c7" + } +]; + +const identityGasRelayABI = [ + { + "constant": true, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + }, + { + "name": "_dataHash", + "type": "bytes32" + }, + { + "name": "_nonce", + "type": "uint256" + }, + { + "name": "_gasPrice", + "type": "uint256" + }, + { + "name": "_gasLimit", + "type": "uint256" + }, + { + "name": "_gasToken", + "type": "address" + } + ], + "name": "callGasRelayHash", + "outputs": [ + { + "name": "_callGasRelayHash", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function", + "signature": "0xe27e2e5c" + }, + { + "constant": true, + "inputs": [], + "name": "nonce", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function", + "signature": "0xaffed0e0" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + }, + { + "name": "_data", + "type": "bytes" + }, + { + "name": "_nonce", + "type": "uint256" + }, + { + "name": "_gasPrice", + "type": "uint256" + }, + { + "name": "_gasLimit", + "type": "uint256" + }, + { + "name": "_gasToken", + "type": "address" + }, + { + "name": "_messageSignatures", + "type": "bytes" + } + ], + "name": "callGasRelayed", + "outputs": [ + { + "name": "success", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function", + "signature": "0xfd0dded5" + }, + { + "constant": false, + "inputs": [ + { + "name": "_baseToken", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + }, + { + "name": "_data", + "type": "bytes" + }, + { + "name": "_nonce", + "type": "uint256" + }, + { + "name": "_gasPrice", + "type": "uint256" + }, + { + "name": "_gasLimit", + "type": "uint256" + }, + { + "name": "_gasToken", + "type": "address" + }, + { + "name": "_messageSignatures", + "type": "bytes" + } + ], + "name": "approveAndCallGasRelayed", + "outputs": [ + { + "name": "success", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function", + "signature": "0x59f4ac61" + }, + { + "constant": true, + "inputs": [ + { + "name": "_value", + "type": "uint256" + }, + { + "name": "_dataHash", + "type": "bytes32" + }, + { + "name": "_nonce", + "type": "uint256" + }, + { + "name": "_gasPrice", + "type": "uint256" + }, + { + "name": "_gasLimit", + "type": "uint256" + }, + { + "name": "_gasToken", + "type": "address" + } + ], + "name": "deployGasRelayHash", + "outputs": [ + { + "name": "_callGasRelayHash", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function", + "signature": "0x86962d85" + } +]; + +export default StatusGasRelayer; diff --git a/test-dapp/contracts/identity/IdentityGasRelay.sol b/test-dapp/contracts/identity/IdentityGasRelay.sol index 8e6941f..99a80a6 100644 --- a/test-dapp/contracts/identity/IdentityGasRelay.sol +++ b/test-dapp/contracts/identity/IdentityGasRelay.sol @@ -119,7 +119,7 @@ contract IdentityGasRelay is Identity { uint startGas = gasleft(); //verify transaction parameters - require(startGas >= _gasLimit); + //require(startGas >= _gasLimit); // TODO: Tune this require(_nonce == nonce); //verify if signatures are valid and came from correct actors; @@ -255,7 +255,7 @@ contract IdentityGasRelay is Identity { uint startGas = gasleft(); //verify transaction parameters - require(startGas >= _gasLimit); + // require(startGas >= _gasLimit); // TODO: tune this require(_nonce == nonce); //verify if signatures are valid and came from correct actors;