Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bright-foxes-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/mobile": patch
---

💄 restructure swap state screen layouts
5 changes: 5 additions & 0 deletions .changeset/clever-toes-beam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/mobile": patch
---

🐛 improve repay disabled button condition
5 changes: 5 additions & 0 deletions .changeset/cyan-flies-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/mobile": patch
---

🐛 fix swaps query invalidation
5 changes: 5 additions & 0 deletions .changeset/funny-aliens-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/mobile": patch
---

🐛 fix pay simulation pending state
5 changes: 5 additions & 0 deletions .changeset/great-dryers-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/mobile": patch
---

🐛 pass chain id to bridge calls
Comment thread
dieguezguille marked this conversation as resolved.
5 changes: 5 additions & 0 deletions .changeset/humble-cities-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/mobile": patch
---

♻️ pass explicit chain id to send calls hooks
5 changes: 5 additions & 0 deletions .changeset/khaki-pens-post.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/mobile": patch
---

♻️ decouple swap input state from route result
5 changes: 5 additions & 0 deletions .changeset/loose-papers-take.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/mobile": patch
---

♻️ migrate remaining flows to send calls
Comment on lines +1 to +5
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make this changeset empty unless the refactor is user-visible.

migrate remaining flows to send calls reads like an internal implementation detail, not a release note. If this is only transaction-plumbing work, this should be an empty changeset; otherwise, rewrite it in user-facing terms. Based on learnings, empty changesets are required in this repo for non-user-facing changes that do not warrant release notes.

5 changes: 5 additions & 0 deletions .changeset/wide-cats-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/mobile": patch
---

♻️ pass explicit chain id to read hooks
126 changes: 69 additions & 57 deletions src/components/add-funds/Bridge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,14 @@ import { useToastController } from "@tamagui/toast";
import { ScrollView, Spinner, Square, XStack, YStack } from "tamagui";

