527 lines
20 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|