Adding gas-relayer source

This commit is contained in:
Richard Ramos 2018-04-10 11:42:36 -04:00
commit 7bae020fa8
12 changed files with 847 additions and 0 deletions

31
app/gas-relayer/README.md Normal file
View 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
```

View 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"
}
]

View 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"}]

View 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":"_gasMinimal","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":"_gasMinimal","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"}]

View 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"}]

View File

@ -0,0 +1,59 @@
{
"blockchain": {
"account": "0x9e14016ba37b23498885864053fded5226161a3a",
"rpcHost": "localhost",
"rpcPort": 8545
},
"whisper": {
"privateKey": "0x1d8ef80c9933e20fa9720bf82f3ad481c6be9e44920932c008bb76655a211add",
"protocol": "ws",
"host": "localhost",
"port": 8546,
"ttl": 20,
"minPow": 0.8,
"powTime": 1000
},
"contracts":{
"IdentityGasRelay": {
"isIdentity": true,
"factoryAddress": "0x24cb6a5b6c81e8ce3edb059ab97984c3d3ea2e3e",
"abiFile": "../abi/IdentityGasRelay.json",
"allowedFunctions": [
{
"function": "approveAndCallGasRelayed(address,address,uint256,bytes,uint256,uint256,uint256,address,bytes)",
"to": "_to",
"value": "_value",
"data": "_data",
"isToken": true,
"token": "_baseToken"
},
{
"function": "callGasRelayed(address,uint256,bytes,uint256,uint256,uint256,address,bytes)",
"to": "_to",
"value": "_value",
"data": "_data",
"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"
}
]
}
}
}

View File

@ -0,0 +1,16 @@
{
"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",
"md5": "^2.2.1",
"web3": "^1.0.0-beta.33"
}
}

View File

@ -0,0 +1,207 @@
const md5 = require('md5');
const Web3 = require('web3');
const config = require('../config/config.json');
const web3 = new Web3(`${config.whisper.protocol}://${config.whisper.host}:${config.whisper.port}`);
const erc20ABI = require('../abi/ERC20.json');
console.info("Starting...")
async function start(){
config.topics = [];
for(let contractName in config.contracts){
// Obtaining the abis
config.contracts[contractName].abi = require(config.contracts[contractName].abiFile);
const lngt = config.contracts[contractName].allowedFunctions.length;
for(i = 0; i < lngt; i++){
config.contracts[contractName].allowedFunctions[i].functionName = config.contracts[contractName].allowedFunctions[i].function.slice(0, config.contracts[contractName].allowedFunctions[i].function.indexOf('('));
// Extracting input
config.contracts[contractName].allowedFunctions[i].inputs = config.contracts[contractName].abi.filter(x => x.name == config.contracts[contractName].allowedFunctions[i].functionName && x.type == "function")[0].inputs;
// Obtaining function signatures
let functionSignature = web3.utils.sha3(config.contracts[contractName].allowedFunctions[i].function).slice(0, 10);
config.contracts[contractName].allowedFunctions[functionSignature] = config.contracts[contractName].allowedFunctions[i];
delete config.contracts[contractName].allowedFunctions[i];
}
config.contracts[contractName].functionSignatures = Object.keys(config.contracts[contractName].allowedFunctions);
// Extracting topics and available functions
let topicName = web3.utils.toHex(contractName).slice(0, 10);
config.topics.push(topicName);
config.contracts[topicName] = config.contracts[contractName];
config.contracts[topicName].name = contractName;
delete config.contracts[contractName];
// Get Contract Bytecode
let contractAddress = config.contracts[topicName].address;
if(config.contracts[topicName].isIdentity){
const lastKernelSignature = "0x4ac99424";
let kernel = await web3.eth.call({to: config.contracts[topicName].factoryAddress, data: lastKernelSignature});
contractAddress = '0x' + kernel.slice(26);
}
try {
config.contracts[topicName].code = md5(await web3.eth.getCode(contractAddress));
} catch(err){
console.error("Invalid contract for " + contractName);
console.error(err);
process.exit();
}
}
// Setting up Whisper options
const shhOptions = {
ttl: config.whisper.ttl,
minPow: config.whisper.minPow,
};
let kId;
// Listening to whisper
web3.shh.addPrivateKey(config.whisper.privateKey)
.then((keyId) => {
shhOptions.privateKeyID = keyId;
kId = keyId;
web3.shh.getPublicKey(keyId).then(pk => {
console.info(`Public Key: ${pk}`);
console.info("Topics Available:");
config.topics = [];
for(let contractName in config.contracts) {
console.info("- %s: %s [%s]", config.contracts[contractName].name, contractName, Object.keys(config.contracts[contractName].allowedFunctions).join(', '));
}
});
console.info("Started.");
console.info("Listening for messages...")
web3.shh.subscribe('messages', shhOptions, processMessages);
});
const reply = async function(text, message){
try {
if(message.sig){
let shhOptions = {
pubKey: message.sig,
sig: kId,
ttl: config.whisper.ttl,
powTarget:config.whisper.minPow,
powTime: config.whisper.powTime,
topic: message.topic,
payload: web3.utils.fromAscii(text)
};
await web3.shh.post(shhOptions);
}
} catch(Err){
// TODO
console.error(Err);
}
}
// Process individual whisper message
const processMessages = async function(error, message, subscription){
if(error){
// TODO log
console.error(error);
} else {
const address = message.payload.slice(0, 42);
const functionName = '0x' + message.payload.slice(42, 50);
const functionParameters = '0x' + message.payload.slice(50);
const payload = '0x' + message.payload.slice(42);
console.info("Processing request to: %s, %s", address, functionName);
if(!/^0x[0-9a-f]{40}$/i.test(address))
return reply('Invalid address', message);
if(config.contracts[message.topic] == undefined)
return reply('Invalid topic', message);
const contract = config.contracts[message.topic];
if(!contract.functionSignatures.includes(functionName))
return reply('Function not allowed', message) // TODO Log this
// Get code from address and compare it against the contract code
const code = md5(await web3.eth.getCode(address));
if(code != contract.code){
return reply('Invalid contract code', message); // TODO Log this
}
// Determining balances
const params = web3.eth.abi.decodeParameters(contract.allowedFunctions[functionName].inputs, functionParameters);
let balance;
if(contract.isIdentity){
if(contract.allowedFunctions[functionName].isToken){
const Token = new web3.eth.Contract(erc20ABI, params[contracts.allowedFunctions[functionName].token]);
balance = new web3.utils.BN(await Token.methods.balanceOf(address).call());
} else {
balance = new web3.utils.BN(await web3.eth.getBalance(address));
}
} else {
// TODO SNT Controller
}
// Estimating gas
let estimatedGas = new web3.utils.BN(await web3.eth.estimateGas({
to: address,
data: params[contracts.allowedFunctions[functionName].data]
}));
// TODO determine if balance is enough
web3.eth.sendTransaction({
from: config.blockchain.account,
to: address,
value: 0,
data: payload
})
.then(function(receipt){
return reply("Transaction mined;" + receipt.transactionHash, message);
}).catch(function(err){
reply("Couldn't mine transaction", message);
// TODO log this?
//console.error(err);
});
}
}
}
start();
// Daemon helper functions
process.on("uncaughtException", function(err) {
});
process.on("SIGUSR1", function() {
log("Reloading...");
log("Reloaded.");
});
process.once("SIGTERM", function() {
log("Stopping...");
});

