openethereum/js/src/api/contract/contract.spec.js
2016-11-10 14:26:06 +01:00

527 lines
20 KiB
JavaScript

// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js';
import sinon from 'sinon';
import { TEST_HTTP_URL, mockHttp } from '../../../test/mockRpc';
import Abi from '../../abi';
import Api from '../api';
import Contract from './contract';
import { isInstanceOf, isFunction } from '../util/types';
const transport = new Api.Transport.Http(TEST_HTTP_URL);
const eth = new Api(transport);
describe('api/contract/Contract', () => {
const ADDR = '0x0123456789';
const ABI = [
{
type: 'function', name: 'test',
inputs: [{ name: 'boolin', type: 'bool' }, { name: 'stringin', type: 'string' }],
outputs: [{ type: 'uint' }]
},
{
type: 'function', name: 'test2',
outputs: [{ type: 'uint' }, { type: 'uint' }]
},
{ type: 'constructor' },
{ type: 'event', name: 'baz' },
{ type: 'event', name: 'foo' }
];
const VALUES = [true, 'jacogr'];
const ENCODED = '0x023562050000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000066a61636f67720000000000000000000000000000000000000000000000000000';
const RETURN1 = '0000000000000000000000000000000000000000000000000000000000123456';
const RETURN2 = '0000000000000000000000000000000000000000000000000000000000456789';
let scope;
describe('constructor', () => {
it('needs an EthAbi instance', () => {
expect(() => new Contract()).to.throw(/API instance needs to be provided to Contract/);
});
it('needs an ABI', () => {
expect(() => new Contract(eth)).to.throw(/ABI needs to be provided to Contract instance/);
});
describe('internal setup', () => {
const contract = new Contract(eth, ABI);
it('sets EthApi & parsed interface', () => {
expect(contract.address).to.not.be.ok;
expect(contract.api).to.deep.equal(eth);
expect(isInstanceOf(contract.abi, Abi)).to.be.ok;
});
it('attaches functions', () => {
expect(contract.functions.length).to.equal(2);
expect(contract.functions[0].name).to.equal('test');
});
it('attaches constructors', () => {
expect(contract.constructors.length).to.equal(1);
});
it('attaches events', () => {
expect(contract.events.length).to.equal(2);
expect(contract.events[0].name).to.equal('baz');
});
});
});
describe('at', () => {
it('sets returns the functions, events & sets the address', () => {
const contract = new Contract(eth, [
{
constant: true,
inputs: [{
name: '_who',
type: 'address'
}],
name: 'balanceOf',
outputs: [{
name: '',
type: 'uint256'
}],
type: 'function'
},
{
anonymous: false,
inputs: [{
indexed: false,
name: 'amount',
type: 'uint256'
}],
name: 'Drained',
type: 'event'
}
]);
contract.at('6789');
expect(Object.keys(contract.instance)).to.deep.equal(['Drained', 'balanceOf', 'address']);
expect(contract.address).to.equal('6789');
});
});
describe('parseTransactionEvents', () => {
it('parses a transaction log into the data', () => {
const contract = new Contract(eth, [
{
anonymous: false, name: 'Message', type: 'event',
inputs: [
{ indexed: true, name: 'postId', type: 'uint256' },
{ indexed: false, name: 'parentId', type: 'uint256' },
{ indexed: false, name: 'sender', type: 'address' },
{ indexed: false, name: 'at', type: 'uint256' },
{ indexed: false, name: 'messageId', type: 'uint256' },
{ indexed: false, name: 'message', type: 'string' }
]
}
]);
const decoded = contract.parseTransactionEvents({
blockHash: '0xa9280530a3b47bee2fc80f2862fd56502ae075350571d724d6442ea4c597347b',
blockNumber: '0x4fcd',
cumulativeGasUsed: '0xb57f',
gasUsed: '0xb57f',
logs: [{
address: '0x22bff18ec62281850546a664bb63a5c06ac5f76c',
blockHash: '0xa9280530a3b47bee2fc80f2862fd56502ae075350571d724d6442ea4c597347b',
blockNumber: '0x4fcd',
data: '0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000063cf90d3f0410092fc0fca41846f5962239791950000000000000000000000000000000000000000000000000000000056e6c85f0000000000000000000000000000000000000000000000000001000000004fcd00000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000d706f7374286d6573736167652900000000000000000000000000000000000000',
logIndex: '0x0',
topics: [
'0x954ba6c157daf8a26539574ffa64203c044691aa57251af95f4b48d85ec00dd5',
'0x0000000000000000000000000000000000000000000000000001000000004fe0'
],
transactionHash: '0xca16f537d761d13e4e80953b754e2b15541f267d6cad9381f750af1bae1e4917',
transactionIndex: '0x0'
}],
to: '0x22bff18ec62281850546a664bb63a5c06ac5f76c',
transactionHash: '0xca16f537d761d13e4e80953b754e2b15541f267d6cad9381f750af1bae1e4917',
transactionIndex: '0x0'
});
const log = decoded.logs[0];
expect(log.event).to.equal('Message');
expect(log.address).to.equal('0x22bff18ec62281850546a664bb63a5c06ac5f76c');
expect(log.params).to.deep.equal({
at: { type: 'uint', value: new BigNumber('1457965151') },
message: { type: 'string', value: 'post(message)' },
messageId: { type: 'uint', value: new BigNumber('281474976731085') },
parentId: { type: 'uint', value: new BigNumber(0) },
postId: { type: 'uint', value: new BigNumber('281474976731104') },
sender: { type: 'address', value: '0x63Cf90D3f0410092FC0fca41846f596223979195' }
});
});
});
describe('_pollTransactionReceipt', () => {
const contract = new Contract(eth, ABI);
const ADDRESS = '0xD337e80eEdBdf86eDBba021797d7e4e00Bb78351';
const BLOCKNUMBER = '555000';
const RECEIPT = { contractAddress: ADDRESS.toLowerCase(), blockNumber: BLOCKNUMBER };
const EXPECT = { contractAddress: ADDRESS, blockNumber: new BigNumber(BLOCKNUMBER) };
let scope;
let receipt;
describe('success', () => {
before(() => {
scope = mockHttp([
{ method: 'eth_getTransactionReceipt', reply: { result: null } },
{ method: 'eth_getTransactionReceipt', reply: { result: null } },
{ method: 'eth_getTransactionReceipt', reply: { result: RECEIPT } }
]);
return contract
._pollTransactionReceipt('0x123')
.then((_receipt) => {
receipt = _receipt;
});
});
it('sends multiple getTransactionReceipt calls', () => {
expect(scope.isDone()).to.be.true;
});
it('passes the txhash through', () => {
expect(scope.body.eth_getTransactionReceipt.params[0]).to.equal('0x123');
});
it('receives the final receipt', () => {
expect(receipt).to.deep.equal(EXPECT);
});
});
describe('error', () => {
before(() => {
scope = mockHttp([{ method: 'eth_getTransactionReceipt', reply: { error: { code: -1, message: 'failure' } } }]);
});
it('returns the errors', () => {
return contract
._pollTransactionReceipt('0x123')
.catch((error) => {
expect(error.message).to.match(/failure/);
});
});
});
});
describe('deploy', () => {
const contract = new Contract(eth, ABI);
const ADDRESS = '0xD337e80eEdBdf86eDBba021797d7e4e00Bb78351';
const RECEIPT_PEND = { contractAddress: ADDRESS.toLowerCase(), gasUsed: 50, blockNumber: 0 };
const RECEIPT_DONE = { contractAddress: ADDRESS.toLowerCase(), gasUsed: 50, blockNumber: 2500 };
const RECEIPT_EXCP = { contractAddress: ADDRESS.toLowerCase(), gasUsed: 1200, blockNumber: 2500 };
let scope;
describe('success', () => {
before(() => {
scope = mockHttp([
{ method: 'eth_estimateGas', reply: { result: 1000 } },
{ method: 'parity_postTransaction', reply: { result: '0x678' } },
{ method: 'parity_checkRequest', reply: { result: null } },
{ method: 'parity_checkRequest', reply: { result: '0x890' } },
{ method: 'eth_getTransactionReceipt', reply: { result: null } },
{ method: 'eth_getTransactionReceipt', reply: { result: RECEIPT_PEND } },
{ method: 'eth_getTransactionReceipt', reply: { result: RECEIPT_DONE } },
{ method: 'eth_getCode', reply: { result: '0x456' } }
]);
return contract.deploy({ data: '0x123' }, []);
});
it('calls estimateGas, postTransaction, checkRequest, getTransactionReceipt & getCode in order', () => {
expect(scope.isDone()).to.be.true;
});
it('passes the options through to postTransaction (incl. gas calculation)', () => {
expect(scope.body.parity_postTransaction.params).to.deep.equal([
{ data: '0x123', gas: '0x4b0' }
]);
});
it('sets the address of the contract', () => {
expect(contract.address).to.equal(ADDRESS);
});
});
describe('error', () => {
it('fails when gasUsed == gas', () => {
mockHttp([
{ method: 'eth_estimateGas', reply: { result: 1000 } },
{ method: 'parity_postTransaction', reply: { result: '0x678' } },
{ method: 'parity_checkRequest', reply: { result: '0x789' } },
{ method: 'eth_getTransactionReceipt', reply: { result: RECEIPT_EXCP } }
]);
return contract
.deploy({ data: '0x123' }, [])
.catch((error) => {
expect(error.message).to.match(/not deployed, gasUsed/);
});
});
it('fails when no code was deployed', () => {
mockHttp([
{ method: 'eth_estimateGas', reply: { result: 1000 } },
{ method: 'parity_postTransaction', reply: { result: '0x678' } },
{ method: 'parity_checkRequest', reply: { result: '0x789' } },
{ method: 'eth_getTransactionReceipt', reply: { result: RECEIPT_DONE } },
{ method: 'eth_getCode', reply: { result: '0x' } }
]);
return contract
.deploy({ data: '0x123' }, [])
.catch((error) => {
expect(error.message).to.match(/not deployed, getCode/);
});
});
});
});
describe('bindings', () => {
let contract;
let cons;
let func;
beforeEach(() => {
contract = new Contract(eth, ABI);
contract.at(ADDR);
cons = contract.constructors[0];
func = contract.functions.find((fn) => fn.name === 'test');
});
describe('_addOptionsTo', () => {
it('works on no object specified', () => {
expect(contract._addOptionsTo()).to.deep.equal({ to: ADDR });
});
it('uses the contract address when none specified', () => {
expect(contract._addOptionsTo({ from: 'me' })).to.deep.equal({ to: ADDR, from: 'me' });
});
it('overrides the contract address when specified', () => {
expect(contract._addOptionsTo({ to: 'you', from: 'me' })).to.deep.equal({ to: 'you', from: 'me' });
});
});
describe('attachments', () => {
it('attaches .call, .postTransaction & .estimateGas to constructors', () => {
expect(isFunction(cons.call)).to.be.true;
expect(isFunction(cons.postTransaction)).to.be.true;
expect(isFunction(cons.estimateGas)).to.be.true;
});
it('attaches .call, .postTransaction & .estimateGas to functions', () => {
expect(isFunction(func.call)).to.be.true;
expect(isFunction(func.postTransaction)).to.be.true;
expect(isFunction(func.estimateGas)).to.be.true;
});
it('attaches .call only to constant functions', () => {
func = (new Contract(eth, [{ type: 'function', name: 'test', constant: true }])).functions[0];
expect(isFunction(func.call)).to.be.true;
expect(isFunction(func.postTransaction)).to.be.false;
expect(isFunction(func.estimateGas)).to.be.false;
});
});
describe('postTransaction', () => {
beforeEach(() => {
scope = mockHttp([{ method: 'parity_postTransaction', reply: { result: ['hashId'] } }]);
});
it('encodes options and mades an parity_postTransaction call', () => {
return func
.postTransaction({ someExtras: 'foo' }, VALUES)
.then(() => {
expect(scope.isDone()).to.be.true;
expect(scope.body.parity_postTransaction.params[0]).to.deep.equal({
someExtras: 'foo',
to: ADDR,
data: ENCODED
});
});
});
});
describe('estimateGas', () => {
beforeEach(() => {
scope = mockHttp([{ method: 'eth_estimateGas', reply: { result: ['0x123'] } }]);
});
it('encodes options and mades an eth_estimateGas call', () => {
return func
.estimateGas({ someExtras: 'foo' }, VALUES)
.then((amount) => {
expect(scope.isDone()).to.be.true;
expect(amount.toString(16)).to.equal('123');
expect(scope.body.eth_estimateGas.params).to.deep.equal([{
someExtras: 'foo',
to: ADDR,
data: ENCODED
}]);
});
});
});
describe('call', () => {
it('encodes options and mades an eth_call call', () => {
scope = mockHttp([{ method: 'eth_call', reply: { result: RETURN1 } }]);
return func
.call({ someExtras: 'foo' }, VALUES)
.then((result) => {
expect(scope.isDone()).to.be.true;
expect(scope.body.eth_call.params).to.deep.equal([{
someExtras: 'foo',
to: ADDR,
data: ENCODED
}, 'latest']);
expect(result.toString(16)).to.equal('123456');
});
});
it('encodes options and mades an eth_call call (multiple returns)', () => {
scope = mockHttp([{ method: 'eth_call', reply: { result: `${RETURN1}${RETURN2}` } }]);
return contract.functions[1]
.call({}, [])
.then((result) => {
expect(scope.isDone()).to.be.true;
expect(result.length).to.equal(2);
expect(result[0].toString(16)).to.equal('123456');
expect(result[1].toString(16)).to.equal('456789');
});
});
});
});
describe('subscribe', () => {
const abi = [
{
anonymous: false, name: 'Message', type: 'event',
inputs: [
{ indexed: true, name: 'postId', type: 'uint256' },
{ indexed: false, name: 'parentId', type: 'uint256' },
{ indexed: false, name: 'sender', type: 'address' },
{ indexed: false, name: 'at', type: 'uint256' },
{ indexed: false, name: 'messageId', type: 'uint256' },
{ indexed: false, name: 'message', type: 'string' }
]
}
];
const logs = [{
address: '0x22bff18ec62281850546a664bb63a5c06ac5f76c',
blockHash: '0xa9280530a3b47bee2fc80f2862fd56502ae075350571d724d6442ea4c597347b',
blockNumber: '0x4fcd',
data: '0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000063cf90d3f0410092fc0fca41846f5962239791950000000000000000000000000000000000000000000000000000000056e6c85f0000000000000000000000000000000000000000000000000001000000004fcd00000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000d706f7374286d6573736167652900000000000000000000000000000000000000',
logIndex: '0x0',
topics: [
'0x954ba6c157daf8a26539574ffa64203c044691aa57251af95f4b48d85ec00dd5',
'0x0000000000000000000000000000000000000000000000000001000000004fe0'
],
transactionHash: '0xca16f537d761d13e4e80953b754e2b15541f267d6cad9381f750af1bae1e4917',
transactionIndex: '0x0'
}];
const parsed = [{
address: '0x22bfF18ec62281850546a664bb63a5C06AC5F76C',
blockHash: '0xa9280530a3b47bee2fc80f2862fd56502ae075350571d724d6442ea4c597347b',
blockNumber: new BigNumber(20429),
data: '0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000063cf90d3f0410092fc0fca41846f5962239791950000000000000000000000000000000000000000000000000000000056e6c85f0000000000000000000000000000000000000000000000000001000000004fcd00000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000d706f7374286d6573736167652900000000000000000000000000000000000000',
event: 'Message',
logIndex: new BigNumber(0),
params: {
at: { type: 'uint', value: new BigNumber(1457965151) },
message: { type: 'string', value: 'post(message)' },
messageId: { type: 'uint', value: new BigNumber(281474976731085) },
parentId: { type: 'uint', value: new BigNumber(0) },
postId: { type: 'uint', value: new BigNumber(281474976731104) },
sender: { type: 'address', value: '0x63Cf90D3f0410092FC0fca41846f596223979195' }
},
topics: [
'0x954ba6c157daf8a26539574ffa64203c044691aa57251af95f4b48d85ec00dd5', '0x0000000000000000000000000000000000000000000000000001000000004fe0'
],
transactionHash: '0xca16f537d761d13e4e80953b754e2b15541f267d6cad9381f750af1bae1e4917',
transactionIndex: new BigNumber(0)
}];
let contract;
beforeEach(() => {
contract = new Contract(eth, abi);
contract.at(ADDR);
});
describe('invalid events', () => {
it('fails to subscribe to an invalid names', () => {
return contract
.subscribe('invalid')
.catch((error) => {
expect(error.message).to.match(/invalid is not a valid eventName/);
});
});
});
describe('valid events', () => {
let cbb;
let cbe;
beforeEach(() => {
scope = mockHttp([
{ method: 'eth_newFilter', reply: { result: '0x123' } },
{ method: 'eth_getFilterLogs', reply: { result: logs } },
{ method: 'eth_newFilter', reply: { result: '0x123' } },
{ method: 'eth_getFilterLogs', reply: { result: logs } }
]);
cbb = sinon.stub();
cbe = sinon.stub();
return contract.subscribe('Message', {}, cbb);
});
it('sets the subscriptionId returned', () => {
return contract
.subscribe('Message', {}, cbe)
.then((subscriptionId) => {
expect(subscriptionId).to.equal(1);
});
});
it('creates a new filter and retrieves the logs on it', () => {
return contract
.subscribe('Message', {}, cbe)
.then((subscriptionId) => {
expect(scope.isDone()).to.be.true;
});
});
it('returns the logs to the callback', () => {
return contract
.subscribe('Message', {}, cbe)
.then((subscriptionId) => {
expect(cbe).to.have.been.calledWith(null, parsed);
});
});
});
});
});