Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/ sync staging → main #151

Merged
merged 7 commits into from
Feb 1, 2024
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
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ cp .env.example .env
Make sure to set the following environment variables in `.env` file:

```bash
NEXT_PUBLIC_API_URL="https://api.skip.money" # required
NEXT_PUBLIC_CLIENT_ID=
POLKACHU_USER= # required
POLKACHU_PASSWORD= # required
NEXT_PUBLIC_API_URL="https://api.skip.money"
NEXT_PUBLIC_CLIENT_ID= # optional
POLKACHU_USER= # required
POLKACHU_PASSWORD= # required
NEXT_PUBLIC_EDGE_CONFIG= # required
```

To retrieve `NEXT_PUBLIC_EDGE_CONFIG`, visit the [edge config token setup page](https://link.skip.money/ibc-fun-edge-config-token).

Read more on all available environment variables in [`.env.example`](.env.example) file.

## Script commands
Expand Down
70 changes: 55 additions & 15 deletions src/components/RouteDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @next/next/no-img-element */
import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/20/solid";
import { BridgeType, RouteResponse } from "@skip-router/core";
import { ethers } from "ethers";
import { formatUnits } from "ethers";
import { ComponentProps, Dispatch, Fragment, SetStateAction, SyntheticEvent, useMemo } from "react";

import { useAssets } from "@/context/assets";
Expand Down Expand Up @@ -175,7 +175,14 @@ function TransferStep({ action, actions, id, statusData }: TransferStepProps) {

const { getAsset } = useAssets();

const asset = getAsset(action.asset, action.sourceChain);
const asset = (() => {
const currentAsset = getAsset(action.asset, action.sourceChain);
if (currentAsset) return currentAsset;
const prevAction = actions[operationIndex - 1];
if (!prevAction || prevAction.type !== "TRANSFER") return;
const prevAsset = getAsset(prevAction.asset, prevAction.sourceChain);
return prevAsset;
})();

if (!sourceChain || !destinationChain) {
// this should be unreachable
Expand All @@ -184,17 +191,50 @@ function TransferStep({ action, actions, id, statusData }: TransferStepProps) {

if (!asset) {
return (
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<div className="flex h-14 w-14 flex-shrink-0 items-center justify-center">{renderTransferState}</div>
<div className="flex-1">
<Gap.Parent className="max-w-full text-sm text-neutral-500">
<span>Transfer to</span>
<img
className="inline-block h-4 w-4"
src={destinationChain.logoURI}
alt={destinationChain.prettyName}
/>
<span className="font-semibold text-black">{destinationChain.prettyName}</span>
<div className="max-w-[18rem] space-y-1 text-sm text-neutral-500">
<Gap.Parent>
<span>Transfer</span>
<span>from</span>
<Gap.Child>
<img
className="inline-block h-4 w-4"
src={sourceChain.logoURI}
alt={sourceChain.prettyName}
onError={onImageError}
/>
<span className="font-semibold text-black">{sourceChain.prettyName}</span>
</Gap.Child>
</Gap.Parent>
<Gap.Parent>
<span>to</span>
<Gap.Child>
<img
className="inline-block h-4 w-4"
src={destinationChain.logoURI}
alt={destinationChain.prettyName}
onError={onImageError}
/>
<span className="font-semibold text-black">{destinationChain.prettyName}</span>
</Gap.Child>
{bridge && (
<>
<span>with</span>
<Gap.Child>
{bridge.name.toLowerCase() !== "ibc" && (
<img
className="inline-block h-4 w-4"
src={bridge.logoURI}
alt={bridge.name}
onError={onImageError}
/>
)}

<span className="font-semibold text-black">{bridge.name}</span>
</Gap.Child>
</>
)}
</Gap.Parent>
{explorerLink && (
<AdaptiveLink
Expand Down Expand Up @@ -515,19 +555,19 @@ function RouteDisplay({ route, isRouteExpanded, setIsRouteExpanded, broadcastedT

const amountIn = useMemo(() => {
try {
return ethers.formatUnits(route.amountIn, sourceAsset?.decimals ?? 6);
return formatUnits(route.amountIn, sourceAsset?.decimals ?? 6);
} catch {
return "0.0";
}
}, [route.amountIn, sourceAsset?.decimals]);

const amountOut = useMemo(() => {
try {
return ethers.formatUnits(route.estimatedAmountOut ?? 0, destinationAsset?.decimals ?? 6);
return formatUnits(route.amountOut ?? 0, destinationAsset?.decimals ?? 6);
} catch {
return "0.0";
}
}, [route.estimatedAmountOut, destinationAsset?.decimals]);
}, [route.amountOut, destinationAsset?.decimals]);

const actions = useMemo(() => {
const _actions: Action[] = [];
Expand Down
12 changes: 7 additions & 5 deletions src/components/SwapWidget/useSwapWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { createJSONStorage, persist, subscribeWithSelector } from "zustand/middl
import { shallow } from "zustand/shallow";
import { createWithEqualityFn as create } from "zustand/traditional";

import { EVMOS_GAS_AMOUNT, isChainIdEvmos } from "@/constants/gas";
import { EVMOS_GAS_AMOUNT, getHotfixedGasPrice, isChainIdEvmos } from "@/constants/gas";
import { useAssets } from "@/context/assets";
import { useAnyDisclosureOpen } from "@/context/disclosures";
import { useSettingsStore } from "@/context/settings";
Expand Down Expand Up @@ -141,9 +141,10 @@ export function useSwapWidget() {
const parsedFeeBalance = BigNumber(balances[srcFeeAsset.denom] ?? "0").shiftedBy(-(srcFeeAsset.decimals ?? 6));
const parsedGasRequired = BigNumber(gasRequired || "0");
if (
srcFeeAsset.denom === srcAsset.denom
parsedGasRequired.gt(0) &&
(srcFeeAsset.denom === srcAsset.denom
? parsedAmount.isGreaterThan(parsedBalance.minus(parsedGasRequired))
: parsedFeeBalance.minus(parsedGasRequired).isLessThanOrEqualTo(0)
: parsedFeeBalance.minus(parsedGasRequired).isLessThanOrEqualTo(0))
) {
return `Insufficient balance. You need ≈${gasRequired} ${srcFeeAsset.recommendedSymbol} to accomodate gas fees.`;
}
Expand Down Expand Up @@ -453,12 +454,13 @@ export function useSwapWidget() {
async ([srcChain, srcAsset, srcFeeAsset]) => {
if (!(srcChain?.chainType === "cosmos" && srcAsset)) return;

const suggestedPrice = await skipClient.getRecommendedGasPrice(srcChain.chainID);
let suggestedPrice = await getHotfixedGasPrice(srcChain.chainID);
suggestedPrice ??= await skipClient.getRecommendedGasPrice(srcChain.chainID);

if (!srcFeeAsset || srcFeeAsset.chainID !== srcChain.chainID) {
if (suggestedPrice) {
srcFeeAsset = assetsByChainID(srcChain.chainID).find(({ denom }) => {
return denom === suggestedPrice.denom;
return denom === suggestedPrice!.denom;
});
} else {
srcFeeAsset = await getFeeAsset(srcChain.chainID);
Expand Down
75 changes: 6 additions & 69 deletions src/components/TransactionDialog/TransactionDialogContent.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { useManager } from "@cosmos-kit/react";
import { ArrowLeftIcon, CheckCircleIcon, InformationCircleIcon, XMarkIcon } from "@heroicons/react/20/solid";
import * as Sentry from "@sentry/react";
import { RouteResponse } from "@skip-router/core";
import { useState } from "react";
import { toast } from "react-hot-toast";
import { useAccount as useWagmiAccount } from "wagmi";

import { getHotfixedGasPrice } from "@/constants/gas";
import { useSettingsStore } from "@/context/settings";
import { txHistory } from "@/context/tx-history";
import { useAccount } from "@/hooks/useAccount";
import { useChains } from "@/hooks/useChains";
import { useFinalityTimeEstimate } from "@/hooks/useFinalityTimeEstimate";
import { useWalletAddresses } from "@/hooks/useWalletAddresses";
import { useBroadcastedTxsStatus, useSkipClient } from "@/solve";
import { isUserRejectedRequestError } from "@/utils/error";
import { getExplorerUrl } from "@/utils/explorer";
Expand All @@ -37,10 +36,7 @@ export interface BroadcastedTx {
}

function TransactionDialogContent({ route, onClose, isAmountError, transactionCount }: Props) {
const { data: chains = [] } = useChains();

const skipClient = useSkipClient();
const { address: evmAddress } = useWagmiAccount();

const [isOngoing, setOngoing] = useState(false);

Expand All @@ -53,82 +49,23 @@ function TransactionDialogContent({ route, onClose, isAmountError, transactionCo
txsRequired: route.txsRequired,
});

const { getWalletRepo } = useManager();

const srcAccount = useAccount("source");
const dstAccount = useAccount("destination");

const { data: userAddresses } = useWalletAddresses(route.chainIDs);

async function onSubmit() {
if (!userAddresses) return;
setOngoing(true);
setIsExpanded(true);
const historyId = randomId();
try {
const userAddresses: Record<string, string> = {};

const srcChain = chains.find((c) => {
return c.chainID === route.sourceAssetChainID;
});
const dstChain = chains.find((c) => {
return c.chainID === route.destAssetChainID;
});

for (const chainID of route.chainIDs) {
const chain = chains.find((c) => c.chainID === chainID);
if (!chain) {
throw new Error(`executeRoute error: cannot find chain '${chainID}'`);
}

if (chain.chainType === "cosmos") {
const { wallets } = getWalletRepo(chain.chainName);

const walletName = (() => {
// if `chainID` is the source or destination chain
if (srcChain?.chainID === chainID) {
return srcAccount?.wallet?.walletName;
}
if (dstChain?.chainID === chainID) {
return dstAccount?.wallet?.walletName;
}

// if `chainID` isn't the source or destination chain
if (srcChain?.chainType === "cosmos") {
return srcAccount?.wallet?.walletName;
}
if (dstChain?.chainType === "cosmos") {
return dstAccount?.wallet?.walletName;
}
})();

if (!walletName) {
throw new Error(`executeRoute error: cannot find wallet for '${chain.chainName}'`);
}

const wallet = wallets.find((w) => w.walletName === walletName);
if (!wallet) {
throw new Error(`executeRoute error: cannot find wallet for '${chain.chainName}'`);
}
if (wallet.isWalletDisconnected || !wallet.isWalletConnected) {
await wallet.connect();
}
if (!wallet.address) {
throw new Error(`executeRoute error: cannot resolve wallet address for '${chain.chainName}'`);
}
userAddresses[chainID] = wallet.address;
}

if (chain.chainType === "evm") {
if (!evmAddress) {
throw new Error(`executeRoute error: evm wallet not connected`);
}
userAddresses[chainID] = evmAddress;
}
}

await skipClient.executeRoute({
route,
userAddresses,
validateGasBalance: route.txsRequired === 1,
slippageTolerancePercent: useSettingsStore.getState().slippage,
getGasPrice: getHotfixedGasPrice,
onTransactionBroadcast: async (txStatus) => {
const makeExplorerUrl = await getExplorerUrl(txStatus.chainID);
const explorerLink = makeExplorerUrl?.(txStatus.txHash);
Expand Down
8 changes: 8 additions & 0 deletions src/constants/gas.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { GasPrice } from "@cosmjs/stargate";

export const DEFAULT_GAS_AMOUNT = (200_000).toString();

export const EVMOS_GAS_AMOUNT = (280_000).toString();

export function isChainIdEvmos(chainID: string) {
return chainID === "evmos_9001-2" || chainID.includes("evmos");
}

export async function getHotfixedGasPrice(chainID: string) {
if (chainID === "noble-1") {
return GasPrice.fromString("0.0uusdc");
}
}
87 changes: 87 additions & 0 deletions src/hooks/useWalletAddresses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { useManager } from "@cosmos-kit/react";
import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import { useAccount as useWagmiAccount } from "wagmi";

import { useAccount } from "@/hooks/useAccount";
import { useChains } from "@/hooks/useChains";

export function useWalletAddresses(chainIDs: string[]) {
const { data: chains = [] } = useChains();

const { address: evmAddress } = useWagmiAccount();
const { getWalletRepo } = useManager();

const srcAccount = useAccount("source");
const dstAccount = useAccount("destination");

const queryKey = useMemo(() => ["USE_WALLET_ADDRESSES", chainIDs] as const, [chainIDs]);

return useQuery({
queryKey,
queryFn: async ({ queryKey: [, chainIDs] }) => {
const record: Record<string, string> = {};

const srcChain = chains.find(({ chainID }) => {
return chainID === chainIDs.at(0);
});
const dstChain = chains.find(({ chainID }) => {
return chainID === chainIDs.at(-1);
});

for (const currentChainID of chainIDs) {
const chain = chains.find(({ chainID }) => chainID === currentChainID);
if (!chain) {
throw new Error(`useWalletAddresses error: cannot find chain '${currentChainID}'`);
}

if (chain.chainType === "cosmos") {
const { wallets } = getWalletRepo(chain.chainName);

const currentWalletName = (() => {
// if `chainID` is the source or destination chain
if (srcChain?.chainID === currentChainID) {
return srcAccount?.wallet?.walletName;
}
if (dstChain?.chainID === currentChainID) {
return dstAccount?.wallet?.walletName;
}

// if `chainID` isn't the source or destination chain
if (srcChain?.chainType === "cosmos") {
return srcAccount?.wallet?.walletName;
}
if (dstChain?.chainType === "cosmos") {
return dstAccount?.wallet?.walletName;
}
})();

if (!currentWalletName) {
throw new Error(`useWalletAddresses error: cannot find wallet for '${chain.chainName}'`);
}

const wallet = wallets.find(({ walletName }) => walletName === currentWalletName);
if (!wallet) {
throw new Error(`useWalletAddresses error: cannot find wallet for '${chain.chainName}'`);
}
if (wallet.isWalletDisconnected || !wallet.isWalletConnected) {
await wallet.connect();
}
if (!wallet.address) {
throw new Error(`useWalletAddresses error: cannot resolve wallet address for '${chain.chainName}'`);
}
record[currentChainID] = wallet.address;
}

if (chain.chainType === "evm") {
if (!evmAddress) {
throw new Error(`useWalletAddresses error: evm wallet not connected`);
}
record[currentChainID] = evmAddress;
}
}

return record;
},
});
}
Loading