From 23ca56fe05bebdfa2002070d36b90f371f0bccf1 Mon Sep 17 00:00:00 2001 From: moneroexamples Date: Mon, 13 Nov 2017 14:11:13 +0800 Subject: [PATCH] initial support to send to a subaddress added based on https://github.com/monero-project/monero/pull/2056#issuecomment-343823399 --- README.md | 9 ++--- html/js/cn_util.js | 61 ++++++++++++++++++++++++++++--- html/js/config.js | 2 + html/js/controllers/send_coins.js | 6 ++- 4 files changed, 65 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index d327f073b..a4dec2e26 100755 --- a/README.md +++ b/README.md @@ -32,15 +32,12 @@ to MyMonero. They include: - legacy address + payment_id system replaced with integrated addresses for better privacy. - free, time based imports of recent transactions added. - new transaction details window. + - sending xmr to a subaddress (not receiving nor generating subaddresses for now). ## Testnet version - [http://139.162.32.245:81](http://139.162.32.245:81) -or alternatively, owning to [Gingeropolous](https://github.com/Gingeropolous): - -- [http://testwallet.moneroworld.com](http://testwallet.moneroworld.com) - This is Open Monero running on testnet network. You can use it to play around with it. Since this is testnet version, frequent changes and database resets are expected. Also, it is running on cheap vps, which may result in some lag. @@ -91,7 +88,7 @@ For other Linux operating systems, the instructions are analogical. #### Monero download and compilation -Download and compile recent Monero realease into your home folder: +Download and compile recent Monero release into your home folder: ```bash # first install monero dependecines @@ -109,7 +106,7 @@ git clone https://github.com/monero-project/monero cd monero/ # checkout last monero version -git checkout -b last_release v0.11.0.0 +git checkout -b last_release v0.11.1.0 make ``` diff --git a/html/js/cn_util.js b/html/js/cn_util.js index 32ef01b9a..4b1987741 100755 --- a/html/js/cn_util.js +++ b/html/js/cn_util.js @@ -47,11 +47,15 @@ var cnUtil = (function(initConfig) { var ENCRYPTED_PAYMENT_ID_TAIL = 141; var CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX = config.addressPrefix; var CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX = config.integratedAddressPrefix; + var CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX = config.subAddressPrefix; + if (config.testnet === true) { CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX = config.addressPrefixTestnet; CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX = config.integratedAddressPrefixTestnet; + CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX = config.subAddressPrefixTestnet; } + var UINT64_MAX = new JSBigInt(2).pow(64); var CURRENT_TX_VERSION = 2; var OLD_TX_VERSION = 1; @@ -507,13 +511,21 @@ var cnUtil = (function(initConfig) { var prefix = this.encode_varint(CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX); return cnBase58.encode(prefix + spend.pub).slice(0, 44); }; + + this.is_subaddress = function(address) { + var dec = cnBase58.decode(address); + var expectedPrefixSub = this.encode_varint(CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX); + var prefix = dec.slice(0, expectedPrefixSub.length); + return (prefix === expectedPrefixSub); + } this.decode_address = function(address) { var dec = cnBase58.decode(address); var expectedPrefix = this.encode_varint(CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX); var expectedPrefixInt = this.encode_varint(CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX); + var expectedPrefixSub = this.encode_varint(CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX); var prefix = dec.slice(0, expectedPrefix.length); - if (prefix !== expectedPrefix && prefix !== expectedPrefixInt) { + if (prefix !== expectedPrefix && prefix !== expectedPrefixInt && prefix !== expectedPrefixSub) { throw "Invalid address prefix"; } dec = dec.slice(expectedPrefix.length); @@ -523,7 +535,11 @@ var cnUtil = (function(initConfig) { var intPaymentId = dec.slice(128, 128 + (INTEGRATED_ID_SIZE * 2)); var checksum = dec.slice(128 + (INTEGRATED_ID_SIZE * 2), 128 + (INTEGRATED_ID_SIZE * 2) + (ADDRESS_CHECKSUM_SIZE * 2)); var expectedChecksum = this.cn_fast_hash(prefix + spend + view + intPaymentId).slice(0, ADDRESS_CHECKSUM_SIZE * 2); + } else if (prefix === expectedPrefix) { + var checksum = dec.slice(128, 128 + (ADDRESS_CHECKSUM_SIZE * 2)); + var expectedChecksum = this.cn_fast_hash(prefix + spend + view).slice(0, ADDRESS_CHECKSUM_SIZE * 2); } else { + // if its not regular address, nor integrated, than it must be subaddress var checksum = dec.slice(128, 128 + (ADDRESS_CHECKSUM_SIZE * 2)); var expectedChecksum = this.cn_fast_hash(prefix + spend + view).slice(0, ADDRESS_CHECKSUM_SIZE * 2); } @@ -1629,7 +1645,7 @@ var cnUtil = (function(initConfig) { return sigs; }; - this.construct_tx = function(keys, sources, dsts, fee_amount, payment_id, pid_encrypt, realDestViewKey, unlock_time, rct) { + this.construct_tx = function(keys, sources, dsts, fee_amount, payment_id, pid_encrypt, realDestViewKey, unlock_time, rct, changeAddress) { //we move payment ID stuff here, because we need txkey to encrypt var txkey = this.random_keypair(); console.log(txkey); @@ -1648,6 +1664,12 @@ var cnUtil = (function(initConfig) { console.log("Extra nonce: " + nonce); extra = this.add_nonce_to_extra(extra, nonce); } + + // for sending to a subaddress, need to generate new tx public key + + //var sub_addr_decoded = this.decode_address(dsts[0].address); // for example dest[0] is subaddress + //txkey.pub = ge_scalarmult(sub_addr_decoded.spend, txkey.sec); + var tx = { unlock_time: unlock_time, version: rct ? CURRENT_TX_VERSION : OLD_TX_VERSION, @@ -1661,7 +1683,8 @@ var cnUtil = (function(initConfig) { } else { tx.signatures = []; } - tx.extra = this.add_pub_key_to_extra(tx.extra, txkey.pub); + + //tx.extra = this.add_pub_key_to_extra(tx.extra, txkey.pub); tx.prvkey = txkey.sec; var in_contexts = []; @@ -1712,12 +1735,33 @@ var cnUtil = (function(initConfig) { var outputs_money = JSBigInt.ZERO; var out_index = 0; var amountKeys = []; //rct only + + // keep generated tx public key, just in case when sending to a subaddress. + // when sending to a subaddress, new txkey.pub is generated, but txkey.sec stays same + var orginal_tx_pub_key = txkey.pub; + for (i = 0; i < dsts.length; ++i) { if (new JSBigInt(dsts[i].amount).compare(0) < 0) { throw "dst.amount < 0"; //amount can be zero if no change } dsts[i].keys = this.decode_address(dsts[i].address); - var out_derivation = this.generate_key_derivation(dsts[i].keys.view, txkey.sec); + + if (this.is_subaddress(dsts[i].address)) + { + txkey.pub = ge_scalarmult(dsts[i].keys.spend, txkey.sec); + } + + var out_derivation; + + if (dsts[i].address === changeAddress.address) + { + out_derivation = this.generate_key_derivation(txkey.pub, changeAddress.view_key); + } + else + { + out_derivation = this.generate_key_derivation(dsts[i].keys.view, txkey.sec); + } + if (rct) { amountKeys.push(this.derivation_to_scalar(out_derivation, out_index)); } @@ -1734,9 +1778,14 @@ var cnUtil = (function(initConfig) { ++out_index; outputs_money = outputs_money.add(dsts[i].amount); } + + tx.extra = this.add_pub_key_to_extra(tx.extra, txkey.pub); + if (outputs_money.add(fee_amount).compare(inputs_money) > 0) { throw "outputs money (" + this.formatMoneyFull(outputs_money) + ") + fee (" + this.formatMoneyFull(fee_amount) + ") > inputs money (" + this.formatMoneyFull(inputs_money) + ")"; } + + if (!rct) { for (i = 0; i < sources.length; ++i) { var src_keys = []; @@ -1795,7 +1844,7 @@ var cnUtil = (function(initConfig) { console.log(tx); return tx; }; - this.create_transaction = function(pub_keys, sec_keys, dsts, outputs, mix_outs, fake_outputs_count, fee_amount, payment_id, pid_encrypt, realDestViewKey, unlock_time, rct) { + this.create_transaction = function(pub_keys, sec_keys, dsts, outputs, mix_outs, fake_outputs_count, fee_amount, payment_id, pid_encrypt, realDestViewKey, unlock_time, rct, changeAddress) { unlock_time = unlock_time || 0; mix_outs = mix_outs || []; var i, j; @@ -1915,7 +1964,7 @@ var cnUtil = (function(initConfig) { } else if (cmp > 0) { throw "Need more money than found! (have: " + cnUtil.formatMoney(found_money) + " need: " + cnUtil.formatMoney(needed_money) + ")"; } - return this.construct_tx(keys, sources, dsts, fee_amount, payment_id, pid_encrypt, realDestViewKey, unlock_time, rct); + return this.construct_tx(keys, sources, dsts, fee_amount, payment_id, pid_encrypt, realDestViewKey, unlock_time, rct, changeAddress); }; this.estimateRctSize = function(inputs, mixin, outputs) { diff --git a/html/js/config.js b/html/js/config.js index cceaf4668..f6f451357 100755 --- a/html/js/config.js +++ b/html/js/config.js @@ -12,8 +12,10 @@ var config = { coinUriPrefix: 'monero:', addressPrefix: 18, integratedAddressPrefix: 19, + subAddressPrefix: 42, addressPrefixTestnet: 53, integratedAddressPrefixTestnet: 54, + subAddressPrefixTestnet: 63, feePerKB: new JSBigInt('2000000000'),//20^10 - for testnet its not used, as fee is dynamic. dustThreshold: new JSBigInt('1000000000'),//10^10 used for choosing outputs/change - we decompose all the way down if the receiver wants now regardless of threshold txChargeRatio: 0.5, diff --git a/html/js/controllers/send_coins.js b/html/js/controllers/send_coins.js index 3dcd9629f..d6a10f938 100755 --- a/html/js/controllers/send_coins.js +++ b/html/js/controllers/send_coins.js @@ -45,6 +45,9 @@ thinwalletCtrls.controller('SendCoinsCtrl', function($scope, $http, $q, var view_only = AccountService.isViewOnly(); + // our address and associated prv view key + var changeAddress = AccountService.getAddressAndViewKey(); + var explorerUrl = config.testnet ? config.testnetExplorerUrl : config.mainnetExplorerUrl; // few multiplayers based on uint64_t wallet2::get_fee_multiplier @@ -649,7 +652,8 @@ thinwalletCtrls.controller('SendCoinsCtrl', function($scope, $http, $q, splittedDsts, using_outs, mix_outs, mixin, neededFee, payment_id, pid_encrypt, - realDestViewKey, 0, rct); + realDestViewKey, 0, rct, + changeAddress); } catch (e) { deferred.reject("Failed to create transaction: " + e);