bitcoinjs / bitcoinjs-lib

A javascript Bitcoin library for node.js and browsers.
MIT License
5.68k stars 2.1k forks source link

Example of how to use Wallet and mkSend #73

Closed williamcotton closed 10 years ago

williamcotton commented 10 years ago

Currently there is no documentation nor tests nor examples of how to use a Wallet object to mkSend, how to use processOutput, etc...

weilu commented 10 years ago

It's on my to-do list right after replacing current RNG. Thanks for bringing this up.

williamcotton commented 10 years ago

Quick question, is it even possible to use the Wallet object and to build transactions with mkSend? I've been banging my head against a wall all day trying to get it right. I've resorted to forking the repo and starting to build my own mechanism in the Wallet object to make a transaction.

williamcotton commented 10 years ago

Here's what I'm trying to do:


// from https://blockchain.info/unspent?address=

var unspentOutputs = {

    "unspent_outputs":[

        {
            "tx_hash":"XXXX",
            "tx_index":52832096,
            "tx_output_n": 0,   
            "script":"XXXXX",
            "value": 4200000,
            "value_hex": "401640",
            "confirmations":7
        }

    ]
}

var wallet = new Bitcoin.Wallet(XXXX);
wallet.generateAddress();
var sourceAddress = wallet.addresses[0];
var destinationAddress = "XXX";
var amountForDestination = 21000;

for (var i = 0; i < unspentOutputs.length; i++) {
  var rawUotx = unspentOutputs[i];

  var uotx = {
    output: rawUotx.tx_hash + ":" + i,
    value: rawUotx.value,
    address: sourceAddress
  };

  wallet.processOutput(uotx);
}

var newTx = wallet.mkSend(destinationAddress, amountForDestination, transactionFee);

var buf = newTx.serialize();
var txHex = Bitcoin.convert.bytesToHex(buf);

and then trying https://blockchain.info/pushtx with the txHex...

...which results in: Parse: String index out of range: 463

What am I doing wrong or forgetting here?

weilu commented 10 years ago

@williamcotton what does decoderawtransaction in bitcoind give you?

williamcotton commented 10 years ago

I think the issue is with the tx_hash value returned from https://blockchain.info/unspent?address

It appears to be a different kind of hash than wallet.processOutput() expects.

I'm investigating now...

BTW, I'm upgrading from an older bitcoinjs-lib version, the one currently used by brainwallet.

kyledrake commented 10 years ago

@williamcotton our goal is to streamline the inputs so that they are either byteArray or hex, which should resolve this. This -may- be an endianness issue, something to keep in mind. This is definitely a workflow we want to support and make easy to use.

I wanted to give you a heads up that I'm not a fan of the HASH:INDEX input format at all, and will be proposing to change it soon. But I will make sure to give you a heads up when/if that happens.

The wallet in its current form is kindof a mystery meat object. When I built Coinpunk, I skipped wallet altogether and used Transaction directly. It's very possible that we'll be rewriting the wallet object, I cannot attest for the quality of it in it's current form but @weilu has a stronger opinion about it since she's done some work in there.

williamcotton commented 10 years ago

@kyledrake Thanks for the update!

It turns out it was an endian issue...

var hash = Bitcoin.convert.bytesToHex(Bitcoin.convert.hexToBytes(rawUotx.tx_hash).reverse());

Now that I have that working the Wallet object seems to be in a somewhat functional state.

Maybe if someone could do a brief writeup the Wallet will make more sense, but as it stands right now the API is pretty confusing... processTx? processExistingOutputs? mkSendToOutputs? what is this stuff and why is it exposed as a public API?

All that a wallet needs to do is generate itself from a seed and then hit the Bitcoin network (via bitcoind or blockchain.info) to get unspent outputs and transaction history... from there all it needs is the ability to create, sign, and send a transaction given a destination address, amount, and transaction fee. Wallet's don't really do much more than that. You open them up, take something out and give it to someone, and then close the wallet and put it away.

While I like the change address functionality for some situations there should be a way to bypass those features.

Otherwise, great work! It's nice to see the project getting more organized than it has been in the past!

weilu commented 10 years ago

@williamcotton Thanks! Good points there.

Wallet is still work in progress -- so far I've only done work to get the HD structure into it for address generation. Can you make a pull request with the changes you have that got it to a working state? I can work on improving the interface from there.

kyledrake commented 10 years ago

