From 7a9dbce79b36b8693fdbbda6c325d92fddd07c65 Mon Sep 17 00:00:00 2001 From: Thunnini Date: Mon, 3 Jul 2023 20:53:07 +0900 Subject: [PATCH 01/26] Add "ibc-pfm" feature --- packages/chain-validator/src/feature.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/chain-validator/src/feature.ts b/packages/chain-validator/src/feature.ts index 3769646132..1e29681c07 100644 --- a/packages/chain-validator/src/feature.ts +++ b/packages/chain-validator/src/feature.ts @@ -19,6 +19,7 @@ export const SupportedChainFeatures = [ "osmosis-txfees", "terra-classic-fee", "ibc-go-v7-hot-fix", + "ibc-pfm", ]; /** @@ -79,6 +80,24 @@ export const RecognizableChainFeaturesMethod: { ); }, }, + { + feature: "ibc-pfm", + fetch: async (features, _rpc, rest) => { + if (features.includes("ibc-go") && features.includes("ibc-transfer")) { + const result = await simpleFetch(rest, "/ibc/apps/router/v1/params", { + validateStatus: (status) => { + return status === 200 || status === 501; + }, + }); + + if (result.status === 200) { + return true; + } + } + + return false; + }, + }, { feature: "wasmd_0.24+", fetch: async (features, _rpc, rest) => { From 44fd39c6f37b810066dce38e98374adff4f25648 Mon Sep 17 00:00:00 2001 From: Thunnini Date: Wed, 5 Jul 2023 17:07:57 +0900 Subject: [PATCH 02/26] Minor refactor on `IBCChannelStore` --- packages/extension/src/index.tsx | 7 + .../ibc-transfer/add-channel-modal/index.tsx | 4 +- .../ibc-transfer/select-channel/index.tsx | 10 +- packages/extension/src/stores/root.tsx | 3 +- packages/stores/src/ibc/channel.ts | 245 +++++++++++------- 5 files changed, 160 insertions(+), 109 deletions(-) diff --git a/packages/extension/src/index.tsx b/packages/extension/src/index.tsx index 519158d5f1..155b66bbf1 100644 --- a/packages/extension/src/index.tsx +++ b/packages/extension/src/index.tsx @@ -120,6 +120,7 @@ const RoutesAfterReady: FunctionComponent = observer(() => { accountStore, keyRingStore, ibcCurrencyRegistrar, + ibcChannelStore, gravityBridgeCurrencyRegistrar, axelarEVMBridgeCurrencyRegistrar, priceStore, @@ -196,6 +197,12 @@ const RoutesAfterReady: FunctionComponent = observer(() => { return false; } + if (uiConfigStore.isDeveloper) { + if (!ibcChannelStore.isInitialized) { + return false; + } + } + if (!gravityBridgeCurrencyRegistrar.isInitialized) { return false; } diff --git a/packages/extension/src/pages/ibc-transfer/add-channel-modal/index.tsx b/packages/extension/src/pages/ibc-transfer/add-channel-modal/index.tsx index 0d5ae6f356..73ae56cb77 100644 --- a/packages/extension/src/pages/ibc-transfer/add-channel-modal/index.tsx +++ b/packages/extension/src/pages/ibc-transfer/add-channel-modal/index.tsx @@ -147,13 +147,13 @@ export const IBCAddChannelModal: FunctionComponent<{ setError(error); if (channel && clientState && error === "") { - await ibcChannelStore.get(chainId).addChannel({ + ibcChannelStore.addChannel(chainId, { portId: "transfer", channelId, counterpartyChainId: selectedChainId, }); - await ibcChannelStore.get(selectedChainId).addChannel({ + ibcChannelStore.addChannel(selectedChainId, { portId: channel.data.channel.counterparty.port_id, channelId: channel.data.channel.counterparty.channel_id, counterpartyChainId: chainId, diff --git a/packages/extension/src/pages/ibc-transfer/select-channel/index.tsx b/packages/extension/src/pages/ibc-transfer/select-channel/index.tsx index d5c4de5912..d8cc2985bf 100644 --- a/packages/extension/src/pages/ibc-transfer/select-channel/index.tsx +++ b/packages/extension/src/pages/ibc-transfer/select-channel/index.tsx @@ -45,8 +45,6 @@ export const IBCTransferSelectChannelView: FunctionComponent<{ const intl = useIntl(); const theme = useTheme(); - const ibcChannelInfo = ibcChannelStore.get(chainId); - const [isOpenSelectChannel, setIsOpenSelectChannel] = useState(false); const [selectedChannelId, setSelectedChannelId] = useState< string | undefined @@ -110,8 +108,8 @@ export const IBCTransferSelectChannelView: FunctionComponent<{ id: "page.ibc-transfer.select-channel.destination-chain-label", })} menuContainerMaxHeight="10rem" - items={ibcChannelInfo - .getTransferChannels() + items={ibcChannelStore + .getTransferChannels(chainId) .filter((channel) => chainStore.hasChain(channel.counterpartyChainId) ) @@ -146,8 +144,8 @@ export const IBCTransferSelectChannelView: FunctionComponent<{ if (key === "add-channel") { setIsOpenSelectChannel(true); } else { - const channel = ibcChannelInfo - .getTransferChannels() + const channel = ibcChannelStore + .getTransferChannels(chainId) .find((channel) => channel.channelId === key); if (channel) { channelConfig.setChannel(channel); diff --git a/packages/extension/src/stores/root.tsx b/packages/extension/src/stores/root.tsx index 0fe05f8190..9d8241d760 100644 --- a/packages/extension/src/stores/root.tsx +++ b/packages/extension/src/stores/root.tsx @@ -148,7 +148,8 @@ export class RootStore { ); this.ibcChannelStore = new IBCChannelStore( - new ExtensionKVStore("store_ibc_channel") + new ExtensionKVStore("store_ibc_channel"), + this.chainStore ); this.permissionStore = new PermissionStore( diff --git a/packages/stores/src/ibc/channel.ts b/packages/stores/src/ibc/channel.ts index b70b5ad695..4477eb99b9 100644 --- a/packages/stores/src/ibc/channel.ts +++ b/packages/stores/src/ibc/channel.ts @@ -1,134 +1,179 @@ -import { KVStore, toGenerator } from "@keplr-wallet/common"; -import { flow, makeObservable, observable, runInAction } from "mobx"; +import { KVStore, PrefixKVStore } from "@keplr-wallet/common"; +import { action, autorun, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; import { Channel } from "./types"; -import { HasMapStore } from "../common"; import { ChainIdHelper } from "@keplr-wallet/cosmos"; +import { IChainStore } from "../chain"; -export class IBCChannelStoreInner { - // channelMap[portId][channelId] - @observable.shallow +/** + * IBCChannelStore saves the IBC channel infomations to the storage. + */ +export class IBCChannelStore { + // Key: chainIdentifier, second key: ${portId}/${channelId} + @observable protected channelMap: Map> = new Map(); + @observable + public isInitialized = false; + + protected readonly legacyKVStore: KVStore; + constructor( protected readonly kvStore: KVStore, - protected readonly chainId: string + protected readonly chainStore: IChainStore & { + waitUntilInitialized?: () => Promise; + } ) { + this.legacyKVStore = kvStore; + this.kvStore = new PrefixKVStore(kvStore, "v2"); + makeObservable(this); - this.loadChannels(); + this.init(); } - getTransferChannels(): Channel[] { - return this.getChannelsToPort("transfer"); - } + async init(): Promise { + if (this.chainStore.waitUntilInitialized) { + await this.chainStore.waitUntilInitialized(); + } - readonly getChannelsToPort = computedFn((portId: string) => { - if (!this.channelMap.has(portId)) { - runInAction(() => { - this.channelMap.set( - portId, - observable.map( - {}, - { - deep: false, + const migrate = await this.kvStore.get("migrate/v1"); + if (!migrate) { + const migrationData = new Map>(); + + for (const chainInfo of this.chainStore.chainInfos) { + const chainIdentifier = ChainIdHelper.parse(chainInfo.chainId); + const legacyKey = `${chainIdentifier.identifier}-channels`; + const legacyObj = await this.legacyKVStore.get< + Record | undefined> + >(legacyKey); + if (legacyObj) { + for (const portId of Object.keys(legacyObj)) { + const map = legacyObj[portId]; + if (map) { + for (const channelId of Object.keys(map)) { + if (!migrationData.has(chainIdentifier.identifier)) { + migrationData.set(chainIdentifier.identifier, new Map()); + } + + const innerMap = migrationData.get(chainIdentifier.identifier)!; + const channel = map[channelId] as Channel; + innerMap.set(`${channel.portId}/${channel.channelId}`, channel); + } } - ) - ); - }); - } + } + } + } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const channelMapOfPort = this.channelMap.get(portId)!; + runInAction(() => { + this.channelMap = migrationData; + }); - const channels: Channel[] = []; - for (const channel of channelMapOfPort.values()) { - channels.push(channel); + await this.kvStore.set("migrate/v1", true); } - return channels; - }); - - readonly getChannel = computedFn((portId: string, channelId: string) => { - return this.channelMap.get(portId)?.get(channelId); - }); - - @flow - *addChannel(channel: Channel) { - if (!this.channelMap.has(channel.portId)) { - this.channelMap.set( - channel.portId, - observable.map( - {}, - { - deep: false, + const saved = await this.kvStore.get< + Record | undefined> + >("channelMap"); + if (saved) { + runInAction(() => { + for (const [chainIdentifier, inner] of Object.entries(saved)) { + const map = new Map(); + if (inner) { + for (const [key, channel] of Object.entries(inner)) { + if (channel) { + map.set(key, channel); + } + } } - ) - ); + this.channelMap.set(chainIdentifier, map); + } + }); } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.channelMap.get(channel.portId)!.set(channel.channelId, channel); - - yield this.saveChannels(); - } - - @flow - protected *loadChannels() { - const obj = yield* toGenerator( - this.kvStore.get<{ - [portId: string]: { [channelId: string]: Channel }; - }>(`${ChainIdHelper.parse(this.chainId).identifier}-channels`) - ); - - if (obj) { - for (const portId of Object.keys(obj)) { - const map = obj[portId]; - for (const channelId of Object.keys(map)) { - if (!this.channelMap.has(portId)) { - this.channelMap.set(portId, observable.map({}, { deep: false })); - } - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const innerMap = this.channelMap.get(portId)!; - innerMap.set(channelId, map[channelId]); + autorun(() => { + const data: Record< + string, + Record | undefined + > = {}; + for (const [chainIdentifier, inner] of this.channelMap.entries()) { + const map: Record = {}; + for (const [key, channel] of inner.entries()) { + map[key] = channel; } + data[chainIdentifier] = map; } - } - } + this.kvStore.set("channelMap", data); + }); - async saveChannels() { - const obj: { - [portId: string]: { [channelId: string]: Channel }; - } = {}; - this.channelMap.forEach((v, portId) => { - obj[portId] = (() => { - const obj: { [channelId: string]: Channel } = {}; - v.forEach((channel, channelId) => { - obj[channelId] = channel; + autorun(() => { + // Clear the channel map if the chain is removed. + const savedChainIdentifiers = new Set(Object.keys(saved ?? {})); + const removingChainIdentifiers: string[] = []; + for (const savedChainIdentifier of savedChainIdentifiers) { + if (!this.chainStore.hasChain(savedChainIdentifier)) { + removingChainIdentifiers.push(savedChainIdentifier); + } + } + if (removingChainIdentifiers.length > 0) { + runInAction(() => { + for (const removingChainIdentifier of removingChainIdentifiers) { + this.channelMap.delete(removingChainIdentifier); + } }); - return obj; - })(); + } }); - await this.kvStore.set( - `${ChainIdHelper.parse(this.chainId).identifier}-channels`, - obj - ); + runInAction(() => { + this.isInitialized = true; + }); } -} -/** - * IBCChannelStore saves the IBC channel infomations to the storage. - */ -export class IBCChannelStore extends HasMapStore { - constructor(protected readonly kvStore: KVStore) { - super((chainId: string) => { - return new IBCChannelStoreInner(kvStore, chainId); - }); + getTransferChannels(chainId: string): Channel[] { + return this.getChannels(chainId, "transfer"); } - override get(chainId: string): IBCChannelStoreInner { - return super.get(chainId); + readonly getChannels = computedFn( + (chainId: string, portId: string): Channel[] => { + const inner = this.channelMap.get( + ChainIdHelper.parse(chainId).identifier + ); + if (!inner) { + return []; + } + const res = []; + for (const [key, channel] of inner.entries()) { + if (key.startsWith(`${portId}/`)) { + res.push(channel); + } + } + return res; + } + ); + + readonly getChannel = computedFn( + ( + chainId: string, + portId: string, + channelId: string + ): Channel | undefined => { + const inner = this.channelMap.get( + ChainIdHelper.parse(chainId).identifier + ); + if (!inner) { + return undefined; + } + return inner.get(`${portId}/${channelId}`); + } + ); + + @action + addChannel(chainId: string, channel: Channel) { + const chainIdentifier = ChainIdHelper.parse(chainId).identifier; + if (!this.channelMap.has(chainIdentifier)) { + this.channelMap.set(chainIdentifier, new Map()); + } + const inner = this.channelMap.get(chainIdentifier)!; + inner.set(`${channel.portId}/${channel.channelId}`, channel); } } From 267013b3f462496b4ec2ff038d22117145437965 Mon Sep 17 00:00:00 2001 From: Thunnini Date: Wed, 5 Jul 2023 21:26:11 +0900 Subject: [PATCH 03/26] Make `IBCChannelConfig` have multiple channels --- .../use-ibc-channel-config-query-string.ts | 32 +++++++------------ .../src/pages/ibc-transfer/index.tsx | 12 ++++--- .../ibc-transfer/select-channel/index.tsx | 23 +++++++++---- packages/hooks/src/ibc/channel.ts | 12 +++---- packages/hooks/src/ibc/reciepient.ts | 5 +-- packages/hooks/src/ibc/types.ts | 4 +-- 6 files changed, 48 insertions(+), 40 deletions(-) diff --git a/packages/extension/src/hooks/use-ibc-channel-config-query-string.ts b/packages/extension/src/hooks/use-ibc-channel-config-query-string.ts index 3c8b86b9d2..971fbe2eac 100644 --- a/packages/extension/src/hooks/use-ibc-channel-config-query-string.ts +++ b/packages/extension/src/hooks/use-ibc-channel-config-query-string.ts @@ -2,6 +2,7 @@ import { useEffectOnce } from "./use-effect-once"; import { useEffect } from "react"; import { useSearchParams } from "react-router-dom"; import { IIBCChannelConfig } from "@keplr-wallet/hooks"; +import { toJS } from "mobx"; export const useIBCChannelConfigQueryString = ( channelConfig: IIBCChannelConfig @@ -9,34 +10,25 @@ export const useIBCChannelConfigQueryString = ( const [searchParams, setSearchParams] = useSearchParams(); useEffectOnce(() => { - const initialCounterpartyChainId = searchParams.get( - "initialCounterpartyChainId" - ); - const initialPortId = searchParams.get("initialPortId"); - const initialChannelId = searchParams.get("initialChannelId"); - if (initialCounterpartyChainId && initialPortId && initialChannelId) { - channelConfig.setChannel({ - counterpartyChainId: initialCounterpartyChainId, - portId: initialPortId, - channelId: initialChannelId, - }); + const initialIBCChannels = searchParams.get("initialIBCChannels"); + if (initialIBCChannels) { + const channels = JSON.parse(initialIBCChannels); + channelConfig.setChannels(channels); } }); useEffect(() => { setSearchParams( (prev) => { - if (channelConfig.channel) { + if (channelConfig.channels.length > 0) { prev.set( - "initialCounterpartyChainId", - channelConfig.channel.counterpartyChainId + "initialIBCChannels", + // toJS는 당장은 필요없기는 한데... 나중에 deep observable이 될 가능성이 있기도하고 + // 해서 나쁠께 없어서 해줌 + JSON.stringify(toJS(channelConfig.channels)) ); - prev.set("initialPortId", channelConfig.channel.portId); - prev.set("initialChannelId", channelConfig.channel.channelId); } else { - prev.delete("initialCounterpartyChainId"); - prev.delete("initialPortId"); - prev.delete("initialChannelId"); + prev.delete("initialIBCChannels"); } return prev; @@ -45,5 +37,5 @@ export const useIBCChannelConfigQueryString = ( replace: true, } ); - }, [channelConfig.channel, setSearchParams]); + }, [channelConfig.channels, setSearchParams]); }; diff --git a/packages/extension/src/pages/ibc-transfer/index.tsx b/packages/extension/src/pages/ibc-transfer/index.tsx index f1f2e66ad4..1485bf6e97 100644 --- a/packages/extension/src/pages/ibc-transfer/index.tsx +++ b/packages/extension/src/pages/ibc-transfer/index.tsx @@ -61,6 +61,10 @@ export const IBCTransferPage: FunctionComponent = observer(() => { chainStore.getChain(chainId).forceFindCurrency(coinMinimalDenom) ); + if (ibcTransferConfigs.channelConfig.channels.length > 1) { + throw new Error("IBC channel config must have only one channel"); + } + const gasSimulator = useGasSimulator( new ExtensionKVStore("gas-simulator.ibc.transfer"), chainStore, @@ -69,7 +73,7 @@ export const IBCTransferPage: FunctionComponent = observer(() => { ibcTransferConfigs.feeConfig, "native", () => { - if (!ibcTransferConfigs.channelConfig.channel) { + if (ibcTransferConfigs.channelConfig.channels.length === 0) { throw new Error("Channel not set yet"); } @@ -88,7 +92,7 @@ export const IBCTransferPage: FunctionComponent = observer(() => { } return accountInfo.cosmos.makeIBCTransferTx( - ibcTransferConfigs.channelConfig.channel, + ibcTransferConfigs.channelConfig.channels[0], ibcTransferConfigs.amountConfig.amount[0].toDec().toString(), ibcTransferConfigs.amountConfig.amount[0].currency, ibcTransferConfigs.recipientConfig.recipient @@ -152,10 +156,10 @@ export const IBCTransferPage: FunctionComponent = observer(() => { if (isSelectChannelPhase) { setPhase("amount"); } else { - if (ibcTransferConfigs.channelConfig.channel) { + if (ibcTransferConfigs.channelConfig.channels.length === 1) { try { const tx = accountInfo.cosmos.makeIBCTransferTx( - ibcTransferConfigs.channelConfig.channel, + ibcTransferConfigs.channelConfig.channels[0], ibcTransferConfigs.amountConfig.amount[0].toDec().toString(), ibcTransferConfigs.amountConfig.amount[0].currency, ibcTransferConfigs.recipientConfig.recipient diff --git a/packages/extension/src/pages/ibc-transfer/select-channel/index.tsx b/packages/extension/src/pages/ibc-transfer/select-channel/index.tsx index d8cc2985bf..502a2c0243 100644 --- a/packages/extension/src/pages/ibc-transfer/select-channel/index.tsx +++ b/packages/extension/src/pages/ibc-transfer/select-channel/index.tsx @@ -45,18 +45,29 @@ export const IBCTransferSelectChannelView: FunctionComponent<{ const intl = useIntl(); const theme = useTheme(); + if (channelConfig.channels.length > 1) { + throw new Error("IBC channel config must have only one channel"); + } + const [isOpenSelectChannel, setIsOpenSelectChannel] = useState(false); const [selectedChannelId, setSelectedChannelId] = useState< string | undefined - >(channelConfig.channel?.channelId); + >( + channelConfig.channels.length === 1 + ? channelConfig.channels[0].channelId + : undefined + ); useEffect(() => { - if (channelConfig.channel?.channelId !== selectedChannelId) { + if ( + channelConfig.channels.length === 1 && + channelConfig.channels[0].channelId !== selectedChannelId + ) { // channel이 다른 컴포넌트에서 바꼈을때를 대비해서 // 여기서 selectedChannelId를 업데이트 해준다. - setSelectedChannelId(channelConfig.channel?.channelId); + setSelectedChannelId(channelConfig.channels[0].channelId); } - }, [channelConfig.channel?.channelId, selectedChannelId]); + }, [channelConfig.channels, selectedChannelId]); const sender = accountStore.getAccount( chainStore.getChain(chainId).chainId @@ -148,10 +159,10 @@ export const IBCTransferSelectChannelView: FunctionComponent<{ .getTransferChannels(chainId) .find((channel) => channel.channelId === key); if (channel) { - channelConfig.setChannel(channel); + channelConfig.setChannels([channel]); setSelectedChannelId(key); } else { - channelConfig.setChannel(undefined); + channelConfig.setChannels([]); setSelectedChannelId(undefined); } } diff --git a/packages/hooks/src/ibc/channel.ts b/packages/hooks/src/ibc/channel.ts index 61af711ab3..78c90d194f 100644 --- a/packages/hooks/src/ibc/channel.ts +++ b/packages/hooks/src/ibc/channel.ts @@ -5,27 +5,27 @@ import { useState } from "react"; export class IBCChannelConfig implements IIBCChannelConfig { @observable.ref - protected _channel: Channel | undefined = undefined; + protected _channels: Channel[] = []; constructor() { makeObservable(this); } - get channel(): Channel | undefined { - return this._channel; + get channels(): Channel[] { + return this._channels; } @computed get error(): Error | undefined { - if (!this._channel) { + if (this._channels.length === 0) { return new ChannelNotSetError("Channel not set"); } return undefined; } @action - setChannel(channel: Channel | undefined): void { - this._channel = channel; + setChannels(channels: Channel[]): void { + this._channels = [...channels]; } } diff --git a/packages/hooks/src/ibc/reciepient.ts b/packages/hooks/src/ibc/reciepient.ts index 4f74f48f12..83767670a7 100644 --- a/packages/hooks/src/ibc/reciepient.ts +++ b/packages/hooks/src/ibc/reciepient.ts @@ -19,8 +19,9 @@ export class IBCRecipientConfig extends RecipientConfig { } override get chainId(): string { - return this.channelConfig.channel - ? this.channelConfig.channel.counterpartyChainId + return this.channelConfig.channels.length > 0 + ? this.channelConfig.channels[this.channelConfig.channels.length - 1] + .counterpartyChainId : super.chainId; } } diff --git a/packages/hooks/src/ibc/types.ts b/packages/hooks/src/ibc/types.ts index 984bc8b796..17ba8b6ce3 100644 --- a/packages/hooks/src/ibc/types.ts +++ b/packages/hooks/src/ibc/types.ts @@ -5,8 +5,8 @@ export interface Channel { } export interface IIBCChannelConfig { - channel: Channel | undefined; - setChannel(channel: Channel | undefined): void; + channels: Channel[]; + setChannels(channels: Channel[]): void; error: Error | undefined; } From 7a3cb4c9d770e84d055140b6a3ff83b119d3c5df Mon Sep 17 00:00:00 2001 From: Thunnini Date: Wed, 5 Jul 2023 21:29:13 +0900 Subject: [PATCH 04/26] Minor change --- packages/chain-validator/src/feature.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/chain-validator/src/feature.ts b/packages/chain-validator/src/feature.ts index 1e29681c07..bd1114d27d 100644 --- a/packages/chain-validator/src/feature.ts +++ b/packages/chain-validator/src/feature.ts @@ -83,7 +83,7 @@ export const RecognizableChainFeaturesMethod: { { feature: "ibc-pfm", fetch: async (features, _rpc, rest) => { - if (features.includes("ibc-go") && features.includes("ibc-transfer")) { + if (features.includes("ibc-go")) { const result = await simpleFetch(rest, "/ibc/apps/router/v1/params", { validateStatus: (status) => { return status === 200 || status === 501; From 9368b4a11358a9b3e7c7a4c437ec47072dac60e6 Mon Sep 17 00:00:00 2001 From: Thunnini Date: Thu, 6 Jul 2023 21:14:49 +0900 Subject: [PATCH 05/26] WIP --- .../pages/send/amount/ibc-transfer/index.ts | 1 + .../pages/send/amount/ibc-transfer/modal.tsx | 179 ++++++++++++++++++ .../extension/src/pages/send/amount/index.tsx | 49 ++++- 3 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 packages/extension/src/pages/send/amount/ibc-transfer/index.ts create mode 100644 packages/extension/src/pages/send/amount/ibc-transfer/modal.tsx diff --git a/packages/extension/src/pages/send/amount/ibc-transfer/index.ts b/packages/extension/src/pages/send/amount/ibc-transfer/index.ts new file mode 100644 index 0000000000..031608e25f --- /dev/null +++ b/packages/extension/src/pages/send/amount/ibc-transfer/index.ts @@ -0,0 +1 @@ +export * from "./modal"; diff --git a/packages/extension/src/pages/send/amount/ibc-transfer/modal.tsx b/packages/extension/src/pages/send/amount/ibc-transfer/modal.tsx new file mode 100644 index 0000000000..e1d3d1de7c --- /dev/null +++ b/packages/extension/src/pages/send/amount/ibc-transfer/modal.tsx @@ -0,0 +1,179 @@ +import React, { FunctionComponent, useEffect, useState } from "react"; +import { Box } from "../../../../components/box"; +import { ColorPalette } from "../../../../styles"; +import { useTheme } from "styled-components"; +import { SearchTextInput } from "../../../../components/input"; +import { useFocusOnMount } from "../../../../hooks/use-focus-on-mount"; +import { Gutter } from "../../../../components/gutter"; +import SimpleBar from "simplebar-react"; +import { observer } from "mobx-react-lite"; +import { useStore } from "../../../../stores"; +import { Subtitle2 } from "../../../../components/typography"; +import { IIBCChannelConfig } from "@keplr-wallet/hooks"; + +async function testIBCChannels(chainId: string) { + return new Promise< + | { + channels: { + port: string; + channelId: string; + chainId: string; + }[]; + }[] + | undefined + >((resolve) => { + setTimeout(() => { + if (chainId.startsWith("cosmoshub")) { + resolve([ + { + channels: [ + { + port: "transfer", + channelId: "channel-0", + chainId: "osmosis-1", + }, + ], + }, + ]); + } else { + resolve(undefined); + } + }, 1000); + }); +} + +export const IBCTransferSelectDestinationModal: FunctionComponent<{ + chainId: string; + ibcChannelConfig: IIBCChannelConfig; +}> = observer(({ chainId, ibcChannelConfig }) => { + const { chainStore } = useStore(); + + const theme = useTheme(); + + const [destinationChannelsMap, setDestinationChannelsMap] = useState< + Map< + string, + { + port: string; + channelId: string; + chainId: string; + }[] + > + >(new Map()); + + useEffect(() => { + testIBCChannels(chainId).then((channels) => { + if (!channels) { + setDestinationChannelsMap(new Map()); + return; + } + + const map = new Map< + string, + { + port: string; + channelId: string; + chainId: string; + }[] + >(); + + for (const c of channels) { + if (c.channels.length > 0) { + let allExist = true; + for (const channel of c.channels) { + if (!chainStore.hasChain(channel.chainId)) { + allExist = false; + break; + } + } + + if (allExist) { + const destChainId = c.channels[c.channels.length - 1].chainId; + if (!map.has(destChainId)) { + map.set(destChainId, c.channels); + } + } + } + } + + setDestinationChannelsMap(map); + }); + }, [chainId]); + + const [search, setSearch] = useState(""); + + const searchRef = useFocusOnMount(); + + return ( + + { + e.preventDefault(); + + setSearch(e.target.value); + }} + placeholder="Search for a chain" + /> + + + + + {Array.from(destinationChannelsMap.keys()).map((chainId) => { + const chainInfo = chainStore.getChain(chainId); + + return ( + { + e.preventDefault(); + + const channels = destinationChannelsMap.get(chainId); + if (channels) { + ibcChannelConfig.setChannels( + channels.map((c) => { + return { + portId: c.port, + channelId: c.channelId, + counterpartyChainId: c.chainId, + }; + }) + ); + } else { + console.log("No channels"); + ibcChannelConfig.setChannels([]); + } + }} + > + {chainInfo.chainName} + + ); + })} + + + ); +}); diff --git a/packages/extension/src/pages/send/amount/index.tsx b/packages/extension/src/pages/send/amount/index.tsx index 084a8074a5..bfe7651890 100644 --- a/packages/extension/src/pages/send/amount/index.tsx +++ b/packages/extension/src/pages/send/amount/index.tsx @@ -1,4 +1,10 @@ -import React, { FunctionComponent, useEffect, useMemo, useRef } from "react"; +import React, { + FunctionComponent, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { observer } from "mobx-react-lite"; import { HeaderLayout } from "../../../layouts/header"; import { BackButton } from "../../../layouts/header/components"; @@ -34,6 +40,9 @@ import { BACKGROUND_PORT } from "@keplr-wallet/router"; import { SendTxAndRecordMsg } from "@keplr-wallet/background"; import { FormattedMessage, useIntl } from "react-intl"; import { useTxConfigsQueryString } from "../../../hooks/use-tx-config-query-string"; +import { HorizontalRadioGroup } from "../../../components/radio-group"; +import { Modal } from "../../../components/modal"; +import { IBCTransferSelectDestinationModal } from "./ibc-transfer"; const Styles = { Flex1: styled.div` @@ -58,6 +67,12 @@ export const SendAmountPage: FunctionComponent = observer(() => { initialCoinMinimalDenom || chainStore.getChain(chainId).currencies[0].coinMinimalDenom; + const [isIBCTransfer, setIsIBCTransfer] = useState(false); + const [ + isIBCTransferDestinationModalOpen, + setIsIBCTransferDestinationModalOpen, + ] = useState(true); + useEffect(() => { if (addressRef.current) { addressRef.current.focus(); @@ -356,6 +371,28 @@ export const SendAmountPage: FunctionComponent = observer(() => { /> + { + if (key === "ibc-transfer") { + setIsIBCTransfer(true); + } else { + setIsIBCTransfer(false); + } + }} + /> + { /> + + { + setIsIBCTransferDestinationModalOpen(false); + }} + > + + ); }); From 9720226c92906cafb3959cf2a2fc175d79e39f51 Mon Sep 17 00:00:00 2001 From: Thunnini Date: Mon, 17 Jul 2023 16:08:08 +0200 Subject: [PATCH 06/26] WIP --- .../src/recent-send-history/handler.ts | 34 ++- .../src/recent-send-history/init.ts | 7 +- .../src/recent-send-history/messages.ts | 96 +++++++ .../src/recent-send-history/service.ts | 259 ++++++++++++++++- .../src/recent-send-history/types.ts | 40 +++ packages/cosmos/src/tx-tracer/index.ts | 153 ++++++++-- .../use-ibc-channel-config-query-string.ts | 13 +- .../src/pages/ibc-transfer/index.tsx | 15 +- .../ibc-transfer/destination-chain-view.tsx | 105 +++++++ .../pages/send/amount/ibc-transfer/index.ts | 1 + .../pages/send/amount/ibc-transfer/modal.tsx | 267 +++++++----------- .../extension/src/pages/send/amount/index.tsx | 239 +++++++++++----- packages/extension/src/stores/root.tsx | 6 + .../src/stores/skip/assets-from-source.ts | 182 ++++++++++++ packages/extension/src/stores/skip/chains.ts | 84 ++++++ .../src/stores/skip/ibc-pfm-transfer.ts | 262 +++++++++++++++++ packages/extension/src/stores/skip/index.ts | 6 + packages/extension/src/stores/skip/queries.ts | 38 +++ packages/extension/src/stores/skip/route.ts | 105 +++++++ packages/extension/src/stores/skip/types.ts | 51 ++++ packages/hooks/src/ibc/amount.ts | 42 ++- packages/hooks/src/ibc/channel.ts | 36 ++- packages/hooks/src/ibc/reciepient.ts | 47 ++- packages/hooks/src/ibc/send-ibc-transfer.ts | 69 ++++- packages/hooks/src/ibc/types.ts | 4 +- packages/hooks/src/tx/amount.ts | 12 +- packages/hooks/src/tx/errors.ts | 8 + packages/hooks/src/tx/types.ts | 2 +- packages/hooks/src/tx/validate.ts | 4 + packages/stores/src/account/cosmos.ts | 89 +++++- packages/stores/src/ibc/currency-registrar.ts | 65 ++++- packages/stores/src/query/queries.ts | 2 +- packages/types/src/currency.ts | 4 + 33 files changed, 2015 insertions(+), 332 deletions(-) create mode 100644 packages/extension/src/pages/send/amount/ibc-transfer/destination-chain-view.tsx create mode 100644 packages/extension/src/stores/skip/assets-from-source.ts create mode 100644 packages/extension/src/stores/skip/chains.ts create mode 100644 packages/extension/src/stores/skip/ibc-pfm-transfer.ts create mode 100644 packages/extension/src/stores/skip/index.ts create mode 100644 packages/extension/src/stores/skip/queries.ts create mode 100644 packages/extension/src/stores/skip/route.ts create mode 100644 packages/extension/src/stores/skip/types.ts diff --git a/packages/background/src/recent-send-history/handler.ts b/packages/background/src/recent-send-history/handler.ts index 399d3cdb86..00a8bcf096 100644 --- a/packages/background/src/recent-send-history/handler.ts +++ b/packages/background/src/recent-send-history/handler.ts @@ -5,7 +5,11 @@ import { KeplrError, Message, } from "@keplr-wallet/router"; -import { GetRecentSendHistoriesMsg, SendTxAndRecordMsg } from "./messages"; +import { + GetRecentSendHistoriesMsg, + SendTxAndRecordMsg, + SendTxAndRecordWithIBCPacketForwardingMsg, +} from "./messages"; import { RecentSendHistoryService } from "./service"; export const getHandler: (service: RecentSendHistoryService) => Handler = ( @@ -23,6 +27,11 @@ export const getHandler: (service: RecentSendHistoryService) => Handler = ( env, msg as SendTxAndRecordMsg ); + case SendTxAndRecordWithIBCPacketForwardingMsg: + return handleSendTxAndRecordWithIBCPacketForwardingMsg(service)( + env, + msg as SendTxAndRecordWithIBCPacketForwardingMsg + ); default: throw new KeplrError("tx", 110, "Unknown msg type"); } @@ -51,7 +60,28 @@ const handleSendTxAndRecordMsg: ( msg.sender, msg.recipient, msg.amount, - msg.memo + msg.memo, + undefined + ); + }; +}; + +const handleSendTxAndRecordWithIBCPacketForwardingMsg: ( + service: RecentSendHistoryService +) => InternalHandler = (service) => { + return async (_env, msg) => { + return await service.sendTxAndRecord( + msg.historyType, + msg.sourceChainId, + msg.destinationChainId, + msg.tx, + msg.mode, + msg.silent, + msg.sender, + msg.recipient, + msg.amount, + msg.memo, + msg.channels ); }; }; diff --git a/packages/background/src/recent-send-history/init.ts b/packages/background/src/recent-send-history/init.ts index 981a1de195..aa7571f5d0 100644 --- a/packages/background/src/recent-send-history/init.ts +++ b/packages/background/src/recent-send-history/init.ts @@ -1,5 +1,9 @@ import { Router } from "@keplr-wallet/router"; -import { GetRecentSendHistoriesMsg, SendTxAndRecordMsg } from "./messages"; +import { + GetRecentSendHistoriesMsg, + SendTxAndRecordMsg, + SendTxAndRecordWithIBCPacketForwardingMsg, +} from "./messages"; import { ROUTE } from "./constants"; import { getHandler } from "./handler"; import { RecentSendHistoryService } from "./service"; @@ -7,6 +11,7 @@ import { RecentSendHistoryService } from "./service"; export function init(router: Router, service: RecentSendHistoryService): void { router.registerMessage(GetRecentSendHistoriesMsg); router.registerMessage(SendTxAndRecordMsg); + router.registerMessage(SendTxAndRecordWithIBCPacketForwardingMsg); router.addHandler(ROUTE, getHandler(service)); } diff --git a/packages/background/src/recent-send-history/messages.ts b/packages/background/src/recent-send-history/messages.ts index ce3a83c362..ec5522fae1 100644 --- a/packages/background/src/recent-send-history/messages.ts +++ b/packages/background/src/recent-send-history/messages.ts @@ -96,4 +96,100 @@ export class SendTxAndRecordMsg extends Message { type(): string { return SendTxAndRecordMsg.type(); } + + withIBCPacketForwarding( + channels: { + portId: string; + channelId: string; + counterpartyChainId: string; + }[] + ): SendTxAndRecordWithIBCPacketForwardingMsg { + return new SendTxAndRecordWithIBCPacketForwardingMsg( + this.historyType, + this.sourceChainId, + this.destinationChainId, + this.tx, + channels, + this.mode, + this.silent, + this.sender, + this.recipient, + this.amount, + this.memo + ); + } +} + +export class SendTxAndRecordWithIBCPacketForwardingMsg extends Message { + public static type() { + return "send-tx-and-record-with-ibc-packet-forwarding"; + } + + constructor( + public readonly historyType: string, + public readonly sourceChainId: string, + public readonly destinationChainId: string, + public readonly tx: unknown, + public readonly channels: { + portId: string; + channelId: string; + counterpartyChainId: string; + }[], + public readonly mode: "async" | "sync" | "block", + public readonly silent: boolean, + public readonly sender: string, + public readonly recipient: string, + public readonly amount: { + readonly amount: string; + readonly denom: string; + }[], + public readonly memo: string + ) { + super(); + } + + validateBasic(): void { + if (!this.historyType) { + throw new Error("type is empty"); + } + + if (!this.sourceChainId) { + throw new Error("chain id is empty"); + } + + if (!this.destinationChainId) { + throw new Error("chain id is empty"); + } + + if (!this.tx) { + throw new Error("tx is empty"); + } + + if (this.channels.length === 0) { + throw new Error("channels is empty"); + } + + if ( + !this.mode || + (this.mode !== "sync" && this.mode !== "async" && this.mode !== "block") + ) { + throw new Error("invalid mode"); + } + + 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 SendTxAndRecordWithIBCPacketForwardingMsg.type(); + } } diff --git a/packages/background/src/recent-send-history/service.ts b/packages/background/src/recent-send-history/service.ts index 931eb2d1ad..b3d9cfbe9d 100644 --- a/packages/background/src/recent-send-history/service.ts +++ b/packages/background/src/recent-send-history/service.ts @@ -1,5 +1,9 @@ import { ChainsService } from "../chains"; -import { Bech32Address, ChainIdHelper } from "@keplr-wallet/cosmos"; +import { + Bech32Address, + ChainIdHelper, + TendermintTxTracer, +} from "@keplr-wallet/cosmos"; import { BackgroundTxService } from "../tx"; import { action, @@ -10,7 +14,8 @@ import { toJS, } from "mobx"; import { KVStore } from "@keplr-wallet/common"; -import { RecentSendHistory } from "./types"; +import { IBCTransferHistory, RecentSendHistory } from "./types"; +import { Buffer } from "buffer/"; export class RecentSendHistoryService { // Key: {chain_identifier}/{type} @@ -18,6 +23,13 @@ export class RecentSendHistoryService { protected readonly recentSendHistoryMap: Map = new Map(); + // Key: id + @observable + protected readonly recentIBCTransferHistoryMap: Map< + string, + IBCTransferHistory + > = new Map(); + constructor( protected readonly kvStore: KVStore, protected readonly chainsService: ChainsService, @@ -27,12 +39,12 @@ export class RecentSendHistoryService { } async init(): Promise { - const saved = await this.kvStore.get>( - "recentSendHistoryMap" - ); - if (saved) { + const recentSendHistoryMapSaved = await this.kvStore.get< + Record + >("recentSendHistoryMap"); + if (recentSendHistoryMapSaved) { runInAction(() => { - for (const [key, value] of Object.entries(saved)) { + for (const [key, value] of Object.entries(recentSendHistoryMapSaved)) { this.recentSendHistoryMap.set(key, value); } }); @@ -45,6 +57,27 @@ export class RecentSendHistoryService { obj ); }); + + const recentIBCTransferHistoryMapSaved = await this.kvStore.get< + Record + >("recentIBCTransferHistoryMap"); + if (recentIBCTransferHistoryMapSaved) { + runInAction(() => { + for (const [key, value] of Object.entries( + recentIBCTransferHistoryMapSaved + )) { + this.recentIBCTransferHistoryMap.set(key, value); + } + }); + } + autorun(() => { + const js = toJS(this.recentIBCTransferHistoryMap); + const obj = Object.fromEntries(js); + this.kvStore.set>( + "recentIBCTransferHistoryMap", + obj + ); + }); } async sendTxAndRecord( @@ -60,7 +93,14 @@ export class RecentSendHistoryService { amount: string; denom: string; }[], - memo: string + memo: string, + ibcChannels: + | { + portId: string; + channelId: string; + counterpartyChainId: string; + }[] + | undefined ): Promise { const sourceChainInfo = this.chainsService.getChainInfoOrThrow(sourceChainId); @@ -76,7 +116,7 @@ export class RecentSendHistoryService { destinationChainInfo.bech32Config.bech32PrefixAccAddr ); - return await this.txService.sendTx(sourceChainId, tx, mode, { + const txHash = await this.txService.sendTx(sourceChainId, tx, mode, { silent, onFulfill: (tx) => { if (tx.code == null || tx.code === 0) { @@ -85,10 +125,152 @@ export class RecentSendHistoryService { recipient, amount, memo, + ibcChannels, }); } }, }); + + if (ibcChannels && ibcChannels.length > 0) { + const id = this.addRecentIBCTransferHistory( + sourceChainId, + destinationChainId, + sender, + recipient, + amount, + memo, + ibcChannels, + txHash + ); + + this.trackIBCPacketForwardingRecersive(id); + } + + return txHash; + } + + trackIBCPacketForwardingRecersive(id: string) { + const history = this.getRecentIBCTransferHistory(id); + if (!history) { + return; + } + + if (!history.txFulfilled) { + const chainId = history.chainId; + const chainInfo = this.chainsService.getChainInfo(chainId); + const txHash = Buffer.from(history.txHash, "hex"); + + if (chainInfo) { + const txTracer = new TendermintTxTracer(chainInfo.rpc, "/websocket"); + txTracer.traceTx(txHash).then((tx) => { + txTracer.close(); + + runInAction(() => { + history.txFulfilled = true; + if (tx.code != null && tx.code !== 0) { + history.txError = tx.log || tx.raw_log || "Unknown error"; + } + + if (history.ibcHistory.length > 0) { + const firstChannel = history.ibcHistory[0]; + + const events = tx.events; + if (!events) { + throw new Error("Invalid tx"); + } + if (!Array.isArray(events)) { + throw new Error("Invalid tx"); + } + + const packetEvent = events.find((event) => { + if (event.type !== "send_packet") { + return false; + } + const sourcePortAttr = event.attributes.find( + (attr: { key: string }) => { + return ( + attr.key === + Buffer.from("packet_src_port").toString("base64") + ); + } + ); + if (!sourcePortAttr) { + return false; + } + const sourceChannelAttr = event.attributes.find( + (attr: { key: string }) => { + return ( + attr.key === + Buffer.from("packet_src_channel").toString("base64") + ); + } + ); + if (!sourceChannelAttr) { + return false; + } + return ( + sourcePortAttr.value === + Buffer.from(firstChannel.portId).toString("base64") && + sourceChannelAttr.value === + Buffer.from(firstChannel.channelId).toString("base64") + ); + }); + + if (packetEvent) { + const sequenceAttr = packetEvent.attributes.find( + (attr: { key: string }) => { + return ( + attr.key === + Buffer.from("packet_sequence").toString("base64") + ); + } + ); + if (!sequenceAttr) { + return; + } + + firstChannel.sequence = Buffer.from( + sequenceAttr.value, + "base64" + ).toString(); + + this.trackIBCPacketForwardingRecersive(id); + } + } + }); + }); + } + } else if (history.ibcHistory.length > 0) { + const targetChannel = history.ibcHistory.find((history) => { + return !history.completed; + }); + + if (targetChannel && targetChannel.sequence) { + const chainInfo = this.chainsService.getChainInfo( + targetChannel.counterpartyChainId + ); + if (chainInfo) { + const txTracer = new TendermintTxTracer(chainInfo.rpc, "/websocket"); + txTracer + .traceTx({ + "recv_packet.packet_src_port": targetChannel.portId, + "recv_packet.packet_src_channel": targetChannel.channelId, + "recv_packet.packet_sequence": targetChannel.sequence, + }) + .then((tx) => { + txTracer.close(); + + runInAction(() => { + if (tx.code != null && tx.code !== 0) { + targetChannel.completed = true; + + this.trackIBCPacketForwardingRecersive(id); + } + }); + }); + } + } + } } getRecentSendHistories(chainId: string, type: string): RecentSendHistory[] { @@ -113,4 +295,63 @@ export class RecentSendHistoryService { this.recentSendHistoryMap.set(key, histories); } + + @action + addRecentIBCTransferHistory( + chainId: string, + destinationChainId: string, + sender: string, + recipient: string, + amount: { + amount: string; + denom: string; + }[], + memo: string, + ibcChannels: + | { + portId: string; + channelId: string; + counterpartyChainId: string; + }[], + txHash: Uint8Array + ): string { + const bytes = new Uint8Array(10); + crypto.getRandomValues(bytes); + const id = Buffer.from(bytes).toString("hex"); + + const history: IBCTransferHistory = { + id, + chainId, + destinationChainId, + timestamp: Date.now(), + sender, + recipient, + amount, + memo, + + ibcHistory: ibcChannels.map((channel) => { + return { + portId: channel.portId, + channelId: channel.channelId, + counterpartyChainId: channel.counterpartyChainId, + + completed: false, + }; + }), + txHash: Buffer.from(txHash).toString("hex"), + }; + + this.recentIBCTransferHistoryMap.set(id, history); + + return id; + } + + getRecentIBCTransferHistory(id: string): IBCTransferHistory | undefined { + return this.recentIBCTransferHistoryMap.get(id); + } + + @action + removeRecentIBCTransferHistory(id: string): boolean { + return this.recentIBCTransferHistoryMap.delete(id); + } } diff --git a/packages/background/src/recent-send-history/types.ts b/packages/background/src/recent-send-history/types.ts index 6fe2570510..0e49ecab99 100644 --- a/packages/background/src/recent-send-history/types.ts +++ b/packages/background/src/recent-send-history/types.ts @@ -7,4 +7,44 @@ export interface RecentSendHistory { denom: string; }[]; memo: string; + + ibcChannels: + | { + portId: string; + channelId: string; + counterpartyChainId: string; + }[] + | undefined; +} + +export interface IBCTransferHistory { + id: string; + + chainId: string; + destinationChainId: string; + timestamp: number; + sender: string; + recipient: string; + amount: { + amount: string; + denom: string; + }[]; + memo: string; + + txHash: string; + + txFulfilled?: boolean; + txError?: string; + + ibcHistory: + | { + portId: string; + channelId: string; + counterpartyChainId: string; + + sequence?: string; + + completed: boolean; + error?: string; + }[]; } diff --git a/packages/cosmos/src/tx-tracer/index.ts b/packages/cosmos/src/tx-tracer/index.ts index 20c5e29a62..42ef1e5f85 100644 --- a/packages/cosmos/src/tx-tracer/index.ts +++ b/packages/cosmos/src/tx-tracer/index.ts @@ -16,7 +16,7 @@ export class TendermintTxTracer { protected txSubscribes: Map< number, { - hash: Uint8Array; + params: Record; resolver: (data?: unknown) => void; rejector: (e: Error) => void; } @@ -27,7 +27,7 @@ export class TendermintTxTracer { number, { method: string; - params: unknown[]; + params: Record; resolver: (data?: unknown) => void; rejector: (e: Error) => void; } @@ -105,7 +105,7 @@ export class TendermintTxTracer { } for (const [id, tx] of this.txSubscribes) { - this.sendSubscribeTxRpc(id, tx.hash); + this.sendSubscribeTxRpc(id, tx.params); } for (const [id, query] of this.pendingQueries) { @@ -189,6 +189,12 @@ export class TendermintTxTracer { } }; + /** + * SubscribeBlock receives the handler for the block. + * The handelrs shares the subscription of block. + * @param handler + * @return unsubscriber + */ subscribeBlock(handler: (block: any) => void) { this.newBlockSubscribes.push({ handler, @@ -197,6 +203,12 @@ export class TendermintTxTracer { if (this.newBlockSubscribes.length === 1) { this.sendSubscribeBlockRpc(); } + + return () => { + this.newBlockSubscribes = this.newBlockSubscribes.filter( + (s) => s.handler !== handler + ); + }; } protected sendSubscribeBlockRpc(): void { @@ -213,17 +225,29 @@ export class TendermintTxTracer { } // Query the tx and subscribe the tx. - traceTx(hash: Uint8Array): Promise { + traceTx( + query: Uint8Array | Record + ): Promise { return new Promise((resolve) => { // At first, try to query the tx at the same time of subscribing the tx. // But, the querying's error will be ignored. - this.queryTx(hash) - .then(resolve) + this.queryTx(query) + .then((result) => { + if (query instanceof Uint8Array) { + resolve(result); + return; + } + + if (result?.total_count !== "0") { + resolve(result); + return; + } + }) .catch(() => { // noop }); - this.subscribeTx(hash).then(resolve); + this.subscribeTx(query).then(resolve); }).then((tx) => { // Occasionally, even if the subscribe tx event occurs, the state through query is not changed yet. // Perhaps it is because the block has not been committed yet even though the result of deliverTx in tendermint is complete. @@ -235,42 +259,115 @@ export class TendermintTxTracer { }); } - subscribeTx(hash: Uint8Array): Promise { - const id = this.createRandomId(); + subscribeTx( + query: Uint8Array | Record + ): Promise { + if (query instanceof Uint8Array) { + const id = this.createRandomId(); - return new Promise((resolve, reject) => { - this.txSubscribes.set(id, { - hash, - resolver: resolve, - rejector: reject, + const params = { + query: `tm.event='Tx' AND tx.hash='${Buffer.from(query) + .toString("hex") + .toUpperCase()}'`, + }; + + return new Promise((resolve, reject) => { + this.txSubscribes.set(id, { + params, + resolver: resolve, + rejector: reject, + }); + + this.sendSubscribeTxRpc(id, params); }); + } else { + const id = this.createRandomId(); + + const params = { + query: + `tm.event='Tx' and ` + + Object.keys(query) + .map((key) => { + return { + key, + value: query[key], + }; + }) + .map((obj) => { + return `${obj.key}=${ + typeof obj.value === "string" ? `'${obj.value}'` : obj.value + }`; + }) + .join(" and "), + page: "1", + per_page: "1", + order_by: "desc", + }; + + return new Promise((resolve, reject) => { + this.txSubscribes.set(id, { + params, + resolver: resolve, + rejector: reject, + }); - this.sendSubscribeTxRpc(id, hash); - }); + this.sendSubscribeTxRpc(id, params); + }); + } } - protected sendSubscribeTxRpc(id: number, hash: Uint8Array): void { + protected sendSubscribeTxRpc( + id: number, + params: Record + ): void { if (this.readyState === WsReadyState.OPEN) { this.ws.send( JSON.stringify({ jsonrpc: "2.0", method: "subscribe", - params: [ - `tm.event='Tx' AND tx.hash='${Buffer.from(hash) - .toString("hex") - .toUpperCase()}'`, - ], + params: params, id, }) ); } } - queryTx(hash: Uint8Array): Promise { - return this.query("tx", [Buffer.from(hash).toString("base64"), false]); + queryTx( + query: Uint8Array | Record + ): Promise { + if (query instanceof Uint8Array) { + return this.query("tx", { + hash: Buffer.from(query).toString("base64"), + prove: false, + }); + } else { + const params = { + query: Object.keys(query) + .map((key) => { + return { + key, + value: query[key], + }; + }) + .map((obj) => { + return `${obj.key}=${ + typeof obj.value === "string" ? `'${obj.value}'` : obj.value + }`; + }) + .join(" and "), + page: "1", + per_page: "1", + order_by: "desc", + }; + + return this.query("tx_search", params); + } } - protected query(method: string, params: unknown[]): Promise { + protected query( + method: string, + params: Record + ): Promise { const id = this.createRandomId(); return new Promise((resolve, reject) => { @@ -285,7 +382,11 @@ export class TendermintTxTracer { }); } - protected sendQueryRpc(id: number, method: string, params: unknown[]) { + protected sendQueryRpc( + id: number, + method: string, + params: Record + ) { if (this.readyState === WsReadyState.OPEN) { this.ws.send( JSON.stringify({ diff --git a/packages/extension/src/hooks/use-ibc-channel-config-query-string.ts b/packages/extension/src/hooks/use-ibc-channel-config-query-string.ts index 971fbe2eac..c49b55899b 100644 --- a/packages/extension/src/hooks/use-ibc-channel-config-query-string.ts +++ b/packages/extension/src/hooks/use-ibc-channel-config-query-string.ts @@ -1,11 +1,12 @@ import { useEffectOnce } from "./use-effect-once"; import { useEffect } from "react"; import { useSearchParams } from "react-router-dom"; -import { IIBCChannelConfig } from "@keplr-wallet/hooks"; +import { Channel, IIBCChannelConfig } from "@keplr-wallet/hooks"; import { toJS } from "mobx"; export const useIBCChannelConfigQueryString = ( - channelConfig: IIBCChannelConfig + channelConfig: IIBCChannelConfig, + mounted?: (channels: Channel[] | undefined) => void ) => { const [searchParams, setSearchParams] = useSearchParams(); @@ -14,6 +15,14 @@ export const useIBCChannelConfigQueryString = ( if (initialIBCChannels) { const channels = JSON.parse(initialIBCChannels); channelConfig.setChannels(channels); + + if (mounted) { + mounted(channels); + } + } else { + if (mounted) { + mounted(undefined); + } } }); diff --git a/packages/extension/src/pages/ibc-transfer/index.tsx b/packages/extension/src/pages/ibc-transfer/index.tsx index 1485bf6e97..cb30560f0b 100644 --- a/packages/extension/src/pages/ibc-transfer/index.tsx +++ b/packages/extension/src/pages/ibc-transfer/index.tsx @@ -1,4 +1,4 @@ -import React, { FunctionComponent, useMemo, useState } from "react"; +import React, { FunctionComponent, useState } from "react"; import { observer } from "mobx-react-lite"; import { HeaderLayout } from "../../layouts/header"; import { useLocation, useSearchParams } from "react-router-dom"; @@ -102,20 +102,11 @@ export const IBCTransferPage: FunctionComponent = observer(() => { const isSelectChannelPhase = phase === "channel"; - const _isSelectChannelInteractionBlocked = useTxConfigsValidate({ + const isSelectChannelInteractionBlocked = useTxConfigsValidate({ recipientConfig: ibcTransferConfigs.recipientConfig, memoConfig: ibcTransferConfigs.memoConfig, + channelConfig: ibcTransferConfigs.channelConfig, }).interactionBlocked; - const isSelectChannelInteractionBlocked = useMemo(() => { - if (_isSelectChannelInteractionBlocked) { - return true; - } - - return ibcTransferConfigs.channelConfig.error != null; - }, [ - _isSelectChannelInteractionBlocked, - ibcTransferConfigs.channelConfig.error, - ]); useTxConfigsQueryString(chainId, { ...ibcTransferConfigs, diff --git a/packages/extension/src/pages/send/amount/ibc-transfer/destination-chain-view.tsx b/packages/extension/src/pages/send/amount/ibc-transfer/destination-chain-view.tsx new file mode 100644 index 0000000000..824f8f3543 --- /dev/null +++ b/packages/extension/src/pages/send/amount/ibc-transfer/destination-chain-view.tsx @@ -0,0 +1,105 @@ +import React, { FunctionComponent } from "react"; +import { Box } from "../../../../components/box"; +import { IIBCChannelConfig } from "@keplr-wallet/hooks"; +import { observer } from "mobx-react-lite"; +import { useStore } from "../../../../stores"; +import { Subtitle2 } from "../../../../components/typography"; +import { useTheme } from "styled-components"; +import { ColorPalette } from "../../../../styles"; +import { Label } from "../../../../components/input"; +import { XAxis } from "../../../../components/axis"; +import { ChainImageFallback } from "../../../../components/image"; +import { Gutter } from "../../../../components/gutter"; +import { ArrowRightIcon } from "../../../../components/icon"; + +export const DestinationChainView: FunctionComponent<{ + ibcChannelConfig: IIBCChannelConfig; + onClick: () => void; +}> = observer(({ ibcChannelConfig, onClick }) => { + const { chainStore } = useStore(); + + const theme = useTheme(); + + return ( + +