diff --git a/components/brave_wallet/browser/brave_wallet_constants.cc b/components/brave_wallet/browser/brave_wallet_constants.cc index 87ca34ed1b6b..de46375488f9 100644 --- a/components/brave_wallet/browser/brave_wallet_constants.cc +++ b/components/brave_wallet/browser/brave_wallet_constants.cc @@ -99,4 +99,33 @@ const base::flat_map& GetAnkrBlockchains() { return *blockchains; } + +// See https://0x.org/docs/introduction/0x-cheat-sheet#allowanceholder-address +std::optional GetZeroExAllowanceHolderAddress( + const std::string& chain_id) { + // key = chain_id, value = allowance_holder_contract_address + static base::NoDestructor> + allowance_holder_addresses( + {{mojom::kMainnetChainId, kZeroExAllowanceHolderCancun}, + {mojom::kArbitrumMainnetChainId, kZeroExAllowanceHolderCancun}, + {mojom::kAvalancheMainnetChainId, kZeroExAllowanceHolderShanghai}, + {mojom::kBaseMainnetChainId, kZeroExAllowanceHolderCancun}, + {mojom::kBlastMainnetChainId, kZeroExAllowanceHolderCancun}, + {mojom::kBnbSmartChainMainnetChainId, kZeroExAllowanceHolderCancun}, + {mojom::kLineaChainId, kZeroExAllowanceHolderLondon}, + {mojom::kOptimismMainnetChainId, kZeroExAllowanceHolderCancun}, + {mojom::kPolygonMainnetChainId, kZeroExAllowanceHolderCancun}, + {mojom::kScrollChainId, kZeroExAllowanceHolderShanghai}}); + + auto allowance_holder_address_pair = + allowance_holder_addresses->find(chain_id.c_str()); + + if (allowance_holder_address_pair == allowance_holder_addresses->end()) { + // not found + return std::nullopt; + } + + return allowance_holder_address_pair->second; +} + } // namespace brave_wallet diff --git a/components/brave_wallet/browser/brave_wallet_constants.h b/components/brave_wallet/browser/brave_wallet_constants.h index e7f96302b23f..be10dd772d01 100644 --- a/components/brave_wallet/browser/brave_wallet_constants.h +++ b/components/brave_wallet/browser/brave_wallet_constants.h @@ -1609,30 +1609,17 @@ inline constexpr webui::LocalizedString kLocalizedStrings[] = { {"braveWalletDetails", IDS_BRAVE_WALLET_DETAILS}}; // 0x swap constants -inline constexpr char kZeroExSepoliaBaseAPIURL[] = - "https://sepolia.api.0x.wallet.brave.com"; -inline constexpr char kZeroExPolygonBaseAPIURL[] = - "https://polygon.api.0x.wallet.brave.com"; -inline constexpr char kZeroExBinanceSmartChainBaseAPIURL[] = - "https://bsc.api.0x.wallet.brave.com"; -inline constexpr char kZeroExAvalancheBaseAPIURL[] = - "https://avalanche.api.0x.wallet.brave.com"; -inline constexpr char kZeroExFantomBaseAPIURL[] = - "https://fantom.api.0x.wallet.brave.com"; -inline constexpr char kZeroExCeloBaseAPIURL[] = - "https://celo.api.0x.wallet.brave.com"; -inline constexpr char kZeroExOptimismBaseAPIURL[] = - "https://optimism.api.0x.wallet.brave.com"; -inline constexpr char kZeroExArbitrumBaseAPIURL[] = - "https://arbitrum.api.0x.wallet.brave.com"; -inline constexpr char kZeroExBaseBaseAPIURL[] = - "https://base.api.0x.wallet.brave.com"; -inline constexpr char kZeroExEthereumBaseAPIURL[] = - "https://api.0x.wallet.brave.com"; +inline constexpr char kZeroExBaseAPIURL[] = "https://api.0x.wallet.brave.com"; inline constexpr char kEVMFeeRecipient[] = "0xbd9420A98a7Bd6B89765e5715e169481602D9c3d"; -inline constexpr char kAffiliateAddress[] = - "0xbd9420A98a7Bd6B89765e5715e169481602D9c3d"; +inline constexpr char kZeroExAllowanceHolderCancun[] = + "0x0000000000001fF3684f28c67538d4D072C22734"; +inline constexpr char kZeroExAllowanceHolderShanghai[] = + "0x0000000000005E88410CcDFaDe4a5EfaE4b49562"; +inline constexpr char kZeroExAllowanceHolderLondon[] = + "0x000000000000175a8b9bC6d539B3708EEd92EA6c"; +inline constexpr char kZeroExAPIVersionHeader[] = "0x-version"; +inline constexpr char kZeroExAPIVersion[] = "v2"; // Jupiter swap constants inline constexpr char kJupiterBaseAPIURL[] = "https://jupiter.wallet.brave.com"; @@ -1712,6 +1699,10 @@ const std::string GetAssetRatioBaseURL(); const base::flat_map& GetAnkrBlockchains(); // https://docs.rs/solana-program/1.18.10/src/solana_program/clock.rs.html#129-131 inline constexpr int kSolanaValidBlockHeightThreshold = 150; + +std::optional GetZeroExAllowanceHolderAddress( + const std::string& chain_id); + } // namespace brave_wallet #endif // BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_BRAVE_WALLET_CONSTANTS_H_ diff --git a/components/brave_wallet/browser/swap_response_parser.cc b/components/brave_wallet/browser/swap_response_parser.cc index b221a2c7e101..0ff8efa859fd 100644 --- a/components/brave_wallet/browser/swap_response_parser.cc +++ b/components/brave_wallet/browser/swap_response_parser.cc @@ -27,10 +27,6 @@ namespace brave_wallet { namespace zeroex { namespace { -constexpr char kSwapValidationErrorCode[] = "100"; -constexpr char kInsufficientAssetLiquidity[] = "INSUFFICIENT_ASSET_LIQUIDITY"; -constexpr char kTransferAmountExceedsAllowanceMessage[] = - "ERC20: transfer amount exceeds allowance"; mojom::ZeroExFeePtr ParseZeroExFee(const base::Value& value) { if (value.is_none()) { @@ -48,146 +44,220 @@ mojom::ZeroExFeePtr ParseZeroExFee(const base::Value& value) { } auto zero_ex_fee = mojom::ZeroExFee::New(); - zero_ex_fee->fee_type = zero_ex_fee_value->fee_type; - zero_ex_fee->fee_token = zero_ex_fee_value->fee_token; - zero_ex_fee->fee_amount = zero_ex_fee_value->fee_amount; - zero_ex_fee->billing_type = zero_ex_fee_value->billing_type; + zero_ex_fee->token = zero_ex_fee_value->token; + zero_ex_fee->amount = zero_ex_fee_value->amount; + zero_ex_fee->type = zero_ex_fee_value->type; return zero_ex_fee; } +mojom::ZeroExRoutePtr ParseRoute(const swap_responses::ZeroExRoute& value) { + auto route = mojom::ZeroExRoute::New(); + for (const auto& fill_value : value.fills) { + auto fill = mojom::ZeroExRouteFill::New(); + fill->from = fill_value.from; + fill->to = fill_value.to; + fill->source = fill_value.source; + fill->proportion_bps = fill_value.proportion_bps; + route->fills.push_back(std::move(fill)); + } + + return route; +} + +mojom::ZeroExQuotePtr ParseQuote( + const swap_responses::ZeroExQuoteResponse& value) { + auto quote = mojom::ZeroExQuote::New(); + + if (value.buy_amount.has_value()) { + quote->buy_amount = value.buy_amount.value(); + } else { + return nullptr; + } + + if (value.buy_token.has_value()) { + quote->buy_token = value.buy_token.value(); + } else { + return nullptr; + } + + if (value.gas.has_value()) { + quote->gas = value.gas.value(); + } else { + return nullptr; + } + + if (value.gas_price.has_value()) { + quote->gas_price = value.gas_price.value(); + } else { + return nullptr; + } + + quote->liquidity_available = value.liquidity_available; + + if (value.min_buy_amount.has_value()) { + quote->min_buy_amount = value.min_buy_amount.value(); + } else { + return nullptr; + } + + if (value.sell_amount.has_value()) { + quote->sell_amount = value.sell_amount.value(); + } else { + return nullptr; + } + + if (value.sell_token.has_value()) { + quote->sell_token = value.sell_token.value(); + } else { + return nullptr; + } + + if (value.total_network_fee.has_value()) { + quote->total_network_fee = value.total_network_fee.value(); + } else { + return nullptr; + } + + if (value.route.has_value()) { + quote->route = ParseRoute(value.route.value()); + } else { + return nullptr; + } + + if (value.fees.has_value()) { + auto fees = mojom::ZeroExFees::New(); + if (auto zero_ex_fee = ParseZeroExFee(value.fees.value().zero_ex_fee); + zero_ex_fee) { + fees->zero_ex_fee = std::move(zero_ex_fee); + } + quote->fees = std::move(fees); + } else { + return nullptr; + } + + return quote; +} + } // namespace mojom::ZeroExQuotePtr ParseQuoteResponse(const base::Value& json_value, - bool expect_transaction_data) { + const std::string& chain_id) { // { - // "price":"1916.27547998814058355", - // "guaranteedPrice":"1935.438234788021989386", - // "to":"0xdef1c0ded9bec7f1a1670819833240f027b25eff", - // "data":"...", - // "value":"0", - // "gas":"719000", - // "estimatedGas":"719000", - // "gasPrice":"26000000000", - // "protocolFee":"0", - // "minimumProtocolFee":"0", - // "buyTokenAddress":"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - // "sellTokenAddress":"0x6b175474e89094c44da98b954eedeac495271d0f", - // "buyAmount":"1000000000000000000000", - // "sellAmount":"1916275479988140583549706", - // "sources":[...], - // "allowanceTarget":"0xdef1c0ded9bec7f1a1670819833240f027b25eff", - // "sellTokenToEthRate":"1900.44962824532464391", - // "buyTokenToEthRate":"1", - // "estimatedPriceImpact": "0.7232", - // "sources": [ - // { - // "name": "0x", - // "proportion": "0", + // "blockNumber": "20114692", + // "buyAmount": "100037537", + // "buyToken": "0xdac17f958d2ee523a2206206994597c13d831ec7", + // "fees": { + // "integratorFee": null, + // "zeroExFee": null, + // "gasFee": null + // }, + // "issues": { + // "allowance": { + // "actual": "0", + // "spender": "0x0000000000001ff3684f28c67538d4d072c22734" // }, - // { - // "name": "Uniswap_V2", - // "proportion": "1", + // "balance": { + // "token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // "actual": "0", + // "expected": "100000000" // }, - // { - // "name": "Curve", - // "proportion": "0", - // } - // ], - // "fees": { - // "zeroExFee": { - // "feeType": "volume", - // "feeToken": "0x8f3cf7ad23cd3cadbd9735aff958023239c6a063", - // "feeAmount": "148470027512868522", - // "billingType": "on-chain" + // "simulationIncomplete": false, + // "invalidSourcesPassed": [] + // }, + // "liquidityAvailable": true, + // "minBuyAmount": "99037162", + // "route": { + // "fills": [ + // { + // "from": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // "to": "0xdac17f958d2ee523a2206206994597c13d831ec7", + // "source": "SolidlyV3", + // "proportionBps": "10000" + // } + // ], + // "tokens": [ + // { + // "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // "symbol": "USDC" + // }, + // { + // "address": "0xdac17f958d2ee523a2206206994597c13d831ec7", + // "symbol": "USDT" + // } + // ] + // }, + // "sellAmount": "100000000", + // "sellToken": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // "tokenMetadata": { + // "buyToken": { + // "buyTaxBps": "0", + // "sellTaxBps": "0" + // }, + // "sellToken": { + // "buyTaxBps": "0", + // "sellTaxBps": "0" // } - // } + // }, + // "totalNetworkFee": "1393685870940000", + // "transaction": { + // "to": "0x7f6cee965959295cc64d0e6c00d99d6532d8e86b", + // "data": + // "0x1fff991f00000000000000000000000070a9f34f9b34c64957b9c401a97bfed35b95049e000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000000000000000000000000000000000000005e72fea00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000144c1fb425e0000000000000000000000007f6cee965959295cc64d0e6c00d99d6532d8e86b000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000006e898131631616b1779bad70bc17000000000000000000000000000000000000000000000000000000006670d06c00000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000041ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016438c9c147000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000027100000000000000000000000006146be494fee4c73540cb1c5f87536abf1452500000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000084c31b8d7a0000000000000000000000007f6cee965959295cc64d0e6c00d99d6532d8e86b00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000001000276a40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + // "gas": "288079", + // "gasPrice": "4837860000", + // "value": "0" + // }, + // "zid": "0x111111111111111111111111" // } auto swap_response_value = - swap_responses::SwapResponse0x::FromValue(json_value); + swap_responses::ZeroExQuoteResponse::FromValue(json_value); if (!swap_response_value) { return nullptr; } auto swap_response = mojom::ZeroExQuote::New(); - swap_response->price = swap_response_value->price; - if (expect_transaction_data) { - if (!swap_response_value->guaranteed_price) { - return nullptr; - } - swap_response->guaranteed_price = *swap_response_value->guaranteed_price; + if (!swap_response_value->liquidity_available) { + swap_response->liquidity_available = false; + return swap_response; + } - if (!swap_response_value->to) { - return nullptr; - } - swap_response->to = *swap_response_value->to; + swap_response = ParseQuote(swap_response_value.value()); + if (!swap_response) { + return nullptr; + } - if (!swap_response_value->data) { - return nullptr; - } - swap_response->data = *swap_response_value->data; - } - - swap_response->value = swap_response_value->value; - swap_response->gas = swap_response_value->gas; - swap_response->estimated_gas = swap_response_value->estimated_gas; - swap_response->gas_price = swap_response_value->gas_price; - swap_response->protocol_fee = swap_response_value->protocol_fee; - swap_response->minimum_protocol_fee = - swap_response_value->minimum_protocol_fee; - swap_response->buy_token_address = swap_response_value->buy_token_address; - swap_response->sell_token_address = swap_response_value->sell_token_address; - swap_response->buy_amount = swap_response_value->buy_amount; - swap_response->sell_amount = swap_response_value->sell_amount; - swap_response->allowance_target = swap_response_value->allowance_target; - swap_response->sell_token_to_eth_rate = - 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)); - } - - auto fees = mojom::ZeroExFees::New(); - if (auto zero_ex_fee = ParseZeroExFee(swap_response_value->fees.zero_ex_fee); - zero_ex_fee) { - fees->zero_ex_fee = std::move(zero_ex_fee); - } - swap_response->fees = std::move(fees); + swap_response->allowance_target = + GetZeroExAllowanceHolderAddress(chain_id).value_or(""); return swap_response; } +mojom::ZeroExTransactionPtr ParseTransactionResponse( + const base::Value& json_value) { + auto swap_response_value = + swap_responses::ZeroExTransactionResponse::FromValue(json_value); + if (!swap_response_value) { + return nullptr; + } + + auto transaction = mojom::ZeroExTransaction::New(); + transaction->to = swap_response_value->transaction.to; + transaction->data = swap_response_value->transaction.data; + transaction->gas = swap_response_value->transaction.gas; + transaction->gas_price = swap_response_value->transaction.gas_price; + transaction->value = swap_response_value->transaction.value; + + return transaction; +} + mojom::ZeroExErrorPtr ParseErrorResponse(const base::Value& json_value) { - // https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_error_response_schema.json - // // { - // "code": "100", - // "reason": "Validation Failed", - // "validationErrors": [{ - // "field": "sellAmount", - // "code": "1001", - // "reason": "should match pattern \"^\\d+$\"" - // }, - // { - // "field": "sellAmount", - // "code": "1001", - // "reason": "should be integer" - // }, - // { - // "field": "sellAmount", - // "code": "1001", - // "reason": "should match some schema in anyOf" - // } - // ] + // "code": "SWAP_VALIDATION_FAILED", + // "message": "Validation Failed" // } - auto swap_error_response_value = swap_responses::ZeroExErrorResponse::FromValue(json_value); if (!swap_error_response_value) { @@ -195,31 +265,8 @@ mojom::ZeroExErrorPtr ParseErrorResponse(const base::Value& json_value) { } auto result = mojom::ZeroExError::New(); - result->code = swap_error_response_value->code; - result->reason = swap_error_response_value->reason; - - if (swap_error_response_value->validation_errors) { - for (auto& error_item : *swap_error_response_value->validation_errors) { - result->validation_errors.emplace_back(mojom::ZeroExValidationError::New( - error_item.field, error_item.code, error_item.reason)); - } - } - result->is_insufficient_liquidity = false; - if (result->code == kSwapValidationErrorCode) { - for (auto& item : result->validation_errors) { - if (item->reason == kInsufficientAssetLiquidity) { - result->is_insufficient_liquidity = true; - } - } - } - - // This covers the case when an insufficient allowance can only be detected - // by the 0x Quote API, for example when swapping in ExactOut mode. - if (swap_error_response_value->values && - base::Contains(swap_error_response_value->values->message, - kTransferAmountExceedsAllowanceMessage)) { - result->is_insufficient_allowance = true; - } + result->name = swap_error_response_value->name; + result->message = swap_error_response_value->message; return result; } diff --git a/components/brave_wallet/browser/swap_response_parser.h b/components/brave_wallet/browser/swap_response_parser.h index a8a8d8d381a6..0acb01656120 100644 --- a/components/brave_wallet/browser/swap_response_parser.h +++ b/components/brave_wallet/browser/swap_response_parser.h @@ -16,7 +16,9 @@ namespace brave_wallet { namespace zeroex { mojom::ZeroExQuotePtr ParseQuoteResponse(const base::Value& json_value, - bool expect_transaction_data); + const std::string& chain_id); +mojom::ZeroExTransactionPtr ParseTransactionResponse( + const base::Value& json_value); mojom::ZeroExErrorPtr ParseErrorResponse(const base::Value& json_value); } // namespace zeroex diff --git a/components/brave_wallet/browser/swap_response_parser_unittest.cc b/components/brave_wallet/browser/swap_response_parser_unittest.cc index 2b103cf70866..d701380cfae9 100644 --- a/components/brave_wallet/browser/swap_response_parser_unittest.cc +++ b/components/brave_wallet/browser/swap_response_parser_unittest.cc @@ -88,294 +88,295 @@ TEST(SwapResponseParserUnitTest, ParseZeroExQuoteResponse) { // Case 1: non-null zeroExFee std::string json(R"( { - "price":"1916.27547998814058355", - "guaranteedPrice":"1935.438234788021989386", - "to":"0xdef1c0ded9bec7f1a1670819833240f027b25eff", - "data":"0x0", - "value":"0", - "gas":"719000", - "estimatedGas":"719001", - "gasPrice":"26000000000", - "protocolFee":"0", - "minimumProtocolFee":"0", - "buyTokenAddress":"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "sellTokenAddress":"0x6b175474e89094c44da98b954eedeac495271d0f", - "buyAmount":"1000000000000000000000", - "sellAmount":"1916275479988140583549706", - "sources":[], - "allowanceTarget":"0xdef1c0ded9bec7f1a1670819833240f027b25eff", - "sellTokenToEthRate":"1900.44962824532464391", - "buyTokenToEthRate":"1", - "estimatedPriceImpact": "0.7232", - "sources": [ - { - "name": "Uniswap_V2", - "proportion": "1" - } - ], + "blockNumber": "20114676", + "buyAmount": "100032748", + "buyToken": "0xdac17f958d2ee523a2206206994597c13d831ec7", "fees": { + "integratorFee": null, "zeroExFee": { - "feeType": "volume", - "feeToken": "0x8f3cf7ad23cd3cadbd9735aff958023239c6a063", - "feeAmount": "148470027512868522", - "billingType": "on-chain" + "amount": "0", + "token": "0xdeadbeef", + "type": "volume" + }, + "gasFee": null + }, + "gas": "288095", + "gasPrice": "7062490000", + "issues": { + "allowance": { + "actual": "0", + "spender": "0x0000000000001ff3684f28c67538d4d072c22734" + }, + "balance": { + "token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "actual": "0", + "expected": "100000000" + }, + "simulationIncomplete": false, + "invalidSourcesPassed": [] + }, + "liquidityAvailable": true, + "minBuyAmount": "99032421", + "route": { + "fills": [ + { + "from": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "to": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "source": "SolidlyV3", + "proportionBps": "10000" + } + ], + "tokens": [ + { + "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "symbol": "USDC" + }, + { + "address": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "symbol": "USDT" + } + ] + }, + "sellAmount": "100000000", + "sellToken": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "tokenMetadata": { + "buyToken": { + "buyTaxBps": "0", + "sellTaxBps": "0" + }, + "sellToken": { + "buyTaxBps": "0", + "sellTaxBps": "0" } - } + }, + "totalNetworkFee": "2034668056550000", + "zid": "0x111111111111111111111111" } )"); - mojom::ZeroExQuotePtr quote = - zeroex::ParseQuoteResponse(ParseJson(json), false); + auto quote = + zeroex::ParseQuoteResponse(ParseJson(json), mojom::kMainnetChainId); ASSERT_TRUE(quote); - EXPECT_EQ(quote->price, "1916.27547998814058355"); - EXPECT_TRUE(quote->guaranteed_price.empty()); - EXPECT_TRUE(quote->to.empty()); - EXPECT_TRUE(quote->data.empty()); - - EXPECT_EQ(quote->value, "0"); - EXPECT_EQ(quote->gas, "719000"); - EXPECT_EQ(quote->estimated_gas, "719001"); - EXPECT_EQ(quote->gas_price, "26000000000"); - EXPECT_EQ(quote->protocol_fee, "0"); - EXPECT_EQ(quote->minimum_protocol_fee, "0"); - EXPECT_EQ(quote->buy_token_address, - "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"); - EXPECT_EQ(quote->sell_token_address, - "0x6b175474e89094c44da98b954eedeac495271d0f"); - EXPECT_EQ(quote->buy_amount, "1000000000000000000000"); - EXPECT_EQ(quote->sell_amount, "1916275479988140583549706"); - EXPECT_EQ(quote->allowance_target, - "0xdef1c0ded9bec7f1a1670819833240f027b25eff"); - EXPECT_EQ(quote->sell_token_to_eth_rate, "1900.44962824532464391"); - EXPECT_EQ(quote->buy_token_to_eth_rate, "1"); - EXPECT_EQ(quote->estimated_price_impact, "0.7232"); - EXPECT_EQ(quote->sources.size(), 1UL); - EXPECT_EQ(quote->sources.at(0)->name, "Uniswap_V2"); - EXPECT_EQ(quote->sources.at(0)->proportion, "1"); + EXPECT_EQ(quote->buy_amount, "100032748"); + EXPECT_EQ(quote->buy_token, "0xdac17f958d2ee523a2206206994597c13d831ec7"); + ASSERT_TRUE(quote->fees->zero_ex_fee); - EXPECT_EQ(quote->fees->zero_ex_fee->fee_type, "volume"); - EXPECT_EQ(quote->fees->zero_ex_fee->fee_token, - "0x8f3cf7ad23cd3cadbd9735aff958023239c6a063"); - EXPECT_EQ(quote->fees->zero_ex_fee->fee_amount, "148470027512868522"); - EXPECT_EQ(quote->fees->zero_ex_fee->billing_type, "on-chain"); + EXPECT_EQ(quote->fees->zero_ex_fee->amount, "0"); + EXPECT_EQ(quote->fees->zero_ex_fee->token, "0xdeadbeef"); + EXPECT_EQ(quote->fees->zero_ex_fee->type, "volume"); + + EXPECT_EQ(quote->gas, "288095"); + EXPECT_EQ(quote->gas_price, "7062490000"); + EXPECT_EQ(quote->liquidity_available, true); + EXPECT_EQ(quote->min_buy_amount, "99032421"); + + ASSERT_EQ(quote->route->fills.size(), 1UL); + EXPECT_EQ(quote->route->fills.at(0)->from, + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"); + EXPECT_EQ(quote->route->fills.at(0)->to, + "0xdac17f958d2ee523a2206206994597c13d831ec7"); + EXPECT_EQ(quote->route->fills.at(0)->source, "SolidlyV3"); + EXPECT_EQ(quote->route->fills.at(0)->proportion_bps, "10000"); + + EXPECT_EQ(quote->sell_amount, "100000000"); + EXPECT_EQ(quote->sell_token, "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"); + EXPECT_EQ(quote->total_network_fee, "2034668056550000"); + + EXPECT_EQ(quote->liquidity_available, true); + EXPECT_EQ(quote->allowance_target, + "0x0000000000001fF3684f28c67538d4D072C22734"); // Case 2: null zeroExFee json = R"( { - "price":"1916.27547998814058355", - "guaranteedPrice":"1935.438234788021989386", - "to":"0xdef1c0ded9bec7f1a1670819833240f027b25eff", - "data":"0x0", - "value":"0", - "gas":"719000", - "estimatedGas":"719001", - "gasPrice":"26000000000", - "protocolFee":"0", - "minimumProtocolFee":"0", - "buyTokenAddress":"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "sellTokenAddress":"0x6b175474e89094c44da98b954eedeac495271d0f", - "buyAmount":"1000000000000000000000", - "sellAmount":"1916275479988140583549706", - "sources":[], - "allowanceTarget":"0xdef1c0ded9bec7f1a1670819833240f027b25eff", - "sellTokenToEthRate":"1900.44962824532464391", - "buyTokenToEthRate":"1", - "estimatedPriceImpact": "0.7232", - "sources":[], + "blockNumber": "20114676", + "buyAmount": "100032748", + "buyToken": "0xdac17f958d2ee523a2206206994597c13d831ec7", "fees": { - "zeroExFee": null - } + "integratorFee": null, + "zeroExFee": null, + "gasFee": null + }, + "gas": "288095", + "gasPrice": "7062490000", + "issues": { + "allowance": { + "actual": "0", + "spender": "0x0000000000001ff3684f28c67538d4d072c22734" + }, + "balance": { + "token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "actual": "0", + "expected": "100000000" + }, + "simulationIncomplete": false, + "invalidSourcesPassed": [] + }, + "liquidityAvailable": true, + "minBuyAmount": "99032421", + "route": { + "fills": [ + { + "from": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "to": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "source": "SolidlyV3", + "proportionBps": "10000" + } + ], + "tokens": [ + { + "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "symbol": "USDC" + }, + { + "address": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "symbol": "USDT" + } + ] + }, + "sellAmount": "100000000", + "sellToken": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "tokenMetadata": { + "buyToken": { + "buyTaxBps": "0", + "sellTaxBps": "0" + }, + "sellToken": { + "buyTaxBps": "0", + "sellTaxBps": "0" + } + }, + "totalNetworkFee": "2034668056550000", + "zid": "0x111111111111111111111111" } )"; - quote = zeroex::ParseQuoteResponse(ParseJson(json), false); + quote = zeroex::ParseQuoteResponse(ParseJson(json), mojom::kMainnetChainId); ASSERT_TRUE(quote); EXPECT_FALSE(quote->fees->zero_ex_fee); + EXPECT_EQ(quote->liquidity_available, true); + EXPECT_EQ(quote->allowance_target, + "0x0000000000001fF3684f28c67538d4D072C22734"); // Case 3: malformed fees field json = R"( { - "price":"1916.27547998814058355", - "guaranteedPrice":"1935.438234788021989386", - "to":"0xdef1c0ded9bec7f1a1670819833240f027b25eff", - "data":"0x0", - "value":"0", - "gas":"719000", - "estimatedGas":"719001", - "gasPrice":"26000000000", - "protocolFee":"0", - "minimumProtocolFee":"0", - "buyTokenAddress":"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "sellTokenAddress":"0x6b175474e89094c44da98b954eedeac495271d0f", - "buyAmount":"1000000000000000000000", - "sellAmount":"1916275479988140583549706", - "sources":[], - "allowanceTarget":"0xdef1c0ded9bec7f1a1670819833240f027b25eff", - "sellTokenToEthRate":"1900.44962824532464391", - "buyTokenToEthRate":"1", - "estimatedPriceImpact": "0.7232", - "sources":[], - "fees": null + "blockNumber": "20114676", + "buyAmount": "100032748", + "buyToken": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "fees": null, + "gas": "288095", + "gasPrice": "7062490000", + "issues": { + "allowance": { + "actual": "0", + "spender": "0x0000000000001ff3684f28c67538d4d072c22734" + }, + "balance": { + "token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "actual": "0", + "expected": "100000000" + }, + "simulationIncomplete": false, + "invalidSourcesPassed": [] + }, + "liquidityAvailable": true, + "minBuyAmount": "99032421", + "route": { + "fills": [ + { + "from": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "to": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "source": "SolidlyV3", + "proportionBps": "10000" + } + ], + "tokens": [ + { + "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "symbol": "USDC" + }, + { + "address": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "symbol": "USDT" + } + ] + }, + "sellAmount": "100000000", + "sellToken": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "tokenMetadata": { + "buyToken": { + "buyTaxBps": "0", + "sellTaxBps": "0" + }, + "sellToken": { + "buyTaxBps": "0", + "sellTaxBps": "0" + } + }, + "totalNetworkFee": "2034668056550000", + "zid": "0x111111111111111111111111" } )"; - EXPECT_FALSE(zeroex::ParseQuoteResponse(ParseJson(json), false)); + EXPECT_FALSE( + zeroex::ParseQuoteResponse(ParseJson(json), mojom::kMainnetChainId)); - // Case 4: other invalid cases - json = R"({"price": "3"})"; - EXPECT_FALSE(zeroex::ParseQuoteResponse(ParseJson(json), false)); - json = R"({"price": 3})"; - EXPECT_FALSE(zeroex::ParseQuoteResponse(ParseJson(json), false)); + // Case 4: insufficient liquidity + json = R"( + { + "liquidityAvailable": false, + } + )"; + quote = zeroex::ParseQuoteResponse(ParseJson(json), mojom::kMainnetChainId); + ASSERT_TRUE(quote); + EXPECT_FALSE(quote->liquidity_available); + EXPECT_EQ(quote->buy_token, ""); + + // Case 5: other invalid cases + json = R"({"totalNetworkFee": "2034668056550000"})"; + EXPECT_FALSE( + zeroex::ParseQuoteResponse(ParseJson(json), mojom::kMainnetChainId)); + json = R"({"totalNetworkFee": 2034668056550000})"; + EXPECT_FALSE( + zeroex::ParseQuoteResponse(ParseJson(json), mojom::kMainnetChainId)); json = "3"; - EXPECT_FALSE(zeroex::ParseQuoteResponse(ParseJson(json), false)); + EXPECT_FALSE( + zeroex::ParseQuoteResponse(ParseJson(json), mojom::kMainnetChainId)); json = "[3]"; - EXPECT_FALSE(zeroex::ParseQuoteResponse(ParseJson(json), false)); - EXPECT_FALSE(zeroex::ParseQuoteResponse(base::Value(), false)); + EXPECT_FALSE( + zeroex::ParseQuoteResponse(ParseJson(json), mojom::kMainnetChainId)); + EXPECT_FALSE( + zeroex::ParseQuoteResponse(base::Value(), mojom::kMainnetChainId)); } TEST(SwapResponseParserUnitTest, ParseZeroExTransactionResponse) { - // Case 1: non-null zeroExFee + // Case 1: valid transaction std::string json(R"( { - "price":"1916.27547998814058355", - "guaranteedPrice":"1935.438234788021989386", - "to":"0xdef1c0ded9bec7f1a1670819833240f027b25eff", - "data":"0x0", - "value":"0", - "gas":"719000", - "estimatedGas":"719001", - "gasPrice":"26000000000", - "protocolFee":"0", - "minimumProtocolFee":"0", - "buyTokenAddress":"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "sellTokenAddress":"0x6b175474e89094c44da98b954eedeac495271d0f", - "buyAmount":"1000000000000000000000", - "sellAmount":"1916275479988140583549706", - "sources":[], - "allowanceTarget":"0xdef1c0ded9bec7f1a1670819833240f027b25eff", - "sellTokenToEthRate":"1900.44962824532464391", - "buyTokenToEthRate":"1", - "estimatedPriceImpact": "0.7232", - "sources": [ - { - "name": "Uniswap_V2", - "proportion": "1" - } - ], - "fees": { - "zeroExFee": { - "feeType": "volume", - "feeToken": "0x8f3cf7ad23cd3cadbd9735aff958023239c6a063", - "feeAmount": "148470027512868522", - "billingType": "on-chain" - } + "transaction": { + "to": "0x7f6cee965959295cc64d0e6c00d99d6532d8e86b", + "data": "0xdeadbeef", + "gas": "288079", + "gasPrice": "4837860000", + "value": "0" } } )"); - mojom::ZeroExQuotePtr quote = - zeroex::ParseQuoteResponse(ParseJson(json), true); - ASSERT_TRUE(quote); - - EXPECT_EQ(quote->price, "1916.27547998814058355"); - EXPECT_EQ(quote->guaranteed_price, "1935.438234788021989386"); - EXPECT_EQ(quote->to, "0xdef1c0ded9bec7f1a1670819833240f027b25eff"); - EXPECT_EQ(quote->data, "0x0"); - - EXPECT_EQ(quote->value, "0"); - EXPECT_EQ(quote->gas, "719000"); - EXPECT_EQ(quote->estimated_gas, "719001"); - EXPECT_EQ(quote->gas_price, "26000000000"); - EXPECT_EQ(quote->protocol_fee, "0"); - EXPECT_EQ(quote->minimum_protocol_fee, "0"); - EXPECT_EQ(quote->buy_token_address, - "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"); - EXPECT_EQ(quote->sell_token_address, - "0x6b175474e89094c44da98b954eedeac495271d0f"); - EXPECT_EQ(quote->buy_amount, "1000000000000000000000"); - EXPECT_EQ(quote->sell_amount, "1916275479988140583549706"); - EXPECT_EQ(quote->allowance_target, - "0xdef1c0ded9bec7f1a1670819833240f027b25eff"); - EXPECT_EQ(quote->sell_token_to_eth_rate, "1900.44962824532464391"); - EXPECT_EQ(quote->buy_token_to_eth_rate, "1"); - EXPECT_EQ(quote->estimated_price_impact, "0.7232"); - EXPECT_EQ(quote->sources.size(), 1UL); - EXPECT_EQ(quote->sources.at(0)->name, "Uniswap_V2"); - EXPECT_EQ(quote->sources.at(0)->proportion, "1"); - ASSERT_TRUE(quote->fees->zero_ex_fee); - EXPECT_EQ(quote->fees->zero_ex_fee->fee_type, "volume"); - EXPECT_EQ(quote->fees->zero_ex_fee->fee_token, - "0x8f3cf7ad23cd3cadbd9735aff958023239c6a063"); - EXPECT_EQ(quote->fees->zero_ex_fee->fee_amount, "148470027512868522"); - EXPECT_EQ(quote->fees->zero_ex_fee->billing_type, "on-chain"); - - // Case 2: null zeroExFee - json = R"( - { - "price":"1916.27547998814058355", - "guaranteedPrice":"1935.438234788021989386", - "to":"0xdef1c0ded9bec7f1a1670819833240f027b25eff", - "data":"0x0", - "value":"0", - "gas":"719000", - "estimatedGas":"719001", - "gasPrice":"26000000000", - "protocolFee":"0", - "minimumProtocolFee":"0", - "buyTokenAddress":"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "sellTokenAddress":"0x6b175474e89094c44da98b954eedeac495271d0f", - "buyAmount":"1000000000000000000000", - "sellAmount":"1916275479988140583549706", - "sources":[], - "allowanceTarget":"0xdef1c0ded9bec7f1a1670819833240f027b25eff", - "sellTokenToEthRate":"1900.44962824532464391", - "buyTokenToEthRate":"1", - "estimatedPriceImpact": "0.7232", - "sources":[], - "fees": { - "zeroExFee": null - } - } - )"; - quote = zeroex::ParseQuoteResponse(ParseJson(json), true); - ASSERT_TRUE(quote); - EXPECT_FALSE(quote->fees->zero_ex_fee); + mojom::ZeroExTransactionPtr transaction = + zeroex::ParseTransactionResponse(ParseJson(json)); + ASSERT_TRUE(transaction); - // Case 3: malformed fees field - json = R"( - { - "price":"1916.27547998814058355", - "guaranteedPrice":"1935.438234788021989386", - "to":"0xdef1c0ded9bec7f1a1670819833240f027b25eff", - "data":"0x0", - "value":"0", - "gas":"719000", - "estimatedGas":"719001", - "gasPrice":"26000000000", - "protocolFee":"0", - "minimumProtocolFee":"0", - "buyTokenAddress":"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "sellTokenAddress":"0x6b175474e89094c44da98b954eedeac495271d0f", - "buyAmount":"1000000000000000000000", - "sellAmount":"1916275479988140583549706", - "sources":[], - "allowanceTarget":"0xdef1c0ded9bec7f1a1670819833240f027b25eff", - "sellTokenToEthRate":"1900.44962824532464391", - "buyTokenToEthRate":"1", - "estimatedPriceImpact": "0.7232", - "sources":[], - "fees": null - } - )"; - EXPECT_FALSE(zeroex::ParseQuoteResponse(ParseJson(json), true)); + EXPECT_EQ(transaction->to, "0x7f6cee965959295cc64d0e6c00d99d6532d8e86b"); + EXPECT_EQ(transaction->data, "0xdeadbeef"); + EXPECT_EQ(transaction->gas, "288079"); + EXPECT_EQ(transaction->gas_price, "4837860000"); + EXPECT_EQ(transaction->value, "0"); - // Case 4: other invalid cases - json = R"({"price": "3"})"; - EXPECT_FALSE(zeroex::ParseQuoteResponse(ParseJson(json), true)); - json = R"({"price": 3})"; - EXPECT_FALSE(zeroex::ParseQuoteResponse(ParseJson(json), true)); + // Case 2: invalid cases json = "3"; - EXPECT_FALSE(zeroex::ParseQuoteResponse(ParseJson(json), true)); + EXPECT_FALSE(zeroex::ParseTransactionResponse(ParseJson(json))); json = "[3]"; - EXPECT_FALSE(zeroex::ParseQuoteResponse(ParseJson(json), true)); - EXPECT_FALSE(zeroex::ParseQuoteResponse(base::Value(), true)); + EXPECT_FALSE(zeroex::ParseTransactionResponse(ParseJson(json))); + EXPECT_FALSE(zeroex::ParseTransactionResponse(base::Value())); } TEST(SwapResponseParserUnitTest, ParseJupiterQuoteResponse) { @@ -493,52 +494,14 @@ TEST(SwapResponseParserUnitTest, ParseJupiterTransactionResponse) { TEST(SwapResponseParserUnitTest, ParseZeroExErrorResponse) { { std::string json(R"( - { - "code": "100", - "reason": "Validation Failed", - "validationErrors": [ - { - "field": "buyAmount", - "code": "1004", - "reason": "INSUFFICIENT_ASSET_LIQUIDITY" - } - ] - })"); - - auto swap_error = zeroex::ParseErrorResponse(ParseJson(json)); - EXPECT_EQ(swap_error->code, "100"); - EXPECT_EQ(swap_error->reason, "Validation Failed"); - EXPECT_EQ(swap_error->validation_errors.size(), 1u); - EXPECT_EQ(swap_error->validation_errors.front()->field, "buyAmount"); - EXPECT_EQ(swap_error->validation_errors.front()->code, "1004"); - EXPECT_EQ(swap_error->validation_errors.front()->reason, - "INSUFFICIENT_ASSET_LIQUIDITY"); - - EXPECT_TRUE(swap_error->is_insufficient_liquidity); - } - { - std::string json(R"( - { - "code": "100", - "reason": "Validation Failed", - "validationErrors": [ - { - "field": "buyAmount", - "code": "1004", - "reason": "SOMETHING_ELSE" - } - ] - })"); + { + "name": "INPUT_INVALID", + "message": "Validation Failed" + })"); auto swap_error = zeroex::ParseErrorResponse(ParseJson(json)); - EXPECT_EQ(swap_error->code, "100"); - EXPECT_EQ(swap_error->reason, "Validation Failed"); - EXPECT_EQ(swap_error->validation_errors.size(), 1u); - EXPECT_EQ(swap_error->validation_errors.front()->field, "buyAmount"); - EXPECT_EQ(swap_error->validation_errors.front()->code, "1004"); - EXPECT_EQ(swap_error->validation_errors.front()->reason, "SOMETHING_ELSE"); - - EXPECT_FALSE(swap_error->is_insufficient_liquidity); + EXPECT_EQ(swap_error->name, "INPUT_INVALID"); + EXPECT_EQ(swap_error->message, "Validation Failed"); } } diff --git a/components/brave_wallet/browser/swap_responses.idl b/components/brave_wallet/browser/swap_responses.idl index 7f84be791738..000fd7bac758 100644 --- a/components/brave_wallet/browser/swap_responses.idl +++ b/components/brave_wallet/browser/swap_responses.idl @@ -4,16 +4,10 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ namespace swap_responses { - dictionary ZeroExSource { - DOMString name; - DOMString proportion; - }; - dictionary ZeroExFee { - DOMString feeType; - DOMString feeToken; - DOMString feeAmount; - DOMString billingType; + DOMString amount; + DOMString token; + DOMString type; }; dictionary ZeroExFees { @@ -21,45 +15,47 @@ namespace swap_responses { any zeroExFee; }; - dictionary SwapResponse0x { - DOMString price; - DOMString? guaranteedPrice; - DOMString? to; - DOMString? data; - DOMString value; + dictionary ZeroExTransaction { + DOMString to; + DOMString data; DOMString gas; - DOMString estimatedGas; DOMString gasPrice; - DOMString protocolFee; - DOMString minimumProtocolFee; - DOMString buyTokenAddress; - DOMString buyTokenAddress; - DOMString sellTokenAddress; - DOMString buyAmount; - DOMString sellAmount; - DOMString allowanceTarget; - DOMString sellTokenToEthRate; - DOMString buyTokenToEthRate; - DOMString estimatedPriceImpact; - ZeroExSource[] sources; - ZeroExFees fees; - }; - - dictionary ZeroExValidationError { - DOMString field; - DOMString code; - DOMString reason; - }; - - dictionary ZeroExGenericError { - DOMString message; + DOMString value; + }; + + dictionary ZeroExRouteFill { + DOMString from; + DOMString to; + DOMString source; + DOMString proportionBps; + }; + + dictionary ZeroExRoute { + ZeroExRouteFill[] fills; + }; + + dictionary ZeroExQuoteResponse { + DOMString? blockNumber; + DOMString? buyAmount; + DOMString? buyToken; + ZeroExFees? fees; + DOMString? gas; + DOMString? gasPrice; + boolean liquidityAvailable; + DOMString? minBuyAmount; + ZeroExRoute? route; + DOMString? sellAmount; + DOMString? sellToken; + DOMString? totalNetworkFee; + }; + + dictionary ZeroExTransactionResponse { + ZeroExTransaction transaction; }; dictionary ZeroExErrorResponse { - DOMString code; - DOMString reason; - ZeroExValidationError[]? validationErrors; - ZeroExGenericError? values; + DOMString name; + DOMString message; }; dictionary JupiterPlatformFee { diff --git a/components/brave_wallet/browser/swap_service.cc b/components/brave_wallet/browser/swap_service.cc index e7365aecfc40..20a45b0dd746 100644 --- a/components/brave_wallet/browser/swap_service.cc +++ b/components/brave_wallet/browser/swap_service.cc @@ -5,6 +5,7 @@ #include "brave/components/brave_wallet/browser/swap_service.h" +#include #include #include @@ -17,6 +18,7 @@ #include "brave/components/brave_wallet/browser/swap_response_parser.h" #include "brave/components/brave_wallet/common/brave_wallet.mojom.h" #include "brave/components/brave_wallet/common/buildflags.h" +#include "brave/components/brave_wallet/common/hex_utils.h" #include "brave/components/constants/brave_services_key.h" #include "net/base/load_flags.h" #include "net/base/url_util.h" @@ -54,17 +56,10 @@ net::NetworkTrafficAnnotationTag GetNetworkTrafficAnnotationTag() { namespace brave_wallet { namespace { + +// Ref: https://0x.org/docs/introduction/0x-cheat-sheet#-chain-support bool IsNetworkSupportedByZeroEx(const std::string& chain_id) { - return (chain_id == mojom::kSepoliaChainId || - chain_id == mojom::kMainnetChainId || - chain_id == mojom::kPolygonMainnetChainId || - chain_id == mojom::kBnbSmartChainMainnetChainId || - chain_id == mojom::kAvalancheMainnetChainId || - chain_id == mojom::kFantomMainnetChainId || - chain_id == mojom::kCeloMainnetChainId || - chain_id == mojom::kOptimismMainnetChainId || - chain_id == mojom::kArbitrumMainnetChainId || - chain_id == mojom::kBaseMainnetChainId); + return GetZeroExAllowanceHolderAddress(chain_id).has_value(); } bool IsNetworkSupportedByJupiter(const std::string& chain_id) { @@ -106,17 +101,17 @@ bool IsNetworkSupportedBySquid(const std::string& chain_id) { chain_id == mojom::kImmutableZkEVMChainId); } -bool HasRFQTLiquidity(const std::string& chain_id) { - return (chain_id == mojom::kMainnetChainId || - chain_id == mojom::kPolygonMainnetChainId); -} +std::optional EncodeChainId(const std::string& value) { + uint256_t val; + if (!HexValueToUint256(value, &val)) { + return std::nullopt; + } -std::string GetAffiliateAddress(const std::string& chain_id) { - if (IsNetworkSupportedByZeroEx(chain_id)) { - return kAffiliateAddress; + if (val > std::numeric_limits::max()) { + return std::nullopt; } - return ""; + return base::NumberToString(static_cast(val)); } mojom::SwapFeesPtr GetZeroSwapFee() { @@ -133,8 +128,17 @@ GURL AppendZeroExSwapParams(const GURL& swap_url, const mojom::SwapQuoteParams& params, const std::optional& fee_param) { GURL url = swap_url; + + if (!IsNetworkSupportedByZeroEx(params.from_chain_id)) { + return GURL(); + } + + if (auto chain_id = EncodeChainId(params.from_chain_id)) { + url = net::AppendQueryParameter(url, "chainId", chain_id.value()); + } + if (!params.from_account_id->address.empty()) { - url = net::AppendQueryParameter(url, "takerAddress", + url = net::AppendQueryParameter(url, "taker", params.from_account_id->address); } if (!params.from_amount.empty()) { @@ -144,32 +148,27 @@ GURL AppendZeroExSwapParams(const GURL& swap_url, url = net::AppendQueryParameter(url, "buyAmount", params.to_amount); } - url = net::AppendQueryParameter(url, "buyToken", - params.to_token.empty() - ? kNativeEVMAssetContractAddress - : params.to_token); + auto buy_token = params.to_token.empty() ? kNativeEVMAssetContractAddress + : params.to_token; + url = net::AppendQueryParameter(url, "buyToken", buy_token); url = net::AppendQueryParameter(url, "sellToken", params.from_token.empty() ? kNativeEVMAssetContractAddress : params.from_token); if (fee_param.has_value() && !fee_param->empty()) { - url = net::AppendQueryParameter(url, "buyTokenPercentageFee", - fee_param.value()); - url = net::AppendQueryParameter(url, "feeRecipient", kEVMFeeRecipient); + url = net::AppendQueryParameter(url, "swapFeeBps", fee_param.value()); + url = net::AppendQueryParameter(url, "swapFeeRecipient", kEVMFeeRecipient); + url = net::AppendQueryParameter(url, "swapFeeToken", buy_token); } double slippage_percentage = 0.0; if (base::StringToDouble(params.slippage_percentage, &slippage_percentage)) { url = net::AppendQueryParameter( - url, "slippagePercentage", - base::StringPrintf("%.6f", slippage_percentage / 100)); + url, "slippageBps", + base::StringPrintf("%d", static_cast(slippage_percentage * 100))); } - std::string affiliate_address = GetAffiliateAddress(params.from_chain_id); - if (!affiliate_address.empty()) { - url = net::AppendQueryParameter(url, "affiliateAddress", affiliate_address); - } // TODO(onyb): custom gas_price is currently unused and may be removed in // future. // if (!params.gas_price.empty()) { @@ -220,32 +219,10 @@ base::flat_map GetHeaders() { return {{kBraveServicesKeyHeader, BUILDFLAG(BRAVE_SERVICES_KEY)}}; } -std::string GetBaseSwapURL(const std::string& chain_id) { - if (chain_id == brave_wallet::mojom::kSepoliaChainId) { - return brave_wallet::kZeroExSepoliaBaseAPIURL; - } else if (chain_id == brave_wallet::mojom::kMainnetChainId) { - return brave_wallet::kZeroExEthereumBaseAPIURL; - } else if (chain_id == brave_wallet::mojom::kPolygonMainnetChainId) { - return brave_wallet::kZeroExPolygonBaseAPIURL; - } else if (chain_id == brave_wallet::mojom::kBnbSmartChainMainnetChainId) { - return brave_wallet::kZeroExBinanceSmartChainBaseAPIURL; - } else if (chain_id == brave_wallet::mojom::kAvalancheMainnetChainId) { - return brave_wallet::kZeroExAvalancheBaseAPIURL; - } else if (chain_id == brave_wallet::mojom::kFantomMainnetChainId) { - return brave_wallet::kZeroExFantomBaseAPIURL; - } else if (chain_id == brave_wallet::mojom::kCeloMainnetChainId) { - return brave_wallet::kZeroExCeloBaseAPIURL; - } else if (chain_id == brave_wallet::mojom::kOptimismMainnetChainId) { - return brave_wallet::kZeroExOptimismBaseAPIURL; - } else if (chain_id == brave_wallet::mojom::kArbitrumMainnetChainId) { - return brave_wallet::kZeroExArbitrumBaseAPIURL; - } else if (chain_id == brave_wallet::mojom::kSolanaMainnet) { - return brave_wallet::kJupiterBaseAPIURL; - } else if (chain_id == brave_wallet::mojom::kBaseMainnetChainId) { - return brave_wallet::kZeroExBaseBaseAPIURL; - } - - return ""; +base::flat_map GetHeadersForZeroEx() { + auto headers = GetHeaders(); + headers[kZeroExAPIVersionHeader] = kZeroExAPIVersion; + return headers; } } // namespace @@ -271,49 +248,23 @@ void SwapService::Bind(mojo::PendingReceiver receiver) { GURL SwapService::GetZeroExQuoteURL( const mojom::SwapQuoteParams& params, const std::optional& fee_param) { - const bool use_rfqt = HasRFQTLiquidity(params.from_chain_id); - - // If chain has RFQ-T liquidity available, use the /quote endpoint for - // fetching the indicative price, since /price is often inaccurate. This is - // discouraged in 0x docs, particularly for RFQ-T trades, since it locks up - // the capital of market makers. However, the 0x team has approved us to do - // this, noting the inability of /price to discover optimal RFQ quotes. This - // should be considered a temporary workaround until 0x comes up with a - // solution. - auto url = GURL(GetBaseSwapURL(params.from_chain_id)) - .Resolve(use_rfqt ? "/swap/v1/quote" : "/swap/v1/price"); - url = AppendZeroExSwapParams(url, params, fee_param); - // That flag prevents an allowance validation by the 0x router. Disable it - // here and perform the validation on the client side. - url = net::AppendQueryParameter(url, "skipValidation", "true"); - - if (use_rfqt) { - url = net::AppendQueryParameter(url, "intentOnFilling", "false"); - } - - return url; + auto url = GURL(kZeroExBaseAPIURL).Resolve("/swap/allowance-holder/price"); + return AppendZeroExSwapParams(url, params, fee_param); } // static GURL SwapService::GetZeroExTransactionURL( const mojom::SwapQuoteParams& params, const std::optional& fee_param) { - auto url = - GURL(GetBaseSwapURL(params.from_chain_id)).Resolve("/swap/v1/quote"); - url = AppendZeroExSwapParams(url, params, fee_param); - - if (HasRFQTLiquidity(params.from_chain_id)) { - url = net::AppendQueryParameter(url, "intentOnFilling", "true"); - } - - return url; + auto url = GURL(kZeroExBaseAPIURL).Resolve("/swap/allowance-holder/quote"); + return AppendZeroExSwapParams(url, params, fee_param); } // static GURL SwapService::GetJupiterQuoteURL( const mojom::SwapQuoteParams& params, const std::optional& fee_param) { - auto url = GURL(GetBaseSwapURL(params.from_chain_id)).Resolve("/v6/quote"); + auto url = GURL(kJupiterBaseAPIURL).Resolve("/v6/quote"); url = AppendJupiterQuoteParams(url, params, fee_param); return url; @@ -321,7 +272,7 @@ GURL SwapService::GetJupiterQuoteURL( // static GURL SwapService::GetJupiterTransactionURL(const std::string& chain_id) { - return GURL(GetBaseSwapURL(chain_id)).Resolve("/v6/swap"); + return GURL(kJupiterBaseAPIURL).Resolve("/v6/swap"); } // static @@ -427,11 +378,12 @@ void SwapService::GetQuote(mojom::SwapQuoteParamsPtr params, auto internal_callback = base::BindOnce( &SwapService::OnGetZeroExQuote, weak_ptr_factory_.GetWeakPtr(), - std::move(swap_fee), std::move(callback)); + params->from_chain_id, std::move(swap_fee), std::move(callback)); api_request_helper_.Request(net::HttpRequestHeaders::kGetMethod, GetZeroExQuoteURL(*params, fee_param), "", "", - std::move(internal_callback), GetHeaders(), {}, + std::move(internal_callback), + GetHeadersForZeroEx(), {}, std::move(conversion_callback)); return; @@ -466,7 +418,8 @@ void SwapService::GetQuote(mojom::SwapQuoteParamsPtr params, l10n_util::GetStringUTF8(IDS_BRAVE_WALLET_UNSUPPORTED_NETWORK)); } -void SwapService::OnGetZeroExQuote(mojom::SwapFeesPtr swap_fee, +void SwapService::OnGetZeroExQuote(const std::string& chain_id, + mojom::SwapFeesPtr swap_fee, GetQuoteCallback callback, APIRequestResult api_request_result) { if (!api_request_result.Is2XXResponseCode()) { @@ -484,8 +437,20 @@ void SwapService::OnGetZeroExQuote(mojom::SwapFeesPtr swap_fee, return; } - if (auto swap_response = - zeroex::ParseQuoteResponse(api_request_result.value_body(), false)) { + if (auto swap_response = zeroex::ParseQuoteResponse( + api_request_result.value_body(), chain_id)) { + if (!swap_response->liquidity_available) { + std::move(callback).Run( + nullptr, nullptr, + mojom::SwapErrorUnion::NewZeroExError(mojom::ZeroExError::New( + "INSUFFICIENT_LIQUIDITY", + l10n_util::GetStringUTF8( + IDS_BRAVE_WALLET_SWAP_INSUFFICIENT_LIQUIDITY), + true)), + ""); + return; + } + std::move(callback).Run( mojom::SwapQuoteUnion::NewZeroExQuote(std::move(swap_response)), std::move(swap_fee), nullptr, ""); @@ -605,7 +570,7 @@ void SwapService::GetTransaction(mojom::SwapTransactionParamsUnionPtr params, net::HttpRequestHeaders::kGetMethod, GetZeroExTransactionURL(*params->get_zero_ex_transaction_params(), swap_fee->fee_param), - "", "", std::move(internal_callback), GetHeaders(), {}, + "", "", std::move(internal_callback), GetHeadersForZeroEx(), {}, std::move(conversion_callback)); return; @@ -705,7 +670,7 @@ void SwapService::OnGetZeroExTransaction(GetTransactionCallback callback, } if (auto swap_response = - zeroex::ParseQuoteResponse(api_request_result.value_body(), true)) { + zeroex::ParseTransactionResponse(api_request_result.value_body())) { std::move(callback).Run(mojom::SwapTransactionUnion::NewZeroExTransaction( std::move(swap_response)), nullptr, ""); diff --git a/components/brave_wallet/browser/swap_service.h b/components/brave_wallet/browser/swap_service.h index 5b5f1709abac..48ef666022ea 100644 --- a/components/brave_wallet/browser/swap_service.h +++ b/components/brave_wallet/browser/swap_service.h @@ -63,7 +63,8 @@ class SwapService : public KeyedService, public mojom::SwapService { static GURL GetSquidURL(); private: - void OnGetZeroExQuote(mojom::SwapFeesPtr swap_fee, + void OnGetZeroExQuote(const std::string& chain_id, + mojom::SwapFeesPtr swap_fee, GetQuoteCallback callback, APIRequestResult api_request_result); void OnGetJupiterQuote(mojom::SwapFeesPtr swap_fee, diff --git a/components/brave_wallet/browser/swap_service_unittest.cc b/components/brave_wallet/browser/swap_service_unittest.cc index ab87be55d50b..0749d1932ae6 100644 --- a/components/brave_wallet/browser/swap_service_unittest.cc +++ b/components/brave_wallet/browser/swap_service_unittest.cc @@ -490,70 +490,103 @@ TEST_F(SwapServiceUnitTest, GetZeroExQuote) { // Case 1: non-null zeroExFee SetInterceptor(R"( { - "price":"1916.27547998814058355", - "value":"0", - "gas":"719000", - "estimatedGas":"719000", - "gasPrice":"26000000000", - "protocolFee":"0", - "minimumProtocolFee":"0", - "buyTokenAddress":"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "sellTokenAddress":"0x6b175474e89094c44da98b954eedeac495271d0f", - "buyAmount":"1000000000000000000000", - "sellAmount":"1916275479988140583549706", - "allowanceTarget":"0xdef1c0ded9bec7f1a1670819833240f027b25eff", - "sellTokenToEthRate":"1900.44962824532464391", - "buyTokenToEthRate":"1", - "estimatedPriceImpact": "0.7232", - "sources": [ - { - "name": "Uniswap_V2", - "proportion": "1" - } - ], + "blockNumber": "20114676", + "buyAmount": "100032748", + "buyToken": "0xdac17f958d2ee523a2206206994597c13d831ec7", "fees": { - "zeroExFee" : { - "feeType" : "volume", - "feeToken" : "0x8f3cf7ad23cd3cadbd9735aff958023239c6a063", - "feeAmount" : "148470027512868522", - "billingType" : "on-chain" + "integratorFee": null, + "zeroExFee": { + "amount": "0", + "token": "0xdeadbeef", + "type": "volume" + }, + "gasFee": null + }, + "gas": "288095", + "gasPrice": "7062490000", + "issues": { + "allowance": { + "actual": "0", + "spender": "0x0000000000001ff3684f28c67538d4d072c22734" + }, + "balance": { + "token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "actual": "0", + "expected": "100000000" + }, + "simulationIncomplete": false, + "invalidSourcesPassed": [] + }, + "liquidityAvailable": true, + "minBuyAmount": "99032421", + "route": { + "fills": [ + { + "from": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "to": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "source": "SolidlyV3", + "proportionBps": "10000" + } + ], + "tokens": [ + { + "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "symbol": "USDC" + }, + { + "address": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "symbol": "USDT" + } + ] + }, + "sellAmount": "100000000", + "sellToken": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "tokenMetadata": { + "buyToken": { + "buyTaxBps": "0", + "sellTaxBps": "0" + }, + "sellToken": { + "buyTaxBps": "0", + "sellTaxBps": "0" } - } - })"); + }, + "totalNetworkFee": "2034668056550000", + "zid": "0x111111111111111111111111" + } + )"); auto expected_zero_ex_quote = mojom::ZeroExQuote::New(); - expected_zero_ex_quote->price = "1916.27547998814058355"; - expected_zero_ex_quote->value = "0"; - expected_zero_ex_quote->gas = "719000"; - expected_zero_ex_quote->estimated_gas = "719000"; - expected_zero_ex_quote->gas_price = "26000000000"; - expected_zero_ex_quote->protocol_fee = "0"; - expected_zero_ex_quote->minimum_protocol_fee = "0"; - expected_zero_ex_quote->buy_token_address = - "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; - expected_zero_ex_quote->sell_token_address = - "0x6b175474e89094c44da98b954eedeac495271d0f"; - expected_zero_ex_quote->buy_amount = "1000000000000000000000"; - expected_zero_ex_quote->sell_amount = "1916275479988140583549706"; + expected_zero_ex_quote->buy_amount = "100032748"; + expected_zero_ex_quote->buy_token = + "0xdac17f958d2ee523a2206206994597c13d831ec7"; + expected_zero_ex_quote->sell_amount = "100000000"; + expected_zero_ex_quote->sell_token = + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"; + + auto zero_ex_fee = mojom::ZeroExFee::New(); + zero_ex_fee->type = "volume"; + zero_ex_fee->token = "0xdeadbeef"; + zero_ex_fee->amount = "0"; + expected_zero_ex_quote->fees = mojom::ZeroExFees::New(); + expected_zero_ex_quote->fees->zero_ex_fee = std::move(zero_ex_fee); + + expected_zero_ex_quote->gas = "288095"; + expected_zero_ex_quote->gas_price = "7062490000"; + expected_zero_ex_quote->liquidity_available = true; + expected_zero_ex_quote->min_buy_amount = "99032421"; + expected_zero_ex_quote->total_network_fee = "2034668056550000"; + + auto fill = mojom::ZeroExRouteFill::New(); + fill->from = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"; + fill->to = "0xdac17f958d2ee523a2206206994597c13d831ec7"; + fill->source = "SolidlyV3"; + fill->proportion_bps = "10000"; + expected_zero_ex_quote->route = mojom::ZeroExRoute::New(); + expected_zero_ex_quote->route->fills.push_back(fill.Clone()); + expected_zero_ex_quote->allowance_target = - "0xdef1c0ded9bec7f1a1670819833240f027b25eff"; - expected_zero_ex_quote->sell_token_to_eth_rate = "1900.44962824532464391"; - expected_zero_ex_quote->buy_token_to_eth_rate = "1"; - expected_zero_ex_quote->estimated_price_impact = "0.7232"; - - auto source = brave_wallet::mojom::ZeroExSource::New(); - source->name = "Uniswap_V2"; - source->proportion = "1"; - expected_zero_ex_quote->sources.push_back(source.Clone()); - - auto fees = brave_wallet::mojom::ZeroExFees::New(); - auto zero_ex_fee = brave_wallet::mojom::ZeroExFee::New(); - zero_ex_fee->fee_type = "volume"; - zero_ex_fee->fee_token = "0x8f3cf7ad23cd3cadbd9735aff958023239c6a063"; - zero_ex_fee->fee_amount = "148470027512868522"; - zero_ex_fee->billing_type = "on-chain"; - fees->zero_ex_fee = std::move(zero_ex_fee); - expected_zero_ex_quote->fees = std::move(fees); + "0x0000000000001fF3684f28c67538d4D072C22734"; auto expected_swap_fees = mojom::SwapFees::New(); expected_swap_fees->fee_pct = "0"; @@ -580,31 +613,67 @@ TEST_F(SwapServiceUnitTest, GetZeroExQuote) { // Case 2: null zeroExFee SetInterceptor(R"( { - "price":"1916.27547998814058355", - "value":"0", - "gas":"719000", - "estimatedGas":"719000", - "gasPrice":"26000000000", - "protocolFee":"0", - "minimumProtocolFee":"0", - "buyTokenAddress":"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "sellTokenAddress":"0x6b175474e89094c44da98b954eedeac495271d0f", - "buyAmount":"1000000000000000000000", - "sellAmount":"1916275479988140583549706", - "allowanceTarget":"0xdef1c0ded9bec7f1a1670819833240f027b25eff", - "sellTokenToEthRate":"1900.44962824532464391", - "buyTokenToEthRate":"1", - "estimatedPriceImpact": "0.7232", - "sources": [ - { - "name": "Uniswap_V2", - "proportion": "1" - } - ], + "blockNumber": "20114676", + "buyAmount": "100032748", + "buyToken": "0xdac17f958d2ee523a2206206994597c13d831ec7", "fees": { - "zeroExFee": null - } - })"); + "integratorFee": null, + "zeroExFee": null, + "gasFee": null + }, + "gas": "288095", + "gasPrice": "7062490000", + "issues": { + "allowance": { + "actual": "0", + "spender": "0x0000000000001ff3684f28c67538d4d072c22734" + }, + "balance": { + "token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "actual": "0", + "expected": "100000000" + }, + "simulationIncomplete": false, + "invalidSourcesPassed": [] + }, + "liquidityAvailable": true, + "minBuyAmount": "99032421", + "route": { + "fills": [ + { + "from": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "to": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "source": "SolidlyV3", + "proportionBps": "10000" + } + ], + "tokens": [ + { + "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "symbol": "USDC" + }, + { + "address": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "symbol": "USDT" + } + ] + }, + "sellAmount": "100000000", + "sellToken": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "tokenMetadata": { + "buyToken": { + "buyTaxBps": "0", + "sellTaxBps": "0" + }, + "sellToken": { + "buyTaxBps": "0", + "sellTaxBps": "0" + } + }, + "totalNetworkFee": "2034668056550000", + "zid": "0x111111111111111111111111" + } + )"); expected_zero_ex_quote->fees->zero_ex_fee = nullptr; EXPECT_CALL(callback, Run(EqualsMojo(mojom::SwapQuoteUnion::NewZeroExQuote( @@ -623,27 +692,11 @@ TEST_F(SwapServiceUnitTest, GetZeroExQuote) { } TEST_F(SwapServiceUnitTest, GetZeroExQuoteError) { + // Case 1: validation error std::string error = R"( { - "code": "100", - "reason": "Validation Failed", - "validationErrors": [ - { - "code": "1000", - "field": "sellAmount", - "reason": "should have required property 'sellAmount'" - }, - { - "code": "1000", - "field": "buyAmount", - "reason": "should have required property 'buyAmount'" - }, - { - "code": "1001", - "field": "", - "reason": "should match exactly one schema in oneOf" - } - ] + "name": "INPUT_INVALID", + "message": "Validation Failed" })"; SetErrorInterceptor(error); @@ -661,6 +714,34 @@ TEST_F(SwapServiceUnitTest, GetZeroExQuoteError) { mojom::SwapProvider::kZeroEx), callback.Get()); task_environment_.RunUntilIdle(); + testing::Mock::VerifyAndClearExpectations(&callback); + + // Case 2: insufficient liquidity + SetInterceptor(R"( + { + "liquidityAvailable": false, + "zid": "0x111111111111111111111111" + } + )"); + auto error_response = mojom::ZeroExError::New(); + error_response->name = "INSUFFICIENT_LIQUIDITY"; + error_response->message = + l10n_util::GetStringUTF8(IDS_BRAVE_WALLET_SWAP_INSUFFICIENT_LIQUIDITY); + error_response->is_insufficient_liquidity = true; + + EXPECT_CALL(callback, Run(EqualsMojo(mojom::SwapQuoteUnionPtr()), + EqualsMojo(mojom::SwapFeesPtr()), + EqualsMojo(mojom::SwapErrorUnion::NewZeroExError( + std::move(error_response))), + "")); + + swap_service_->GetQuote( + GetCannedSwapQuoteParams( + mojom::CoinType::ETH, mojom::kPolygonMainnetChainId, "DAI", + mojom::CoinType::ETH, mojom::kPolygonMainnetChainId, "ETH", + mojom::SwapProvider::kZeroEx), + callback.Get()); + task_environment_.RunUntilIdle(); } TEST_F(SwapServiceUnitTest, GetZeroExQuoteUnexpectedReturn) { @@ -684,80 +765,25 @@ TEST_F(SwapServiceUnitTest, GetZeroExQuoteUnexpectedReturn) { } TEST_F(SwapServiceUnitTest, GetZeroExTransaction) { - // Case 1: non-null zeroExFee SetInterceptor(R"( { - "price":"1916.27547998814058355", - "guaranteedPrice":"1935.438234788021989386", - "to":"0xdef1c0ded9bec7f1a1670819833240f027b25eff", - "data":"0x0", - "value":"0", - "gas":"719000", - "estimatedGas":"719000", - "gasPrice":"26000000000", - "protocolFee":"0", - "minimumProtocolFee":"0", - "buyTokenAddress":"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "sellTokenAddress":"0x6b175474e89094c44da98b954eedeac495271d0f", - "buyAmount":"1000000000000000000000", - "sellAmount":"1916275479988140583549706", - "allowanceTarget":"0xdef1c0ded9bec7f1a1670819833240f027b25eff", - "sellTokenToEthRate":"1900.44962824532464391", - "buyTokenToEthRate":"1", - "estimatedPriceImpact": "0.7232", - "sources": [ - { - "name": "Uniswap_V2", - "proportion": "1" - } - ], - "fees": { - "zeroExFee": { - "feeType": "volume", - "feeToken": "0x8f3cf7ad23cd3cadbd9735aff958023239c6a063", - "feeAmount": "148470027512868522", - "billingType": "on-chain" - } + "transaction": { + "to": "0x7f6cee965959295cc64d0e6c00d99d6532d8e86b", + "data": "0xdeadbeef", + "gas": "288079", + "gasPrice": "4837860000", + "value": "0" } - })"); + } + )"); - auto expected_zero_ex_transaction = mojom::ZeroExQuote::New(); - expected_zero_ex_transaction->price = "1916.27547998814058355"; - expected_zero_ex_transaction->guaranteed_price = "1935.438234788021989386"; + auto expected_zero_ex_transaction = mojom::ZeroExTransaction::New(); expected_zero_ex_transaction->to = - "0xdef1c0ded9bec7f1a1670819833240f027b25eff"; - expected_zero_ex_transaction->data = "0x0"; + "0x7f6cee965959295cc64d0e6c00d99d6532d8e86b"; + expected_zero_ex_transaction->data = "0xdeadbeef"; + expected_zero_ex_transaction->gas = "288079"; + expected_zero_ex_transaction->gas_price = "4837860000"; expected_zero_ex_transaction->value = "0"; - expected_zero_ex_transaction->gas = "719000"; - expected_zero_ex_transaction->estimated_gas = "719000"; - expected_zero_ex_transaction->gas_price = "26000000000"; - expected_zero_ex_transaction->protocol_fee = "0"; - expected_zero_ex_transaction->minimum_protocol_fee = "0"; - expected_zero_ex_transaction->buy_token_address = - "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; - expected_zero_ex_transaction->sell_token_address = - "0x6b175474e89094c44da98b954eedeac495271d0f"; - expected_zero_ex_transaction->buy_amount = "1000000000000000000000"; - expected_zero_ex_transaction->sell_amount = "1916275479988140583549706"; - expected_zero_ex_transaction->allowance_target = - "0xdef1c0ded9bec7f1a1670819833240f027b25eff"; - expected_zero_ex_transaction->sell_token_to_eth_rate = - "1900.44962824532464391"; - expected_zero_ex_transaction->buy_token_to_eth_rate = "1"; - expected_zero_ex_transaction->estimated_price_impact = "0.7232"; - auto source = brave_wallet::mojom::ZeroExSource::New(); - source->name = "Uniswap_V2"; - source->proportion = "1"; - expected_zero_ex_transaction->sources.push_back(source.Clone()); - - auto fees = brave_wallet::mojom::ZeroExFees::New(); - auto zero_ex_fee = brave_wallet::mojom::ZeroExFee::New(); - zero_ex_fee->fee_type = "volume"; - zero_ex_fee->fee_token = "0x8f3cf7ad23cd3cadbd9735aff958023239c6a063"; - zero_ex_fee->fee_amount = "148470027512868522"; - zero_ex_fee->billing_type = "on-chain"; - fees->zero_ex_fee = std::move(zero_ex_fee); - expected_zero_ex_transaction->fees = std::move(fees); base::MockCallback callback; EXPECT_CALL(callback, @@ -774,59 +800,11 @@ TEST_F(SwapServiceUnitTest, GetZeroExTransaction) { callback.Get()); task_environment_.RunUntilIdle(); testing::Mock::VerifyAndClearExpectations(&callback); - - // Case 2: null zeroExFee - SetInterceptor(R"( - { - "price":"1916.27547998814058355", - "guaranteedPrice":"1935.438234788021989386", - "to":"0xdef1c0ded9bec7f1a1670819833240f027b25eff", - "data":"0x0", - "value":"0", - "gas":"719000", - "estimatedGas":"719000", - "gasPrice":"26000000000", - "protocolFee":"0", - "minimumProtocolFee":"0", - "buyTokenAddress":"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "sellTokenAddress":"0x6b175474e89094c44da98b954eedeac495271d0f", - "buyAmount":"1000000000000000000000", - "sellAmount":"1916275479988140583549706", - "allowanceTarget":"0xdef1c0ded9bec7f1a1670819833240f027b25eff", - "sellTokenToEthRate":"1900.44962824532464391", - "buyTokenToEthRate":"1", - "estimatedPriceImpact": "0.7232", - "sources": [ - { - "name": "Uniswap_V2", - "proportion": "1" - } - ], - "fees": { - "zeroExFee": null - } - })"); - - expected_zero_ex_transaction->fees->zero_ex_fee = nullptr; - EXPECT_CALL(callback, - Run(EqualsMojo(mojom::SwapTransactionUnion::NewZeroExTransaction( - std::move(expected_zero_ex_transaction))), - EqualsMojo(mojom::SwapErrorUnionPtr()), "")); - - swap_service_->GetTransaction( - mojom::SwapTransactionParamsUnion::NewZeroExTransactionParams( - GetCannedSwapQuoteParams( - mojom::CoinType::ETH, mojom::kPolygonMainnetChainId, "DAI", - mojom::CoinType::ETH, mojom::kPolygonMainnetChainId, "ETH", - mojom::SwapProvider::kZeroEx)), - callback.Get()); - task_environment_.RunUntilIdle(); - testing::Mock::VerifyAndClearExpectations(&callback); } TEST_F(SwapServiceUnitTest, GetZeroExTransactionError) { std::string error = - R"({"code":"100","reason":"Validation Failed","validationErrors":[{"code":"1000","field":"sellAmount","reason":"should have required property 'sellAmount'"},{"code":"1000","field":"buyAmount","reason":"should have required property 'buyAmount'"},{"code":"1001","field":"","reason":"should match exactly one schema in oneOf"}]})"; + R"({"name":"INPUT_INVALID","message":"Validation Failed"})"; SetErrorInterceptor(error); base::MockCallback callback; @@ -865,65 +843,19 @@ TEST_F(SwapServiceUnitTest, GetZeroExTransactionUnexpectedReturn) { } TEST_F(SwapServiceUnitTest, GetZeroExQuoteURL) { - const std::map non_rfqt_chain_ids = { - {mojom::kSepoliaChainId, "sepolia.api.0x.wallet.brave.com"}, - {mojom::kBnbSmartChainMainnetChainId, "bsc.api.0x.wallet.brave.com"}, - {mojom::kAvalancheMainnetChainId, "avalanche.api.0x.wallet.brave.com"}, - {mojom::kFantomMainnetChainId, "fantom.api.0x.wallet.brave.com"}, - {mojom::kCeloMainnetChainId, "celo.api.0x.wallet.brave.com"}, - {mojom::kOptimismMainnetChainId, "optimism.api.0x.wallet.brave.com"}, - {mojom::kArbitrumMainnetChainId, "arbitrum.api.0x.wallet.brave.com"}, - {mojom::kBaseMainnetChainId, "base.api.0x.wallet.brave.com"}}; - - const std::map rfqt_chain_ids = { - {mojom::kMainnetChainId, "api.0x.wallet.brave.com"}, - {mojom::kPolygonMainnetChainId, "polygon.api.0x.wallet.brave.com"}}; - - for (const auto& [chain_id, domain] : non_rfqt_chain_ids) { - SCOPED_TRACE(testing::Message() << "chain_id: " << chain_id); - - // OK: with fees - auto url = swap_service_->GetZeroExQuoteURL( - *GetCannedSwapQuoteParams(mojom::CoinType::ETH, chain_id, "DAI", - mojom::CoinType::ETH, chain_id, "ETH", - mojom::SwapProvider::kZeroEx), - "0.00875"); - EXPECT_EQ(url, - base::StringPrintf( - "https://%s/swap/v1/price?" - "takerAddress=0xa92D461a9a988A7f11ec285d39783A637Fdd6ba4&" - "sellAmount=1000000000000000000000&" - "buyToken=ETH&" - "sellToken=DAI&" - "buyTokenPercentageFee=0.00875&" - "feeRecipient=0xbd9420A98a7Bd6B89765e5715e169481602D9c3d&" - "slippagePercentage=0.030000&" - "affiliateAddress=0xbd9420A98a7Bd6B89765e5715e169481602D9c3d&" - "skipValidation=true", - domain.c_str())); - - // Ok: no fees - url = swap_service_->GetZeroExQuoteURL( - *GetCannedSwapQuoteParams(mojom::CoinType::ETH, chain_id, "DAI", - mojom::CoinType::ETH, chain_id, "ETH", - mojom::SwapProvider::kZeroEx), - ""); - EXPECT_EQ(url, - base::StringPrintf( - "https://%s/swap/v1/price?" - "takerAddress=0xa92D461a9a988A7f11ec285d39783A637Fdd6ba4&" - "sellAmount=1000000000000000000000&" - "buyToken=ETH&" - "sellToken=DAI&" - "slippagePercentage=0.030000&" - "affiliateAddress=0xbd9420A98a7Bd6B89765e5715e169481602D9c3d&" - "skipValidation=true", - domain.c_str())); - } - - // If RFQ-T liquidity is available, so intentOnFilling=true is specified - // while fetching firm quotes. - for (const auto& [chain_id, domain] : rfqt_chain_ids) { + const std::map chain_ids = { + {mojom::kMainnetChainId, "1"}, + {mojom::kArbitrumMainnetChainId, "42161"}, + {mojom::kAvalancheMainnetChainId, "43114"}, + {mojom::kBaseMainnetChainId, "8453"}, + {mojom::kBlastMainnetChainId, "238"}, + {mojom::kBnbSmartChainMainnetChainId, "56"}, + {mojom::kLineaChainId, "59144"}, + {mojom::kOptimismMainnetChainId, "10"}, + {mojom::kPolygonMainnetChainId, "137"}, + {mojom::kScrollChainId, "534352"}}; + + for (const auto& [chain_id, encoded_chain_id] : chain_ids) { SCOPED_TRACE(testing::Message() << "chain_id: " << chain_id); // OK: with fees @@ -931,21 +863,20 @@ TEST_F(SwapServiceUnitTest, GetZeroExQuoteURL) { *GetCannedSwapQuoteParams(mojom::CoinType::ETH, chain_id, "DAI", mojom::CoinType::ETH, chain_id, "ETH", mojom::SwapProvider::kZeroEx), - "0.00875"); + "85"); EXPECT_EQ(url, base::StringPrintf( - "https://%s/swap/v1/quote?" - "takerAddress=0xa92D461a9a988A7f11ec285d39783A637Fdd6ba4&" + "https://api.0x.wallet.brave.com/swap/allowance-holder/price?" + "chainId=%s&" + "taker=0xa92D461a9a988A7f11ec285d39783A637Fdd6ba4&" "sellAmount=1000000000000000000000&" "buyToken=ETH&" "sellToken=DAI&" - "buyTokenPercentageFee=0.00875&" - "feeRecipient=0xbd9420A98a7Bd6B89765e5715e169481602D9c3d&" - "slippagePercentage=0.030000&" - "affiliateAddress=0xbd9420A98a7Bd6B89765e5715e169481602D9c3d&" - "skipValidation=true&" - "intentOnFilling=false", - domain.c_str())); + "swapFeeBps=85&" + "swapFeeRecipient=0xbd9420A98a7Bd6B89765e5715e169481602D9c3d&" + "swapFeeToken=ETH&" + "slippageBps=300", + encoded_chain_id.c_str())); // Ok: no fees url = swap_service_->GetZeroExQuoteURL( @@ -955,16 +886,14 @@ TEST_F(SwapServiceUnitTest, GetZeroExQuoteURL) { ""); EXPECT_EQ(url, base::StringPrintf( - "https://%s/swap/v1/quote?" - "takerAddress=0xa92D461a9a988A7f11ec285d39783A637Fdd6ba4&" + "https://api.0x.wallet.brave.com/swap/allowance-holder/price?" + "chainId=%s&" + "taker=0xa92D461a9a988A7f11ec285d39783A637Fdd6ba4&" "sellAmount=1000000000000000000000&" "buyToken=ETH&" "sellToken=DAI&" - "slippagePercentage=0.030000&" - "affiliateAddress=0xbd9420A98a7Bd6B89765e5715e169481602D9c3d&" - "skipValidation=true&" - "intentOnFilling=false", - domain.c_str())); + "slippageBps=300", + encoded_chain_id.c_str())); } // KO: unsupported network @@ -977,21 +906,19 @@ TEST_F(SwapServiceUnitTest, GetZeroExQuoteURL) { } TEST_F(SwapServiceUnitTest, GetZeroExTransactionURL) { - const std::map non_rfqt_chain_ids = { - {mojom::kSepoliaChainId, "sepolia.api.0x.wallet.brave.com"}, - {mojom::kBnbSmartChainMainnetChainId, "bsc.api.0x.wallet.brave.com"}, - {mojom::kAvalancheMainnetChainId, "avalanche.api.0x.wallet.brave.com"}, - {mojom::kFantomMainnetChainId, "fantom.api.0x.wallet.brave.com"}, - {mojom::kCeloMainnetChainId, "celo.api.0x.wallet.brave.com"}, - {mojom::kOptimismMainnetChainId, "optimism.api.0x.wallet.brave.com"}, - {mojom::kArbitrumMainnetChainId, "arbitrum.api.0x.wallet.brave.com"}, - {mojom::kBaseMainnetChainId, "base.api.0x.wallet.brave.com"}}; - - const std::map rfqt_chain_ids = { - {mojom::kMainnetChainId, "api.0x.wallet.brave.com"}, - {mojom::kPolygonMainnetChainId, "polygon.api.0x.wallet.brave.com"}}; - - for (const auto& [chain_id, domain] : non_rfqt_chain_ids) { + const std::map chain_ids = { + {mojom::kMainnetChainId, "1"}, + {mojom::kArbitrumMainnetChainId, "42161"}, + {mojom::kAvalancheMainnetChainId, "43114"}, + {mojom::kBaseMainnetChainId, "8453"}, + {mojom::kBlastMainnetChainId, "238"}, + {mojom::kBnbSmartChainMainnetChainId, "56"}, + {mojom::kLineaChainId, "59144"}, + {mojom::kOptimismMainnetChainId, "10"}, + {mojom::kPolygonMainnetChainId, "137"}, + {mojom::kScrollChainId, "534352"}}; + + for (const auto& [chain_id, encoded_chain_id] : chain_ids) { SCOPED_TRACE(testing::Message() << "chain_id: " << chain_id); // OK: with fees @@ -999,19 +926,20 @@ TEST_F(SwapServiceUnitTest, GetZeroExTransactionURL) { *GetCannedSwapQuoteParams(mojom::CoinType::ETH, chain_id, "DAI", mojom::CoinType::ETH, chain_id, "ETH", mojom::SwapProvider::kZeroEx), - "0.00875"); + "85"); EXPECT_EQ(url, base::StringPrintf( - "https://%s/swap/v1/quote?" - "takerAddress=0xa92D461a9a988A7f11ec285d39783A637Fdd6ba4&" + "https://api.0x.wallet.brave.com/swap/allowance-holder/quote?" + "chainId=%s&" + "taker=0xa92D461a9a988A7f11ec285d39783A637Fdd6ba4&" "sellAmount=1000000000000000000000&" "buyToken=ETH&" "sellToken=DAI&" - "buyTokenPercentageFee=0.00875&" - "feeRecipient=0xbd9420A98a7Bd6B89765e5715e169481602D9c3d&" - "slippagePercentage=0.030000&" - "affiliateAddress=0xbd9420A98a7Bd6B89765e5715e169481602D9c3d", - domain.c_str())); + "swapFeeBps=85&" + "swapFeeRecipient=0xbd9420A98a7Bd6B89765e5715e169481602D9c3d&" + "swapFeeToken=ETH&" + "slippageBps=300", + encoded_chain_id.c_str())); // OK: no fees url = swap_service_->GetZeroExTransactionURL( @@ -1021,58 +949,14 @@ TEST_F(SwapServiceUnitTest, GetZeroExTransactionURL) { ""); EXPECT_EQ(url, base::StringPrintf( - "https://%s/swap/v1/quote?" - "takerAddress=0xa92D461a9a988A7f11ec285d39783A637Fdd6ba4&" + "https://api.0x.wallet.brave.com/swap/allowance-holder/quote?" + "chainId=%s&" + "taker=0xa92D461a9a988A7f11ec285d39783A637Fdd6ba4&" "sellAmount=1000000000000000000000&" "buyToken=ETH&" "sellToken=DAI&" - "slippagePercentage=0.030000&" - "affiliateAddress=0xbd9420A98a7Bd6B89765e5715e169481602D9c3d", - domain.c_str())); - } - - // If RFQ-T liquidity is available, so intentOnFilling=true is specified - // while fetching firm quotes. - for (const auto& [chain_id, domain] : rfqt_chain_ids) { - SCOPED_TRACE(testing::Message() << "chain_id: " << chain_id); - - // OK: with fees - auto url = swap_service_->GetZeroExTransactionURL( - *GetCannedSwapQuoteParams(mojom::CoinType::ETH, chain_id, "DAI", - mojom::CoinType::ETH, chain_id, "ETH", - mojom::SwapProvider::kZeroEx), - "0.00875"); - EXPECT_EQ(url, - base::StringPrintf( - "https://%s/swap/v1/quote?" - "takerAddress=0xa92D461a9a988A7f11ec285d39783A637Fdd6ba4&" - "sellAmount=1000000000000000000000&" - "buyToken=ETH&" - "sellToken=DAI&" - "buyTokenPercentageFee=0.00875&" - "feeRecipient=0xbd9420A98a7Bd6B89765e5715e169481602D9c3d&" - "slippagePercentage=0.030000&" - "affiliateAddress=0xbd9420A98a7Bd6B89765e5715e169481602D9c3d&" - "intentOnFilling=true", - domain.c_str())); - - // OK: no fees - url = swap_service_->GetZeroExTransactionURL( - *GetCannedSwapQuoteParams(mojom::CoinType::ETH, chain_id, "DAI", - mojom::CoinType::ETH, chain_id, "ETH", - mojom::SwapProvider::kZeroEx), - ""); - EXPECT_EQ(url, - base::StringPrintf( - "https://%s/swap/v1/quote?" - "takerAddress=0xa92D461a9a988A7f11ec285d39783A637Fdd6ba4&" - "sellAmount=1000000000000000000000&" - "buyToken=ETH&" - "sellToken=DAI&" - "slippagePercentage=0.030000&" - "affiliateAddress=0xbd9420A98a7Bd6B89765e5715e169481602D9c3d&" - "intentOnFilling=true", - domain.c_str())); + "slippageBps=300", + encoded_chain_id.c_str())); } // KO: unsupported network @@ -1080,18 +964,18 @@ TEST_F(SwapServiceUnitTest, GetZeroExTransactionURL) { *GetCannedSwapQuoteParams(mojom::CoinType::ETH, "0x3", "DAI", mojom::CoinType::ETH, "0x3", "ETH", mojom::SwapProvider::kZeroEx), - "0.00875"), + "85"), ""); } TEST_F(SwapServiceUnitTest, IsSwapSupported) { const std::vector supported_chain_ids( {// ZeroEx - mojom::kMainnetChainId, mojom::kSepoliaChainId, - mojom::kPolygonMainnetChainId, mojom::kBnbSmartChainMainnetChainId, - mojom::kAvalancheMainnetChainId, mojom::kFantomMainnetChainId, - mojom::kCeloMainnetChainId, mojom::kOptimismMainnetChainId, - mojom::kArbitrumMainnetChainId, mojom::kBaseMainnetChainId, + mojom::kMainnetChainId, mojom::kArbitrumMainnetChainId, + mojom::kAvalancheMainnetChainId, mojom::kBaseMainnetChainId, + mojom::kBlastMainnetChainId, mojom::kBnbSmartChainMainnetChainId, + mojom::kLineaChainId, mojom::kOptimismMainnetChainId, + mojom::kPolygonMainnetChainId, mojom::kScrollChainId, // Jupiter mojom::kSolanaMainnet, diff --git a/components/brave_wallet/common/brave_wallet.mojom b/components/brave_wallet/common/brave_wallet.mojom index ce2b99865ab4..cc8fa0c4dd0c 100644 --- a/components/brave_wallet/common/brave_wallet.mojom +++ b/components/brave_wallet/common/brave_wallet.mojom @@ -455,67 +455,61 @@ union SwapQuoteUnion { union SwapTransactionUnion { string jupiter_transaction; - ZeroExQuote zero_ex_transaction; + ZeroExTransaction zero_ex_transaction; LiFiTransactionUnion lifi_transaction; SquidTransactionUnion squid_transaction; }; -struct ZeroExSource { - string name; - string proportion; -}; - struct ZeroExFee { - string fee_type; - string fee_token; - string fee_amount; - string billing_type; + string amount; + string token; + string type; }; struct ZeroExFees { ZeroExFee? zero_ex_fee; }; -struct ZeroExQuote { - string price; - string guaranteed_price; // Unused for price quote response - string to; // Unused for price quote response - string data; // Unused for price quote response - string value; +struct ZeroExTransaction { + string to; + string data; string gas; - string estimated_gas; string gas_price; - string protocol_fee; - string minimum_protocol_fee; - string buy_token_address; - string sell_token_address; - string buy_amount; - string sell_amount; - string allowance_target; - string sell_token_to_eth_rate; - string buy_token_to_eth_rate; - string estimated_price_impact; - array sources; - ZeroExFees fees; + string value; }; -struct ZeroExValidationError { - string field; - string code; - string reason; +struct ZeroExRouteFill { + string from; + string to; + string source; + string proportion_bps; }; -struct ZeroExGenericError { - string message; +struct ZeroExRoute { + array fills; +}; + +struct ZeroExQuote { + string buy_amount; + string buy_token; + ZeroExFees fees; + string gas; + string gas_price; + bool liquidity_available; + string min_buy_amount; + ZeroExRoute route; + string sell_amount; + string sell_token; + string total_network_fee; + + // Custom field + string allowance_target; }; struct ZeroExError { - string code; - string reason; - array validation_errors; - ZeroExGenericError? values; + string name; + string message; bool is_insufficient_liquidity; - bool is_insufficient_allowance; }; union SwapErrorUnion { diff --git a/components/brave_wallet_ui/common/async/__mocks__/bridge.ts b/components/brave_wallet_ui/common/async/__mocks__/bridge.ts index f909c27f44e6..c3bb70ed175e 100644 --- a/components/brave_wallet_ui/common/async/__mocks__/bridge.ts +++ b/components/brave_wallet_ui/common/async/__mocks__/bridge.ts @@ -140,29 +140,30 @@ export class MockedWalletApiProxy { createEmptyTokenBalancesRegistry() mockZeroExQuote = { - price: '1705.399509', - guaranteedPrice: '', - to: '', - data: '', - value: '124067000000000000', - gas: '280000', - estimatedGas: '280000', - gasPrice: '2000000000', - protocolFee: '0', - minimumProtocolFee: '0', - sellTokenAddress: '0x07865c6e87b9f70255377e024ace6630c1eaa37f', - buyTokenAddress: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - buyAmount: '211599920', - sellAmount: '124067000000000000', - allowanceTarget: '0x0000000000000000000000000000000000000000', - sellTokenToEthRate: '1', - buyTokenToEthRate: '1720.180416', - estimatedPriceImpact: '0.0782', - sources: [], + buyAmount: '100032748', + buyToken: '0xdac17f958d2ee523a2206206994597c13d831ec7', + sellAmount: '100000000', + sellToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', fees: { zeroExFee: undefined - } - } + }, + gas: '288095', + gasPrice: '7062490000', + liquidityAvailable: true, + minBuyAmount: '99032421', + route: { + fills: [ + { + from: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + to: '0xdac17f958d2ee523a2206206994597c13d831ec7', + source: 'SolidlyV3', + proportionBps: '10000' + } + ] + }, + totalNetworkFee: '2034668056550000', + allowanceTarget: '0x0000000000001fF3684f28c67538d4D072C22734' + } as BraveWallet.ZeroExQuote mockZeroExTransaction = { allowanceTarget: '', @@ -521,19 +522,15 @@ export class MockedWalletApiProxy { } } - const { fromToken, toToken, fromAmount, toAmount } = - zeroExTransactionParams - return { error: null, response: { zeroExTransaction: { - ...this.mockZeroExQuote, - buyTokenAddress: toToken, - sellTokenAddress: fromToken, - buyAmount: toAmount || '', - sellAmount: fromAmount || '', - price: '1' + to: '0x7f6cee965959295cc64d0e6c00d99d6532d8e86b', + data: '0xdeadbeef', + gas: '288079', + gasPrice: '4837860000', + value: '0' }, jupiterTransaction: undefined, lifiTransaction: undefined, @@ -1401,7 +1398,7 @@ export class MockedWalletApiProxy { this.mockZeroExQuote = newQuote } - setMockedTransactionPayload(newTx: typeof this.mockZeroExQuote) { + setMockedTransactionPayload(newTx: typeof this.mockZeroExTransaction) { this.mockZeroExTransaction = newTx } diff --git a/components/brave_wallet_ui/page/screens/swap/hooks/useSwap.ts b/components/brave_wallet_ui/page/screens/swap/hooks/useSwap.ts index 82690469c263..a0dab4ff549d 100644 --- a/components/brave_wallet_ui/page/screens/swap/hooks/useSwap.ts +++ b/components/brave_wallet_ui/page/screens/swap/hooks/useSwap.ts @@ -1006,15 +1006,12 @@ export const useSwap = () => { return 'insufficientAllowance' } + // 0x specific validations if (quoteErrorUnion?.zeroExError) { if (quoteErrorUnion.zeroExError.isInsufficientLiquidity) { return 'insufficientLiquidity' } - if (quoteErrorUnion.zeroExError.isInsufficientAllowance) { - return 'insufficientAllowance' - } - return 'unknownError' } diff --git a/components/brave_wallet_ui/page/screens/swap/hooks/useZeroEx.ts b/components/brave_wallet_ui/page/screens/swap/hooks/useZeroEx.ts index b52885a63025..cef442252806 100644 --- a/components/brave_wallet_ui/page/screens/swap/hooks/useZeroEx.ts +++ b/components/brave_wallet_ui/page/screens/swap/hooks/useZeroEx.ts @@ -112,7 +112,7 @@ export function useZeroEx(params: SwapParams) { return } - const { data, to, value, estimatedGas } = + const { data, to, value, gas } = transactionResponse.response.zeroExTransaction try { @@ -120,7 +120,7 @@ export function useZeroEx(params: SwapParams) { fromAccount, to, value: new Amount(value).toHex(), - gasLimit: new Amount(estimatedGas).toHex(), + gasLimit: new Amount(gas).toHex(), data: hexStrToNumberArray(data), network: fromNetwork }) diff --git a/components/brave_wallet_ui/page/screens/swap/swap.utils.ts b/components/brave_wallet_ui/page/screens/swap/swap.utils.ts index 04b10559c361..5a4767d25263 100644 --- a/components/brave_wallet_ui/page/screens/swap/swap.utils.ts +++ b/components/brave_wallet_ui/page/screens/swap/swap.utils.ts @@ -71,6 +71,26 @@ export function getZeroExQuoteOptions({ }): QuoteOption[] { const networkFee = getZeroExNetworkFee({ quote, fromNetwork }) + const fromAmount = new Amount(quote.sellAmount).divideByDecimals( + fromToken.decimals + ) + + const toAmount = new Amount(quote.buyAmount).divideByDecimals( + toToken.decimals + ) + + const fromAmountFiat = fromAmount.times( + getTokenPriceAmountFromRegistry(spotPrices, fromToken) + ) + + const toAmountFiat = toAmount.times( + getTokenPriceAmountFromRegistry(spotPrices, toToken) + ) + + const fiatDiff = toAmountFiat.minus(fromAmountFiat) + const fiatDiffRatio = fiatDiff.div(fromAmountFiat) + const impact = fiatDiffRatio.times(100).toAbsoluteValue() + return [ { fromAmount: new Amount(quote.sellAmount).divideByDecimals( @@ -83,11 +103,11 @@ export function getZeroExQuoteOptions({ rate: new Amount(quote.buyAmount) .divideByDecimals(toToken.decimals) .div(new Amount(quote.sellAmount).divideByDecimals(fromToken.decimals)), - impact: new Amount(quote.estimatedPriceImpact), - sources: ensureUnique(quote.sources, 'name') - .map((source) => ({ - name: source.name, - proportion: new Amount(source.proportion) + impact, + sources: ensureUnique(quote.route.fills, 'source') + .map((fill) => ({ + name: fill.source, + proportion: new Amount(fill.proportionBps).times(0.00001) })) .filter((source) => source.proportion.gt(0)), routing: 'split', // 0x supports split routing only diff --git a/ios/brave-ios/Sources/BraveWallet/Crypto/Stores/SwapTokenStore.swift b/ios/brave-ios/Sources/BraveWallet/Crypto/Stores/SwapTokenStore.swift index a85c0b7f8dd5..db6d20ee2feb 100644 --- a/ios/brave-ios/Sources/BraveWallet/Crypto/Stores/SwapTokenStore.swift +++ b/ios/brave-ios/Sources/BraveWallet/Crypto/Stores/SwapTokenStore.swift @@ -424,7 +424,7 @@ public class SwapTokenStore: ObservableObject, WalletObserverStore { } // these values are already in wei gasLimit = - "0x\(walletAmountFormatter.weiString(from: zeroExQuote.estimatedGas, radix: .hex, decimals: 0) ?? "0")" + "0x\(walletAmountFormatter.weiString(from: zeroExQuote.gas, radix: .hex, decimals: 0) ?? "0")" to = zeroExQuote.to value = "0x\(walletAmountFormatter.weiString(from: zeroExQuote.value, radix: .hex, decimals: 0) ?? "0")" @@ -647,43 +647,45 @@ public class SwapTokenStore: ObservableObject, WalletObserverStore { ) async { guard !Task.isCancelled else { return } let walletAmountFormatter = WalletAmountFormatter(decimalFormatStyle: .decimals(precision: 18)) + var buyTokenDecimal = 18 + var sellTokenDecimal = 18 + if let buyToken = selectedToToken { + buyTokenDecimal = Int(buyToken.decimals) + } + if let sellToken = selectedFromToken { + sellTokenDecimal = Int(sellToken.decimals) + } + let buyAmountDecimalString = + walletAmountFormatter.decimalString( + for: zeroExQuote.buyAmount, + decimals: buyTokenDecimal + ) ?? "" + let sellAmountDecimalString = + walletAmountFormatter.decimalString( + for: zeroExQuote.sellAmount, + decimals: sellTokenDecimal + ) ?? "" switch base { case .perSellAsset: - var decimal = 18 - if let buyToken = selectedToToken { - decimal = Int(buyToken.decimals) - } - let decimalString = - walletAmountFormatter.decimalString(for: zeroExQuote.buyAmount, decimals: decimal) ?? "" - if let bv = BDouble(decimalString) { - buyAmount = bv.decimalDescription + if let buyAmountBDouble = BDouble(buyAmountDecimalString) { + buyAmount = buyAmountBDouble.decimalDescription + if let sellAmountBDouble = BDouble(sellAmountDecimalString), sellAmountBDouble != 0 { + let rate = buyAmountBDouble / sellAmountBDouble + selectedFromTokenPrice = rate.decimalDescription + } } case .perBuyAsset: - var decimal = 18 - if let sellToken = selectedFromToken { - decimal = Int(sellToken.decimals) - } - let decimalString = - walletAmountFormatter.decimalString(for: zeroExQuote.sellAmount, decimals: decimal) ?? "" - if let bv = BDouble(decimalString) { - sellAmount = bv.decimalDescription - } - } - - if let bv = BDouble(zeroExQuote.price) { - switch base { - case .perSellAsset: - selectedFromTokenPrice = bv.decimalDescription - case .perBuyAsset: - // will need to invert price if price quote is based on buyAmount - if bv != 0 { - selectedFromTokenPrice = (1 / bv).decimalDescription + if let sellAmountBDouble = BDouble(sellAmountDecimalString) { + sellAmount = sellAmountBDouble.decimalDescription + if let buyAmountBDouble = BDouble(buyAmountDecimalString), buyAmountBDouble != 0 { + let rate = sellAmountBDouble / buyAmountBDouble + selectedFromTokenPrice = rate.decimalDescription } } } guard let accountInfo, - let gasLimit = BDouble(zeroExQuote.estimatedGas), + let gasLimit = BDouble(zeroExQuote.gas), let gasPrice = BDouble(zeroExQuote.gasPrice, over: "1000000000000000000"), let sellAmountValue = BDouble(sellAmount.normalizedDecimals), let fromToken = selectedFromToken diff --git a/ios/brave-ios/Sources/BraveWallet/Preview Content/MockContent.swift b/ios/brave-ios/Sources/BraveWallet/Preview Content/MockContent.swift index b6c202dfd7ba..ae374ed79d57 100644 --- a/ios/brave-ios/Sources/BraveWallet/Preview Content/MockContent.swift +++ b/ios/brave-ios/Sources/BraveWallet/Preview Content/MockContent.swift @@ -642,33 +642,31 @@ extension BraveWallet.SwapFees { extension BraveWallet.ZeroExQuote { /// Price quote for 1 ETH to BAT (sell amount entered) static let mockOneETHToBATQuote: BraveWallet.ZeroExQuote = .init( - price: "10137.516188895530252258", - guaranteedPrice: "", - to: "", - data: "", - value: "1000000000000000000", // 1 ETH - gas: "375000", - estimatedGas: "375000", - gasPrice: "46500000000", - protocolFee: "0", - minimumProtocolFee: "0", - buyTokenAddress: "0x0d8775f648430679a709e98d2b0cb6250d2887ef", // BAT - sellTokenAddress: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", // ETH buyAmount: "10137516188895530252258", - sellAmount: "1000000000000000000", // 1 ETH - allowanceTarget: "0x0000000000000000000000000000000000000000", - sellTokenToEthRate: "1", - buyTokenToEthRate: "10439.88960404354826969", - estimatedPriceImpact: "1.8958", - sources: [], + buyToken: "0x0d8775f648430679a709e98d2b0cb6250d2887ef", // BAT fees: .init( zeroExFee: .init( - feeType: "volume", - feeToken: "0x0d8775f648430679a709e98d2b0cb6250d2887ef", - feeAmount: "15286142457313100889", - billingType: "on-chain" + amount: "15286142457313100889", + token: "0x0d8775f648430679a709e98d2b0cb6250d2887ef", + type: "volume" ) - ) + ), + gas: "375000", + gasPrice: "46500000000", + liquidityAvailable: true, + minBuyAmount: "99032421", + route: .init( + fills: [.init( + from: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + to: "0xdac17f958d2ee523a2206206994597c13d831ec7", + source: "SolidlyV3", + proportionBps: "10000" + )] + ), + sellAmount: "1000000000000000000", // 1 ETH + sellToken: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", // ETH + totalNetworkFee: "2034668056550000", + allowanceTarget: "0x0000000000000000000000000000000000000000" ) } diff --git a/ios/brave-ios/Sources/BraveWallet/Preview Content/MockSwapService.swift b/ios/brave-ios/Sources/BraveWallet/Preview Content/MockSwapService.swift index 2b61fd55231a..d2e22ba774b1 100644 --- a/ios/brave-ios/Sources/BraveWallet/Preview Content/MockSwapService.swift +++ b/ios/brave-ios/Sources/BraveWallet/Preview Content/MockSwapService.swift @@ -28,26 +28,11 @@ class MockSwapService: BraveWalletSwapService { completion( .init( zeroExTransaction: .init( - price: "", - guaranteedPrice: "", to: "", data: "", - value: "", gas: "", - estimatedGas: "", gasPrice: "", - protocolFee: "", - minimumProtocolFee: "", - buyTokenAddress: "", - sellTokenAddress: "", - buyAmount: "", - sellAmount: "", - allowanceTarget: "", - sellTokenToEthRate: "", - buyTokenToEthRate: "", - estimatedPriceImpact: "", - sources: [], - fees: .init(zeroExFee: nil) + value: "" ) ), nil, diff --git a/ios/brave-ios/Tests/BraveWalletTests/SwapTokenStoreTests.swift b/ios/brave-ios/Tests/BraveWalletTests/SwapTokenStoreTests.swift index daf4756a2fac..596e2553491f 100644 --- a/ios/brave-ios/Tests/BraveWalletTests/SwapTokenStoreTests.swift +++ b/ios/brave-ios/Tests/BraveWalletTests/SwapTokenStoreTests.swift @@ -363,33 +363,24 @@ class SwapStoreTests: XCTestCase { ethTxManagerProxy, solTxManagerProxy, mockAssetManager ) = setupServices() let zeroExQuote: BraveWallet.ZeroExQuote = .init( - price: "", - guaranteedPrice: "", - to: "", - data: "", - value: "", - gas: "", - estimatedGas: "", - gasPrice: "", - protocolFee: "", - minimumProtocolFee: "", - buyTokenAddress: "", - sellTokenAddress: "", buyAmount: "2000000000000000000", - sellAmount: "", - allowanceTarget: "", - sellTokenToEthRate: "", - buyTokenToEthRate: "", - estimatedPriceImpact: "", - sources: [], + buyToken: "", fees: .init( zeroExFee: .init( - feeType: "", - feeToken: "", - feeAmount: "", - billingType: "" + amount: "", + token: "", + type: "" ) - ) + ), + gas: "", + gasPrice: "", + liquidityAvailable: true, + minBuyAmount: "", + route: .init(fills: []), + sellAmount: "", + sellToken: "", + totalNetworkFee: "", + allowanceTarget: "" ) swapService._quote = { _, completion in completion(.init(zeroExQuote: zeroExQuote), .mockEthFees, nil, "")