architecture refactor

This commit is contained in:
Manuel Araoz 2014-04-14 15:31:10 -03:00
commit 59c00da592
17 changed files with 151 additions and 47 deletions

View file

@ -0,0 +1,69 @@
'use strict';
var imports = require('soop').imports();
var bitcore = require('bitcore');
var BIP32 = bitcore.BIP32;
var WalletKey = bitcore.WalletKey;
var networks = bitcore.networks;
var util = bitcore.util;
var PublicKeyRing = require('./PublicKeyRing');
function PrivateKey(opts) {
this.network = opts.networkName === 'testnet' ?
networks.testnet : networks.livenet;
var init = opts.extendedPrivateKeyString || this.network.name;
this.BIP32 = opts.BIP32 || new BIP32(init);
this._calcId();
};
PrivateKey.prototype._calcId = function() {
this.id = util.ripe160(this.BIP32.extendedPublicKey).toString('hex');
};
PrivateKey.prototype.getBIP32 = function(index,isChange) {
if (typeof index === 'undefined') {
return this.BIP32;
}
return this.BIP32.derive( isChange ?
PublicKeyRing.ChangeBranch(index):PublicKeyRing.PublicBranch(index) );
};
PrivateKey.fromObj = function(o) {
return new PrivateKey({
extendedPrivateKeyString: o.extendedPrivateKeyString,
networkName: o.networkName,
});
};
PrivateKey.prototype.toObj = function() {
return {
extendedPrivateKeyString: this.BIP32.extendedPrivateKeyString(),
networkName: this.network.name,
};
};
PrivateKey.prototype.get = function(index,isChange) {
var derivedBIP32 = this.getBIP32(index,isChange);
var wk = new WalletKey({network: this.network});
var p = derivedBIP32.eckey.private.toString('hex');
wk.fromObj({priv: p});
return wk;
};
PrivateKey.prototype.getAll = function(addressIndex, changeAddressIndex) {
var ret = [];
for(var i=0;i<addressIndex; i++) {
ret.push(this.get(i,false));
}
for(var i=0; i<changeAddressIndex; i++) {
ret.push(this.get(i,true));
}
return ret;
};
module.exports = require('soop')(PrivateKey);

View file

