diff --git a/README.md b/README.md index 8ef53890c4..888a2952d4 100644 --- a/README.md +++ b/README.md @@ -88,22 +88,21 @@ Clone this repository, run `docker-compose up` and visit localhost:8080 ## Contributing -Please work off the staging branch and make pull requests to that branch. The master branch will only be updated for new releases. +Please work off the develop branch and make pull requests to that branch. The master branch will only be updated for new releases. -The Bitshares UI team is supported by this [worker proposal](http://www.bitshares.foundation/workers/2018-02-bitshares-ui). It provides the funds needed to pay the coordinator and the bounties and the Bitshares Foundation. +The Bitshares UI team is supported by this [worker](https://www.bitshares.foundation/workers/2018-08-bitshares-ui). It provides the funds needed to pay the coordinator and the bounties and the Bitshares Foundation. If you would like to get involved, we have a [Telegram chatroom](https://t.me/BitSharesDEX) where you can ask questions and get help. You may also join [BitShares on Discord](https://discord.gg/GsjQfAJ) -- Coordinator: Bill Butler, @billbutler +- Project Manager: Magnus Anderson, @startail +- Issue and Funds Coordinator: Bill Butler, @billbutler - Lead Developer: Sigve Kvalsvik, @sigvek -- Developer: Calvin Froedge, @calvin -- Code Review: Fabian Schuh, @xeroc ## Development process - New issues will, after enough discussion and clarification, be assigned an estimate time to complete, as well as assigned to the next unstarted milestone, by a project coordinator. - Milestones are numbered YYMMDD and refer to the **anticipated release date of the next Release Candidate**. -- Release Candidates sits 1 milestone period (2 weeks) for evaluation by the public before release +- Release Candidates sits 1-2 weeks for evaluation by the public before release - Bugs are always worked before enhancements - Developers should work each issue according to a numbered branch corresponding to the issue `git checkout -b 123` - We pay **bounties** for issues that have been estimated. An estimated issue is prefixed with a number in brackets like this: `[2] An nasty bug`. In this example, the bug is valued at two hours ($125 per hour). If you fix this issue according to these guidelines and your PR is accepted, this will earn you $250 bitUSD. You must have a Bitshares wallet and a Bitshares account to receive payment. diff --git a/app/api/apiConfig.js b/app/api/apiConfig.js index 3e6351de40..b330ddb5a6 100644 --- a/app/api/apiConfig.js +++ b/app/api/apiConfig.js @@ -45,6 +45,16 @@ export const widechainAPIs = { DEPOSIT_HISTORY: "/latelyRecharge" }; +export const citadelAPIs = { + BASE: "https://citadel.li/trade", + COINS_LIST: "/coins", + ACTIVE_WALLETS: "/active-wallets", + TRADING_PAIRS: "/trading-pairs", + DEPOSIT_LIMIT: "/deposit-limits", + ESTIMATE_OUTPUT: "/estimate-output-amount", + ESTIMATE_INPUT: "/estimate-input-amount" +}; + export const gdex2APIs = { BASE: "https://api.gdex.io/adjust", COINS_LIST: "/coins", @@ -137,12 +147,6 @@ export const settingsAPIs = { operator: "Witness: openledger-dc", contact: "telegram:mtopenledger" }, - { - url: "wss://bitshares.nu/ws", - location: "Stockholm", - region: "Northern Europe", - country: "Sweden" - }, { url: "wss://bit.btsabc.org/ws", region: "Eastern Asia", @@ -161,9 +165,138 @@ export const settingsAPIs = { }, { url: "wss://japan.bitshares.apasia.tech/ws", + location: "Tokyo", country: "Japan", region: "Southeastern Asia", - operator: "APAsia", + operator: "Flash Infrastructure Worker", + contact: "telegram:murda_ra" + }, + { + url: "wss://status200.bitshares.apasia.tech/ws", + location: "New Jersey", + country: "U.S.A.", + region: "Central America", + operator: "Flash Infrastructure Worker", + contact: "telegram:murda_ra" + }, + { + url: "wss://new-york.bitshares.apasia.tech/ws", + location: "New York", + country: "U.S.A.", + region: "Central America", + operator: "Flash Infrastructure Worker", + contact: "telegram:murda_ra" + }, + { + url: "wss://dallas.bitshares.apasia.tech/ws", + location: "Dallas", + country: "U.S.A.", + region: "Central America", + operator: "Flash Infrastructure Worker", + contact: "telegram:murda_ra" + }, + { + url: "wss://chicago.bitshares.apasia.tech/ws", + location: "Chicago", + country: "U.S.A.", + region: "Central America", + operator: "Flash Infrastructure Worker", + contact: "telegram:murda_ra" + }, + { + url: "wss://atlanta.bitshares.apasia.tech/ws", + location: "Atlanta", + country: "U.S.A.", + region: "Central America", + operator: "Flash Infrastructure Worker", + contact: "telegram:murda_ra" + }, + { + url: "wss://us-la.bitshares.apasia.tech/ws", + location: "Los Angeles", + country: "U.S.A.", + region: "Central America", + operator: "Flash Infrastructure Worker", + contact: "telegram:murda_ra" + }, + { + url: "wss://seattle.bitshares.apasia.tech/ws", + location: "Seattle", + country: "U.S.A.", + region: "Central America", + operator: "Flash Infrastructure Worker", + contact: "telegram:murda_ra" + }, + { + url: "wss://miami.bitshares.apasia.tech/ws", + location: "Miami", + country: "U.S.A.", + region: "Central America", + operator: "Flash Infrastructure Worker", + contact: "telegram:murda_ra" + }, + { + url: "wss://valley.bitshares.apasia.tech/ws", + location: "Silicone Valley", + country: "U.S.A.", + region: "Central America", + operator: "Flash Infrastructure Worker", + contact: "telegram:murda_ra" + }, + { + url: "wss://canada6.daostreet.com", + location: "Toronto", + country: "Canada", + region: "Northern America", + operator: "Flash Infrastructure Worker", + contact: "telegram:murda_ra" + }, + { + url: "wss://bitshares.nu/ws", + location: "Stockholm", + region: "Northern Europe", + country: "Sweden", + operator: "Flash Infrastructure Worker", + contact: "telegram:StaflunD" + }, + { + url: "wss://api.open-asset.tech/ws", + location: "Frankfurt", + region: "Western Europe", + country: "Germany", + operator: "Flash Infrastructure Worker", + contact: "telegram:StaflunD" + }, + { + url: "wss://france.bitshares.apasia.tech/ws", + location: "Paris", + country: "France", + region: "Western Europe", + operator: "Flash Infrastructure Worker", + contact: "telegram:murda_ra" + }, + { + url: "wss://england.bitshares.apasia.tech/ws", + location: "London", + country: "England", + region: "Northern Europe", + operator: "Flash Infrastructure Worker", + contact: "telegram:murda_ra" + }, + { + url: "wss://netherlands.bitshares.apasia.tech/ws", + location: "Amsterdam", + country: "Netherlands", + region: "Northern Europe", + operator: "Flash Infrastructure Worker", + contact: "telegram:murda_ra" + }, + { + url: "wss://australia.bitshares.apasia.tech/ws", + location: "Sidney", + country: "Australia", + region: "Australia", + operator: "Flash Infrastructure Worker", contact: "telegram:murda_ra" }, { @@ -184,8 +317,11 @@ export const settingsAPIs = { }, { url: "wss://dex.rnglab.org", - location: "Netherlands", - operator: "Witness: rnglab" + region: "Northern Europe", + country: "Netherlands", + location: "Amsterdam", + operator: "Witness: rnglab", + contact: "keybase:rnglab" }, { url: "wss://la.dexnode.net/ws", @@ -211,7 +347,6 @@ export const settingsAPIs = { operator: "Witness: xeldal", contact: "telegram:xeldal" }, - {url: "wss://btsza.co.za:8091/ws", location: "Cape Town, South Africa"}, { url: "wss://api.bts.blckchnd.com", region: "Western Europe", @@ -301,9 +436,11 @@ export const settingsAPIs = { }, { url: "wss://api.btsxchng.com", - location: - "Global (Asia Pacific (Singapore) / US East (N. Virginia) / EU (London))", - operator: "Witness: elmato" + region: "Multiple", + country: "Worldwide", + location: "Singapore / N. Virginia / London", + operator: "Witness: elmato", + contact: "telegram:elmato" }, { url: "wss://api.bts.network/", @@ -353,8 +490,11 @@ export const settingsAPIs = { }, { url: "wss://bts.proxyhosts.info/wss", - location: "Germany", - operator: "Witness: verbaltech2" + region: "Western Europe", + country: "Germany", + location: "", + operator: "Witness: verbaltech2", + contact: "keybase:jgaltman" }, { url: "wss://bts.open.icowallet.net/ws", @@ -364,6 +504,22 @@ export const settingsAPIs = { operator: "Witness: magicwallet.witness", contact: "telegram:plus_wave" }, + { + url: "wss://de.bts.dcn.cx/ws", + region: "Western Europe", + country: "Germany", + location: "Nuremberg", + operator: "Witness: fla01", + contact: "telegram:Otherego;telegram:BarefootMouse" + }, + { + url: "wss://fi.bts.dcn.cx/ws", + region: "Northern Europe", + country: "Finland", + location: "Helsinki", + operator: "Witness: fla01", + contact: "telegram:Otherego;telegram:BarefootMouse" + }, { url: "wss://crazybit.online", region: "Asia", @@ -444,6 +600,14 @@ export const settingsAPIs = { operator: "Witness: zapata42-witness", contact: "telegram:Zapata_42" }, + { + url: "wss://citadel.li/node", + region: "Western Europe", + country: "Iceland", + location: "Reykjavik", + operator: "CITADEL", + contact: "email:citadel.li;support" + }, // Testnet { url: "wss://node.testnet.bitshares.eu", @@ -466,7 +630,15 @@ export const settingsAPIs = { region: "TESTNET - Northern America", country: "U.S.A.", location: "Dallas", - operator: "APAsia", + operator: "Flash Infrastructure Worker", + contact: "telegram:murda_ra" + }, + { + url: "wss://testnet-eu.bitshares.apasia.tech/ws", + region: "TESTNET - Northern Europe", + country: "Netherlands", + location: "Amsterdam", + operator: "Flash Infrastructure Worker", contact: "telegram:murda_ra" }, { @@ -476,6 +648,14 @@ export const settingsAPIs = { location: "Paris", operator: "Witness: zapata42-witness", contact: "telegram:Zapata_42" + }, + { + url: "wss://testnet.bts.dcn.cx/ws", + region: "TESTNET - Europe", + country: "Germany / Finland", + location: "Nurenberg / Helsinki", + operator: "Witness: fla-test", + contact: "telegram:Otherego;telegram:BarefootMouse" } ], DEFAULT_FAUCET: getFaucet().url, diff --git a/app/assets/locales/locale-de.json b/app/assets/locales/locale-de.json index c4bbaa3596..2b98baac96 100644 --- a/app/assets/locales/locale-de.json +++ b/app/assets/locales/locale-de.json @@ -799,6 +799,13 @@ "calc": "Calculating", "choose_deposit": "Please select the coin you would like to deposit", "choose_withdraw": "Please select the coin you would like to withdraw", + "citadel": { + "coming_soon": "Coming soon", + "min_amount": "Minimum amount: %(minAmount)s %(symbol)s", + "min_amount_error": "Please enter number >= minimum", + "support_block": "For support, please contact citadel.li at:", + "under_construction": "Under Construction" + }, "contact_TRADE": "Contact Blocktrades", "convert": "Internal conversion", "convert_coin": "Convert to %(coin)s (%(symbol)s)", diff --git a/app/assets/locales/locale-en.json b/app/assets/locales/locale-en.json index a1dc48c16f..d18a30846e 100644 --- a/app/assets/locales/locale-en.json +++ b/app/assets/locales/locale-en.json @@ -253,7 +253,9 @@ "force_settlement_offset_percent": "Percent offset of forced settlements", "global_settle": "Allow issuer to force a global settling", "id": "ID", + "invalid_backing_asset_change": "You can't change the backing asset of an asset that has a non-zero current supply", "invalid_market_pair": "Preferred market pairing can not be the same market", + "invalid_permissions_change": "You can't enable a permission for an asset that has a non-zero current supply", "issued_assets": "Issued Assets", "market": "Preferred market pairing", "market_fee": "Market fee", @@ -390,6 +392,10 @@ "understand": "I understand", "ws_status": "Full node connection status" }, + "boolean": { + "true": "True", + "false": "False" + }, "borrow": { "adjust": "Update position", "adjust_short": "Adjust", @@ -461,6 +467,7 @@ "chart_height": "Chart height (pixels)", "chart_modal": "Chart options", "chart_type": "Chart type", + "checking_for_worth_less_settlement": "Checking if you can get a better price by selling to market, please wait..", "confirm_buy": "Your order is %(diff)s times higher than the lowest ask, are you sure?", "confirm_no_orders_buy": "You are placing a buy order in a market with no open sell orders. Are you certain you wish to proceed?", "confirm_no_orders_sell": "You are placing a sell order in a market with no open buy orders. Are you certain you wish to proceed?", @@ -553,6 +560,7 @@ "vol_short": "Vol", "volume": "Volume", "volume_24": "24hr Volume", + "worth_less_settlement_warning": "Warning! You can get a better price by selling to {market_link}.", "your_price": "Your Call Price", "zoom": "Zoom", "zoom_all": "All" @@ -801,6 +809,13 @@ "calc": "Calculating", "choose_deposit": "Please select the coin you would like to deposit", "choose_withdraw": "Please select the coin you would like to withdraw", + "citadel": { + "coming_soon": "Coming soon", + "min_amount": "Minimum amount: %(minAmount)s %(symbol)s", + "min_amount_error": "Please enter number >= minimum", + "support_block": "For support, please contact citadel.li at:", + "under_construction": "Under Construction" + }, "contact_TRADE": "Contact Blocktrades", "convert": "Internal conversion", "convert_coin": "Convert to %(coin)s (%(symbol)s)", @@ -884,6 +899,7 @@ "unavailable_OPEN": "The OpenLedger Gateway is down or not responding", "unavailable_RUDEX": "The RuDEX Gateway is down or not responding", "unavailable_TRADE": "The Blocktrades Bridge is down or not responding", + "unavailable_CITADEL": "The Citadel Bridge is down or not responding", "unavailable_bridge": "The bridge service for this asset is currently down, please try again later", "use_copy_button": "PLEASE USE COPY BUTTON TO MAKE COPY OF ADDRESS OR MEMO ON THIS PAGE", "user_unavailable": "User informaction is current unavailable, please try again later", @@ -1127,6 +1143,11 @@ }, "ok": "OK", "proposals": { + "actions": { + "approve": "Approve proposal", + "reject": "Reject proposal", + "delete": "Permanently reject proposal" + }, "approval_add": "Approval to add", "approval_remove": "Approval to remove", "key_approval_add": "Key approval to add", @@ -1222,9 +1243,9 @@ "no_recent": "No recent transactions", "override_transfer": "{issuer} transferred {amount} from {from} to {to}", "pending": "pending %(blocks)s blocks", - "proposal_create": "{account} created a proposed transaction", - "proposal_delete": "{account} deleted a proposed transaction", - "proposal_update": "{account} updated a proposed transaction", + "proposal_create": "{account} created the proposed transaction {proposal}", + "proposal_delete": "{account} deleted the proposed transaction {proposal}", + "proposal_update": "{account} updated the proposed transaction {proposal}", "publish_feed": "{account} published feed price of {price}", "reg_account": "{registrar} registered the account {new_account}", "set_proxy": "{account} set {proxy} as their voting proxy", @@ -1262,6 +1283,7 @@ "asset_update": "Update the asset {asset} using the account {account}", "call_order_update": "Change {account} {debtSymbol} debt by {debt} and collateral by {collateral}", "committee_member_update_global_parameters": "Update committee global parameters by {account}", + "delete": "Permanently reject", "expires": "Expires", "feed_producer": "Update the feed producers for the asset {asset} using the account {account}", "limit_order_buy": "Place an order to buy {amount} at {price} for {account}", @@ -1280,6 +1302,14 @@ "owner_approvals_to_add": "Owner approvals to add", "owner_approvals_to_remove": "Owner approvals to remove" }, + "updated": { + "active_approvals_to_add": "Active approval(s) added", + "active_approvals_to_remove": "Active approval(s) removed", + "key_approvals_to_add": "Key approval(s) added", + "key_approvals_to_remove": "Key approval(s) removed", + "owner_approvals_to_add": "Owner approval(s) added", + "owner_approvals_to_remove": "Owner approval(s) removed" + }, "update_account": "Update account data for {account}", "vesting_balance_withdraw": "Withdraw {amount} from vesting balance of {account}" }, @@ -1290,6 +1320,9 @@ "proposed_operations": "Proposed operations", "review_period": "Review period begin" }, + "proposal_delete": { + "using_owner_authority": "Using owner authority" + }, "propose": "Propose", "qr_address_scanner": { "address_found": "Address found", diff --git a/app/assets/locales/locale-es.json b/app/assets/locales/locale-es.json index 2e785054c1..06daa629e1 100644 --- a/app/assets/locales/locale-es.json +++ b/app/assets/locales/locale-es.json @@ -799,6 +799,13 @@ "calc": "Calculando", "choose_deposit": "Seleccione la moneda que desea depositar", "choose_withdraw": "Seleccione la moneda que desea retirar", + "citadel": { + "coming_soon": "Coming soon", + "min_amount": "Minimum amount: %(minAmount)s %(symbol)s", + "min_amount_error": "Please enter number >= minimum", + "support_block": "For support, please contact citadel.li at:", + "under_construction": "Under Construction" + }, "contact_TRADE": "Contacta Blocktrades", "convert": "Conversión interna", "convert_coin": "Convertir a %(coin)s (%(symbol)s)", diff --git a/app/assets/locales/locale-fr.json b/app/assets/locales/locale-fr.json index ee43abeae3..d7219f2e27 100644 --- a/app/assets/locales/locale-fr.json +++ b/app/assets/locales/locale-fr.json @@ -799,6 +799,13 @@ "calc": "Calculating", "choose_deposit": "Please select the coin you would like to deposit", "choose_withdraw": "Please select the coin you would like to withdraw", + "citadel": { + "coming_soon": "Coming soon", + "min_amount": "Minimum amount: %(minAmount)s %(symbol)s", + "min_amount_error": "Please enter number >= minimum", + "support_block": "For support, please contact citadel.li at:", + "under_construction": "Under Construction" + }, "contact_TRADE": "Contact Blocktrades", "convert": "Internal conversion", "convert_coin": "Convert to %(coin)s (%(symbol)s)", diff --git a/app/assets/locales/locale-it.json b/app/assets/locales/locale-it.json index 984554444f..92bbccbb38 100644 --- a/app/assets/locales/locale-it.json +++ b/app/assets/locales/locale-it.json @@ -799,8 +799,15 @@ "calc": "Calcolo in corso", "choose_deposit": "Per favore seleziona la valuta che vuoi depositare", "choose_withdraw": "Per favore seleziona la valuta che vuoi prelevare", + "citadel": { + "coming_soon": "Prossimamente", + "min_amount": "Minimum amount: %(minAmount)s %(symbol)s", + "min_amount_error": "Per favore inserisci un numero >= minimo", + "support_block": "Per supporto, contatta citadel.li a:", + "under_construction": "Under Construction" + }, "contact_TRADE": "Contact Blocktrades", - "convert": "Conversione interna", + "convert": "Quantità minima: %(minAmount)s %(symbol)s", "convert_coin": "Converti in %(coin)s (%(symbol)s)", "convert_now": "Converti adesso", "copy_address": "Copy address", diff --git a/app/assets/locales/locale-ja.json b/app/assets/locales/locale-ja.json index 8176c9785d..559f7a926d 100644 --- a/app/assets/locales/locale-ja.json +++ b/app/assets/locales/locale-ja.json @@ -799,6 +799,13 @@ "calc": "計算中", "choose_deposit": "入金したいコインを選択してください", "choose_withdraw": "出金したいコインを選択してください", + "citadel": { + "coming_soon": "近日公開", + "min_amount": "最低数量: %(minAmount)s %(symbol)s", + "min_amount_error": "数字を入力してください >= minimum", + "support_block": "サポートについては、citadel.li までご連絡ください。:", + "under_construction": "工事中" + }, "contact_TRADE": "Blocktradesに連絡する", "convert": "内部変換", "convert_coin": "%(coin)s (%(symbol)s) に変換", diff --git a/app/assets/locales/locale-ko.json b/app/assets/locales/locale-ko.json index 64b186499d..7f423b9367 100644 --- a/app/assets/locales/locale-ko.json +++ b/app/assets/locales/locale-ko.json @@ -799,6 +799,13 @@ "calc": "Calculating", "choose_deposit": "Please select the coin you would like to deposit", "choose_withdraw": "Please select the coin you would like to withdraw", + "citadel": { + "coming_soon": "Coming soon", + "min_amount": "Minimum amount: %(minAmount)s %(symbol)s", + "min_amount_error": "Please enter number >= minimum", + "support_block": "For support, please contact citadel.li at:", + "under_construction": "Under Construction" + }, "contact_TRADE": "Contact Blocktrades", "convert": "Internal conversion", "convert_coin": "Convert to %(coin)s (%(symbol)s)", diff --git a/app/assets/locales/locale-ru.json b/app/assets/locales/locale-ru.json index 7de37e62c5..fccce072f8 100644 --- a/app/assets/locales/locale-ru.json +++ b/app/assets/locales/locale-ru.json @@ -799,6 +799,13 @@ "calc": "Расчет", "choose_deposit": "Пожалуйста, выберите валюту, в которой хотели бы разместить депозит", "choose_withdraw": "Пожалуйста, выберите валюту, которую хотели бы вывести", + "citadel": { + "coming_soon": "Скоро", + "min_amount": "Минимальная сумма: %(minAmount)s %(symbol)s", + "min_amount_error": "Пожалуйста введите число >= минимума", + "support_block": "Для получения поддержки, пожалуйста, свяжитесь с Citadel по адресу:", + "under_construction": "Технические работы" + }, "contact_TRADE": "Связаться с Blocktrades", "convert": "Конвертировать внутри системы", "convert_coin": "Конвертировать в %(coin)s (%(symbol)s)", @@ -866,8 +873,7 @@ "coming_soon": "Скоро", "min_amount": "Минимальная сумма: %(minAmount)s %(symbol)s", "min_amount_error": "Пожалуйста введите число >= минимума", - "support_block": - "Для получения поддержки, пожалуйста, свяжитесь с XBTS по адресу:", + "support_block": "Для получения поддержки, пожалуйста, свяжитесь с XBTS по адресу:", "under_construction": "Технические работы" }, "scan_qr": "Сканировать QR-код", @@ -883,6 +889,7 @@ "unavailable_OPEN": "Шлюз OpenLedger не работает или не отвечает", "unavailable_RUDEX": "Шлюз RuDEX не работает или не отвечает", "unavailable_TRADE": "Шлюз Blocktrades не работает или не отвечает", + "unavailable_CITADEL": "Шлюз Citadel не работает или не отвечает", "unavailable_bridge": "Мост для данного актива в настоящее время не работает, пожалуйста, повторите попытку позже", "use_copy_button": "ПОЖАЛУЙСТА ИСПОЛЬЗУЙТЕ КНОПКУ ДЛЯ КОПИРОВАНИЯ АДРЕСА ИЛИ МЕМО", "user_unavailable": "В настоящий момент информация о пользователе недоступна, повторите попытку позже", diff --git a/app/assets/locales/locale-tr.json b/app/assets/locales/locale-tr.json index 1d7772e014..ac9b049693 100644 --- a/app/assets/locales/locale-tr.json +++ b/app/assets/locales/locale-tr.json @@ -799,6 +799,13 @@ "calc": "Hesaplanıyor...", "choose_deposit": "Lütfen yatırmak istediğiniz dijital varlığı seçin", "choose_withdraw": "Lütfen çekmek istediğiniz dijital varlığı seçin", + "citadel": { + "coming_soon": "Coming soon", + "min_amount": "Minimum amount: %(minAmount)s %(symbol)s", + "min_amount_error": "Please enter number >= minimum", + "support_block": "For support, please contact citadel.li at:", + "under_construction": "Under Construction" + }, "contact_TRADE": "Contact Blocktrades", "convert": "Internal conversion", "convert_coin": "Convert to %(coin)s (%(symbol)s)", diff --git a/app/assets/locales/locale-zh.json b/app/assets/locales/locale-zh.json index c235c918e4..55bb4a90fa 100644 --- a/app/assets/locales/locale-zh.json +++ b/app/assets/locales/locale-zh.json @@ -799,6 +799,13 @@ "calc": "计算中", "choose_deposit": "请选择充值的币种", "choose_withdraw": "请选择提现的币种", + "citadel": { + "coming_soon": "即将上线", + "min_amount": "最小金额:%(minAmount)s %(symbol)s", + "min_amount_error": "请输入大于最小金额的数量", + "support_block": "请联系 citadel.li 获取客户支持:", + "under_construction": "Under Construction" + }, "contact_TRADE": "联系 Blocktrades", "convert": "内部兑换", "convert_coin": "兑换成 %(coin)s (%(symbol)s)", diff --git a/app/assets/stylesheets/themes/_theme-template.scss b/app/assets/stylesheets/themes/_theme-template.scss index 7027fba360..ac4e513682 100644 --- a/app/assets/stylesheets/themes/_theme-template.scss +++ b/app/assets/stylesheets/themes/_theme-template.scss @@ -1920,6 +1920,9 @@ .footer { padding-right: 0; } + .tab-no-background { + background-color: transparent !important; + } .header-container { background-color: $account-background; .menu-dropdown-wrapper { diff --git a/app/components/Account/AccountAssetCreate.jsx b/app/components/Account/AccountAssetCreate.jsx index b2c339b04c..a9a1e6330f 100644 --- a/app/components/Account/AccountAssetCreate.jsx +++ b/app/components/Account/AccountAssetCreate.jsx @@ -42,10 +42,13 @@ class BitAssetOptions extends React.Component { } _onInputBackingAsset(asset) { - this.setState({ - backingAsset: asset.toUpperCase(), - error: null - }); + if (this.props.disableBackingAssetChange) + this.props.disabledBackingAssetChangeCallback(); + else + this.setState({ + backingAsset: asset.toUpperCase(), + error: null + }); } _onFoundBackingAsset(asset) { diff --git a/app/components/Account/AccountAssetUpdate.jsx b/app/components/Account/AccountAssetUpdate.jsx index d24721bb35..c2d760d839 100644 --- a/app/components/Account/AccountAssetUpdate.jsx +++ b/app/components/Account/AccountAssetUpdate.jsx @@ -24,11 +24,19 @@ import AssetFeedProducers from "./AssetFeedProducers"; import BaseModal from "components/Modal/BaseModal"; import ZfApi from "react-foundation-apps/src/utils/foundation-api"; import {withRouter} from "react-router-dom"; +import notify from "actions/NotificationActions"; let GRAPHENE_MAX_SHARE_SUPPLY = new big( assetConstants.GRAPHENE_MAX_SHARE_SUPPLY ); +const disabledBackingAssetChangeCallback = () => + notify.error( + counterpart.translate( + "account.user_issued_assets.invalid_backing_asset_change" + ) + ); + class AccountAssetUpdate extends React.Component { static propTypes = { globalObject: ChainTypes.ChainObject.isRequired @@ -446,6 +454,10 @@ class AccountAssetUpdate extends React.Component { bitasset_opts[value] = e; break; + case "minimum_feeds": + bitasset_opts[value] = parseInt(e.target.value, 10); + break; + default: break; } @@ -682,8 +694,35 @@ class AccountAssetUpdate extends React.Component { this._validateEditFields({}); } + _getCurrentSupply() { + const {asset, getDynamicObject} = this.props; + + return ( + getDynamicObject && + getDynamicObject(asset.get("dynamic_asset_data_id")).get( + "current_supply" + ) + ); + } + _onPermissionChange(key) { - let booleans = this.state.permissionBooleans; + const {isBitAsset, permissionBooleans} = this.state; + + const disabled = !assetUtils.getFlagBooleans( + this.props.asset.getIn(["options", "issuer_permissions"]), + isBitAsset + )[key]; + + if (this._getCurrentSupply() > 0 && disabled) { + notify.error( + counterpart.translate( + "account.user_issued_assets.invalid_permissions_change" + ) + ); + return; + } + + let booleans = permissionBooleans; booleans[key] = !booleans[key]; this.setState({ permissionBooleans: booleans @@ -1303,6 +1342,12 @@ class AccountAssetUpdate extends React.Component { "precision" )} assetSymbol={asset.get("symbol")} + disableBackingAssetChange={ + this._getCurrentSupply() > 0 + } + disabledBackingAssetChangeCallback={ + disabledBackingAssetChangeCallback + } /> {

