feat(swap): add Swap v2 integration

This commit is contained in:
Anirudha Bose
2022-11-26 00:21:19 +05:30
parent 17f929c267
commit 3b3f2f29c4
20 changed files with 6921 additions and 119 deletions
@@ -971,7 +971,72 @@ constexpr webui::LocalizedString kLocalizedStrings[] = {
IDS_BRAVE_WALLET_ADD_ASSET_TOKEN_TAB_TITLE},
{"braveWalletAddAssetNftTabTitle",
IDS_BRAVE_WALLET_ADD_ASSET_NFT_TAB_TITLE},
{"braveWalletNftFetchingError", IDS_BRAVE_WALLET_NFT_FETCHING_ERROR}};
{"braveWalletNftFetchingError", IDS_BRAVE_WALLET_NFT_FETCHING_ERROR},
// Brave Swap
{"braveSwap", IDS_BRAVE_SWAP},
{"braveSwapReviewOrder", IDS_BRAVE_SWAP_REVIEW_ORDER},
{"braveSwapApproveToken", IDS_BRAVE_SWAP_APPROVE_TOKEN},
{"braveSwapInsufficientBalance", IDS_BRAVE_SWAP_INSUFFICIENT_BALANCE},
{"braveSwapSelectToken", IDS_BRAVE_SWAP_SELECT_TOKEN},
{"braveSwapHalf", IDS_BRAVE_SWAP_HALF},
{"braveSwapMax", IDS_BRAVE_SWAP_MAX},
{"braveSwapEnterAnAmount", IDS_BRAVE_SWAP_ENTER_AN_AMOUNT},
{"braveSwapFindingPrice", IDS_BRAVE_SWAP_FINDING_PRICE},
{"braveSwapBalance", IDS_BRAVE_SWAP_BALANCE},
{"braveSwapSelectAToken", IDS_BRAVE_SWAP_SELECT_A_TOKEN},
{"braveSwapShowTokensWithZeroBalances",
IDS_BRAVE_SWAP_SHOW_TOKENS_WITH_ZERO_BALANCES},
{"braveSwapHideTokensWithZeroBalances",
IDS_BRAVE_SWAP_HIDE_TOKENS_WITH_ZERO_BALANCES},
{"braveSwapSearchToken", IDS_BRAVE_SWAP_SEARCH_TOKEN},
{"braveSwapOption", IDS_BRAVE_SWAP_OPTION},
{"braveSwapRate", IDS_BRAVE_SWAP_RATE},
{"braveSwapPriceImpact", IDS_BRAVE_SWAP_PRICE_IMPACT},
{"braveSwapMinimumReceivedAfterSlippage",
IDS_BRAVE_SWAP_MINIMUM_RECEIVED_AFTER_SLIPPAGE},
{"braveSwapNetworkFee", IDS_BRAVE_SWAP_NETWORK_FEE},
{"braveSwapBraveFee", IDS_BRAVE_SWAP_BRAVE_FEE},
{"braveSwapFree", IDS_BRAVE_SWAP_FREE},
{"braveSwapLiquidityProvider", IDS_BRAVE_SWAP_LIQUIDITY_PROVIDER},
{"braveSwapSwapAndSend", IDS_BRAVE_SWAP_SWAP_AND_SEND},
{"braveSwapNoExtraFees", IDS_BRAVE_SWAP_NO_EXTRA_FEES},
{"braveSwapConfirmAddress", IDS_BRAVE_SWAP_CONFIRM_ADDRESS},
{"braveSwapAddressInputePlaceholder",
IDS_BRAVE_SWAP_ADDRESS_INPUT_PLACEHOLDER},
{"braveSwapSelectAccount", IDS_BRAVE_SWAP_SELECT_ACCOUNT},
{"braveSwapToAccount", IDS_BRAVE_SWAP_TO_ACCOUNT},
{"braveSwapToAddress", IDS_BRAVE_SWAP_TO_ADDRESS},
{"braveSwapSlow", IDS_BRAVE_SWAP_SLOW},
{"braveSwapAverage", IDS_BRAVE_SWAP_AVERAGE},
{"braveSwapFast", IDS_BRAVE_SWAP_FAST},
{"braveSwapGwei", IDS_BRAVE_SWAP_GWEI},
{"braveSwapSettings", IDS_BRAVE_SWAP_SETTINGS},
{"braveSwapSlippageTolerance", IDS_BRAVE_SWAP_SLIPPAGE_TOLERANCE},
{"braveSwapExchanges", IDS_BRAVE_SWAP_EXCHANGES},
{"braveSwapDirectRouteTitle", IDS_BRAVE_SWAP_DIRECT_ROUTE_TITLE},
{"braveSwapDirectRouteDescription",
IDS_BRAVE_SWAP_DIRECT_ROUTE_DESCRIPTION},
{"braveSwapNetworkFeeDescription", IDS_BRAVE_SWAP_NETWORK_FEE_DESCRIPTION},
{"braveSwapCoinGeckoCheaper", IDS_BRAVE_SWAP_COINGECKO_CHEAPER},
{"braveSwapCoinGeckoWithin", IDS_BRAVE_SWAP_COINGECKO_WITHIN},
{"braveSwapCoinGeckoExpensive", IDS_BRAVE_SWAP_COINGECKO_EXPENSIVE},
{"braveSwapAPI", IDS_BRAVE_SWAP_API},
{"braveSwapName", IDS_BRAVE_SWAP_NAME},
{"braveSwapConnectWallet", IDS_BRAVE_SWAP_CONNECT_WALLET},
{"braveSwapDisconnectWallet", IDS_BRAVE_SWAP_DISCONNECT_WALLET},
{"braveSwapSwitchNetwork", IDS_BRAVE_SWAP_SWITCH_NETWORK},
{"braveSwapAdvanced", IDS_BRAVE_SWAP_ADVANCED},
{"braveSwapAccounts", IDS_BRAVE_SWAP_ACCOUNTS},
{"braveSwapWallet", IDS_BRAVE_SWAP_WALLET},
{"braveSwapBest", IDS_BRAVE_SWAP_BEST},
{"braveSwapHelpCenter", IDS_BRAVE_SWAP_HELP_CENTER},
{"braveSwapPrivacyPolicy", IDS_BRAVE_SWAP_PRIVACY_POLICY},
{"braveSwapPrivacyDescription", IDS_BRAVE_SWAP_PRIVACY_DESCRIPTION},
{"braveSwapZeroXDisclaimer", IDS_BRAVE_SWAP_ZERO_EX_DISCLAIMER},
{"braveSwapJupiterDisclaimer", IDS_BRAVE_SWAP_JUPITER_DISCLAIMER},
{"braveSwapZeroXPrivacy", IDS_BRAVE_SWAP_ZERO_EX_PRIVACY},
{"braveSwapJupiterPrivacy", IDS_BRAVE_SWAP_JUPITER_PRIVACY}};
// 0x swap constants
constexpr char kGoerliSwapBaseAPIURL[] = "https://goerli.api.0x.org/";
@@ -41,7 +41,22 @@ mojom::SwapResponsePtr ParseSwapResponse(const std::string& json,
// "sources":[...],
// "allowanceTarget":"0xdef1c0ded9bec7f1a1670819833240f027b25eff",
// "sellTokenToEthRate":"1900.44962824532464391",
// "buyTokenToEthRate":"1"
// "buyTokenToEthRate":"1",
// "estimatedPriceImpact": "0.7232",
// "sources": [
// {
// "name": "0x",
// "proportion": "0",
// },
// {
// "name": "Uniswap_V2",
// "proportion": "1",
// },
// {
// "name": "Curve",
// "proportion": "0",
// }
// ]
// }
absl::optional<base::Value> records_v =
@@ -89,6 +104,14 @@ mojom::SwapResponsePtr ParseSwapResponse(const std::string& json,
swap_response_value->sell_token_to_eth_rate;
swap_response->buy_token_to_eth_rate =
swap_response_value->buy_token_to_eth_rate;
swap_response->estimated_price_impact =
swap_response_value->estimated_price_impact;
for (const auto& source_value : swap_response_value->sources) {
swap_response->sources.push_back(
mojom::ZeroExSource::New(source_value.name, source_value.proportion));
}
return swap_response;
}
@@ -80,7 +80,14 @@ TEST(SwapResponseParserUnitTest, ParsePriceQuote) {
"sources":[],
"allowanceTarget":"0xdef1c0ded9bec7f1a1670819833240f027b25eff",
"sellTokenToEthRate":"1900.44962824532464391",
"buyTokenToEthRate":"1"
"buyTokenToEthRate":"1",
"estimatedPriceImpact": "0.7232",
"sources": [
{
"name": "Uniswap_V2",
"proportion": "1"
}
]
}
)");
mojom::SwapResponsePtr swap_response = ParseSwapResponse(json, false);
@@ -110,6 +117,10 @@ TEST(SwapResponseParserUnitTest, ParsePriceQuote) {
"0xdef1c0ded9bec7f1a1670819833240f027b25eff");
ASSERT_EQ(swap_response->sell_token_to_eth_rate, "1900.44962824532464391");
ASSERT_EQ(swap_response->buy_token_to_eth_rate, "1");
ASSERT_EQ(swap_response->estimated_price_impact, "0.7232");
ASSERT_EQ(swap_response->sources.size(), 1UL);
ASSERT_EQ(swap_response->sources.at(0)->name, "Uniswap_V2");
ASSERT_EQ(swap_response->sources.at(0)->proportion, "1");
json = R"({"price": "3"})";
ASSERT_FALSE(ParseSwapResponse(json, false));
@@ -143,7 +154,14 @@ TEST(SwapResponseParserUnitTest, ParseTransactionPayload) {
"sources":[],
"allowanceTarget":"0xdef1c0ded9bec7f1a1670819833240f027b25eff",
"sellTokenToEthRate":"1900.44962824532464391",
"buyTokenToEthRate":"1"
"buyTokenToEthRate":"1",
"estimatedPriceImpact": "0.7232",
"sources": [
{
"name": "Uniswap_V2",
"proportion": "1"
}
]
}
)");
mojom::SwapResponsePtr swap_response = ParseSwapResponse(json, true);
@@ -170,6 +188,10 @@ TEST(SwapResponseParserUnitTest, ParseTransactionPayload) {
"0xdef1c0ded9bec7f1a1670819833240f027b25eff");
ASSERT_EQ(swap_response->sell_token_to_eth_rate, "1900.44962824532464391");
ASSERT_EQ(swap_response->buy_token_to_eth_rate, "1");
ASSERT_EQ(swap_response->estimated_price_impact, "0.7232");
ASSERT_EQ(swap_response->sources.size(), 1UL);
ASSERT_EQ(swap_response->sources.at(0)->name, "Uniswap_V2");
ASSERT_EQ(swap_response->sources.at(0)->proportion, "1");
json = R"({"price": "3"})";
ASSERT_FALSE(ParseSwapResponse(json, true));
@@ -4,6 +4,11 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
namespace swap_responses {
dictionary ZeroExSource {
DOMString name;
DOMString proportion;
};
dictionary SwapResponse0x {
DOMString price;
DOMString? guaranteedPrice;
@@ -20,10 +25,11 @@ namespace swap_responses {
DOMString sellTokenAddress;
DOMString buyAmount;
DOMString sellAmount;
// TODO(bbondy): sources
DOMString allowanceTarget;
DOMString sellTokenToEthRate;
DOMString buyTokenToEthRate;
DOMString estimatedPriceImpact;
ZeroExSource[] sources;
};
dictionary JupiterMarketInfoFee {
@@ -250,7 +250,14 @@ TEST_F(SwapServiceUnitTest, GetPriceQuote) {
"sources":[],
"allowanceTarget":"0xdef1c0ded9bec7f1a1670819833240f027b25eff",
"sellTokenToEthRate":"1900.44962824532464391",
"buyTokenToEthRate":"1"
"buyTokenToEthRate":"1",
"estimatedPriceImpact": "0.7232",
"sources": [
{
"name": "Uniswap_V2",
"proportion": "1"
}
]
})");
auto expected_swap_response = brave_wallet::mojom::SwapResponse::New();
@@ -271,6 +278,12 @@ TEST_F(SwapServiceUnitTest, GetPriceQuote) {
"0xdef1c0ded9bec7f1a1670819833240f027b25eff";
expected_swap_response->sell_token_to_eth_rate = "1900.44962824532464391";
expected_swap_response->buy_token_to_eth_rate = "1";
expected_swap_response->estimated_price_impact = "0.7232";
auto source = brave_wallet::mojom::ZeroExSource::New();
source->name = "Uniswap_V2";
source->proportion = "1";
expected_swap_response->sources.push_back(source.Clone());
bool callback_run = false;
swap_service_->GetPriceQuote(
@@ -325,7 +338,14 @@ TEST_F(SwapServiceUnitTest, GetTransactionPayload) {
"sources":[],
"allowanceTarget":"0xdef1c0ded9bec7f1a1670819833240f027b25eff",
"sellTokenToEthRate":"1900.44962824532464391",
"buyTokenToEthRate":"1"
"buyTokenToEthRate":"1",
"estimatedPriceImpact": "0.7232",
"sources": [
{
"name": "Uniswap_V2",
"proportion": "1"
}
]
})");
auto expected_swap_response = brave_wallet::mojom::SwapResponse::New();
@@ -349,6 +369,11 @@ TEST_F(SwapServiceUnitTest, GetTransactionPayload) {
"0xdef1c0ded9bec7f1a1670819833240f027b25eff";
expected_swap_response->sell_token_to_eth_rate = "1900.44962824532464391";
expected_swap_response->buy_token_to_eth_rate = "1";
expected_swap_response->estimated_price_impact = "0.7232";
auto source = brave_wallet::mojom::ZeroExSource::New();
source->name = "Uniswap_V2";
source->proportion = "1";
expected_swap_response->sources.push_back(source.Clone());
bool callback_run = false;
swap_service_->GetTransactionPayload(
@@ -322,6 +322,11 @@ struct SwapParams {
string gas_price;
};
struct ZeroExSource {
string name;
string proportion;
};
struct SwapResponse {
string price;
string guaranteed_price; // Unused for price quote response
@@ -340,7 +345,8 @@ struct SwapResponse {
string allowance_target;
string sell_token_to_eth_rate;
string buy_token_to_eth_rate;
// Note we could also expose "sources" later
string estimated_price_impact;
array<ZeroExSource> sources;
};
// Structs to model Jupiter (swap) HTTP API interactions
@@ -56,7 +56,9 @@ export class MockedWalletApiProxy {
sellAmount: '124067000000000000',
allowanceTarget: '0x0000000000000000000000000000000000000000',
sellTokenToEthRate: '1',
buyTokenToEthRate: '1720.180416'
buyTokenToEthRate: '1720.180416',
estimatedPriceImpact: '0.0782',
sources: []
}
mockTransaction = {
@@ -76,7 +78,9 @@ export class MockedWalletApiProxy {
buyAmount: '0',
sellAmount: '0',
sellTokenToEthRate: '1',
buyTokenToEthRate: '1'
buyTokenToEthRate: '1',
estimatedPriceImpact: '0.0782',
sources: []
}
swapService: Partial<InstanceType<typeof BraveWallet.SwapServiceInterface>> = {
@@ -346,11 +346,20 @@ handler.on(WalletActions.selectPortfolioTimeline.type, async (store: Store, payl
handler.on(WalletActions.sendTransaction.type, async (
store: Store,
payload: SendEthTransactionParams | SendFilTransactionParams | SendSolTransactionParams
payload:
| Omit<SendEthTransactionParams, 'hasEIP1559Support'>
| SendFilTransactionParams
| SendSolTransactionParams
) => {
const { wallet: walletState } = store.getState()
let addResult
if (payload.coin === BraveWallet.CoinType.ETH) {
addResult = await sendEthTransaction(store, payload as SendEthTransactionParams)
addResult = await sendEthTransaction({
...payload,
hasEIP1559Support: !!walletState.selectedNetwork && !!walletState.selectedAccount &&
hasEIP1559Support(walletState.selectedAccount, walletState.selectedNetwork)
})
} else if (payload.coin === BraveWallet.CoinType.FIL) {
addResult = await sendFilTransaction(payload as SendFilTransactionParams)
} else if (payload.coin === BraveWallet.CoinType.SOL) {
+47 -15
View File
@@ -34,7 +34,7 @@ import { loadTimeData } from '../../../common/loadTimeData'
import { walletApi } from '../slices/api.slice'
import getAPIProxy from './bridge'
import { Dispatch, State, Store } from './types'
import { Dispatch, State } from './types'
import { getHardwareKeyring } from '../api/hardware_keyrings'
import { GetAccountsHardwareOperationResult, SolDerivationPaths } from '../hardware/types'
import EthereumLedgerBridgeKeyring from '../hardware/ledgerjs/eth_ledger_bridge_keyring'
@@ -61,7 +61,7 @@ export const getERC20Allowance = (
if (result.error === BraveWallet.ProviderError.kSuccess) {
resolve(result.allowance)
} else {
reject()
reject(result.errorMessage)
}
})
}
@@ -107,25 +107,50 @@ export const onConnectHardwareWallet = (opts: HardwareWalletConnectOpts): Promis
})
}
export const getBalance = (address: string, coin: BraveWallet.CoinType): Promise<string> => {
export const getBalance = async (address: string, coin: BraveWallet.CoinType): Promise<string> => {
const { jsonRpcService } = getAPIProxy()
const chainId = await jsonRpcService.getChainId(coin)
return await getBalanceForChainId(address, coin, chainId.chainId)
}
export function getBalanceForChainId (address: string, coin: BraveWallet.CoinType, chainId: string): Promise<string> {
return new Promise(async (resolve, reject) => {
const { jsonRpcService } = getAPIProxy()
const chainId = await jsonRpcService.getChainId(coin)
if (coin === BraveWallet.CoinType.SOL) {
const result = await jsonRpcService.getSolanaBalance(address, chainId.chainId)
const result = await jsonRpcService.getSolanaBalance(address, chainId)
if (result.error === BraveWallet.SolanaProviderError.kSuccess) {
resolve(Amount.normalize(result.balance.toString()))
} else {
reject()
reject(result.errorMessage)
}
return
}
const result = await jsonRpcService.getBalance(address, coin, chainId.chainId)
const result = await jsonRpcService.getBalance(address, coin, chainId)
if (result.error === BraveWallet.ProviderError.kSuccess) {
resolve(Amount.normalize(result.balance))
} else {
reject()
reject(result.errorMessage)
}
})
}
export function getTokenBalanceForChainId (contract: string, address: string, coin: BraveWallet.CoinType, chainId: string): Promise<string> {
return new Promise(async (resolve, reject) => {
const { jsonRpcService } = getAPIProxy()
if (coin === BraveWallet.CoinType.SOL) {
const result = await jsonRpcService.getSPLTokenAccountBalance(address, contract, chainId)
if (result.error === BraveWallet.SolanaProviderError.kSuccess) {
resolve(Amount.normalize(result.amount))
} else {
reject(result.errorMessage)
}
return
}
const result = await jsonRpcService.getERC20TokenBalance(contract, address, chainId)
if (result.error === BraveWallet.ProviderError.kSuccess) {
resolve(Amount.normalize(result.balance))
} else {
reject(result.errorMessage)
}
})
}
@@ -883,7 +908,7 @@ export function hasEIP1559Support (account: WalletAccountType, network: BraveWal
return keyringSupportsEIP1559 && network.isEip1559
}
export async function sendEthTransaction (store: Store, payload: SendEthTransactionParams) {
export async function sendEthTransaction (payload: SendEthTransactionParams) {
const apiProxy = getAPIProxy()
/***
* Determine whether to create a legacy or EIP-1559 transaction.
@@ -910,10 +935,7 @@ export async function sendEthTransaction (store: Store, payload: SendEthTransact
// Check if network and keyring support EIP-1559.
default:
const { selectedAccount, selectedNetwork } = store.getState().wallet
isEIP1559 = selectedNetwork && selectedAccount
? hasEIP1559Support(selectedAccount, selectedNetwork)
: false
isEIP1559 = payload.hasEIP1559Support
}
const { chainId } = await apiProxy.jsonRpcService.getChainId(BraveWallet.CoinType.ETH)
@@ -989,7 +1011,7 @@ export async function sendSolanaSerializedTransaction (payload: SolanaSerialized
)
if (result.error !== BraveWallet.ProviderError.kSuccess) {
console.error(`Failed to sign Solana message: ${result.errorMessage}`)
return { success: false, errorMessage: result.errorMessage }
return { success: false, errorMessage: result.errorMessage, txMetaId: '' }
} else {
return await txService.addUnapprovedTransaction(
// @ts-expect-error google closure is ok with undefined for other fields but mojom runtime is not
@@ -1000,3 +1022,13 @@ export async function sendSolanaSerializedTransaction (payload: SolanaSerialized
)
}
}
export function getSwapService () {
const { swapService } = getAPIProxy()
return swapService
}
export function getEthTxManagerProxy () {
const { ethTxManagerProxy } = getAPIProxy()
return ethTxManagerProxy
}
@@ -437,7 +437,8 @@ export default function useSend (isSendTab?: boolean) {
.multiplyByDecimals(selectedSendAsset.decimals) // ETH → Wei conversion
.value // extract BigNumber object wrapped by Amount
return amountBN && amountBN.decimalPlaces() > 0
const amountDP = amountBN && amountBN.decimalPlaces()
return amountDP && amountDP > 0
? 'fromAmountDecimalsOverflow'
: undefined
}, [sendAmount, selectedSendAsset])
@@ -14,7 +14,7 @@ import {
ExpirationPresetObjectType,
JupiterErrorResponse,
OrderTypes,
SendTransactionParams,
SendEthTransactionParams,
SlippagePresetObjectType,
SwapErrorResponse,
SwapValidationErrorType,
@@ -78,7 +78,8 @@ const hasDecimalsOverflow = (amount: string, asset?: BraveWallet.BlockchainToken
}
const amountBaseWrapped = new Amount(amount).multiplyByDecimals(asset.decimals)
return amountBaseWrapped.value && amountBaseWrapped.value.decimalPlaces() > 0
const amountBaseWrappedDP = amountBaseWrapped.value && amountBaseWrapped.value.decimalPlaces()
return amountBaseWrappedDP && amountBaseWrappedDP > 0
}
export enum SwapProvider {
@@ -442,7 +443,7 @@ export default function useSwap ({ fromAsset: fromAssetProp, toAsset: toAssetPro
maxFeePerGas = gasEstimates?.fastMaxFeePerGas
}
const params: SendTransactionParams = {
const params: SendEthTransactionParams = {
from: accountAddress,
to,
value: new Amount(value).toHex(),
@@ -450,7 +451,8 @@ export default function useSwap ({ fromAsset: fromAssetProp, toAsset: toAssetPro
data: hexStrToNumberArray(data),
maxPriorityFeePerGas,
maxFeePerGas,
coin: BraveWallet.CoinType.ETH
coin: BraveWallet.CoinType.ETH,
hasEIP1559Support: isEIP1559
}
dispatch(WalletActions.sendTransaction(params))
@@ -464,6 +464,7 @@ export interface SolanaSerializedTransactionParams {
export interface SendEthTransactionParams extends BaseEthTransactionParams {
data?: number[]
hasEIP1559Support: boolean
}
export type SendTransactionParams = SendEthTransactionParams | SendFilTransactionParams | SendSolTransactionParams
@@ -0,0 +1,337 @@
// Copyright (c) 2022 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// you can obtain one at https://mozilla.org/MPL/2.0/.
import {
ApproveERC20Params,
BlockchainToken,
ETHSendTransactionParams,
JupiterQuoteParams,
JupiterSwapParams,
NetworkInfo,
SOLSendTransactionParams,
WalletAccount,
ZeroExQuoteResponse,
ZeroExSwapParams,
ZeroExSwapResponse,
SwapFee
} from '@brave/swap-interface'
import {
BraveWallet,
SendEthTransactionParams,
SolanaSerializedTransactionParams,
WalletAccountType
} from '../../../constants/types'
import getAPIProxy from '../../../common/async/bridge'
import { getNetworkLogo } from '../../../options/asset-options'
function getTokenParam (token: BlockchainToken): string {
if (token.coingeckoId) {
return token.coingeckoId
}
const isEthereumNetwork = token.chainId === BraveWallet.MAINNET_CHAIN_ID
if (!isEthereumNetwork) {
return token.symbol.toLowerCase()
}
if (token.contractAddress === '') {
return token.symbol.toLowerCase()
}
return token.contractAddress
}
export function makeGetTokenPrice (toAssetSymbol: string) {
async function getTokenPrice (token: BlockchainToken) {
const { assetRatioService } = getAPIProxy()
const param = getTokenParam(token)
const { success, values } = await assetRatioService.getPrice(
[param],
[toAssetSymbol.toLowerCase()],
BraveWallet.AssetPriceTimeframe.OneDay
)
if (success) {
return values[0].price
}
throw new Error(`Failed to fetch price: ${param}`)
}
return getTokenPrice
}
export function makeNetworkInfo (network: BraveWallet.NetworkInfo): NetworkInfo {
return {
chainId: network.chainId,
chainName: network.chainName,
blockExplorerUrl: network.blockExplorerUrls[0],
logo: getNetworkLogo(network),
symbol: network.symbol,
symbolName: network.symbolName,
decimals: network.decimals,
coin: network.coin,
isEIP1559: network.isEip1559
}
}
export function makeWalletAccount (account: WalletAccountType): WalletAccount {
return {
id: account.address,
name: account.name,
address: account.address,
coin: account.coin
}
}
export function makeBlockchainToken (token: BraveWallet.BlockchainToken): BlockchainToken {
return {
contractAddress: token.contractAddress,
name: token.name,
logo: token.logo,
isToken: token.coin === BraveWallet.CoinType.ETH ? token.isErc20 : token.contractAddress !== '',
visible: token.visible,
decimals: token.decimals,
symbol: token.symbol,
coingeckoId: token.coingeckoId,
chainId: token.chainId,
coin: token.coin
}
}
export function makeSwapService () {
async function getZeroExPriceQuote (params: ZeroExSwapParams) {
const { swapService } = getAPIProxy()
const { success, errorResponse, response } = await swapService.getPriceQuote(params)
if (success) {
return {
price: response?.price || '',
value: response?.value || '',
gas: response?.gas || '',
estimatedGas: response?.estimatedGas || '',
gasPrice: response?.gasPrice || '',
protocolFee: response?.protocolFee || '',
minimumProtocolFee: response?.minimumProtocolFee || '',
buyTokenAddress: response?.buyTokenAddress || '',
sellTokenAddress: response?.sellTokenAddress || '',
buyAmount: response?.buyAmount || '',
sellAmount: response?.sellAmount || '',
allowanceTarget: response?.allowanceTarget || '',
sellTokenToEthRate: response?.sellTokenToEthRate || '',
buyTokenToEthRate: response?.buyTokenToEthRate || '',
estimatedPriceImpact: response?.estimatedPriceImpact || '',
sources: response?.sources || []
} as ZeroExQuoteResponse
}
throw new Error(errorResponse || '')
}
async function getZeroExTransactionPayload (params: ZeroExSwapParams) {
const { swapService } = getAPIProxy()
const { success, errorResponse, response } = await swapService.getTransactionPayload(params)
if (success) {
return {
price: response?.price || '',
value: response?.value || '',
gas: response?.gas || '',
estimatedGas: response?.estimatedGas || '',
gasPrice: response?.gasPrice || '',
protocolFee: response?.protocolFee || '',
minimumProtocolFee: response?.minimumProtocolFee || '',
buyTokenAddress: response?.buyTokenAddress || '',
sellTokenAddress: response?.sellTokenAddress || '',
buyAmount: response?.buyAmount || '',
sellAmount: response?.sellAmount || '',
allowanceTarget: response?.allowanceTarget || '',
sellTokenToEthRate: response?.sellTokenToEthRate || '',
buyTokenToEthRate: response?.buyTokenToEthRate || '',
estimatedPriceImpact: response?.estimatedPriceImpact || '',
sources: response?.sources || [],
guaranteedPrice: response?.guaranteedPrice || '',
to: response?.to || '',
data: response?.data || ''
} as ZeroExSwapResponse
}
throw new Error(errorResponse || '')
}
async function getJupiterQuote (params: JupiterQuoteParams) {
const { swapService } = getAPIProxy()
const { success, errorResponse, response } = await swapService.getJupiterQuote(params)
if (success) {
return {
routes: response?.routes || []
}
}
throw new Error(errorResponse || '')
}
async function getJupiterTransactionsPayload (params: JupiterSwapParams) {
const { swapService } = getAPIProxy()
const { success, errorResponse, response } = await swapService.getJupiterSwapTransactions(
params
)
if (success) {
return {
setupTransaction: response?.setupTransaction || '',
swapTransaction: response?.swapTransaction || '',
cleanupTransaction: response?.cleanupTransaction || ''
}
}
throw new Error(errorResponse || '')
}
async function isSwapSupported (chainId: string) {
const { swapService } = getAPIProxy()
return (await swapService.isSwapSupported(chainId)).result
}
async function getBraveFeeForAsset (asset: BlockchainToken) {
if (asset.coin === BraveWallet.CoinType.ETH) {
return {
fee: '0.875',
discount: '0'
} as SwapFee
}
const { swapService } = getAPIProxy()
const hasFee = (await swapService.hasJupiterFeesForTokenMint(asset.contractAddress)).result
return {
fee: '0.85',
discount: hasFee ? '0' : '100'
} as SwapFee
}
return {
getZeroExPriceQuote,
getZeroExTransactionPayload,
getJupiterQuote,
getJupiterTransactionsPayload,
isSwapSupported,
getBraveFeeForAsset
}
}
export function makeGetGasPrice () {
async function getGasPrice () {
// FIXME: return the result of eth_gasPrice RPC
return ''
}
return getGasPrice
}
export function makeGetGasPrice1559 () {
async function getGasPrice1559 () {
const { ethTxManagerProxy } = getAPIProxy()
const result = await ethTxManagerProxy.getGasEstimation1559()
if (result && result.estimation) {
return result.estimation
} else {
throw Error('Failed to fetch EIP-1559 gas-pricing estimates')
}
}
return getGasPrice1559
}
export function makeGetERC20ApproveData () {
// FIXME - Remove contractAddress from ApproveERC20Params
async function getERC20ApproveData (params: ApproveERC20Params) {
const { ethTxManagerProxy } = getAPIProxy()
const { success, data } = await ethTxManagerProxy.makeERC20ApproveData(
params.spenderAddress,
params.allowance
)
if (success) {
return data
} else {
throw Error('Failed to make ERC20 approve data')
}
}
return getERC20ApproveData
}
export function makeETHSendTransaction (
func: (
payload: SendEthTransactionParams
) => Promise<{ success: boolean, txMetaId: string, errorMessage: string }>,
hasEIP1559Support: boolean
) {
async function sendTransaction (params: ETHSendTransactionParams) {
const { errorMessage } = await func({
...params,
hasEIP1559Support,
coin: BraveWallet.CoinType.ETH
})
if (errorMessage) {
throw new Error(errorMessage)
}
}
return sendTransaction
}
export function makeSOLSendTransaction (
func: (
params: SolanaSerializedTransactionParams
) => Promise<{ success: boolean, txMetaId: string, errorMessage: string }>
) {
async function sendTransaction (params: SOLSendTransactionParams) {
const { errorMessage } = await func({
encodedTransaction: params.encodedTransaction,
from: params.from,
txType: BraveWallet.TransactionType.SolanaSwap,
sendOptions: {
maxRetries:
params.sendOptions?.maxRetries !== undefined
? ({
maxRetries: params.sendOptions.maxRetries
} as unknown as BraveWallet.OptionalMaxRetries)
: undefined,
preflightCommitment: params.sendOptions?.preflightCommitment,
skipPreflight:
params.sendOptions?.skipPreflight !== undefined
? { skipPreflight: params.sendOptions.skipPreflight }
: undefined
}
// TODO: support groupId
})
if (errorMessage) {
throw new Error(errorMessage)
}
}
return sendTransaction
}
export function makeSwitchAccount () {
async function switchAccount (account: WalletAccount) {
const { keyringService } = getAPIProxy()
await keyringService.setSelectedAccount(account.address, account.coin)
}
return switchAccount
}
export function makeSwitchNetwork () {
async function switchNetwork (network: NetworkInfo) {
const { jsonRpcService } = getAPIProxy()
await jsonRpcService.setNetwork(network.chainId, network.coin)
}
return switchNetwork
}
@@ -4,12 +4,149 @@
// you can obtain one at https://mozilla.org/MPL/2.0/.
import * as React from 'react'
import { useHistory } from 'react-router-dom'
import { NetworkInfo, Swap as SwapInterface } from '@brave/swap-interface'
import '@brave/swap-interface/dist/style.css'
import { getLocale } from '$web-common/locale'
import { WalletSelectors } from '../../../common/selectors'
import {
useSafeWalletSelector,
useUnsafeWalletSelector
} from '../../../common/hooks/use-safe-selector'
import { useLib } from '../../../common/hooks'
import { BraveWallet, WalletAccountType, WalletRoutes } from '../../../constants/types'
import {
makeBlockchainToken,
makeNetworkInfo,
makeWalletAccount,
makeSwapService,
makeSOLSendTransaction,
makeGetGasPrice,
makeGetGasPrice1559,
makeGetERC20ApproveData,
makeETHSendTransaction,
makeSwitchNetwork,
makeSwitchAccount,
makeGetTokenPrice
} from './adapters'
export const Swap = () => {
// render
const selectedNetwork = useUnsafeWalletSelector(WalletSelectors.selectedNetwork)
const selectedAccount = useUnsafeWalletSelector(
WalletSelectors.selectedAccount
)
const accounts: WalletAccountType[] = useUnsafeWalletSelector(WalletSelectors.accounts)
const networks: BraveWallet.NetworkInfo[] = useUnsafeWalletSelector(WalletSelectors.networkList)
const defaultFiatCurrency = useSafeWalletSelector(WalletSelectors.defaultFiatCurrency)
const fullTokenList: BraveWallet.BlockchainToken[] = useUnsafeWalletSelector(
WalletSelectors.fullTokenList
)
const userVisibleTokensInfo: BraveWallet.BlockchainToken[] = useUnsafeWalletSelector(
WalletSelectors.userVisibleTokensInfo
)
const [supportedNetworks, setSupportedNetworks] = React.useState<NetworkInfo[]>([])
const tokensList = React.useMemo(() => {
return [
...userVisibleTokensInfo.filter(asset => asset.isErc20),
...fullTokenList.filter(
asset =>
!userVisibleTokensInfo.some(
token => token.contractAddress.toLowerCase() === asset.contractAddress.toLowerCase()
)
)
]
.filter(asset => !asset.isErc721)
.map(asset => makeBlockchainToken(asset))
}, [fullTokenList, userVisibleTokensInfo])
const {
getBalanceForChainId,
getTokenBalanceForChainId,
sendSolanaSerializedTransaction,
getSwapService,
getERC20Allowance,
sendEthTransaction,
hasEIP1559Support
} = useLib()
const swapServiceMojo = getSwapService()
React.useEffect(() => {
;(async () => {
const results = await Promise.all(
networks.map(async e => (await swapServiceMojo.isSwapSupported(e.chainId)).result)
)
const result = networks.filter((_, index) => results[index]).map(e => makeNetworkInfo(e))
setSupportedNetworks(result)
})()
}, [networks])
// Memos
const walletAccounts = React.useMemo(() => {
return accounts.map(account => makeWalletAccount(account))
}, [accounts])
const ethWalletAdapter = React.useMemo(() => {
return {
getGasPrice: makeGetGasPrice(),
getGasPrice1559: makeGetGasPrice1559(),
getERC20Allowance,
getERC20ApproveData: makeGetERC20ApproveData(),
sendTransaction: makeETHSendTransaction(
sendEthTransaction,
!!selectedNetwork && !!selectedAccount && hasEIP1559Support(selectedAccount, selectedNetwork)
)
}
}, [selectedAccount, selectedNetwork])
const solWalletAdapter = React.useMemo(() => {
return {
sendTransaction: makeSOLSendTransaction(sendSolanaSerializedTransaction)
}
}, [])
let history = useHistory()
const goBack = React.useCallback(() => {
history.push(WalletRoutes.Portfolio)
}, [history])
return (
<div>
<span>Brave Swap</span>
{selectedNetwork && selectedAccount && (
<SwapInterface
getLocale={getLocale}
network={makeNetworkInfo(selectedNetwork)}
account={makeWalletAccount(selectedAccount)}
walletAccounts={walletAccounts}
switchAccount={makeSwitchAccount()}
switchNetwork={makeSwitchNetwork()}
getBalance={getBalanceForChainId}
getTokenBalance={getTokenBalanceForChainId}
// // FIXME - remove isSwapSupported()
swapService={makeSwapService()}
// // FIXME - implement mojo method to query available 0x exchanges
// // This feature may be disabled until then.
exchanges={[]}
// // FIXME - remove getNetworkFeeEstimate, since chain-specific methods are
// // already available in the wallet adapters. This is a stub to make tsc happy.
getNetworkFeeEstimate={async () => ({ gasFee: '' })}
assetsList={tokensList}
solWalletAdapter={solWalletAdapter}
ethWalletAdapter={ethWalletAdapter}
getTokenPrice={makeGetTokenPrice(defaultFiatCurrency)}
supportedNetworks={supportedNetworks}
defaultBaseCurrency={defaultFiatCurrency}
routeBackToWallet={goBack}
isWalletConnected={true}
isReady={!!selectedNetwork}
/>
)}
</div>
)
}
+8 -2
View File
@@ -243,10 +243,16 @@ export default class Amount {
let decimals
let value
if (this.value === undefined || this.value.isNaN()) {
const valueDP = this.value && this.value.decimalPlaces()
if (
this.value === undefined ||
this.value.isNaN() ||
valueDP === null ||
valueDP === undefined
) {
return ''
} else if (
this.value.decimalPlaces() < 2 || this.value.isGreaterThanOrEqualTo(10)
valueDP < 2 || this.value.isGreaterThanOrEqualTo(10)
) {
decimals = 2
value = this.value.toNumber()
+58
View File
@@ -735,4 +735,62 @@
<message name="IDS_BRAVE_WALLET_NFT_FETCHING_ERROR" desc="Error message displayed when nft metadata is not successfully fetched">Something went wrong when fetching NFT details. Please try again later.</message>
<message name="IDS_BRAVE_WALLET_ENTER_A_PASSSWORD_TO_CONTINUE" desc="Prompt to enter a password before continuing">Enter a password to continue</message>
<message name="IDS_BRAVE_WALLET_ENTER_YOUR_PASSWORD_TO_START_BACKUP" desc="Password prompt description for backing up wallet">Enter your Brave Wallet password to start backing up wallet.</message>
<message name="IDS_BRAVE_SWAP" desc="Swap main title">Swap</message>
<message name="IDS_BRAVE_SWAP_REVIEW_ORDER" desc="Text for swap button">Review order</message>
<message name="IDS_BRAVE_SWAP_APPROVE_TOKEN" desc="Text for swap button while approving ERC20 token">Approve <ph name="TOKEN">$1</ph></message>
<message name="IDS_BRAVE_SWAP_INSUFFICIENT_BALANCE" desc="Text for swap button when token token balance is insufficient">Insufficient <ph name="TOKEN">$1</ph> balance</message>
<message name="IDS_BRAVE_SWAP_SELECT_TOKEN" desc="Dropdown text to select token">Select token</message>
<message name="IDS_BRAVE_SWAP_HALF" desc="Amount preset button text for half">Half</message>
<message name="IDS_BRAVE_SWAP_MAX" desc="Amount preset button text for max">Max</message>
<message name="IDS_BRAVE_SWAP_ENTER_AN_AMOUNT" desc="Text prompting to enter an amount">Enter an amount</message>
<message name="IDS_BRAVE_SWAP_FINDING_PRICE" desc="Loader text while quotes are fetched">Finding the best price for you</message>
<message name="IDS_BRAVE_SWAP_BALANCE" desc="Text indicating account balance">Balance:</message>
<message name="IDS_BRAVE_SWAP_SELECT_A_TOKEN" desc="Text prompting to select a token to swap">Select a token to swap</message>
<message name="IDS_BRAVE_SWAP_SHOW_TOKENS_WITH_ZERO_BALANCES" desc="Button text to show tokens with zero balance">Show tokens with no balance</message>
<message name="IDS_BRAVE_SWAP_HIDE_TOKENS_WITH_ZERO_BALANCES" desc="Button text to hide tokens with zero balance">Hide zero balance</message>
<message name="IDS_BRAVE_SWAP_SEARCH_TOKEN" desc="Text prompt to search token by name">Search token by name</message>
<message name="IDS_BRAVE_SWAP_OPTION" desc="Swap quote option">Option</message>
<message name="IDS_BRAVE_SWAP_RATE" desc="Swap exchange rate">Rate</message>
<message name="IDS_BRAVE_SWAP_PRICE_IMPACT" desc="Swap price impact">Price impact</message>
<message name="IDS_BRAVE_SWAP_MINIMUM_RECEIVED_AFTER_SLIPPAGE" desc="Minimum guaranteed amount user will get after the swap">Minimum received after slippage</message>
<message name="IDS_BRAVE_SWAP_NETWORK_FEE" desc="Text indicating network fees">Network fee</message>
<message name="IDS_BRAVE_SWAP_BRAVE_FEE" desc="Text indicating Brave fee for swaps">Brave fee</message>
<message name="IDS_BRAVE_SWAP_FREE" desc="Text indicating free Brave fees for swaps">Free</message>
<message name="IDS_BRAVE_SWAP_LIQUIDITY_PROVIDER" desc="Liquidity provider for the swap quote">Liquidity provider</message>
<message name="IDS_BRAVE_SWAP_SWAP_AND_SEND" desc="Swap and send to a specific address in one transaction">Swap &amp; send</message>
<message name="IDS_BRAVE_SWAP_NO_EXTRA_FEES" desc="Text indicating swap and send has no extra fees">NO extra fees!</message>
<message name="IDS_BRAVE_SWAP_CONFIRM_ADDRESS" desc="Text to confirm that the address is correct">I confirm that the address above is correct</message>
<message name="IDS_BRAVE_SWAP_ADDRESS_INPUT_PLACEHOLDER" desc="Text to prompt user to enter address for swap and send">Enter recipient's Ethereum address</message>
<message name="IDS_BRAVE_SWAP_SELECT_ACCOUNT" desc="Button text to select an account">Select an account</message>
<message name="IDS_BRAVE_SWAP_TO_ACCOUNT" desc="Text option indicating that Swap and Send must use one of user's accounts">To my account</message>
<message name="IDS_BRAVE_SWAP_TO_ADDRESS" desc="Text option indicating that Swap and Send must use a specific address">To another address</message>
<message name="IDS_BRAVE_SWAP_SLOW" desc="Gas price corresponding to slow speed for confirmation">Slow</message>
<message name="IDS_BRAVE_SWAP_AVERAGE" desc="Gas price corresponding to average speed for confirmation">Average</message>
<message name="IDS_BRAVE_SWAP_FAST" desc="Gas price corresponding to fast speed for confirmation">Fast</message>
<message name="IDS_BRAVE_SWAP_GWEI" desc="Unit to use for gas price (giga-wei)">Gwei</message>
<message name="IDS_BRAVE_SWAP_SETTINGS" desc="Swap settings">Swap settings</message>
<message name="IDS_BRAVE_SWAP_SLIPPAGE_TOLERANCE" desc="Percentage tolerance to allow for slippage during the swap">Slippage tolerance</message>
<message name="IDS_BRAVE_SWAP_EXCHANGES" desc="Exchanges to use for the swap trade">Exchanges</message>
<message name="IDS_BRAVE_SWAP_DIRECT_ROUTE_TITLE" desc="Text indicating that swap must use direct routes in the trade">Direct route only</message>
<message name="IDS_BRAVE_SWAP_DIRECT_ROUTE_DESCRIPTION" desc="Text description direct swap routes option">It insures only direct routing and also disables split trade trading</message>
<message name="IDS_BRAVE_SWAP_NETWORK_FEE_DESCRIPTION" desc="Network fee description">Brave uses the top 60 tokens for intermediate routing for performance optimization</message>
<message name="IDS_BRAVE_SWAP_COINGECKO_CHEAPER" desc="Quote rate is cheaper than CoinGecko"><ph name="VALUE">$1<ex>0.12</ex></ph>% cheaper than CoinGecko</message>
<message name="IDS_BRAVE_SWAP_COINGECKO_WITHIN" desc="Quote rate is within small percentage of CoinGecko">Within <ph name="VALUE">$1<ex>0.12</ex></ph>% CoinGecko</message>
<message name="IDS_BRAVE_SWAP_COINGECKO_EXPENSIVE" desc="Quote rate is more expensive than CoinGecko"><ph name="VALUE">$1<ex>12</ex></ph>% more expensive than CoinGecko</message>
<message name="IDS_BRAVE_SWAP_API" desc="Application Programming Interface (API)">API</message>
<message name="IDS_BRAVE_SWAP_NAME" desc="Name">Name</message>
<message name="IDS_BRAVE_SWAP_CONNECT_WALLET" desc="Connect wallet button text">Connect Wallet</message>
<message name="IDS_BRAVE_SWAP_DISCONNECT_WALLET" desc="Disconnect wallet">Disconnect</message>
<message name="IDS_BRAVE_SWAP_SWITCH_NETWORK" desc="Switch network button text">Switch Network</message>
<message name="IDS_BRAVE_SWAP_ADVANCED" desc="Advanced quote details">Advanced</message>
<message name="IDS_BRAVE_SWAP_ACCOUNTS" desc="Accounts">Accounts</message>
<message name="IDS_BRAVE_SWAP_WALLET" desc="Wallet">Wallet</message>
<message name="IDS_BRAVE_SWAP_BEST" desc="Best quote option">Best</message>
<message name="IDS_BRAVE_SWAP_HELP_CENTER" desc="Help center">Help Center</message>
<message name="IDS_BRAVE_SWAP_PRIVACY_POLICY" desc="Privacy policy">Privacy Policy</message>
<message name="IDS_BRAVE_SWAP_PRIVACY_DESCRIPTION" desc="Privacy policy description">This app uses the following third-party APIs:</message>
<message name="IDS_BRAVE_SWAP_ZERO_EX_DISCLAIMER" desc="0x swap privacy policy disclaimer">Brave Swap uses 0x as a DEX aggregator for supported EVM networks. 0x will process the Ethereum wallet address and IP address to fulfill a transaction (including getting quotes). 0x will ONLY use this data for the purposes of processing transactions.</message>
<message name="IDS_BRAVE_SWAP_JUPITER_DISCLAIMER" desc="Jupiter swap privacy policy disclaimer">Brave Swap uses Jupiter as a DEX aggregator for Solana network. Jupiter will process the Solana wallet address and IP address to fulfill a transaction (including getting quotes). Jupiter will ONLY use this data for the purposes of processing transactions.</message>
<message name="IDS_BRAVE_SWAP_ZERO_EX_PRIVACY" desc="0x swap full privacy policy link text">Click here for 0x Privacy Policy.</message>
<message name="IDS_BRAVE_SWAP_JUPITER_PRIVACY" desc="Jupiter swap full privacy policy link text">Click here for Jupiter Privacy Policy.</message>
</grit-part>
+6126 -83
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -294,6 +294,7 @@
"acorn": "8.0.5",
"babel-loader": "8.2.3",
"brave-ui": "github:brave/brave-ui#e4c049880199ac652b8f69610b8b70d1cd6d906f",
"@brave/swap-interface": "0.12.4",
"chalk": "4.1.0",
"commander": "2.20.3",
"css-loader": "3.6.0",
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Brian Vaughn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+3
View File
@@ -0,0 +1,3 @@
Name: @brave/swap-interface
URL: https://github.com/brave/swap
License: MPL-2.0