Giter Club home page Giter Club logo

cw-flash-ui-tutorial's Introduction

CosmWasm Flash Loan UI Tutorial

Installation

First, install Node.js, then install Yarn.

Then, clone this repo and set it up:

git clone https://github.com/NoahSaso/cw-flash-ui-tutorial.git
cd cw-flash-ui-tutorial

# Install packages
yarn

# Follow tutorial below and fill in missing queries and executions.

# Run development server
yarn dev

Tutorial

There are really just TWO CosmJS functions that you will use to talk to smart contracts when building a front end. (aside from the initial connect call).

queryContractSmart

docs
source

cosmWasmClient.queryContractSmart(
  address: string,
  queryMsg: Record<string, unknown>,
): Promise<JsonObject>

execute

docs
source

signingCosmWasmClient.execute(
  senderAddress: string,
  contractAddress: string,
  msg: Record<string, unknown>,
  fee: StdFee | "auto" | number,
  memo = "",
  funds?: readonly Coin[],
): Promise<ExecuteResult>

Setup clients

Queries (CosmWasmClient)

import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate'

// Connect to chain
const cosmWasmClient = await CosmWasmClient.connect(CHAIN_RPC_ENDPOINT)

Executions (SigningCosmWasmClient)

Manual

import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import { GasPrice } from '@cosmjs/stargate'
import { getKeplrFromWindow } from '@keplr-wallet/stores'

// Connect Keplr
const keplr = await getKeplrFromWindow()
await keplr.enable(CHAIN_ID)
const offlineSigner = await keplr.getOfflineSignerAuto(CHAIN_ID)

// Retrieve wallet address
const walletAddress = (await keplr.getKey(CHAIN_ID)).bech32Address

// Connect to chain
const signingCosmWasmClient = await SigningCosmWasmClient.connect(
  CHAIN_RPC_ENDPOINT,
  offlineSigner,
  { gasPrice: GasPrice.fromString('0.0025ujunox') }
)

Automatic

Managing wallet connection, supporting mobile WalletConnect, and setting up signing clients are patterns that repeat themselves in all Cosmos apps, so naturally there are libraries to handle all of this for you. I refactored cosmodal (one of those libraries) to be a bit more stable and contain additional common patterns. As of writing this, my version of cosmodal is also being refactored to be more adaptable and support more than just React apps. In the meantime, my version is a straightforward solution when using React, and we will use it in this tutorial going forward. There's really no good reason to set up and maintain wallet connection and signing clients yourself.

Installation instructions and an example can be found on cosmodal's README. It is also implemented in this repo already. Check out _app.tsx.

To access the signing client and wallet address, which are both necessary to use the execute function shown at the beginning, we can simply use the hooks provided by cosmodal in any component:

import { useWallet, useWalletManager } from '@noahsaso/cosmodal'

const Component: () => {
  const { connect, connected, disconnect } = useWalletManager()
  const { signingCosmWasmClient, address } = useWallet()

  const doSomething = () => {
    if (!signingCosmWasmClient || !address) {
      alert("Wallet not connected")
      return
    }

    signingCosmWasmClient.execute(
      address,
      "junoSomeContractAddress",
      { msg },
      'auto',
      ...
    )
  }

  return connected ? (
    <div>
      <button onClick={doSomething}>
        Do something!
      </button>

      <button onClick={disconnect}>
        Disconnect
      </button>
    </div>
  ) : (
    <button onClick={connect}>
      Connect
    </button>
  )
}

Example Usages

Query

Say we want to determine the current USDC price of the wallet's JUNO. From JunoSwap's JSON of its liquidity pools, we find that the JUNO-USDC swap smart contract address is juno1ctsmp54v79x7ea970zejlyws50cj9pkrmw49x46085fn80znjmpqz2n642.

The swap smart contract code can be found here, the specific query message variant we care about is here, and its response is here.

// Query
#[serde(rename_all = "snake_case")]
enum QueryMsg {
  ...
  Token1ForToken2Price {
    token1_amount: Uint128,
  },
  ...
}
// Response
struct Token1ForToken2PriceResponse {
  token2_amount: Uint128,
}

The query code becomes:

