diff --git a/apps/ui/package.json b/apps/ui/package.json
index be5a4b78e..1f2a4ea9e 100644
--- a/apps/ui/package.json
+++ b/apps/ui/package.json
@@ -51,15 +51,15 @@
"@solana/spl-token": "^0.3.5",
"@solana/web3.js": "^1.62.0",
"@swim-io/aptos": "workspace:^",
- "@swim-io/core": "^0.39.0",
- "@swim-io/evm": "^0.39.0",
- "@swim-io/evm-contracts": "^0.39.0",
- "@swim-io/pool-math": "^0.39.0",
- "@swim-io/solana": "^0.39.0",
- "@swim-io/solana-contracts": "^0.39.0",
- "@swim-io/token-projects": "workspace:^",
- "@swim-io/utils": "^0.39.0",
- "@swim-io/wormhole": "^0.39.0",
+ "@swim-io/core": "^0.40.0",
+ "@swim-io/evm": "^0.40.0",
+ "@swim-io/evm-contracts": "^0.40.0",
+ "@swim-io/pool-math": "^0.40.0",
+ "@swim-io/solana": "^0.40.0",
+ "@swim-io/solana-contracts": "^0.40.0",
+ "@swim-io/token-projects": "^0.40.0",
+ "@swim-io/utils": "^0.40.0",
+ "@swim-io/wormhole": "^0.40.0",
"bn.js": "^5.2.1",
"classnames": "^2.3.1",
"decimal.js": "^10.3.1",
diff --git a/apps/ui/src/components/AddForm.tsx b/apps/ui/src/components/AddForm.tsx
index dc530bf98..5d1287552 100644
--- a/apps/ui/src/components/AddForm.tsx
+++ b/apps/ui/src/components/AddForm.tsx
@@ -37,9 +37,9 @@ import {
useMultipleUserBalances,
usePool,
usePoolMath,
- useSplTokenAccountsQuery,
useUserBalanceAmount,
useUserNativeBalances,
+ useUserSolanaTokenAccountsQuery,
useWallets,
} from "../hooks";
import {
@@ -206,7 +206,7 @@ export const AddForm = ({
poolTokens,
poolSpec.isLegacyPool ? undefined : poolSpec.ecosystem,
);
- const { data: splTokenAccounts = null } = useSplTokenAccountsQuery();
+ const { data: splTokenAccounts = null } = useUserSolanaTokenAccountsQuery();
const startNewInteraction = useStartNewInteraction(() => {
setFormInputAmounts(poolTokens.map(() => "0"));
});
diff --git a/apps/ui/src/components/RecentInteractions.tsx b/apps/ui/src/components/RecentInteractions.tsx
index 93557a68d..cb5d2c6e3 100644
--- a/apps/ui/src/components/RecentInteractions.tsx
+++ b/apps/ui/src/components/RecentInteractions.tsx
@@ -9,7 +9,7 @@ import { Fragment, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useEnvironment, useInteractionState } from "../core/store";
-import { useSplTokenAccountsQuery, useWallets } from "../hooks";
+import { useUserSolanaTokenAccountsQuery, useWallets } from "../hooks";
import { isEveryAddressConnected } from "../models";
import type { InteractionType } from "../models";
@@ -34,7 +34,8 @@ export const RecentInteractions = ({
loadInteractionStatesFromIdb(env).catch(console.error);
}, [env, loadInteractionStatesFromIdb]);
- const { isSuccess: didLoadSplTokenAccounts } = useSplTokenAccountsQuery();
+ const { isSuccess: didLoadSplTokenAccounts } =
+ useUserSolanaTokenAccountsQuery();
const wallets = useWallets();
// Don’t display current interaction
const { recentInteractionId, interactionStates } = useInteractionState();
diff --git a/apps/ui/src/components/RecentInteractionsV2.tsx b/apps/ui/src/components/RecentInteractionsV2.tsx
index 727ca36c2..5838f910e 100644
--- a/apps/ui/src/components/RecentInteractionsV2.tsx
+++ b/apps/ui/src/components/RecentInteractionsV2.tsx
@@ -12,7 +12,6 @@ import { useWallets } from "../hooks";
import { isEveryAddressConnected } from "../models";
import type { InteractionType } from "../models";
-import { MultiConnectButton } from "./ConnectButton";
import { ConnectedWallets } from "./ConnectedWallets";
import { InteractionStateComponentV2 } from "./molecules/InteractionStateComponentV2";
@@ -55,7 +54,6 @@ export const RecentInteractionsV2 = ({
}
>
diff --git a/apps/ui/src/components/RemoveForm.tsx b/apps/ui/src/components/RemoveForm.tsx
index ae4962d20..0a4ac7775 100644
--- a/apps/ui/src/components/RemoveForm.tsx
+++ b/apps/ui/src/components/RemoveForm.tsx
@@ -40,9 +40,9 @@ import {
usePoolMath,
useRemoveFeesEstimationQuery,
useRemoveFeesEstimationQueryV2,
- useSplTokenAccountsQuery,
useUserLpBalances,
useUserNativeBalances,
+ useUserSolanaTokenAccountsQuery,
useWallets,
} from "../hooks";
import {
@@ -93,7 +93,7 @@ export const RemoveForm = ({
state: poolState,
} = usePool(poolSpec.id);
const poolMath = usePoolMath(poolSpec.id);
- const { data: splTokenAccounts = null } = useSplTokenAccountsQuery();
+ const { data: splTokenAccounts = null } = useUserSolanaTokenAccountsQuery();
const startNewInteractionV1 = useStartNewInteraction(() => {
if (method === RemoveMethod.ExactOutput) {
setFormOutputAmounts(formOutputAmounts.map(() => "0"));
diff --git a/apps/ui/src/components/SwapForm/SwapForm.test.tsx b/apps/ui/src/components/SwapForm/SwapForm.test.tsx
index 586d1d72c..773e865bf 100644
--- a/apps/ui/src/components/SwapForm/SwapForm.test.tsx
+++ b/apps/ui/src/components/SwapForm/SwapForm.test.tsx
@@ -14,12 +14,12 @@ import {
useErc20BalanceQuery,
useGetSwapFormErrors,
useSolanaClient,
- useSolanaLiquidityQueries,
- useSplTokenAccountsQuery,
- useSplUserBalance,
+ useSolanaTokenAccountQueries,
useStartNewInteraction,
useSwapFeesEstimationQuery,
useUserNativeBalances,
+ useUserSolanaTokenAccountsQuery,
+ useUserSolanaTokenBalance,
} from "../../hooks";
import { mockOf, renderWithAppContext } from "../../testUtils";
@@ -37,9 +37,9 @@ jest.mock(
jest.mock("../../hooks/solana", () => ({
...jest.requireActual("../../hooks/solana"),
- useSplTokenAccountsQuery: jest.fn(),
- useSplUserBalance: jest.fn(),
- useSolanaLiquidityQueries: jest.fn(),
+ useUserSolanaTokenAccountsQuery: jest.fn(),
+ useUserSolanaTokenBalance: jest.fn(),
+ useSolanaTokenAccountQueries: jest.fn(),
}));
jest.mock("../../hooks/solana/useSolanaClient", () => ({
@@ -66,13 +66,15 @@ jest.mock("../../hooks", () => ({
const useGetSwapFormErrorsMock = mockOf(useGetSwapFormErrors);
const useSolanaClientMock = mockOf(useSolanaClient);
-const useSplTokenAccountsQueryMock = mockOf(useSplTokenAccountsQuery);
+const useUserSolanaTokenAccountsQueryMock = mockOf(
+ useUserSolanaTokenAccountsQuery,
+);
const useStartNewInteractionMock = mockOf(useStartNewInteraction);
const useSwapFeesEstimationQueryMock = mockOf(useSwapFeesEstimationQuery);
const useErc20BalanceQueryMock = mockOf(useErc20BalanceQuery);
const useUserNativeBalancesMock = mockOf(useUserNativeBalances);
-const useSplUserBalanceMock = mockOf(useSplUserBalance);
-const useSolanaLiquidityQueriesMock = mockOf(useSolanaLiquidityQueries);
+const useUserSolanaTokenBalanceMock = mockOf(useUserSolanaTokenBalance);
+const useSolanaTokenAccountQueriesMock = mockOf(useSolanaTokenAccountQueries);
const findFromTokenButton = () => screen.queryAllByRole("button")[0];
const findToTokenButton = () => screen.queryAllByRole("button")[3];
@@ -86,7 +88,7 @@ describe("SwapForm", () => {
},
} as Partial as unknown as CustomConnection,
});
- useSplTokenAccountsQueryMock.mockReturnValue({
+ useUserSolanaTokenAccountsQueryMock.mockReturnValue({
data: [],
});
@@ -107,8 +109,8 @@ describe("SwapForm", () => {
useSwapFeesEstimationQueryMock.mockReturnValue(null);
useGetSwapFormErrorsMock.mockReturnValue(() => []);
useErc20BalanceQueryMock.mockReturnValue({ data: zero });
- useSplUserBalanceMock.mockReturnValue(zero);
- useSolanaLiquidityQueriesMock.mockReturnValue([
+ useUserSolanaTokenBalanceMock.mockReturnValue(zero);
+ useSolanaTokenAccountQueriesMock.mockReturnValue([
{ data: [] },
] as unknown as readonly UseQueryResult[]);
});
diff --git a/apps/ui/src/components/SwapForm/SwapForm.tsx b/apps/ui/src/components/SwapForm/SwapForm.tsx
index 1c79c92d8..e71d75a38 100644
--- a/apps/ui/src/components/SwapForm/SwapForm.tsx
+++ b/apps/ui/src/components/SwapForm/SwapForm.tsx
@@ -24,12 +24,12 @@ import {
useIsLargeSwap,
usePoolMaths,
usePools,
- useSplTokenAccountsQuery,
useSwapFeesEstimationQuery,
useSwapOutputAmountEstimate,
useSwapTokensContext,
useUserBalanceAmount,
useUserNativeBalances,
+ useUserSolanaTokenAccountsQuery,
} from "../../hooks";
import {
useHasActiveInteraction,
@@ -59,7 +59,7 @@ export const SwapForm = ({ maxSlippageFraction }: Props): ReactElement => {
const { t } = useTranslation();
const config = useEnvironment(selectConfig, shallow);
const { notify } = useNotification();
- const { data: splTokenAccounts = null } = useSplTokenAccountsQuery();
+ const { data: splTokenAccounts = null } = useUserSolanaTokenAccountsQuery();
const startNewInteraction = useStartNewInteraction(() => {
setFormInputAmount("");
});
diff --git a/apps/ui/src/components/TokenSearchModal.tsx b/apps/ui/src/components/TokenSearchModal.tsx
index 0d80ee2cb..06577f061 100644
--- a/apps/ui/src/components/TokenSearchModal.tsx
+++ b/apps/ui/src/components/TokenSearchModal.tsx
@@ -16,7 +16,7 @@ import { useTranslation } from "react-i18next";
import shallow from "zustand/shallow.js";
import type { EcosystemId, TokenConfig } from "../config";
-import { ECOSYSTEM_LIST, isEcosystemEnabled } from "../config";
+import { ECOSYSTEM_LIST, isEcosystemEnabled, isSwimUsd } from "../config";
import { selectConfig } from "../core/selectors";
import { useEnvironment } from "../core/store";
import { useUserBalanceAmount } from "../hooks";
@@ -40,6 +40,7 @@ interface Props {
readonly handleSelectEcosystem: (ecosystemId: EcosystemId) => void;
readonly tokenOptionIds: readonly string[];
readonly selectedEcosystemId: EcosystemId;
+ readonly showSwimUsd?: boolean;
}
interface TokenProps {
@@ -63,6 +64,7 @@ export const TokenSearchModal = ({
handleSelectEcosystem,
selectedEcosystemId,
tokenOptionIds,
+ showSwimUsd = false,
}: Props): ReactElement => {
const { t } = useTranslation();
const { tokens } = useEnvironment(selectConfig, shallow);
@@ -81,7 +83,8 @@ export const TokenSearchModal = ({
const filteredTokens = tokens.filter(
(token) =>
tokenOptionIds.includes(token.id) &&
- token.nativeEcosystemId === selectedEcosystemId,
+ (token.nativeEcosystemId === selectedEcosystemId ||
+ (showSwimUsd && isSwimUsd(token))),
);
const options = filteredTokens.map((token) => {
diff --git a/apps/ui/src/components/TokenSearchModalV2.tsx b/apps/ui/src/components/TokenSearchModalV2.tsx
deleted file mode 100644
index 2097adfac..000000000
--- a/apps/ui/src/components/TokenSearchModalV2.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-import {
- EuiModalBody,
- EuiModalHeader,
- EuiModalHeaderTitle,
- EuiSelectable,
-} from "@elastic/eui";
-import type { EuiSelectableOption } from "@elastic/eui";
-import { TOKEN_PROJECTS_BY_ID } from "@swim-io/token-projects";
-import type { ReactElement } from "react";
-import { useCallback } from "react";
-import { useTranslation } from "react-i18next";
-
-import { ECOSYSTEMS } from "../config";
-import type { TokenOption } from "../models";
-
-import { CustomModal } from "./CustomModal";
-import { TokenConfigIcon } from "./TokenIcon";
-
-type Option = EuiSelectableOption<{ readonly data: Readonly }>;
-
-const renderOption = (option: Option) => {
- return (
-
- );
-};
-
-interface Props {
- readonly handleClose: () => void;
- readonly handleSelectTokenOption: (tokenOption: TokenOption) => void;
- readonly tokenOptions: readonly TokenOption[];
-}
-
-export const TokenSearchModalV2 = ({
- handleClose,
- handleSelectTokenOption,
- tokenOptions,
-}: Props): ReactElement => {
- const { t } = useTranslation();
- const options = tokenOptions.map((option) => {
- const { tokenConfig, ecosystemId } = option;
- const ecosystem = ECOSYSTEMS[ecosystemId];
- const tokenProject = TOKEN_PROJECTS_BY_ID[tokenConfig.projectId];
- return {
- label: `${tokenProject.symbol} on ${ecosystem.displayName}`,
- searchableLabel: `${tokenProject.symbol} ${tokenProject.displayName} ${ecosystem.displayName}`,
- showIcons: false,
- data: option,
- };
- });
-
- const onSelectToken = useCallback(
- (opts: readonly Option[]) => {
- const selected = opts.find(({ checked }) => checked);
- if (selected) {
- handleSelectTokenOption(selected.data);
- handleClose();
- }
- },
- [handleSelectTokenOption, handleClose],
- );
-
- return (
-
-
-
- {t("token_search_modal.title")}
-
-
-
-
-
- {(list, search) => (
- <>
- {search}
- {list}
- >
- )}
-
-
-
- );
-};
diff --git a/apps/ui/src/components/TokenSelectV2.tsx b/apps/ui/src/components/TokenSelectV2.tsx
index 16c624aab..54321e499 100644
--- a/apps/ui/src/components/TokenSelectV2.tsx
+++ b/apps/ui/src/components/TokenSelectV2.tsx
@@ -1,11 +1,12 @@
-import { EuiButton } from "@elastic/eui";
+import { EuiButton, EuiFlexGroup, EuiFlexItem } from "@elastic/eui";
import type { ReactElement } from "react";
-import { useCallback, useState } from "react";
+import { useCallback, useMemo, useState } from "react";
+import type { TokenConfig } from "../config";
import type { TokenOption } from "../models";
import { TokenConfigIcon } from "./TokenIcon";
-import { TokenSearchModalV2 } from "./TokenSearchModalV2";
+import { TokenSearchModal } from "./TokenSearchModal";
interface Props {
readonly onSelectTokenOption: (tokenOption: TokenOption) => void;
@@ -19,23 +20,52 @@ export const TokenSelectV2 = ({
selectedTokenOption,
}: Props): ReactElement => {
const [showModal, setShowModal] = useState(false);
+ const [selectedEcosystemId, setSelectedEcosystemId] = useState(
+ selectedTokenOption.ecosystemId,
+ );
+
+ const tokenOptionIds = useMemo(
+ () => tokenOptions.map(({ tokenConfig }) => tokenConfig.id),
+ [tokenOptions],
+ );
+
+ const handleSelectToken = useCallback(
+ (tokenConfig: TokenConfig) =>
+ onSelectTokenOption({
+ tokenConfig,
+ ecosystemId: selectedEcosystemId,
+ }),
+ [onSelectTokenOption, selectedEcosystemId],
+ );
const openModal = useCallback(() => setShowModal(true), [setShowModal]);
const closeModal = useCallback(() => setShowModal(false), [setShowModal]);
return (
<>
-
-
+
+
+
+
+
+
{showModal && (
-
)}
>
diff --git a/apps/ui/src/components/molecules/InteractionTitle/InteractionTitle.tsx b/apps/ui/src/components/molecules/InteractionTitle/InteractionTitle.tsx
index 555dd596b..dbced9a73 100644
--- a/apps/ui/src/components/molecules/InteractionTitle/InteractionTitle.tsx
+++ b/apps/ui/src/components/molecules/InteractionTitle/InteractionTitle.tsx
@@ -64,7 +64,7 @@ export const InteractionTitle: React.FC = ({ interaction }) => {
);
}
case InteractionType.RemoveUniform: {
- const { minimumOutputAmounts } = interaction.params;
+ const { minimumOutputAmounts, exactBurnAmount } = interaction.params;
const nonZeroOutputAmounts = [...minimumOutputAmounts.values()].filter(
(amount) => !amount.isZero(),
);
@@ -73,6 +73,7 @@ export const InteractionTitle: React.FC = ({ interaction }) => {
,
tokenAmounts: (
),
@@ -82,12 +83,13 @@ export const InteractionTitle: React.FC = ({ interaction }) => {
);
}
case InteractionType.RemoveExactBurn: {
- const { minimumOutputAmount } = interaction.params;
+ const { minimumOutputAmount, exactBurnAmount } = interaction.params;
return (
,
tokenAmounts: (
= ({ interaction }) => {
);
}
case InteractionType.RemoveExactOutput: {
- const { exactOutputAmounts } = interaction.params;
+ const { exactOutputAmounts, maximumBurnAmount } = interaction.params;
const nonZeroOutputAmounts = [...exactOutputAmounts.values()].filter(
(amount) => !amount.isZero(),
);
@@ -109,6 +111,9 @@ export const InteractionTitle: React.FC = ({ interaction }) => {
+ ),
tokenAmounts: (
),
diff --git a/apps/ui/src/components/molecules/TokenAmountInputV2.tsx b/apps/ui/src/components/molecules/TokenAmountInputV2.tsx
index 79a71193d..bb3339861 100644
--- a/apps/ui/src/components/molecules/TokenAmountInputV2.tsx
+++ b/apps/ui/src/components/molecules/TokenAmountInputV2.tsx
@@ -14,7 +14,6 @@ import { getTokenDetailsForEcosystem } from "../../config";
import { i18next } from "../../i18n";
import type { TokenOption } from "../../models";
import { Amount } from "../../models";
-import { ConnectButton } from "../ConnectButton";
import { EuiFieldIntlNumber } from "../EuiFieldIntlNumber";
import { TokenSelectV2 } from "../TokenSelectV2";
@@ -80,7 +79,7 @@ export const TokenAmountInputV2: React.FC = ({
return (
-
+
= ({
)}
-
-
-
-
-
);
};
diff --git a/apps/ui/src/fixtures/tx/useReloadInteractionStateMutationFixture.ts b/apps/ui/src/fixtures/tx/useReloadInteractionStateMutationFixture.ts
index 872f41715..b5ded97f3 100644
--- a/apps/ui/src/fixtures/tx/useReloadInteractionStateMutationFixture.ts
+++ b/apps/ui/src/fixtures/tx/useReloadInteractionStateMutationFixture.ts
@@ -133,7 +133,7 @@ export const SOLANA_TXS_FOR_RELOAD_INTERACTION: readonly SolanaTx[] = [
id: "53PBEMpqPraH1KFGSQfGn8JR62kndfU6iv6XqeJdDtpuEyD9FLkGjtnUZUB6TPv4H8A7kVxk2WiyEJPY7bLCNQGC",
timestamp: 1656406854,
interactionId: "a9747f341d116e592f6eac839b7f222d",
- parsedTx: {
+ original: {
blockTime: 1656406854,
meta: {
err: null,
@@ -483,7 +483,7 @@ export const SOLANA_TXS_FOR_RELOAD_INTERACTION: readonly SolanaTx[] = [
id: "53mCCVJEvoERa1anMkJxm5JD3doRcMBoQVyw8ZgtJ5sMuDZsw1QaW8worMbsbWBqAhwAheURKNKA7xrafSHyDEjA",
timestamp: 1656406848,
interactionId: "a9747f341d116e592f6eac839b7f222d",
- parsedTx: {
+ original: {
blockTime: 1656406848,
meta: {
err: null,
@@ -850,7 +850,7 @@ export const SOLANA_TXS_FOR_RELOAD_INTERACTION: readonly SolanaTx[] = [
id: "4LCZusMofy5oPLZe5cX5VCn4T1n6qgGsxCRhbwTVAcKSvZRvQLdeEWXJef2m5sD9u6XfRgRNRcBHJBwB48tun2eQ",
timestamp: 1656406843,
interactionId: "a9747f341d116e592f6eac839b7f222d",
- parsedTx: {
+ original: {
blockTime: 1656406843,
meta: {
err: null,
@@ -1345,7 +1345,7 @@ export const SOLANA_TXS_FOR_RELOAD_INTERACTION: readonly SolanaTx[] = [
id: "5UfH9wni8vGP8Ch2KQp2JjoPKyWssFjePVpxAduFErWQVFEfF7Av3iCK9wA7CyQTWUkHZtr6ThoWxZXjr73dVQqF",
timestamp: 1656406839,
interactionId: "a9747f341d116e592f6eac839b7f222d",
- parsedTx: {
+ original: {
blockTime: 1656406839,
meta: {
err: null,
@@ -1642,7 +1642,7 @@ export const SOLANA_TXS_FOR_RELOAD_INTERACTION: readonly SolanaTx[] = [
id: "reEurpv1vonjzLPpqoMWvcNV5bbJmhJwfPPM7d7PEEVcb8mN6DzZTqPtYMLcenJ6VLMa3naXe4gPzPkxurjQy4e",
timestamp: 1656406836,
interactionId: "a9747f341d116e592f6eac839b7f222d",
- parsedTx: {
+ original: {
blockTime: 1656406836,
meta: {
err: null,
@@ -1812,7 +1812,7 @@ export const SOLANA_TXS_FOR_RELOAD_INTERACTION: readonly SolanaTx[] = [
id: "61FvZ4bp3Ua2ED6cv32rqZnLnW5hGDYMf6racBeoZXJaxzVUVZzEEqtut29aqeBoGwxk3Dhr7mbXY6ziVpCDiHTT",
timestamp: 1656406832,
interactionId: "a9747f341d116e592f6eac839b7f222d",
- parsedTx: {
+ original: {
blockTime: 1656406832,
meta: {
err: null,
@@ -1982,36 +1982,36 @@ export const EVM_TXS_FOR_RELOAD_INTERACTION = [
id: "0xdacf9f474992e86e079b588573eff53542f1722386280c55aa71057e5771732f",
timestamp: 1656406577,
interactionId: "a9747f341d116e592f6eac839b7f222d",
- response: {
- hash: "0xdacf9f474992e86e079b588573eff53542f1722386280c55aa71057e5771732f",
- type: 0,
- accessList: null,
- blockHash:
- "0xa0cef0931f71340206080d50fd4f00fb1d924f3bd8a4ff436027f05147f2f2f2",
- blockNumber: 7132783,
- transactionIndex: 15,
- confirmations: 60,
- from: "0xb0A05611328d1068c91F58e2c83Ab4048De8CD7f",
- gasPrice: {
- type: "BigNumber",
- hex: "0x59682f07",
- },
- gasLimit: {
- type: "BigNumber",
- hex: "0x0191ca",
- },
- to: "0xF890982f9310df57d00f659cf4fd87e65adEd8d7",
- value: {
- type: "BigNumber",
- hex: "0x00",
- },
- nonce: 41,
- data: "0x0f5287b000000000000000000000000045b167cf5b14007ca0490dcfb7c4b870ec0c0aa6000000000000000000000000000000000000000000000000000000002b5c01900000000000000000000000000000000000000000000000000000000000000001a77f337a7b4a9d31232af9108048171b0b120b5cf09e06469b980e64c97f0dd400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000059080100a9747f341d116e592f6eac839b7f222d",
- creates: null,
- chainId: 0,
- timestamp: 1656406577,
- },
- receipt: {
+ // response: {
+ // hash: "0xdacf9f474992e86e079b588573eff53542f1722386280c55aa71057e5771732f",
+ // type: 0,
+ // accessList: null,
+ // blockHash:
+ // "0xa0cef0931f71340206080d50fd4f00fb1d924f3bd8a4ff436027f05147f2f2f2",
+ // blockNumber: 7132783,
+ // transactionIndex: 15,
+ // confirmations: 60,
+ // from: "0xb0A05611328d1068c91F58e2c83Ab4048De8CD7f",
+ // gasPrice: {
+ // type: "BigNumber",
+ // hex: "0x59682f07",
+ // },
+ // gasLimit: {
+ // type: "BigNumber",
+ // hex: "0x0191ca",
+ // },
+ // to: "0xF890982f9310df57d00f659cf4fd87e65adEd8d7",
+ // value: {
+ // type: "BigNumber",
+ // hex: "0x00",
+ // },
+ // nonce: 41,
+ // data: "0x0f5287b000000000000000000000000045b167cf5b14007ca0490dcfb7c4b870ec0c0aa6000000000000000000000000000000000000000000000000000000002b5c01900000000000000000000000000000000000000000000000000000000000000001a77f337a7b4a9d31232af9108048171b0b120b5cf09e06469b980e64c97f0dd400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000059080100a9747f341d116e592f6eac839b7f222d",
+ // creates: null,
+ // chainId: 0,
+ // timestamp: 1656406577,
+ // },
+ original: {
to: "0xF890982f9310df57d00f659cf4fd87e65adEd8d7",
from: "0xb0A05611328d1068c91F58e2c83Ab4048De8CD7f",
contractAddress: null,
@@ -2095,36 +2095,36 @@ export const EVM_TXS_FOR_RELOAD_INTERACTION = [
id: "0x5ddfb1925096babf7939393b62970700a4db183a5dd9ae36dfd2fc9c5d7da302",
timestamp: 1656406883,
interactionId: "a9747f341d116e592f6eac839b7f222d",
- response: {
- hash: "0x5ddfb1925096babf7939393b62970700a4db183a5dd9ae36dfd2fc9c5d7da302",
- type: 0,
- accessList: null,
- blockHash:
- "0x54d21312a280bff7c065a94a0f79c5d63067353ae3519a77127b661387d6c7f7",
- blockNumber: 11039320,
- transactionIndex: 1,
- confirmations: 232,
- from: "0xb0A05611328d1068c91F58e2c83Ab4048De8CD7f",
- gasPrice: {
- type: "BigNumber",
- hex: "0x062b85e900",
- },
- gasLimit: {
- type: "BigNumber",
- hex: "0x01ea1a",
- },
- to: "0x61E44E506Ca5659E6c0bba9b678586fA2d729756",
- value: {
- type: "BigNumber",
- hex: "0x00",
- },
- nonce: 4,
- data: "0xc687851900000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000100010000000001003c0275cf8e546d379e4af28406521092c8e7e4bc4854d9ee834f0c34cae36ae215eaedadf3f85e46ea9d0e865afc2af74998ab0c5112f8f492a5aeeef910dde20162bac3460000d2e900013b26409f8aaded3f5ddca184695aa6a0fa829b0c85caf84856324896d214ca9800000000000007752001000000000000000000000000000000000000000000000000000000002f32206c00000000000000000000000092934a8b10ddf85e81b65be1d6810544744700dc0006000000000000000000000000b0a05611328d1068c91f58e2c83ab4048de8cd7f00060000000000000000000000000000000000000000000000000000000000000000a9747f341d116e592f6eac839b7f222d",
- creates: null,
- chainId: 0,
- timestamp: 1656406883,
- },
- receipt: {
+ // response: {
+ // hash: "0x5ddfb1925096babf7939393b62970700a4db183a5dd9ae36dfd2fc9c5d7da302",
+ // type: 0,
+ // accessList: null,
+ // blockHash:
+ // "0x54d21312a280bff7c065a94a0f79c5d63067353ae3519a77127b661387d6c7f7",
+ // blockNumber: 11039320,
+ // transactionIndex: 1,
+ // confirmations: 232,
+ // from: "0xb0A05611328d1068c91F58e2c83Ab4048De8CD7f",
+ // gasPrice: {
+ // type: "BigNumber",
+ // hex: "0x062b85e900",
+ // },
+ // gasLimit: {
+ // type: "BigNumber",
+ // hex: "0x01ea1a",
+ // },
+ // to: "0x61E44E506Ca5659E6c0bba9b678586fA2d729756",
+ // value: {
+ // type: "BigNumber",
+ // hex: "0x00",
+ // },
+ // nonce: 4,
+ // data: "0xc687851900000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000100010000000001003c0275cf8e546d379e4af28406521092c8e7e4bc4854d9ee834f0c34cae36ae215eaedadf3f85e46ea9d0e865afc2af74998ab0c5112f8f492a5aeeef910dde20162bac3460000d2e900013b26409f8aaded3f5ddca184695aa6a0fa829b0c85caf84856324896d214ca9800000000000007752001000000000000000000000000000000000000000000000000000000002f32206c00000000000000000000000092934a8b10ddf85e81b65be1d6810544744700dc0006000000000000000000000000b0a05611328d1068c91f58e2c83ab4048de8cd7f00060000000000000000000000000000000000000000000000000000000000000000a9747f341d116e592f6eac839b7f222d",
+ // creates: null,
+ // chainId: 0,
+ // timestamp: 1656406883,
+ // },
+ original: {
to: "0x61E44E506Ca5659E6c0bba9b678586fA2d729756",
from: "0xb0A05611328d1068c91F58e2c83Ab4048De8CD7f",
contractAddress: null,
diff --git a/apps/ui/src/hooks/crossEcosystem/useMultipleUserBalances.ts b/apps/ui/src/hooks/crossEcosystem/useMultipleUserBalances.ts
index 8a0a229a0..8748a494f 100644
--- a/apps/ui/src/hooks/crossEcosystem/useMultipleUserBalances.ts
+++ b/apps/ui/src/hooks/crossEcosystem/useMultipleUserBalances.ts
@@ -11,7 +11,7 @@ import { getTokenDetailsForEcosystem } from "../../config";
import { Amount } from "../../models";
import { useAptosTokenBalancesQuery } from "../aptos";
import { useErc20BalancesQuery } from "../evm";
-import { useSolanaWallet, useSplTokenAccountsQuery } from "../solana";
+import { useSolanaWallet, useUserSolanaTokenAccountsQuery } from "../solana";
const getTokenDetailsByEcosystem = (
tokenConfigs: readonly TokenConfig[],
@@ -134,7 +134,7 @@ export const useMultipleUserBalances = (
acala,
} = getTokenDetailsByEcosystem(tokenConfigs);
const { address: solanaWalletAddress } = useSolanaWallet();
- const { data: splTokenAccounts = [] } = useSplTokenAccountsQuery();
+ const { data: splTokenAccounts = [] } = useUserSolanaTokenAccountsQuery();
const solanaTokenAccounts = solana.map((tokenDetails) =>
solanaWalletAddress !== null
? findTokenAccountForMint(
diff --git a/apps/ui/src/hooks/crossEcosystem/useUserBalances.ts b/apps/ui/src/hooks/crossEcosystem/useUserBalances.ts
index 332ed6ce2..1e65bb0df 100644
--- a/apps/ui/src/hooks/crossEcosystem/useUserBalances.ts
+++ b/apps/ui/src/hooks/crossEcosystem/useUserBalances.ts
@@ -8,7 +8,7 @@ import { getTokenDetailsForEcosystem } from "../../config";
import { Amount } from "../../models";
import { useAptosTokenBalanceQuery } from "../aptos";
import { useErc20BalanceQuery } from "../evm";
-import { useSplUserBalance } from "../solana";
+import { useUserSolanaTokenBalance } from "../solana";
const useUserBalance = (
tokenConfig: TokenConfig | null,
@@ -20,7 +20,7 @@ const useUserBalance = (
(getTokenDetailsForEcosystem(tokenConfig, APTOS_ECOSYSTEM_ID) ?? null),
{ enabled: ecosystemId === APTOS_ECOSYSTEM_ID },
);
- const splBalance = useSplUserBalance(
+ const splBalance = useUserSolanaTokenBalance(
tokenConfig &&
(getTokenDetailsForEcosystem(tokenConfig, SOLANA_ECOSYSTEM_ID) ?? null),
{ enabled: ecosystemId === SOLANA_ECOSYSTEM_ID },
diff --git a/apps/ui/src/hooks/crossEcosystem/useUserNativeBalances.ts b/apps/ui/src/hooks/crossEcosystem/useUserNativeBalances.ts
index c4e9dab8b..a066c0f89 100644
--- a/apps/ui/src/hooks/crossEcosystem/useUserNativeBalances.ts
+++ b/apps/ui/src/hooks/crossEcosystem/useUserNativeBalances.ts
@@ -8,7 +8,7 @@ import type { EcosystemId } from "../../config";
import { ECOSYSTEM_IDS } from "../../config";
import { useAptosGasBalanceQuery } from "../aptos";
import { useEvmUserNativeBalanceQuery } from "../evm";
-import { useSolBalanceQuery } from "../solana";
+import { useSolanaGasBalanceQuery } from "../solana";
export const useUserNativeBalances = (
/** only fetch the ecosystems specified to reduce network calls */
@@ -17,7 +17,7 @@ export const useUserNativeBalances = (
const { data: aptBalance = new Decimal(0) } = useAptosGasBalanceQuery({
enabled: ecosystemIds.includes(APTOS_ECOSYSTEM_ID),
});
- const { data: solBalance = new Decimal(0) } = useSolBalanceQuery({
+ const { data: solBalance = new Decimal(0) } = useSolanaGasBalanceQuery({
enabled: ecosystemIds.includes(SOLANA_ECOSYSTEM_ID),
});
const { data: ethBalance = new Decimal(0) } = useEvmUserNativeBalanceQuery(
diff --git a/apps/ui/src/hooks/interaction/useAddInteractionMutation.ts b/apps/ui/src/hooks/interaction/useAddInteractionMutation.ts
index 1b22e38f4..75dbf29de 100644
--- a/apps/ui/src/hooks/interaction/useAddInteractionMutation.ts
+++ b/apps/ui/src/hooks/interaction/useAddInteractionMutation.ts
@@ -17,7 +17,7 @@ import {
import type { AddInteractionState } from "../../models";
import { useWallets } from "../crossEcosystem";
import { useGetEvmClient } from "../evm";
-import { useSolanaClient, useSplTokenAccountsQuery } from "../solana";
+import { useSolanaClient, useUserSolanaTokenAccountsQuery } from "../solana";
export const useAddInteractionMutation = () => {
const queryClient = useQueryClient();
@@ -26,7 +26,8 @@ export const useAddInteractionMutation = () => {
const wallets = useWallets();
const solanaClient = useSolanaClient();
const getEvmClient = useGetEvmClient();
- const { data: existingSplTokenAccounts = [] } = useSplTokenAccountsQuery();
+ const { data: existingSplTokenAccounts = [] } =
+ useUserSolanaTokenAccountsQuery();
const { updateInteractionState } = useInteractionStateV2();
const tokensByPoolId = getTokensByPool(config);
@@ -123,24 +124,21 @@ export const useAddInteractionMutation = () => {
if (tokenDetails === null) {
throw new Error("Missing token detail");
}
- const responses = await evmClient.approveTokenAmount({
+ const approveTxGenerator = evmClient.generateErc20ApproveTxs({
atomicAmount: amount.toAtomicString(ecosystem),
wallet,
mintAddress: tokenDetails.address,
spenderAddress: poolSpec.address,
});
- await Promise.all(
- responses.map(async (response) => {
- const tx = await evmClient.getTx(response);
- updateInteractionState(interaction.id, (draft) => {
- if (draft.interactionType !== interaction.type) {
- throw new Error("Interaction type mismatch");
- }
- draft.approvalTxIds = [...draft.approvalTxIds, tx.id];
- });
- }),
- );
+ for await (const result of approveTxGenerator) {
+ updateInteractionState(interaction.id, (draft) => {
+ if (draft.interactionType !== interaction.type) {
+ throw new Error("Interaction type mismatch");
+ }
+ draft.approvalTxIds.push(result.tx.id);
+ });
+ }
}),
);
diff --git a/apps/ui/src/hooks/interaction/useCreateInteractionState.test.ts b/apps/ui/src/hooks/interaction/useCreateInteractionState.test.ts
index b60504750..79d730544 100644
--- a/apps/ui/src/hooks/interaction/useCreateInteractionState.test.ts
+++ b/apps/ui/src/hooks/interaction/useCreateInteractionState.test.ts
@@ -17,7 +17,7 @@ import {
import { Amount, InteractionType } from "../../models";
import { mockOf, renderHookWithAppContext } from "../../testUtils";
import { useWallets } from "../crossEcosystem";
-import { useSolanaWallet, useSplTokenAccountsQuery } from "../solana";
+import { useSolanaWallet, useUserSolanaTokenAccountsQuery } from "../solana";
import { usePoolMathByPoolIds } from "../swim";
import { useCreateInteractionState } from "./useCreateInteractionState";
@@ -37,7 +37,7 @@ jest.mock("../crossEcosystem", () => ({
jest.mock("../solana", () => ({
...jest.requireActual("../solana"),
useSolanaWallet: jest.fn(),
- useSplTokenAccountsQuery: jest.fn(),
+ useUserSolanaTokenAccountsQuery: jest.fn(),
}));
jest.mock("../swim", () => ({
@@ -48,7 +48,9 @@ jest.mock("../swim", () => ({
// Make typescript happy with jest
const useSolanaWalletMock = mockOf(useSolanaWallet);
const usePoolMathByPoolIdsMock = mockOf(usePoolMathByPoolIds);
-const useSplTokenAccountsQueryMock = mockOf(useSplTokenAccountsQuery);
+const useUserSolanaTokenAccountsQueryMock = mockOf(
+ useUserSolanaTokenAccountsQuery,
+);
const useWalletsMock = mockOf(useWallets);
describe("useCreateInteractionState", () => {
@@ -64,7 +66,9 @@ describe("useCreateInteractionState", () => {
address: "6sbzC1eH4FTujJXWj51eQe25cYvr4xfXbJ1vAj7j2k5J",
});
usePoolMathByPoolIdsMock.mockReturnValue(MOCK_POOL_MATHS_BY_ID);
- useSplTokenAccountsQueryMock.mockReturnValue({ data: MOCK_TOKEN_ACCOUNTS });
+ useUserSolanaTokenAccountsQueryMock.mockReturnValue({
+ data: MOCK_TOKEN_ACCOUNTS,
+ });
useWalletsMock.mockReturnValue(MOCK_WALLETS);
});
@@ -105,7 +109,7 @@ describe("useCreateInteractionState", () => {
});
it("create state from SOLANA USDC to ETHEREUM USDC", () => {
- useSplTokenAccountsQueryMock.mockReturnValue({ data: [] });
+ useUserSolanaTokenAccountsQueryMock.mockReturnValue({ data: [] });
const { result } = renderHookWithAppContext(() =>
useCreateInteractionState(),
);
@@ -142,7 +146,7 @@ describe("useCreateInteractionState", () => {
});
it("create state from BNB USDT to ETHEREUM USDC", () => {
- useSplTokenAccountsQueryMock.mockReturnValue({ data: [] });
+ useUserSolanaTokenAccountsQueryMock.mockReturnValue({ data: [] });
const { result } = renderHookWithAppContext(() =>
useCreateInteractionState(),
);
diff --git a/apps/ui/src/hooks/interaction/useCreateInteractionState.ts b/apps/ui/src/hooks/interaction/useCreateInteractionState.ts
index 925ebc277..d12520ed0 100644
--- a/apps/ui/src/hooks/interaction/useCreateInteractionState.ts
+++ b/apps/ui/src/hooks/interaction/useCreateInteractionState.ts
@@ -30,7 +30,7 @@ import {
getTokensByPool,
} from "../../models";
import { useWallets } from "../crossEcosystem";
-import { useSolanaWallet, useSplTokenAccountsQuery } from "../solana";
+import { useSolanaWallet, useUserSolanaTokenAccountsQuery } from "../solana";
import { usePoolMathByPoolIds } from "../swim";
export const createRequiredSplTokenAccounts = (
@@ -203,7 +203,7 @@ export const useCreateInteractionState = () => {
const wallets = useWallets();
const { env } = useEnvironment();
const tokensByPoolId = getTokensByPool(config);
- const { data: tokenAccounts = [] } = useSplTokenAccountsQuery();
+ const { data: tokenAccounts = [] } = useUserSolanaTokenAccountsQuery();
const { address: walletAddress } = useSolanaWallet();
const poolMathsByPoolId = usePoolMathByPoolIds();
diff --git a/apps/ui/src/hooks/interaction/useCreateInteractionStateV2.test.ts b/apps/ui/src/hooks/interaction/useCreateInteractionStateV2.test.ts
index dc0827312..c78400acf 100644
--- a/apps/ui/src/hooks/interaction/useCreateInteractionStateV2.test.ts
+++ b/apps/ui/src/hooks/interaction/useCreateInteractionStateV2.test.ts
@@ -10,7 +10,7 @@ import { MOCK_TOKEN_ACCOUNTS, MOCK_WALLETS } from "../../fixtures";
import { Amount, InteractionType, generateId } from "../../models";
import { mockOf, renderHookWithAppContext } from "../../testUtils";
import { useWallets } from "../crossEcosystem";
-import { useSplTokenAccountsQuery } from "../solana";
+import { useUserSolanaTokenAccountsQuery } from "../solana";
import { useCreateInteractionStateV2 } from "./useCreateInteractionStateV2";
@@ -35,12 +35,14 @@ jest.mock("../crossEcosystem", () => ({
jest.mock("../solana", () => ({
...jest.requireActual("../solana"),
- useSplTokenAccountsQuery: jest.fn(),
+ useUserSolanaTokenAccountsQuery: jest.fn(),
}));
// Make typescript happy with jest
const generateIdMock = mockOf(generateId);
-const useSplTokenAccountsQueryMock = mockOf(useSplTokenAccountsQuery);
+const useUserSolanaTokenAccountsQueryMock = mockOf(
+ useUserSolanaTokenAccountsQuery,
+);
const useWalletsMock = mockOf(useWallets);
describe("useCreateInteractionStateV2", () => {
@@ -51,7 +53,9 @@ describe("useCreateInteractionStateV2", () => {
envStore.current.setEnv(Env.Testnet);
});
generateIdMock.mockReturnValue("11111111111111111111111111111111");
- useSplTokenAccountsQueryMock.mockReturnValue({ data: MOCK_TOKEN_ACCOUNTS });
+ useUserSolanaTokenAccountsQueryMock.mockReturnValue({
+ data: MOCK_TOKEN_ACCOUNTS,
+ });
useWalletsMock.mockReturnValue(MOCK_WALLETS);
jest.spyOn(Date, "now").mockImplementation(() => 1657544558283);
});
diff --git a/apps/ui/src/hooks/interaction/useCreateInteractionStateV2.ts b/apps/ui/src/hooks/interaction/useCreateInteractionStateV2.ts
index 6243cf789..09ad899a4 100644
--- a/apps/ui/src/hooks/interaction/useCreateInteractionStateV2.ts
+++ b/apps/ui/src/hooks/interaction/useCreateInteractionStateV2.ts
@@ -29,7 +29,7 @@ import {
getTokensByPool,
} from "../../models";
import { useWallets } from "../crossEcosystem";
-import { useSplTokenAccountsQuery } from "../solana";
+import { useUserSolanaTokenAccountsQuery } from "../solana";
const calculateRequiredSplTokenAccounts = (
interaction: SwapInteractionV2,
@@ -224,7 +224,7 @@ export const useCreateInteractionStateV2 = () => {
const config = useEnvironment(selectConfig, shallow);
const wallets = useWallets();
const { env } = useEnvironment();
- const { data: tokenAccounts = [] } = useSplTokenAccountsQuery();
+ const { data: tokenAccounts = [] } = useUserSolanaTokenAccountsQuery();
const solanaWalletAddress = wallets[SOLANA_ECOSYSTEM_ID].address;
const tokensByPoolId = getTokensByPool(config);
diff --git a/apps/ui/src/hooks/interaction/useCrossChainEvmToEvmSwapInteractionMutation.ts b/apps/ui/src/hooks/interaction/useCrossChainEvmToEvmSwapInteractionMutation.ts
index 85e4d9dd5..981822b5e 100644
--- a/apps/ui/src/hooks/interaction/useCrossChainEvmToEvmSwapInteractionMutation.ts
+++ b/apps/ui/src/hooks/interaction/useCrossChainEvmToEvmSwapInteractionMutation.ts
@@ -95,22 +95,23 @@ export const useCrossChainEvmToEvmSwapInteractionMutation = () => {
fromTokenData.tokenConfig,
fromTokenData.ecosystemId,
);
- const approvalResponses = await fromEvmClient.approveTokenAmount({
+ const approvalTxGenerator = fromEvmClient.generateErc20ApproveTxs({
atomicAmount,
mintAddress: fromTokenDetails.address,
wallet,
spenderAddress: fromChainConfig.routingContractAddress,
});
- const approvalTxs = await fromEvmClient.getTxs(approvalResponses);
- updateInteractionState(interaction.id, (draft) => {
- if (
- draft.interactionType !== InteractionType.SwapV2 ||
- draft.swapType !== SwapType.CrossChainEvmToEvm
- ) {
- throw new Error("Interaction type mismatch");
- }
- draft.approvalTxIds = approvalTxs.map((tx) => tx.id);
- });
+ for await (const result of approvalTxGenerator) {
+ updateInteractionState(interaction.id, (draft) => {
+ if (
+ draft.interactionType !== InteractionType.SwapV2 ||
+ draft.swapType !== SwapType.CrossChainEvmToEvm
+ ) {
+ throw new Error("Interaction type mismatch");
+ }
+ draft.approvalTxIds.push(result.tx.id);
+ });
+ }
const crossChainInitiateRequest = await fromRouting.populateTransaction[
"crossChainInitiate(address,uint256,uint256,uint16,bytes32,bytes16)"
](
@@ -144,7 +145,7 @@ export const useCrossChainEvmToEvmSwapInteractionMutation = () => {
crossChainInitiateTxId,
);
const wormholeSequence = parseSequenceFromLogEth(
- crossChainInitiateTx.receipt,
+ crossChainInitiateTx.original,
fromChainConfig.wormhole.bridge,
);
const { wormholeChainId: emitterChainId } = ECOSYSTEMS[fromEcosystem];
diff --git a/apps/ui/src/hooks/interaction/useFromSolanaTransferMutation.ts b/apps/ui/src/hooks/interaction/useFromSolanaTransferMutation.ts
index 60c24960c..89f8d5370 100644
--- a/apps/ui/src/hooks/interaction/useFromSolanaTransferMutation.ts
+++ b/apps/ui/src/hooks/interaction/useFromSolanaTransferMutation.ts
@@ -1,6 +1,6 @@
import { getEmitterAddressSolana } from "@certusone/wormhole-sdk";
import { Keypair } from "@solana/web3.js";
-import type { SolanaClient, TokenAccount } from "@swim-io/solana";
+import type { SolanaClient, SolanaTx, TokenAccount } from "@swim-io/solana";
import {
SOLANA_ECOSYSTEM_ID,
findTokenAccountForMint,
@@ -32,7 +32,7 @@ import {
} from "../../models";
import { useWallets } from "../crossEcosystem";
import { useGetEvmClient } from "../evm";
-import { useSolanaClient, useSplTokenAccountsQuery } from "../solana";
+import { useSolanaClient, useUserSolanaTokenAccountsQuery } from "../solana";
const getTransferredAmountsByTokenId = async (
interactionState: InteractionState,
@@ -60,7 +60,7 @@ const getTransferredAmountsByTokenId = async (
};
export const useFromSolanaTransferMutation = () => {
- const { data: splTokenAccounts = [] } = useSplTokenAccountsQuery();
+ const { data: splTokenAccounts = [] } = useUserSolanaTokenAccountsQuery();
const config = useEnvironment(selectConfig);
const { chains, wormhole } = config;
const getEvmClient = useGetEvmClient();
@@ -104,7 +104,7 @@ export const useFromSolanaTransferMutation = () => {
config,
);
- let transferSplTokenTxIds: readonly string[] = [];
+ let transferSplTokenTxs: readonly SolanaTx[] = [];
for (const [index, transfer] of fromSolanaTransfers.entries()) {
const toEcosystem = getToEcosystemOfFromSolanaTransfer(
transfer,
@@ -113,10 +113,8 @@ export const useFromSolanaTransferMutation = () => {
const { token, txIds } = transfer;
// Transfer already completed, skip
if (txIds.transferSplToken !== null) {
- transferSplTokenTxIds = [
- ...transferSplTokenTxIds,
- txIds.transferSplToken,
- ];
+ const tx = await solanaClient.getTx(txIds.transferSplToken);
+ transferSplTokenTxs = [...transferSplTokenTxs, tx];
continue;
}
@@ -152,32 +150,30 @@ export const useFromSolanaTransferMutation = () => {
throw new Error("Missing SPL token account");
}
- let transferSplTokenTxId = transfer.txIds.transferSplToken;
- if (transferSplTokenTxId === null) {
+ if (transfer.txIds.transferSplToken === null) {
// No existing tx
const auxiliarySigner = Keypair.generate();
- transferSplTokenTxId = await solanaClient.initiateWormholeTransfer({
- atomicAmount: amount.toAtomicString(SOLANA_ECOSYSTEM_ID),
- interactionId,
- targetAddress: formatWormholeAddress(
- evmEcosystem.protocol,
- evmWalletAddress,
- ),
- targetChainId: evmEcosystem.wormholeChainId,
- tokenProjectId: token.projectId,
- wallet: solanaWallet,
- auxiliarySigner,
- wrappedTokenInfo: getWrappedTokenInfo(token, SOLANA_ECOSYSTEM_ID),
- });
- // Update transfer state with txId
- updateInteractionState(interactionId, (draft) => {
- draft.fromSolanaTransfers[index].txIds.transferSplToken =
- transferSplTokenTxId;
- });
- transferSplTokenTxIds = [
- ...transferSplTokenTxIds,
- transferSplTokenTxId,
- ];
+ const initiateTransferTxGenerator =
+ solanaClient.generateInitiatePortalTransferTxs({
+ atomicAmount: amount.toAtomicString(SOLANA_ECOSYSTEM_ID),
+ interactionId,
+ targetAddress: formatWormholeAddress(
+ evmEcosystem.protocol,
+ evmWalletAddress,
+ ),
+ targetChainId: evmEcosystem.wormholeChainId,
+ tokenProjectId: token.projectId,
+ wallet: solanaWallet,
+ auxiliarySigner,
+ wrappedTokenInfo: getWrappedTokenInfo(token, SOLANA_ECOSYSTEM_ID),
+ });
+ for await (const result of initiateTransferTxGenerator) {
+ transferSplTokenTxs = [...transferSplTokenTxs, result.tx];
+ updateInteractionState(interactionId, (draft) => {
+ draft.fromSolanaTransfers[index].txIds.transferSplToken =
+ result.tx.id;
+ });
+ }
}
}
@@ -190,7 +186,7 @@ export const useFromSolanaTransferMutation = () => {
transfer,
interaction,
);
- const transferSplTokenTxId = transferSplTokenTxIds[index];
+ const transferTx = transferSplTokenTxs[index];
const evmWallet = wallets[toEcosystem].wallet;
if (!evmWallet) {
throw new Error("No EVM wallet");
@@ -199,8 +195,7 @@ export const useFromSolanaTransferMutation = () => {
chains[Protocol.Evm],
({ ecosystem }) => ecosystem === toEcosystem,
);
- const transferTx = await solanaClient.getTx(transferSplTokenTxId);
- const sequence = parseSequenceFromLogSolana(transferTx.parsedTx);
+ const sequence = parseSequenceFromLogSolana(transferTx.original);
const emitterAddress = await getEmitterAddressSolana(
solanaWormhole.portal,
);
@@ -217,21 +212,17 @@ export const useFromSolanaTransferMutation = () => {
);
await evmWallet.switchNetwork(evmChain.chainId);
const evmClient = getEvmClient(toEcosystem);
- const redeemResponse = await evmClient.completeWormholeTransfer({
- interactionId,
- vaa: vaaBytesResponse.vaaBytes,
- wallet: evmWallet,
- });
- if (redeemResponse === null) {
- throw new Error(
- `Transaction not found: (unlock/mint on ${evmChain.ecosystem})`,
- );
+ const completeTransferTxGenerator =
+ evmClient.generateCompletePortalTransferTxs({
+ interactionId,
+ vaa: vaaBytesResponse.vaaBytes,
+ wallet: evmWallet,
+ });
+ for await (const result of completeTransferTxGenerator) {
+ updateInteractionState(interactionId, (draft) => {
+ draft.fromSolanaTransfers[index].txIds.claimTokenOnEvm = result.tx.id;
+ });
}
- // Update transfer state with txId
- updateInteractionState(interactionId, (draft) => {
- draft.fromSolanaTransfers[index].txIds.claimTokenOnEvm =
- redeemResponse.hash;
- });
}
});
};
diff --git a/apps/ui/src/hooks/interaction/usePrepareSplTokenAccountMutation.ts b/apps/ui/src/hooks/interaction/usePrepareSplTokenAccountMutation.ts
index cd4d6a43a..ac1975e28 100644
--- a/apps/ui/src/hooks/interaction/usePrepareSplTokenAccountMutation.ts
+++ b/apps/ui/src/hooks/interaction/usePrepareSplTokenAccountMutation.ts
@@ -4,10 +4,10 @@ import { useMutation, useQueryClient } from "react-query";
import { selectGetInteractionState } from "../../core/selectors";
import { useInteractionState } from "../../core/store";
import {
- getSplTokenAccountsQueryKey,
+ getUserSolanaTokenAccountsQueryKey,
useSolanaClient,
useSolanaWallet,
- useSplTokenAccountsQuery,
+ useUserSolanaTokenAccountsQuery,
} from "../solana";
export const usePrepareSplTokenAccountMutation = () => {
@@ -18,7 +18,7 @@ export const usePrepareSplTokenAccountMutation = () => {
);
const getInteractionState = useInteractionState(selectGetInteractionState);
const queryClient = useQueryClient();
- const { data: splTokenAccounts = [] } = useSplTokenAccountsQuery();
+ const { data: splTokenAccounts = [] } = useUserSolanaTokenAccountsQuery();
return useMutation(async (interactionId: string) => {
if (wallet === null) {
@@ -56,7 +56,7 @@ export const usePrepareSplTokenAccountMutation = () => {
);
if (missingAccountMints.length > 0) {
- const splTokenAccountsQueryKey = getSplTokenAccountsQueryKey(
+ const splTokenAccountsQueryKey = getUserSolanaTokenAccountsQueryKey(
interaction.env,
solanaAddress,
);
diff --git a/apps/ui/src/hooks/interaction/useReloadInteractionStateMutation.test.ts b/apps/ui/src/hooks/interaction/useReloadInteractionStateMutation.test.ts
index 0e086ae14..e35c324af 100644
--- a/apps/ui/src/hooks/interaction/useReloadInteractionStateMutation.test.ts
+++ b/apps/ui/src/hooks/interaction/useReloadInteractionStateMutation.test.ts
@@ -12,12 +12,12 @@ import {
SOLANA_TXS_FOR_RELOAD_INTERACTION,
} from "../../fixtures/tx/useReloadInteractionStateMutationFixture";
import {
- fetchEvmTxForInteractionId,
+ fetchEvmTxsForInteractionId,
fetchSolanaTxsForInteractionId,
} from "../../models";
import { mockOf, renderHookWithAppContext } from "../../testUtils";
import { useEvmWallet } from "../evm";
-import { useSolanaWallet, useSplTokenAccountsQuery } from "../solana";
+import { useSolanaWallet, useUserSolanaTokenAccountsQuery } from "../solana";
import { useReloadInteractionStateMutation } from "./useReloadInteractionStateMutation";
@@ -33,18 +33,20 @@ const useEvmWalletMock = mockOf(useEvmWallet);
jest.mock("../solana", () => ({
...jest.requireActual("../solana"),
useSolanaWallet: jest.fn(),
- useSplTokenAccountsQuery: jest.fn(),
+ useUserSolanaTokenAccountsQuery: jest.fn(),
}));
const useSolanaWalletMock = mockOf(useSolanaWallet);
-const useSplTokenAccountsQueryMock = mockOf(useSplTokenAccountsQuery);
+const useUserSolanaTokenAccountsQueryMock = mockOf(
+ useUserSolanaTokenAccountsQuery,
+);
jest.mock("../../models", () => ({
...jest.requireActual("../../models"),
- fetchEvmTxForInteractionId: jest.fn(),
+ fetchEvmTxsForInteractionId: jest.fn(),
fetchSolanaTxsForInteractionId: jest.fn(),
EvmConnection: jest.fn(),
}));
-const fetchEvmTxForInteractionIdMock = mockOf(fetchEvmTxForInteractionId);
+const fetchEvmTxsForInteractionIdMock = mockOf(fetchEvmTxsForInteractionId);
const fetchSolanaTxsForInteractionIdMock = mockOf(
fetchSolanaTxsForInteractionId,
);
@@ -68,7 +70,7 @@ describe("useReloadInteractionStateMutation", () => {
});
it("should reload recent tx and recover interaction state", async () => {
- useSplTokenAccountsQueryMock.mockReturnValue({
+ useUserSolanaTokenAccountsQueryMock.mockReturnValue({
data: [
{
mint: new PublicKey("7Lf95y8NuCU5RRC95oUtbBtckPAtbr9ubTgrCiyZ1kEf"),
@@ -90,7 +92,7 @@ describe("useReloadInteractionStateMutation", () => {
useEvmWalletMock.mockReturnValue({
address: "0xb0a05611328d1068c91f58e2c83ab4048de8cd7f",
});
- fetchEvmTxForInteractionIdMock.mockReturnValue(
+ fetchEvmTxsForInteractionIdMock.mockReturnValue(
Promise.resolve(EVM_TXS_FOR_RELOAD_INTERACTION),
);
fetchSolanaTxsForInteractionIdMock.mockReturnValue(
diff --git a/apps/ui/src/hooks/interaction/useReloadInteractionStateMutation.ts b/apps/ui/src/hooks/interaction/useReloadInteractionStateMutation.ts
index 900ec91cc..235afe098 100644
--- a/apps/ui/src/hooks/interaction/useReloadInteractionStateMutation.ts
+++ b/apps/ui/src/hooks/interaction/useReloadInteractionStateMutation.ts
@@ -7,7 +7,7 @@ import { Protocol, getSolanaTokenDetails } from "../../config";
import { selectConfig, selectGetInteractionState } from "../../core/selectors";
import { useEnvironment, useInteractionState } from "../../core/store";
import {
- fetchEvmTxForInteractionId,
+ fetchEvmTxsForInteractionId,
fetchSolanaTxsForInteractionId,
getFromEcosystemOfToSolanaTransfer,
getRequiredEcosystems,
@@ -25,13 +25,13 @@ import { useEvmWallet, useGetEvmClient } from "../evm";
import {
useSolanaClient,
useSolanaWallet,
- useSplTokenAccountsQuery,
+ useUserSolanaTokenAccountsQuery,
} from "../solana";
export const useReloadInteractionStateMutation = () => {
const queryClient = useQueryClient();
const getEvmClient = useGetEvmClient();
- const { data: splTokenAccounts = [] } = useSplTokenAccountsQuery();
+ const { data: splTokenAccounts = [] } = useUserSolanaTokenAccountsQuery();
const solanaClient = useSolanaClient();
const { address: solanaAddress } = useSolanaWallet();
const { address: evmAddress } = useEvmWallet();
@@ -80,7 +80,7 @@ export const useReloadInteractionStateMutation = () => {
);
// Get other evm tx
- const evmTxs = await fetchEvmTxForInteractionId(
+ const evmTxs = await fetchEvmTxsForInteractionId(
interactionId,
queryClient,
interaction.env,
@@ -170,7 +170,7 @@ export const useReloadInteractionStateMutation = () => {
const match = solanaTxs.find(
(solanaTx) =>
isPoolTx(poolSpec.contract, solanaTx) &&
- solanaTx.parsedTx.transaction.message.accountKeys.some(
+ solanaTx.original.transaction.message.accountKeys.some(
(key) => key.pubkey.toBase58() === poolSpec.address,
),
);
diff --git a/apps/ui/src/hooks/interaction/useRemoveInteractionMutation.ts b/apps/ui/src/hooks/interaction/useRemoveInteractionMutation.ts
index 1cd073fe3..1f4ea6ea2 100644
--- a/apps/ui/src/hooks/interaction/useRemoveInteractionMutation.ts
+++ b/apps/ui/src/hooks/interaction/useRemoveInteractionMutation.ts
@@ -25,7 +25,7 @@ import {
} from "../../models";
import { useWallets } from "../crossEcosystem";
import { useGetEvmClient } from "../evm";
-import { useSolanaClient, useSplTokenAccountsQuery } from "../solana";
+import { useSolanaClient, useUserSolanaTokenAccountsQuery } from "../solana";
const getPopulatedTxForEvmRemoveInteraction = (
interaction:
@@ -90,7 +90,8 @@ export const useRemoveInteractionMutation = () => {
const wallets = useWallets();
const solanaClient = useSolanaClient();
const getEvmClient = useGetEvmClient();
- const { data: existingSplTokenAccounts = [] } = useSplTokenAccountsQuery();
+ const { data: existingSplTokenAccounts = [] } =
+ useUserSolanaTokenAccountsQuery();
const { updateInteractionState } = useInteractionStateV2();
const tokensByPoolId = getTokensByPool(config);
@@ -183,23 +184,20 @@ export const useRemoveInteractionMutation = () => {
if (tokenDetails === null) {
throw new Error("Missing token detail");
}
- const approvalResponses = await evmClient.approveTokenAmount({
+ const approveTxGenerator = evmClient.generateErc20ApproveTxs({
atomicAmount: removeAmount.toAtomicString(ecosystem),
wallet,
mintAddress: tokenDetails.address,
spenderAddress: poolSpec.address,
});
- await Promise.all(
- approvalResponses.map(async (response) => {
- const tx = await evmClient.getTx(response);
- updateInteractionState(interaction.id, (draft) => {
- if (draft.interactionType !== interaction.type) {
- throw new Error("Interaction type mismatch");
- }
- draft.approvalTxIds.push(tx.id);
- });
- }),
- );
+ for await (const result of approveTxGenerator) {
+ updateInteractionState(interaction.id, (draft) => {
+ if (draft.interactionType !== interaction.type) {
+ throw new Error("Interaction type mismatch");
+ }
+ draft.approvalTxIds.push(result.tx.id);
+ });
+ }
const poolContract = Pool__factory.connect(poolSpec.address, signer);
const txRequest = await getPopulatedTxForEvmRemoveInteraction(
diff --git a/apps/ui/src/hooks/interaction/useSingleChainEvmSwapInteractionMutation.ts b/apps/ui/src/hooks/interaction/useSingleChainEvmSwapInteractionMutation.ts
index fc91308f4..1e8582abd 100644
--- a/apps/ui/src/hooks/interaction/useSingleChainEvmSwapInteractionMutation.ts
+++ b/apps/ui/src/hooks/interaction/useSingleChainEvmSwapInteractionMutation.ts
@@ -66,27 +66,24 @@ export const useSingleChainEvmSwapInteractionMutation = () => {
fromTokenSpec,
fromTokenData.ecosystemId,
);
- const approvalResponses = await client.approveTokenAmount({
+ const approveTxGenerator = client.generateErc20ApproveTxs({
atomicAmount: inputAmountAtomicString,
wallet,
mintAddress: tokenDetails.address,
spenderAddress: routingContractAddress,
});
- await Promise.all(
- approvalResponses.map(async (response) => {
- const tx = await client.getTx(response);
- updateInteractionState(interaction.id, (draft) => {
- if (
- draft.interactionType !== InteractionType.SwapV2 ||
- draft.swapType !== SwapType.SingleChainEvm
- ) {
- throw new Error("Interaction type mismatch");
- }
- draft.approvalTxIds.push(tx.id);
- });
- }),
- );
+ for await (const result of approveTxGenerator) {
+ updateInteractionState(interaction.id, (draft) => {
+ if (
+ draft.interactionType !== InteractionType.SwapV2 ||
+ draft.swapType !== SwapType.SingleChainEvm
+ ) {
+ throw new Error("Interaction type mismatch");
+ }
+ draft.approvalTxIds.push(result.tx.id);
+ });
+ }
const routingContract = Routing__factory.connect(
routingContractAddress,
diff --git a/apps/ui/src/hooks/interaction/useSingleChainSolanaSwapInteractionMutation.ts b/apps/ui/src/hooks/interaction/useSingleChainSolanaSwapInteractionMutation.ts
index 3b0b6cf8d..db7d60f72 100644
--- a/apps/ui/src/hooks/interaction/useSingleChainSolanaSwapInteractionMutation.ts
+++ b/apps/ui/src/hooks/interaction/useSingleChainSolanaSwapInteractionMutation.ts
@@ -23,7 +23,7 @@ import {
getTokensByPool,
} from "../../models";
import { useWallets } from "../crossEcosystem";
-import { useSolanaClient, useSplTokenAccountsQuery } from "../solana";
+import { useSolanaClient, useUserSolanaTokenAccountsQuery } from "../solana";
import { useSwimUsd } from "../swim";
const createOperationSpec = (
@@ -101,7 +101,8 @@ export const useSingleChainSolanaSwapInteractionMutation = () => {
const config = useEnvironment(selectConfig, shallow);
const wallets = useWallets();
const solanaClient = useSolanaClient();
- const { data: existingSplTokenAccounts = [] } = useSplTokenAccountsQuery();
+ const { data: existingSplTokenAccounts = [] } =
+ useUserSolanaTokenAccountsQuery();
const { updateInteractionState } = useInteractionStateV2();
const swimUsd = useSwimUsd();
const tokensByPoolId = getTokensByPool(config);
diff --git a/apps/ui/src/hooks/interaction/useSolanaPoolOperationsMutation.ts b/apps/ui/src/hooks/interaction/useSolanaPoolOperationsMutation.ts
index bb96b57be..c848f5d2f 100644
--- a/apps/ui/src/hooks/interaction/useSolanaPoolOperationsMutation.ts
+++ b/apps/ui/src/hooks/interaction/useSolanaPoolOperationsMutation.ts
@@ -13,14 +13,14 @@ import {
import {
useSolanaClient,
useSolanaWallet,
- useSplTokenAccountsQuery,
+ useUserSolanaTokenAccountsQuery,
} from "../solana";
export const useSolanaPoolOperationsMutation = () => {
const { env } = useEnvironment();
const config = useEnvironment(selectConfig, shallow);
const { pools } = config;
- const { data: splTokenAccounts = [] } = useSplTokenAccountsQuery();
+ const { data: splTokenAccounts = [] } = useUserSolanaTokenAccountsQuery();
const solanaClient = useSolanaClient();
const { wallet, address: solanaWalletAddress } = useSolanaWallet();
const tokensByPoolId = getTokensByPool(config);
diff --git a/apps/ui/src/hooks/interaction/useToSolanaTransferMutation.test.ts b/apps/ui/src/hooks/interaction/useToSolanaTransferMutation.test.ts
index 37d090567..107f8890a 100644
--- a/apps/ui/src/hooks/interaction/useToSolanaTransferMutation.test.ts
+++ b/apps/ui/src/hooks/interaction/useToSolanaTransferMutation.test.ts
@@ -1,7 +1,7 @@
import { PublicKey } from "@solana/web3.js";
-import { EvmEcosystemId } from "@swim-io/evm";
-import type { TokenAccount } from "@swim-io/solana";
-import { SOLANA_ECOSYSTEM_ID } from "@swim-io/solana";
+import { EvmEcosystemId, EvmTxType } from "@swim-io/evm";
+import type { SolanaTx, TokenAccount } from "@swim-io/solana";
+import { SOLANA_ECOSYSTEM_ID, SolanaTxType } from "@swim-io/solana";
import { act, renderHook } from "@testing-library/react-hooks";
import { useQueryClient } from "react-query";
@@ -14,7 +14,7 @@ import type { Wallets } from "../../models";
import { mockOf, renderHookWithAppContext } from "../../testUtils";
import { useWallets } from "../crossEcosystem";
import { useGetEvmClient } from "../evm";
-import { useSolanaClient, useSplTokenAccountsQuery } from "../solana";
+import { useSolanaClient, useUserSolanaTokenAccountsQuery } from "../solana";
import { useToSolanaTransferMutation } from "./useToSolanaTransferMutation";
@@ -42,10 +42,12 @@ const getSignedVaaWithRetryMock = mockOf(getSignedVaaWithRetry);
jest.mock("../solana", () => ({
...jest.requireActual("../solana"),
useSolanaClient: jest.fn(),
- useSplTokenAccountsQuery: jest.fn(),
+ useUserSolanaTokenAccountsQuery: jest.fn(),
}));
const useSolanaClientMock = mockOf(useSolanaClient);
-const useSplTokenAccountsQueryMock = mockOf(useSplTokenAccountsQuery);
+const useUserSolanaTokenAccountsQueryMock = mockOf(
+ useUserSolanaTokenAccountsQuery,
+);
describe("useToSolanaTransferMutation", () => {
beforeEach(() => {
@@ -60,7 +62,7 @@ describe("useToSolanaTransferMutation", () => {
});
it("should handle the transfer and patch interactionState with txIds", async () => {
- useSplTokenAccountsQueryMock.mockReturnValue({
+ useUserSolanaTokenAccountsQueryMock.mockReturnValue({
data: [
{
mint: new PublicKey("9idXDPGb5jfwaf5fxjiMacgUcwpy3ZHfdgqSjAV5XLDr"),
@@ -75,19 +77,26 @@ describe("useToSolanaTransferMutation", () => {
mint,
} as unknown as TokenAccount),
),
- generateCompleteWormholeTransferTxIds: jest
- .fn()
- .mockReturnValue([
- Promise.resolve(
- "3o1NH8sMDs5m9DMoVcqD5eZRny2JrrFBohn9TwEKHXhX4Xxg6uQV7JrupVuDJcwaHBuP8fCZhv1HWBYicMixsSPg",
- ),
- Promise.resolve(
- "3ok2VJpHqZ2EqoDGVMyugENdKawTjNbmM4sm4tHpsoF6T8BHx78fk5vZBXH7KRpgX7P43vhnMnN5zb5NSogUfCsj",
- ),
- Promise.resolve(
- "5rYoqeehFL7j5MbMqzE8NruiUeBaRVhwFpCKsdXUnuAr6NNcPiX3XUxq72SA2MtPhtEhEDU2ZPVP9m4rmkHgy2cC",
- ),
- ] as Partial>),
+ generateCompletePortalTransferTxs: jest.fn().mockReturnValue([
+ Promise.resolve({
+ tx: {
+ id: "3o1NH8sMDs5m9DMoVcqD5eZRny2JrrFBohn9TwEKHXhX4Xxg6uQV7JrupVuDJcwaHBuP8fCZhv1HWBYicMixsSPg",
+ },
+ type: SolanaTxType.WormholeVerifySignatures,
+ }),
+ Promise.resolve({
+ tx: {
+ id: "3ok2VJpHqZ2EqoDGVMyugENdKawTjNbmM4sm4tHpsoF6T8BHx78fk5vZBXH7KRpgX7P43vhnMnN5zb5NSogUfCsj",
+ },
+ type: SolanaTxType.WormholePostVaa,
+ }),
+ Promise.resolve({
+ tx: {
+ id: "5rYoqeehFL7j5MbMqzE8NruiUeBaRVhwFpCKsdXUnuAr6NNcPiX3XUxq72SA2MtPhtEhEDU2ZPVP9m4rmkHgy2cC",
+ },
+ type: SolanaTxType.PortalRedeem,
+ }),
+ ] as Partial>),
});
useWalletsMock.mockReturnValue({
[EvmEcosystemId.Bnb]: {
@@ -109,14 +118,14 @@ describe("useToSolanaTransferMutation", () => {
id: hash,
}),
),
- initiateWormholeTransfer: jest.fn(() => {
- return Promise.resolve({
- approvalResponses: [],
- transferResponse: {
- hash: "0xd528c49eedda9d5a5a7f04a00355b7b124a30502b46532503cc83891844715b9",
+ generateInitiatePortalTransferTxs: jest.fn().mockReturnValue([
+ Promise.resolve({
+ tx: {
+ id: "0xd528c49eedda9d5a5a7f04a00355b7b124a30502b46532503cc83891844715b9",
},
- });
- }),
+ type: EvmTxType.PortalTransferTokens,
+ }),
+ ]),
provider: {
getTransaction: jest.fn(() =>
Promise.resolve({
diff --git a/apps/ui/src/hooks/interaction/useToSolanaTransferMutation.ts b/apps/ui/src/hooks/interaction/useToSolanaTransferMutation.ts
index a9231e96c..1662c04e3 100644
--- a/apps/ui/src/hooks/interaction/useToSolanaTransferMutation.ts
+++ b/apps/ui/src/hooks/interaction/useToSolanaTransferMutation.ts
@@ -3,7 +3,13 @@ import {
parseSequenceFromLogEth,
} from "@certusone/wormhole-sdk";
import { Keypair } from "@solana/web3.js";
-import { SOLANA_ECOSYSTEM_ID, findTokenAccountForMint } from "@swim-io/solana";
+import type { EvmTx } from "@swim-io/evm";
+import { EvmTxType } from "@swim-io/evm";
+import {
+ SOLANA_ECOSYSTEM_ID,
+ SolanaTxType,
+ findTokenAccountForMint,
+} from "@swim-io/solana";
import { findOrThrow } from "@swim-io/utils";
import { WormholeChainId } from "@swim-io/wormhole";
import { useMutation } from "react-query";
@@ -26,10 +32,10 @@ import {
} from "../../models";
import { useWallets } from "../crossEcosystem";
import { useGetEvmClient } from "../evm";
-import { useSolanaClient, useSplTokenAccountsQuery } from "../solana";
+import { useSolanaClient, useUserSolanaTokenAccountsQuery } from "../solana";
export const useToSolanaTransferMutation = () => {
- const { data: splTokenAccounts = [] } = useSplTokenAccountsQuery();
+ const { data: splTokenAccounts = [] } = useUserSolanaTokenAccountsQuery();
const { chains, wormhole } = useEnvironment(selectConfig, shallow);
const getEvmClient = useGetEvmClient();
const solanaClient = useSolanaClient();
@@ -62,7 +68,7 @@ export const useToSolanaTransferMutation = () => {
);
const evmClients = fromEcosystems.map(getEvmClient);
- let transferTxIds: readonly string[] = [];
+ let transferTxs: readonly EvmTx[] = [];
for (const [index, transfer] of toSolanaTransfers.entries()) {
const { token, value, txIds } = transfer;
const fromEcosystem = getFromEcosystemOfToSolanaTransfer(
@@ -71,12 +77,12 @@ export const useToSolanaTransferMutation = () => {
);
// Transfer completed, skip
if (txIds.approveAndTransferEvmToken.length > 0) {
- transferTxIds = [
- ...transferTxIds,
+ const transferTx = await evmClients[index].getTx(
txIds.approveAndTransferEvmToken[
txIds.approveAndTransferEvmToken.length - 1
],
- ];
+ );
+ transferTxs = [...transferTxs, transferTx];
continue;
}
const evmWallet = wallets[fromEcosystem].wallet;
@@ -94,9 +100,9 @@ export const useToSolanaTransferMutation = () => {
}
// Process transfer if transfer txId does not exist
- const { approvalResponses, transferResponse } = await evmClients[
+ const evmTxGenerator = evmClients[
index
- ].initiateWormholeTransfer({
+ ].generateInitiatePortalTransferTxs({
atomicAmount: humanDecimalToAtomicString(value, token, fromEcosystem),
interactionId,
targetAddress: formatWormholeAddress(
@@ -109,31 +115,35 @@ export const useToSolanaTransferMutation = () => {
wrappedTokenInfo: getWrappedTokenInfo(token, fromEcosystem),
});
- // Update transfer state with txId
- const approveAndTransferEvmTokenTxIds = [
- ...approvalResponses,
- transferResponse,
- ].map(({ hash }) => hash);
- updateInteractionState(interactionId, (draft) => {
- draft.toSolanaTransfers[index].txIds.approveAndTransferEvmToken =
- approveAndTransferEvmTokenTxIds;
- });
- transferTxIds = [...transferTxIds, transferResponse.hash];
+ for await (const result of evmTxGenerator) {
+ switch (result.type) {
+ case EvmTxType.PortalTransferTokens:
+ case EvmTxType.Erc20Approve:
+ if (result.type === EvmTxType.PortalTransferTokens) {
+ transferTxs = [...transferTxs, result.tx];
+ }
+ updateInteractionState(interactionId, (draft) => {
+ draft.toSolanaTransfers[
+ index
+ ].txIds.approveAndTransferEvmToken.push(result.tx.id);
+ });
+ break;
+ default:
+ throw new Error(`Unexpected transaction type: ${result.tx.id}`);
+ }
+ }
}
- const sequences = await Promise.all(
- toSolanaTransfers.map(async (transfer, index) => {
- // Claim token completed, skip
- if (transfer.txIds.claimTokenOnSolana !== null) {
- return null;
- }
- const transferTx = await evmClients[index].getTx(transferTxIds[index]);
- return parseSequenceFromLogEth(
- transferTx.receipt,
- evmChains[index].wormhole.bridge,
- );
- }),
- );
+ const sequences = toSolanaTransfers.map((transfer, index) => {
+ // Claim token completed, skip
+ if (transfer.txIds.claimTokenOnSolana !== null) {
+ return null;
+ }
+ return parseSequenceFromLogEth(
+ transferTxs[index].original,
+ evmChains[index].wormhole.bridge,
+ );
+ });
for (const [index, transfer] of toSolanaTransfers.entries()) {
const fromEcosystem = getFromEcosystemOfToSolanaTransfer(
@@ -167,25 +177,32 @@ export const useToSolanaTransferMutation = () => {
retries,
);
const unlockSplTokenTxIdsGenerator =
- solanaClient.generateCompleteWormholeTransferTxIds({
+ solanaClient.generateCompletePortalTransferTxs({
interactionId,
vaa,
wallet: solanaWallet,
auxiliarySigner,
});
- let unlockSplTokenTxIds: readonly string[] = [];
- for await (const txId of unlockSplTokenTxIdsGenerator) {
- unlockSplTokenTxIds = [...unlockSplTokenTxIds, txId];
+ for await (const result of unlockSplTokenTxIdsGenerator) {
+ switch (result.type) {
+ case SolanaTxType.WormholeVerifySignatures:
+ case SolanaTxType.WormholePostVaa:
+ updateInteractionState(interactionId, (draft) => {
+ draft.toSolanaTransfers[index].txIds.postVaaOnSolana.push(
+ result.tx.id,
+ );
+ });
+ break;
+ case SolanaTxType.PortalRedeem:
+ updateInteractionState(interactionId, (draft) => {
+ draft.toSolanaTransfers[index].txIds.claimTokenOnSolana =
+ result.tx.id;
+ });
+ break;
+ default:
+ throw new Error(`Unexpected transaction type: ${result.tx.id}`);
+ }
}
- // Update transfer state with txId
- const postVaaOnSolanaTxIds = unlockSplTokenTxIds.slice(0, -1);
- const [claimTokenOnSolanaTxId] = unlockSplTokenTxIds.slice(-1);
- updateInteractionState(interactionId, (draft) => {
- draft.toSolanaTransfers[index].txIds.postVaaOnSolana =
- postVaaOnSolanaTxIds;
- draft.toSolanaTransfers[index].txIds.claimTokenOnSolana =
- claimTokenOnSolanaTxId;
- });
}
});
};
diff --git a/apps/ui/src/hooks/solana/index.ts b/apps/ui/src/hooks/solana/index.ts
index 53241b3b8..5023809c8 100644
--- a/apps/ui/src/hooks/solana/index.ts
+++ b/apps/ui/src/hooks/solana/index.ts
@@ -1,8 +1,8 @@
export * from "./useAnchorProvider";
export * from "./useCreateSplTokenAccountsMutation";
-export * from "./useSolanaLiquidityQuery";
export * from "./useSolanaClient";
+export * from "./useSolanaGasBalanceQuery";
+export * from "./useSolanaTokenAccountQueries";
export * from "./useSolanaWallet";
-export * from "./useSolBalanceQuery";
-export * from "./useSplTokenAccountsQuery";
-export * from "./useSplUserBalance";
+export * from "./useUserSolanaTokenAccountsQuery";
+export * from "./useUserSolanaTokenBalance";
diff --git a/apps/ui/src/hooks/solana/useCreateSplTokenAccountsMutation.ts b/apps/ui/src/hooks/solana/useCreateSplTokenAccountsMutation.ts
index e949a1ab2..0f16a4dfb 100644
--- a/apps/ui/src/hooks/solana/useCreateSplTokenAccountsMutation.ts
+++ b/apps/ui/src/hooks/solana/useCreateSplTokenAccountsMutation.ts
@@ -7,7 +7,10 @@ import { findOrCreateSplTokenAccount } from "../../models";
import { useSolanaClient } from "./useSolanaClient";
import { useSolanaWallet } from "./useSolanaWallet";
-import { useSplTokenAccountsQuery } from "./useSplTokenAccountsQuery";
+import {
+ getUserSolanaTokenAccountsQueryKey,
+ useUserSolanaTokenAccountsQuery,
+} from "./useUserSolanaTokenAccountsQuery";
export const useCreateSplTokenAccountsMutation = (): UseMutationResult<
readonly TokenAccount[],
@@ -18,7 +21,7 @@ export const useCreateSplTokenAccountsMutation = (): UseMutationResult<
const queryClient = useQueryClient();
const solanaClient = useSolanaClient();
const { wallet, address } = useSolanaWallet();
- const { data: splTokenAccounts = null } = useSplTokenAccountsQuery();
+ const { data: splTokenAccounts = null } = useUserSolanaTokenAccountsQuery();
return useMutation(
async (mints: readonly string[]): Promise => {
@@ -42,7 +45,9 @@ export const useCreateSplTokenAccountsMutation = (): UseMutationResult<
}),
),
);
- await queryClient.invalidateQueries([env, "tokenAccounts", address]);
+ await queryClient.invalidateQueries(
+ getUserSolanaTokenAccountsQueryKey(env, address),
+ );
return tokenAccountData.map((data) => data.tokenAccount);
},
);
diff --git a/apps/ui/src/hooks/solana/useSolBalanceQuery.ts b/apps/ui/src/hooks/solana/useSolBalanceQuery.ts
deleted file mode 100644
index 4a274c501..000000000
--- a/apps/ui/src/hooks/solana/useSolBalanceQuery.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
-import Decimal from "decimal.js";
-import { useEffect, useMemo } from "react";
-import type { UseQueryOptions, UseQueryResult } from "react-query";
-import { useQuery, useQueryClient } from "react-query";
-
-import { useEnvironment } from "../../core/store";
-
-import { useSolanaClient } from "./useSolanaClient";
-import { useSolanaWallet } from "./useSolanaWallet";
-
-const lamportsToSol = (balance: Decimal.Value): Decimal => {
- return new Decimal(balance).dividedBy(LAMPORTS_PER_SOL);
-};
-
-// Returns user's Solana balance in SOL.
-export const useSolBalanceQuery = (
- options?: Omit, "staleTime">,
-): UseQueryResult => {
- const { env } = useEnvironment();
- const solanaClient = useSolanaClient();
- const { address: walletAddress } = useSolanaWallet();
-
- const queryClient = useQueryClient();
- const queryKey = useMemo(
- () => [env, "solBalance", walletAddress],
- [env, walletAddress],
- );
-
- useEffect(() => {
- if (!walletAddress || !options?.enabled) {
- return;
- }
-
- // Make sure all network requests are ignored after exit, so the state is not mixed, e.g. state between different wallet addresses
- let isExited = false;
-
- const clientSubscriptionId = solanaClient.connection.onAccountChange(
- new PublicKey(walletAddress),
- (accountInfo) => {
- if (isExited) return;
-
- queryClient.setQueryData(queryKey, lamportsToSol(accountInfo.lamports));
- },
- );
- return () => {
- isExited = true;
-
- solanaClient.connection
- .removeAccountChangeListener(clientSubscriptionId)
- .catch(console.error);
- };
- }, [
- options?.enabled,
- queryClient,
- queryKey,
- // make sure we are depending on `solanaClient.connection` not `solanaClient` because the reference of `solanaClient` won't change when we rotate the connection
- solanaClient.connection,
- walletAddress,
- ]);
-
- return useQuery(
- queryKey,
- async () => {
- if (!walletAddress) {
- return new Decimal(0);
- }
- try {
- const balance = await solanaClient.connection.getBalance(
- new PublicKey(walletAddress),
- );
- return lamportsToSol(balance);
- } catch {
- return new Decimal(0);
- }
- },
- {
- ...options,
- // rely on websocket to update outdated data
- staleTime: Infinity,
- },
- );
-};
diff --git a/apps/ui/src/hooks/solana/useSolBalanceQuery.test.ts b/apps/ui/src/hooks/solana/useSolanaGasBalanceQuery.test.ts
similarity index 85%
rename from apps/ui/src/hooks/solana/useSolBalanceQuery.test.ts
rename to apps/ui/src/hooks/solana/useSolanaGasBalanceQuery.test.ts
index bcda26b1f..6355cc97f 100644
--- a/apps/ui/src/hooks/solana/useSolBalanceQuery.test.ts
+++ b/apps/ui/src/hooks/solana/useSolanaGasBalanceQuery.test.ts
@@ -5,8 +5,8 @@ import { useQueryClient } from "react-query";
import { mockOf, renderHookWithAppContext } from "../../testUtils";
-import { useSolBalanceQuery } from "./useSolBalanceQuery";
import { useSolanaClient } from "./useSolanaClient";
+import { useSolanaGasBalanceQuery } from "./useSolanaGasBalanceQuery";
import { useSolanaWallet } from "./useSolanaWallet";
jest.mock("./useSolanaClient", () => ({
@@ -23,7 +23,7 @@ jest.mock("./useSolanaWallet", () => ({
const useSolanaWalletMock = mockOf(useSolanaWallet);
const useSolanaClientMock = mockOf(useSolanaClient);
-describe("useSolBalanceQuery", () => {
+describe("useSolanaGasBalanceQuery", () => {
beforeEach(() => {
// Reset queryClient cache, otherwise test might return previous value
renderHookWithAppContext(() => useQueryClient().clear());
@@ -35,12 +35,10 @@ describe("useSolBalanceQuery", () => {
connection: {
// eslint-disable-next-line @typescript-eslint/require-await
getBalance: async () => 999,
- onAccountChange: jest.fn(),
- removeAccountChangeListener: jest.fn(async () => {}),
} as Partial as unknown as CustomConnection,
});
const { result, waitFor } = renderHookWithAppContext(() =>
- useSolBalanceQuery(),
+ useSolanaGasBalanceQuery(),
);
await waitFor(() => result.current.isSuccess);
expect(result.current.data).toEqual(new Decimal("0"));
@@ -54,12 +52,10 @@ describe("useSolBalanceQuery", () => {
connection: {
// eslint-disable-next-line @typescript-eslint/require-await
getBalance: async () => 123 * LAMPORTS_PER_SOL,
- onAccountChange: jest.fn(),
- removeAccountChangeListener: jest.fn(async () => {}),
} as Partial as unknown as CustomConnection,
});
const { result, waitFor } = renderHookWithAppContext(() =>
- useSolBalanceQuery(),
+ useSolanaGasBalanceQuery(),
);
await waitFor(() => result.current.isSuccess);
expect(result.current.data).toEqual(new Decimal("123"));
@@ -75,12 +71,10 @@ describe("useSolBalanceQuery", () => {
getBalance: async () => {
throw new Error("Something went wrong");
},
- onAccountChange: jest.fn(),
- removeAccountChangeListener: jest.fn(async () => {}),
} as Partial as unknown as CustomConnection,
});
const { result, waitFor } = renderHookWithAppContext(() =>
- useSolBalanceQuery(),
+ useSolanaGasBalanceQuery(),
);
await waitFor(() => result.current.isSuccess);
expect(result.current.data).toEqual(new Decimal("0"));
diff --git a/apps/ui/src/hooks/solana/useSolanaGasBalanceQuery.ts b/apps/ui/src/hooks/solana/useSolanaGasBalanceQuery.ts
new file mode 100644
index 000000000..270676c2b
--- /dev/null
+++ b/apps/ui/src/hooks/solana/useSolanaGasBalanceQuery.ts
@@ -0,0 +1,37 @@
+import { LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
+import Decimal from "decimal.js";
+import type { UseQueryOptions, UseQueryResult } from "react-query";
+import { useQuery } from "react-query";
+
+import { useEnvironment } from "../../core/store";
+
+import { useSolanaClient } from "./useSolanaClient";
+import { useSolanaWallet } from "./useSolanaWallet";
+
+// Returns user's Solana balance in SOL.
+export const useSolanaGasBalanceQuery = (
+ options?: UseQueryOptions,
+): UseQueryResult => {
+ const { env } = useEnvironment();
+ const solanaClient = useSolanaClient();
+ const { address: walletAddress } = useSolanaWallet();
+
+ return useQuery(
+ [env, "solanaGasBalance", walletAddress],
+ async () => {
+ if (!walletAddress) {
+ return new Decimal(0);
+ }
+ try {
+ const balance = await solanaClient.connection.getBalance(
+ new PublicKey(walletAddress),
+ );
+ // Convert lamports to SOL.
+ return new Decimal(balance).dividedBy(LAMPORTS_PER_SOL);
+ } catch {
+ return new Decimal(0);
+ }
+ },
+ options,
+ );
+};
diff --git a/apps/ui/src/hooks/solana/useSolanaLiquidityQuery.ts b/apps/ui/src/hooks/solana/useSolanaTokenAccountQueries.ts
similarity index 57%
rename from apps/ui/src/hooks/solana/useSolanaLiquidityQuery.ts
rename to apps/ui/src/hooks/solana/useSolanaTokenAccountQueries.ts
index c4d5034f2..5881e62f0 100644
--- a/apps/ui/src/hooks/solana/useSolanaLiquidityQuery.ts
+++ b/apps/ui/src/hooks/solana/useSolanaTokenAccountQueries.ts
@@ -1,30 +1,12 @@
import type { TokenAccount } from "@swim-io/solana";
import type { UseQueryResult } from "react-query";
-import { useQueries, useQuery } from "react-query";
+import { useQueries } from "react-query";
import { useEnvironment } from "../../core/store";
import { useSolanaClient } from "./useSolanaClient";
-export const useSolanaLiquidityQuery = (
- tokenAccountAddresses: readonly string[],
-): UseQueryResult => {
- const { env } = useEnvironment();
- const solanaClient = useSolanaClient();
-
- return useQuery(
- [env, "liquidity", tokenAccountAddresses.join("")],
- async () => {
- if (tokenAccountAddresses.length === 0) {
- return [];
- }
-
- return await solanaClient.getMultipleTokenAccounts(tokenAccountAddresses);
- },
- );
-};
-
-export const useSolanaLiquidityQueries = (
+export const useSolanaTokenAccountQueries = (
tokenAccountAddresses: readonly (readonly string[])[],
): readonly UseQueryResult[] => {
const { env } = useEnvironment();
@@ -32,7 +14,7 @@ export const useSolanaLiquidityQueries = (
return useQueries(
tokenAccountAddresses.map((addresses) => ({
- queryKey: [env, "liquidity", addresses.join("")],
+ queryKey: [env, "solanaTokenAccounts", addresses.join()],
queryFn: async (): Promise => {
if (addresses.length === 0) {
return [];
diff --git a/apps/ui/src/hooks/solana/useSplTokenAccountsQuery.ts b/apps/ui/src/hooks/solana/useUserSolanaTokenAccountsQuery.ts
similarity index 86%
rename from apps/ui/src/hooks/solana/useSplTokenAccountsQuery.ts
rename to apps/ui/src/hooks/solana/useUserSolanaTokenAccountsQuery.ts
index f32f7f4ed..0cc0c8f77 100644
--- a/apps/ui/src/hooks/solana/useSplTokenAccountsQuery.ts
+++ b/apps/ui/src/hooks/solana/useUserSolanaTokenAccountsQuery.ts
@@ -11,12 +11,12 @@ import { useEnvironment } from "../../core/store";
import { useSolanaClient } from "./useSolanaClient";
import { useSolanaWallet } from "./useSolanaWallet";
-export const getSplTokenAccountsQueryKey = (
+export const getUserSolanaTokenAccountsQueryKey = (
env: Env,
address: string | null,
-) => [env, "tokenAccounts", address];
+) => [env, "userSolanaTokenAccounts", address];
-export const useSplTokenAccountsQuery = (
+export const useUserSolanaTokenAccountsQuery = (
owner?: string,
options?: UseQueryOptions,
): UseQueryResult => {
@@ -25,7 +25,7 @@ export const useSplTokenAccountsQuery = (
const { address: userAddress } = useSolanaWallet();
const address = owner ?? userAddress;
- const queryKey = getSplTokenAccountsQueryKey(env, address);
+ const queryKey = getUserSolanaTokenAccountsQueryKey(env, address);
return useQuery(
queryKey,
async () => {
diff --git a/apps/ui/src/hooks/solana/useSplUserBalance.ts b/apps/ui/src/hooks/solana/useUserSolanaTokenBalance.ts
similarity index 83%
rename from apps/ui/src/hooks/solana/useSplUserBalance.ts
rename to apps/ui/src/hooks/solana/useUserSolanaTokenBalance.ts
index 90d61139f..2f721dda9 100644
--- a/apps/ui/src/hooks/solana/useSplUserBalance.ts
+++ b/apps/ui/src/hooks/solana/useUserSolanaTokenBalance.ts
@@ -4,9 +4,9 @@ import { atomicToHuman } from "@swim-io/utils";
import Decimal from "decimal.js";
import { useSolanaWallet } from "./useSolanaWallet";
-import { useSplTokenAccountsQuery } from "./useSplTokenAccountsQuery";
+import { useUserSolanaTokenAccountsQuery } from "./useUserSolanaTokenAccountsQuery";
-export const useSplUserBalance = (
+export const useUserSolanaTokenBalance = (
tokenDetails: TokenDetails | null,
{
enabled = true,
@@ -18,7 +18,7 @@ export const useSplUserBalance = (
} = {},
): Decimal | null => {
const { address: walletAddress } = useSolanaWallet();
- const { data: splTokenAccounts = null } = useSplTokenAccountsQuery(
+ const { data: splTokenAccounts = null } = useUserSolanaTokenAccountsQuery(
undefined,
{ enabled },
);
diff --git a/apps/ui/src/hooks/swim/usePoolBalances.ts b/apps/ui/src/hooks/swim/usePoolBalances.ts
index bcb468d40..c005a50bc 100644
--- a/apps/ui/src/hooks/swim/usePoolBalances.ts
+++ b/apps/ui/src/hooks/swim/usePoolBalances.ts
@@ -8,14 +8,14 @@ import { getSolanaTokenDetails } from "../../config";
import { selectConfig } from "../../core/selectors";
import { useEnvironment } from "../../core/store";
import { isEvmPoolState, isSolanaPool } from "../../models";
-import { useSolanaLiquidityQueries } from "../solana";
+import { useSolanaTokenAccountQueries } from "../solana";
import { usePoolStateQueries } from "./usePoolStateQueries";
export const usePoolBalances = (poolSpecs: readonly PoolSpec[]) => {
const { tokens } = useEnvironment(selectConfig, shallow);
const poolStateQueries = usePoolStateQueries(poolSpecs);
- const liquidityQueries = useSolanaLiquidityQueries(
+ const liquidityQueries = useSolanaTokenAccountQueries(
poolSpecs.map((poolSpec) =>
poolSpec.ecosystem === SOLANA_ECOSYSTEM_ID
? [...poolSpec.tokenAccounts.values()]
diff --git a/apps/ui/src/hooks/swim/usePools.ts b/apps/ui/src/hooks/swim/usePools.ts
index 9d4e97ff9..066416e49 100644
--- a/apps/ui/src/hooks/swim/usePools.ts
+++ b/apps/ui/src/hooks/swim/usePools.ts
@@ -11,9 +11,9 @@ import { useEnvironment } from "../../core/store";
import type { PoolState } from "../../models";
import { getPoolUsdValue, isEvmPoolState } from "../../models";
import {
- useSolanaLiquidityQueries,
+ useSolanaTokenAccountQueries,
useSolanaWallet,
- useSplTokenAccountsQuery,
+ useUserSolanaTokenAccountsQuery,
} from "../solana";
import { usePoolLpMints } from "./usePoolLpMint";
@@ -119,13 +119,13 @@ const constructPool = (
export const usePools = (poolIds: readonly string[]): readonly PoolData[] => {
const { pools, tokens: allTokens } = useEnvironment(selectConfig, shallow);
const { address: walletAddress } = useSolanaWallet();
- const { data: splTokenAccounts = null } = useSplTokenAccountsQuery();
+ const { data: splTokenAccounts = null } = useUserSolanaTokenAccountsQuery();
const poolSpecs = poolIds.map((poolId) =>
findOrThrow(pools, (pool) => pool.id === poolId),
);
const poolStates = usePoolStateQueries(poolSpecs);
const lpMints = usePoolLpMints(poolSpecs);
- const liquidityQueries = useSolanaLiquidityQueries(
+ const liquidityQueries = useSolanaTokenAccountQueries(
poolSpecs.map((poolSpec) =>
poolSpec.ecosystem === SOLANA_ECOSYSTEM_ID
? [...poolSpec.tokenAccounts.values()]
diff --git a/apps/ui/src/models/crossEcosystem/tx.test.ts b/apps/ui/src/models/crossEcosystem/tx.test.ts
index d3bc723ce..a0c04b97b 100644
--- a/apps/ui/src/models/crossEcosystem/tx.test.ts
+++ b/apps/ui/src/models/crossEcosystem/tx.test.ts
@@ -15,7 +15,7 @@ describe("Cross-ecosystem tx", () => {
id: "34PhSGJi3XboZEhZEirTM6FEh1hNiYHSio1va1nNgH7S9LSNJQGSAiizEyVbgbVJzFjtsbyuJ2WijN53FSC83h7h",
timestamp: defaultTimestamp,
interactionId: defaultInteractionId,
- parsedTx: parsedSwimSwapTx,
+ original: parsedSwimSwapTx,
};
const ethereumTx: EvmTx = {
@@ -23,7 +23,7 @@ describe("Cross-ecosystem tx", () => {
id: "0x743087e871039d66b82fcb2cb719f6a541e650e05735c32c1be871ef9ae9a456",
timestamp: defaultTimestamp,
interactionId: defaultInteractionId,
- receipt: mock(),
+ original: mock(),
};
const bnbTx: EvmTx = {
diff --git a/apps/ui/src/models/solana/findOrCreateSplTokenAccount.ts b/apps/ui/src/models/solana/findOrCreateSplTokenAccount.ts
index 2c6e7b7af..5e38580f6 100644
--- a/apps/ui/src/models/solana/findOrCreateSplTokenAccount.ts
+++ b/apps/ui/src/models/solana/findOrCreateSplTokenAccount.ts
@@ -47,7 +47,11 @@ export const findOrCreateSplTokenAccount = async ({
);
await solanaClient.confirmTx(createSplTokenAccountTxId);
await sleep(1000); // TODO: Find a better condition
- await queryClient.invalidateQueries([env, "tokenAccounts", solanaAddress]);
+ await queryClient.invalidateQueries([
+ env,
+ "userSolanaTokenAccounts",
+ solanaAddress,
+ ]);
const tokenAccount = await solanaClient.getTokenAccountWithRetry(
splTokenMintAddress,
solanaAddress,
diff --git a/apps/ui/src/models/solana/getSwimUsdBalanceChange.ts b/apps/ui/src/models/solana/getSwimUsdBalanceChange.ts
index 482e02c7f..468965d7f 100644
--- a/apps/ui/src/models/solana/getSwimUsdBalanceChange.ts
+++ b/apps/ui/src/models/solana/getSwimUsdBalanceChange.ts
@@ -6,8 +6,8 @@ export const getSwimUsdBalanceChange = async (
solanaClient: SolanaClient,
swimUsdSplTokenAccount: TokenAccount,
): Promise => {
- const { parsedTx } = await solanaClient.getTx(swapToSwimUsdTxId);
- const { preTokenBalances, postTokenBalances } = parsedTx.meta ?? {};
+ const { original } = await solanaClient.getTx(swapToSwimUsdTxId);
+ const { preTokenBalances, postTokenBalances } = original.meta ?? {};
if (!preTokenBalances || !postTokenBalances) {
throw new Error(`Invalid transaction: ${swapToSwimUsdTxId}`);
}
diff --git a/apps/ui/src/models/swim/doSingleSolanaPoolOperation.ts b/apps/ui/src/models/swim/doSingleSolanaPoolOperation.ts
index 5cac9885f..d77634e37 100644
--- a/apps/ui/src/models/swim/doSingleSolanaPoolOperation.ts
+++ b/apps/ui/src/models/swim/doSingleSolanaPoolOperation.ts
@@ -91,18 +91,18 @@ const getTransferredAmount = (
mintAddress: string,
walletAddress: string,
splTokenAccounts: readonly TokenAccount[],
- tx: SolanaTx,
+ { original }: SolanaTx,
): Decimal =>
inputOperationSpec.instruction === SwimDefiInstruction.Add
? getAmountMintedToAccountByMint(
splTokenAccounts,
- tx.parsedTx,
+ original,
mintAddress,
walletAddress,
)
: getAmountTransferredToAccountByMint(
splTokenAccounts,
- tx.parsedTx,
+ original,
mintAddress,
walletAddress,
);
diff --git a/apps/ui/src/models/swim/getTransferredAmounts.ts b/apps/ui/src/models/swim/getTransferredAmounts.ts
index 031b85eef..d21ec3fde 100644
--- a/apps/ui/src/models/swim/getTransferredAmounts.ts
+++ b/apps/ui/src/models/swim/getTransferredAmounts.ts
@@ -33,7 +33,7 @@ export const getTransferredAmounts = (
// Solana-native token
amount = getAmountTransferredToAccountByMint(
splTokenAccounts,
- tx.parsedTx,
+ tx.original,
mint,
walletAddress,
);
@@ -43,7 +43,7 @@ export const getTransferredAmounts = (
// Wormhole-wrapped token
amount = getAmountMintedToAccountByMint(
splTokenAccounts,
- tx.parsedTx,
+ tx.original,
mint,
walletAddress,
);
diff --git a/apps/ui/src/models/swim/interactionId.ts b/apps/ui/src/models/swim/interactionId.ts
index 9faa8d8ea..7e59617ee 100644
--- a/apps/ui/src/models/swim/interactionId.ts
+++ b/apps/ui/src/models/swim/interactionId.ts
@@ -27,7 +27,7 @@ const findEvmInteractionId = (
return dataHex.slice(-INTERACTION_ID_LENGTH_HEX);
};
-export const fetchEvmTxForInteractionId = async (
+export const fetchEvmTxsForInteractionId = async (
interactionId: string,
queryClient: QueryClient,
env: Env,
@@ -87,7 +87,7 @@ const addSolanaInteractionId = async (
const tx = await solanaClient.getTx(signatureInfo.signature);
// NOTE: Getting the ID from the log is more brittle but simpler than getting it from the instructions
const memoLog =
- tx.parsedTx.meta?.logMessages?.find((log) => MEMO_LOG_REGEXP.test(log)) ??
+ tx.original.meta?.logMessages?.find((log) => MEMO_LOG_REGEXP.test(log)) ??
null;
const match = memoLog?.match(MEMO_LOG_REGEXP);
const interactionId = match?.groups?.[INTERACTION_ID_MATCH_GROUP] ?? null;
diff --git a/apps/ui/src/models/swim/pool.test.ts b/apps/ui/src/models/swim/pool.test.ts
index 658786d02..221fdef2e 100644
--- a/apps/ui/src/models/swim/pool.test.ts
+++ b/apps/ui/src/models/swim/pool.test.ts
@@ -47,7 +47,7 @@ describe("Pool tests", () => {
id: "string",
timestamp: 123456789,
ecosystemId: ecosystemId,
- receipt: txReceipt,
+ original: txReceipt,
interactionId: "1",
};
expect(isPoolTx(contractAddress, tx)).toBe(false);
@@ -55,34 +55,34 @@ describe("Pool tests", () => {
it("returns false, if not pool Solana tx", () => {
const contractAddress = "SWiMDJYFUGj6cPrQ6QYYYWZtvXQdRChSVAygDZDsCHC";
- const ptx = {
+ const parsedTx = {
...mockDeep(),
transaction: parsedWormholeRedeemEvmUnlockWrappedTx.transaction,
};
- const txs: SolanaTx = {
+ const tx: SolanaTx = {
ecosystemId: SOLANA_ECOSYSTEM_ID,
- parsedTx: ptx,
+ original: parsedTx,
id: "string",
timestamp: 123456789,
interactionId: "1",
};
- expect(isPoolTx(contractAddress, txs)).toBe(false);
+ expect(isPoolTx(contractAddress, tx)).toBe(false);
});
it("returns true, if it's pool solana tx", () => {
const contractAddress = "wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb";
- const ptx = {
+ const parsedTx = {
...mockDeep(),
transaction: parsedWormholeRedeemEvmUnlockWrappedTx.transaction,
};
- const txs: SolanaTx = {
+ const tx: SolanaTx = {
ecosystemId: SOLANA_ECOSYSTEM_ID,
- parsedTx: ptx,
+ original: parsedTx,
id: "string",
timestamp: 123456789,
interactionId: "1",
};
- expect(isPoolTx(contractAddress, txs)).toBe(true);
+ expect(isPoolTx(contractAddress, tx)).toBe(true);
});
});
diff --git a/apps/ui/src/models/swim/pool.ts b/apps/ui/src/models/swim/pool.ts
index 8e570657b..636fd6ca6 100644
--- a/apps/ui/src/models/swim/pool.ts
+++ b/apps/ui/src/models/swim/pool.ts
@@ -75,7 +75,7 @@ export const isPoolTx = (
if (!isSolanaTx(tx)) {
return false;
}
- const { message } = tx.parsedTx.transaction;
+ const { message } = tx.original.transaction;
return message.instructions.some(
(ix) => ix.programId.toBase58() === poolContractAddress,
);
diff --git a/apps/ui/src/models/wormhole/evm.ts b/apps/ui/src/models/wormhole/evm.ts
index 59ad92cb9..0daf4fd58 100644
--- a/apps/ui/src/models/wormhole/evm.ts
+++ b/apps/ui/src/models/wormhole/evm.ts
@@ -14,11 +14,11 @@ export const isLockEvmTx = (
return false;
}
if (
- tx.receipt.to.toLowerCase() !== wormholeChainConfig.portal.toLowerCase()
+ tx.original.to.toLowerCase() !== wormholeChainConfig.portal.toLowerCase()
) {
return false;
}
- return tx.receipt.logs.some(
+ return tx.original.logs.some(
(log) => log.address.toLowerCase() === tokenDetails.address.toLowerCase(),
);
};
@@ -33,11 +33,11 @@ export const isUnlockEvmTx = (
return false;
}
if (
- tx.receipt.to.toLowerCase() !== wormholeChainConfig.portal.toLowerCase()
+ tx.original.to.toLowerCase() !== wormholeChainConfig.portal.toLowerCase()
) {
return false;
}
- return tx.receipt.logs.some(
+ return tx.original.logs.some(
(log) => log.address.toLowerCase() === tokenDetails.address.toLowerCase(),
);
};
diff --git a/apps/ui/src/models/wormhole/solana.test.ts b/apps/ui/src/models/wormhole/solana.test.ts
index 38a4f5c24..19934d10e 100644
--- a/apps/ui/src/models/wormhole/solana.test.ts
+++ b/apps/ui/src/models/wormhole/solana.test.ts
@@ -41,7 +41,7 @@ describe("models - Wormhole utils", () => {
ecosystemId: SOLANA_ECOSYSTEM_ID,
timestamp: parsedSwimSwapTx.blockTime ?? null,
id: parsedSwimSwapTx.transaction.signatures[0],
- parsedTx: parsedSwimSwapTx,
+ original: parsedSwimSwapTx,
};
expect(
@@ -76,7 +76,7 @@ describe("models - Wormhole utils", () => {
ecosystemId: SOLANA_ECOSYSTEM_ID,
timestamp: parsedTx.blockTime!,
id: parsedTx.transaction.signatures[0],
- parsedTx,
+ original: parsedTx,
};
const result = isPostVaaSolanaTx(
@@ -101,7 +101,7 @@ describe("models - Wormhole utils", () => {
ecosystemId: SOLANA_ECOSYSTEM_ID,
timestamp: parsedWormholeRedeemEvmUnlockWrappedTx.blockTime!,
id: parsedWormholeRedeemEvmUnlockWrappedTx.transaction.signatures[0],
- parsedTx: parsedWormholeRedeemEvmUnlockWrappedTx,
+ original: parsedWormholeRedeemEvmUnlockWrappedTx,
};
const result = isPostVaaSolanaTx(
@@ -132,7 +132,7 @@ describe("models - Wormhole utils", () => {
ecosystemId: SOLANA_ECOSYSTEM_ID,
timestamp: parsedTx.blockTime!,
id: parsedTx.transaction.signatures[0],
- parsedTx,
+ original: parsedTx,
};
const result = isRedeemOnSolanaTx(
@@ -160,7 +160,7 @@ describe("models - Wormhole utils", () => {
ecosystemId: SOLANA_ECOSYSTEM_ID,
timestamp: parsedWormholeRedeemEvmUnlockWrappedTx.blockTime!,
id: parsedWormholeRedeemEvmUnlockWrappedTx.transaction.signatures[0],
- parsedTx: parsedWormholeRedeemEvmUnlockWrappedTx,
+ original: parsedWormholeRedeemEvmUnlockWrappedTx,
};
const result = isRedeemOnSolanaTx(
diff --git a/apps/ui/src/models/wormhole/solana.ts b/apps/ui/src/models/wormhole/solana.ts
index bdb2a1457..e90049e1e 100644
--- a/apps/ui/src/models/wormhole/solana.ts
+++ b/apps/ui/src/models/wormhole/solana.ts
@@ -19,10 +19,10 @@ export const isLockSplTx = (
wormholeChainConfig: WormholeChainConfig,
splTokenAccountAddress: string,
token: TokenConfig,
- { parsedTx }: SolanaTx,
+ { original }: SolanaTx,
): boolean => {
if (
- !parsedTx.transaction.message.instructions.some(
+ !original.transaction.message.instructions.some(
(ix) => ix.programId.toBase58() === wormholeChainConfig.portal,
)
) {
@@ -31,11 +31,11 @@ export const isLockSplTx = (
return token.nativeEcosystemId === SOLANA_ECOSYSTEM_ID
? getAmountTransferredFromAccount(
- parsedTx,
+ original,
splTokenAccountAddress,
).greaterThan(0)
: getAmountBurnedByMint(
- parsedTx,
+ original,
getSolanaTokenDetails(token).address,
).greaterThan(0);
};
@@ -53,7 +53,7 @@ export const isPostVaaSolanaTx = (
if (signatureSetAddress === null) {
return false;
}
- return tx.parsedTx.transaction.message.instructions.some(
+ return tx.original.transaction.message.instructions.some(
(ix) =>
isPartiallyDecodedInstruction(ix) &&
ix.programId.toBase58() === wormholeChainConfig.bridge &&
@@ -65,16 +65,16 @@ export const isRedeemOnSolanaTx = (
wormholeChainConfig: WormholeChainConfig,
token: TokenConfig,
splTokenAccount: string,
- { parsedTx }: SolanaTx,
+ { original }: SolanaTx,
): boolean => {
if (
- !parsedTx.transaction.message.instructions.some(
+ !original.transaction.message.instructions.some(
(ix) => ix.programId.toBase58() === wormholeChainConfig.portal,
)
) {
return false;
}
return token.nativeEcosystemId === SOLANA_ECOSYSTEM_ID
- ? getAmountTransferredToAccount(parsedTx, splTokenAccount).greaterThan(0)
- : getAmountMintedToAccount(parsedTx, splTokenAccount).greaterThan(0);
+ ? getAmountTransferredToAccount(original, splTokenAccount).greaterThan(0)
+ : getAmountMintedToAccount(original, splTokenAccount).greaterThan(0);
};
diff --git a/apps/ui/src/pages/SwapPageV2/SwapPageV2.scss b/apps/ui/src/pages/SwapPageV2/SwapPageV2.scss
index 31f8dbf47..a57a0d267 100644
--- a/apps/ui/src/pages/SwapPageV2/SwapPageV2.scss
+++ b/apps/ui/src/pages/SwapPageV2/SwapPageV2.scss
@@ -6,3 +6,12 @@
max-width: 400px;
}
}
+.buttons {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ .euiPopover {
+ margin-left: 5px;
+ }
+}
diff --git a/apps/ui/src/pages/SwapPageV2/SwapPageV2.tsx b/apps/ui/src/pages/SwapPageV2/SwapPageV2.tsx
index dd6c296f4..92e770c6b 100644
--- a/apps/ui/src/pages/SwapPageV2/SwapPageV2.tsx
+++ b/apps/ui/src/pages/SwapPageV2/SwapPageV2.tsx
@@ -16,6 +16,7 @@ import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import shallow from "zustand/shallow.js";
+import { MultiConnectButton } from "../../components/ConnectButton";
import { RecentInteractionsV2 } from "../../components/RecentInteractionsV2";
import { SlippageButton } from "../../components/SlippageButton";
import { SwapFormV2 } from "../../components/SwapFormV2";
@@ -44,7 +45,7 @@ const SwapPageV2 = (): ReactElement => {
);
return (
-
+
@@ -54,7 +55,8 @@ const SwapPageV2 = (): ReactElement => {
{t("nav.swap_v2")}
-
+
+
{
@@ -116,11 +123,23 @@ export class AptosClient extends Client<
throw new Error("Not implemented");
}
- public initiateWormholeTransfer(): Promise {
+ public generateInitiatePortalTransferTxs(): AsyncGenerator<
+ TxGeneratorResult
+ > {
+ throw new Error("Not implemented");
+ }
+
+ public generateCompletePortalTransferTxs(): AsyncGenerator<
+ TxGeneratorResult
+ > {
throw new Error("Not implemented");
}
- public completeWormholeTransfer(): Promise {
+ public generateInitiatePropellerTxs(): AsyncGenerator<
+ TxGeneratorResult,
+ any,
+ AptosTxType
+ > {
throw new Error("Not implemented");
}
}
diff --git a/packages/aptos/src/protocol.ts b/packages/aptos/src/protocol.ts
index cc552c1dc..1ae5aa779 100644
--- a/packages/aptos/src/protocol.ts
+++ b/packages/aptos/src/protocol.ts
@@ -29,9 +29,10 @@ export interface AptosEcosystemConfig extends EcosystemConfig {
readonly chains: Partial>;
}
-export interface AptosTx extends Tx {
- readonly ecosystemId: AptosEcosystemId;
-}
+// TODO: Make an enum
+export type AptosTxType = string;
+
+export type AptosTx = Tx;
-export const isAptosTx = (tx: Tx): tx is AptosTx =>
+export const isAptosTx = (tx: Tx): tx is AptosTx =>
tx.ecosystemId === APTOS_ECOSYSTEM_ID;
diff --git a/packages/core/package.json b/packages/core/package.json
index 99902856f..5dfe4d12f 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
{
"name": "@swim-io/core",
- "version": "0.39.0",
+ "version": "0.40.0",
"description": "Basic ecosystem-neutral types and helpers used by Swim.",
"main": "build",
"types": "types",
diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts
index dd3bfabcb..312808b33 100644
--- a/packages/core/src/client.ts
+++ b/packages/core/src/client.ts
@@ -14,27 +14,50 @@ export interface WrappedTokenInfo {
readonly wrappedAddress: string;
}
-export interface InitiateWormholeTransferParams {
- readonly atomicAmount: string;
+export interface InitiatePortalTransferParams {
+ readonly wallet: Wallet;
readonly interactionId: string;
+ readonly tokenProjectId: TokenProjectId;
/** Standardized Wormhole format, ie 32 bytes */
readonly targetAddress: Uint8Array;
readonly targetChainId: ChainId;
- readonly tokenProjectId: TokenProjectId;
- readonly wallet: Wallet;
+ readonly atomicAmount: string;
readonly wrappedTokenInfo?: WrappedTokenInfo;
}
-export interface CompleteWormholeTransferParams {
+export interface CompletePortalTransferParams {
+ readonly wallet: Wallet;
readonly interactionId: string;
readonly vaa: Uint8Array;
+}
+
+export interface InitiatePropellerParams {
readonly wallet: Wallet;
+ readonly interactionId: string;
+ readonly sourceTokenId: TokenProjectId;
+ readonly targetWormholeChainId: ChainId;
+ readonly targetTokenNumber: number;
+ readonly targetWormholeAddress: Uint8Array;
+ readonly inputAmount: Decimal;
+ readonly maxPropellerFeeAtomic: string;
+ readonly gasKickStart: boolean;
+}
+
+export interface TxGeneratorResult<
+ OriginalTx,
+ T extends Tx,
+ TxType extends string,
+> {
+ readonly tx: T;
+ readonly type: TxType;
}
export abstract class Client<
EcosystemId extends string,
C extends ChainConfig,
- T extends Tx,
+ OriginalTx,
+ TxType extends string,
+ T extends Tx,
Wallet,
> {
public readonly ecosystemId: EcosystemId;
@@ -59,11 +82,15 @@ export abstract class Client<
owner: string,
tokenDetails: readonly TokenDetails[],
): Promise;
- public abstract initiateWormholeTransfer(
- params: InitiateWormholeTransferParams,
- ): Promise;
- public abstract completeWormholeTransfer(
- params: CompleteWormholeTransferParams,
- ): Promise;
+ public abstract generateInitiatePortalTransferTxs(
+ params: InitiatePortalTransferParams,
+ ): AsyncGenerator>;
+ public abstract generateCompletePortalTransferTxs(
+ params: CompletePortalTransferParams,
+ ): AsyncGenerator>;
+
+ public abstract generateInitiatePropellerTxs(
+ params: InitiatePropellerParams,
+ ): AsyncGenerator>;
}
diff --git a/packages/core/src/tx.ts b/packages/core/src/tx.ts
index d94195ca7..350ffa233 100644
--- a/packages/core/src/tx.ts
+++ b/packages/core/src/tx.ts
@@ -1,8 +1,9 @@
/** Ecosystem-neutral transaction interface */
-export interface Tx {
+export interface Tx {
readonly id: string;
- readonly ecosystemId: string;
+ readonly ecosystemId: EcosystemId;
/** The time in seconds since Unix epoch */
readonly timestamp: number | null;
readonly interactionId: string | null;
+ readonly original: OriginalTx;
}
diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json
index 136eb6358..6627a8a6c 100644
--- a/packages/eslint-config/package.json
+++ b/packages/eslint-config/package.json
@@ -1,6 +1,6 @@
{
"name": "@swim-io/eslint-config",
- "version": "0.39.0",
+ "version": "0.40.0",
"description": "Shared default ESLint configuration for Swim TS projects.",
"exports": {
".": "./index.cjs",
diff --git a/packages/evm-contracts/package.json b/packages/evm-contracts/package.json
index b29469338..a84333f4c 100644
--- a/packages/evm-contracts/package.json
+++ b/packages/evm-contracts/package.json
@@ -1,6 +1,6 @@
{
"name": "@swim-io/evm-contracts",
- "version": "0.39.0",
+ "version": "0.40.0",
"description": "TS bindings for Swim EVM pool contracts.",
"main": "build",
"types": "types",
diff --git a/packages/evm/package.json b/packages/evm/package.json
index b96c282d6..f96ea8aaf 100644
--- a/packages/evm/package.json
+++ b/packages/evm/package.json
@@ -1,6 +1,6 @@
{
"name": "@swim-io/evm",
- "version": "0.39.0",
+ "version": "0.40.0",
"description": "Swim code relating to EVM.",
"main": "build",
"types": "types",
diff --git a/packages/evm/src/client.ts b/packages/evm/src/client.ts
index 10566f473..5e80f9b32 100644
--- a/packages/evm/src/client.ts
+++ b/packages/evm/src/client.ts
@@ -6,17 +6,20 @@ import {
} from "@certusone/wormhole-sdk";
import { Client, getTokenDetails } from "@swim-io/core";
import type {
- CompleteWormholeTransferParams,
- InitiateWormholeTransferParams,
+ CompletePortalTransferParams,
+ InitiatePortalTransferParams,
+ InitiatePropellerParams,
TokenDetails,
+ TxGeneratorResult,
} from "@swim-io/core";
-import { ERC20__factory } from "@swim-io/evm-contracts";
+import { ERC20__factory, Routing__factory } from "@swim-io/evm-contracts";
import { isNotNull } from "@swim-io/utils";
import Decimal from "decimal.js";
import type { ethers, providers } from "ethers";
import { utils as ethersUtils } from "ethers";
import type { EvmChainConfig, EvmEcosystemId, EvmTx } from "./protocol";
+import { EvmTxType } from "./protocol";
import { appendHexDataToEvmTx } from "./utils";
import type { EvmWalletAdapter } from "./walletAdapters";
@@ -62,6 +65,8 @@ export interface EvmClientOptions {
export class EvmClient extends Client<
EvmEcosystemId,
EvmChainConfig,
+ TransactionReceipt,
+ EvmTxType,
EvmTx,
EvmWalletAdapter
> {
@@ -93,18 +98,11 @@ export class EvmClient extends Client<
txIdOrResponse: TransactionResponse | string,
): Promise {
const response = typeof txIdOrResponse === "string" ? null : txIdOrResponse;
- const id =
- typeof txIdOrResponse === "string" ? txIdOrResponse : txIdOrResponse.hash;
- const receipt = await this.getTxReceipt(txIdOrResponse);
-
- if (receipt === null) {
- throw new Error(`Transaction not found: ${id}`);
- }
-
+ const receipt = await this.getTxReceiptOrThrow(txIdOrResponse);
return {
id: receipt.transactionHash,
ecosystemId: this.ecosystemId,
- receipt,
+ original: receipt,
timestamp: response?.timestamp ?? null,
interactionId: null,
};
@@ -154,7 +152,7 @@ export class EvmClient extends Client<
);
}
- public async initiateWormholeTransfer({
+ public async *generateInitiatePortalTransferTxs({
atomicAmount,
interactionId,
targetAddress,
@@ -162,24 +160,31 @@ export class EvmClient extends Client<
tokenProjectId,
wallet,
wrappedTokenInfo,
- }: InitiateWormholeTransferParams): Promise<{
- readonly approvalResponses: readonly ethers.providers.TransactionResponse[];
- readonly transferResponse: ethers.providers.TransactionResponse;
- }> {
+ }: InitiatePortalTransferParams): AsyncGenerator<
+ TxGeneratorResult<
+ TransactionReceipt,
+ EvmTx,
+ EvmTxType.Erc20Approve | EvmTxType.PortalTransferTokens
+ >
+ > {
const mintAddress =
wrappedTokenInfo?.wrappedAddress ??
getTokenDetails(this.chainConfig, tokenProjectId).address;
await wallet.switchNetwork(this.chainConfig.chainId);
- const approvalResponses = await this.approveTokenAmount({
+ const approvalGenerator = this.generateErc20ApproveTxs({
atomicAmount,
mintAddress,
spenderAddress: this.chainConfig.wormhole.portal,
wallet,
});
- const transferResponse = await this.transferToken({
+ for await (const approvalTxResult of approvalGenerator) {
+ yield approvalTxResult;
+ }
+
+ const tx = await this.transferToken({
interactionId,
mintAddress,
atomicAmount,
@@ -187,27 +192,26 @@ export class EvmClient extends Client<
targetAddress,
wallet,
});
-
- if (transferResponse === null) {
- throw new Error(
- `Transaction not found (lock/burn from ${this.chainConfig.name})`,
- );
- }
-
- return {
- approvalResponses,
- transferResponse,
+ yield {
+ tx,
+ type: EvmTxType.PortalTransferTokens,
};
}
/**
* Adapted from https://github.com/certusone/wormhole/blob/2998031b164051a466bb98c71d89301ed482b4c5/sdk/js/src/token_bridge/redeem.ts#L24-L33
*/
- public async completeWormholeTransfer({
+ public async *generateCompletePortalTransferTxs({
interactionId,
vaa,
wallet,
- }: CompleteWormholeTransferParams): Promise {
+ }: CompletePortalTransferParams): AsyncGenerator<
+ TxGeneratorResult<
+ TransactionReceipt,
+ EvmTx,
+ EvmTxType.PortalCompleteTransfer
+ >
+ > {
const { signer } = wallet;
if (!signer) {
throw new Error("No EVM signer");
@@ -218,16 +222,89 @@ export class EvmClient extends Client<
);
const populatedTx = await bridge.populateTransaction.completeTransfer(vaa);
const txRequest = appendHexDataToEvmTx(interactionId, populatedTx);
- return signer.sendTransaction(txRequest);
+ const completeResponse = await signer.sendTransaction(txRequest);
+ const tx = await this.getTx(completeResponse);
+ yield {
+ tx,
+ type: EvmTxType.PortalCompleteTransfer,
+ };
+ }
+
+ public async *generateInitiatePropellerTxs({
+ wallet,
+ interactionId,
+ sourceTokenId,
+ targetWormholeChainId,
+ targetTokenNumber,
+ targetWormholeAddress,
+ inputAmount,
+ maxPropellerFeeAtomic,
+ gasKickStart,
+ }: InitiatePropellerParams): AsyncGenerator<
+ TxGeneratorResult<
+ ethers.providers.TransactionReceipt,
+ EvmTx,
+ EvmTxType.Erc20Approve | EvmTxType.SwimInitiatePropeller
+ >,
+ any,
+ unknown
+ > {
+ const { signer } = wallet;
+ if (signer === null) {
+ throw new Error("Missing EVM wallet");
+ }
+
+ await wallet.switchNetwork(this.chainConfig.chainId);
+
+ const sourceTokenDetails = getTokenDetails(this.chainConfig, sourceTokenId);
+ const inputAmountAtomic = ethersUtils.parseUnits(
+ inputAmount.toString(),
+ sourceTokenDetails.decimals,
+ );
+
+ const approvalGenerator = this.generateErc20ApproveTxs({
+ atomicAmount: inputAmountAtomic.toString(),
+ mintAddress: sourceTokenDetails.address,
+ spenderAddress: this.chainConfig.routingContractAddress,
+ wallet,
+ });
+
+ for await (const approvalTxResult of approvalGenerator) {
+ yield approvalTxResult;
+ }
+
+ const memo = Buffer.from(interactionId, "hex");
+ const routingContract = Routing__factory.connect(
+ this.chainConfig.routingContractAddress,
+ signer,
+ );
+ const initiatePropellerResponse = await routingContract[
+ "propellerInitiate(address,uint256,uint16,bytes32,bool,uint64,uint16,bytes16)"
+ ](
+ sourceTokenDetails.address,
+ inputAmountAtomic,
+ targetWormholeChainId,
+ targetWormholeAddress,
+ gasKickStart,
+ maxPropellerFeeAtomic,
+ targetTokenNumber,
+ memo,
+ // overrides, // TODO: allow EVM overrides?
+ );
+ const initiatePropellerTx = await this.getTx(initiatePropellerResponse);
+ yield {
+ tx: initiatePropellerTx,
+ type: EvmTxType.SwimInitiatePropeller,
+ };
}
- public async approveTokenAmount({
+ public async *generateErc20ApproveTxs({
atomicAmount,
mintAddress,
spenderAddress,
wallet,
- }: ApproveTokenAmountParams): Promise<
- readonly ethers.providers.TransactionResponse[]
+ }: ApproveTokenAmountParams): AsyncGenerator<
+ TxGeneratorResult
> {
const { signer } = wallet;
if (!signer) {
@@ -235,47 +312,40 @@ export class EvmClient extends Client<
}
await wallet.switchNetwork(this.chainConfig.chainId);
-
const allowance = await getAllowanceEth(
spenderAddress,
mintAddress,
signer,
);
- let approvalResponses: readonly ethers.providers.TransactionResponse[] = [];
if (allowance.lt(atomicAmount)) {
if (!allowance.isZero()) {
// Reset to 0 to avoid a race condition allowing Wormhole to steal funds
// See https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
// Note this is required by some ERC20 implementations such as USDT
// See line 205 here: https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7#code
- const resetApprovalResponse = await this.approveErc20Token({
+ const resetTx = await this.approveErc20Token({
atomicAmount: "0",
mintAddress,
signer,
spenderAddress,
});
- approvalResponses = resetApprovalResponse
- ? [...approvalResponses, resetApprovalResponse]
- : approvalResponses;
+ yield {
+ tx: resetTx,
+ type: EvmTxType.Erc20Approve,
+ };
}
- const approvalResponse = await this.approveErc20Token({
+ const tx = await this.approveErc20Token({
atomicAmount,
mintAddress,
signer,
spenderAddress,
});
- approvalResponses = approvalResponse
- ? [...approvalResponses, approvalResponse]
- : approvalResponses;
-
- // Wait for approvalResponse to be mined, otherwise the transfer might fail ("transfer amount exceeds allowance")
- if (approvalResponse) {
- await this.getTxReceiptOrThrow(approvalResponse);
- }
+ yield {
+ tx,
+ type: EvmTxType.Erc20Approve,
+ };
}
-
- return approvalResponses;
}
private async getTxReceipt(
@@ -319,11 +389,15 @@ export class EvmClient extends Client<
}
private async getTxReceiptOrThrow(
- txResponse: TransactionResponse,
+ txIdOrResponse: string | TransactionResponse,
): Promise {
- const txReceipt = await this.getTxReceipt(txResponse);
+ const txReceipt = await this.getTxReceipt(txIdOrResponse);
if (txReceipt === null) {
- throw new Error(`Transaction not found: ${txResponse.hash}`);
+ const id =
+ typeof txIdOrResponse === "string"
+ ? txIdOrResponse
+ : txIdOrResponse.hash;
+ throw new Error(`Transaction not found: ${id}`);
}
return txReceipt;
}
@@ -333,9 +407,10 @@ export class EvmClient extends Client<
signer,
spenderAddress,
mintAddress,
- }: ApproveErc20TokenParams): Promise {
+ }: ApproveErc20TokenParams): Promise {
const token = ERC20__factory.connect(mintAddress, signer);
- return token.approve(spenderAddress, atomicAmount);
+ const txResponse = await token.approve(spenderAddress, atomicAmount);
+ return await this.getTx(txResponse);
}
/**
@@ -348,7 +423,7 @@ export class EvmClient extends Client<
targetChainId,
mintAddress,
wallet,
- }: TransferTokenParams): Promise {
+ }: TransferTokenParams): Promise {
const { signer } = wallet;
if (!signer) {
throw new Error("No EVM signer");
@@ -367,6 +442,7 @@ export class EvmClient extends Client<
createNonce(),
);
const txRequest = appendHexDataToEvmTx(interactionId, populatedTx);
- return signer.sendTransaction(txRequest);
+ const txResponse = await signer.sendTransaction(txRequest);
+ return await this.getTx(txResponse);
}
}
diff --git a/packages/evm/src/protocol.ts b/packages/evm/src/protocol.ts
index 4d6da2e86..c197cd5b2 100644
--- a/packages/evm/src/protocol.ts
+++ b/packages/evm/src/protocol.ts
@@ -46,10 +46,14 @@ export interface EvmEcosystemConfig
readonly chains: Partial>>;
}
-export interface EvmTx extends Tx {
- readonly ecosystemId: EvmEcosystemId;
- readonly receipt: ethers.providers.TransactionReceipt;
+export enum EvmTxType {
+ Erc20Approve = "erc20:approve",
+ PortalTransferTokens = "portal:transferTokens",
+ PortalCompleteTransfer = "portal:completeTransfer",
+ SwimInitiatePropeller = "swim:initiatePropeller",
}
-export const isEvmTx = (tx: Tx): tx is EvmTx =>
+export type EvmTx = Tx;
+
+export const isEvmTx = (tx: Tx): tx is EvmTx =>
isEvmEcosystemId(tx.ecosystemId);
diff --git a/packages/pool-math/package.json b/packages/pool-math/package.json
index e71f451f8..a3cb02426 100644
--- a/packages/pool-math/package.json
+++ b/packages/pool-math/package.json
@@ -1,6 +1,6 @@
{
"name": "@swim-io/pool-math",
- "version": "0.39.0",
+ "version": "0.40.0",
"description": "A package for calculating input/output amounts for Swim liquidity pools.",
"main": "build",
"types": "types",
diff --git a/packages/solana-contracts/package.json b/packages/solana-contracts/package.json
index 03142d05d..ea176d2a5 100644
--- a/packages/solana-contracts/package.json
+++ b/packages/solana-contracts/package.json
@@ -1,6 +1,6 @@
{
"name": "@swim-io/solana-contracts",
- "version": "0.39.0",
+ "version": "0.40.0",
"description": "TS bindings for Swim Solana pool contracts.",
"main": "build",
"types": "types",
diff --git a/packages/solana-usdc-usdt-swap/package.json b/packages/solana-usdc-usdt-swap/package.json
index 360de68c7..ba8163dc5 100644
--- a/packages/solana-usdc-usdt-swap/package.json
+++ b/packages/solana-usdc-usdt-swap/package.json
@@ -1,6 +1,6 @@
{
"name": "@swim-io/solana-usdc-usdt-swap",
- "version": "0.39.0",
+ "version": "0.40.0",
"description": "Create Swim swap instructions for hexapool to swap Solana native USDC and USDT",
"main": "build",
"types": "types",
diff --git a/packages/solana/package.json b/packages/solana/package.json
index f50224923..feb2b2570 100644
--- a/packages/solana/package.json
+++ b/packages/solana/package.json
@@ -1,6 +1,6 @@
{
"name": "@swim-io/solana",
- "version": "0.39.0",
+ "version": "0.40.0",
"description": "Swim code relating to Solana.",
"main": "build",
"types": "types",
@@ -36,12 +36,15 @@
"@ledgerhq/hw-transport-webusb": "^6.0.2",
"@project-serum/sol-wallet-adapter": "0.2.2",
"@swim-io/core": "workspace:^",
+ "@swim-io/solana-contracts": "workspace:^",
"@swim-io/token-projects": "workspace:^",
"@swim-io/utils": "workspace:^"
},
"devDependencies": {
"@certusone/wormhole-sdk": "^0.6.2",
+ "@project-serum/anchor": "^0.25.0",
"@project-serum/borsh": "^0.2.5",
+ "@solana/spl-memo": "^0.2.2",
"@solana/spl-token": "^0.3.4",
"@solana/web3.js": "^1.62.0",
"@swim-io/eslint-config": "workspace:^",
@@ -69,7 +72,9 @@
},
"peerDependencies": {
"@certusone/wormhole-sdk": "^0.6.2",
+ "@project-serum/anchor": "^0.25.0",
"@project-serum/borsh": "^0.2.5",
+ "@solana/spl-memo": "^0.2.2",
"@solana/spl-token": "^0.3.4",
"@solana/web3.js": "^1.62.0",
"bn.js": "^5.2.1",
diff --git a/packages/solana/src/client.ts b/packages/solana/src/client.ts
index 8337acf84..2f9dfdbaa 100644
--- a/packages/solana/src/client.ts
+++ b/packages/solana/src/client.ts
@@ -1,8 +1,10 @@
+import type { ChainId } from "@certusone/wormhole-sdk";
+import { createVerifySignaturesInstructionsSolana } from "@certusone/wormhole-sdk";
+import type { Accounts } from "@project-serum/anchor";
+import { AnchorProvider, Program } from "@project-serum/anchor";
+import { createMemoInstruction } from "@solana/spl-memo";
import {
- createPostVaaInstructionSolana,
- createVerifySignaturesInstructionsSolana,
-} from "@certusone/wormhole-sdk";
-import {
+ TOKEN_PROGRAM_ID,
createAssociatedTokenAccountInstruction,
getAssociatedTokenAddress,
getAssociatedTokenAddressSync,
@@ -17,18 +19,27 @@ import type {
TransactionInstruction,
} from "@solana/web3.js";
import {
+ ComputeBudgetProgram,
Connection,
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
+ SYSVAR_CLOCK_PUBKEY,
} from "@solana/web3.js";
import type {
- CompleteWormholeTransferParams,
- InitiateWormholeTransferParams,
+ CompletePortalTransferParams,
+ InitiatePortalTransferParams,
+ InitiatePropellerParams,
TokenDetails,
+ TxGeneratorResult,
} from "@swim-io/core";
import { Client, getTokenDetails } from "@swim-io/core";
-import { atomicToHuman, chunks, sleep } from "@swim-io/utils";
+import type { Propeller } from "@swim-io/solana-contracts";
+import { idl } from "@swim-io/solana-contracts";
+import { TokenProjectId } from "@swim-io/token-projects";
+import type { ReadonlyRecord } from "@swim-io/utils";
+import { atomicToHuman, chunks, humanToAtomic, sleep } from "@swim-io/utils";
+import BN from "bn.js";
import Decimal from "decimal.js";
import type {
@@ -36,12 +47,21 @@ import type {
SolanaEcosystemId,
SolanaTx,
} from "./protocol";
-import { SOLANA_ECOSYSTEM_ID } from "./protocol";
+import { SOLANA_ECOSYSTEM_ID, SolanaTxType } from "./protocol";
import type { TokenAccount } from "./serialization";
import { deserializeTokenAccount } from "./serialization";
-import { createMemoIx, createTx } from "./utils";
+import {
+ createApproveAndRevokeIxs,
+ createTx,
+ parsedTxToSolanaTx,
+} from "./utils";
+import { extractOutputAmountFromAddTx } from "./utils/propeller";
import type { SolanaWalletAdapter } from "./walletAdapters";
-import { createRedeemOnSolanaTx, createTransferFromSolanaTx } from "./wormhole";
+import {
+ createPostVaaTx,
+ createRedeemOnSolanaTx,
+ createTransferFromSolanaTx,
+} from "./wormhole";
export const DEFAULT_MAX_RETRIES = 10;
export const DEFAULT_COMMITMENT_LEVEL: Finality = "confirmed";
@@ -51,15 +71,51 @@ type WithOptionalAuxiliarySigner = T & {
readonly auxiliarySigner?: Keypair;
};
-interface GeneratePostVaaTxIdsParams
- extends Omit<
- WithOptionalAuxiliarySigner<
- CompleteWormholeTransferParams
- >,
- "wallet"
- > {
+type CompleteWormholeMessageParams =
+ CompletePortalTransferParams;
+
+interface GenerateVerifySignaturesTxsParams
+ extends Omit, "wallet"> {
readonly signTx: (tx: Transaction) => Promise;
readonly payerAddress: string;
+ readonly auxiliarySigner: Keypair;
+}
+
+type SupportedTokenProjectId =
+ | TokenProjectId.SwimUsd
+ | TokenProjectId.Usdc
+ | TokenProjectId.Usdt;
+
+const SUPPORTED_TOKEN_PROJECT_IDS = [
+ TokenProjectId.SwimUsd,
+ TokenProjectId.Usdc,
+ TokenProjectId.Usdt,
+];
+
+const isSupportedTokenProjectId = (
+ id: TokenProjectId,
+): id is SupportedTokenProjectId => SUPPORTED_TOKEN_PROJECT_IDS.includes(id);
+
+interface PropellerAddParams {
+ readonly wallet: SolanaWalletAdapter;
+ readonly routingContract: Program;
+ readonly interactionId: string;
+ readonly senderPublicKey: PublicKey;
+ readonly sourceTokenId: SupportedTokenProjectId;
+ readonly inputAmountAtomic: string;
+}
+
+interface PropellerTransferParams {
+ readonly wallet: SolanaWalletAdapter;
+ readonly routingContract: Program;
+ readonly interactionId: string;
+ readonly senderPublicKey: PublicKey;
+ readonly targetWormholeChainId: ChainId;
+ readonly targetTokenNumber: number;
+ readonly targetWormholeAddress: Uint8Array;
+ readonly inputAmountAtomic: string;
+ readonly maxPropellerFeeAtomic: string;
+ readonly gasKickStart: boolean;
}
export interface GetSolanaTransactionOptions {
@@ -83,17 +139,6 @@ export class CustomConnection extends Connection {
}
}
-export const parsedTxToSolanaTx = (
- txId: string,
- parsedTx: ParsedTransactionWithMeta,
-): SolanaTx => ({
- id: txId,
- ecosystemId: SOLANA_ECOSYSTEM_ID,
- timestamp: parsedTx.blockTime ?? null,
- interactionId: null,
- parsedTx,
-});
-
export interface SolanaClientOptions {
readonly endpoints?: readonly string[];
}
@@ -105,6 +150,8 @@ export interface SolanaClientOptions {
export class SolanaClient extends Client<
SolanaEcosystemId,
SolanaChainConfig,
+ ParsedTransactionWithMeta,
+ SolanaTxType,
SolanaTx,
SolanaWalletAdapter
> {
@@ -132,14 +179,12 @@ export class SolanaClient extends Client<
public async getTx(txId: string): Promise {
const parsedTx = await this.getParsedTx(txId);
- return parsedTxToSolanaTx(txId, parsedTx);
+ return parsedTxToSolanaTx(parsedTx);
}
public async getTxs(txIds: readonly string[]): Promise {
const parsedTxs = await this.getParsedTxs(txIds);
- return parsedTxs.map((parsedTx, i) =>
- parsedTxToSolanaTx(txIds[i], parsedTx),
- );
+ return parsedTxs.map((parsedTx) => parsedTxToSolanaTx(parsedTx));
}
public async getGasBalance(address: string): Promise {
@@ -175,7 +220,7 @@ export class SolanaClient extends Client<
);
}
- public async initiateWormholeTransfer({
+ public async *generateInitiatePortalTransferTxs({
atomicAmount,
targetChainId,
targetAddress,
@@ -185,8 +230,14 @@ export class SolanaClient extends Client<
wrappedTokenInfo,
auxiliarySigner = Keypair.generate(),
}: WithOptionalAuxiliarySigner<
- InitiateWormholeTransferParams
- >): Promise {
+ InitiatePortalTransferParams
+ >): AsyncGenerator<
+ TxGeneratorResult<
+ ParsedTransactionWithMeta,
+ SolanaTx,
+ SolanaTxType.PortalInitiateTransfer
+ >
+ > {
const solanaWalletAddress = wallet.publicKey?.toBase58() ?? null;
if (solanaWalletAddress === null) {
throw new Error("No Solana wallet address");
@@ -198,7 +249,7 @@ export class SolanaClient extends Client<
new PublicKey(mintAddress),
new PublicKey(solanaWalletAddress),
).toBase58();
- const tx = await createTransferFromSolanaTx({
+ const txRequest = await createTransferFromSolanaTx({
interactionId,
connection: this.connection,
bridgeAddress: this.chainConfig.wormhole.bridge,
@@ -210,23 +261,31 @@ export class SolanaClient extends Client<
amount: BigInt(atomicAmount),
targetAddress,
targetChainId,
- originAddress: wrappedTokenInfo?.originAddress,
- originChain: wrappedTokenInfo?.originChainId,
+ wrappedTokenInfo,
});
- return await this.sendAndConfirmTx(async (txToSign: Transaction) => {
+ const txId = await this.sendAndConfirmTx(async (txToSign: Transaction) => {
txToSign.partialSign(auxiliarySigner);
return wallet.signTransaction(txToSign);
- }, tx);
+ }, txRequest);
+ const tx = await this.getTx(txId);
+ yield {
+ tx,
+ type: SolanaTxType.PortalInitiateTransfer,
+ };
}
- public async *generateCompleteWormholeTransferTxIds({
+ public async *generateCompleteWormholeMessageTxs({
interactionId,
vaa,
wallet,
- auxiliarySigner,
- }: WithOptionalAuxiliarySigner<
- CompleteWormholeTransferParams
- >): AsyncGenerator {
+ auxiliarySigner = Keypair.generate(),
+ }: WithOptionalAuxiliarySigner): AsyncGenerator<
+ TxGeneratorResult<
+ ParsedTransactionWithMeta,
+ SolanaTx,
+ SolanaTxType.WormholeVerifySignatures | SolanaTxType.WormholePostVaa
+ >
+ > {
const walletAddress = wallet.publicKey?.toBase58();
if (!walletAddress) {
throw new Error("No Solana public key");
@@ -234,37 +293,170 @@ export class SolanaClient extends Client<
const signTx = wallet.signTransaction.bind(wallet);
- const postVaaSolanaTxIdsGenerator = this.generatePostVaaTxIds({
+ const verifySignaturesTxsGenerator = this.generateVerifySignaturesTxs({
interactionId,
signTx,
payerAddress: walletAddress,
vaa,
auxiliarySigner,
});
- for await (const txId of postVaaSolanaTxIdsGenerator) {
- yield txId;
+ for await (const verifyTxResult of verifySignaturesTxsGenerator) {
+ yield verifyTxResult;
+ }
+
+ const postVaaTxRequest = await createPostVaaTx({
+ interactionId,
+ bridgeAddress: this.chainConfig.wormhole.bridge,
+ payerAddress: walletAddress,
+ vaa,
+ auxiliarySigner,
+ });
+ const postVaaTxId = await this.sendAndConfirmTx(signTx, postVaaTxRequest);
+ const postVaaTx = await this.getTx(postVaaTxId);
+ yield {
+ tx: postVaaTx,
+ type: SolanaTxType.WormholePostVaa,
+ };
+ }
+
+ public async *generateCompletePortalTransferTxs({
+ interactionId,
+ vaa,
+ wallet,
+ auxiliarySigner = Keypair.generate(),
+ }: WithOptionalAuxiliarySigner<
+ CompletePortalTransferParams
+ >): AsyncGenerator<
+ TxGeneratorResult<
+ ParsedTransactionWithMeta,
+ SolanaTx,
+ | SolanaTxType.WormholeVerifySignatures
+ | SolanaTxType.WormholePostVaa
+ | SolanaTxType.PortalRedeem
+ >
+ > {
+ const walletAddress = wallet.publicKey?.toBase58();
+ if (!walletAddress) {
+ throw new Error("No Solana public key");
}
- const redeemTx = await createRedeemOnSolanaTx({
+
+ const signTx = wallet.signTransaction.bind(wallet);
+
+ const completeWormholeTransferTxsGenerator =
+ this.generateCompleteWormholeMessageTxs({
+ interactionId,
+ vaa,
+ wallet,
+ auxiliarySigner,
+ });
+
+ for await (const result of completeWormholeTransferTxsGenerator) {
+ yield result;
+ }
+
+ const redeemTxRequest = await createRedeemOnSolanaTx({
interactionId,
bridgeAddress: this.chainConfig.wormhole.bridge,
portalAddress: this.chainConfig.wormhole.portal,
payerAddress: walletAddress,
vaa,
});
- yield await this.sendAndConfirmTx(signTx, redeemTx);
+ const redeemTxId = await this.sendAndConfirmTx(signTx, redeemTxRequest);
+ const redeemTx = await this.getTx(redeemTxId);
+ yield {
+ tx: redeemTx,
+ type: SolanaTxType.PortalRedeem,
+ };
}
- public async completeWormholeTransfer(
- params: WithOptionalAuxiliarySigner<
- CompleteWormholeTransferParams
- >,
- ): Promise {
- let txIds: readonly string[] = [];
- const generator = this.generateCompleteWormholeTransferTxIds(params);
- for await (const txId of generator) {
- txIds = [...txIds, txId];
+ public async *generateInitiatePropellerTxs({
+ wallet,
+ interactionId,
+ sourceTokenId,
+ targetWormholeChainId,
+ targetTokenNumber,
+ targetWormholeAddress,
+ inputAmount,
+ maxPropellerFeeAtomic,
+ gasKickStart,
+ auxiliarySigner = Keypair.generate(),
+ }: WithOptionalAuxiliarySigner<
+ InitiatePropellerParams
+ >): AsyncGenerator<
+ TxGeneratorResult,
+ any,
+ unknown
+ > {
+ const senderPublicKey = wallet.publicKey;
+ if (senderPublicKey === null) {
+ throw new Error("Missing Solana wallet");
}
- return txIds;
+ if (!isSupportedTokenProjectId(sourceTokenId)) {
+ throw new Error("Invalid source token id");
+ }
+
+ const sourceTokenDetails = getTokenDetails(this.chainConfig, sourceTokenId);
+ const inputAmountAtomic = humanToAtomic(
+ inputAmount,
+ sourceTokenDetails.decimals,
+ ).toString();
+
+ const anchorProvider = new AnchorProvider(
+ this.connection,
+ {
+ ...wallet,
+ publicKey: senderPublicKey,
+ },
+ { commitment: "confirmed" },
+ );
+ const routingContract = new Program(
+ idl.propeller,
+ this.chainConfig.routingContractAddress,
+ anchorProvider,
+ );
+
+ let addOutputAmountAtomic: string | null = null;
+ if (sourceTokenId !== TokenProjectId.SwimUsd) {
+ const addTx = await this.propellerAdd({
+ wallet,
+ routingContract,
+ interactionId,
+ senderPublicKey,
+ sourceTokenId,
+ inputAmountAtomic,
+ });
+
+ yield {
+ tx: addTx,
+ type: SolanaTxType.SwimPropellerAdd,
+ };
+
+ const outputAmount = extractOutputAmountFromAddTx(addTx.original);
+ if (!outputAmount) {
+ throw new Error("Could not parse propeller add output amount from log");
+ }
+ addOutputAmountAtomic = outputAmount;
+ }
+
+ const swimUsdInputAmountAtomic = addOutputAmountAtomic ?? inputAmountAtomic;
+
+ const transferTx = await this.propellerTransfer({
+ wallet,
+ routingContract,
+ interactionId,
+ senderPublicKey,
+ targetWormholeChainId,
+ targetTokenNumber,
+ targetWormholeAddress,
+ inputAmountAtomic: swimUsdInputAmountAtomic,
+ maxPropellerFeeAtomic,
+ gasKickStart,
+ auxiliarySigner,
+ });
+ yield {
+ tx: transferTx,
+ type: SolanaTxType.SwimPropellerTransfer,
+ };
}
public async confirmTx(
@@ -415,13 +607,11 @@ export class SolanaClient extends Client<
),
),
);
- return results.flatMap((accounts, i) =>
- accounts.map((account) =>
- account === null
- ? null
- : deserializeTokenAccount(new PublicKey(keys[i]), account.data),
- ),
- );
+ return results.flat().map((account, i) => {
+ return account === null
+ ? null
+ : deserializeTokenAccount(new PublicKey(keys[i]), account.data);
+ });
}
private incrementRpcProvider() {
@@ -567,14 +757,20 @@ export class SolanaClient extends Client<
);
}
- private async *generatePostVaaTxIds({
+ private async *generateVerifySignaturesTxs({
interactionId,
payerAddress,
vaa,
signTx,
- auxiliarySigner = Keypair.generate(),
- }: GeneratePostVaaTxIdsParams): AsyncGenerator {
- const memoIx = createMemoIx(interactionId, []);
+ auxiliarySigner,
+ }: GenerateVerifySignaturesTxsParams): AsyncGenerator<
+ TxGeneratorResult<
+ ParsedTransactionWithMeta,
+ SolanaTx,
+ SolanaTxType.WormholeVerifySignatures
+ >
+ > {
+ const memoIx = createMemoInstruction(interactionId);
const verifyIxs: readonly TransactionInstruction[] =
await createVerifySignaturesInstructionsSolana(
this.connection,
@@ -583,24 +779,14 @@ export class SolanaClient extends Client<
Buffer.from(vaa),
auxiliarySigner,
);
- const finalIx: TransactionInstruction =
- await createPostVaaInstructionSolana(
- this.chainConfig.wormhole.bridge,
- payerAddress,
- Buffer.from(vaa),
- auxiliarySigner,
- );
// The verify signatures instructions can be batched into groups of 2 safely,
// reducing the total number of transactions
const batchableChunks = chunks(verifyIxs, 2);
const feePayer = new PublicKey(payerAddress);
- const unsignedTxs = batchableChunks.map((chunk) =>
+ const verifyTxRequests = batchableChunks.map((chunk) =>
createTx({ feePayer }).add(...chunk, memoIx),
);
- // The postVaa instruction can only execute after the verifySignature transactions have
- // successfully completed
- const finalTx = createTx({ feePayer }).add(finalIx, memoIx);
// The signatureSet keypair also needs to sign the verifySignature transactions, thus a wrapper is needed
const partialSignWrapper = async (
@@ -610,9 +796,215 @@ export class SolanaClient extends Client<
return signTx(tx);
};
- for (const tx of unsignedTxs) {
- yield await this.sendAndConfirmTx(partialSignWrapper, tx);
+ for (const txRequest of verifyTxRequests) {
+ const txId = await this.sendAndConfirmTx(partialSignWrapper, txRequest);
+ const tx = await this.getTx(txId);
+ yield {
+ tx,
+ type: SolanaTxType.WormholeVerifySignatures,
+ };
}
- yield await this.sendAndConfirmTx(signTx, finalTx);
+ }
+
+ private getAddAccounts(
+ userSwimUsdAtaPublicKey: PublicKey,
+ userTokenAccounts: readonly PublicKey[],
+ auxiliarySigner: PublicKey,
+ lpMint: PublicKey,
+ poolTokenAccounts: readonly PublicKey[],
+ poolGovernanceFeeAccount: PublicKey,
+ ): Accounts {
+ return {
+ propeller: new PublicKey(this.chainConfig.routingContractStateAddress),
+ tokenProgram: TOKEN_PROGRAM_ID,
+ poolTokenAccount0: poolTokenAccounts[0],
+ poolTokenAccount1: poolTokenAccounts[1],
+ lpMint,
+ governanceFee: poolGovernanceFeeAccount,
+ userTransferAuthority: auxiliarySigner,
+ userTokenAccount0: userTokenAccounts[0],
+ userTokenAccount1: userTokenAccounts[1],
+ userLpTokenAccount: userSwimUsdAtaPublicKey,
+ twoPoolProgram: new PublicKey(this.chainConfig.twoPoolContractAddress),
+ };
+ }
+
+ private async getPropellerTransferAccounts(
+ walletPublicKey: PublicKey,
+ swimUsdAtaPublicKey: PublicKey,
+ auxiliarySigner: PublicKey,
+ ): Promise {
+ const bridgePublicKey = new PublicKey(this.chainConfig.wormhole.bridge);
+ const portalPublicKey = new PublicKey(this.chainConfig.wormhole.portal);
+ const swimUsdMintPublicKey = new PublicKey(
+ this.chainConfig.swimUsdDetails.address,
+ );
+ const [wormholeConfig] = await PublicKey.findProgramAddress(
+ [Buffer.from("Bridge")],
+ bridgePublicKey,
+ );
+ const [tokenBridgeConfig] = await PublicKey.findProgramAddress(
+ [Buffer.from("config")],
+ portalPublicKey,
+ );
+ const [custody] = await PublicKey.findProgramAddress(
+ [swimUsdMintPublicKey.toBytes()],
+ portalPublicKey,
+ );
+ const [custodySigner] = await PublicKey.findProgramAddress(
+ [Buffer.from("custody_signer")],
+ portalPublicKey,
+ );
+ const [authoritySigner] = await PublicKey.findProgramAddress(
+ [Buffer.from("authority_signer")],
+ portalPublicKey,
+ );
+ const [wormholeEmitter] = await PublicKey.findProgramAddress(
+ [Buffer.from("emitter")],
+ portalPublicKey,
+ );
+ const [wormholeSequence] = await PublicKey.findProgramAddress(
+ [Buffer.from("Sequence"), wormholeEmitter.toBytes()],
+ bridgePublicKey,
+ );
+ const [wormholeFeeCollector] = await PublicKey.findProgramAddress(
+ [Buffer.from("fee_collector")],
+ bridgePublicKey,
+ );
+ return {
+ propeller: new PublicKey(this.chainConfig.routingContractStateAddress),
+ tokenProgram: TOKEN_PROGRAM_ID,
+ payer: walletPublicKey,
+ wormhole: bridgePublicKey,
+ tokenBridgeConfig,
+ userSwimUsdAta: swimUsdAtaPublicKey,
+ swimUsdMint: swimUsdMintPublicKey,
+ custody,
+ tokenBridge: portalPublicKey,
+ custodySigner,
+ authoritySigner,
+ wormholeConfig,
+ wormholeMessage: auxiliarySigner,
+ wormholeEmitter,
+ wormholeSequence,
+ wormholeFeeCollector,
+ clock: SYSVAR_CLOCK_PUBKEY,
+ };
+ }
+
+ private async propellerAdd({
+ wallet,
+ routingContract,
+ interactionId,
+ senderPublicKey,
+ sourceTokenId,
+ inputAmountAtomic,
+ auxiliarySigner = Keypair.generate(),
+ }: WithOptionalAuxiliarySigner): Promise {
+ const [twoPoolConfig] = this.chainConfig.pools;
+ const addInputAmounts =
+ sourceTokenId === TokenProjectId.Usdc
+ ? [inputAmountAtomic, "0"]
+ : ["0", inputAmountAtomic];
+ const addMaxFee = "0"; // TODO: Change to a real value
+
+ const userTokenAccounts = SUPPORTED_TOKEN_PROJECT_IDS.reduce(
+ (accumulator, tokenProjectId) => {
+ const { address } = getTokenDetails(this.chainConfig, tokenProjectId);
+ return {
+ ...accumulator,
+ [tokenProjectId]: getAssociatedTokenAddressSync(
+ new PublicKey(address),
+ senderPublicKey,
+ ),
+ };
+ },
+ {} as ReadonlyRecord,
+ );
+ const addAccounts = this.getAddAccounts(
+ userTokenAccounts[TokenProjectId.SwimUsd],
+ [
+ userTokenAccounts[TokenProjectId.Usdc],
+ userTokenAccounts[TokenProjectId.Usdt],
+ ],
+ auxiliarySigner.publicKey,
+ new PublicKey(this.chainConfig.swimUsdDetails.address),
+ [...twoPoolConfig.tokenAccounts.values()].map(
+ (address) => new PublicKey(address),
+ ),
+ new PublicKey(twoPoolConfig.governanceFeeAccount),
+ );
+
+ const [approveIx, revokeIx] = await createApproveAndRevokeIxs(
+ userTokenAccounts[sourceTokenId],
+ inputAmountAtomic,
+ auxiliarySigner.publicKey,
+ senderPublicKey,
+ );
+ const memoIx = createMemoInstruction(interactionId);
+
+ const txRequest = await routingContract.methods
+ .propellerAdd(
+ addInputAmounts.map((amount) => new BN(amount)),
+ new BN(addMaxFee),
+ )
+ .accounts(addAccounts)
+ .preInstructions([approveIx])
+ .postInstructions([revokeIx, memoIx])
+ .signers([auxiliarySigner])
+ .transaction();
+ const txId = await this.sendAndConfirmTx(async (tx) => {
+ tx.partialSign(auxiliarySigner);
+ return wallet.signTransaction(tx);
+ }, txRequest);
+ return await this.getTx(txId);
+ }
+
+ private async propellerTransfer({
+ wallet,
+ routingContract,
+ interactionId,
+ senderPublicKey,
+ targetWormholeChainId,
+ targetTokenNumber,
+ targetWormholeAddress,
+ inputAmountAtomic,
+ maxPropellerFeeAtomic,
+ gasKickStart,
+ auxiliarySigner = Keypair.generate(),
+ }: WithOptionalAuxiliarySigner): Promise {
+ const memo = Buffer.from(interactionId, "hex");
+ const setComputeUnitLimitIx = ComputeBudgetProgram.setComputeUnitLimit({
+ units: 350_000,
+ });
+ const swimUsdTokenAccount = await getAssociatedTokenAddress(
+ new PublicKey(this.chainConfig.swimUsdDetails.address),
+ senderPublicKey,
+ );
+ const transferAccounts = await this.getPropellerTransferAccounts(
+ senderPublicKey,
+ swimUsdTokenAccount,
+ auxiliarySigner.publicKey,
+ );
+ const txRequest = await routingContract.methods
+ .propellerTransferNativeWithPayload(
+ new BN(inputAmountAtomic),
+ targetWormholeChainId,
+ targetWormholeAddress,
+ gasKickStart,
+ new BN(maxPropellerFeeAtomic),
+ targetTokenNumber,
+ memo,
+ )
+ .accounts(transferAccounts)
+ .preInstructions([setComputeUnitLimitIx])
+ .signers([auxiliarySigner])
+ .transaction();
+
+ const txId = await this.sendAndConfirmTx(async (tx) => {
+ tx.partialSign(auxiliarySigner);
+ return wallet.signTransaction(tx);
+ }, txRequest);
+ return await this.getTx(txId);
}
}
diff --git a/packages/solana/src/protocol.ts b/packages/solana/src/protocol.ts
index d04232a0a..fd4b43525 100644
--- a/packages/solana/src/protocol.ts
+++ b/packages/solana/src/protocol.ts
@@ -37,10 +37,16 @@ export interface SolanaEcosystemConfig extends EcosystemConfig {
readonly chains: Partial>;
}
-export interface SolanaTx extends Tx {
- readonly ecosystemId: SolanaEcosystemId;
- readonly parsedTx: ParsedTransactionWithMeta;
+export enum SolanaTxType {
+ PortalInitiateTransfer = "portal:initiateTransfer",
+ PortalRedeem = "portal:redeem",
+ WormholeVerifySignatures = "wormhole:verifySignatures",
+ WormholePostVaa = "wormhole:postVaa",
+ SwimPropellerAdd = "swimPropeller:add",
+ SwimPropellerTransfer = "swimPropeller:transfer",
}
-export const isSolanaTx = (tx: Tx): tx is SolanaTx =>
+export type SolanaTx = Tx;
+
+export const isSolanaTx = (tx: Tx): tx is SolanaTx =>
tx.ecosystemId === SOLANA_ECOSYSTEM_ID;
diff --git a/packages/solana/src/utils/propeller.ts b/packages/solana/src/utils/propeller.ts
new file mode 100644
index 000000000..9431ce316
--- /dev/null
+++ b/packages/solana/src/utils/propeller.ts
@@ -0,0 +1,12 @@
+import type { ParsedTransactionWithMeta } from "@solana/web3.js";
+
+const PROPELLER_OUTPUT_AMOUNT_REGEX =
+ /^Program log: propeller_add output_amount: (?\d+)/;
+export const extractOutputAmountFromAddTx = (
+ tx: ParsedTransactionWithMeta | null,
+): string | null => {
+ const addLog = tx?.meta?.logMessages?.find((log) =>
+ PROPELLER_OUTPUT_AMOUNT_REGEX.test(log),
+ );
+ return addLog?.match(PROPELLER_OUTPUT_AMOUNT_REGEX)?.groups?.amount ?? null;
+};
diff --git a/packages/solana/src/utils/splToken.ts b/packages/solana/src/utils/splToken.ts
index 32d9b8630..ed8cdb30a 100644
--- a/packages/solana/src/utils/splToken.ts
+++ b/packages/solana/src/utils/splToken.ts
@@ -1,6 +1,11 @@
+import { Spl } from "@project-serum/anchor";
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
-import type { ParsedTransactionWithMeta } from "@solana/web3.js";
+import type {
+ ParsedTransactionWithMeta,
+ TransactionInstruction,
+} from "@solana/web3.js";
import { PublicKey } from "@solana/web3.js";
+import { BN } from "bn.js";
import Decimal from "decimal.js";
import type { TokenAccount } from "../serialization";
@@ -24,6 +29,31 @@ export const findTokenAccountForMint = (
);
};
+export const createApproveAndRevokeIxs = async (
+ tokenAccount: PublicKey,
+ amount: string,
+ delegate: PublicKey,
+ authority: PublicKey,
+): Promise => {
+ const splToken = Spl.token();
+ const approveIx = splToken.methods
+ .approve(new BN(amount))
+ .accounts({
+ source: tokenAccount,
+ delegate,
+ authority,
+ })
+ .instruction();
+ const revokeIx = splToken.methods
+ .revoke()
+ .accounts({
+ source: tokenAccount,
+ authority,
+ })
+ .instruction();
+ return Promise.all([approveIx, revokeIx]);
+};
+
interface ParsedSplTokenTransferInstruction {
readonly program: string; // "spl-token"
readonly programId: PublicKey;
diff --git a/packages/solana/src/utils/tx.ts b/packages/solana/src/utils/tx.ts
index 849f7f9f1..4b74321ac 100644
--- a/packages/solana/src/utils/tx.ts
+++ b/packages/solana/src/utils/tx.ts
@@ -1,6 +1,12 @@
-import type { TransactionBlockhashCtor } from "@solana/web3.js";
+import type {
+ ParsedTransactionWithMeta,
+ TransactionBlockhashCtor,
+} from "@solana/web3.js";
import { Transaction } from "@solana/web3.js";
+import type { SolanaTx } from "../protocol";
+import { SOLANA_ECOSYSTEM_ID } from "../protocol";
+
export type CreateTxOptions = Omit<
TransactionBlockhashCtor,
"blockhash" | "lastValidBlockHeight"
@@ -9,3 +15,13 @@ export type CreateTxOptions = Omit<
/** Create transaction with dummy blockhash and lastValidBlockHeight, expected to be overwritten by solanaClient.sendAndConfirmTx to prevent expired blockhash */
export const createTx = (opts: CreateTxOptions): Transaction =>
new Transaction({ ...opts, blockhash: "", lastValidBlockHeight: 0 });
+
+export const parsedTxToSolanaTx = (
+ parsedTx: ParsedTransactionWithMeta,
+): SolanaTx => ({
+ id: parsedTx.transaction.signatures[0],
+ ecosystemId: SOLANA_ECOSYSTEM_ID,
+ timestamp: parsedTx.blockTime ?? null,
+ interactionId: null,
+ original: parsedTx,
+});
diff --git a/packages/solana/src/wormhole.ts b/packages/solana/src/wormhole.ts
index a6d7e70cf..087db56df 100644
--- a/packages/solana/src/wormhole.ts
+++ b/packages/solana/src/wormhole.ts
@@ -2,6 +2,7 @@ import type { ChainId } from "@certusone/wormhole-sdk";
import {
CHAIN_ID_SOLANA,
createNonce,
+ createPostVaaInstructionSolana,
getBridgeFeeIx,
importCoreWasm,
importTokenWasm,
@@ -10,11 +11,13 @@ import {
import { createApproveInstruction } from "@solana/spl-token";
import type {
Connection,
+ Keypair,
ParsedTransactionWithMeta,
Transaction,
VersionedTransactionResponse,
} from "@solana/web3.js";
import { PublicKey } from "@solana/web3.js";
+import type { WrappedTokenInfo } from "@swim-io/core/types";
import { createMemoIx, createTx } from "./utils";
@@ -48,8 +51,7 @@ export interface CreateTransferFromSolanaTxParams {
readonly amount: bigint;
readonly targetAddress: Uint8Array;
readonly targetChainId: ChainId;
- readonly originAddress?: Uint8Array;
- readonly originChain?: ChainId;
+ readonly wrappedTokenInfo?: WrappedTokenInfo;
readonly fromOwnerAddress?: string;
}
export const createTransferFromSolanaTx = async ({
@@ -64,34 +66,32 @@ export const createTransferFromSolanaTx = async ({
amount,
targetAddress,
targetChainId,
- originAddress,
- originChain,
+ wrappedTokenInfo,
fromOwnerAddress,
}: CreateTransferFromSolanaTxParams): Promise => {
const nonce = createNonce().readUInt32LE(0);
- const fee = BigInt(0); // for now, this won't do anything, we may add later
+ const fee = BigInt(0); // not currently used
const bridgeFeeIx = await getBridgeFeeIx(
connection,
bridgeAddress,
payerAddress,
);
+
// eslint-disable-next-line @typescript-eslint/unbound-method
const {
+ approval_authority_address,
transfer_native_ix,
transfer_wrapped_ix,
- approval_authority_address,
} = await importTokenWasm();
const approvalIx = createApproveInstruction(
new PublicKey(fromAddress),
new PublicKey(approval_authority_address(portalAddress)),
- new PublicKey(fromOwnerAddress || payerAddress),
+ new PublicKey(fromOwnerAddress ?? payerAddress),
amount,
);
const isSolanaNative =
- originChain === undefined || originChain === CHAIN_ID_SOLANA;
- if (!isSolanaNative && !originAddress) {
- throw new Error("originAddress is required when specifying originChain");
- }
+ wrappedTokenInfo === undefined ||
+ wrappedTokenInfo.originChainId === CHAIN_ID_SOLANA;
const transferIx = ixFromRust(
isSolanaNative
? transfer_native_ix(
@@ -114,8 +114,8 @@ export const createTransferFromSolanaTx = async ({
auxiliarySignerAddress,
fromAddress,
fromOwnerAddress || payerAddress,
- originChain as number, // checked by isSolanaNative
- originAddress as Uint8Array, // checked by throw
+ wrappedTokenInfo.originChainId,
+ wrappedTokenInfo.originAddress,
nonce,
amount,
fee,
@@ -123,10 +123,37 @@ export const createTransferFromSolanaTx = async ({
targetChainId,
),
);
+
const memoIx = createMemoIx(interactionId, []);
- const tx = createTx({
+ return createTx({
feePayer: new PublicKey(payerAddress),
}).add(bridgeFeeIx, approvalIx, transferIx, memoIx);
+};
+
+export interface CreatePostVaaTxParams {
+ readonly interactionId: string;
+ readonly bridgeAddress: string;
+ readonly payerAddress: string;
+ readonly vaa: Uint8Array;
+ readonly auxiliarySigner: Keypair;
+}
+export const createPostVaaTx = async ({
+ interactionId,
+ bridgeAddress,
+ payerAddress,
+ vaa,
+ auxiliarySigner,
+}: CreatePostVaaTxParams): Promise => {
+ const postVaaIx = await createPostVaaInstructionSolana(
+ bridgeAddress,
+ payerAddress,
+ Buffer.from(vaa),
+ auxiliarySigner,
+ );
+ const memoIx = createMemoIx(interactionId, []);
+ const tx = createTx({
+ feePayer: new PublicKey(payerAddress),
+ }).add(postVaaIx, memoIx);
return tx;
};
diff --git a/packages/token-projects/package.json b/packages/token-projects/package.json
index 1f5d738f2..76f55e3c2 100644
--- a/packages/token-projects/package.json
+++ b/packages/token-projects/package.json
@@ -1,6 +1,6 @@
{
"name": "@swim-io/token-projects",
- "version": "0.39.0",
+ "version": "0.40.0",
"description": "A registry of token projects used by Swim.",
"main": "build",
"types": "types",
diff --git a/packages/tsconfig/package.json b/packages/tsconfig/package.json
index 78a54c8a8..858d7c773 100644
--- a/packages/tsconfig/package.json
+++ b/packages/tsconfig/package.json
@@ -1,6 +1,6 @@
{
"name": "@swim-io/tsconfig",
- "version": "0.39.0",
+ "version": "0.40.0",
"description": "Shared default TypeScript configuration for Swim TS projects.",
"files": [
"*.json",
diff --git a/packages/utils/package.json b/packages/utils/package.json
index 1ea75ca8a..2bce0da3f 100644
--- a/packages/utils/package.json
+++ b/packages/utils/package.json
@@ -1,6 +1,6 @@
{
"name": "@swim-io/utils",
- "version": "0.39.0",
+ "version": "0.40.0",
"description": "Minimal-dependency general-purpose utils.",
"main": "build",
"types": "types",
diff --git a/packages/wormhole/package.json b/packages/wormhole/package.json
index 69af16861..d059923d1 100644
--- a/packages/wormhole/package.json
+++ b/packages/wormhole/package.json
@@ -1,6 +1,6 @@
{
"name": "@swim-io/wormhole",
- "version": "0.39.0",
+ "version": "0.40.0",
"description": "Swim code relating to Wormhole.",
"main": "build",
"types": "types",
diff --git a/yarn.lock b/yarn.lock
index f0c822356..6980d7cd4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7248,6 +7248,19 @@ __metadata:
languageName: node
linkType: hard
+"@swim-io/core@npm:^0.40.0":
+ version: 0.40.0
+ resolution: "@swim-io/core@npm:0.40.0"
+ dependencies:
+ "@swim-io/token-projects": ^0.40.0
+ "@swim-io/utils": ^0.40.0
+ decimal.js: ^10.3.1
+ peerDependencies:
+ "@certusone/wormhole-sdk": ^0.6.2
+ checksum: 8f71a7c1708ae099d70fa6f39c3eac475702c948ed670924a66053ed8def4578358365a8e48d1fb31f253fd6df515b1827977f6072561159d7e604cfa8207afc
+ languageName: node
+ linkType: hard
+
"@swim-io/core@workspace:^, @swim-io/core@workspace:packages/core":
version: 0.0.0-use.local
resolution: "@swim-io/core@workspace:packages/core"
@@ -7339,10 +7352,10 @@ __metadata:
languageName: node
linkType: hard
-"@swim-io/evm-contracts@npm:^0.39.0":
- version: 0.39.0
- resolution: "@swim-io/evm-contracts@npm:0.39.0"
- checksum: 9c375aed437790788e6ac53ebf13829f34da2fe3d00859aed4d563b3e173d8ec2faac0b217c0ee11069978b755fa47c1db94b2a8cf3596157605716798a432ea
+"@swim-io/evm-contracts@npm:^0.40.0":
+ version: 0.40.0
+ resolution: "@swim-io/evm-contracts@npm:0.40.0"
+ checksum: 5061e0d2594e4912f271613f2cbbfb819238a809cb3c01a086339e325890607fd67efe8f5b47d6d434b016d8b92c6968331458a49ed4da9333753cc2fcf19145
languageName: node
linkType: hard
@@ -7400,14 +7413,14 @@ __metadata:
languageName: node
linkType: hard
-"@swim-io/evm@npm:^0.39.0":
- version: 0.39.0
- resolution: "@swim-io/evm@npm:0.39.0"
+"@swim-io/evm@npm:^0.40.0":
+ version: 0.40.0
+ resolution: "@swim-io/evm@npm:0.40.0"
dependencies:
- "@swim-io/core": ^0.39.0
- "@swim-io/evm-contracts": ^0.39.0
- "@swim-io/token-projects": ^0.39.0
- "@swim-io/utils": ^0.39.0
+ "@swim-io/core": ^0.40.0
+ "@swim-io/evm-contracts": ^0.40.0
+ "@swim-io/token-projects": ^0.40.0
+ "@swim-io/utils": ^0.40.0
graphql: ^16.6.0
graphql-request: ^4.3.0
moralis: ^1.8.0
@@ -7416,7 +7429,7 @@ __metadata:
decimal.js: ^10.3.1
ethers: ^5.6.9
eventemitter3: ^4.0.7
- checksum: d45ae02e098b910f357ad4f584c74405d5882f223ba8f1469532e20739ad189e46729ee258421170fe5d5b4dab06486b74406beab0a4a3bc4896794dac6d6a2d
+ checksum: ba65ee3dcf51c9a4c1e329a7597b41c4226028f2fc76afd57c9e8185b20e04ef5563d47cdf017ec9aa122e148162408b16d544a167c6953b251a77fc83cebdfb
languageName: node
linkType: hard
@@ -7479,12 +7492,12 @@ __metadata:
languageName: unknown
linkType: soft
-"@swim-io/pool-math@npm:^0.39.0":
- version: 0.39.0
- resolution: "@swim-io/pool-math@npm:0.39.0"
+"@swim-io/pool-math@npm:^0.40.0":
+ version: 0.40.0
+ resolution: "@swim-io/pool-math@npm:0.40.0"
peerDependencies:
decimal.js: ^10.3.1
- checksum: 68f86c9a614630192dd54a04a7f96765823e2fb2444a8807de172714570695f0ee0f8c82a676b682973dc64f54eca422b95828b7df9eacfa9b71d5cc5a819a63
+ checksum: 6b3de0916194bcab630111aa1bc34d46d046c7d1960437a734b354a5f376e23e89ccfa228a077332ff33989373a9de6d883ff437849bd2cf61526d6db52d5843
languageName: node
linkType: hard
@@ -7614,19 +7627,19 @@ __metadata:
languageName: unknown
linkType: soft
-"@swim-io/solana-contracts@npm:^0.39.0":
- version: 0.39.0
- resolution: "@swim-io/solana-contracts@npm:0.39.0"
+"@swim-io/solana-contracts@npm:^0.40.0":
+ version: 0.40.0
+ resolution: "@swim-io/solana-contracts@npm:0.40.0"
peerDependencies:
"@project-serum/anchor": ^0.25.0
"@project-serum/borsh": ^0.2.5
"@solana/spl-token": ^0.2.0
"@solana/web3.js": ^1.50.1
- checksum: 891cea2e53838d8e6048054174c5685782cd5ef86c0d8e062e123fd26839bf03d0e15b9245f037de27fcca87352b695735e0fbde6a3abfa8dba156c5d8cb7575
+ checksum: 5f78da938ff92fa660024ed2ad63345db68bee8b0f83f7e26cb86aa7e959a6e35d8afece1d11374274037b0821b4d55e1597f1ba40e1814a65e8faf75e75c041
languageName: node
linkType: hard
-"@swim-io/solana-contracts@workspace:packages/solana-contracts":
+"@swim-io/solana-contracts@workspace:^, @swim-io/solana-contracts@workspace:packages/solana-contracts":
version: 0.0.0-use.local
resolution: "@swim-io/solana-contracts@workspace:packages/solana-contracts"
dependencies:
@@ -7753,6 +7766,28 @@ __metadata:
languageName: node
linkType: hard
+"@swim-io/solana@npm:^0.40.0":
+ version: 0.40.0
+ resolution: "@swim-io/solana@npm:0.40.0"
+ dependencies:
+ "@ledgerhq/hw-transport": ^6.27.1
+ "@ledgerhq/hw-transport-webusb": ^6.0.2
+ "@project-serum/sol-wallet-adapter": 0.2.2
+ "@swim-io/core": ^0.40.0
+ "@swim-io/token-projects": ^0.40.0
+ "@swim-io/utils": ^0.40.0
+ peerDependencies:
+ "@certusone/wormhole-sdk": ^0.6.2
+ "@project-serum/borsh": ^0.2.5
+ "@solana/spl-token": ^0.3.4
+ "@solana/web3.js": ^1.62.0
+ bn.js: ^5.2.1
+ decimal.js: ^10.3.1
+ ethers: ^5.7.0
+ checksum: 207c35b40b87f6e2bdbec0a50dcbe0cb5bb437d5170e2691790b56c1c91fff508ea2cb5ec9cb8abbf52dcd23a1ca1e1663e843abee9e10c57eec1996584c186b
+ languageName: node
+ linkType: hard
+
"@swim-io/solana@workspace:^, @swim-io/solana@workspace:packages/solana":
version: 0.0.0-use.local
resolution: "@swim-io/solana@workspace:packages/solana"
@@ -7760,12 +7795,15 @@ __metadata:
"@certusone/wormhole-sdk": ^0.6.2
"@ledgerhq/hw-transport": ^6.27.1
"@ledgerhq/hw-transport-webusb": ^6.0.2
+ "@project-serum/anchor": ^0.25.0
"@project-serum/borsh": ^0.2.5
"@project-serum/sol-wallet-adapter": 0.2.2
+ "@solana/spl-memo": ^0.2.2
"@solana/spl-token": ^0.3.4
"@solana/web3.js": ^1.62.0
"@swim-io/core": "workspace:^"
"@swim-io/eslint-config": "workspace:^"
+ "@swim-io/solana-contracts": "workspace:^"
"@swim-io/token-projects": "workspace:^"
"@swim-io/tsconfig": "workspace:^"
"@swim-io/utils": "workspace:^"
@@ -7791,7 +7829,9 @@ __metadata:
typescript: ~4.8.4
peerDependencies:
"@certusone/wormhole-sdk": ^0.6.2
+ "@project-serum/anchor": ^0.25.0
"@project-serum/borsh": ^0.2.5
+ "@solana/spl-memo": ^0.2.2
"@solana/spl-token": ^0.3.4
"@solana/web3.js": ^1.62.0
bn.js: ^5.2.1
@@ -7843,6 +7883,15 @@ __metadata:
languageName: node
linkType: hard
+"@swim-io/token-projects@npm:^0.40.0":
+ version: 0.40.0
+ resolution: "@swim-io/token-projects@npm:0.40.0"
+ dependencies:
+ "@swim-io/utils": ^0.40.0
+ checksum: 48958357bcb6d30978833321f9e6db875581102416327f50b345515166505a5ce5e6e5f1b0d3d458ac440503f3cc9cb9436384e59da86da703f1e95e15518733
+ languageName: node
+ linkType: hard
+
"@swim-io/token-projects@workspace:^, @swim-io/token-projects@workspace:packages/token-projects":
version: 0.0.0-use.local
resolution: "@swim-io/token-projects@workspace:packages/token-projects"
@@ -7910,17 +7959,17 @@ __metadata:
"@storybook/node-logger": ^6.5.10
"@storybook/react": ^6.5.10
"@swim-io/aptos": "workspace:^"
- "@swim-io/core": ^0.39.0
+ "@swim-io/core": ^0.40.0
"@swim-io/eslint-config": "workspace:^"
- "@swim-io/evm": ^0.39.0
- "@swim-io/evm-contracts": ^0.39.0
- "@swim-io/pool-math": ^0.39.0
- "@swim-io/solana": ^0.39.0
- "@swim-io/solana-contracts": ^0.39.0
- "@swim-io/token-projects": "workspace:^"
+ "@swim-io/evm": ^0.40.0
+ "@swim-io/evm-contracts": ^0.40.0
+ "@swim-io/pool-math": ^0.40.0
+ "@swim-io/solana": ^0.40.0
+ "@swim-io/solana-contracts": ^0.40.0
+ "@swim-io/token-projects": ^0.40.0
"@swim-io/tsconfig": "workspace:^"
- "@swim-io/utils": ^0.39.0
- "@swim-io/wormhole": ^0.39.0
+ "@swim-io/utils": ^0.40.0
+ "@swim-io/wormhole": ^0.40.0
"@testing-library/jest-dom": ^5.16.4
"@testing-library/react": ^12.1.5
"@testing-library/react-hooks": ^7.0.2
@@ -7993,6 +8042,15 @@ __metadata:
languageName: node
linkType: hard
+"@swim-io/utils@npm:^0.40.0":
+ version: 0.40.0
+ resolution: "@swim-io/utils@npm:0.40.0"
+ peerDependencies:
+ decimal.js: ^10.3.1
+ checksum: 0a9c84c556c57015aa118ddb4b72a00e01ac837c1c7870e475897b85a0cfdc1583f9e90b6615a804a90e8185fa80bc05e06e8f15275dda0dcd8475bbbb219bb3
+ languageName: node
+ linkType: hard
+
"@swim-io/utils@workspace:^, @swim-io/utils@workspace:packages/utils":
version: 0.0.0-use.local
resolution: "@swim-io/utils@workspace:packages/utils"
@@ -8020,20 +8078,20 @@ __metadata:
languageName: unknown
linkType: soft
-"@swim-io/wormhole@npm:^0.39.0":
- version: 0.39.0
- resolution: "@swim-io/wormhole@npm:0.39.0"
+"@swim-io/wormhole@npm:^0.40.0":
+ version: 0.40.0
+ resolution: "@swim-io/wormhole@npm:0.40.0"
dependencies:
- "@swim-io/core": ^0.39.0
- "@swim-io/solana": ^0.39.0
- "@swim-io/utils": ^0.39.0
+ "@swim-io/core": ^0.40.0
+ "@swim-io/solana": ^0.40.0
+ "@swim-io/utils": ^0.40.0
grpc-web: ^1.3.1
peerDependencies:
"@certusone/wormhole-sdk": ^0.6.2
"@solana/spl-token": ^0.3.4
"@solana/web3.js": ^1.62.0
ethers: ^5.6.9
- checksum: 953ed6377f6614fffa9b7a335543e804598380092bbdf71dee4ab52a90eef4738f3249133f729e4e89888592e76bf5205195931444f73b3aeaad25519cb85055
+ checksum: 12af304e4184bb1db601df1bc8a3c40f38f76ef64d18de5bfcc783b55680adc3090fb7beb08bcee15c9a22313a724fb6418713eaa628f0fad1be449865455f9e
languageName: node
linkType: hard