@ -0,0 +1,341 @@
'use strict';
var imports = require('soop').imports();
var bitcore = require('bitcore');
var BIP32 = bitcore.BIP32;
var Address = bitcore.Address;
var Script = bitcore.Script;
var coinUtil = bitcore.util;
var Transaction = bitcore.Transaction;
var buffertools = bitcore.buffertools;
var Storage = imports.Storage || require('../storage/Base.js');
var storage = Storage.default();
function PublicKeyRing(opts) {
opts = opts || {};
this.network = opts.networkName === 'livenet' ?
bitcore.networks.livenet : bitcore.networks.testnet;
this.requiredCopayers = opts.requiredCopayers || 3;
this.totalCopayers = opts.totalCopayers || 5;
this.id = opts.id || PublicKeyRing.getRandomId();
this.copayersBIP32 = [];
this.changeAddressIndex=0;
this.addressIndex=0;
}
/*
* This follow Electrum convetion, as described in
* https://bitcointalk.org/index.php?topic=274182.0
*
* We should probably adopt the next standard once it's ready, as discussed in:
* http://sourceforge.net/p/bitcoin/mailman/message/32148600/
*
*/
PublicKeyRing.PublicBranch = function (index) {
return 'm/0/'+index;
};
PublicKeyRing.ChangeBranch = function (index) {
return 'm/1/'+index;
};
PublicKeyRing.getRandomId = function () {
var r = buffertools.toHex(coinUtil.generateNonce());
return r;
};
PublicKeyRing.decrypt = function (passphrase, encPayload) {
console.log('[wallet.js.35] TODO READ: passphrase IGNORED');
return encPayload;
};
PublicKeyRing.encrypt = function (passphrase, payload) {
console.log('[wallet.js.92] TODO: passphrase IGNORED');
return payload;
};
PublicKeyRing.fromObj = function (data) {
if (!data.ts) {
throw new Error('bad data format: Did you use .toObj()?');
}
var config = { networkName: data.networkName || 'livenet' };
var w = new PublicKeyRing(config);
w.id = data.id;
w.requiredCopayers = data.requiredCopayers;
w.totalCopayers = data.totalCopayers;
w.addressIndex = data.addressIndex;
w.changeAddressIndex = data.changeAddressIndex;
w.copayersBIP32 = data.copayersExtPubKeys.map( function (pk) {
return new BIP32(pk);
});
w.ts = data.ts;
return w;
};
PublicKeyRing.read = function (encPayload, id, passphrase) {
if (!encPayload)
throw new Error('Could not find wallet data');
var data;
try {
data = JSON.parse( PublicKeyRing.decrypt( passphrase, encPayload ));
} catch (e) {
throw new Error('error in read: '+ e.toString());
}
if (data.id !== id)
throw new Error('Wrong id in data');
return PublicKeyRing.fromObj(data);
};
PublicKeyRing.prototype.toObj = function() {
return {
id: this.id,
networkName: this.network.name,
requiredCopayers: this.requiredCopayers,
totalCopayers: this.totalCopayers,
changeAddressIndex: this.changeAddressIndex,
addressIndex: this.addressIndex,
copayersExtPubKeys: this.copayersBIP32.map( function (b) {
return b.extendedPublicKeyString();
}),
ts: parseInt(Date.now() / 1000),
};
};
PublicKeyRing.prototype.serialize = function () {
return JSON.stringify(this.toObj());
};
PublicKeyRing.prototype.toStore = function (passphrase) {
if (!this.id)
throw new Error('wallet has no id');
return PublicKeyRing.encrypt(passphrase,this.serialize());
};
PublicKeyRing.prototype.registeredCopayers = function () {
return this.copayersBIP32.length;
};
PublicKeyRing.prototype.isComplete = function () {
return this.registeredCopayers() >= this.totalCopayers;
};
PublicKeyRing.prototype._checkKeys = function() {
if (!this.isComplete())
throw new Error('dont have required keys yet');
};
PublicKeyRing.prototype._newExtendedPublicKey = function () {
return new BIP32(this.network.name)
.extendedPublicKeyString();
};
PublicKeyRing.prototype.addCopayer = function (newEpk) {
if (this.isComplete())
throw new Error('already have all required key:' + this.totalCopayers);
if (!newEpk) {
newEpk = this._newExtendedPublicKey();
}
this.copayersBIP32.forEach(function(b){
if (b.extendedPublicKeyString() === newEpk)
throw new Error('already have that key');
});
this.copayersBIP32.push(new BIP32(newEpk));
return newEpk;
};
PublicKeyRing.prototype.getPubKeys = function (index, isChange) {
this._checkKeys();
var pubKeys = [];
var l = this.copayersBIP32.length;
for(var i=0; i<l; i++) {
var path = isChange ? PublicKeyRing.ChangeBranch(index) : PublicKeyRing.PublicBranch(index);
var bip32 = this.copayersBIP32[i].derive(path);
pubKeys[i] = bip32.eckey.public;
}
return pubKeys;
};
PublicKeyRing.prototype._checkIndexRange = function (index, isChange) {
if ( (isChange && index > this.changeAddressIndex) ||
(!isChange && index > this.addressIndex)) {
console.log('Out of bounds at getAddress: Index %d isChange: %d', index, isChange);
throw new Error('index out of bound');
}
};
PublicKeyRing.prototype.getRedeemScript = function (index, isChange) {
this._checkIndexRange(index, isChange);
var pubKeys = this.getPubKeys(index, isChange);
var script = Script.createMultisig(this.requiredCopayers, pubKeys);
return script;
};
PublicKeyRing.prototype.getAddress = function (index, isChange) {
this._checkIndexRange(index, isChange);
var script = this.getRedeemScript(index,isChange);
var hash = coinUtil.sha256ripe160(script.getBuffer());
var version = this.network.P2SHVersion;
var addr = new Address(version, hash);
return addr;
};
PublicKeyRing.prototype.getScriptPubKeyHex = function (index, isChange) {
this._checkIndexRange(index, isChange);
var addr = this.getAddress(index,isChange);
return Script.createP2SH(addr.payload()).getBuffer().toString('hex');
};
//generate a new address, update index.
PublicKeyRing.prototype.generateAddress = function(isChange) {
var ret =
this.getAddress(isChange ? this.changeAddressIndex : this.addressIndex, isChange);
if (isChange)
this.changeAddressIndex++;
else
this.addressIndex++;
return ret;
};
PublicKeyRing.prototype.getAddresses = function() {
var ret = [];
for (var i=0; i<this.changeAddressIndex; i++) {
ret.push(this.getAddress(i,true));
}
for (var i=0; i<this.addressIndex; i++) {
ret.push(this.getAddress(i,false));
}
return ret;
};
PublicKeyRing.prototype.getRedeemScriptMap = function () {
var ret = {};
for (var i=0; i<this.changeAddressIndex; i++) {
ret[this.getAddress(i,true)] = this.getRedeemScript(i,true).getBuffer().toString('hex');
}
for (var i=0; i<this.addressIndex; i++) {
ret[this.getAddress(i)] = this.getRedeemScript(i).getBuffer().toString('hex');
}
return ret;
};
PublicKeyRing.prototype._checkInPRK = function(inPKR, ignoreId) {
if (!ignoreId && this.id !== inPKR.id) {
throw new Error('inPRK id mismatch');
}
if (this.network.name !== inPKR.network.name)
throw new Error('inPRK network mismatch');
if (
this.requiredCopayers && inPKR.requiredCopayers &&
(this.requiredCopayers !== inPKR.requiredCopayers))
throw new Error('inPRK requiredCopayers mismatch');
if (
this.totalCopayers && inPKR.totalCopayers &&
(this.totalCopayers !== inPKR.totalCopayers))
throw new Error('inPRK requiredCopayers mismatch');
};
PublicKeyRing.prototype._mergeIndexes = function(inPKR) {
var hasChanged = false;
// Indexes
if (inPKR.changeAddressIndex > this.changeAddressIndex) {
this.changeAddressIndex = inPKR.changeAddressIndex;
hasChanged = true;
}
if (inPKR.addressIndex > this.addressIndex) {
this.addressIndex = inPKR.addressIndex;
hasChanged = true;
}
return hasChanged;
};
PublicKeyRing.prototype._mergePubkeys = function(inPKR) {
var self = this;
var hasChanged = false;
var l= self.copayersBIP32.length;
inPKR.copayersBIP32.forEach( function(b) {
var haveIt = false;
var epk = b.extendedPublicKeyString();
for(var j=0; j<l; j++) {
if (self.copayersBIP32[j].extendedPublicKeyString() === epk) {
haveIt=true;
break;
}
}
if (!haveIt) {
if (self.isComplete()) {
console.log('[PublicKeyRing.js.318] REPEATED KEY', epk); //TODO
throw new Error('trying to add more pubkeys, when PKR isComplete at merge');
}
self.copayersBIP32.push(new BIP32(epk));
hasChanged=true;
}
});
return hasChanged;
};
PublicKeyRing.prototype.merge = function(inPKR, ignoreId) {
var hasChanged = false;
this._checkInPRK(inPKR, ignoreId);
if (this._mergeIndexes(inPKR))
hasChanged = true;
if (this._mergePubkeys(inPKR))
hasChanged = true;
return hasChanged;
};
module.exports = require('soop')(PublicKeyRing);

