diff --git a/package.json b/package.json index 7b5cc21..43a563d 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "@babel/core": "^7.0.0", + "@cosmjs/cosmwasm-stargate": "^0.29.4", "@cosmjs/stargate": "0.26.5", "@emotion/react": "11.6.0", "@emotion/styled": "11.6.0", diff --git a/public/seasy.png b/public/seasy.png new file mode 100644 index 0000000..a65ce24 Binary files /dev/null and b/public/seasy.png differ diff --git a/src/App.tsx b/src/App.tsx index 65cf1d7..8b2daa4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useState } from "react"; import ReactDOM from "react-dom"; import { Breakpoint, BreakpointProvider } from "react-socks"; import { SecretNetworkClient } from "secretjs"; -import { chains, tokens, snips } from "./config"; +import { chains, tokens, snips, cw20s } from "./config"; import { faucetURL } from "./commons"; import "./index.css"; import { KeplrPanel } from "./KeplrStuff"; @@ -110,7 +110,7 @@ export default function App() { const denoms = Array.from( new Set( - tokens.map((t) => t.withdrawals.map((w) => w.from_denom)).flat() + (tokens.concat(cw20s)).map((t) => t.withdrawals.map((w) => w.from_denom)).flat(), ) ); @@ -298,6 +298,29 @@ export default function App() { /> ))} + + + CW20s via IBC from JUNO + + {cw20s.map((t) => ( + + + + ))} (""); const [availableBalance, setAvailableBalance] = useState(""); const [loadingTx, setLoading] = useState(false); - const [sourceCosmJs, setSourceCosmJs] = - useState(null); + const sourceCosmJsRef = useRef< + SigningCosmWasmClient | SigningStargateClient | null>(null); const [selectedChainIndex, setSelectedChainIndex] = useState(0); const [fetchBalanceInterval, setFetchBalanceInterval] = useState(null); const inputRef = useRef(); @@ -67,18 +68,32 @@ export default function Deposit({ chains[token.deposits[selectedChainIndex].source_chain_name].lcd }/cosmos/bank/v1beta1/balances/${sourceAddress}`; try { - const { - balances, - }: { - balances: Array<{ denom: string; amount: string }>; - } = await (await fetch(url)).json(); - - const balance = - balances.find( - (c) => c.denom === token.deposits[selectedChainIndex].from_denom - )?.amount || "0"; - - setAvailableBalance(balance); + if ((token.deposits[selectedChainIndex].from_denom).startsWith('juno1')) { + if (!sourceCosmJsRef.current) { + return; + } + + const result = await (sourceCosmJsRef.current as SigningCosmWasmClient).queryContractSmart( + token.deposits[selectedChainIndex].from_denom, + { + balance: { address: sourceAddress }, + } + ); + setAvailableBalance(result.balance); + } else { + const { + balances, + }: { + balances: Array<{ denom: string; amount: string }>; + } = await (await fetch(url)).json(); + + const balance = + balances.find( + (c) => c.denom === token.deposits[selectedChainIndex].from_denom + )?.amount || "0"; + + setAvailableBalance(balance); + } } catch (e) { console.error(`Error while trying to query ${url}:`, e); setAvailableBalance("Error"); @@ -99,7 +114,7 @@ export default function Deposit({ fetchSourceBalance(sourceAddress); const interval = setInterval( () => fetchSourceBalance(sourceAddress), - 10_000 + 2_000 ); setFetchBalanceInterval(interval); @@ -135,12 +150,22 @@ export default function Deposit({ const sourceOfflineSigner = window.getOfflineSignerOnlyAmino(chain_id); const depositFromAccounts = await sourceOfflineSigner.getAccounts(); setSourceAddress(depositFromAccounts[0].address); - const cosmjs = await SigningStargateClient.connectWithSigner( - rpc, - sourceOfflineSigner, - { prefix: bech32_prefix, broadcastPollIntervalMs: 10_000 } - ); - setSourceCosmJs(cosmjs); + + if ((token.deposits[selectedChainIndex].from_denom).startsWith('juno1')) { + const cosmjs = await SigningCosmWasmClient.connectWithSigner( + rpc, + sourceOfflineSigner, + { prefix: bech32_prefix, broadcastPollIntervalMs: 10_000 } + ); + sourceCosmJsRef.current = cosmjs; + } else { + const cosmjs = await SigningStargateClient.connectWithSigner( + rpc, + sourceOfflineSigner, + { prefix: bech32_prefix, broadcastPollIntervalMs: 10_000 } + ); + sourceCosmJsRef.current = cosmjs; + } })(); }, [selectedChainIndex]); @@ -343,7 +368,7 @@ export default function Deposit({ }} loading={loadingTx} onClick={async () => { - if (!sourceCosmJs) { + if (!sourceCosmJsRef.current) { console.error("No cosmjs"); return; } @@ -398,20 +423,45 @@ export default function Deposit({ ) ) { // Regular cosmos chain (not ethermint signing) - const txResponse = await sourceCosmJs.sendIbcTokens( - sourceAddress, - secretAddress, - { - amount, - denom: token.deposits[selectedChainIndex].from_denom, - }, - "transfer", - deposit_channel_id, - undefined, - Math.floor(Date.now() / 1000) + 10 * 60, // 10 minute timeout (sec) - gasToFee(deposit_gas) - ); - transactionHash = txResponse.transactionHash; + if ((token.deposits[selectedChainIndex].from_denom).startsWith('juno1')) { + if (!sourceCosmJsRef.current) { + return; + } + + const txResponse = await (sourceCosmJsRef.current as SigningCosmWasmClient).execute( + sourceAddress, + token.deposits[selectedChainIndex].from_denom, + { + send: { + contract: "juno105pfttgc76dcrw44jl5067we23q7he85k5893aaaek09rdz0srlqydutp0", // ics20 on Juno (Juno<>Scrt) + amount: amount, + msg: Buffer.from(JSON.stringify({ + channel: deposit_channel_id, + remote_address: secretAddress, + // 15 min + timeout: 900, + })).toString("base64"), + }, + }, + gasToFee(deposit_gas) + ); + transactionHash = txResponse.transactionHash; + } else { + const txResponse = await (sourceCosmJsRef.current as SigningStargateClient).sendIbcTokens( + sourceAddress, + secretAddress, + { + amount, + denom: token.deposits[selectedChainIndex].from_denom, + }, + "transfer", + deposit_channel_id, + undefined, + Math.floor(Date.now() / 1000) + 10 * 60, // 10 minute timeout (sec) + gasToFee(deposit_gas) + ); + transactionHash = txResponse.transactionHash; + } } else { // Handle IBC transfers from Ethermint chains like Evmos & Injective @@ -513,7 +563,7 @@ export default function Deposit({ // cosmjs can broadcast to Ethermint but cannot handle the response // Broadcast the tx to Evmos - sourceCosmJs.broadcastTx(txBytes); + sourceCosmJsRef.current.broadcastTx(txBytes); transactionHash = toHex(sha256(txBytes)); } diff --git a/src/config.ts b/src/config.ts index fe5b6cf..0a6e3da 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,6 +3,8 @@ export type Token = { name: string; /** a snip20 token that's originated from Secret Network */ is_snip20?: boolean; + /** a cw20 token that's originated from Juno Network */ + is_cw20?: boolean; /** secret contract address of the token */ address: string; /** secret contract code hash of the token */ @@ -798,6 +800,38 @@ export const snips: Token[] = [ } ]; +// These are cw20 tokens that are IBC compatible originating from Juno +export const cw20s: Token[] = [ + { + name: "SEASY", + is_cw20: true, + address: "secret17gg8xcx04ldqkvkrd7r9w60rdae4ck8aslt9cf", // TODO: update this + code_hash: + "5a085bd8ed89de92b35134ddd12505a602c7759ea25fb5c089ba03c8535b3042", // TODO: update this + image: "/seasy.png", + decimals: 6, + coingecko_id: "seasy", + deposits: [ + { + source_chain_name: "Juno", + from_denom: + "juno19rqljkh95gh40s7qdx40ksx3zq5tm4qsmsrdz9smw668x9zdr3lqtg33mf", // SEASY cw20 address on Juno + channel_id: "channel-161", + gas: 350_000, + } + ], + withdrawals: [ + { + target_chain_name: "Juno", + from_denom: + "ibc/70FF6A895E39F2EF8C679684215830B138B97222C615DADB8D8B0EDF9A27966F", // SEASY IBC asset denom on Secret + channel_id: "channel-40", + gas: 130_000, + }, + ], + }, +]; + export type Chain = { /** display name of the chain */ chain_name: string;