flex-contract
A modern, flexible Ethereum smart contract abstraction that:
- Requires minimal configuration to get going on all networks (no provider necessary).
- Can sign and send transactions from arbitrary wallets (private keys).
- Can decode internal events (transaction events raised in other contracts).
- Facilitates easy event filtering and monitoring.
- Provides separate promises for transaction hashes, receipts, and confirmations.
- Automatically calculates gas and gas price for transactions in a configurable manner.
- Automatically resolves ENS addresses across all inputs.
- Experimental ABIEncoderV2 support.
Flex-Ether
If you want a simple library for working with more general (ether) transactions,
check out the flex-ether package,
upon which this library is based.
Installation
npm install flex-contract
yarn install flex-contract
Preview
const FlexContract = require('flex-contract');
const ABI = require('./MyContract.ABI.json');
const BYTECODE = require('./MyContract.bytecode.bin');
const DEPLOYED_AT = '0xf6fb5b73987d6d9a139e23bab97be6fc89e0dcd1';
const PRIVATE_KEY = '0xb3734ec890893585330c71ece72afb05058192b6be47bee2b99714e6bb5696ab';
let contract = new FlexContract(ABI);
const tx = contract.new({key: PRIVATE_KEY, bytecode: BYTECODE}).send();
await tx.txId;
await tx.receipt;
await tx.confirmed(3);
await contract.myConstantFn().call();
let receipt = await contract.myTransactionFn('1234').send({ key: PRIVATE_KEY });
let events = receipt.findEvents('MyEvent');
events = await contract.MyEvent().since({ fromBlock: -16 });
const watcher = contract.MyEvent().watch();
watcher.on('data', event => {
});
User Guide
Creating a flex contract
The only requirement for creating an instance is the ABI, which can be a plain
ABI outputted by solc, or a
Truffle artifact produced by the truffle suite.
By default, the instance will create an Infura provider to
talk to the main network. You can modify this behavior with the options
network
, infuraKey
, web3
, eth
, provider
, or providerURI
.
Some options can be overridden in method calls.
contract = new FlexContract(
ABI: object | Array,
address: string,
{
network: string,
infuraKey: string,
ws: boolean,
providerURI: string,
net: object,
provider: object,
web3: object,
eth: FlexEther,
bytecode: string,
gasPriceBonus: string,
gasBonus: string
});
Calling contract functions
The contract instance is automatically populated with the contract functions. Arguments can be passed in positionally or by name through a single dictionary object:
const contract = new FlexContract(ABI, DEPLOYED_ADDRESS);
const call1 = contract.myContractFn(
1337,
'0xebca483a47b9ef4817ecf0b6d326833020a1e21ba067a25bf089e47ba634f87c',
);
const call2 = contract.myContractFn({
a: 1337,
b: `0xebca483a47b9ef4817ecf0b6d326833020a1e21ba067a25bf089e47ba634f87c`
});
Calling the function will return a bound function call object, which allows you to do 3 things:
gas()
: Estimate the gas cost of the function call.call()
: Simulate a call to the function, without modifying the blockchain state. This is the only way to get the return value of a contract function.send()
: Send the call as a transaction, which modifies the blockchain state.
See Encoding/Decoding Rules for information on how function arguments and return values are encoded and decoded.
Estimating gas
Calling gas()
on a bound function call will simulate the call and estimate the gas used.
Example
const gasUsed = await contract.myContractFn(...args).gas();
Options
gas()
accepts a single options object with the following optional fields:
{
from: string,
key: string,
address: string,
value: string,
block: string,
data: string,
}
Making read-only calls
Calling call()
on a bound function call will simulate the function call without modifying the blockchain state. This is the only way to get the return value from a contract function, as transactions resolve to receipts, not return values.
Example
const result = await contract.myContractFn(...args).call();
Options
call()
can accept a single options object with the following optional fields:
{
from: string,
key: string,
address: string,
value: string,
block: string,
gas: number,
data: string,
overrides: object,
}
Working with raw (encoded) results
For some advanced applications you may find yourself handling ABI-encoded, hex result data. Bound functions also have a decodeCallResult()
method which can decode these results into more conventional values. For this use, the parameters passed into the bound function do not matter. You can either re-use an existing instance of the bound function or create a new one with dummy values.
const MY_CONSTANT_FN_HEX_RESULT = '0x...';
const reuslt = contract.myConstantFn(1337, 'foo').decodeCallResult(MY_CONSTANT_FN_HEX_RESULT);
Making transactions
To actually modify the blockchain, you can execute a contract function call as a transaction by calling send()
on a bound function object. This resolves to a receipt object once the transaction is successfully mined.
send()
returns an augmented Promise
object with the following fields:
txId
: A Promise
that resolves once the transaction hash of the call is available.receipt
: A Promise
that resolves to a receipt once the transaction is mined. Same as waiting on the container Promise
object.confirmed(count)
: A Promise
that rsolves to a receipt once the transaction is mind and has been confirmed by count
blocks.
Examples
const receipt = await contract.myContractFn(...args).send();
const receipt = await contract.myContractFn(...args).send().receipt;
const txHash = await contract.myContractFn(...args).send().txId;
const receipt = await contract.myContractFn(...args).send().confirmed(4);
Options
send()
can accept a single options object with the following optional fields:
{
from: string,
key: string,
address: string,
value: string,
gas: number,
gasPrice: string,
gasPriceBonus: string,
gasBonus: string,
data: string,
Deploying a new contract instance
A contract can be deployed via new()
which, like normal function calls, returns a bound function object with gas()
, call()
, and send()
functions.
Example
const FlexContract = require('flex-contract');
const ABI = require('./MyContract.ABI.json');
const BYTECODE = require('./MyContract.bytecode.bin');
const contract = FlexContract(ABI, {bytecode: BYTECODE});
const receipt = await contract.new(arg1, arg2).send();
contract.address;
receipt.address;
Getting encoded call data
Calling encode()
on a bound function call will return the encoded call data.
Example
const encoded = await contract.myContractFn(...args).encode();
Receipt Events
Receipts resolved from transaction calls follow the format of web3
transaction receipts,
augmented with a few extra fields:
events
: array of parsed event objects.findEvent(name, args)
: method to find the first event matching a provided arguments object.findEvents(name, args)
: method to find all events matching a provided arguments object.
The Event object
Event objects follow the format:
{
transactionHash: '0x1234...',
blockNumber: 1234,
logIndex: 1234,
address: '0x1234...',
name: 'MyEventName',
args: {
'0': FIRST_VALUE,
'FIRST_VALUE_NAME': FIRST_VALUE,
'1': SECOND_VALUE,
'SECOND_VALUE_NAME': SECOND_VALUE,
...
}
}
Searching events
const receipt = await contract.someTransactionFn(...args).send();
receipt.events;
receipt.findEvent('MyEvent', {argName0: argValue0, ...});
receipt.findEvents('MyEvent', {argName0: argValue0, ...});
Decoding internal events
Internal events are events that are raised in other contracts during a transaction. The library will attempt to decode these events only if a flex-contract had been previously instantiated to that address, from construction, deployment, or by explicitly setting a contract's address field.
Past Events
Past events can be retrieved by calling a method on the contract instance sharing the same name as the event, then calling since()
on the returned object. Arguments passed into the method will filter results to only those whose arguments match. You may pass null
for arguments that should match any value. Event objects follow the format defined in
receipt objects.
The range of blocks to search for events can be set through the fromBlock
and toBlock
options. Possible values are all mined block numbers. Negative numbers can also be used to specify a backwards offset from the last block, where -1
is the last block, -2
is the second to last block, and so on.
Examples
let events = await contract.MyEvent(null, null).since();
events = await contract.MyEvent(1234, null).since({
fromBlock: -10,
toBlock: -1,
});
events = await contract.MyEvent({
arg1Name: 1234,
arg2Name: null,
});
Options
since()
can take the an options object with the following optional fields:
{
fromBlock: string,
toBlock: string,
address: string,
}
Live Events
Events can be monitored as they happen by calling a method with the same name as the event then calling watch()
on returned object. This will create an EventEmitter object. Filters are defined as in past events,
but you cannot specify a block range, since watches always scan the current block.
Internally, watches are implemented as polled versions of past events and you can configure the poll rate via the pollRate
option. When you no longer need a watcher, you should call its close()
method to avoid memory leaks and network
congestion.
Examples
let watcher = contract.MyEvent(1234, null).watch();
watcher.on('data', function(event) => {
this.close();
});
watcher = contract.MyEvent({arg1Name: 1234, arg2Name: null})
.watch({ pollRate: 15000 });
watcher.close();
Full options
watch()
can take the following options:
{
pollRate: string,
address: string,
args: object
}
Encoding/Decoding rules
There are a few rules to follow when passing values into contract methods and
event filters, and how to expect them.
Integer Types
- Should be passed in as a native
number
type or
converted to base-10 or base-16 string (.e.g, '1234'
or '0x04d2'
). - Decoded as a base-10 string. (.e.g.,
'1234'
).
Bytes and Address Types
- Bytes be passed in as a hex string (e.g.,
'0x1337b33f...'
). - Addresses can be either a hex string or an ENS address (e.g.,
'ethereum.eth'
). - If they are not the correct size, they will be left-padded to fit, which
can have unintended consequences, so you should normalize the input yourself.
- Bytes types are decoded as a lowercase hex string (e.g.,
'0x1337b33f...'
). - Address types are decoded as a checksum address, which is a mixed case hex
string.
Tuples (multiple return values)
- Decoded as an object with keys for both each value's position and name
(if available). For example:
function myConstantFn() pure returns (uint256 a, address b, bytes32 c) {
return (1024,
0x0420DC92A955e3e139b52142f32Bd54C6D46c023,
0x3dffba3b7f99285cc73642eac5ac7110ec7da4b4618d99f3dc9f9954a3dacf27);
}
await contract.myConstantFn();
ENS addresses
Anywhere you can pass an address, you can instead pass an
ENS address, such as
'thisismyensaddress.eth'
. If an ENS address cannot be resolved, an
exception will be raised. For event watchers, it will be emitted
in an 'error'
event.
ENS is only available on the main, ropsten, and rinkeby networks.
The ENS address will also have to be set up with the ENS contract on the
respective network to properly resolve.
The ENS cache
Once an address is resolved, the address will be cached for future calls.
Each address has a TTL, or time-to-live, defined, which specifies how long
the cache should be retained. However, many ENS registrations unintentionally
leave the TTL at the default of 0
, which would imply no caching.
So, by default, cache TTLs are clamped to be at least one hour. You can
configure this behavior yourself by setting the
FlexContract.ens.minTTL
property to the minimum number of milliseconds to
keep a cache entry.
Cloning
You can clone an existing flex-contract instance with the clone()
method.
This method accepts an options object that overrides certain properties of the
original instance.
Full options
cloned = conract.clone(
{
address: string,
bytecode: string,
gasPriceBonus: string,
gasBonus: string,
web3: object,
provider: object,
providerURI: string,
network: string,
infuraKey: string
});
Instance Properties
A contract instance exposes a few properties, most of which you are free to
change. Many of these can also be overridden in individual call options.
address (string)
Address the contract is deployed to (may be ENS).gasBonus (string)
Gas limit estimate bonus for transactions, where 0.01 = +1%
. May be negative.gasPriceBonus (string)
Gas price bonus for transactions, where 0.01 = +1%
. May be negative.bytecode
Bytecode of the contract (if available), used for deployment with new()
.web3 (Web3)
The wrapped Web3 instance used.eth (FlexEther)
The flex-ether instance used.abi
(Read-only) The ABI defining the contract.