View file

@ -0,0 +1,200 @@
'use strict';
var imports = require('soop').imports();
var bitcore = require('bitcore');
var util = bitcore.util;
var Transaction = bitcore.Transaction;
var Builder = bitcore.TransactionBuilder;
var Script = bitcore.Script;
var buffertools = bitcore.buffertools;
var Storage = imports.Storage || require('../storage/Base');
var storage = Storage.default();
function TxProposal(opts) {
this.seenBy = opts.seenBy || {};
this.signedBy = opts.signedBy || {};
this.builder = opts.builder;
};
module.exports = require('soop')(TxProposal);
function TxProposals(opts) {
opts = opts || {};
this.walletId = opts.walletId;
this.network = opts.networkName === 'livenet' ?
bitcore.networks.livenet : bitcore.networks.testnet;
this.publicKeyRing = opts.publicKeyRing;
this.txps = [];
}
TxProposals.fromObj = function(o) {
var ret = new TxProposals({
networkName: o.networkName,
walletId: o.walletId,
});
o.txps.forEach(function(t) {
ret.txps.push({
seenBy: t.seenBy,
signedBy: t.signedBy,
builder: new Builder.fromObj(t.builderObj),
});
});
return ret;
};
TxProposals.prototype.toObj = function() {
var ret = [];
this.txps.forEach(function(t) {
ret.push({
seenBy: t.seenBy,
signedBy: t.signedBy,
builderObj: t.builder.toObj(),
});
});
return {
txps: ret,
walletId: this.walletId,
networkName: this.network.name,
};
};
TxProposals.prototype._getNormHash = function(txps) {
var ret = {};
txps.forEach(function(txp) {
var hash = txp.builder.build().getNormalizedHash().toString('hex');
ret[hash]=txp;
});
return ret;
};
TxProposals.prototype._startMerge = function(myTxps, theirTxps) {
var fromUs=0, fromTheirs=0, merged =0;
var toMerge = {}, ready=[];
Object.keys(theirTxps).forEach(function(hash) {
if (!myTxps[hash]) {
ready.push(theirTxps[hash]); // only in theirs;
fromTheirs++;
}
else {
toMerge[hash]=theirTxps[hash]; // need Merging
merged++;
}
});
Object.keys(myTxps).forEach(function(hash) {
if(!toMerge[hash]) {
ready.push(myTxps[hash]); // only in myTxps;
fromUs++;
}
});
return {
stats: {
fromUs: fromUs,
fromTheirs: fromTheirs,
merged: merged,
},
ready: ready,
toMerge: toMerge,
};
};
TxProposals.prototype._mergeMetadata = function(myTxps, theirTxps, mergeInfo) {
var toMerge = mergeInfo.toMerge;
Object.keys(toMerge).forEach(function(hash) {
var v0 = myTxps[hash];
var v1 = toMerge[hash];
Object.keys(v1.seenBy).forEach(function(k) {
v0.seenBy[k] = v1.seenBy[k];
});
Object.keys(v1.signedBy).forEach(function(k) {
v0.signedBy[k] = v1.signedBy[k];
});
});
};
TxProposals.prototype._mergeBuilder = function(myTxps, theirTxps, mergeInfo) {
var self = this;
var toMerge = mergeInfo.toMerge;
Object.keys(toMerge).forEach(function(hash) {
var v0 = myTxps[hash].builder;
var v1 = toMerge[hash].builder;
v0.merge(v1);
});
};
TxProposals.prototype.merge = function(t) {
if (this.network.name !== t.network.name)
throw new Error('network mismatch in:', t);
var res = [];
var myTxps = this._getNormHash(this.txps);
var theirTxps = this._getNormHash(t.txps);
var mergeInfo = this._startMerge(myTxps, theirTxps);
this._mergeMetadata(myTxps, theirTxps, mergeInfo);
this._mergeBuilder(myTxps, theirTxps, mergeInfo);
Object.keys(mergeInfo.toMerge).forEach(function(hash) {
mergeInfo.ready.push(myTxps[hash]);
});
this.txps=mergeInfo.ready;
return mergeInfo.stats;
};
TxProposals.prototype.create = function(toAddress, amountSatStr, utxos, priv, opts) {
var pkr = this.publicKeyRing;
opts = opts || {};
var amountSat = bitcore.bignum(amountSatStr);
if (! pkr.isComplete() ) {
throw new Error('publicKeyRing is not complete');
}
var opts = {
remainderOut: opts.remainderOut || { address: pkr.generateAddress(true).toString() }
};
var b = new Builder(opts)
.setUnspent(utxos)
.setHashToScriptMap(pkr.getRedeemScriptMap())
.setOutputs([{address: toAddress, amountSat: amountSat}])
;
var signRet;
if (priv) {
b.sign( priv.getAll(pkr.addressIndex, pkr.changeAddressIndex) );
}
var me = {};
if (priv) me[priv.id] = Date.now();
this.txps.push(
new TxProposal({
signedBy: priv && b.signaturesAdded ? me : {},
seenBy: priv ? me : {},
builder: b,
})
);
return 1;
};
TxProposals.prototype.sign = function(index) {
};
module.exports = require('soop')(TxProposals);