const response = await cosmWasmClient.queryContractSmart(
  JUNO_USDC_SWAP_ADDRESS,
  {
    token1_for_token2_price: {
      token1_amount: junoBalance,
    },
  }
)
const usdcBalance = response.token2_amount

Using these message and response types, and the clients and hooks we saw above, we can get the USDC value of the JUNO in the connected wallet. Here is a React component that implements the above query and displays the results:

ViewBalance Component
import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import { useWallet, useWalletManager } from '@noahsaso/cosmodal'
import { useEffect, useState } from 'react'

const CHAIN_RPC_ENDPOINT = 'https://rpc-juno.itastakers.com:443'
const JUNO_USDC_SWAP_ADDRESS =
  'juno1ctsmp54v79x7ea970zejlyws50cj9pkrmw49x46085fn80znjmpqz2n642'

const ViewBalance = () => {
  const { connect, connected, disconnect } = useWalletManager()
  // Retrieve wallet name and address from cosmodal. We don't need the signing
  // client since we're just querying a contract, and don't need to sign any
  // transactions just to ask for data.
  const { name, address } = useWallet()

  const [cosmWasmClient, setCosmWasmClient] = useState<
    CosmWasmClient | undefined
  >()
  // Connect to chain on component mount
  useEffect(() => {
    CosmWasmClient.connect(CHAIN_RPC_ENDPOINT).then((client) =>
      setCosmWasmClient(client)
    )
  }, [])

  const [junoBalance, setJunoBalance] = useState<string | undefined>()
  const [usdcBalance, setUsdcBalance] = useState<string | undefined>()
  useEffect(() => {
    // Cannot load data until these are loaded.
    if (!cosmWasmClient || !address) {
      return
    }

    const fetch = async () => {
      // Retrieve wallet balance of ujuno
      const ujunoBalance = (await cosmWasmClient.getBalance(address, 'ujuno'))
        .amount

      // Query the Juno-USDC swap smart contract for the USDC value.
      const response = await cosmWasmClient.queryContractSmart(
        JUNO_USDC_SWAP_ADDRESS,
        {
          token1_for_token2_price: {
            token1_amount: ujunoBalance,
          },
        }
      )

      // ujuno is the smallest denomination of juno, i.e. 1 ujuno = 0.000001
      // juno (or ujuno = juno * 1e6)
      // Since the contract expects an integer and ujuno has no decimals, we use
      // it as the input, and convert the output by shifting it 6 decimal places
      // only after performing the price conversion. `response.token2_amount`
      // has units of USDC * 1e6 since the input `token1_amount`/`ujunoBalance`
      // has units of JUNO * 1e6. Thus, to get USDC, we divide output by 1e6.
      const usdcBalance = Number(response.token2_amount) / 1e6

      const junoBalance = Number(ujunoBalance) / 1e6
      setJunoBalance(junoBalance.toFixed(6))
      setUsdcBalance(usdcBalance.toFixed(6))
    }

    fetch().catch(console.error)
  }, [cosmWasmClient, address])

  return connected ? (
    <div>
      {junoBalance && usdcBalance ? (
        <p>
          Your wallet named "{name}" contains {junoBalance} JUNO, currently
          worth {usdcBalance} USDC.
        </p>
      ) : (
        <p>Loading...</p>
      )}

      <br />

      <button onClick={disconnect}>Disconnect</button>
    </div>
  ) : (
    <button onClick={connect}>Connect</button>
  )
}

A really important concept to keep in mind about this example is managing the currency decimals. For native tokens, generally speaking, the denominations we are familiar with (e.g. JUNO, ATOM, STARS) are just conveniences for our human brains. In reality, they all exist as integers with u-prefixed denoms, such as ujuno, uatom, and ustars. The number of decimals a token has actually corresponds to the conversion between its true micro denomination, represented in smart contracts and the blockchain itself, and the larger denomination we expect to see. The majority of tokens in the Cosmos use 6 decimals.

Other things worth noting:

  • Uint128 is represented in JSON as a string, not a number.
  • The #[serde(rename_all = "snake_case")] line above the QueryMsg enum means that Token1ForToken2Price becomes snake_case'd into token1_for_token2_price for the JSON-serialized msg that we construct.

Execute

