We are going to create a website to use our Faucet-API and to send coins between wallets, that will look something like this:
Requirements
nvm use v18.12.0 # only if you are using nvm to manage your node installation
cd /tmp
npx create-next-app@latest workshop --typescript
cd workshop
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
npm add evmosjs
npm add @hanchon/signature-to-pubkey
Bash
Add tailwind support
./tailwind.config.js, change the content variable
content: [
"./pages/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
"./public/**/*.html",
],
JSON
./styles/globals.css, replace the content
@tailwind base;
@tailwind components;
@tailwind utilities;
CSS
Faucet Component
Letβs create a component to connect to our API
Create the file: src/components/faucet.tsx
import { useEffect, useState } from "react";
import { addressConverter } from "evmosjs";
declare global {
interface Window {
ethereum?: any;
keplr?: any;
getOfflineSigner?: any;
}
}
export async function getKeplrAddress() {
try {
await window.keplr.enable("evmos_9001-2");
const offlineSigner = window.getOfflineSigner("evmos_9001-2");
const wallets = await offlineSigner.getAccounts();
return wallets[0].address;
} catch (e) {
return "";
}
}
export async function getMetamaskAddress() {
try {
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
return accounts[0];
} catch (e) {
return "";
}
}
export async function alertTxResult(response: any) {
if (response.tx_response.code !== 0) {
return alert(`Transaction Failed:${response.tx_response.raw_log}`);
} else {
return alert(`Transaction sent! ${response.tx_response.txhash}`);
}
}
export const buttonStyle = "border-green-300 border-2 rounded-lg p-2";
export default function Faucet() {
const [wallet, setWallet] = useState("");
const [faucetWalletEvmos, setFaucetWalletEvmos] = useState("");
const [faucetWalletEth, setFaucetWalletEth] = useState("");
const [balance, setBalance] = useState("");
useEffect(() => {
(async () => {
const res = await fetch("http://localhost:8080/address");
const values = await res.json();
setFaucetWalletEth(values.eth);
setFaucetWalletEvmos(values.evmos);
})();
(async () => {
const res = await fetch("http://localhost:8080/balance");
const values = await res.json();
setBalance(values.balance);
})();
});
return (
<div className="flex flex-col">
<div className="flex flex-col w-full text-center p-2">
<h1>Faucet Wallet:</h1>
<h2>{faucetWalletEvmos}</h2>
<h2>{faucetWalletEth}</h2>
<h1>Balance:</h1>
<h2>{balance}</h2>
</div>
<div className="flex flex-row space-x-7 py-5 mx-auto">
<button
className={buttonStyle}
onClick={async () => {
const tempWallet = await getMetamaskAddress();
setWallet(addressConverter.ethToEvmos(tempWallet));
}}
>
Get Address Metamask
</button>
<button
className={buttonStyle}
onClick={async () => {
const tempWallet = await getKeplrAddress();
setWallet(tempWallet);
}}
>
Get Address Keplr
</button>
</div>
<div className="flex flex-col w-full text-center">
<div className="text-lg">Selected Wallet</div>
<div>{wallet}</div>
</div>
<div className="flex justify-center py-2">
<button
className={buttonStyle}
onClick={async () => {
const res = await fetch(`http://localhost:8080/faucet/${wallet}`);
const response = await res.json();
alertTxResult(response);
}}
>
Request coins
</button>
</div>
</div>
);
}
TypeScript
Edit your file pages/index.tsx to look like this:
import Head from "next/head";
import Faucet from "../src/components/faucet";
import styles from "../styles/Home.module.css";
export default function Home() {
return (
<div className="bg-gray-700 text-white">
<Head>
<title>Wallet workshop</title>
<meta name="description" content="Evmosjs example" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<h1 className={styles.title}>Wallet workshop</h1>
<Faucet />
</main>
</div>
);
}
TypeScript
Message Send between wallets
Letβs create a new component to send coins using Metamask/Keplr and EvmosJS
Constants:
Create the file src/components/constants.ts:
export const MAINNET_CHAIN = {
chainId: 9001,
cosmosChainId: "evmos_9001-2",
};
export const MAINNET_FEE = {
amount: "3000000000000000",
denom: "aevmos",
gas: "150000",
};
export const ENDPOINT_URL = "https://rest.bd.evmos.org:1317";
TypeScript
Helpers:
Create the file src/components/chain.ts:
import { provider, transactions } from "evmosjs";
import { ENDPOINT_URL } from "./constants";
export async function getSender(address: string, pubkey: string) {
const walletInfoEndpoint = provider.generateEndpointAccount(address);
const res = await (
await fetch(`${ENDPOINT_URL}${walletInfoEndpoint}`)
).json();
const sender: transactions.Sender = {
accountAddress: address,
sequence: res.account.base_account.sequence,
accountNumber: res.account.base_account.account_number,
pubkey: pubkey,
};
return sender;
}
TypeScript
Metamask:
Create the file src/components/metamask.ts:
import { signatureToPubkey } from "@hanchon/signature-to-pubkey";
import { addressConverter, provider, transactions } from "evmosjs";
import { getSender } from "./chain";
import { MAINNET_CHAIN, ENDPOINT_URL } from "./constants";
import { getMetamaskAddress } from "./faucet";
export async function getMetamaskSender() {
const mmAddress = await getMetamaskAddress();
const address = addressConverter.ethToEvmos(mmAddress);
const signature = await window.ethereum.request({
method: "personal_sign",
params: [mmAddress, "generate_pubkey"],
});
const message = Buffer.from([
50, 215, 18, 245, 169, 63, 252, 16, 225, 169, 71, 95, 254, 165, 146, 216,
40, 162, 115, 78, 147, 125, 80, 182, 25, 69, 136, 250, 65, 200, 94, 178,
]);
const pubkey = signatureToPubkey(signature, message);
return getSender(address, pubkey);
}
export async function signAndBroadcastWithMetamask(
sender: transactions.Sender,
tx: transactions.TxGenerated
) {
const signature = await window.ethereum.request({
method: "eth_signTypedData_v4",
params: [
addressConverter.evmosToEth(sender.accountAddress),
JSON.stringify(tx.eipToSign),
],
});
const extension = transactions.signatureToWeb3Extension(
MAINNET_CHAIN,
sender,
signature
);
const txToBroadcast = transactions.createTxRawEIP712(
tx.legacyAmino.body,
tx.legacyAmino.authInfo,
extension
);
const postOptions = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: provider.generatePostBodyBroadcast(txToBroadcast),
};
let broadcastPost = await fetch(
`${ENDPOINT_URL}${provider.generateEndpointBroadcast()}`,
postOptions
);
return await broadcastPost.json();
}
TypeScript
Keplr
Create the src/components/keplr.ts file:
import { proto, provider, transactions } from "evmosjs";
import { getSender } from "./chain";
import { ENDPOINT_URL, MAINNET_CHAIN } from "./constants";
import { getKeplrAddress } from "./faucet";
export async function getKeplrSender() {
const address = await getKeplrAddress();
const offlineSigner = window.getOfflineSigner(MAINNET_CHAIN.cosmosChainId);
const wallets = await offlineSigner.getAccounts();
const pubkey = Buffer.from(wallets[0].pubkey).toString("base64");
return getSender(address, pubkey);
}
export async function signAndBroadcastKeplr(
sender: transactions.Sender,
tx: transactions.TxGenerated
) {
await window.keplr.enable(MAINNET_CHAIN.cosmosChainId);
const sign: {
signed: {
bodyBytes: Uint8Array;
authInfoBytes: Uint8Array;
};
signature: {
signature: string;
};
} = await window.keplr.signDirect(
MAINNET_CHAIN.cosmosChainId,
sender.accountAddress,
{
bodyBytes: tx.signDirect.body.serializeBinary(),
authInfoBytes: tx.signDirect.authInfo.serializeBinary(),
chainId: MAINNET_CHAIN.cosmosChainId,
accountNumber: sender.accountNumber,
},
{ isEthereum: true }
);
const txToBroadcast = proto.createTxRaw(
sign.signed.bodyBytes,
sign.signed.authInfoBytes,
[new Uint8Array(Buffer.from(sign.signature.signature, "base64"))]
);
const postOptions = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: provider.generatePostBodyBroadcast(txToBroadcast),
};
let broadcastPost = await fetch(
`${ENDPOINT_URL}${provider.generateEndpointBroadcast()}`,
postOptions
);
return await broadcastPost.json();
}
TypeScript
Transaction generation component:
Create the src/components/msgsend.ts file:
import { addressConverter, transactions } from "evmosjs";
import { useState } from "react";
import { alertTxResult, buttonStyle } from "./faucet";
import { getMetamaskSender, signAndBroadcastWithMetamask } from "./metamask";
import { MAINNET_CHAIN, MAINNET_FEE } from "./constants";
import { getKeplrSender, signAndBroadcastKeplr } from "./keplr";
export default function MsgSend() {
const [dest, setDest] = useState("");
const [amount, setAmount] = useState("1000000000000000");
const [sender, setSender] = useState<transactions.Sender>({
accountAddress: "",
accountNumber: 0,
sequence: 0,
pubkey: "",
});
const [walletProvider, setWalletProvider] = useState("");
async function createTransaction() {
if (dest === "" || amount === "" || sender.accountAddress === "") {
alert("Please select your wallet and set the inputs");
return;
}
let destInEvmosFormat = dest;
if (dest.startsWith("0x")) {
destInEvmosFormat = addressConverter.ethToEvmos(dest);
}
const params: transactions.MessageSendParams = {
destinationAddress: destInEvmosFormat,
amount: amount,
denom: "aevmos",
};
return transactions.createMessageSend(
MAINNET_CHAIN,
sender,
MAINNET_FEE,
"workshop transaction",
params
);
}
return (
<div className="w-full p-10 text-center">
<h1 className="text-lg pt-10">Message Send</h1>
<div className="flex flex-col text-center">
<span>Sender:</span>
<span>{sender.accountAddress}</span>
<span>{sender.sequence}</span>
</div>
<div className="flex flex-row space-x-2 justify-center mt-2">
<button
className={buttonStyle}
onClick={async () => {
setSender(await getMetamaskSender());
setWalletProvider("metamask");
}}
>
Use Metamask
</button>
<button
className={buttonStyle}
onClick={async () => {
setSender(await getKeplrSender());
setWalletProvider("keplr");
}}
>
Use Keplr
</button>
</div>
<form className="flex flex-col w-full">
<input
className="text-black w-full my-2 rounded-md p-2"
type="text"
placeholder="0x.../evmos1..."
value={dest}
onChange={(e) => {
setDest(e.target.value);
}}
/>
<div className="flex flex-row space-x-2 my-auto">
<input
className="text-black w-full rounded-md p-2"
type="text"
placeholder="10000"
value={amount}
onChange={(e) => {
setAmount(e.target.value);
}}
/>
<span className="py-2">aevmos</span>
</div>
<button
className={`${buttonStyle} mt-2`}
onClick={async (e) => {
e.preventDefault();
let tx = await createTransaction();
if (tx) {
if (walletProvider === "metamask") {
alertTxResult(await signAndBroadcastWithMetamask(sender, tx));
} else {
alertTxResult(await signAndBroadcastKeplr(sender, tx));
}
}
}}
>
Send Transaction
</button>
</form>
</div>
);
}
TypeScript