115
js/models/core/Wallet.js Normal file
View file

@ -0,0 +1,115 @@
'use strict';
var imports = require('soop').imports();
var bitcore = require('bitcore');
var http = require('http');
function Wallet(opts) {
opts = opts || {};
this.host = 'localhost';
this.port = '3001';
}
function asyncForEach(array, fn, callback) {
array = array.slice(0);
function processOne() {
var item = array.pop();
fn(item, function(result) {
if(array.length > 0) {
setTimeout(processOne, 0); // schedule immediately
} else {
callback(); // Done!
}
});
}
if(array.length > 0) {
setTimeout(processOne, 0); // schedule immediately
} else {
callback(); // Done!
}
};
Wallet.prototype.getBalance = function(unspent) {
var balance = 0;
for(var i=0;i<unspent.length; i++) {
balance = balance + unspent[i].amount;
}
return balance;
};
Wallet.prototype.listUnspent = function(addresses, cb) {
var self = this;
if (!addresses || !addresses.length) return cb();
var all = [];
asyncForEach(addresses, function(addr, callback) {
var options = {
host: self.host,
port: self.port,
method: 'GET',
path: '/api/addr/' + addr + '/utxo'
};
self.request(options, function(err, res) {
if (res && res.length > 0) {
all = all.concat(res);
}
callback();
});
}, function() {
return cb(all);
});
};
Wallet.prototype.sendRawTransaction = function(rawtx, cb) {
if (!rawtx) return callback();
var options = {
host: this.host,
port: this.port,
method: 'POST',
path: '/api/tx/send',
data: 'rawtx='+rawtx,
headers: { 'content-type' : 'application/x-www-form-urlencoded' }
};
this.request(options, function(err,res) {
if (err) return cb();
return cb(res.txid);
});
};
Wallet.prototype.request = function(options, callback) {
var req = http.request(options, function(response) {
var ret;
if (response.statusCode == 200) {
response.on('data', function(chunk) {
try {
ret = JSON.parse(chunk);
} catch (e) {
callback({message: "Wrong response from insight"});
return;
}
});
response.on('end', function () {
callback(undefined, ret);
return;
});
}
else {
callback({message: 'Error ' + response.statusCode});
return;
}
});
if (options.data) {
req.write(options.data);
}
req.end();
}
module.exports = require('soop')(Wallet);