@williamcotton could you check bitcoind and see if it returns the utxo hash in the same endian format? I want to see if everybody's on the same endian, if so we should just default to the endian structure everybody's using. If bitcoind is different.. then I'm going to scratch my head for a while ;)

williamcotton commented 10 years ago

Using BitcoinJS to create a simple wallet and send a transaction

First we need to generate a wallet and a wallet address:

var seed = Bitcoin.convert.wordArrayToBytes(Bitcoin.Crypto.SHA256("secret"));
var wallet = new Bitcoin.Wallet(seed);
wallet.generateAddress();
var walletAddress = wallet.addresses[0];

Then we need to get the unspent outputs for this wallet address. Blockchain.info has a convenient public API that unfortunately doesn't have CORS enabled so you'll have to come up with another way to access the API from the browser.

https://blockchain.info/unspent?address=walletAddress

Here's an example using node.js with the express and request modules:

app.get('/getUnspent/:walletAddress', function(req, res) {
  request.get({
    url: "https://blockchain.info/unspent?address=" + req.params.walletAddress
  }, function (error, response, body) {
    res.send(body);
  });
});

And the matching client side code:

var getUnspent = function(walletAddress, callback) {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/getUnspent/" + walletAddress, true);
  xhr.onload = function(res) {
    var rawUnspentOutputs = JSON.parse(res.target.response);
    callback(rawUnspentOutputs);
  };
  xhr.send();
};

Then we'll need to build and sign a transaction based on these unspent outputs. You'll notice there is a decent amount of translation required to go from the Blockchain.info API to the BitcoinJS API.

function buildTransaction(destinationAddress, amountForDestinationInBTC, callback) {
  getUnspent(walletAddress, function(rawUnspentOutputs) {
    var unspentOutputs = rawUnspentOutputs.unspent_outputs;
    for (var i = 0; i < unspentOutputs.length; i++) {
      var rawUotx = unspentOutputs[i];
      var hash = Bitcoin.convert.bytesToHex(Bitcoin.convert.hexToBytes(rawUotx.tx_hash).reverse());
      var value = rawUotx.value;
      var uotx = {
        output: hash + ":" + rawUotx.tx_output_n,
        value: value,
        address: sourceAddress
      };
      wallet.processOutput(uotx);
    }
    var amountForDestinationInSatoshis = parseFloat(amountForDestinationInBTC) * 100000000;
    var transactionFeeInSatoshis = TRANSACTION_FEE * 100000000;
    var newTx = wallet.mkSend(destinationAddress, amountForDestinationInSatoshis, transactionFeeInSatoshis);
    var transactionFeeInBTC = transactionFeeInSatoshis/100000000.0;
    var buf = newTx.serialize();
    var txHex = Bitcoin.convert.bytesToHex(buf);
    var txHash = Bitcoin.convert.bytesToHex(newTx.getHash());
    var transaction = { 
      hex: txHex, 
      hash: txHash, 
      destinationAddress: destinationAddress,
      amountForDestination: amountForDestinationInBTC,
      transactionFee: transactionFeeInBTC
    };
    callback(transaction);
  });
}

We will then need to take that transaction, or at least the hex part of it, and push it to the Bitcoin network. Again, Blockchain.info offers a a nice public API but we will have to bounce it off of something in order to get around the lack of CORS.

I'll leave the exact implementation up to the reader, but it construct a POST to https://blockchain.info/pushtx with the form field of tx that has a value of transaction.hex

Proposal for a new API

Attaining the previous unspent outputs is crucial to the creation of any transaction. Some sort of function is needed to retrieve and process these unspent outputs. Using a functional approach we could instruct consumers of BitcoinJS to build something like this on their end:


function getUnspentOutputs(wallet, callback) {
  // get from the network somewhere...
  // ...loop through each unspent output...
  // ...process in to an array, and callback
  callback(error, unspentOutputs);
}

And then pass it in either during object creation:

new Bitcoin.Wallet(seed, getUnspentOutputs); 

Or at some point after:

wallet.setUnspentOutputsFunction(getUnspentOutputs); 

This function will be called and expected to return the latest unspent outputs in a standard format for BitcoinJS. It should probably check the network before attempting to construct any new transactions.

BitcoinJS could provide functions to translate from known providers like Blockchain.info.

Additional functions could provide interfaces for sending transactions, looking up transaction history, current balance, confirmations on transactions, and any number of other important information related to the wallet from the Bitcoin network.

This makes the requisite hooks in to the Bitcoin network obvious to the consumer of the API

Let me know what you think!

weilu commented 10 years ago