diff --git a/app/components/Account/AccountDepositWithdraw.jsx b/app/components/Account/AccountDepositWithdraw.jsx index 0390aa877b..6d37ea869e 100644 --- a/app/components/Account/AccountDepositWithdraw.jsx +++ b/app/components/Account/AccountDepositWithdraw.jsx @@ -6,10 +6,12 @@ import utils from "common/utils"; import Translate from "react-translate-component"; import ChainTypes from "../Utility/ChainTypes"; import BindToChainState from "../Utility/BindToChainState"; +import CitadelGateway from "../DepositWithdraw/citadel/CitadelGateway"; import OpenledgerGateway from "../DepositWithdraw/OpenledgerGateway"; import OpenLedgerFiatDepositWithdrawal from "../DepositWithdraw/openledger/OpenLedgerFiatDepositWithdrawal"; import OpenLedgerFiatTransactionHistory from "../DepositWithdraw/openledger/OpenLedgerFiatTransactionHistory"; import BlockTradesBridgeDepositRequest from "../DepositWithdraw/blocktrades/BlockTradesBridgeDepositRequest"; +import CitadelBridgeDepositRequest from "../DepositWithdraw/citadel/CitadelBridgeDepositRequest"; import HelpContent from "../Utility/HelpContent"; import AccountStore from "stores/AccountStore"; import SettingsStore from "stores/SettingsStore"; @@ -41,6 +43,7 @@ class AccountDepositWithdraw extends React.Component { rudexService: props.viewSettings.get("rudexService", "gateway"), xbtsxService: props.viewSettings.get("xbtsxService", "gateway"), btService: props.viewSettings.get("btService", "bridge"), + citadelService: props.viewSettings.get("citadelService", "bridge"), metaService: props.viewSettings.get("metaService", "bridge"), activeService: props.viewSettings.get("activeService", 0) }; @@ -58,10 +61,15 @@ class AccountDepositWithdraw extends React.Component { nextProps.openLedgerBackedCoins, this.props.openLedgerBackedCoins ) || + !utils.are_equal_shallow( + nextProps.citadelBackedCoins, + this.props.citadelBackedCoins + ) || nextState.olService !== this.state.olService || nextState.rudexService !== this.state.rudexService || nextState.xbtsxService !== this.state.xbtsxService || nextState.btService !== this.state.btService || + nextState.citadelService !== this.state.citadelService || nextState.metaService !== this.state.metaService || nextState.activeService !== this.state.activeService ); @@ -111,6 +119,15 @@ class AccountDepositWithdraw extends React.Component { }); } + toggleCitadelService(service) { + this.setState({ + citadelService: service + }); + SettingsActions.changeViewSetting({ + citadelService: service + }); + } + toggleMetaService(service) { this.setState({ metaService: service @@ -140,8 +157,13 @@ class AccountDepositWithdraw extends React.Component { //let services = ["Openledger (OPEN.X)", "BlockTrades (TRADE.X)", "Transwiser", "BitKapital"]; let serList = []; let {account} = this.props; - let {olService, btService, rudexService, xbtsxService} = this.state; - + let { + olService, + btService, + rudexService, + xbtsxService, + citadelService + } = this.state; serList.push({ name: "Openledger (OPEN.X)", template: ( @@ -376,6 +398,51 @@ class AccountDepositWithdraw extends React.Component { ) }); + serList.push({ + name: "Citadel", + template: ( +

+
+ {/*
*/} +
+ +
+ +
+
+
+ ) + }); + serList.push({ name: "BitKapital", template: ( @@ -462,7 +529,8 @@ class AccountDepositWithdraw extends React.Component { "RUDEX", "TRADE", "BITKAPITAL", - "XBTSX" + "XBTSX", + "CITADEL" ]; const currentServiceName = serviceNames[activeService]; const currentServiceDown = servicesDown.get(currentServiceName); @@ -597,6 +665,10 @@ export default connect( "TRADE", [] ), + citadelBackedCoins: GatewayStore.getState().backedCoins.get( + "CITADEL", + [] + ), winexBackedCoins: GatewayStore.getState().backedCoins.get( "WIN", [] diff --git a/app/components/Account/Proposals.jsx b/app/components/Account/Proposals.jsx index 22ff160c1c..9954076c35 100644 --- a/app/components/Account/Proposals.jsx +++ b/app/components/Account/Proposals.jsx @@ -7,9 +7,7 @@ import ProposedOperation, { import BindToChainState from "components/Utility/BindToChainState"; import ChainTypes from "components/Utility/ChainTypes"; import utils from "common/utils"; -import ProposalApproveModal, { - finalRequiredPerms -} from "../Modal/ProposalApproveModal"; +import ProposalModal, {finalRequiredPerms} from "../Modal/ProposalModal"; import NestedApprovalState from "../Account/NestedApprovalState"; import {ChainStore} from "bitsharesjs"; import counterpart from "counterpart"; @@ -242,7 +240,7 @@ class Proposals extends Component { )} - ) : null} - + + ); diff --git a/app/components/Blockchain/Asset.jsx b/app/components/Blockchain/Asset.jsx index c2a2590620..3813145057 100644 --- a/app/components/Blockchain/Asset.jsx +++ b/app/components/Blockchain/Asset.jsx @@ -610,7 +610,7 @@ class Asset extends React.Component { content="explorer.asset.feed_producer_text" /> @@ -1253,18 +1253,21 @@ class Asset extends React.Component { } } -Asset = connect(Asset, { - listenTo() { - return [AccountStore]; - }, - getProps() { - return { - currentAccount: - AccountStore.getState().currentAccount || - AccountStore.getState().passwordAccount - }; +Asset = connect( + Asset, + { + listenTo() { + return [AccountStore]; + }, + getProps() { + return { + currentAccount: + AccountStore.getState().currentAccount || + AccountStore.getState().passwordAccount + }; + } } -}); +); Asset = AssetWrapper(Asset, { propNames: ["backingAsset"] diff --git a/app/components/Blockchain/AssetPublishFeed.jsx b/app/components/Blockchain/AssetPublishFeed.jsx index a966911145..279a1d4042 100644 --- a/app/components/Blockchain/AssetPublishFeed.jsx +++ b/app/components/Blockchain/AssetPublishFeed.jsx @@ -23,7 +23,7 @@ class AssetPublishFeed extends React.Component { resetState(props = this.props) { let publisher_id = props.account.get("id"); - const currentFeed = props.base.getIn(["bitasset", "current_feed"]); + const currentFeed = props.asset.getIn(["bitasset", "current_feed"]); /* Might need to check these default values */ let mcr = currentFeed.get("maintenance_collateral_ratio", 1750); @@ -54,7 +54,7 @@ class AssetPublishFeed extends React.Component { onSubmit() { AssetActions.publishFeed({ publisher: this.state.publisher_id, - asset_id: this.props.base.get("id"), + asset_id: this.props.asset.get("id"), mcr: this.state.mcr, mssr: this.state.mssr, settlementPrice: this.state.settlementPrice, @@ -88,9 +88,16 @@ class AssetPublishFeed extends React.Component { } render() { - const {quote, base} = this.props; + const {asset} = this.props; const {mcrValue, mssrValue, publisher} = this.state; + const base = asset.get("id"); + const quote = asset.getIn([ + "bitasset", + "options", + "short_backing_asset" + ]); + return (
{/* Settlement Price */} @@ -124,8 +131,8 @@ class AssetPublishFeed extends React.Component { "settlementPrice" )} label="explorer.asset.price_feed.settlement_price" - quote={quote.get("id")} - base={base.get("id")} + quote={quote} + base={base} /> {/* MCR */} @@ -179,10 +186,5 @@ class AssetPublishFeed extends React.Component { } AssetPublishFeed = BindToChainState(AssetPublishFeed); -AssetPublishFeed = AssetWrapper(AssetPublishFeed, { - propNames: ["quote", "base"], - defaultProps: { - quote: "1.3.0" - } -}); +AssetPublishFeed = AssetWrapper(AssetPublishFeed); export default AssetPublishFeed; diff --git a/app/components/Blockchain/Operation.jsx b/app/components/Blockchain/Operation.jsx index 67f415d4a4..1bfada4d76 100644 --- a/app/components/Blockchain/Operation.jsx +++ b/app/components/Blockchain/Operation.jsx @@ -26,6 +26,15 @@ require("./operations.scss"); let ops = Object.keys(operations); let listings = account_constants.account_listing; +const ShortObjectId = ({objectId}) => { + if (typeof objectId === "string") { + const parts = objectId.split("."); + const {length} = parts; + if (length > 0) return "#" + parts[length - 1]; + } + return objectId; +}; + class TransactionLabel extends React.Component { shouldComponentUpdate(nextProps) { return ( @@ -663,25 +672,65 @@ class Operation extends React.Component { case "asset_settle": color = "warning"; - column = ( - - - - ); + + const baseAmount = op[1].amount; + const { + result: [resultCode, quoteAmount] + } = this.props; + const instantSettleCode = 2; + + switch (resultCode) { + case instantSettleCode: + column = ( + + + + ); + break; + default: + column = ( + + + + ); + } + break; case "asset_global_settle": @@ -827,6 +876,14 @@ class Operation extends React.Component { type: "account", value: op[1].fee_paying_account, arg: "account" + }, + { + value: ( + + ), + arg: "proposal" } ]} /> @@ -853,15 +910,86 @@ class Operation extends React.Component { break; case "proposal_update": + const fields = [ + "active_approvals_to_add", + "active_approvals_to_remove", + "owner_approvals_to_add", + "owner_approvals_to_remove", + "key_approvals_to_add", + "key_approvals_to_remove" + ]; + column = ( +
+ + + ), + arg: "proposal" + } + ]} + /> + +
+ {fields.map(field => { + if (op[1][field].length) { + return ( +
+ +
    + {op[1][field].map(value => { + return ( +
  • + {field.startsWith( + "key" + ) + ? value + : this.linkToAccount( + value + )} +
  • + ); + })} +
+
+ ); + } else return null; + })} +
+
+ ); + break; + + case "proposal_delete": column = ( + ), + arg: "proposal" } ]} /> @@ -869,17 +997,6 @@ class Operation extends React.Component { ); break; - case "proposal_delete": - column = ( - - - - ); - break; - case "withdraw_permission_create": column = ( diff --git a/app/components/Blockchain/Transaction.jsx b/app/components/Blockchain/Transaction.jsx index d5bcf502a8..b22d922dad 100644 --- a/app/components/Blockchain/Transaction.jsx +++ b/app/components/Blockchain/Transaction.jsx @@ -36,6 +36,13 @@ require("./json-inspector.scss"); let ops = Object.keys(operations); let listings = Object.keys(account_constants.account_listing); +const TranslateBoolean = ({value, ...otherProps}) => ( + +); + class OpType extends React.Component { shouldComponentUpdate(nextProps) { return nextProps.type !== this.props.type; @@ -169,7 +176,12 @@ class Transaction extends React.Component { ); memo = text ? ( - {text} + + {text} + ) : !text && isMine ? ( @@ -1791,7 +1803,48 @@ class Transaction extends React.Component { break; - // proposal_delete + case "proposal_delete": + color = "cancel"; + rows.push( + + + + + + {this.linkToAccount(op[1].fee_paying_account)} + + + ); + rows.push( + + + + + + + + + ); + rows.push( + + + + + {op[1].proposal} + + ); + break; case "asset_claim_fees": color = "success"; diff --git a/app/components/Blockchain/operations.scss b/app/components/Blockchain/operations.scss index 434472d4f7..def0a766b1 100644 --- a/app/components/Blockchain/operations.scss +++ b/app/components/Blockchain/operations.scss @@ -7,3 +7,10 @@ .right-td { text-align: left; } + +.proposal-update { + padding-top: 5px; + li { + font-size: 0.9rem; + } +} diff --git a/app/components/DepositWithdraw/citadel/CitadelBridgeDepositRequest.jsx b/app/components/DepositWithdraw/citadel/CitadelBridgeDepositRequest.jsx new file mode 100755 index 0000000000..9881846117 --- /dev/null +++ b/app/components/DepositWithdraw/citadel/CitadelBridgeDepositRequest.jsx @@ -0,0 +1,1851 @@ +import React from "react"; +import Translate from "react-translate-component"; +import ChainTypes from "components/Utility/ChainTypes"; +import BindToChainState from "components/Utility/BindToChainState"; +import BaseModal from "../../Modal/BaseModal"; +import ZfApi from "react-foundation-apps/src/utils/foundation-api"; +import AccountBalance from "../../Account/AccountBalance"; +import WithdrawModalCitadel from "./WithdrawModalCitadel"; +import CitadelDepositAddressCache from "common/CitadelDepositAddressCache"; +import utils from "common/utils"; +import AccountActions from "actions/AccountActions"; +import TransactionConfirmStore from "stores/TransactionConfirmStore"; +import {citadelAPIs} from "api/apiConfig"; +import {debounce} from "lodash-es"; +import {checkFeeStatusAsync, checkBalance} from "common/trxHelper"; +import {Asset} from "common/MarketClasses"; +import {ChainStore} from "bitsharesjs/es"; +import {getConversionJson} from "common/gatewayMethods"; +import PropTypes from "prop-types"; + +class ButtonWithdraw extends React.Component { + static propTypes = { + balance: ChainTypes.ChainObject, + url: PropTypes.string.isRequired + }; + + getWithdrawModalId() { + return "withdraw_asset_" + this.props.gateway + "_bridge"; + } + + onWithdraw() { + ZfApi.publish(this.getWithdrawModalId(), "open"); + } + + render() { + let withdraw_modal_id = this.getWithdrawModalId(); + + let button_class = "button disabled"; + if ( + Object.keys(this.props.account.get("balances").toJS()).includes( + this.props.asset.get("id") + ) + ) { + if ( + !!this.props.amount_to_withdraw && + !(this.props.amount_to_withdraw.indexOf(" ") >= 0) && + !isNaN(this.props.amount_to_withdraw) && + this.props.amount_to_withdraw > 0 && + this.props.amount_to_withdraw <= + this.props.balance.toJS().balance / + utils.get_asset_precision( + this.props.asset.get("precision") + ) + ) { + button_class = "button"; + } + } + + return ( + + + + + +
+
+ +
+
+
+ ); + } +} + +ButtonWithdraw = BindToChainState(ButtonWithdraw); + +class ButtonWithdrawContainer extends React.Component { + static propTypes = { + account: ChainTypes.ChainAccount.isRequired, + asset: ChainTypes.ChainAsset.isRequired, + output_coin_type: PropTypes.string.isRequired, + url: PropTypes.string.isRequired + }; + + render() { + let withdraw_button = ( + + ); + + return {withdraw_button}; + } +} + +ButtonWithdrawContainer = BindToChainState(ButtonWithdrawContainer); + +class CitadelBridgeDepositRequest extends React.Component { + static propTypes = { + url: PropTypes.string, + gateway: PropTypes.string, + account: ChainTypes.ChainAccount, + issuer_account: ChainTypes.ChainAccount, + initial_deposit_input_coin_type: PropTypes.string, + initial_deposit_output_coin_type: PropTypes.string, + initial_deposit_estimated_input_amount: PropTypes.string, + initial_withdraw_input_coin_type: PropTypes.string, + initial_withdraw_output_coin_type: PropTypes.string, + initial_withdraw_estimated_input_amount: PropTypes.string + }; + + constructor(props) { + super(props); + this.refresh_interval = 2 * 60 * 1000; // update deposit limit/estimates every two minutes + + this.deposit_address_cache = new CitadelDepositAddressCache(); + + this.coin_info_request_states = { + request_in_progress: 0, + request_complete: 1, + request_failed: 2 + }; + + this.estimation_directions = { + output_from_input: 0, + input_from_output: 1 + }; + + this.state = { + coin_symbol: "xmr", + key_for_withdrawal_dialog: "xmr", + supports_output_memos: "", + url: citadelAPIs.BASE, + error: null, + + // things that get displayed for deposits + deposit_input_coin_type: null, + deposit_output_coin_type: null, + input_address_and_memo: null, + deposit_estimated_input_amount: + this.props.initial_deposit_estimated_input_amount || "1.0", + deposit_estimated_output_amount: null, + deposit_limit: null, + deposit_error: null, + failed_calculate_deposit: null, + + // things that get displayed for withdrawals + withdraw_input_coin_type: null, + withdraw_output_coin_type: null, + withdraw_estimated_input_amount: + this.props.initial_withdraw_estimated_input_amount || "1.0", + withdraw_estimated_output_amount: null, + withdraw_limit: null, + withdraw_error: null, + failed_calculate_withdraw: null, + + // input address-related + coin_info_request_state: this.coin_info_request_states + .request_in_progress, + input_address_requests_in_progress: {}, + + // estimate-related + deposit_estimate_direction: this.estimation_directions + .output_from_input, + + // deposit limit-related + deposit_limit_cache: {}, + deposit_limit_requests_in_progress: {}, + + // generic data from BlockTrades + coins_by_type: null, + allowed_mappings_for_deposit: null, + allowed_mappings_for_withdraw: null, + + // announcements data + announcements: [] + }; + } + + urlConnection(checkUrl, state_coin_info) { + this.setState({ + url: checkUrl + }); + + // get basic data from citadel + let coin_types_url = checkUrl + "/coins"; + let coin_types_promise = fetch(coin_types_url, { + method: "get", + headers: new Headers({Accept: "application/json"}) + }).then(response => response.json()); + + let wallet_types_url = checkUrl + "/wallets"; + let wallet_types_promise = fetch(wallet_types_url, { + method: "get", + headers: new Headers({Accept: "application/json"}) + }).then(response => response.json()); + + let trading_pairs_url = checkUrl + "/trading-pairs"; + let trading_pairs_promise = fetch(trading_pairs_url, { + method: "get", + headers: new Headers({Accept: "application/json"}) + }).then(response => response.json()); + + let active_wallets_url = checkUrl + "/active-wallets"; + let active_wallets_promise = fetch(active_wallets_url, { + method: "get", + headers: new Headers({Accept: "application/json"}) + }).then(response => response.json()); + + Promise.all([ + coin_types_promise, + wallet_types_promise, + trading_pairs_promise, + active_wallets_promise + ]) + .then(json_responses => { + let [ + coin_types, + wallet_types_reply, + trading_pairs, + active_wallets + ] = json_responses; + + // get quick access to coins by their types + let coins_by_type = {}; + coin_types.forEach( + coin_type => (coins_by_type[coin_type.coinType] = coin_type) + ); + + // determine which mappings we will display for deposits and withdrawals + let allowed_mappings_for_deposit = {}; // all non-bts to bts + let allowed_mappings_for_withdraw = {}; // all bts to non-bts + trading_pairs.forEach(pair => { + let input_coin_info = coins_by_type[pair.inputCoinType]; + let output_coin_info = coins_by_type[pair.outputCoinType]; + + // filter out pairs where one asset is a backed asset and the other is a backing asset, + // those pairs rightly belong under the gateway section, not under the bridge section. + if ( + input_coin_info.backingCoinType != + pair.outputCoinType || + (output_coin_info.backingCoinType != + pair.inputCoinType && + input_coin_info.restricted == false && + output_coin_info.restricted == false) + ) { + // filter out mappings where one of the wallets is offline + if ( + active_wallets.indexOf( + input_coin_info.walletType + ) != -1 && + active_wallets.indexOf( + output_coin_info.walletType + ) != -1 + ) { + if ( + input_coin_info.walletType != "bitshares2" && + output_coin_info.walletType == "bitshares2" + ) { + allowed_mappings_for_deposit[ + pair.inputCoinType + ] = + allowed_mappings_for_deposit[ + pair.inputCoinType + ] || []; + allowed_mappings_for_deposit[ + pair.inputCoinType + ].push(pair.outputCoinType); + } else { + allowed_mappings_for_withdraw[ + pair.inputCoinType + ] = + allowed_mappings_for_withdraw[ + pair.inputCoinType + ] || []; + allowed_mappings_for_withdraw[ + pair.inputCoinType + ].push(pair.outputCoinType); + } + } + } + }); + + // we can now set the input and output coin types + let deposit_input_coin_type = null; + let deposit_output_coin_type = null; + let allowed_deposit_coin_types = Object.keys( + allowed_mappings_for_deposit + ); + allowed_deposit_coin_types.forEach(deposit_coin_type => { + allowed_mappings_for_deposit[deposit_coin_type].sort(); + }); + + if (allowed_deposit_coin_types.length) { + if ( + this.props.initial_deposit_input_coin_type && + this.props.initial_deposit_input_coin_type in + allowed_mappings_for_deposit + ) + deposit_input_coin_type = this.props + .initial_deposit_input_coin_type; + else + deposit_input_coin_type = allowed_deposit_coin_types[0]; + let output_coin_types_for_this_input = + allowed_mappings_for_deposit[deposit_input_coin_type]; + if ( + this.props.initial_deposit_output_coin_type && + output_coin_types_for_this_input.indexOf( + this.props.initial_deposit_output_coin_type + ) != -1 + ) + deposit_output_coin_type = this.props + .initial_deposit_output_coin_type; + else + deposit_output_coin_type = + output_coin_types_for_this_input[0]; + } + + let withdraw_input_coin_type = null; + let withdraw_output_coin_type = null; + let allowed_withdraw_coin_types = Object.keys( + allowed_mappings_for_withdraw + ); + allowed_withdraw_coin_types.forEach(withdraw_coin_type => { + allowed_mappings_for_withdraw[withdraw_coin_type].sort(); + }); + + if (allowed_withdraw_coin_types.length) { + if ( + this.props.initial_withdraw_input_coin_type && + this.props.initial_withdraw_input_coin_type in + allowed_mappings_for_withdraw + ) + withdraw_input_coin_type = this.props + .initial_withdraw_input_coin_type; + else + withdraw_input_coin_type = + allowed_withdraw_coin_types[0]; + let output_coin_types_for_this_input = + allowed_mappings_for_withdraw[withdraw_input_coin_type]; + if ( + this.props.initial_withdraw_output_coin_type && + output_coin_types_for_this_input.indexOf( + this.props.initial_withdraw_output_coin_type + ) != -1 + ) + withdraw_output_coin_type = this.props + .initial_withdraw_output_coin_type; + else + withdraw_output_coin_type = + output_coin_types_for_this_input[0]; + } + + let input_address_and_memo = this.getCachedOrGeneratedInputAddress( + deposit_input_coin_type, + deposit_output_coin_type + ); + + let deposit_limit = this.getCachedOrFreshDepositLimit( + "deposit", + deposit_input_coin_type, + deposit_output_coin_type + ); + let deposit_estimated_output_amount = this.getAndUpdateOutputEstimate( + "deposit", + deposit_input_coin_type, + deposit_output_coin_type, + this.state.deposit_estimated_input_amount + ); + + let withdraw_estimated_output_amount = this.getAndUpdateOutputEstimate( + "withdraw", + withdraw_input_coin_type, + withdraw_output_coin_type, + this.state.withdraw_estimated_input_amount + ); + let withdraw_limit = this.getCachedOrFreshDepositLimit( + "withdraw", + withdraw_input_coin_type, + withdraw_output_coin_type + ); + + if (this.unMounted) return; + + this.setState({ + coin_info_request_state: this.coin_info_request_states + .request_complete, + coins_by_type: coins_by_type, + allowed_mappings_for_deposit: allowed_mappings_for_deposit, + allowed_mappings_for_withdraw: allowed_mappings_for_withdraw, + deposit_input_coin_type: deposit_input_coin_type, + deposit_output_coin_type: deposit_output_coin_type, + input_address_and_memo: input_address_and_memo, + deposit_limit: deposit_limit, + deposit_estimated_output_amount: deposit_estimated_output_amount, + deposit_estimate_direction: this.estimation_directions + .output_from_input, + withdraw_input_coin_type: withdraw_input_coin_type, + withdraw_output_coin_type: withdraw_output_coin_type, + withdraw_limit: withdraw_limit, + withdraw_estimated_output_amount: withdraw_estimated_output_amount, + withdraw_estimate_direction: this.estimation_directions + .output_from_input, + supports_output_memos: + coins_by_type["xmr"].supportsOutputMemos + }); + }) + .catch(error => { + this.setState({ + coin_info_request_state: state_coin_info, + coins_by_type: null, + allowed_mappings_for_deposit: null, + allowed_mappings_for_withdraw: null + }); + }); + } + + // functions for periodically updating our deposit limit and estimates + updateEstimates() { + if ( + this.state.deposit_input_coin_type && + this.state.deposit_output_coin_type + ) { + // input address won't usually need refreshing unless there was an error + // generating it last time around + let new_input_address_and_memo = this.getCachedOrGeneratedInputAddress( + this.state.deposit_input_coin_type, + this.state.deposit_output_coin_type + ); + + let new_deposit_limit = this.getCachedOrFreshDepositLimit( + "deposit", + this.state.deposit_input_coin_type, + this.state.deposit_output_coin_type + ); + let new_deposit_estimated_input_amount = this.state + .deposit_estimated_input_amount; + let new_deposit_estimated_output_amount = this.state + .deposit_estimated_output_amount; + + if ( + this.state.deposit_estimate_direction == + this.estimation_directions.output_from_input + ) + new_deposit_estimated_output_amount = this.getAndUpdateOutputEstimate( + "deposit", + this.state.deposit_input_coin_type, + this.state.deposit_output_coin_type, + new_deposit_estimated_input_amount + ); + else + new_deposit_estimated_input_amount = this.getAndUpdateInputEstimate( + "deposit", + this.state.deposit_input_coin_type, + this.state.deposit_output_coin_type, + new_deposit_estimated_output_amount + ); + + let new_withdraw_limit = this.getCachedOrFreshDepositLimit( + "withdraw", + this.state.withdraw_input_coin_type, + this.state.withdraw_output_coin_type + ); + let new_withdraw_estimated_input_amount = this.state + .withdraw_estimated_input_amount; + let new_withdraw_estimated_output_amount = this.state + .withdraw_estimated_output_amount; + + if ( + this.state.withdraw_estimate_direction == + this.estimation_directions.output_from_input + ) + new_withdraw_estimated_output_amount = this.getAndUpdateOutputEstimate( + "withdraw", + this.state.withdraw_input_coin_type, + this.state.withdraw_output_coin_type, + new_withdraw_estimated_input_amount + ); + else + new_withdraw_estimated_input_amount = this.getAndUpdateinputEstimate( + "withdraw", + this.state.withdraw_input_coin_type, + this.state.withdraw_output_coin_type, + new_withdraw_estimated_output_amount + ); + + this.setState({ + input_address_and_memo: new_input_address_and_memo, + deposit_limit: new_deposit_limit, + deposit_estimated_input_amount: new_deposit_estimated_input_amount, + deposit_estimated_output_amount: new_deposit_estimated_output_amount, + withdraw_limit: new_withdraw_limit, + withdraw_estimated_input_amount: new_withdraw_estimated_input_amount, + withdraw_estimated_output_amount: new_withdraw_estimated_output_amount, + key_for_withdrawal_dialog: new_withdraw_estimated_input_amount + }); + } + } + + componentWillMount() { + // check + let checkUrl = this.state.url; + this.urlConnection(checkUrl, 0); + let coin_types_promisecheck = fetch(checkUrl + "/coins", { + method: "get", + headers: new Headers({Accept: "application/json"}) + }).then(response => response.json()); + let trading_pairs_promisecheck = fetch(checkUrl + "/trading-pairs", { + method: "get", + headers: new Headers({Accept: "application/json"}) + }).then(response => response.json()); + let active_wallets_promisecheck = fetch(checkUrl + "/active-wallets", { + method: "get", + headers: new Headers({Accept: "application/json"}) + }).then(response => response.json()); + Promise.all([ + coin_types_promisecheck, + trading_pairs_promisecheck, + active_wallets_promisecheck + ]) + .then(json_responses => { + let [ + coin_types, + trading_pairs, + active_wallets + ] = json_responses; + let coins_by_type = {}; + coin_types.forEach( + coin_type => (coins_by_type[coin_type.coinType] = coin_type) + ); + trading_pairs.forEach(pair => { + let input_coin_info = coins_by_type[pair.inputCoinType]; + let output_coin_info = coins_by_type[pair.outputCoinType]; + if ( + input_coin_info.backingCoinType != + pair.outputCoinType && + output_coin_info.backingCoinType != pair.inputCoinType + ) { + if ( + active_wallets.indexOf( + input_coin_info.walletType + ) != -1 && + active_wallets.indexOf( + output_coin_info.walletType + ) != -1 + ) { + } + } + }); + }) + .catch(error => { + this.urlConnection("https://citadel.li/trade", 2); + this.setState({ + coin_info_request_state: 0, + coins_by_type: null, + allowed_mappings_for_deposit: null, + allowed_mappings_for_withdraw: null + }); + }); + } + + componentDidMount() { + this.update_timer = setInterval( + this.updateEstimates.bind(this), + this.refresh_interval + ); + } + + componentWillUnmount() { + clearInterval(this.update_timer); + this.unMounted = true; + } + + // functions for managing input addresses + getCachedInputAddress(input_coin_type, output_coin_type, memo) { + let account_name = this.props.account.get("name"); + return this.deposit_address_cache.getCachedInputAddress( + this.props.gateway, + account_name, + input_coin_type, + output_coin_type + ); + } + + cacheInputAddress(input_coin_type, output_coin_type, address, memo) { + let account_name = this.props.account.get("name"); + this.deposit_address_cache.cacheInputAddress( + this.props.gateway, + account_name, + input_coin_type, + output_coin_type, + address, + memo + ); + } + + getCachedOrGeneratedInputAddress(input_coin_type, output_coin_type) { + // if we already have an address, just return it + let cached_input_address_and_memo = this.getCachedInputAddress( + input_coin_type, + output_coin_type + ); + if (cached_input_address_and_memo) { + return cached_input_address_and_memo; + } + + // if we've already asked for this address, return null, it will trigger a refresh when it completes + this.state.input_address_requests_in_progress[input_coin_type] = + this.state.input_address_requests_in_progress[input_coin_type] || + {}; + if ( + this.state.input_address_requests_in_progress[input_coin_type][ + output_coin_type + ] + ) + return null; + + // else, no active request for this mapping, kick one off + let body = JSON.stringify({ + inputCoinType: input_coin_type, + outputCoinType: output_coin_type, + outputAddress: this.props.account.get("name") + }); + + this.state.input_address_requests_in_progress[input_coin_type][ + output_coin_type + ] = true; + + fetch(this.state.url + "/simple-api/initiate-trade", { + method: "post", + headers: new Headers({ + Accept: "application/json", + "Content-Type": "application/json" + }), + body: body + }).then( + reply => { + reply.json().then( + json => { + console.assert( + json.inputCoinType == input_coin_type, + "unexpected reply from initiate-trade" + ); + console.assert( + json.outputCoinType == output_coin_type, + "unexpected reply from initiate-trade" + ); + if ( + json.inputCoinType != input_coin_type || + json.outputCoinType != output_coin_type + ) + throw Error("unexpected reply from initiate-trade"); + this.cacheInputAddress( + json.inputCoinType, + json.outputCoinType, + json.inputAddress, + json.inputMemo + ); + delete this.state.input_address_requests_in_progress[ + input_coin_type + ][output_coin_type]; + if ( + this.state.deposit_input_coin_type == + json.inputCoinType && + this.state.deposit_output_coin_type == + json.outputCoinType + ) + this.setState({ + input_address_and_memo: { + address: json.inputAddress, + memo: json.inputMemo + } + }); + }, + error => { + delete this.state.input_address_requests_in_progress[ + input_coin_type + ][output_coin_type]; + if ( + this.state.deposit_input_coin_type == + input_coin_type && + this.state.deposit_output_coin_type == + output_coin_type + ) + this.setState({ + input_address_and_memo: { + address: "error generating address", + memo: null + } + }); + } + ); + }, + error => { + delete this.state.input_address_requests_in_progress[ + input_coin_type + ][output_coin_type]; + if ( + this.state.deposit_input_coin_type == input_coin_type && + this.state.deposit_output_coin_type == output_coin_type + ) + this.setState({ + input_address_and_memo: { + address: "error generating address", + memo: null + } + }); + } + ); + return null; + } + + // functions for managing deposit limits + getCachedDepositLimit(input_coin_type, output_coin_type) { + this.state.deposit_limit_cache[input_coin_type] = + this.state.deposit_limit_cache[input_coin_type] || {}; + if (this.state.deposit_limit_cache[input_coin_type][output_coin_type]) { + let deposit_limit_record = this.state.deposit_limit_cache[ + input_coin_type + ][output_coin_type]; + let cache_age = new Date() - deposit_limit_record.timestamp; + if (cache_age < this.refresh_interval) return deposit_limit_record; + delete this.state.deposit_limit_cache[input_coin_type][ + output_coin_type + ]; + } + return null; + } + + cacheDepositLimit(input_coin_type, output_coin_type, deposit_limit_record) { + deposit_limit_record.timestamp = new Date(); + this.state.deposit_limit_cache[input_coin_type] = + this.state.deposit_limit_cache[input_coin_type] || {}; + this.state.deposit_limit_cache[input_coin_type][ + output_coin_type + ] = deposit_limit_record; + } + + getCachedOrFreshDepositLimit( + deposit_withdraw_or_convert, + input_coin_type, + output_coin_type + ) { + let deposit_limit_record = this.getCachedDepositLimit( + input_coin_type, + output_coin_type + ); + + if (deposit_limit_record) return deposit_limit_record; + + this.state.deposit_limit_requests_in_progress[input_coin_type] = + this.state.input_address_requests_in_progress[input_coin_type] || + {}; + this.state.deposit_limit_requests_in_progress[input_coin_type][ + output_coin_type + ] = true; + + let deposit_limit_url = + this.state.url + + "/deposit-limits?inputCoinType=" + + encodeURIComponent(input_coin_type) + + "&outputCoinType=" + + encodeURIComponent(output_coin_type); + + let deposit_limit_promise = fetch(deposit_limit_url, { + method: "get", + headers: new Headers({Accept: "application/json"}) + }).then(response => response.json()); + deposit_limit_promise.then( + reply => { + if (this.unMounted) return; + console.assert( + reply.inputCoinType == input_coin_type && + reply.outputCoinType == output_coin_type, + "unexpected reply from deposit-limits" + ); + if ( + reply.inputCoinType != input_coin_type || + reply.outputCoinType != output_coin_type + ) + throw Error("unexpected reply from deposit-limits"); + let new_deposit_limit_record = { + timestamp: new Date(), + limit: reply.depositLimit + }; + this.cacheDepositLimit( + input_coin_type, + output_coin_type, + new_deposit_limit_record + ); + delete this.state.deposit_limit_requests_in_progress[ + input_coin_type + ][output_coin_type]; + if ( + this.state[ + deposit_withdraw_or_convert + "_input_coin_type" + ] == input_coin_type && + this.state[ + deposit_withdraw_or_convert + "_output_coin_type" + ] == output_coin_type + ) + this.setState({ + [deposit_withdraw_or_convert + + "_limit"]: new_deposit_limit_record + }); + }, + error => { + delete this.state.deposit_limit_requests_in_progress[ + input_coin_type + ][output_coin_type]; + } + ); + return null; + } + + getAndUpdateOutputEstimate( + deposit_withdraw_or_convert, + input_coin_type, + output_coin_type, + input_amount + ) { + if (this.unMounted) return; + if (deposit_withdraw_or_convert == "deposit") { + this.setState({failed_calculate_deposit: null}); + } + if (deposit_withdraw_or_convert == "withdraw") { + this.setState({failed_calculate_withdraw: null}); + } + + let estimate_output_url = + this.state.url + + "/estimate-output-amount?inputAmount=" + + encodeURIComponent(input_amount) + + "&inputCoinType=" + + encodeURIComponent(input_coin_type) + + "&outputCoinType=" + + encodeURIComponent(output_coin_type); + + let estimate_output_promise = fetch(estimate_output_url, { + method: "get", + headers: new Headers({Accept: "application/json"}) + }).then(response => response.json()); + estimate_output_promise.then( + reply => { + if (this.unMounted) return; + if (reply.error) { + if ( + this.state[ + deposit_withdraw_or_convert + "_input_coin_type" + ] == input_coin_type && + this.state[ + deposit_withdraw_or_convert + "_output_coin_type" + ] == output_coin_type && + this.state[ + deposit_withdraw_or_convert + + "_estimated_input_amount" + ] == input_amount && + this.state[ + deposit_withdraw_or_convert + "_estimate_direction" + ] == this.estimation_directions.output_from_input + ) { + let user_message = reply.error.message; + + if (deposit_withdraw_or_convert == "deposit") { + this.setState({ + failed_calculate_deposit: "Failed to calculate" + }); + } + if (deposit_withdraw_or_convert == "withdraw") { + this.setState({ + failed_calculate_withdraw: "Failed to calculate" + }); + } + + let expected_prefix = "Internal Server Error: "; + if (user_message.startsWith(expected_prefix)) + user_message = user_message.substr( + expected_prefix.length + ); + + this.setState({ + [deposit_withdraw_or_convert + + "_error"]: user_message + }); + } + } else { + console.assert( + reply.inputCoinType == input_coin_type && + reply.outputCoinType == output_coin_type && + reply.inputAmount == input_amount, + "unexpected reply from estimate-output-amount" + ); + if ( + reply.inputCoinType != input_coin_type || + reply.outputCoinType != output_coin_type || + reply.inputAmount != input_amount + ) { + this.setState({ + [deposit_withdraw_or_convert + + "_estimated_output_amount"]: reply.outputAmount, + [deposit_withdraw_or_convert + "_error"]: null + }); + } + if ( + this.state[ + deposit_withdraw_or_convert + "_input_coin_type" + ] == input_coin_type && + this.state[ + deposit_withdraw_or_convert + "_output_coin_type" + ] == output_coin_type && + this.state[ + deposit_withdraw_or_convert + + "_estimated_input_amount" + ] == input_amount && + this.state[ + deposit_withdraw_or_convert + "_estimate_direction" + ] == this.estimation_directions.output_from_input + ) { + this.setState({ + [deposit_withdraw_or_convert + + "_estimated_output_amount"]: reply.outputAmount, + [deposit_withdraw_or_convert + "_error"]: null + }); + } + } + }, + error => {} + ); + + return null; + } + + getAndUpdateInputEstimate( + deposit_withdraw_or_convert, + input_coin_type, + output_coin_type, + output_amount + ) { + if (this.unMounted) return; + if (deposit_withdraw_or_convert == "deposit") { + this.setState({failed_calculate_deposit: null}); + } + if (deposit_withdraw_or_convert == "withdraw") { + this.setState({failed_calculate_withdraw: null}); + } + + let estimate_input_url = + this.state.url + + "/estimate-input-amount?outputAmount=" + + encodeURIComponent(output_amount) + + "&inputCoinType=" + + encodeURIComponent(input_coin_type) + + "&outputCoinType=" + + encodeURIComponent(output_coin_type); + let estimate_input_promise = fetch(estimate_input_url, { + method: "get", + headers: new Headers({Accept: "application/json"}) + }).then(response => response.json()); + estimate_input_promise.then( + reply => { + if (this.unMounted) return; + + console.assert( + reply.inputCoinType == input_coin_type && + reply.outputCoinType == output_coin_type && + reply.outputAmount == output_amount, + "unexpected reply from estimate-input-amount" + ); + if ( + reply.inputCoinType != input_coin_type || + reply.outputCoinType != output_coin_type || + reply.outputAmount != output_amount + ) { + if (deposit_withdraw_or_convert == "deposit") { + this.setState({ + failed_calculate_deposit: "Failed to calculate" + }); + } + if (deposit_withdraw_or_convert == "withdraw") { + this.setState({ + failed_calculate_withdraw: "Failed to calculate" + }); + } + } + + if ( + this.state[ + deposit_withdraw_or_convert + "_input_coin_type" + ] == input_coin_type && + this.state[ + deposit_withdraw_or_convert + "_output_coin_type" + ] == output_coin_type && + this.state[ + deposit_withdraw_or_convert + "_estimated_output_amount" + ] == output_amount && + this.state[ + deposit_withdraw_or_convert + "_estimate_direction" + ] == this.estimation_directions.input_from_output + ) + this.setState({ + [deposit_withdraw_or_convert + + "_estimated_input_amount"]: reply.inputAmount, + key_for_withdrawal_dialog: reply.inputAmount + }); + }, + error => {} + ); + + return null; + } + + onInputAmountChanged(deposit_withdraw_or_convert, event) { + let new_estimated_input_amount = event.target.value; + if (new_estimated_input_amount == "") { + new_estimated_input_amount = "0"; + } + + let new_estimated_output_amount = this.getAndUpdateOutputEstimate( + deposit_withdraw_or_convert, + this.state[deposit_withdraw_or_convert + "_input_coin_type"], + this.state[deposit_withdraw_or_convert + "_output_coin_type"], + new_estimated_input_amount + ); + + this.setState({ + [deposit_withdraw_or_convert + + "_estimated_input_amount"]: new_estimated_input_amount, + [deposit_withdraw_or_convert + + "_estimated_output_amount"]: new_estimated_output_amount, + [deposit_withdraw_or_convert + "_estimate_direction"]: this + .estimation_directions.output_from_input, + key_for_withdrawal_dialog: new_estimated_input_amount + }); + } + + onOutputAmountChanged(deposit_withdraw_or_convert, event) { + let new_estimated_output_amount = event.target.value; + if (new_estimated_output_amount == "") { + new_estimated_output_amount = "0"; + } + + let new_estimated_input_amount = this.getAndUpdateInputEstimate( + deposit_withdraw_or_convert, + this.state[deposit_withdraw_or_convert + "_input_coin_type"], + this.state[deposit_withdraw_or_convert + "_output_coin_type"], + new_estimated_output_amount + ); + + this.setState({ + [deposit_withdraw_or_convert + + "_estimated_output_amount"]: new_estimated_output_amount, + [deposit_withdraw_or_convert + + "_estimated_input_amount"]: new_estimated_input_amount, + [deposit_withdraw_or_convert + "_estimate_direction"]: this + .estimation_directions.input_from_output + }); + } + + getWithdrawModalId() { + return "withdraw_asset_" + this.props.gateway + "_bridge"; + } + + onWithdraw() { + ZfApi.publish(this.getWithdrawModalId(), "open"); + } + + onInputCoinTypeChanged(deposit_withdraw_or_convert, event) { + let estimated_input_output_amount = null; + let estimated_input_output_amount_state = "_estimated_output_amount"; + let new_input_coin_type = event.target.value; + let possible_output_coin_types = this.state[ + "allowed_mappings_for_" + deposit_withdraw_or_convert + ][new_input_coin_type]; + let new_output_coin_type = possible_output_coin_types[0]; + + if ( + possible_output_coin_types.indexOf( + this.state[deposit_withdraw_or_convert + "_output_coin_type"] + ) != -1 + ) + new_output_coin_type = this.state[ + deposit_withdraw_or_convert + "_output_coin_type" + ]; + + let new_input_address_and_memo = this.state.input_address_and_memo; + if (deposit_withdraw_or_convert == "deposit") + new_input_address_and_memo = this.getCachedOrGeneratedInputAddress( + new_input_coin_type, + new_output_coin_type + ); + let new_deposit_limit = this.getCachedOrFreshDepositLimit( + deposit_withdraw_or_convert, + new_input_coin_type, + new_output_coin_type + ); + + if ( + !this.state[deposit_withdraw_or_convert + "_estimated_input_amount"] + ) { + estimated_input_output_amount = this.getAndUpdateInputEstimate( + deposit_withdraw_or_convert, + new_input_coin_type, + new_output_coin_type, + this.state[ + deposit_withdraw_or_convert + "_estimated_output_amount" + ] + ); + estimated_input_output_amount_state = "_estimated_input_amount"; + } else { + estimated_input_output_amount = this.getAndUpdateOutputEstimate( + deposit_withdraw_or_convert, + new_input_coin_type, + new_output_coin_type, + this.state[ + deposit_withdraw_or_convert + "_estimated_input_amount" + ] + ); + } + + if (deposit_withdraw_or_convert == "withdraw") { + possible_output_coin_types.forEach( + allowed_withdraw_output_coin_type => { + if ( + new_output_coin_type === + allowed_withdraw_output_coin_type + ) { + this.setState({ + coin_symbol: new_input_coin_type + "input", + supports_output_memos: this.state.coins_by_type[ + allowed_withdraw_output_coin_type + ].supportsOutputMemos + }); + } + } + ); + } + + this.setState({ + [deposit_withdraw_or_convert + + "_input_coin_type"]: new_input_coin_type, + [deposit_withdraw_or_convert + + "_output_coin_type"]: new_output_coin_type, + input_address_and_memo: new_input_address_and_memo, + [deposit_withdraw_or_convert + "_limit"]: new_deposit_limit, + [deposit_withdraw_or_convert + + estimated_input_output_amount_state]: estimated_input_output_amount, + [deposit_withdraw_or_convert + "_estimate_direction"]: this + .estimation_directions.output_from_input + }); + } + + onOutputCoinTypeChanged(deposit_withdraw_or_convert, event) { + let estimated_input_output_amount = null; + let estimated_input_output_amount_state = "_estimated_output_amount"; + let new_output_coin_type = event.target.value; + let withdraw_output_coin_types = this.state + .allowed_mappings_for_withdraw[this.state.withdraw_input_coin_type]; + + if (deposit_withdraw_or_convert == "withdraw") { + withdraw_output_coin_types.forEach( + allowed_withdraw_output_coin_type => { + if ( + new_output_coin_type === + allowed_withdraw_output_coin_type + ) { + this.setState({ + coin_symbol: new_output_coin_type + "output", + supports_output_memos: this.state.coins_by_type[ + allowed_withdraw_output_coin_type + ].supportsOutputMemos, + key_for_withdrawal_dialog: new_output_coin_type + }); + } + } + ); + } + + let new_input_address_and_memo = this.state.input_address_and_memo; + if (deposit_withdraw_or_convert == "deposit") + new_input_address_and_memo = this.getCachedOrGeneratedInputAddress( + this.state[deposit_withdraw_or_convert + "_input_coin_type"], + new_output_coin_type + ); + let new_deposit_limit = this.getCachedOrFreshDepositLimit( + deposit_withdraw_or_convert, + this.state[deposit_withdraw_or_convert + "_input_coin_type"], + new_output_coin_type + ); + + if ( + !this.state[deposit_withdraw_or_convert + "_estimated_input_amount"] + ) { + estimated_input_output_amount = this.getAndUpdateInputEstimate( + deposit_withdraw_or_convert, + this.state[deposit_withdraw_or_convert + "_input_coin_type"], + new_output_coin_type, + this.state[ + deposit_withdraw_or_convert + "_estimated_output_amount" + ] + ); + estimated_input_output_amount_state = "_estimated_input_amount"; + } else { + estimated_input_output_amount = this.getAndUpdateOutputEstimate( + deposit_withdraw_or_convert, + this.state[deposit_withdraw_or_convert + "_input_coin_type"], + new_output_coin_type, + this.state[ + deposit_withdraw_or_convert + "_estimated_input_amount" + ] + ); + } + + this.setState({ + [deposit_withdraw_or_convert + + "_output_coin_type"]: new_output_coin_type, + input_address_and_memo: new_input_address_and_memo, + [deposit_withdraw_or_convert + "_limit"]: new_deposit_limit, + [deposit_withdraw_or_convert + + estimated_input_output_amount_state]: estimated_input_output_amount, + [deposit_withdraw_or_convert + "_estimate_direction"]: this + .estimation_directions.output_from_input + }); + } + + render() { + if ( + !this.props.account || + !this.props.issuer_account || + !this.props.gateway + ) + return
; + + let announcements, + deposit_body, + deposit_header, + withdraw_body, + withdraw_header, + conversion_body, + conversion_header, + withdraw_modal_id, + conversion_modal_id; + + if ( + this.state.coin_info_request_state == + this.coin_info_request_states.request_failed + ) { + return ( +
+

+ Error connecting to citadel.li, please try again later +

+
+ ); + } else if ( + this.state.coin_info_request_state == + this.coin_info_request_states.never_requested || + this.state.coin_info_request_state == + this.coin_info_request_states.request_in_progress + ) { + return ( +
+

Retrieving current trade data from citadel.li

+
+ ); + } else { + // depending on what wallets are online, we might support deposits, withdrawals, conversions, all, or neither at any given time. + let deposit_table = null; + let withdraw_table = null; + let amount_to_withdraw = null; + + let calcTextDeposit = ; + if (this.state.failed_calculate_deposit != null) { + calcTextDeposit = this.state.failed_calculate_deposit; + } + let calcTextWithdraw = ; + if (this.state.failed_calculate_withdraw != null) { + calcTextWithdraw = this.state.failed_calculate_withdraw; + } + let calcTextConversion = ; + if (this.state.failed_calculate_conversion != null) { + calcTextConversion = this.state.failed_calculate_conversion; + } + + if ( + Object.getOwnPropertyNames( + this.state.allowed_mappings_for_deposit + ).length > 0 + ) { + // deposit + let deposit_input_coin_type_options = []; + Object.keys(this.state.allowed_mappings_for_deposit) + .sort() + .forEach(allowed_deposit_input_coin_type => { + deposit_input_coin_type_options.push( + + ); + }); + let deposit_input_coin_type_select = ( + + ); + + let deposit_output_coin_type_options = []; + let deposit_output_coin_types = this.state + .allowed_mappings_for_deposit[ + this.state.deposit_input_coin_type + ]; + deposit_output_coin_types.forEach( + allowed_deposit_output_coin_type => { + deposit_output_coin_type_options.push( + + ); + } + ); + let deposit_output_coin_type_select = ( + + ); + + let input_address_and_memo = this.state.input_address_and_memo + ? this.state.input_address_and_memo + : {address: "unknown", memo: null}; + + let estimated_input_amount_text = this.state + .deposit_estimated_input_amount; + let estimated_output_amount_text = this.state + .deposit_estimated_output_amount; + + let deposit_input_amount_edit_box = estimated_input_amount_text ? ( + + ) : ( + calcTextDeposit + ); + let deposit_output_amount_edit_box = estimated_output_amount_text ? ( + + ) : ( + calcTextDeposit + ); + + let deposit_limit_element = updating; + if (this.state.deposit_limit) { + if (this.state.deposit_limit.limit) + deposit_limit_element = ( +
+ + + +
+ ); + else deposit_limit_element = null; + //else + // deposit_limit_element = no limit; + } + + let deposit_error_element = null; + if (this.state.deposit_error) + deposit_error_element = ( +
{this.state.deposit_error}
+ ); + + deposit_header = ( + + + + + + + + + + + + + + ); + + let deposit_address_and_memo_element = null; + if (input_address_and_memo.memo) + deposit_address_and_memo_element = ( + + ); + else + deposit_address_and_memo_element = ( + {input_address_and_memo.address} + ); + //with memo {input_address_and_memo.memo}; + + deposit_body = ( + + + +
+
+
+ {deposit_input_coin_type_select} +
+
+ {deposit_input_amount_edit_box} +
+
+ → +
+
+ {deposit_output_coin_type_select} +
+
+ {deposit_output_amount_edit_box} +
+
+
{deposit_error_element}
+
+ + + + + + {deposit_address_and_memo_element} +
+ {deposit_limit_element} + + + + ); + } + + if ( + Object.getOwnPropertyNames( + this.state.allowed_mappings_for_withdraw + ).length > 0 + ) { + withdraw_modal_id = this.getWithdrawModalId(); + let withdraw_asset_symbol = this.state.coins_by_type[ + this.state.withdraw_input_coin_type + ].symbol; + + // withdrawal + + amount_to_withdraw = this.state.withdraw_estimated_input_amount; + + let withdraw_input_coin_type_options = []; + Object.keys(this.state.allowed_mappings_for_withdraw) + .sort() + .forEach(allowed_withdraw_input_coin_type => { + withdraw_input_coin_type_options.push( + + ); + }); + let withdraw_input_coin_type_select = ( + + ); + + let withdraw_output_coin_type_options = []; + let withdraw_output_coin_types = this.state + .allowed_mappings_for_withdraw[ + this.state.withdraw_input_coin_type + ]; + withdraw_output_coin_types.forEach( + allowed_withdraw_output_coin_type => { + withdraw_output_coin_type_options.push( + + ); + } + ); + let withdraw_output_coin_type_select = ( + + ); + + let estimated_input_amount_text = this.state + .withdraw_estimated_input_amount; + + let withdraw_input_amount_edit_box = estimated_input_amount_text ? ( + + ) : ( + calcTextWithdraw + ); + + let estimated_output_amount_text = this.state + .withdraw_estimated_output_amount; + + let withdraw_output_amount_edit_box = estimated_output_amount_text ? ( + + ) : ( + calcTextWithdraw + ); + + let withdraw_button = ( + + ); + + let withdraw_error_element = null; + if (this.state.withdraw_error) + withdraw_error_element = ( +
{this.state.withdraw_error}
+ ); + + let withdraw_limit_element = ...; + if (this.state.withdraw_limit) { + if (this.state.withdraw_limit.limit) + withdraw_limit_element = ( +
+ + + +
+ ); + else + withdraw_limit_element = ( +
+ no limit +
+ ); + } + + withdraw_header = ( + + + + + + + + + + + + ); + + withdraw_body = ( + + + +
+
+
+ {withdraw_input_coin_type_select} +
+
+ {withdraw_input_amount_edit_box} +
+
+ → +
+
+ {withdraw_output_coin_type_select} +
+
+ {withdraw_output_amount_edit_box} +
+
+
{withdraw_error_element}
+
+ + + + + + {withdraw_button} +
+ {withdraw_limit_element} + + + + ); + } + + if (this.state.announcements.length > 0) { + announcements = ( +
+ {this.state.announcements.map(function(data, index) { + let add_color = "txtann info"; + + if (data.status === 10) { + add_color = "txtann error"; + } else if (data.status === 20) { + add_color = "txtann warning"; + } else if (data.status === 30) { + add_color = "txtann success"; + } else if (data.status === 40) { + add_color = "txtann info"; + } + + if (data.format === 1) { + data.message.replace(/\r\n|\r|\n/g, "
"); + } + + return ( +
+ {data.message} +
+ ); + }, this)} +
+ ); + } + + return ( +
+
+ +
+ {announcements} + + {deposit_header} + {deposit_body} + {withdraw_header} + {withdraw_body} +
+
+ ); + } + } +} // CitadelBridgeDepositRequest + +export default BindToChainState(CitadelBridgeDepositRequest); diff --git a/app/components/DepositWithdraw/citadel/CitadelGateway.jsx b/app/components/DepositWithdraw/citadel/CitadelGateway.jsx new file mode 100755 index 0000000000..92abb939f0 --- /dev/null +++ b/app/components/DepositWithdraw/citadel/CitadelGateway.jsx @@ -0,0 +1,291 @@ +import React from "react"; +import CitadelGatewayDepositRequest from "./CitadelGatewayDepositRequest"; +import Translate from "react-translate-component"; +import {connect} from "alt-react"; +import SettingsStore from "stores/SettingsStore"; +import SettingsActions from "actions/SettingsActions"; +import { + RecentTransactions, + TransactionWrapper +} from "components/Account/RecentTransactions"; +import Immutable from "immutable"; +import cnames from "classnames"; +import LoadingIndicator from "../../LoadingIndicator"; + +class CitadelGateway extends React.Component { + constructor(props) { + super(); + + const action = props.viewSettings.get( + `${props.provider}Action`, + "deposit" + ); + this.state = { + activeCoin: this._getActiveCoin(props, {action}), + action + }; + } + + _getActiveCoin(props, state) { + let cachedCoin = props.viewSettings.get( + `activeCoin_citadel_${state.action}`, + null + ); + let firstTimeCoin = null; + if (state.action == "deposit") { + firstTimeCoin = "XMR"; + } + if (state.action == "withdraw") { + firstTimeCoin = "XMR"; + } + let activeCoin = cachedCoin ? cachedCoin : firstTimeCoin; + return activeCoin; + } + + componentWillReceiveProps(nextProps) { + if (nextProps.provider !== this.props.provider) { + this.setState({ + activeCoin: this._getActiveCoin(nextProps, this.state.action) + }); + } + } + + onSelectCoin(e) { + this.setState({ + activeCoin: e.target.value + }); + + let setting = {}; + setting[`activeCoin_citadel_${this.state.action}`] = e.target.value; + SettingsActions.changeViewSetting(setting); + } + + changeAction(type) { + let activeCoin = this._getActiveCoin(this.props, {action: type}); + + this.setState({ + action: type, + activeCoin: activeCoin + }); + + SettingsActions.changeViewSetting({ + [`${this.props.provider}Action`]: type + }); + } + + render() { + let {coins, account, provider} = this.props; + let {activeCoin, action} = this.state; + + if (!coins.length) { + return ; + } + + let filteredCoins = coins.filter(a => { + if (!a || !a.symbol) { + return false; + } else { + return action === "deposit" + ? a.depositAllowed + : a.withdrawalAllowed; + } + }); + + let coinOptions = filteredCoins + .map(coin => { + let option = + action === "deposit" + ? coin.backingCoinType.toUpperCase() + : coin.symbol; + return ( + + ); + }) + .filter(a => { + return a !== null; + }); + + let coin = filteredCoins.filter(coin => { + return action === "deposit" + ? coin.backingCoinType.toUpperCase() === activeCoin + : coin.symbol === activeCoin; + })[0]; + + if (!coin) coin = filteredCoins[0]; + + let isDeposit = this.state.action === "deposit"; + + let supportUrl = "https://citadel.li"; + + return ( +
+
+
+
+ + +
+
+ +
+ +
+ +
+
+
+ + {!coin ? null : ( +
+
+ + + +
+ + {coin && coin.symbol ? ( + + {({asset, to, fromAccount}) => { + return ( + + } + customFilter={{ + fields: [ + "to", + "from", + "asset_id" + ], + values: { + to: to.get("id"), + from: fromAccount.get("id"), + asset_id: asset.get("id") + } + }} + /> + ); + }} + + ) : null} +
+ )} +
+ ); + } +} + +export default connect( + CitadelGateway, + { + listenTo() { + return [SettingsStore]; + }, + getProps() { + return { + viewSettings: SettingsStore.getState().viewSettings + }; + } + } +); diff --git a/app/components/DepositWithdraw/citadel/CitadelGatewayDepositRequest.jsx b/app/components/DepositWithdraw/citadel/CitadelGatewayDepositRequest.jsx new file mode 100755 index 0000000000..f6e50ca063 --- /dev/null +++ b/app/components/DepositWithdraw/citadel/CitadelGatewayDepositRequest.jsx @@ -0,0 +1,594 @@ +import React from "react"; +import Translate from "react-translate-component"; +import {ChainStore} from "bitsharesjs/es"; +import ChainTypes from "components/Utility/ChainTypes"; +import BindToChainState from "components/Utility/BindToChainState"; +import WithdrawModalCitadel from "./WithdrawModalCitadel"; +import BaseModal from "../../Modal/BaseModal"; +import ZfApi from "react-foundation-apps/src/utils/foundation-api"; +import AccountBalance from "../../Account/AccountBalance"; +import AssetName from "components/Utility/AssetName"; +import LinkToAccountById from "components/Utility/LinkToAccountById"; +import {requestDepositAddress, getDepositAddress} from "common/gatewayMethods"; +import {blockTradesAPIs, openledgerAPIs, citadelAPIs} from "api/apiConfig"; +import LoadingIndicator from "components/LoadingIndicator"; +import counterpart from "counterpart"; +import PropTypes from "prop-types"; + +class CitadelGatewayDepositRequest extends React.Component { + static propTypes = { + url: PropTypes.string, + gateway: PropTypes.string, + deposit_coin_type: PropTypes.string, + deposit_asset_name: PropTypes.string, + deposit_account: PropTypes.string, + receive_coin_type: PropTypes.string, + account: ChainTypes.ChainAccount, + issuer_account: ChainTypes.ChainAccount, + deposit_asset: PropTypes.string, + deposit_wallet_type: PropTypes.string, + receive_asset: ChainTypes.ChainAsset, + deprecated_in_favor_of: ChainTypes.ChainAsset, + deprecated_message: PropTypes.string, + action: PropTypes.string, + supports_output_memos: PropTypes.bool.isRequired + }; + + static defaultProps = { + autosubscribe: false + }; + + constructor(props) { + super(props); + + let urls = { + blocktrades: blockTradesAPIs.BASE, + openledger: openledgerAPIs.BASE, + citadel: citadelAPIs.BASE + }; + + this.state = { + receive_address: null, + url: props.url || urls[props.gateway], + loading: false, + emptyAddressDeposit: false + }; + + this.addDepositAddress = this.addDepositAddress.bind(this); + this._copy = this._copy.bind(this); + document.addEventListener("copy", this._copy); + } + + _copy(e) { + try { + if (this.state.clipboardText) + e.clipboardData.setData("text/plain", this.state.clipboardText); + else + e.clipboardData.setData( + "text/plain", + counterpart + .translate("gateway.use_copy_button") + .toUpperCase() + ); + e.preventDefault(); + } catch (err) { + console.error(err); + } + } + + _getDepositObject() { + return { + inputCoinType: this.props.deposit_coin_type, + outputCoinType: this.props.receive_coin_type, + outputAddress: this.props.account.get("name"), + url: this.state.url, + stateCallback: this.addDepositAddress + }; + } + + componentWillMount() { + getDepositAddress({ + coin: this.props.receive_coin_type, + account: this.props.account.get("name"), + stateCallback: this.addDepositAddress + }); + } + + componentWillUnmount() { + document.removeEventListener("copy", this._copy); + } + + componentWillReceiveProps(np) { + if (np.account !== this.props.account) { + getDepositAddress({ + coin: np.receive_coin_type, + account: np.account.get("name"), + stateCallback: this.addDepositAddress + }); + } + } + + addDepositAddress(receive_address) { + if (receive_address.error) { + receive_address.error.message === "no_address" + ? this.setState({emptyAddressDeposit: true}) + : this.setState({emptyAddressDeposit: false}); + } + + this.setState({receive_address}); + this.setState({ + loading: false + }); + this.setState({receive_address}); + } + + requestDepositAddressLoad() { + this.setState({ + loading: true, + emptyAddressDeposit: false + }); + requestDepositAddress(this._getDepositObject()); + } + + getWithdrawModalId() { + // console.log( "this.props.issuer: ", this.props.issuer_account.toJS() ) + // console.log( "this.receive_asset.issuer: ", this.props.receive_asset.toJS() ) + return ( + "withdraw_asset_" + + this.props.issuer_account.get("name") + + "_" + + this.props.receive_asset.get("symbol") + ); + } + + onWithdraw() { + ZfApi.publish(this.getWithdrawModalId(), "open"); + } + + toClipboard(clipboardText) { + try { + this.setState({clipboardText}, () => { + document.execCommand("copy"); + }); + } catch (err) { + console.error(err); + } + } + + render() { + const isDeposit = this.props.action === "deposit"; + let emptyRow = ; + if ( + !this.props.account || + !this.props.issuer_account || + !this.props.receive_asset + ) + return emptyRow; + + let account_balances_object = this.props.account.get("balances"); + + const {gateFee} = this.props; + + let balance = "0 " + this.props.receive_asset.get("symbol"); + if (this.props.deprecated_in_favor_of) { + let has_nonzero_balance = false; + let balance_object_id = account_balances_object.get( + this.props.receive_asset.get("id") + ); + if (balance_object_id) { + let balance_object = ChainStore.getObject(balance_object_id); + if (balance_object) { + let balance = balance_object.get("balance"); + if (balance != 0) has_nonzero_balance = true; + } + } + if (!has_nonzero_balance) return emptyRow; + } + + let receive_address = this.state.receive_address; + let {emptyAddressDeposit} = this.state; + let indicatorButtonAddr = this.state.loading; + + if (!receive_address) { + return ( +
+ +
+ ); + } + + let withdraw_modal_id = this.getWithdrawModalId(); + let deposit_address_fragment = null; + let deposit_memo = null; + + let clipboardText = ""; + let memoText; + if (this.props.deposit_account) { + deposit_address_fragment = ( + {this.props.deposit_account} + ); + clipboardText = + this.props.receive_coin_type + + ":" + + this.props.account.get("name"); + deposit_memo = {clipboardText}; + var withdraw_memo_prefix = this.props.deposit_coin_type + ":"; + } else { + if (receive_address.memo) { + // This is a client that uses a deposit memo (like ethereum), we need to display both the address and the memo they need to send + memoText = receive_address.memo; + clipboardText = receive_address.address; + deposit_address_fragment = ( + {receive_address.address} + ); + deposit_memo = {receive_address.memo}; + } else { + // This is a client that uses unique deposit addresses to select the output + clipboardText = receive_address.address; + deposit_address_fragment = ( + {receive_address.address} + ); + } + var withdraw_memo_prefix = ""; + } + + if ( + !this.props.isAvailable || + ((isDeposit && !this.props.deposit_account && !receive_address) || + (receive_address && receive_address.address === "unknown")) + ) { + return ( +
+ +
+ ); + } + + if (isDeposit) { + return ( +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ {this.props.deposit_asset} +
+ +
+ +
+ +
+ + : + + +
+
+
+
+ + + +
+ {emptyAddressDeposit ? ( + + ) : ( + deposit_address_fragment + )} +
+ {deposit_memo && ( + memo: {deposit_memo} + )} +
+
+ {deposit_address_fragment ? ( +
+ +
+ ) : null} + {memoText ? ( +
+ +
+ ) : null} + +
+ + +
+
+
+ ); + } else { + return ( +
+
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ {this.props.deposit_asset} +
+ +
+ + : + + +
+
+ + {/*

When you withdraw {this.props.receive_asset.get("symbol")}, you will receive {this.props.deposit_asset} at a 1:1 ratio (minus fees).

*/} +
+
+ + +
+ +
+
+ +
+
+ +
+
+
+ ); + } + } +} + +export default BindToChainState(CitadelGatewayDepositRequest); diff --git a/app/components/DepositWithdraw/citadel/WithdrawModalCitadel.jsx b/app/components/DepositWithdraw/citadel/WithdrawModalCitadel.jsx new file mode 100755 index 0000000000..fb8bd87900 --- /dev/null +++ b/app/components/DepositWithdraw/citadel/WithdrawModalCitadel.jsx @@ -0,0 +1,856 @@ +import React from "react"; +import Trigger from "react-foundation-apps/src/trigger"; +import Translate from "react-translate-component"; +import ChainTypes from "components/Utility/ChainTypes"; +import BindToChainState from "components/Utility/BindToChainState"; +import utils from "common/utils"; +import BalanceComponent from "components/Utility/BalanceComponent"; +import counterpart from "counterpart"; +import AmountSelector from "components/Utility/AmountSelector"; +import AccountActions from "actions/AccountActions"; +import ZfApi from "react-foundation-apps/src/utils/foundation-api"; +import {validateAddress, WithdrawAddresses} from "common/gatewayMethods"; +import {ChainStore} from "bitsharesjs/es"; +import Modal from "react-foundation-apps/src/modal"; +import {checkFeeStatusAsync, checkBalance} from "common/trxHelper"; +import {debounce} from "lodash-es"; +import {Price, Asset} from "common/MarketClasses"; +import PropTypes from "prop-types"; + +class WithdrawModalCitadel extends React.Component { + static propTypes = { + account: ChainTypes.ChainAccount.isRequired, + issuer: ChainTypes.ChainAccount.isRequired, + asset: ChainTypes.ChainAsset.isRequired, + output_coin_name: PropTypes.string.isRequired, + output_coin_symbol: PropTypes.string.isRequired, + output_coin_type: PropTypes.string.isRequired, + url: PropTypes.string, + output_wallet_type: PropTypes.string, + output_supports_memos: PropTypes.bool.isRequired, + amount_to_withdraw: PropTypes.string, + balance: ChainTypes.ChainObject + }; + + constructor(props) { + super(props); + + this.state = { + withdraw_amount: this.props.amount_to_withdraw, + withdraw_address: WithdrawAddresses.getLast( + props.output_wallet_type + ), + withdraw_address_check_in_progress: true, + withdraw_address_is_valid: null, + options_is_valid: false, + confirmation_is_valid: false, + withdraw_address_selected: WithdrawAddresses.getLast( + props.output_wallet_type + ), + memo: "", + withdraw_address_first: true, + empty_withdraw_value: false, + from_account: props.account, + fee_asset_id: "1.3.0", + feeStatus: {} + }; + + this._validateAddress(this.state.withdraw_address, props); + + this._checkBalance = this._checkBalance.bind(this); + this._updateFee = debounce(this._updateFee.bind(this), 250); + } + + componentWillMount() { + this._updateFee(); + this._checkFeeStatus(); + } + + componentWillUnmount() { + this.unMounted = true; + } + + componentWillReceiveProps(np) { + if ( + np.account !== this.state.from_account && + np.account !== this.props.account + ) { + this.setState( + { + from_account: np.account, + feeStatus: {}, + fee_asset_id: "1.3.0", + feeAmount: new Asset({amount: 0}) + }, + () => { + this._updateFee(); + this._checkFeeStatus(); + } + ); + } + } + + _updateFee(state = this.state) { + let {fee_asset_id, from_account} = state; + const {fee_asset_types} = this._getAvailableAssets(state); + if ( + fee_asset_types.length === 1 && + fee_asset_types[0] !== fee_asset_id + ) { + fee_asset_id = fee_asset_types[0]; + } + + if (!from_account) return null; + checkFeeStatusAsync({ + accountID: from_account.get("id"), + feeID: fee_asset_id, + options: ["price_per_kbyte"], + data: { + type: "memo", + content: + this.props.output_coin_type + + ":" + + state.withdraw_address + + (state.memo ? ":" + state.memo : "") + } + }).then(({fee, hasBalance, hasPoolBalance}) => { + if (this.unMounted) return; + + this.setState( + { + feeAmount: fee, + hasBalance, + hasPoolBalance, + error: !hasBalance || !hasPoolBalance + }, + this._checkBalance + ); + }); + } + + _checkFeeStatus(state = this.state) { + let account = state.from_account; + if (!account) return; + + const {fee_asset_types: assets} = this._getAvailableAssets(state); + // const assets = ["1.3.0", this.props.asset.get("id")]; + let feeStatus = {}; + let p = []; + assets.forEach(a => { + p.push( + checkFeeStatusAsync({ + accountID: account.get("id"), + feeID: a, + options: ["price_per_kbyte"], + data: { + type: "memo", + content: + this.props.output_coin_type + + ":" + + state.withdraw_address + + (state.memo ? ":" + state.memo : "") + } + }) + ); + }); + Promise.all(p) + .then(status => { + assets.forEach((a, idx) => { + feeStatus[a] = status[idx]; + }); + if (!utils.are_equal_shallow(state.feeStatus, feeStatus)) { + this.setState({ + feeStatus + }); + } + this._checkBalance(); + }) + .catch(err => { + console.error(err); + }); + } + + onMemoChanged(e) { + this.setState({memo: e.target.value}, this._updateFee); + } + + onWithdrawAmountChange({amount}) { + this.setState( + { + withdraw_amount: amount, + empty_withdraw_value: + amount !== undefined && !parseFloat(amount) + }, + this._checkBalance + ); + } + + onSelectChanged(index) { + let new_withdraw_address = WithdrawAddresses.get( + this.props.output_wallet_type + )[index]; + WithdrawAddresses.setLast({ + wallet: this.props.output_wallet_type, + address: new_withdraw_address + }); + + this.setState( + { + withdraw_address_selected: new_withdraw_address, + options_is_valid: false, + withdraw_address: new_withdraw_address, + withdraw_address_check_in_progress: true, + withdraw_address_is_valid: null + }, + this._updateFee + ); + this._validateAddress(new_withdraw_address); + } + + onWithdrawAddressChanged(e) { + let new_withdraw_address = e.target.value.trim(); + + this.setState( + { + withdraw_address: new_withdraw_address, + withdraw_address_check_in_progress: true, + withdraw_address_selected: new_withdraw_address, + withdraw_address_is_valid: null + }, + this._updateFee + ); + this._validateAddress(new_withdraw_address); + } + + _validateAddress(new_withdraw_address, props = this.props) { + validateAddress({ + url: props.url, + walletType: props.output_wallet_type, + newAddress: new_withdraw_address + }).then(isValid => { + if (this.state.withdraw_address === new_withdraw_address) { + this.setState({ + withdraw_address_check_in_progress: false, + withdraw_address_is_valid: isValid + }); + } + }); + } + + _checkBalance() { + const {feeAmount, withdraw_amount} = this.state; + const {asset, balance} = this.props; + if (!balance || !feeAmount) return; + const hasBalance = checkBalance( + withdraw_amount, + asset, + feeAmount, + balance + ); + if (hasBalance === null) return; + this.setState({balanceError: !hasBalance}); + return hasBalance; + } + + onSubmit() { + if ( + !this.state.withdraw_address_check_in_progress && + (this.state.withdraw_address && + this.state.withdraw_address.length) && + this.state.withdraw_amount !== null + ) { + if (!this.state.withdraw_address_is_valid) { + ZfApi.publish(this.getWithdrawModalId(), "open"); + } else if (parseFloat(this.state.withdraw_amount) > 0) { + if (!WithdrawAddresses.has(this.props.output_wallet_type)) { + let withdrawals = []; + withdrawals.push(this.state.withdraw_address); + WithdrawAddresses.set({ + wallet: this.props.output_wallet_type, + addresses: withdrawals + }); + } else { + let withdrawals = WithdrawAddresses.get( + this.props.output_wallet_type + ); + if ( + withdrawals.indexOf(this.state.withdraw_address) == -1 + ) { + withdrawals.push(this.state.withdraw_address); + WithdrawAddresses.set({ + wallet: this.props.output_wallet_type, + addresses: withdrawals + }); + } + } + WithdrawAddresses.setLast({ + wallet: this.props.output_wallet_type, + address: this.state.withdraw_address + }); + let asset = this.props.asset; + + const {feeAmount} = this.state; + + const amount = parseFloat( + String.prototype.replace.call( + this.state.withdraw_amount, + /,/g, + "" + ) + ); + const gateFee = + typeof this.props.gateFee != "undefined" + ? parseFloat( + String.prototype.replace.call( + this.props.gateFee, + /,/g, + "" + ) + ) + : 0.0; + + let sendAmount = new Asset({ + asset_id: asset.get("id"), + precision: asset.get("precision"), + real: amount + }); + + let balanceAmount = new Asset({ + asset_id: asset.get("id"), + precision: asset.get("precision"), + real: 0 + }); + + if (typeof this.props.balance != "undefined") { + balanceAmount = sendAmount.clone( + this.props.balance.get("balance") + ); + } + + const gateFeeAmount = new Asset({ + asset_id: asset.get("id"), + precision: asset.get("precision"), + real: gateFee + }); + + sendAmount.plus(gateFeeAmount); + + /* Insufficient balance */ + if (balanceAmount.lt(sendAmount)) { + sendAmount = balanceAmount; + } + + AccountActions.transfer( + this.props.account.get("id"), + this.props.issuer.get("id"), + sendAmount.getAmount(), + asset.get("id"), + this.props.output_coin_type + + ":" + + this.state.withdraw_address + + (this.state.memo + ? ":" + new Buffer(this.state.memo, "utf-8") + : ""), + null, + feeAmount ? feeAmount.asset_id : "1.3.0" + ); + + this.setState({ + empty_withdraw_value: false + }); + } else { + this.setState({ + empty_withdraw_value: true + }); + } + } + } + + onSubmitConfirmation() { + ZfApi.publish(this.getWithdrawModalId(), "close"); + + if (!WithdrawAddresses.has(this.props.output_wallet_type)) { + let withdrawals = []; + withdrawals.push(this.state.withdraw_address); + WithdrawAddresses.set({ + wallet: this.props.output_wallet_type, + addresses: withdrawals + }); + } else { + let withdrawals = WithdrawAddresses.get( + this.props.output_wallet_type + ); + if (withdrawals.indexOf(this.state.withdraw_address) == -1) { + withdrawals.push(this.state.withdraw_address); + WithdrawAddresses.set({ + wallet: this.props.output_wallet_type, + addresses: withdrawals + }); + } + } + WithdrawAddresses.setLast({ + wallet: this.props.output_wallet_type, + address: this.state.withdraw_address + }); + let asset = this.props.asset; + let precision = utils.get_asset_precision(asset.get("precision")); + let amount = String.prototype.replace.call( + this.state.withdraw_amount, + /,/g, + "" + ); + + const {feeAmount} = this.state; + + AccountActions.transfer( + this.props.account.get("id"), + this.props.issuer.get("id"), + parseInt(amount * precision, 10), + asset.get("id"), + this.props.output_coin_type + + ":" + + this.state.withdraw_address + + (this.state.memo + ? ":" + new Buffer(this.state.memo, "utf-8") + : ""), + null, + feeAmount ? feeAmount.asset_id : "1.3.0" + ); + } + + onDropDownList() { + if (WithdrawAddresses.has(this.props.output_wallet_type)) { + if (this.state.options_is_valid === false) { + this.setState({options_is_valid: true}); + this.setState({withdraw_address_first: false}); + } + + if (this.state.options_is_valid === true) { + this.setState({options_is_valid: false}); + } + } + } + + getWithdrawModalId() { + return "confirmation"; + } + + onAccountBalance() { + const {feeAmount} = this.state; + if ( + Object.keys(this.props.account.get("balances").toJS()).includes( + this.props.asset.get("id") + ) + ) { + let total = new Asset({ + amount: this.props.balance.get("balance"), + asset_id: this.props.asset.get("id"), + precision: this.props.asset.get("precision") + }); + + // Subtract the fee if it is using the same asset + if (total.asset_id === feeAmount.asset_id) { + total.minus(feeAmount); + } + + this.setState( + { + withdraw_amount: total.getAmount({real: true}), + empty_withdraw_value: false + }, + this._checkBalance + ); + } + } + + setNestedRef(ref) { + this.nestedRef = ref; + } + + onFeeChanged({asset}) { + this.setState( + { + fee_asset_id: asset.get("id") + }, + this._updateFee + ); + } + + _getAvailableAssets(state = this.state) { + const {from_account, feeStatus} = state; + function hasFeePoolBalance(id) { + if (feeStatus[id] === undefined) return true; + return feeStatus[id] && feeStatus[id].hasPoolBalance; + } + + function hasBalance(id) { + if (feeStatus[id] === undefined) return true; + return feeStatus[id] && feeStatus[id].hasBalance; + } + + let fee_asset_types = []; + if (!(from_account && from_account.get("balances"))) { + return {fee_asset_types}; + } + let account_balances = state.from_account.get("balances").toJS(); + fee_asset_types = Object.keys(account_balances).sort(utils.sortID); + for (let key in account_balances) { + let asset = ChainStore.getObject(key); + let balanceObject = ChainStore.getObject(account_balances[key]); + if (balanceObject && balanceObject.get("balance") === 0) { + if (fee_asset_types.indexOf(key) !== -1) { + fee_asset_types.splice(fee_asset_types.indexOf(key), 1); + } + } + + if (asset) { + // Remove any assets that do not have valid core exchange rates + let priceIsValid = false, + p; + try { + p = new Price({ + base: new Asset( + asset + .getIn([ + "options", + "core_exchange_rate", + "base" + ]) + .toJS() + ), + quote: new Asset( + asset + .getIn([ + "options", + "core_exchange_rate", + "quote" + ]) + .toJS() + ) + }); + priceIsValid = p.isValid(); + } catch (err) { + priceIsValid = false; + } + + if (asset.get("id") !== "1.3.0" && !priceIsValid) { + fee_asset_types.splice(fee_asset_types.indexOf(key), 1); + } + } + } + + fee_asset_types = fee_asset_types.filter(a => { + return hasFeePoolBalance(a) && hasBalance(a); + }); + + return {fee_asset_types}; + } + + render() { + let {withdraw_address_selected, memo} = this.state; + let storedAddress = WithdrawAddresses.get( + this.props.output_wallet_type + ); + let balance = null; + + let account_balances = this.props.account.get("balances").toJS(); + let asset_types = Object.keys(account_balances); + + let withdrawModalId = this.getWithdrawModalId(); + let invalid_address_message = null; + let options = null; + let confirmation = null; + + if (this.state.options_is_valid) { + options = ( +
+ {storedAddress.map(function(name, index) { + return ( + + {name} + + ); + }, this)} +
+ ); + } + + if ( + !this.state.withdraw_address_check_in_progress && + (this.state.withdraw_address && this.state.withdraw_address.length) + ) { + if (!this.state.withdraw_address_is_valid) { + invalid_address_message = ( +
+ +
+ ); + confirmation = ( + + + + × + + +
+ +
+
+ + + + + + +
+
+ ); + } + // if (this.state.withdraw_address_is_valid) + // invalid_address_message = ; + // else + // invalid_address_message = ; + } + + let tabIndex = 1; + let withdraw_memo = null; + + if (this.props.output_supports_memos) { + withdraw_memo = ( +
+ +