mirror of
https://github.com/status-im/snt-gas-relay.git
synced 2025-01-27 14:44:47 +00:00
reorg
This commit is contained in:
commit
bf63ff1d91
31
app/gas-relayer/README.md
Normal file
31
app/gas-relayer/README.md
Normal file
@ -0,0 +1,31 @@
|
||||
# token-gas-relayer
|
||||
Gas Relayer implementation for Idea #73
|
||||
|
||||
|
||||
To execute as a daemon (only on POSIX systems)
|
||||
```
|
||||
bin/gas-relayer start
|
||||
bin/gas-relayer status
|
||||
bin/gas-relayer stop
|
||||
```
|
||||
|
||||
To execute js file directly
|
||||
```
|
||||
node src/service.js
|
||||
```
|
||||
|
||||
How to send a message to this service (all accounts and privatekeys should be replaced by your own test data)
|
||||
```
|
||||
shh.post({pubKey: PUBLIC_KEY, ttl: 1000, powTarget: 1, powTime: 20, topic: TOPIC_NAME, payload: PAYLOAD_BYTES});
|
||||
```
|
||||
- `PUBLIC_KEY` must contain the whisper public key used. It is shown on the console when running the service with `node`. With the provided configuration you can use the value:
|
||||
```
|
||||
0x044f1ee672354e54eab177ac4d5ce689d8b1f3d41dfc9778f494b99919c0cef9138f821611aecf84f1f2b972b5490d559e521a7a551f69c63e527ba29fbc06406a`
|
||||
```
|
||||
- `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)` in contract `0x692a70d2e424a56d2c6c27aa97d1a86395877b3a`, with these values: `"0x11223344556677889900998877665544332211",100,"0x00",1,10,20,"0x1122334455"` `PAYLOAD_BYTES` would contain the following hex string, where the first 20 bytes are the contract address, the next 4 bytes are the function signature (`0xfd0dded5`), and the rest of the values are the encoded parameters
|
||||
```
|
||||
0x692a70d2e424a56d2c6c27aa97d1a86395877b3afd0dded50000000000000000000000000011223344556677889900998877665544332211000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000011223344550000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000430783030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
```
|
272
app/gas-relayer/abi/ERC20.json
Normal file
272
app/gas-relayer/abi/ERC20.json
Normal file
@ -0,0 +1,272 @@
|
||||
[
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "name",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "approve",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "success",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "totalSupply",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transferFrom",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "success",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "decimals",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint8"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "version",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_owner",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "balanceOf",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "balance",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "symbol",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transfer",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "success",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_value",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "_extraData",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "approveAndCall",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "success",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_spender",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "allowance",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "remaining",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_initialAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "_tokenName",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "_decimalUnits",
|
||||
"type": "uint8"
|
||||
},
|
||||
{
|
||||
"name": "_tokenSymbol",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"type": "constructor"
|
||||
},
|
||||
{
|
||||
"payable": false,
|
||||
"type": "fallback"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "_from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "_to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "_value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Transfer",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "_owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "_spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "_value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Approval",
|
||||
"type": "event"
|
||||
}
|
||||
]
|
1
app/gas-relayer/abi/IdentityFactory.json
Normal file
1
app/gas-relayer/abi/IdentityFactory.json
Normal file
@ -0,0 +1 @@
|
||||
[{"constant":true,"inputs":[],"name":"latestKernel","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]
|
1
app/gas-relayer/abi/IdentityGasRelay.json
Normal file
1
app/gas-relayer/abi/IdentityGasRelay.json
Normal file
@ -0,0 +1 @@
|
||||
[{"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":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"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":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[],"name":"Debug","type":"event"}]
|
1
app/gas-relayer/abi/SNTController.json
Normal file
1
app/gas-relayer/abi/SNTController.json
Normal file
@ -0,0 +1 @@
|
||||
[{"constant":false,"inputs":[{"name":"a","type":"address"},{"name":"b","type":"bytes"},{"name":"c","type":"uint256"},{"name":"d","type":"uint256"},{"name":"e","type":"uint256"},{"name":"f","type":"bytes"}],"name":"executeGasRelayed","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"a","type":"address"},{"name":"b","type":"uint256"},{"name":"c","type":"uint256"},{"name":"d","type":"uint256"},{"name":"f","type":"bytes"}],"name":"transferSNT","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[],"name":"Debug","type":"event"}]
|
78
app/gas-relayer/config/config.json
Normal file
78
app/gas-relayer/config/config.json
Normal file
@ -0,0 +1,78 @@
|
||||
{
|
||||
"node": {
|
||||
"protocol": "ws",
|
||||
"host": "localhost",
|
||||
"port": 8546,
|
||||
"blockchain": {
|
||||
"account": "0x9e14016ba37b23498885864053fded5226161a3a"
|
||||
},
|
||||
"whisper": {
|
||||
"symKey": "0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b",
|
||||
"ttl": 20,
|
||||
"minPow": 0.8,
|
||||
"powTime": 1000
|
||||
}
|
||||
},
|
||||
|
||||
"tokens": {
|
||||
"0x0000000000000000000000000000000000000000": {
|
||||
"minRelayFactor": 1,
|
||||
"name": "Ethereum",
|
||||
"symbol": "ETH"
|
||||
},
|
||||
"0x1d97fCe424c029Fc504a171918F934C6dBAcB6aE": {
|
||||
"minRelayFactor": 1000000000000,
|
||||
"name": "RandomToken",
|
||||
"symbol": "RDN",
|
||||
"pricePlugin": "../plugins/token-utils.js"
|
||||
}
|
||||
},
|
||||
|
||||
"contracts":{
|
||||
"IdentityGasRelay": {
|
||||
"isIdentity": true,
|
||||
"factoryAddress": "0x18f71378f4735b35e01eb71afcc66df090eccea7",
|
||||
"abiFile": "../abi/IdentityGasRelay.json",
|
||||
"allowedFunctions": [
|
||||
{
|
||||
"function": "approveAndCallGasRelayed(address,address,uint256,bytes,uint256,uint256,uint256,address,bytes)",
|
||||
"to": "_to",
|
||||
"value": "_value",
|
||||
"data": "_data",
|
||||
"gasPrice": "_gasPrice",
|
||||
"gasToken": "_gasToken",
|
||||
"gasLimit": "_gasLimit",
|
||||
"isToken": true,
|
||||
"token": "_baseToken"
|
||||
},
|
||||
{
|
||||
"function": "callGasRelayed(address,uint256,bytes,uint256,uint256,uint256,address,bytes)",
|
||||
"to": "_to",
|
||||
"value": "_value",
|
||||
"data": "_data",
|
||||
"gasPrice": "_gasPrice",
|
||||
"gasToken": "_gasToken",
|
||||
"gasLimit": "_gasLimit",
|
||||
"isToken": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"SNTController": {
|
||||
"isIdentity": false,
|
||||
"address": "0x96f0811c6484c59c2674da1f64e725c01d82c1b5",
|
||||
"abiFile": "../abi/SNTController.json",
|
||||
"allowedFunctions": [
|
||||
{
|
||||
"function":"transferSNT(address,uint256,uint256,uint256,bytes)",
|
||||
"to": "_to",
|
||||
"value": "_amount"
|
||||
},
|
||||
{
|
||||
"function":"executeGasRelayed(address,bytes,uint256,uint256,uint256,bytes)",
|
||||
"to": "_to",
|
||||
"value": "_amount"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
17
app/gas-relayer/package.json
Normal file
17
app/gas-relayer/package.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "gas-relayer",
|
||||
"version": "0.0.1",
|
||||
"description": "Gas relayer to avoid having to hold ether to perform transactions when you already have a token",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"daemonize2": "^0.4.2",
|
||||
"ganache-cli": "^6.1.0",
|
||||
"md5": "^2.2.1",
|
||||
"web3": "^1.0.0-beta.33"
|
||||
}
|
||||
}
|
15
app/gas-relayer/plugins/token-utils.js
Normal file
15
app/gas-relayer/plugins/token-utils.js
Normal file
@ -0,0 +1,15 @@
|
||||
class TokenUtils {
|
||||
constructor(tokenConfig){
|
||||
this.name = tokenConfig.name || "";
|
||||
this.symbol = tokenConfig.symbol || "";
|
||||
this.minRelayFactor = tokenConfig.minRelayFactor || 1;
|
||||
}
|
||||
|
||||
getFactor(){
|
||||
// TODO get price from somewhere
|
||||
return 100000;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = TokenUtils;
|
111
app/gas-relayer/src/contract-settings.js
Normal file
111
app/gas-relayer/src/contract-settings.js
Normal file
@ -0,0 +1,111 @@
|
||||
const md5 = require('md5');
|
||||
|
||||
class ContractSettings {
|
||||
|
||||
constructor(config, web3, eventEmitter){
|
||||
this.tokens = config.tokens;
|
||||
this.topics = [];
|
||||
this.contracts = config.contracts;
|
||||
|
||||
this.web3 = web3;
|
||||
this.events = eventEmitter;
|
||||
|
||||
this.pendingToLoad = 0;
|
||||
|
||||
this.events.on('setup:bytecode-address', this._obtainContractBytecode.bind(this))
|
||||
}
|
||||
|
||||
process(){
|
||||
this._setTokenPricePlugin();
|
||||
this._processContracts();
|
||||
}
|
||||
|
||||
getToken(token){
|
||||
return this.tokens[token];
|
||||
}
|
||||
|
||||
getContractByTopic(topicName){
|
||||
return this.contracts[topicName];
|
||||
}
|
||||
|
||||
getTopicName(contractName){
|
||||
return this.web3.utils.toHex(contractName).slice(0, 10);
|
||||
}
|
||||
|
||||
_setTokenPricePlugin(){
|
||||
for(let token in this.tokens){
|
||||
if(this.tokens[token].pricePlugin !== undefined){
|
||||
let PricePlugin = require(this.tokens[token].pricePlugin);
|
||||
this.tokens[token].pricePlugin = new PricePlugin(this.tokens[token]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_determineBytecodeAddress(topicName, i){
|
||||
let contractAddress = this.contracts[topicName].address;
|
||||
if(this.contracts[topicName].isIdentity){
|
||||
this.pendingToLoad++;
|
||||
const lastKernelSignature = "0x4ac99424"; // REFACTOR
|
||||
this.web3.eth.call({to: this.contracts[topicName].factoryAddress, data: lastKernelSignature})
|
||||
.then(kernel => {
|
||||
contractAddress = '0x' + kernel.slice(26);
|
||||
this.events.emit('setup:bytecode-address', topicName, contractAddress);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_obtainContractBytecode(topicName, contractAddress){
|
||||
this.web3.eth.getCode(contractAddress)
|
||||
.then(code => {
|
||||
this.contracts[topicName].code = md5(code);
|
||||
this.pendingToLoad--;
|
||||
if(this.pendingToLoad == 0) this.events.emit("setup:complete", this);
|
||||
})
|
||||
.catch(function(err){
|
||||
console.error("Invalid contract for " + contractName);
|
||||
console.error(err);
|
||||
process.exit();
|
||||
});
|
||||
}
|
||||
|
||||
_extractFunctions(topicName){
|
||||
const contract = this.getContractByTopic(topicName);
|
||||
|
||||
for(let i = 0; i < contract.allowedFunctions.length; i++){
|
||||
contract.allowedFunctions[i].functionName = contract.allowedFunctions[i].function.slice(0, contract.allowedFunctions[i].function.indexOf('('));
|
||||
|
||||
// Extracting input
|
||||
contract.allowedFunctions[i].inputs = contract.abi.filter(x => x.name == contract.allowedFunctions[i].functionName && x.type == "function")[0].inputs;
|
||||
|
||||
// Obtaining function signatures
|
||||
let functionSignature = this.web3.utils.sha3(contract.allowedFunctions[i].function).slice(0, 10);
|
||||
contract.allowedFunctions[functionSignature] = contract.allowedFunctions[i];
|
||||
delete this.contracts[topicName].allowedFunctions[i];
|
||||
}
|
||||
|
||||
contract.functionSignatures = Object.keys(contract.allowedFunctions);
|
||||
this.contracts[topicName] = contract;
|
||||
}
|
||||
|
||||
_processContracts(){
|
||||
for(let contractName in this.contracts){
|
||||
// Obtaining the abis
|
||||
this.contracts[contractName].abi = require(this.contracts[contractName].abiFile);
|
||||
|
||||
const topicName = this.getTopicName(contractName);
|
||||
|
||||
// Extracting topic
|
||||
this.topics.push(topicName);
|
||||
this.contracts[topicName] = this.contracts[contractName];
|
||||
this.contracts[topicName].name = contractName;
|
||||
delete this.contracts[contractName];
|
||||
|
||||
this._determineBytecodeAddress(topicName);
|
||||
|
||||
this._extractFunctions(topicName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = ContractSettings;
|
184
app/gas-relayer/src/message-processor.js
Normal file
184
app/gas-relayer/src/message-processor.js
Normal file
@ -0,0 +1,184 @@
|
||||
const md5 = require('md5');
|
||||
const erc20ABI = require('../abi/ERC20.json');
|
||||
const ganache = require("ganache-cli");
|
||||
|
||||
class MessageProcessor {
|
||||
|
||||
constructor(config, settings, web3, kId){
|
||||
this.config = config;
|
||||
this.settings = settings;
|
||||
this.web3 = web3;
|
||||
this.kId = kId;
|
||||
}
|
||||
|
||||
|
||||
_reply(text, message){
|
||||
if(message.sig !== undefined){
|
||||
this.web3.shh.post({
|
||||
pubKey: message.sig,
|
||||
sig: this.kId,
|
||||
ttl: this.config.node.whisper.ttl,
|
||||
powTarget:this.config.node.whisper.minPow,
|
||||
powTime: this.config.node.whisper.powTime,
|
||||
topic: message.topic,
|
||||
payload: this.web3.utils.fromAscii(text)
|
||||
}).catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async _validateInput(message, input){
|
||||
const contract = this.settings.getContractByTopic(message.topic);
|
||||
|
||||
if(!/^0x[0-9a-f]{40}$/i.test(input.address)){
|
||||
this._reply('Invalid address', message);
|
||||
return false;
|
||||
}
|
||||
|
||||
if(contract == undefined){
|
||||
this._reply('Invalid topic', message);
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!contract.functionSignatures.includes(input.functionName)){
|
||||
this._reply('Function not allowed', message);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get code from address and compare it against the contract code
|
||||
const code = md5(await this.web3.eth.getCode(input.address));
|
||||
if(code != contract.code){
|
||||
this._reply('Invalid contract code', message);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
_extractInput(message){
|
||||
return {
|
||||
address: message.payload.slice(0, 42),
|
||||
functionName: '0x' + message.payload.slice(42, 50),
|
||||
functionParameters: '0x' + message.payload.slice(50),
|
||||
payload: '0x' + message.payload.slice(42)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
_obtainParametersFunc(contract, input){
|
||||
const parameterList = this.web3.eth.abi.decodeParameters(contract.allowedFunctions[input.functionName].inputs, input.functionParameters);
|
||||
return function(parameterName){
|
||||
return parameterList[contract.allowedFunctions[input.functionName][parameterName]];
|
||||
}
|
||||
}
|
||||
|
||||
_getFactor(input, contract, gasToken){
|
||||
if(contract.allowedFunctions[input.functionName].isToken){
|
||||
return this.web3.utils.toBN(this.settings.getToken(gasToken).pricePlugin.getFactor());
|
||||
} else {
|
||||
return this.web3.utils.toBN(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async getBalance(token, input){
|
||||
// Determining balances of token used
|
||||
if(token.symbol == "ETH")
|
||||
return new this.web3.utils.BN(await this.web3.eth.getBalance(input.address));
|
||||
else {
|
||||
const Token = new this.web3.eth.Contract(erc20ABI);
|
||||
Token.options.address = params('gasToken');
|
||||
return new this.web3.utils.BN(await Token.methods.balanceOf(input.address).call());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
_estimateGas(input){
|
||||
const web3Sim = new Web3(ganache.provider({fork: `${this.config.node.protocol}://${this.config.node.host}:${this.config.node.port}`}));
|
||||
const simAccounts = await web3Sim.eth.getAccounts();
|
||||
let simulatedReceipt = await web3Sim.eth.sendTransaction({
|
||||
from: simAccounts[0],
|
||||
to: input.address,
|
||||
value: 0,
|
||||
data: input.payload
|
||||
});
|
||||
return web3Sim.utils.toBN(simulatedReceipt.gasUsed);
|
||||
}
|
||||
|
||||
|
||||
async process(error, message){
|
||||
|
||||
if(error){
|
||||
console.error(error);
|
||||
} else {
|
||||
|
||||
let input = this._extractInput(message);
|
||||
|
||||
const contract = this.settings.getContractByTopic(message.topic);
|
||||
|
||||
console.info("Processing request to: %s, %s", input.address, input.functionName);
|
||||
|
||||
if(!this._validateInput(message, input)) return; // TODO Log
|
||||
|
||||
const params = this._obtainParametersFunc(contract, input);
|
||||
|
||||
const token = this.settings.getToken(params('gasToken'));
|
||||
if(token == undefined)
|
||||
return reply("Token not allowed", message);
|
||||
|
||||
const gasPrice = this.web3.utils.toBN(params('gasPrice'));
|
||||
const gasLimit = this.web3.utils.toBN(params('gasLimit'));
|
||||
|
||||
|
||||
// Determine if enough balance for baseToken
|
||||
if(contract.allowedFunctions[input.functionName].isToken){
|
||||
const Token = new this.web3.eth.Contract(erc20ABI);
|
||||
Token.options.address = params('token');
|
||||
const baseToken = new this.web3.utils.BN(await Token.methods.balanceOf(input.address).call());
|
||||
if(balance.lt(this.web3.utils.BN(params('value')))){
|
||||
this._reply("Not enough balance", message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const balance = await this.getBalance(token, input);
|
||||
const gasToken = params('gasToken');
|
||||
const factor = this._getFactor(input, contract, gasToken);
|
||||
|
||||
|
||||
const balanceInETH = balance.div(factor);
|
||||
const gasLimitInETH = gasLimit.div(factor);
|
||||
|
||||
if(balanceInETH.lt(this.web3.utils.toBN(gasPrice.mul(gasLimit)))) {
|
||||
this._reply("Not enough balance", message);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const estimatedGas = this._estimateGas(input);
|
||||
if(gasLimit.lt(estimatedGas)) {
|
||||
return this._reply("Gas limit below estimated gas", message);
|
||||
}
|
||||
|
||||
this.web3.eth.sendTransaction({
|
||||
from: this.config.node.blockchain.account,
|
||||
to: address,
|
||||
value: 0,
|
||||
data: input.payload,
|
||||
gasLimit: gasLimitInETH
|
||||
})
|
||||
.then(function(receipt){
|
||||
return this._reply("Transaction mined;" + receipt.transactionHash, message);
|
||||
}).catch(function(err){
|
||||
this._reply("Couldn't mine transaction", message);
|
||||
// TODO log this?
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MessageProcessor;
|
79
app/gas-relayer/src/service.js
Normal file
79
app/gas-relayer/src/service.js
Normal file
@ -0,0 +1,79 @@
|
||||
const EventEmitter = require('events');
|
||||
const Web3 = require('web3');
|
||||
const config = require('../config/config.json');
|
||||
|
||||
const ContractSettings = require('./contract-settings');
|
||||
const MessageProcessor = require('./message-processor');
|
||||
|
||||
|
||||
// TODO 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();
|
||||
|
||||
// Web3 Connection
|
||||
let connectionURL = `${config.node.protocol}://${config.node.host}:${config.node.port}`;
|
||||
const web3 = new Web3(connectionURL);
|
||||
|
||||
web3.eth.net.isListening()
|
||||
.then(listening => events.emit('web3:connected', connectionURL))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
process.exit();
|
||||
});
|
||||
|
||||
|
||||
events.on('web3:connected', connURL => {
|
||||
console.info("Connected to '%s'", connURL);
|
||||
let settings = new ContractSettings(config, web3, events);
|
||||
settings.process();
|
||||
});
|
||||
|
||||
|
||||
events.on('setup:complete', (settings) => {
|
||||
// Setting up Whisper options
|
||||
const shhOptions = {
|
||||
ttl: config.node.whisper.ttl,
|
||||
minPow: config.node.whisper.minPow,
|
||||
};
|
||||
|
||||
let kId;
|
||||
let symKId;
|
||||
|
||||
// Listening to whisper
|
||||
web3.shh.addSymKey(config.node.whisper.symKey)
|
||||
.then(symKeyId => {
|
||||
symKId = symKeyId;
|
||||
return web3.shh.newKeyPair();
|
||||
})
|
||||
.then(keyId => {
|
||||
shhOptions.symKeyId = symKId;
|
||||
shhOptions.kId = keyId;
|
||||
|
||||
console.info(`Sym Key: ${config.node.whisper.symKey}`);
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
events.on('server:listen', (shhOptions, settings) => {
|
||||
let processor = new MessageProcessor(config, settings, web3, shhOptions.kId);
|
||||
web3.shh.subscribe('messages', shhOptions, (error, message, subscription) => processor.process(error, message));
|
||||
});
|
||||
|
||||
|
||||
// Daemon helper functions
|
||||
|
||||
process.on("uncaughtException", function(err) {
|
||||
|
||||
});
|
||||
|
||||
process.once("SIGTERM", function() {
|
||||
log("Stopping...");
|
||||
});
|
53
app/gas-relayer/test/sampleContracts.sol
Normal file
53
app/gas-relayer/test/sampleContracts.sol
Normal file
@ -0,0 +1,53 @@
|
||||
pragma solidity ^0.4.21;
|
||||
|
||||
contract TestIdentityGasRelay {
|
||||
event Debug();
|
||||
|
||||
function approveAndCallGasRelayed(
|
||||
address _baseToken,
|
||||
address _to,
|
||||
uint256 _value,
|
||||
bytes _data,
|
||||
uint _nonce,
|
||||
uint _gasPrice,
|
||||
uint _gasLimit,
|
||||
address _gasToken,
|
||||
bytes _messageSignatures
|
||||
) external {
|
||||
emit Debug();
|
||||
}
|
||||
|
||||
function callGasRelayed(
|
||||
address _to,
|
||||
uint256 _value,
|
||||
bytes _data,
|
||||
uint _nonce,
|
||||
uint _gasPrice,
|
||||
uint _gasLimit,
|
||||
address _gasToken,
|
||||
bytes _messageSignatures
|
||||
) external {
|
||||
emit Debug();
|
||||
}
|
||||
|
||||
function() payable {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
contract TestIdentityFactory {
|
||||
address public latestKernel;
|
||||
function TestIdentityFactory(){
|
||||
latestKernel = address(new TestIdentityGasRelay());
|
||||
}
|
||||
}
|
||||
|
||||
contract TestSNTController {
|
||||
event Debug();
|
||||
function transferSNT(address a,uint256 b,uint256 c,uint256 d, bytes f){
|
||||
emit Debug();
|
||||
}
|
||||
function executeGasRelayed(address a,bytes b,uint256 c,uint256 d,uint256 e,bytes f){
|
||||
emit Debug();
|
||||
}
|
||||
}
|
100
app/gas-relayer/test/sendmsg.html
Normal file
100
app/gas-relayer/test/sendmsg.html
Normal file
@ -0,0 +1,100 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||
<title>Send whisper message to node</title>
|
||||
<style type="text/css">
|
||||
h4 small {
|
||||
color: #c3c3c3;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<body class="bg-light">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12 order-md-1">
|
||||
<h4 class="mb-3"><br />Send whisper message <small>ws://localhost:8546</small></h4>
|
||||
<p><code>web3</code> is available in your browser's console: <code>Tools > Developer Tools</code></p>
|
||||
<b>Keys</b>:
|
||||
<ul style="font-size:12px">
|
||||
<li>Public: <span class="pub"></span></li>
|
||||
<li>Private <span class="priv"></span></li>
|
||||
</ul>
|
||||
<form novalidate>
|
||||
<div class="mb-3">
|
||||
<label for="sKey">Symkey</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">0x</span>
|
||||
</div>
|
||||
<input type="text" class="form-control" id="sKey" placeholder="Public Key" required value="0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b">
|
||||
</div>
|
||||
<div class="invalid-feedback sKey" style="width: 100%;">
|
||||
Invalid Sym Key
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-3">
|
||||
<label for="topic">Topic</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">0x</span>
|
||||
</div>
|
||||
<input type="text" class="form-control" id="topic" placeholder="" value="" required>
|
||||
</div>
|
||||
<div class="invalid-feedback topic" style="width: 100%;">
|
||||
Invalid Topic
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label for="ttl">TTL</label>
|
||||
<input type="text" class="form-control" id="ttl" value="1000" placeholder="" required>
|
||||
<div class="invalid-feedback ttl" style="width: 100%;">
|
||||
Invalid TTL
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label for="powTarget">PoW Target</label>
|
||||
<input type="text" class="form-control" id="powTarget" value="1" placeholder="" required>
|
||||
<div class="invalid-feedback powTarget" style="width: 100%;">
|
||||
Invalid PoW Target
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label for="powTime">PoW Time</label>
|
||||
<input type="text" class="form-control" id="powTime" value="20" placeholder="" required>
|
||||
<div class="invalid-feedback powTime" style="width: 100%;">
|
||||
Invalid PoW Time
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="payload">Payload</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">0x</span>
|
||||
</div>
|
||||
<input type="text" class="form-control" id="payload" placeholder="Payload" required>
|
||||
</div>
|
||||
<div class="invalid-feedback payload" style="width: 100%;">
|
||||
Invalid Payload
|
||||
</div>
|
||||
</div>
|
||||
<hr class="mb-4">
|
||||
<p class="result"></p>
|
||||
<p id="messageArea"></p>
|
||||
<button class="btn btn-primary btn-lg btn-block" type="submit">Send Message</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
|
||||
<script src="web3.min.js"></script>
|
||||
<script src="sendmsg.js"></script>
|
||||
</body>
|
||||
</html>
|
109
app/gas-relayer/test/sendmsg.js
Normal file
109
app/gas-relayer/test/sendmsg.js
Normal file
@ -0,0 +1,109 @@
|
||||
$(function(){
|
||||
|
||||
const connUrl = "ws://localhost:8546";
|
||||
let web3 = new Web3(connUrl);
|
||||
window.web3 = web3;
|
||||
|
||||
let keyPair;
|
||||
|
||||
web3.shh.newKeyPair().then(async function(kid){
|
||||
$('.pub').text(await web3.shh.getPublicKey(kid));
|
||||
$('.priv').text(await web3.shh.getPrivateKey(kid));
|
||||
keyPair = kid;
|
||||
window.signature = kid;
|
||||
|
||||
web3.shh.subscribe('messages', {
|
||||
"privateKeyID": signature,
|
||||
"ttl": 20,
|
||||
"minPow": 0.8,
|
||||
"powTime": 1000
|
||||
}, function(error, message, subscription){
|
||||
console.log(web3.utils.hexToAscii(message.payload));
|
||||
$('#messageArea').text(web3.utils.hexToAscii(message.payload));
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
console.log("Connected to: %c%s", 'font-weight: bold', connUrl);
|
||||
|
||||
const add0x = function(elem){
|
||||
if(elem.val().slice(0, 2) != '0x'){
|
||||
return '0x' + elem.val();
|
||||
} else {
|
||||
let val = elem.val();
|
||||
elem.val(elem.val().slice(2));
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
$('button').on('click', async function(e){
|
||||
e.preventDefault();
|
||||
|
||||
$('p.result').text('');
|
||||
|
||||
let sKey = add0x($("#sKey"));
|
||||
let msgTopic = add0x($('#topic'));
|
||||
let msgPayload = add0x($('#payload'));
|
||||
let timeToLive = $('#ttl').val();
|
||||
let powTarget = $('#powTarget').val();
|
||||
let powTime = $('#powTime').val();
|
||||
|
||||
$('.invalid-feedback').hide();
|
||||
$('.is-invalid').removeClass('is-invalid');
|
||||
|
||||
if(!/^0x[0-9a-f]{64}$/i.test(sKey)){
|
||||
$('#sKey').addClass('is-invalid');
|
||||
$('.invalid-feedback.sKey').show();
|
||||
}
|
||||
|
||||
if(!/^0x[0-9a-f]{8}$/i.test(msgTopic)){
|
||||
$('#topic').addClass('is-invalid');
|
||||
$('.invalid-feedback.topic').show();
|
||||
}
|
||||
|
||||
if(!/^0x[0-9a-f]+$/i.test(msgPayload) || msgPayload.length%2 > 0){
|
||||
$('#payload').addClass('is-invalid');
|
||||
$('.invalid-feedback.payload').show();
|
||||
}
|
||||
|
||||
if(!/^[0-9]+$/i.test(timeToLive)){
|
||||
$('#ttl').addClass('is-invalid');
|
||||
$('.invalid-feedback.ttl').show();
|
||||
}
|
||||
|
||||
if(!/^[+-]?([0-9]*[.])?[0-9]+$/.test(powTarget)){
|
||||
$('#powTarget').addClass('is-invalid');
|
||||
$('.invalid-feedback.powTarget').show();
|
||||
}
|
||||
|
||||
if(!/^[+-]?([0-9]*[.])?[0-9]+$/.test(powTime)){
|
||||
$('#powTime').addClass('is-invalid');
|
||||
$('.invalid-feedback.powTime').show();
|
||||
}
|
||||
|
||||
|
||||
if($('.is-invalid').length > 0) return;
|
||||
|
||||
console.log(`%c await web3.shh.post({symKeyID: "${sKey}", sig: signature, ttl: ${timeToLive}, powTarget: ${powTarget}, powTime: ${powTime}, topic: "${msgTopic}", payload: "${msgPayload}"})`, 'font-weight: bold');
|
||||
|
||||
let identity;
|
||||
|
||||
|
||||
let _symKeyId = await web3.shh.addSymKey(sKey);
|
||||
|
||||
|
||||
web3.shh.post({
|
||||
symKeyID: _symKeyId,
|
||||
sig: keyPair,
|
||||
ttl: parseInt(timeToLive),
|
||||
powTarget: parseFloat(powTarget),
|
||||
powTime: parseFloat(powTime),
|
||||
topic: msgTopic,
|
||||
payload: msgPayload})
|
||||
.then(result => {
|
||||
console.log(result);
|
||||
$('p.result').html("<b>Response:</b> " + result);
|
||||
});
|
||||
});
|
||||
});
|
1
app/gas-relayer/test/web3.min.js
vendored
Normal file
1
app/gas-relayer/test/web3.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user