It does look cumbersome to create a transaction.

What's the rationale behind wallet.setUnspentOutputsFunction instead of wallet.setUnspentOutputs?

One case where a method like setUnspentOutputs is useful is upon wallet initialization for an existing seed -- we just want to grab a bunch of addresses, hit an unspent api endpoint, call setUnspentOutputs with the return results. From there the users can immediately start sending money.

Once the wallet is setup, I prefer to add/update/remove related unspent outputs on transaction (as opposed to setting all unspent outputs over and over again). There are services(including blockchain.info) that provide websocket APIs for watching addresses, where applications will receive messages on new transactions for any of the watched addresses. It's common for the message to include the transaction or its hash.

It sucks that some APIs use little endian hash (blockchain for example), so to make developers' lives' easier, it'll be good to support hash in both formats.

Based on the above assumptions, here's a draft of what I have in mind for the wallet API:

describe('Unspent Outputs', function(){
  var expectedUtxo, expectedOutputKey;
  beforeEach(function(){
    expectedUtxo = [
        {
          "hash":"6a4062273ac4f9ea4ffca52d9fd102b08f6c32faa0a4d1318e3a7b2e437bb9c7",
          "hashLittleEndian":"c7b97b432e7b3a8e31d1a4a0fa326c8fb002d19f2da5fc4feaf9c43a2762406a",
          "outputIndex": 0,
          "scriptPubKey":"76a91468edf28474ee22f68dfe7e56e76c017c1701b84f88ac",
          "address" : "1azpkpcfczkduetfbqul4mokqai3m3hmxv",
          "value": 20000
        }
      ]
    expectedOutputKey = expectedUtxo[0].hash + ":" + expectedUtxo[0].outputIndex
  })

  describe('getUnspentOutputs', function(){
    it('parses wallet outputs to the expect format', function(){
      wallet.outputs[expectedOutputKey] = {
        output: expectedOutputKey,
        scriptPubKey: expectedUtxo[0].scriptPubKey,
        address: expectedUtxo[0].address,
        value: expectedUtxo[0].value
      }

      assert.deepEqual(wallet.getUnspentOutputs(), expectedUtxo)
    })
  })

  describe('setUnspentOutputs', function(){
    var utxo;
    beforeEach(function(){
      utxo = cloneObject(expectedUtxo)
    })

    it('uses hashLittleEndian when hash is not present', function(){
      delete utxo['hash']

      wallet.setUnspentOutputs(utxo)
      verifyOutputs()
    })

    it('uses hash when hashLittleEndian is not present', function(){
      delete utxo['hashLittleEndian']

      wallet.setUnspentOutputs(utxo)
      verifyOutputs()
    })

    it('uses hash when both hash and hashLittleEndian are present', function(){
      wallet.setUnspentOutputs(utxo)
      verifyOutputs()
    })

    function verifyOutputs() {
      var output = wallet.outputs[expectedOutputKey]
      assert(output)
      assert.equal(output.value, utxo[0].value)
      assert.equal(output.address, utxo[0].address)
      assert.equal(output.scriptPubKey, utxo[0].scriptPubKey)
    }
  })
})

describe('onTx', function(){
  it("updates the wallet's outputs according to the Transaction instance's inputs and outputs", function(){
    var tx = Transaction.deserialize('...')
    wallet.onTx(tx)
  })
})

describe('createTx', function(){
  var to, value

  beforeEach(function(){
    to = '15mMHKL96tWAUtqF3tbVf99Z8arcmnJrr3'
    value = 50000
  })

  it('works', function(){
    wallet.createTx(to, value)
  })

  it('allows fee to be specified', function(){
    var fee = 20000
    wallet.createTx(to, value, fee)
  })
})

And an estimateFee method for Transaction API, which wallet will use:

describe('estimateFee', function(){
  it('works', function(){
    var tx = Transaction.deserialize('...')
    tx.estimateFee()
  })
})
weilu commented 10 years ago

I'm happy to provide both sync and async APIs for potentially slow operations.

vshabelnyk commented 10 years ago

Is it possible to get wallet amount and transactions history?

dcousens commented 10 years ago

wallet amount

wallet.getBalance() Note, this is only based on the UTXO's you've provided it.

transactions history?

You (the developer) will be the one initiating transactions, is it too much to manage these yourself? (Just be careful to not trust transactions until after they have been confirmed). In any case, I guess you could just take the set difference of this.outputs and this.getUnspentOutputs() and review output.spend to see previous txhash's that were spends.