Now say we want to swap 10% of a wallet's balance for USDC. We use the same swap smart contract address from the Query section above to perform the swap.

The swap smart contract code can be found here and the specific execute message variant we care about is here.

enum TokenSelect {
  Token1,
  Token2,
}

// Execute
#[serde(rename_all = "snake_case")]
enum ExecuteMsg {
  ...
  Swap {
      input_token: TokenSelect,
      input_amount: Uint128,
      min_output: Uint128,
      expiration: Option<Expiration>,
  },
  ...
}

The execute code becomes:

const response = await signingCosmWasmClient.execute(
  walletAddress,
  JUNO_USDC_SWAP_ADDRESS,
  {
    swap: {
      input_token: 'Token1',
      input_amount: ujunoInput,
      min_output: minUsdcOutput,
    },
  },
  'auto',
  undefined,
  coins(ujunoInput, 'ujuno')
)

Using these message and response types, and the queries from the last example, we can add just one execute call to make the swap. Here is a React component that implements the above execution to let you make a swap:

Swap Component
import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import { coins } from '@cosmjs/stargate'
import { useWallet, useWalletManager } from '@noahsaso/cosmodal'
import { useCallback, useEffect, useState } from 'react'

const CHAIN_RPC_ENDPOINT = 'https://rpc-juno.itastakers.com:443'
const JUNO_USDC_SWAP_ADDRESS =
  'juno1ctsmp54v79x7ea970zejlyws50cj9pkrmw49x46085fn80znjmpqz2n642'

const Swap = () => {
  const { connect, connected, disconnect } = useWalletManager()
  // Retrieve wallet name, address, and signing client, since we need to perform
  // an execution and need to sign a transaction.
  const { name, address, signingCosmWasmClient } = useWallet()

  const [cosmWasmClient, setCosmWasmClient] = useState<
    CosmWasmClient | undefined
  >()
  // Connect to chain on Component mount
  useEffect(() => {
    CosmWasmClient.connect(CHAIN_RPC_ENDPOINT).then((client) =>
      setCosmWasmClient(client)
    )
  }, [])

  // Current balances
  const [junoBalance, setJunoBalance] = useState<string | undefined>()
  const [usdcBalance, setUsdcBalance] = useState<string | undefined>()
  // Swap balances
  const [junoBalanceToSwap, setJunoBalanceToSwap] = useState<
    number | undefined
  >()
  const [minUsdcBalanceOutput, setMinUsdcBalanceOutput] = useState<
    number | undefined
  >()

  // Swap transaction response info.
  const [swapTx, setSwapTx] = useState<string | undefined>()
  const [swapError, setSwapError] = useState<string | undefined>()

  // Retrieve current balances.
  useEffect(() => {
    // Cannot load data until these are loaded.
    if (!cosmWasmClient || !address) {
      return
    }

    const fetch = async () => {
      setJunoBalance(undefined)
      setUsdcBalance(undefined)
      setJunoBalanceToSwap(undefined)
      setMinUsdcBalanceOutput(undefined)

      const ujunoBalance = (await cosmWasmClient.getBalance(address, 'ujuno'))
        .amount

      const response = await cosmWasmClient.queryContractSmart(
        JUNO_USDC_SWAP_ADDRESS,
        {
          token1_for_token2_price: {
            token1_amount: ujunoBalance,
          },
        }
      )

      const usdcBalance = Number(response.token2_amount) / 1e6
      const junoBalance = Number(ujunoBalance) / 1e6

      setJunoBalance(junoBalance.toFixed(6))
      setUsdcBalance(usdcBalance.toFixed(6))

      // Calculate balances for 10% swap.
      setJunoBalanceToSwap(Number((junoBalance * 0.1).toFixed(6)))
      // Allow for 1% slippage in price since we checked the swap price.
      setMinUsdcBalanceOutput(Number((usdcBalance * 0.1 * 0.99).toFixed(6)))
    }

    fetch().catch(console.error)
  }, [
    cosmWasmClient,
    address,
    // Refresh balances when a swap transaction succeeds.
    swapTx,
  ])

  const swapTenPercent = useCallback(async () => {
    // Cannot swap until these are loaded.
    if (
      !signingCosmWasmClient ||
      !address ||
      !junoBalanceToSwap ||
      !minUsdcBalanceOutput
    ) {
      return
    }

    setSwapError(undefined)

    // Convert to micro denominations and cut off all decimals.
    const ujunoBalanceToSwap = Math.floor(junoBalanceToSwap * 1e6).toFixed(0)
    const minMicroUsdcBalanceOutput = Math.floor(
      minUsdcBalanceOutput * 1e6
    ).toFixed(0)

    try {
      // Execute the swap message on the Juno-USDC swap smart contract.
      // TokenSelect is "Token1" or "Token2".
      // Option types can be omitted or set to null.
      const response = await signingCosmWasmClient.execute(
        address,
        JUNO_USDC_SWAP_ADDRESS,
        {
          swap: {
            input_token: 'Token1',
            input_amount: ujunoBalanceToSwap,
            min_output: minMicroUsdcBalanceOutput,
          },
        },
        'auto',
        undefined,
        coins(ujunoBalanceToSwap, 'ujuno')
      )

      // Log response to console so we can inspect its data.
      console.log(response)
      setSwapTx(response.transactionHash)
    } catch (error) {
      // Log error to console and update state so we display it.
      console.error(error)
      setSwapError(error instanceof Error ? error.message : `${error}`)
    }
  }, [signingCosmWasmClient, address, junoBalanceToSwap, minUsdcBalanceOutput])

  return connected ? (
    <div>
      {junoBalance && usdcBalance ? (
        <>
          <p>
            Your wallet named "{name}" contains {junoBalance} JUNO, currently
            worth {usdcBalance} USDC.
          </p>

          <br />

          {junoBalanceToSwap !== undefined &&
            minUsdcBalanceOutput !== undefined && (
              <>
                <button onClick={swapTenPercent}>
                  Swap 10% ({junoBalanceToSwap?.toFixed(6)} JUNO) for at least{' '}
                  {minUsdcBalanceOutput?.toFixed(6)} USDC
                </button>
                <br />

                {!!swapTx && (
                  <p>
                    Swap succeeded with transaction hash <b>{swapTx}</b>
                  </p>
                )}
                {!!swapError && (
                  <>
                    <p>Swap failed with error:</p>
                    <pre>{swapError}</pre>
                  </>
                )}
              </>
            )}
        </>
      ) : (
        <p>Loading...</p>
      )}

      <br />
      <button onClick={disconnect}>Disconnect</button>
    </div>
  ) : (
    <button onClick={connect}>Connect</button>
  )
}

