Skip to content

Commit

Permalink
Add support for named parameters when using the JSON-RPC interface
Browse files Browse the repository at this point in the history
  • Loading branch information
Pedro Branco authored and joaopaulofonseca committed Nov 27, 2017
1 parent c9eb658 commit d4ac43e
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 21 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,27 @@ client.getInfo().then(([body, headers]) => console.log(body, headers));
const [body, headers] = await client.getInfo();
```

## Named parameters

Since version v0.14.0, it is possible to send commands via the JSON-RPC interface using named parameters instead of positional ones. This comes with the advantage of making the order of arguments irrelevant. It also helps improving the readability of certain function calls when leaving out arguments for their default value.

For instance, take the `getBalance()` call written using positional arguments:

```js
const balance = await new Client().getBalance('*', 0);
```

It is functionally equivalent to using the named arguments `account` and `minconf`, leaving out `include_watchonly` (defaults to `false`):

```js
const balance = await new Client().getBalance({
account: '*',
minconf: 0
});
```

This feature is available to all JSON-RPC methods that accept arguments.

### Floating point number precision in JavaScript

Due to [Javascript's limited floating point precision](http://floating-point-gui.de/), all big numbers (numbers with more than 15 significant digits) are returned as strings to prevent precision loss.
Expand Down
16 changes: 16 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class Client {

this.agentOptions = agentOptions;
this.auth = (password || username) && { pass: password, user: username };
this.hasNamedParametersSupport = false;
this.headers = headers;
this.host = host;
this.password = password;
Expand All @@ -77,6 +78,17 @@ class Client {
let unsupported = [];

if (version) {
// Capture X.Y.Z when X.Y.Z.A is passed to support oddly formatted Bitcoin Core
// versions such as 0.15.0.1.
const result = /[0-9]+\.[0-9]+\.[0-9]+/.exec(version);

if (!result) {
throw new Error(`Invalid Version "${version}"`, { version });
}

[version] = result;

this.hasNamedParametersSupport = semver.satisfies(version, '>=0.14.0');
unsupported = _.chain(methods)
.pickBy(method => !semver.satisfies(version, method.version))
.keys()
Expand Down Expand Up @@ -113,6 +125,10 @@ class Client {
parameters = _.dropRight(parameters);
}

if (this.hasNamedParametersSupport && parameters.length === 1 && _.isPlainObject(parameters[0])) {
parameters = parameters[0];
}

return Promise.try(() => {
if (Array.isArray(input)) {
body = input.map((method, index) => this.requester.prepare({ method: method.method, parameters: method.parameters, suffix: index }));
Expand Down
118 changes: 97 additions & 21 deletions test/index_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ import should from 'should';
* Test `Client`.
*/

before(async () => {
const client = new Client(config.bitcoind);
const [tip] = await client.getChainTips();

if (tip.height === 200) {
return null;
}

await client.generate(200);
});

afterEach(() => {
if (nock.pendingMocks().length) {
throw new Error('Unexpected pending mocks');
Expand Down Expand Up @@ -191,6 +202,20 @@ describe('Client', () => {

balance.should.be.a.Number();
});

it('should support named parameters', async () => {
const client = new Client(_.defaults({ version: '0.15.0' }, config.bitcoind));

// Make sure that the balance on the main wallet is always higher than the one on the test wallet.
await client.generate(51);

const mainWalletBalance = await client.getBalance({ account: '*', minconf: 0 });
const mainWalletBalanceWithoutParameters = await client.getBalance('*', 0);
const testWalletBalance = await client.getBalance({ account: 'test', minconf: 0 });

mainWalletBalance.should.not.equal(testWalletBalance);
mainWalletBalanceWithoutParameters.should.equal(mainWalletBalance);
});
});

describe('getDifficulty()', () => {
Expand Down Expand Up @@ -230,15 +255,60 @@ describe('Client', () => {

describe('listTransactions()', () => {
it('should return the most recent list of transactions from all accounts using specific count', async () => {
const transactions = await new Client(config.bitcoind).listTransactions('test', 15);
const address = await client.getNewAddress('test');

// Generate 5 transactions.
for (let i = 0; i < 5; i++) {
await client.sendToAddress(address, 0.1);
}

transactions.should.be.an.Array().and.empty();
const transactions = await new Client(config.bitcoind).listTransactions('test', 5);

transactions.should.be.an.Array();
transactions.length.should.be.greaterThanOrEqual(5);
});

it('should return the most recent list of transactions from all accounts using default count', async () => {
const transactions = await new Client(config.bitcoind).listTransactions('test');

transactions.should.be.an.Array().and.empty();
transactions.should.be.an.Array();
transactions.should.matchEach(value => {
value.should.have.keys(
'account',
'address',
'amount',
'bip125-replaceable',
'category',
'confirmations',
'label',
'time',
'timereceived',
'trusted',
'txid',
'vout',
'walletconflicts'
);
});
});

it('should support named parameters', async () => {
const address = await client.getNewAddress('test');

// Generate 5 transactions.
for (let i = 0; i < 5; i++) {
await client.sendToAddress(address, 0.1);
}

let transactions = await new Client(_.defaults({ version: '0.15.0' }, config.bitcoind)).listTransactions({ account: 'test' });

transactions.should.be.an.Array();
transactions.length.should.be.greaterThanOrEqual(5);

// Make sure `count` is read correctly.
transactions = await new Client(_.defaults({ version: '0.15.0' }, config.bitcoind)).listTransactions({ account: 'test', count: 1 });

transactions.should.be.an.Array();
transactions.should.have.length(1);
});
});
});
Expand Down Expand Up @@ -320,6 +390,22 @@ describe('Client', () => {
}
});

it('should throw an error if version is invalid', async () => {
try {
await new Client({ version: '0.12' }).getHashesPerSec();

should.fail();
} catch (e) {
e.should.be.an.instanceOf(Error);
e.message.should.equal('Invalid Version "0.12"');
}
});

it('should accept valid versions', async () => {
await new Client(_.defaults({ version: '0.15.0.1' }, config.bitcoind)).getInfo();
await new Client(_.defaults({ version: '0.15.0' }, config.bitcoind)).getInfo();
});

it('should throw an error if version does not support a given method', async () => {
try {
await new Client({ version: '0.12.0' }).getHashesPerSec();
Expand Down Expand Up @@ -376,16 +462,6 @@ describe('Client', () => {
});

describe('rest', () => {
before(async () => {
const [tip] = await client.getChainTips();

if (tip.height === 200) {
return null;
}

await client.generate(200);
});

describe('getTransactionByHash()', () => {
it('should return a transaction json-encoded by default', async () => {
const [{ txid }] = await client.listUnspent();
Expand Down Expand Up @@ -521,22 +597,22 @@ describe('Client', () => {
describe('getMemoryPoolContent()', () => {
it('should return memory pool content json-encoded by default', async () => {
const content = await new Client(config.bitcoind).getMemoryPoolContent();
const transactions = await new Client(config.bitcoind).listTransactions('test');

content.should.eql({});
Object.keys(content).length.should.be.greaterThanOrEqual(transactions.length);
});
});

describe('getMemoryPoolInformation()', () => {
it('should return memory pool information json-encoded by default', async () => {
const information = await new Client(config.bitcoind).getMemoryPoolInformation();

information.should.eql({
bytes: 0,
maxmempool: 300000000,
mempoolminfee: 0,
size: 0,
usage: 0
});
information.should.have.keys('bytes', 'maxmempool', 'mempoolminfee', 'size', 'usage');
information.bytes.should.be.a.Number();
information.maxmempool.should.be.a.Number();
information.mempoolminfee.should.be.a.Number();
information.size.should.be.a.Number();
information.usage.should.be.a.Number();
});
});
});
Expand Down

0 comments on commit d4ac43e

Please sign in to comment.