From 167a074035c1ab1d19821188a5d8be4503b0dbc4 Mon Sep 17 00:00:00 2001 From: J-Dog Date: Wed, 27 Sep 2017 19:37:27 -0700 Subject: [PATCH] Subassets and Custom Fees support (#834) * Trivial: Fixes wrong support URL #802 * Added CP Logo * allow for setting of fee (2 presets) when creating new asset, sending, and making an order * add ability to specify custom fees (in sat/B) * replace blockscan urls with xchain * add subassets support * Change Travis to docker-ce and lock npm version (#835) --- .travis.yml | 2 +- Dockerfile | 2 +- src/css/components/balances.css | 7 + src/css/default.css | 3 +- src/css/misc.css | 2 +- src/js/components/address.js | 26 +- src/js/components/asset.js | 18 +- src/js/components/balances.js | 38 ++- src/js/components/balances_assets.js | 155 ++++++++-- src/js/components/donate.js | 2 +- src/js/components/exchange.js | 355 +++++++++++++++------- src/js/components/feed_notifications.js | 34 ++- src/js/components/feed_pending_actions.js | 34 ++- src/js/components/history.js | 25 +- src/js/components/simplebuy.js | 2 +- src/js/components/wallet.js | 158 ++++++---- src/js/consts.js | 5 +- src/js/locale.js | 2 +- src/js/messagefeed.js | 10 +- src/js/smartadmin.app.js | 7 +- src/js/util.api.js | 2 +- src/js/util.js | 6 +- src/js/util.knockout.js | 93 +++--- src/locales/en/translation.json | 23 +- src/pages/balances.html | 78 ++++- src/pages/exchange.html | 158 +++++----- 26 files changed, 851 insertions(+), 396 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7a1e8bdf6..7b13ba1d6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ env: before_install: - sudo apt-get -qq update # upgrade docker, for build argument support -- sudo apt-get install -o Dpkg::Options::="--force-confold" --force-yes -y docker-engine +- sudo apt-get install -o Dpkg::Options::="--force-confold" --force-yes -y docker-ce - docker version - docker ps -a # get the current PR and branch name diff --git a/Dockerfile b/Dockerfile index 1932f1b75..5f131435c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -77,7 +77,7 @@ COPY . /counterwallet RUN rm -rf /counterwallet/build WORKDIR /counterwallet RUN git rev-parse HEAD -RUN npm -g install npm@latest +RUN npm -g install npm@4.6.1 RUN npm config set strict-ssl false ENV PHANTOMJS_CDNURL="http://cnpmjs.org/downloads" RUN npm install -g bower grunt mocha-phantomjs diff --git a/src/css/components/balances.css b/src/css/components/balances.css index 62c9bca5e..72982c2ed 100644 --- a/src/css/components/balances.css +++ b/src/css/components/balances.css @@ -76,6 +76,13 @@ div#showAssetInfoModal table tbody { color: white; } +.asset-item .name-subasset { + font-size: 28px; + font-weight: bold; + color: white; + word-wrap: break-word; +} + .asset-item .balance { position: relative; /*bottom: -5px;*/ diff --git a/src/css/default.css b/src/css/default.css index 965bb4f8f..79b626532 100644 --- a/src/css/default.css +++ b/src/css/default.css @@ -104,7 +104,8 @@ } .notoAssetColor { - color: #f89406; + color: #f89406; + word-wrap: break-word; /* subasset names may be pretty long*/ } .font-medlg { diff --git a/src/css/misc.css b/src/css/misc.css index e5d7d1b59..674f8cf2a 100644 --- a/src/css/misc.css +++ b/src/css/misc.css @@ -140,4 +140,4 @@ textarea.quickAccessUrl { #langSelector a img { filter : alpha(opacity=50); opacity : 0.5; -} \ No newline at end of file +} diff --git a/src/js/components/address.js b/src/js/components/address.js index 398ac1988..5b41733dd 100644 --- a/src/js/components/address.js +++ b/src/js/components/address.js @@ -41,6 +41,7 @@ function AddressViewModel(type, key, address, initialLabel, pubKeys) { return ko.utils.arrayFilter(self.assets(), function(asset) { return asset.ASSET == 'BTC' || asset.ASSET == 'XCP'; }); + } else if (self.assetFilter() == 'mine') { return ko.utils.arrayFilter(self.assets(), function(asset) { return asset.isMine(); @@ -120,6 +121,18 @@ function AddressViewModel(type, key, address, initialLabel, pubKeys) { }, 500); } + self.initTooltip = function(asset, assetInfo) { + /* initialize tooltip for a truncated subasset name */ + if(!assetInfo['asset_longname']) { + return; + } + + setTimeout(function() { + $("h3.name-subasset").tooltip(); + //$("[rel=tooltip]").tooltip(); + }, 500); + } + self.addOrUpdateAsset = function(asset, assetInfo, initialRawBalance, escrowedBalance) { //Update asset property changes (ONLY establishes initial balance when logging in! -- past that, balance changes // come from debit and credit messages) @@ -145,6 +158,7 @@ function AddressViewModel(type, key, address, initialLabel, pubKeys) { var assetProps = { address: self.ADDRESS, asset: asset, + asset_longname: assetInfo['asset_longname'], divisible: assetInfo['divisible'], owner: assetInfo['owner'] || assetInfo['issuer'], locked: assetInfo['locked'], @@ -156,7 +170,7 @@ function AddressViewModel(type, key, address, initialLabel, pubKeys) { }; self.assets.push(new AssetViewModel(assetProps)); //add new self.initDropDown(asset); - + self.initTooltip(asset, assetInfo); } else { //update existing. NORMALLY this logic is really only reached from the messages feed, however, we can have the // case where if we have a sweep operation for instance (which will show up as an asset transfer and credit @@ -168,9 +182,6 @@ function AddressViewModel(type, key, address, initialLabel, pubKeys) { return; } - //Now that that's out of the way, in cases after here, we should only reach this from the messages feed - assert(assetInfo['owner'] === undefined, "Logic should only be reached via messages feed data, not with get_asset_info data"); - if (assetInfo['description'] != match.description()) { //when the description changes, the balance will get 0 passed into it to note this. obviously, don't take that as the literal balance :) $.jqlog.debug("Updating token " + asset + " @ " + self.ADDRESS + " description to '" + assetInfo['description'] + "'"); @@ -262,12 +273,7 @@ function AddressViewModel(type, key, address, initialLabel, pubKeys) { if (!WALLET.canDoTransaction(self.ADDRESS)) return false; var xcpBalance = WALLET.getBalance(self.ADDRESS, 'XCP'); - var noXCP = false; - if (xcpBalance < ASSET_CREATION_FEE_XCP) { - noXCP = true; - } - - CREATE_ASSET_MODAL.show(self.ADDRESS, true, noXCP); + CREATE_ASSET_MODAL.show(self.ADDRESS, xcpBalance, true); } self.payDividend = function() { diff --git a/src/js/components/asset.js b/src/js/components/asset.js index f83026abb..194e5e20d 100644 --- a/src/js/components/asset.js +++ b/src/js/components/asset.js @@ -2,7 +2,12 @@ function AssetViewModel(props) { //An address has 2 or more assets (BTC, XCP, and any others) var self = this; self.ADDRESS = props['address']; //will not change - self.ASSET = props['asset']; //assetID, will not change + self.ASSET = props['asset']; //assetID (asset name), will not change. + self.ASSET_LONGNAME = props['asset_longname']; //for subassets, this is the entire asset name (asset_longname). for everything else, this is == .ASSET + + self.ASSET_DISP_FULL = self.ASSET_LONGNAME || self.ASSET; //the human readable name of the asset + self.ASSET_DISP = _.truncate(self.ASSET_LONGNAME || self.ASSET, SUBASSET_MAX_DISP_LENGTH); // truncate if necessary + self.DIVISIBLE = props['divisible'] !== undefined ? props['divisible'] : true; self.owner = ko.observable(props['owner']); self.locked = ko.observable(props['locked'] !== undefined ? props['locked'] : false); @@ -48,6 +53,15 @@ function AssetViewModel(props) { return smartFormat(self.normalizedTotalIssued()); }, self); + self.assetType = ko.computed(function() { + if(_.startsWith(self.ASSET, 'A') && !self.ASSET_LONGNAME) { + return 'numeric'; + } else if(self.ASSET_LONGNAME) { + return 'subasset'; + } else { + return 'named'; + } + }, self); self.unconfirmedBalance = ko.observable(0); self.unconfirmedBalance.subscribe(function(value) { @@ -80,7 +94,7 @@ function AssetViewModel(props) { return; } if (!WALLET.canDoTransaction(self.ADDRESS)) return false; - SEND_MODAL.show(self.ADDRESS, self.ASSET, self.rawAvailableBalance(), self.DIVISIBLE); + SEND_MODAL.show(self.ADDRESS, self.ASSET, self.ASSET_DISP, self.rawAvailableBalance(), self.DIVISIBLE); }; self.showInfo = function() { diff --git a/src/js/components/balances.js b/src/js/components/balances.js index 752127af6..a41b50d01 100644 --- a/src/js/components/balances.js +++ b/src/js/components/balances.js @@ -80,7 +80,7 @@ function CreateNewAddressModalViewModel() { var self = this; self.shown = ko.observable(false); - self.addressType = ko.observable(null); //addressType is one of: normal, watch, or armory + self.addressType = ko.observable(null); //addressType is one of: normal, watch, armory or multisig self.armoryPubKey = ko.observable(null); //only set with armory offline addresses self.watchAddress = ko.observable('').extend({ isValidMonosigAddressIfSpecified: self, @@ -407,14 +407,32 @@ function CreateNewAddressModalViewModel() { } } - function SendModalViewModel() { var self = this; self.shown = ko.observable(false); self.address = ko.observable(null); //address string, not an Address object self.asset = ko.observable(); + self.assetDisp = ko.observable(); self.rawBalance = ko.observable(null); self.divisible = ko.observable(); + self.feeOption = ko.observable('optimal'); + self.customFee = ko.observable(null).extend({ + validation: [{ + validator: function(val, self) { + return self.feeOption() === 'custom' ? val : true; + }, + message: i18n.t('field_required'), + params: self + }], + isValidCustomFeeIfSpecified: self + }); + + self.feeOption.subscribeChanged(function(newValue, prevValue) { + if(newValue !== 'custom') { + self.customFee(null); + self.customFee.isModified(false); + } + }); self.destAddress = ko.observable('').extend({ required: true, @@ -586,6 +604,7 @@ function SendModalViewModel() { self.validationModel = ko.validatedObservable({ destAddress: self.destAddress, quantity: self.quantity, + customFee: self.customFee, pubkey1: self.pubkey1, pubkey2: self.pubkey2, pubkey3: self.pubkey3 @@ -602,6 +621,9 @@ function SendModalViewModel() { self.missingPubkey3(false); self.missingPubkey3Address(''); + self.feeOption('optimal'); + self.customFee(null); + self.validationModel.errors.showAllMessages(false); } @@ -641,8 +663,10 @@ function SendModalViewModel() { destination: self.destAddress(), quantity: denormalizeQuantity(parseFloat(self.quantity()), self.divisible()), asset: self.asset(), - _divisible: self.divisible(), - _pubkeys: additionalPubkeys.concat(self._additionalPubkeys) + _asset_divisible: self.divisible(), + _pubkeys: additionalPubkeys.concat(self._additionalPubkeys), + _fee_option: self.feeOption(), + _custom_fee: self.customFee() }, function(txHash, data, endpoint, addressType, armoryUTx) { var message = "" + (armoryUTx ? i18n.t("will_be_sent") : i18n.t("were_sent")) + " "; @@ -653,7 +677,7 @@ function SendModalViewModel() { trackEvent('Balances', 'Send', self.asset()); } - self.show = function(fromAddress, asset, rawBalance, isDivisible, resetForm) { + self.show = function(fromAddress, asset, assetDisp, rawBalance, isDivisible, resetForm) { if (asset == 'BTC' && rawBalance == null) { return bootbox.alert(i18n.t("cannot_send_server_unavailable")); } @@ -663,8 +687,10 @@ function SendModalViewModel() { if (resetForm) self.resetForm(); self.address(fromAddress); self.asset(asset); + self.assetDisp(assetDisp); self.rawBalance(rawBalance); self.divisible(isDivisible); + $('#sendFeeOption').select2("val", self.feeOption()); //hack self.shown(true); trackDialogShow('Send'); } @@ -1300,7 +1326,7 @@ function SweepModalViewModel() { 'to': self.destAddress(), 'normalized_quantity': normalizedQuantity }); - sendData['_divisible'] = !(selectedAsset.RAW_BALANCE == selectedAsset.NORMALIZED_BALANCE); //if the balances match, the asset is NOT divisible + sendData['_assset_divisible'] = !(selectedAsset.RAW_BALANCE == selectedAsset.NORMALIZED_BALANCE); //if the balances match, the asset is NOT divisible PENDING_ACTION_FEED.add(sendTxHash, "sends", sendData); // here we adjust the BTC balance whith the change output diff --git a/src/js/components/balances_assets.js b/src/js/components/balances_assets.js index 3e5cfb695..1bb0afe02 100644 --- a/src/js/components/balances_assets.js +++ b/src/js/components/balances_assets.js @@ -1,20 +1,57 @@ +var ParentAssetInDropdownItemModel = function(asset) { + this.ASSET = asset; +}; + +function createCreateAssetKnockoutValidators() { + ko.validation.rules['assetNameIsTaken'] = { + async: true, + message: i18n.t('token_already_exists'), + validator: function(val, self, callback) { + if(self.tokenNameType() == 'subasset' && self.selectedParentAsset()) { //is a subasset + failoverAPI("get_issuances", {'filters': {'field': 'asset_longname', 'op': '==', 'value': self.selectedParentAsset() + '.' + val}, 'status': 'valid'}, + function(data, endpoint) { + $.jqlog.debug("Asset exists: " + data.length); + return data.length ? callback(false) : callback(true); //empty list -> false (valid = false) + } + ); + } else { + failoverAPI("get_issuances", {'filters': {'field': 'asset', 'op': '==', 'value': val}, 'status': 'valid'}, + function(data, endpoint) { + $.jqlog.debug("Asset exists: " + data.length); + return data.length ? callback(false) : callback(true); //empty list -> false (valid = false) + } + ); + } + } + }; + ko.validation.registerExtenders(); +} function CreateAssetModalViewModel() { var self = this; + createCreateAssetKnockoutValidators(); + self.shown = ko.observable(false); self.address = ko.observable(''); - self.noEnoughXCP = ko.observable(false) + self.xcpBalance = ko.observable(0); self.tokenNameType = ko.observable('alphabetic'); - self.tokenNameType.subscribe(function(val) { - if (val == 'numeric') self.generateRandomId(); - else if (val == 'alphabetic') self.name(''); - }); + self.tokenNameType.subscribe( + function(val) { + if (val == 'numeric') { + self.generateRandomId(); + } else { + self.name(''); + self.name.isModified(false); + } + } + ); self.name = ko.observable('').extend({ required: true, isValidAssetName: self, assetNameIsTaken: self }); + self.selectedParentAsset = ko.observable(''); self.description = ko.observable('').extend({ required: false }); @@ -24,11 +61,55 @@ function CreateAssetModalViewModel() { isValidPositiveQuantityOrZero: self, isValidQtyForDivisibility: self }); + self.feeOption = ko.observable('optimal'); + self.customFee = ko.observable(null).extend({ + validation: [{ + validator: function(val, self) { + return self.feeOption() === 'custom' ? val : true; + }, + message: i18n.t('field_required'), + params: self + }], + isValidCustomFeeIfSpecified: self + }); + + self.hasXCPForNamedAsset = ko.computed(function() { + return self.xcpBalance() >= ASSET_CREATION_FEE_XCP; + }); + self.hasXCPForSubAsset = ko.computed(function() { + return self.xcpBalance() >= SUBASSET_CREATION_FEE_XCP; + }); + + self.ownedNamedAssets = ko.computed(function() { //stores BuySellAddressInDropdownItemModel objects + if (!self.address()) return []; + var ownedAssets = []; + //Get a list of all of my available assets this address owns + var assets = WALLET.getAddressObj(self.address()).assets(); + for (var i = 0; i < assets.length; i++) { + if(assets[i].isMine() && assets[i].assetType() === 'named') { + ownedAssets.push(new ParentAssetInDropdownItemModel(assets[i].ASSET)); + } + } + + ownedAssets.sort(function(left, right) { + return left.ASSET == right.ASSET ? 0 : (left.ASSET > right.ASSET ? -1 : 1); + }); + + return ownedAssets; + }, self); + + self.feeOption.subscribeChanged(function(newValue, prevValue) { + if(newValue !== 'custom') { + self.customFee(null); + self.customFee.isModified(false); + } + }); self.validationModel = ko.validatedObservable({ name: self.name, description: self.description, - quantity: self.quantity + quantity: self.quantity, + customFee: self.customFee }); self.generateRandomId = function() { @@ -41,6 +122,8 @@ function CreateAssetModalViewModel() { self.description(''); self.divisible(true); self.quantity(null); + self.feeOption('optimal'); + self.customFee(null); self.validationModel.errors.showAllMessages(false); } @@ -70,22 +153,27 @@ function CreateAssetModalViewModel() { return false; } - + var name = self.name(); + if(self.tokenNameType() === 'subasset' && self.selectedParentAsset()) { + name = self.selectedParentAsset() + '.' + self.name(); + } WALLET.doTransaction(self.address(), "create_issuance", { source: self.address(), - asset: self.name(), + asset: name, quantity: rawQuantity, divisible: self.divisible(), description: self.description(), - transfer_destination: null + transfer_destination: null, + _fee_option: self.feeOption(), + _custom_fee: self.customFee() }, function(txHash, data, endpoint, addressType, armoryUTx) { var message = ""; if (armoryUTx) { - message = i18n.t("token_will_be_created", self.name()); + message = i18n.t("token_will_be_created", name); } else { - message = i18n.t("token_has_been_created", self.name()); + message = i18n.t("token_has_been_created", name); } message += "

"; if (self.tokenNameType() == 'alphabetic') { @@ -100,13 +188,14 @@ function CreateAssetModalViewModel() { trackEvent('Assets', 'CreateAsset'); } - self.show = function(address, resetForm, noXCP) { - self.noEnoughXCP(noXCP || false); + self.show = function(address, xcpBalance, resetForm) { if (typeof(resetForm) === 'undefined') resetForm = true; if (resetForm) self.resetForm(); + self.xcpBalance(xcpBalance); self.address(address); self.tokenNameType('numeric'); self.generateRandomId(); + $('#createAssetFeeOption').select2("val", self.feeOption()); //hack self.shown(true); trackDialogShow('CreateAsset'); } @@ -356,14 +445,40 @@ function ChangeAssetDescriptionModalViewModel() { } -var DividendAssetInDropdownItemModel = function(asset, rawBalance, normalizedBalance) { +function createPayDividendKnockoutValidators() { + ko.validation.rules['assetNameExists'] = { + async: true, + message: i18n.t('token_dont_exists'), + validator: function(val, self, callback) { + if(val.includes('.')) { //subasset + failoverAPI("get_issuances", {'filters': {'field': 'asset_longname', 'op': '==', 'value': val}, 'status': 'valid'}, + function(data, endpoint) { + $.jqlog.debug("Subasset exists: " + data.length); + return data.length ? callback(true) : callback(false) //empty list -> false (valid = false) + } + ); + } else { //named asset or numeric asset + failoverAPI("get_issuances", {'filters': {'field': 'asset', 'op': '==', 'value': val}, 'status': 'valid'}, + function(data, endpoint) { + $.jqlog.debug("Asset exists: " + data.length); + return data.length ? callback(true) : callback(false) //empty list -> false (valid = false) + } + ); + } + } + }; + ko.validation.registerExtenders(); +} +var DividendAssetInDropdownItemModel = function(asset, assetDisp, rawBalance, normalizedBalance) { this.ASSET = asset; + this.ASSET_DISP = assetDisp; this.RAW_BALANCE = rawBalance; //raw this.NORMALIZED_BALANCE = normalizedBalance; //normalized - this.SELECT_LABEL = asset + " (" + i18n.t('bal') + ": " + normalizedBalance + ")"; + this.SELECT_LABEL = assetDisp + " (" + i18n.t('bal') + ": " + normalizedBalance + ")"; }; function PayDividendModalViewModel() { var self = this; + createPayDividendKnockoutValidators(); self.shown = ko.observable(false); self.addressVM = ko.observable(null); // SOURCE address view model(supplied) @@ -415,8 +530,12 @@ function PayDividendModalViewModel() { required: true }); self.selectedDividendAssetDivisibility = ko.observableArray(null); + self.dispSelectedDividendAsset = ko.observableArray(''); self.selectedDividendAsset.subscribe(function(asset) { self.selectedDividendAssetDivisibility(WALLET.isAssetDivisibilityAvailable(asset) == 0 ? false : true); // asset divisibility should be available.. + if(self.addressVM()) { + self.dispSelectedDividendAsset(self.addressVM().getAssetObj(asset).ASSET_LONGNAME); + } }); self.quantityPerUnit = ko.observable('').extend({ @@ -568,13 +687,13 @@ function PayDividendModalViewModel() { failoverAPI("get_normalized_balances", {'addresses': [address.ADDRESS]}, function(data, endpoint) { for (var i = 0; i < data.length; i++) { if (data[i]['quantity'] !== null && data[i]['quantity'] !== 0) - self.availableDividendAssets.push(new DividendAssetInDropdownItemModel(data[i]['asset'], data[i]['quantity'], data[i]['normalized_quantity'])); + self.availableDividendAssets.push(new DividendAssetInDropdownItemModel(data[i]['asset'], data[i]['asset_longname'] || data[i]['asset'], data[i]['quantity'], data[i]['normalized_quantity'])); } //Also get the BTC balance at this address and put at head of the list WALLET.retrieveBTCBalance(address.ADDRESS, function(balance) { if (balance) { - self.availableDividendAssets.unshift(new DividendAssetInDropdownItemModel("BTC", balance, normalizeQuantity(balance))); + self.availableDividendAssets.unshift(new DividendAssetInDropdownItemModel("BTC", "BTC", balance, normalizeQuantity(balance))); } }); }); @@ -632,6 +751,7 @@ function ShowAssetInfoModalViewModel() { self.shown = ko.observable(false); self.address = ko.observable(null); self.asset = ko.observable(null); + self.assetDisp = ko.observable(null); self.owner = ko.observable(null); self.description = ko.observable(null); self.totalIssued = ko.observable(null); @@ -655,6 +775,7 @@ function ShowAssetInfoModalViewModel() { self.show = function(assetObj) { self.address(assetObj.ADDRESS); self.asset(assetObj.ASSET); + self.assetDisp(assetObj.ASSET_DISP); self.owner(assetObj.owner()); self.description(assetObj.description()); self.totalIssued(assetObj.normalizedTotalIssued()); diff --git a/src/js/components/donate.js b/src/js/components/donate.js index e13ce140c..9b0d852fa 100644 --- a/src/js/components/donate.js +++ b/src/js/components/donate.js @@ -73,7 +73,7 @@ function DonationViewModel() { quantity: denormalizeQuantity(self.quantity()), asset: self.donationCurrency(), destination: DONATION_ADDRESS, - _divisible: true + _asset_divisible: true }; $.jqlog.debug(params); diff --git a/src/js/components/exchange.js b/src/js/components/exchange.js index 560916147..ffa4db005 100644 --- a/src/js/components/exchange.js +++ b/src/js/components/exchange.js @@ -10,7 +10,7 @@ function createExchangeKnockoutValidators() { validator: function(asset, self) { if (asset == 'XCP') return true; var match = ko.utils.arrayFirst(self.allAssets(), function(item) { - return item == asset; + return asset == item['asset'] || (item['asset_longname'] && asset == item['asset_longname']); //matches asset name or asset longname }); return match; }, @@ -61,11 +61,20 @@ function ExchangeViewModel() { self.asset1IsDivisible = ko.observable(null); self.asset2IsDivisible = ko.observable(null); - self.asset1 = ko.observable('').extend({ + self.asset1Raw = ko.observable('').extend({ required: true, ordersIsExistingAssetName: self }); - self.asset2 = ko.observable('').extend({ + self.asset1 = ko.computed(function() { + /* "Token 1" as entered under "Select Another Pair". Will be the numeric asset name with subassets, while asset1Raw is == the subasset longname (or whatever was entered)*/ + if (!self.asset1Raw() || !self.allAssets()) return null; + var match = ko.utils.arrayFirst(self.allAssets(), function(item) { + return self.asset1Raw() == item['asset'] || (item['asset_longname'] && self.asset1Raw() == item['asset_longname']); //matches asset name or asset longname + }); + return match['asset']; + }, self); + self.asset1Longname = ko.observable(''); + self.asset2Raw = ko.observable('').extend({ required: true, ordersIsExistingAssetName: self, validation: { @@ -76,11 +85,23 @@ function ExchangeViewModel() { params: self } }); + self.asset2 = ko.computed(function() { + /* "Token 2" as entered under "Select Another Pair". Will be the numeric asset name with subassets, while asset2Raw is == the subasset longname (or whatever was entered)*/ + if (!self.asset2Raw() || !self.allAssets()) return null; + var match = ko.utils.arrayFirst(self.allAssets(), function(item) { + return self.asset2Raw() == item['asset'] || (item['asset_longname'] && self.asset2Raw() == item['asset_longname']); //matches asset name or asset longname + }); + return match['asset']; + }, self); + self.asset2Longname = ko.observable(''); self.selectedQuoteAsset = ko.observable(); self.selectedQuoteAsset.subscribe(function(value) { - if (value == 'XCP') self.asset2(value); - else self.asset2(''); + if (value == 'XCP') { + self.asset2Raw(value); + } else { + self.asset2Raw(''); + } }) self.assetPair = ko.computed(function() { @@ -89,9 +110,13 @@ function ExchangeViewModel() { return pair; //2 element array, as [baseAsset, quoteAsset] }, self); self.dispAssetPair = ko.computed(function() { - if (!self.assetPair()) return null; - var pair = self.assetPair(); - return pair[0] + "/" + pair[1]; + if (!self.asset1() || !self.asset2()) return null; + var pair = assetsToAssetPair(self.asset1(), self.asset2()); + if(pair[0] === self.asset1()) { + return (self.asset1Longname() || self.asset1()) + '/' + (self.asset2Longname() || self.asset2()); + } else { + return (self.asset2Longname() || self.asset2()) + '/' + (self.asset1Longname() || self.asset1()); + } }, self); self.dispAssetPair.subscribeChanged(function(newValue, prevValue) { self.currentMarketPrice(0); @@ -100,18 +125,43 @@ function ExchangeViewModel() { if (!self.assetPair()) return null; return self.assetPair()[0]; }, self); - self.quoteAsset = ko.computed(function() { - if (!self.assetPair()) return null; - return self.assetPair()[1]; - }, self); self.baseAssetIsDivisible = ko.computed(function() { if (!self.assetPair()) return null; return self.baseAsset() == self.asset1() ? self.asset1IsDivisible() : self.asset2IsDivisible(); }, self); + self.baseAssetLongname = ko.computed(function() { + if (!self.assetPair()) return null; + if(self.assetPair()[0] === self.asset1()) { + return self.asset1Longname(); + } else { + return self.asset2Longname(); + } + }, self); + self.dispBaseAsset = ko.computed(function() { + if (!self.baseAsset()) return null; + return self.baseAssetLongname() ? _.truncate(self.baseAssetLongname(), 24) : self.baseAsset(); + }, self); + + self.quoteAsset = ko.computed(function() { + if (!self.assetPair()) return null; + return self.assetPair()[1]; + }, self); self.quoteAssetIsDivisible = ko.computed(function() { if (!self.assetPair()) return null; return self.quoteAsset() == self.asset1() ? self.asset1IsDivisible() : self.asset2IsDivisible(); }, self); + self.quoteAssetLongname = ko.computed(function() { + if (!self.assetPair()) return null; + if(self.assetPair()[1] === self.asset1()) { + return self.asset1Longname(); + } else { + return self.asset2Longname(); + } + }, self); + self.dispQuoteAsset = ko.computed(function() { + if (!self.quoteAsset()) return null; + return self.quoteAssetLongname() ? _.truncate(self.quoteAssetLongname(), 24) : self.quoteAsset(); + }, self); self.delayedAssetPairSelection = ko.computed(self.assetPair).extend({ rateLimit: { @@ -130,18 +180,37 @@ function ExchangeViewModel() { self.sellTotal(0); self.selectedAddressForBuy(null); self.selectedAddressForSell(null); - $('table.buySellForm span.invalid').hide() // hack + $('table.buySellForm span.invalid').hide(); // hack self.baseAssetImage(''); self.dexHome(false); self.fetchMarketDetails(); $('a.top_user_pair').removeClass('selected_pair'); $('a.top_user_pair.pair_' + self.baseAsset() + self.quoteAsset()).addClass('selected_pair'); + + self.buyValidation.errors.showAllMessages(false); + self.sellValidation.errors.showAllMessages(false); + + if(self.asset1Raw() != self.asset1()) { + assert(self.asset1Raw().includes('.')); + self.asset1Longname(self.asset1Raw()); + } + if(self.asset2Raw() != self.asset2()) { + assert(self.asset2Raw().includes('.')); + self.asset2Longname(self.asset2Raw()); + } + + self.buyFeeOption('optimal'); + self.buyCustomFee(null); + self.sellFeeOption('optimal'); + self.sellCustomFee(null); + $('#buyFeeOption').select2("val", self.buyFeeOption()); //hack + $('#sellFeeOption').select2("val", self.sellFeeOption()); //hack }); //VALIDATION MODELS self.validationModelBaseOrders = ko.validatedObservable({ - asset1: self.asset1, - asset2: self.asset2 + asset1Raw: self.asset1Raw, + asset2Raw: self.asset2Raw }); @@ -182,6 +251,17 @@ function ExchangeViewModel() { isValidPositiveQuantity: self, quoteDivisibilityIsOk: self }); + self.sellFeeOption = ko.observable('optimal'); + self.sellCustomFee = ko.observable(null).extend({ + validation: [{ + validator: function(val, self) { + return self.sellFeeOption() === 'custom' ? val : true; + }, + message: i18n.t('field_required'), + params: self + }], + isValidCustomFeeIfSpecified: self + }); self.sellPriceHasFocus = ko.observable(); self.sellAmountHasFocus = ko.observable(); self.sellTotalHasFocus = ko.observable(); @@ -189,6 +269,13 @@ function ExchangeViewModel() { self.selectedAddressForSell = ko.observable(); self.availableBalanceForSell = ko.observable(); + self.sellFeeOption.subscribeChanged(function(newValue, prevValue) { + if(newValue !== 'custom') { + self.sellCustomFee(null); + self.sellCustomFee.isModified(false); + } + }); + self.availableAddressesForSell = ko.computed(function() { //stores BuySellAddressInDropdownItemModel objects if (!self.baseAsset()) return null; //must have a sell asset selected //Get a list of all of my available addresses with the specified sell asset balance @@ -265,16 +352,10 @@ function ExchangeViewModel() { self.sellValidation = ko.validatedObservable({ sellAmount: self.sellAmount, sellPrice: self.sellPrice, - sellTotal: self.sellTotal + sellTotal: self.sellTotal, + sellCustomFee: self.sellCustomFee }); - self.sellFee = ko.computed(function() { - var give_quantity = denormalizeQuantity(self.sellAmount(), self.baseAssetIsDivisible()); - var fee_provided = MIN_FEE; - return normalizeQuantity(fee_provided); - }); - - self.sellRedeemableFee = ko.observable(normalizeQuantity(2 * MULTISIG_DUST_SIZE)); self.selectBuyOrder = function(order, notFromClick) { var price = new Decimal(cleanHtmlPrice(order.price)); @@ -327,13 +408,17 @@ function ExchangeViewModel() { source: self.selectedAddressForSell(), give_quantity: give_quantity, give_asset: self.baseAsset(), - _give_divisible: self.baseAssetIsDivisible(), + _give_asset_divisible: self.baseAssetIsDivisible(), + _give_asset_longname: self.baseAssetLongname(), get_quantity: get_quantity, get_asset: self.quoteAsset(), - _get_divisible: self.quoteAssetIsDivisible(), + _get_asset_divisible: self.quoteAssetIsDivisible(), + _get_asset_longname: self.quoteAssetLongname(), fee_required: fee_required, fee_provided: fee_provided, - expiration: expiration + expiration: expiration, + _fee_option: self.sellFeeOption(), + _custom_fee: self.sellCustomFee() } var onSuccess = function(txHash, data, endpoint, addressType, armoryUTx) { @@ -341,9 +426,9 @@ function ExchangeViewModel() { var message = ""; if (armoryUTx) { - message = i18n.t("you_sell_order_will_be_placed", self.sellAmount(), self.baseAsset()); + message = i18n.t("you_sell_order_will_be_placed", self.sellAmount(), self.dispBaseAsset()); } else { - message = i18n.t("you_sell_order_has_been_placed", self.sellAmount(), self.baseAsset()); + message = i18n.t("you_sell_order_has_been_placed", self.sellAmount(), self.dispBaseAsset()); } WALLET.showTransactionCompleteDialog(message + " " + i18n.t(ACTION_PENDING_NOTICE), message, armoryUTx); @@ -378,10 +463,10 @@ function ExchangeViewModel() { estimatedTotalPrice = smartFormat(estimatedTotalPrice); var message = ''; - message += ''; - message += ''; - message += ''; - message += ''; + message += ''; + message += ''; + message += ''; + message += ''; message += '
' + i18n.t('price') + ': ' + self.sellPrice() + '' + self.quoteAsset() + '/' + self.baseAsset() + '
' + i18n.t('amount') + ': ' + self.sellAmount() + '' + self.baseAsset() + '
' + i18n.t('total') + ': ' + self.sellTotal() + '' + self.quoteAsset() + '
' + i18n.t('real_estimated_total') + ': ' + estimatedTotalPrice + '' + self.quoteAsset() + '
' + i18n.t('price') + ': ' + self.sellPrice() + '' + self.dispQuoteAsset() + '
' + i18n.t('amount') + ': ' + self.sellAmount() + '' + self.dispBaseAsset() + '
' + i18n.t('total') + ': ' + self.sellTotal() + '' + self.dispQuoteAsset() + '
' + i18n.t('real_estimated_total') + ': ' + estimatedTotalPrice + '' + self.dispQuoteAsset() + '
'; bootbox.dialog({ @@ -433,6 +518,18 @@ function ExchangeViewModel() { isValidPositiveQuantity: self, quoteDivisibilityIsOk: self }); + self.buyFeeOption = ko.observable('optimal'); + self.buyCustomFee = ko.observable(null).extend({ + validation: [{ + validator: function(val, self) { + return self.buyFeeOption() === 'custom' ? val : true; + }, + message: i18n.t('field_required'), + params: self + }], + isValidCustomFeeIfSpecified: self + }); + self.buyPriceHasFocus = ko.observable(); self.buyAmountHasFocus = ko.observable(); self.buyTotalHasFocus = ko.observable(); @@ -440,6 +537,13 @@ function ExchangeViewModel() { self.selectedAddressForBuy = ko.observable(); self.availableBalanceForBuy = ko.observable(); + self.buyFeeOption.subscribeChanged(function(newValue, prevValue) { + if(newValue !== 'custom') { + self.buyCustomFee(null); + self.buyCustomFee.isModified(false); + } + }); + self.availableAddressesForBuy = ko.computed(function() { //stores BuySellAddressInDropdownItemModel objects if (!self.quoteAsset()) return null; //must have a sell asset selected //Get a list of all of my available addresses with the specified sell asset balance @@ -520,17 +624,10 @@ function ExchangeViewModel() { self.buyValidation = ko.validatedObservable({ buyTotal: self.buyTotal, buyPrice: self.buyPrice, - buyAmount: self.buyAmount - }); - - self.buyFee = ko.computed(function() { - var give_quantity = denormalizeQuantity(self.buyTotal(), self.quoteAssetIsDivisible()); - var fee_provided = MIN_FEE; - return normalizeQuantity(fee_provided); + buyAmount: self.buyAmount, + buyCustomFee: self.buyCustomFee }); - self.buyRedeemableFee = ko.observable(normalizeQuantity(2 * MULTISIG_DUST_SIZE)); - self.selectSellOrder = function(order, notFromClick) { var price = new Decimal(cleanHtmlPrice(order.price)); var amount = new Decimal(order.base_depth); @@ -586,13 +683,17 @@ function ExchangeViewModel() { source: self.selectedAddressForBuy(), give_quantity: give_quantity, give_asset: self.quoteAsset(), - _give_divisible: self.quoteAssetIsDivisible(), + _give_asset_divisible: self.quoteAssetIsDivisible(), + _give_asset_longname: self.quoteAssetLongname(), get_quantity: get_quantity, get_asset: self.baseAsset(), - _get_divisible: self.baseAssetIsDivisible(), + _get_asset_divisible: self.baseAssetIsDivisible(), + _get_asset_longname: self.baseAssetLongname(), fee_required: fee_required, fee_provided: fee_provided, - expiration: expiration + expiration: expiration, + _fee_option: self.buyFeeOption(), + _custom_fee: self.buyCustomFee() } var onSuccess = function(txHash, data, endpoint, addressType, armoryUTx) { @@ -600,9 +701,9 @@ function ExchangeViewModel() { var message = ""; if (armoryUTx) { - message = i18n.t("you_buy_order_will_be_placed", self.buyAmount(), self.baseAsset()); + message = i18n.t("you_buy_order_will_be_placed", self.buyAmount(), self.dispBaseAsset()); } else { - message = i18n.t("you_buy_order_has_been_placed", self.buyAmount(), self.baseAsset()); + message = i18n.t("you_buy_order_has_been_placed", self.buyAmount(), self.dispBaseAsset()); } WALLET.showTransactionCompleteDialog(message + " " + i18n.t(ACTION_PENDING_NOTICE), message, armoryUTx); @@ -637,10 +738,10 @@ function ExchangeViewModel() { estimatedTotalPrice = smartFormat(estimatedTotalPrice); var message = ''; - message += ''; - message += ''; - message += ''; - message += ''; + message += ''; + message += ''; + message += ''; + message += ''; message += '
' + i18n.t('price') + ': ' + self.buyPrice() + '' + self.quoteAsset() + '/' + self.baseAsset() + '
' + i18n.t('amount') + ': ' + self.buyAmount() + '' + self.baseAsset() + '
' + i18n.t('total') + ': ' + self.buyTotal() + '' + self.quoteAsset() + '
' + i18n.t('real_estimated_total') + ': ' + estimatedTotalPrice + '' + self.quoteAsset() + '
' + i18n.t('price') + ': ' + self.buyPrice() + '' + self.dispQuoteAsset() + '
' + i18n.t('amount') + ': ' + self.buyAmount() + '' + self.dispBaseAsset() + '
' + i18n.t('total') + ': ' + self.buyTotal() + '' + self.dispQuoteAsset() + '
' + i18n.t('real_estimated_total') + ': ' + estimatedTotalPrice + '' + self.dispQuoteAsset() + '
'; bootbox.dialog({ @@ -677,13 +778,25 @@ function ExchangeViewModel() { self.displayTopUserPairs = function(data) { for (var p in data) { var classes = ['top_user_pair']; - if (data[p]['trend'] > 0) classes.push('txt-color-greenDark'); - else if (data[p]['trend'] < 0) classes.push('txt-color-red'); - if (parseFloat(data[p]['progression']) > 0) classes.push('progression-up'); - else if (parseFloat(data[p]['progression']) < 0) classes.push('progression-down'); - if (data[p]['my_order_count']) classes.push('with-open-order'); + + if (data[p]['trend'] > 0) { + classes.push('txt-color-greenDark'); + } else if (data[p]['trend'] < 0) { + classes.push('txt-color-red'); + } + + if (parseFloat(data[p]['progression']) > 0) { + classes.push('progression-up'); + } else if (parseFloat(data[p]['progression']) < 0) { + classes.push('progression-down'); + } + + if (data[p]['my_order_count']) { + classes.push('with-open-order'); + } + classes.push("pair_" + data[p]['base_asset'] + data[p]['quote_asset']); - data[p]['pair_classes'] = classes.join(" "); + data[p]['pair_classes'] = classes.join(' '); } self.topUserPairs(data); } @@ -803,23 +916,15 @@ function ExchangeViewModel() { self.fetchAllPairs = function() { try { - //$('#wid-id-assetPairMarketInfo header span.jarviswidget-loader').show(); self.allPairs([]); $('#assetPairMarketInfo').dataTable().fnClearTable(); - //$('#assetPairMarketInfo_wrapper').hide(); } catch (e) {} - failoverAPI('get_markets_list', [], self.displayAllPairs) - /*failoverAPI('get_markets_list', [], function(data, endpoint) { - self.displayAllPairs(data); - $('#assetPairMarketInfo_wrapper').show(); - //$('#wid-id-assetPairMarketInfo header span.jarviswidget-loader').hide(); - });*/ + failoverAPI('get_markets_list', [], self.displayAllPairs); } /* MARKET DETAILS */ self.displayMarketDetails = function(data) { - if (data['base_asset_infos'] && data['base_asset_infos']['valid_image']) { self.baseAssetImage(assetImageUrl(data['base_asset'])); } @@ -917,13 +1022,16 @@ function ExchangeViewModel() { } self.selectMarket = function(item) { - self.asset1(item.base_asset); + self.asset1Raw(item.base_asset_longname || item.base_asset); + self.asset1Longname(item.base_asset_longname); if (item.quote_asset == 'XCP') { self.selectedQuoteAsset(item.quote_asset); } else { self.selectedQuoteAsset('Other'); - self.asset2(item.quote_asset); + self.asset2Raw(item.quote_asset_longname || item.quote_asset); + self.asset2Longname(item.quote_asset_longname); } + trackEvent('Exchange', 'MarketSelected', self.dispAssetPair()); } @@ -947,27 +1055,30 @@ function ExchangeViewModel() { self.fetchAllPairs(); //Get a list of all assets - failoverAPI("get_asset_names", {}, function(data, endpoint) { - data = ['XCP'].concat(data); + failoverAPI("get_assets_names_and_longnames", {}, function(data, endpoint) { + //result is a list of tuples. each entry in the tuple is (asset, asset_longname) + //XCP is already included self.allAssets(data); //Set up typeahead bindings manually for now (can't get knockout and typeahead playing well together...) var assets = new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.whitespace, + //datumTokenizer: function (data) { return Bloodhound.tokenizers.whitespace(data[1] || data[0]); }, + datumTokenizer: function (data) { return Bloodhound.tokenizers.whitespace(data['asset_longname'] || data['asset']); }, queryTokenizer: Bloodhound.tokenizers.whitespace, local: self.allAssets() }); assets.initialize(); - $('#asset1, #asset2').typeahead(null, { + $('#asset1Raw, #asset2Raw').typeahead(null, { source: assets.ttAdapter(), - displayKey: function(obj) { - return obj; + displayKey: function(data) { + return data['asset_longname'] || data['asset']; } - }).on('typeahead:selected', function($e, datum) { - if ($($e.target).attr('name') == 'asset1') - self.asset1(datum); //gotta do a manual update...doesn't play well with knockout - else if ($($e.target).attr('name') == 'asset2') - self.asset2(datum); //gotta do a manual update...doesn't play well with knockout + }).on('typeahead:selected', function($e, data) { + if ($($e.target).attr('name') == 'asset1Raw') { + self.asset1Raw(data['asset_longname'] || data['asset']); //gotta do a manual update...doesn't play well with knockout + } else if ($($e.target).attr('name') == 'asset2Raw') { + self.asset2Raw(data['asset_longname'] || data['asset']); //gotta do a manual update...doesn't play well with knockout + } }); }); } @@ -1197,6 +1308,7 @@ function OpenOrdersViewModel() { self.openOrders = ko.observableArray([]); self.addressesLabels = {}; + self.allAssets = ko.observableArray([]); self.init = function() { self.addressesLabels = {}; @@ -1207,14 +1319,20 @@ function OpenOrdersViewModel() { self.addressesLabels[wallet_adressess[i][0]] = wallet_adressess[i][1]; } - var params = { - filters: [ - {'field': 'source', 'op': 'IN', 'value': addresses}, - {'field': 'give_remaining', 'op': '>', 'value': 0} - ], - status: 'open' - }; - failoverAPI("get_orders", params, self.displayOpenOrders); + failoverAPI("get_assets_names_and_longnames", {}, function(data, endpoint) { + //result is a list of tuples. each entry in the tuple is (asset, asset_longname) + //XCP is already included + self.allAssets(data); + + var params = { + filters: [ + {'field': 'source', 'op': 'IN', 'value': addresses}, + {'field': 'give_remaining', 'op': '>', 'value': 0} + ], + status: 'open' + }; + failoverAPI("get_orders", params, self.displayOpenOrders); + }); } self.displayOpenOrders = function(data) { @@ -1227,8 +1345,20 @@ function OpenOrdersViewModel() { order.tx_hash = data[i].tx_hash; order.source = data[i].source; order.address_label = self.addressesLabels[order.source]; + + //TODO: yes this is essentially O(n^2) ... fix later + var match = null; order.give_asset = data[i].give_asset; + match = ko.utils.arrayFirst(self.allAssets(), function(item) { + return order.give_asset == item['asset']; //matches asset name or asset longname + }); + order.give_asset_disp = match['asset_longname'] || match['asset']; order.get_asset = data[i].get_asset; + match = ko.utils.arrayFirst(self.allAssets(), function(item) { + return order.get_asset == item['asset']; //matches asset name or asset longname + }); + order.get_asset_disp = match['asset_longname'] || match['asset']; + order.give_quantity = data[i].give_quantity; order.get_quantity = data[i].get_quantity; order.give_remaining = Math.max(data[i].give_remaining, 0); @@ -1244,10 +1374,10 @@ function OpenOrdersViewModel() { WALLET.getAssetsDivisibility(assets, function(assetsDivisibility) { for (var i = 0; i < orders.length; i++) { - orders[i].give_quantity_str = smartFormat(normalizeQuantity(orders[i].give_quantity, assetsDivisibility[orders[i].give_asset])) + ' ' + orders[i].give_asset; - orders[i].get_quantity_str = smartFormat(normalizeQuantity(orders[i].get_quantity, assetsDivisibility[orders[i].get_asset])) + ' ' + orders[i].get_asset; - orders[i].give_remaining_str = smartFormat(normalizeQuantity(orders[i].give_remaining, assetsDivisibility[orders[i].give_asset])) + ' ' + orders[i].give_asset; - orders[i].get_remaining_str = smartFormat(normalizeQuantity(orders[i].get_remaining, assetsDivisibility[orders[i].get_asset])) + ' ' + orders[i].get_asset; + orders[i].give_quantity_str = smartFormat(normalizeQuantity(orders[i].give_quantity, assetsDivisibility[orders[i].give_asset])) + ' ' + orders[i].give_asset_disp; + orders[i].get_quantity_str = smartFormat(normalizeQuantity(orders[i].get_quantity, assetsDivisibility[orders[i].get_asset])) + ' ' + orders[i].get_asset_disp; + orders[i].give_remaining_str = smartFormat(normalizeQuantity(orders[i].give_remaining, assetsDivisibility[orders[i].give_asset])) + ' ' + orders[i].give_asset_disp; + orders[i].get_remaining_str = smartFormat(normalizeQuantity(orders[i].get_remaining, assetsDivisibility[orders[i].get_asset])) + ' ' + orders[i].get_asset_disp; } self.openOrders(orders); var openOrdersTable = $('#openOrdersTable').dataTable(); @@ -1314,6 +1444,7 @@ function OrderMatchesViewModel() { self.orderMatches = ko.observableArray([]); self.addressesLabels = {}; + self.allAssets = ko.observableArray([]); self.init = function() { self.addressesLabels = {}; @@ -1324,17 +1455,23 @@ function OrderMatchesViewModel() { self.addressesLabels[wallet_adressess[i][0]] = wallet_adressess[i][1]; } - var params = { - filters: [ - {'field': 'tx0_address', 'op': 'IN', 'value': addresses}, - {'field': 'tx1_address', 'op': 'IN', 'value': addresses} - ], - filterop: 'OR', - status: ['pending', 'completed', 'expired'], - order_by: 'block_index', - order_dir: 'DESC' - }; - failoverAPI("get_order_matches", params, self.displayOrderMatches); + failoverAPI("get_assets_names_and_longnames", {}, function(data, endpoint) { + //result is a list of tuples. each entry in the tuple is (asset, asset_longname) + //XCP is already included + self.allAssets(data); + + var params = { + filters: [ + {'field': 'tx0_address', 'op': 'IN', 'value': addresses}, + {'field': 'tx1_address', 'op': 'IN', 'value': addresses} + ], + filterop: 'OR', + status: ['pending', 'completed', 'expired'], + order_by: 'block_index', + order_dir: 'DESC' + }; + failoverAPI("get_order_matches", params, self.displayOrderMatches); + }); } self.displayOrderMatches = function(data) { @@ -1358,6 +1495,18 @@ function OrderMatchesViewModel() { order_match.give_asset = data[i].backward_asset; order_match.get_asset = data[i].forward_asset; } + + //TODO: yes this is essentially O(n^2) ... fix later + var match = null; + match = ko.utils.arrayFirst(self.allAssets(), function(item) { + return order_match.give_asset == item['asset']; //matches asset name or asset longname + }); + order_match.give_asset_disp = match['asset_longname'] || match['asset']; + match = ko.utils.arrayFirst(self.allAssets(), function(item) { + return order_match.get_asset == item['asset']; //matches asset name or asset longname + }); + order_match.get_asset_disp = match['asset_longname'] || match['asset']; + order_match.status = data[i].status; order_match.block_index = data[i].block_index; @@ -1383,8 +1532,8 @@ function OrderMatchesViewModel() { WALLET.getAssetsDivisibility(assets, function(assetsDivisibility) { for (var i = 0; i < order_matches.length; i++) { - order_matches[i].give_quantity_str = smartFormat(normalizeQuantity(order_matches[i].give_quantity, assetsDivisibility[order_matches[i].give_asset])) + ' ' + order_matches[i].give_asset; - order_matches[i].get_quantity_str = smartFormat(normalizeQuantity(order_matches[i].get_quantity, assetsDivisibility[order_matches[i].get_asset])) + ' ' + order_matches[i].get_asset; + order_matches[i].give_quantity_str = smartFormat(normalizeQuantity(order_matches[i].give_quantity, assetsDivisibility[order_matches[i].give_asset])) + ' ' + order_matches[i].give_asset_disp; + order_matches[i].get_quantity_str = smartFormat(normalizeQuantity(order_matches[i].get_quantity, assetsDivisibility[order_matches[i].get_asset])) + ' ' + order_matches[i].get_asset_disp; } $('#orderMatchesTable').dataTable().fnClearTable(); self.orderMatches(order_matches); diff --git a/src/js/components/feed_notifications.js b/src/js/components/feed_notifications.js index fcb9483af..49619be71 100644 --- a/src/js/components/feed_notifications.js +++ b/src/js/components/feed_notifications.js @@ -40,14 +40,14 @@ NotificationViewModel.calcText = function(category, message) { if (category == "sends") { if (WALLET.getAddressObj(message['source']) && WALLET.getAddressObj(message['destination'])) { - desc = i18n.t("notif_you_transferred", smartFormat(normalizeQuantity(message['quantity'], message['_divisible'])), - message['asset'], getAddressLabel(message['source']), getAddressLabel(message['destination'])); + desc = i18n.t("notif_you_transferred", smartFormat(normalizeQuantity(message['quantity'], message['_asset_divisible'])), + message['_asset_longname'] || message['asset'], getAddressLabel(message['source']), getAddressLabel(message['destination'])); } else if (WALLET.getAddressObj(message['source'])) { //we sent funds - desc = i18n.t("notif_you_sent", smartFormat(normalizeQuantity(message['quantity'], message['_divisible'])), - message['asset'], getAddressLabel(message['source']), getAddressLabel(message['destination'])); + desc = i18n.t("notif_you_sent", smartFormat(normalizeQuantity(message['quantity'], message['_asset_divisible'])), + message['_asset_longname'] || message['asset'], getAddressLabel(message['source']), getAddressLabel(message['destination'])); } else if (WALLET.getAddressObj(message['destination'])) { //we received funds - desc = i18n.t("notif_you_received", smartFormat(normalizeQuantity(message['quantity'], message['_divisible'])), - message['asset'], getAddressLabel(message['source']), getAddressLabel(message['destination'])); + desc = i18n.t("notif_you_received", smartFormat(normalizeQuantity(message['quantity'], message['_asset_divisible'])), + message['_asset_longname'] || message['asset'], getAddressLabel(message['source']), getAddressLabel(message['destination'])); } } else if (category == "btcpays" && (WALLET.getAddressObj(message['source']) || WALLET.getAddressObj(message['destination']))) { desc = i18n.t("notif_btcpay_from", getAddressLabel(message['source']), getAddressLabel(message['destination']), @@ -62,7 +62,8 @@ NotificationViewModel.calcText = function(category, message) { // NOTE that counterpartyd has automatically already adusted the balances of all asset holders...we just need to notify var addressesWithAsset = WALLET.getAddressesWithAsset(message['asset']); if (!addressesWithAsset.length) return; - desc = i18n.t("notif_dividend_done", message['dividend_asset'], addressesWithAsset.join(', '), message['quantity_per_unit'], message['asset']); + desc = i18n.t("notif_dividend_done", message['_dividend_asset_longname'] || message['dividend_asset'], + addressesWithAsset.join(', '), message['quantity_per_unit'], message['_asset_longname'] || message['asset']); } else if (category == 'issuances') { var addresses = WALLET.getAddressesList(); var assetObj = null; @@ -74,34 +75,35 @@ NotificationViewModel.calcText = function(category, message) { //Detect transfers, whether we currently have the object in our wallet or not (as it could be // a transfer FROM an address outside of our wallet) if (addresses.indexOf(message['source']) != -1 || addresses.indexOf(message['issuer']) != -1) { - desc = i18n.t("notif_token_transferred", message['asset'], getLinkForCPData('address', message['source'], getAddressLabel(message['source'])), + desc = i18n.t("notif_token_transferred", message['_asset_longname'] || message['asset'], getLinkForCPData('address', message['source'], getAddressLabel(message['source'])), getLinkForCPData('address', message['issuer'], getAddressLabel(message['issuer']))); } } else if (assetObj) { //the address is in the wallet //Detect everything else besides transfers, which we only care to see if the asset is listed in one of the wallet addresses if (message['locked']) { assert(!assetObj.locked()); - desc = i18n.t("notif_token_locked", message['asset']); + desc = i18n.t("notif_token_locked", message['_asset_longname'] || message['asset']); } else if (message['description'] != assetObj.description()) { - desc = i18n.t("notif_token_desc_changed", message['asset'], assetObj.description(), message['description']); + desc = i18n.t("notif_token_desc_changed", message['_asset_longname'] || message['asset'], assetObj.description(), message['description']); } else { var additionalQuantity = message['quantity']; if (additionalQuantity) { - desc = i18n.t("notif_additional_issued", smartFormat(normalizeQuantity(additionalQuantity, assetObj.DIVISIBLE)), message['asset']); + desc = i18n.t("notif_additional_issued", smartFormat(normalizeQuantity(additionalQuantity, assetObj.DIVISIBLE)), message['_asset_longname'] || message['asset']); } else { //this is not a transfer, but it is not in our wallet as well we can assume it's an issuance of a totally new asset - desc = i18n.t("notif_token_issued", message['asset'], smartFormat(normalizeQuantity(message['quantity'], message['divisible']))); + desc = i18n.t("notif_token_issued", message['_asset_longname'] || message['asset'], smartFormat(normalizeQuantity(message['quantity'], message['divisible']))); } } } } else if (category == "orders" && WALLET.getAddressObj(message['source'])) { desc = i18n.t("notif_order_buy_active", smartFormat(normalizeQuantity(message['get_quantity'], message['_get_asset_divisible'])), - message['get_asset'], getAddressLabel(message['source']), smartFormat(normalizeQuantity(message['give_quantity'], message['_give_asset_divisible'])), - message['give_asset']); + message['_get_asset_longname'] || message['get_asset'], getAddressLabel(message['source']), smartFormat(normalizeQuantity(message['give_quantity'], message['_give_asset_divisible'])), + message['_give_asset_longname'] || message['give_asset']); } else if (category == "order_matches" && (WALLET.getAddressObj(message['tx0_address']) || WALLET.getAddressObj(message['tx1_address']))) { desc = i18n.t("notif_order_matched", getAddressLabel(message['tx0_address']), smartFormat(normalizeQuantity(message['forward_quantity'], message['_forward_asset_divisible'])), - message['forward_asset'], getAddressLabel(message['tx1_address']), smartFormat(normalizeQuantity(message['backward_quantity'], message['_backward_asset_divisible'])), - message['backward_asset']); + message['_forward_asset_longname'] || message['forward_asset'], getAddressLabel(message['tx1_address']), + smartFormat(normalizeQuantity(message['backward_quantity'], message['_backward_asset_divisible'])), + message['_backward_asset_longname'] || message['backward_asset']); } else if (category == "order_expirations" && WALLET.getAddressObj(message['source'])) { desc = i18n.t("notif_order_expired", message['order_index'], getAddressLabel(message['source'])); } else if (category == "order_match_expirations") { diff --git a/src/js/components/feed_pending_actions.js b/src/js/components/feed_pending_actions.js index 1bb40ea6e..5f6c2f8f4 100644 --- a/src/js/components/feed_pending_actions.js +++ b/src/js/components/feed_pending_actions.js @@ -11,35 +11,41 @@ function PendingActionViewModel(txHash, category, data, when) { PendingActionViewModel.calcText = function(category, data) { //This is data as it is specified from the relevant create_ API request parameters (NOT as it comes in from the message feed) var desc = ""; - var divisible = null; + var divisible = null, asset_longname = null; var pending = data['mempool'] ? 'Unconfirmed' : 'Pending'; //The category being allowable was checked in the factory class if (data['source'] && data['asset']) { - divisible = data['divisible'] !== undefined ? data['divisible'] : (data['_divisible'] !== undefined ? data['_divisible'] : WALLET.getAddressObj(data['source']).getAssetObj(data['asset']).DIVISIBLE); + divisible = data['divisible'] !== undefined ? data['divisible'] : (data['_asset_divisible'] !== undefined ? data['_asset_divisible'] : WALLET.getAddressObj(data['source']).getAssetObj(data['asset']).DIVISIBLE); //^ if the asset is being created, data['divisible'] should be present (or [_divisible] if coming in from message feed oftentimes), // otherwise, get it from an existing asset in our wallet + asset_longname = data['asset_longname'] !== undefined ? data['asset_longname'] : (data['_asset_longname'] !== undefined ? data['_asset_longname'] : WALLET.getAddressObj(data['source']).getAssetObj(data['asset']).ASSET_LONGNAME); } if (category == 'burns') { desc = i18n.t("pend_or_unconf_burn", pending, normalizeQuantity(data['quantity'])); } else if (category == 'sends') { - desc = i18n.t("pend_or_unconf_send", pending, numberWithCommas(normalizeQuantity(data['quantity'], divisible)), data['asset'], + desc = i18n.t("pend_or_unconf_send", pending, numberWithCommas(normalizeQuantity(data['quantity'], divisible)), + asset_longname || data['asset'], getLinkForCPData('address', data['source'], getAddressLabel(data['source'])), getLinkForCPData('address', data['destination'], getAddressLabel(data['destination']))); } else if (category == 'orders') { - desc = i18n.t("pend_or_unconf_order", pending, numberWithCommas(normalizeQuantity(data['give_quantity'], data['_give_divisible'])), - data['give_asset'], numberWithCommas(normalizeQuantity(data['get_quantity'], data['_get_divisible'])), data['get_asset']); + desc = i18n.t("pend_or_unconf_order", pending, numberWithCommas(normalizeQuantity(data['give_quantity'], data['_give_asset_divisible'])), + data['_give_asset_longname'] || data['give_asset'], numberWithCommas(normalizeQuantity(data['get_quantity'], data['_get_asset_divisible'])), + data['_get_asset_longname'] || data['get_asset']); } else if (category == 'issuances') { if (data['transfer_destination']) { - desc = i18n.t("pend_or_unconf_transfer", pending, data['asset'], getLinkForCPData('address', data['source'], getAddressLabel(data['source'])), + desc = i18n.t("pend_or_unconf_transfer", pending, + asset_longname || data['asset'], + getLinkForCPData('address', data['source'], getAddressLabel(data['source'])), getLinkForCPData('address', data['transfer_destination'], getAddressLabel(data['transfer_destination']))); } else if (data['locked']) { - desc = i18n.t("pend_or_unconf_lock", pending, data['asset']); + desc = i18n.t("pend_or_unconf_lock", pending, asset_longname || data['asset']); } else if (data['quantity'] == 0) { if (assetObj) { - desc = i18n.t("pend_or_unconf_change_desc", pending, data['asset'], data['description']); + desc = i18n.t("pend_or_unconf_change_desc", pending, asset_longname || data['asset'], data['description']); } else { - desc = i18n.t("pend_or_unconf_issuance", pending, data['asset'], numberWithCommas(normalizeQuantity(data['quantity'], data['divisible']))); + desc = i18n.t("pend_or_unconf_issuance", pending, asset_longname || data['asset'], + numberWithCommas(normalizeQuantity(data['quantity'], data['divisible']))); } } else { //See if this is a new issuance or not @@ -50,9 +56,10 @@ PendingActionViewModel.calcText = function(category, data) { if (assetObj) { //the asset exists in our wallet already somewhere, so it's an additional issuance of more units for it desc = i18n.t("pend_or_unconf_issuance_add", pending, numberWithCommas(normalizeQuantity(data['quantity'], data['divisible'])), - data['asset']); + asset_longname || data['asset']); } else { //new issuance - desc = i18n.t("pend_or_unconf_issuance", pending, data['asset'], numberWithCommas(normalizeQuantity(data['quantity'], data['divisible']))); + desc = i18n.t("pend_or_unconf_issuance", pending, asset_longname || data['asset'], + numberWithCommas(normalizeQuantity(data['quantity'], data['divisible']))); } } } else if (category == 'broadcasts') { @@ -66,12 +73,13 @@ PendingActionViewModel.calcText = function(category, data) { var divUnitDivisible; if (WALLET.getAddressObj(data['source'])) { divUnitDivisible = WALLET.getAddressObj(data['source']).getAssetObj(data['dividend_asset']).DIVISIBLE; + divLongname = WALLET.getAddressObj(data['source']).getAssetObj(data['dividend_asset']).ASSET_LONGNAME; desc = i18n.t("pend_or_unconf_dividend_payment", pending, numberWithCommas(normalizeQuantity(data['quantity_per_unit'], divUnitDivisible)), - data['dividend_asset'], data['asset']); + divLongname || data['dividend_asset'], asset_longname || data['asset']); } else { divUnitDivisible = data['dividend_asset_divisible']; desc = i18n.t("pend_or_unconf_dividend_reception", pending, numberWithCommas(normalizeQuantity(data['quantity_per_unit'], divUnitDivisible)), - data['dividend_asset'], data['asset']); + divLongname || data['dividend_asset'], asset_longname || data['asset']); } diff --git a/src/js/components/history.js b/src/js/components/history.js index 10029f9f5..dda16d4fc 100644 --- a/src/js/components/history.js +++ b/src/js/components/history.js @@ -169,18 +169,20 @@ function TransactionHistoryItemViewModel(data) { if (self.RAW_TX_TYPE == 'burns') { desc = i18n.t("hist_burn", normalizeQuantity(self.DATA['burned']), smartFormat(normalizeQuantity(self.DATA['earned']))); } else if (self.RAW_TX_TYPE == 'sends') { - desc = i18n.t("hist_send", smartFormat(normalizeQuantity(self.DATA['quantity'], self.DATA['_divisible'])), self.DATA['asset'], + desc = i18n.t("hist_send", smartFormat(normalizeQuantity(self.DATA['quantity'], + self.DATA['_asset_divisible'])), self.DATA['_asset_longname'] || self.DATA['asset'], getLinkForCPData('address', self.DATA['destination'], getAddressLabel(self.DATA['destination']))); } else if (self.RAW_TX_TYPE == 'orders') { desc = i18n.t("hist_sell", smartFormat(normalizeQuantity(self.DATA['give_quantity'], self.DATA['_give_asset_divisible'])), - self.DATA['give_asset'], smartFormat(normalizeQuantity(self.DATA['get_quantity'], self.DATA['_get_asset_divisible'])), - self.DATA['get_asset']); + self.DATA['_give_asset_longname'] || self.DATA['give_asset'], + smartFormat(normalizeQuantity(self.DATA['get_quantity'], self.DATA['_get_asset_divisible'])), + self.DATA['_get_asset_longname'] || self.DATA['get_asset']); } else if (self.RAW_TX_TYPE == 'order_matches') { desc = i18n.t("hist_order_match", getAddressLabel(self.DATA['tx0_address']), smartFormat(normalizeQuantity(self.DATA['forward_quantity'], self.DATA['_forward_asset_divisible'])), - self.DATA['forward_asset'], getAddressLabel(self.DATA['tx1_address']), + self.DATA['_forward_asset_longname'] || self.DATA['forward_asset'], getAddressLabel(self.DATA['tx1_address']), smartFormat(normalizeQuantity(self.DATA['backward_quantity'], self.DATA['_backward_asset_divisible'])), - self.DATA['backward_asset']); + self.DATA['_backward_asset_longname'] || self.DATA['backward_asset']); if (self.DATA['forward_asset'] == 'BTC' || self.DATA['backward_asset'] == 'BTC') { desc += " (" + i18n.t("pending BTCpay") + ")"; } @@ -188,11 +190,13 @@ function TransactionHistoryItemViewModel(data) { desc = i18n.t("hist_btcpay", smartFormat(normalizeQuantity(self.DATA['btc_amount']))); } else if (self.RAW_TX_TYPE == 'issuances') { if (self.DATA['transfer']) { - desc = i18n.t("hist_transfer", self.DATA['asset'], getLinkForCPData('address', self.DATA['issuer'], getAddressLabel(self.DATA['issuer']))); + desc = i18n.t("hist_transfer", self.DATA['_asset_longname'] || self.DATA['asset'], + getLinkForCPData('address', self.DATA['issuer'], getAddressLabel(self.DATA['issuer']))); } else if (self.DATA['locked']) { - desc = i18n.t("hist_lock", self.DATA['asset']); + desc = i18n.t("hist_lock", self.DATA['_asset_longname'] || self.DATA['asset']); } else { - desc = i18n.t("hist_issuance", smartFormat(normalizeQuantity(self.DATA['quantity'], self.DATA['divisible'])).toString(), self.DATA['asset']); + desc = i18n.t("hist_issuance", smartFormat(normalizeQuantity(self.DATA['quantity'], self.DATA['divisible'])).toString(), + self.DATA['_asset_longname'] || self.DATA['asset']); } } else if (self.RAW_TX_TYPE == 'broadcasts') { desc = i18n.t("hist_broadcast", self.DATA['text'], self.DATA['value']); @@ -209,7 +213,8 @@ function TransactionHistoryItemViewModel(data) { getAddressLabel(self.DATA['tx1_address']), smartFormat(normalizeQuantity(self.DATA['backward_quantity']))); } else if (self.RAW_TX_TYPE == 'dividends') { - desc = i18n.t("hist_dividend", smartFormat(normalizeQuantity(self.DATA['quantity_per_unit'])), self.DATA['dividend_asset'], self.DATA['asset']); + desc = i18n.t("hist_dividend", smartFormat(normalizeQuantity(self.DATA['quantity_per_unit'])), + self.DATA['_dividend_asset_longname'] || self.DATA['dividend_asset'], self.DATA['_asset_longname'] || self.DATA['asset']); } else if (self.RAW_TX_TYPE == 'cancels') { desc = i18n.t("hist_cancellation", data['offer_hash']); } else if (self.RAW_TX_TYPE == 'bet_expirations') { @@ -223,7 +228,7 @@ function TransactionHistoryItemViewModel(data) { } else if (self.RAW_TX_TYPE == 'credits' || self.RAW_TX_TYPE == 'debits') { var tx_type = (self.RAW_TX_TYPE == 'credits' ? i18n.t('hist_credited_with') : i18n.t('hist_debited_for')) desc = i18n.t("hist_credit_debit", getLinkForCPData('address', self.DATA['address'], getAddressLabel(self.DATA['address'])), tx_type, - smartFormat(normalizeQuantity(self.DATA['quantity'], self.DATA['_divisible'])), self.DATA['asset']); + smartFormat(normalizeQuantity(self.DATA['quantity'], self.DATA['_asset_divisible'])), self.DATA['_asset_longname'] || self.DATA['asset']); } else { desc = i18n.t("hist_unknown"); diff --git a/src/js/components/simplebuy.js b/src/js/components/simplebuy.js index 766523a29..eb26f400f 100644 --- a/src/js/components/simplebuy.js +++ b/src/js/components/simplebuy.js @@ -254,7 +254,7 @@ function VendingMachineViewModel() { quantity: denormalizeQuantity(self.quantity()), asset: self.currency(), destination: self.desinationAddress(), - _divisible: true + _asset_divisible: true }; var onSuccess = function(txHash, data, endpoint, addressType, armoryUTx) { diff --git a/src/js/components/wallet.js b/src/js/components/wallet.js index 7d8e79bf4..aab52fa0f 100644 --- a/src/js/components/wallet.js +++ b/src/js/components/wallet.js @@ -626,20 +626,24 @@ function WalletViewModel() { //hacks for passing in some data that should be sent to PENDING_ACTION_FEED.add(), but not the create_ API call // here we only have to worry about what we create a txn for (so not order matches, debits/credits, etc) - var extra1 = null, extra2 = null; + var extra = {}; if (action == 'create_order') { - extra1 = data['_give_divisible']; - delete data['_give_divisible']; - extra2 = data['_get_divisible']; - delete data['_get_divisible']; + extra['_give_asset_divisible'] = data['_give_asset_divisible']; + delete data['_give_asset_divisible']; + extra['_get_asset_divisible'] = data['_get_asset_divisible']; + delete data['_get_asset_divisible']; + extra['_give_asset_longname'] = data['_give_asset_longname']; + delete data['_give_asset_longname']; + extra['_get_asset_longname'] = data['_get_asset_longname']; + delete data['_get_asset_longname']; } else if (action == 'create_cancel') { - extra1 = data['_type']; + extra['_type'] = data['_type']; delete data['_type']; - extra2 = data['_tx_index']; + extra['_tx_index'] = data['_tx_index']; delete data['_tx_index']; } else if (action == 'create_send') { - extra1 = data['_divisible']; - delete data['_divisible']; + extra['asset_divisible'] = data['_asset_divisible']; + delete data['_asset_divisible']; } var verifyDestAddr = data['destination'] || data['transfer_destination'] || data['feed_address'] || data['destBtcPay'] || data['source']; @@ -654,71 +658,91 @@ function WalletViewModel() { verifyDestAddr = [verifyDestAddr]; } - //Do the transaction - multiAPIConsensus(action, data, - function(unsignedTxHex, numTotalEndpoints, numConsensusEndpoints) { - $.jqlog.debug("TXN CREATED. numTotalEndpoints=" - + numTotalEndpoints + ", numConsensusEndpoints=" - + numConsensusEndpoints + ", RAW HEX=" + unsignedTxHex); - - //if the address is an armory wallet, then generate an offline transaction to get signed - if (addressObj.IS_ARMORY_OFFLINE) { - - multiAPIConsensus("create_armory_utx", { - 'unsigned_tx_hex': unsignedTxHex, - 'public_key_hex': addressObj.PUBKEY - }, - function(asciiUTx, numTotalEndpoints, numConsensusEndpoints) { - //DO not add to pending action feed (it will be added automatically via zeroconf when the p2p network sees the tx) - $.jqlog.debug("ARMORY UTX GENERATED: " + asciiUTx); - return onSuccess ? onSuccess(null, data, null, 'armory', asciiUTx) : null; - } - ); - if (action == 'create_cancel') { - $('#btcancel_' + data['offer_hash']).removeClass('disabled'); + //Determine the fee to use + failoverAPI("get_optimal_fee_per_kb", {}, + function(fee_per_kb) { + data['fee_per_kb'] = fee_per_kb['optimal']; + if (data.hasOwnProperty('_fee_option')) { + if (data['_fee_option'] === 'low_priority') { + data['fee_per_kb'] = fee_per_kb['low_priority']; } - return; - - } else if (addressObj.IS_MULTISIG_ADDRESS) { - - self.showTransactionCompleteDialog("" + i18n.t('mutisig_tx_read') + "", null, null, unsignedTxHex); - if (action == 'create_cancel') { - $('#btcancel_' + data['offer_hash']).removeClass('disabled'); + else if (data['_fee_option'] === 'custom') { + assert(data.hasOwnProperty('_custom_fee')); + data['fee_per_kb'] = data['_custom_fee'] * 1024; } - return; + delete data['_fee_option']; + } + if (data.hasOwnProperty('_custom_fee')) { + delete data['_custom_fee']; + } - } else { + //Do the transaction + multiAPIConsensus(action, data, + function(unsignedTxHex, numTotalEndpoints, numConsensusEndpoints) { + $.jqlog.info("TXN CREATED. numTotalEndpoints=" + + numTotalEndpoints + ", numConsensusEndpoints=" + + numConsensusEndpoints + ", FEE=" + data['fee_per_kb'] + ", RAW HEX=" + unsignedTxHex); + + //if the address is an armory wallet, then generate an offline transaction to get signed + if (addressObj.IS_ARMORY_OFFLINE) { + + multiAPIConsensus("create_armory_utx", { + 'unsigned_tx_hex': unsignedTxHex, + 'public_key_hex': addressObj.PUBKEY + }, + function(asciiUTx, numTotalEndpoints, numConsensusEndpoints) { + //DO not add to pending action feed (it will be added automatically via zeroconf when the p2p network sees the tx) + $.jqlog.info("ARMORY UTX GENERATED: " + asciiUTx); + return onSuccess ? onSuccess(null, data, null, 'armory', asciiUTx) : null; + } + ); + if (action == 'create_cancel') { + $('#btcancel_' + data['offer_hash']).removeClass('disabled'); + } + return; - WALLET.signAndBroadcastTx(address, unsignedTxHex, function(txHash, endpoint) { - //register this as a pending transaction - var category = action.replace('create_', '') + 's'; //hack - if (data['source'] === undefined) data['source'] = address; - if (action == 'create_order') { - data['_give_divisible'] = extra1; - data['_get_divisible'] = extra2; - } else if (action == 'create_cancel') { - data['_type'] = extra1; - data['_tx_index'] = extra2; - } else if (action == 'create_send') { - data['_divisible'] = extra1; - } - PENDING_ACTION_FEED.add(txHash, category, data); + } else if (addressObj.IS_MULTISIG_ADDRESS) { - if (action == 'create_cancel') { - $('#btcancel_' + data['offer_hash']).addClass('disabled'); - self.cancelOrders.push(data['offer_hash']); - localStorage.setObject("cancelOrders", self.cancelOrders); - } + self.showTransactionCompleteDialog("" + i18n.t('mutisig_tx_read') + "", null, null, unsignedTxHex); + if (action == 'create_cancel') { + $('#btcancel_' + data['offer_hash']).removeClass('disabled'); + } + return; + + } else { + + WALLET.signAndBroadcastTx(address, unsignedTxHex, function(txHash, endpoint) { + //register this as a pending transaction + var category = action.replace('create_', '') + 's'; //hack + if (data['source'] === undefined) data['source'] = address; + if (action == 'create_order') { + data['_give_asset_divisible'] = extra['_give_asset_divisible']; + data['_get_asset_divisible'] = extra['_get_asset_divisible']; + data['_give_asset_longname'] = extra['_give_asset_longname']; + data['_get_asset_longname'] = extra['_get_asset_longname']; + } else if (action == 'create_cancel') { + data['_type'] = extra['_type']; + data['_tx_index'] = extra['_tx_index']; + } else if (action == 'create_send') { + data['_asset_divisible'] = extra['_asset_divisible']; + } + PENDING_ACTION_FEED.add(txHash, category, data); - return onSuccess ? onSuccess(txHash, data, endpoint, 'normal', null) : null; - }, function(jqXHR, textStatus, errorThrown) { - if (action == 'create_cancel') { - $('#btcancel_' + data['offer_hash']).removeClass('disabled'); - } - onError(jqXHR, textStatus, errorThrown); - }, verifyDestAddr); - } + if (action == 'create_cancel') { + $('#btcancel_' + data['offer_hash']).addClass('disabled'); + self.cancelOrders.push(data['offer_hash']); + localStorage.setObject("cancelOrders", self.cancelOrders); + } + return onSuccess ? onSuccess(txHash, data, endpoint, 'normal', null) : null; + }, function(jqXHR, textStatus, errorThrown) { + if (action == 'create_cancel') { + $('#btcancel_' + data['offer_hash']).removeClass('disabled'); + } + onError(jqXHR, textStatus, errorThrown); + }, verifyDestAddr); + } + }); }); } diff --git a/src/js/consts.js b/src/js/consts.js index e5a38c17f..05158bba4 100644 --- a/src/js/consts.js +++ b/src/js/consts.js @@ -46,6 +46,8 @@ var COUNTERWALLET_CONF_LOCATION = "/counterwallet.conf.json"; var NUMERIC_ASSET_ID_MIN = bigInt(26).pow(12).add(1); var NUMERIC_ASSET_ID_MAX = bigInt(256).pow(8); +var SUBASSET_MAX_DISP_LENGTH = 20; + var IS_MOBILE_OR_TABLET = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); var MAX_INT = Math.pow(2, 63) - 1; var UNIT = 100000000; //# satoshis in whole @@ -54,6 +56,7 @@ var REGULAR_DUST_SIZE = 5430; var MULTISIG_DUST_SIZE = 7800; var MIN_BALANCE_FOR_ACTION = 50000; //in satoshis ... == .0005 var ASSET_CREATION_FEE_XCP = 0.5; //in normalized XCP +var SUBASSET_CREATION_FEE_XCP = 0.25; //in normalized XCP var DIVIDEND_FEE_PER_HOLDER = 0.0002 var MAX_ASSET_DESC_LENGTH = 41; //42, minus a null term character? var FEE_FRACTION_REQUIRED_DEFAULT_PCT = .9; //0.90% of total order @@ -238,7 +241,7 @@ var USE_TESTNET = ( (((location.pathname == "/" || location.pathname == "/src/ || location.hostname.indexOf('testnet') != -1) ? true : false ); -var BLOCKEXPLORER_URL = USE_TESTNET ? "http://testnet.counterpartychain.io" : "https://counterpartychain.io"; +var BLOCKEXPLORER_URL = USE_TESTNET ? "https://testnet.xchain.io" : "https://xchain.io"; var GOOGLE_ANALYTICS_UAID = null; //will be set in counterwallet.js var ROLLBAR_ACCESS_TOKEN = null; //will be set in counterwallet.js diff --git a/src/js/locale.js b/src/js/locale.js index ba8a826fd..a64f0c961 100644 --- a/src/js/locale.js +++ b/src/js/locale.js @@ -50,7 +50,7 @@ function localeInit(callback) { lng: LANG, fallbackLng: DEFAULT_LANG, lngWhitelist: AVAILABLE_LANGUAGES, - resGetPath: '/locales/__lng__/__ns__.json', + resGetPath: (_.startsWith(location.pathname, "/src") ? '/src': '') + '/locales/__lng__/__ns__.json', shorcutFunction: 'sprintf' } i18n.init(options, function() { diff --git a/src/js/messagefeed.js b/src/js/messagefeed.js index f565a930c..2c854579f 100644 --- a/src/js/messagefeed.js +++ b/src/js/messagefeed.js @@ -88,7 +88,9 @@ function MessageFeed() { } if (displayTx) { - var asset1 = message['bindings']['asset'] || 'BTC'; + PENDING_ACTION_FEED.add(txHash, category, message['bindings']); + + /*var asset1 = message['bindings']['asset'] || 'BTC'; WALLET.getAssetsDivisibility([asset1], function(divisibility) { message['bindings']['divisible'] = divisibility[asset1]; @@ -106,13 +108,13 @@ function MessageFeed() { PENDING_ACTION_FEED.add(txHash, category, message['bindings']); } - }); + });*/ } } self.parseMessage = function(seq, when, message) { - if (!message || (message.substring && message.startswith(""))) return; + if (!message || (message.substring && _.startsWith(message, ""))) return; //^ sometimes nginx can trigger this via its proxy handling it seems, with a blank payload (or a html 502 Bad Gateway // payload) -- especially if the backend server reloads. Just ignore it. // Also, a message may be sent over as None if it is a stub message conterblock uses to initialize the sequence count @@ -231,7 +233,7 @@ function MessageFeed() { } else if (category == "dividends") { } else if (category == "issuances") { - + //the 'asset' field is == asset_longname for subassets var addressesWithAsset = WALLET.getAddressesWithAsset(message['asset']); for (var i = 0; i < addressesWithAsset.length; i++) { WALLET.getAddressObj(addressesWithAsset[i]).addOrUpdateAsset(message['asset'], message, null); diff --git a/src/js/smartadmin.app.js b/src/js/smartadmin.app.js index d30a0e72b..011e9599b 100644 --- a/src/js/smartadmin.app.js +++ b/src/js/smartadmin.app.js @@ -1364,7 +1364,12 @@ function pageSetUp() { // is desktop // activate tooltips - $("[rel=tooltip]").tooltip(); + setTimeout(function() { + if ($("[rel=tooltip]").length) { + $("[rel=tooltip]").tooltip(); + } + }, 700); + //$("[rel=tooltip]").tooltip(); // activate popovers $("[rel=popover]").popover(); diff --git a/src/js/util.api.js b/src/js/util.api.js index 5c7df912d..27abc58f7 100644 --- a/src/js/util.api.js +++ b/src/js/util.api.js @@ -204,7 +204,7 @@ function _getDestTypeFromMethod(method) { 'get_users_pairs', 'get_market_orders', 'get_market_trades', 'get_markets_list', 'get_market_details', 'get_pubkey_for_address', 'create_armory_utx', 'convert_armory_signedtx_to_raw_hex', 'create_support_case', 'get_escrowed_balances', 'proxy_to_autobtcescrow', 'get_vennd_machine', 'get_script_pub_key', 'get_assets_info', 'broadcast_tx', - 'get_latest_wallet_messages'].indexOf(method) >= 0) { + 'get_latest_wallet_messages', 'get_optimal_fee_per_kb', 'get_assets_names_and_longnames'].indexOf(method) >= 0) { destType = "counterblockd"; } return destType; diff --git a/src/js/util.js b/src/js/util.js index 98f759de3..d8f090d64 100644 --- a/src/js/util.js +++ b/src/js/util.js @@ -186,7 +186,7 @@ function getLinkForCPData(type, dataID, dataTitle, htmlize) { if (typeof(type) === 'undefined') type = 'tx'; var url = null; if (type == 'address') { //dataID is an address - url = "http://" + (USE_TESTNET ? 'testnet.' : '') + "blockscan.com/address/" + dataID; + url = "https://" + (USE_TESTNET ? 'testnet.' : '') + "xchain.io/address/" + dataID; //format multisig addresses if (dataTitle.indexOf("_") > -1) { var parts = dataTitle.split('_'); @@ -196,10 +196,8 @@ function getLinkForCPData(type, dataID, dataTitle, htmlize) { parts.pop(); dataTitle += " (" + parts.join(', ') + ")"; } - } else if (type == 'order') { //txID is an order ID - url = "http://" + (USE_TESTNET ? 'testnet.' : '') + "blockscan.com/orderinfo/" + dataID; } else if (type == 'tx') { //generic TX - url = "http://" + (USE_TESTNET ? 'testnet.' : '') + "blockscan.com/txInfo/" + dataID; + url = "https://" + (USE_TESTNET ? 'testnet.' : '') + "xchain.io/tx/" + dataID; } else { assert(false, "Unknown type of " + type); } diff --git a/src/js/util.knockout.js b/src/js/util.knockout.js index 712dfafcd..f09f166c6 100644 --- a/src/js/util.knockout.js +++ b/src/js/util.knockout.js @@ -181,6 +181,14 @@ function createSharedKnockoutValidators() { message: i18n.t('must_be_url') }; + ko.validation.rules['isValidCustomFeeIfSpecified'] = { + validator: function(val, self) { + if (!val) return true; //the "if specified" part of the name + return val.toString().match(/^[0-9]+$/) && parseFloat(val) > 0 && parseFloat(val) <= 1000; + }, + message: i18n.t('must_be_valid_custom_fee') + }; + ko.validation.rules['isValidUrlOrValidBitcoinAdressOrJsonBet'] = { validator: function(val, self) { if (!val) return false; @@ -227,45 +235,24 @@ function createSharedKnockoutValidators() { } }; - ko.validation.rules['assetNameIsTaken'] = { - async: true, - message: i18n.t('token_already_exists'), - validator: function(val, self, callback) { - failoverAPI("get_issuances", - {'filters': {'field': 'asset', 'op': '==', 'value': val}, 'status': 'valid'}, - function(data, endpoint) { - return data.length ? callback(false) : callback(true) //empty list -> true (valid = true) - } - ); - } - }; - - // TODO: DRY!! - ko.validation.rules['assetNameExists'] = { - async: true, - message: i18n.t('token_dont_exists'), - validator: function(val, self, callback) { - failoverAPI("get_issuances", {'filters': {'field': 'asset', 'op': '==', 'value': val}, 'status': 'valid'}, - function(data, endpoint) { - $.jqlog.debug("Asset exists: " + data.length); - return data.length ? callback(true) : callback(false) //empty list -> false (valid = false) - } - ); - } - }; - ko.validation.rules['isValidAssetName'] = { validator: function(val, self) { if (self.tokenNameType() == 'alphabetic') { var patt = new RegExp("^[B-Z][A-Z]{3,11}$"); return patt.test(val); + } else if (self.tokenNameType() == 'subasset') { + if(_.startsWith(val, '.') || _.endsWith(val, '.') || val.includes('..')) { + return false; + } + var patt = new RegExp("^[A-Za-z0-9.\\-_@!]{1,250}$"); + return patt.test(val); } else if (self.tokenNameType() == 'numeric') { var patt = new RegExp("^A[0-9]{17,}$"); if (patt.test(val)) { var id = bigInt(val.substr(1)); return id.geq(NUMERIC_ASSET_ID_MIN) && id.leq(NUMERIC_ASSET_ID_MAX); } else { - return false + return false; } } }, @@ -366,19 +353,45 @@ ko.bindingHandlers.fadeVisibleInOnly = { } }; -/*ko.bindingHandlers.fadeVisibleInOnlyKeepLayout = { - init: function(element, valueAccessor) { - // Initially set the element to be instantly visible/hidden depending on the value - var value = valueAccessor(); - $(element).toggle(ko.unwrap(value)); // Use "unwrapObservable" so we can handle values that may or may not be observable - }, - update: function(element, valueAccessor) { - // Whenever the value subsequently changes, slowly fade the element in or out - var value = valueAccessor(); - ko.unwrap(value) ? $(element).animate({opacity:1}) : $(element).show().css({opacity:0}); - //ko.unwrap(value) ? $(element).animate({opacity:100}) : $(element).show().animate({opacity:0}); +ko.bindingHandlers.attrIf = { + update: function (element, valueAccessor, allBindingsAccessor) { + var h = ko.utils.unwrapObservable(valueAccessor()); + var show = ko.utils.unwrapObservable(h._if); + if (show) { + ko.bindingHandlers.attr.update(element, valueAccessor, allBindingsAccessor); + } else { + for (var k in h) { + if (h.hasOwnProperty(k) && k.indexOf("_") !== 0) { + $(element).removeAttr(k); + } + } + } + } +}; + +ko.bindingHandlers.truncatedText = { + update: function (element, valueAccessor, allBindingsAccessor) { + var originalText = ko.utils.unwrapObservable(valueAccessor()), + // 10 is a default maximum length + length = ko.utils.unwrapObservable(allBindingsAccessor().maxTextLength) || 20, + truncatedText = originalText.length > length ? originalText.substring(0, length) + "..." : originalText; + // updating text binding handler to show truncatedText + ko.bindingHandlers.text.update(element, function () { + return truncatedText; + }); + } +}; + +ko.bindingHandlers['visibleInline'] = { + 'update': function (element, valueAccessor) { + var value = ko.utils.unwrapObservable(valueAccessor()); + var isCurrentlyVisible = !(element.style.display == "none"); + if (value && !isCurrentlyVisible) + element.style.display = "inline"; + else if ((!value) && isCurrentlyVisible) + element.style.display = "none"; } -};*/ +}; ko.bindingHandlers.fadeInText = { 'update': function(element, valueAccessor) { diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 1b6ff8ed8..3bd8a11bf 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -119,7 +119,7 @@ "having_issue": "Having Issues?", "page_common_questions": "Common Questions", "try_the_following": "If you are having issues using Counterwallet, please try the following:", - "support_2": "Check out our forums: You can post your question or issue in the Counterparty support forums.", + "support_2": "Check out our forums: You can post your question or issue in the Counterparty support forums.", "support_3": "Send a private message to our support team on the forums: If all else fails, you may send a private message to our support tem describing your issue. For more information see our support thread.", "answers_to_common_questions": "Answers to common questions. Just click the one you're interested in:", "answer_1": "I logged in and my address is different, and I have no balance! Help!", @@ -174,7 +174,7 @@ "will_be_sent": "Your funds will be sent.", "were_sent": "Funds sent.", "cannot_send_server_unavailable": "Cannot send BTC right now, as we cannot currently get in touch with the server to get your balance. Please try again later.", - "bal": "bal:", + "bal": "bal", "not_valid_testnet_pk": "Not a valid TESTNET private key", "not_valid_pk": "Not a valid private key", "not_able_to_sweep": "Counterwallet cannot sweep all of the tokens you selected. Please send %s BTC to address %s and try again. OR use the following fields to pay fees with another address", @@ -216,6 +216,7 @@ "asset_name_invalid": "Asset name invalid", "token_desc_too_long": "Token description is more than %s bytes long.", "token_name_rules": "Must contain between 4 and 12 uppercase letters only (A-Z), and cannot start with 'A'.", + "token_subname_rules": "Subasset name must be < 255 characters, may contain A-Z, a-z, 0-9, or characters .-_@! and must not end with a period", "free_token_name_rules": "Must start with 'A', followed by a large random number.", "issuance_quantity_too_high": "The quantity desired to be issued for this token is too high.", "token_will_be_created": "Your token %s will be created.", @@ -618,7 +619,9 @@ "make_divisible": "Make divisible?", "make_divisible_note": "Divisible tokens can be subdivided into decimal places. If you are unsure, keep this option checked.", "token_name": "Token Name", - "token_name_note": "The token's trading symbol.", + "parent_asset_name": "Parent Asset", + "parent_asset_name_note": "Select the parent asset for this subasset", + "token_name_note": "The token's name, or the subasset name (if a subasset, do not include the parent asset name).", "description_note": "Optional text that provides more information about the token.", "quantity": "Quantity", "quantity_note": "How many of the units of the token to issue (can be increased later).", @@ -654,7 +657,8 @@ "change_token_description_p2": "Enter a new description for your token", "token_an_image": "Want to give your token an image, enhanced description text, or more? See this link.", "token_name_type": "Name type", - "alphabetic_name": "Alphabetic name with anti-spam fee (0.5 XCP)", + "alphabetic_name": "Alphabetic name with anti-spam fee (0.5 XCP)", + "alphabetic_sub_name": "Subasset with anti-spam fee (0.25 XCP)", "numeric_name": "Free numeric name", "burn_btc_testnet": "Burn BTC for XCP (Testnet Only)", "btc_to_burn": "BTC to Burn", @@ -678,8 +682,6 @@ "broadcast_interval": "Broadcast interval", "url": "URL", "fee": "Fee", - "miners_fee": "Miners fee", - "redeemable_fee": "Reedemable fee", "owner": "Owner", "select_feed": "Select feed", "enter_bet": "Enter bet", @@ -811,6 +813,7 @@ "must_be_positive_integer": "Must be a positive integer", "must_be_url": "This field must be a valid url", "must_be_url_or_address": "This field must be a valid url or a valid Bitcoin address", + "must_be_valid_custom_fee": "This field must be a valid fee (in satoshi per byte)", "first_address": "First Address", "second_address": "Second Address", @@ -821,5 +824,11 @@ "pubkey": "PubKey", "for_the_address": "For the address %s", "enter_pubkey": "Enter PubKey", - "pubkey_not_match": "PubKey do not match with the address" + "pubkey_not_match": "PubKey do not match with the address", + + "fee_option": "Bitcoin Fee", + "fee_option_optimal": "Normal Priority", + "fee_option_low_priority": "Low Priority (Cheaper)", + "fee_option_custom": "Custom Fee", + "enter_fee_option_custom": "Fee in Satoshi/Byte" } diff --git a/src/pages/balances.html b/src/pages/balances.html index 6d058661e..67e31cc20 100644 --- a/src/pages/balances.html +++ b/src/pages/balances.html @@ -138,7 +138,9 @@

  • -

    + +

    +

    @@ -318,7 +320,7 @@

    + +
    + +
    + +
    +
    + + +
    +
    + -

    +

    + +