Skip to content

gaswelder/ethanol

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

94 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ethanol

Humane JS wrappers around web3 and solc.

Defining the RPC endpoint

The work starts with getting a Blockchain object:

const { Blockchain } = require("ethanol");
const bc = Blockchain.at("http://localhost:8545");

The argument to the at function is the URL of an RPC node representing the blockchain. Besides the http and https schemas the ipc schema can also be used:

const bc = Blockchain.at("ipc:///home/john/geth/geth.ipc");

The blockchain provides several query functions:

const latestBlockNumber = await bc.blockNumber();
const firstBlock = await bc.block(1);
const someBalance = await bc.balanceOf(
	"0x5e94baef74b60e98116b971e9240d914f4059e27"
);

Users

Because any transaction assumes a sender account, a user object is needed for most of the tasks. A default user can be obtained with the user function of a blockchain:

const alice = await bc.user();

If the blockchain is connected via IPC, then the user embedded in the local client (for example, geth) will be assumed. The local user is typically an unlocked account that can send signed transactions.

If the interface is HTTP, then a user with a hardcoded mnemonic will be assumed. A user with a specific BIP44 mnemonic can be obtained using the userFromMnemonic function:

const { Blockchain } = require("ethanol");
const bc = Blockchain.at("http://localhost:8545");
const bob = await bc.userFromMnemonic("science truck gospel ...", 0);

The second argument is optional and defines the index in the sequence of private keys generated by the mnemonic. The mnemonic and the index define the private key.

Users can report their address and balance, and transfer Ether (in Wei units):

const { Blockchain } = require("ethanol");
const bc = Blockchain.at("http://localhost:8545");

const alice = await bc.userFromMnemonic(
	"eeny meeny miny moe catch a tiger by the toe"
);
const bob = await bc.user();

const address = alice.address();
const balance = await alice.balance();

// Give 10 Wei to Bob
const transaction = await alice.give(bob, 10);

Transactions

Transaction objects are returned for plain ether transfers, and also for contract calls and deployments.

const transaction1 = await alice.give(bob, 10);
const transaction2 = await alice.call(contract, "funcName");

The transaction's success function returns a promise which resolves when the transaction is mined successfully or rejects when the transaction is rejected:

const transaction = await alice.call(contract, "funcName");
try {
	await transaction.success();
} catch (error) {
	console.log("transaction failed:", error);
}

An often used shorthand for making a transaction and waiting for its completion is:

await alice.give(bob, 1).then(t => t.success());

This additional step has to be explicit to allow an application choose whether to wait or not for the actual result.

Calling contracts

In order to work with a contract one needs to know its ABI (which is typically saved as JSON) and its address. The contract function on a blockchain object returns a "deployed contract" object.

const abi = JSON.parse(fs.readFileSync("contract.abi"));
const address = "0x1234...";

const contract = bc.contract(abi, address);

Only a user can call a contract function, so a user object is required. User instances have read and call methods to statically read the contract's variables, make dry function runs or make actual calls:

const val = await alice.read(contract, "variableName");
const result = await alice.read(contract, "functionName", ["arg1", "arg2"]);
const transaction = await alice.call(contract, "functionName");

The result of the read promise is the value of the variable being read or the return value of the function. The result of the call promise is a contract call transaction which, like other transactions, has the success function:

const transaction = await alice.call(contract, "funcName");
try {
	await transaction.success();
} catch (error) {
	// transaction failed
}

In addition to the generic success promise, call transactions can return the contract's event logs for that particular call:

const transaction = await alice.call(contract, "funcName");
try {
	const logs = await transaction.logs();
} catch (error) {
	// transaction failed
}

Obtaining logs assumes waiting for the transaction to be mined, so it's not necessary to await for transaction.success in this case.

A transaction can also be obtained from a contract using the transaction's hash value.

const transaction = await contract.transaction("0x1234abcd...");

The contract is necessary because it provides context (like an ABI definition) to the transaction so that the logs function will work. The transaction function will also verify that the transaction exists and belongs to the same contract.

Getting contract event logs

Given a deployed contract object, it's possible to request its logs for a given range of block numbers:

const contract = bc.contract(abi, address);
const block = await bc.blockNumber();
const logs = await contract.history("ContractEventName", block - 100, block);

Even item is an instance of the ContractEvent object which has the name method that has the event name and also the values method that returns the event's arguments as an object:

for (const event of logs) {
	console.log(`event ${event.name()} with values ${event.values}`);
}

Deploying contracts

A contract image is a combination of abi and bin properties which have the corresponding outputs of a Solidity compiler. It will usually be obtained from files:

const abi = fs.readFileSync("hello-world.abi");
const bin = fs.readFileSync("hello-world.bin");
const image = { abi, bin };

Given an existing contract image, it can be deployed from a user's account:

const transaction = await user.deploy(image, [arg1, arg2]);
const contract = await transaction.contract();

The deploy function accepts a list of arguments which will be passed to the contract's constructor. The returned promise will turn into a DeployedContract instance which can be used as usual.

Building contracts

A contract can be compiled using the Compiler object:

const { Compiler } = require("ethanol");
const com = new Compiler();
const image = await com.compile("hello-world.sol");

The Compiler object will call the host system's solc compiler through the command line.

The constructor takes a map of compiler options which exactly correspond to command line options of the solc compiler. For example, to get binaries compatible with older versions of the blockchain:

const com = new Compiler({ "evm-version": "spuriousDragon" });
const image = await com.compile("hello-world.sol");

About

Humane wrapper around web3

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published