diff --git a/examples/next/src/components/Swap.tsx b/examples/next/src/components/Swap.tsx index eea723be41..782970fa1d 100644 --- a/examples/next/src/components/Swap.tsx +++ b/examples/next/src/components/Swap.tsx @@ -22,12 +22,19 @@ export function Swap() { [account, ctrlConnector], ); + if (!account) { + return null; + } + return (

Token Swaps

- - + + + @@ -39,7 +46,50 @@ export function Swap() { ); } -const SWAP_SINGLE = [ +const AVNU_SWAP_SINGLE = [ + { + contractAddress: + "0x124aeb495b947201f5fac96fd1138e326ad86195b98df6dec9009158a533b49", + entrypoint: "approve", + calldata: [ + "0x4270219d365d6b017231b52e92b3fb5d7c8378b05e9abc97724537a80e93b0f", + "0x8ac7230489e80000", + "0x0", + ], + }, + { + contractAddress: + "0x4270219d365d6b017231b52e92b3fb5d7c8378b05e9abc97724537a80e93b0f", + entrypoint: "multi_route_swap", + calldata: [ + "0x124aeb495b947201f5fac96fd1138e326ad86195b98df6dec9009158a533b49", + "0x8ac7230489e80000", + "0x0", + "0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + "0x1c7af15aad8a1b00", + "0x0", + "0x1c73a6df73828b19", + "0x0", + "0x76a3565794db7894484718be7f51ad5b2e76605e22722887c1260e2451ad945", + "0x0", + "0x0", + "0x1", + "0x124aeb495b947201f5fac96fd1138e326ad86195b98df6dec9009158a533b49", + "0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + "0x5dd3d2f4429af886cd1a3b08289dbcea99a294197e9eb43b0e0325b4b", + "0xe8d4a51000", + "0x6", + "0x124aeb495b947201f5fac96fd1138e326ad86195b98df6dec9009158a533b49", + "0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + "0x28f5c28f5c28f5c28f5c28f5c28f5c2", + "0x4d5a", + "0x0", + "0xa26ea81948000000000000000000", + ], + }, +]; + +const EKUBO_SWAP_SINGLE = [ { contractAddress: "0x0124aeb495b947201f5faC96fD1138E326AD86195B98df6DEc9009158A533B49", @@ -97,7 +147,7 @@ const SWAP_SINGLE = [ }, ]; -const SWAP_MULTIPLE = [ +const EKUBO_SWAP_MULTIPLE = [ { contractAddress: "0x0124aeb495b947201f5faC96fD1138E326AD86195B98df6DEc9009158A533B49", @@ -217,7 +267,7 @@ const LS2_PURCHASE_GAME = [ entrypoint: "transfer", calldata: [ "0x0199741822c2dc722f6f605204f35e56dbc23bceed54818168c4c49e4fb8737e", - "0x5bbb37da193af4ba9", + "0x155ee7c39a3d69a76", "0x0", ], }, @@ -289,7 +339,7 @@ const LS2_PURCHASE_GAME_ERROR = [ entrypoint: "transfer", calldata: [ "0x0199741822c2dc722f6f605204f35e56dbc23bceed54818168c4c49e4fb8737e", - "0x5bbb37da193af4ba90000000", + "0x5bbb37d193af4ba", "0x0", ], }, diff --git a/packages/keychain/src/components/ExecutionContainer.tsx b/packages/keychain/src/components/ExecutionContainer.tsx index b9805e3fb2..07c6c5930c 100644 --- a/packages/keychain/src/components/ExecutionContainer.tsx +++ b/packages/keychain/src/components/ExecutionContainer.tsx @@ -10,9 +10,10 @@ import { HeaderInner, type HeaderProps, LayoutFooter, + VerifiedIcon, } from "@cartridge/ui"; import { isEqual } from "@/utils"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { createDeployUrl } from "@/utils/connection/deploy"; import { Fees, FeesData } from "@/components/Fees"; @@ -45,7 +46,7 @@ export function ExecutionContainer({ children, }: ExecutionContainerProps & Pick) { - const { controller } = useConnection(); + const { controller, policies } = useConnection(); const { navigate } = useNavigation(); const [maxFee, setMaxFee] = useState(); const [ctrlError, setCtrlError] = useState( @@ -151,11 +152,28 @@ export function ExecutionContainer({ } }, [ctaState, ctrlError, controller, navigate, onDeploy, resetState]); + const isVerified = useMemo( + () => + typeof description === "string" && description.startsWith("http") + ? policies?.verified === true + : undefined, + [policies, description], + ); + return ( <> + + {description} +
+ ) : ( + description + ) + } icon={icon} right={right} hideIcon={!icon} diff --git a/packages/keychain/src/components/Fees.tsx b/packages/keychain/src/components/Fees.tsx index ad55cc813a..e1fde1d562 100644 --- a/packages/keychain/src/components/Fees.tsx +++ b/packages/keychain/src/components/Fees.tsx @@ -76,7 +76,7 @@ export function Fees({ /> )} {ctrlError} - {(displayFees || additionalFees?.length !== 0) && ( + {(displayFees || (additionalFees && additionalFees.length > 0)) && (
- {walletIcon} Purchase with {walletName}{" "} + {walletIcon} + Purchase with {walletName} {bridgeFrom ? `(${bridgeFrom})` : ""}
diff --git a/packages/keychain/src/components/purchasenew/starterpack/starterpack.tsx b/packages/keychain/src/components/purchasenew/starterpack/starterpack.tsx index 2a6a9166c6..6e30a58163 100644 --- a/packages/keychain/src/components/purchasenew/starterpack/starterpack.tsx +++ b/packages/keychain/src/components/purchasenew/starterpack/starterpack.tsx @@ -293,13 +293,14 @@ export function OnchainStarterPackInner({ )}
- {wallet.subIcon} Purchase with {wallet.name} + {wallet.subIcon} + Purchase with {wallet.name}
diff --git a/packages/keychain/src/components/swap/ConfirmSwap.tsx b/packages/keychain/src/components/swap/ConfirmSwap.tsx index 296f0ae8af..54369c67ad 100644 --- a/packages/keychain/src/components/swap/ConfirmSwap.tsx +++ b/packages/keychain/src/components/swap/ConfirmSwap.tsx @@ -1,7 +1,4 @@ -import { useState } from "react"; import { - Button, - GearIcon, LayoutContent, TokenCard, TokenSummary, @@ -30,15 +27,12 @@ export function ConfirmSwap({ executionError, origin, }: ConfirmSwapProps) { - const [advancedVisible, setAdvancedVisible] = useState(false); - - const { isSwap, swapTransactions, additionalMethodCount } = - useSwapTransactions(transactions); + const { isSwap, swapTransfers } = useSwapTransactions(transactions); const { tokenSwapData: sellingSwapData } = useTokenSwapData( - swapTransactions.selling, + swapTransfers.selling, ); const { tokenSwapData: buyingSwapData } = useTokenSwapData( - swapTransactions.buying, + swapTransfers.buying, ); const formatAmount = (token: TokenSwapData) => { @@ -62,21 +56,9 @@ export function ConfirmSwap({ executionError={executionError} onSubmit={onSubmit} onError={onError} - buttonText={`Swap ${additionalMethodCount > 0 ? `+ ${additionalMethodCount}` : ""}`} - right={ - !advancedVisible ? ( - - ) : undefined - } + buttonText="Swap" additionalFees={sellingSwapData.map((token) => ({ - label: "Total", + label: "Cost", contractAddress: token.address, amount: token?.amount ?? 0, usdValue: formatValue(token), @@ -88,14 +70,19 @@ export function ConfirmSwap({ ) : ( <> - + {sellingSwapData.map((token) => ( @@ -103,16 +90,21 @@ export function ConfirmSwap({ {buyingSwapData.map((token) => ( ))} - {advancedVisible && } + )} diff --git a/packages/keychain/src/components/swap/abis/avnuExchangeAbi.json b/packages/keychain/src/components/swap/abis/avnuExchangeAbi.json new file mode 100644 index 0000000000..c5e3692ac1 --- /dev/null +++ b/packages/keychain/src/components/swap/abis/avnuExchangeAbi.json @@ -0,0 +1,840 @@ +[ + { + "type": "impl", + "name": "ExchangeLocker", + "interface_name": "avnu::interfaces::locker::ILocker" + }, + { + "type": "interface", + "name": "avnu::interfaces::locker::ILocker", + "items": [ + { + "type": "function", + "name": "locked", + "inputs": [ + { + "name": "id", + "type": "core::integer::u32" + }, + { + "name": "data", + "type": "core::array::Array::" + } + ], + "outputs": [ + { + "type": "core::array::Array::" + } + ], + "state_mutability": "external" + } + ] + }, + { + "type": "impl", + "name": "Exchange", + "interface_name": "avnu::exchange::IExchange" + }, + { + "type": "enum", + "name": "core::bool", + "variants": [ + { + "name": "False", + "type": "()" + }, + { + "name": "True", + "type": "()" + } + ] + }, + { + "type": "struct", + "name": "core::integer::u256", + "members": [ + { + "name": "low", + "type": "core::integer::u128" + }, + { + "name": "high", + "type": "core::integer::u128" + } + ] + }, + { + "type": "struct", + "name": "avnu::models::DirectSwap", + "members": [ + { + "name": "exchange_address", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "percent", + "type": "core::integer::u128" + }, + { + "name": "additional_swap_params", + "type": "core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "avnu::models::AlternativeSwap", + "members": [ + { + "name": "exchange_address", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "percent", + "type": "core::integer::u128" + }, + { + "name": "additional_swap_params", + "type": "core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "avnu::models::BranchSwap", + "members": [ + { + "name": "principal", + "type": "avnu::models::DirectSwap" + }, + { + "name": "alternatives", + "type": "core::array::Array::" + } + ] + }, + { + "type": "enum", + "name": "avnu::models::RouteSwap", + "variants": [ + { + "name": "Direct", + "type": "avnu::models::DirectSwap" + }, + { + "name": "Branch", + "type": "avnu::models::BranchSwap" + } + ] + }, + { + "type": "struct", + "name": "avnu::models::Route", + "members": [ + { + "name": "sell_token", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "buy_token", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "swap", + "type": "avnu::models::RouteSwap" + } + ] + }, + { + "type": "interface", + "name": "avnu::exchange::IExchange", + "items": [ + { + "type": "function", + "name": "initialize", + "inputs": [ + { + "name": "owner", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "fee_recipient", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "fees_bps_0", + "type": "core::integer::u128" + }, + { + "name": "fees_bps_1", + "type": "core::integer::u128" + }, + { + "name": "swap_exact_token_to_fees_bps", + "type": "core::integer::u128" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "get_adapter_class_hash", + "inputs": [ + { + "name": "exchange_address", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [ + { + "type": "core::starknet::class_hash::ClassHash" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "set_adapter_class_hash", + "inputs": [ + { + "name": "exchange_address", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "adapter_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "external" + }, + { + "type": "function", + "name": "get_external_solver_adapter_class_hash", + "inputs": [ + { + "name": "external_solver_address", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [ + { + "type": "core::starknet::class_hash::ClassHash" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "set_external_solver_adapter_class_hash", + "inputs": [ + { + "name": "external_solver_address", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "adapter_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "external" + }, + { + "type": "function", + "name": "multi_route_swap", + "inputs": [ + { + "name": "sell_token_address", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "sell_token_amount", + "type": "core::integer::u256" + }, + { + "name": "buy_token_address", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "buy_token_amount", + "type": "core::integer::u256" + }, + { + "name": "buy_token_min_amount", + "type": "core::integer::u256" + }, + { + "name": "beneficiary", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "integrator_fee_amount_bps", + "type": "core::integer::u128" + }, + { + "name": "integrator_fee_recipient", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "routes", + "type": "core::array::Array::" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "external" + }, + { + "type": "function", + "name": "swap_exact_token_to", + "inputs": [ + { + "name": "sell_token_address", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "sell_token_amount", + "type": "core::integer::u256" + }, + { + "name": "sell_token_max_amount", + "type": "core::integer::u256" + }, + { + "name": "buy_token_address", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "buy_token_amount", + "type": "core::integer::u256" + }, + { + "name": "beneficiary", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "integrator_fee_amount_bps", + "type": "core::integer::u128" + }, + { + "name": "integrator_fee_recipient", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "routes", + "type": "core::array::Array::" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "external" + }, + { + "type": "function", + "name": "swap_external_solver", + "inputs": [ + { + "name": "user_address", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "sell_token_address", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "buy_token_address", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "beneficiary", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "external_solver_address", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "external_solver_adapter_calldata", + "type": "core::array::Array::" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "external" + } + ] + }, + { + "type": "impl", + "name": "FeeImpl", + "interface_name": "avnu::components::fee::IFee" + }, + { + "type": "struct", + "name": "avnu::components::fee::TokenFeeConfig", + "members": [ + { + "name": "weight", + "type": "core::integer::u32" + } + ] + }, + { + "type": "interface", + "name": "avnu::components::fee::IFee", + "items": [ + { + "type": "function", + "name": "get_fees_recipient", + "inputs": [], + "outputs": [ + { + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "set_fees_recipient", + "inputs": [ + { + "name": "fees_recipient", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "external" + }, + { + "type": "function", + "name": "get_fees_bps_0", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u128" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "set_fees_bps_0", + "inputs": [ + { + "name": "bps", + "type": "core::integer::u128" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "external" + }, + { + "type": "function", + "name": "get_fees_bps_1", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u128" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "set_fees_bps_1", + "inputs": [ + { + "name": "bps", + "type": "core::integer::u128" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "external" + }, + { + "type": "function", + "name": "get_swap_exact_token_to_fees_bps", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u128" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "set_swap_exact_token_to_fees_bps", + "inputs": [ + { + "name": "bps", + "type": "core::integer::u128" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "external" + }, + { + "type": "function", + "name": "get_token_fee_config", + "inputs": [ + { + "name": "token", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [ + { + "type": "avnu::components::fee::TokenFeeConfig" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "set_token_fee_config", + "inputs": [ + { + "name": "token", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "config", + "type": "avnu::components::fee::TokenFeeConfig" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "external" + }, + { + "type": "function", + "name": "is_integrator_whitelisted", + "inputs": [ + { + "name": "integrator", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "set_whitelisted_integrator", + "inputs": [ + { + "name": "integrator", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "whitelisted", + "type": "core::bool" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "external" + } + ] + }, + { + "type": "impl", + "name": "OwnableImpl", + "interface_name": "avnu_lib::components::ownable::IOwnable" + }, + { + "type": "interface", + "name": "avnu_lib::components::ownable::IOwnable", + "items": [ + { + "type": "function", + "name": "get_owner", + "inputs": [], + "outputs": [ + { + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "transfer_ownership", + "inputs": [ + { + "name": "new_owner", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "impl", + "name": "UpgradableImpl", + "interface_name": "avnu_lib::components::upgradable::IUpgradable" + }, + { + "type": "interface", + "name": "avnu_lib::components::upgradable::IUpgradable", + "items": [ + { + "type": "function", + "name": "upgrade_class", + "inputs": [ + { + "name": "new_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "constructor", + "name": "constructor", + "inputs": [ + { + "name": "owner", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "fee_recipient", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "fees_bps_0", + "type": "core::integer::u128" + }, + { + "name": "fees_bps_1", + "type": "core::integer::u128" + }, + { + "name": "swap_exact_token_to_fees_bps", + "type": "core::integer::u128" + } + ] + }, + { + "type": "event", + "name": "avnu::components::fee::FeeComponent::Event", + "kind": "enum", + "variants": [] + }, + { + "type": "event", + "name": "avnu_lib::components::ownable::OwnableComponent::OwnershipTransferred", + "kind": "struct", + "members": [ + { + "name": "previous_owner", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + }, + { + "name": "new_owner", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + } + ] + }, + { + "type": "event", + "name": "avnu_lib::components::ownable::OwnableComponent::Event", + "kind": "enum", + "variants": [ + { + "name": "OwnershipTransferred", + "type": "avnu_lib::components::ownable::OwnableComponent::OwnershipTransferred", + "kind": "nested" + } + ] + }, + { + "type": "event", + "name": "avnu_lib::components::upgradable::UpgradableComponent::Upgraded", + "kind": "struct", + "members": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "avnu_lib::components::upgradable::UpgradableComponent::Event", + "kind": "enum", + "variants": [ + { + "name": "Upgraded", + "type": "avnu_lib::components::upgradable::UpgradableComponent::Upgraded", + "kind": "nested" + } + ] + }, + { + "type": "event", + "name": "avnu::exchange::Exchange::Swap", + "kind": "struct", + "members": [ + { + "name": "taker_address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + }, + { + "name": "sell_address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + }, + { + "name": "sell_amount", + "type": "core::integer::u256", + "kind": "data" + }, + { + "name": "buy_address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + }, + { + "name": "buy_amount", + "type": "core::integer::u256", + "kind": "data" + }, + { + "name": "beneficiary", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "avnu::exchange::Exchange::OptimizedSwap", + "kind": "struct", + "members": [ + { + "name": "sell_token", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + }, + { + "name": "buy_token", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + }, + { + "name": "principal_price", + "type": "core::integer::u256", + "kind": "data" + }, + { + "name": "principal_amount_in", + "type": "core::integer::u256", + "kind": "data" + }, + { + "name": "alternative_price", + "type": "core::integer::u256", + "kind": "data" + }, + { + "name": "sell_token_amount_optimized", + "type": "core::integer::u256", + "kind": "data" + }, + { + "name": "buy_token_amount_optimized", + "type": "core::integer::u256", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "avnu::exchange::Exchange::Event", + "kind": "enum", + "variants": [ + { + "name": "FeeEvent", + "type": "avnu::components::fee::FeeComponent::Event", + "kind": "flat" + }, + { + "name": "OwnableEvent", + "type": "avnu_lib::components::ownable::OwnableComponent::Event", + "kind": "flat" + }, + { + "name": "UpgradableEvent", + "type": "avnu_lib::components::upgradable::UpgradableComponent::Event", + "kind": "flat" + }, + { + "name": "Swap", + "type": "avnu::exchange::Exchange::Swap", + "kind": "nested" + }, + { + "name": "OptimizedSwap", + "type": "avnu::exchange::Exchange::OptimizedSwap", + "kind": "nested" + } + ] + } +] diff --git a/packages/keychain/src/components/swap/ekuboRouterAbi.json b/packages/keychain/src/components/swap/abis/ekuboRouterAbi.json similarity index 100% rename from packages/keychain/src/components/swap/ekuboRouterAbi.json rename to packages/keychain/src/components/swap/abis/ekuboRouterAbi.json diff --git a/packages/keychain/src/components/swap/swap.test.ts b/packages/keychain/src/components/swap/swap.test.ts index 4cf92df8f7..70ef74248d 100644 --- a/packages/keychain/src/components/swap/swap.test.ts +++ b/packages/keychain/src/components/swap/swap.test.ts @@ -4,7 +4,7 @@ import { getChecksumAddress, type Call } from "starknet"; import { useIsSwapTransaction, useSwapTransactions } from "./swap"; // Transactions mirrored from examples/next/src/components/Swap.tsx -const SWAP_SINGLE: Call[] = [ +const EKUBO_SWAP_SINGLE: Call[] = [ { contractAddress: "0x0124aeb495b947201f5faC96fD1138E326AD86195B98df6DEc9009158A533B49", @@ -62,7 +62,7 @@ const SWAP_SINGLE: Call[] = [ }, ]; -const SWAP_MULTIPLE: Call[] = [ +const EKUBO_SWAP_MULTIPLE: Call[] = [ { contractAddress: "0x0124aeb495b947201f5faC96fD1138E326AD86195B98df6DEc9009158A533B49", @@ -247,6 +247,59 @@ const LS2_PURCHASE_GAME: Call[] = [ }, ]; +const EKUBO_SWAP_WITH_EXTRA: Call[] = [ + ...EKUBO_SWAP_SINGLE, + { contractAddress: "0x4", entrypoint: "buy_game", calldata: [] }, +]; + +const AVNU_SWAP: Call[] = [ + { + contractAddress: + "0x124aeb495b947201f5fac96fd1138e326ad86195b98df6dec9009158a533b49", + entrypoint: "approve", + calldata: [ + "0x4270219d365d6b017231b52e92b3fb5d7c8378b05e9abc97724537a80e93b0f", + "0x8ac7230489e80000", + "0x0", + ], + }, + { + contractAddress: + "0x4270219d365d6b017231b52e92b3fb5d7c8378b05e9abc97724537a80e93b0f", + entrypoint: "multi_route_swap", + calldata: [ + "0x124aeb495b947201f5fac96fd1138e326ad86195b98df6dec9009158a533b49", + "0x8ac7230489e80000", + "0x0", + "0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + "0x1c7af15aad8a1b00", + "0x0", + "0x1c73a6df73828b19", + "0x0", + "0x76a3565794db7894484718be7f51ad5b2e76605e22722887c1260e2451ad945", + "0x0", + "0x0", + "0x1", + "0x124aeb495b947201f5fac96fd1138e326ad86195b98df6dec9009158a533b49", + "0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + "0x5dd3d2f4429af886cd1a3b08289dbcea99a294197e9eb43b0e0325b4b", + "0xe8d4a51000", + "0x6", + "0x124aeb495b947201f5fac96fd1138e326ad86195b98df6dec9009158a533b49", + "0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + "0x28f5c28f5c28f5c28f5c28f5c28f5c2", + "0x4d5a", + "0x0", + "0xa26ea81948000000000000000000", + ], + }, +]; + +const AVNU_SWAP_WITH_EXTRA: Call[] = [ + ...AVNU_SWAP, + { contractAddress: "0x4", entrypoint: "buy_game", calldata: [] }, +]; + describe("useIsSwapTransaction", () => { it("returns false for empty transactions", () => { const { result } = renderHook(() => useIsSwapTransaction([])); @@ -264,6 +317,18 @@ describe("useIsSwapTransaction", () => { expect(result.current.isSwap).toBe(false); }); + it("returns false when ekubo methods are out of order", () => { + const { result } = renderHook(() => + useIsSwapTransaction([ + { contractAddress: "0x1", entrypoint: "multihop_swap", calldata: [] }, + { contractAddress: "0x1", entrypoint: "transfer", calldata: [] }, + { contractAddress: "0x1", entrypoint: "clear_minimum", calldata: [] }, + { contractAddress: "0x1", entrypoint: "clear", calldata: [] }, + ]), + ); + expect(result.current.isSwap).toBe(false); + }); + it("returns false when transfer entrypoint is missing", () => { const { result } = renderHook(() => useIsSwapTransaction([ @@ -312,159 +377,159 @@ describe("useIsSwapTransaction", () => { expect(result.current.isSwap).toBe(false); }); - it("returns true for SWAP_SINGLE", () => { - const { result } = renderHook(() => useIsSwapTransaction(SWAP_SINGLE)); - expect(result.current.isSwap).toBe(true); - }); - - it("returns true for SWAP_MULTIPLE", () => { - const { result } = renderHook(() => useIsSwapTransaction(SWAP_MULTIPLE)); - expect(result.current.isSwap).toBe(true); - }); - - it("returns true for LS2_PURCHASE_GAME (swap + additional calls)", () => { + it("returns false when avnu methods are out of order", () => { const { result } = renderHook(() => - useIsSwapTransaction(LS2_PURCHASE_GAME), + useIsSwapTransaction([ + { + contractAddress: "0x1", + entrypoint: "multi_route_swap", + calldata: [], + }, + { contractAddress: "0x1", entrypoint: "approve", calldata: [] }, + ]), ); - expect(result.current.isSwap).toBe(true); + expect(result.current.isSwap).toBe(false); }); }); describe("useSwapTransactions", () => { - it("returns correct defaults for empty transactions", () => { + it("no swaps", () => { const { result } = renderHook(() => useSwapTransactions([])); expect(result.current.isSwap).toBe(false); - expect(result.current.swapMethodCount).toBe(0); - expect(result.current.additionalMethodCount).toBe(0); - expect(result.current.swapTransactions.selling).toHaveLength(0); - expect(result.current.swapTransactions.buying).toHaveLength(0); + expect(result.current.swapCount).toBe(0); + expect(result.current.swapTransfers.selling).toHaveLength(0); + expect(result.current.swapTransfers.buying).toHaveLength(0); }); - describe("SWAP_SINGLE", () => { - it("is detected as a swap", () => { - const { result } = renderHook(() => useSwapTransactions(SWAP_SINGLE)); - expect(result.current.isSwap).toBe(true); - }); - - it("counts 4 swap methods and 0 additional calls", () => { - const { result } = renderHook(() => useSwapTransactions(SWAP_SINGLE)); - expect(result.current.swapMethodCount).toBe(4); - expect(result.current.additionalMethodCount).toBe(0); - }); - - it("has one selling token (the transferred token)", () => { - const { result } = renderHook(() => useSwapTransactions(SWAP_SINGLE)); - expect(result.current.swapTransactions.selling).toHaveLength(1); - expect(result.current.swapTransactions.selling[0].address).toBe( - getChecksumAddress( - "0x0124aeb495b947201f5faC96fD1138E326AD86195B98df6DEc9009158A533B49", - ), - ); - }); - - it("has one buying token (from clear_minimum)", () => { - const { result } = renderHook(() => useSwapTransactions(SWAP_SINGLE)); - expect(result.current.swapTransactions.buying).toHaveLength(1); - expect(result.current.swapTransactions.buying[0].address).toBe( - getChecksumAddress( - "0x016dea82a6588ca9fb7200125fa05631b1c1735a313e24afe9c90301e441a796", - ), - ); - }); - - it("decodes selling amount from transfer calldata", () => { - const { result } = renderHook(() => useSwapTransactions(SWAP_SINGLE)); - // transfer calldata: [recipient, amount_low="0x32a03ab37fef8ba51", amount_high="0x0"] - expect(result.current.swapTransactions.selling[0].amount).toBe( - BigInt("0x32a03ab37fef8ba51"), - ); - }); - - it("decodes buying minimum from clear_minimum calldata", () => { - const { result } = renderHook(() => useSwapTransactions(SWAP_SINGLE)); - // clear_minimum calldata: [token, minimum_low="0xa4de3d0e9ba40000", minimum_high="0x0"] - expect(result.current.swapTransactions.buying[0].amount).toBe( - BigInt("0xa4de3d0e9ba40000"), - ); - }); + it("Ekubo swap single", () => { + const { result } = renderHook(() => useSwapTransactions(EKUBO_SWAP_SINGLE)); + expect(result.current.isSwap).toBe(true); + expect(result.current.swapCount).toBe(1); + expect(result.current.swapTransfers.selling).toHaveLength(1); + expect(result.current.swapTransfers.selling[0].address).toBe( + getChecksumAddress( + "0x0124aeb495b947201f5faC96fD1138E326AD86195B98df6DEc9009158A533B49", + ), + ); + expect(result.current.swapTransfers.selling[0].amount).toBe( + BigInt("0x32a03ab37fef8ba51"), + ); + expect(result.current.swapTransfers.buying).toHaveLength(1); + expect(result.current.swapTransfers.buying[0].address).toBe( + getChecksumAddress( + "0x016dea82a6588ca9fb7200125fa05631b1c1735a313e24afe9c90301e441a796", + ), + ); + expect(result.current.swapTransfers.buying[0].amount).toBe( + BigInt("0xa4de3d0e9ba40000"), + ); }); - describe("SWAP_MULTIPLE", () => { - it("is detected as a swap", () => { - const { result } = renderHook(() => useSwapTransactions(SWAP_MULTIPLE)); - expect(result.current.isSwap).toBe(true); - }); - - it("counts 8 swap methods and 0 additional calls", () => { - const { result } = renderHook(() => useSwapTransactions(SWAP_MULTIPLE)); - // 2 transfers + 2 multihop_swaps + 2 clear_minimums + 2 clears = 8 - expect(result.current.swapMethodCount).toBe(8); - expect(result.current.additionalMethodCount).toBe(0); - }); - - it("aggregates selling amounts for the same token across both swaps", () => { - const { result } = renderHook(() => useSwapTransactions(SWAP_MULTIPLE)); - // Both transfers use the same contractAddress, so they're merged into one entry - expect(result.current.swapTransactions.selling).toHaveLength(1); - expect(result.current.swapTransactions.selling[0].address).toBe( - getChecksumAddress( - "0x0124aeb495b947201f5faC96fD1138E326AD86195B98df6DEc9009158A533B49", - ), - ); - expect(result.current.swapTransactions.selling[0].amount).toBe( - BigInt("0x176e9649d99dd740a") + BigInt("0x4f1eba34861ddd0"), - ); - }); - - it("has two distinct buying tokens from the two clear_minimums", () => { - const { result } = renderHook(() => useSwapTransactions(SWAP_MULTIPLE)); - expect(result.current.swapTransactions.buying).toHaveLength(2); - expect(result.current.swapTransactions.buying[0].address).toBe( - getChecksumAddress( - "0x01c3c8284d7eed443b42f47e764032a56eaf50a9079d67993b633930e3689814", - ), - ); - expect(result.current.swapTransactions.buying[1].address).toBe( - getChecksumAddress( - "0x0103eafe79f8631932530cc687dfcdeb013c883a82619ebf81be393e2953a87a", - ), - ); - }); + it("Ekubo swap multiple", () => { + const { result } = renderHook(() => + useSwapTransactions(EKUBO_SWAP_MULTIPLE), + ); + expect(result.current.isSwap).toBe(true); + expect(result.current.swapCount).toBe(2); + expect(result.current.swapTransfers.selling).toHaveLength(1); + expect(result.current.swapTransfers.selling[0].address).toBe( + getChecksumAddress( + "0x0124aeb495b947201f5faC96fD1138E326AD86195B98df6DEc9009158A533B49", + ), + ); + expect(result.current.swapTransfers.selling[0].amount).toBe( + BigInt("0x176e9649d99dd740a") + BigInt("0x4f1eba34861ddd0"), + ); + expect(result.current.swapTransfers.buying).toHaveLength(2); + expect(result.current.swapTransfers.buying[0].address).toBe( + getChecksumAddress( + "0x01c3c8284d7eed443b42f47e764032a56eaf50a9079d67993b633930e3689814", + ), + ); + expect(result.current.swapTransfers.buying[1].address).toBe( + getChecksumAddress( + "0x0103eafe79f8631932530cc687dfcdeb013c883a82619ebf81be393e2953a87a", + ), + ); }); - describe("LS2_PURCHASE_GAME", () => { - it("is detected as a swap", () => { - const { result } = renderHook(() => - useSwapTransactions(LS2_PURCHASE_GAME), - ); - expect(result.current.isSwap).toBe(true); - }); + it("Ekubo swap with extra transactions", () => { + const { result } = renderHook(() => + useSwapTransactions(EKUBO_SWAP_WITH_EXTRA), + ); + expect(result.current.isSwap).toBe(true); + expect(result.current.swapCount).toBe(1); + expect(result.current.swapTransfers.selling).toHaveLength(1); + expect(result.current.swapTransfers.selling[0].address).toBe( + getChecksumAddress( + "0x0124aeb495b947201f5faC96fD1138E326AD86195B98df6DEc9009158A533B49", + ), + ); + expect(result.current.swapTransfers.selling[0].amount).toBe( + BigInt("0x32a03ab37fef8ba51"), + ); + expect(result.current.swapTransfers.buying).toHaveLength(1); + expect(result.current.swapTransfers.buying[0].address).toBe( + getChecksumAddress( + "0x016dea82a6588ca9fb7200125fa05631b1c1735a313e24afe9c90301e441a796", + ), + ); + expect(result.current.swapTransfers.buying[0].amount).toBe( + BigInt("0xa4de3d0e9ba40000"), + ); + }); - it("counts 4 swap methods and 2 additional calls (approve + buy_game)", () => { - const { result } = renderHook(() => - useSwapTransactions(LS2_PURCHASE_GAME), - ); - expect(result.current.swapMethodCount).toBe(4); - expect(result.current.additionalMethodCount).toBe(2); - }); + it("LS2 purchase game", () => { + const { result } = renderHook(() => useSwapTransactions(LS2_PURCHASE_GAME)); + expect(result.current.isSwap).toBe(true); + expect(result.current.swapCount).toBe(1); + expect(result.current.swapTransfers.selling).toHaveLength(1); + expect(result.current.swapTransfers.selling[0].address).toBe( + getChecksumAddress( + "0x0124aeb495b947201f5fac96fd1138e326ad86195b98df6dec9009158a533b49", + ), + ); + expect(result.current.swapTransfers.selling[0].amount).toBe( + BigInt("0x5bbb37da193af4ba9"), + ); + expect(result.current.swapTransfers.buying).toHaveLength(1); + expect(result.current.swapTransfers.buying[0].address).toBe( + getChecksumAddress( + "0x036017e69d21d6d8c13e266eabb73ef1f1d02722d86bdcabe5f168f8e549d3cd", + ), + ); + expect(result.current.swapTransfers.buying[0].amount).toBe(BigInt("1")); + }); - it("has one selling token", () => { - const { result } = renderHook(() => - useSwapTransactions(LS2_PURCHASE_GAME), - ); - expect(result.current.swapTransactions.selling).toHaveLength(1); - expect(result.current.swapTransactions.selling[0].address).toBe( - getChecksumAddress( - "0x0124aeb495b947201f5fac96fd1138e326ad86195b98df6dec9009158a533b49", - ), - ); - }); + it("Avnu swap", () => { + const { result } = renderHook(() => useSwapTransactions(AVNU_SWAP)); + expect(result.current.isSwap).toBe(true); + expect(result.current.swapCount).toBe(1); + expect(result.current.swapTransfers.selling).toHaveLength(1); + expect(result.current.swapTransfers.selling[0].address).toBe( + getChecksumAddress( + "0x124aeb495b947201f5fac96fd1138e326ad86195b98df6dec9009158a533b49", + ), + ); + expect(result.current.swapTransfers.selling[0].amount).toBe( + BigInt("0x8ac7230489e80000"), + ); + expect(result.current.swapTransfers.buying).toHaveLength(1); + expect(result.current.swapTransfers.buying[0].address).toBe( + getChecksumAddress( + "0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + ), + ); + expect(result.current.swapTransfers.buying[0].amount).toBe( + BigInt("0x1c7af15aad8a1b00"), + ); + }); - it("has one buying token", () => { - const { result } = renderHook(() => - useSwapTransactions(LS2_PURCHASE_GAME), - ); - expect(result.current.swapTransactions.buying).toHaveLength(1); - }); + it("Avnu swap plus extra transactions", () => { + const { result } = renderHook(() => + useSwapTransactions(AVNU_SWAP_WITH_EXTRA), + ); + expect(result.current.isSwap).toBe(true); + expect(result.current.swapCount).toBe(1); }); }); diff --git a/packages/keychain/src/components/swap/swap.ts b/packages/keychain/src/components/swap/swap.ts index 1d9fe958be..ff754e5860 100644 --- a/packages/keychain/src/components/swap/swap.ts +++ b/packages/keychain/src/components/swap/swap.ts @@ -1,10 +1,47 @@ import { useMemo } from "react"; import { getChecksumAddress, type Call } from "starknet"; -import { useDecodeTransactionInputs } from "@/hooks/calldata-decode"; import { TokenSwap } from "@/hooks/token"; +import { + decodeErc20TransferInputs, + decodeEkuboClearMinimumInputs, + decodeAvnuMultiRouteSwapInputs, +} from "@/hooks/calldata-decode"; -const findTransactions = (transactions: Call[], entrypoint: string) => { - return transactions.filter((t) => t.entrypoint === entrypoint); +// Each entry is an ordered sequence of entrypoints that constitutes a swap group. +// Add new DEX patterns here without changing any other logic. +const swapPatterns: string[][] = [ + [ + "transfer", + "multihop_swap", + "clear_minimum", + "clear", + "approve", + "buy_game", + ], // LS2 + ["transfer", "multihop_swap", "clear_minimum", "clear"], // Ekubo + ["approve", "multi_route_swap"], // Avnu +]; + +const detectSwapGroups = (transactions: Call[]): Call[][] => { + const groups: Call[][] = []; + let i = 0; + + while (i < transactions.length) { + const pattern = swapPatterns.find( + (p) => + i + p.length <= transactions.length && + p.every((method, j) => transactions[i + j].entrypoint === method), + ); + + if (pattern) { + groups.push(transactions.slice(i, i + pattern.length)); + i += pattern.length; + } else { + i++; + } + } + + return groups; }; // detect swap transaction @@ -14,19 +51,11 @@ export const useIsSwapTransaction = ( ): { isSwap: boolean; } => { - const isSwap = useMemo( - () => - transactions.length >= 4 && - findTransactions(transactions, "transfer").length > 0 && - findTransactions(transactions, "multihop_swap").length > 0 && - findTransactions(transactions, "clear_minimum").length > 0 && - findTransactions(transactions, "clear").length > 0, - [transactions], - ); - return { isSwap }; + const swaps = useMemo(() => detectSwapGroups(transactions), [transactions]); + return { isSwap: swaps.length > 0 }; }; -export type SwapTransactions = { +export type SwapTransfers = { selling: TokenSwap[]; buying: TokenSwap[]; }; @@ -35,33 +64,15 @@ export const useSwapTransactions = ( transactions: Call[], ): { isSwap: boolean; - swapTransactions: SwapTransactions; - swapMethodCount: number; - additionalMethodCount: number; + swapTransfers: SwapTransfers; + swapCount: number; + swapTransactionCount: number; + additionalTransactionCount: number; } => { - const { isSwap } = useIsSwapTransaction(transactions); + const swaps = useMemo(() => detectSwapGroups(transactions), [transactions]); - const { decodeTransferInputs, decodeClearMinimumInputs } = - useDecodeTransactionInputs(); - - const [swapTransactions, swapMethodCount] = useMemo(() => { - const swapTransactions: SwapTransactions = { - selling: [], - buying: [], - }; - if (!isSwap) return [swapTransactions, 0]; - - const transfers = findTransactions(transactions, "transfer"); - const multihop_swaps = findTransactions(transactions, "multihop_swap"); - const clear_minimuns = findTransactions(transactions, "clear_minimum"); - const clears = findTransactions(transactions, "clear"); - - const transferInputs = transfers.map((transfer) => - decodeTransferInputs(transfer.calldata as string[]), - ); - const clearInputs = clear_minimuns.map((clear_minimum) => - decodeClearMinimumInputs(clear_minimum.calldata as string[]), - ); + const swapTransfers = useMemo(() => { + const result: SwapTransfers = { selling: [], buying: [] }; const addToken = (acc: TokenSwap[], address: string, amount: bigint) => { const token = acc.find((t) => t.address === address); @@ -70,38 +81,77 @@ export const useSwapTransactions = ( } else { acc.push({ address, amount }); } - return acc; }; - swapTransactions.selling = transferInputs.reduce((acc, input, index) => { - return addToken( - acc, - getChecksumAddress(transfers[index].contractAddress), - input.amount, - ); - }, [] as TokenSwap[]); - - swapTransactions.buying = clearInputs.reduce((acc, input) => { - return addToken( - acc, - getChecksumAddress(input.token.contract_address), - input.minimum, - ); - }, [] as TokenSwap[]); + for (const group of swaps) { + for (const call of group) { + try { + if (call.entrypoint === "transfer") { + const input = decodeErc20TransferInputs(call.calldata as string[]); + addToken( + result.selling, + getChecksumAddress(call.contractAddress), + input.amount, + ); + } else if (call.entrypoint === "clear_minimum") { + const input = decodeEkuboClearMinimumInputs( + call.calldata as string[], + ); + addToken( + result.buying, + getChecksumAddress(input.token.contract_address), + input.minimum, + ); + } else if (call.entrypoint === "multi_route_swap") { + const input = decodeAvnuMultiRouteSwapInputs( + call.calldata as string[], + ); + addToken( + result.selling, + getChecksumAddress(input.sell_token_address), + input.sell_token_amount, + ); + addToken( + result.buying, + getChecksumAddress(input.buy_token_address), + input.buy_token_amount, + ); + } else if (call.entrypoint === "buy_game") { + result.buying = []; + addToken( + result.buying, + getChecksumAddress( + "0x036017e69d21d6d8c13e266eabb73ef1f1d02722d86bdcabe5f168f8e549d3cd", + ), + 1n, + ); + } + } catch (error) { + console.error( + `[useSwapTransactions] Error decoding transaction inputs:`, + call, + error, + ); + return null; + } + } + } - const count = - transfers.length + - multihop_swaps.length + - clear_minimuns.length + - clears.length; + return result; + }, [swaps]); - return [swapTransactions, count]; - }, [isSwap, transactions, decodeClearMinimumInputs, decodeTransferInputs]); + const swapCount = swapTransfers != null ? swaps.length : 0; + const swapTransactionCount = swaps.reduce( + (acc, group) => acc + group.length, + 0, + ); + const additionalTransactionCount = transactions.length - swapTransactionCount; return { - isSwap, - swapTransactions, - swapMethodCount, - additionalMethodCount: transactions.length - swapMethodCount, + isSwap: swapCount > 0, + swapTransfers: swapTransfers ?? { selling: [], buying: [] }, + swapCount, + swapTransactionCount, + additionalTransactionCount, }; }; diff --git a/packages/keychain/src/components/transaction/TransactionSummary.tsx b/packages/keychain/src/components/transaction/TransactionSummary.tsx index 95dc1122cb..2c22d2d794 100644 --- a/packages/keychain/src/components/transaction/TransactionSummary.tsx +++ b/packages/keychain/src/components/transaction/TransactionSummary.tsx @@ -4,7 +4,6 @@ import { AccordionItem, AccordionTrigger, CheckboxIcon, - GearIcon, Thumbnail, } from "@cartridge/ui"; import { cn } from "@cartridge/ui/utils"; @@ -33,9 +32,10 @@ export function TransactionSummary({ collapsible className={cn( "rounded border", + isExpanded ? "text-foreground-100" : "text-foreground-300", isOpened - ? "text-foreground-100 bg-background-200 border-transparent" - : "text-foreground-300 bg-transparent border-background-200", + ? "bg-background-200 border-transparent" + : "bg-transparent border-background-200", className, )} value={isOpened ? "item" : ""} @@ -46,43 +46,33 @@ export function TransactionSummary({ parentClassName="h-11 p-3 transition-none" className={cn( "flex items-center text-sm font-medium gap-1.5", - isOpened ? "text-foreground-100" : "text-foreground-300", + isExpanded ? "text-foreground-100" : "text-foreground-300", )} - color={cn(isOpened ? "text-foreground-100" : "text-foreground-300")} + color={cn(isExpanded ? "text-foreground-100" : "text-foreground-300")} > } + icon={calls.length} centered={true} className={cn( - isOpened - ? "text-foreground-100" - : "text-foreground-300 bg-background-200", + "text-xs", + isExpanded ? "text-foreground-100" : "text-foreground-300", + isOpened ? "bg-background-300" : "bg-background-200", )} /> -

Advanced

+

Operations

- {calls.map((call) => ( + {calls.map((call, index) => ( - {/*
-
-
-
-

- {humanizeString(call.entrypoint)} -

-
-
-
-
*/}
))}
@@ -94,11 +84,13 @@ export function TransactionSummary({ interface CollapsibleTransactionProps extends PropsWithChildren { transaction: Call; enabled: boolean; + highlighted?: boolean; } export function CollapsibleTransactionRow({ transaction, children, + highlighted, }: CollapsibleTransactionProps) { const [value, setValue] = useState(""); @@ -108,7 +100,10 @@ export function CollapsibleTransactionRow({
{ - const decodeInputs = useCallback( - (abi: Abi, interfaceName: string, method: string, args: string[]) => { - const interfaceAbi = abi.find( - (a) => a.name === interfaceName, - ) as InterfaceAbi; - const { inputs } = (interfaceAbi?.items ?? abi).find( - (a) => a.name === method, - ) as FunctionAbi; - const callData = new CallData(abi); - const decoded = callData.decodeParameters( - inputs.map((i) => i.type), - args, - ); - const result = inputs.reduce( - (acc, input, index) => { - acc[input.name] = Array.isArray(decoded) ? decoded[index] : decoded; - return acc; - }, - {} as { [key: string]: CallResult }, - ); - return result as T; +const decodeInputs = ( + abi: Abi, + interfaceName: string, + method: string, + args: string[], +) => { + const interfaceAbi = abi.find( + (a) => a.name === interfaceName, + ) as InterfaceAbi; + const { inputs } = (interfaceAbi?.items ?? abi).find( + (a) => a.name === method, + ) as FunctionAbi; + const callData = new CallData(abi); + const decoded = callData.decodeParameters( + inputs.map((i) => i.type), + args, + ); + const result = inputs.reduce( + (acc, input, index) => { + acc[input.name] = Array.isArray(decoded) ? decoded[index] : decoded; + return acc; }, - [], + {} as { [key: string]: CallResult }, ); - return { decodeInputs }; + return result as T; }; export type TransferInputs = { @@ -39,7 +38,12 @@ export type TransferInputs = { recipient: string; }; -export type MultihopSwapInputs = { +export type ApproveInputs = { + spender: string; + amount: bigint; +}; + +export type EkuboMultihopSwapInputs = { route: [ { pool_key: { @@ -62,70 +66,85 @@ export type MultihopSwapInputs = { }; }; -export interface ClearMinimumInputs { +export interface EkuboClearMinimumInputs { minimum: bigint; token: { contract_address: bigint; }; } -export interface ClearInputs { +export interface EkuboClearInputs { minimum: bigint; token: { contract_address: bigint; }; } -export const useDecodeTransactionInputs = () => { - const { decodeInputs } = useDecodeCallbackInputs(); +export type AvnuMultiRouteSwapInputs = { + sell_token_address: string; + sell_token_amount: bigint; + buy_token_address: string; + buy_token_amount: bigint; + buy_token_min_amount: bigint; + beneficiary: string; + integrator_fee_amount_bps: bigint; + integrator_fee_recipient: string; + routes: string[]; +}; - const decodeTransferInputs = useCallback( - (args: string[]) => { - return decodeInputs(erc20Abi, "ERC20", "transfer", args); - }, - [decodeInputs], +const decodeErc20TransferInputs = (args: string[]) => { + return decodeInputs(erc20Abi, "ERC20", "transfer", args); +}; + +const decodeErc20ApproveInputs = (args: string[]) => { + return decodeInputs(erc20Abi, "ERC20", "approve", args); +}; + +const decodeEkuboMultihopSwapInputs = (args: string[]) => { + return decodeInputs( + ekuboRouterAbi, + "ekubo::router::IRouter", + "multihop_swap", + args, ); +}; - const decodeMultihopSwapInputs = useCallback( - (args: string[]) => { - return decodeInputs( - ekuboRouterAbi, - "ekubo::router::IRouter", - "multihop_swap", - args, - ); - }, - [decodeInputs], +const decodeEkuboClearMinimumInputs = (args: string[]) => { + return decodeInputs( + ekuboRouterAbi, + "ekubo::components::clear::IClear", + "clear_minimum", + args, ); +}; - const decodeClearMinimumInputs = useCallback( - (args: string[]) => { - return decodeInputs( - ekuboRouterAbi, - "ekubo::components::clear::IClear", - "clear_minimum", - args, - ); - }, - [decodeInputs], +const decodeEkuboClearInputs = (args: string[]) => { + return decodeInputs( + ekuboRouterAbi, + "ekubo::components::clear::IClear", + "clear", + args, ); +}; - const decodeClearInputs = useCallback( - (args: string[]) => { - return decodeInputs( - ekuboRouterAbi, - "ekubo::components::clear::IClear", - "clear", - args, - ); - }, - [decodeInputs], +const decodeAvnuMultiRouteSwapInputs = (args: string[]) => { + const argsWithoutRoutes = [...args.slice(0, 11), "0x0"]; + return decodeInputs( + avnuExchangeAbi, + "avnu::exchange::IExchange", + "multi_route_swap", + argsWithoutRoutes, ); +}; - return { - decodeTransferInputs, - decodeMultihopSwapInputs, - decodeClearMinimumInputs, - decodeClearInputs, - }; +export { + //erc20 + decodeErc20TransferInputs, + decodeErc20ApproveInputs, + // Ekubo + decodeEkuboMultihopSwapInputs, + decodeEkuboClearMinimumInputs, + decodeEkuboClearInputs, + // Avnu + decodeAvnuMultiRouteSwapInputs, }; diff --git a/packages/keychain/src/hooks/token.ts b/packages/keychain/src/hooks/token.ts index 076ac45624..05399d8498 100644 --- a/packages/keychain/src/hooks/token.ts +++ b/packages/keychain/src/hooks/token.ts @@ -400,6 +400,7 @@ export type TokenSwap = { export type TokenSwapData = Metadata & { amount: number; value: number | null | undefined; + rounded: boolean; }; export type UseTokenSwapDataResponse = { @@ -414,8 +415,28 @@ export function useTokenSwapData( addresses: tokens.map((token) => token.address), }); - const tokenSwapData = useMemo(() => { + const tokenSwapData = useMemo(() => { return tokens.map((token) => { + if ( + token.address == + getChecksumAddress( + "0x036017e69d21d6d8c13e266eabb73ef1f1d02722d86bdcabe5f168f8e549d3cd", + ) + ) { + // special treatment for LS2 games + // to be replaced for balance simulation + return { + amount: 1, + name: "Adventurer", + symbol: "Adventurer", + decimals: 0, + address: token.address, + image: + "https://api.cartridge.gg/x/arcade-main/torii/static/0x036017e69d21d6d8c13e266eabb73ef1f1d02722d86bdcabe5f168f8e549d3cd/0x0000000000000000000000000000000000000000000000000000000000000001/image", + value: undefined, + rounded: false, + }; + } const metadata = erc20Metadata.find( (m) => BigInt(m.l2_token_address) === BigInt(token.address), ); @@ -434,6 +455,7 @@ export function useTokenSwapData( value: price ? (Number(price.amount) / 10 ** (price.decimals || 18)) * amount : undefined, + rounded: true, }; return tokenData; }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78a006214c..68448cd1f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ catalogs: specifier: ^6.2.4 version: 6.2.4 '@cartridge/ui': - specifier: github:cartridge-gg/ui#b4646934 + specifier: github:cartridge-gg/ui#27507b4 version: 0.7.14-alpha.2 '@eslint/js': specifier: ^9.18.0 @@ -166,7 +166,7 @@ importers: version: https://codeload.github.com/cartridge-gg/presets/tar.gz/476cc4f '@cartridge/ui': specifier: 'catalog:' - version: https://codeload.github.com/cartridge-gg/ui/tar.gz/b4646934(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(starknet@8.5.4)(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.11.24(@swc/helpers@0.5.17))(@types/node@16.18.11)(typescript@5.8.3)))(viem@2.28.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.24.4)) + version: https://codeload.github.com/cartridge-gg/ui/tar.gz/27507b4(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(starknet@8.5.4)(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.11.24(@swc/helpers@0.5.17))(@types/node@16.18.11)(typescript@5.8.3)))(viem@2.28.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.24.4)) '@graphql-codegen/cli': specifier: ^2.6.2 version: 2.16.5(@babel/core@7.27.1)(@swc/core@1.11.24(@swc/helpers@0.5.17))(@types/node@16.18.11)(bufferutil@4.0.9)(encoding@0.1.13)(graphql@16.12.0)(typescript@5.8.3)(utf-8-validate@5.0.10) @@ -246,7 +246,7 @@ importers: version: link:../../packages/controller '@cartridge/ui': specifier: 'catalog:' - version: https://codeload.github.com/cartridge-gg/ui/tar.gz/b4646934(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(starknet@8.5.4)(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.11.24(@swc/helpers@0.5.17))(@types/node@18.19.87)(typescript@5.8.3)))(viem@2.28.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.24.4)) + version: https://codeload.github.com/cartridge-gg/ui/tar.gz/27507b4(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(starknet@8.5.4)(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.11.24(@swc/helpers@0.5.17))(@types/node@18.19.87)(typescript@5.8.3)))(viem@2.28.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.24.4)) '@starknet-react/chains': specifier: ^5.0.1 version: 5.0.1 @@ -567,7 +567,7 @@ importers: version: 6.2.4 '@cartridge/ui': specifier: 'catalog:' - version: https://codeload.github.com/cartridge-gg/ui/tar.gz/b4646934(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(starknet@8.5.4)(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.11.24(@swc/helpers@0.5.17))(@types/node@18.19.87)(typescript@5.8.3)))(viem@2.28.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.24.4)) + version: https://codeload.github.com/cartridge-gg/ui/tar.gz/27507b4(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(starknet@8.5.4)(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.11.24(@swc/helpers@0.5.17))(@types/node@18.19.87)(typescript@5.8.3)))(viem@2.28.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.24.4)) '@dojoengine/sdk': specifier: 1.8.12 version: 1.8.12(@effect/platform@0.93.8(effect@3.19.8))(@opentelemetry/semantic-conventions@1.38.0)(@tanstack/react-query@5.75.0(react@18.3.1))(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(bufferutil@4.0.9)(get-starknet-core@4.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(starknet@8.5.4)(typescript@5.8.3)(use-sync-external-store@1.6.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@3.24.4) @@ -1269,8 +1269,8 @@ packages: resolution: {tarball: https://codeload.github.com/cartridge-gg/presets/tar.gz/90a5fe0} version: 0.0.1 - '@cartridge/ui@https://codeload.github.com/cartridge-gg/ui/tar.gz/b4646934': - resolution: {tarball: https://codeload.github.com/cartridge-gg/ui/tar.gz/b4646934} + '@cartridge/ui@https://codeload.github.com/cartridge-gg/ui/tar.gz/27507b4': + resolution: {tarball: https://codeload.github.com/cartridge-gg/ui/tar.gz/27507b4} version: 0.7.14-alpha.2 peerDependencies: react: ^18.2.0 || ^19.0.0 @@ -11485,7 +11485,7 @@ snapshots: dependencies: '@starknet-io/types-js': 0.8.4 - '@cartridge/ui@https://codeload.github.com/cartridge-gg/ui/tar.gz/b4646934(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(starknet@8.5.4)(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.11.24(@swc/helpers@0.5.17))(@types/node@16.18.11)(typescript@5.8.3)))(viem@2.28.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.24.4))': + '@cartridge/ui@https://codeload.github.com/cartridge-gg/ui/tar.gz/27507b4(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(starknet@8.5.4)(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.11.24(@swc/helpers@0.5.17))(@types/node@16.18.11)(typescript@5.8.3)))(viem@2.28.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.24.4))': dependencies: '@cartridge/presets': https://codeload.github.com/cartridge-gg/presets/tar.gz/90a5fe0 '@radix-ui/react-accordion': 1.2.1(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -11530,7 +11530,7 @@ snapshots: - react-native - tailwindcss - '@cartridge/ui@https://codeload.github.com/cartridge-gg/ui/tar.gz/b4646934(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(starknet@8.5.4)(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.11.24(@swc/helpers@0.5.17))(@types/node@18.19.87)(typescript@5.8.3)))(viem@2.28.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.24.4))': + '@cartridge/ui@https://codeload.github.com/cartridge-gg/ui/tar.gz/27507b4(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(starknet@8.5.4)(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.11.24(@swc/helpers@0.5.17))(@types/node@18.19.87)(typescript@5.8.3)))(viem@2.28.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.24.4))': dependencies: '@cartridge/presets': https://codeload.github.com/cartridge-gg/presets/tar.gz/90a5fe0 '@radix-ui/react-accordion': 1.2.1(@types/react-dom@18.3.7(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 46ca4bec6f..8bcc41ce99 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,7 +8,7 @@ catalog: "@cartridge/arcade": "0.3.12" "@cartridge/controller-wasm": "0.10.0" "@cartridge/penpal": "^6.2.4" - "@cartridge/ui": "github:cartridge-gg/ui#b4646934" + "@cartridge/ui": "github:cartridge-gg/ui#27507b4" "@eslint/js": "^9.18.0" "@noble/curves": "^1.9.0" "@noble/hashes": "^1.8.0"