Skip to content

Commit

Permalink
feat: sync staging → main
Browse files Browse the repository at this point in the history
  • Loading branch information
codingki authored Feb 1, 2024
2 parents 0054008 + a0d3eb4 commit a071dfe
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 93 deletions.
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;
},
});
}

0 comments on commit a071dfe

Please sign in to comment.