ethers.js/src.ts/contracts/contract.ts

432 lines
17 KiB
TypeScript
Raw Normal View History

2018-06-13 22:39:39 +03:00
'use strict';
import { EventDescription, Interface } from './interface';
import { Provider, TransactionRequest, TransactionResponse } from '../providers/provider';
import { Signer } from '../wallet/wallet';
2018-06-13 22:39:39 +03:00
import { defaultAbiCoder } from '../utils/abi-coder';
import { getContractAddress } from '../utils/address';
import { hexDataLength, hexDataSlice, isHexString } from '../utils/bytes';
2018-06-13 22:39:39 +03:00
import { ParamType } from '../utils/abi-coder';
import { BigNumber, ConstantZero } from '../utils/bignumber';
2018-06-18 12:42:41 +03:00
import { defineReadOnly, shallowCopy } from '../utils/properties';
import { poll } from '../utils/web';
2018-06-13 22:39:39 +03:00
import * as errors from '../utils/errors';
var allowedTransactionKeys: { [ key: string ]: boolean } = {
2018-06-13 22:39:39 +03:00
data: true, from: true, gasLimit: true, gasPrice:true, nonce: true, to: true, value: true
}
2018-06-18 12:42:41 +03:00
// Recursively replaces ENS names with promises to resolve the name and
// stalls until all promises have returned
// @TODO: Expand this to resolve any promises too
function resolveAddresses(provider: Provider, value: any, paramType: ParamType | Array<ParamType>): Promise<any> {
if (Array.isArray(paramType)) {
var promises: Array<Promise<string>> = [];
paramType.forEach((paramType, index) => {
var v = null;
if (Array.isArray(value)) {
v = value[index];
} else {
v = value[paramType.name];
}
promises.push(resolveAddresses(provider, v, paramType));
});
return Promise.all(promises);
}
if (paramType.type === 'address') {
return provider.resolveName(value);
}
2018-06-13 22:39:39 +03:00
if (paramType.components) {
return resolveAddresses(provider, value, paramType.components);
}
return Promise.resolve(value);
}
2018-06-18 12:42:41 +03:00
type RunFunction = (...params: Array<any>) => Promise<any>;
function runMethod(contract: Contract, functionName: string, estimateOnly: boolean): RunFunction {
2018-06-13 22:39:39 +03:00
let method = contract.interface.functions[functionName];
return function(...params): Promise<any> {
2018-06-18 12:42:41 +03:00
var tx: any = {}
2018-06-13 22:39:39 +03:00
// If 1 extra parameter was passed in, it contains overrides
if (params.length === method.inputs.length + 1 && typeof(params[params.length - 1]) === 'object') {
2018-06-18 12:42:41 +03:00
tx = shallowCopy(params.pop());
2018-06-13 22:39:39 +03:00
// Check for unexpected keys (e.g. using "gas" instead of "gasLimit")
2018-06-18 12:42:41 +03:00
for (var key in tx) {
2018-06-13 22:39:39 +03:00
if (!allowedTransactionKeys[key]) {
throw new Error('unknown transaction override ' + key);
}
}
}
if (params.length != method.inputs.length) {
throw new Error('incorrect number of arguments');
}
// Check overrides make sense
['data', 'to'].forEach(function(key) {
2018-06-18 12:42:41 +03:00
if (tx[key] != null) {
errors.throwError('cannot override ' + key, errors.UNSUPPORTED_OPERATION, { operation: key })
2018-06-13 22:39:39 +03:00
}
});
// Send to the contract address
2018-06-18 12:42:41 +03:00
tx.to = contract.addressPromise;
2018-06-13 22:39:39 +03:00
return resolveAddresses(contract.provider, params, method.inputs).then((params) => {
2018-06-18 12:42:41 +03:00
tx.data = method.encode(params);
if (method.type === 'call') {
2018-06-13 22:39:39 +03:00
// Call (constant functions) always cost 0 ether
if (estimateOnly) {
return Promise.resolve(ConstantZero);
2018-06-13 22:39:39 +03:00
}
2018-06-18 12:42:41 +03:00
if (!contract.provider) {
errors.throwError('call (constant functions) require a provider or a signer with a provider', errors.UNSUPPORTED_OPERATION, { operation: 'call' })
}
// Check overrides make sense
['gasLimit', 'gasPrice', 'value'].forEach(function(key) {
2018-06-18 12:42:41 +03:00
if (tx[key] != null) {
throw new Error('call cannot override ' + key) ;
}
});
2018-06-13 22:39:39 +03:00
2018-06-18 12:42:41 +03:00
if (tx.from == null && contract.signer) {
tx.from = contract.signer.getAddress()
}
2018-06-13 22:39:39 +03:00
2018-06-18 12:42:41 +03:00
return contract.provider.call(tx).then((value) => {
if ((hexDataLength(value) % 32) === 4 && hexDataSlice(value, 0, 4) === '0x08c379a0') {
let reason = defaultAbiCoder.decode([ 'string' ], hexDataSlice(value, 4));
errors.throwError('call revert exception', errors.CALL_EXCEPTION, {
address: contract.address,
args: params,
method: method.signature,
errorSignature: 'Error(string)',
errorArgs: [ reason ],
reason: reason,
transaction: tx
});
}
2018-06-18 12:42:41 +03:00
try {
let result = method.decode(value);
if (method.outputs.length === 1) {
result = result[0];
2018-06-13 22:39:39 +03:00
}
2018-06-18 12:42:41 +03:00
return result;
} catch (error) {
if (value === '0x' && method.outputs.length > 0) {
errors.throwError('call exception', errors.CALL_EXCEPTION, {
address: contract.address,
method: method.signature,
args: params
2018-06-18 12:42:41 +03:00
});
}
throw error;
}
2018-06-13 22:39:39 +03:00
});
} else if (method.type === 'transaction') {
2018-06-13 22:39:39 +03:00
// Only computing the transaction estimate
if (estimateOnly) {
2018-06-18 12:42:41 +03:00
if (!contract.provider) {
errors.throwError('estimate gas require a provider or a signer with a provider', errors.UNSUPPORTED_OPERATION, { operation: 'estimateGas' })
}
2018-06-13 22:39:39 +03:00
2018-06-18 12:42:41 +03:00
if (tx.from == null && contract.signer) {
tx.from = contract.signer.getAddress()
}
2018-06-13 22:39:39 +03:00
2018-06-18 12:42:41 +03:00
return contract.provider.estimateGas(tx);
}
2018-06-13 22:39:39 +03:00
2018-06-18 12:42:41 +03:00
if (!contract.signer) {
errors.throwError('sending a transaction require a signer', errors.UNSUPPORTED_OPERATION, { operation: 'sendTransaction' })
2018-06-13 22:39:39 +03:00
}
2018-06-18 12:42:41 +03:00
// Make sure they aren't overriding something they shouldn't
if (tx.from != null) {
errors.throwError('cannot override from in a transaction', errors.UNSUPPORTED_OPERATION, { operation: 'sendTransaction' })
}
2018-06-18 12:42:41 +03:00
return contract.signer.sendTransaction(tx);
2018-06-13 22:39:39 +03:00
}
throw new Error('invalid type - ' + method.type);
return null;
});
2018-06-13 22:39:39 +03:00
}
}
export type ContractEstimate = (...params: Array<any>) => Promise<BigNumber>;
export type ContractFunction = (...params: Array<any>) => Promise<any>;
export type ContractEvent = (...params: Array<any>) => void;
interface Bucket<T> {
[name: string]: T;
}
export type ErrorCallback = (error: Error) => void;
export type Contractish = Array<string | ParamType> | Interface | string;
2018-06-13 22:39:39 +03:00
export class Contract {
readonly address: string;
readonly interface: Interface;
2018-06-18 12:42:41 +03:00
2018-06-13 22:39:39 +03:00
readonly signer: Signer;
readonly provider: Provider;
readonly estimate: Bucket<ContractEstimate>;
readonly functions: Bucket<ContractFunction>;
readonly events: Bucket<ContractEvent>;
readonly addressPromise: Promise<string>;
// This is only set if the contract was created with a call to deploy
readonly deployTransaction: TransactionResponse;
private _onerror: ErrorCallback;
// https://github.com/Microsoft/TypeScript/issues/5453
2018-06-18 12:42:41 +03:00
// Once this issue is resolved (there are open PR) we can do this nicer
// by making addressOrName default to null for 2 operand calls. :)
constructor(addressOrName: string, contractInterface: Contractish, signerOrProvider: Signer | Provider) {
errors.checkNew(this, Contract);
2018-06-13 22:39:39 +03:00
// @TODO: Maybe still check the addressOrName looks like a valid address or name?
//address = getAddress(address);
if (contractInterface instanceof Interface) {
defineReadOnly(this, 'interface', contractInterface);
} else {
defineReadOnly(this, 'interface', new Interface(contractInterface));
}
2018-06-18 12:42:41 +03:00
if (signerOrProvider instanceof Signer) {
defineReadOnly(this, 'provider', signerOrProvider.provider);
defineReadOnly(this, 'signer', signerOrProvider);
2018-06-18 12:42:41 +03:00
} else if (signerOrProvider instanceof Provider) {
defineReadOnly(this, 'provider', signerOrProvider);
defineReadOnly(this, 'signer', null);
2018-06-18 12:42:41 +03:00
} else {
errors.throwError('invalid signer or provider', errors.INVALID_ARGUMENT, { arg: 'signerOrProvider', value: signerOrProvider });
2018-06-13 22:39:39 +03:00
}
defineReadOnly(this, 'estimate', { });
defineReadOnly(this, 'events', { });
defineReadOnly(this, 'functions', { });
// Not connected to an on-chain instance, so do not connect functions and events
if (!addressOrName) {
defineReadOnly(this, 'address', null);
defineReadOnly(this, 'addressPromise', Promise.resolve(null));
return;
}
2018-06-18 12:42:41 +03:00
defineReadOnly(this, 'address', addressOrName);
defineReadOnly(this, 'addressPromise', this.provider.resolveName(addressOrName));
2018-06-13 22:39:39 +03:00
Object.keys(this.interface.functions).forEach((name) => {
var run = runMethod(this, name, false);
if ((<any>this)[name] == null) {
2018-06-13 22:39:39 +03:00
defineReadOnly(this, name, run);
} else {
console.log('WARNING: Multiple definitions for ' + name);
}
if (this.functions[name] == null) {
defineReadOnly(this.functions, name, run);
defineReadOnly(this.estimate, name, runMethod(this, name, true));
}
});
Object.keys(this.interface.events).forEach((eventName) => {
let eventInfo: EventDescription = this.interface.events[eventName];
2018-06-13 22:39:39 +03:00
type Callback = (...args: Array<any>) => void;
let eventCallback: Callback = null;
2018-06-13 22:39:39 +03:00
let contract = this;
function handleEvent(log: any): void {
contract.addressPromise.then((address) => {
2018-06-13 22:39:39 +03:00
// Not meant for us (the topics just has the same name)
if (address != log.address) { return; }
try {
let result = eventInfo.decode(log.data, log.topics);
// Some useful things to have with the log
log.args = result;
log.event = eventName;
log.decode = eventInfo.decode;
2018-06-13 22:39:39 +03:00
log.removeListener = function() {
2018-06-18 12:42:41 +03:00
contract.provider.removeListener([ eventInfo.topic ], handleEvent);
2018-06-13 22:39:39 +03:00
}
log.getBlock = function() { return contract.provider.getBlock(log.blockHash);; }
log.getTransaction = function() { return contract.provider.getTransaction(log.transactionHash); }
log.getTransactionReceipt = function() { return contract.provider.getTransactionReceipt(log.transactionHash); }
2018-06-13 22:39:39 +03:00
log.eventSignature = eventInfo.signature;
eventCallback.apply(log, Array.prototype.slice.call(result));
} catch (error) {
console.log(error);
let onerror = contract._onerror;
if (onerror) { setTimeout(() => { onerror(error); }); }
2018-06-13 22:39:39 +03:00
}
});
}
var property = {
enumerable: true,
get: function() {
return eventCallback;
},
set: function(value: Callback) {
2018-06-13 22:39:39 +03:00
if (!value) { value = null; }
2018-06-18 12:42:41 +03:00
if (!contract.provider) {
errors.throwError('events require a provider or a signer with a provider', errors.UNSUPPORTED_OPERATION, { operation: 'events' })
}
2018-06-13 22:39:39 +03:00
if (!value && eventCallback) {
2018-06-18 12:42:41 +03:00
contract.provider.removeListener([ eventInfo.topic ], handleEvent);
2018-06-13 22:39:39 +03:00
} else if (value && !eventCallback) {
2018-06-18 12:42:41 +03:00
contract.provider.on([ eventInfo.topic ], handleEvent);
2018-06-13 22:39:39 +03:00
}
eventCallback = value;
}
};
var propertyName = 'on' + eventName.toLowerCase();
if ((<any>this)[propertyName] == null) {
2018-06-13 22:39:39 +03:00
Object.defineProperty(this, propertyName, property);
}
Object.defineProperty(this.events, eventName, property);
}, this);
}
get onerror() { return this._onerror; }
set onerror(callback: ErrorCallback) {
this._onerror = callback;
}
// @TODO: Allow timeout?
deployed() {
// If we were just deployed, we know the transaction we should occur in
if (this.deployTransaction) {
return this.deployTransaction.wait().then(() => {
return this;
});
}
// Otherwise, poll for our code to be deployed
return poll(() => {
return this.provider.getCode(this.address).then((code) => {
if (code === '0x') { return undefined; }
return this;
});
});
}
// @TODO:
// estimateFallback(overrides?: TransactionRequest): Promise<BigNumber>
// @TODO:
// estimateDeploy(bytecode: string, ...args): Promise<BigNumber>
fallback(overrides?: TransactionRequest): Promise<TransactionResponse> {
if (!this.signer) {
errors.throwError('sending a transaction require a signer', errors.UNSUPPORTED_OPERATION, { operation: 'sendTransaction(fallback)' })
}
var tx: TransactionRequest = shallowCopy(overrides || {});
['from', 'to'].forEach(function(key) {
if ((<any>tx)[key] == null) { return; }
errors.throwError('cannot override ' + key, errors.UNSUPPORTED_OPERATION, { operation: key })
});
tx.to = this.addressPromise;
return this.signer.sendTransaction(tx);
}
// Reconnect to a different signer or provider
connect(signerOrProvider: Signer | Provider): Contract {
2018-06-13 22:39:39 +03:00
return new Contract(this.address, this.interface, signerOrProvider);
}
// Re-attach to a different on=chain instance of this contract
attach(addressOrName: string): Contract {
return new Contract(addressOrName, this.interface, this.signer || this.provider);
}
// Deploy the contract with the bytecode, resolving to the deployed address.
// Use contract.deployTransaction.wait() to wait until the contract has
// been mined.
deploy(bytecode: string, ...args: Array<any>): Promise<Contract> {
2018-06-13 22:39:39 +03:00
if (this.signer == null) {
throw new Error('missing signer'); // @TODO: errors.throwError
}
// A lot of common tools do not prefix bytecode with a 0x
if (typeof(bytecode) === 'string' && bytecode.match(/^[0-9a-f]*$/i) && (bytecode.length % 2) == 0) {
bytecode = '0x' + bytecode;
}
if (!isHexString(bytecode)) {
errors.throwError('bytecode must be a valid hex string', errors.INVALID_ARGUMENT, { arg: 'bytecode', value: bytecode });
}
if ((bytecode.length % 2) !== 0) {
errors.throwError('bytecode must be valid data (even length)', errors.INVALID_ARGUMENT, { arg: 'bytecode', value: bytecode });
}
let tx: TransactionRequest = { };
if (args.length === this.interface.deployFunction.inputs.length + 1) {
tx = shallowCopy(args.pop());
for (var key in tx) {
if (!allowedTransactionKeys[key]) {
throw new Error('unknown transaction override ' + key);
}
}
}
['data', 'from', 'to'].forEach(function(key) {
if ((<any>tx)[key] == null) { return; }
errors.throwError('cannot override ' + key, errors.UNSUPPORTED_OPERATION, { operation: key })
});
tx.data = this.interface.deployFunction.encode(bytecode, args);
errors.checkArgumentCount(args.length, this.interface.deployFunction.inputs.length, 'in Contract constructor');
2018-06-13 22:39:39 +03:00
// @TODO: overrides of args.length = this.interface.deployFunction.inputs.length + 1
return this.signer.sendTransaction(tx).then((tx) => {
2018-06-18 12:42:41 +03:00
let contract = new Contract(getContractAddress(tx), this.interface, this.signer || this.provider);
defineReadOnly(contract, 'deployTransaction', tx);
return contract;
2018-06-13 22:39:39 +03:00
});
}
}