2018-06-13 22:39:39 +03:00
'use strict' ;
2018-06-14 03:02:28 +03:00
import { Interface } from './interface' ;
2018-06-22 09:10:46 +03:00
import { Provider , TransactionRequest , TransactionResponse } from '../providers/provider' ;
2018-06-17 23:32:57 +03:00
import { Signer } from '../wallet/wallet' ;
2018-06-13 22:39:39 +03:00
2018-06-14 04:10:41 +03:00
import { getContractAddress } from '../utils/address' ;
2018-06-17 23:47:28 +03:00
import { isHexString } from '../utils/bytes' ;
2018-06-13 22:39:39 +03:00
import { ParamType } from '../utils/abi-coder' ;
2018-06-14 03:02:28 +03:00
import { BigNumber , ConstantZero } from '../utils/bignumber' ;
2018-06-18 12:42:41 +03:00
import { defineReadOnly , shallowCopy } from '../utils/properties' ;
2018-06-13 22:39:39 +03:00
import * as errors from '../utils/errors' ;
var allowedTransactionKeys = {
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
2018-06-14 03:02:28 +03:00
// @TODO: Expand this to resolve any promises too
function resolveAddresses ( provider , value , paramType ) : Promise < any > {
if ( Array . isArray ( paramType ) ) {
var promises = [ ] ;
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
2018-06-14 03:02:28 +03:00
if ( paramType . components ) {
return resolveAddresses ( provider , value , paramType . components ) ;
}
return Promise . resolve ( value ) ;
}
2018-06-18 12:42:41 +03:00
2018-06-14 03:02:28 +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
2018-06-14 03:02:28 +03:00
return resolveAddresses ( contract . provider , params , method . inputs ) . then ( ( params ) = > {
2018-06-18 12:42:41 +03:00
tx . data = method . encode ( params ) ;
2018-06-14 03:02:28 +03:00
if ( method . type === 'call' ) {
2018-06-13 22:39:39 +03:00
2018-06-14 03:02:28 +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' } )
}
2018-06-14 03:02:28 +03:00
// Check overrides make sense
[ 'gasLimit' , 'gasPrice' , 'value' ] . forEach ( function ( key ) {
2018-06-18 12:42:41 +03:00
if ( tx [ key ] != null ) {
2018-06-14 03:02:28 +03:00
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-14 03:02:28 +03:00
}
2018-06-13 22:39:39 +03:00
2018-06-18 12:42:41 +03:00
return contract . provider . call ( tx ) . then ( ( value ) = > {
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 ,
value : params
} ) ;
}
throw error ;
}
2018-06-13 22:39:39 +03:00
} ) ;
2018-06-14 03:02:28 +03:00
} else if ( method . type === 'transaction' ) {
2018-06-13 22:39:39 +03:00
2018-06-14 03:02:28 +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-14 03:02:28 +03:00
}
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-14 03:02:28 +03:00
}
2018-06-13 22:39:39 +03:00
2018-06-18 12:42:41 +03:00
return contract . provider . estimateGas ( tx ) ;
2018-06-14 03:02:28 +03:00
}
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-14 03:02:28 +03:00
}
2018-06-18 12:42:41 +03:00
return contract . signer . sendTransaction ( tx ) ;
2018-06-13 22:39:39 +03:00
}
2018-06-14 03:02:28 +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 ;
}
2018-06-14 03:02:28 +03:00
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 > ;
2018-06-14 04:10:41 +03:00
// This is only set if the contract was created with a call to deploy
readonly deployTransaction : TransactionResponse ;
2018-06-14 03:02:28 +03:00
// 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. :)
2018-06-14 03:02:28 +03:00
2018-06-14 04:10:41 +03:00
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 ) {
2018-06-14 04:10:41 +03:00
defineReadOnly ( this , 'provider' , signerOrProvider . provider ) ;
defineReadOnly ( this , 'signer' , signerOrProvider ) ;
2018-06-18 12:42:41 +03:00
} else if ( signerOrProvider instanceof Provider ) {
2018-06-14 04:10:41 +03:00
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' , { } ) ;
2018-06-14 04:10:41 +03:00
// 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-14 04:10:41 +03:00
2018-06-13 22:39:39 +03:00
Object . keys ( this . interface . functions ) . forEach ( ( name ) = > {
var run = runMethod ( this , name , false ) ;
if ( this [ name ] == null ) {
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 = this . interface . events [ eventName ] ;
let eventCallback = null ;
2018-06-14 04:10:41 +03:00
let contract = this ;
2018-06-13 22:39:39 +03:00
function handleEvent ( log ) {
2018-06-14 04:10:41 +03:00
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 . parse = eventInfo . parse ;
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
}
2018-06-14 04:10:41 +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 ) ;
}
} ) ;
}
var property = {
enumerable : true ,
get : function ( ) {
return eventCallback ;
} ,
set : function ( value ) {
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 ( this [ propertyName ] == null ) {
Object . defineProperty ( this , propertyName , property ) ;
}
Object . defineProperty ( this . events , eventName , property ) ;
} , this ) ;
}
2018-06-22 09:10:46 +03:00
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 ( tx . to == null ) { return ; }
errors . throwError ( 'cannot override ' + key , errors . UNSUPPORTED_OPERATION , { operation : key } )
} ) ;
tx . to = this . addressPromise ;
return this . signer . sendTransaction ( tx ) ;
}
callFallback ( overrides? : TransactionRequest ) : Promise < string > {
if ( ! this . provider ) {
errors . throwError ( 'call (constant functions) require a provider or a signer with a provider' , errors . UNSUPPORTED_OPERATION , { operation : 'call(fallback)' } )
}
var tx : TransactionRequest = shallowCopy ( overrides || { } ) ;
[ 'to' , 'value' ] . forEach ( function ( key ) {
if ( tx . to == null ) { return ; }
errors . throwError ( 'cannot override ' + key , errors . UNSUPPORTED_OPERATION , { operation : key } )
} ) ;
tx . to = this . addressPromise ;
return this . provider . call ( tx ) ;
}
2018-06-14 04:10:41 +03:00
// 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 ) ;
}
2018-06-14 04:10:41 +03:00
// 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
}
2018-06-22 09:10:46 +03:00
// 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 ;
}
2018-06-17 23:32:57 +03:00
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 } ) ;
}
2018-06-13 22:39:39 +03:00
// @TODO: overrides of args.length = this.interface.deployFunction.inputs.length + 1
return this . signer . sendTransaction ( {
data : this.interface.deployFunction.encode ( bytecode , args )
2018-06-14 04:10:41 +03:00
} ) . then ( ( tx ) = > {
2018-06-18 12:42:41 +03:00
let contract = new Contract ( getContractAddress ( tx ) , this . interface , this . signer || this . provider ) ;
2018-06-14 04:10:41 +03:00
defineReadOnly ( contract , 'deployTransaction' , tx ) ;
return contract ;
2018-06-13 22:39:39 +03:00
} ) ;
}
}