Some things worth noting:

  • @cosmjs/stargate has coin and coins (source) helper methods to facilitate sending funds in the proper format and keep your code readable.
  • We could omit expiration from the message since it is an Option Rust type. This is equivalent to setting expiration: null in the message.
  • It is very important to keep track of currency denominations being used. Contracts can only accept micro denominations: ujuno, uatom, etc. because those are the only currencies that actually exist in the blockchain. Pay very close attention to decimals, always.
  • Uint128 is represented in JSON as a string, not a number.
  • The #[serde(rename_all = "snake_case")] line above the ExecuteMsg enum means that Swap becomes snake_case'd into swap for the JSON-serialized msg that we construct.

Exercises

For all exercises, assume CONTRACT_ADDR is a globally-defined constant that refers to the flash loan smart contract's address.

(1) selectors/contract.ts line 28

We want to get the fee from the flash loan smart contract config to inform the user what fee must be applied to their loan. If they borrow 100 tokens, and the fee is 0.01 (1%), they must return 101 tokens for the borrow to succeed.

// TODO: Get the fee from CONTRACT_ADDR's QueryMsg::GetConfig response

Query msg:

#[serde(rename_all = "snake_case")]
enum QueryMsg {
  ...
  GetConfig {},
  ...
}

Query response:

pub struct ConfigResponse {
  pub admin: Option<String>,
  pub fee: Decimal,
  pub loan_denom: CheckedLoanDenom,
}

Write the query in JS!

Solution
const config = await client.queryContractSmart(CONTRACT_ADDR, {
  get_config: {},
})
const fee = config.fee

(2) selectors/contract.ts line 48

Now we want to inform the user how much their wallet has already provided to the flash loan smart contract. Assume we know the wallet address (walletAddress) since we set it up earlier.

