02 Feb
2017
Technology , Opinion

Creating offline Ethereum transactions in JavaScript

mikko@tokenmarket.net

Preface

In this blog post we show how to create Ethereum blockchain transactions programmatically in JavaScript and offline. This is the second part of tutorial blog posts continuing the earlier Ethereum JavaScript tutorial left.

We bend the meaning of the word offline here a bit. We still communicate with Ethereum network to get the gas cost and the next transaction nonce. However, the private key never leaves the computer.

This blog post serves mostly as an example. Advanced users can use the script presented here for doing Ether cold wallet and offline transactions.

Ethereum transaction dissemination

Each Ethereum transaction consists of

  • Target address, either a contract or an Ethereum account.

  • Nonce, incremented once per transaction per account, to prevent double spend attacks.

  • Value, how many Ethers are transferred. Can be 0 for contract calls, but you still need to pay for the gas.

  • The maximum gas the transaction is allowed pull from the sending account.

  • The gas price if the transaction needs a preferential treatment.

  • Optional data payload that would include any function call and arguments encoded.

Transaction is signed with the private key of the sending account address. Based on these arguments you can create a raw transaction, as a hex string, that can be pushed to the network.

Pushing can happen either through EtherScan.io service or through web3.eth.sendRawTransaction API.

Furthermore in this particular example we also need

  • EtherScan.io API key - we use their API to communicate with the network

Example script

The example script is available on TokenMarket Github repository.

To run the script

  • You need to understand Node.js and Babel basic

  • Node 7 or newer

  • Clone the git repository

  • Install dependencies with NPM - see previous blog post

Please note that this command line monster will go away in the future when Node.js starts to support ECMAScript 2017 standard natively.

Example how to run:

./node_modules/babel-cli/bin/babel-node.js --presets es2015 --plugins transform-async-to-generator ./src/offlinetx.js --private-key 0xaaaaaaaaa --value 0.9 --to 0xbbbbbbbbb --api-key ABCDEFABCDEFABCDEF

The output looks like:

Building transaction with parameters
 { contractAddress: ',
  privateKey: '0xaaaaaaaaaaaaaaaaaaa',
  nonce: '0x3',
  functionSignature: null,
  functionParameters: null,
  value: '0xc7d713b49da0000',
  gasLimit: '0x30d40',
  gasPrice: '0x4a817c800',
  fromAddress: '0xbbbbbbbbbbbbbbbb',
  gasLimitInt: 200000,
  gasPriceInt: 20000000000,
  weiValueInt: 900000000000000000 }
Your raw transaction is:  0xf86d038504a817c80083030d4094aaf2ac6b800398f671b0d24cb8fccc3897b6ae49880c7d713b49da0000801ba0599365a98d5469b28538e57da6a8e2210ed593e4137224f9820ae7a86b3012aea06307c82e0a1cfb4c3cfd0b603d3c2280b118302ac43f90e864ac05d3cb10ad95
Visit at https://etherscan.io/pushTx to broadcast your transaction.

The example script that is also available on the Github repository.

Code:

/**
 * Sign an Ethereum transaction offline.
 *
 *
 * To run:
 *
 *   nvm use 7.2.1
 *   ./node_modules/babel-cli/bin/babel-node.js --presets es2015 --plugins transform-async-to-generator ./src/offlinetx.js --private-key 0xaaaaaaaaaaaaaaaaaaa --value 0.95 --to 0xAaF2ac6b800398F671b0D24cb8FccC3897B6aE49 --api-key HHHHHHHHHHHHHHHHHHHHHH

 *
 */

import Web3 from 'web3'; // https://www.npmjs.com/package/web3
import argv from 'optimist';
import {buildTx, getAddressFromPrivateKey} from './txbuilder';
import {API} from './etherscan';

let web3 = new Web3();

let _argv = argv.usage("offlinetx $0 --api-key 0x0000000 --private-key 0x00000000000000 --value 1.20 --to 0x1231231231")
  .describe("value", "Transaction amount in ETH")
  .describe("private-key", "32 bytes private key has hexadecimal format")
  .describe("to", "Ethereum 0x address where to send")
  .describe("nonce", "Nonce as hexacimal format, like 0x1")
  .describe("api-key", "etherscan.io API key used to get network status")
  .describe("gas-limt", "Maximum gas limit for the transaction as a hexadecmial")
  .default("gas-limit", web3.toHex(200000))
  .string("private-key")  // Heurestics is number by default which doesn't work for bigints
  .string("to")
  .string("api-key")
  .string("value")
  .string("nonce")
  .string("gas-limit")
  .demand(["private-key", "value", "to"]);

async function run(argv) {

  // Parse command line
  let apiKey = argv["api-key"];
  let privateKey = argv["private-key"];
  let value = argv["value"];
  let to = argv["to"];
  let nonce = argv["nonce"];
  let gasLimit = argv["gas-limit"];

  if(!privateKey) {
    console.error("No private key given");
    process.exit(1);
  }

  if(!apiKey) {
    console.error("No EtherScan.io API key given");
    process.exit(1);
  }

  // Build EtherScan.io API wrapper object
  const api = new API("https://api.etherscan.io/api", apiKey);

  let fromAddress = getAddressFromPrivateKey(privateKey);

  // If nonce is not given get it usig Etherscan
  if(!nonce) {
    nonce = await api.getTransactionCount(fromAddress);
  }

  let gasPrice = await api.getGasPrice();

  value = web3.toHex(web3.toWei(value, "ether"));

  // What goes into our transaction
  let txData = {
    contractAddress: to,
    privateKey: privateKey,
    nonce: nonce,
    functionSignature: null, // TODO: Add contract call support here
    functionParameters: null,
    value: value,
    gasLimit: gasLimit,
    gasPrice: gasPrice,
  };

  // User readable output
  let info = Object.assign({}, txData);
  info.fromAddress = fromAddress;
  info.gasLimitInt = parseInt(txData.gasLimit, 16);
  info.gasPriceInt = parseInt(txData.gasPrice, 16);
  info.weiValueInt = parseInt(txData.value, 16);
  console.log("Building transaction with parameters\n", info);

  let tx = buildTx(txData);

  console.log("Your raw transaction is: ", tx);
  console.log("Visit at https://etherscan.io/pushTx to broadcast your transaction.");

}

run(_argv.argv)
  .catch(function(e) {
    console.error(e);
  });

Used components