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"