// TODO: Get CONTRACT_ADDR's QueryMsg::Provided response

Query msg:

#[serde(rename_all = "snake_case")]
enum QueryMsg {
  ...
  Provided { address: String },
  ...
}

This query doesn't have a custom struct response, so we must check the contract.rs code, find the function, and see what it returns.

Query function:

pub fn query_provided(deps: Deps, address: String) -> StdResult<Binary> {
  let address = deps.api.addr_validate(&address)?;
  let provided = PROVISIONS
    .may_load(deps.storage, address)
    .unwrap_or_default();

  match provided {
    Some(provided) => to_binary(&provided),
    None => to_binary(&Uint128::zero()),
  }
}

It seems like we need to know what PROVISIONS is. PROVISIONS is a piece of state, found in state.rs, that maps addresses to Uint128s.

pub const PROVISIONS: Map<Addr, Uint128> = Map::new("provision");

Now we know that the query function returns a Uint128 directly!

Write the query in JS!

Solution
const walletAddress = 'junoWallet'

const provided = await client.queryContractSmart(CONTRACT_ADDR, {
  provided: { address: walletAddress },
})

(3) pages/index.tsx line 90

Let's execute a loan! Assume we know the wallet address (walletAddress), receiving smart contract's address (receiverAddress), and the amount in JUNO (junoAmount) we want to borrow.

const walletAddress = 'junoWallet'
const receiverAddess = 'junoReceivingSmartContract'
const junoAmount = 100

// TODO: Execute CONTRACT_ADDR's ExecuteMsg::Loan action

Execute msg:

#[serde(rename_all = "snake_case")]
enum ExecuteMsg {
  ...
  Loan { receiver: String, amount: Uint128 },
  ...
}

Write the query in JS!

Solution
const walletAddress = 'junoWallet'
const receiverAddess = 'junoReceivingSmartContract'
const junoAmount = 100

const execution = client.execute(
  walletAddress,
  CONTRACT_ADDR,
  {
    loan: {
      receiver: receiverAddess,
      amount: Math.floor(junoAmount * Math.pow(10, 6)).toString(),
    },
  },
  'auto'
)

(4) pages/provide.tsx line 114

Let's provide the flash loan smart contract with some JUNO so it can make loans. Assume we know the wallet address (walletAddress) and want to provide 1000 JUNO (junoAmount).

const walletAddress = 'junoWallet'
const junoAmount = 1000

// TODO: Execute CONTRACT_ADDR's ExecuteMsg::Provide action

Execute msg:

#[serde(rename_all = "snake_case")]
enum ExecuteMsg {
  ...
  Provide {},
  ...
}

Write the query in JS!

(Try using the coins function we saw before.)

Solution
import { coins } from '@cosmjs/stargate'

const walletAddress = 'junoWallet'
const junoAmount = 1000

const execution = client.execute(
  walletAddress,
  CONTRACT_ADDR,
  {
    provide: {},
  },
  'auto',
  undefined,
  coins(Math.floor(junoAmount * Math.pow(10, 6)).toString(), 'ujuno')
)

(5) pages/provide.tsx line 150

Now the market is down and we need to pay rent because we live in late-stage capitalism and life is hard. Let's withdraw our provided JUNO from the flash loan smart contract so we can survive. Assume we know the wallet address (walletAddress).

const walletAddress = 'junoWallet'

// TODO: Execute CONTRACT_ADDR's ExecuteMsg::Withdraw action

Execute msg:

#[serde(rename_all = "snake_case")]
enum ExecuteMsg {
  ...
  Withdraw {},
  ...
}

Write the query in JS!

Solution
const walletAddress = 'junoWallet'

const execution = client.execute(
  walletAddress,
  CONTRACT_ADDR,
  {
    withdraw: {},
  },
  'auto'
)

CONGRATULATIONS

You just wrote a frontend that knows how to talk to smart contracts running on the blockchain and access value stored in a wallet. Now go off and make the world a better place :) Please....

cw-flash-ui-tutorial's People

Contributors

0xekez avatar noahsaso avatar

Stargazers

 avatar  avatar  avatar

Forkers

bmorphism

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.