From 5ddb672580161fe918daf96a4ad40fa0a6f28693 Mon Sep 17 00:00:00 2001 From: ryanml Date: Mon, 13 Jul 2020 00:00:07 -0700 Subject: [PATCH] Production Execution Flow --- browser/extensions/api/gemini_api.cc | 29 +++++ browser/extensions/api/gemini_api.h | 12 +++ browser/ui/webui/brave_webui_source.cc | 9 +- common/extensions/api/gemini.json | 41 +++++++ .../components/default/gemini/index.tsx | 102 ++++++++++++++++-- .../components/default/gemini/style.ts | 30 ++++++ .../reducers/gemini_reducer.ts | 6 +- components/definitions/chromel.d.ts | 1 + components/gemini/browser/gemini_service.cc | 43 ++++++++ components/gemini/browser/gemini_service.h | 11 ++ .../resources/brave_components_strings.grd | 7 +- 11 files changed, 280 insertions(+), 11 deletions(-) diff --git a/browser/extensions/api/gemini_api.cc b/browser/extensions/api/gemini_api.cc index 9d01f7bbef9..2ea23be23d6 100644 --- a/browser/extensions/api/gemini_api.cc +++ b/browser/extensions/api/gemini_api.cc @@ -227,5 +227,34 @@ void GeminiGetOrderQuoteFunction::OnOrderQuoteResult( Respond(OneArgument(std::move(quote))); } +ExtensionFunction::ResponseAction +GeminiExecuteOrderFunction::Run() { + if (!IsGeminiAPIAvailable(browser_context())) { + return RespondNow(Error("Not available in Tor/incognito/guest profile")); + } + + std::unique_ptr params( + gemini::ExecuteOrder::Params::Create(*args_)); + EXTENSION_FUNCTION_VALIDATE(params.get()); + + auto* service = GetGeminiService(browser_context()); + bool balance_success = service->ExecuteOrder( + params->symbol, params->side, params->quantity, + params->price, params->fee, params->quote_id, + base::BindOnce( + &GeminiExecuteOrderFunction::OnOrderExecuted, + this)); + + if (!balance_success) { + return RespondNow(Error("Could not send request to execute order")); + } + + return RespondLater(); +} + +void GeminiExecuteOrderFunction::OnOrderExecuted(bool success) { + Respond(OneArgument(std::make_unique(success))); +} + } // namespace api } // namespace extensions diff --git a/browser/extensions/api/gemini_api.h b/browser/extensions/api/gemini_api.h index 06293ac6434..5fbee21a7e7 100644 --- a/browser/extensions/api/gemini_api.h +++ b/browser/extensions/api/gemini_api.h @@ -104,6 +104,18 @@ class GeminiGetOrderQuoteFunction : ResponseAction Run() override; }; +class GeminiExecuteOrderFunction : + public ExtensionFunction { + public: + DECLARE_EXTENSION_FUNCTION("gemini.executeOrder", UNKNOWN) + + protected: + ~GeminiExecuteOrderFunction() override {} + void OnOrderExecuted(bool success); + + ResponseAction Run() override; +}; + } // namespace api } // namespace extensions diff --git a/browser/ui/webui/brave_webui_source.cc b/browser/ui/webui/brave_webui_source.cc index 45ea55f27a7..71c6f8075af 100644 --- a/browser/ui/webui/brave_webui_source.cc +++ b/browser/ui/webui/brave_webui_source.cc @@ -306,7 +306,7 @@ void CustomizeWebUIHTMLSource(const std::string &name, { "geminiWidgetConnectTitle", IDS_GEMINI_WIDGET_CONNECT_TITLE }, { "geminiWidgetConnectCopy", IDS_GEMINI_WIDGET_CONNECT_COPY }, { "geminiWidgetConnectButton", IDS_GEMINI_WIDGET_CONNECT_BUTTON }, - { "geminiWidgetFailedTrade", IDS_GEMINI_WIDGET_FAILED_TRADE }, + { "geminiWidgetFailedTrade", IDS_GEMINI_WIDGET_FAILED_TRADE }, { "geminiWidgetInsufficientFunds", IDS_BINANCE_WIDGET_INSUFFICIENT_FUNDS }, // NOLINT { "geminiWidgetError", IDS_GEMINI_WIDGET_ERROR }, { "geminiWidgetConfirmTrade", IDS_GEMINI_WIDGET_CONFIRM_TRADE }, @@ -320,6 +320,13 @@ void CustomizeWebUIHTMLSource(const std::string &name, { "geminiWidgetDepositLabel", IDS_BINANCE_WIDGET_DEPOSIT_LABEL }, { "geminiWidgetTradeLabel", IDS_GEMINI_WIDGET_TRADE_LABEL }, { "geminiWidgetBalanceLabel", IDS_GEMINI_WIDGET_BALANCE_LABEL }, + { "geminiWidgetBuying", IDS_GEMINI_WIDGET_BUYING }, + { "geminiWidgetSelling", IDS_GEMINI_WIDGET_SELLING }, + { "geminiWidgetContinue", IDS_BINANCE_WIDGET_CONTINUE }, + { "geminiWidgetBought", IDS_GEMINI_WIDGET_BOUGHT }, + { "geminiWidgetSold", IDS_GEMINI_WIDGET_SOLD }, + { "geminiWidgetFee", IDS_BINANCE_WIDGET_FEE }, + { "geminiWidgetPrice", IDS_GEMINI_WIDGET_PRICE } } }, { std::string("wallet"), { diff --git a/common/extensions/api/gemini.json b/common/extensions/api/gemini.json index 1a03c9f642b..fb93de7141f 100644 --- a/common/extensions/api/gemini.json +++ b/common/extensions/api/gemini.json @@ -172,6 +172,47 @@ ] } ] + }, + { + "name": "executeOrder", + "type": "function", + "description": "Gets a quote for a given order", + "parameters": [ + { + "type": "string", + "name": "symbol" + }, + { + "type": "string", + "name": "side" + }, + { + "type": "string", + "name": "quantity" + }, + { + "type": "string", + "name": "price" + }, + { + "type": "string", + "name": "fee" + }, + { + "type": "number", + "name": "quoteId" + }, + { + "type": "function", + "name": "callback", + "parameters": [ + { + "type": "boolean", + "name": "success" + } + ] + } + ] } ], "types": [ diff --git a/components/brave_new_tab_ui/components/default/gemini/index.tsx b/components/brave_new_tab_ui/components/default/gemini/index.tsx index 9883c57ae09..dee9afaf504 100644 --- a/components/brave_new_tab_ui/components/default/gemini/index.tsx +++ b/components/brave_new_tab_ui/components/default/gemini/index.tsx @@ -68,13 +68,19 @@ import { DisconnectCopy, InvalidWrapper, InvalidTitle, - InvalidCopy + InvalidCopy, + TradeInfoWrapper, + TradeInfoItem, + TradeItemLabel, + TradeValue, + StyledParty } from './style' import { SearchIcon, QRIcon, ShowIcon, - HideIcon + HideIcon, + PartyIcon } from '../exchangeWidget/shared-assets' import GeminiLogo from './assets/gemini-logo' import { CaratLeftIcon, CaratDownIcon } from 'brave-ui/components/icons' @@ -239,7 +245,8 @@ class Gemini extends React.PureComponent { currentQRAsset, insufficientFunds, tradeFailed, - showTradePreview + showTradePreview, + tradeSuccess } = this.state const { authInvalid, disconnectInProgress } = this.props @@ -253,6 +260,8 @@ class Gemini extends React.PureComponent { return this.renderInsufficientFundsView() } else if (tradeFailed) { return this.renderUnableToTradeView() + } else if (tradeSuccess) { + return this.renderTradeSuccess() } else if (showTradePreview) { return this.renderTradeConfirm() } @@ -331,6 +340,10 @@ class Gemini extends React.PureComponent { USDValue += price * assetBalance } + if ('USD' in accountBalances) { + USDValue += parseFloat(accountBalances['USD']) + } + return USDValue.toFixed(2) } @@ -579,13 +592,43 @@ class Gemini extends React.PureComponent { }) } - shouldShowTradePreview () { + processTrade = () => { + const { + currentTradeId, + currentTradeAsset, + currentTradeFee: fee, + currentTradePrice: price, + currentTradeMode: side, + currentTradeQuantityLive: quantity + } = this.state + const quoteId = parseInt(currentTradeId, 10) + const symbol = `${currentTradeAsset}usd`.toUpperCase() + chrome.gemini.executeOrder(symbol, side, quantity, price, fee, quoteId, (success: boolean) => { + if (success) { + this.setState({ tradeSuccess: true }) + } else { + this.setState({ tradeFailed: true }) + } + }) + } + + finishTrade = () => { + this.cancelTrade() + this.props.onSetSelectedView('balance') + } + + shouldShowTradePreview = () => { const { currentTradeMode, currentTradeAsset, currentTradeQuantity } = this.state const { accountBalances } = this.props + + if (!currentTradeQuantity || isNaN(parseFloat(currentTradeQuantity))) { + return + } + const compare = currentTradeMode === 'buy' ? (accountBalances['USD'] || '0') : (accountBalances[currentTradeAsset] || '0') @@ -661,21 +704,64 @@ class Gemini extends React.PureComponent { ) } + renderTradeSuccess = () => { + const { + currentTradeAsset, + currentTradeQuantityLive, + currentTradeMode + } = this.state + const quantity = this.formatCryptoBalance(currentTradeQuantityLive) + const actionLabel = currentTradeMode === 'buy' ? 'geminiWidgetBought' : 'geminiWidgetSold' + + return ( + + + + + + {`${getLocale(actionLabel)} ${quantity} ${currentTradeAsset}!`} + + + {getLocale('geminiWidgetContinue')} + + + ) + } + renderTradeConfirm = () => { const { - currentTradeQuantityLive, currentTradeFee, - currentTradeExpiryTime + currentTradeAsset, + currentTradeQuantityLive, + currentTradeMode, + currentTradeExpiryTime, + currentTradePrice } = this.state - console.log(currentTradeExpiryTime, currentTradeFee, currentTradeQuantityLive) + const tradeLabel = currentTradeMode === 'buy' ? 'geminiWidgetBuying' : 'geminiWidgetSelling' + const quantity = this.formatCryptoBalance(currentTradeQuantityLive) + const fee = this.formatCryptoBalance(currentTradeFee) return ( {getLocale('geminiWidgetConfirmTrade')} + + + {getLocale(tradeLabel)} + {`${quantity} ${currentTradeAsset}`} + + + {getLocale('geminiWidgetPrice')} + {`${currentTradePrice} USD`} + + + {getLocale('geminiWidgetFee')} + {`${fee} USD`} + + - + {`${getLocale('geminiWidgetConfirm')} (${currentTradeExpiryTime}s)`} diff --git a/components/brave_new_tab_ui/components/default/gemini/style.ts b/components/brave_new_tab_ui/components/default/gemini/style.ts index 412a9cd7925..b61809250f3 100644 --- a/components/brave_new_tab_ui/components/default/gemini/style.ts +++ b/components/brave_new_tab_ui/components/default/gemini/style.ts @@ -507,3 +507,33 @@ export const InvalidCopy = styled(DisconnectCopy)` export const InvalidWrapper = styled(DisconnectWrapper)` min-width: 244px; ` + +export const TradeItemLabel = styled<{}, 'span'>('span')` + float: left; + width: 45%; + text-align: left; + font-size: 15px; +` + +export const TradeValue = styled<{}, 'span'>('span')` + font-weight: bold; + float: right; + width: 55%; + text-align: right; + font-size: 15px; +` + +export const TradeInfoWrapper = styled('div')` + margin: 20px 0; + overflow-y: auto; +` + +export const TradeInfoItem = styled('div')` + margin: 5px 0; + overflow-y: hidden; + margin-top: ${p => p.isLast ? '15' : '5'}px; +` + +export const StyledParty = styled<{}, 'div'>('div')` + margin: 10px 0px; +` diff --git a/components/brave_new_tab_ui/reducers/gemini_reducer.ts b/components/brave_new_tab_ui/reducers/gemini_reducer.ts index 0cbda87f94f..c89a31b38ce 100644 --- a/components/brave_new_tab_ui/reducers/gemini_reducer.ts +++ b/components/brave_new_tab_ui/reducers/gemini_reducer.ts @@ -50,7 +50,11 @@ const geminiReducer: Reducer = (state: NewTab.State, a case types.SET_ACCOUNT_BALANCES: const { balances } = payload state = { ...state } - state.geminiState.accountBalances = balances + for (let balance in balances) { + if (balances[balance]) { + state.geminiState.accountBalances[balance] = balances[balance] + } + } break case types.ON_DEPOSIT_QR_FOR_ASSET: diff --git a/components/definitions/chromel.d.ts b/components/definitions/chromel.d.ts index ae8dc8d27dc..c558d512760 100644 --- a/components/definitions/chromel.d.ts +++ b/components/definitions/chromel.d.ts @@ -175,6 +175,7 @@ declare namespace chrome.gemini { const getDepositInfo: (asset: string, callback: (depositAddress: string, depositTag: string) => void) => {} const revokeToken: (callback: (success: boolean) => void) => {} const getOrderQuote: (side: string, symbol: string, spend: string, callback: (quote: any) => void) => {} + const executeOrder: (symbol: string, side: string, quantity: string, price: string, fee: string, quoteId: number, callback: (success: boolean) => void) => {} } declare namespace chrome.braveTogether { diff --git a/components/gemini/browser/gemini_service.cc b/components/gemini/browser/gemini_service.cc index 172c3ba179d..e74790c142a 100644 --- a/components/gemini/browser/gemini_service.cc +++ b/components/gemini/browser/gemini_service.cc @@ -81,6 +81,26 @@ namespace { return encoded_payload; } + std::string GetEncodedExecutePayload(std::string symbol, + std::string side, + std::string quantity, + std::string price, + std::string fee, + int quote_id) { + std::string encoded_payload; + std::string request_payload = "{\"request\": \"" + + std::string(api_path_execute_quote) + + "\",\"symbol\": \"" + symbol + + "\",\"side\": \"" + side + + "\",\"quantity\": \"" + quantity + + "\",\"price\": \"" + price + + "\",\"fee\": \"" + fee + + "\",\"quoteId\": \"" + std::to_string(quote_id) + + "\"}"; + base::Base64Encode(request_payload, &encoded_payload); + return encoded_payload; + } + } // namespace GeminiService::GeminiService(content::BrowserContext* context) @@ -259,6 +279,29 @@ void GeminiService::OnGetOrderQuote(GetOrderQuoteCallback callback, std::move(callback).Run(quote_id, quantity, fee, price); } +bool GeminiService::ExecuteOrder(const std::string& symbol, + const std::string& side, + const std::string& quantity, + const std::string& price, + const std::string& fee, + const int quote_id, + ExecuteOrderCallback callback) { + auto internal_callback = base::BindOnce(&GeminiService::OnOrderExecuted, + base::Unretained(this), std::move(callback)); + std::string payload = GetEncodedExecutePayload( + symbol, side, quantity, price, fee, quote_id); + GURL url = GetURLWithPath(api_host, api_path_execute_quote); + return OAuthRequest( + url, "POST", "", std::move(internal_callback), true, true, payload); +} + +void GeminiService::OnOrderExecuted(ExecuteOrderCallback callback, + const int status, const std::string& body, + const std::map& headers) { + bool success = (status >= 200 && status <= 299); + std::move(callback).Run(success); +} + bool GeminiService::SetAccessTokens(const std::string& access_token, const std::string& refresh_token) { access_token_ = access_token; diff --git a/components/gemini/browser/gemini_service.h b/components/gemini/browser/gemini_service.h index 56c00e3e5e6..fc6ed68fe8c 100644 --- a/components/gemini/browser/gemini_service.h +++ b/components/gemini/browser/gemini_service.h @@ -65,6 +65,7 @@ class GeminiService : public KeyedService { const std::string&, const std::string&, const std::string&)>; + using ExecuteOrderCallback = base::OnceCallback; std::string GetOAuthClientUrl(); void SetAuthToken(const std::string& auth_token); @@ -79,6 +80,13 @@ class GeminiService : public KeyedService { const std::string& symbol, const std::string& spend, GetOrderQuoteCallback callback); + bool ExecuteOrder(const std::string& symbol, + const std::string& side, + const std::string& quantity, + const std::string& price, + const std::string& fee, + const int quote_id, + ExecuteOrderCallback callback); private: base::SequencedTaskRunner* io_task_runner(); @@ -109,6 +117,9 @@ class GeminiService : public KeyedService { void OnGetOrderQuote(GetOrderQuoteCallback callback, const int status, const std::string& body, const std::map& headers); + void OnOrderExecuted(ExecuteOrderCallback callback, + const int status, const std::string& body, + const std::map& headers); bool OAuthRequest(const GURL& url, const std::string& method, const std::string& post_data, URLRequestCallback callback, diff --git a/components/resources/brave_components_strings.grd b/components/resources/brave_components_strings.grd index f9c0b711872..471ea9045cf 100644 --- a/components/resources/brave_components_strings.grd +++ b/components/resources/brave_components_strings.grd @@ -889,7 +889,7 @@ Purchase and trade with Gemini Enable a Gemini connection to view your Gemini account balance and trade crypto. - Connect to Gemini. + Connect to Gemini Your Gemini access token has expired and could not be refreshed. Please reconnect. Unable to perform trade Something went wrong @@ -898,6 +898,11 @@ Get a Quote Trade Balance + Buying + Selling + You bought + You sold + Price Crypto Wallets