View File

@ -0,0 +1,55 @@
pragma solidity ^0.4.21;
contract TestIdentityGasRelay {
event Debug();
function approveAndCallGasRelayed(
address _baseToken,
address _to,
uint256 _value,
bytes _data,
uint _nonce,
uint _gasPrice,
uint _gasMinimal,
address _gasToken,
bytes _messageSignatures
) external {
emit Debug();
}
function callGasRelayed(
address _to,
uint256 _value,
bytes _data,
uint _nonce,
uint _gasPrice,
uint _gasMinimal,
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();
}
}

View 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</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 &gt; 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="publicKey">Public Key</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="publicKey" placeholder="Public Key" required>
</div>
<div class="invalid-feedback publicKey" style="width: 100%;">
Invalid Public 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>

View File

@ -0,0 +1,103 @@
$(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();
let publicKey = add0x($("#publicKey"));
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]{130}$/i.test(publicKey)){
$('#publicKey').addClass('is-invalid');
$('.invalid-feedback.publicKey').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({pubKey: "${publicKey}", sig: signature, ttl: ${timeToLive}, powTarget: ${powTarget}, powTime: ${powTime}, topic: "${msgTopic}", payload: "${msgPayload}"})`, 'font-weight: bold');
let identity;
web3.shh.post({ pubKey: publicKey,
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

File diff suppressed because one or more lines are too long