diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8b77bfc --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +NEXT_PUBLIC_WL_MINT_SOLO=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +NEXT_PUBLIC_WL_MINT_CREW=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +NEXT_PUBLIC_WL_MINT_SQUAD=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +NEXT_PUBLIC_WL_MINT_CLAN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +NEXT_PUBLIC_WL_MINT_LEGION=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +NEXT_PUBLIC_WL_MINT_EMPIRE=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +NEXT_PUBLIC_SOLANA_NETWORK=devnet +NEXT_PUBLIC_SOLANA_RPC_HOST=https://api.devnet.solana.com/ + +NEXT_PUBLIC_SIGNER=[50,182,175,213] diff --git a/.github/workflows/deploy_preview.yaml b/.github/workflows/deploy_preview.yaml new file mode 100644 index 0000000..e0c23e0 --- /dev/null +++ b/.github/workflows/deploy_preview.yaml @@ -0,0 +1,19 @@ +name: Deploy preview +on: + pull_request: + branches: [dev] +jobs: + deploy: + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[skip ci]')" + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Deploy to Vercel Action + uses: BetaHuhn/deploy-to-vercel-action@v1 + with: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + PRODUCTION: false # Don't deploy to production environment diff --git a/.github/workflows/deploy_prod.yaml b/.github/workflows/deploy_prod.yaml new file mode 100644 index 0000000..cccd7da --- /dev/null +++ b/.github/workflows/deploy_prod.yaml @@ -0,0 +1,20 @@ +name: Deploy to production +on: + push: + branches: + - dev +jobs: + deploy: + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[skip ci]')" + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Deploy to Vercel Action + uses: BetaHuhn/deploy-to-vercel-action@v1 + with: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + PRODUCTION: true diff --git a/.gitignore b/.gitignore index 737d872..7c52653 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ yarn-error.log* # typescript *.tsbuildinfo +.env diff --git a/.vercelignore b/.vercelignore new file mode 100644 index 0000000..e6d5d28 --- /dev/null +++ b/.vercelignore @@ -0,0 +1,6 @@ +.env +.vercel +build +node_modules +.prettierrc +.gitignore \ No newline at end of file diff --git a/components/Layout/FontanaSVG.tsx b/components/Layout/FontanaSVG.tsx new file mode 100644 index 0000000..11226ed --- /dev/null +++ b/components/Layout/FontanaSVG.tsx @@ -0,0 +1,17 @@ +const FontanaSVG: React.FC<{ width: number }> = ({ width }) => { + return ( + + + + ); +}; + +export default FontanaSVG; diff --git a/components/Layout/Header.tsx b/components/Layout/Header.tsx new file mode 100644 index 0000000..1e56765 --- /dev/null +++ b/components/Layout/Header.tsx @@ -0,0 +1,28 @@ +import { Box, Text } from "@primer/react"; +import { FontanaSVG } from "components/Layout"; + +const Header: React.FC = () => { + return ( + + + + Fontana - The Solana SPL multi-token generic faucet + + + ); +}; + +export default Header; diff --git a/components/Layout/Wallet.tsx b/components/Layout/Wallet.tsx new file mode 100644 index 0000000..1f897a3 --- /dev/null +++ b/components/Layout/Wallet.tsx @@ -0,0 +1,25 @@ +import { Box } from "@primer/react"; +import { WalletMultiButton } from "@solana/wallet-adapter-react-ui"; +import { FC } from "react"; + +const Wallet: FC = () => { + return ( + + + + ); +}; + +export default Wallet; diff --git a/components/Layout/index.ts b/components/Layout/index.ts new file mode 100644 index 0000000..31acfcc --- /dev/null +++ b/components/Layout/index.ts @@ -0,0 +1,3 @@ +export { default as Wallet } from "./Wallet"; +export { default as Header } from "./Header"; +export { default as FontanaSVG } from "./FontanaSVG"; diff --git a/components/Table/HeaderTable.tsx b/components/Table/HeaderTable.tsx new file mode 100644 index 0000000..b4910e8 --- /dev/null +++ b/components/Table/HeaderTable.tsx @@ -0,0 +1,88 @@ +import { Header, Text, Button, Box, StyledOcticon } from "@primer/react"; +import { CheckIcon, SyncIcon } from "@primer/octicons-react"; +import { useRefresh } from "./Table"; + +const HeaderTable: React.FC<{ tokensAmount: number }> = ({ + tokensAmount = 0, +}) => { + const { r, refresh } = useRefresh(); + + function triggerRefresh() { + refresh(!r); + } + return ( +
+ + + + + {tokensAmount} SPL Tokens available + + + + Available + + + In wallet + + + + + +
+ ); +}; + +export default HeaderTable; diff --git a/components/Table/Row.tsx b/components/Table/Row.tsx new file mode 100644 index 0000000..5e75be5 --- /dev/null +++ b/components/Table/Row.tsx @@ -0,0 +1,347 @@ +import { + Header, + Text, + Button, + Box, + StyledOcticon, + TextInput, + Flash, +} from "@primer/react"; +import { + IssueOpenedIcon, + HourglassIcon, + CheckIcon, +} from "@primer/octicons-react"; +import { useConnection, useWallet } from "@solana/wallet-adapter-react"; +import { useCallback, useEffect, useState, useRef } from "react"; +import { RpcMethods } from "lib/spl"; +import { useRefresh } from "./Table"; +import { useHandleDestroyAnimated } from "hooks"; +enum Actions { + Mint, + Sending, +} +const Row: React.FC<{ + tokenName: string; + tokenOwner: string; + tokenKeypair: string; + tokenTicker?: string; +}> = ({ tokenName, tokenOwner, tokenKeypair, tokenTicker }) => { + const { connection } = useConnection(); + const { publicKey } = useWallet(); + const [mintedAmount, setMintedAmount] = useState(null); + const [walletAmount, setWalletAmount] = useState(null); + const [action, setAction] = useState(null); + const [mintAmount, setMintAmount] = useState(1); + const [transferAmount, setTransferAmount] = useState(1); + const [destinationAddress, setDestinationAddress] = useState(""); + const [mintError, setMintError] = useState(null); + const [sendError, setSendError] = useState(null); + const { r } = useRefresh(); + const flashRef = useRef(null); + + const [sendSuccess, setSendSuccess] = useHandleDestroyAnimated(flashRef); + function setWalletAddress() { + if (!publicKey) return; + setDestinationAddress(publicKey?.toBase58()); + } + const getTokenBalance = useCallback(async () => { + try { + const rpc = new RpcMethods(connection); + const amount = await rpc.getTokenBalance(tokenOwner, tokenName); + setMintedAmount(amount); + if (!publicKey) return; + const walletAmount = await rpc.getTokenBalance( + publicKey.toBase58(), + tokenName + ); + setWalletAmount(walletAmount); + } catch (e) { + console.error(e); + } + }, [connection, publicKey, tokenName, tokenOwner]); + + useEffect(() => { + getTokenBalance(); + }, [getTokenBalance, r]); + + async function mintTokens() { + setMintError(null); + setAction(Actions.Mint); + if (mintAmount === 0) { + setAction(null); + return; + } + try { + const res = await fetch("api/mint/", { + method: "POST", + body: JSON.stringify({ + owner: tokenOwner, + token: tokenName, + keypair: tokenKeypair, + amount: mintAmount, + }), + }); + const data = await res.json(); + if ("err" in data) { + setMintError(data.err); + } else { + setSendSuccess(true); + } + } catch (err) { + console.error(err); + } + setAction(null); + getTokenBalance(); + } + async function transferTokens() { + setSendError(null); + setAction(Actions.Sending); + if (transferAmount === 0) { + setAction(null); + return; + } + try { + const res = await fetch("api/transfer/", { + method: "POST", + body: JSON.stringify({ + owner: tokenOwner, + token: tokenName, + keypair: tokenKeypair, + amount: transferAmount, + recipient: destinationAddress, + }), + }); + const data = await res.json(); + if ("err" in data) { + setSendError(data.err); + } else { + setSendSuccess(true); + } + } catch (err) { + console.error(err); + } + setAction(null); + getTokenBalance(); + } + const issueColor = () => { + if (mintedAmount === null) return "primary"; + if (mintedAmount > 0) { + return "green"; + } else { + return "red"; + } + }; + + return ( + <> +
+ + + + + + Token name{" "} + {tokenTicker && ( + + [{tokenTicker}] + + )} + + + + {tokenName} + + + + + {mintedAmount ?? "-"} + + + {walletAmount ?? "-"} + + + setMintAmount(parseInt(e.target.value))} + sx={{ + border: "1px solid #ccc", + }} + /> + + {mintError} + + + + + setTransferAmount(parseInt(e.target.value))} + sx={{ + border: "1px solid #ccc", + }} + /> + + {sendError} + + + setDestinationAddress(e.target.value)} + sx={{ + border: "1px solid #ccc", + }} + /> + {publicKey && ( + + )} + + + + +
+ {sendSuccess && ( +
+ + + Success! + +
+ )} + + ); +}; + +export default Row; diff --git a/components/Table/Table.tsx b/components/Table/Table.tsx new file mode 100644 index 0000000..457f3a4 --- /dev/null +++ b/components/Table/Table.tsx @@ -0,0 +1,63 @@ +import { Box } from "@primer/react"; +import { useState, useMemo, createContext, useContext } from "react"; +import Row from "./Row"; +import HeaderTable from "./HeaderTable"; +import fontanaConfig from "../../fontana.config"; + +type Refresh = { + r: boolean; + refresh: (r: boolean) => void; +}; + +const SiteMintingContext = createContext({ + r: false, + refresh: () => {}, +}); + +export const useRefresh = () => useContext(SiteMintingContext); + +const Table: React.FC = () => { + const [r, refresh] = useState(false); + const tokens = useMemo(() => { + return fontanaConfig.map((token) => { + return { + keypair: token.keypair, + token: token.token, + owner: token.owner, + ticker: token.ticker, + }; + }); + }, []); + + return ( + + {" "} + + + {tokens.map((token, i) => { + return ( + + ); + })} + + + ); +}; + +export default Table; diff --git a/components/Table/index.ts b/components/Table/index.ts new file mode 100644 index 0000000..03e96c6 --- /dev/null +++ b/components/Table/index.ts @@ -0,0 +1,3 @@ +export { default as HeaderTable } from "./HeaderTable"; +export { default as Table } from "./Table"; +export { default as Row } from "./Row"; diff --git a/fontana.config.ts b/fontana.config.ts new file mode 100644 index 0000000..dc6f688 --- /dev/null +++ b/fontana.config.ts @@ -0,0 +1,33 @@ +interface Config { + keypair: string; + token: string; + owner: string; + ticker?: string; +} + +/** + * This config is used to show the tokens to be minted and sent. Add as many as you wish. The ticker is optional. + * The keypair is the public key of the account that will mint the tokens. It should match the keypair on the env. + * If the keypair in the config file is WALLET_1, in the .env file it should be NEXT_PUBLIC_WALLET_1. + */ +const config: Config[] = [ + { + keypair: "WALLET_1", + owner: "BoX451MZzydoVdZE4NFfmMT3J5Ztqo7YgUNbwwMfjPFu", + token: "Gqv2ULNwn7DpU2FRfDwagwNifX4WKPaduah43d5xJGU9", + ticker: "test", + }, + { + keypair: "WALLET_2", + owner: "BoX451MZzydoVdZE4NFfmMT3J5Ztqo7YgUNbwwMfjPFu", + token: "3ji7s3pT4j6EVx4HKFq2PUe2vw7kzzfWCSLdqsQdvk6T", + }, + { + keypair: "WALLET_1", + owner: "BoX451MZzydoVdZE4NFfmMT3J5Ztqo7YgUNbwwMfjPFu", + token: "7efhjQucjgVCgijLewbJZrE16GHba9vdUzmFUdi6vwyc", + ticker: "lol", + }, +]; + +export default config; diff --git a/hooks/index.ts b/hooks/index.ts new file mode 100644 index 0000000..b45547b --- /dev/null +++ b/hooks/index.ts @@ -0,0 +1 @@ +export {default as useHandleDestroyAnimated} from './useHandleDestroyAnimated'; \ No newline at end of file diff --git a/hooks/useHandleDestroyAnimated.ts b/hooks/useHandleDestroyAnimated.ts new file mode 100644 index 0000000..96054ab --- /dev/null +++ b/hooks/useHandleDestroyAnimated.ts @@ -0,0 +1,35 @@ +import { MutableRefObject, useEffect, useState } from "react"; + +const styles = Object.freeze({ + opacity: "0", + transform: "translateY(-50%)", + transition: "all 0.5s", +}); + +export default function useHandleDestroyAnimated( + ref: MutableRefObject +): [boolean, (_: boolean) => void] { + const [sendSuccess, setSendSuccess] = useState(false); + + useEffect(() => { + if (sendSuccess) { + handleDeletion(ref); + } + }, [ref, sendSuccess]); + + function handleDeletion( + element: MutableRefObject + ) { + const style = element?.current?.style; + if (!style) return; + setTimeout(() => { + style.transition = styles.transition; + style.transform = styles.transform; + style.opacity = styles.opacity; + setTimeout(() => { + setSendSuccess(false); + }, 600); + }, 4000); + } + return [sendSuccess, setSendSuccess]; +} diff --git a/lib/spl.ts b/lib/spl.ts new file mode 100644 index 0000000..b6724bf --- /dev/null +++ b/lib/spl.ts @@ -0,0 +1,124 @@ +import { + Connection, + Keypair, + PublicKey, + Transaction, + TransactionInstruction, +} from "@solana/web3.js"; +import { + getAssociatedTokenAddress, + getOrCreateAssociatedTokenAccount, + createMintToInstruction, + createTransferInstruction, +} from "@solana/spl-token"; + +abstract class Rpc { + connection: Connection; + constructor(connection: Connection) { + this.connection = connection; + } +} + +export class RpcMethods extends Rpc { + constructor(connection: Connection) { + super(connection); + } + async getTokenBalance(owner: string, token: string): Promise { + const tokens = await this.connection.getParsedTokenAccountsByOwner( + new PublicKey(owner), + { + mint: new PublicKey(token), + } + ); + if (tokens.value.length === 0) return 0; + const amount = parseInt( + tokens.value?.[0]?.account?.data?.parsed?.info?.tokenAmount?.amount + ); + return amount; + } + + private async getAssociatedTokenAccount(token: string, owner: string) { + return await getAssociatedTokenAddress( + new PublicKey(token), + new PublicKey(owner) + ); + } + private async getOrCreateAssociatedTokenAccount(token: string, signer:Keypair, recipient:string) { + return await getOrCreateAssociatedTokenAccount( + this.connection, + signer, + new PublicKey(token), + new PublicKey(recipient) + ); + } + async mintTokensInstruction( + owner: string, + token: string, + amount: number + ): Promise { + const tokenAccount = await this.getAssociatedTokenAccount(token, owner); + + const tx = createMintToInstruction( + new PublicKey(token), + tokenAccount, + new PublicKey(owner), + amount + ); + return tx; + } + + async transferInstruction( + owner: string, + token: string, + amount: number, + recipient: string, + signer: Keypair + ): Promise { + const sourceAccount = await this.getAssociatedTokenAccount(token, owner); + const destinationAccount = await this.getOrCreateAssociatedTokenAccount( + token, + signer, + recipient + ); + const tx = createTransferInstruction( + sourceAccount, + destinationAccount.address, + new PublicKey(owner), + amount + ); + return tx; + } + + static createTx(...instructions: TransactionInstruction[]): Transaction { + let transaction = new Transaction(); + transaction.add(...instructions); + return transaction; + } + + async sendTx(transaction: Transaction, signer: Keypair): Promise { + const signature = await this.connection.sendTransaction(transaction, [ + signer, + ]); + return signature; + } + + private async getLatestBlockhash(): Promise< + Readonly<{ + blockhash: string; + lastValidBlockHeight: number; + }> + > { + const blockhash = await this.connection.getLatestBlockhash(); + console.log("blockhash", blockhash); + return blockhash; + } + + async confirmTransaction(signature: string) { + const latestBlockHash = await this.getLatestBlockhash(); + await this.connection.confirmTransaction({ + blockhash: latestBlockHash.blockhash, + lastValidBlockHeight: latestBlockHash.lastValidBlockHeight, + signature, + }); + } +} diff --git a/next.config.js b/next.config.js index a843cbe..91ef62f 100644 --- a/next.config.js +++ b/next.config.js @@ -1,6 +1,6 @@ /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, -} +}; -module.exports = nextConfig +module.exports = nextConfig; diff --git a/package.json b/package.json index 5af500b..f92d720 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,22 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "format": "prettier --write \"./**/*.{tsx,ts,js}\"" }, "dependencies": { + "@primer/react": "^35.4.0", + "@solana/spl-token": "^0.2.0", + "@solana/wallet-adapter-base": "^0.9.5", + "@solana/wallet-adapter-react": "^0.15.5", + "@solana/wallet-adapter-react-ui": "^0.9.7", + "@solana/wallet-adapter-wallets": "^0.16.1", + "@solana/web3.js": "^1.44.0", + "deepmerge": "^4.2.2", "next": "12.1.6", "react": "18.1.0", - "react-dom": "18.1.0" + "react-dom": "18.1.0", + "styled-components": "^5.3.5" }, "devDependencies": { "@types/node": "17.0.42", @@ -19,6 +29,7 @@ "@types/react-dom": "18.0.5", "eslint": "8.17.0", "eslint-config-next": "12.1.6", + "prettier": "^2.6.2", "typescript": "4.7.3" } } diff --git a/pages/_app.tsx b/pages/_app.tsx index 3f5c9d5..d3712c3 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,8 +1,71 @@ -import '../styles/globals.css' -import type { AppProps } from 'next/app' +import "../styles/globals.css"; +import type { AppProps } from "next/app"; +import React, { useMemo } from "react"; +import { + ConnectionProvider, + WalletProvider, +} from "@solana/wallet-adapter-react"; +import { WalletAdapterNetwork } from "@solana/wallet-adapter-base"; +import { + GlowWalletAdapter, + PhantomWalletAdapter, + SlopeWalletAdapter, + SolflareWalletAdapter, + SolletExtensionWalletAdapter, + SolletWalletAdapter, + TorusWalletAdapter, +} from "@solana/wallet-adapter-wallets"; +import { WalletModalProvider } from "@solana/wallet-adapter-react-ui"; +import { clusterApiUrl } from "@solana/web3.js"; +import { Wallet } from "components/Layout"; +import { ThemeProvider, BaseStyles, theme } from "@primer/react"; +import deepmerge from "deepmerge"; +const rpc = process.env.NEXT_PUBLIC_SOLANA_RPC_HOST; + +const customTheme = deepmerge(theme, { + fonts: { + mono: "MonoLisa, monospace", + }, + colors: { + text: "#000", + background: "#fff", + primary: "gray", + navbarBackground: "#24292E", + }, +}); function MyApp({ Component, pageProps }: AppProps) { - return + const network = WalletAdapterNetwork.Devnet; + const endpoint = useMemo(() => rpc || clusterApiUrl(network), [network]); + + const wallets = useMemo( + () => [ + new PhantomWalletAdapter(), + new GlowWalletAdapter(), + new SlopeWalletAdapter(), + new SolflareWalletAdapter({ network }), + new TorusWalletAdapter(), + new SolletWalletAdapter({ network }), + new SolletExtensionWalletAdapter({ network }), + ], + [network] + ); + + return ( + + + + + {/* @ts-ignore */} + + + + + + + + + ); } -export default MyApp +export default MyApp; diff --git a/pages/api/hello.ts b/pages/api/hello.ts deleted file mode 100644 index f8bcc7e..0000000 --- a/pages/api/hello.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Next.js API route support: https://nextjs.org/docs/api-routes/introduction -import type { NextApiRequest, NextApiResponse } from 'next' - -type Data = { - name: string -} - -export default function handler( - req: NextApiRequest, - res: NextApiResponse -) { - res.status(200).json({ name: 'John Doe' }) -} diff --git a/pages/api/mint.ts b/pages/api/mint.ts new file mode 100644 index 0000000..1982b45 --- /dev/null +++ b/pages/api/mint.ts @@ -0,0 +1,49 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from "next"; +import { RpcMethods } from "lib/spl"; +import { Connection, Keypair } from "@solana/web3.js"; +type Data = { + tx?: string; + err?: string; +}; + +export default function handler( + req: NextApiRequest, + res: NextApiResponse +) { + console.log("$req", req.body); + const { owner, token, amount, keypair: _keypair } = JSON.parse(req.body); + (async () => { + try { + const connection = new Connection( + process.env.NEXT_PUBLIC_SOLANA_RPC_HOST! + ); + const rpc = new RpcMethods(connection); + const ix = rpc.mintTokensInstruction(owner, token, amount); + + const tx = RpcMethods.createTx(await ix); + console.log("tx", tx); + console.log("_keypair", _keypair); + const signer = process.env[`NEXT_PUBLIC_${_keypair}`]; + + if(!signer) throw new Error ("No keypair found on env"); + + const signerParsed = signer + .slice(1, -1) + .split(",") + .map((x) => parseInt(x)); + + const keypair = Keypair.fromSecretKey(new Uint8Array(signerParsed)); + console.log("keypair", keypair.publicKey.toBase58()); + + const signature = await rpc.sendTx(tx, keypair); + + await rpc.confirmTransaction(signature); + + res.status(200).json({ tx: signature }); + } catch (e) { + console.log("e", e); + res.status(500).json({ err: (e as Error).message }); + } + })(); +} diff --git a/pages/api/transfer.ts b/pages/api/transfer.ts new file mode 100644 index 0000000..30c4754 --- /dev/null +++ b/pages/api/transfer.ts @@ -0,0 +1,61 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from "next"; +import { RpcMethods } from "lib/spl"; +import { Connection, Keypair } from "@solana/web3.js"; +type Data = { + tx?: string; + err?: string; +}; + +export default function handler( + req: NextApiRequest, + res: NextApiResponse +) { + console.log("req", req.body); + const { + owner, + token, + amount, + recipient, + keypair: _keypair, + } = JSON.parse(req.body); + (async () => { + try{ + + + const signer = process.env[`NEXT_PUBLIC_${_keypair}`]; + if(!signer) throw new Error ("No keypair found on env"); + + const signerParsed = signer + .slice(1, -1) + .split(",") + .map((x) => parseInt(x)); + + const keypair = Keypair.fromSecretKey(new Uint8Array(signerParsed)); + console.log("keypair", keypair.publicKey.toBase58()); + + const connection = new Connection(process.env.NEXT_PUBLIC_SOLANA_RPC_HOST!); + const rpc = new RpcMethods(connection); + const ix = rpc.transferInstruction( + owner, + token, + amount, + recipient, + keypair + ); + + const tx = RpcMethods.createTx(await ix); + console.log("tx", tx); + + const signature = await rpc.sendTx(tx, keypair); + + await rpc.confirmTransaction(signature); + + res.status(200).json({ tx: signature }); + } + catch (e) { + console.log("e", e); + res.status(500).json({ err: (e as Error).message }); + } + })(); +} diff --git a/pages/index.tsx b/pages/index.tsx index 86b5b3b..5b2ceb0 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,72 +1,68 @@ -import type { NextPage } from 'next' -import Head from 'next/head' -import Image from 'next/image' -import styles from '../styles/Home.module.css' +import type { NextPage } from "next"; +import Head from "next/head"; +import { Header } from "components/Layout"; +import { Table } from "components/Table"; +import { Box, StyledOcticon, Text } from "@primer/react"; +import { HeartFillIcon } from "@primer/octicons-react"; const Home: NextPage = () => { return ( -
+ <> - Create Next App - - + Fontana + + - -
-

- Welcome to Next.js! -

- -

- Get started by editing{' '} - pages/index.tsx -

- - -
- -