import { useMutation, useQuery } from "@tanstack/react-query";
import { switchChain, waitForTransactionReceipt } from "@wagmi/core";
import { switchChain, waitForCallsStatus, waitForTransactionReceipt } from "@wagmi/core";
import {
encodeFunctionData,
erc20Abi,
formatUnits,
getAddress,
isAddress,
parseUnits,
TransactionExecutionError,
UserRejectedRequestError,
zeroAddress,
type Hex,
} from "viem";
Expand All @@ -32,7 +30,7 @@ import AssetSelectSheet from "./AssetSelectSheet";
import { getBridgeSources, getRouteFrom, tokenCorrelation, type BridgeSources, type RouteFrom } from "../../utils/lifi";
import openBrowser from "../../utils/openBrowser";
import queryClient from "../../utils/queryClient";
import reportError from "../../utils/reportError";
import reportError, { classifyError } from "../../utils/reportError";
import useAccount from "../../utils/useAccount";
import useMarkets from "../../utils/useMarkets";
import ownerConfig from "../../utils/wagmi/owner";
Expand Down Expand Up @@ -116,7 +114,7 @@ export default function Bridge() {

const previousSourceRef = useRef<string | undefined>(undefined);

const effectiveSource = useMemo(() => {
const source = useMemo(() => {
if (assetGroups.length === 0) return;
const isValid =
!!selectedSource &&
Expand All @@ -135,17 +133,17 @@ export default function Bridge() {
if (group && asset) return { chain: group.chain.id, address: asset.token.address };
}, [assetGroups, selectedSource, bridge?.defaultChainId, bridge?.defaultTokenAddress]);

const selectedGroup = assetGroups.find((group) => group.chain.id === effectiveSource?.chain);
const selectedAsset = selectedGroup?.assets.find((asset) => asset.token.address === effectiveSource?.address);
const selectedGroup = assetGroups.find((group) => group.chain.id === source?.chain);
const selectedAsset = selectedGroup?.assets.find((asset) => asset.token.address === source?.address);

const sourceToken = selectedAsset?.token;
const sourceBalance = selectedAsset?.balance ?? 0n;
const sourceTokenAddress = sourceToken?.address;
const sourceTokenSymbol = sourceToken?.symbol;

const insufficientBalance = sourceAmount > sourceBalance;
const isSameChain = effectiveSource?.chain === chain.id;
const isNativeSource = effectiveSource?.address === zeroAddress;
const isSameChain = source?.chain === chain.id;
const isNativeSource = source?.address === zeroAddress;

const destinationTokens = useMemo(() => bridge?.tokensByChain[chain.id] ?? [], [bridge?.tokensByChain]);
const destinationBalances = useMemo(() => bridge?.balancesByChain[chain.id] ?? [], [bridge?.balancesByChain]);
Expand Down Expand Up @@ -198,7 +196,7 @@ export default function Bridge() {
const bridgeQuoteEnabled =
!!senderAddress &&
!!account &&
!!effectiveSource &&
!!source &&
!!sourceToken &&
!!destinationToken &&
sourceAmount > 0n &&
Expand All @@ -215,32 +213,37 @@ export default function Bridge() {
"quote",
senderAddress,
account,
effectiveSource,
source,
sourceToken,
destinationToken,
sourceAmount,
isSameChain,
],
queryFn: () => {
queryFn: async () => {
if (
!senderAddress ||
!account ||
!effectiveSource ||
!source ||
!sourceToken ||
!destinationToken ||
sourceAmount === 0n ||
isSameChain
)
throw new Error("invalid bridge parameters");
return getRouteFrom({
fromChainId: effectiveSource.chain,
toChainId: chain.id,
fromTokenAddress: sourceToken.address,
toTokenAddress: destinationToken.address,
fromAmount: sourceAmount,
fromAddress: senderAddress,
toAddress: account,
});
try {
return await getRouteFrom({
fromChainId: source.chain,
toChainId: chain.id,
fromTokenAddress: sourceToken.address,
toTokenAddress: destinationToken.address,
fromAmount: sourceAmount,
fromAddress: senderAddress,
toAddress: account,
});
} catch (error) {
reportError(error, { level: "warning" });
throw error;
}
},
enabled: bridgeQuoteEnabled,
refetchInterval: 15_000,
Expand All @@ -264,22 +267,26 @@ export default function Bridge() {
} = useSimulateContract({
config: senderConfig,
account: senderAddress,
chainId: transferSimulationEnabled ? effectiveSource.chain : undefined,
address: transferSimulationEnabled ? getAddress(effectiveSource.address) : undefined,
chainId: transferSimulationEnabled ? source.chain : undefined,
address: transferSimulationEnabled ? getAddress(source.address) : undefined,
abi: erc20Abi,
functionName: "transfer",
args: transferSimulationEnabled ? ([getAddress(account), sourceAmount] as const) : undefined,
query: { enabled: transferSimulationEnabled },
});

const approvalTokenAddress =
effectiveSource?.address && isAddress(effectiveSource.address) ? effectiveSource.address : undefined;
useEffect(() => {
if (transferSimulationError) reportError(transferSimulationError, { level: "warning" });
}, [transferSimulationError]);

const approvalTokenAddress = source?.address && isAddress(source.address) ? source.address : undefined;
const approvalSpenderAddress = bridgeQuote?.estimate.approvalAddress;
const approvalChainId = bridgeQuote?.chainId;

const canReadAllowance =
!!senderAddress &&
!!approvalTokenAddress &&
approvalTokenAddress !== zeroAddress &&
!!approvalChainId &&
!!approvalSpenderAddress &&
approvalSpenderAddress !== zeroAddress &&
Expand Down Expand Up @@ -309,19 +316,15 @@ export default function Bridge() {
setBridgePreview({ sourceToken, sourceAmount: BigInt(route.estimate.fromAmount) });
},
mutationFn: async (from) => {
if (!senderAddress || !effectiveSource || !account) throw new Error("missing bridge context");
if (!senderAddress || !source || !account) throw new Error("missing bridge context");
if (isSameChain) throw new Error("invalid bridge context");

setBridgeStatus(t("Switching to {{chain}}...", { chain: selectedGroup?.chain.name ?? `Chain ${from.chainId}` }));
await switchChain(senderConfig, { chainId: from.chainId });

const spender = from.estimate.approvalAddress;
const requiresApproval =
!!spender &&
spender !== zeroAddress &&
effectiveSource.address !== zeroAddress &&
source.address !== zeroAddress &&
isAddress(spender) &&
isAddress(effectiveSource.address);
isAddress(source.address);

let approval: Hex | undefined;
let currentAllowance = allowanceData;
Expand All @@ -348,24 +351,37 @@ export default function Bridge() {
}
}
setBridgeStatus(t("Submitting bridge transaction..."));
let id: string | undefined;
try {
await sendCallsTx({
const result = await sendCallsTx({
chainId: source.chain,
calls: [
...(approval ? [{ to: getAddress(effectiveSource.address), data: approval }] : []),
...(approval ? [{ to: getAddress(source.address), data: approval }] : []),
{ to: from.to, data: from.data, value: from.value },
],
});
setBridgeStatus(t("Bridge transaction submitted"));
id = result.id;
} catch (error) {
if (classifyError(error).authKnown) throw error;
reportError(error);
if (approval) {
const hash = await sendTx({ to: getAddress(effectiveSource.address), data: approval });
await waitForTransactionReceipt(senderConfig, { hash });
await switchChain(senderConfig, { chainId: source.chain });
try {
if (approval) {
const hash = await sendTx({ chainId: source.chain, to: getAddress(source.address), data: approval });
await waitForTransactionReceipt(senderConfig, { hash, chainId: source.chain });
}
const hash = await sendTx({ chainId: source.chain, to: from.to, data: from.data, value: from.value });
await waitForTransactionReceipt(senderConfig, { hash, chainId: source.chain });
} finally {
await switchChain(senderConfig, { chainId: chain.id }).catch(reportError);
}
const hash = await sendTx({ to: from.to, data: from.data, value: from.value });
await waitForTransactionReceipt(senderConfig, { hash });
setBridgeStatus(t("Bridge transaction submitted"));
return;
}
if (!id) throw new Error("missing sendCalls id");
const { status } = await waitForCallsStatus(senderConfig, { id });
if (status === "failure") throw new Error("failed to submit bridge transaction");
Comment thread
dieguezguille marked this conversation as resolved.
setBridgeStatus(t("Bridge transaction submitted"));
},
onSuccess: async () => {
toast.show(t("Bridge transaction submitted"), {
Expand Down Expand Up @@ -397,20 +413,19 @@ export default function Bridge() {
setBridgePreview({ sourceToken, sourceAmount });
},
mutationFn: async () => {
if (!senderAddress || !effectiveSource || !account) throw new Error("missing transfer context");
if (!senderAddress || !source || !account) throw new Error("missing transfer context");
if (!isSameChain) throw new Error("transfer mutation invoked for different chains");

await switchChain(senderConfig, { chainId: effectiveSource.chain });
setBridgeStatus(t("Submitting transfer transaction..."));
await switchChain(senderConfig, { chainId: chain.id });
Comment thread
dieguezguille marked this conversation as resolved.
const recipient = getAddress(account);
let hash: Hex;
if (isNativeSource) {
hash = await sendTx({ to: recipient, value: sourceAmount });
hash = await sendTx({ chainId: source.chain, to: recipient, value: sourceAmount });
} else {
if (!transferSimulation) throw new Error("missing transfer simulation");
hash = await transfer(transferSimulation.request);
Comment thread
dieguezguille marked this conversation as resolved.
Comment thread
sentry[bot] marked this conversation as resolved.
Comment thread
dieguezguille marked this conversation as resolved.
}
await waitForTransactionReceipt(senderConfig, { hash });
await waitForTransactionReceipt(senderConfig, { hash, chainId: source.chain });
setBridgeStatus(t("Transfer transaction submitted"));
},
onSuccess: async () => {
Expand Down Expand Up @@ -790,8 +805,7 @@ export default function Bridge() {
{t("Source network")}
</Text>
<Text caption color="$uiNeutralPrimary" textAlign="right" flexShrink={1}>
{selectedGroup?.chain.name ??
(effectiveSource?.chain ? t("Chain {{id}}", { id: effectiveSource.chain }) : "—")}
{selectedGroup?.chain.name ?? (source?.chain ? t("Chain {{id}}", { id: source.chain }) : "—")}
</Text>
</XStack>
<XStack justifyContent="space-between" alignItems="flex-start" flexWrap="wrap" gap="$s2">
Expand Down Expand Up @@ -961,7 +975,7 @@ export default function Bridge() {
setAssetSheetOpen(false);
}}
groups={assetGroups}
selected={effectiveSource}
selected={source}
onSelect={(chainId, token) => {
setSourceAmount(0n);
setSelectedSource({ chain: chainId, address: token.address });
Expand All @@ -987,12 +1001,10 @@ export default function Bridge() {
}

function handleError(error: unknown, toast: ReturnType<typeof useToastController>, t: TFunction, isTransfer?: boolean) {
if (error instanceof UserRejectedRequestError) return;
if (error instanceof TransactionExecutionError && error.shortMessage === "User rejected the request.") return;
toast.show(isTransfer ? t("Transfer failed. Please try again.") : t("Bridge failed. Please try again."), {
native: true,
duration: 1000,
burntOptions: { haptic: "error", preset: "error" },
});
reportError(error);
if (!reportError(error).authKnown)
toast.show(isTransfer ? t("Transfer failed. Please try again.") : t("Bridge failed. Please try again."), {
native: true,
duration: 1000,
burntOptions: { haptic: "error", preset: "error" },
});
}
3 changes: 2 additions & 1 deletion src/components/card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ScrollView, Separator, Spinner, Square, Switch, XStack, YStack } from "
import { useMutation, useQuery } from "@tanstack/react-query";

import accountInit from "@exactly/common/accountInit";
import { marketUSDCAddress } from "@exactly/common/generated/chain";
import chain, { marketUSDCAddress } from "@exactly/common/generated/chain";
import { useReadUpgradeableModularAccountGetInstalledPlugins } from "@exactly/common/generated/hooks";

import CardDetails from "./CardDetails";
Expand Down Expand Up @@ -97,6 +97,7 @@ export default function Card() {
const { refetch: refetchInstalledPlugins, isFetching: isFetchingPlugins } =
useReadUpgradeableModularAccountGetInstalledPlugins({
address,
chainId: chain.id,
factory: credential?.factory,
factoryData: credential && accountInit(credential),
query: { enabled: !!address && !!credential },
Expand Down
4 changes: 3 additions & 1 deletion src/components/defi/DeFi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { ScrollView, useTheme, XStack, YStack } from "tamagui";
import { useQuery } from "@tanstack/react-query";
import { useBytecode } from "wagmi";

import chain from "@exactly/common/generated/chain";

import AboutDefiSheet from "./AboutDefiSheet";
import ConnectionSheet from "./ConnectionSheet";
import DisconnectSheet from "./DisconnectSheet";
Expand All @@ -32,7 +34,7 @@ export default function DeFi() {
const { data: fundingConnected } = useQuery<boolean>({ queryKey: ["defi", "usdc-funding-connected"] });
const { data: lifiConnected } = useQuery<boolean>({ queryKey: ["defi", "lifi-connected"] });
const { address } = useAccount();
const { data: bytecode } = useBytecode({ address, query: { enabled: !!address } });
const { data: bytecode } = useBytecode({ address, chainId: chain.id, query: { enabled: !!address } });
const [aboutDefiSheetOpen, setAboutDefiSheetOpen] = useState(false);
const [fundingSheetOpen, setFundingSheetOpen] = useState(false);
const [lifiSheetOpen, setLifiSheetOpen] = useState(false);
Expand Down
4 changes: 3 additions & 1 deletion src/components/getting-started/GettingStarted.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { ScrollView, XStack, YStack } from "tamagui";
import { useQuery } from "@tanstack/react-query";
import { useBytecode } from "wagmi";

import chain from "@exactly/common/generated/chain";

import Step from "./Step";
import { presentArticle } from "../../utils/intercom";
import reportError from "../../utils/reportError";
Expand All @@ -25,7 +27,7 @@ import type { KYCStatus } from "../../utils/server";

function useOnboardingState() {
const { address: account } = useAccount();
const { data: bytecode } = useBytecode({ address: account, query: { enabled: !!account } });
const { data: bytecode } = useBytecode({ address: account, chainId: chain.id, query: { enabled: !!account } });
const { data: kycStatus } = useQuery<KYCStatus>({ queryKey: ["kyc", "status"] });
const isDeployed = !!bytecode;
const hasKYC = Boolean(
Expand Down
Loading