From c9333c93cc1a35feca027073a7a42c6ab5321522 Mon Sep 17 00:00:00 2001 From: delivan Date: Wed, 11 Dec 2024 22:05:41 +0900 Subject: [PATCH 01/43] Show evm assets on swap from, to currencies --- apps/extension/src/config.ui.ts | 33 ++++++++++++- apps/stores-internal/src/skip/assets.ts | 49 ++++++++++++++------ apps/stores-internal/src/skip/chains.ts | 40 ++++++++++++++-- apps/stores-internal/src/skip/ibc-swap.ts | 17 +++++-- apps/stores-internal/src/skip/msgs-direct.ts | 3 ++ apps/stores-internal/src/skip/route.ts | 3 ++ apps/stores-internal/src/skip/types.ts | 3 ++ 7 files changed, 127 insertions(+), 21 deletions(-) diff --git a/apps/extension/src/config.ui.ts b/apps/extension/src/config.ui.ts index 505ba745c7..279810b17e 100644 --- a/apps/extension/src/config.ui.ts +++ b/apps/extension/src/config.ui.ts @@ -216,6 +216,38 @@ export const SwapVenues: { name: "chihuahua-white-whale", chainId: "chihuahua-1", }, + { + name: "arbitrum-uniswap", + chainId: "eip155:42161", + }, + { + name: "base-uniswap", + chainId: "eip155:8453", + }, + { + name: "binance-uniswap", + chainId: "eip155:56", + }, + { + name: "avalanche-uniswap", + chainId: "eip155:43114", + }, + { + name: "optimism-uniswap", + chainId: "eip155:10", + }, + { + name: "polygon-uniswap", + chainId: "eip155:137", + }, + { + name: "blast-uniswap", + chainId: "eip155:81457", + }, + { + name: "ethereum-uniswap", + chainId: "eip155:1", + }, ]; export const SwapFeeBps = { @@ -225,7 +257,6 @@ export const SwapFeeBps = { chainId: "osmosis-1", address: "osmo1my4tk420gjmhggqwvvha6ey9390gqwfree2p4u", }, - // TODO: 여기 밑으론 실제 receiver 주소로 변경해야 함 { chainId: "injective-1", address: "inj1tfn0awxutuvrgqvme7g3e9nd2fe5r3uzqa4fjr", diff --git a/apps/stores-internal/src/skip/assets.ts b/apps/stores-internal/src/skip/assets.ts index c51b34584b..7f2c53f556 100644 --- a/apps/stores-internal/src/skip/assets.ts +++ b/apps/stores-internal/src/skip/assets.ts @@ -20,6 +20,8 @@ const Schema = Joi.object({ chain_id: Joi.string().required(), origin_denom: Joi.string().required(), origin_chain_id: Joi.string().required(), + is_evm: Joi.boolean().required(), + token_contract: Joi.string().optional(), }).unknown(true) ), }).unknown(true) @@ -37,7 +39,10 @@ export class ObservableQueryAssetsInner extends ObservableQuery super( sharedContext, skipURL, - `/v2/fungible/assets?chain_id=${chainId}&native_only=false` + `/v2/fungible/assets?chain_id=${chainId.replace( + "eip155:", + "" + )}&native_only=false&include_evm_assets=true` ); makeObservable(this); @@ -68,7 +73,9 @@ export class ObservableQueryAssetsInner extends ObservableQuery } const assetsInResponse = - this.response.data.chain_to_assets_map[chainInfo.chainId]; + this.response.data.chain_to_assets_map[ + this.chainId.replace("eip155:", "") + ]; if (assetsInResponse) { const res: { denom: string; @@ -78,26 +85,42 @@ export class ObservableQueryAssetsInner extends ObservableQuery }[] = []; for (const asset of assetsInResponse.assets) { + const chainId = asset.is_evm + ? `eip155:${asset.chain_id}` + : asset.chain_id; + const originChainId = asset.is_evm + ? `eip155:${asset.origin_chain_id}` + : asset.origin_chain_id; if ( - this.chainStore.hasChain(asset.chain_id) && - this.chainStore.hasChain(asset.origin_chain_id) + this.chainStore.hasChain(chainId) && + this.chainStore.hasChain(originChainId) ) { // IBC asset일 경우 그냥 넣는다. if (asset.denom.startsWith("ibc/")) { res.push({ denom: asset.denom, - chainId: asset.chain_id, + chainId: chainId, originDenom: asset.origin_denom, - originChainId: asset.origin_chain_id, + originChainId: originChainId, }); // IBC asset이 아니라면 알고있는 currency만 넣는다. - } else if (chainInfo.findCurrencyWithoutReaction(asset.denom)) { - res.push({ - denom: asset.denom, - chainId: asset.chain_id, - originDenom: asset.origin_denom, - originChainId: asset.origin_chain_id, - }); + } else { + const coinMinimalDenom = + asset.is_evm && asset.token_contract != null + ? `erc20:${asset.denom}` + : asset.denom; + const originCoinMinimalDenom = + asset.is_evm && asset.token_contract != null + ? `erc20:${asset.origin_denom}` + : asset.denom; + if (chainInfo.findCurrencyWithoutReaction(coinMinimalDenom)) { + res.push({ + denom: coinMinimalDenom, + chainId: chainId, + originDenom: originCoinMinimalDenom, + originChainId: originChainId, + }); + } } } } diff --git a/apps/stores-internal/src/skip/chains.ts b/apps/stores-internal/src/skip/chains.ts index d9245cf18c..3d8b8b2551 100644 --- a/apps/stores-internal/src/skip/chains.ts +++ b/apps/stores-internal/src/skip/chains.ts @@ -17,6 +17,7 @@ const Schema = Joi.object({ chain_id: Joi.string(), pfm_enabled: Joi.boolean(), supports_memo: Joi.boolean(), + chain_type: Joi.string(), }).unknown(true) ), }).unknown(true); @@ -27,7 +28,7 @@ export class ObservableQueryChains extends ObservableQuery { protected readonly chainStore: InternalChainStore, protected readonly skipURL: string ) { - super(sharedContext, skipURL, "/v2/info/chains"); + super(sharedContext, skipURL, "/v2/info/chains?include_evm=true"); makeObservable(this); } @@ -70,6 +71,7 @@ export class ObservableQueryChains extends ObservableQuery { chainInfo: IChainInfoImpl; pfmEnabled: boolean; supportsMemo: boolean; + chainType: string; }[] { if (!this.response) { return []; @@ -77,16 +79,32 @@ export class ObservableQueryChains extends ObservableQuery { return this.response.data.chains .filter((chain) => { - return this.chainStore.hasChain(chain.chain_id); + const isEVMChain = chain.chain_type === "evm"; + const chainId = isEVMChain + ? `eip155:${chain.chain_id}` + : chain.chain_id; + + return this.chainStore.hasChain(chainId); }) .filter((chain) => { - return this.chainStore.isInChainInfosInListUI(chain.chain_id); + const isEVMChain = chain.chain_type === "evm"; + const chainId = isEVMChain + ? `eip155:${chain.chain_id}` + : chain.chain_id; + + return this.chainStore.isInChainInfosInListUI(chainId); }) .map((chain) => { + const isEVMChain = chain.chain_type === "evm"; + const chainId = isEVMChain + ? `eip155:${chain.chain_id}` + : chain.chain_id; + return { - chainInfo: this.chainStore.getChain(chain.chain_id), + chainInfo: this.chainStore.getChain(chainId), pfmEnabled: chain.pfm_enabled, supportsMemo: chain.supports_memo ?? false, + chainType: chain.chain_type, }; }); } @@ -118,4 +136,18 @@ export class ObservableQueryChains extends ObservableQuery { return chain.supportsMemo; }); + + isChainTypeEVM = computedFn((chainId: string): boolean => { + const chain = this.chains.find((chain) => { + return ( + chain.chainInfo.chainIdentifier === + ChainIdHelper.parse(chainId).identifier + ); + }); + if (!chain) { + return false; + } + + return chain.chainType === "evm"; + }); } diff --git a/apps/stores-internal/src/skip/ibc-swap.ts b/apps/stores-internal/src/skip/ibc-swap.ts index fcd80af75c..8e89723061 100644 --- a/apps/stores-internal/src/skip/ibc-swap.ts +++ b/apps/stores-internal/src/skip/ibc-swap.ts @@ -348,9 +348,20 @@ export class ObservableQueryIbcSwap extends HasMapStore { cumulative_affiliate_fee_bps: this.affiliateFeeBps.toString(), swap_venues: this.swapVenues, allow_unsafe: true, + smart_swap_options: { + evm_swaps: true, + }, }), signal: abortController.signal, }); diff --git a/apps/stores-internal/src/skip/types.ts b/apps/stores-internal/src/skip/types.ts index f86b0004a8..917e870c0e 100644 --- a/apps/stores-internal/src/skip/types.ts +++ b/apps/stores-internal/src/skip/types.ts @@ -22,6 +22,8 @@ export interface AssetsResponse { chain_id: string; origin_denom: string; origin_chain_id: string; + is_evm: boolean; + token_contract?: string; }[]; } | undefined; @@ -97,5 +99,6 @@ export interface ChainsResponse { chain_id: string; pfm_enabled: boolean; supports_memo?: boolean; + chain_type: string; }[]; } From 80af452e3e8ff1128bd0a2befd82d2d9e391d77e Mon Sep 17 00:00:00 2001 From: delivan Date: Thu, 12 Dec 2024 22:33:21 +0900 Subject: [PATCH 02/43] Add react-window `FixedSizeList` to improve rendering performance --- apps/extension/package.json | 3 + apps/extension/src/layouts/header/header.tsx | 2 + apps/extension/src/layouts/header/types.ts | 1 + .../src/pages/ibc-swap/select-asset/index.tsx | 144 +++++++++++------- apps/stores-internal/src/skip/assets.ts | 6 +- yarn.lock | 37 ++++- 6 files changed, 136 insertions(+), 57 deletions(-) diff --git a/apps/extension/package.json b/apps/extension/package.json index 8b1f69141e..05fd9b600c 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -82,6 +82,8 @@ "react-is": "^18.2.0", "react-router": "^6.8.1", "react-router-dom": "^6.8.1", + "react-virtualized-auto-sizer": "^1.0.24", + "react-window": "^1.8.10", "scrypt-js": "^3.0.1", "secp256k1": "^5.0.0", "semver": "^7.6.0", @@ -116,6 +118,7 @@ "@types/firefox-webext-browser": "^120.0.3", "@types/js-yaml": "^4", "@types/react-is": "^16.7.2", + "@types/react-window": "^1", "@types/resize-observer-browser": "^0.1.7", "@types/secp256k1": "^4.0.1", "@types/semver": "^7", diff --git a/apps/extension/src/layouts/header/header.tsx b/apps/extension/src/layouts/header/header.tsx index b2bdadb725..3de5740790 100644 --- a/apps/extension/src/layouts/header/header.tsx +++ b/apps/extension/src/layouts/header/header.tsx @@ -196,6 +196,7 @@ export const HeaderLayout: FunctionComponent< isNotReady, additionalPaddingBottom, headerContainerStyle, + contentContainerStyle, fixedTop, }) => { @@ -296,6 +297,7 @@ export const HeaderLayout: FunctionComponent< fixedMinHeight={fixedMinHeight || false} bottomPadding={bottomPadding} fixedTopHeight={fixedTop?.height} + style={contentContainerStyle} > {children} diff --git a/apps/extension/src/layouts/header/types.ts b/apps/extension/src/layouts/header/types.ts index d2a59fd09e..c5b1c2c627 100644 --- a/apps/extension/src/layouts/header/types.ts +++ b/apps/extension/src/layouts/header/types.ts @@ -22,6 +22,7 @@ export interface HeaderProps { isNotReady?: boolean; headerContainerStyle?: React.CSSProperties; + contentContainerStyle?: React.CSSProperties; // MainHeaderLayout에서만 테스트해봄 // 다른 props 옵션과 섞였을때 잘 작동되는지는 모름 diff --git a/apps/extension/src/pages/ibc-swap/select-asset/index.tsx b/apps/extension/src/pages/ibc-swap/select-asset/index.tsx index 218dd0a672..6b13d33454 100644 --- a/apps/extension/src/pages/ibc-swap/select-asset/index.tsx +++ b/apps/extension/src/pages/ibc-swap/select-asset/index.tsx @@ -18,6 +18,8 @@ import { computed, makeObservable } from "mobx"; import { ObservableQueryIbcSwap } from "@keplr-wallet/stores-internal"; import { Currency } from "@keplr-wallet/types"; import { IChainInfoImpl } from "@keplr-wallet/stores"; +import { FixedSizeList } from "react-window"; +import AutoSizer from "react-virtualized-auto-sizer"; // 계산이 복잡해서 memoize을 적용해야하는데 // mobx와 useMemo()는 같이 사용이 어려워서 @@ -118,6 +120,7 @@ class IBCSwapDestinationState { const Styles = { Container: styled(Stack)` + height: 100%; padding: 0.75rem; `, }; @@ -219,6 +222,7 @@ export const IBCSwapDestinationSelectAssetPage: FunctionComponent = observer( } + contentContainerStyle={{ height: "100vh" }} > - {filteredTokens.map((viewToken) => { - return ( - { - if (paramNavigateTo) { - navigate( - paramNavigateTo - .replace("{chainId}", viewToken.chainInfo.chainId) - .replace( - "{coinMinimalDenom}", - viewToken.token.currency.coinMinimalDenom - ), - { - replace: paramNavigateReplace === "true", - } - ); - } else { - console.error("Empty navigateTo param"); - } + + {({ height, width }: { height: number; width: number }) => ( + { + if (paramNavigateTo) { + navigate( + paramNavigateTo + .replace("{chainId}", chainId) + .replace("{coinMinimalDenom}", coinMinimalDenom), + { + replace: paramNavigateReplace === "true", + } + ); + } else { + console.error("Empty navigateTo param"); + } + }, }} - /> - ); - })} - {filteredRemaining.map((item) => { - return ( - { - if (paramNavigateTo) { - navigate( - paramNavigateTo - .replace("{chainId}", item.chainInfo.chainId) - .replace( - "{coinMinimalDenom}", - item.currency.coinMinimalDenom - ), - { - replace: paramNavigateReplace === "true", - } - ); - } else { - console.error("Empty navigateTo param"); - } - }} - /> - ); - })} + width={width} + height={height} + itemCount={filteredTokens.length + filteredRemaining.length} + itemSize={76} + > + {TokenListItem} + + )} + ); } ); + +const TOKEN_LIST_ITEM_GUTTER = 8; + +const TokenListItem = ({ + data, + index, + style, +}: { + data: { + filteredTokens: ViewToken[]; + filteredRemaining: { + currency: Currency; + chainInfo: IChainInfoImpl; + }[]; + onClick: (chainId: string, coinMinimalDenom: string) => void; + }; + index: number; + style: any; +}) => { + const isFilteredTokens = index < data.filteredTokens.length; + const item = isFilteredTokens + ? data.filteredTokens[index] + : data.filteredRemaining[index - data.filteredTokens.length]; + const viewToken = + "currency" in item + ? { + chainInfo: item.chainInfo, + token: new CoinPretty(item.currency, new Dec(0)), + isFetching: false, + error: undefined, + } + : item; + + return ( +
+ + data.onClick( + viewToken.chainInfo.chainId, + viewToken.token.currency.coinMinimalDenom + ) + } + /> +
+ ); +}; diff --git a/apps/stores-internal/src/skip/assets.ts b/apps/stores-internal/src/skip/assets.ts index 7f2c53f556..87da983dbf 100644 --- a/apps/stores-internal/src/skip/assets.ts +++ b/apps/stores-internal/src/skip/assets.ts @@ -113,7 +113,11 @@ export class ObservableQueryAssetsInner extends ObservableQuery asset.is_evm && asset.token_contract != null ? `erc20:${asset.origin_denom}` : asset.denom; - if (chainInfo.findCurrencyWithoutReaction(coinMinimalDenom)) { + const currencyFound = + chainInfo.findCurrencyWithoutReaction(coinMinimalDenom); + // decimals이 18 이하인 경우만을 고려해서 짜여진 코드가 많아서 임시로 18 이하인 경우만 고려한다. + // TODO: Dec, Int 같은 곳에서 18 이상인 경우도 고려하도록 수정 + if (currencyFound && currencyFound.coinDecimals <= 18) { res.push({ denom: coinMinimalDenom, chainId: chainId, diff --git a/yarn.lock b/yarn.lock index e462b7121e..7339db691a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8828,6 +8828,7 @@ __metadata: "@types/firefox-webext-browser": ^120.0.3 "@types/js-yaml": ^4 "@types/react-is": ^16.7.2 + "@types/react-window": ^1 "@types/resize-observer-browser": ^0.1.7 "@types/secp256k1": ^4.0.1 "@types/semver": ^7 @@ -8864,6 +8865,8 @@ __metadata: react-is: ^18.2.0 react-router: ^6.8.1 react-router-dom: ^6.8.1 + react-virtualized-auto-sizer: ^1.0.24 + react-window: ^1.8.10 scrypt-js: ^3.0.1 secp256k1: ^5.0.0 semver: ^7.6.0 @@ -15163,6 +15166,15 @@ __metadata: languageName: node linkType: hard +"@types/react-window@npm:^1": + version: 1.8.8 + resolution: "@types/react-window@npm:1.8.8" + dependencies: + "@types/react": "*" + checksum: 253c9d6e0c942f34633edbddcbc369324403c42458ff004457c5bd5972961d8433a909c0cc1a89c918063d5eb85ecbdd774142af2555fae61f4ceb3ba9884b5a + languageName: node + linkType: hard + "@types/react@npm:^18.2.19": version: 18.2.19 resolution: "@types/react@npm:18.2.19" @@ -31342,7 +31354,7 @@ __metadata: languageName: node linkType: hard -"memoize-one@npm:^5.0.0": +"memoize-one@npm:>=3.1.1 <6, memoize-one@npm:^5.0.0": version: 5.2.1 resolution: "memoize-one@npm:5.2.1" checksum: a3cba7b824ebcf24cdfcd234aa7f86f3ad6394b8d9be4c96ff756dafb8b51c7f71320785fbc2304f1af48a0467cbbd2a409efc9333025700ed523f254cb52e3d @@ -37097,6 +37109,29 @@ __metadata: languageName: node linkType: hard +"react-virtualized-auto-sizer@npm:^1.0.24": + version: 1.0.24 + resolution: "react-virtualized-auto-sizer@npm:1.0.24" + peerDependencies: + react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 + react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 + checksum: e7d98563735dabbd1c58727c9d3e9f08f6a60a9964d25507cf4ef08f8964b6e421491c892ee0a99e47630118fdca42f1c60cef15ebda3659face58025dba3e98 + languageName: node + linkType: hard + +"react-window@npm:^1.8.10": + version: 1.8.10 + resolution: "react-window@npm:1.8.10" + dependencies: + "@babel/runtime": ^7.0.0 + memoize-one: ">=3.1.1 <6" + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + checksum: e8830f32e3ad4bf91af9cdc5cead84148c7694ce6abd9fdb447fb609da6cd4bbd0bbc75ff985f78828f4bbbd3ba4cbc98235cc9c056b5e5787578518f7fafbb9 + languageName: node + linkType: hard + "react@npm:18.2.0": version: 18.2.0 resolution: "react@npm:18.2.0" From 3bbab51c13014933f7047932b7bebba5cec86340 Mon Sep 17 00:00:00 2001 From: delivan Date: Fri, 13 Dec 2024 14:33:49 +0900 Subject: [PATCH 03/43] Enable `split_routes` option on skip api --- apps/stores-internal/src/skip/msgs-direct.ts | 1 + apps/stores-internal/src/skip/route.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/apps/stores-internal/src/skip/msgs-direct.ts b/apps/stores-internal/src/skip/msgs-direct.ts index aec74021a2..c3eb64ca18 100644 --- a/apps/stores-internal/src/skip/msgs-direct.ts +++ b/apps/stores-internal/src/skip/msgs-direct.ts @@ -212,6 +212,7 @@ export class ObservableQueryMsgsDirectInner extends ObservableQuery { allow_unsafe: true, smart_swap_options: { evm_swaps: true, + split_routes: true, }, }), signal: abortController.signal, From 39b9c597ed515d528feb71a7c1f963726277141c Mon Sep 17 00:00:00 2001 From: delivan Date: Mon, 16 Dec 2024 19:58:40 +0900 Subject: [PATCH 04/43] Update skip api(route, msgs-direct) to support evm tx --- apps/extension/src/pages/ibc-swap/index.tsx | 7 +- apps/hooks-internal/src/ibc-swap/amount.ts | 163 ++++++++++++++----- apps/stores-internal/src/skip/msgs-direct.ts | 17 +- apps/stores-internal/src/skip/route.ts | 64 +++++++- apps/stores-internal/src/skip/types.ts | 28 ++++ 5 files changed, 218 insertions(+), 61 deletions(-) diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index 7b352f2d5b..e5e2849438 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -161,13 +161,18 @@ export const IBCSwapPage: FunctionComponent = observer(() => { // ---- const [swapFeeBps, setSwapFeeBps] = useState(SwapFeeBps.value); + const isInChainEVMOnly = chainStore.isEvmOnlyChain(inChainId); + const inChainAccount = accountStore.getAccount(inChainId); + const ibcSwapConfigs = useIBCSwapConfig( chainStore, queriesStore, accountStore, skipQueriesStore, inChainId, - accountStore.getAccount(inChainId).bech32Address, + isInChainEVMOnly + ? inChainAccount.ethereumHexAddress + : inChainAccount.bech32Address, // TODO: config로 빼기 200000, outChainId, diff --git a/apps/hooks-internal/src/ibc-swap/amount.ts b/apps/hooks-internal/src/ibc-swap/amount.ts index 6feb210423..c39033ef7e 100644 --- a/apps/hooks-internal/src/ibc-swap/amount.ts +++ b/apps/hooks-internal/src/ibc-swap/amount.ts @@ -151,51 +151,86 @@ export class IBCSwapAmountConfig extends AmountConfig { } const chainIdsToAddresses: Record = {}; + const sourceAccount = this.accountStore.getAccount(this.chainId); - const destinationChainIds = queryRouteResponse.data.chain_ids; if (sourceAccount.walletStatus === WalletStatus.NotInit) { await sourceAccount.init(); } + + const isSourceAccountEVMOnly = this.chainId.startsWith("eip155:"); + if ( + isSourceAccountEVMOnly + ? !sourceAccount.ethereumHexAddress + : !sourceAccount.bech32Address + ) { + throw new Error("Source account is not set"); + } + chainIdsToAddresses[this.chainId.replace("eip155:", "")] = + isSourceAccountEVMOnly + ? sourceAccount.ethereumHexAddress + : sourceAccount.bech32Address; + + const destinationChainIds = queryRouteResponse.data.chain_ids.map( + (chainId) => { + const evmLikeChainId = Number(chainId); + const isEVMChainId = + !Number.isNaN(evmLikeChainId) && evmLikeChainId !== 0; + if (isEVMChainId) { + return `eip155:${chainId}`; + } + return chainId; + } + ); for (const destinationChainId of destinationChainIds) { const destinationAccount = this.accountStore.getAccount(destinationChainId); if (destinationAccount.walletStatus === WalletStatus.NotInit) { await destinationAccount.init(); } - } - if (!sourceAccount.bech32Address) { - throw new Error("Source account is not set"); - } - for (const destinationChainId of destinationChainIds) { - const destinationAccount = - this.accountStore.getAccount(destinationChainId); - if (!destinationAccount.bech32Address) { + const isDestinationChainEVMOnly = + destinationChainId.startsWith("eip155:"); + if ( + isDestinationChainEVMOnly + ? !destinationAccount.ethereumHexAddress + : !destinationAccount.bech32Address + ) { throw new Error("Destination account is not set"); } - } - - chainIdsToAddresses[this.chainId] = sourceAccount.bech32Address; - for (const destinationChainId of destinationChainIds) { - const destinationAccount = - this.accountStore.getAccount(destinationChainId); - chainIdsToAddresses[destinationChainId] = - destinationAccount.bech32Address; + chainIdsToAddresses[destinationChainId.replace("eip155", "")] = + isDestinationChainEVMOnly + ? destinationAccount.ethereumHexAddress + : destinationAccount.bech32Address; } for (const swapVenue of queryRouteResponse.data.swap_venues ?? [ queryRouteResponse.data.swap_venue, ]) { if (swapVenue) { - const swapAccount = this.accountStore.getAccount(swapVenue.chain_id); + const swapVenueChainId = (() => { + const evmLikeChainId = Number(swapVenue.chain_id); + const isEVMChainId = + !Number.isNaN(evmLikeChainId) && evmLikeChainId !== 0; + if (isEVMChainId) { + return `eip155:${swapVenue.chain_id}`; + } + return swapVenue.chain_id; + })(); + const swapAccount = this.accountStore.getAccount(swapVenueChainId); if (swapAccount.walletStatus === WalletStatus.NotInit) { await swapAccount.init(); } - if (!swapAccount.bech32Address) { + + const isSwapVenueChainEVMOnly = swapVenueChainId.startsWith("eip155:"); + if ( + isSwapVenueChainEVMOnly + ? !swapAccount.ethereumHexAddress + : !swapAccount.bech32Address + ) { const swapVenueChainInfo = - this.chainGetter.hasChain(swapVenue.chain_id) && - this.chainGetter.getChain(swapVenue.chain_id); + this.chainGetter.hasChain(swapVenueChainId) && + this.chainGetter.getChain(swapVenueChainId); if ( swapAccount.isNanoLedger && swapVenueChainInfo && @@ -211,8 +246,10 @@ export class IBCSwapAmountConfig extends AmountConfig { throw new Error("Swap account is not set"); } - - chainIdsToAddresses[swapVenue.chain_id] = swapAccount.bech32Address; + chainIdsToAddresses[swapVenueChainId.replace("eip155:", "")] = + isSwapVenueChainEVMOnly + ? swapAccount.ethereumHexAddress + : swapAccount.bech32Address; } } @@ -294,52 +331,90 @@ export class IBCSwapAmountConfig extends AmountConfig { } const chainIdsToAddresses: Record = {}; - const sourceAccount = this.accountStore.getAccount(this.chainId); - const destinationChainIds = queryRouteResponse.data.chain_ids; + const sourceAccount = this.accountStore.getAccount(this.chainId); if (sourceAccount.walletStatus === WalletStatus.NotInit) { sourceAccount.init(); } + + const isSourceAccountEVMOnly = this.chainId.startsWith("eip155:"); + if ( + isSourceAccountEVMOnly + ? !sourceAccount.ethereumHexAddress + : !sourceAccount.bech32Address + ) { + return; + } + chainIdsToAddresses[this.chainId.replace("eip155:", "")] = + isSourceAccountEVMOnly + ? sourceAccount.ethereumHexAddress + : sourceAccount.bech32Address; + + const destinationChainIds = queryRouteResponse.data.chain_ids.map( + (chainId) => { + const evmLikeChainId = Number(chainId); + const isEVMChainId = + !Number.isNaN(evmLikeChainId) && evmLikeChainId !== 0; + if (isEVMChainId) { + return `eip155:${chainId}`; + } + return chainId; + } + ); for (const destinationChainId of destinationChainIds) { const destinationAccount = this.accountStore.getAccount(destinationChainId); + if (destinationAccount.walletStatus === WalletStatus.NotInit) { destinationAccount.init(); } - } - if (!sourceAccount.bech32Address) { - return; - } - for (const destinationChainId of destinationChainIds) { - const destinationAccount = - this.accountStore.getAccount(destinationChainId); - if (!destinationAccount.bech32Address) { + const isDestinationChainEVMOnly = + destinationChainId.startsWith("eip155:"); + if ( + isDestinationChainEVMOnly + ? !destinationAccount.ethereumHexAddress + : !destinationAccount.bech32Address + ) { return; } - } - - chainIdsToAddresses[this.chainId] = sourceAccount.bech32Address; - for (const destinationChainId of destinationChainIds) { - const destinationAccount = - this.accountStore.getAccount(destinationChainId); - chainIdsToAddresses[destinationChainId] = - destinationAccount.bech32Address; + chainIdsToAddresses[destinationChainId.replace("eip155:", "")] = + isDestinationChainEVMOnly + ? destinationAccount.ethereumHexAddress + : destinationAccount.bech32Address; } for (const swapVenue of queryRouteResponse.data.swap_venues ?? [ queryRouteResponse.data.swap_venue, ]) { if (swapVenue) { - const swapAccount = this.accountStore.getAccount(swapVenue.chain_id); + const swapVenueChainId = (() => { + const evmLikeChainId = Number(swapVenue.chain_id); + const isEVMChainId = + !Number.isNaN(evmLikeChainId) && evmLikeChainId !== 0; + if (isEVMChainId) { + return `eip155:${swapVenue.chain_id}`; + } + + return swapVenue.chain_id; + })(); + const swapAccount = this.accountStore.getAccount(swapVenueChainId); if (swapAccount.walletStatus === WalletStatus.NotInit) { swapAccount.init(); } - if (!swapAccount.bech32Address) { + const isSwapVenueChainEVMOnly = swapVenueChainId.startsWith("eip155:"); + if ( + isSwapVenueChainEVMOnly + ? !swapAccount.ethereumHexAddress + : !swapAccount.bech32Address + ) { return; } - chainIdsToAddresses[swapVenue.chain_id] = swapAccount.bech32Address; + chainIdsToAddresses[swapVenueChainId.replace("eip155:", "")] = + isSwapVenueChainEVMOnly + ? swapAccount.ethereumHexAddress + : swapAccount.bech32Address; } } diff --git a/apps/stores-internal/src/skip/msgs-direct.ts b/apps/stores-internal/src/skip/msgs-direct.ts index c3eb64ca18..507cf96a4f 100644 --- a/apps/stores-internal/src/skip/msgs-direct.ts +++ b/apps/stores-internal/src/skip/msgs-direct.ts @@ -193,9 +193,9 @@ export class ObservableQueryMsgsDirectInner extends ObservableQuery ({ + ...swapVenue, + chainId: swapVenue.chainId.replace("eip155:", ""), + })), allow_unsafe: true, + smart_relay: true, + go_fast: true, + experimental_features: ["hyperlane"], smart_swap_options: { evm_swaps: true, split_routes: true, @@ -225,10 +231,7 @@ export class ObservableQueryMsgsDirectInner extends ObservableQuery({ }) .required() .unknown(true), + }).unknown(true), + Joi.object({ + evm_swap: Joi.object({ + amount_in: Joi.string().required(), + amount_out: Joi.string().required(), + denom_in: Joi.string().required(), + denom_out: Joi.string().required(), + from_chain_id: Joi.string().required(), + swap_calldata: Joi.string().required(), + swap_venues: Joi.array() + .items( + Joi.object({ + name: Joi.string().required(), + chain_id: Joi.string().required(), + logo_uri: Joi.string().required(), + }).unknown(true) + ) + .required(), + }) + .required() + .unknown(true), + }).unknown(true), + Joi.object({ + cctp_transfer: Joi.object({ + bridge_id: Joi.string().required(), + burn_token: Joi.string().required(), + denom_in: Joi.string().required(), + denom_out: Joi.string().required(), + from_chain_id: Joi.string().required(), + to_chain_id: Joi.string().required(), + smart_relay: Joi.boolean().required(), + smart_relay_fee_quote: Joi.object({ + fee_amount: Joi.string().required(), + fee_denom: Joi.string().required(), + relayer_address: Joi.string().required(), + expiration: Joi.string().required(), + }) + .required() + .unknown(true), + }) + .required() + .unknown(true), }).unknown(true) ) .required(), @@ -193,12 +235,18 @@ export class ObservableQueryRouteInner extends ObservableQuery { body: JSON.stringify({ amount_in: this.sourceAmount, source_asset_denom: this.sourceDenom, - source_asset_chain_id: this.sourceChainId, + source_asset_chain_id: this.sourceChainId.replace("eip155:", ""), dest_asset_denom: this.destDenom, - dest_asset_chain_id: this.destChainId, + dest_asset_chain_id: this.destChainId.replace("eip155:", ""), cumulative_affiliate_fee_bps: this.affiliateFeeBps.toString(), - swap_venues: this.swapVenues, + swap_venues: this.swapVenues.map((swapVenue) => ({ + ...swapVenue, + chainId: swapVenue.chainId.replace("eip155:", ""), + })), allow_unsafe: true, + smart_relay: true, + go_fast: true, + experimental_features: ["hyperlane"], smart_swap_options: { evm_swaps: true, split_routes: true, @@ -212,11 +260,9 @@ export class ObservableQueryRouteInner extends ObservableQuery { }; const validated = Schema.validate(result.data); + if (validated.error) { - console.log( - "Failed to validate assets from source response", - validated.error - ); + console.log("Failed to validate route response", validated.error); throw validated.error; } @@ -230,9 +276,9 @@ export class ObservableQueryRouteInner extends ObservableQuery { return `${super.getCacheKey()}-${JSON.stringify({ amount_in: this.sourceAmount, source_asset_denom: this.sourceDenom, - source_asset_chain_id: this.sourceChainId, + source_asset_chain_id: this.sourceChainId.replace("eip155:", ""), dest_asset_denom: this.destDenom, - dest_asset_chain_id: this.destChainId, + dest_asset_chain_id: this.destChainId.replace("eip155:", ""), affiliateFeeBps: this.affiliateFeeBps, swap_venue: this.swapVenues, })}`; diff --git a/apps/stores-internal/src/skip/types.ts b/apps/stores-internal/src/skip/types.ts index 917e870c0e..41fc0e88d6 100644 --- a/apps/stores-internal/src/skip/types.ts +++ b/apps/stores-internal/src/skip/types.ts @@ -78,6 +78,34 @@ export interface RouteResponse { estimated_affiliate_fee: string; }; } + | { + evm_swap: { + amount_in: string; + amount_out: string; + denom_in: string; + denom_out: string; + from_chain_id: string; + input_token: string; + swap_calldata: string; + }; + } + | { + cctp_transfer: { + bridge_id: string; + burn_token: string; + denom_in: string; + denom_out: string; + from_chain_id: string; + to_chain_id: string; + smart_relay: boolean; + smart_relay_fee_quote: { + fee_amount: string; + fee_denom: string; + relayer_address: string; + expiration: string; + }; + }; + } )[]; chain_ids: string[]; does_swap?: boolean; From 0ecc2f2dc5cfd10c20928a787e2f5c0370320f58 Mon Sep 17 00:00:00 2001 From: delivan Date: Mon, 16 Dec 2024 20:58:14 +0900 Subject: [PATCH 05/43] Add go fast transfer support on skip api --- apps/extension/src/pages/ibc-swap/index.tsx | 2 + apps/hooks-internal/package.json | 1 + apps/hooks-internal/src/ibc-swap/amount.ts | 103 ++++++++------- apps/hooks-internal/src/ibc-swap/use.ts | 3 + apps/mobile/src/screen/ibc-swap/index.tsx | 2 + apps/stores-internal/src/skip/msgs-direct.ts | 132 +++++++++++-------- apps/stores-internal/src/skip/route.ts | 42 +++++- apps/stores-internal/src/skip/types.ts | 38 +++++- yarn.lock | 1 + 9 files changed, 219 insertions(+), 105 deletions(-) diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index e5e2849438..312eaaf648 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -109,6 +109,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { chainStore, queriesStore, accountStore, + ethereumAccountStore, skipQueriesStore, uiConfigStore, keyRingStore, @@ -168,6 +169,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { chainStore, queriesStore, accountStore, + ethereumAccountStore, skipQueriesStore, inChainId, isInChainEVMOnly diff --git a/apps/hooks-internal/package.json b/apps/hooks-internal/package.json index c7cfc1dfaf..08396ec70c 100644 --- a/apps/hooks-internal/package.json +++ b/apps/hooks-internal/package.json @@ -16,6 +16,7 @@ "dependencies": { "@keplr-wallet/hooks": "0.12.160", "@keplr-wallet/stores": "0.12.160", + "@keplr-wallet/stores-eth": "0.12.160", "@keplr-wallet/stores-internal": "0.12.160", "@keplr-wallet/types": "0.12.160", "@keplr-wallet/unit": "0.12.160" diff --git a/apps/hooks-internal/src/ibc-swap/amount.ts b/apps/hooks-internal/src/ibc-swap/amount.ts index c39033ef7e..47445deade 100644 --- a/apps/hooks-internal/src/ibc-swap/amount.ts +++ b/apps/hooks-internal/src/ibc-swap/amount.ts @@ -18,6 +18,7 @@ import { SkipQueries, ObservableQueryIBCSwapInner, } from "@keplr-wallet/stores-internal"; +import { EthereumAccountStore } from "@keplr-wallet/stores-eth"; export class IBCSwapAmountConfig extends AmountConfig { @observable @@ -33,6 +34,7 @@ export class IBCSwapAmountConfig extends AmountConfig { protected readonly accountStore: IAccountStoreWithInjects< [CosmosAccount, CosmwasmAccount] >, + public readonly ethereumAccountStore: EthereumAccountStore, protected readonly skipQueries: SkipQueries, initialChainId: string, senderConfig: ISenderConfig, @@ -451,6 +453,7 @@ export class IBCSwapAmountConfig extends AmountConfig { ); tx.ui.overrideType("ibc-swap"); return tx; + } else if (msg.type === "evmTx") { } } @@ -476,70 +479,72 @@ export class IBCSwapAmountConfig extends AmountConfig { let key = ""; for (const msg of response.msgs) { - if ( - msg.multi_chain_msg.msg_type_url === - "/ibc.applications.transfer.v1.MsgTransfer" - ) { - const memo = JSON.parse(msg.multi_chain_msg.msg).memo; - if (memo) { - const obj = JSON.parse(memo); - const wasms: any = []; + if (msg.multi_chain_msg) { + if ( + msg.multi_chain_msg.msg_type_url === + "/ibc.applications.transfer.v1.MsgTransfer" + ) { + const memo = JSON.parse(msg.multi_chain_msg.msg).memo; + if (memo) { + const obj = JSON.parse(memo); + const wasms: any = []; - if (obj.wasm) { - wasms.push(obj.wasm); - } + if (obj.wasm) { + wasms.push(obj.wasm); + } - let forward = obj.forward; - if (forward) { - while (true) { - if (forward) { - if (forward.memo) { - const obj = JSON.parse(forward.memo); - if (obj.wasm) { - wasms.push(obj.wasm); + let forward = obj.forward; + if (forward) { + while (true) { + if (forward) { + if (forward.memo) { + const obj = JSON.parse(forward.memo); + if (obj.wasm) { + wasms.push(obj.wasm); + } } - } - - if (forward.wasm) { - wasms.push(forward.wasm); - } - if (forward.next) { - const obj = - typeof forward.next === "string" - ? JSON.parse(forward.next) - : forward.next; + if (forward.wasm) { + wasms.push(forward.wasm); + } - if (obj.forward) { - forward = obj.forward; + if (forward.next) { + const obj = + typeof forward.next === "string" + ? JSON.parse(forward.next) + : forward.next; + + if (obj.forward) { + forward = obj.forward; + } else { + forward = obj; + } } else { - forward = obj; + break; } } else { break; } - } else { - break; } } - } - for (const wasm of wasms) { - for (const operation of wasm.msg.swap_and_action.user_swap - .swap_exact_asset_in.operations) { - key += `/${operation.pool}/${operation.denom_in}/${operation.denom_out}`; + for (const wasm of wasms) { + for (const operation of wasm.msg.swap_and_action.user_swap + .swap_exact_asset_in.operations) { + key += `/${operation.pool}/${operation.denom_in}/${operation.denom_out}`; + } } } } - } - if ( - msg.multi_chain_msg.msg_type_url === - "/cosmwasm.wasm.v1.MsgExecuteContract" - ) { - const obj = JSON.parse(msg.multi_chain_msg.msg); - for (const operation of obj.msg.swap_and_action.user_swap - .swap_exact_asset_in.operations) { - key += `/${operation.pool}/${operation.denom_in}/${operation.denom_out}`; + if ( + msg.multi_chain_msg.msg_type_url === + "/cosmwasm.wasm.v1.MsgExecuteContract" + ) { + const obj = JSON.parse(msg.multi_chain_msg.msg); + for (const operation of obj.msg.swap_and_action.user_swap + .swap_exact_asset_in.operations) { + key += `/${operation.pool}/${operation.denom_in}/${operation.denom_out}`; + } } } } @@ -651,6 +656,7 @@ export const useIBCSwapAmountConfig = ( chainGetter: ChainGetter, queriesStore: IQueriesStore, accountStore: IAccountStoreWithInjects<[CosmosAccount, CosmwasmAccount]>, + ethereumAccountStore: EthereumAccountStore, skipQueries: SkipQueries, chainId: string, senderConfig: ISenderConfig, @@ -664,6 +670,7 @@ export const useIBCSwapAmountConfig = ( chainGetter, queriesStore, accountStore, + ethereumAccountStore, skipQueries, chainId, senderConfig, diff --git a/apps/hooks-internal/src/ibc-swap/use.ts b/apps/hooks-internal/src/ibc-swap/use.ts index e5a1443726..fda2d5cdc5 100644 --- a/apps/hooks-internal/src/ibc-swap/use.ts +++ b/apps/hooks-internal/src/ibc-swap/use.ts @@ -14,11 +14,13 @@ import { import { useIBCSwapAmountConfig } from "./amount"; import { SkipQueries } from "@keplr-wallet/stores-internal"; import { AppCurrency } from "@keplr-wallet/types"; +import { EthereumAccountStore } from "@keplr-wallet/stores-eth"; export const useIBCSwapConfig = ( chainGetter: ChainGetter, queriesStore: IQueriesStore, accountStore: IAccountStoreWithInjects<[CosmosAccount, CosmwasmAccount]>, + ethereumAccountStore: EthereumAccountStore, skipQueries: SkipQueries, chainId: string, sender: string, @@ -32,6 +34,7 @@ export const useIBCSwapConfig = ( chainGetter, queriesStore, accountStore, + ethereumAccountStore, skipQueries, chainId, senderConfig, diff --git a/apps/mobile/src/screen/ibc-swap/index.tsx b/apps/mobile/src/screen/ibc-swap/index.tsx index 9a38eaeca6..26881fb16b 100644 --- a/apps/mobile/src/screen/ibc-swap/index.tsx +++ b/apps/mobile/src/screen/ibc-swap/index.tsx @@ -49,6 +49,7 @@ export const IBCSwapScreen: FunctionComponent = observer(() => { chainStore, queriesStore, accountStore, + ethereumAccountStore, skipQueriesStore, uiConfigStore, priceStore, @@ -100,6 +101,7 @@ export const IBCSwapScreen: FunctionComponent = observer(() => { chainStore, queriesStore, accountStore, + ethereumAccountStore, skipQueriesStore, inChainId, accountStore.getAccount(inChainId).bech32Address, diff --git a/apps/stores-internal/src/skip/msgs-direct.ts b/apps/stores-internal/src/skip/msgs-direct.ts index 507cf96a4f..1aceed125e 100644 --- a/apps/stores-internal/src/skip/msgs-direct.ts +++ b/apps/stores-internal/src/skip/msgs-direct.ts @@ -20,6 +20,14 @@ const Schema = Joi.object({ msg: Joi.string().required(), msg_type_url: Joi.string().required(), }).unknown(true), + evm_tx: Joi.object({ + chain_id: Joi.string().required(), + data: Joi.string().required(), + required_erc20_approvals: Joi.array().items(Joi.string()).required(), + signer_address: Joi.string().required(), + to: Joi.string().required(), + value: Joi.string().required(), + }).unknown(true), }).unknown(true) ) .required(), @@ -67,6 +75,10 @@ export class ObservableQueryMsgsDirectInner extends ObservableQuery { - return new CoinPretty( - this.chainGetter - .getChain(msg.multi_chain_msg.chain_id) - .forceFindCurrency(fund.denom), - fund.amount - ); - }), - contract: chainMsg.contract, - msg: chainMsg.msg, + type: "evmTx", + chainId: `eip155:${msg.evm_tx.chain_id}`, }; - } else if ( - msg.multi_chain_msg.msg_type_url === - "/ibc.applications.transfer.v1.MsgTransfer" - ) { - if (msg.multi_chain_msg.path.length < 2) { + } + + if (msg.multi_chain_msg) { + if ( + msg.multi_chain_msg.msg_type_url !== + "/ibc.applications.transfer.v1.MsgTransfer" && + msg.multi_chain_msg.msg_type_url !== + "/cosmwasm.wasm.v1.MsgExecuteContract" + ) { return; } - return { - type: "MsgTransfer", - receiver: chainMsg.receiver, - sourcePort: chainMsg.source_port, - sourceChannel: chainMsg.source_channel, - counterpartyChainId: msg.multi_chain_msg.path[1], - timeoutTimestamp: chainMsg.timeout_timestamp, - token: new CoinPretty( - this.chainGetter - .getChain(msg.multi_chain_msg.chain_id) - .forceFindCurrency(chainMsg.token.denom), - chainMsg.token.amount - ), - memo: chainMsg.memo, - }; + const chainMsg = JSON.parse(msg.multi_chain_msg.msg); + if ( + msg.multi_chain_msg.msg_type_url === + "/cosmwasm.wasm.v1.MsgExecuteContract" + ) { + return { + type: "MsgExecuteContract", + funds: chainMsg.funds.map( + (fund: { denom: string; amount: string }) => { + return new CoinPretty( + this.chainGetter + .getChain(msg.multi_chain_msg!.chain_id) + .forceFindCurrency(fund.denom), + fund.amount + ); + } + ), + contract: chainMsg.contract, + msg: chainMsg.msg, + }; + } else if ( + msg.multi_chain_msg.msg_type_url === + "/ibc.applications.transfer.v1.MsgTransfer" + ) { + if (msg.multi_chain_msg.path.length < 2) { + return; + } + + return { + type: "MsgTransfer", + receiver: chainMsg.receiver, + sourcePort: chainMsg.source_port, + sourceChannel: chainMsg.source_channel, + counterpartyChainId: msg.multi_chain_msg.path[1], + timeoutTimestamp: chainMsg.timeout_timestamp, + token: new CoinPretty( + this.chainGetter + .getChain(msg.multi_chain_msg.chain_id) + .forceFindCurrency(chainMsg.token.denom), + chainMsg.token.amount + ), + memo: chainMsg.memo, + }; + } } throw new Error("Unknown error"); @@ -151,7 +174,12 @@ export class ObservableQueryMsgsDirectInner extends ObservableQuery({ }) .required() .unknown(true), + }).unknown(true), + Joi.object({ + go_fast_transfer: Joi.object({ + from_chain_id: Joi.string().required(), + to_chain_id: Joi.string().required(), + fee: Joi.object({ + fee_asset: Joi.object({ + denom: Joi.string().required(), + chain_id: Joi.string().required(), + is_cw20: Joi.boolean().required(), + is_evm: Joi.boolean().required(), + is_svm: Joi.boolean().required(), + symbol: Joi.string().required(), + decimals: Joi.number().required(), + }) + .required() + .unknown(true), + bps_fee: Joi.string().required(), + bps_fee_amount: Joi.string().required(), + bps_fee_usd: Joi.string().required(), + source_chain_fee_amount: Joi.string().required(), + source_chain_fee_usd: Joi.string().required(), + destination_chain_fee_amount: Joi.string().required(), + destination_chain_fee_usd: Joi.string().required(), + }) + .required() + .unknown(true), + denom_in: Joi.string().required(), + denom_out: Joi.string().required(), + source_domain: Joi.string().required(), + destination_domain: Joi.string().required(), + }) + .required() + .unknown(true), }).unknown(true) ) .required(), @@ -234,9 +268,9 @@ export class ObservableQueryRouteInner extends ObservableQuery { }, body: JSON.stringify({ amount_in: this.sourceAmount, - source_asset_denom: this.sourceDenom, + source_asset_denom: this.sourceDenom.replace("erc20:", ""), source_asset_chain_id: this.sourceChainId.replace("eip155:", ""), - dest_asset_denom: this.destDenom, + dest_asset_denom: this.destDenom.replace("erc20:", ""), dest_asset_chain_id: this.destChainId.replace("eip155:", ""), cumulative_affiliate_fee_bps: this.affiliateFeeBps.toString(), swap_venues: this.swapVenues.map((swapVenue) => ({ @@ -275,9 +309,9 @@ export class ObservableQueryRouteInner extends ObservableQuery { protected override getCacheKey(): string { return `${super.getCacheKey()}-${JSON.stringify({ amount_in: this.sourceAmount, - source_asset_denom: this.sourceDenom, + source_asset_denom: this.sourceDenom.replace("erc20:", ""), source_asset_chain_id: this.sourceChainId.replace("eip155:", ""), - dest_asset_denom: this.destDenom, + dest_asset_denom: this.destDenom.replace("erc20:", ""), dest_asset_chain_id: this.destChainId.replace("eip155:", ""), affiliateFeeBps: this.affiliateFeeBps, swap_venue: this.swapVenues, diff --git a/apps/stores-internal/src/skip/types.ts b/apps/stores-internal/src/skip/types.ts index 41fc0e88d6..50e9cbdf61 100644 --- a/apps/stores-internal/src/skip/types.ts +++ b/apps/stores-internal/src/skip/types.ts @@ -32,12 +32,20 @@ export interface AssetsResponse { export interface MsgsDirectResponse { msgs: { - multi_chain_msg: { + multi_chain_msg?: { chain_id: string; path: string[]; msg: string; msg_type_url: string; }; + evm_tx?: { + chain_id: string; + data: string; + required_erc20_approvals: string[]; + signer_address: string; + to: string; + value: string; + }; }[]; route: RouteResponse; } @@ -106,6 +114,34 @@ export interface RouteResponse { }; }; } + | { + go_fast_transfer: { + from_chain_id: string; + to_chain_id: string; + fee: { + fee_asset: { + denom: string; + chain_id: string; + is_cw20: boolean; + is_evm: boolean; + is_svm: boolean; + symbol: string; + decimals: number; + }; + bps_fee: string; + bps_fee_amount: string; + bps_fee_usd: string; + source_chain_fee_amount: string; + source_chain_fee_usd: string; + destination_chain_fee_amount: string; + destination_chain_fee_usd: string; + }; + denom_in: string; + denom_out: string; + source_domain: string; + destination_domain: string; + }; + } )[]; chain_ids: string[]; does_swap?: boolean; diff --git a/yarn.lock b/yarn.lock index 7339db691a..520be0f122 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8909,6 +8909,7 @@ __metadata: dependencies: "@keplr-wallet/hooks": 0.12.160 "@keplr-wallet/stores": 0.12.160 + "@keplr-wallet/stores-eth": 0.12.160 "@keplr-wallet/stores-internal": 0.12.160 "@keplr-wallet/types": 0.12.160 "@keplr-wallet/unit": 0.12.160 From 956676c9a0134a7e36af0d3163b2e7a84289b1ed Mon Sep 17 00:00:00 2001 From: delivan Date: Tue, 17 Dec 2024 17:33:23 +0900 Subject: [PATCH 06/43] Add axelar transfer support on skip api --- apps/stores-internal/src/skip/ibc-swap.ts | 10 ++++++--- apps/stores-internal/src/skip/route.ts | 25 ++++++++++++++++++++++ apps/stores-internal/src/skip/types.ts | 26 +++++++++++++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/apps/stores-internal/src/skip/ibc-swap.ts b/apps/stores-internal/src/skip/ibc-swap.ts index 8e89723061..ff8b5afc33 100644 --- a/apps/stores-internal/src/skip/ibc-swap.ts +++ b/apps/stores-internal/src/skip/ibc-swap.ts @@ -351,9 +351,8 @@ export class ObservableQueryIbcSwap extends HasMapStore({ }) .required() .unknown(true), + }).unknown(true), + Joi.object({ + axelar_transfer: Joi.object({ + from_chain: Joi.string().required(), + from_chain_id: Joi.string().required(), + to_chain: Joi.string().required(), + to_chain_id: Joi.string().required(), + asset: Joi.string().required(), + should_unwrap: Joi.boolean().required(), + denom_in: Joi.string().required(), + denom_out: Joi.string().required(), + fee_amount: Joi.string().required(), + usd_fee_amount: Joi.string().required(), + fee_asset: Joi.object({ + denom: Joi.string().required(), + chain_id: Joi.string().required(), + is_cw20: Joi.boolean().required(), + is_evm: Joi.boolean().required(), + is_svm: Joi.boolean().required(), + symbol: Joi.string().required(), + decimals: Joi.number().required(), + }).unknown(true), + bridge_id: Joi.string().required(), + smart_relay: Joi.boolean().required(), + }).unknown(true), }).unknown(true) ) .required(), diff --git a/apps/stores-internal/src/skip/types.ts b/apps/stores-internal/src/skip/types.ts index 50e9cbdf61..3f3fc7ba1c 100644 --- a/apps/stores-internal/src/skip/types.ts +++ b/apps/stores-internal/src/skip/types.ts @@ -142,6 +142,32 @@ export interface RouteResponse { destination_domain: string; }; } + | { + axelar_transfer: { + from_chain: string; + from_chain_id: string; + to_chain: string; + to_chain_id: string; + asset: string; + should_unwrap: boolean; + denom_in: string; + denom_out: string; + fee_amount: string; + usd_fee_amount: string; + fee_asset: { + denom: string; + chain_id: string; + is_cw20: boolean; + is_evm: boolean; + is_svm: boolean; + symbol: string; + name: string; + decimals: number; + }; + bridge_id: string; + smart_relay: boolean; + }; + } )[]; chain_ids: string[]; does_swap?: boolean; From ea619e1152e8d8d65f8f672a5b164f7a5a19dab8 Mon Sep 17 00:00:00 2001 From: delivan Date: Tue, 17 Dec 2024 23:37:43 +0900 Subject: [PATCH 07/43] Make evm tx from `msgs_direct` skip api --- apps/extension/src/pages/ibc-swap/index.tsx | 505 +++++++++++-------- apps/hooks-internal/src/ibc-swap/amount.ts | 14 +- apps/stores-internal/src/skip/msgs-direct.ts | 25 +- packages/stores-eth/src/account/base.ts | 39 +- packages/stores-eth/src/index.ts | 1 + packages/stores-eth/src/types.ts | 3 + 6 files changed, 344 insertions(+), 243 deletions(-) create mode 100644 packages/stores-eth/src/types.ts diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index 312eaaf648..279ce33b79 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -52,6 +52,8 @@ import { useEffectOnce } from "../../hooks/use-effect-once"; import { amountToAmbiguousAverage, amountToAmbiguousString } from "../../utils"; import { Button } from "../../components/button"; import { TextButtonProps } from "../../components/button-text"; +import { UnsignedEVMTransaction } from "@keplr-wallet/stores-eth"; +import { EthTxStatus } from "@keplr-wallet/types"; const TextButtonStyles = { Container: styled.div` @@ -377,7 +379,17 @@ export const IBCSwapPage: FunctionComponent = observer(() => { throw new Error("Not ready to simulate tx"); } - return tx; + if ("send" in tx) { + return tx; + } else { + const ethereumAccount = ethereumAccountStore.getAccount( + ibcSwapConfigs.amountConfig.chainId + ); + const sender = ibcSwapConfigs.senderConfig.sender; + return { + simulate: () => ethereumAccount.simulateGas(sender, tx), + }; + } } ); @@ -657,7 +669,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { if (!interactionBlocked) { setIsTxLoading(true); - let tx: MakeTxResponse; + let tx: MakeTxResponse | UnsignedEVMTransaction; const queryRoute = ibcSwapConfigs.amountConfig .getQueryIBCSwap()! @@ -773,242 +785,297 @@ export const IBCSwapPage: FunctionComponent = observer(() => { setCalculatingTxError(undefined); try { - await tx.send( - ibcSwapConfigs.feeConfig.toStdFee(), - ibcSwapConfigs.memoConfig.memo, - { - preferNoSetFee: true, - preferNoSetMemo: false, - - sendTx: async (chainId, tx, mode) => { - if (ibcSwapConfigs.amountConfig.type === "transfer") { - const msg: Message = new SendTxAndRecordMsg( - "ibc-swap/ibc-transfer", - chainId, - outChainId, - tx, - mode, - false, - ibcSwapConfigs.senderConfig.sender, - accountStore.getAccount(outChainId).bech32Address, - ibcSwapConfigs.amountConfig.amount.map((amount) => { - return { - amount: DecUtils.getTenExponentN( - amount.currency.coinDecimals + if ("send" in tx) { + await tx.send( + ibcSwapConfigs.feeConfig.toStdFee(), + ibcSwapConfigs.memoConfig.memo, + { + preferNoSetFee: true, + preferNoSetMemo: false, + + sendTx: async (chainId, tx, mode) => { + if (ibcSwapConfigs.amountConfig.type === "transfer") { + const msg: Message = new SendTxAndRecordMsg( + "ibc-swap/ibc-transfer", + chainId, + outChainId, + tx, + mode, + false, + ibcSwapConfigs.senderConfig.sender, + accountStore.getAccount(outChainId).bech32Address, + ibcSwapConfigs.amountConfig.amount.map((amount) => { + return { + amount: DecUtils.getTenExponentN( + amount.currency.coinDecimals + ) + .mul(amount.toDec()) + .toString(), + denom: amount.currency.coinMinimalDenom, + }; + }), + ibcSwapConfigs.memoConfig.memo, + true + ).withIBCPacketForwarding(channels, { + currencies: chainStore.getChain(chainId).currencies, + }); + return await new InExtensionMessageRequester().sendMessage( + BACKGROUND_PORT, + msg + ); + } else { + const msg = new SendTxAndRecordWithIBCSwapMsg( + "amount-in", + chainId, + outChainId, + tx, + channels, + { + chainId: outChainId, + denom: outCurrency.coinMinimalDenom, + }, + swapChannelIndex, + swapReceiver, + mode, + false, + ibcSwapConfigs.senderConfig.sender, + ibcSwapConfigs.amountConfig.amount.map((amount) => { + return { + amount: DecUtils.getTenExponentN( + amount.currency.coinDecimals + ) + .mul(amount.toDec()) + .toString(), + denom: amount.currency.coinMinimalDenom, + }; + }), + ibcSwapConfigs.memoConfig.memo, + { + currencies: + chainStore.getChain(outChainId).currencies, + }, + true + ); + + return await new InExtensionMessageRequester().sendMessage( + BACKGROUND_PORT, + msg + ); + } + }, + }, + { + onBroadcasted: () => { + if ( + !chainStore.isEnabledChain( + ibcSwapConfigs.amountConfig.outChainId + ) + ) { + chainStore.enableChainInfoInUI( + ibcSwapConfigs.amountConfig.outChainId + ); + + if (keyRingStore.selectedKeyInfo) { + const outChainInfo = chainStore.getChain( + ibcSwapConfigs.amountConfig.outChainId + ); + if ( + keyRingStore.needKeyCoinTypeFinalize( + keyRingStore.selectedKeyInfo.id, + outChainInfo ) - .mul(amount.toDec()) - .toString(), - denom: amount.currency.coinMinimalDenom, - }; - }), - ibcSwapConfigs.memoConfig.memo, - true - ).withIBCPacketForwarding(channels, { - currencies: chainStore.getChain(chainId).currencies, - }); - return await new InExtensionMessageRequester().sendMessage( - BACKGROUND_PORT, - msg + ) { + keyRingStore.finalizeKeyCoinType( + keyRingStore.selectedKeyInfo.id, + outChainInfo.chainId, + outChainInfo.bip44.coinType + ); + } + } + } + + const params: Record< + string, + | number + | string + | boolean + | number[] + | string[] + | undefined + > = { + inChainId: inChainId, + inChainIdentifier: + ChainIdHelper.parse(inChainId).identifier, + inCurrencyMinimalDenom: inCurrency.coinMinimalDenom, + inCurrencyDenom: inCurrency.coinDenom, + inCurrencyCommonMinimalDenom: inCurrency.coinMinimalDenom, + inCurrencyCommonDenom: inCurrency.coinDenom, + outChainId: outChainId, + outChainIdentifier: + ChainIdHelper.parse(outChainId).identifier, + outCurrencyMinimalDenom: outCurrency.coinMinimalDenom, + outCurrencyDenom: outCurrency.coinDenom, + outCurrencyCommonMinimalDenom: + outCurrency.coinMinimalDenom, + outCurrencyCommonDenom: outCurrency.coinDenom, + swapType: ibcSwapConfigs.amountConfig.type, + }; + if ( + "originChainId" in inCurrency && + inCurrency.originChainId + ) { + const originChainId = inCurrency.originChainId; + params["inOriginChainId"] = originChainId; + params["inOriginChainIdentifier"] = + ChainIdHelper.parse(originChainId).identifier; + + params["inToDifferentChain"] = true; + } + if ( + "originCurrency" in inCurrency && + inCurrency.originCurrency + ) { + params["inCurrencyCommonMinimalDenom"] = + inCurrency.originCurrency.coinMinimalDenom; + params["inCurrencyCommonDenom"] = + inCurrency.originCurrency.coinDenom; + } + if ( + "originChainId" in outCurrency && + outCurrency.originChainId + ) { + const originChainId = outCurrency.originChainId; + params["outOriginChainId"] = originChainId; + params["outOriginChainIdentifier"] = + ChainIdHelper.parse(originChainId).identifier; + + params["outToDifferentChain"] = true; + } + if ( + "originCurrency" in outCurrency && + outCurrency.originCurrency + ) { + params["outCurrencyCommonMinimalDenom"] = + outCurrency.originCurrency.coinMinimalDenom; + params["outCurrencyCommonDenom"] = + outCurrency.originCurrency.coinDenom; + } + params["inRange"] = amountToAmbiguousString( + ibcSwapConfigs.amountConfig.amount[0] ); - } else { - const msg = new SendTxAndRecordWithIBCSwapMsg( - "amount-in", - chainId, - outChainId, - tx, - channels, - { - chainId: outChainId, - denom: outCurrency.coinMinimalDenom, - }, - swapChannelIndex, - swapReceiver, - mode, - false, - ibcSwapConfigs.senderConfig.sender, - ibcSwapConfigs.amountConfig.amount.map((amount) => { - return { - amount: DecUtils.getTenExponentN( - amount.currency.coinDecimals - ) - .mul(amount.toDec()) - .toString(), - denom: amount.currency.coinMinimalDenom, - }; - }), - ibcSwapConfigs.memoConfig.memo, - { - currencies: chainStore.getChain(outChainId).currencies, - }, - true + params["outRange"] = amountToAmbiguousString( + ibcSwapConfigs.amountConfig.outAmount ); - return await new InExtensionMessageRequester().sendMessage( - BACKGROUND_PORT, - msg + // UI 상에서 in currency의 가격은 in input에서 표시되고 + // out currency의 가격은 swap fee에서 표시된다. + // price store에서 usd는 무조건 쿼리하므로 in, out currency의 usd는 보장된다. + const inCurrencyPrice = priceStore.calculatePrice( + ibcSwapConfigs.amountConfig.amount[0], + "usd" ); - } - }, - }, - { - onBroadcasted: () => { - if ( - !chainStore.isEnabledChain( - ibcSwapConfigs.amountConfig.outChainId - ) - ) { - chainStore.enableChainInfoInUI( - ibcSwapConfigs.amountConfig.outChainId + if (inCurrencyPrice) { + params["inFiatRange"] = + amountToAmbiguousString(inCurrencyPrice); + params["inFiatAvg"] = + amountToAmbiguousAverage(inCurrencyPrice); + } + const outCurrencyPrice = priceStore.calculatePrice( + ibcSwapConfigs.amountConfig.outAmount, + "usd" ); + if (outCurrencyPrice) { + params["outFiatRange"] = + amountToAmbiguousString(outCurrencyPrice); + params["outFiatAvg"] = + amountToAmbiguousAverage(outCurrencyPrice); + } - if (keyRingStore.selectedKeyInfo) { - const outChainInfo = chainStore.getChain( - ibcSwapConfigs.amountConfig.outChainId + new InExtensionMessageRequester().sendMessage( + BACKGROUND_PORT, + new LogAnalyticsEventMsg("ibc_swap", params) + ); + + analyticsStore.logEvent("swap_occurred", { + in_chain_id: inChainId, + in_chain_identifier: + ChainIdHelper.parse(inChainId).identifier, + in_currency_minimal_denom: inCurrency.coinMinimalDenom, + in_currency_denom: inCurrency.coinDenom, + out_chain_id: outChainId, + out_chain_identifier: + ChainIdHelper.parse(outChainId).identifier, + out_currency_minimal_denom: outCurrency.coinMinimalDenom, + out_currency_denom: outCurrency.coinDenom, + }); + }, + onFulfill: (tx: any) => { + if (tx.code != null && tx.code !== 0) { + console.log(tx.log ?? tx.raw_log); + notification.show( + "failed", + intl.formatMessage({ id: "error.transaction-failed" }), + "" ); + return; + } + notification.show( + "success", + intl.formatMessage({ + id: "notification.transaction-success", + }), + "" + ); + }, + } + ); + } else { + const ethereumAccount = ethereumAccountStore.getAccount( + ibcSwapConfigs.amountConfig.chainId + ); + const sender = ibcSwapConfigs.senderConfig.sender; + const queryBalances = queriesStore.get( + ibcSwapConfigs.amountConfig.chainId + ).queryBalances; + + ethereumAccount.setIsSendingTx(true); + await ethereumAccount.sendEthereumTx(sender, tx, { + onFulfill: (txReceipt) => { + queryBalances + .getQueryEthereumHexAddress(sender) + .balances.forEach((balance) => { if ( - keyRingStore.needKeyCoinTypeFinalize( - keyRingStore.selectedKeyInfo.id, - outChainInfo + balance.currency.coinMinimalDenom === + ibcSwapConfigs.amountConfig.currency + .coinMinimalDenom || + ibcSwapConfigs.feeConfig.fees.some( + (fee) => + fee.currency.coinMinimalDenom === + balance.currency.coinMinimalDenom ) ) { - keyRingStore.finalizeKeyCoinType( - keyRingStore.selectedKeyInfo.id, - outChainInfo.chainId, - outChainInfo.bip44.coinType - ); + balance.fetch(); } - } - } - - const params: Record< - string, - number | string | boolean | number[] | string[] | undefined - > = { - inChainId: inChainId, - inChainIdentifier: - ChainIdHelper.parse(inChainId).identifier, - inCurrencyMinimalDenom: inCurrency.coinMinimalDenom, - inCurrencyDenom: inCurrency.coinDenom, - inCurrencyCommonMinimalDenom: inCurrency.coinMinimalDenom, - inCurrencyCommonDenom: inCurrency.coinDenom, - outChainId: outChainId, - outChainIdentifier: - ChainIdHelper.parse(outChainId).identifier, - outCurrencyMinimalDenom: outCurrency.coinMinimalDenom, - outCurrencyDenom: outCurrency.coinDenom, - outCurrencyCommonMinimalDenom: outCurrency.coinMinimalDenom, - outCurrencyCommonDenom: outCurrency.coinDenom, - swapType: ibcSwapConfigs.amountConfig.type, - }; - if ( - "originChainId" in inCurrency && - inCurrency.originChainId - ) { - const originChainId = inCurrency.originChainId; - params["inOriginChainId"] = originChainId; - params["inOriginChainIdentifier"] = - ChainIdHelper.parse(originChainId).identifier; - - params["inToDifferentChain"] = true; - } - if ( - "originCurrency" in inCurrency && - inCurrency.originCurrency - ) { - params["inCurrencyCommonMinimalDenom"] = - inCurrency.originCurrency.coinMinimalDenom; - params["inCurrencyCommonDenom"] = - inCurrency.originCurrency.coinDenom; - } - if ( - "originChainId" in outCurrency && - outCurrency.originChainId - ) { - const originChainId = outCurrency.originChainId; - params["outOriginChainId"] = originChainId; - params["outOriginChainIdentifier"] = - ChainIdHelper.parse(originChainId).identifier; - - params["outToDifferentChain"] = true; - } - if ( - "originCurrency" in outCurrency && - outCurrency.originCurrency - ) { - params["outCurrencyCommonMinimalDenom"] = - outCurrency.originCurrency.coinMinimalDenom; - params["outCurrencyCommonDenom"] = - outCurrency.originCurrency.coinDenom; - } - params["inRange"] = amountToAmbiguousString( - ibcSwapConfigs.amountConfig.amount[0] - ); - params["outRange"] = amountToAmbiguousString( - ibcSwapConfigs.amountConfig.outAmount - ); - - // UI 상에서 in currency의 가격은 in input에서 표시되고 - // out currency의 가격은 swap fee에서 표시된다. - // price store에서 usd는 무조건 쿼리하므로 in, out currency의 usd는 보장된다. - const inCurrencyPrice = priceStore.calculatePrice( - ibcSwapConfigs.amountConfig.amount[0], - "usd" - ); - if (inCurrencyPrice) { - params["inFiatRange"] = - amountToAmbiguousString(inCurrencyPrice); - params["inFiatAvg"] = - amountToAmbiguousAverage(inCurrencyPrice); - } - const outCurrencyPrice = priceStore.calculatePrice( - ibcSwapConfigs.amountConfig.outAmount, - "usd" - ); - if (outCurrencyPrice) { - params["outFiatRange"] = - amountToAmbiguousString(outCurrencyPrice); - params["outFiatAvg"] = - amountToAmbiguousAverage(outCurrencyPrice); - } - - new InExtensionMessageRequester().sendMessage( - BACKGROUND_PORT, - new LogAnalyticsEventMsg("ibc_swap", params) - ); - - analyticsStore.logEvent("swap_occurred", { - in_chain_id: inChainId, - in_chain_identifier: - ChainIdHelper.parse(inChainId).identifier, - in_currency_minimal_denom: inCurrency.coinMinimalDenom, - in_currency_denom: inCurrency.coinDenom, - out_chain_id: outChainId, - out_chain_identifier: - ChainIdHelper.parse(outChainId).identifier, - out_currency_minimal_denom: outCurrency.coinMinimalDenom, - out_currency_denom: outCurrency.coinDenom, - }); - }, - onFulfill: (tx: any) => { - if (tx.code != null && tx.code !== 0) { - console.log(tx.log ?? tx.raw_log); + }); + if (txReceipt.status === EthTxStatus.Success) { + notification.show( + "success", + intl.formatMessage({ + id: "notification.transaction-success", + }), + "" + ); + } else { notification.show( "failed", intl.formatMessage({ id: "error.transaction-failed" }), "" ); - return; } - notification.show( - "success", - intl.formatMessage({ - id: "notification.transaction-success", - }), - "" - ); }, - } - ); + }); + ethereumAccount.setIsSendingTx(false); + } navigate("/", { replace: true, diff --git a/apps/hooks-internal/src/ibc-swap/amount.ts b/apps/hooks-internal/src/ibc-swap/amount.ts index 47445deade..d1ab6d7481 100644 --- a/apps/hooks-internal/src/ibc-swap/amount.ts +++ b/apps/hooks-internal/src/ibc-swap/amount.ts @@ -18,7 +18,10 @@ import { SkipQueries, ObservableQueryIBCSwapInner, } from "@keplr-wallet/stores-internal"; -import { EthereumAccountStore } from "@keplr-wallet/stores-eth"; +import { + EthereumAccountStore, + UnsignedEVMTransaction, +} from "@keplr-wallet/stores-eth"; export class IBCSwapAmountConfig extends AmountConfig { @observable @@ -140,7 +143,7 @@ export class IBCSwapAmountConfig extends AmountConfig { slippageTolerancePercent: number, affiliateFeeReceiver: string, priorOutAmount?: Int - ): Promise { + ): Promise { const queryIBCSwap = this.getQueryIBCSwap(); if (!queryIBCSwap) { throw new Error("Query IBC Swap is not initialized"); @@ -199,7 +202,7 @@ export class IBCSwapAmountConfig extends AmountConfig { ) { throw new Error("Destination account is not set"); } - chainIdsToAddresses[destinationChainId.replace("eip155", "")] = + chainIdsToAddresses[destinationChainId.replace("eip155:", "")] = isDestinationChainEVMOnly ? destinationAccount.ethereumHexAddress : destinationAccount.bech32Address; @@ -309,7 +312,7 @@ export class IBCSwapAmountConfig extends AmountConfig { getTxIfReady( slippageTolerancePercent: number, affiliateFeeReceiver: string - ): MakeTxResponse | undefined { + ): MakeTxResponse | UnsignedEVMTransaction | undefined { if (!this.currency) { return; } @@ -454,6 +457,9 @@ export class IBCSwapAmountConfig extends AmountConfig { tx.ui.overrideType("ibc-swap"); return tx; } else if (msg.type === "evmTx") { + const ethereumAccount = this.ethereumAccountStore.getAccount(msg.chainId); + const tx = ethereumAccount.makeTx(msg.to, msg.value, msg.data); + return tx; } } diff --git a/apps/stores-internal/src/skip/msgs-direct.ts b/apps/stores-internal/src/skip/msgs-direct.ts index 1aceed125e..c3e449bb04 100644 --- a/apps/stores-internal/src/skip/msgs-direct.ts +++ b/apps/stores-internal/src/skip/msgs-direct.ts @@ -23,7 +23,15 @@ const Schema = Joi.object({ evm_tx: Joi.object({ chain_id: Joi.string().required(), data: Joi.string().required(), - required_erc20_approvals: Joi.array().items(Joi.string()).required(), + required_erc20_approvals: Joi.array() + .items( + Joi.object({ + amount: Joi.string().required(), + spender: Joi.string().required(), + token_contract: Joi.string().required(), + }).unknown(true) + ) + .required(), signer_address: Joi.string().required(), to: Joi.string().required(), value: Joi.string().required(), @@ -78,6 +86,9 @@ export class ObservableQueryMsgsDirectInner extends ObservableQuery Date: Wed, 18 Dec 2024 11:05:37 +0900 Subject: [PATCH 08/43] Add evm tx fee calulation on swap page --- .../components/swap-fee-info/index.tsx | 25 +++- apps/extension/src/pages/ibc-swap/index.tsx | 110 ++++++++++++------ 2 files changed, 96 insertions(+), 39 deletions(-) diff --git a/apps/extension/src/pages/ibc-swap/components/swap-fee-info/index.tsx b/apps/extension/src/pages/ibc-swap/components/swap-fee-info/index.tsx index da36ee035f..c6dd490363 100644 --- a/apps/extension/src/pages/ibc-swap/components/swap-fee-info/index.tsx +++ b/apps/extension/src/pages/ibc-swap/components/swap-fee-info/index.tsx @@ -29,8 +29,16 @@ export const SwapFeeInfo: FunctionComponent<{ gasConfig: IGasConfig; feeConfig: IFeeConfig; gasSimulator: IGasSimulator; + isForEVMTx?: boolean; }> = observer( - ({ senderConfig, amountConfig, gasConfig, feeConfig, gasSimulator }) => { + ({ + senderConfig, + amountConfig, + gasConfig, + feeConfig, + gasSimulator, + isForEVMTx, + }) => { const { queriesStore, chainStore, priceStore, uiConfigStore } = useStore(); const theme = useTheme(); @@ -213,6 +221,8 @@ export const SwapFeeInfo: FunctionComponent<{ } })(); + const isShowingEstimatedFee = isForEVMTx && !!gasSimulator?.gasEstimated; + return ( {fee + .quo( + new Dec( + isShowingEstimatedFee ? gasConfig?.gas || 1 : 1 + ) + ) + .mul( + new Dec( + isShowingEstimatedFee + ? gasSimulator?.gasEstimated || 1 + : 1 + ) + ) .maxDecimals(6) .trim(true) .shrink(true) @@ -506,6 +528,7 @@ export const SwapFeeInfo: FunctionComponent<{ feeConfig={feeConfig} gasConfig={gasConfig} gasSimulator={gasSimulator} + isForEVMTx={isForEVMTx} /> diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index 279ce33b79..8f03d6f08f 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -1033,47 +1033,80 @@ export const IBCSwapPage: FunctionComponent = observer(() => { const ethereumAccount = ethereumAccountStore.getAccount( ibcSwapConfigs.amountConfig.chainId ); + ethereumAccount.setIsSendingTx(true); + + const { maxFeePerGas, maxPriorityFeePerGas, gasPrice } = + ibcSwapConfigs.feeConfig.getEIP1559TxFees( + ibcSwapConfigs.feeConfig.type + ); + const feeObject = + maxFeePerGas && maxPriorityFeePerGas + ? { + maxFeePerGas: `0x${BigInt( + maxFeePerGas.truncate().toString() + ).toString(16)}`, + maxPriorityFeePerGas: `0x${BigInt( + maxPriorityFeePerGas.truncate().toString() + ).toString(16)}`, + gasLimit: `0x${ibcSwapConfigs.gasConfig.gas.toString( + 16 + )}`, + } + : { + gasPrice: `0x${BigInt( + gasPrice.truncate().toString() + ).toString(16)}`, + gasLimit: `0x${ibcSwapConfigs.gasConfig.gas.toString( + 16 + )}`, + }; const sender = ibcSwapConfigs.senderConfig.sender; - const queryBalances = queriesStore.get( - ibcSwapConfigs.amountConfig.chainId - ).queryBalances; - ethereumAccount.setIsSendingTx(true); - await ethereumAccount.sendEthereumTx(sender, tx, { - onFulfill: (txReceipt) => { - queryBalances - .getQueryEthereumHexAddress(sender) - .balances.forEach((balance) => { - if ( - balance.currency.coinMinimalDenom === - ibcSwapConfigs.amountConfig.currency - .coinMinimalDenom || - ibcSwapConfigs.feeConfig.fees.some( - (fee) => - fee.currency.coinMinimalDenom === - balance.currency.coinMinimalDenom - ) - ) { - balance.fetch(); - } - }); - if (txReceipt.status === EthTxStatus.Success) { - notification.show( - "success", - intl.formatMessage({ - id: "notification.transaction-success", - }), - "" - ); - } else { - notification.show( - "failed", - intl.formatMessage({ id: "error.transaction-failed" }), - "" - ); - } + await ethereumAccount.sendEthereumTx( + sender, + { + ...tx, + ...feeObject, }, - }); + { + onFulfill: (txReceipt) => { + const queryBalances = queriesStore.get( + ibcSwapConfigs.amountConfig.chainId + ).queryBalances; + queryBalances + .getQueryEthereumHexAddress(sender) + .balances.forEach((balance) => { + if ( + balance.currency.coinMinimalDenom === + ibcSwapConfigs.amountConfig.currency + .coinMinimalDenom || + ibcSwapConfigs.feeConfig.fees.some( + (fee) => + fee.currency.coinMinimalDenom === + balance.currency.coinMinimalDenom + ) + ) { + balance.fetch(); + } + }); + if (txReceipt.status === EthTxStatus.Success) { + notification.show( + "success", + intl.formatMessage({ + id: "notification.transaction-success", + }), + "" + ); + } else { + notification.show( + "failed", + intl.formatMessage({ id: "error.transaction-failed" }), + "" + ); + } + }, + } + ); ethereumAccount.setIsSendingTx(false); } @@ -1274,6 +1307,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { gasConfig={ibcSwapConfigs.gasConfig} feeConfig={ibcSwapConfigs.feeConfig} gasSimulator={gasSimulator} + isForEVMTx={isInChainEVMOnly} /> Date: Wed, 18 Dec 2024 11:34:38 +0900 Subject: [PATCH 09/43] Add hyperlane transfer support on skip api --- apps/stores-internal/src/skip/route.ts | 30 ++++++++++++++++++++++---- apps/stores-internal/src/skip/types.ts | 22 +++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/apps/stores-internal/src/skip/route.ts b/apps/stores-internal/src/skip/route.ts index 3f70150854..dd82ae1a83 100644 --- a/apps/stores-internal/src/skip/route.ts +++ b/apps/stores-internal/src/skip/route.ts @@ -157,6 +157,28 @@ const Schema = Joi.object({ bridge_id: Joi.string().required(), smart_relay: Joi.boolean().required(), }).unknown(true), + }).unknown(true), + Joi.object({ + hyperlane_transfer: Joi.object({ + from_chain_id: Joi.string().required(), + to_chain_id: Joi.string().required(), + denom_in: Joi.string().required(), + denom_out: Joi.string().required(), + hyperlane_contract_address: Joi.string().required(), + fee_amount: Joi.string().required(), + usd_fee_amount: Joi.string().required(), + fee_asset: Joi.object({ + denom: Joi.string().required(), + chain_id: Joi.string().required(), + is_cw20: Joi.boolean().required(), + is_evm: Joi.boolean().required(), + is_svm: Joi.boolean().required(), + symbol: Joi.string().required(), + decimals: Joi.number().required(), + }).unknown(true), + bridge_id: Joi.string().required(), + smart_relay: Joi.boolean().required(), + }).unknown(true), }).unknown(true) ) .required(), @@ -334,10 +356,10 @@ export class ObservableQueryRouteInner extends ObservableQuery { protected override getCacheKey(): string { return `${super.getCacheKey()}-${JSON.stringify({ amount_in: this.sourceAmount, - source_asset_denom: this.sourceDenom.replace("erc20:", ""), - source_asset_chain_id: this.sourceChainId.replace("eip155:", ""), - dest_asset_denom: this.destDenom.replace("erc20:", ""), - dest_asset_chain_id: this.destChainId.replace("eip155:", ""), + source_asset_denom: this.sourceDenom, + source_asset_chain_id: this.sourceChainId, + dest_asset_denom: this.destDenom, + dest_asset_chain_id: this.destChainId, affiliateFeeBps: this.affiliateFeeBps, swap_venue: this.swapVenues, })}`; diff --git a/apps/stores-internal/src/skip/types.ts b/apps/stores-internal/src/skip/types.ts index 3f3fc7ba1c..a33aabd4a1 100644 --- a/apps/stores-internal/src/skip/types.ts +++ b/apps/stores-internal/src/skip/types.ts @@ -168,6 +168,28 @@ export interface RouteResponse { smart_relay: boolean; }; } + | { + hyperlane_transfer: { + from_chain_id: string; + to_chain_id: string; + denom_in: string; + denom_out: string; + hyperlane_contract_address: string; + fee_amount: string; + usd_fee_amount: string; + fee_asset: { + denom: string; + chain_id: string; + is_cw20: boolean; + is_evm: boolean; + is_svm: boolean; + symbol: string; + decimals: number; + }; + bridge_id: string; + smart_relay: boolean; + }; + } )[]; chain_ids: string[]; does_swap?: boolean; From 3527e043aac98ac1fae3e1e83c29732472b43dd3 Mon Sep 17 00:00:00 2001 From: delivan Date: Wed, 18 Dec 2024 12:48:57 +0900 Subject: [PATCH 10/43] Add comment --- apps/extension/src/pages/ibc-swap/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index 8f03d6f08f..27a7acb900 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -1069,6 +1069,9 @@ export const IBCSwapPage: FunctionComponent = observer(() => { ...feeObject, }, { + onBroadcasted: () => { + // TODO: Add history + }, onFulfill: (txReceipt) => { const queryBalances = queriesStore.get( ibcSwapConfigs.amountConfig.chainId From 5e30bc5ab93c29f057d9909588d05fa92cf55452 Mon Sep 17 00:00:00 2001 From: delivan Date: Thu, 19 Dec 2024 17:03:10 +0900 Subject: [PATCH 11/43] Fix some from review --- .../src/pages/ibc-swap/components/swap-fee-info/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/extension/src/pages/ibc-swap/components/swap-fee-info/index.tsx b/apps/extension/src/pages/ibc-swap/components/swap-fee-info/index.tsx index c6dd490363..82710aab7a 100644 --- a/apps/extension/src/pages/ibc-swap/components/swap-fee-info/index.tsx +++ b/apps/extension/src/pages/ibc-swap/components/swap-fee-info/index.tsx @@ -221,7 +221,7 @@ export const SwapFeeInfo: FunctionComponent<{ } })(); - const isShowingEstimatedFee = isForEVMTx && !!gasSimulator?.gasEstimated; + const isShowingEstimatedFee = isForEVMTx && !!gasSimulator.gasEstimated; return ( @@ -386,13 +386,13 @@ export const SwapFeeInfo: FunctionComponent<{ {fee .quo( new Dec( - isShowingEstimatedFee ? gasConfig?.gas || 1 : 1 + isShowingEstimatedFee ? gasConfig.gas || 1 : 1 ) ) .mul( new Dec( isShowingEstimatedFee - ? gasSimulator?.gasEstimated || 1 + ? gasSimulator.gasEstimated || 1 : 1 ) ) From c9f034408171cc95554168a9b254f1216ad1e394 Mon Sep 17 00:00:00 2001 From: delivan Date: Thu, 19 Dec 2024 17:12:19 +0900 Subject: [PATCH 12/43] Implement erc20 approval logic on swap --- apps/extension/src/pages/ibc-swap/index.tsx | 178 ++++++++++++++++-- .../extension/src/pages/send/amount/index.tsx | 38 +--- apps/hooks-internal/src/ibc-swap/amount.ts | 11 +- apps/stores-internal/src/skip/msgs-direct.ts | 12 ++ apps/stores-internal/src/skip/types.ts | 6 +- packages/stores-eth/src/account/base.ts | 22 ++- packages/stores-eth/src/types.ts | 9 + 7 files changed, 218 insertions(+), 58 deletions(-) diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index 27a7acb900..1e03697248 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -52,7 +52,7 @@ import { useEffectOnce } from "../../hooks/use-effect-once"; import { amountToAmbiguousAverage, amountToAmbiguousString } from "../../utils"; import { Button } from "../../components/button"; import { TextButtonProps } from "../../components/button-text"; -import { UnsignedEVMTransaction } from "@keplr-wallet/stores-eth"; +import { UnsignedEVMTransactionWithErc20Approvals } from "@keplr-wallet/stores-eth"; import { EthTxStatus } from "@keplr-wallet/types"; const TextButtonStyles = { @@ -314,10 +314,11 @@ export const IBCSwapPage: FunctionComponent = observer(() => { // 이 가정에 따라서 첫로드시에 gas를 restore하기 위해서 트랜잭션을 보내는 체인에서 swap 할 경우 // 일단 swap-1로 설정한다. if ( - queryRoute.response.data.swap_venue && + queryRoute.response.data.swap_venues && ibcSwapConfigs.amountConfig.chainInfo.chainIdentifier === - chainStore.getChain(queryRoute.response.data.swap_venue.chain_id) - .chainIdentifier + chainStore.getChain( + queryRoute.response.data.swap_venues[0].chain_id + ).chainIdentifier ) { type = `swap-1`; } @@ -329,6 +330,26 @@ export const IBCSwapPage: FunctionComponent = observer(() => { type = `swap-${firstOperation.swap.swap_in.swap_operations.length}`; } } + + if ("axelar_transfer" in firstOperation) { + type = "axelar_transfer"; + } + + if ("cctp_transfer" in firstOperation) { + type = "cctp_transfer"; + } + + if ("go_fast_transfer" in firstOperation) { + type = "go_fast_transfer"; + } + + if ("hyperlane_transfer" in firstOperation) { + type = "hyperlane_transfer"; + } + + if ("evm_swap" in firstOperation) { + type = "evm_swap"; + } } } @@ -375,6 +396,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { // 코스모스 스왑은 스왑베뉴가 무조건 하나라고 해서 일단 처음걸 쓰기로 한다. swapFeeBpsReceiver[0] ); + if (!tx) { throw new Error("Not ready to simulate tx"); } @@ -386,6 +408,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { ibcSwapConfigs.amountConfig.chainId ); const sender = ibcSwapConfigs.senderConfig.sender; + return { simulate: () => ethereumAccount.simulateGas(sender, tx), }; @@ -669,7 +692,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { if (!interactionBlocked) { setIsTxLoading(true); - let tx: MakeTxResponse | UnsignedEVMTransaction; + let tx: MakeTxResponse | UnsignedEVMTransactionWithErc20Approvals; const queryRoute = ibcSwapConfigs.amountConfig .getQueryIBCSwap()! @@ -1029,12 +1052,14 @@ export const IBCSwapPage: FunctionComponent = observer(() => { }, } ); + + navigate("/", { + replace: true, + }); } else { const ethereumAccount = ethereumAccountStore.getAccount( ibcSwapConfigs.amountConfig.chainId ); - ethereumAccount.setIsSendingTx(true); - const { maxFeePerGas, maxPriorityFeePerGas, gasPrice } = ibcSwapConfigs.feeConfig.getEIP1559TxFees( ibcSwapConfigs.feeConfig.type @@ -1042,6 +1067,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { const feeObject = maxFeePerGas && maxPriorityFeePerGas ? { + type: 2, maxFeePerGas: `0x${BigInt( maxFeePerGas.truncate().toString() ).toString(16)}`, @@ -1062,15 +1088,85 @@ export const IBCSwapPage: FunctionComponent = observer(() => { }; const sender = ibcSwapConfigs.senderConfig.sender; + const isErc20InCurrency = + ("type" in inCurrency && inCurrency.type === "erc20") || + inCurrency.coinMinimalDenom.startsWith("erc20:"); + const erc20Approval = tx.requiredErc20Approvals?.[0]; + const erc20ApprovalTxTemp = + erc20Approval && isErc20InCurrency + ? ethereumAccount.makeErc20ApprovalTx( + { + ...inCurrency, + type: "erc20", + contractAddress: inCurrency.coinMinimalDenom.replace( + "erc20:", + "" + ), + }, + erc20Approval.spender, + erc20Approval.amount + ) + : undefined; + const erc20ApprovalFeeObject = await (async () => { + if (erc20ApprovalTxTemp) { + const { gasUsed } = await ethereumAccount.simulateGas( + sender, + erc20ApprovalTxTemp + ); + + const { maxFeePerGas, maxPriorityFeePerGas, gasPrice } = + ibcSwapConfigs.feeConfig.getEIP1559TxFees( + ibcSwapConfigs.feeConfig.type + ); + + const feeObject = + maxFeePerGas && maxPriorityFeePerGas + ? { + type: 2, + maxFeePerGas: `0x${Number( + maxFeePerGas.toString() + ).toString(16)}`, + maxPriorityFeePerGas: `0x${Number( + maxPriorityFeePerGas.toString() + ).toString(16)}`, + gasLimit: `0x${gasUsed.toString(16)}`, + } + : { + gasPrice: `0x${Number(gasPrice ?? "0").toString(16)}`, + gasLimit: `0x${gasUsed.toString(16)}`, + }; + + return feeObject; + } + + return {}; + })(); + const erc20ApprovalTx = erc20ApprovalTxTemp + ? { + ...erc20ApprovalTxTemp, + ...erc20ApprovalFeeObject, + } + : undefined; + + ethereumAccount.setIsSendingTx(true); + await ethereumAccount.sendEthereumTx( sender, { - ...tx, - ...feeObject, + ...(erc20ApprovalTx ?? { + ...tx, + ...feeObject, + }), }, { onBroadcasted: () => { // TODO: Add history + ethereumAccount.setIsSendingTx(false); + if (!erc20ApprovalTx) { + navigate("/", { + replace: true, + }); + } }, onFulfill: (txReceipt) => { const queryBalances = queriesStore.get( @@ -1093,6 +1189,65 @@ export const IBCSwapPage: FunctionComponent = observer(() => { } }); if (txReceipt.status === EthTxStatus.Success) { + if (erc20ApprovalTx) { + delete tx.requiredErc20Approvals; + ethereumAccount.setIsSendingTx(true); + ethereumAccount.sendEthereumTx( + sender, + { + ...tx, + ...feeObject, + }, + { + onBroadcasted: () => { + // TODO: Add history + ethereumAccount.setIsSendingTx(false); + navigate("/", { + replace: true, + }); + }, + onFulfill: (txReceipt) => { + const queryBalances = queriesStore.get( + ibcSwapConfigs.amountConfig.chainId + ).queryBalances; + queryBalances + .getQueryEthereumHexAddress(sender) + .balances.forEach((balance) => { + if ( + balance.currency.coinMinimalDenom === + ibcSwapConfigs.amountConfig.currency + .coinMinimalDenom || + ibcSwapConfigs.feeConfig.fees.some( + (fee) => + fee.currency.coinMinimalDenom === + balance.currency.coinMinimalDenom + ) + ) { + balance.fetch(); + } + }); + if (txReceipt.status === EthTxStatus.Success) { + notification.show( + "success", + intl.formatMessage({ + id: "notification.transaction-success", + }), + "" + ); + } else { + notification.show( + "failed", + intl.formatMessage({ + id: "error.transaction-failed", + }), + "" + ); + } + }, + } + ); + } + notification.show( "success", intl.formatMessage({ @@ -1110,12 +1265,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { }, } ); - ethereumAccount.setIsSendingTx(false); } - - navigate("/", { - replace: true, - }); } catch (e) { if (e?.message === "Request rejected") { return; diff --git a/apps/extension/src/pages/send/amount/index.tsx b/apps/extension/src/pages/send/amount/index.tsx index fd65c8cbf6..bf2ada4889 100644 --- a/apps/extension/src/pages/send/amount/index.tsx +++ b/apps/extension/src/pages/send/amount/index.tsx @@ -32,7 +32,7 @@ import { FeeControl } from "../../../components/input/fee-control"; import { useNotification } from "../../../hooks/notification"; import { DenomHelper, ExtensionKVStore } from "@keplr-wallet/common"; import { ENSInfo, ICNSInfo } from "../../../config.ui"; -import { CoinPretty, Dec, DecUtils } from "@keplr-wallet/unit"; +import { CoinPretty, DecUtils } from "@keplr-wallet/unit"; import { ColorPalette } from "../../../styles"; import { openPopupWindow } from "@keplr-wallet/popup"; import { InExtensionMessageRequester } from "@keplr-wallet/router-extension"; @@ -307,42 +307,6 @@ export const SendAmountPage: FunctionComponent = observer(() => { chainInfo.currencies, ]); - useEffect(() => { - (async () => { - if (chainInfo.features.includes("op-stack-l1-data-fee")) { - const { maxFeePerGas, maxPriorityFeePerGas, gasPrice } = - sendConfigs.feeConfig.getEIP1559TxFees(sendConfigs.feeConfig.type); - - const { to, gasLimit, value, data, chainId } = - ethereumAccount.makeSendTokenTx({ - currency: sendConfigs.amountConfig.amount[0].currency, - amount: sendConfigs.amountConfig.amount[0].toDec().toString(), - to: sendConfigs.recipientConfig.recipient, - gasLimit: sendConfigs.gasConfig.gas, - maxFeePerGas: maxFeePerGas?.toString(), - maxPriorityFeePerGas: maxPriorityFeePerGas?.toString(), - gasPrice: gasPrice?.toString(), - }); - - const l1DataFee = await ethereumAccount.simulateOpStackL1Fee({ - to, - gasLimit, - value, - data, - chainId, - }); - sendConfigs.feeConfig.setL1DataFee(new Dec(BigInt(l1DataFee))); - } - })(); - }, [ - chainInfo.features, - ethereumAccount, - sendConfigs.amountConfig.amount, - sendConfigs.feeConfig, - sendConfigs.gasConfig.gas, - sendConfigs.recipientConfig.recipient, - ]); - useEffect(() => { if (isEvmTx) { // Refresh EIP-1559 fee every 12 seconds. diff --git a/apps/hooks-internal/src/ibc-swap/amount.ts b/apps/hooks-internal/src/ibc-swap/amount.ts index d1ab6d7481..eb25f21d09 100644 --- a/apps/hooks-internal/src/ibc-swap/amount.ts +++ b/apps/hooks-internal/src/ibc-swap/amount.ts @@ -20,7 +20,7 @@ import { } from "@keplr-wallet/stores-internal"; import { EthereumAccountStore, - UnsignedEVMTransaction, + UnsignedEVMTransactionWithErc20Approvals, } from "@keplr-wallet/stores-eth"; export class IBCSwapAmountConfig extends AmountConfig { @@ -143,7 +143,7 @@ export class IBCSwapAmountConfig extends AmountConfig { slippageTolerancePercent: number, affiliateFeeReceiver: string, priorOutAmount?: Int - ): Promise { + ): Promise { const queryIBCSwap = this.getQueryIBCSwap(); if (!queryIBCSwap) { throw new Error("Query IBC Swap is not initialized"); @@ -312,7 +312,7 @@ export class IBCSwapAmountConfig extends AmountConfig { getTxIfReady( slippageTolerancePercent: number, affiliateFeeReceiver: string - ): MakeTxResponse | UnsignedEVMTransaction | undefined { + ): MakeTxResponse | UnsignedEVMTransactionWithErc20Approvals | undefined { if (!this.currency) { return; } @@ -459,7 +459,10 @@ export class IBCSwapAmountConfig extends AmountConfig { } else if (msg.type === "evmTx") { const ethereumAccount = this.ethereumAccountStore.getAccount(msg.chainId); const tx = ethereumAccount.makeTx(msg.to, msg.value, msg.data); - return tx; + return { + ...tx, + requiredErc20Approvals: msg.requiredErc20Approvals, + }; } } diff --git a/apps/stores-internal/src/skip/msgs-direct.ts b/apps/stores-internal/src/skip/msgs-direct.ts index c3e449bb04..10d211a11c 100644 --- a/apps/stores-internal/src/skip/msgs-direct.ts +++ b/apps/stores-internal/src/skip/msgs-direct.ts @@ -89,6 +89,11 @@ export class ObservableQueryMsgsDirectInner extends ObservableQuery ({ + amount: approval.amount, + spender: approval.spender, + tokenAddress: approval.token_contract, + }) + ), }; } diff --git a/apps/stores-internal/src/skip/types.ts b/apps/stores-internal/src/skip/types.ts index a33aabd4a1..376e83ee07 100644 --- a/apps/stores-internal/src/skip/types.ts +++ b/apps/stores-internal/src/skip/types.ts @@ -41,7 +41,11 @@ export interface MsgsDirectResponse { evm_tx?: { chain_id: string; data: string; - required_erc20_approvals: string[]; + required_erc20_approvals: { + amount: string; + spender: string; + token_contract: string; + }[]; signer_address: string; to: string; value: string; diff --git a/packages/stores-eth/src/account/base.ts b/packages/stores-eth/src/account/base.ts index 8c3a40c1bb..8c608a25e6 100644 --- a/packages/stores-eth/src/account/base.ts +++ b/packages/stores-eth/src/account/base.ts @@ -1,6 +1,7 @@ import { ChainGetter } from "@keplr-wallet/stores"; import { AppCurrency, + ERC20Currency, EthSignType, EthTxReceipt, Keplr, @@ -231,7 +232,7 @@ export class EthereumAccountBase { }; default: return { - ...this.makeTx(to, hexValue(parsedAmount), "0x0"), + ...this.makeTx(to, hexValue(parsedAmount)), ...feeObject, }; } @@ -240,7 +241,24 @@ export class EthereumAccountBase { return unsignedTx; } - makeTx(to: string, value: string, data: string): UnsignedTransaction { + makeErc20ApprovalTx( + currency: ERC20Currency, + spender: string, + amount: string + ): UnsignedTransaction { + const parsedAmount = parseUnits(amount, currency.coinDecimals); + + return this.makeTx( + currency.contractAddress, + "0x0", + erc20ContractInterface.encodeFunctionData("approve", [ + spender, + hexValue(parsedAmount), + ]) + ); + } + + makeTx(to: string, value: string, data?: string): UnsignedTransaction { const chainInfo = this.chainGetter.getChain(this.chainId); const evmInfo = chainInfo.evm; if (!evmInfo) { diff --git a/packages/stores-eth/src/types.ts b/packages/stores-eth/src/types.ts index 9ca6192403..2cbd0abd4f 100644 --- a/packages/stores-eth/src/types.ts +++ b/packages/stores-eth/src/types.ts @@ -1,3 +1,12 @@ import { UnsignedTransaction } from "@ethersproject/transactions"; export type UnsignedEVMTransaction = UnsignedTransaction; + +export type UnsignedEVMTransactionWithErc20Approvals = + UnsignedEVMTransaction & { + requiredErc20Approvals?: { + amount: string; + spender: string; + tokenAddress: string; + }[]; + }; From 358cf0d7e50786bbfb46d71540eefbc37d22fdd3 Mon Sep 17 00:00:00 2001 From: delivan Date: Thu, 19 Dec 2024 17:42:42 +0900 Subject: [PATCH 13/43] Fix an issue that get error if `split_routes` option enabled on skip api --- apps/extension/src/pages/ibc-swap/index.tsx | 1 + apps/hooks-internal/src/ibc-swap/amount.ts | 12 +++++-- apps/stores-internal/src/skip/route.ts | 39 ++++++++++++++++++--- apps/stores-internal/src/skip/types.ts | 18 +++++++++- 4 files changed, 62 insertions(+), 8 deletions(-) diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index 1e03697248..34f1569f59 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -315,6 +315,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { // 일단 swap-1로 설정한다. if ( queryRoute.response.data.swap_venues && + queryRoute.response.data.swap_venues[0] && ibcSwapConfigs.amountConfig.chainInfo.chainIdentifier === chainStore.getChain( queryRoute.response.data.swap_venues[0].chain_id diff --git a/apps/hooks-internal/src/ibc-swap/amount.ts b/apps/hooks-internal/src/ibc-swap/amount.ts index eb25f21d09..9723245f8d 100644 --- a/apps/hooks-internal/src/ibc-swap/amount.ts +++ b/apps/hooks-internal/src/ibc-swap/amount.ts @@ -472,8 +472,16 @@ export class IBCSwapAmountConfig extends AmountConfig { for (const operation of response.operations) { if ("swap" in operation) { - for (const swapOperation of operation.swap.swap_in.swap_operations) { - key += `/${swapOperation.pool}/${swapOperation.denom_in}/${swapOperation.denom_out}`; + if (operation.swap.swap_in) { + for (const swapOperation of operation.swap.swap_in.swap_operations) { + key += `/${swapOperation.pool}/${swapOperation.denom_in}/${swapOperation.denom_out}`; + } + } else if (operation.swap.smart_swap_in) { + for (const swapRoute of operation.swap.smart_swap_in.swap_routes) { + for (const swapOperation of swapRoute.swap_operations) { + key += `/${swapOperation.pool}/${swapOperation.denom_in}/${swapOperation.denom_out}`; + } + } } } } diff --git a/apps/stores-internal/src/skip/route.ts b/apps/stores-internal/src/skip/route.ts index dd82ae1a83..ff3e5715ce 100644 --- a/apps/stores-internal/src/skip/route.ts +++ b/apps/stores-internal/src/skip/route.ts @@ -40,6 +40,32 @@ const Schema = Joi.object({ swap_amount_in: Joi.string().required(), price_impact_percent: Joi.string(), }).unknown(true), + smart_swap_in: Joi.object({ + swap_venue: Joi.object({ + name: Joi.string().required(), + chain_id: Joi.string().required(), + }) + .unknown(true) + .required(), + swap_routes: Joi.array() + .items( + Joi.object({ + swap_amount_in: Joi.string().required(), + denom_in: Joi.string().required(), + swap_operations: Joi.array() + .items( + Joi.object({ + pool: Joi.string().required(), + denom_in: Joi.string().required(), + denom_out: Joi.string().required(), + }).unknown(true) + ) + .required(), + }).unknown(true) + ) + .required(), + estimated_amount_out: Joi.string().required(), + }).unknown(true), estimated_affiliate_fee: Joi.string().required(), }) .required() @@ -259,11 +285,14 @@ export class ObservableQueryRouteInner extends ObservableQuery { }[] = []; for (const operation of this.response.data.operations) { if ("swap" in operation) { - estimatedAffiliateFees.push({ - fee: operation.swap.estimated_affiliate_fee, - // QUESTION: swap_out이 생기면...? - venueChainId: operation.swap.swap_in.swap_venue.chain_id, - }); + const swapIn = operation.swap.swap_in ?? operation.swap.smart_swap_in; + if (swapIn) { + estimatedAffiliateFees.push({ + fee: operation.swap.estimated_affiliate_fee, + // QUESTION: swap_out이 생기면...? + venueChainId: swapIn.swap_venue.chain_id, + }); + } } } diff --git a/apps/stores-internal/src/skip/types.ts b/apps/stores-internal/src/skip/types.ts index 376e83ee07..61c0a61d0e 100644 --- a/apps/stores-internal/src/skip/types.ts +++ b/apps/stores-internal/src/skip/types.ts @@ -74,7 +74,7 @@ export interface RouteResponse { } | { swap: { - swap_in: { + swap_in?: { swap_venue: { name: string; chain_id: string; @@ -87,6 +87,22 @@ export interface RouteResponse { swap_amount_in: string; price_impact_percent?: string; }; + smart_swap_in?: { + swap_venue: { + name: string; + chain_id: string; + }; + swap_routes: { + swap_amount_in: string; + denom_in: string; + swap_operations: { + pool: string; + denom_in: string; + denom_out: string; + }[]; + }[]; + estimated_amount_out: string; + }; estimated_affiliate_fee: string; }; } From 000248d01fbc80ded855f88d223688d4b83bd0a3 Mon Sep 17 00:00:00 2001 From: delivan Date: Thu, 19 Dec 2024 21:44:10 +0900 Subject: [PATCH 14/43] Fix an issue that evm tx gas simulation failed on swap --- apps/extension/src/pages/ibc-swap/index.tsx | 111 ++++++++++++++------ 1 file changed, 76 insertions(+), 35 deletions(-) diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index 34f1569f59..a822a29867 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -315,20 +315,40 @@ export const IBCSwapPage: FunctionComponent = observer(() => { // 일단 swap-1로 설정한다. if ( queryRoute.response.data.swap_venues && - queryRoute.response.data.swap_venues[0] && - ibcSwapConfigs.amountConfig.chainInfo.chainIdentifier === - chainStore.getChain( - queryRoute.response.data.swap_venues[0].chain_id - ).chainIdentifier + queryRoute.response.data.swap_venues.length === 1 ) { - type = `swap-1`; + const swapVenueChainId = (() => { + const evmLikeChainId = Number( + queryRoute.response.data.swap_venues[0].chain_id + ); + const isEVMChainId = + !Number.isNaN(evmLikeChainId) && evmLikeChainId > 0; + + return isEVMChainId + ? `eip155:${evmLikeChainId}` + : queryRoute.response.data.swap_venues[0].chain_id; + })(); + + if ( + ibcSwapConfigs.amountConfig.chainInfo.chainIdentifier === + chainStore.getChain(swapVenueChainId).chainIdentifier + ) { + type = `swap-1`; + } } if (queryRoute.response.data.operations.length > 0) { const firstOperation = queryRoute.response.data.operations[0]; if ("swap" in firstOperation) { - if ("swap_in" in firstOperation.swap) { + if (firstOperation.swap.swap_in) { type = `swap-${firstOperation.swap.swap_in.swap_operations.length}`; + } else if (firstOperation.swap.smart_swap_in) { + type = `swap-${firstOperation.swap.smart_swap_in.swap_routes.reduce( + (acc, cur) => { + return (acc += cur.swap_operations.length); + }, + 0 + )}`; } } @@ -380,11 +400,15 @@ export const IBCSwapPage: FunctionComponent = observer(() => { if (queryRoute.response.data.operations.length > 0) { for (const operation of queryRoute.response.data.operations) { if ("swap" in operation) { - const swapFeeBpsReceiverAddress = SwapFeeBps.receivers.find( - (r) => r.chainId === operation.swap.swap_in.swap_venue.chain_id - ); - if (swapFeeBpsReceiverAddress) { - swapFeeBpsReceiver.push(swapFeeBpsReceiverAddress.address); + const swapIn = + operation.swap.swap_in ?? operation.swap.smart_swap_in; + if (swapIn) { + const swapFeeBpsReceiverAddress = SwapFeeBps.receivers.find( + (r) => r.chainId === swapIn.swap_venue.chain_id + ); + if (swapFeeBpsReceiverAddress) { + swapFeeBpsReceiver.push(swapFeeBpsReceiverAddress.address); + } } } } @@ -410,8 +434,29 @@ export const IBCSwapPage: FunctionComponent = observer(() => { ); const sender = ibcSwapConfigs.senderConfig.sender; + const isErc20InCurrency = + ("type" in inCurrency && inCurrency.type === "erc20") || + inCurrency.coinMinimalDenom.startsWith("erc20:"); + const erc20Approval = tx.requiredErc20Approvals?.[0]; + const erc20ApprovalTx = + erc20Approval && isErc20InCurrency + ? ethereumAccount.makeErc20ApprovalTx( + { + ...inCurrency, + type: "erc20", + contractAddress: inCurrency.coinMinimalDenom.replace( + "erc20:", + "" + ), + }, + erc20Approval.spender, + erc20Approval.amount + ) + : undefined; + return { - simulate: () => ethereumAccount.simulateGas(sender, tx), + simulate: () => + ethereumAccount.simulateGas(sender, erc20ApprovalTx ?? tx), }; } } @@ -748,12 +793,15 @@ export const IBCSwapPage: FunctionComponent = observer(() => { counterpartyChainId: queryClientState.clientChainId, }); } else if ("swap" in operation) { - const swapFeeBpsReceiverAddress = SwapFeeBps.receivers.find( - (r) => - r.chainId === operation.swap.swap_in.swap_venue.chain_id - ); - if (swapFeeBpsReceiverAddress) { - swapFeeBpsReceiver.push(swapFeeBpsReceiverAddress.address); + const swapIn = + operation.swap.swap_in ?? operation.swap.smart_swap_in; + if (swapIn) { + const swapFeeBpsReceiverAddress = SwapFeeBps.receivers.find( + (r) => r.chainId === swapIn.swap_venue.chain_id + ); + if (swapFeeBpsReceiverAddress) { + swapFeeBpsReceiver.push(swapFeeBpsReceiverAddress.address); + } } swapChannelIndex = channels.length - 1; } @@ -1065,7 +1113,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { ibcSwapConfigs.feeConfig.getEIP1559TxFees( ibcSwapConfigs.feeConfig.type ); - const feeObject = + const firstTxFeeObject = maxFeePerGas && maxPriorityFeePerGas ? { type: 2, @@ -1093,7 +1141,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { ("type" in inCurrency && inCurrency.type === "erc20") || inCurrency.coinMinimalDenom.startsWith("erc20:"); const erc20Approval = tx.requiredErc20Approvals?.[0]; - const erc20ApprovalTxTemp = + const erc20ApprovalTx = erc20Approval && isErc20InCurrency ? ethereumAccount.makeErc20ApprovalTx( { @@ -1108,11 +1156,12 @@ export const IBCSwapPage: FunctionComponent = observer(() => { erc20Approval.amount ) : undefined; - const erc20ApprovalFeeObject = await (async () => { - if (erc20ApprovalTxTemp) { + + const secondTxFeeObject = await (async () => { + if (erc20ApprovalTx) { const { gasUsed } = await ethereumAccount.simulateGas( sender, - erc20ApprovalTxTemp + tx ); const { maxFeePerGas, maxPriorityFeePerGas, gasPrice } = @@ -1142,22 +1191,14 @@ export const IBCSwapPage: FunctionComponent = observer(() => { return {}; })(); - const erc20ApprovalTx = erc20ApprovalTxTemp - ? { - ...erc20ApprovalTxTemp, - ...erc20ApprovalFeeObject, - } - : undefined; ethereumAccount.setIsSendingTx(true); await ethereumAccount.sendEthereumTx( sender, { - ...(erc20ApprovalTx ?? { - ...tx, - ...feeObject, - }), + ...(erc20ApprovalTx ?? tx), + ...firstTxFeeObject, }, { onBroadcasted: () => { @@ -1197,7 +1238,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { sender, { ...tx, - ...feeObject, + ...secondTxFeeObject, }, { onBroadcasted: () => { From 669318b42dd44a356dd536f094fa408072915a2d Mon Sep 17 00:00:00 2001 From: delivan Date: Fri, 20 Dec 2024 17:03:50 +0900 Subject: [PATCH 15/43] Add evm chain supports to swap destination alternative chain --- apps/stores-internal/src/skip/assets.ts | 12 ++++++++++ apps/stores-internal/src/skip/ibc-swap.ts | 29 +++++++++++++++++++++++ apps/stores-internal/src/skip/types.ts | 1 + 3 files changed, 42 insertions(+) diff --git a/apps/stores-internal/src/skip/assets.ts b/apps/stores-internal/src/skip/assets.ts index 87da983dbf..287386541b 100644 --- a/apps/stores-internal/src/skip/assets.ts +++ b/apps/stores-internal/src/skip/assets.ts @@ -22,6 +22,7 @@ const Schema = Joi.object({ origin_chain_id: Joi.string().required(), is_evm: Joi.boolean().required(), token_contract: Joi.string().optional(), + recommended_symbol: Joi.string().optional(), }).unknown(true) ), }).unknown(true) @@ -54,6 +55,9 @@ export class ObservableQueryAssetsInner extends ObservableQuery chainId: string; originDenom: string; originChainId: string; + isEvm: boolean; + tokenContract?: string; + recommendedSymbol?: string; }[] { if ( !this.response || @@ -82,6 +86,9 @@ export class ObservableQueryAssetsInner extends ObservableQuery chainId: string; originDenom: string; originChainId: string; + isEvm: boolean; + tokenContract?: string; + recommendedSymbol?: string; }[] = []; for (const asset of assetsInResponse.assets) { @@ -102,6 +109,8 @@ export class ObservableQueryAssetsInner extends ObservableQuery chainId: chainId, originDenom: asset.origin_denom, originChainId: originChainId, + isEvm: false, + recommendedSymbol: asset.recommended_symbol, }); // IBC asset이 아니라면 알고있는 currency만 넣는다. } else { @@ -123,6 +132,9 @@ export class ObservableQueryAssetsInner extends ObservableQuery chainId: chainId, originDenom: originCoinMinimalDenom, originChainId: originChainId, + isEvm: asset.is_evm, + tokenContract: asset.token_contract, + recommendedSymbol: asset.recommended_symbol, }); } } diff --git a/apps/stores-internal/src/skip/ibc-swap.ts b/apps/stores-internal/src/skip/ibc-swap.ts index ff8b5afc33..c589594928 100644 --- a/apps/stores-internal/src/skip/ibc-swap.ts +++ b/apps/stores-internal/src/skip/ibc-swap.ts @@ -599,6 +599,35 @@ export class ObservableQueryIbcSwap extends HasMapStore asset.denom === currency.coinMinimalDenom); + for (const candidateChain of this.queryChains.chains) { + const isCandidateChainEVMOnlyChain = + candidateChain.chainInfo.chainId.startsWith("eip155:"); + const isCandidateChain = + candidateChain.chainInfo.chainId !== chainInfo.chainId && + (isEVMOnlyChain ? true : isCandidateChainEVMOnlyChain); + if (isCandidateChain) { + const candidateAsset = this.queryAssets + .getAssets(candidateChain.chainInfo.chainId) + .assets.find( + (a) => + a.recommendedSymbol && + a.recommendedSymbol === asset?.recommendedSymbol + ); + + if (candidateAsset) { + res.push({ + denom: candidateAsset.denom, + chainId: candidateChain.chainInfo.chainId, + }); + } + } + } + const channels = this.queryIBCPacketForwardingTransfer.getIBCChannels( originOutChainId, originOutCurrency.coinMinimalDenom diff --git a/apps/stores-internal/src/skip/types.ts b/apps/stores-internal/src/skip/types.ts index 61c0a61d0e..09c73eb969 100644 --- a/apps/stores-internal/src/skip/types.ts +++ b/apps/stores-internal/src/skip/types.ts @@ -24,6 +24,7 @@ export interface AssetsResponse { origin_chain_id: string; is_evm: boolean; token_contract?: string; + recommended_symbol?: string; }[]; } | undefined; From 70615db03ed614e0847ca350b716f5c2fd7c5f23 Mon Sep 17 00:00:00 2001 From: rowan Date: Fri, 20 Dec 2024 20:28:59 +0900 Subject: [PATCH 16/43] Add stub logic for handling `evm <> comsos` swap history --- apps/extension/src/pages/ibc-swap/index.tsx | 52 ++- .../src/recent-send-history/handler.ts | 60 ++++ .../src/recent-send-history/init.ts | 10 + .../src/recent-send-history/service.ts | 38 +++ .../recent-send-history/temp-skip-message.ts | 98 ++++++ .../recent-send-history/temp-skip-types.ts | 300 ++++++++++++++++++ 6 files changed, 556 insertions(+), 2 deletions(-) create mode 100644 packages/background/src/recent-send-history/temp-skip-message.ts create mode 100644 packages/background/src/recent-send-history/temp-skip-types.ts diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index 27a7acb900..daa5d08389 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -54,6 +54,8 @@ import { Button } from "../../components/button"; import { TextButtonProps } from "../../components/button-text"; import { UnsignedEVMTransaction } from "@keplr-wallet/stores-eth"; import { EthTxStatus } from "@keplr-wallet/types"; +import { simpleFetch } from "@keplr-wallet/simple-fetch"; +import { RecordTxWithSkipSwapMsg } from "@keplr-wallet/background/build/recent-send-history/temp-skip-message"; const TextButtonStyles = { Container: styled.div` @@ -1069,8 +1071,54 @@ export const IBCSwapPage: FunctionComponent = observer(() => { ...feeObject, }, { - onBroadcasted: () => { - // TODO: Add history + onBroadcasted: (txHash) => { + const evmChainId = parseInt(inChainId.split(":")[1]); + + // 1. track the transaction + setTimeout(() => { + // no wait + simpleFetch( + "https://api.skip.build/", + "/v2/tx/track", + { + method: "POST", + headers: { + "content-type": "application/json", + ...(() => { + const res: { authorization?: string } = {}; + if (process.env["SKIP_API_KEY"]) { + res.authorization = process.env["SKIP_API_KEY"]; + } + return res; + })(), + }, + body: JSON.stringify({ + tx_hash: txHash, + chain_id: evmChainId, + }), + } + ) + .then((result) => { + console.log( + `Skip tx track result: ${JSON.stringify(result)}` + ); + }) + .catch((e) => { + console.log(e); + }); + }, 2000); + + // 2. create message and send it to the background script + // to create IBCHistory for displaying the transaction history in the home screen. + + // 2-1. Define new message + const msg = new RecordTxWithSkipSwapMsg(); + + // 2-2. Send the message to the background script + new InExtensionMessageRequester().sendMessage( + BACKGROUND_PORT, + msg + ); }, onFulfill: (txReceipt) => { const queryBalances = queriesStore.get( diff --git a/packages/background/src/recent-send-history/handler.ts b/packages/background/src/recent-send-history/handler.ts index f02e9a8761..ec17e7ccdc 100644 --- a/packages/background/src/recent-send-history/handler.ts +++ b/packages/background/src/recent-send-history/handler.ts @@ -16,6 +16,12 @@ import { ClearAllIBCHistoryMsg, } from "./messages"; import { RecentSendHistoryService } from "./service"; +import { + ClearAllSkipHistoryMsg, + GetSkipHistoriesMsg, + RecordTxWithSkipSwapMsg, + RemoveSkipHistoryMsg, +} from "./temp-skip-message"; export const getHandler: (service: RecentSendHistoryService) => Handler = ( service: RecentSendHistoryService @@ -62,6 +68,26 @@ export const getHandler: (service: RecentSendHistoryService) => Handler = ( env, msg as ClearAllIBCHistoryMsg ); + case RecordTxWithSkipSwapMsg: + return handleRecordTxWithSkipSwapMsg(service)( + env, + msg as RecordTxWithSkipSwapMsg + ); + case GetSkipHistoriesMsg: + return handleGetSkipHistoriesMsg(service)( + env, + msg as GetSkipHistoriesMsg + ); + case RemoveSkipHistoryMsg: + return handleRemoveSkipHistoryMsg(service)( + env, + msg as RemoveSkipHistoryMsg + ); + case ClearAllSkipHistoryMsg: + return handleClearAllSkipHistoryMsg(service)( + env, + msg as ClearAllSkipHistoryMsg + ); default: throw new KeplrError("tx", 110, "Unknown msg type"); } @@ -184,3 +210,37 @@ const handleClearAllIBCHistoryMsg: ( service.clearAllRecentIBCHistory(); }; }; + +const handleRecordTxWithSkipSwapMsg: ( + service: RecentSendHistoryService +) => InternalHandler = (service) => { + return async (_env, _msg) => { + // TODO: Implement this + return service.recordTxWithSkipSwap(); + }; +}; + +const handleGetSkipHistoriesMsg: ( + service: RecentSendHistoryService +) => InternalHandler = (service) => { + return (_env, _msg) => { + return service.getRecentSkipHistories(); + }; +}; + +const handleRemoveSkipHistoryMsg: ( + service: RecentSendHistoryService +) => InternalHandler = (service) => { + return async (_env, msg) => { + service.removeRecentSkipHistory(msg.id); + return service.getRecentSkipHistories(); + }; +}; + +const handleClearAllSkipHistoryMsg: ( + service: RecentSendHistoryService +) => InternalHandler = (service) => { + return (_env, _msg) => { + service.clearAllRecentSkipHistory(); + }; +}; diff --git a/packages/background/src/recent-send-history/init.ts b/packages/background/src/recent-send-history/init.ts index 880d334f6e..35611abacb 100644 --- a/packages/background/src/recent-send-history/init.ts +++ b/packages/background/src/recent-send-history/init.ts @@ -12,6 +12,12 @@ import { import { ROUTE } from "./constants"; import { getHandler } from "./handler"; import { RecentSendHistoryService } from "./service"; +import { + ClearAllSkipHistoryMsg, + GetSkipHistoriesMsg, + RecordTxWithSkipSwapMsg, + RemoveSkipHistoryMsg, +} from "./temp-skip-message"; export function init(router: Router, service: RecentSendHistoryService): void { router.registerMessage(GetRecentSendHistoriesMsg); @@ -22,6 +28,10 @@ export function init(router: Router, service: RecentSendHistoryService): void { router.registerMessage(GetIBCHistoriesMsg); router.registerMessage(RemoveIBCHistoryMsg); router.registerMessage(ClearAllIBCHistoryMsg); + router.registerMessage(RecordTxWithSkipSwapMsg); + router.registerMessage(GetSkipHistoriesMsg); + router.registerMessage(RemoveSkipHistoryMsg); + router.registerMessage(ClearAllSkipHistoryMsg); router.addHandler(ROUTE, getHandler(service)); } diff --git a/packages/background/src/recent-send-history/service.ts b/packages/background/src/recent-send-history/service.ts index a9982fdf3b..af46a716cb 100644 --- a/packages/background/src/recent-send-history/service.ts +++ b/packages/background/src/recent-send-history/service.ts @@ -20,6 +20,7 @@ import { Buffer } from "buffer/"; import { AppCurrency, ChainInfo } from "@keplr-wallet/types"; import { CoinPretty } from "@keplr-wallet/unit"; import { simpleFetch } from "@keplr-wallet/simple-fetch"; +import { SkipHistory } from "./temp-skip-message"; export class RecentSendHistoryService { // Key: {chain_identifier}/{type} @@ -33,6 +34,12 @@ export class RecentSendHistoryService { @observable protected readonly recentIBCHistoryMap: Map = new Map(); + @observable + protected recentSkipHistorySeq: number = 0; + // Key: id (sequence, it should be increased by 1 for each + @observable + protected readonly recentSkipHistoryMap: Map = new Map(); + constructor( protected readonly kvStore: KVStore, protected readonly chainsService: ChainsService, @@ -110,6 +117,9 @@ export class RecentSendHistoryService { this.trackIBCPacketForwardingRecursive(history.id); } + // TODO: Load skip history from storage + // and track unfinished skip history if exists + this.chainsService.addChainRemovedHandler(this.onChainRemoved); } @@ -1056,6 +1066,34 @@ export class RecentSendHistoryService { this.recentIBCHistoryMap.clear(); } + // skip related methods + recordTxWithSkipSwap(): string { + const id = (this.recentIBCHistorySeq++).toString(); + + // TODO: Implement this + + return id; + } + + getRecentSkipHistory(id: string): SkipHistory | undefined { + return this.recentSkipHistoryMap.get(id); + } + + getRecentSkipHistories(): SkipHistory[] { + // TODO: Implement this + return Array.from(this.recentSkipHistoryMap.values()); + } + + @action + removeRecentSkipHistory(id: string): boolean { + return this.recentSkipHistoryMap.delete(id); + } + + @action + clearAllRecentSkipHistory(): void { + this.recentSkipHistoryMap.clear(); + } + protected getIBCWriteAcknowledgementAckFromTx( tx: any, sourcePortId: string, diff --git a/packages/background/src/recent-send-history/temp-skip-message.ts b/packages/background/src/recent-send-history/temp-skip-message.ts new file mode 100644 index 0000000000..f2b704f157 --- /dev/null +++ b/packages/background/src/recent-send-history/temp-skip-message.ts @@ -0,0 +1,98 @@ +import { Message } from "@keplr-wallet/router"; +import { ROUTE } from "./constants"; + +export type SkipHistory = { + chainId: string; + // TODO: Define the properties of the skip history +}; + +export class RecordTxWithSkipSwapMsg extends Message { + public static type() { + return "record-tx-with-skip-swap"; + } + + // TODO: Define the properties of the message + constructor() { + super(); + } + + validateBasic(): void { + // TODO: Implement validation + } + + route(): string { + return ROUTE; + } + + type(): string { + return RecordTxWithSkipSwapMsg.type(); + } +} + +export class GetSkipHistoriesMsg extends Message { + public static type() { + return "get-skip-histories"; + } + + constructor() { + super(); + } + + validateBasic(): void { + // noop + } + + route(): string { + return ROUTE; + } + + type(): string { + return GetSkipHistoriesMsg.type(); + } +} + +export class RemoveSkipHistoryMsg extends Message { + public static type() { + return "remove-skip-histories"; + } + + constructor(public readonly id: string) { + super(); + } + + validateBasic(): void { + if (!this.id) { + throw new Error("id is empty"); + } + } + + route(): string { + return ROUTE; + } + + type(): string { + return RemoveSkipHistoryMsg.type(); + } +} + +export class ClearAllSkipHistoryMsg extends Message { + public static type() { + return "clear-all-skip-histories"; + } + + constructor() { + super(); + } + + validateBasic(): void { + // noop + } + + route(): string { + return ROUTE; + } + + type(): string { + return ClearAllSkipHistoryMsg.type(); + } +} diff --git a/packages/background/src/recent-send-history/temp-skip-types.ts b/packages/background/src/recent-send-history/temp-skip-types.ts new file mode 100644 index 0000000000..9b7abdf6c8 --- /dev/null +++ b/packages/background/src/recent-send-history/temp-skip-types.ts @@ -0,0 +1,300 @@ +export type StatusState = + | "STATE_UNKNOWN" + | "STATE_SUBMITTED" + | "STATE_PENDING" // route is in progress + | "STATE_RECEIVED" + | "STATE_COMPLETED" // route is completed + | "STATE_ABANDONED" // Tracking has stopped + | "STATE_COMPLETED_SUCCESS" // The route has completed successfully + | "STATE_COMPLETED_ERROR" // The route errored somewhere and the user has their tokens unlocked in one of their wallets + | "STATE_PENDING_ERROR"; // The route is in progress and an error has occurred somewhere (specially for IBC, where the asset is locked on the source chain) + +export type NextBlockingTransfer = { + transfer_sequence_index: number; +}; + +export type StatusRequest = { + tx_hash: string; + chain_id: string; +}; + +// This is for the IBC transfer +export type TransferState = + | "TRANSFER_UNKNOWN" + | "TRANSFER_PENDING" + | "TRANSFER_RECEIVED" // The packet has been received on the destination chain + | "TRANSFER_SUCCESS" // The packet has been successfully acknowledged + | "TRANSFER_FAILURE"; + +export type TransferInfo = { + from_chain_id: string; + to_chain_id: string; + state: TransferState; + packet_txs: Packet; + + // Deprecated + src_chain_id: string; + dst_chain_id: string; +}; + +export type TransferAssetRelease = { + chain_id: string; // Chain where the assets are released or will be released + denom: string; // Denom of the tokens the user will have + released: boolean; // Boolean given whether the funds are currently available (if the state is STATE_PENDING_ERROR , this will be false) +}; + +export type TxStatusResponse = { + status: StatusState; + transfer_sequence: TransferEvent[]; + next_blocking_transfer: NextBlockingTransfer | null; // give the index of the next blocking transfer in the sequence + transfer_asset_release: TransferAssetRelease | null; // Info about where the users tokens will be released when the route completes () + error: StatusError | null; + state: StatusState; + transfers: TransferStatus[]; +}; + +export type TransferStatus = { + state: StatusState; + transfer_sequence: TransferEvent[]; + next_blocking_transfer: NextBlockingTransfer | null; + transfer_asset_release: TransferAssetRelease | null; + error: StatusError | null; +}; + +export type Packet = { + send_tx: ChainTransaction | null; + receive_tx: ChainTransaction | null; + acknowledge_tx: ChainTransaction | null; + timeout_tx: ChainTransaction | null; + + error: PacketError | null; +}; + +export type StatusErrorType = + | "STATUS_ERROR_UNKNOWN" + | "STATUS_ERROR_TRANSACTION_EXECUTION" + | "STATUS_ERROR_INDEXING"; + +export type TransactionExecutionError = { + code: number; + message: string; +}; + +export type StatusError = { + code: number; + message: string; + type: StatusErrorType; + details: { + transactionExecutionError: TransactionExecutionError; + }; +}; + +export type PacketErrorType = + | "PACKET_ERROR_UNKNOWN" + | "PACKET_ERROR_ACKNOWLEDGEMENT" + | "PACKET_ERROR_TIMEOUT"; + +export type AcknowledgementError = { + message: string; + code: number; +}; + +export type PacketError = { + code: number; + message: string; + type: PacketErrorType; + details: { + acknowledgement_error: AcknowledgementError; + }; +}; + +export type ChainTransaction = { + chain_id: string; + tx_hash: string; + explorer_link: string; +}; + +export type TrackTxRequest = { + tx_hash: string; + chain_id: string; +}; + +export type TrackTxResponse = { + tx_hash: string; + explorer_link: string; +}; + +export type AxelarTransferType = + | "AXELAR_TRANSFER_CONTRACT_CALL_WITH_TOKEN" + | "AXELAR_TRANSFER_SEND_TOKEN"; + +export type AxelarTransferState = + | "AXELAR_TRANSFER_UNKNOWN" + | "AXELAR_TRANSFER_PENDING_CONFIRMATION" + | "AXELAR_TRANSFER_PENDING_RECEIPT" + | "AXELAR_TRANSFER_SUCCESS" // Desirable state + | "AXELAR_TRANSFER_FAILURE"; + +export type AxelarTransferInfo = { + from_chain_id: string; + to_chain_id: string; + type: AxelarTransferType; + state: AxelarTransferState; + txs: AxelarTransferTransactions; + axelar_scan_link: string; + + // Deprecated + src_chain_id: string; + dst_chain_id: string; +}; + +export type AxelarTransferTransactions = + | { + contract_call_with_token_txs: ContractCallWithTokenTransactions; + } + | { + send_token_txs: SendTokenTransactions; + }; + +export type ContractCallWithTokenTransactions = { + send_tx: ChainTransaction | null; + gas_paid_tx: ChainTransaction | null; + confirm_tx: ChainTransaction | null; + approve_tx: ChainTransaction | null; + execute_tx: ChainTransaction | null; + error: ContractCallWithTokenError | null; +}; + +export type ContractCallWithTokenError = { + message: string; + type: ContractCallWithTokenErrorType; +}; + +export type ContractCallWithTokenErrorType = + "CONTRACT_CALL_WITH_TOKEN_EXECUTION_ERROR"; + +export type SendTokenTransactions = { + send_tx: ChainTransaction | null; + confirm_tx: ChainTransaction | null; + execute_tx: ChainTransaction | null; + error: SendTokenError | null; +}; + +export type SendTokenErrorType = "SEND_TOKEN_EXECUTION_ERROR"; + +export type SendTokenError = { + message: string; + type: SendTokenErrorType; +}; + +export type CCTPTransferState = + | "CCTP_TRANSFER_UNKNOWN" + | "CCTP_TRANSFER_SENT" + | "CCTP_TRANSFER_PENDING_CONFIRMATION" + | "CCTP_TRANSFER_CONFIRMED" + | "CCTP_TRANSFER_RECEIVED"; // Desirable state + +export type CCTPTransferTransactions = { + send_tx: ChainTransaction | null; + receive_tx: ChainTransaction | null; +}; + +export type CCTPTransferInfo = { + from_chain_id: string; + to_chain_id: string; + state: CCTPTransferState; + txs: CCTPTransferTransactions; + + // Deprecated + src_chain_id: string; + dst_chain_id: string; +}; + +export type HyperlaneTransferState = + | "HYPERLANE_TRANSFER_UNKNOWN" + | "HYPERLANE_TRANSFER_SENT" + | "HYPERLANE_TRANSFER_FAILED" + | "HYPERLANE_TRANSFER_RECEIVED"; // Desirable state + +export type HyperlaneTransferTransactions = { + send_tx: ChainTransaction | null; + receive_tx: ChainTransaction | null; +}; + +export type HyperlaneTransferInfo = { + from_chain_id: string; + to_chain_id: string; + state: HyperlaneTransferState; + txs: HyperlaneTransferTransactions; +}; + +export type GoFastTransferTransactions = { + order_submitted_tx: ChainTransaction | null; + order_filled_tx: ChainTransaction | null; + order_refunded_tx: ChainTransaction | null; + order_timeout_tx: ChainTransaction | null; +}; + +export type GoFastTransferState = + | "GO_FAST_TRANSFER_UNKNOWN" + | "GO_FAST_TRANSFER_SENT" + | "GO_FAST_POST_ACTION_FAILED" + | "GO_FAST_TRANSFER_TIMEOUT" + | "GO_FAST_TRANSFER_FILLED" // Desirable state + | "GO_FAST_TRANSFER_REFUNDED"; + +export type GoFastTransferInfo = { + from_chain_id: string; + to_chain_id: string; + state: GoFastTransferState; + txs: GoFastTransferTransactions; +}; + +export type StargateTransferState = + | "STARGATE_TRANSFER_UNKNOWN" + | "STARGATE_TRANSFER_SENT" + | "STARGATE_TRANSFER_RECEIVED" // Desirable state + | "STARGATE_TRANSFER_FAILED"; + +export type StargateTransferTransactions = { + send_tx: ChainTransaction | null; + receive_tx: ChainTransaction | null; + error_tx: ChainTransaction | null; +}; + +export type StargateTransferInfo = { + from_chain_id: string; + to_chain_id: string; + state: StargateTransferState; + txs: StargateTransferTransactions; +}; + +export type OPInitTransferState = + | "OPINIT_TRANSFER_UNKNOWN" + | "OPINIT_TRANSFER_SENT" + | "OPINIT_TRANSFER_RECEIVED"; // Desirable state + +export type OPInitTransferTransactions = { + send_tx: ChainTransaction | null; + receive_tx: ChainTransaction | null; +}; + +export type OPInitTransferInfo = { + from_chain_id: string; + to_chain_id: string; + state: OPInitTransferState; + txs: OPInitTransferTransactions; +}; + +export type TransferEvent = + | { + ibc_transfer: TransferInfo; + } + | { + axelar_transfer: AxelarTransferInfo; + } + | { cctp_transfer: CCTPTransferInfo } + | { hyperlane_transfer: HyperlaneTransferInfo } + | { op_init_transfer: OPInitTransferInfo } + | { go_fast_transfer: GoFastTransferInfo } + | { stargate_transfer: StargateTransferInfo }; From 74494efd0b1ce714819e51213fbb46717e7968db Mon Sep 17 00:00:00 2001 From: Thunnini Date: Fri, 20 Dec 2024 20:56:48 +0900 Subject: [PATCH 17/43] =?UTF-8?q?getSwapDestinationCurrencyAlternativeChai?= =?UTF-8?q?ns=20method=EC=97=90=EC=84=9C=20evm=20chain=EC=9D=84=20?= =?UTF-8?q?=EC=B0=BE=EC=9D=84=EB=95=8C=20assets=EC=97=90=EC=84=9C=20curren?= =?UTF-8?q?cy=EB=A5=BC=20=EC=B0=BE=EC=9D=84=EB=95=8C=20=EB=84=88=EB=AC=B4?= =?UTF-8?q?=20=EB=A7=8E=EC=9D=80=20state=20=EB=B3=80=ED=99=94=EB=A5=BC=20?= =?UTF-8?q?=EC=9D=BC=EC=9C=BC=ED=82=A4=EB=8A=94=20=EB=AC=B8=EC=A0=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=99=84=ED=99=94=EC=8B=9C=ED=82=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/stores-internal/src/skip/assets.ts | 95 +++++++++++++++++++++++ apps/stores-internal/src/skip/ibc-swap.ts | 16 ++-- apps/stores-internal/src/skip/types.ts | 1 + 3 files changed, 107 insertions(+), 5 deletions(-) diff --git a/apps/stores-internal/src/skip/assets.ts b/apps/stores-internal/src/skip/assets.ts index 287386541b..f25be80f84 100644 --- a/apps/stores-internal/src/skip/assets.ts +++ b/apps/stores-internal/src/skip/assets.ts @@ -23,6 +23,7 @@ const Schema = Joi.object({ is_evm: Joi.boolean().required(), token_contract: Joi.string().optional(), recommended_symbol: Joi.string().optional(), + decimals: Joi.number().required(), }).unknown(true) ), }).unknown(true) @@ -49,6 +50,100 @@ export class ObservableQueryAssetsInner extends ObservableQuery makeObservable(this); } + @computed + get assetsRaw(): { + denom: string; + chainId: string; + originDenom: string; + originChainId: string; + isEvm: boolean; + tokenContract?: string; + recommendedSymbol?: string; + }[] { + if ( + !this.response || + !this.response.data || + !this.response.data.chain_to_assets_map + ) { + return []; + } + + if (!this.chainStore.hasChain(this.chainId)) { + return []; + } + + const chainInfo = this.chainStore.getChain(this.chainId); + if (!this.chainStore.isInChainInfosInListUI(chainInfo.chainId)) { + return []; + } + + const assetsInResponse = + this.response.data.chain_to_assets_map[ + this.chainId.replace("eip155:", "") + ]; + if (assetsInResponse) { + const res: { + denom: string; + chainId: string; + originDenom: string; + originChainId: string; + isEvm: boolean; + tokenContract?: string; + recommendedSymbol?: string; + }[] = []; + + for (const asset of assetsInResponse.assets) { + const chainId = asset.is_evm + ? `eip155:${asset.chain_id}` + : asset.chain_id; + const originChainId = asset.is_evm + ? `eip155:${asset.origin_chain_id}` + : asset.origin_chain_id; + if ( + this.chainStore.hasChain(chainId) && + this.chainStore.hasChain(originChainId) + ) { + // IBC asset일 경우 그냥 넣는다. + if (asset.denom.startsWith("ibc/")) { + res.push({ + denom: asset.denom, + chainId: chainId, + originDenom: asset.origin_denom, + originChainId: originChainId, + isEvm: false, + recommendedSymbol: asset.recommended_symbol, + }); + } else { + const coinMinimalDenom = + asset.is_evm && asset.token_contract != null + ? `erc20:${asset.denom}` + : asset.denom; + const originCoinMinimalDenom = + asset.is_evm && asset.token_contract != null + ? `erc20:${asset.origin_denom}` + : asset.denom; + // TODO: Dec, Int 같은 곳에서 18 이상인 경우도 고려하도록 수정 + if (asset.decimals <= 18) { + res.push({ + denom: coinMinimalDenom, + chainId: chainId, + originDenom: originCoinMinimalDenom, + originChainId: originChainId, + isEvm: asset.is_evm, + tokenContract: asset.token_contract, + recommendedSymbol: asset.recommended_symbol, + }); + } + } + } + } + + return res; + } + + return []; + } + @computed get assets(): { denom: string; diff --git a/apps/stores-internal/src/skip/ibc-swap.ts b/apps/stores-internal/src/skip/ibc-swap.ts index c589594928..b7e42e9792 100644 --- a/apps/stores-internal/src/skip/ibc-swap.ts +++ b/apps/stores-internal/src/skip/ibc-swap.ts @@ -613,17 +613,23 @@ export class ObservableQueryIbcSwap extends HasMapStore a.recommendedSymbol && a.recommendedSymbol === asset?.recommendedSymbol ); if (candidateAsset) { - res.push({ - denom: candidateAsset.denom, - chainId: candidateChain.chainInfo.chainId, - }); + const currencyFound = this.chainStore + .getChain(candidateChain.chainInfo.chainId) + .findCurrencyWithoutReaction(candidateAsset.denom); + + if (currencyFound) { + res.push({ + denom: candidateAsset.denom, + chainId: candidateChain.chainInfo.chainId, + }); + } } } } diff --git a/apps/stores-internal/src/skip/types.ts b/apps/stores-internal/src/skip/types.ts index 09c73eb969..3b81bd823d 100644 --- a/apps/stores-internal/src/skip/types.ts +++ b/apps/stores-internal/src/skip/types.ts @@ -25,6 +25,7 @@ export interface AssetsResponse { is_evm: boolean; token_contract?: string; recommended_symbol?: string; + decimals: number; }[]; } | undefined; From 0fc620bd2e2539bd4733f8ab507c858016e5183d Mon Sep 17 00:00:00 2001 From: delivan Date: Fri, 20 Dec 2024 23:31:34 +0900 Subject: [PATCH 18/43] Add circle cctp proto file --- packages/proto-types/outputHash | 2 +- .../proto/circle/cctp/v1/tx.proto | 405 ++++++++++++++++++ .../proto-types-gen/scripts/proto-gen.mjs | 1 + 3 files changed, 407 insertions(+), 1 deletion(-) create mode 100644 packages/proto-types/proto-types-gen/proto/circle/cctp/v1/tx.proto diff --git a/packages/proto-types/outputHash b/packages/proto-types/outputHash index 2b722f8c61..be709a8ff7 100644 --- a/packages/proto-types/outputHash +++ b/packages/proto-types/outputHash @@ -1 +1 @@ -47xVj6yH8GxPnu1BOIH1MiJVtlE6POLMYvtUZYTBu/5MPH+/VZKmDSx8zscGTLTJpxn9rxdR/+cPKXJdeaqXzulJEYlXTSU7x80PSUD7rlp2mWT0RYGFDIN+Fik5sFOv7Um1WRmAnzsEu+hDNatREyB47p1Gxms6JX0klLRyAyxUnUJRvDIjqQiAHvjtBGK0z5Fgtw08ei6MwIGQTf1GQHMO/FAQfd3uq8plIG5KNxinIvHEhmHGgaIg4xI/FZ/rWiyM34f1rcsIV73pCYbInRh3jcatX/QI80oBDMXBo9DCv5IaZQQs1G7JPcfzThPm \ No newline at end of file +47xVj6yH8GxPnu1BOIH1MiJVtlE6POLMYvtUZYTBu/5MPH+/VZKmDW4mzVCJYr7jyzpAUKPWeAkdNtxjLHzOxwZMtMmnGf2vF1H/5w8pcl15qpfO6UkRiVdNJTvHzQ9JQPuuWnaZZPRFgYUMg34WKTmwU6/tSbVZGYCfOwS76EM1q1ETIHjunUbGazolfSSUtHIDLFSdQlG8MiOpCIAe+O0EYrTPkWC3DTx6LozAgZBN/UZAcw78UBB93e6rymUgbko3GKci8cSGYcaBoiDjEj8Vn+taLIzfh/WtywhXvekJhsidGHeNxq1f9AjzSgEMxcGj0MK/khplBCzUbsk9x/NOE+Y= \ No newline at end of file diff --git a/packages/proto-types/proto-types-gen/proto/circle/cctp/v1/tx.proto b/packages/proto-types/proto-types-gen/proto/circle/cctp/v1/tx.proto new file mode 100644 index 0000000000..7bd4021ddb --- /dev/null +++ b/packages/proto-types/proto-types-gen/proto/circle/cctp/v1/tx.proto @@ -0,0 +1,405 @@ +syntax = "proto3"; + +package circle.cctp.v1; + +import "amino/amino.proto"; +import "cosmos/msg/v1/msg.proto"; +import "cosmos_proto/cosmos.proto"; +import "gogoproto/gogo.proto"; + +option go_package = "github.com/circlefin/noble-cctp/x/cctp/types"; + +// Msg defines the Msg service. +service Msg { + option (cosmos.msg.v1.service) = true; + + rpc AcceptOwner(MsgAcceptOwner) returns (MsgAcceptOwnerResponse); + rpc AddRemoteTokenMessenger(MsgAddRemoteTokenMessenger) returns (MsgAddRemoteTokenMessengerResponse); + rpc DepositForBurn(MsgDepositForBurn) returns (MsgDepositForBurnResponse); + rpc DepositForBurnWithCaller(MsgDepositForBurnWithCaller) returns (MsgDepositForBurnWithCallerResponse); + rpc DisableAttester(MsgDisableAttester) returns (MsgDisableAttesterResponse); + rpc EnableAttester(MsgEnableAttester) returns (MsgEnableAttesterResponse); + rpc LinkTokenPair(MsgLinkTokenPair) returns (MsgLinkTokenPairResponse); + rpc PauseBurningAndMinting(MsgPauseBurningAndMinting) returns (MsgPauseBurningAndMintingResponse); + rpc PauseSendingAndReceivingMessages(MsgPauseSendingAndReceivingMessages) returns (MsgPauseSendingAndReceivingMessagesResponse); + rpc ReceiveMessage(MsgReceiveMessage) returns (MsgReceiveMessageResponse); + rpc RemoveRemoteTokenMessenger(MsgRemoveRemoteTokenMessenger) returns (MsgRemoveRemoteTokenMessengerResponse); + rpc ReplaceDepositForBurn(MsgReplaceDepositForBurn) returns (MsgReplaceDepositForBurnResponse); + rpc ReplaceMessage(MsgReplaceMessage) returns (MsgReplaceMessageResponse); + rpc SendMessage(MsgSendMessage) returns (MsgSendMessageResponse); + rpc SendMessageWithCaller(MsgSendMessageWithCaller) returns (MsgSendMessageWithCallerResponse); + rpc UnlinkTokenPair(MsgUnlinkTokenPair) returns (MsgUnlinkTokenPairResponse); + rpc UnpauseBurningAndMinting(MsgUnpauseBurningAndMinting) returns (MsgUnpauseBurningAndMintingResponse); + rpc UnpauseSendingAndReceivingMessages(MsgUnpauseSendingAndReceivingMessages) returns (MsgUnpauseSendingAndReceivingMessagesResponse); + rpc UpdateOwner(MsgUpdateOwner) returns (MsgUpdateOwnerResponse); + rpc UpdateAttesterManager(MsgUpdateAttesterManager) returns (MsgUpdateAttesterManagerResponse); + rpc UpdateTokenController(MsgUpdateTokenController) returns (MsgUpdateTokenControllerResponse); + rpc UpdatePauser(MsgUpdatePauser) returns (MsgUpdatePauserResponse); + rpc UpdateMaxMessageBodySize(MsgUpdateMaxMessageBodySize) returns (MsgUpdateMaxMessageBodySizeResponse); + rpc SetMaxBurnAmountPerMessage(MsgSetMaxBurnAmountPerMessage) returns (MsgSetMaxBurnAmountPerMessageResponse); + rpc UpdateSignatureThreshold(MsgUpdateSignatureThreshold) returns (MsgUpdateSignatureThresholdResponse); +} + +message MsgUpdateOwner { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/UpdateOwner"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + string new_owner = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; +} + +message MsgUpdateOwnerResponse {} + +message MsgUpdateAttesterManager { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/UpdateAttesterManager"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + string new_attester_manager = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; +} + +message MsgUpdateAttesterManagerResponse {} + +message MsgUpdateTokenController { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/UpdateTokenController"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + string new_token_controller = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; +} + +message MsgUpdateTokenControllerResponse {} + +message MsgUpdatePauser { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/UpdatePauser"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + string new_pauser = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; +} + +message MsgUpdatePauserResponse {} + +message MsgAcceptOwner { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/AcceptOwner"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; +} + +message MsgAcceptOwnerResponse {} + +message MsgEnableAttester { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/EnableAttester"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + string attester = 2; +} + +message MsgEnableAttesterResponse {} + +message MsgDisableAttester { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/DisableAttester"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + string attester = 2; +} + +message MsgDisableAttesterResponse {} + +message MsgPauseBurningAndMinting { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/PauseBurningAndMinting"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; +} + +message MsgPauseBurningAndMintingResponse {} + +message MsgUnpauseBurningAndMinting { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/UnpauseBurningAndMinting"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; +} + +message MsgUnpauseBurningAndMintingResponse {} + +message MsgPauseSendingAndReceivingMessages { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/PauseSendingAndReceivingMessages"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; +} + +message MsgPauseSendingAndReceivingMessagesResponse {} + +message MsgUnpauseSendingAndReceivingMessages { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/UnpauseSendingAndReceivingMessages"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; +} + +message MsgUnpauseSendingAndReceivingMessagesResponse {} + +message MsgUpdateMaxMessageBodySize { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/UpdateMaxMessageBodySize"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + uint64 message_size = 2; +} + +message MsgUpdateMaxMessageBodySizeResponse {} + +message MsgSetMaxBurnAmountPerMessage { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/SetMaxBurnAmountPerMessage"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + string local_token = 2; + string amount = 3 [ + (gogoproto.customtype) = "cosmossdk.io/math.Int", + (gogoproto.nullable) = false + ]; +} + +message MsgSetMaxBurnAmountPerMessageResponse {} + +message MsgDepositForBurn { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/DepositForBurn"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + string amount = 2 [ + (gogoproto.customtype) = "cosmossdk.io/math.Int", + (gogoproto.nullable) = false + ]; + uint32 destination_domain = 3; + bytes mint_recipient = 4; + string burn_token = 5; +} + +message MsgDepositForBurnResponse { + uint64 nonce = 1; +} + +message MsgDepositForBurnWithCaller { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/DepositForBurnWithCaller"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + string amount = 2 [ + (gogoproto.customtype) = "cosmossdk.io/math.Int", + (gogoproto.nullable) = false + ]; + uint32 destination_domain = 3; + bytes mint_recipient = 4; + string burn_token = 5; + bytes destination_caller = 6; +} + +message MsgDepositForBurnWithCallerResponse { + uint64 nonce = 1; +} + +message MsgReplaceDepositForBurn { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/ReplaceDepositForBurn"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + bytes original_message = 2; + bytes original_attestation = 3; + bytes new_destination_caller = 4; + bytes new_mint_recipient = 5; +} + +message MsgReplaceDepositForBurnResponse {} + +message MsgReceiveMessage { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/ReceiveMessage"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + bytes message = 2; + bytes attestation = 3; +} + +message MsgReceiveMessageResponse { + bool success = 1; +} + +message MsgSendMessage { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/SendMessage"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + uint32 destination_domain = 2; + bytes recipient = 3; + bytes message_body = 4; +} + +message MsgSendMessageResponse { + uint64 nonce = 1; +} + +message MsgSendMessageWithCaller { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/SendMessageWithCaller"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + uint32 destination_domain = 2; + bytes recipient = 3; + bytes message_body = 4; + bytes destination_caller = 5; +} + +message MsgSendMessageWithCallerResponse { + uint64 nonce = 1; +} + +message MsgReplaceMessage { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/ReplaceMessage"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + bytes original_message = 2; + bytes original_attestation = 3; + bytes new_message_body = 4; + bytes new_destination_caller = 5; +} + +message MsgReplaceMessageResponse {} + +message MsgUpdateSignatureThreshold { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/UpdateSignatureThreshold"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + uint32 amount = 2; +} + +message MsgUpdateSignatureThresholdResponse {} + +message MsgLinkTokenPair { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/LinkTokenPair"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + uint32 remote_domain = 2; + bytes remote_token = 3; + string local_token = 4; +} + +message MsgLinkTokenPairResponse {} + +message MsgUnlinkTokenPair { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/UnlinkTokenPair"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + uint32 remote_domain = 2; + bytes remote_token = 3; + string local_token = 4; +} + +message MsgUnlinkTokenPairResponse {} + +message MsgAddRemoteTokenMessenger { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/AddRemoteTokenMessenger"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + uint32 domain_id = 2; + bytes address = 3; +} + +message MsgAddRemoteTokenMessengerResponse {} + +message MsgRemoveRemoteTokenMessenger { + option (cosmos.msg.v1.signer) = "from"; + option (amino.name) = "cctp/RemoveRemoteTokenMessenger"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + uint32 domain_id = 2; +} + +message MsgRemoveRemoteTokenMessengerResponse {} \ No newline at end of file diff --git a/packages/proto-types/proto-types-gen/scripts/proto-gen.mjs b/packages/proto-types/proto-types-gen/scripts/proto-gen.mjs index 9bd013f5af..2317e29d4a 100644 --- a/packages/proto-types/proto-types-gen/scripts/proto-gen.mjs +++ b/packages/proto-types/proto-types-gen/scripts/proto-gen.mjs @@ -122,6 +122,7 @@ function setOutputHash(root, hash) { "stride/stakeibc/tx.proto", "stride/staketia/tx.proto", "stride/stakedym/tx.proto", + "circle/cctp/v1/tx.proto", ]; const thirdPartyInputs = ["tendermint/crypto/keys.proto"]; From 149910193f3ca7af5a7c3cb69e9313ffd1a8340f Mon Sep 17 00:00:00 2001 From: delivan Date: Fri, 20 Dec 2024 23:32:49 +0900 Subject: [PATCH 19/43] Add cctp tx support from skip api --- apps/extension/src/pages/ibc-swap/index.tsx | 33 +++++- apps/hooks-internal/src/ibc-swap/amount.ts | 23 +++-- apps/stores-internal/src/skip/ibc-swap.ts | 2 +- apps/stores-internal/src/skip/msgs-direct.ts | 100 +++++++++++++++---- apps/stores-internal/src/skip/types.ts | 23 +++++ packages/stores/src/account/cosmos.ts | 61 +++++++++++ 6 files changed, 212 insertions(+), 30 deletions(-) diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index a822a29867..7fe4b2dda1 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -858,6 +858,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { try { if ("send" in tx) { + const isCCTPTx = tx.ui.type(); await tx.send( ibcSwapConfigs.feeConfig.toStdFee(), ibcSwapConfigs.memoConfig.memo, @@ -866,7 +867,37 @@ export const IBCSwapPage: FunctionComponent = observer(() => { preferNoSetMemo: false, sendTx: async (chainId, tx, mode) => { - if (ibcSwapConfigs.amountConfig.type === "transfer") { + if (isCCTPTx) { + // TODO: CCTP를 위한 msg 필요 + const msg: Message = new SendTxAndRecordMsg( + "ibc-swap/cctp", + chainId, + outChainId, + tx, + mode, + false, + ibcSwapConfigs.senderConfig.sender, + accountStore.getAccount(outChainId).bech32Address, + ibcSwapConfigs.amountConfig.amount.map((amount) => { + return { + amount: DecUtils.getTenExponentN( + amount.currency.coinDecimals + ) + .mul(amount.toDec()) + .toString(), + denom: amount.currency.coinMinimalDenom, + }; + }), + ibcSwapConfigs.memoConfig.memo, + true + ); + return await new InExtensionMessageRequester().sendMessage( + BACKGROUND_PORT, + msg + ); + } else if ( + ibcSwapConfigs.amountConfig.type === "transfer" + ) { const msg: Message = new SendTxAndRecordMsg( "ibc-swap/ibc-transfer", chainId, diff --git a/apps/hooks-internal/src/ibc-swap/amount.ts b/apps/hooks-internal/src/ibc-swap/amount.ts index 9723245f8d..e7be13f59c 100644 --- a/apps/hooks-internal/src/ibc-swap/amount.ts +++ b/apps/hooks-internal/src/ibc-swap/amount.ts @@ -463,6 +463,12 @@ export class IBCSwapAmountConfig extends AmountConfig { ...tx, requiredErc20Approvals: msg.requiredErc20Approvals, }; + } else if (msg.type === "MsgCCTP") { + const tx = sourceAccount.cosmos.makeCCTPTx( + msg.msgs[0].msg, + msg.msgs[1].msg + ); + return tx; } } @@ -495,13 +501,13 @@ export class IBCSwapAmountConfig extends AmountConfig { ): string { let key = ""; - for (const msg of response.msgs) { - if (msg.multi_chain_msg) { + for (const msg of response.txs) { + if (msg.cosmos_tx) { + const cosmosMsg = msg.cosmos_tx.msgs[0]; if ( - msg.multi_chain_msg.msg_type_url === - "/ibc.applications.transfer.v1.MsgTransfer" + cosmosMsg.msg_type_url === "/ibc.applications.transfer.v1.MsgTransfer" ) { - const memo = JSON.parse(msg.multi_chain_msg.msg).memo; + const memo = JSON.parse(cosmosMsg.msg).memo; if (memo) { const obj = JSON.parse(memo); const wasms: any = []; @@ -553,11 +559,8 @@ export class IBCSwapAmountConfig extends AmountConfig { } } } - if ( - msg.multi_chain_msg.msg_type_url === - "/cosmwasm.wasm.v1.MsgExecuteContract" - ) { - const obj = JSON.parse(msg.multi_chain_msg.msg); + if (cosmosMsg.msg_type_url === "/cosmwasm.wasm.v1.MsgExecuteContract") { + const obj = JSON.parse(cosmosMsg.msg); for (const operation of obj.msg.swap_and_action.user_swap .swap_exact_asset_in.operations) { key += `/${operation.pool}/${operation.denom_in}/${operation.denom_out}`; diff --git a/apps/stores-internal/src/skip/ibc-swap.ts b/apps/stores-internal/src/skip/ibc-swap.ts index b7e42e9792..a888e2a78d 100644 --- a/apps/stores-internal/src/skip/ibc-swap.ts +++ b/apps/stores-internal/src/skip/ibc-swap.ts @@ -599,7 +599,7 @@ export class ObservableQueryIbcSwap extends HasMapStore({ }).unknown(true) ) .required(), + txs: Joi.array() + .items( + Joi.object({ + cosmos_tx: Joi.object({ + chain_id: Joi.string().required(), + path: Joi.array().items(Joi.string()).required(), + msgs: Joi.array() + .items( + Joi.object({ + msg: Joi.string().required(), + msg_type_url: Joi.string().required(), + }).unknown(true) + ) + .required(), + signer_address: Joi.string().required(), + }).unknown(true), + evm_tx: Joi.object({ + chain_id: Joi.string().required(), + data: Joi.string().required(), + required_erc20_approvals: Joi.array() + .items( + Joi.object({ + amount: Joi.string().required(), + spender: Joi.string().required(), + token_contract: Joi.string().required(), + }).unknown(true) + ) + .required(), + signer_address: Joi.string().required(), + to: Joi.string().required(), + value: Joi.string().required(), + }).unknown(true), + }).unknown(true) + ) + .required(), }).unknown(true); export class ObservableQueryMsgsDirectInner extends ObservableQuery { @@ -95,20 +130,27 @@ export class ObservableQueryMsgsDirectInner extends ObservableQuery= 2) { + if (this.response.data.txs.length >= 2) { return; } - const msg = this.response.data.msgs[0]; + const msg = this.response.data.txs[0]; if (msg.evm_tx) { return { @@ -127,28 +169,36 @@ export class ObservableQueryMsgsDirectInner extends ObservableQuery { return new CoinPretty( this.chainGetter - .getChain(msg.multi_chain_msg!.chain_id) + .getChain(msg.cosmos_tx!.chain_id) .forceFindCurrency(fund.denom), fund.amount ); @@ -158,10 +208,9 @@ export class ObservableQueryMsgsDirectInner extends ObservableQuery { + if (tx.code == null || tx.code === 0) { + this.queries.queryBalances + .getQueryBech32Address(this.base.bech32Address) + .balances.forEach((queryBalance) => queryBalance.fetch()); + } + } + ); + } + protected get queries(): DeepReadonly { return this.queriesStore.get(this.chainId); } From 0132d95449cfe14a57cfd1254027abaacc8e6ac2 Mon Sep 17 00:00:00 2001 From: rowan Date: Sat, 21 Dec 2024 15:08:24 +0900 Subject: [PATCH 20/43] Add `estimated_route_duration_seconds` field to `RouteResponse` type --- apps/stores-internal/src/skip/route.ts | 9 ++++++++- apps/stores-internal/src/skip/types.ts | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/stores-internal/src/skip/route.ts b/apps/stores-internal/src/skip/route.ts index dd82ae1a83..d7dc3fdcec 100644 --- a/apps/stores-internal/src/skip/route.ts +++ b/apps/stores-internal/src/skip/route.ts @@ -185,12 +185,19 @@ const Schema = Joi.object({ chain_ids: Joi.array().items(Joi.string()).required(), does_swap: Joi.boolean(), estimated_amount_out: Joi.string(), + swap_price_impact_percent: Joi.string(), swap_venue: Joi.object({ name: Joi.string().required(), chain_id: Joi.string().required(), }).unknown(true), - swap_price_impact_percent: Joi.string(), + swap_venues: Joi.array().items( + Joi.object({ + name: Joi.string().required(), + chain_id: Joi.string().required(), + }).unknown(true) + ), txs_required: Joi.number().required(), + estimated_route_duration_seconds: Joi.number(), }).unknown(true); export class ObservableQueryRouteInner extends ObservableQuery { diff --git a/apps/stores-internal/src/skip/types.ts b/apps/stores-internal/src/skip/types.ts index a33aabd4a1..8d7d81bbef 100644 --- a/apps/stores-internal/src/skip/types.ts +++ b/apps/stores-internal/src/skip/types.ts @@ -204,6 +204,7 @@ export interface RouteResponse { chain_id: string; }[]; txs_required: number; + estimated_route_duration_seconds: number; } export interface ChainsResponse { From dffbc6376a0eb40c9ec11307d6333805a3bd41d7 Mon Sep 17 00:00:00 2001 From: rowan Date: Sat, 21 Dec 2024 17:11:19 +0900 Subject: [PATCH 21/43] Implement `RecordTxWithSkipSwapMsg` --- apps/extension/src/pages/ibc-swap/index.tsx | 144 ++++++++++++------ .../src/recent-send-history/handler.ts | 17 ++- .../src/recent-send-history/service.ts | 25 ++- .../recent-send-history/temp-skip-message.ts | 44 +++++- 4 files changed, 178 insertions(+), 52 deletions(-) diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index daa5d08389..09ced555b4 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -360,6 +360,8 @@ export const IBCSwapPage: FunctionComponent = observer(() => { if (queryRoute.response.data.operations.length > 0) { for (const operation of queryRoute.response.data.operations) { if ("swap" in operation) { + // CHECK: operation.swap.swap_in이 undefined로 가져와져서 + // 아래 swap_venue를 읽어올 때 에러가 발생함 const swapFeeBpsReceiverAddress = SwapFeeBps.receivers.find( (r) => r.chainId === operation.swap.swap_in.swap_venue.chain_id ); @@ -684,6 +686,8 @@ export const IBCSwapPage: FunctionComponent = observer(() => { let swapChannelIndex: number = -1; const swapReceiver: string[] = []; const swapFeeBpsReceiver: string[] = []; + let simpleRoute: string[] = []; + let routeDurationSeconds: number = 0; // queryRoute는 ibc history를 추적하기 위한 채널 정보 등을 얻기 위해서 사용된다. // /msgs_direct로도 얻을 순 있지만 따로 데이터를 해석해야되기 때문에 좀 힘들다... @@ -701,39 +705,64 @@ export const IBCSwapPage: FunctionComponent = observer(() => { if (!queryRoute.response) { throw new Error("queryRoute.response is undefined"); } - for (const operation of queryRoute.response.data.operations) { - if ("transfer" in operation) { - const queryClientState = queriesStore - .get(operation.transfer.chain_id) - .cosmos.queryIBCClientState.getClientState( - operation.transfer.port, - operation.transfer.channel - ); - await queryClientState.waitResponse(); - if (!queryClientState.response) { - throw new Error("queryClientState.response is undefined"); - } - if (!queryClientState.clientChainId) { - throw new Error( - "queryClientState.clientChainId is undefined" - ); + // bridge가 필요한 경우와, 아닌 경우를 나눠서 처리 + // swap, transfer 이외의 다른 operation이 있으면 bridge가 사용된다. + const operations = queryRoute.response.data.operations; + const isInterchainSwap = operations.some( + (operation) => + !("swap" in operation) && !("transfer" in operation) + ); + + if (isInterchainSwap) { + // TODO: 예상 시간이 없는 경우에 대한 처리 + routeDurationSeconds = + queryRoute.response.data.estimated_route_duration_seconds ?? 0; + + // CHECK: improve this logic + // 일단은 체인 id를 keplr에서 사용하는 형태로 바꿔서 사용한다. + simpleRoute = queryRoute.response.data.chain_ids.map((id) => { + const isEvmChain = chainStore.hasChain(`eip155:${id}`); + if (!isEvmChain && !chainStore.hasChain(id)) { + throw new Error("Chain not found"); } + return isEvmChain ? `eip155:${id}` : id; + }); + } else { + for (const operation of operations) { + if ("transfer" in operation) { + const queryClientState = queriesStore + .get(operation.transfer.chain_id) + .cosmos.queryIBCClientState.getClientState( + operation.transfer.port, + operation.transfer.channel + ); - channels.push({ - portId: operation.transfer.port, - channelId: operation.transfer.channel, - counterpartyChainId: queryClientState.clientChainId, - }); - } else if ("swap" in operation) { - const swapFeeBpsReceiverAddress = SwapFeeBps.receivers.find( - (r) => - r.chainId === operation.swap.swap_in.swap_venue.chain_id - ); - if (swapFeeBpsReceiverAddress) { - swapFeeBpsReceiver.push(swapFeeBpsReceiverAddress.address); + await queryClientState.waitResponse(); + if (!queryClientState.response) { + throw new Error("queryClientState.response is undefined"); + } + if (!queryClientState.clientChainId) { + throw new Error( + "queryClientState.clientChainId is undefined" + ); + } + + channels.push({ + portId: operation.transfer.port, + channelId: operation.transfer.channel, + counterpartyChainId: queryClientState.clientChainId, + }); + } else if ("swap" in operation) { + const swapFeeBpsReceiverAddress = SwapFeeBps.receivers.find( + (r) => + r.chainId === operation.swap.swap_in.swap_venue.chain_id + ); + if (swapFeeBpsReceiverAddress) { + swapFeeBpsReceiver.push(swapFeeBpsReceiverAddress.address); + } + swapChannelIndex = channels.length - 1; } - swapChannelIndex = channels.length - 1; } } @@ -1072,7 +1101,47 @@ export const IBCSwapPage: FunctionComponent = observer(() => { }, { onBroadcasted: (txHash) => { - const evmChainId = parseInt(inChainId.split(":")[1]); + const evmChainId = inChainId.replace("eip155:", ""); + // 2. create message and send it to the background script + // to create SkipHistory for displaying the transaction history in the home screen. + const msg = new RecordTxWithSkipSwapMsg( + inChainId, + outChainId, + { + chainId: outChainId, + denom: outCurrency.coinMinimalDenom, + }, + simpleRoute, + swapReceiver, + sender, + ibcSwapConfigs.amountConfig.amount.map((amount) => { + return { + amount: DecUtils.getTenExponentN( + amount.currency.coinDecimals + ) + .mul(amount.toDec()) + .toString(), + denom: amount.currency.coinMinimalDenom, + }; + }), + { + currencies: chainStore.getChain(outChainId).currencies, + }, + routeDurationSeconds, + true, + { + txHash, + chainId: evmChainId, + } + ); + + new InExtensionMessageRequester().sendMessage( + BACKGROUND_PORT, + msg + ); + }, + onFulfill: (txReceipt) => { + const evmChainId = inChainId.replace("eip155:", ""); // 1. track the transaction setTimeout(() => { @@ -1093,7 +1162,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { })(), }, body: JSON.stringify({ - tx_hash: txHash, + tx_hash: txReceipt.transactionHash, chain_id: evmChainId, }), } @@ -1108,19 +1177,6 @@ export const IBCSwapPage: FunctionComponent = observer(() => { }); }, 2000); - // 2. create message and send it to the background script - // to create IBCHistory for displaying the transaction history in the home screen. - - // 2-1. Define new message - const msg = new RecordTxWithSkipSwapMsg(); - - // 2-2. Send the message to the background script - new InExtensionMessageRequester().sendMessage( - BACKGROUND_PORT, - msg - ); - }, - onFulfill: (txReceipt) => { const queryBalances = queriesStore.get( ibcSwapConfigs.amountConfig.chainId ).queryBalances; diff --git a/packages/background/src/recent-send-history/handler.ts b/packages/background/src/recent-send-history/handler.ts index ec17e7ccdc..35823e0485 100644 --- a/packages/background/src/recent-send-history/handler.ts +++ b/packages/background/src/recent-send-history/handler.ts @@ -214,9 +214,20 @@ const handleClearAllIBCHistoryMsg: ( const handleRecordTxWithSkipSwapMsg: ( service: RecentSendHistoryService ) => InternalHandler = (service) => { - return async (_env, _msg) => { - // TODO: Implement this - return service.recordTxWithSkipSwap(); + return async (_env, msg) => { + return service.recordTxWithSkipSwap( + msg.sourceChainId, + msg.destinationChainId, + msg.destinationAsset, + msg.simpleRoute, + msg.swapReceiver, + msg.sender, + msg.amount, + msg.notificationInfo, + msg.routeDurationSeconds, + msg.isSkipTrack, + msg.trackParams + ); }; }; diff --git a/packages/background/src/recent-send-history/service.ts b/packages/background/src/recent-send-history/service.ts index af46a716cb..6284e2ad03 100644 --- a/packages/background/src/recent-send-history/service.ts +++ b/packages/background/src/recent-send-history/service.ts @@ -1067,7 +1067,30 @@ export class RecentSendHistoryService { } // skip related methods - recordTxWithSkipSwap(): string { + recordTxWithSkipSwap( + _sourceChainId: string, + _destinationChainId: string, + _destinationAsset: { + chainId: string; + denom: string; + }, + _simpleRoute: string[], + _swapReceiver: string[], + _sender: string, + _amount: { + amount: string; + denom: string; + }[], + _notificationInfo: { + currencies: AppCurrency[]; + }, + _routeDurationSeconds: number, + _isSkipTrack: boolean = false, + _trackParams: { + txHash: string; + chainId: string; + } + ): string { const id = (this.recentIBCHistorySeq++).toString(); // TODO: Implement this diff --git a/packages/background/src/recent-send-history/temp-skip-message.ts b/packages/background/src/recent-send-history/temp-skip-message.ts index f2b704f157..744ab92ef9 100644 --- a/packages/background/src/recent-send-history/temp-skip-message.ts +++ b/packages/background/src/recent-send-history/temp-skip-message.ts @@ -1,8 +1,8 @@ import { Message } from "@keplr-wallet/router"; import { ROUTE } from "./constants"; +import { AppCurrency } from "@keplr-wallet/types"; export type SkipHistory = { - chainId: string; // TODO: Define the properties of the skip history }; @@ -11,13 +11,49 @@ export class RecordTxWithSkipSwapMsg extends Message { return "record-tx-with-skip-swap"; } - // TODO: Define the properties of the message - constructor() { + constructor( + public readonly sourceChainId: string, + public readonly destinationChainId: string, + public readonly destinationAsset: { + chainId: string; + denom: string; + }, + public readonly simpleRoute: string[], + public readonly swapReceiver: string[], + public readonly sender: string, + public readonly amount: { + readonly amount: string; + readonly denom: string; + }[], + public readonly notificationInfo: { + currencies: AppCurrency[]; + }, + public readonly routeDurationSeconds: number, + public readonly isSkipTrack: boolean = false, + public readonly trackParams: { + txHash: string; + chainId: string; // e.g. cosmos - "cosmoshub-4", evm "4853" + } + ) { super(); } validateBasic(): void { - // TODO: Implement validation + if (!this.sourceChainId) { + throw new Error("chain id is empty"); + } + + if (!this.destinationChainId) { + throw new Error("chain id is empty"); + } + + if (!this.simpleRoute) { + throw new Error("simple route is empty"); + } + + if (!this.sender) { + throw new Error("sender is empty"); + } } route(): string { From 18d120b824c78966dff52356c42b35878f04a450 Mon Sep 17 00:00:00 2001 From: rowan Date: Sat, 21 Dec 2024 17:28:38 +0900 Subject: [PATCH 22/43] Implement `SkipHistory` type --- .../src/recent-send-history/service.ts | 3 +- .../recent-send-history/temp-skip-message.ts | 5 +-- .../src/recent-send-history/types.ts | 41 +++++++++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/packages/background/src/recent-send-history/service.ts b/packages/background/src/recent-send-history/service.ts index 6284e2ad03..6ee18e8408 100644 --- a/packages/background/src/recent-send-history/service.ts +++ b/packages/background/src/recent-send-history/service.ts @@ -15,12 +15,11 @@ import { toJS, } from "mobx"; import { KVStore, retry } from "@keplr-wallet/common"; -import { IBCHistory, RecentSendHistory } from "./types"; +import { IBCHistory, RecentSendHistory, SkipHistory } from "./types"; import { Buffer } from "buffer/"; import { AppCurrency, ChainInfo } from "@keplr-wallet/types"; import { CoinPretty } from "@keplr-wallet/unit"; import { simpleFetch } from "@keplr-wallet/simple-fetch"; -import { SkipHistory } from "./temp-skip-message"; export class RecentSendHistoryService { // Key: {chain_identifier}/{type} diff --git a/packages/background/src/recent-send-history/temp-skip-message.ts b/packages/background/src/recent-send-history/temp-skip-message.ts index 744ab92ef9..efafbce396 100644 --- a/packages/background/src/recent-send-history/temp-skip-message.ts +++ b/packages/background/src/recent-send-history/temp-skip-message.ts @@ -1,10 +1,7 @@ import { Message } from "@keplr-wallet/router"; import { ROUTE } from "./constants"; import { AppCurrency } from "@keplr-wallet/types"; - -export type SkipHistory = { - // TODO: Define the properties of the skip history -}; +import { SkipHistory } from "./types"; export class RecordTxWithSkipSwapMsg extends Message { public static type() { diff --git a/packages/background/src/recent-send-history/types.ts b/packages/background/src/recent-send-history/types.ts index 1a47634499..a97ad035f4 100644 --- a/packages/background/src/recent-send-history/types.ts +++ b/packages/background/src/recent-send-history/types.ts @@ -1,4 +1,5 @@ import { AppCurrency } from "@keplr-wallet/types"; +import { TransferAssetRelease } from "./temp-skip-types"; export interface RecentSendHistory { timestamp: number; @@ -92,3 +93,43 @@ export interface IBCSwapHistory { }[]; }; } + +export type SkipHistory = { + id: string; + chainId: string; + destinationChainId: string; + timestamp: number; + sender: string; + + amount: { + amount: string; + denom: string; + }[]; + txHash: string; + + txFulfilled?: boolean; + txError?: string; + + notified?: boolean; + notificationInfo?: { + currencies: AppCurrency[]; + }; + + swapReceiver: string[]; + + simpleRoute: string[]; + routeIndex: number; + routeDurationSeconds: number; + + destinationAsset: { + chainId: string; + denom: string; + }; + + resAmount: { + amount: string; + denom: string; + }[][]; + + transferAssetRelease?: TransferAssetRelease; +}; From a579912dbf7d3dbb66504143f5081b6158b7242b Mon Sep 17 00:00:00 2001 From: rowan Date: Sat, 21 Dec 2024 19:36:21 +0900 Subject: [PATCH 23/43] Modify `RecordTxWithSkipSwapMsg` and `SkipHistory` types --- apps/extension/src/pages/ibc-swap/index.tsx | 227 ++++++++++++------ .../components/ibc-history-view/index.tsx | 11 +- .../src/recent-send-history/handler.ts | 3 +- .../src/recent-send-history/service.ts | 51 ++-- .../recent-send-history/temp-skip-message.ts | 16 +- .../src/recent-send-history/types.ts | 5 +- 6 files changed, 209 insertions(+), 104 deletions(-) diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index 09ced555b4..48c6099897 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -686,8 +686,12 @@ export const IBCSwapPage: FunctionComponent = observer(() => { let swapChannelIndex: number = -1; const swapReceiver: string[] = []; const swapFeeBpsReceiver: string[] = []; - let simpleRoute: string[] = []; - let routeDurationSeconds: number = 0; + const simpleRoute: { + isOnlyEvm: boolean; + chainId: string; + receiver: string; + }[] = []; + let routeDurationSeconds: number | undefined; // queryRoute는 ibc history를 추적하기 위한 채널 정보 등을 얻기 위해서 사용된다. // /msgs_direct로도 얻을 순 있지만 따로 데이터를 해석해야되기 때문에 좀 힘들다... @@ -714,21 +718,79 @@ export const IBCSwapPage: FunctionComponent = observer(() => { !("swap" in operation) && !("transfer" in operation) ); + // 브릿지를 사용하는 경우, ibc swap channel까지 보여주면 ui가 너무 복잡해질 수 있으므로 + // evm -> osmosis -> destination 식으로 뭉퉁그려서 보여주는 것이 좋다고 판단, 경로를 간소화한다. + // 문제는 chain_ids에 이미 ibc swap channel이 포함되어 있을 가능성 (아직 확인은 안됨) if (isInterchainSwap) { - // TODO: 예상 시간이 없는 경우에 대한 처리 routeDurationSeconds = - queryRoute.response.data.estimated_route_duration_seconds ?? 0; + queryRoute.response.data.estimated_route_duration_seconds; - // CHECK: improve this logic - // 일단은 체인 id를 keplr에서 사용하는 형태로 바꿔서 사용한다. - simpleRoute = queryRoute.response.data.chain_ids.map((id) => { - const isEvmChain = chainStore.hasChain(`eip155:${id}`); - if (!isEvmChain && !chainStore.hasChain(id)) { + // 일단은 체인 id를 keplr에서 사용하는 형태로 바꿔야 한다. + for (const chainId of queryRoute.response.data.chain_ids) { + const isOnlyEvm = chainStore.hasChain(`eip155:${chainId}`); + if (!isOnlyEvm && !chainStore.hasChain(chainId)) { throw new Error("Chain not found"); } - return isEvmChain ? `eip155:${id}` : id; - }); + + if (isOnlyEvm) { + const receiverAccount = accountStore.getAccount( + `eip155:${chainId}` + ); + + const ethereumHexAddress = + receiverAccount.hasEthereumHexAddress + ? receiverAccount.ethereumHexAddress + : undefined; + + if (!ethereumHexAddress) { + throw new Error("ethereumHexAddress is undefined"); + } + + simpleRoute.push({ + isOnlyEvm, + chainId: `eip155:${chainId}`, + receiver: ethereumHexAddress, + }); + } else { + const receiverAccount = accountStore.getAccount(chainId); + + if (receiverAccount.walletStatus !== WalletStatus.Loaded) { + await receiverAccount.init(); + } + + if (!receiverAccount.bech32Address) { + const receiverChainInfo = + chainStore.hasChain(chainId) && + chainStore.getChain(chainId); + if ( + receiverAccount.isNanoLedger && + receiverChainInfo && + (receiverChainInfo.bip44.coinType === 60 || + receiverChainInfo.features.includes( + "eth-address-gen" + ) || + receiverChainInfo.features.includes("eth-key-sign") || + receiverChainInfo.evm != null) + ) { + throw new Error( + "Please connect Ethereum app on Ledger with Keplr to get the address" + ); + } + + throw new Error( + "receiverAccount.bech32Address is undefined" + ); + } + + simpleRoute.push({ + isOnlyEvm, + chainId, + receiver: receiverAccount.bech32Address, + }); + } + } } else { + // 브릿지를 사용하지 않는 경우, 자세한 ibc swap channel 정보를 보여준다. for (const operation of operations) { if ("transfer" in operation) { const queryClientState = queriesStore @@ -764,38 +826,39 @@ export const IBCSwapPage: FunctionComponent = observer(() => { swapChannelIndex = channels.length - 1; } } - } - const receiverChainIds = [inChainId]; - for (const channel of channels) { - receiverChainIds.push(channel.counterpartyChainId); - } - for (const receiverChainId of receiverChainIds) { - const receiverAccount = accountStore.getAccount(receiverChainId); - if (receiverAccount.walletStatus !== WalletStatus.Loaded) { - await receiverAccount.init(); + const receiverChainIds = [inChainId]; + for (const channel of channels) { + receiverChainIds.push(channel.counterpartyChainId); } - - if (!receiverAccount.bech32Address) { - const receiverChainInfo = - chainStore.hasChain(receiverChainId) && - chainStore.getChain(receiverChainId); - if ( - receiverAccount.isNanoLedger && - receiverChainInfo && - (receiverChainInfo.bip44.coinType === 60 || - receiverChainInfo.features.includes("eth-address-gen") || - receiverChainInfo.features.includes("eth-key-sign") || - receiverChainInfo.evm != null) - ) { - throw new Error( - "Please connect Ethereum app on Ledger with Keplr to get the address" - ); + for (const receiverChainId of receiverChainIds) { + const receiverAccount = + accountStore.getAccount(receiverChainId); + if (receiverAccount.walletStatus !== WalletStatus.Loaded) { + await receiverAccount.init(); } - throw new Error("receiverAccount.bech32Address is undefined"); + if (!receiverAccount.bech32Address) { + const receiverChainInfo = + chainStore.hasChain(receiverChainId) && + chainStore.getChain(receiverChainId); + if ( + receiverAccount.isNanoLedger && + receiverChainInfo && + (receiverChainInfo.bip44.coinType === 60 || + receiverChainInfo.features.includes("eth-address-gen") || + receiverChainInfo.features.includes("eth-key-sign") || + receiverChainInfo.evm != null) + ) { + throw new Error( + "Please connect Ethereum app on Ledger with Keplr to get the address" + ); + } + + throw new Error("receiverAccount.bech32Address is undefined"); + } + swapReceiver.push(receiverAccount.bech32Address); } - swapReceiver.push(receiverAccount.bech32Address); } const [_tx] = await Promise.all([ @@ -1101,38 +1164,47 @@ export const IBCSwapPage: FunctionComponent = observer(() => { }, { onBroadcasted: (txHash) => { - const evmChainId = inChainId.replace("eip155:", ""); - // 2. create message and send it to the background script - // to create SkipHistory for displaying the transaction history in the home screen. const msg = new RecordTxWithSkipSwapMsg( inChainId, outChainId, { chainId: outChainId, denom: outCurrency.coinMinimalDenom, + expectedAmount: ibcSwapConfigs.amountConfig.outAmount + .toDec() + .toString(), }, simpleRoute, - swapReceiver, sender, - ibcSwapConfigs.amountConfig.amount.map((amount) => { - return { + [ + ...ibcSwapConfigs.amountConfig.amount.map((amount) => { + return { + amount: DecUtils.getTenExponentN( + amount.currency.coinDecimals + ) + .mul(amount.toDec()) + .toString(), + denom: amount.currency.coinMinimalDenom, + }; + }), + { amount: DecUtils.getTenExponentN( - amount.currency.coinDecimals + ibcSwapConfigs.amountConfig.outAmount.currency + .coinDecimals ) - .mul(amount.toDec()) + .mul(ibcSwapConfigs.amountConfig.outAmount.toDec()) .toString(), - denom: amount.currency.coinMinimalDenom, - }; - }), + denom: + ibcSwapConfigs.amountConfig.outAmount.currency + .coinMinimalDenom, + }, + ], { currencies: chainStore.getChain(outChainId).currencies, }, - routeDurationSeconds, + routeDurationSeconds ?? 0, true, - { - txHash, - chainId: evmChainId, - } + txHash ); new InExtensionMessageRequester().sendMessage( @@ -1141,9 +1213,30 @@ export const IBCSwapPage: FunctionComponent = observer(() => { ); }, onFulfill: (txReceipt) => { - const evmChainId = inChainId.replace("eip155:", ""); + const queryBalances = queriesStore.get( + ibcSwapConfigs.amountConfig.chainId + ).queryBalances; + queryBalances + .getQueryEthereumHexAddress(sender) + .balances.forEach((balance) => { + if ( + balance.currency.coinMinimalDenom === + ibcSwapConfigs.amountConfig.currency + .coinMinimalDenom || + ibcSwapConfigs.feeConfig.fees.some( + (fee) => + fee.currency.coinMinimalDenom === + balance.currency.coinMinimalDenom + ) + ) { + balance.fetch(); + } + }); + + // onBroadcasted에서 실행하기에는 너무 빨라서 tx not found가 발생할 수 있음 + // 재실행 로직을 사용해도 되지만, txReceipt를 기다렸다가 실행하는 게 자원 낭비가 적음 + const chainIdForTrack = inChainId.replace("eip155:", ""); - // 1. track the transaction setTimeout(() => { // no wait simpleFetch( @@ -1163,7 +1256,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { }, body: JSON.stringify({ tx_hash: txReceipt.transactionHash, - chain_id: evmChainId, + chain_id: chainIdForTrack, }), } ) @@ -1176,26 +1269,6 @@ export const IBCSwapPage: FunctionComponent = observer(() => { console.log(e); }); }, 2000); - - const queryBalances = queriesStore.get( - ibcSwapConfigs.amountConfig.chainId - ).queryBalances; - queryBalances - .getQueryEthereumHexAddress(sender) - .balances.forEach((balance) => { - if ( - balance.currency.coinMinimalDenom === - ibcSwapConfigs.amountConfig.currency - .coinMinimalDenom || - ibcSwapConfigs.feeConfig.fees.some( - (fee) => - fee.currency.coinMinimalDenom === - balance.currency.coinMinimalDenom - ) - ) { - balance.fetch(); - } - }); if (txReceipt.status === EthTxStatus.Success) { notification.show( "success", diff --git a/apps/extension/src/pages/main/components/ibc-history-view/index.tsx b/apps/extension/src/pages/main/components/ibc-history-view/index.tsx index 72809934aa..7ec0f89e2e 100644 --- a/apps/extension/src/pages/main/components/ibc-history-view/index.tsx +++ b/apps/extension/src/pages/main/components/ibc-history-view/index.tsx @@ -35,6 +35,7 @@ import { useSpringValue, animated, easings } from "@react-spring/web"; import { defaultSpringConfig } from "../../../../styles/spring"; import { VerticalCollapseTransition } from "../../../../components/transition/vertical-collapse"; import { FormattedMessage, useIntl } from "react-intl"; +import { GetSkipHistoriesMsg } from "@keplr-wallet/background/build/recent-send-history/temp-skip-message"; export const IbcHistoryView: FunctionComponent<{ isNotReady: boolean; @@ -45,9 +46,9 @@ export const IbcHistoryView: FunctionComponent<{ useLayoutEffectOnce(() => { let count = 0; const alreadyCompletedHistoryMap = new Map(); + const requester = new InExtensionMessageRequester(); const fn = () => { - const requester = new InExtensionMessageRequester(); const msg = new GetIBCHistoriesMsg(); requester.sendMessage(BACKGROUND_PORT, msg).then((newHistories) => { setHistories((histories) => { @@ -97,7 +98,15 @@ export const IbcHistoryView: FunctionComponent<{ }); }; + const skipFn = () => { + const msg = new GetSkipHistoriesMsg(); + requester.sendMessage(BACKGROUND_PORT, msg).then((histories) => { + console.log("skip histories", histories); + }); + }; + fn(); + skipFn(); const interval = setInterval(fn, 1000); return () => { diff --git a/packages/background/src/recent-send-history/handler.ts b/packages/background/src/recent-send-history/handler.ts index 35823e0485..a533415d1b 100644 --- a/packages/background/src/recent-send-history/handler.ts +++ b/packages/background/src/recent-send-history/handler.ts @@ -220,13 +220,12 @@ const handleRecordTxWithSkipSwapMsg: ( msg.destinationChainId, msg.destinationAsset, msg.simpleRoute, - msg.swapReceiver, msg.sender, msg.amount, msg.notificationInfo, msg.routeDurationSeconds, msg.isSkipTrack, - msg.trackParams + msg.txHash ); }; }; diff --git a/packages/background/src/recent-send-history/service.ts b/packages/background/src/recent-send-history/service.ts index 6ee18e8408..1a6e5e78c1 100644 --- a/packages/background/src/recent-send-history/service.ts +++ b/packages/background/src/recent-send-history/service.ts @@ -1067,32 +1067,53 @@ export class RecentSendHistoryService { // skip related methods recordTxWithSkipSwap( - _sourceChainId: string, - _destinationChainId: string, - _destinationAsset: { + sourceChainId: string, + destinationChainId: string, + destinationAsset: { chainId: string; denom: string; + expectedAmount: string; }, - _simpleRoute: string[], - _swapReceiver: string[], - _sender: string, - _amount: { + simpleRoute: { + isOnlyEvm: boolean; + chainId: string; + receiver: string; + }[], + sender: string, + amount: { amount: string; denom: string; }[], - _notificationInfo: { + notificationInfo: { currencies: AppCurrency[]; }, - _routeDurationSeconds: number, - _isSkipTrack: boolean = false, - _trackParams: { - txHash: string; - chainId: string; - } + routeDurationSeconds: number = 0, + isSkipTrack: boolean = false, + txHash: string ): string { const id = (this.recentIBCHistorySeq++).toString(); - // TODO: Implement this + const history: SkipHistory = { + id, + chainId: sourceChainId, + destinationChainId: destinationChainId, + destinationAsset, + simpleRoute, + sender, + amount, + notificationInfo, + routeDurationSeconds, + txHash, + routeIndex: 0, + resAmount: [], + timestamp: Date.now(), + }; + + this.recentSkipHistoryMap.set(id, history); + + if (isSkipTrack) { + // TODO: run skip track + } return id; } diff --git a/packages/background/src/recent-send-history/temp-skip-message.ts b/packages/background/src/recent-send-history/temp-skip-message.ts index efafbce396..a0334d44d6 100644 --- a/packages/background/src/recent-send-history/temp-skip-message.ts +++ b/packages/background/src/recent-send-history/temp-skip-message.ts @@ -14,10 +14,17 @@ export class RecordTxWithSkipSwapMsg extends Message { public readonly destinationAsset: { chainId: string; denom: string; + expectedAmount: string; }, - public readonly simpleRoute: string[], - public readonly swapReceiver: string[], + public readonly simpleRoute: { + isOnlyEvm: boolean; + chainId: string; + receiver: string; + }[], public readonly sender: string, + + // amount 대신 amountIn, amountOut을 사용하도록 변경 + public readonly amount: { readonly amount: string; readonly denom: string; @@ -27,10 +34,7 @@ export class RecordTxWithSkipSwapMsg extends Message { }, public readonly routeDurationSeconds: number, public readonly isSkipTrack: boolean = false, - public readonly trackParams: { - txHash: string; - chainId: string; // e.g. cosmos - "cosmoshub-4", evm "4853" - } + public readonly txHash: string ) { super(); } diff --git a/packages/background/src/recent-send-history/types.ts b/packages/background/src/recent-send-history/types.ts index a97ad035f4..f0a8c1b470 100644 --- a/packages/background/src/recent-send-history/types.ts +++ b/packages/background/src/recent-send-history/types.ts @@ -115,15 +115,14 @@ export type SkipHistory = { currencies: AppCurrency[]; }; - swapReceiver: string[]; - - simpleRoute: string[]; + simpleRoute: { isOnlyEvm: boolean; chainId: string; receiver: string }[]; routeIndex: number; routeDurationSeconds: number; destinationAsset: { chainId: string; denom: string; + expectedAmount: string; }; resAmount: { From a953b4ba03b22009a369327af4f4e4b4d6e45cb8 Mon Sep 17 00:00:00 2001 From: rowan Date: Sun, 22 Dec 2024 00:44:39 +0900 Subject: [PATCH 24/43] Enable to save and load skip history from kvstore --- .../src/recent-send-history/service.ts | 73 +++++++++++++++++-- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/packages/background/src/recent-send-history/service.ts b/packages/background/src/recent-send-history/service.ts index 1a6e5e78c1..00f5b05f08 100644 --- a/packages/background/src/recent-send-history/service.ts +++ b/packages/background/src/recent-send-history/service.ts @@ -35,7 +35,7 @@ export class RecentSendHistoryService { @observable protected recentSkipHistorySeq: number = 0; - // Key: id (sequence, it should be increased by 1 for each + // Key: id (sequence, it should be increased by 1 for each) @observable protected readonly recentSkipHistoryMap: Map = new Map(); @@ -116,8 +116,54 @@ export class RecentSendHistoryService { this.trackIBCPacketForwardingRecursive(history.id); } - // TODO: Load skip history from storage - // and track unfinished skip history if exists + // Load skip history sequence from the storage + const recentSkipHistorySeqSaved = await this.kvStore.get( + "recentSkipHistorySeq" + ); + + if (recentSkipHistorySeqSaved) { + // Set the loaded sequence to the observable + runInAction(() => { + this.recentSkipHistorySeq = recentSkipHistorySeqSaved; + }); + } + + // Save the sequence to the storage when the sequence is changed + autorun(() => { + const js = toJS(this.recentSkipHistorySeq); + this.kvStore.set("recentSkipHistorySeq", js); + }); + + // Load skip history from the storage + const recentSkipHistoryMapSaved = await this.kvStore.get< + Record + >("recentSkipHistoryMap"); + if (recentSkipHistoryMapSaved) { + runInAction(() => { + let entries = Object.entries(recentSkipHistoryMapSaved); + entries = entries.sort(([, a], [, b]) => { + return parseInt(a.id) - parseInt(b.id); + }); + for (const [key, value] of entries) { + this.recentSkipHistoryMap.set(key, value); + } + }); + } + + // Save the skip history to the storage when the skip history is changed + autorun(() => { + const js = toJS(this.recentSkipHistoryMap); + const obj = Object.fromEntries(js); + this.kvStore.set>( + "recentSkipHistoryMap", + obj + ); + }); + + // Track the recent skip history + for (const _history of this.getRecentSkipHistories()) { + // TODO: Implement the logic to track the skip history + } this.chainsService.addChainRemovedHandler(this.onChainRemoved); } @@ -1123,8 +1169,25 @@ export class RecentSendHistoryService { } getRecentSkipHistories(): SkipHistory[] { - // TODO: Implement this - return Array.from(this.recentSkipHistoryMap.values()); + return Array.from(this.recentSkipHistoryMap.values()).filter((history) => { + if (!this.chainsService.hasChainInfo(history.chainId)) { + return false; + } + + if (!this.chainsService.hasChainInfo(history.destinationChainId)) { + return false; + } + + if ( + history.simpleRoute.some((route) => { + return !this.chainsService.hasChainInfo(route.chainId); + }) + ) { + return false; + } + + return true; + }); } @action From cc2addb2d8633c45fce57ed0f78daa5a88afcab4 Mon Sep 17 00:00:00 2001 From: rowan Date: Sun, 22 Dec 2024 15:31:55 +0900 Subject: [PATCH 25/43] Add stub `trackSkipSwapRecursive` method --- apps/extension/src/pages/ibc-swap/index.tsx | 3 +- .../components/ibc-history-view/index.tsx | 1 + apps/stores-internal/src/skip/route.ts | 4 + .../src/recent-send-history/handler.ts | 1 - .../src/recent-send-history/service.ts | 181 +++++++++++++++++- .../recent-send-history/temp-skip-message.ts | 1 - .../src/recent-send-history/types.ts | 13 +- 7 files changed, 187 insertions(+), 17 deletions(-) diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index 48c6099897..bb3051a5b0 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -718,7 +718,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { !("swap" in operation) && !("transfer" in operation) ); - // 브릿지를 사용하는 경우, ibc swap channel까지 보여주면 ui가 너무 복잡해질 수 있으므로 + // 브릿지를 사용하는 경우, ibc swap channel까지 보여주면 ui가 너무 복잡해질 수 있으므로 (operation이 최소 3개 이상) // evm -> osmosis -> destination 식으로 뭉퉁그려서 보여주는 것이 좋다고 판단, 경로를 간소화한다. // 문제는 chain_ids에 이미 ibc swap channel이 포함되어 있을 가능성 (아직 확인은 안됨) if (isInterchainSwap) { @@ -1203,7 +1203,6 @@ export const IBCSwapPage: FunctionComponent = observer(() => { currencies: chainStore.getChain(outChainId).currencies, }, routeDurationSeconds ?? 0, - true, txHash ); diff --git a/apps/extension/src/pages/main/components/ibc-history-view/index.tsx b/apps/extension/src/pages/main/components/ibc-history-view/index.tsx index 7ec0f89e2e..2d6084bbae 100644 --- a/apps/extension/src/pages/main/components/ibc-history-view/index.tsx +++ b/apps/extension/src/pages/main/components/ibc-history-view/index.tsx @@ -101,6 +101,7 @@ export const IbcHistoryView: FunctionComponent<{ const skipFn = () => { const msg = new GetSkipHistoriesMsg(); requester.sendMessage(BACKGROUND_PORT, msg).then((histories) => { + // TODO: 메시지 들어오는 것 까지 확인. 백그라운드에서 polling 돌리고 가져온 메시지 보여주는 작업 추가 console.log("skip histories", histories); }); }; diff --git a/apps/stores-internal/src/skip/route.ts b/apps/stores-internal/src/skip/route.ts index d7dc3fdcec..1ece203d3f 100644 --- a/apps/stores-internal/src/skip/route.ts +++ b/apps/stores-internal/src/skip/route.ts @@ -264,11 +264,15 @@ export class ObservableQueryRouteInner extends ObservableQuery { fee: string; venueChainId: string; }[] = []; + for (const operation of this.response.data.operations) { if ("swap" in operation) { estimatedAffiliateFees.push({ fee: operation.swap.estimated_affiliate_fee, // QUESTION: swap_out이 생기면...? + // TODO: swap operation에 swap_in이 없을 수 있고, swap_out이 있을 수 있음. 둘 중 하나는 있고 둘 다 있을 수도 있음. 이와 관련해서 수정이 필요함. + // smart_swap_in이라는 것도 추가됨 + // https://github.com/skip-mev/skip-go/blob/38b19cb92e64870b5424f741a3022669381d829c/packages/client/src/types/shared.ts#L462 venueChainId: operation.swap.swap_in.swap_venue.chain_id, }); } diff --git a/packages/background/src/recent-send-history/handler.ts b/packages/background/src/recent-send-history/handler.ts index a533415d1b..8b0a24235b 100644 --- a/packages/background/src/recent-send-history/handler.ts +++ b/packages/background/src/recent-send-history/handler.ts @@ -224,7 +224,6 @@ const handleRecordTxWithSkipSwapMsg: ( msg.amount, msg.notificationInfo, msg.routeDurationSeconds, - msg.isSkipTrack, msg.txHash ); }; diff --git a/packages/background/src/recent-send-history/service.ts b/packages/background/src/recent-send-history/service.ts index 00f5b05f08..d6957bb3ae 100644 --- a/packages/background/src/recent-send-history/service.ts +++ b/packages/background/src/recent-send-history/service.ts @@ -20,6 +20,11 @@ import { Buffer } from "buffer/"; import { AppCurrency, ChainInfo } from "@keplr-wallet/types"; import { CoinPretty } from "@keplr-wallet/unit"; import { simpleFetch } from "@keplr-wallet/simple-fetch"; +import { + StatusRequest, + TransferAssetRelease, + TxStatusResponse, +} from "./temp-skip-types"; export class RecentSendHistoryService { // Key: {chain_identifier}/{type} @@ -161,8 +166,8 @@ export class RecentSendHistoryService { }); // Track the recent skip history - for (const _history of this.getRecentSkipHistories()) { - // TODO: Implement the logic to track the skip history + for (const history of this.getRecentSkipHistories()) { + this.trackSkipSwapRecursive(history.id); } this.chainsService.addChainRemovedHandler(this.onChainRemoved); @@ -590,6 +595,7 @@ export class RecentSendHistoryService { const txTracer = new TendermintTxTracer(chainInfo.rpc, "/websocket"); txTracer.addEventListener("close", onClose); txTracer.addEventListener("error", onError); + txTracer.traceTx(txHash).then((tx) => { txTracer.close(); @@ -1134,7 +1140,6 @@ export class RecentSendHistoryService { currencies: AppCurrency[]; }, routeDurationSeconds: number = 0, - isSkipTrack: boolean = false, txHash: string ): string { const id = (this.recentIBCHistorySeq++).toString(); @@ -1148,18 +1153,15 @@ export class RecentSendHistoryService { sender, amount, notificationInfo, - routeDurationSeconds, + routeDurationSeconds: routeDurationSeconds, txHash, - routeIndex: 0, + routeIndex: -1, resAmount: [], timestamp: Date.now(), }; this.recentSkipHistoryMap.set(id, history); - - if (isSkipTrack) { - // TODO: run skip track - } + this.trackSkipSwapRecursive(id); return id; } @@ -1190,6 +1192,167 @@ export class RecentSendHistoryService { }); } + trackSkipSwapRecursive(id: string): void { + const history = this.getRecentSkipHistory(id); + if (!history) { + return; + } + + const now = Date.now(); + const expectedEndTimestamp = + history.timestamp + history.routeDurationSeconds * 1000; + const diff = expectedEndTimestamp - now; + + const waitMsAfterError = 10 * 1000; + const maxRetries = diff > 0 ? (diff / waitMsAfterError) * 2 : 10; + + retry( + () => { + return new Promise((resolve, reject) => { + this.trackSkipSwapRecursiveInternal( + id, + () => { + resolve(); + }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + () => {}, + () => { + reject(); + } + ); + }); + }, + { + maxRetries, + waitMsAfterError, + maxWaitMsAfterError: 5 * 60 * 1000, // 5min + } + ); + } + + protected trackSkipSwapRecursiveInternal = ( + id: string, + onFulfill: () => void, + _onClose: () => void, + onError: () => void + ): void => { + const history = this.getRecentSkipHistory(id); + if (!history) { + onFulfill(); + return; + } + + if (history.trackDone) { + onFulfill(); + return; + } + + const chainId = history.chainId.replace("eip155:", ""); + const request: StatusRequest = { + tx_hash: history.txHash, + chain_id: chainId, + }; + const requestParams = new URLSearchParams(request).toString(); + + simpleFetch( + "https://api.skip.build/", + `v2/tx/status?${requestParams}`, + { + method: "GET", + headers: { + "content-type": "application/json", + ...(() => { + const res: { authorization?: string } = {}; + if (process.env["SKIP_API_KEY"]) { + res.authorization = process.env["SKIP_API_KEY"]; + } + return res; + })(), + }, + } + ) + .then((res) => { + const { + status, + state, + error, + transfer_sequence, + next_blocking_transfer, + transfer_asset_release, + } = res.data; + + history.trackStatus = status; // 상태 업데이트 + + // state는 status와 비슷하지만 상위 범주의 상태를 나타냄 + // e.g. state가 completed이면 status는 completed, completed_error, completed_success 중 하나 + + // history 업데이트 함수 + const handleHistoryUpdate = () => { + const simpleRoute = history.simpleRoute; + const currentRouteIndex = history.routeIndex; + const errorMessage: string | undefined = error?.message; // detail한 에러 메시지는 transfer에서 가져옴 + const transferAssetRelease: TransferAssetRelease | null = + transfer_asset_release; + + const nextBlockingTransferIndex = + next_blocking_transfer?.transfer_sequence_index ?? -1; + + // 다음 블로킹 트랜스퍼 정보가 있는 경우 (다음 블로킹 트랜스퍼가 없으면 -1) + if (nextBlockingTransferIndex >= 0) { + // transfer_sequence의 길이 체크 + if (nextBlockingTransferIndex >= simpleRoute.length) { + history.trackError = "Invalid next_blocking_transfer index"; + // TODO: 어떻게 처리할지 고민 필요, 일단은 에러 처리 + onError(); + return; + } + + // 다음 블로킹 트랜스퍼 정보 가져오기 + const transfer = transfer_sequence[nextBlockingTransferIndex]; + } + + history.trackError = errorMessage; + if (transferAssetRelease) { + history.transferAssetRelease = transferAssetRelease; + } + + switch (state) { + case "STATE_ABANDONED": // 30분 이상 트래킹했는데 상태 변화가 없는 경우. 문제가 뭔지 진단하기 어려움. 웬만하면 발생하지 않는다고 optimistic하게 + case "STATE_COMPLETED_ERROR": // 오류가 발생하였고, 오류 전파 및 처리가 완료된 상태 + case "STATE_COMPLETED_SUCCESS": // 성공적으로 완료된 상태 + // 더 이상 트래킹할 필요가 없는 상태 + history.trackDone = true; + onFulfill(); + break; + case "STATE_PENDING": // route 진행 중 + case "STATE_PENDING_ERROR": // route 진행 중에 에러가 발생하였고 오류가 전파되는 중 + // 다시 트래킹 요청 (retry) + onError(); + } + }; + + switch (status) { + case "STATE_SUBMITTED": + case "STATE_RECEIVED": + case "STATE_COMPLETED": + case "STATE_UNKNOWN": + // 트래킹 요청이 성공적으로 전달되었지만, 아직 트래킹이 시작되지 않은 상태 + // 또는 트래킹이 완료되었지만, 결과가 아직 처리되지 않은 상태 + // `UNKNOWN`은 상태를 알 수 없는 경우 (TODO: 어떻게 처리할지 고민 필요) + // 다음 상태가 주어질 때까지 일단 오류 처리하고 retry 로직이 동작하도록 한다. + onError(); + break; + default: + handleHistoryUpdate(); + break; + } + }) + .catch((e) => { + console.error(e); + onError(); + }); + }; + @action removeRecentSkipHistory(id: string): boolean { return this.recentSkipHistoryMap.delete(id); diff --git a/packages/background/src/recent-send-history/temp-skip-message.ts b/packages/background/src/recent-send-history/temp-skip-message.ts index a0334d44d6..287e99d41a 100644 --- a/packages/background/src/recent-send-history/temp-skip-message.ts +++ b/packages/background/src/recent-send-history/temp-skip-message.ts @@ -33,7 +33,6 @@ export class RecordTxWithSkipSwapMsg extends Message { currencies: AppCurrency[]; }, public readonly routeDurationSeconds: number, - public readonly isSkipTrack: boolean = false, public readonly txHash: string ) { super(); diff --git a/packages/background/src/recent-send-history/types.ts b/packages/background/src/recent-send-history/types.ts index f0a8c1b470..1299803969 100644 --- a/packages/background/src/recent-send-history/types.ts +++ b/packages/background/src/recent-send-history/types.ts @@ -1,5 +1,5 @@ import { AppCurrency } from "@keplr-wallet/types"; -import { TransferAssetRelease } from "./temp-skip-types"; +import { StatusState, TransferAssetRelease } from "./temp-skip-types"; export interface RecentSendHistory { timestamp: number; @@ -107,15 +107,20 @@ export type SkipHistory = { }[]; txHash: string; - txFulfilled?: boolean; - txError?: string; + trackDone?: boolean; + trackError?: string; + trackStatus?: StatusState; notified?: boolean; notificationInfo?: { currencies: AppCurrency[]; }; - simpleRoute: { isOnlyEvm: boolean; chainId: string; receiver: string }[]; + simpleRoute: { + isOnlyEvm: boolean; + chainId: string; + receiver: string; + }[]; routeIndex: number; routeDurationSeconds: number; From 7a070d8df65f769639b26d83031512ae222b1f39 Mon Sep 17 00:00:00 2001 From: rowan Date: Sun, 22 Dec 2024 17:34:25 +0900 Subject: [PATCH 26/43] Add handling tracking logic for skip history in the background --- .../src/recent-send-history/service.ts | 316 ++++++++++++++---- .../src/recent-send-history/types.ts | 2 +- 2 files changed, 256 insertions(+), 62 deletions(-) diff --git a/packages/background/src/recent-send-history/service.ts b/packages/background/src/recent-send-history/service.ts index d6957bb3ae..83d260c491 100644 --- a/packages/background/src/recent-send-history/service.ts +++ b/packages/background/src/recent-send-history/service.ts @@ -20,11 +20,7 @@ import { Buffer } from "buffer/"; import { AppCurrency, ChainInfo } from "@keplr-wallet/types"; import { CoinPretty } from "@keplr-wallet/unit"; import { simpleFetch } from "@keplr-wallet/simple-fetch"; -import { - StatusRequest, - TransferAssetRelease, - TxStatusResponse, -} from "./temp-skip-types"; +import { StatusRequest, TxStatusResponse } from "./temp-skip-types"; export class RecentSendHistoryService { // Key: {chain_identifier}/{type} @@ -1242,7 +1238,28 @@ export class RecentSendHistoryService { return; } - if (history.trackDone) { + const needRun = (() => { + const status = history.trackStatus; + const trackDone = history.trackDone; + const routeIndex = history.routeIndex; + + // status가 COMPLETED이지만, 아직 다음 라우트가 남아있는 경우 + if ( + status?.includes("COMPLETED") && + routeIndex !== history.simpleRoute.length - 1 + ) { + return true; + } + + // status가 없거나, track이 완료되지 않은 경우 + if (!status || !trackDone) { + return true; + } + + return false; + })(); + + if (!needRun) { onFulfill(); return; } @@ -1281,69 +1298,246 @@ export class RecentSendHistoryService { transfer_asset_release, } = res.data; - history.trackStatus = status; // 상태 업데이트 - - // state는 status와 비슷하지만 상위 범주의 상태를 나타냄 - // e.g. state가 completed이면 status는 completed, completed_error, completed_success 중 하나 - - // history 업데이트 함수 - const handleHistoryUpdate = () => { - const simpleRoute = history.simpleRoute; - const currentRouteIndex = history.routeIndex; - const errorMessage: string | undefined = error?.message; // detail한 에러 메시지는 transfer에서 가져옴 - const transferAssetRelease: TransferAssetRelease | null = - transfer_asset_release; - - const nextBlockingTransferIndex = - next_blocking_transfer?.transfer_sequence_index ?? -1; - - // 다음 블로킹 트랜스퍼 정보가 있는 경우 (다음 블로킹 트랜스퍼가 없으면 -1) - if (nextBlockingTransferIndex >= 0) { - // transfer_sequence의 길이 체크 - if (nextBlockingTransferIndex >= simpleRoute.length) { - history.trackError = "Invalid next_blocking_transfer index"; - // TODO: 어떻게 처리할지 고민 필요, 일단은 에러 처리 - onError(); - return; - } - - // 다음 블로킹 트랜스퍼 정보 가져오기 - const transfer = transfer_sequence[nextBlockingTransferIndex]; - } - - history.trackError = errorMessage; - if (transferAssetRelease) { - history.transferAssetRelease = transferAssetRelease; - } - - switch (state) { - case "STATE_ABANDONED": // 30분 이상 트래킹했는데 상태 변화가 없는 경우. 문제가 뭔지 진단하기 어려움. 웬만하면 발생하지 않는다고 optimistic하게 - case "STATE_COMPLETED_ERROR": // 오류가 발생하였고, 오류 전파 및 처리가 완료된 상태 - case "STATE_COMPLETED_SUCCESS": // 성공적으로 완료된 상태 - // 더 이상 트래킹할 필요가 없는 상태 - history.trackDone = true; - onFulfill(); - break; - case "STATE_PENDING": // route 진행 중 - case "STATE_PENDING_ERROR": // route 진행 중에 에러가 발생하였고 오류가 전파되는 중 - // 다시 트래킹 요청 (retry) - onError(); - } - }; + // 상태 저장 + history.trackStatus = state; + // status(상위 상태)와 state(하위 범주)에 따른 분기 switch (status) { + // 트래킹 불확실/미완료 상태 => 에러 처리 후 재시도 case "STATE_SUBMITTED": case "STATE_RECEIVED": case "STATE_COMPLETED": case "STATE_UNKNOWN": - // 트래킹 요청이 성공적으로 전달되었지만, 아직 트래킹이 시작되지 않은 상태 - // 또는 트래킹이 완료되었지만, 결과가 아직 처리되지 않은 상태 - // `UNKNOWN`은 상태를 알 수 없는 경우 (TODO: 어떻게 처리할지 고민 필요) - // 다음 상태가 주어질 때까지 일단 오류 처리하고 retry 로직이 동작하도록 한다. onError(); - break; + return; + default: - handleHistoryUpdate(); + // 여기서부터는 status가 '정상적으로 트래킹이 진행 중'이라고 가정 + + // 라우트, 현재 라우트 인덱스 가져오기 + const route = history.simpleRoute; + const currentRouteIndex = history.routeIndex; + + let nextRouteIndex = currentRouteIndex; // 다음 라우트 인덱스, src와 dst가 동일할 수 있으므로 초기값은 현재 인덱스로 설정 + let errorMsg: string | undefined = error?.message; // 우선 API에서 내려오는 error?.message 세팅 + + // 언락된 asset 정보가 있는 경우 저장 + if (transfer_asset_release) { + history.transferAssetRelease = transfer_asset_release; + } + + // 현재 처리중인 transfer의 인덱스 가져오기, 없을 경우 -1 + const nextBlockingTransferIndex = + next_blocking_transfer?.transfer_sequence_index ?? -1; + + // 다음 블로킹 트랜스퍼가 있을 경우, 해당 정보를 통해 다음 체인 인덱스 계산 + if (nextBlockingTransferIndex >= 0) { + // 인덱스 유효성 검사 (없어도 되지만 혹시 모르니) + if (nextBlockingTransferIndex >= transfer_sequence.length) { + history.trackError = "Invalid next_blocking_transfer index"; + onError(); + return; + } + + // 다음 blocking transfer의 정보를 가져옴 + const transfer = transfer_sequence[nextBlockingTransferIndex]; + + // ------------------------- + // 1) 일단 targetChainId/errorMsg를 구하는 단계 + // ------------------------- + let targetChainId: string | undefined; + // 예: `transfer`가 여러 타입 중 하나일 것이므로 순서대로 체크 + if ("ibc_transfer" in transfer) { + const ibc = transfer.ibc_transfer; + switch (ibc.state) { + case "TRANSFER_UNKNOWN": + case "TRANSFER_FAILURE": + targetChainId = ibc.src_chain_id; + errorMsg = ibc.packet_txs.error?.message; + break; + case "TRANSFER_PENDING": + case "TRANSFER_RECEIVED": + targetChainId = ibc.src_chain_id; + break; + case "TRANSFER_SUCCESS": + targetChainId = ibc.dst_chain_id; + break; + } + } else if ("axelar_transfer" in transfer) { + const axelar = transfer.axelar_transfer; + switch (axelar.state) { + case "AXELAR_TRANSFER_UNKNOWN": + targetChainId = axelar.from_chain_id; + errorMsg = "Unknown Axelar transfer error"; + break; + case "AXELAR_TRANSFER_FAILURE": + targetChainId = axelar.from_chain_id; + errorMsg = (axelar.txs as any).error?.message; + break; + case "AXELAR_TRANSFER_PENDING_CONFIRMATION": + case "AXELAR_TRANSFER_PENDING_RECEIPT": + targetChainId = axelar.from_chain_id; + break; + case "AXELAR_TRANSFER_SUCCESS": + targetChainId = axelar.to_chain_id; + break; + } + } else if ("cctp_transfer" in transfer) { + const cctp = transfer.cctp_transfer; + switch (cctp.state) { + case "CCTP_TRANSFER_UNKNOWN": + targetChainId = cctp.from_chain_id; + errorMsg = "Unknown CCTP transfer error"; + break; + case "CCTP_TRANSFER_CONFIRMED": + case "CCTP_TRANSFER_PENDING_CONFIRMATION": + case "CCTP_TRANSFER_SENT": + targetChainId = cctp.from_chain_id; + break; + case "CCTP_TRANSFER_RECEIVED": + targetChainId = cctp.to_chain_id; + break; + } + } else if ("hyperlane_transfer" in transfer) { + const hyperlane = transfer.hyperlane_transfer; + switch (hyperlane.state) { + case "HYPERLANE_TRANSFER_UNKNOWN": + targetChainId = hyperlane.from_chain_id; + errorMsg = "Unknown Hyperlane transfer error"; + break; + case "HYPERLANE_TRANSFER_FAILED": + targetChainId = hyperlane.from_chain_id; + errorMsg = "Hyperlane transfer failed"; + break; + case "HYPERLANE_TRANSFER_SENT": + targetChainId = hyperlane.from_chain_id; + break; + case "HYPERLANE_TRANSFER_RECEIVED": + targetChainId = hyperlane.to_chain_id; + break; + } + } else if ("op_init_transfer" in transfer) { + const opinit = transfer.op_init_transfer; + switch (opinit.state) { + case "OPINIT_TRANSFER_UNKNOWN": + targetChainId = opinit.from_chain_id; + errorMsg = "Unknown OP_INIT transfer error"; + break; + case "OPINIT_TRANSFER_RECEIVED": + targetChainId = opinit.from_chain_id; + break; + case "OPINIT_TRANSFER_SENT": + targetChainId = opinit.to_chain_id; + break; + } + } else if ("go_fast_transfer" in transfer) { + const gofast = transfer.go_fast_transfer; + switch (gofast.state) { + case "GO_FAST_TRANSFER_UNKNOWN": + targetChainId = gofast.from_chain_id; + errorMsg = "Unknown GoFast transfer error"; + break; + case "GO_FAST_TRANSFER_TIMEOUT": + targetChainId = gofast.from_chain_id; + errorMsg = "GoFast transfer timeout"; + break; + case "GO_FAST_POST_ACTION_FAILED": + targetChainId = gofast.from_chain_id; + errorMsg = "GoFast post action failed"; + break; + case "GO_FAST_TRANSFER_REFUNDED": + targetChainId = gofast.from_chain_id; + errorMsg = "GoFast transfer refunded"; + break; + case "GO_FAST_TRANSFER_SENT": + targetChainId = gofast.from_chain_id; + break; + case "GO_FAST_TRANSFER_FILLED": + targetChainId = gofast.to_chain_id; + break; + } + } else { + // stargate_transfer + const stargate = transfer.stargate_transfer; + switch (stargate.state) { + case "STARGATE_TRANSFER_UNKNOWN": + targetChainId = stargate.from_chain_id; + errorMsg = "Unknown Stargate transfer error"; + break; + case "STARGATE_TRANSFER_FAILED": + targetChainId = stargate.from_chain_id; + errorMsg = "Stargate transfer failed"; + break; + case "STARGATE_TRANSFER_SENT": + targetChainId = stargate.from_chain_id; + break; + case "STARGATE_TRANSFER_RECEIVED": + targetChainId = stargate.to_chain_id; + break; + } + } + + // ------------------------- + // 2) 구한 targetChainId로 nextRouteIndex를 찾는 단계 + // ------------------------- + if (targetChainId) { + const chainIdToFind = targetChainId.replace("eip155:", ""); + let foundNextRouteIndex = false; + for (let i = currentRouteIndex; i < route.length; i++) { + if ( + route[i].chainId.replace("eip155:", "") === chainIdToFind + ) { + nextRouteIndex = i; + foundNextRouteIndex = true; + break; + } + } + + // 찾지 못한 경우 에러 처리 후 재시도 + if (!foundNextRouteIndex) { + history.trackError = "Invalid next_blocking_transfer chainId"; + onError(); + return; + } + } + + // 에러 메시지 갱신 + history.trackError = errorMsg; + } + + // 최종적으로 routeIndex 업데이트 + history.routeIndex = nextRouteIndex; + + // state(하위 범주 상태)에 따라 트래킹 완료/재시도 결정 + switch (state) { + case "STATE_ABANDONED": + case "STATE_COMPLETED_ERROR": + case "STATE_COMPLETED_SUCCESS": + // 더 이상 트래킹하지 않아도 됨 + history.trackDone = true; + + if (nextRouteIndex !== route.length - 1) { + history.routeIndex = route.length - 1; + } + + // TODO: routeIndex가 끝까지 도달했을 때, 최종적으로 도달한 자산의 양을 가져오는 로직 추가 필요 (어려울 듯) + + onFulfill(); + break; + + case "STATE_PENDING": + case "STATE_PENDING_ERROR": + // 트래킹 중 (또는 에러 상태 전파 중) => 재시도 + onError(); + break; + + default: + // 그 외 예외 상태. 에러 메시지가 있으면 에러 처리 + if (errorMsg) { + onError(); + } + break; + } break; } }) diff --git a/packages/background/src/recent-send-history/types.ts b/packages/background/src/recent-send-history/types.ts index 1299803969..5c419f3f8c 100644 --- a/packages/background/src/recent-send-history/types.ts +++ b/packages/background/src/recent-send-history/types.ts @@ -127,7 +127,7 @@ export type SkipHistory = { destinationAsset: { chainId: string; denom: string; - expectedAmount: string; + expectedAmount?: string; }; resAmount: { From 74b44fd6baee6cfd74d14df8a05c3b74b162f234 Mon Sep 17 00:00:00 2001 From: rowan Date: Sun, 22 Dec 2024 19:35:58 +0900 Subject: [PATCH 27/43] Fix route index not updated while tracking skip history --- .../src/recent-send-history/service.ts | 356 +++++++++--------- 1 file changed, 173 insertions(+), 183 deletions(-) diff --git a/packages/background/src/recent-send-history/service.ts b/packages/background/src/recent-send-history/service.ts index 83d260c491..6475c926f1 100644 --- a/packages/background/src/recent-send-history/service.ts +++ b/packages/background/src/recent-send-history/service.ts @@ -1290,7 +1290,7 @@ export class RecentSendHistoryService { ) .then((res) => { const { - status, + // status, state, error, transfer_sequence, @@ -1302,7 +1302,7 @@ export class RecentSendHistoryService { history.trackStatus = state; // status(상위 상태)와 state(하위 범주)에 따른 분기 - switch (status) { + switch (state) { // 트래킹 불확실/미완료 상태 => 에러 처리 후 재시도 case "STATE_SUBMITTED": case "STATE_RECEIVED": @@ -1314,9 +1314,10 @@ export class RecentSendHistoryService { default: // 여기서부터는 status가 '정상적으로 트래킹이 진행 중'이라고 가정 - // 라우트, 현재 라우트 인덱스 가져오기 + // 라우트, 현재 라우트 인덱스 가져오기 (처음에 -1로 초기화되어 있음) const route = history.simpleRoute; - const currentRouteIndex = history.routeIndex; + const currentRouteIndex = + history.routeIndex < 0 ? 0 : history.routeIndex; let nextRouteIndex = currentRouteIndex; // 다음 라우트 인덱스, src와 dst가 동일할 수 있으므로 초기값은 현재 인덱스로 설정 let errorMsg: string | undefined = error?.message; // 우선 API에서 내려오는 error?.message 세팅 @@ -1328,183 +1329,176 @@ export class RecentSendHistoryService { // 현재 처리중인 transfer의 인덱스 가져오기, 없을 경우 -1 const nextBlockingTransferIndex = - next_blocking_transfer?.transfer_sequence_index ?? -1; - - // 다음 블로킹 트랜스퍼가 있을 경우, 해당 정보를 통해 다음 체인 인덱스 계산 - if (nextBlockingTransferIndex >= 0) { - // 인덱스 유효성 검사 (없어도 되지만 혹시 모르니) - if (nextBlockingTransferIndex >= transfer_sequence.length) { - history.trackError = "Invalid next_blocking_transfer index"; - onError(); - return; + next_blocking_transfer?.transfer_sequence_index ?? + transfer_sequence.length - 1; + + // 다음 blocking transfer의 정보를 가져옴 + const transfer = transfer_sequence[nextBlockingTransferIndex]; + + // ------------------------- + // 1) 일단 targetChainId/errorMsg를 구하는 단계 + // ------------------------- + let targetChainId: string | undefined; + // 예: `transfer`가 여러 타입 중 하나일 것이므로 순서대로 체크 + if ("ibc_transfer" in transfer) { + const ibc = transfer.ibc_transfer; + switch (ibc.state) { + case "TRANSFER_UNKNOWN": + targetChainId = ibc.from_chain_id; + errorMsg = + ibc.packet_txs.error?.message ?? + "Unknown IBC transfer error"; + break; + case "TRANSFER_FAILURE": + targetChainId = ibc.from_chain_id; + errorMsg = + ibc.packet_txs.error?.message ?? "IBC transfer failed"; + break; + case "TRANSFER_PENDING": + case "TRANSFER_RECEIVED": + targetChainId = ibc.from_chain_id; + break; + case "TRANSFER_SUCCESS": + targetChainId = ibc.to_chain_id; + break; } - - // 다음 blocking transfer의 정보를 가져옴 - const transfer = transfer_sequence[nextBlockingTransferIndex]; - - // ------------------------- - // 1) 일단 targetChainId/errorMsg를 구하는 단계 - // ------------------------- - let targetChainId: string | undefined; - // 예: `transfer`가 여러 타입 중 하나일 것이므로 순서대로 체크 - if ("ibc_transfer" in transfer) { - const ibc = transfer.ibc_transfer; - switch (ibc.state) { - case "TRANSFER_UNKNOWN": - case "TRANSFER_FAILURE": - targetChainId = ibc.src_chain_id; - errorMsg = ibc.packet_txs.error?.message; - break; - case "TRANSFER_PENDING": - case "TRANSFER_RECEIVED": - targetChainId = ibc.src_chain_id; - break; - case "TRANSFER_SUCCESS": - targetChainId = ibc.dst_chain_id; - break; - } - } else if ("axelar_transfer" in transfer) { - const axelar = transfer.axelar_transfer; - switch (axelar.state) { - case "AXELAR_TRANSFER_UNKNOWN": - targetChainId = axelar.from_chain_id; - errorMsg = "Unknown Axelar transfer error"; - break; - case "AXELAR_TRANSFER_FAILURE": - targetChainId = axelar.from_chain_id; - errorMsg = (axelar.txs as any).error?.message; - break; - case "AXELAR_TRANSFER_PENDING_CONFIRMATION": - case "AXELAR_TRANSFER_PENDING_RECEIPT": - targetChainId = axelar.from_chain_id; - break; - case "AXELAR_TRANSFER_SUCCESS": - targetChainId = axelar.to_chain_id; - break; - } - } else if ("cctp_transfer" in transfer) { - const cctp = transfer.cctp_transfer; - switch (cctp.state) { - case "CCTP_TRANSFER_UNKNOWN": - targetChainId = cctp.from_chain_id; - errorMsg = "Unknown CCTP transfer error"; - break; - case "CCTP_TRANSFER_CONFIRMED": - case "CCTP_TRANSFER_PENDING_CONFIRMATION": - case "CCTP_TRANSFER_SENT": - targetChainId = cctp.from_chain_id; - break; - case "CCTP_TRANSFER_RECEIVED": - targetChainId = cctp.to_chain_id; - break; - } - } else if ("hyperlane_transfer" in transfer) { - const hyperlane = transfer.hyperlane_transfer; - switch (hyperlane.state) { - case "HYPERLANE_TRANSFER_UNKNOWN": - targetChainId = hyperlane.from_chain_id; - errorMsg = "Unknown Hyperlane transfer error"; - break; - case "HYPERLANE_TRANSFER_FAILED": - targetChainId = hyperlane.from_chain_id; - errorMsg = "Hyperlane transfer failed"; - break; - case "HYPERLANE_TRANSFER_SENT": - targetChainId = hyperlane.from_chain_id; - break; - case "HYPERLANE_TRANSFER_RECEIVED": - targetChainId = hyperlane.to_chain_id; - break; - } - } else if ("op_init_transfer" in transfer) { - const opinit = transfer.op_init_transfer; - switch (opinit.state) { - case "OPINIT_TRANSFER_UNKNOWN": - targetChainId = opinit.from_chain_id; - errorMsg = "Unknown OP_INIT transfer error"; - break; - case "OPINIT_TRANSFER_RECEIVED": - targetChainId = opinit.from_chain_id; - break; - case "OPINIT_TRANSFER_SENT": - targetChainId = opinit.to_chain_id; - break; - } - } else if ("go_fast_transfer" in transfer) { - const gofast = transfer.go_fast_transfer; - switch (gofast.state) { - case "GO_FAST_TRANSFER_UNKNOWN": - targetChainId = gofast.from_chain_id; - errorMsg = "Unknown GoFast transfer error"; - break; - case "GO_FAST_TRANSFER_TIMEOUT": - targetChainId = gofast.from_chain_id; - errorMsg = "GoFast transfer timeout"; - break; - case "GO_FAST_POST_ACTION_FAILED": - targetChainId = gofast.from_chain_id; - errorMsg = "GoFast post action failed"; - break; - case "GO_FAST_TRANSFER_REFUNDED": - targetChainId = gofast.from_chain_id; - errorMsg = "GoFast transfer refunded"; - break; - case "GO_FAST_TRANSFER_SENT": - targetChainId = gofast.from_chain_id; - break; - case "GO_FAST_TRANSFER_FILLED": - targetChainId = gofast.to_chain_id; - break; - } - } else { - // stargate_transfer - const stargate = transfer.stargate_transfer; - switch (stargate.state) { - case "STARGATE_TRANSFER_UNKNOWN": - targetChainId = stargate.from_chain_id; - errorMsg = "Unknown Stargate transfer error"; - break; - case "STARGATE_TRANSFER_FAILED": - targetChainId = stargate.from_chain_id; - errorMsg = "Stargate transfer failed"; - break; - case "STARGATE_TRANSFER_SENT": - targetChainId = stargate.from_chain_id; - break; - case "STARGATE_TRANSFER_RECEIVED": - targetChainId = stargate.to_chain_id; - break; - } + } else if ("axelar_transfer" in transfer) { + const axelar = transfer.axelar_transfer; + switch (axelar.state) { + case "AXELAR_TRANSFER_UNKNOWN": + targetChainId = axelar.from_chain_id; + errorMsg = "Unknown Axelar transfer error"; + break; + case "AXELAR_TRANSFER_FAILURE": + targetChainId = axelar.from_chain_id; + errorMsg = + (axelar.txs as any).error?.message ?? + "Axelar transfer failed"; + break; + case "AXELAR_TRANSFER_PENDING_CONFIRMATION": + case "AXELAR_TRANSFER_PENDING_RECEIPT": + targetChainId = axelar.from_chain_id; + break; + case "AXELAR_TRANSFER_SUCCESS": + targetChainId = axelar.to_chain_id; + break; } + } else if ("cctp_transfer" in transfer) { + const cctp = transfer.cctp_transfer; + switch (cctp.state) { + case "CCTP_TRANSFER_UNKNOWN": + targetChainId = cctp.from_chain_id; + errorMsg = "Unknown CCTP transfer error"; + break; + case "CCTP_TRANSFER_CONFIRMED": + case "CCTP_TRANSFER_PENDING_CONFIRMATION": + case "CCTP_TRANSFER_SENT": + targetChainId = cctp.from_chain_id; + break; + case "CCTP_TRANSFER_RECEIVED": + targetChainId = cctp.to_chain_id; + break; + } + } else if ("hyperlane_transfer" in transfer) { + const hyperlane = transfer.hyperlane_transfer; + switch (hyperlane.state) { + case "HYPERLANE_TRANSFER_UNKNOWN": + targetChainId = hyperlane.from_chain_id; + errorMsg = "Unknown Hyperlane transfer error"; + break; + case "HYPERLANE_TRANSFER_FAILED": + targetChainId = hyperlane.from_chain_id; + errorMsg = "Hyperlane transfer failed"; + break; + case "HYPERLANE_TRANSFER_SENT": + targetChainId = hyperlane.from_chain_id; + break; + case "HYPERLANE_TRANSFER_RECEIVED": + targetChainId = hyperlane.to_chain_id; + break; + } + } else if ("op_init_transfer" in transfer) { + const opinit = transfer.op_init_transfer; + switch (opinit.state) { + case "OPINIT_TRANSFER_UNKNOWN": + targetChainId = opinit.from_chain_id; + errorMsg = "Unknown OP_INIT transfer error"; + break; + case "OPINIT_TRANSFER_RECEIVED": + targetChainId = opinit.from_chain_id; + break; + case "OPINIT_TRANSFER_SENT": + targetChainId = opinit.to_chain_id; + break; + } + } else if ("go_fast_transfer" in transfer) { + const gofast = transfer.go_fast_transfer; + switch (gofast.state) { + case "GO_FAST_TRANSFER_UNKNOWN": + targetChainId = gofast.from_chain_id; + errorMsg = "Unknown GoFast transfer error"; + break; + case "GO_FAST_TRANSFER_TIMEOUT": + targetChainId = gofast.from_chain_id; + errorMsg = "GoFast transfer timeout"; + break; + case "GO_FAST_POST_ACTION_FAILED": + targetChainId = gofast.from_chain_id; + errorMsg = "GoFast post action failed"; + break; + case "GO_FAST_TRANSFER_REFUNDED": + targetChainId = gofast.from_chain_id; + errorMsg = "GoFast transfer refunded"; + break; + case "GO_FAST_TRANSFER_SENT": + targetChainId = gofast.from_chain_id; + break; + case "GO_FAST_TRANSFER_FILLED": + targetChainId = gofast.to_chain_id; + break; + } + } else { + // stargate_transfer + const stargate = transfer.stargate_transfer; + switch (stargate.state) { + case "STARGATE_TRANSFER_UNKNOWN": + targetChainId = stargate.from_chain_id; + errorMsg = "Unknown Stargate transfer error"; + break; + case "STARGATE_TRANSFER_FAILED": + targetChainId = stargate.from_chain_id; + errorMsg = "Stargate transfer failed"; + break; + case "STARGATE_TRANSFER_SENT": + targetChainId = stargate.from_chain_id; + break; + case "STARGATE_TRANSFER_RECEIVED": + targetChainId = stargate.to_chain_id; + break; + } + } - // ------------------------- - // 2) 구한 targetChainId로 nextRouteIndex를 찾는 단계 - // ------------------------- - if (targetChainId) { - const chainIdToFind = targetChainId.replace("eip155:", ""); - let foundNextRouteIndex = false; - for (let i = currentRouteIndex; i < route.length; i++) { - if ( - route[i].chainId.replace("eip155:", "") === chainIdToFind - ) { - nextRouteIndex = i; - foundNextRouteIndex = true; - break; - } - } - - // 찾지 못한 경우 에러 처리 후 재시도 - if (!foundNextRouteIndex) { - history.trackError = "Invalid next_blocking_transfer chainId"; - onError(); - return; + // ------------------------- + // 2) 구한 targetChainId로 nextRouteIndex를 찾는 단계 + // ------------------------- + if (targetChainId) { + for (let i = currentRouteIndex; i < route.length; i++) { + if ( + route[i].chainId + .replace("eip155:", "") + .toLocaleLowerCase() === targetChainId.toLocaleLowerCase() + ) { + nextRouteIndex = i; + break; } } - // 에러 메시지 갱신 - history.trackError = errorMsg; + // 찾지 못하더라도 optimistic하게 넘어가기 (일단은) } + // 에러 메시지 갱신 + history.trackError = errorMsg; + // 최종적으로 routeIndex 업데이트 history.routeIndex = nextRouteIndex; @@ -1516,8 +1510,12 @@ export class RecentSendHistoryService { // 더 이상 트래킹하지 않아도 됨 history.trackDone = true; - if (nextRouteIndex !== route.length - 1) { - history.routeIndex = route.length - 1; + if (state === "STATE_COMPLETED_SUCCESS") { + // 성공적으로 완료되었는데, 다음 라우트가 남아있는 경우 + // 마지막 라우트로 항상 도달하도록 업데이트 + if (nextRouteIndex !== route.length - 1) { + history.routeIndex = route.length - 1; + } } // TODO: routeIndex가 끝까지 도달했을 때, 최종적으로 도달한 자산의 양을 가져오는 로직 추가 필요 (어려울 듯) @@ -1530,15 +1528,7 @@ export class RecentSendHistoryService { // 트래킹 중 (또는 에러 상태 전파 중) => 재시도 onError(); break; - - default: - // 그 외 예외 상태. 에러 메시지가 있으면 에러 처리 - if (errorMsg) { - onError(); - } - break; } - break; } }) .catch((e) => { From 0132bb56582abaded39fbfece8d10f7db678cfe6 Mon Sep 17 00:00:00 2001 From: rowan Date: Sun, 22 Dec 2024 23:20:10 +0900 Subject: [PATCH 28/43] Add stub `SkipHistoryViewItem` component --- .../components/ibc-history-view/index.tsx | 525 +++++++++++++++++- 1 file changed, 517 insertions(+), 8 deletions(-) diff --git a/apps/extension/src/pages/main/components/ibc-history-view/index.tsx b/apps/extension/src/pages/main/components/ibc-history-view/index.tsx index 2d6084bbae..638fa32c36 100644 --- a/apps/extension/src/pages/main/components/ibc-history-view/index.tsx +++ b/apps/extension/src/pages/main/components/ibc-history-view/index.tsx @@ -4,6 +4,7 @@ import { GetIBCHistoriesMsg, IBCHistory, RemoveIBCHistoryMsg, + SkipHistory, } from "@keplr-wallet/background"; import { InExtensionMessageRequester } from "@keplr-wallet/router-extension"; import { BACKGROUND_PORT } from "@keplr-wallet/router"; @@ -35,14 +36,19 @@ import { useSpringValue, animated, easings } from "@react-spring/web"; import { defaultSpringConfig } from "../../../../styles/spring"; import { VerticalCollapseTransition } from "../../../../components/transition/vertical-collapse"; import { FormattedMessage, useIntl } from "react-intl"; -import { GetSkipHistoriesMsg } from "@keplr-wallet/background/build/recent-send-history/temp-skip-message"; +import { + GetSkipHistoriesMsg, + RemoveSkipHistoryMsg, +} from "@keplr-wallet/background/build/recent-send-history/temp-skip-message"; export const IbcHistoryView: FunctionComponent<{ isNotReady: boolean; }> = observer(({ isNotReady }) => { - const { queriesStore, accountStore } = useStore(); + const { queriesStore, accountStore, chainStore } = useStore(); const [histories, setHistories] = useState([]); + const [skipHistories, setSkipHistories] = useState([]); + useLayoutEffectOnce(() => { let count = 0; const alreadyCompletedHistoryMap = new Map(); @@ -98,16 +104,74 @@ export const IbcHistoryView: FunctionComponent<{ }); }; - const skipFn = () => { + fn(); + const interval = setInterval(fn, 1000); + + return () => { + clearInterval(interval); + }; + }); + + useLayoutEffectOnce(() => { + let count = 0; + const alreadyCompletedHistoryMap = new Map(); + const requester = new InExtensionMessageRequester(); + + const fn = () => { const msg = new GetSkipHistoriesMsg(); - requester.sendMessage(BACKGROUND_PORT, msg).then((histories) => { - // TODO: 메시지 들어오는 것 까지 확인. 백그라운드에서 polling 돌리고 가져온 메시지 보여주는 작업 추가 - console.log("skip histories", histories); + requester.sendMessage(BACKGROUND_PORT, msg).then((newHistories) => { + setSkipHistories((histories) => { + if (JSON.stringify(histories) !== JSON.stringify(newHistories)) { + count++; + + // Currently there is no elegant way to automatically refresh when an ibc transfer is complete. + // For now, deal with it here + const newCompletes = newHistories.filter((history) => { + if (alreadyCompletedHistoryMap.get(history.id)) { + return false; + } + return ( + !!history.trackDone && + history.routeIndex === history.simpleRoute.length - 1 + ); + }); + + if (count > 1) { + // There is no need to refresh balance if first time. (onMount) + for (const newComplete of newCompletes) { + const lastRoute = + newComplete.simpleRoute[newComplete.routeIndex]; + + if (lastRoute.isOnlyEvm) { + queriesStore + .get(`eip155:${lastRoute.chainId}`) + .queryBalances.getQueryEthereumHexAddress( + newComplete.simpleRoute[newComplete.routeIndex].receiver + ) + .fetch(); + } else { + queriesStore + .get(newComplete.destinationChainId) + .queryBalances.getQueryBech32Address( + newComplete.simpleRoute[newComplete.routeIndex].receiver + ) + .fetch(); + } + } + } + for (const newComplete of newCompletes) { + alreadyCompletedHistoryMap.set(newComplete.id, true); + } + + return newHistories; + } + return histories; + }); }); }; fn(); - skipFn(); + const interval = setInterval(fn, 1000); return () => { @@ -123,6 +187,25 @@ export const IbcHistoryView: FunctionComponent<{ return false; }); + const filteredSkipHistories = skipHistories.filter((history) => { + const firstRoute = history.simpleRoute[0]; + const account = accountStore.getAccount(firstRoute.chainId); + + if (firstRoute.isOnlyEvm) { + if (account.ethereumHexAddress === history.sender) { + return true; + } + return false; + } + + if (account.bech32Address === history.sender) { + return true; + } + return false; + }); + + console.log(histories, skipHistories); + if (isNotReady) { return null; } @@ -144,7 +227,22 @@ export const IbcHistoryView: FunctionComponent<{ /> ); })} - {filteredHistories.length > 0 ? : null} + {filteredSkipHistories.reverse().map((_history) => ( + { + const requester = new InExtensionMessageRequester(); + const msg = new RemoveSkipHistoryMsg(id); + requester.sendMessage(BACKGROUND_PORT, msg).then((histories) => { + setSkipHistories(histories); + }); + }} + /> + ))} + {filteredHistories.length > 0 || filteredSkipHistories.length > 0 ? ( + + ) : null} ); }); @@ -683,6 +781,417 @@ const IbcHistoryViewItem: FunctionComponent<{ ); }); +const SkipHistoryViewItem: FunctionComponent<{ + history: SkipHistory; + removeHistory: (id: string) => void; +}> = observer(({ history, removeHistory }) => { + const { chainStore } = useStore(); + + const theme = useTheme(); + const intl = useIntl(); + + const isIBCSwap = "swapType" in history; + + const historyCompleted = (() => { + if (!history.trackDone) { + return false; + } + + if (history.trackError) { + return false; + } + + return ( + history.trackStatus === "STATE_COMPLETED_SUCCESS" && + history.routeIndex === history.simpleRoute.length - 1 + ); + })(); + + const failedRouteIndex = (() => { + return history.trackError ? history.routeIndex : -1; + })(); + + const failedRoute = (() => { + if (failedRouteIndex >= 0) { + return history.simpleRoute[failedRouteIndex]; + } + })(); + + return ( + + + + {(() => { + if (failedRouteIndex >= 0) { + return ( + + ); + } + + if (!historyCompleted) { + return ( + + ); + } + + return ( + + ); + })()} + + + + + {(() => { + if (failedRouteIndex >= 0) { + if ( + history.trackStatus === "STATE_COMPLETED_ERROR" && + history.transferAssetRelease && + history.transferAssetRelease.released + ) { + return intl.formatMessage({ + id: "page.main.components.ibc-history-view.ibc-swap.item.refund.succeed", + }); + } + return intl.formatMessage({ + id: "page.main.components.ibc-history-view.ibc-swap.item.refund.pending", + }); + } + + return !historyCompleted + ? intl.formatMessage({ + id: isIBCSwap + ? "page.main.components.ibc-history-view.ibc-swap.item.pending" + : "page.main.components.ibc-history-view.item.pending", + }) + : intl.formatMessage({ + id: isIBCSwap + ? "page.main.components.ibc-history-view.ibc-swap.item.succeed" + : "page.main.components.ibc-history-view.item.succeed", + }); + })()} + +
+ { + e.preventDefault(); + + removeHistory(history.id); + }} + > + + + + + + + + {(() => { + const sourceChain = chainStore.getChain(history.chainId); + // const destinationChain = chainStore.getChain( + // history.destinationChainId + // ); + + if (historyCompleted && failedRouteIndex < 0) { + // const chainId = history.destinationChainId; + // const chainInfo = chainStore.getChain(chainId); + const assets = (() => { + return "Unknown"; + })(); + + return intl.formatMessage( + { + id: "page.main.components.ibc-history-view.ibc-swap.succeed.paragraph", + }, + { + assets, + } + ); + } + + const assets = history.amount + .map((amount) => { + const currency = sourceChain.forceFindCurrency(amount.denom); + const pretty = new CoinPretty(currency, amount.amount); + return pretty + .hideIBCMetadata(true) + .shrink(true) + .maxDecimals(6) + .inequalitySymbol(true) + .trim(true) + .toString(); + }) + .join(", "); + + return intl.formatMessage( + { + id: "page.main.components.ibc-history-view.ibc-swap.paragraph", + }, + { + assets, + destinationDenom: (() => { + const currency = chainStore + .getChain(history.destinationAsset.chainId) + .forceFindCurrency(history.destinationAsset.denom); + + if ("originCurrency" in currency && currency.originCurrency) { + return currency.originCurrency.coinDenom; + } + + return currency.coinDenom; + })(), + } + ); + })()} + + + + + + + {(() => { + const chainIds = history.simpleRoute.map((route) => { + return route.chainId; + }); + + return chainIds.map((chainId, i) => { + const chainInfo = chainStore.getChain(chainId); + const completed = + !!history.trackDone && i <= history.routeIndex; + const error = !!history.trackError; + + return ( + // 일부분 순환하는 경우도 이론적으로 가능은 하기 때문에 chain id를 key로 사용하지 않음. + { + if (failedRoute) { + return i === failedRouteIndex; + } + + if (completed) { + return false; + } + + if (i === 0 && !completed) { + return true; + } + + if (!history.trackDone) { + return false; + } + + return i - 1 === history.routeIndex; + })()} + arrowDirection={(() => { + if (!failedRoute) { + return "right"; + } + + return i <= failedRouteIndex ? "left" : "hide"; + })()} + error={error} + isLast={chainIds.length - 1 === i} + /> + ); + }); + })()} + + + + + + + { + const complete = !failedRoute; + + if ( + history.trackDone && + history.trackError && + history.transferAssetRelease + ) { + return intl.formatMessage( + { + id: "page.main.components.ibc-history-view.ibc-swap.failed.after-swap.complete", + }, + { + chain: chainStore.getChain( + history.transferAssetRelease.chain_id + ).chainName, + assets: "Unknown", + } + ); + } + + return complete + ? "page.main.components.ibc-history-view.ibc-swap.failed.complete" + : "page.main.components.ibc-history-view.ibc-swap.failed.in-progress"; + })()} + /> + + + + { + if (historyCompleted) { + return true; + } + + if (failedRouteIndex >= 0) { + // if ( + // !history.ibcHistory + // .slice(0, failedChannelIndex + 1) + // .some((h) => !h.rewound) || + // history.ibcHistory + // .slice(0, failedChannelIndex + 1) + // .some((h) => h.rewoundButNextRewindingBlocked) + // ) { + // return true; + // } + } + + return false; + })()} + > + + + + + + + + +
+ + + + + + + + + + + + + + ); +}); + const ChainImageFallbackAnimated = animated(ChainImageFallback); const IbcHistoryViewItemChainImage: FunctionComponent<{ From 8a97930b54fe295a5df7ccacf6b23d1fe1bee812 Mon Sep 17 00:00:00 2001 From: rowan Date: Sun, 22 Dec 2024 23:51:56 +0900 Subject: [PATCH 29/43] Adjust max wait ms for retry to track skip history status --- .../src/pages/main/components/ibc-history-view/index.tsx | 2 +- packages/background/src/recent-send-history/service.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/extension/src/pages/main/components/ibc-history-view/index.tsx b/apps/extension/src/pages/main/components/ibc-history-view/index.tsx index 638fa32c36..f8569b8959 100644 --- a/apps/extension/src/pages/main/components/ibc-history-view/index.tsx +++ b/apps/extension/src/pages/main/components/ibc-history-view/index.tsx @@ -44,7 +44,7 @@ import { export const IbcHistoryView: FunctionComponent<{ isNotReady: boolean; }> = observer(({ isNotReady }) => { - const { queriesStore, accountStore, chainStore } = useStore(); + const { queriesStore, accountStore } = useStore(); const [histories, setHistories] = useState([]); const [skipHistories, setSkipHistories] = useState([]); diff --git a/packages/background/src/recent-send-history/service.ts b/packages/background/src/recent-send-history/service.ts index 6475c926f1..d816e4a5a0 100644 --- a/packages/background/src/recent-send-history/service.ts +++ b/packages/background/src/recent-send-history/service.ts @@ -1200,7 +1200,7 @@ export class RecentSendHistoryService { const diff = expectedEndTimestamp - now; const waitMsAfterError = 10 * 1000; - const maxRetries = diff > 0 ? (diff / waitMsAfterError) * 2 : 10; + const maxRetries = diff > 0 ? diff / waitMsAfterError : 10; retry( () => { @@ -1221,7 +1221,7 @@ export class RecentSendHistoryService { { maxRetries, waitMsAfterError, - maxWaitMsAfterError: 5 * 60 * 1000, // 5min + maxWaitMsAfterError: 60 * 1000, // 1min } ); } From c48387ebc06bd802b968d67c1ec8041c7cdda4cf Mon Sep 17 00:00:00 2001 From: rowan Date: Mon, 23 Dec 2024 10:02:14 +0900 Subject: [PATCH 30/43] Improve information displayed by `SkipHistoryViewItem` component --- .../components/ibc-history-view/index.tsx | 177 ++++++++++-------- 1 file changed, 100 insertions(+), 77 deletions(-) diff --git a/apps/extension/src/pages/main/components/ibc-history-view/index.tsx b/apps/extension/src/pages/main/components/ibc-history-view/index.tsx index f8569b8959..4e3ce58c40 100644 --- a/apps/extension/src/pages/main/components/ibc-history-view/index.tsx +++ b/apps/extension/src/pages/main/components/ibc-history-view/index.tsx @@ -144,7 +144,7 @@ export const IbcHistoryView: FunctionComponent<{ if (lastRoute.isOnlyEvm) { queriesStore - .get(`eip155:${lastRoute.chainId}`) + .get(lastRoute.chainId) .queryBalances.getQueryEthereumHexAddress( newComplete.simpleRoute[newComplete.routeIndex].receiver ) @@ -204,8 +204,6 @@ export const IbcHistoryView: FunctionComponent<{ return false; }); - console.log(histories, skipHistories); - if (isNotReady) { return null; } @@ -790,14 +788,16 @@ const SkipHistoryViewItem: FunctionComponent<{ const theme = useTheme(); const intl = useIntl(); - const isIBCSwap = "swapType" in history; - const historyCompleted = (() => { if (!history.trackDone) { return false; } if (history.trackError) { + if (history.transferAssetRelease) { + return history.transferAssetRelease.released; + } + return false; } @@ -898,14 +898,10 @@ const SkipHistoryViewItem: FunctionComponent<{ return !historyCompleted ? intl.formatMessage({ - id: isIBCSwap - ? "page.main.components.ibc-history-view.ibc-swap.item.pending" - : "page.main.components.ibc-history-view.item.pending", + id: "page.main.components.ibc-history-view.ibc-swap.item.pending", }) : intl.formatMessage({ - id: isIBCSwap - ? "page.main.components.ibc-history-view.ibc-swap.item.succeed" - : "page.main.components.ibc-history-view.item.succeed", + id: "page.main.components.ibc-history-view.ibc-swap.item.succeed", }); })()} @@ -945,15 +941,18 @@ const SkipHistoryViewItem: FunctionComponent<{ > {(() => { const sourceChain = chainStore.getChain(history.chainId); - // const destinationChain = chainStore.getChain( - // history.destinationChainId - // ); if (historyCompleted && failedRouteIndex < 0) { - // const chainId = history.destinationChainId; - // const chainInfo = chainStore.getChain(chainId); - const assets = (() => { - return "Unknown"; + const destinationDenom = (() => { + const currency = chainStore + .getChain(history.destinationAsset.chainId) + .forceFindCurrency(history.destinationAsset.denom); + + if ("originCurrency" in currency && currency.originCurrency) { + return currency.originCurrency.coinDenom; + } + + return currency.coinDenom; })(); return intl.formatMessage( @@ -961,24 +960,37 @@ const SkipHistoryViewItem: FunctionComponent<{ id: "page.main.components.ibc-history-view.ibc-swap.succeed.paragraph", }, { - assets, + assets: destinationDenom, } ); } - const assets = history.amount - .map((amount) => { - const currency = sourceChain.forceFindCurrency(amount.denom); - const pretty = new CoinPretty(currency, amount.amount); - return pretty - .hideIBCMetadata(true) - .shrink(true) - .maxDecimals(6) - .inequalitySymbol(true) - .trim(true) - .toString(); - }) - .join(", "); + // bridge, swap 등으로 경로가 길어져서 자산 정보가 2개 이상인 경우가 많음. + // 따라서 첫 번째 자산 정보만 보여줌. + const assets = (() => { + const amount = history.amount[0]; + const currency = sourceChain.forceFindCurrency(amount.denom); + const pretty = new CoinPretty(currency, amount.amount); + return pretty + .hideIBCMetadata(true) + .shrink(true) + .maxDecimals(6) + .inequalitySymbol(true) + .trim(true) + .toString(); + })(); + + const destinationDenom = (() => { + const currency = chainStore + .getChain(history.destinationAsset.chainId) + .forceFindCurrency(history.destinationAsset.denom); + + if ("originCurrency" in currency && currency.originCurrency) { + return currency.originCurrency.coinDenom; + } + + return currency.coinDenom; + })(); return intl.formatMessage( { @@ -986,17 +998,7 @@ const SkipHistoryViewItem: FunctionComponent<{ }, { assets, - destinationDenom: (() => { - const currency = chainStore - .getChain(history.destinationAsset.chainId) - .forceFindCurrency(history.destinationAsset.denom); - - if ("originCurrency" in currency && currency.originCurrency) { - return currency.originCurrency.coinDenom; - } - - return currency.coinDenom; - })(), + destinationDenom, } ); })()} @@ -1023,7 +1025,7 @@ const SkipHistoryViewItem: FunctionComponent<{ const chainInfo = chainStore.getChain(chainId); const completed = !!history.trackDone && i <= history.routeIndex; - const error = !!history.trackError; + const error = !!history.trackError && i === failedRouteIndex; return ( // 일부분 순환하는 경우도 이론적으로 가능은 하기 때문에 chain id를 key로 사용하지 않음. @@ -1077,27 +1079,64 @@ const SkipHistoryViewItem: FunctionComponent<{ > { - const complete = !failedRoute; + const completedAnyways = + history.trackStatus?.includes("COMPLETED"); + const transferAssetRelease = history.transferAssetRelease; + // status tracking이 오류로 끝난 경우 if ( history.trackDone && history.trackError && - history.transferAssetRelease + transferAssetRelease ) { + const isOnlyEvm = chainStore.hasChain( + `eip155:${transferAssetRelease.chain_id}` + ); + const releasedChain = chainStore.getChain( + isOnlyEvm + ? `eip155:${transferAssetRelease.chain_id}` + : transferAssetRelease.chain_id + ); + const destinationDenom = (() => { + const currency = releasedChain.forceFindCurrency( + transferAssetRelease.denom + ); + + if ( + "originCurrency" in currency && + currency.originCurrency + ) { + return currency.originCurrency.coinDenom; + } + + return currency.coinDenom; + })(); + // 자산이 성공적으로 반환된 경우 + if (transferAssetRelease.released) { + return intl.formatMessage( + { + id: "page.main.components.ibc-history-view.ibc-swap.failed.after-swap.complete", + }, + { + chain: releasedChain.chainName, + assets: destinationDenom, + } + ); + } + + // 자산이 반환되지 않은 경우 return intl.formatMessage( { id: "page.main.components.ibc-history-view.ibc-swap.failed.after-swap.complete", }, { - chain: chainStore.getChain( - history.transferAssetRelease.chain_id - ).chainName, - assets: "Unknown", + chain: releasedChain.chainName, + assets: destinationDenom, } ); } - return complete + return completedAnyways ? "page.main.components.ibc-history-view.ibc-swap.failed.complete" : "page.main.components.ibc-history-view.ibc-swap.failed.in-progress"; })()} @@ -1105,28 +1144,7 @@ const SkipHistoryViewItem: FunctionComponent<{ - { - if (historyCompleted) { - return true; - } - - if (failedRouteIndex >= 0) { - // if ( - // !history.ibcHistory - // .slice(0, failedChannelIndex + 1) - // .some((h) => !h.rewound) || - // history.ibcHistory - // .slice(0, failedChannelIndex + 1) - // .some((h) => h.rewoundButNextRewindingBlocked) - // ) { - // return true; - // } - } - - return false; - })()} - > + { + const minutes = Math.floor( + history.routeDurationSeconds / 60 + ); + const seconds = history.routeDurationSeconds % 60; + + return minutes + Math.ceil(seconds / 60); + })(), }} /> @@ -1180,9 +1205,7 @@ const SkipHistoryViewItem: FunctionComponent<{ > From d9f4cb5f86edc4096adc6f2046363857ded40644 Mon Sep 17 00:00:00 2001 From: rowan Date: Mon, 23 Dec 2024 10:23:10 +0900 Subject: [PATCH 31/43] Improve indicating refund occurred chain on skip history --- .../components/ibc-history-view/index.tsx | 4 +- .../src/recent-send-history/service.ts | 72 ++++++++++++++++--- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/apps/extension/src/pages/main/components/ibc-history-view/index.tsx b/apps/extension/src/pages/main/components/ibc-history-view/index.tsx index 4e3ce58c40..135b6e69ae 100644 --- a/apps/extension/src/pages/main/components/ibc-history-view/index.tsx +++ b/apps/extension/src/pages/main/components/ibc-history-view/index.tsx @@ -1057,7 +1057,7 @@ const SkipHistoryViewItem: FunctionComponent<{ return "right"; } - return i <= failedRouteIndex ? "left" : "hide"; + return i === failedRouteIndex ? "left" : "hide"; })()} error={error} isLast={chainIds.length - 1 === i} @@ -1144,7 +1144,7 @@ const SkipHistoryViewItem: FunctionComponent<{ - + Date: Mon, 23 Dec 2024 11:19:45 +0900 Subject: [PATCH 32/43] Remove skip history when tx failed --- apps/extension/src/pages/ibc-swap/index.tsx | 95 ++++++++++++--------- 1 file changed, 54 insertions(+), 41 deletions(-) diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index bb3051a5b0..a38a98ed93 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -55,7 +55,10 @@ import { TextButtonProps } from "../../components/button-text"; import { UnsignedEVMTransaction } from "@keplr-wallet/stores-eth"; import { EthTxStatus } from "@keplr-wallet/types"; import { simpleFetch } from "@keplr-wallet/simple-fetch"; -import { RecordTxWithSkipSwapMsg } from "@keplr-wallet/background/build/recent-send-history/temp-skip-message"; +import { + RecordTxWithSkipSwapMsg, + RemoveSkipHistoryMsg, +} from "@keplr-wallet/background/build/recent-send-history/temp-skip-message"; const TextButtonStyles = { Container: styled.div` @@ -1155,6 +1158,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { )}`, }; const sender = ibcSwapConfigs.senderConfig.sender; + let historyId: string | undefined = undefined; await ethereumAccount.sendEthereumTx( sender, @@ -1206,10 +1210,11 @@ export const IBCSwapPage: FunctionComponent = observer(() => { txHash ); - new InExtensionMessageRequester().sendMessage( - BACKGROUND_PORT, - msg - ); + new InExtensionMessageRequester() + .sendMessage(BACKGROUND_PORT, msg) + .then((response) => { + historyId = response; + }); }, onFulfill: (txReceipt) => { const queryBalances = queriesStore.get( @@ -1232,43 +1237,44 @@ export const IBCSwapPage: FunctionComponent = observer(() => { } }); - // onBroadcasted에서 실행하기에는 너무 빨라서 tx not found가 발생할 수 있음 - // 재실행 로직을 사용해도 되지만, txReceipt를 기다렸다가 실행하는 게 자원 낭비가 적음 - const chainIdForTrack = inChainId.replace("eip155:", ""); - - setTimeout(() => { - // no wait - simpleFetch( - "https://api.skip.build/", - "/v2/tx/track", - { - method: "POST", - headers: { - "content-type": "application/json", - ...(() => { - const res: { authorization?: string } = {}; - if (process.env["SKIP_API_KEY"]) { - res.authorization = process.env["SKIP_API_KEY"]; - } - return res; - })(), - }, - body: JSON.stringify({ - tx_hash: txReceipt.transactionHash, - chain_id: chainIdForTrack, - }), - } - ) - .then((result) => { - console.log( - `Skip tx track result: ${JSON.stringify(result)}` - ); - }) - .catch((e) => { - console.log(e); - }); - }, 2000); if (txReceipt.status === EthTxStatus.Success) { + // onBroadcasted에서 실행하기에는 너무 빨라서 tx not found가 발생할 수 있음 + // 재실행 로직을 사용해도 되지만, txReceipt를 기다렸다가 실행하는 게 자원 낭비가 적음 + const chainIdForTrack = inChainId.replace("eip155:", ""); + setTimeout(() => { + // no wait + simpleFetch( + "https://api.skip.build/", + "/v2/tx/track", + { + method: "POST", + headers: { + "content-type": "application/json", + ...(() => { + const res: { authorization?: string } = {}; + if (process.env["SKIP_API_KEY"]) { + res.authorization = + process.env["SKIP_API_KEY"]; + } + return res; + })(), + }, + body: JSON.stringify({ + tx_hash: txReceipt.transactionHash, + chain_id: chainIdForTrack, + }), + } + ) + .then((result) => { + console.log( + `Skip tx track result: ${JSON.stringify(result)}` + ); + }) + .catch((e) => { + console.log(e); + }); + }, 2000); + notification.show( "success", intl.formatMessage({ @@ -1277,6 +1283,13 @@ export const IBCSwapPage: FunctionComponent = observer(() => { "" ); } else { + if (historyId) { + new InExtensionMessageRequester().sendMessage( + BACKGROUND_PORT, + new RemoveSkipHistoryMsg(historyId) + ); + } + notification.show( "failed", intl.formatMessage({ id: "error.transaction-failed" }), From bbd94d7831292d9394d3cfb14e453fc767a2e630 Mon Sep 17 00:00:00 2001 From: delivan Date: Mon, 23 Dec 2024 13:06:57 +0900 Subject: [PATCH 33/43] Update `SendTxAndRecord` service to handle hex address --- apps/extension/src/pages/ibc-swap/index.tsx | 5 ++++- .../background/src/recent-send-history/service.ts | 14 ++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index 7fe4b2dda1..73a5969156 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -877,7 +877,10 @@ export const IBCSwapPage: FunctionComponent = observer(() => { mode, false, ibcSwapConfigs.senderConfig.sender, - accountStore.getAccount(outChainId).bech32Address, + chainStore.isEvmOnlyChain(outChainId) + ? accountStore.getAccount(outChainId) + .ethereumHexAddress + : accountStore.getAccount(outChainId).bech32Address, ibcSwapConfigs.amountConfig.amount.map((amount) => { return { amount: DecUtils.getTenExponentN( diff --git a/packages/background/src/recent-send-history/service.ts b/packages/background/src/recent-send-history/service.ts index a9982fdf3b..b64e4f76b1 100644 --- a/packages/background/src/recent-send-history/service.ts +++ b/packages/background/src/recent-send-history/service.ts @@ -148,10 +148,16 @@ export class RecentSendHistoryService { const destinationChainInfo = this.chainsService.getChainInfoOrThrow(destinationChainId); - Bech32Address.validate( - recipient, - destinationChainInfo.bech32Config?.bech32PrefixAccAddr - ); + if (recipient.startsWith("0x")) { + if (!recipient.match(/^0x[0-9A-Fa-f]*$/) || recipient.length !== 42) { + throw new Error("Recipient address is not valid hex address"); + } + } else { + Bech32Address.validate( + recipient, + destinationChainInfo.bech32Config?.bech32PrefixAccAddr + ); + } const txHash = await this.txService.sendTx(sourceChainId, tx, mode, { silent, From 0af0c3aa22352d168e6e5a4fb4ff767104f0c35a Mon Sep 17 00:00:00 2001 From: delivan Date: Mon, 23 Dec 2024 15:34:07 +0900 Subject: [PATCH 34/43] Fix type error --- apps/extension/src/pages/ibc-swap/index.tsx | 5 +++-- apps/stores-internal/src/skip/ibc-swap.ts | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index 73a5969156..2711718a34 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -1266,12 +1266,13 @@ export const IBCSwapPage: FunctionComponent = observer(() => { }); if (txReceipt.status === EthTxStatus.Success) { if (erc20ApprovalTx) { - delete tx.requiredErc20Approvals; + delete (tx as UnsignedEVMTransactionWithErc20Approvals) + .requiredErc20Approvals; ethereumAccount.setIsSendingTx(true); ethereumAccount.sendEthereumTx( sender, { - ...tx, + ...(tx as UnsignedEVMTransactionWithErc20Approvals), ...secondTxFeeObject, }, { diff --git a/apps/stores-internal/src/skip/ibc-swap.ts b/apps/stores-internal/src/skip/ibc-swap.ts index a888e2a78d..48b734580c 100644 --- a/apps/stores-internal/src/skip/ibc-swap.ts +++ b/apps/stores-internal/src/skip/ibc-swap.ts @@ -1,5 +1,5 @@ import { HasMapStore, IChainInfoImpl } from "@keplr-wallet/stores"; -import { AppCurrency, Currency } from "@keplr-wallet/types"; +import { AppCurrency, Currency, ERC20Currency } from "@keplr-wallet/types"; import { ObservableQueryAssets } from "./assets"; import { computed, makeObservable } from "mobx"; import { ObservableQueryChains } from "./chains"; @@ -457,7 +457,8 @@ export class ObservableQueryIbcSwap extends HasMapStore Date: Mon, 23 Dec 2024 15:39:28 +0900 Subject: [PATCH 35/43] Enable `cosmos <> evm` skip swap history --- apps/extension/src/pages/ibc-swap/index.tsx | 136 +++++++++++++++--- .../src/recent-send-history/service.ts | 105 ++++---------- 2 files changed, 141 insertions(+), 100 deletions(-) diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index a38a98ed93..b5a2f25e74 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -695,6 +695,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { receiver: string; }[] = []; let routeDurationSeconds: number | undefined; + let isInterchainSwap: boolean = false; // queryRoute는 ibc history를 추적하기 위한 채널 정보 등을 얻기 위해서 사용된다. // /msgs_direct로도 얻을 순 있지만 따로 데이터를 해석해야되기 때문에 좀 힘들다... @@ -716,7 +717,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { // bridge가 필요한 경우와, 아닌 경우를 나눠서 처리 // swap, transfer 이외의 다른 operation이 있으면 bridge가 사용된다. const operations = queryRoute.response.data.operations; - const isInterchainSwap = operations.some( + isInterchainSwap = operations.some( (operation) => !("swap" in operation) && !("transfer" in operation) ); @@ -951,7 +952,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { currencies: chainStore.getChain(outChainId).currencies, }, - true + !isInterchainSwap // ibc swap이 아닌 interchain swap인 경우, ibc swap history에 추가하는 대신 skip swap history를 추가한다. ); return await new InExtensionMessageRequester().sendMessage( @@ -962,31 +963,85 @@ export const IBCSwapPage: FunctionComponent = observer(() => { }, }, { - onBroadcasted: () => { - if ( - !chainStore.isEnabledChain( - ibcSwapConfigs.amountConfig.outChainId - ) - ) { - chainStore.enableChainInfoInUI( - ibcSwapConfigs.amountConfig.outChainId + onBroadcasted: (txHash) => { + if (isInterchainSwap) { + const msg = new RecordTxWithSkipSwapMsg( + inChainId, + outChainId, + { + chainId: outChainId, + denom: outCurrency.coinMinimalDenom, + expectedAmount: ibcSwapConfigs.amountConfig.outAmount + .toDec() + .toString(), + }, + simpleRoute, + ibcSwapConfigs.senderConfig.sender, + [ + ...ibcSwapConfigs.amountConfig.amount.map( + (amount) => { + return { + amount: DecUtils.getTenExponentN( + amount.currency.coinDecimals + ) + .mul(amount.toDec()) + .toString(), + denom: amount.currency.coinMinimalDenom, + }; + } + ), + { + amount: DecUtils.getTenExponentN( + ibcSwapConfigs.amountConfig.outAmount.currency + .coinDecimals + ) + .mul( + ibcSwapConfigs.amountConfig.outAmount.toDec() + ) + .toString(), + denom: + ibcSwapConfigs.amountConfig.outAmount.currency + .coinMinimalDenom, + }, + ], + { + currencies: + chainStore.getChain(outChainId).currencies, + }, + routeDurationSeconds ?? 0, + Buffer.from(txHash).toString("hex") + ); + + new InExtensionMessageRequester().sendMessage( + BACKGROUND_PORT, + msg ); - if (keyRingStore.selectedKeyInfo) { - const outChainInfo = chainStore.getChain( + if ( + !chainStore.isEnabledChain( + ibcSwapConfigs.amountConfig.outChainId + ) + ) { + chainStore.enableChainInfoInUI( ibcSwapConfigs.amountConfig.outChainId ); - if ( - keyRingStore.needKeyCoinTypeFinalize( - keyRingStore.selectedKeyInfo.id, - outChainInfo - ) - ) { - keyRingStore.finalizeKeyCoinType( - keyRingStore.selectedKeyInfo.id, - outChainInfo.chainId, - outChainInfo.bip44.coinType + + if (keyRingStore.selectedKeyInfo) { + const outChainInfo = chainStore.getChain( + ibcSwapConfigs.amountConfig.outChainId ); + if ( + keyRingStore.needKeyCoinTypeFinalize( + keyRingStore.selectedKeyInfo.id, + outChainInfo + ) + ) { + keyRingStore.finalizeKeyCoinType( + keyRingStore.selectedKeyInfo.id, + outChainInfo.chainId, + outChainInfo.bip44.coinType + ); + } } } } @@ -1116,6 +1171,43 @@ export const IBCSwapPage: FunctionComponent = observer(() => { ); return; } + + if (isInterchainSwap) { + setTimeout(() => { + // no wait + simpleFetch( + "https://api.skip.build/", + "/v2/tx/track", + { + method: "POST", + headers: { + "content-type": "application/json", + ...(() => { + const res: { authorization?: string } = {}; + if (process.env["SKIP_API_KEY"]) { + res.authorization = + process.env["SKIP_API_KEY"]; + } + return res; + })(), + }, + body: JSON.stringify({ + tx_hash: tx.hash, + chain_id: inChainId, + }), + } + ) + .then((result) => { + console.log( + `Skip tx track result: ${JSON.stringify(result)}` + ); + }) + .catch((e) => { + console.log(e); + }); + }, 2000); + } + notification.show( "success", intl.formatMessage({ diff --git a/packages/background/src/recent-send-history/service.ts b/packages/background/src/recent-send-history/service.ts index 7d75e50c8e..3e28bb83cc 100644 --- a/packages/background/src/recent-send-history/service.ts +++ b/packages/background/src/recent-send-history/service.ts @@ -357,22 +357,24 @@ export class RecentSendHistoryService { }, }); - const id = this.addRecentIBCSwapHistory( - swapType, - sourceChainId, - destinationChainId, - sender, - amount, - memo, - ibcChannels, - destinationAsset, - swapChannelIndex, - swapReceiver, - notificationInfo, - txHash - ); + if (isSkipTrack) { + const id = this.addRecentIBCSwapHistory( + swapType, + sourceChainId, + destinationChainId, + sender, + amount, + memo, + ibcChannels, + destinationAsset, + swapChannelIndex, + swapReceiver, + notificationInfo, + txHash + ); - this.trackIBCPacketForwardingRecursive(id); + this.trackIBCPacketForwardingRecursive(id); + } return txHash; } @@ -1221,7 +1223,7 @@ export class RecentSendHistoryService { { maxRetries, waitMsAfterError, - maxWaitMsAfterError: 60 * 1000, // 1min + maxWaitMsAfterError: 30 * 1000, // 30sec } ); } @@ -1264,10 +1266,9 @@ export class RecentSendHistoryService { return; } - const chainId = history.chainId.replace("eip155:", ""); const request: StatusRequest = { tx_hash: history.txHash, - chain_id: chainId, + chain_id: history.chainId.replace("eip155:", ""), }; const requestParams = new URLSearchParams(request).toString(); @@ -1313,6 +1314,7 @@ export class RecentSendHistoryService { default: // 여기서부터는 status가 '정상적으로 트래킹이 진행 중'이라고 가정 + history.trackError = undefined; // 에러 초기화 // 라우트, 현재 라우트 인덱스 가져오기 (처음에 -1로 초기화되어 있음) const route = history.simpleRoute; @@ -1350,7 +1352,7 @@ export class RecentSendHistoryService { "Unknown IBC transfer error"; break; case "TRANSFER_FAILURE": - targetChainId = ibc.from_chain_id; + targetChainId = ibc.to_chain_id; // to에서 오류가 발생하여 from으로 돌아감 errorMsg = ibc.packet_txs.error?.message ?? "IBC transfer failed"; break; @@ -1370,17 +1372,7 @@ export class RecentSendHistoryService { errorMsg = "Unknown Axelar transfer error"; break; case "AXELAR_TRANSFER_FAILURE": - const fromChainId = axelar.from_chain_id; - const toChainId = axelar.to_chain_id; - if ( - transfer_asset_release && - transfer_asset_release.chain_id === toChainId - ) { - targetChainId = toChainId; - } else { - targetChainId = fromChainId; - } - + targetChainId = axelar.to_chain_id; // to로 자산이 이동하다가 실패 (source <- target 표시를 위해 to_chain_id로 설정) errorMsg = (axelar.txs as any).error?.message ?? "Axelar transfer failed"; @@ -1417,17 +1409,7 @@ export class RecentSendHistoryService { errorMsg = "Unknown Hyperlane transfer error"; break; case "HYPERLANE_TRANSFER_FAILED": - const fromChainId = hyperlane.from_chain_id; - const toChainId = hyperlane.to_chain_id; - if ( - transfer_asset_release && - transfer_asset_release.chain_id === toChainId - ) { - targetChainId = toChainId; - } else { - targetChainId = fromChainId; - } - + targetChainId = hyperlane.to_chain_id; // to로 자산이 이동하다가 실패 errorMsg = "Hyperlane transfer failed"; break; case "HYPERLANE_TRANSFER_SENT": @@ -1453,8 +1435,6 @@ export class RecentSendHistoryService { } } else if ("go_fast_transfer" in transfer) { const gofast = transfer.go_fast_transfer; - const fromChainId = gofast.from_chain_id; - const toChainId = gofast.to_chain_id; switch (gofast.state) { case "GO_FAST_TRANSFER_UNKNOWN": @@ -1465,37 +1445,15 @@ export class RecentSendHistoryService { targetChainId = gofast.from_chain_id; break; case "GO_FAST_TRANSFER_TIMEOUT": - if ( - transfer_asset_release && - transfer_asset_release.chain_id === toChainId - ) { - targetChainId = toChainId; - } else { - targetChainId = fromChainId; - } - + targetChainId = gofast.from_chain_id; errorMsg = "GoFast transfer timeout"; break; case "GO_FAST_POST_ACTION_FAILED": - if ( - transfer_asset_release && - transfer_asset_release.chain_id === toChainId - ) { - targetChainId = toChainId; - } else { - targetChainId = fromChainId; - } + targetChainId = gofast.from_chain_id; errorMsg = "GoFast post action failed"; break; case "GO_FAST_TRANSFER_REFUNDED": - if ( - transfer_asset_release && - transfer_asset_release.chain_id === toChainId - ) { - targetChainId = toChainId; - } else { - targetChainId = fromChainId; - } + targetChainId = gofast.to_chain_id; errorMsg = "GoFast transfer refunded"; break; case "GO_FAST_TRANSFER_FILLED": @@ -1511,16 +1469,7 @@ export class RecentSendHistoryService { errorMsg = "Unknown Stargate transfer error"; break; case "STARGATE_TRANSFER_FAILED": - const fromChainId = stargate.from_chain_id; - const toChainId = stargate.to_chain_id; - if ( - transfer_asset_release && - transfer_asset_release.chain_id === toChainId - ) { - targetChainId = toChainId; - } else { - targetChainId = fromChainId; - } + targetChainId = stargate.to_chain_id; errorMsg = "Stargate transfer failed"; break; case "STARGATE_TRANSFER_SENT": From 989aaee3e999d5a40201028ea6df194956b0378d Mon Sep 17 00:00:00 2001 From: rowan Date: Mon, 23 Dec 2024 16:24:44 +0900 Subject: [PATCH 36/43] Improve information displayed by `SkipHistoryViewItem` component --- .../components/ibc-history-view/index.tsx | 71 ++++++++----------- 1 file changed, 28 insertions(+), 43 deletions(-) diff --git a/apps/extension/src/pages/main/components/ibc-history-view/index.tsx b/apps/extension/src/pages/main/components/ibc-history-view/index.tsx index 135b6e69ae..a7b5d21f98 100644 --- a/apps/extension/src/pages/main/components/ibc-history-view/index.tsx +++ b/apps/extension/src/pages/main/components/ibc-history-view/index.tsx @@ -957,7 +957,7 @@ const SkipHistoryViewItem: FunctionComponent<{ return intl.formatMessage( { - id: "page.main.components.ibc-history-view.ibc-swap.succeed.paragraph", + id: "page.main.components.ibc-history-view.ibc-swap.succeed.paragraph", // TODO: Change the content }, { assets: destinationDenom, @@ -965,8 +965,8 @@ const SkipHistoryViewItem: FunctionComponent<{ ); } - // bridge, swap 등으로 경로가 길어져서 자산 정보가 2개 이상인 경우가 많음. - // 따라서 첫 번째 자산 정보만 보여줌. + // skip history의 amount에는 [sourceChain의 amount, destinationChain의 expected amount]가 들어있으므로 + // 첫 번째 amount를 사용 const assets = (() => { const amount = history.amount[0]; const currency = sourceChain.forceFindCurrency(amount.denom); @@ -1023,8 +1023,7 @@ const SkipHistoryViewItem: FunctionComponent<{ return chainIds.map((chainId, i) => { const chainInfo = chainStore.getChain(chainId); - const completed = - !!history.trackDone && i <= history.routeIndex; + const completed = !!history.trackDone || i < history.routeIndex; const error = !!history.trackError && i === failedRouteIndex; return ( @@ -1046,11 +1045,7 @@ const SkipHistoryViewItem: FunctionComponent<{ return true; } - if (!history.trackDone) { - return false; - } - - return i - 1 === history.routeIndex; + return i === history.routeIndex; })()} arrowDirection={(() => { if (!failedRoute) { @@ -1089,33 +1084,34 @@ const SkipHistoryViewItem: FunctionComponent<{ history.trackError && transferAssetRelease ) { - const isOnlyEvm = chainStore.hasChain( - `eip155:${transferAssetRelease.chain_id}` - ); - const releasedChain = chainStore.getChain( - isOnlyEvm - ? `eip155:${transferAssetRelease.chain_id}` - : transferAssetRelease.chain_id - ); - const destinationDenom = (() => { - const currency = releasedChain.forceFindCurrency( - transferAssetRelease.denom + // 자산이 성공적으로 반환된 경우 + if (transferAssetRelease && transferAssetRelease.released) { + const isOnlyEvm = chainStore.hasChain( + `eip155:${transferAssetRelease.chain_id}` + ); + const releasedChain = chainStore.getChain( + isOnlyEvm + ? `eip155:${transferAssetRelease.chain_id}` + : transferAssetRelease.chain_id ); + const destinationDenom = (() => { + const currency = releasedChain.forceFindCurrency( + transferAssetRelease.denom + ); + + if ( + "originCurrency" in currency && + currency.originCurrency + ) { + return currency.originCurrency.coinDenom; + } - if ( - "originCurrency" in currency && - currency.originCurrency - ) { - return currency.originCurrency.coinDenom; - } + return currency.coinDenom; + })(); - return currency.coinDenom; - })(); - // 자산이 성공적으로 반환된 경우 - if (transferAssetRelease.released) { return intl.formatMessage( { - id: "page.main.components.ibc-history-view.ibc-swap.failed.after-swap.complete", + id: "page.main.components.ibc-history-view.ibc-swap.failed.after-swap.complete", // TODO: Change the content }, { chain: releasedChain.chainName, @@ -1123,17 +1119,6 @@ const SkipHistoryViewItem: FunctionComponent<{ } ); } - - // 자산이 반환되지 않은 경우 - return intl.formatMessage( - { - id: "page.main.components.ibc-history-view.ibc-swap.failed.after-swap.complete", - }, - { - chain: releasedChain.chainName, - assets: destinationDenom, - } - ); } return completedAnyways From 9928b6a67186dc564ecec93bc77c55aba7555cd6 Mon Sep 17 00:00:00 2001 From: rowan Date: Mon, 23 Dec 2024 17:34:06 +0900 Subject: [PATCH 37/43] Refactor `trackSkipSwapRecursiveInternal` method for `RecentSendHistoryService` --- apps/extension/src/pages/ibc-swap/index.tsx | 24 +- .../components/ibc-history-view/index.tsx | 91 ++-- .../src/recent-send-history/service.ts | 484 +++++++++--------- .../recent-send-history/temp-skip-types.ts | 5 + .../src/recent-send-history/types.ts | 20 +- 5 files changed, 332 insertions(+), 292 deletions(-) diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index b5a2f25e74..02ac300479 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -696,6 +696,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { }[] = []; let routeDurationSeconds: number | undefined; let isInterchainSwap: boolean = false; + let skipHistoryId: string | undefined = undefined; // queryRoute는 ibc history를 추적하기 위한 채널 정보 등을 얻기 위해서 사용된다. // /msgs_direct로도 얻을 순 있지만 따로 데이터를 해석해야되기 때문에 좀 힘들다... @@ -1012,10 +1013,11 @@ export const IBCSwapPage: FunctionComponent = observer(() => { Buffer.from(txHash).toString("hex") ); - new InExtensionMessageRequester().sendMessage( - BACKGROUND_PORT, - msg - ); + new InExtensionMessageRequester() + .sendMessage(BACKGROUND_PORT, msg) + .then((response) => { + skipHistoryId = response; + }); if ( !chainStore.isEnabledChain( @@ -1164,6 +1166,13 @@ export const IBCSwapPage: FunctionComponent = observer(() => { onFulfill: (tx: any) => { if (tx.code != null && tx.code !== 0) { console.log(tx.log ?? tx.raw_log); + if (skipHistoryId) { + new InExtensionMessageRequester().sendMessage( + BACKGROUND_PORT, + new RemoveSkipHistoryMsg(skipHistoryId) + ); + } + notification.show( "failed", intl.formatMessage({ id: "error.transaction-failed" }), @@ -1250,7 +1259,6 @@ export const IBCSwapPage: FunctionComponent = observer(() => { )}`, }; const sender = ibcSwapConfigs.senderConfig.sender; - let historyId: string | undefined = undefined; await ethereumAccount.sendEthereumTx( sender, @@ -1305,7 +1313,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { new InExtensionMessageRequester() .sendMessage(BACKGROUND_PORT, msg) .then((response) => { - historyId = response; + skipHistoryId = response; }); }, onFulfill: (txReceipt) => { @@ -1375,10 +1383,10 @@ export const IBCSwapPage: FunctionComponent = observer(() => { "" ); } else { - if (historyId) { + if (skipHistoryId) { new InExtensionMessageRequester().sendMessage( BACKGROUND_PORT, - new RemoveSkipHistoryMsg(historyId) + new RemoveSkipHistoryMsg(skipHistoryId) ); } diff --git a/apps/extension/src/pages/main/components/ibc-history-view/index.tsx b/apps/extension/src/pages/main/components/ibc-history-view/index.tsx index a7b5d21f98..ea7a3be061 100644 --- a/apps/extension/src/pages/main/components/ibc-history-view/index.tsx +++ b/apps/extension/src/pages/main/components/ibc-history-view/index.tsx @@ -179,66 +179,77 @@ export const IbcHistoryView: FunctionComponent<{ }; }); - const filteredHistories = histories.filter((history) => { - const account = accountStore.getAccount(history.chainId); - if (account.bech32Address === history.sender) { - return true; - } - return false; - }); + const filteredHistories = (() => { + const filteredIBCHistories = histories.filter((history) => { + const account = accountStore.getAccount(history.chainId); + if (account.bech32Address === history.sender) { + return true; + } + return false; + }); + + const filteredSkipHistories = skipHistories.filter((history) => { + const firstRoute = history.simpleRoute[0]; + const account = accountStore.getAccount(firstRoute.chainId); - const filteredSkipHistories = skipHistories.filter((history) => { - const firstRoute = history.simpleRoute[0]; - const account = accountStore.getAccount(firstRoute.chainId); + if (firstRoute.isOnlyEvm) { + if (account.ethereumHexAddress === history.sender) { + return true; + } + return false; + } - if (firstRoute.isOnlyEvm) { - if (account.ethereumHexAddress === history.sender) { + if (account.bech32Address === history.sender) { return true; } return false; - } + }); - if (account.bech32Address === history.sender) { - return true; + if (isNotReady) { + return null; } - return false; - }); - if (isNotReady) { - return null; - } + return [...filteredIBCHistories, ...filteredSkipHistories].sort( + (a, b) => b.timestamp - a.timestamp // The latest history should be shown first + ); + })(); return ( - {filteredHistories.reverse().map((history) => { + {filteredHistories?.map((history) => { + if ("ibcHistory" in history) { + return ( + { + const requester = new InExtensionMessageRequester(); + const msg = new RemoveIBCHistoryMsg(id); + requester + .sendMessage(BACKGROUND_PORT, msg) + .then((histories) => { + setHistories(histories); + }); + }} + /> + ); + } + return ( - { const requester = new InExtensionMessageRequester(); - const msg = new RemoveIBCHistoryMsg(id); + const msg = new RemoveSkipHistoryMsg(id); requester.sendMessage(BACKGROUND_PORT, msg).then((histories) => { - setHistories(histories); + setSkipHistories(histories); }); }} /> ); })} - {filteredSkipHistories.reverse().map((_history) => ( - { - const requester = new InExtensionMessageRequester(); - const msg = new RemoveSkipHistoryMsg(id); - requester.sendMessage(BACKGROUND_PORT, msg).then((histories) => { - setSkipHistories(histories); - }); - }} - /> - ))} - {filteredHistories.length > 0 || filteredSkipHistories.length > 0 ? ( + {filteredHistories && filteredHistories.length > 0 ? ( ) : null} @@ -966,7 +977,7 @@ const SkipHistoryViewItem: FunctionComponent<{ } // skip history의 amount에는 [sourceChain의 amount, destinationChain의 expected amount]가 들어있으므로 - // 첫 번째 amount를 사용 + // 첫 번째 amount만 사용 const assets = (() => { const amount = history.amount[0]; const currency = sourceChain.forceFindCurrency(amount.denom); @@ -1024,7 +1035,7 @@ const SkipHistoryViewItem: FunctionComponent<{ return chainIds.map((chainId, i) => { const chainInfo = chainStore.getChain(chainId); const completed = !!history.trackDone || i < history.routeIndex; - const error = !!history.trackError && i === failedRouteIndex; + const error = !!history.trackError && i >= failedRouteIndex; return ( // 일부분 순환하는 경우도 이론적으로 가능은 하기 때문에 chain id를 key로 사용하지 않음. diff --git a/packages/background/src/recent-send-history/service.ts b/packages/background/src/recent-send-history/service.ts index 3e28bb83cc..d3925e4519 100644 --- a/packages/background/src/recent-send-history/service.ts +++ b/packages/background/src/recent-send-history/service.ts @@ -1201,7 +1201,7 @@ export class RecentSendHistoryService { history.timestamp + history.routeDurationSeconds * 1000; const diff = expectedEndTimestamp - now; - const waitMsAfterError = 10 * 1000; + const waitMsAfterError = 5 * 1000; const maxRetries = diff > 0 ? diff / waitMsAfterError : 10; retry( @@ -1240,35 +1240,35 @@ export class RecentSendHistoryService { return; } - const needRun = (() => { - const status = history.trackStatus; - const trackDone = history.trackDone; - const routeIndex = history.routeIndex; + const { txHash, chainId, trackStatus, trackDone, routeIndex, simpleRoute } = + history; - // status가 COMPLETED이지만, 아직 다음 라우트가 남아있는 경우 + // 실행이 필요한지 판별 + const needRun = (() => { + // 1) 상태가 COMPLETED인데 아직 다음 라우트가 남아 있는 경우 if ( - status?.includes("COMPLETED") && - routeIndex !== history.simpleRoute.length - 1 + trackStatus?.includes("COMPLETED") && + routeIndex !== simpleRoute.length - 1 ) { return true; } - - // status가 없거나, track이 완료되지 않은 경우 - if (!status || !trackDone) { + // 2) status가 없거나, track이 완료되지 않은 경우 + if (!trackStatus || !trackDone) { return true; } - return false; })(); + // 더 이상 진행할 필요가 없다면 종료 if (!needRun) { onFulfill(); return; } + // Skip API에 보낼 request 정보 const request: StatusRequest = { - tx_hash: history.txHash, - chain_id: history.chainId.replace("eip155:", ""), + tx_hash: txHash, + chain_id: chainId.replace("eip155:", ""), }; const requestParams = new URLSearchParams(request).toString(); @@ -1291,7 +1291,6 @@ export class RecentSendHistoryService { ) .then((res) => { const { - // status, state, error, transfer_sequence, @@ -1299,239 +1298,256 @@ export class RecentSendHistoryService { transfer_asset_release, } = res.data; - // 상태 저장 + // 상태 갱신 history.trackStatus = state; - // status(상위 상태)와 state(하위 범주)에 따른 분기 - switch (state) { - // 트래킹 불확실/미완료 상태 => 에러 처리 후 재시도 - case "STATE_SUBMITTED": - case "STATE_RECEIVED": - case "STATE_COMPLETED": - case "STATE_UNKNOWN": - onError(); - return; + // 트래킹이 불확실하거나 미완료 상태에 해당하면 에러 처리 후 재시도 + if ( + [ + "STATE_SUBMITTED", + "STATE_RECEIVED", + "STATE_COMPLETED", + "STATE_UNKNOWN", + ].includes(state) + ) { + onError(); + return; + } - default: - // 여기서부터는 status가 '정상적으로 트래킹이 진행 중'이라고 가정 - history.trackError = undefined; // 에러 초기화 + // 정상적인 트래킹 진행으로 가정 + history.trackError = undefined; - // 라우트, 현재 라우트 인덱스 가져오기 (처음에 -1로 초기화되어 있음) - const route = history.simpleRoute; - const currentRouteIndex = - history.routeIndex < 0 ? 0 : history.routeIndex; + const currentRouteIndex = + history.routeIndex < 0 ? 0 : history.routeIndex; + let nextRouteIndex = currentRouteIndex; + let errorMsg: string | undefined = error?.message; - let nextRouteIndex = currentRouteIndex; // 다음 라우트 인덱스, src와 dst가 동일할 수 있으므로 초기값은 현재 인덱스로 설정 - let errorMsg: string | undefined = error?.message; // 우선 API에서 내려오는 error?.message 세팅 + // 언락된 자산 정보가 있으면 저장 + if (transfer_asset_release) { + history.transferAssetRelease = transfer_asset_release; + } - // 언락된 asset 정보가 있는 경우 저장 - if (transfer_asset_release) { - history.transferAssetRelease = transfer_asset_release; - } + // 다음 blocking transfer의 인덱스 + const nextBlockingTransferIndex = + next_blocking_transfer?.transfer_sequence_index ?? + transfer_sequence.length - 1; + const transfer = transfer_sequence[nextBlockingTransferIndex]; + + // ------------------------- + // 어떤 타입의 transfer인지 확인하여 targetChainId / errorMsg 결정 (if-else 체인) + // ------------------------- + let targetChainId: string | undefined; + + if ("ibc_transfer" in transfer) { + const { + state: ibcState, + from_chain_id, + to_chain_id, + packet_txs, + } = transfer.ibc_transfer; + switch (ibcState) { + case "TRANSFER_UNKNOWN": + targetChainId = from_chain_id; + errorMsg = + packet_txs.error?.message ?? "Unknown IBC transfer error"; + break; + case "TRANSFER_FAILURE": + targetChainId = to_chain_id; + errorMsg = packet_txs.error?.message ?? "IBC transfer failed"; + break; + case "TRANSFER_PENDING": + case "TRANSFER_RECEIVED": + targetChainId = from_chain_id; + break; + case "TRANSFER_SUCCESS": + targetChainId = to_chain_id; + break; + } + } else if ("axelar_transfer" in transfer) { + const { + state: axelarState, + from_chain_id, + to_chain_id, + } = transfer.axelar_transfer; + switch (axelarState) { + case "AXELAR_TRANSFER_UNKNOWN": + targetChainId = from_chain_id; + errorMsg = "Unknown Axelar transfer error"; + break; + case "AXELAR_TRANSFER_FAILURE": + targetChainId = to_chain_id; + errorMsg = "Axelar transfer failed"; + break; + case "AXELAR_TRANSFER_PENDING_CONFIRMATION": + case "AXELAR_TRANSFER_PENDING_RECEIPT": + targetChainId = from_chain_id; + break; + case "AXELAR_TRANSFER_SUCCESS": + targetChainId = to_chain_id; + break; + } + } else if ("cctp_transfer" in transfer) { + const { + state: cctpState, + from_chain_id, + to_chain_id, + } = transfer.cctp_transfer; + switch (cctpState) { + case "CCTP_TRANSFER_UNKNOWN": + targetChainId = from_chain_id; + errorMsg = "Unknown CCTP transfer error"; + break; + case "CCTP_TRANSFER_CONFIRMED": + case "CCTP_TRANSFER_PENDING_CONFIRMATION": + case "CCTP_TRANSFER_SENT": + targetChainId = from_chain_id; + break; + case "CCTP_TRANSFER_RECEIVED": + targetChainId = to_chain_id; + break; + } + } else if ("hyperlane_transfer" in transfer) { + const { + state: hyperState, + from_chain_id, + to_chain_id, + } = transfer.hyperlane_transfer; + switch (hyperState) { + case "HYPERLANE_TRANSFER_UNKNOWN": + targetChainId = from_chain_id; + errorMsg = "Unknown Hyperlane transfer error"; + break; + case "HYPERLANE_TRANSFER_FAILED": + targetChainId = to_chain_id; + errorMsg = "Hyperlane transfer failed"; + break; + case "HYPERLANE_TRANSFER_SENT": + targetChainId = from_chain_id; + break; + case "HYPERLANE_TRANSFER_RECEIVED": + targetChainId = to_chain_id; + break; + } + } else if ("op_init_transfer" in transfer) { + const { + state: opState, + from_chain_id, + to_chain_id, + } = transfer.op_init_transfer; + switch (opState) { + case "OPINIT_TRANSFER_UNKNOWN": + targetChainId = from_chain_id; + errorMsg = "Unknown OP_INIT transfer error"; + break; + case "OPINIT_TRANSFER_RECEIVED": + targetChainId = from_chain_id; + break; + case "OPINIT_TRANSFER_SENT": + targetChainId = to_chain_id; + break; + } + } else if ("go_fast_transfer" in transfer) { + const { + state: gofastState, + from_chain_id, + to_chain_id, + } = transfer.go_fast_transfer; + switch (gofastState) { + case "GO_FAST_TRANSFER_UNKNOWN": + targetChainId = from_chain_id; + errorMsg = "Unknown GoFast transfer error"; + break; + case "GO_FAST_TRANSFER_SENT": + targetChainId = from_chain_id; + break; + case "GO_FAST_TRANSFER_TIMEOUT": + targetChainId = from_chain_id; + errorMsg = "GoFast transfer timeout"; + break; + case "GO_FAST_POST_ACTION_FAILED": + targetChainId = from_chain_id; + errorMsg = "GoFast post action failed"; + break; + case "GO_FAST_TRANSFER_REFUNDED": + targetChainId = to_chain_id; + errorMsg = "GoFast transfer refunded"; + break; + case "GO_FAST_TRANSFER_FILLED": + targetChainId = to_chain_id; + break; + } + } else if (transfer.stargate_transfer) { + const { + state: sgState, + from_chain_id, + to_chain_id, + } = transfer.stargate_transfer; + switch (sgState) { + case "STARGATE_TRANSFER_UNKNOWN": + targetChainId = from_chain_id; + errorMsg = "Unknown Stargate transfer error"; + break; + case "STARGATE_TRANSFER_FAILED": + targetChainId = to_chain_id; + errorMsg = "Stargate transfer failed"; + break; + case "STARGATE_TRANSFER_SENT": + targetChainId = from_chain_id; + break; + case "STARGATE_TRANSFER_RECEIVED": + targetChainId = to_chain_id; + break; + } + } - // 현재 처리중인 transfer의 인덱스 가져오기, 없을 경우 -1 - const nextBlockingTransferIndex = - next_blocking_transfer?.transfer_sequence_index ?? - transfer_sequence.length - 1; - - // 다음 blocking transfer의 정보를 가져옴 - const transfer = transfer_sequence[nextBlockingTransferIndex]; - - // ------------------------- - // 1) 일단 targetChainId/errorMsg를 구하는 단계 - // ------------------------- - let targetChainId: string | undefined; - // 예: `transfer`가 여러 타입 중 하나일 것이므로 순서대로 체크 - if ("ibc_transfer" in transfer) { - const ibc = transfer.ibc_transfer; - switch (ibc.state) { - case "TRANSFER_UNKNOWN": - targetChainId = ibc.from_chain_id; - errorMsg = - ibc.packet_txs.error?.message ?? - "Unknown IBC transfer error"; - break; - case "TRANSFER_FAILURE": - targetChainId = ibc.to_chain_id; // to에서 오류가 발생하여 from으로 돌아감 - errorMsg = - ibc.packet_txs.error?.message ?? "IBC transfer failed"; - break; - case "TRANSFER_PENDING": - case "TRANSFER_RECEIVED": - targetChainId = ibc.from_chain_id; - break; - case "TRANSFER_SUCCESS": - targetChainId = ibc.to_chain_id; - break; - } - } else if ("axelar_transfer" in transfer) { - const axelar = transfer.axelar_transfer; - switch (axelar.state) { - case "AXELAR_TRANSFER_UNKNOWN": - targetChainId = axelar.from_chain_id; - errorMsg = "Unknown Axelar transfer error"; - break; - case "AXELAR_TRANSFER_FAILURE": - targetChainId = axelar.to_chain_id; // to로 자산이 이동하다가 실패 (source <- target 표시를 위해 to_chain_id로 설정) - errorMsg = - (axelar.txs as any).error?.message ?? - "Axelar transfer failed"; - break; - case "AXELAR_TRANSFER_PENDING_CONFIRMATION": - case "AXELAR_TRANSFER_PENDING_RECEIPT": - targetChainId = axelar.from_chain_id; - break; - case "AXELAR_TRANSFER_SUCCESS": - targetChainId = axelar.to_chain_id; - break; - } - } else if ("cctp_transfer" in transfer) { - const cctp = transfer.cctp_transfer; - switch (cctp.state) { - case "CCTP_TRANSFER_UNKNOWN": - targetChainId = cctp.from_chain_id; - errorMsg = "Unknown CCTP transfer error"; - break; - case "CCTP_TRANSFER_CONFIRMED": - case "CCTP_TRANSFER_PENDING_CONFIRMATION": - case "CCTP_TRANSFER_SENT": - targetChainId = cctp.from_chain_id; - break; - case "CCTP_TRANSFER_RECEIVED": - targetChainId = cctp.to_chain_id; - break; - } - } else if ("hyperlane_transfer" in transfer) { - const hyperlane = transfer.hyperlane_transfer; - switch (hyperlane.state) { - case "HYPERLANE_TRANSFER_UNKNOWN": - targetChainId = hyperlane.from_chain_id; - errorMsg = "Unknown Hyperlane transfer error"; - break; - case "HYPERLANE_TRANSFER_FAILED": - targetChainId = hyperlane.to_chain_id; // to로 자산이 이동하다가 실패 - errorMsg = "Hyperlane transfer failed"; - break; - case "HYPERLANE_TRANSFER_SENT": - targetChainId = hyperlane.from_chain_id; - break; - case "HYPERLANE_TRANSFER_RECEIVED": - targetChainId = hyperlane.to_chain_id; - break; - } - } else if ("op_init_transfer" in transfer) { - const opinit = transfer.op_init_transfer; - switch (opinit.state) { - case "OPINIT_TRANSFER_UNKNOWN": - targetChainId = opinit.from_chain_id; - errorMsg = "Unknown OP_INIT transfer error"; - break; - case "OPINIT_TRANSFER_RECEIVED": - targetChainId = opinit.from_chain_id; - break; - case "OPINIT_TRANSFER_SENT": - targetChainId = opinit.to_chain_id; - break; - } - } else if ("go_fast_transfer" in transfer) { - const gofast = transfer.go_fast_transfer; - - switch (gofast.state) { - case "GO_FAST_TRANSFER_UNKNOWN": - targetChainId = gofast.from_chain_id; - errorMsg = "Unknown GoFast transfer error"; - break; - case "GO_FAST_TRANSFER_SENT": - targetChainId = gofast.from_chain_id; - break; - case "GO_FAST_TRANSFER_TIMEOUT": - targetChainId = gofast.from_chain_id; - errorMsg = "GoFast transfer timeout"; - break; - case "GO_FAST_POST_ACTION_FAILED": - targetChainId = gofast.from_chain_id; - errorMsg = "GoFast post action failed"; - break; - case "GO_FAST_TRANSFER_REFUNDED": - targetChainId = gofast.to_chain_id; - errorMsg = "GoFast transfer refunded"; - break; - case "GO_FAST_TRANSFER_FILLED": - targetChainId = gofast.to_chain_id; - break; - } - } else { - // stargate_transfer - const stargate = transfer.stargate_transfer; - switch (stargate.state) { - case "STARGATE_TRANSFER_UNKNOWN": - targetChainId = stargate.from_chain_id; - errorMsg = "Unknown Stargate transfer error"; - break; - case "STARGATE_TRANSFER_FAILED": - targetChainId = stargate.to_chain_id; - errorMsg = "Stargate transfer failed"; - break; - case "STARGATE_TRANSFER_SENT": - targetChainId = stargate.from_chain_id; - break; - case "STARGATE_TRANSFER_RECEIVED": - targetChainId = stargate.to_chain_id; - break; - } + // ------------------------- + // 찾은 targetChainId로 다음 라우트 인덱스를 갱신 + // ------------------------- + if (targetChainId) { + for (let i = currentRouteIndex; i < simpleRoute.length; i++) { + const routeChain = simpleRoute[i].chainId.replace("eip155:", ""); + if ( + routeChain.toLocaleLowerCase() === + targetChainId.toLocaleLowerCase() + ) { + nextRouteIndex = i; + break; } + } - // ------------------------- - // 2) 구한 targetChainId로 nextRouteIndex를 찾는 단계 - // ------------------------- - if (targetChainId) { - for (let i = currentRouteIndex; i < route.length; i++) { - if ( - route[i].chainId - .replace("eip155:", "") - .toLocaleLowerCase() === targetChainId.toLocaleLowerCase() - ) { - nextRouteIndex = i; - break; - } - } + // 찾지못하더라도 optimistic하게 다음 트라이로 이동 + } - // 찾지 못하더라도 optimistic하게 넘어가기 (일단은) - } + // 에러 메시지 갱신 + history.trackError = errorMsg; + // 최종 routeIndex 갱신 + history.routeIndex = nextRouteIndex; - // 에러 메시지 갱신 - history.trackError = errorMsg; - - // 최종적으로 routeIndex 업데이트 - history.routeIndex = nextRouteIndex; - - // state(하위 범주 상태)에 따라 트래킹 완료/재시도 결정 - switch (state) { - case "STATE_ABANDONED": - case "STATE_COMPLETED_ERROR": - case "STATE_COMPLETED_SUCCESS": - // 더 이상 트래킹하지 않아도 됨 - history.trackDone = true; - - if (state === "STATE_COMPLETED_SUCCESS") { - // 성공적으로 완료되었는데, 다음 라우트가 남아있는 경우 - // 마지막 라우트로 항상 도달하도록 업데이트 - if (nextRouteIndex !== route.length - 1) { - history.routeIndex = route.length - 1; - } - } + // state에 따라 트래킹 완료/재시도 결정 + switch (state) { + case "STATE_ABANDONED": + case "STATE_COMPLETED_ERROR": + case "STATE_COMPLETED_SUCCESS": + history.trackDone = true; + + // 성공 상태인데 라우트가 남았다면 마지막 라우트로 이동 + if ( + state === "STATE_COMPLETED_SUCCESS" && + nextRouteIndex !== simpleRoute.length - 1 + ) { + history.routeIndex = simpleRoute.length - 1; + } - // TODO: routeIndex가 끝까지 도달했을 때, 최종적으로 도달한 자산의 양을 가져오는 로직 추가 필요 (어려울 듯) + // TODO: destination asset이 얼마나 transfer되었는지 확인 - onFulfill(); - break; + onFulfill(); + break; - case "STATE_PENDING": - case "STATE_PENDING_ERROR": - // 트래킹 중 (또는 에러 상태 전파 중) => 재시도 - onError(); - break; - } + case "STATE_PENDING": + case "STATE_PENDING_ERROR": + // 아직 트래킹 중이거나 에러 상태 전파 중 => 재시도 + onError(); + break; } }) .catch((e) => { diff --git a/packages/background/src/recent-send-history/temp-skip-types.ts b/packages/background/src/recent-send-history/temp-skip-types.ts index 9b7abdf6c8..164764bf97 100644 --- a/packages/background/src/recent-send-history/temp-skip-types.ts +++ b/packages/background/src/recent-send-history/temp-skip-types.ts @@ -1,3 +1,8 @@ +/** + * This file is a temporary file to store types that are required for tracking the status of a transaction related to the skip-go. + * Reference: https://github.com/skip-mev/skip-go/blob/staging/packages/client/src/types/lifecycle.ts + */ + export type StatusState = | "STATE_UNKNOWN" | "STATE_SUBMITTED" diff --git a/packages/background/src/recent-send-history/types.ts b/packages/background/src/recent-send-history/types.ts index 5c419f3f8c..a3cd7504a1 100644 --- a/packages/background/src/recent-send-history/types.ts +++ b/packages/background/src/recent-send-history/types.ts @@ -104,12 +104,12 @@ export type SkipHistory = { amount: { amount: string; denom: string; - }[]; - txHash: string; + }[]; // [sourceChain asset, destinationChain asset] 형태로 저장 + txHash: string; // hex string - trackDone?: boolean; - trackError?: string; - trackStatus?: StatusState; + trackDone?: boolean; // status tracking이 완료되었는지 여부 + trackError?: string; // status tracking 중 에러가 발생했는지 여부 + trackStatus?: StatusState; // status tracking의 현재 상태 notified?: boolean; notificationInfo?: { @@ -120,20 +120,20 @@ export type SkipHistory = { isOnlyEvm: boolean; chainId: string; receiver: string; - }[]; - routeIndex: number; - routeDurationSeconds: number; + }[]; // 세부적인 채널 정보를 제외, 덩어리 경로 정보만 저장 + routeIndex: number; // 현재까지 진행된 라우팅 인덱스 + routeDurationSeconds: number; // 라우팅에 걸리는 예상 시간 destinationAsset: { chainId: string; denom: string; expectedAmount?: string; - }; + }; // 최종 목적지의 asset 정보 resAmount: { amount: string; denom: string; }[][]; - transferAssetRelease?: TransferAssetRelease; + transferAssetRelease?: TransferAssetRelease; // 라우팅 중간에 실패한 경우, 사용자의 자산이 어디에서 릴리즈 되었는지 정보 }; From 4e4b14d1387912d60bddf0eac019cd990a04908e Mon Sep 17 00:00:00 2001 From: delivan Date: Mon, 23 Dec 2024 17:43:32 +0900 Subject: [PATCH 38/43] Fix from review --- apps/extension/src/pages/ibc-swap/index.tsx | 5 ++- .../extension/src/pages/send/amount/index.tsx | 38 ++++++++++++++++++- apps/hooks-internal/src/ibc-swap/amount.ts | 4 +- apps/stores-internal/src/skip/ibc-swap.ts | 2 +- apps/stores-internal/src/skip/msgs-direct.ts | 6 ++- 5 files changed, 49 insertions(+), 6 deletions(-) diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index 2711718a34..5bece86d0c 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -454,6 +454,8 @@ export const IBCSwapPage: FunctionComponent = observer(() => { ) : undefined; + // OP Stack L1 Data Fee 계산은 일단 무시하기로 한다. + return { simulate: () => ethereumAccount.simulateGas(sender, erc20ApprovalTx ?? tx), @@ -858,7 +860,8 @@ export const IBCSwapPage: FunctionComponent = observer(() => { try { if ("send" in tx) { - const isCCTPTx = tx.ui.type(); + const isCCTPTx = tx.ui.type() === "cctp"; + await tx.send( ibcSwapConfigs.feeConfig.toStdFee(), ibcSwapConfigs.memoConfig.memo, diff --git a/apps/extension/src/pages/send/amount/index.tsx b/apps/extension/src/pages/send/amount/index.tsx index bf2ada4889..fd65c8cbf6 100644 --- a/apps/extension/src/pages/send/amount/index.tsx +++ b/apps/extension/src/pages/send/amount/index.tsx @@ -32,7 +32,7 @@ import { FeeControl } from "../../../components/input/fee-control"; import { useNotification } from "../../../hooks/notification"; import { DenomHelper, ExtensionKVStore } from "@keplr-wallet/common"; import { ENSInfo, ICNSInfo } from "../../../config.ui"; -import { CoinPretty, DecUtils } from "@keplr-wallet/unit"; +import { CoinPretty, Dec, DecUtils } from "@keplr-wallet/unit"; import { ColorPalette } from "../../../styles"; import { openPopupWindow } from "@keplr-wallet/popup"; import { InExtensionMessageRequester } from "@keplr-wallet/router-extension"; @@ -307,6 +307,42 @@ export const SendAmountPage: FunctionComponent = observer(() => { chainInfo.currencies, ]); + useEffect(() => { + (async () => { + if (chainInfo.features.includes("op-stack-l1-data-fee")) { + const { maxFeePerGas, maxPriorityFeePerGas, gasPrice } = + sendConfigs.feeConfig.getEIP1559TxFees(sendConfigs.feeConfig.type); + + const { to, gasLimit, value, data, chainId } = + ethereumAccount.makeSendTokenTx({ + currency: sendConfigs.amountConfig.amount[0].currency, + amount: sendConfigs.amountConfig.amount[0].toDec().toString(), + to: sendConfigs.recipientConfig.recipient, + gasLimit: sendConfigs.gasConfig.gas, + maxFeePerGas: maxFeePerGas?.toString(), + maxPriorityFeePerGas: maxPriorityFeePerGas?.toString(), + gasPrice: gasPrice?.toString(), + }); + + const l1DataFee = await ethereumAccount.simulateOpStackL1Fee({ + to, + gasLimit, + value, + data, + chainId, + }); + sendConfigs.feeConfig.setL1DataFee(new Dec(BigInt(l1DataFee))); + } + })(); + }, [ + chainInfo.features, + ethereumAccount, + sendConfigs.amountConfig.amount, + sendConfigs.feeConfig, + sendConfigs.gasConfig.gas, + sendConfigs.recipientConfig.recipient, + ]); + useEffect(() => { if (isEvmTx) { // Refresh EIP-1559 fee every 12 seconds. diff --git a/apps/hooks-internal/src/ibc-swap/amount.ts b/apps/hooks-internal/src/ibc-swap/amount.ts index e7be13f59c..de0dddc12b 100644 --- a/apps/hooks-internal/src/ibc-swap/amount.ts +++ b/apps/hooks-internal/src/ibc-swap/amount.ts @@ -141,7 +141,7 @@ export class IBCSwapAmountConfig extends AmountConfig { async getTx( slippageTolerancePercent: number, - affiliateFeeReceiver: string, + affiliateFeeReceiver: string | undefined, priorOutAmount?: Int ): Promise { const queryIBCSwap = this.getQueryIBCSwap(); @@ -311,7 +311,7 @@ export class IBCSwapAmountConfig extends AmountConfig { getTxIfReady( slippageTolerancePercent: number, - affiliateFeeReceiver: string + affiliateFeeReceiver?: string ): MakeTxResponse | UnsignedEVMTransactionWithErc20Approvals | undefined { if (!this.currency) { return; diff --git a/apps/stores-internal/src/skip/ibc-swap.ts b/apps/stores-internal/src/skip/ibc-swap.ts index 48b734580c..e7977357d4 100644 --- a/apps/stores-internal/src/skip/ibc-swap.ts +++ b/apps/stores-internal/src/skip/ibc-swap.ts @@ -35,7 +35,7 @@ export class ObservableQueryIBCSwapInner { getQueryMsgsDirect( chainIdsToAddresses: Record, slippageTolerancePercent: number, - affiliateFeeReceiver: string + affiliateFeeReceiver: string | undefined ): ObservableQueryMsgsDirectInner { const inAmount = new CoinPretty( this.chainStore diff --git a/apps/stores-internal/src/skip/msgs-direct.ts b/apps/stores-internal/src/skip/msgs-direct.ts index 94f913e55b..45c7aa0fd9 100644 --- a/apps/stores-internal/src/skip/msgs-direct.ts +++ b/apps/stores-internal/src/skip/msgs-direct.ts @@ -181,6 +181,10 @@ export class ObservableQueryMsgsDirectInner extends ObservableQuery= 2) { + return; + } + const cosmosMsg = msg.cosmos_tx.msgs[0]; if ( cosmosMsg.msg_type_url !== @@ -410,7 +414,7 @@ export class ObservableQueryMsgsDirect extends HasMapStore, slippageTolerancePercent: number, affiliateFeeBps: number, - affiliateFeeReceiver: string, + affiliateFeeReceiver: string | undefined, swapVenues: { readonly name: string; readonly chainId: string; From 69b877126c315d4e2f77d519f24954aabab25efc Mon Sep 17 00:00:00 2001 From: delivan Date: Tue, 24 Dec 2024 16:16:12 +0900 Subject: [PATCH 39/43] Implement received amount fetched on skip history --- apps/extension/src/pages/ibc-swap/index.tsx | 264 ++++++--- .../components/ibc-history-view/index.tsx | 34 +- .../src/recent-send-history/handler.ts | 1 + .../src/recent-send-history/service.ts | 510 ++++++++++++------ .../recent-send-history/temp-skip-message.ts | 5 + .../src/recent-send-history/types.ts | 1 + 6 files changed, 550 insertions(+), 265 deletions(-) diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index 662ab0bf4c..92ccdd60e6 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -959,8 +959,6 @@ export const IBCSwapPage: FunctionComponent = observer(() => { try { if ("send" in tx) { - const isCCTPTx = tx.ui.type() === "cctp"; - await tx.send( ibcSwapConfigs.feeConfig.toStdFee(), ibcSwapConfigs.memoConfig.memo, @@ -969,39 +967,9 @@ export const IBCSwapPage: FunctionComponent = observer(() => { preferNoSetMemo: false, sendTx: async (chainId, tx, mode) => { - if (isCCTPTx) { - // TODO: CCTP를 위한 msg 필요 - const msg: Message = new SendTxAndRecordMsg( - "ibc-swap/cctp", - chainId, - outChainId, - tx, - mode, - false, - ibcSwapConfigs.senderConfig.sender, - chainStore.isEvmOnlyChain(outChainId) - ? accountStore.getAccount(outChainId) - .ethereumHexAddress - : accountStore.getAccount(outChainId).bech32Address, - ibcSwapConfigs.amountConfig.amount.map((amount) => { - return { - amount: DecUtils.getTenExponentN( - amount.currency.coinDecimals - ) - .mul(amount.toDec()) - .toString(), - denom: amount.currency.coinMinimalDenom, - }; - }), - ibcSwapConfigs.memoConfig.memo, - true - ); - return await new InExtensionMessageRequester().sendMessage( - BACKGROUND_PORT, - msg - ); - } else if ( - ibcSwapConfigs.amountConfig.type === "transfer" + if ( + ibcSwapConfigs.amountConfig.type === "transfer" && + !isInterchainSwap ) { const msg: Message = new SendTxAndRecordMsg( "ibc-swap/ibc-transfer", @@ -1087,6 +1055,10 @@ export const IBCSwapPage: FunctionComponent = observer(() => { }, simpleRoute, ibcSwapConfigs.senderConfig.sender, + chainStore.isEvmOnlyChain(outChainId) + ? accountStore.getAccount(outChainId) + .ethereumHexAddress + : accountStore.getAccount(outChainId).bech32Address, [ ...ibcSwapConfigs.amountConfig.amount.map( (amount) => { @@ -1437,61 +1409,69 @@ export const IBCSwapPage: FunctionComponent = observer(() => { }, { onBroadcasted: (txHash) => { - ethereumAccount.setIsSendingTx(false); + if (!erc20ApprovalTx) { + ethereumAccount.setIsSendingTx(false); - const msg = new RecordTxWithSkipSwapMsg( - inChainId, - outChainId, - { - chainId: outChainId, - denom: outCurrency.coinMinimalDenom, - expectedAmount: ibcSwapConfigs.amountConfig.outAmount - .toDec() - .toString(), - }, - simpleRoute, - sender, - [ - ...ibcSwapConfigs.amountConfig.amount.map((amount) => { - return { + const msg = new RecordTxWithSkipSwapMsg( + inChainId, + outChainId, + { + chainId: outChainId, + denom: outCurrency.coinMinimalDenom, + expectedAmount: ibcSwapConfigs.amountConfig.outAmount + .toDec() + .toString(), + }, + simpleRoute, + sender, + chainStore.isEvmOnlyChain(outChainId) + ? accountStore.getAccount(outChainId) + .ethereumHexAddress + : accountStore.getAccount(outChainId).bech32Address, + [ + ...ibcSwapConfigs.amountConfig.amount.map( + (amount) => { + return { + amount: DecUtils.getTenExponentN( + amount.currency.coinDecimals + ) + .mul(amount.toDec()) + .toString(), + denom: amount.currency.coinMinimalDenom, + }; + } + ), + { amount: DecUtils.getTenExponentN( - amount.currency.coinDecimals + ibcSwapConfigs.amountConfig.outAmount.currency + .coinDecimals ) - .mul(amount.toDec()) + .mul( + ibcSwapConfigs.amountConfig.outAmount.toDec() + ) .toString(), - denom: amount.currency.coinMinimalDenom, - }; - }), + denom: + ibcSwapConfigs.amountConfig.outAmount.currency + .coinMinimalDenom, + }, + ], { - amount: DecUtils.getTenExponentN( - ibcSwapConfigs.amountConfig.outAmount.currency - .coinDecimals - ) - .mul(ibcSwapConfigs.amountConfig.outAmount.toDec()) - .toString(), - denom: - ibcSwapConfigs.amountConfig.outAmount.currency - .coinMinimalDenom, + currencies: + chainStore.getChain(outChainId).currencies, }, - ], - { - currencies: chainStore.getChain(outChainId).currencies, - }, - routeDurationSeconds ?? 0, - txHash - ); + routeDurationSeconds ?? 0, + txHash + ); - new InExtensionMessageRequester() - .sendMessage(BACKGROUND_PORT, msg) - .then((response) => { - skipHistoryId = response; - }); + new InExtensionMessageRequester() + .sendMessage(BACKGROUND_PORT, msg) + .then((response) => { + skipHistoryId = response; - // ? - if (!erc20ApprovalTx) { - navigate("/", { - replace: true, - }); + navigate("/", { + replace: true, + }); + }); } }, onFulfill: (txReceipt) => { @@ -1527,12 +1507,71 @@ export const IBCSwapPage: FunctionComponent = observer(() => { ...secondTxFeeObject, }, { - onBroadcasted: () => { - // TODO: Add history + onBroadcasted: (txHash) => { ethereumAccount.setIsSendingTx(false); - navigate("/", { - replace: true, - }); + + const msg = new RecordTxWithSkipSwapMsg( + inChainId, + outChainId, + { + chainId: outChainId, + denom: outCurrency.coinMinimalDenom, + expectedAmount: + ibcSwapConfigs.amountConfig.outAmount + .toDec() + .toString(), + }, + simpleRoute, + sender, + chainStore.isEvmOnlyChain(outChainId) + ? accountStore.getAccount(outChainId) + .ethereumHexAddress + : accountStore.getAccount(outChainId) + .bech32Address, + [ + ...ibcSwapConfigs.amountConfig.amount.map( + (amount) => { + return { + amount: DecUtils.getTenExponentN( + amount.currency.coinDecimals + ) + .mul(amount.toDec()) + .toString(), + denom: amount.currency.coinMinimalDenom, + }; + } + ), + { + amount: DecUtils.getTenExponentN( + ibcSwapConfigs.amountConfig.outAmount + .currency.coinDecimals + ) + .mul( + ibcSwapConfigs.amountConfig.outAmount.toDec() + ) + .toString(), + denom: + ibcSwapConfigs.amountConfig.outAmount + .currency.coinMinimalDenom, + }, + ], + { + currencies: + chainStore.getChain(outChainId).currencies, + }, + routeDurationSeconds ?? 0, + txHash + ); + + new InExtensionMessageRequester() + .sendMessage(BACKGROUND_PORT, msg) + .then((response) => { + skipHistoryId = response; + + navigate("/", { + replace: true, + }); + }); }, onFulfill: (txReceipt) => { const queryBalances = queriesStore.get( @@ -1555,6 +1594,50 @@ export const IBCSwapPage: FunctionComponent = observer(() => { } }); if (txReceipt.status === EthTxStatus.Success) { + // onBroadcasted에서 실행하기에는 너무 빨라서 tx not found가 발생할 수 있음 + // 재실행 로직을 사용해도 되지만, txReceipt를 기다렸다가 실행하는 게 자원 낭비가 적음 + const chainIdForTrack = inChainId.replace( + "eip155:", + "" + ); + setTimeout(() => { + // no wait + simpleFetch( + "https://api.skip.build/", + "/v2/tx/track", + { + method: "POST", + headers: { + "content-type": "application/json", + ...(() => { + const res: { + authorization?: string; + } = {}; + if (process.env["SKIP_API_KEY"]) { + res.authorization = + process.env["SKIP_API_KEY"]; + } + return res; + })(), + }, + body: JSON.stringify({ + tx_hash: txReceipt.transactionHash, + chain_id: chainIdForTrack, + }), + } + ) + .then((result) => { + console.log( + `Skip tx track result: ${JSON.stringify( + result + )}` + ); + }) + .catch((e) => { + console.log(e); + }); + }, 2000); + notification.show( "success", intl.formatMessage({ @@ -1563,6 +1646,13 @@ export const IBCSwapPage: FunctionComponent = observer(() => { "" ); } else { + if (skipHistoryId) { + new InExtensionMessageRequester().sendMessage( + BACKGROUND_PORT, + new RemoveSkipHistoryMsg(skipHistoryId) + ); + } + notification.show( "failed", intl.formatMessage({ diff --git a/apps/extension/src/pages/main/components/ibc-history-view/index.tsx b/apps/extension/src/pages/main/components/ibc-history-view/index.tsx index ea7a3be061..762cedd0c8 100644 --- a/apps/extension/src/pages/main/components/ibc-history-view/index.tsx +++ b/apps/extension/src/pages/main/components/ibc-history-view/index.tsx @@ -954,24 +954,38 @@ const SkipHistoryViewItem: FunctionComponent<{ const sourceChain = chainStore.getChain(history.chainId); if (historyCompleted && failedRouteIndex < 0) { - const destinationDenom = (() => { - const currency = chainStore - .getChain(history.destinationAsset.chainId) - .forceFindCurrency(history.destinationAsset.denom); - - if ("originCurrency" in currency && currency.originCurrency) { - return currency.originCurrency.coinDenom; + const destinationAssets = (() => { + if (!history.resAmount[0]) { + return chainStore + .getChain(history.destinationAsset.chainId) + .forceFindCurrency(history.destinationAsset.denom) + .coinDenom; } - return currency.coinDenom; + return history.resAmount[0] + .map((amount) => { + return new CoinPretty( + chainStore + .getChain(history.destinationAsset.chainId) + .forceFindCurrency(amount.denom), + amount.amount + ) + .hideIBCMetadata(true) + .shrink(true) + .maxDecimals(6) + .inequalitySymbol(true) + .trim(true) + .toString(); + }) + .join(", "); })(); return intl.formatMessage( { - id: "page.main.components.ibc-history-view.ibc-swap.succeed.paragraph", // TODO: Change the content + id: "page.main.components.ibc-history-view.ibc-swap.succeed.paragraph", }, { - assets: destinationDenom, + assets: destinationAssets, } ); } diff --git a/packages/background/src/recent-send-history/handler.ts b/packages/background/src/recent-send-history/handler.ts index 8b0a24235b..a50b0acc89 100644 --- a/packages/background/src/recent-send-history/handler.ts +++ b/packages/background/src/recent-send-history/handler.ts @@ -221,6 +221,7 @@ const handleRecordTxWithSkipSwapMsg: ( msg.destinationAsset, msg.simpleRoute, msg.sender, + msg.recipient, msg.amount, msg.notificationInfo, msg.routeDurationSeconds, diff --git a/packages/background/src/recent-send-history/service.ts b/packages/background/src/recent-send-history/service.ts index 25627e9a96..094d405e27 100644 --- a/packages/background/src/recent-send-history/service.ts +++ b/packages/background/src/recent-send-history/service.ts @@ -17,10 +17,11 @@ import { import { KVStore, retry } from "@keplr-wallet/common"; import { IBCHistory, RecentSendHistory, SkipHistory } from "./types"; import { Buffer } from "buffer/"; -import { AppCurrency, ChainInfo } from "@keplr-wallet/types"; +import { AppCurrency, ChainInfo, EthTxReceipt } from "@keplr-wallet/types"; import { CoinPretty } from "@keplr-wallet/unit"; import { simpleFetch } from "@keplr-wallet/simple-fetch"; import { StatusRequest, TxStatusResponse } from "./temp-skip-types"; +import { id } from "@ethersproject/hash"; export class RecentSendHistoryService { // Key: {chain_identifier}/{type} @@ -1136,6 +1137,7 @@ export class RecentSendHistoryService { receiver: string; }[], sender: string, + recipient: string, amount: { amount: string; denom: string; @@ -1155,6 +1157,7 @@ export class RecentSendHistoryService { destinationAsset, simpleRoute, sender, + recipient, amount, notificationInfo, routeDurationSeconds: routeDurationSeconds, @@ -1218,8 +1221,16 @@ export class RecentSendHistoryService { () => { resolve(); }, - // eslint-disable-next-line @typescript-eslint/no-empty-function - () => {}, + () => { + // reject if ws closed before fulfilled + // 하지만 로직상 fulfill 되기 전에 ws가 닫히는게 되기 때문에 + // delay를 좀 준다. + // 현재 trackIBCPacketForwardingRecursiveInternal에 ws close 이후에는 동기적인 로직밖에 없으므로 + // 문제될게 없다. + setTimeout(() => { + reject(); + }, 500); + }, () => { reject(); } @@ -1237,7 +1248,7 @@ export class RecentSendHistoryService { protected trackSkipSwapRecursiveInternal = ( id: string, onFulfill: () => void, - _onClose: () => void, + onClose: () => void, onError: () => void ): void => { const history = this.getRecentSkipHistory(id); @@ -1340,170 +1351,191 @@ export class RecentSendHistoryService { const transfer = transfer_sequence[nextBlockingTransferIndex]; // ------------------------- - // 어떤 타입의 transfer인지 확인하여 targetChainId / errorMsg 결정 (if-else 체인) + // 어떤 타입의 transfer인지 확인하여 targetChainId / errorMsg / receiveTxHash 결정 (if-else 체인) // ------------------------- let targetChainId: string | undefined; - - if ("ibc_transfer" in transfer) { - const { - state: ibcState, - from_chain_id, - to_chain_id, - packet_txs, - } = transfer.ibc_transfer; - switch (ibcState) { - case "TRANSFER_UNKNOWN": - targetChainId = from_chain_id; - errorMsg = - packet_txs.error?.message ?? "Unknown IBC transfer error"; - break; - case "TRANSFER_FAILURE": - targetChainId = to_chain_id; - errorMsg = packet_txs.error?.message ?? "IBC transfer failed"; - break; - case "TRANSFER_PENDING": - case "TRANSFER_RECEIVED": - targetChainId = from_chain_id; - break; - case "TRANSFER_SUCCESS": - targetChainId = to_chain_id; - break; - } - } else if ("axelar_transfer" in transfer) { - const { - state: axelarState, - from_chain_id, - to_chain_id, - } = transfer.axelar_transfer; - switch (axelarState) { - case "AXELAR_TRANSFER_UNKNOWN": - targetChainId = from_chain_id; - errorMsg = "Unknown Axelar transfer error"; - break; - case "AXELAR_TRANSFER_FAILURE": - targetChainId = to_chain_id; - errorMsg = "Axelar transfer failed"; - break; - case "AXELAR_TRANSFER_PENDING_CONFIRMATION": - case "AXELAR_TRANSFER_PENDING_RECEIPT": - targetChainId = from_chain_id; - break; - case "AXELAR_TRANSFER_SUCCESS": - targetChainId = to_chain_id; - break; - } - } else if ("cctp_transfer" in transfer) { - const { - state: cctpState, - from_chain_id, - to_chain_id, - } = transfer.cctp_transfer; - switch (cctpState) { - case "CCTP_TRANSFER_UNKNOWN": - targetChainId = from_chain_id; - errorMsg = "Unknown CCTP transfer error"; - break; - case "CCTP_TRANSFER_CONFIRMED": - case "CCTP_TRANSFER_PENDING_CONFIRMATION": - case "CCTP_TRANSFER_SENT": - targetChainId = from_chain_id; - break; - case "CCTP_TRANSFER_RECEIVED": - targetChainId = to_chain_id; - break; - } - } else if ("hyperlane_transfer" in transfer) { - const { - state: hyperState, - from_chain_id, - to_chain_id, - } = transfer.hyperlane_transfer; - switch (hyperState) { - case "HYPERLANE_TRANSFER_UNKNOWN": - targetChainId = from_chain_id; - errorMsg = "Unknown Hyperlane transfer error"; - break; - case "HYPERLANE_TRANSFER_FAILED": - targetChainId = to_chain_id; - errorMsg = "Hyperlane transfer failed"; - break; - case "HYPERLANE_TRANSFER_SENT": - targetChainId = from_chain_id; - break; - case "HYPERLANE_TRANSFER_RECEIVED": - targetChainId = to_chain_id; - break; - } - } else if ("op_init_transfer" in transfer) { - const { - state: opState, - from_chain_id, - to_chain_id, - } = transfer.op_init_transfer; - switch (opState) { - case "OPINIT_TRANSFER_UNKNOWN": - targetChainId = from_chain_id; - errorMsg = "Unknown OP_INIT transfer error"; - break; - case "OPINIT_TRANSFER_RECEIVED": - targetChainId = from_chain_id; - break; - case "OPINIT_TRANSFER_SENT": - targetChainId = to_chain_id; - break; - } - } else if ("go_fast_transfer" in transfer) { - const { - state: gofastState, - from_chain_id, - to_chain_id, - } = transfer.go_fast_transfer; - switch (gofastState) { - case "GO_FAST_TRANSFER_UNKNOWN": - targetChainId = from_chain_id; - errorMsg = "Unknown GoFast transfer error"; - break; - case "GO_FAST_TRANSFER_SENT": - targetChainId = from_chain_id; - break; - case "GO_FAST_TRANSFER_TIMEOUT": - targetChainId = from_chain_id; - errorMsg = "GoFast transfer timeout"; - break; - case "GO_FAST_POST_ACTION_FAILED": - targetChainId = from_chain_id; - errorMsg = "GoFast post action failed"; - break; - case "GO_FAST_TRANSFER_REFUNDED": - targetChainId = to_chain_id; - errorMsg = "GoFast transfer refunded"; - break; - case "GO_FAST_TRANSFER_FILLED": - targetChainId = to_chain_id; - break; - } - } else if (transfer.stargate_transfer) { - const { - state: sgState, - from_chain_id, - to_chain_id, - } = transfer.stargate_transfer; - switch (sgState) { - case "STARGATE_TRANSFER_UNKNOWN": - targetChainId = from_chain_id; - errorMsg = "Unknown Stargate transfer error"; - break; - case "STARGATE_TRANSFER_FAILED": - targetChainId = to_chain_id; - errorMsg = "Stargate transfer failed"; - break; - case "STARGATE_TRANSFER_SENT": - targetChainId = from_chain_id; - break; - case "STARGATE_TRANSFER_RECEIVED": - targetChainId = to_chain_id; - break; + let receiveTxHash: string | undefined; + + if (transfer) { + if ("ibc_transfer" in transfer) { + const { + state: ibcState, + from_chain_id, + to_chain_id, + packet_txs, + } = transfer.ibc_transfer; + switch (ibcState) { + case "TRANSFER_UNKNOWN": + targetChainId = from_chain_id; + errorMsg = + packet_txs.error?.message ?? "Unknown IBC transfer error"; + break; + case "TRANSFER_FAILURE": + targetChainId = to_chain_id; + errorMsg = packet_txs.error?.message ?? "IBC transfer failed"; + break; + case "TRANSFER_PENDING": + case "TRANSFER_RECEIVED": + targetChainId = from_chain_id; + break; + case "TRANSFER_SUCCESS": + targetChainId = to_chain_id; + receiveTxHash = packet_txs.receive_tx?.tx_hash; + break; + } + } else if ("axelar_transfer" in transfer) { + const { + state: axelarState, + from_chain_id, + to_chain_id, + } = transfer.axelar_transfer; + switch (axelarState) { + case "AXELAR_TRANSFER_UNKNOWN": + targetChainId = from_chain_id; + errorMsg = "Unknown Axelar transfer error"; + break; + case "AXELAR_TRANSFER_FAILURE": + targetChainId = to_chain_id; + errorMsg = "Axelar transfer failed"; + break; + case "AXELAR_TRANSFER_PENDING_CONFIRMATION": + case "AXELAR_TRANSFER_PENDING_RECEIPT": + targetChainId = to_chain_id; + break; + case "AXELAR_TRANSFER_SUCCESS": + targetChainId = to_chain_id; + receiveTxHash = + "contract_call_with_token_txs" in transfer.axelar_transfer.txs + ? transfer.axelar_transfer.txs.contract_call_with_token_txs + .execute_tx?.tx_hash + : transfer.axelar_transfer.txs.send_token_txs.execute_tx + ?.tx_hash; + break; + } + } else if ("cctp_transfer" in transfer) { + const { + state: cctpState, + from_chain_id, + to_chain_id, + } = transfer.cctp_transfer; + switch (cctpState) { + case "CCTP_TRANSFER_UNKNOWN": + targetChainId = from_chain_id; + errorMsg = "Unknown CCTP transfer error"; + break; + case "CCTP_TRANSFER_SENT": + targetChainId = from_chain_id; + break; + case "CCTP_TRANSFER_CONFIRMED": + case "CCTP_TRANSFER_PENDING_CONFIRMATION": + targetChainId = to_chain_id; + break; + case "CCTP_TRANSFER_RECEIVED": + targetChainId = to_chain_id; + receiveTxHash = transfer.cctp_transfer.txs.receive_tx?.tx_hash; + break; + } + } else if ("hyperlane_transfer" in transfer) { + const { + state: hyperState, + from_chain_id, + to_chain_id, + } = transfer.hyperlane_transfer; + switch (hyperState) { + case "HYPERLANE_TRANSFER_UNKNOWN": + targetChainId = from_chain_id; + errorMsg = "Unknown Hyperlane transfer error"; + break; + case "HYPERLANE_TRANSFER_FAILED": + targetChainId = to_chain_id; + errorMsg = "Hyperlane transfer failed"; + break; + case "HYPERLANE_TRANSFER_SENT": + targetChainId = from_chain_id; + break; + case "HYPERLANE_TRANSFER_RECEIVED": + targetChainId = to_chain_id; + receiveTxHash = + transfer.hyperlane_transfer.txs.receive_tx?.tx_hash; + break; + } + } else if ("op_init_transfer" in transfer) { + const { + state: opState, + from_chain_id, + to_chain_id, + } = transfer.op_init_transfer; + switch (opState) { + case "OPINIT_TRANSFER_UNKNOWN": + targetChainId = from_chain_id; + errorMsg = "Unknown OP_INIT transfer error"; + break; + case "OPINIT_TRANSFER_RECEIVED": + targetChainId = from_chain_id; + break; + case "OPINIT_TRANSFER_SENT": + targetChainId = to_chain_id; + break; + } + } else if ("go_fast_transfer" in transfer) { + const { + state: gofastState, + from_chain_id, + to_chain_id, + } = transfer.go_fast_transfer; + switch (gofastState) { + case "GO_FAST_TRANSFER_UNKNOWN": + targetChainId = from_chain_id; + errorMsg = "Unknown GoFast transfer error"; + break; + case "GO_FAST_TRANSFER_SENT": + targetChainId = from_chain_id; + break; + case "GO_FAST_TRANSFER_TIMEOUT": + targetChainId = from_chain_id; + errorMsg = "GoFast transfer timeout"; + break; + case "GO_FAST_POST_ACTION_FAILED": + targetChainId = from_chain_id; + errorMsg = "GoFast post action failed"; + break; + case "GO_FAST_TRANSFER_REFUNDED": + targetChainId = to_chain_id; + errorMsg = "GoFast transfer refunded"; + break; + case "GO_FAST_TRANSFER_FILLED": + targetChainId = to_chain_id; + receiveTxHash = + transfer.go_fast_transfer.txs.order_filled_tx?.tx_hash; + break; + } + } else if (transfer.stargate_transfer) { + const { + state: sgState, + from_chain_id, + to_chain_id, + } = transfer.stargate_transfer; + switch (sgState) { + case "STARGATE_TRANSFER_UNKNOWN": + targetChainId = from_chain_id; + errorMsg = "Unknown Stargate transfer error"; + break; + case "STARGATE_TRANSFER_FAILED": + targetChainId = to_chain_id; + errorMsg = "Stargate transfer failed"; + break; + case "STARGATE_TRANSFER_SENT": + targetChainId = from_chain_id; + break; + case "STARGATE_TRANSFER_RECEIVED": + targetChainId = to_chain_id; + break; + } } + } else { + // 아마 EVM 체인 위에서만 발생하는 경우 transfer가 없는 것으로 처리되는 것 같음 + targetChainId = history.destinationChainId; + receiveTxHash = history.txHash; } // ------------------------- @@ -1544,9 +1576,17 @@ export class RecentSendHistoryService { history.routeIndex = simpleRoute.length - 1; } - // TODO: destination asset이 얼마나 transfer되었는지 확인 - - onFulfill(); + if (receiveTxHash) { + this.trackDestinationAssetAmount( + id, + receiveTxHash, + onFulfill, + onClose, + onError + ); + } else { + onFulfill(); + } break; case "STATE_PENDING": @@ -1562,6 +1602,140 @@ export class RecentSendHistoryService { }); }; + protected trackDestinationAssetAmount( + historyId: string, + txHash: string, + onFullfill: () => void, + onClose: () => void, + onError: () => void + ) { + const history = this.getRecentSkipHistory(historyId); + if (!history) { + onFullfill(); + return; + } + + const chainInfo = this.chainsService.getChainInfo( + history.destinationChainId + ); + if (!chainInfo) { + onFullfill(); + return; + } + + if (this.chainsService.isEvmChain(history.destinationChainId)) { + const evmInfo = chainInfo.evm; + if (!evmInfo) { + onFullfill(); + return; + } + + simpleFetch<{ + result: EthTxReceipt | null; + error?: Error; + }>(evmInfo.rpc, { + method: "POST", + headers: { + "content-type": "application/json", + "request-source": origin, + }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "eth_getTransactionReceipt", + params: [txHash], + id: 1, + }), + }) + .then((res) => { + const txReceipt = res.data.result; + if (txReceipt) { + const logs = txReceipt.logs; + const transferTopic = id("Transfer(address,address,uint256)"); + const withdrawTopic = id("Withdrawal(address,uint256)"); + const hyperlaneReceiveTopic = id( + "ReceivedTransferRemote(uint32,bytes32,uint256)" + ); + for (const log of logs) { + if (log.topics[0] === transferTopic) { + const to = "0x" + log.topics[2].slice(26); + if (to.toLowerCase() === history.recipient.toLowerCase()) { + const amount = BigInt(log.data).toString(10); + history.resAmount.push([ + { + amount, + denom: history.destinationAsset.denom, + }, + ]); + + return; + } + } else if (log.topics[0] === withdrawTopic) { + const to = "0x" + log.topics[1].slice(26); + if (to.toLowerCase() === txReceipt.to.toLowerCase()) { + const amount = BigInt(log.data).toString(10); + history.resAmount.push([ + { amount, denom: history.destinationAsset.denom }, + ]); + return; + } + } else if (log.topics[0] === hyperlaneReceiveTopic) { + const to = "0x" + log.topics[2].slice(26); + if (to.toLowerCase() === history.recipient.toLowerCase()) { + const amount = BigInt(log.data).toString(10); + // Hyperlane을 통해 Forma로 TIA를 받는 경우 토큰 수량이 decimal 6으로 기록되는데, + // Forma에서는 decimal 18이기 때문에 12자리 만큼 0을 붙여준다. + history.resAmount.push([ + { + amount: + history.destinationAsset.denom === "forma-native" + ? `${amount}000000000000` + : amount, + denom: history.destinationAsset.denom, + }, + ]); + return; + } + } + } + } + }) + .finally(() => { + onFullfill(); + }); + } else { + const txTracer = new TendermintTxTracer(chainInfo.rpc, "/websocket"); + txTracer.addEventListener("close", onClose); + txTracer.addEventListener("error", onError); + txTracer + .queryTx({ + "tx.hash": txHash, + }) + .then((res: any) => { + txTracer.close(); + + if (!res) { + return; + } + + const txs = res.txs + ? res.txs.map((res: any) => res.tx_result || res) + : [res.tx_result || res]; + for (const tx of txs) { + const resAmount = this.getIBCSwapResAmountFromTx( + tx, + history.recipient + ); + + history.resAmount.push(resAmount); + return; + } + }) + .finally(() => { + onFullfill(); + }); + } + } + @action removeRecentSkipHistory(id: string): boolean { return this.recentSkipHistoryMap.delete(id); diff --git a/packages/background/src/recent-send-history/temp-skip-message.ts b/packages/background/src/recent-send-history/temp-skip-message.ts index 287e99d41a..bf90a471b8 100644 --- a/packages/background/src/recent-send-history/temp-skip-message.ts +++ b/packages/background/src/recent-send-history/temp-skip-message.ts @@ -22,6 +22,7 @@ export class RecordTxWithSkipSwapMsg extends Message { receiver: string; }[], public readonly sender: string, + public readonly recipient: string, // amount 대신 amountIn, amountOut을 사용하도록 변경 @@ -54,6 +55,10 @@ export class RecordTxWithSkipSwapMsg extends Message { if (!this.sender) { throw new Error("sender is empty"); } + + if (!this.recipient) { + throw new Error("recipient is empty"); + } } route(): string { diff --git a/packages/background/src/recent-send-history/types.ts b/packages/background/src/recent-send-history/types.ts index a3cd7504a1..87b15242f0 100644 --- a/packages/background/src/recent-send-history/types.ts +++ b/packages/background/src/recent-send-history/types.ts @@ -100,6 +100,7 @@ export type SkipHistory = { destinationChainId: string; timestamp: number; sender: string; + recipient: string; amount: { amount: string; From 7e98c3f96ab3f9c3554da9c667bf693d8a7f0d3c Mon Sep 17 00:00:00 2001 From: delivan Date: Wed, 25 Dec 2024 16:40:51 +0900 Subject: [PATCH 40/43] Refactor temp code --- apps/extension/src/pages/ibc-swap/index.tsx | 6 +- .../components/ibc-history-view/index.tsx | 6 +- .../src/recent-send-history/handler.ts | 10 +- .../src/recent-send-history/init.ts | 10 +- .../src/recent-send-history/messages.ts | 137 +++++++- .../src/recent-send-history/service.ts | 9 +- .../recent-send-history/temp-skip-message.ts | 139 -------- .../recent-send-history/temp-skip-types.ts | 305 ----------------- .../src/recent-send-history/types.ts | 307 +++++++++++++++++- 9 files changed, 461 insertions(+), 468 deletions(-) delete mode 100644 packages/background/src/recent-send-history/temp-skip-message.ts delete mode 100644 packages/background/src/recent-send-history/temp-skip-types.ts diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index 92ccdd60e6..9e517250e1 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -42,6 +42,8 @@ import { MakeTxResponse, WalletStatus } from "@keplr-wallet/stores"; import { autorun } from "mobx"; import { LogAnalyticsEventMsg, + RecordTxWithSkipSwapMsg, + RemoveSkipHistoryMsg, SendTxAndRecordMsg, SendTxAndRecordWithIBCSwapMsg, } from "@keplr-wallet/background"; @@ -55,10 +57,6 @@ import { TextButtonProps } from "../../components/button-text"; import { UnsignedEVMTransactionWithErc20Approvals } from "@keplr-wallet/stores-eth"; import { EthTxStatus } from "@keplr-wallet/types"; import { simpleFetch } from "@keplr-wallet/simple-fetch"; -import { - RecordTxWithSkipSwapMsg, - RemoveSkipHistoryMsg, -} from "@keplr-wallet/background/build/recent-send-history/temp-skip-message"; const TextButtonStyles = { Container: styled.div` diff --git a/apps/extension/src/pages/main/components/ibc-history-view/index.tsx b/apps/extension/src/pages/main/components/ibc-history-view/index.tsx index 762cedd0c8..df3e7c4df8 100644 --- a/apps/extension/src/pages/main/components/ibc-history-view/index.tsx +++ b/apps/extension/src/pages/main/components/ibc-history-view/index.tsx @@ -2,8 +2,10 @@ import React, { FunctionComponent, useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; import { GetIBCHistoriesMsg, + GetSkipHistoriesMsg, IBCHistory, RemoveIBCHistoryMsg, + RemoveSkipHistoryMsg, SkipHistory, } from "@keplr-wallet/background"; import { InExtensionMessageRequester } from "@keplr-wallet/router-extension"; @@ -36,10 +38,6 @@ import { useSpringValue, animated, easings } from "@react-spring/web"; import { defaultSpringConfig } from "../../../../styles/spring"; import { VerticalCollapseTransition } from "../../../../components/transition/vertical-collapse"; import { FormattedMessage, useIntl } from "react-intl"; -import { - GetSkipHistoriesMsg, - RemoveSkipHistoryMsg, -} from "@keplr-wallet/background/build/recent-send-history/temp-skip-message"; export const IbcHistoryView: FunctionComponent<{ isNotReady: boolean; diff --git a/packages/background/src/recent-send-history/handler.ts b/packages/background/src/recent-send-history/handler.ts index a50b0acc89..7391aa94ff 100644 --- a/packages/background/src/recent-send-history/handler.ts +++ b/packages/background/src/recent-send-history/handler.ts @@ -14,14 +14,12 @@ import { GetIBCHistoriesMsg, RemoveIBCHistoryMsg, ClearAllIBCHistoryMsg, -} from "./messages"; -import { RecentSendHistoryService } from "./service"; -import { - ClearAllSkipHistoryMsg, GetSkipHistoriesMsg, - RecordTxWithSkipSwapMsg, RemoveSkipHistoryMsg, -} from "./temp-skip-message"; + ClearAllSkipHistoryMsg, + RecordTxWithSkipSwapMsg, +} from "./messages"; +import { RecentSendHistoryService } from "./service"; export const getHandler: (service: RecentSendHistoryService) => Handler = ( service: RecentSendHistoryService diff --git a/packages/background/src/recent-send-history/init.ts b/packages/background/src/recent-send-history/init.ts index 35611abacb..ccde9be379 100644 --- a/packages/background/src/recent-send-history/init.ts +++ b/packages/background/src/recent-send-history/init.ts @@ -8,16 +8,14 @@ import { GetIBCHistoriesMsg, RemoveIBCHistoryMsg, ClearAllIBCHistoryMsg, + ClearAllSkipHistoryMsg, + RecordTxWithSkipSwapMsg, + GetSkipHistoriesMsg, + RemoveSkipHistoryMsg, } from "./messages"; import { ROUTE } from "./constants"; import { getHandler } from "./handler"; import { RecentSendHistoryService } from "./service"; -import { - ClearAllSkipHistoryMsg, - GetSkipHistoriesMsg, - RecordTxWithSkipSwapMsg, - RemoveSkipHistoryMsg, -} from "./temp-skip-message"; export function init(router: Router, service: RecentSendHistoryService): void { router.registerMessage(GetRecentSendHistoriesMsg); diff --git a/packages/background/src/recent-send-history/messages.ts b/packages/background/src/recent-send-history/messages.ts index e298f47931..e9892d789c 100644 --- a/packages/background/src/recent-send-history/messages.ts +++ b/packages/background/src/recent-send-history/messages.ts @@ -1,6 +1,6 @@ import { Message } from "@keplr-wallet/router"; import { ROUTE } from "./constants"; -import { IBCHistory, RecentSendHistory } from "./types"; +import { IBCHistory, RecentSendHistory, SkipHistory } from "./types"; import { AppCurrency } from "@keplr-wallet/types"; export class GetRecentSendHistoriesMsg extends Message { @@ -403,3 +403,138 @@ export class ClearAllIBCHistoryMsg extends Message { return ClearAllIBCHistoryMsg.type(); } } + +export class RecordTxWithSkipSwapMsg extends Message { + public static type() { + return "record-tx-with-skip-swap"; + } + + constructor( + public readonly sourceChainId: string, + public readonly destinationChainId: string, + public readonly destinationAsset: { + chainId: string; + denom: string; + expectedAmount: string; + }, + public readonly simpleRoute: { + isOnlyEvm: boolean; + chainId: string; + receiver: string; + }[], + public readonly sender: string, + public readonly recipient: string, + + // amount 대신 amountIn, amountOut을 사용하도록 변경 + + public readonly amount: { + readonly amount: string; + readonly denom: string; + }[], + public readonly notificationInfo: { + currencies: AppCurrency[]; + }, + public readonly routeDurationSeconds: number, + public readonly txHash: string + ) { + super(); + } + + validateBasic(): void { + if (!this.sourceChainId) { + throw new Error("chain id is empty"); + } + + if (!this.destinationChainId) { + throw new Error("chain id is empty"); + } + + if (!this.simpleRoute) { + throw new Error("simple route is empty"); + } + + if (!this.sender) { + throw new Error("sender is empty"); + } + + if (!this.recipient) { + throw new Error("recipient is empty"); + } + } + + route(): string { + return ROUTE; + } + + type(): string { + return RecordTxWithSkipSwapMsg.type(); + } +} + +export class GetSkipHistoriesMsg extends Message { + public static type() { + return "get-skip-histories"; + } + + constructor() { + super(); + } + + validateBasic(): void { + // noop + } + + route(): string { + return ROUTE; + } + + type(): string { + return GetSkipHistoriesMsg.type(); + } +} + +export class RemoveSkipHistoryMsg extends Message { + public static type() { + return "remove-skip-histories"; + } + + constructor(public readonly id: string) { + super(); + } + + validateBasic(): void { + if (!this.id) { + throw new Error("id is empty"); + } + } + + route(): string { + return ROUTE; + } + + type(): string { + return RemoveSkipHistoryMsg.type(); + } +} + +export class ClearAllSkipHistoryMsg extends Message { + public static type() { + return "clear-all-skip-histories"; + } + + constructor() { + super(); + } + + validateBasic(): void { + // noop + } + + route(): string { + return ROUTE; + } + + type(): string { + return ClearAllSkipHistoryMsg.type(); + } +} diff --git a/packages/background/src/recent-send-history/service.ts b/packages/background/src/recent-send-history/service.ts index 094d405e27..e112b3f8ce 100644 --- a/packages/background/src/recent-send-history/service.ts +++ b/packages/background/src/recent-send-history/service.ts @@ -15,12 +15,17 @@ import { toJS, } from "mobx"; import { KVStore, retry } from "@keplr-wallet/common"; -import { IBCHistory, RecentSendHistory, SkipHistory } from "./types"; +import { + IBCHistory, + RecentSendHistory, + SkipHistory, + StatusRequest, + TxStatusResponse, +} from "./types"; import { Buffer } from "buffer/"; import { AppCurrency, ChainInfo, EthTxReceipt } from "@keplr-wallet/types"; import { CoinPretty } from "@keplr-wallet/unit"; import { simpleFetch } from "@keplr-wallet/simple-fetch"; -import { StatusRequest, TxStatusResponse } from "./temp-skip-types"; import { id } from "@ethersproject/hash"; export class RecentSendHistoryService { diff --git a/packages/background/src/recent-send-history/temp-skip-message.ts b/packages/background/src/recent-send-history/temp-skip-message.ts deleted file mode 100644 index bf90a471b8..0000000000 --- a/packages/background/src/recent-send-history/temp-skip-message.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { Message } from "@keplr-wallet/router"; -import { ROUTE } from "./constants"; -import { AppCurrency } from "@keplr-wallet/types"; -import { SkipHistory } from "./types"; - -export class RecordTxWithSkipSwapMsg extends Message { - public static type() { - return "record-tx-with-skip-swap"; - } - - constructor( - public readonly sourceChainId: string, - public readonly destinationChainId: string, - public readonly destinationAsset: { - chainId: string; - denom: string; - expectedAmount: string; - }, - public readonly simpleRoute: { - isOnlyEvm: boolean; - chainId: string; - receiver: string; - }[], - public readonly sender: string, - public readonly recipient: string, - - // amount 대신 amountIn, amountOut을 사용하도록 변경 - - public readonly amount: { - readonly amount: string; - readonly denom: string; - }[], - public readonly notificationInfo: { - currencies: AppCurrency[]; - }, - public readonly routeDurationSeconds: number, - public readonly txHash: string - ) { - super(); - } - - validateBasic(): void { - if (!this.sourceChainId) { - throw new Error("chain id is empty"); - } - - if (!this.destinationChainId) { - throw new Error("chain id is empty"); - } - - if (!this.simpleRoute) { - throw new Error("simple route is empty"); - } - - if (!this.sender) { - throw new Error("sender is empty"); - } - - if (!this.recipient) { - throw new Error("recipient is empty"); - } - } - - route(): string { - return ROUTE; - } - - type(): string { - return RecordTxWithSkipSwapMsg.type(); - } -} - -export class GetSkipHistoriesMsg extends Message { - public static type() { - return "get-skip-histories"; - } - - constructor() { - super(); - } - - validateBasic(): void { - // noop - } - - route(): string { - return ROUTE; - } - - type(): string { - return GetSkipHistoriesMsg.type(); - } -} - -export class RemoveSkipHistoryMsg extends Message { - public static type() { - return "remove-skip-histories"; - } - - constructor(public readonly id: string) { - super(); - } - - validateBasic(): void { - if (!this.id) { - throw new Error("id is empty"); - } - } - - route(): string { - return ROUTE; - } - - type(): string { - return RemoveSkipHistoryMsg.type(); - } -} - -export class ClearAllSkipHistoryMsg extends Message { - public static type() { - return "clear-all-skip-histories"; - } - - constructor() { - super(); - } - - validateBasic(): void { - // noop - } - - route(): string { - return ROUTE; - } - - type(): string { - return ClearAllSkipHistoryMsg.type(); - } -} diff --git a/packages/background/src/recent-send-history/temp-skip-types.ts b/packages/background/src/recent-send-history/temp-skip-types.ts deleted file mode 100644 index 164764bf97..0000000000 --- a/packages/background/src/recent-send-history/temp-skip-types.ts +++ /dev/null @@ -1,305 +0,0 @@ -/** - * This file is a temporary file to store types that are required for tracking the status of a transaction related to the skip-go. - * Reference: https://github.com/skip-mev/skip-go/blob/staging/packages/client/src/types/lifecycle.ts - */ - -export type StatusState = - | "STATE_UNKNOWN" - | "STATE_SUBMITTED" - | "STATE_PENDING" // route is in progress - | "STATE_RECEIVED" - | "STATE_COMPLETED" // route is completed - | "STATE_ABANDONED" // Tracking has stopped - | "STATE_COMPLETED_SUCCESS" // The route has completed successfully - | "STATE_COMPLETED_ERROR" // The route errored somewhere and the user has their tokens unlocked in one of their wallets - | "STATE_PENDING_ERROR"; // The route is in progress and an error has occurred somewhere (specially for IBC, where the asset is locked on the source chain) - -export type NextBlockingTransfer = { - transfer_sequence_index: number; -}; - -export type StatusRequest = { - tx_hash: string; - chain_id: string; -}; - -// This is for the IBC transfer -export type TransferState = - | "TRANSFER_UNKNOWN" - | "TRANSFER_PENDING" - | "TRANSFER_RECEIVED" // The packet has been received on the destination chain - | "TRANSFER_SUCCESS" // The packet has been successfully acknowledged - | "TRANSFER_FAILURE"; - -export type TransferInfo = { - from_chain_id: string; - to_chain_id: string; - state: TransferState; - packet_txs: Packet; - - // Deprecated - src_chain_id: string; - dst_chain_id: string; -}; - -export type TransferAssetRelease = { - chain_id: string; // Chain where the assets are released or will be released - denom: string; // Denom of the tokens the user will have - released: boolean; // Boolean given whether the funds are currently available (if the state is STATE_PENDING_ERROR , this will be false) -}; - -export type TxStatusResponse = { - status: StatusState; - transfer_sequence: TransferEvent[]; - next_blocking_transfer: NextBlockingTransfer | null; // give the index of the next blocking transfer in the sequence - transfer_asset_release: TransferAssetRelease | null; // Info about where the users tokens will be released when the route completes () - error: StatusError | null; - state: StatusState; - transfers: TransferStatus[]; -}; - -export type TransferStatus = { - state: StatusState; - transfer_sequence: TransferEvent[]; - next_blocking_transfer: NextBlockingTransfer | null; - transfer_asset_release: TransferAssetRelease | null; - error: StatusError | null; -}; - -export type Packet = { - send_tx: ChainTransaction | null; - receive_tx: ChainTransaction | null; - acknowledge_tx: ChainTransaction | null; - timeout_tx: ChainTransaction | null; - - error: PacketError | null; -}; - -export type StatusErrorType = - | "STATUS_ERROR_UNKNOWN" - | "STATUS_ERROR_TRANSACTION_EXECUTION" - | "STATUS_ERROR_INDEXING"; - -export type TransactionExecutionError = { - code: number; - message: string; -}; - -export type StatusError = { - code: number; - message: string; - type: StatusErrorType; - details: { - transactionExecutionError: TransactionExecutionError; - }; -}; - -export type PacketErrorType = - | "PACKET_ERROR_UNKNOWN" - | "PACKET_ERROR_ACKNOWLEDGEMENT" - | "PACKET_ERROR_TIMEOUT"; - -export type AcknowledgementError = { - message: string; - code: number; -}; - -export type PacketError = { - code: number; - message: string; - type: PacketErrorType; - details: { - acknowledgement_error: AcknowledgementError; - }; -}; - -export type ChainTransaction = { - chain_id: string; - tx_hash: string; - explorer_link: string; -}; - -export type TrackTxRequest = { - tx_hash: string; - chain_id: string; -}; - -export type TrackTxResponse = { - tx_hash: string; - explorer_link: string; -}; - -export type AxelarTransferType = - | "AXELAR_TRANSFER_CONTRACT_CALL_WITH_TOKEN" - | "AXELAR_TRANSFER_SEND_TOKEN"; - -export type AxelarTransferState = - | "AXELAR_TRANSFER_UNKNOWN" - | "AXELAR_TRANSFER_PENDING_CONFIRMATION" - | "AXELAR_TRANSFER_PENDING_RECEIPT" - | "AXELAR_TRANSFER_SUCCESS" // Desirable state - | "AXELAR_TRANSFER_FAILURE"; - -export type AxelarTransferInfo = { - from_chain_id: string; - to_chain_id: string; - type: AxelarTransferType; - state: AxelarTransferState; - txs: AxelarTransferTransactions; - axelar_scan_link: string; - - // Deprecated - src_chain_id: string; - dst_chain_id: string; -}; - -export type AxelarTransferTransactions = - | { - contract_call_with_token_txs: ContractCallWithTokenTransactions; - } - | { - send_token_txs: SendTokenTransactions; - }; - -export type ContractCallWithTokenTransactions = { - send_tx: ChainTransaction | null; - gas_paid_tx: ChainTransaction | null; - confirm_tx: ChainTransaction | null; - approve_tx: ChainTransaction | null; - execute_tx: ChainTransaction | null; - error: ContractCallWithTokenError | null; -}; - -export type ContractCallWithTokenError = { - message: string; - type: ContractCallWithTokenErrorType; -}; - -export type ContractCallWithTokenErrorType = - "CONTRACT_CALL_WITH_TOKEN_EXECUTION_ERROR"; - -export type SendTokenTransactions = { - send_tx: ChainTransaction | null; - confirm_tx: ChainTransaction | null; - execute_tx: ChainTransaction | null; - error: SendTokenError | null; -}; - -export type SendTokenErrorType = "SEND_TOKEN_EXECUTION_ERROR"; - -export type SendTokenError = { - message: string; - type: SendTokenErrorType; -}; - -export type CCTPTransferState = - | "CCTP_TRANSFER_UNKNOWN" - | "CCTP_TRANSFER_SENT" - | "CCTP_TRANSFER_PENDING_CONFIRMATION" - | "CCTP_TRANSFER_CONFIRMED" - | "CCTP_TRANSFER_RECEIVED"; // Desirable state - -export type CCTPTransferTransactions = { - send_tx: ChainTransaction | null; - receive_tx: ChainTransaction | null; -}; - -export type CCTPTransferInfo = { - from_chain_id: string; - to_chain_id: string; - state: CCTPTransferState; - txs: CCTPTransferTransactions; - - // Deprecated - src_chain_id: string; - dst_chain_id: string; -}; - -export type HyperlaneTransferState = - | "HYPERLANE_TRANSFER_UNKNOWN" - | "HYPERLANE_TRANSFER_SENT" - | "HYPERLANE_TRANSFER_FAILED" - | "HYPERLANE_TRANSFER_RECEIVED"; // Desirable state - -export type HyperlaneTransferTransactions = { - send_tx: ChainTransaction | null; - receive_tx: ChainTransaction | null; -}; - -export type HyperlaneTransferInfo = { - from_chain_id: string; - to_chain_id: string; - state: HyperlaneTransferState; - txs: HyperlaneTransferTransactions; -}; - -export type GoFastTransferTransactions = { - order_submitted_tx: ChainTransaction | null; - order_filled_tx: ChainTransaction | null; - order_refunded_tx: ChainTransaction | null; - order_timeout_tx: ChainTransaction | null; -}; - -export type GoFastTransferState = - | "GO_FAST_TRANSFER_UNKNOWN" - | "GO_FAST_TRANSFER_SENT" - | "GO_FAST_POST_ACTION_FAILED" - | "GO_FAST_TRANSFER_TIMEOUT" - | "GO_FAST_TRANSFER_FILLED" // Desirable state - | "GO_FAST_TRANSFER_REFUNDED"; - -export type GoFastTransferInfo = { - from_chain_id: string; - to_chain_id: string; - state: GoFastTransferState; - txs: GoFastTransferTransactions; -}; - -export type StargateTransferState = - | "STARGATE_TRANSFER_UNKNOWN" - | "STARGATE_TRANSFER_SENT" - | "STARGATE_TRANSFER_RECEIVED" // Desirable state - | "STARGATE_TRANSFER_FAILED"; - -export type StargateTransferTransactions = { - send_tx: ChainTransaction | null; - receive_tx: ChainTransaction | null; - error_tx: ChainTransaction | null; -}; - -export type StargateTransferInfo = { - from_chain_id: string; - to_chain_id: string; - state: StargateTransferState; - txs: StargateTransferTransactions; -}; - -export type OPInitTransferState = - | "OPINIT_TRANSFER_UNKNOWN" - | "OPINIT_TRANSFER_SENT" - | "OPINIT_TRANSFER_RECEIVED"; // Desirable state - -export type OPInitTransferTransactions = { - send_tx: ChainTransaction | null; - receive_tx: ChainTransaction | null; -}; - -export type OPInitTransferInfo = { - from_chain_id: string; - to_chain_id: string; - state: OPInitTransferState; - txs: OPInitTransferTransactions; -}; - -export type TransferEvent = - | { - ibc_transfer: TransferInfo; - } - | { - axelar_transfer: AxelarTransferInfo; - } - | { cctp_transfer: CCTPTransferInfo } - | { hyperlane_transfer: HyperlaneTransferInfo } - | { op_init_transfer: OPInitTransferInfo } - | { go_fast_transfer: GoFastTransferInfo } - | { stargate_transfer: StargateTransferInfo }; diff --git a/packages/background/src/recent-send-history/types.ts b/packages/background/src/recent-send-history/types.ts index 87b15242f0..a99225b4e5 100644 --- a/packages/background/src/recent-send-history/types.ts +++ b/packages/background/src/recent-send-history/types.ts @@ -1,5 +1,4 @@ import { AppCurrency } from "@keplr-wallet/types"; -import { StatusState, TransferAssetRelease } from "./temp-skip-types"; export interface RecentSendHistory { timestamp: number; @@ -138,3 +137,309 @@ export type SkipHistory = { transferAssetRelease?: TransferAssetRelease; // 라우팅 중간에 실패한 경우, 사용자의 자산이 어디에서 릴리즈 되었는지 정보 }; + +/** + * This file is a temporary file to store types that are required for tracking the status of a transaction related to the skip-go. + * Reference: https://github.com/skip-mev/skip-go/blob/staging/packages/client/src/types/lifecycle.ts + */ + +export type StatusState = + | "STATE_UNKNOWN" + | "STATE_SUBMITTED" + | "STATE_PENDING" // route is in progress + | "STATE_RECEIVED" + | "STATE_COMPLETED" // route is completed + | "STATE_ABANDONED" // Tracking has stopped + | "STATE_COMPLETED_SUCCESS" // The route has completed successfully + | "STATE_COMPLETED_ERROR" // The route errored somewhere and the user has their tokens unlocked in one of their wallets + | "STATE_PENDING_ERROR"; // The route is in progress and an error has occurred somewhere (specially for IBC, where the asset is locked on the source chain) + +export type NextBlockingTransfer = { + transfer_sequence_index: number; +}; + +export type StatusRequest = { + tx_hash: string; + chain_id: string; +}; + +// This is for the IBC transfer +export type TransferState = + | "TRANSFER_UNKNOWN" + | "TRANSFER_PENDING" + | "TRANSFER_RECEIVED" // The packet has been received on the destination chain + | "TRANSFER_SUCCESS" // The packet has been successfully acknowledged + | "TRANSFER_FAILURE"; + +export type TransferInfo = { + from_chain_id: string; + to_chain_id: string; + state: TransferState; + packet_txs: Packet; + + // Deprecated + src_chain_id: string; + dst_chain_id: string; +}; + +export type TransferAssetRelease = { + chain_id: string; // Chain where the assets are released or will be released + denom: string; // Denom of the tokens the user will have + released: boolean; // Boolean given whether the funds are currently available (if the state is STATE_PENDING_ERROR , this will be false) +}; + +export type TxStatusResponse = { + status: StatusState; + transfer_sequence: TransferEvent[]; + next_blocking_transfer: NextBlockingTransfer | null; // give the index of the next blocking transfer in the sequence + transfer_asset_release: TransferAssetRelease | null; // Info about where the users tokens will be released when the route completes () + error: StatusError | null; + state: StatusState; + transfers: TransferStatus[]; +}; + +export type TransferStatus = { + state: StatusState; + transfer_sequence: TransferEvent[]; + next_blocking_transfer: NextBlockingTransfer | null; + transfer_asset_release: TransferAssetRelease | null; + error: StatusError | null; +}; + +export type Packet = { + send_tx: ChainTransaction | null; + receive_tx: ChainTransaction | null; + acknowledge_tx: ChainTransaction | null; + timeout_tx: ChainTransaction | null; + + error: PacketError | null; +}; + +export type StatusErrorType = + | "STATUS_ERROR_UNKNOWN" + | "STATUS_ERROR_TRANSACTION_EXECUTION" + | "STATUS_ERROR_INDEXING"; + +export type TransactionExecutionError = { + code: number; + message: string; +}; + +export type StatusError = { + code: number; + message: string; + type: StatusErrorType; + details: { + transactionExecutionError: TransactionExecutionError; + }; +}; + +export type PacketErrorType = + | "PACKET_ERROR_UNKNOWN" + | "PACKET_ERROR_ACKNOWLEDGEMENT" + | "PACKET_ERROR_TIMEOUT"; + +export type AcknowledgementError = { + message: string; + code: number; +}; + +export type PacketError = { + code: number; + message: string; + type: PacketErrorType; + details: { + acknowledgement_error: AcknowledgementError; + }; +}; + +export type ChainTransaction = { + chain_id: string; + tx_hash: string; + explorer_link: string; +}; + +export type TrackTxRequest = { + tx_hash: string; + chain_id: string; +}; + +export type TrackTxResponse = { + tx_hash: string; + explorer_link: string; +}; + +export type AxelarTransferType = + | "AXELAR_TRANSFER_CONTRACT_CALL_WITH_TOKEN" + | "AXELAR_TRANSFER_SEND_TOKEN"; + +export type AxelarTransferState = + | "AXELAR_TRANSFER_UNKNOWN" + | "AXELAR_TRANSFER_PENDING_CONFIRMATION" + | "AXELAR_TRANSFER_PENDING_RECEIPT" + | "AXELAR_TRANSFER_SUCCESS" // Desirable state + | "AXELAR_TRANSFER_FAILURE"; + +export type AxelarTransferInfo = { + from_chain_id: string; + to_chain_id: string; + type: AxelarTransferType; + state: AxelarTransferState; + txs: AxelarTransferTransactions; + axelar_scan_link: string; + + // Deprecated + src_chain_id: string; + dst_chain_id: string; +}; + +export type AxelarTransferTransactions = + | { + contract_call_with_token_txs: ContractCallWithTokenTransactions; + } + | { + send_token_txs: SendTokenTransactions; + }; + +export type ContractCallWithTokenTransactions = { + send_tx: ChainTransaction | null; + gas_paid_tx: ChainTransaction | null; + confirm_tx: ChainTransaction | null; + approve_tx: ChainTransaction | null; + execute_tx: ChainTransaction | null; + error: ContractCallWithTokenError | null; +}; + +export type ContractCallWithTokenError = { + message: string; + type: ContractCallWithTokenErrorType; +}; + +export type ContractCallWithTokenErrorType = + "CONTRACT_CALL_WITH_TOKEN_EXECUTION_ERROR"; + +export type SendTokenTransactions = { + send_tx: ChainTransaction | null; + confirm_tx: ChainTransaction | null; + execute_tx: ChainTransaction | null; + error: SendTokenError | null; +}; + +export type SendTokenErrorType = "SEND_TOKEN_EXECUTION_ERROR"; + +export type SendTokenError = { + message: string; + type: SendTokenErrorType; +}; + +export type CCTPTransferState = + | "CCTP_TRANSFER_UNKNOWN" + | "CCTP_TRANSFER_SENT" + | "CCTP_TRANSFER_PENDING_CONFIRMATION" + | "CCTP_TRANSFER_CONFIRMED" + | "CCTP_TRANSFER_RECEIVED"; // Desirable state + +export type CCTPTransferTransactions = { + send_tx: ChainTransaction | null; + receive_tx: ChainTransaction | null; +}; + +export type CCTPTransferInfo = { + from_chain_id: string; + to_chain_id: string; + state: CCTPTransferState; + txs: CCTPTransferTransactions; + + // Deprecated + src_chain_id: string; + dst_chain_id: string; +}; + +export type HyperlaneTransferState = + | "HYPERLANE_TRANSFER_UNKNOWN" + | "HYPERLANE_TRANSFER_SENT" + | "HYPERLANE_TRANSFER_FAILED" + | "HYPERLANE_TRANSFER_RECEIVED"; // Desirable state + +export type HyperlaneTransferTransactions = { + send_tx: ChainTransaction | null; + receive_tx: ChainTransaction | null; +}; + +export type HyperlaneTransferInfo = { + from_chain_id: string; + to_chain_id: string; + state: HyperlaneTransferState; + txs: HyperlaneTransferTransactions; +}; + +export type GoFastTransferTransactions = { + order_submitted_tx: ChainTransaction | null; + order_filled_tx: ChainTransaction | null; + order_refunded_tx: ChainTransaction | null; + order_timeout_tx: ChainTransaction | null; +}; + +export type GoFastTransferState = + | "GO_FAST_TRANSFER_UNKNOWN" + | "GO_FAST_TRANSFER_SENT" + | "GO_FAST_POST_ACTION_FAILED" + | "GO_FAST_TRANSFER_TIMEOUT" + | "GO_FAST_TRANSFER_FILLED" // Desirable state + | "GO_FAST_TRANSFER_REFUNDED"; + +export type GoFastTransferInfo = { + from_chain_id: string; + to_chain_id: string; + state: GoFastTransferState; + txs: GoFastTransferTransactions; +}; + +export type StargateTransferState = + | "STARGATE_TRANSFER_UNKNOWN" + | "STARGATE_TRANSFER_SENT" + | "STARGATE_TRANSFER_RECEIVED" // Desirable state + | "STARGATE_TRANSFER_FAILED"; + +export type StargateTransferTransactions = { + send_tx: ChainTransaction | null; + receive_tx: ChainTransaction | null; + error_tx: ChainTransaction | null; +}; + +export type StargateTransferInfo = { + from_chain_id: string; + to_chain_id: string; + state: StargateTransferState; + txs: StargateTransferTransactions; +}; + +export type OPInitTransferState = + | "OPINIT_TRANSFER_UNKNOWN" + | "OPINIT_TRANSFER_SENT" + | "OPINIT_TRANSFER_RECEIVED"; // Desirable state + +export type OPInitTransferTransactions = { + send_tx: ChainTransaction | null; + receive_tx: ChainTransaction | null; +}; + +export type OPInitTransferInfo = { + from_chain_id: string; + to_chain_id: string; + state: OPInitTransferState; + txs: OPInitTransferTransactions; +}; + +export type TransferEvent = + | { + ibc_transfer: TransferInfo; + } + | { + axelar_transfer: AxelarTransferInfo; + } + | { cctp_transfer: CCTPTransferInfo } + | { hyperlane_transfer: HyperlaneTransferInfo } + | { op_init_transfer: OPInitTransferInfo } + | { go_fast_transfer: GoFastTransferInfo } + | { stargate_transfer: StargateTransferInfo }; From 69bdaf0167cb58a1a57a53c8c1a30e6f4fb36d49 Mon Sep 17 00:00:00 2001 From: delivan Date: Wed, 25 Dec 2024 17:15:08 +0900 Subject: [PATCH 41/43] Refactor some code from review --- apps/extension/src/pages/ibc-swap/index.tsx | 84 +++++++------------ .../src/recent-send-history/handler.ts | 4 +- .../src/recent-send-history/messages.ts | 4 +- .../src/recent-send-history/service.ts | 23 ++--- 4 files changed, 42 insertions(+), 73 deletions(-) diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index 9e517250e1..987b39f8a7 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -799,67 +799,43 @@ export const IBCSwapPage: FunctionComponent = observer(() => { // 일단은 체인 id를 keplr에서 사용하는 형태로 바꿔야 한다. for (const chainId of queryRoute.response.data.chain_ids) { - const isOnlyEvm = chainStore.hasChain(`eip155:${chainId}`); - if (!isOnlyEvm && !chainStore.hasChain(chainId)) { - throw new Error("Chain not found"); + const isOnlyEvm = parseInt(chainId) > 0; + const receiverAccount = accountStore.getAccount( + isOnlyEvm ? `eip155:${chainId}` : chainId + ); + if (receiverAccount.walletStatus !== WalletStatus.Loaded) { + await receiverAccount.init(); } - if (isOnlyEvm) { - const receiverAccount = accountStore.getAccount( - `eip155:${chainId}` - ); - - const ethereumHexAddress = - receiverAccount.hasEthereumHexAddress - ? receiverAccount.ethereumHexAddress - : undefined; - - if (!ethereumHexAddress) { - throw new Error("ethereumHexAddress is undefined"); - } - - simpleRoute.push({ - isOnlyEvm, - chainId: `eip155:${chainId}`, - receiver: ethereumHexAddress, - }); - } else { - const receiverAccount = accountStore.getAccount(chainId); - - if (receiverAccount.walletStatus !== WalletStatus.Loaded) { - await receiverAccount.init(); - } - - if (!receiverAccount.bech32Address) { - const receiverChainInfo = - chainStore.hasChain(chainId) && - chainStore.getChain(chainId); - if ( - receiverAccount.isNanoLedger && - receiverChainInfo && - (receiverChainInfo.bip44.coinType === 60 || - receiverChainInfo.features.includes( - "eth-address-gen" - ) || - receiverChainInfo.features.includes("eth-key-sign") || - receiverChainInfo.evm != null) - ) { - throw new Error( - "Please connect Ethereum app on Ledger with Keplr to get the address" - ); - } - + if (isOnlyEvm && !receiverAccount.ethereumHexAddress) { + const receiverChainInfo = + chainStore.hasChain(chainId) && + chainStore.getChain(chainId); + if ( + receiverAccount.isNanoLedger && + receiverChainInfo && + (receiverChainInfo.bip44.coinType === 60 || + receiverChainInfo.features.includes("eth-address-gen") || + receiverChainInfo.features.includes("eth-key-sign") || + receiverChainInfo.evm != null) + ) { throw new Error( - "receiverAccount.bech32Address is undefined" + "Please connect Ethereum app on Ledger with Keplr to get the address" ); } - simpleRoute.push({ - isOnlyEvm, - chainId, - receiver: receiverAccount.bech32Address, - }); + throw new Error( + "receiverAccount.ethereumHexAddress is undefined" + ); } + + simpleRoute.push({ + isOnlyEvm, + chainId, + receiver: isOnlyEvm + ? receiverAccount.ethereumHexAddress + : receiverAccount.bech32Address, + }); } } else { // 브릿지를 사용하지 않는 경우, 자세한 ibc swap channel 정보를 보여준다. diff --git a/packages/background/src/recent-send-history/handler.ts b/packages/background/src/recent-send-history/handler.ts index 7391aa94ff..c461118075 100644 --- a/packages/background/src/recent-send-history/handler.ts +++ b/packages/background/src/recent-send-history/handler.ts @@ -119,7 +119,7 @@ const handleSendTxAndRecordMsg: ( { currencies: [], }, - msg.isSkipTrack + msg.shouldLegacyTrack ); }; }; @@ -179,7 +179,7 @@ const handleSendTxAndRecordWithIBCSwapMsg: ( msg.swapChannelIndex, msg.swapReceiver, msg.notificationInfo, - msg.isSkipTrack + msg.shouldLegacyTrack ); }; }; diff --git a/packages/background/src/recent-send-history/messages.ts b/packages/background/src/recent-send-history/messages.ts index e9892d789c..85552944f3 100644 --- a/packages/background/src/recent-send-history/messages.ts +++ b/packages/background/src/recent-send-history/messages.ts @@ -106,7 +106,7 @@ export class SendTxAndRecordMsg extends Message { readonly denom: string; }[], public readonly memo: string, - public readonly isSkipTrack: boolean = false + public readonly shouldLegacyTrack: boolean = false ) { super(); } @@ -288,7 +288,7 @@ export class SendTxAndRecordWithIBCSwapMsg extends Message { public readonly notificationInfo: { currencies: AppCurrency[]; }, - public readonly isSkipTrack: boolean = false + public readonly shouldLegacyTrack: boolean = false ) { super(); } diff --git a/packages/background/src/recent-send-history/service.ts b/packages/background/src/recent-send-history/service.ts index e112b3f8ce..d18934bbc3 100644 --- a/packages/background/src/recent-send-history/service.ts +++ b/packages/background/src/recent-send-history/service.ts @@ -199,7 +199,7 @@ export class RecentSendHistoryService { notificationInfo: { currencies: AppCurrency[]; }, - isSkipTrack: boolean = false + shouldLegacyTrack: boolean = false ): Promise { const sourceChainInfo = this.chainsService.getChainInfoOrThrow(sourceChainId); @@ -234,7 +234,7 @@ export class RecentSendHistoryService { }); if (tx.hash) { - if (isSkipTrack) { + if (shouldLegacyTrack) { // no wait setTimeout(() => { simpleFetch("https://api.skip.build/", "/v2/tx/track", { @@ -317,7 +317,7 @@ export class RecentSendHistoryService { notificationInfo: { currencies: AppCurrency[]; }, - isSkipTrack: boolean = false + shouldLegacyTrack: boolean = false ): Promise { const sourceChainInfo = this.chainsService.getChainInfoOrThrow(sourceChainId); @@ -333,7 +333,7 @@ export class RecentSendHistoryService { onFulfill: (tx) => { if (tx.code == null || tx.code === 0) { if (tx.hash) { - if (isSkipTrack) { + if (shouldLegacyTrack) { setTimeout(() => { // no wait simpleFetch("https://api.skip.build/", "/v2/tx/track", { @@ -369,7 +369,7 @@ export class RecentSendHistoryService { }, }); - if (isSkipTrack) { + if (shouldLegacyTrack) { const id = this.addRecentIBCSwapHistory( swapType, sourceChainId, @@ -1128,6 +1128,7 @@ export class RecentSendHistoryService { } // skip related methods + @action recordTxWithSkipSwap( sourceChainId: string, destinationChainId: string, @@ -1226,16 +1227,8 @@ export class RecentSendHistoryService { () => { resolve(); }, - () => { - // reject if ws closed before fulfilled - // 하지만 로직상 fulfill 되기 전에 ws가 닫히는게 되기 때문에 - // delay를 좀 준다. - // 현재 trackIBCPacketForwardingRecursiveInternal에 ws close 이후에는 동기적인 로직밖에 없으므로 - // 문제될게 없다. - setTimeout(() => { - reject(); - }, 500); - }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + () => {}, () => { reject(); } From c94b2afed9cb73ab72b22c4447065a5641e93da2 Mon Sep 17 00:00:00 2001 From: delivan Date: Thu, 26 Dec 2024 17:21:45 +0900 Subject: [PATCH 42/43] Make tracking evm swap history on background --- apps/extension/src/pages/ibc-swap/index.tsx | 202 ++------- .../src/recent-send-history/service.ts | 386 ++++++++++++++---- 2 files changed, 327 insertions(+), 261 deletions(-) diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index 987b39f8a7..8fae2cba18 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -43,7 +43,6 @@ import { autorun } from "mobx"; import { LogAnalyticsEventMsg, RecordTxWithSkipSwapMsg, - RemoveSkipHistoryMsg, SendTxAndRecordMsg, SendTxAndRecordWithIBCSwapMsg, } from "@keplr-wallet/background"; @@ -56,7 +55,6 @@ import { Button } from "../../components/button"; import { TextButtonProps } from "../../components/button-text"; import { UnsignedEVMTransactionWithErc20Approvals } from "@keplr-wallet/stores-eth"; import { EthTxStatus } from "@keplr-wallet/types"; -import { simpleFetch } from "@keplr-wallet/simple-fetch"; const TextButtonStyles = { Container: styled.div` @@ -763,7 +761,6 @@ export const IBCSwapPage: FunctionComponent = observer(() => { }[] = []; let routeDurationSeconds: number | undefined; let isInterchainSwap: boolean = false; - let skipHistoryId: string | undefined = undefined; // queryRoute는 ibc history를 추적하기 위한 채널 정보 등을 얻기 위해서 사용된다. // /msgs_direct로도 얻을 순 있지만 따로 데이터를 해석해야되기 때문에 좀 힘들다... @@ -800,9 +797,10 @@ export const IBCSwapPage: FunctionComponent = observer(() => { // 일단은 체인 id를 keplr에서 사용하는 형태로 바꿔야 한다. for (const chainId of queryRoute.response.data.chain_ids) { const isOnlyEvm = parseInt(chainId) > 0; - const receiverAccount = accountStore.getAccount( - isOnlyEvm ? `eip155:${chainId}` : chainId - ); + const chainIdInKeplr = isOnlyEvm + ? `eip155:${chainId}` + : chainId; + const receiverAccount = accountStore.getAccount(chainIdInKeplr); if (receiverAccount.walletStatus !== WalletStatus.Loaded) { await receiverAccount.init(); } @@ -831,7 +829,7 @@ export const IBCSwapPage: FunctionComponent = observer(() => { simpleRoute.push({ isOnlyEvm, - chainId, + chainId: chainIdInKeplr, receiver: isOnlyEvm ? receiverAccount.ethereumHexAddress : receiverAccount.bech32Address, @@ -1068,11 +1066,10 @@ export const IBCSwapPage: FunctionComponent = observer(() => { Buffer.from(txHash).toString("hex") ); - new InExtensionMessageRequester() - .sendMessage(BACKGROUND_PORT, msg) - .then((response) => { - skipHistoryId = response; - }); + new InExtensionMessageRequester().sendMessage( + BACKGROUND_PORT, + msg + ); if ( !chainStore.isEnabledChain( @@ -1217,16 +1214,14 @@ export const IBCSwapPage: FunctionComponent = observer(() => { out_currency_minimal_denom: outCurrency.coinMinimalDenom, out_currency_denom: outCurrency.coinDenom, }); + + navigate("/", { + replace: true, + }); }, onFulfill: (tx: any) => { if (tx.code != null && tx.code !== 0) { console.log(tx.log ?? tx.raw_log); - if (skipHistoryId) { - new InExtensionMessageRequester().sendMessage( - BACKGROUND_PORT, - new RemoveSkipHistoryMsg(skipHistoryId) - ); - } notification.show( "failed", @@ -1236,42 +1231,6 @@ export const IBCSwapPage: FunctionComponent = observer(() => { return; } - if (isInterchainSwap) { - setTimeout(() => { - // no wait - simpleFetch( - "https://api.skip.build/", - "/v2/tx/track", - { - method: "POST", - headers: { - "content-type": "application/json", - ...(() => { - const res: { authorization?: string } = {}; - if (process.env["SKIP_API_KEY"]) { - res.authorization = - process.env["SKIP_API_KEY"]; - } - return res; - })(), - }, - body: JSON.stringify({ - tx_hash: tx.hash, - chain_id: inChainId, - }), - } - ) - .then((result) => { - console.log( - `Skip tx track result: ${JSON.stringify(result)}` - ); - }) - .catch((e) => { - console.log(e); - }); - }, 2000); - } - notification.show( "success", intl.formatMessage({ @@ -1282,10 +1241,6 @@ export const IBCSwapPage: FunctionComponent = observer(() => { }, } ); - - navigate("/", { - replace: true, - }); } else { const ethereumAccount = ethereumAccountStore.getAccount( ibcSwapConfigs.amountConfig.chainId @@ -1437,15 +1392,14 @@ export const IBCSwapPage: FunctionComponent = observer(() => { txHash ); - new InExtensionMessageRequester() - .sendMessage(BACKGROUND_PORT, msg) - .then((response) => { - skipHistoryId = response; + new InExtensionMessageRequester().sendMessage( + BACKGROUND_PORT, + msg + ); - navigate("/", { - replace: true, - }); - }); + navigate("/", { + replace: true, + }); } }, onFulfill: (txReceipt) => { @@ -1537,15 +1491,14 @@ export const IBCSwapPage: FunctionComponent = observer(() => { txHash ); - new InExtensionMessageRequester() - .sendMessage(BACKGROUND_PORT, msg) - .then((response) => { - skipHistoryId = response; + new InExtensionMessageRequester().sendMessage( + BACKGROUND_PORT, + msg + ); - navigate("/", { - replace: true, - }); - }); + navigate("/", { + replace: true, + }); }, onFulfill: (txReceipt) => { const queryBalances = queriesStore.get( @@ -1567,51 +1520,8 @@ export const IBCSwapPage: FunctionComponent = observer(() => { balance.fetch(); } }); - if (txReceipt.status === EthTxStatus.Success) { - // onBroadcasted에서 실행하기에는 너무 빨라서 tx not found가 발생할 수 있음 - // 재실행 로직을 사용해도 되지만, txReceipt를 기다렸다가 실행하는 게 자원 낭비가 적음 - const chainIdForTrack = inChainId.replace( - "eip155:", - "" - ); - setTimeout(() => { - // no wait - simpleFetch( - "https://api.skip.build/", - "/v2/tx/track", - { - method: "POST", - headers: { - "content-type": "application/json", - ...(() => { - const res: { - authorization?: string; - } = {}; - if (process.env["SKIP_API_KEY"]) { - res.authorization = - process.env["SKIP_API_KEY"]; - } - return res; - })(), - }, - body: JSON.stringify({ - tx_hash: txReceipt.transactionHash, - chain_id: chainIdForTrack, - }), - } - ) - .then((result) => { - console.log( - `Skip tx track result: ${JSON.stringify( - result - )}` - ); - }) - .catch((e) => { - console.log(e); - }); - }, 2000); + if (txReceipt.status === EthTxStatus.Success) { notification.show( "success", intl.formatMessage({ @@ -1620,13 +1530,6 @@ export const IBCSwapPage: FunctionComponent = observer(() => { "" ); } else { - if (skipHistoryId) { - new InExtensionMessageRequester().sendMessage( - BACKGROUND_PORT, - new RemoveSkipHistoryMsg(skipHistoryId) - ); - } - notification.show( "failed", intl.formatMessage({ @@ -1638,48 +1541,6 @@ export const IBCSwapPage: FunctionComponent = observer(() => { }, } ); - } else { - // onBroadcasted에서 실행하기에는 너무 빨라서 tx not found가 발생할 수 있음 - // 재실행 로직을 사용해도 되지만, txReceipt를 기다렸다가 실행하는 게 자원 낭비가 적음 - const chainIdForTrack = inChainId.replace( - "eip155:", - "" - ); - setTimeout(() => { - // no wait - simpleFetch( - "https://api.skip.build/", - "/v2/tx/track", - { - method: "POST", - headers: { - "content-type": "application/json", - ...(() => { - const res: { authorization?: string } = {}; - if (process.env["SKIP_API_KEY"]) { - res.authorization = - process.env["SKIP_API_KEY"]; - } - return res; - })(), - }, - body: JSON.stringify({ - tx_hash: txReceipt.transactionHash, - chain_id: chainIdForTrack, - }), - } - ) - .then((result) => { - console.log( - `Skip tx track result: ${JSON.stringify( - result - )}` - ); - }) - .catch((e) => { - console.log(e); - }); - }, 2000); } notification.show( @@ -1690,13 +1551,6 @@ export const IBCSwapPage: FunctionComponent = observer(() => { "" ); } else { - if (skipHistoryId) { - new InExtensionMessageRequester().sendMessage( - BACKGROUND_PORT, - new RemoveSkipHistoryMsg(skipHistoryId) - ); - } - notification.show( "failed", intl.formatMessage({ id: "error.transaction-failed" }), diff --git a/packages/background/src/recent-send-history/service.ts b/packages/background/src/recent-send-history/service.ts index d18934bbc3..a41daad20c 100644 --- a/packages/background/src/recent-send-history/service.ts +++ b/packages/background/src/recent-send-history/service.ts @@ -23,7 +23,12 @@ import { TxStatusResponse, } from "./types"; import { Buffer } from "buffer/"; -import { AppCurrency, ChainInfo, EthTxReceipt } from "@keplr-wallet/types"; +import { + AppCurrency, + ChainInfo, + EthTxReceipt, + EthTxStatus, +} from "@keplr-wallet/types"; import { CoinPretty } from "@keplr-wallet/unit"; import { simpleFetch } from "@keplr-wallet/simple-fetch"; import { id } from "@ethersproject/hash"; @@ -1211,42 +1216,203 @@ export class RecentSendHistoryService { return; } - const now = Date.now(); - const expectedEndTimestamp = - history.timestamp + history.routeDurationSeconds * 1000; - const diff = expectedEndTimestamp - now; - - const waitMsAfterError = 5 * 1000; - const maxRetries = diff > 0 ? diff / waitMsAfterError : 10; - + // check tx fulfilled and update history retry( () => { - return new Promise((resolve, reject) => { - this.trackSkipSwapRecursiveInternal( - id, - () => { - resolve(); + return new Promise((txFulfilledResolve, txFulfilledReject) => { + this.checkAndTrackSkipSwapTxFulfilledRecursive( + history, + (keepTracking: boolean) => { + txFulfilledResolve(); + + if (!keepTracking) { + return; + } + + const now = Date.now(); + const expectedEndTimestamp = + history.timestamp + history.routeDurationSeconds * 1000; + const diff = expectedEndTimestamp - now; + + const waitMsAfterError = 5 * 1000; + const maxRetries = diff > 0 ? diff / waitMsAfterError : 10; + + retry( + () => { + return new Promise((resolve, reject) => { + this.checkAndUpdateSkipSwapHistoryRecursive( + id, + resolve, + reject + ); + }); + }, + { + maxRetries, + waitMsAfterError, + maxWaitMsAfterError: 30 * 1000, // 30sec + } + ); }, - // eslint-disable-next-line @typescript-eslint/no-empty-function - () => {}, - () => { - reject(); - } + txFulfilledReject ); }); }, { - maxRetries, - waitMsAfterError, - maxWaitMsAfterError: 30 * 1000, // 30sec + maxRetries: 10, + waitMsAfterError: 500, + maxWaitMsAfterError: 4000, } ); } - protected trackSkipSwapRecursiveInternal = ( + protected checkAndTrackSkipSwapTxFulfilledRecursive = ( + history: SkipHistory, + onFulfill: (keepTracking: boolean) => void, + onError: () => void + ): void => { + const chainInfo = this.chainsService.getChainInfo(history.chainId); + if (!chainInfo) { + onFulfill(false); + return; + } + + if (this.chainsService.isEvmChain(history.chainId)) { + const evmInfo = chainInfo.evm; + if (!evmInfo) { + onFulfill(false); + return; + } + + simpleFetch<{ + result: EthTxReceipt | null; + error?: Error; + }>(evmInfo.rpc, { + method: "POST", + headers: { + "content-type": "application/json", + "request-source": origin, + }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "eth_getTransactionReceipt", + params: [history.txHash], + id: 1, + }), + }) + .then((res) => { + const txReceipt = res.data.result; + if (txReceipt) { + if (txReceipt.status === EthTxStatus.Success) { + setTimeout(() => { + simpleFetch("https://api.skip.build/", "/v2/tx/track", { + method: "POST", + headers: { + "content-type": "application/json", + ...(() => { + const res: { + authorization?: string; + } = {}; + if (process.env["SKIP_API_KEY"]) { + res.authorization = process.env["SKIP_API_KEY"]; + } + return res; + })(), + }, + body: JSON.stringify({ + tx_hash: history.txHash, + chain_id: history.chainId.replace("eip155:", ""), + }), + }) + .then((result) => { + console.log( + `Skip tx track result: ${JSON.stringify(result)}` + ); + onFulfill(true); + }) + .catch((e) => { + console.log(e); + this.removeRecentSkipHistory(history.id); + onFulfill(false); + }); + }, 2000); + } else { + // tx가 실패한거면 종료 + this.removeRecentSkipHistory(history.id); + onFulfill(false); + } + } else { + onError(); + } + }) + .catch(() => { + // 오류가 발생하면 종료 + onFulfill(false); + }); + } else { + const txTracer = new TendermintTxTracer(chainInfo.rpc, "/websocket"); + txTracer.addEventListener("error", () => onFulfill(false)); + txTracer + .traceTx({ + "tx.hash": history.txHash, + }) + .then((res: any) => { + txTracer.close(); + + if (Array.isArray(res.txs) && res.txs.length > 0) { + const tx = res.txs[0]; + if (tx.tx_result.code === 0) { + setTimeout(() => { + simpleFetch("https://api.skip.build/", "/v2/tx/track", { + method: "POST", + headers: { + "content-type": "application/json", + ...(() => { + const res: { + authorization?: string; + } = {}; + if (process.env["SKIP_API_KEY"]) { + res.authorization = process.env["SKIP_API_KEY"]; + } + return res; + })(), + }, + body: JSON.stringify({ + tx_hash: history.txHash, + chain_id: history.chainId, + }), + }) + .then((result) => { + console.log( + `Skip tx track result: ${JSON.stringify(result)}` + ); + onFulfill(true); + }) + .catch((e) => { + console.log(e); + this.removeRecentSkipHistory(history.id); + onFulfill(false); + }); + }, 2000); + } else { + // tx가 실패한거면 종료 + this.removeRecentSkipHistory(history.id); + onFulfill(false); + } + } else { + onError(); + } + }) + .catch(() => { + // 오류가 발생하면 종료 + onFulfill(false); + }); + } + }; + + protected checkAndUpdateSkipSwapHistoryRecursive = ( id: string, onFulfill: () => void, - onClose: () => void, onError: () => void ): void => { const history = this.getRecentSkipHistory(id); @@ -1575,13 +1741,7 @@ export class RecentSendHistoryService { } if (receiveTxHash) { - this.trackDestinationAssetAmount( - id, - receiveTxHash, - onFulfill, - onClose, - onError - ); + this.trackDestinationAssetAmount(id, receiveTxHash, onFulfill); } else { onFulfill(); } @@ -1603,13 +1763,11 @@ export class RecentSendHistoryService { protected trackDestinationAssetAmount( historyId: string, txHash: string, - onFullfill: () => void, - onClose: () => void, - onError: () => void + onFulfill: () => void ) { const history = this.getRecentSkipHistory(historyId); if (!history) { - onFullfill(); + onFulfill(); return; } @@ -1617,14 +1775,14 @@ export class RecentSendHistoryService { history.destinationChainId ); if (!chainInfo) { - onFullfill(); + onFulfill(); return; } if (this.chainsService.isEvmChain(history.destinationChainId)) { const evmInfo = chainInfo.evm; if (!evmInfo) { - onFullfill(); + onFulfill(); return; } @@ -1647,63 +1805,117 @@ export class RecentSendHistoryService { .then((res) => { const txReceipt = res.data.result; if (txReceipt) { - const logs = txReceipt.logs; - const transferTopic = id("Transfer(address,address,uint256)"); - const withdrawTopic = id("Withdrawal(address,uint256)"); - const hyperlaneReceiveTopic = id( - "ReceivedTransferRemote(uint32,bytes32,uint256)" - ); - for (const log of logs) { - if (log.topics[0] === transferTopic) { - const to = "0x" + log.topics[2].slice(26); - if (to.toLowerCase() === history.recipient.toLowerCase()) { - const amount = BigInt(log.data).toString(10); - history.resAmount.push([ - { - amount, - denom: history.destinationAsset.denom, - }, - ]); - - return; - } - } else if (log.topics[0] === withdrawTopic) { - const to = "0x" + log.topics[1].slice(26); - if (to.toLowerCase() === txReceipt.to.toLowerCase()) { - const amount = BigInt(log.data).toString(10); - history.resAmount.push([ - { amount, denom: history.destinationAsset.denom }, - ]); - return; - } - } else if (log.topics[0] === hyperlaneReceiveTopic) { - const to = "0x" + log.topics[2].slice(26); - if (to.toLowerCase() === history.recipient.toLowerCase()) { - const amount = BigInt(log.data).toString(10); - // Hyperlane을 통해 Forma로 TIA를 받는 경우 토큰 수량이 decimal 6으로 기록되는데, - // Forma에서는 decimal 18이기 때문에 12자리 만큼 0을 붙여준다. - history.resAmount.push([ - { - amount: - history.destinationAsset.denom === "forma-native" - ? `${amount}000000000000` - : amount, - denom: history.destinationAsset.denom, - }, - ]); - return; + simpleFetch<{ + result: any; + error?: Error; + }>(evmInfo.rpc, { + method: "POST", + headers: { + "content-type": "application/json", + "request-source": origin, + }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "debug_traceTransaction", + params: [txHash, { tracer: "callTracer" }], + id: 1, + }), + }).then((res) => { + let isFoundFromCall = false; + if (res.data.result) { + const searchForTransfers = (calls: any) => { + for (const call of calls) { + if ( + call.type === "CALL" && + call.to.toLowerCase() === history.recipient.toLowerCase() + ) { + const isERC20Transfer = + call.input.startsWith("0xa9059cbb"); + const value = BigInt( + isERC20Transfer + ? `0x${call.input.substring(74)}` + : call.value + ); + + history.resAmount.push([ + { + amount: value.toString(10), + denom: history.destinationAsset.denom, + }, + ]); + isFoundFromCall = true; + } + + if (call.calls && call.calls.length > 0) { + searchForTransfers(call.calls); + } + } + }; + + searchForTransfers(res.data.result.calls || []); + } + + if (isFoundFromCall) { + return; + } + + const logs = txReceipt.logs; + const transferTopic = id("Transfer(address,address,uint256)"); + const withdrawTopic = id("Withdrawal(address,uint256)"); + const hyperlaneReceiveTopic = id( + "ReceivedTransferRemote(uint32,bytes32,uint256)" + ); + for (const log of logs) { + if (log.topics[0] === transferTopic) { + const to = "0x" + log.topics[2].slice(26); + if (to.toLowerCase() === history.recipient.toLowerCase()) { + const amount = BigInt(log.data).toString(10); + history.resAmount.push([ + { + amount, + denom: history.destinationAsset.denom, + }, + ]); + + return; + } + } else if (log.topics[0] === withdrawTopic) { + const to = "0x" + log.topics[1].slice(26); + if (to.toLowerCase() === txReceipt.to.toLowerCase()) { + const amount = BigInt(log.data).toString(10); + history.resAmount.push([ + { amount, denom: history.destinationAsset.denom }, + ]); + return; + } + } else if (log.topics[0] === hyperlaneReceiveTopic) { + const to = "0x" + log.topics[2].slice(26); + if (to.toLowerCase() === history.recipient.toLowerCase()) { + const amount = BigInt(log.data).toString(10); + // Hyperlane을 통해 Forma로 TIA를 받는 경우 토큰 수량이 decimal 6으로 기록되는데, + // Forma에서는 decimal 18이기 때문에 12자리 만큼 0을 붙여준다. + history.resAmount.push([ + { + amount: + history.destinationAsset.denom === "forma-native" + ? `${amount}000000000000` + : amount, + denom: history.destinationAsset.denom, + }, + ]); + return; + } } } - } + }); } }) .finally(() => { - onFullfill(); + onFulfill(); }); } else { const txTracer = new TendermintTxTracer(chainInfo.rpc, "/websocket"); - txTracer.addEventListener("close", onClose); - txTracer.addEventListener("error", onError); + txTracer.addEventListener("error", () => onFulfill()); txTracer .queryTx({ "tx.hash": txHash, @@ -1729,7 +1941,7 @@ export class RecentSendHistoryService { } }) .finally(() => { - onFullfill(); + onFulfill(); }); } } From 98dfde024e19e56438939a12e10cf33bf9766fc1 Mon Sep 17 00:00:00 2001 From: delivan Date: Thu, 26 Dec 2024 17:53:14 +0900 Subject: [PATCH 43/43] Update from review --- apps/extension/src/pages/ibc-swap/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/extension/src/pages/ibc-swap/index.tsx b/apps/extension/src/pages/ibc-swap/index.tsx index 8fae2cba18..6282c81610 100644 --- a/apps/extension/src/pages/ibc-swap/index.tsx +++ b/apps/extension/src/pages/ibc-swap/index.tsx @@ -800,6 +800,10 @@ export const IBCSwapPage: FunctionComponent = observer(() => { const chainIdInKeplr = isOnlyEvm ? `eip155:${chainId}` : chainId; + if (!chainStore.hasChain(chainIdInKeplr)) { + continue; + } + const receiverAccount = accountStore.getAccount(chainIdInKeplr); if (receiverAccount.walletStatus !== WalletStatus.Loaded) { await receiverAccount.init();