Difference between revisions of "Proper Money Handling (JSON-RPC)"

From Bitcoin Wiki
Jump to: navigation, search
(But if a library only shows 4 or 6 decimal places, bitcoind *can't* figure out what the lost precision was. And while bitcoind might correct for buggy input beyond 8 decimal places, bitcoind is only an *implementation*, not a protocol spec...)
(Go)
 
(29 intermediate revisions by 9 users not shown)
Line 1: Line 1:
 
== Overview ==
 
== Overview ==
The original bitcoin client stores all bitcoin values as 64-bit integers, with 1 BTC stored as 100,000,000 (one-hundred-thousand of the smallest possible bitcoin unit).  Values are expressed as double-precision Numbers in the JSON API, with 1 BTC expressed as 1.00000000
+
The original bitcoin client stores all bitcoin values as 64-bit integers, with 1 BTC stored as 100,000,000 (one-hundred-million of the smallest possible bitcoin unit).  Values are expressed as double-precision Numbers in the JSON API, with 1 BTC expressed as 1.00000000
  
If you are writing software that uses the JSON-RPC interface you need to be aware of possible floating-point conversion issues.  You, or the JSON library you are using, should convert amounts to either a fixed-point Decimal representation (with 8 digits after the decimal point) or a 64-bit integer representation.
+
If you are writing software that uses the JSON-RPC interface you need to be aware of possible floating-point conversion issues.  You, or the JSON library you are using, should convert amounts to either a fixed-point Decimal representation (with 8 digits after the decimal point) or ideally a 64-bit integer representation. In either case, rounding values is required.
  
 
Improper value handling can lead to embarrassing errors; for example, if you truncate instead of doing proper rounding and your software will display the value "0.1 BTC" as "0.09999999 BTC" (or, worse, "0.09 BTC").
 
Improper value handling can lead to embarrassing errors; for example, if you truncate instead of doing proper rounding and your software will display the value "0.1 BTC" as "0.09999999 BTC" (or, worse, "0.09 BTC").
 +
 +
The original bitcoin client does proper, full-precision rounding for all values passed to it via the RPC interface.  So, for example, if the value 0.1 is converted to the value "0.099999999999" by your JSON-RPC library, that value will be rounded to the nearest 0.00000001 bitcoin and will be treated as exactly 0.10 BTC.
  
 
The rest of this page gives sample code for various JSON libraries and programming languages.
 
The rest of this page gives sample code for various JSON libraries and programming languages.
 +
 +
== BASH ==
 +
<source lang="bash">
 +
function JSONtoAmount() {
 +
    printf '%.8f' "$1" | tr -d '.'
 +
}
 +
</source>
 +
 +
== C/C++ ==
 +
C/C++ JSON libraries return the JavaScript Number type as type 'double'.  To convert, without loss of precision, from a double to a 64-bit integer multiply by 100,000,000 and round to the nearest integer:
 +
int64_t JSONtoAmount(double value) {
 +
    return (int64_t)(value * 1e8 + (value < 0.0 ? -.5 : .5));
 +
}
 +
 +
To convert to a JSON value divide by 100,000,000.0, and make sure your JSON implementation outputs doubles with 8 or more digits after the decimal point:
 +
  double forJSON = (double)amount / 1e8;
  
 
== ECMAScript ==
 
== ECMAScript ==
  
The ECMAScript Number type is double-precision floating point; ECMAScript does not have an integral numeric type.
+
function JSONtoAmount(value) {
 +
    return Math.round(1e8 * value);
 +
}
 +
== Java ==
 +
 
 +
public long JSONtoAmount(double value){
 +
    return (long)(value*100000000L);
 +
  }
  
''JavaScript experts:  what is best practice?  Convert to an integral-like-double, or just do proper rounding on display and do all calculations using doubles?''
+
== Perl ==
* This is not ECMAScript specific: all program internals should use raw/base bitcoin amounts, never floats.
+
sub JSONtoAmount {
 +
    return sprintf '%.0f', 1e8 * shift;
 +
}
  
== C/C++ ==
+
== Go ==
C/C++ JSON libraries return the JavaScript Number type as type 'double'. To convert, without loss of precision, from a double to a 64-bit integer multiply by 100,000,000 and round to the nearest integer:
+
The [https://github.com/btcsuite/btcd/btcjson btcjson package] provides a more complete version of this function (error checking and so on), but for illustrative purposes, this is useful.
  double dValue = ...from JSON library...;
+
 
  long long int64Value;
+
func JSONToAmount(jsonAmount float64) (int64) {
  if (dValue > 0) { int64Value = (long long)(dValue * 10e8 + 0.5);
+
  var amount int64
  else { int64Value = (long long)(dValue * 10e8 - 0.5);
+
  tempVal := 1e8 * jsonAmount
 +
  if tempVal < 0 {
 +
      tempVal = tempVal - 0.5
 +
  }
 +
  if tempVal > 0 {
 +
    tempVal = tempVal + 0.5
 +
  }
 +
  // Then just rely on the integer truncating
 +
  amount = int64(tempVal)
 +
  return amount
 +
}
  
To convert to a JSON value divide by 100,000,000.0, and (somehow) ensure your JSON implementation rounds to 8 decimal places:
+
== PHP ==
  double dValue = (double) int64Value / 10e8
+
function JSONtoAmount($value) {
 +
    return round($value * 1e8);
 +
}
  
 
== Python ==
 
== Python ==
Pass the parse_float arguments to [http://docs.python.org/library/json.html python's JSON parsing routines] to parse JSON values into Decimal:
+
def JSONtoAmount(value):
  import decimal
+
    return long(round(value * 1e8))
  import json
+
def AmountToJSON(amount):
 
+
    return float(amount / 1e8)
  # From http://stackoverflow.com/questions/1960516/python-json-serialize-a-decimal-object
 
  class DecimalEncoder(json.JSONEncoder):
 
    def _iterencode(self, o, markers=None):
 
      if isinstance(o, decimal.Decimal):
 
        return (str(o) for o in [o])
 
      return super(DecimalEncoder, self)._iterencode(o, markers)
 
 
 
  decimal.setcontext(decimal.Context(prec=8))
 
  print json.dumps(decimal.Decimal('10.001'), cls=DecimalEncoder)
 
  print json.dumps({ "decimal" : decimal.Decimal('1.1'), "float" : 1.1, "string" : "1.1" }, cls=DecimalEncoder)
 
  print json.loads('{"amount": 0.333331}', parse_float=decimal.Decimal)
 
  
Output is:
+
== Common Lisp ==
   10.001
+
   (defun json-to-amount (n)
  {"decimal": 1.1, "float": 1.1000000000000001, "string": "1.1"}
+
    (coerce (round (* n 1e8)) 'integer))
  {u'blaa': Decimal('0.333331')}
 
  
 +
'''CAUTION''': The CL-JSON library parses numbers as ''single'' precision floating-point by
 +
default. The default parsing behavior can be overridden as follows:
 +
 +
  (set-custom-vars :real (lambda (n)
 +
                            (json::parse-number (concatenate 'string n "d0"))))
 
[[Category:Technical]]
 
[[Category:Technical]]
 
[[Category:Developer]]
 
[[Category:Developer]]
 +
 +
[[de:Korrektes_Handling_von_Geldbeträgen_(JSON-RPC)]]

Latest revision as of 12:08, 30 April 2015

Overview

The original bitcoin client stores all bitcoin values as 64-bit integers, with 1 BTC stored as 100,000,000 (one-hundred-million of the smallest possible bitcoin unit). Values are expressed as double-precision Numbers in the JSON API, with 1 BTC expressed as 1.00000000

If you are writing software that uses the JSON-RPC interface you need to be aware of possible floating-point conversion issues. You, or the JSON library you are using, should convert amounts to either a fixed-point Decimal representation (with 8 digits after the decimal point) or ideally a 64-bit integer representation. In either case, rounding values is required.

Improper value handling can lead to embarrassing errors; for example, if you truncate instead of doing proper rounding and your software will display the value "0.1 BTC" as "0.09999999 BTC" (or, worse, "0.09 BTC").

The original bitcoin client does proper, full-precision rounding for all values passed to it via the RPC interface. So, for example, if the value 0.1 is converted to the value "0.099999999999" by your JSON-RPC library, that value will be rounded to the nearest 0.00000001 bitcoin and will be treated as exactly 0.10 BTC.

The rest of this page gives sample code for various JSON libraries and programming languages.

BASH

 function JSONtoAmount() {
     printf '%.8f' "$1" | tr -d '.'
 }

C/C++

C/C++ JSON libraries return the JavaScript Number type as type 'double'. To convert, without loss of precision, from a double to a 64-bit integer multiply by 100,000,000 and round to the nearest integer:

int64_t JSONtoAmount(double value) {
    return (int64_t)(value * 1e8 + (value < 0.0 ? -.5 : .5));
}

To convert to a JSON value divide by 100,000,000.0, and make sure your JSON implementation outputs doubles with 8 or more digits after the decimal point:

 double forJSON = (double)amount / 1e8;

ECMAScript

function JSONtoAmount(value) {
    return Math.round(1e8 * value);
}

Java

public long JSONtoAmount(double value){
    return (long)(value*100000000L);
 }

Perl

sub JSONtoAmount {
    return sprintf '%.0f', 1e8 * shift;
}

Go

The btcjson package provides a more complete version of this function (error checking and so on), but for illustrative purposes, this is useful.

func JSONToAmount(jsonAmount float64) (int64) {
  var amount int64
  tempVal := 1e8 * jsonAmount
  if tempVal < 0 {
     tempVal = tempVal - 0.5
  }
  if tempVal > 0 {
    tempVal = tempVal + 0.5
  }
  // Then just rely on the integer truncating
  amount = int64(tempVal)
  return amount
}

PHP

function JSONtoAmount($value) {
    return round($value * 1e8);
}

Python

def JSONtoAmount(value):
    return long(round(value * 1e8))
def AmountToJSON(amount):
    return float(amount / 1e8)

Common Lisp

 (defun json-to-amount (n)
   (coerce (round (* n 1e8)) 'integer))

CAUTION: The CL-JSON library parses numbers as single precision floating-point by default. The default parsing behavior can be overridden as follows:

  (set-custom-vars :real (lambda (n)
                            (json::parse-number (concatenate 'string n "d0"))))