diff --git a/README.md b/README.md index 7f75a4e..41d250c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Intoduction **candy-machine-ui** is a fork of [Fulgurus/candy-machine-v2-responsive-ui](https://github.com/pandao/editor.md "link") repo. +**Up-to-date with metaplex from v1.1.** ## Features - **Auto refresh.** Every few seconds ui is updated, This allows you to track the amount of minted NFTs in real time. diff --git a/public/favicon.ico b/public/favicon.ico index 88837e8..e31463a 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html index a6d4d49..8abd9ac 100644 --- a/public/index.html +++ b/public/index.html @@ -7,7 +7,7 @@ - Forest Industry + Candy Machine UI diff --git a/public/logo192.png b/public/logo192.png index 4f81aea..00e0d55 100644 Binary files a/public/logo192.png and b/public/logo192.png differ diff --git a/public/logo512.png b/public/logo512.png index 95ae3cc..0e277be 100644 Binary files a/public/logo512.png and b/public/logo512.png differ diff --git a/src/App.tsx b/src/App.tsx index f6a590f..d463bd5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,27 +23,36 @@ import { } from '@solana/wallet-adapter-react-ui'; import "./App.css"; +import { DEFAULT_TIMEOUT } from './connection'; import {MintPage} from "./Home"; require('@solana/wallet-adapter-react-ui/styles.css'); +const getCandyMachineId = (): anchor.web3.PublicKey | undefined => { + try { + const candyMachineId = new anchor.web3.PublicKey( + process.env.REACT_APP_CANDY_MACHINE_ID!, + ); -const candyMachineId = new anchor.web3.PublicKey( - process.env.REACT_APP_CANDY_MACHINE_ID! -); + return candyMachineId; + } catch (e) { + console.log('Failed to construct CandyMachineId', e); + return undefined; + } +}; + +const candyMachineId = getCandyMachineId(); const network = process.env.REACT_APP_SOLANA_NETWORK as WalletAdapterNetwork; const rpcHost = process.env.REACT_APP_SOLANA_RPC_HOST!; -const connection = new anchor.web3.Connection(rpcHost); - -const txTimeout = 30000; // milliseconds (confirm this works for your project) - - +const connection = new anchor.web3.Connection( + rpcHost ? rpcHost : anchor.web3.clusterApiUrl('devnet'), +); const App = () => { // Custom RPC endpoint. - const endpoint = useMemo(() => clusterApiUrl(network), []); + const endpoint = useMemo(() => clusterApiUrl(network), []); // @solana/wallet-adapter-wallets includes all the adapters but supports tree shaking and lazy loading -- // Only the wallets you configure here will be compiled into your application, and only the dependencies @@ -63,21 +72,21 @@ const App = () => { [] ); - return ( - + return ( - - - - - + + + + + - ); + ); }; export default App; diff --git a/src/Home.tsx b/src/Home.tsx index 43ea863..9b53ebc 100644 --- a/src/Home.tsx +++ b/src/Home.tsx @@ -1,29 +1,36 @@ -import React, {useEffect, useState} from "react"; +import {useCallback, useEffect, useMemo, useState} from "react"; +import Countdown from "react-countdown"; +import {Snackbar, Alert, Paper, Grid, useTheme, Container, Typography} from "@mui/material"; + import confetti from "canvas-confetti"; import * as anchor from "@project-serum/anchor"; -import {LAMPORTS_PER_SOL, PublicKey} from "@solana/web3.js"; -import {useAnchorWallet} from "@solana/wallet-adapter-react"; + +import { + Commitment, + Connection, + PublicKey, + Transaction, + LAMPORTS_PER_SOL +} from "@solana/web3.js"; +import {WalletAdapterNetwork} from '@solana/wallet-adapter-base'; +import {useWallet} from "@solana/wallet-adapter-react"; +import {WalletMultiButton} from "@solana/wallet-adapter-react-ui"; import {GatewayProvider} from '@civic/solana-gateway-react'; -import Countdown from "react-countdown"; -import {Snackbar, Alert, Paper, Grid, useTheme, Container, Typography} from "@mui/material"; -import {toDate, AlertState, getAtaForMint} from './utils'; -import MintButton from './components/MintButton'; -import './Home.css' -import {MainContainer, - ConnectButton, - FullWidthConnectButton, - Wallet, - WalletAmount, - WalletContainer, -} from "./components/styled"; + +import {AlertState, getAtaForMint, toDate} from './utils'; import { - CandyMachine, awaitTransactionSignatureConfirmation, + CANDY_MACHINE_PROGRAM, + CandyMachineAccount, + createAccountsForMint, getCandyMachineState, + getCollectionPDA, mintOneToken, - CANDY_MACHINE_PROGRAM, + SetupState, } from "./candy-machine"; + + import nft_image from "./img/nft_image.gif" @@ -31,25 +38,39 @@ import Info from "./components/Info"; import CountDown from "./components/Countdown"; import InactiveMintButton from "./components/InactiveMintButton"; import ProgressBar from "./components/ProgressBar"; +import MintButton from './components/MintButton'; + +import './Home.css' +import {MainContainer, + ConnectButton, + FullWidthConnectButton, + Wallet, + WalletAmount, + WalletContainer, +} from "./components/styled"; import isMobile from "./components/isMobile" import {startDate, startWlDate, mintPrice, supply} from "./constants"; +const cluster = process.env.REACT_APP_SOLANA_NETWORK!.toString(); const decimals = process.env.REACT_APP_SPL_TOKEN_TO_MINT_DECIMALS ? +process.env.REACT_APP_SPL_TOKEN_TO_MINT_DECIMALS!.toString() : 9; const splTokenName = process.env.REACT_APP_SPL_TOKEN_TO_MINT_NAME ? process.env.REACT_APP_SPL_TOKEN_TO_MINT_NAME.toString() : "TOKEN"; + export interface HomeProps { - candyMachineId: anchor.web3.PublicKey; + candyMachineId?: anchor.web3.PublicKey; connection: anchor.web3.Connection; txTimeout: number; rpcHost: string; + network: WalletAdapterNetwork; } export const MintPage = (props: HomeProps) => { const [balance, setBalance] = useState(); const [isMinting, setIsMinting] = useState(false); // true when user got to press MINT const [isActive, setIsActive] = useState(false); // true when countdown completes or whitelisted + const [solanaExplorerLink, setSolanaExplorerLink] = useState(""); const [itemsAvailable, setItemsAvailable] = useState(0); const [itemsRedeemed, setItemsRedeemed] = useState(0); const [itemsRemaining, setItemsRemaining] = useState(0); @@ -65,10 +86,6 @@ export const MintPage = (props: HomeProps) => { const [endDate, setEndDate] = useState(); const [isPresale, setIsPresale] = useState(false); const [isWLOnly, setIsWLOnly] = useState(false); - const [refreshFlag, setRefreshFlag] = useState(false) - - const mobileMarker = isMobile() - const theme = useTheme() const [alertState, setAlertState] = useState({ open: false, @@ -76,137 +93,223 @@ export const MintPage = (props: HomeProps) => { severity: undefined, }); - const wallet = useAnchorWallet(); - const [candyMachine, setCandyMachine] = useState(); + const [needTxnSplit, setNeedTxnSplit] = useState(true); + const [setupTxn, setSetupTxn] = useState(); + + const wallet = useWallet(); + const [candyMachine, setCandyMachine] = useState(); const rpcUrl = props.rpcHost; + const solFeesEstimation = 0.012; // approx of account creation fees + + const anchorWallet = useMemo(() => { + if ( + !wallet || + !wallet.publicKey || + !wallet.signAllTransactions || + !wallet.signTransaction + ) { + return; + } - const refreshCandyMachineState = () => { - (async () => { - if (!wallet) return; - - const cndy = await getCandyMachineState( - wallet as anchor.Wallet, - props.candyMachineId, - props.connection - ); - setCandyMachine(cndy); - setItemsAvailable(cndy.state.itemsAvailable); - setItemsRemaining(cndy.state.itemsRemaining); - let tempItems = itemsRedeemed - setItemsRedeemed(cndy.state.itemsRedeemed >= tempItems ? cndy.state.itemsRedeemed : tempItems); - - var divider = 1; - if (decimals) { - divider = +('1' + new Array(decimals).join('0').slice() + '0'); + return { + publicKey: wallet.publicKey, + signAllTransactions: wallet.signAllTransactions, + signTransaction: wallet.signTransaction, + } as anchor.Wallet; + }, [wallet]); + + const refreshCandyMachineState = useCallback( + async (commitment: Commitment = 'confirmed') => { + if (!anchorWallet) { + return; } - // detect if using spl-token to mint - if (cndy.state.tokenMint) { - setPayWithSplToken(true); - // Customize your SPL-TOKEN Label HERE - // TODO: get spl-token metadata name - setPriceLabel(splTokenName); - setPrice(cndy.state.price.toNumber() / divider); - setWhitelistPrice(cndy.state.price.toNumber() / divider); - }else { - setPrice(cndy.state.price.toNumber() / LAMPORTS_PER_SOL); - setWhitelistPrice(cndy.state.price.toNumber() / LAMPORTS_PER_SOL); - } + const connection = new Connection(props.rpcHost, commitment); + if (props.candyMachineId) { + try { + const cndy = await getCandyMachineState( + anchorWallet, + props.candyMachineId, + connection, + ); + + setCandyMachine(cndy); + setItemsAvailable(cndy.state.itemsAvailable); + setItemsRemaining(cndy.state.itemsRemaining); + setItemsRedeemed(cndy.state.itemsRedeemed); - // fetch whitelist token balance - if (cndy.state.whitelistMintSettings) { - setWhitelistEnabled(true); - setIsBurnToken(cndy.state.whitelistMintSettings.mode.burnEveryTime); - setIsPresale(cndy.state.whitelistMintSettings.presale); - setIsWLOnly(!isPresale && cndy.state.whitelistMintSettings.discountPrice === null); + var divider = 1; + if (decimals) { + divider = +('1' + new Array(decimals).join('0').slice() + '0'); + } - if (cndy.state.whitelistMintSettings.discountPrice !== null && cndy.state.whitelistMintSettings.discountPrice !== cndy.state.price) { + // detect if using spl-token to mint if (cndy.state.tokenMint) { - setWhitelistPrice(cndy.state.whitelistMintSettings.discountPrice?.toNumber() / divider); + setPayWithSplToken(true); + // Customize your SPL-TOKEN Label HERE + // TODO: get spl-token metadata name + setPriceLabel(splTokenName); + setPrice(cndy.state.price.toNumber() / divider); + setWhitelistPrice(cndy.state.price.toNumber() / divider); } else { - setWhitelistPrice(cndy.state.whitelistMintSettings.discountPrice?.toNumber() / LAMPORTS_PER_SOL); + setPrice(cndy.state.price.toNumber() / LAMPORTS_PER_SOL); + setWhitelistPrice(cndy.state.price.toNumber() / LAMPORTS_PER_SOL); } - } - let balance = 0; - try { - const tokenBalance = - await props.connection.getTokenAccountBalance( - ( - await getAtaForMint( - cndy.state.whitelistMintSettings.mint, - wallet.publicKey, - ) - )[0], + + // fetch whitelist token balance + if (cndy.state.whitelistMintSettings) { + setWhitelistEnabled(true); + setIsBurnToken(cndy.state.whitelistMintSettings.mode.burnEveryTime); + setIsPresale(cndy.state.whitelistMintSettings.presale); + setIsWLOnly(!isPresale && cndy.state.whitelistMintSettings.discountPrice === null); + + if (cndy.state.whitelistMintSettings.discountPrice !== null && cndy.state.whitelistMintSettings.discountPrice !== cndy.state.price) { + if (cndy.state.tokenMint) { + setWhitelistPrice(cndy.state.whitelistMintSettings.discountPrice?.toNumber() / divider); + } else { + setWhitelistPrice(cndy.state.whitelistMintSettings.discountPrice?.toNumber() / LAMPORTS_PER_SOL); + } + } + + let balance = 0; + try { + const tokenBalance = + await props.connection.getTokenAccountBalance( + ( + await getAtaForMint( + cndy.state.whitelistMintSettings.mint, + anchorWallet.publicKey, + ) + )[0], + ); + + balance = tokenBalance?.value?.uiAmount || 0; + } catch (e) { + console.error(e); + balance = 0; + } + if (commitment !== "processed") { + setWhitelistTokenBalance(balance); + } + setIsActive(isPresale && !isEnded && balance > 0); + + } else { + setWhitelistEnabled(false); + } + + // end the mint when date is reached + if (cndy?.state.endSettings?.endSettingType.date) { + setEndDate(toDate(cndy.state.endSettings.number)); + if ( + cndy.state.endSettings.number.toNumber() < + new Date().getTime() / 1000 + ) { + setIsEnded(true); + setIsActive(false); + } + } + // end the mint when amount is reached + if (cndy?.state.endSettings?.endSettingType.amount) { + let limit = Math.min( + cndy.state.endSettings.number.toNumber(), + cndy.state.itemsAvailable, ); + setItemsAvailable(limit); + if (cndy.state.itemsRedeemed < limit) { + setItemsRemaining(limit - cndy.state.itemsRedeemed); + } else { + setItemsRemaining(0); + cndy.state.isSoldOut = true; + setIsEnded(true); + } + } else { + setItemsRemaining(cndy.state.itemsRemaining); + } - balance = tokenBalance?.value?.uiAmount || 0; - } catch (e) { - console.error(e); - balance = 0; - } - setWhitelistTokenBalance(balance); - setIsActive(isPresale && !isEnded && balance > 0); - } else { - setWhitelistEnabled(false); - } + if (cndy.state.isSoldOut) { + setIsActive(false); + } - // end the mint when date is reached - if (cndy?.state.endSettings?.endSettingType.date) { - setEndDate(toDate(cndy.state.endSettings.number)); - if ( - cndy.state.endSettings.number.toNumber() < - new Date().getTime() / 1000 - ) { - setIsEnded(true); - setIsActive(false); - } - } - // end the mint when amount is reached - if (cndy?.state.endSettings?.endSettingType.amount) { - let limit = Math.min( - cndy.state.endSettings.number.toNumber(), - cndy.state.itemsAvailable, - ); - setItemsAvailable(limit); - if (cndy.state.itemsRedeemed < limit) { - setItemsRemaining(limit - cndy.state.itemsRedeemed); - } else { - setItemsRemaining(0); - cndy.state.isSoldOut = true; - setIsEnded(true); + const [collectionPDA] = await getCollectionPDA(props.candyMachineId); + const collectionPDAAccount = await connection.getAccountInfo( + collectionPDA, + ); + + const txnEstimate = + 892 + + (!!collectionPDAAccount && cndy.state.retainAuthority ? 182 : 0) + + (cndy.state.tokenMint ? 66 : 0) + + (cndy.state.whitelistMintSettings ? 34 : 0) + + (cndy.state.whitelistMintSettings?.mode?.burnEveryTime ? 34 : 0) + + (cndy.state.gatekeeper ? 33 : 0) + + (cndy.state.gatekeeper?.expireOnUse ? 66 : 0); + + setNeedTxnSplit(txnEstimate > 1230); + } catch (e) { + if (e instanceof Error) { + if ( + e.message === `Account does not exist ${props.candyMachineId}` + ) { + setAlertState({ + open: true, + message: `Couldn't fetch candy machine state from candy machine with address: ${props.candyMachineId}, using rpc: ${props.rpcHost}! You probably typed the REACT_APP_CANDY_MACHINE_ID value in wrong in your .env file, or you are using the wrong RPC!`, + severity: 'error', + hideDuration: null, + }); + } else if ( + e.message.startsWith('failed to get info about account') + ) { + setAlertState({ + open: true, + message: `Couldn't fetch candy machine state with rpc: ${props.rpcHost}! This probably means you have an issue with the REACT_APP_SOLANA_RPC_HOST value in your .env file, or you are not using a custom RPC!`, + severity: 'error', + hideDuration: null, + }); + } + } else { + setAlertState({ + open: true, + message: `${e}`, + severity: 'error', + hideDuration: null, + }); + } + console.log(e); } } else { - setItemsRemaining(cndy.state.itemsRemaining); + setAlertState({ + open: true, + message: `Your REACT_APP_CANDY_MACHINE_ID value in the .env file doesn't look right! Make sure you enter it in as plain base-58 address!`, + severity: 'error', + hideDuration: null, + }); } - - if (cndy.state.isSoldOut) { - setIsActive(false); - } - })(); - }; - - const renderGoLiveDateCounter = ({days, hours, minutes, seconds}: any) => { - return - }; + }, + [anchorWallet, props.candyMachineId, props.rpcHost, isEnded, isPresale, props.connection], + ); - function displaySuccess(): void { - let remaining = itemsRemaining - 1; + function displaySuccess(mintPublicKey: any, qty: number = 1): void { + let remaining = itemsRemaining - qty; setItemsRemaining(remaining); setIsSoldOut(remaining === 0); if (isBurnToken && whitelistTokenBalance && whitelistTokenBalance > 0) { - let balance = whitelistTokenBalance - 1; + let balance = whitelistTokenBalance - qty; setWhitelistTokenBalance(balance); setIsActive(isPresale && !isEnded && balance > 0); } - setItemsRedeemed(itemsRedeemed + 1); - const solFeesEstimation = 0.012; // approx + setSetupTxn(undefined); + setItemsRedeemed(itemsRedeemed + qty); if (!payWithSplToken && balance && balance > 0) { - setBalance(balance - (whitelistEnabled ? whitelistPrice : price) - solFeesEstimation); + setBalance(balance - ((whitelistEnabled ? whitelistPrice : price) * qty) - solFeesEstimation); } + setSolanaExplorerLink(cluster === "devnet" || cluster === "testnet" + ? ("https://solscan.io/token/" + mintPublicKey + "?cluster=" + cluster) + : ("https://solscan.io/token/" + mintPublicKey)); + setIsMinting(false); throwConfetti(); }; @@ -218,27 +321,81 @@ export const MintPage = (props: HomeProps) => { }); } - const onMint = async () => { + const onMint = async ( + beforeTransactions: Transaction[] = [], + afterTransactions: Transaction[] = [], + ) => { try { - setIsMinting(true); - if (wallet && candyMachine?.program && wallet.publicKey) { - const mint = anchor.web3.Keypair.generate(); - const mintTxId = ( - await mintOneToken(candyMachine, wallet.publicKey, mint) - )[0]; + if (wallet.connected && candyMachine?.program && wallet.publicKey) { + setIsMinting(true); + let setupMint: SetupState | undefined; + if (needTxnSplit && setupTxn === undefined) { + setAlertState({ + open: true, + message: 'Please validate account setup transaction', + severity: 'info', + }); + setupMint = await createAccountsForMint( + candyMachine, + wallet.publicKey, + ); + let status: any = {err: true}; + if (setupMint.transaction) { + status = await awaitTransactionSignatureConfirmation( + setupMint.transaction, + props.txTimeout, + props.connection, + true, + ); + } + if (status && !status.err) { + setSetupTxn(setupMint); + setAlertState({ + open: true, + message: + 'Setup transaction succeeded! You can now validate mint transaction', + severity: 'info', + }); + } else { + setAlertState({ + open: true, + message: 'Mint failed! Please try again!', + severity: 'error', + }); + return; + } + } + + const setupState = setupMint ?? setupTxn; + const mint = setupState?.mint ?? anchor.web3.Keypair.generate(); + let mintResult = await mintOneToken( + candyMachine, + wallet.publicKey, + mint, + beforeTransactions, + afterTransactions, + setupState, + ); let status: any = {err: true}; - if (mintTxId) { + let metadataStatus = null; + if (mintResult) { status = await awaitTransactionSignatureConfirmation( - mintTxId, + mintResult.mintTxId, props.txTimeout, props.connection, - 'singleGossip', true, ); + + metadataStatus = + await candyMachine.program.provider.connection.getAccountInfo( + mintResult.metadataKey, + 'processed', + ); + console.log('Metadata status: ', !!metadataStatus); } - if (!status?.err) { + if (status && !status.err && metadataStatus) { setAlertState({ open: true, message: 'Congratulations! Mint succeeded!', @@ -246,17 +403,27 @@ export const MintPage = (props: HomeProps) => { }); // update front-end amounts - displaySuccess(); + displaySuccess(mint.publicKey); + refreshCandyMachineState('processed'); + } else if (status && !status.err) { + setAlertState({ + open: true, + message: + 'Mint likely failed! Anti-bot SOL 0.01 fee potentially charged! Check the explorer to confirm the mint failed and if so, make sure you are eligible to mint before trying again.', + severity: 'error', + hideDuration: 8000, + }); + refreshCandyMachineState(); } else { setAlertState({ open: true, message: 'Mint failed! Please try again!', severity: 'error', }); + refreshCandyMachineState(); } } } catch (error: any) { - // TODO: blech: let message = error.msg || 'Minting failed! Please try again!'; if (!error.msg) { if (!error.message) { @@ -285,35 +452,34 @@ export const MintPage = (props: HomeProps) => { } }; - useEffect(() => { (async () => { - if (wallet) { - const balance = await props.connection.getBalance(wallet.publicKey); + if (anchorWallet) { + const balance = await props.connection.getBalance(anchorWallet!.publicKey); setBalance(balance / LAMPORTS_PER_SOL); } })(); - }, [wallet, props.connection]); + }, [anchorWallet, props.connection]); - useEffect(refreshCandyMachineState, [ - wallet, + useEffect(() => { + refreshCandyMachineState(); + }, [ + anchorWallet, props.candyMachineId, props.connection, isEnded, isPresale, - refreshFlag + refreshCandyMachineState ]); - useEffect( - () => { - let timer1 = setInterval(() => setRefreshFlag(!refreshFlag), 5000); - return () => { - clearInterval(timer1); - }; - }, - [refreshFlag] - ); + + const mobileMarker = isMobile() + const theme = useTheme() + + const renderGoLiveDateCounter = ({days, hours, minutes, seconds}: any) => { + return + }; diff --git a/src/candy-machine.ts b/src/candy-machine.ts index 563e401..4785666 100644 --- a/src/candy-machine.ts +++ b/src/candy-machine.ts @@ -1,13 +1,12 @@ -import * as anchor from "@project-serum/anchor"; +import * as anchor from '@project-serum/anchor'; +import { MintLayout, TOKEN_PROGRAM_ID, Token } from '@solana/spl-token'; import { - MintLayout, - TOKEN_PROGRAM_ID, - Token, -} from "@solana/spl-token"; - -import { SystemProgram } from '@solana/web3.js'; -import { sendTransactions } from './connection'; + SystemProgram, + Transaction, + SYSVAR_SLOT_HASHES_PUBKEY, +} from '@solana/web3.js'; +import { sendTransactions, SequenceType } from './connection'; import { CIVIC, @@ -18,27 +17,24 @@ import { } from './utils'; export const CANDY_MACHINE_PROGRAM = new anchor.web3.PublicKey( - "cndy3Z4yapfJBmL3ShUp5exZKqR3z33thTzeNMm2gRZ" + 'cndy3Z4yapfJBmL3ShUp5exZKqR3z33thTzeNMm2gRZ', ); const TOKEN_METADATA_PROGRAM_ID = new anchor.web3.PublicKey( - "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" + 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s', ); -export interface CandyMachine { - id: anchor.web3.PublicKey, - program: anchor.Program; - state: CandyMachineState; -} - interface CandyMachineState { + authority: anchor.web3.PublicKey; itemsAvailable: number; itemsRedeemed: number; itemsRemaining: number; treasury: anchor.web3.PublicKey; - tokenMint: anchor.web3.PublicKey; + tokenMint: null | anchor.web3.PublicKey; isSoldOut: boolean; isActive: boolean; + isPresale: boolean; + isWhitelistOnly: boolean; goLiveDate: anchor.BN; price: anchor.BN; gatekeeper: null | { @@ -60,14 +56,20 @@ interface CandyMachineState { uri: string; hash: Uint8Array; }; + retainAuthority: boolean; +} + +export interface CandyMachineAccount { + id: anchor.web3.PublicKey; + program: anchor.Program; + state: CandyMachineState; } export const awaitTransactionSignatureConfirmation = async ( - txid: anchor.web3.TransactionSignature, - timeout: number, - connection: anchor.web3.Connection, - commitment: anchor.web3.Commitment = 'recent', - queryStatus = false, + txid: anchor.web3.TransactionSignature, + timeout: number, + connection: anchor.web3.Connection, + queryStatus = false, ): Promise => { let done = false; let status: anchor.web3.SignatureStatus | null | void = { @@ -85,6 +87,7 @@ export const awaitTransactionSignatureConfirmation = async ( console.log('Rejecting for timeout...'); reject({ timeout: true }); }, timeout); + while (!done && queryStatus) { // eslint-disable-next-line no-loop-func (async () => { @@ -118,20 +121,16 @@ export const awaitTransactionSignatureConfirmation = async ( } }); - //@ts-ignore - if (connection._signatureSubscriptions[subId]) { - connection.removeSignatureListener(subId); - } done = true; console.log('Returning status', status); return status; }; -/* export */ const createAssociatedTokenAccountInstruction = ( - associatedTokenAddress: anchor.web3.PublicKey, - payer: anchor.web3.PublicKey, - walletAddress: anchor.web3.PublicKey, - splTokenMintAddress: anchor.web3.PublicKey, +const createAssociatedTokenAccountInstruction = ( + associatedTokenAddress: anchor.web3.PublicKey, + payer: anchor.web3.PublicKey, + walletAddress: anchor.web3.PublicKey, + splTokenMintAddress: anchor.web3.PublicKey, ) => { const keys = [ { pubkey: payer, isSigner: true, isWritable: true }, @@ -158,17 +157,17 @@ export const awaitTransactionSignatureConfirmation = async ( }; export const getCandyMachineState = async ( - anchorWallet: anchor.Wallet, - candyMachineId: anchor.web3.PublicKey, - connection: anchor.web3.Connection, -): Promise => { + anchorWallet: anchor.Wallet, + candyMachineId: anchor.web3.PublicKey, + connection: anchor.web3.Connection, +): Promise => { const provider = new anchor.Provider(connection, anchorWallet, { - preflightCommitment: 'recent', + preflightCommitment: 'processed', }); const idl = await anchor.Program.fetchIdl(CANDY_MACHINE_PROGRAM, provider); - const program = new anchor.Program(idl, CANDY_MACHINE_PROGRAM, provider); + const program = new anchor.Program(idl!, CANDY_MACHINE_PROGRAM, provider); const state: any = await program.account.candyMachine.fetch(candyMachineId); const itemsAvailable = state.data.itemsAvailable.toNumber(); @@ -179,18 +178,14 @@ export const getCandyMachineState = async ( id: candyMachineId, program, state: { + authority: state.authority, itemsAvailable, itemsRedeemed, itemsRemaining, isSoldOut: itemsRemaining === 0, - isActive: - state.data.goLiveDate && - state.data.goLiveDate.toNumber() < new Date().getTime() / 1000 && - (state.endSettings - ? state.endSettings.endSettingType.date - ? state.endSettings.number.toNumber() > new Date().getTime() / 1000 - : itemsRedeemed < state.endSettings.number.toNumber() - : true), + isActive: false, + isPresale: false, + isWhitelistOnly: false, goLiveDate: state.data.goLiveDate, treasury: state.wallet, tokenMint: state.tokenMint, @@ -199,112 +194,231 @@ export const getCandyMachineState = async ( whitelistMintSettings: state.data.whitelistMintSettings, hiddenSettings: state.data.hiddenSettings, price: state.data.price, + retainAuthority: state.data.retainAuthority, }, }; }; const getMasterEdition = async ( - mint: anchor.web3.PublicKey, + mint: anchor.web3.PublicKey, ): Promise => { return ( - await anchor.web3.PublicKey.findProgramAddress( - [ - Buffer.from('metadata'), - TOKEN_METADATA_PROGRAM_ID.toBuffer(), - mint.toBuffer(), - Buffer.from('edition'), - ], - TOKEN_METADATA_PROGRAM_ID, - ) + await anchor.web3.PublicKey.findProgramAddress( + [ + Buffer.from('metadata'), + TOKEN_METADATA_PROGRAM_ID.toBuffer(), + mint.toBuffer(), + Buffer.from('edition'), + ], + TOKEN_METADATA_PROGRAM_ID, + ) )[0]; }; const getMetadata = async ( - mint: anchor.web3.PublicKey, + mint: anchor.web3.PublicKey, ): Promise => { return ( - await anchor.web3.PublicKey.findProgramAddress( - [ - Buffer.from('metadata'), - TOKEN_METADATA_PROGRAM_ID.toBuffer(), - mint.toBuffer(), - ], - TOKEN_METADATA_PROGRAM_ID, - ) + await anchor.web3.PublicKey.findProgramAddress( + [ + Buffer.from('metadata'), + TOKEN_METADATA_PROGRAM_ID.toBuffer(), + mint.toBuffer(), + ], + TOKEN_METADATA_PROGRAM_ID, + ) )[0]; }; export const getCandyMachineCreator = async ( - candyMachine: anchor.web3.PublicKey, + candyMachine: anchor.web3.PublicKey, ): Promise<[anchor.web3.PublicKey, number]> => { return await anchor.web3.PublicKey.findProgramAddress( - [Buffer.from('candy_machine'), candyMachine.toBuffer()], - CANDY_MACHINE_PROGRAM, + [Buffer.from('candy_machine'), candyMachine.toBuffer()], + CANDY_MACHINE_PROGRAM, ); }; -export const mintOneToken = async ( - candyMachine: CandyMachine, +export const getCollectionPDA = async ( + candyMachineAddress: anchor.web3.PublicKey, +): Promise<[anchor.web3.PublicKey, number]> => { + return await anchor.web3.PublicKey.findProgramAddress( + [Buffer.from('collection'), candyMachineAddress.toBuffer()], + CANDY_MACHINE_PROGRAM, + ); +}; + +export interface CollectionData { + mint: anchor.web3.PublicKey; + candyMachine: anchor.web3.PublicKey; +} + +export const getCollectionAuthorityRecordPDA = async ( + mint: anchor.web3.PublicKey, + newAuthority: anchor.web3.PublicKey, +): Promise => { + return ( + await anchor.web3.PublicKey.findProgramAddress( + [ + Buffer.from('metadata'), + TOKEN_METADATA_PROGRAM_ID.toBuffer(), + mint.toBuffer(), + Buffer.from('collection_authority'), + newAuthority.toBuffer(), + ], + TOKEN_METADATA_PROGRAM_ID, + ) + )[0]; +}; + +export type SetupState = { + mint: anchor.web3.Keypair; + userTokenAccount: anchor.web3.PublicKey; + transaction: string; +}; + +export const createAccountsForMint = async ( + candyMachine: CandyMachineAccount, payer: anchor.web3.PublicKey, - mint: anchor.web3.Keypair -): Promise<(string | undefined)[]> => { +): Promise => { + const mint = anchor.web3.Keypair.generate(); const userTokenAccountAddress = ( - await getAtaForMint(mint.publicKey, payer) + await getAtaForMint(mint.publicKey, payer) )[0]; - const userPayingAccountAddress = candyMachine.state.tokenMint - ? (await getAtaForMint(candyMachine.state.tokenMint, payer))[0] - : payer; - - const candyMachineAddress = candyMachine.id; - const remainingAccounts = []; const signers: anchor.web3.Keypair[] = [mint]; - const cleanupInstructions = []; const instructions = [ anchor.web3.SystemProgram.createAccount({ fromPubkey: payer, newAccountPubkey: mint.publicKey, space: MintLayout.span, lamports: - await candyMachine.program.provider.connection.getMinimumBalanceForRentExemption( - MintLayout.span, - ), + await candyMachine.program.provider.connection.getMinimumBalanceForRentExemption( + MintLayout.span, + ), programId: TOKEN_PROGRAM_ID, }), Token.createInitMintInstruction( - TOKEN_PROGRAM_ID, - mint.publicKey, - 0, - payer, - payer, + TOKEN_PROGRAM_ID, + mint.publicKey, + 0, + payer, + payer, ), createAssociatedTokenAccountInstruction( - userTokenAccountAddress, - payer, - payer, - mint.publicKey, + userTokenAccountAddress, + payer, + payer, + mint.publicKey, ), Token.createMintToInstruction( - TOKEN_PROGRAM_ID, - mint.publicKey, - userTokenAccountAddress, - payer, - [], - 1, + TOKEN_PROGRAM_ID, + mint.publicKey, + userTokenAccountAddress, + payer, + [], + 1, ), ]; + return { + mint: mint, + userTokenAccount: userTokenAccountAddress, + transaction: ( + await sendTransactions( + candyMachine.program.provider.connection, + candyMachine.program.provider.wallet, + [instructions], + [signers], + SequenceType.StopOnFailure, + 'singleGossip', + () => {}, + () => false, + undefined, + [], + [], + ) + ).txs[0].txid, + }; +}; + +type MintResult = { + mintTxId: string; + metadataKey: anchor.web3.PublicKey; +}; + +export const mintOneToken = async ( + candyMachine: CandyMachineAccount, + payer: anchor.web3.PublicKey, + mint: anchor.web3.Keypair, + beforeTransactions: Transaction[] = [], + afterTransactions: Transaction[] = [], + setupState?: SetupState, +): Promise => { + const userTokenAccountAddress = ( + await getAtaForMint(mint.publicKey, payer) + )[0]; + + const userPayingAccountAddress = candyMachine.state.tokenMint + ? (await getAtaForMint(candyMachine.state.tokenMint, payer))[0] + : payer; + + const candyMachineAddress = candyMachine.id; + const remainingAccounts = []; + const instructions = []; + const signers: anchor.web3.Keypair[] = []; + console.log('SetupState: ', setupState); + if (!setupState) { + signers.push(mint); + instructions.push( + ...[ + anchor.web3.SystemProgram.createAccount({ + fromPubkey: payer, + newAccountPubkey: mint.publicKey, + space: MintLayout.span, + lamports: + await candyMachine.program.provider.connection.getMinimumBalanceForRentExemption( + MintLayout.span, + ), + programId: TOKEN_PROGRAM_ID, + }), + Token.createInitMintInstruction( + TOKEN_PROGRAM_ID, + mint.publicKey, + 0, + payer, + payer, + ), + createAssociatedTokenAccountInstruction( + userTokenAccountAddress, + payer, + payer, + mint.publicKey, + ), + Token.createMintToInstruction( + TOKEN_PROGRAM_ID, + mint.publicKey, + userTokenAccountAddress, + payer, + [], + 1, + ), + ], + ); + } + if (candyMachine.state.gatekeeper) { remainingAccounts.push({ pubkey: ( - await getNetworkToken( - payer, - candyMachine.state.gatekeeper.gatekeeperNetwork, - ) + await getNetworkToken( + payer, + candyMachine.state.gatekeeper.gatekeeperNetwork, + ) )[0], isWritable: true, isSigner: false, }); + if (candyMachine.state.gatekeeper.expireOnUse) { remainingAccounts.push({ pubkey: CIVIC, @@ -313,9 +427,9 @@ export const mintOneToken = async ( }); remainingAccounts.push({ pubkey: ( - await getNetworkExpire( - candyMachine.state.gatekeeper.gatekeeperNetwork, - ) + await getNetworkExpire( + candyMachine.state.gatekeeper.gatekeeperNetwork, + ) )[0], isWritable: false, isSigner: false, @@ -324,7 +438,7 @@ export const mintOneToken = async ( } if (candyMachine.state.whitelistMintSettings) { const mint = new anchor.web3.PublicKey( - candyMachine.state.whitelistMintSettings.mint, + candyMachine.state.whitelistMintSettings.mint, ); const whitelistToken = (await getAtaForMint(mint, payer))[0]; @@ -335,126 +449,139 @@ export const mintOneToken = async ( }); if (candyMachine.state.whitelistMintSettings.mode.burnEveryTime) { - const whitelistBurnAuthority = anchor.web3.Keypair.generate(); - remainingAccounts.push({ pubkey: mint, isWritable: true, isSigner: false, }); remainingAccounts.push({ - pubkey: whitelistBurnAuthority.publicKey, + pubkey: payer, isWritable: false, isSigner: true, }); - signers.push(whitelistBurnAuthority); - const exists = - await candyMachine.program.provider.connection.getAccountInfo( - whitelistToken, - ); - if (exists) { - instructions.push( - Token.createApproveInstruction( - TOKEN_PROGRAM_ID, - whitelistToken, - whitelistBurnAuthority.publicKey, - payer, - [], - 1, - ), - ); - cleanupInstructions.push( - Token.createRevokeInstruction( - TOKEN_PROGRAM_ID, - whitelistToken, - payer, - [], - ), - ); - } } } if (candyMachine.state.tokenMint) { - const transferAuthority = anchor.web3.Keypair.generate(); - - signers.push(transferAuthority); remainingAccounts.push({ pubkey: userPayingAccountAddress, isWritable: true, isSigner: false, }); remainingAccounts.push({ - pubkey: transferAuthority.publicKey, + pubkey: payer, isWritable: false, isSigner: true, }); - - instructions.push( - Token.createApproveInstruction( - TOKEN_PROGRAM_ID, - userPayingAccountAddress, - transferAuthority.publicKey, - payer, - [], - candyMachine.state.price.toNumber(), - ), - ); - cleanupInstructions.push( - Token.createRevokeInstruction( - TOKEN_PROGRAM_ID, - userPayingAccountAddress, - payer, - [], - ), - ); } const metadataAddress = await getMetadata(mint.publicKey); const masterEdition = await getMasterEdition(mint.publicKey); const [candyMachineCreator, creatorBump] = await getCandyMachineCreator( - candyMachineAddress, + candyMachineAddress, ); + console.log(remainingAccounts.map(rm => rm.pubkey.toBase58())); instructions.push( - await candyMachine.program.instruction.mintNft(creatorBump, { - accounts: { - candyMachine: candyMachineAddress, - candyMachineCreator, - payer: payer, - wallet: candyMachine.state.treasury, - mint: mint.publicKey, - metadata: metadataAddress, - masterEdition, - mintAuthority: payer, - updateAuthority: payer, - tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID, - tokenProgram: TOKEN_PROGRAM_ID, - systemProgram: SystemProgram.programId, - rent: anchor.web3.SYSVAR_RENT_PUBKEY, - clock: anchor.web3.SYSVAR_CLOCK_PUBKEY, - recentBlockhashes: anchor.web3.SYSVAR_RECENT_BLOCKHASHES_PUBKEY, - instructionSysvarAccount: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, - }, - remainingAccounts: - remainingAccounts.length > 0 ? remainingAccounts : undefined, - }), + await candyMachine.program.instruction.mintNft(creatorBump, { + accounts: { + candyMachine: candyMachineAddress, + candyMachineCreator, + payer: payer, + wallet: candyMachine.state.treasury, + mint: mint.publicKey, + metadata: metadataAddress, + masterEdition, + mintAuthority: payer, + updateAuthority: payer, + tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID, + tokenProgram: TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + clock: anchor.web3.SYSVAR_CLOCK_PUBKEY, + recentBlockhashes: SYSVAR_SLOT_HASHES_PUBKEY, + instructionSysvarAccount: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, + }, + remainingAccounts: + remainingAccounts.length > 0 ? remainingAccounts : undefined, + }), ); + const [collectionPDA] = await getCollectionPDA(candyMachineAddress); + const collectionPDAAccount = + await candyMachine.program.provider.connection.getAccountInfo( + collectionPDA, + ); + + if (collectionPDAAccount && candyMachine.state.retainAuthority) { + try { + const collectionData = + (await candyMachine.program.account.collectionPda.fetch( + collectionPDA, + )) as CollectionData; + console.log(collectionData); + const collectionMint = collectionData.mint; + const collectionAuthorityRecord = await getCollectionAuthorityRecordPDA( + collectionMint, + collectionPDA, + ); + console.log(collectionMint); + if (collectionMint) { + const collectionMetadata = await getMetadata(collectionMint); + const collectionMasterEdition = await getMasterEdition(collectionMint); + console.log('Collection PDA: ', collectionPDA.toBase58()); + console.log('Authority: ', candyMachine.state.authority.toBase58()); + instructions.push( + await candyMachine.program.instruction.setCollectionDuringMint({ + accounts: { + candyMachine: candyMachineAddress, + metadata: metadataAddress, + payer: payer, + collectionPda: collectionPDA, + tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID, + instructions: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, + collectionMint, + collectionMetadata, + collectionMasterEdition, + authority: candyMachine.state.authority, + collectionAuthorityRecord, + }, + }), + ); + } + } catch (error) { + console.error(error); + } + } + + const instructionsMatrix = [instructions]; + const signersMatrix = [signers]; + try { - return ( - await sendTransactions( - candyMachine.program.provider.connection, - candyMachine.program.provider.wallet, - [instructions, cleanupInstructions], - [signers, []], - ) + const txns = ( + await sendTransactions( + candyMachine.program.provider.connection, + candyMachine.program.provider.wallet, + instructionsMatrix, + signersMatrix, + SequenceType.StopOnFailure, + 'singleGossip', + () => {}, + () => false, + undefined, + beforeTransactions, + afterTransactions, + ) ).txs.map(t => t.txid); + const mintTxn = txns[0]; + return { + mintTxId: mintTxn, + metadataKey: metadataAddress, + }; } catch (e) { console.log(e); } - - return []; + return null; }; export const shortenAddress = (address: string, chars = 4): string => { @@ -462,5 +589,5 @@ export const shortenAddress = (address: string, chars = 4): string => { }; const sleep = (ms: number): Promise => { - return new Promise((resolve) => setTimeout(resolve, ms)); -} \ No newline at end of file + return new Promise(resolve => setTimeout(resolve, ms)); +}; diff --git a/src/components/MintButton/MintButton.tsx b/src/components/MintButton/MintButton.tsx index d27fd09..ea81caa 100644 --- a/src/components/MintButton/MintButton.tsx +++ b/src/components/MintButton/MintButton.tsx @@ -2,7 +2,7 @@ import styled from 'styled-components'; import {useEffect, useState} from 'react'; import {Button, CircularProgress} from '@mui/material'; import {GatewayStatus, useGateway} from '@civic/solana-gateway-react'; -import {CandyMachine} from '../../candy-machine'; +import {CandyMachineAccount} from '../../candy-machine'; const CTAButton = styled(Button)` @@ -26,7 +26,7 @@ export const MintButton = ({ isSoldOut }: { onMint: () => Promise; - candyMachine: CandyMachine | undefined; + candyMachine?: CandyMachineAccount; isMinting: boolean; isEnded: boolean; isActive: boolean; diff --git a/src/connection.tsx b/src/connection.tsx index e2a6b3e..1a534e6 100644 --- a/src/connection.tsx +++ b/src/connection.tsx @@ -1,294 +1,303 @@ import { - Keypair, - Commitment, - Connection, - RpcResponseAndContext, - SignatureStatus, - SimulatedTransactionResponse, - Transaction, - TransactionInstruction, - TransactionSignature, - Blockhash, - FeeCalculator, - } from '@solana/web3.js'; - - import { WalletNotConnectedError } from '@solana/wallet-adapter-base'; - - interface BlockhashAndFeeCalculator { - blockhash: Blockhash; - feeCalculator: FeeCalculator; - } - - export const getErrorForTransaction = async ( + Keypair, + Commitment, + Connection, + RpcResponseAndContext, + SignatureStatus, + SimulatedTransactionResponse, + Transaction, + TransactionInstruction, + TransactionSignature, + Blockhash, + FeeCalculator, +} from '@solana/web3.js'; + +import { WalletNotConnectedError } from '@solana/wallet-adapter-base'; + +interface BlockhashAndFeeCalculator { + blockhash: Blockhash; + feeCalculator: FeeCalculator; +} + +export const DEFAULT_TIMEOUT = 60000; + +export const getErrorForTransaction = async ( connection: Connection, txid: string, - ) => { - // wait for all confirmation before geting transaction - await connection.confirmTransaction(txid, 'max'); - - const tx = await connection.getParsedConfirmedTransaction(txid); - - const errors: string[] = []; - if (tx?.meta && tx.meta.logMessages) { - tx.meta.logMessages.forEach(log => { - const regex = /Error: (.*)/gm; - let m; - while ((m = regex.exec(log)) !== null) { - // This is necessary to avoid infinite loops with zero-width matches - if (m.index === regex.lastIndex) { - regex.lastIndex++; - } - - if (m.length > 1) { - errors.push(m[1]); - } +) => { + // wait for all confirmation before geting transaction + await connection.confirmTransaction(txid, 'max'); + + const tx = await connection.getParsedConfirmedTransaction(txid); + + const errors: string[] = []; + if (tx?.meta && tx.meta.logMessages) { + tx.meta.logMessages.forEach(log => { + const regex = /Error: (.*)/gm; + let m; + while ((m = regex.exec(log)) !== null) { + // This is necessary to avoid infinite loops with zero-width matches + if (m.index === regex.lastIndex) { + regex.lastIndex++; } - }); - } - - return errors; - }; - - export enum SequenceType { - Sequential, - Parallel, - StopOnFailure, + + if (m.length > 1) { + errors.push(m[1]); + } + } + }); } - - export async function sendTransactionsWithManualRetry( + + return errors; +}; + +export enum SequenceType { + Sequential, + Parallel, + StopOnFailure, +} + +export async function sendTransactionsWithManualRetry( connection: Connection, wallet: any, instructions: TransactionInstruction[][], signers: Keypair[][], - ): Promise<(string | undefined)[]> { - let stopPoint = 0; - let tries = 0; - let lastInstructionsLength = null; - let toRemoveSigners: Record = {}; - instructions = instructions.filter((instr, i) => { - if (instr.length > 0) { - return true; - } else { - toRemoveSigners[i] = true; - return false; - } - }); - let ids: string[] = []; - let filteredSigners = signers.filter((_, i) => !toRemoveSigners[i]); - - while (stopPoint < instructions.length && tries < 3) { - instructions = instructions.slice(stopPoint, instructions.length); - filteredSigners = filteredSigners.slice(stopPoint, filteredSigners.length); - - if (instructions.length === lastInstructionsLength) tries = tries + 1; - else tries = 0; - - try { - if (instructions.length === 1) { - const id = await sendTransactionWithRetry( +): Promise<(string | undefined)[]> { + let stopPoint = 0; + let tries = 0; + let lastInstructionsLength = null; + let toRemoveSigners: Record = {}; + instructions = instructions.filter((instr, i) => { + if (instr.length > 0) { + return true; + } else { + toRemoveSigners[i] = true; + return false; + } + }); + let ids: string[] = []; + let filteredSigners = signers.filter((_, i) => !toRemoveSigners[i]); + + while (stopPoint < instructions.length && tries < 3) { + instructions = instructions.slice(stopPoint, instructions.length); + filteredSigners = filteredSigners.slice(stopPoint, filteredSigners.length); + + if (instructions.length === lastInstructionsLength) tries = tries + 1; + else tries = 0; + + try { + if (instructions.length === 1) { + const id = await sendTransactionWithRetry( connection, wallet, instructions[0], filteredSigners[0], 'single', - ); - ids.push(id.txid); - stopPoint = 1; - } else { - const { txs } = await sendTransactions( + ); + ids.push(id.txid); + stopPoint = 1; + } else { + const { txs } = await sendTransactions( connection, wallet, instructions, filteredSigners, SequenceType.StopOnFailure, 'single', - ); - ids = ids.concat(txs.map(t => t.txid)); - } - } catch (e) { - console.error(e); + ); + ids = ids.concat(txs.map(t => t.txid)); } - console.log( + } catch (e) { + console.error(e); + } + console.log( 'Died on ', stopPoint, 'retrying from instruction', instructions[stopPoint], 'instructions length is', instructions.length, - ); - lastInstructionsLength = instructions.length; - } - - return ids; + ); + lastInstructionsLength = instructions.length; } - - export const sendTransactions = async ( + + return ids; +} + +export const sendTransactions = async ( connection: Connection, wallet: any, instructionSet: TransactionInstruction[][], signersSet: Keypair[][], sequenceType: SequenceType = SequenceType.Parallel, commitment: Commitment = 'singleGossip', - successCallback: (txid: string, ind: number) => void = (txid, ind) => { }, + successCallback: (txid: string, ind: number) => void = (txid, ind) => {}, failCallback: (reason: string, ind: number) => boolean = (txid, ind) => false, block?: BlockhashAndFeeCalculator, - ): Promise<{ number: number; txs: { txid: string; slot: number }[] }> => { - if (!wallet.publicKey) throw new WalletNotConnectedError(); - - const unsignedTxns: Transaction[] = []; - - if (!block) { - block = await connection.getRecentBlockhash(commitment); + beforeTransactions: Transaction[] = [], + afterTransactions: Transaction[] = [], +): Promise<{ number: number; txs: { txid: string; slot: number }[] }> => { + if (!wallet.publicKey) throw new WalletNotConnectedError(); + + const unsignedTxns: Transaction[] = beforeTransactions; + + if (!block) { + block = await connection.getRecentBlockhash(commitment); + } + + for (let i = 0; i < instructionSet.length; i++) { + const instructions = instructionSet[i]; + const signers = signersSet[i]; + + if (instructions.length === 0) { + continue; } - - for (let i = 0; i < instructionSet.length; i++) { - const instructions = instructionSet[i]; - const signers = signersSet[i]; - - if (instructions.length === 0) { - continue; - } - - let transaction = new Transaction(); - instructions.forEach(instruction => transaction.add(instruction)); - transaction.recentBlockhash = block.blockhash; - transaction.setSigners( + + let transaction = new Transaction(); + instructions.forEach(instruction => transaction.add(instruction)); + transaction.recentBlockhash = block.blockhash; + transaction.setSigners( // fee payed by the wallet owner wallet.publicKey, ...signers.map(s => s.publicKey), - ); - - if (signers.length > 0) { - transaction.partialSign(...signers); - } - - unsignedTxns.push(transaction); + ); + + if (signers.length > 0) { + transaction.partialSign(...signers); } - - const signedTxns = await wallet.signAllTransactions(unsignedTxns); - - const pendingTxns: Promise<{ txid: string; slot: number }>[] = []; - - let breakEarlyObject = { breakEarly: false, i: 0 }; - console.log( + + unsignedTxns.push(transaction); + } + unsignedTxns.push(...afterTransactions); + + const partiallySignedTransactions = unsignedTxns.filter(t => + t.signatures.find(sig => sig.publicKey.equals(wallet.publicKey)), + ); + const fullySignedTransactions = unsignedTxns.filter( + t => !t.signatures.find(sig => sig.publicKey.equals(wallet.publicKey)), + ); + let signedTxns = await wallet.signAllTransactions( + partiallySignedTransactions, + ); + signedTxns = fullySignedTransactions.concat(signedTxns); + const pendingTxns: Promise<{ txid: string; slot: number }>[] = []; + + console.log( 'Signed txns length', signedTxns.length, 'vs handed in length', instructionSet.length, - ); - for (let i = 0; i < signedTxns.length; i++) { - const signedTxnPromise = sendSignedTransaction({ - connection, - signedTransaction: signedTxns[i], - }); - - signedTxnPromise - .then(({ txid, slot }) => { - successCallback(txid, i); - }) - .catch(reason => { - // @ts-ignore - failCallback(signedTxns[i], i); - if (sequenceType === SequenceType.StopOnFailure) { - breakEarlyObject.breakEarly = true; - breakEarlyObject.i = i; - } - }); - - if (sequenceType !== SequenceType.Parallel) { - try { - await signedTxnPromise; - } catch (e) { - console.log('Caught failure', e); - if (breakEarlyObject.breakEarly) { - console.log('Died on ', breakEarlyObject.i); - // Return the txn we failed on by index - return { - number: breakEarlyObject.i, - txs: await Promise.all(pendingTxns), - }; - } - } - } else { + ); + for (let i = 0; i < signedTxns.length; i++) { + const signedTxnPromise = sendSignedTransaction({ + connection, + signedTransaction: signedTxns[i], + }); + + if (sequenceType !== SequenceType.Parallel) { + try { + await signedTxnPromise.then(({ txid, slot }) => + successCallback(txid, i), + ); pendingTxns.push(signedTxnPromise); + } catch (e) { + console.log('Failed at txn index:', i); + console.log('Caught failure:', e); + + failCallback(signedTxns[i], i); + if (sequenceType === SequenceType.StopOnFailure) { + return { + number: i, + txs: await Promise.all(pendingTxns), + }; + } } + } else { + pendingTxns.push(signedTxnPromise); } - - if (sequenceType !== SequenceType.Parallel) { - await Promise.all(pendingTxns); - } - - return { number: signedTxns.length, txs: await Promise.all(pendingTxns) }; - }; - - export const sendTransaction = async ( + } + + if (sequenceType !== SequenceType.Parallel) { + const result = await Promise.all(pendingTxns); + return { number: signedTxns.length, txs: result }; + } + + return { number: signedTxns.length, txs: await Promise.all(pendingTxns) }; +}; + +export const sendTransaction = async ( connection: Connection, wallet: any, - instructions: TransactionInstruction[], + instructions: TransactionInstruction[] | Transaction, signers: Keypair[], awaitConfirmation = true, commitment: Commitment = 'singleGossip', includesFeePayer: boolean = false, block?: BlockhashAndFeeCalculator, - ) => { - if (!wallet.publicKey) throw new WalletNotConnectedError(); - - let transaction = new Transaction(); +) => { + if (!wallet.publicKey) throw new WalletNotConnectedError(); + + let transaction: Transaction; + if (!Array.isArray(instructions)) { + transaction = instructions; + } else { + transaction = new Transaction(); instructions.forEach(instruction => transaction.add(instruction)); transaction.recentBlockhash = ( - block || (await connection.getRecentBlockhash(commitment)) + block || (await connection.getRecentBlockhash(commitment)) ).blockhash; - + if (includesFeePayer) { transaction.setSigners(...signers.map(s => s.publicKey)); } else { transaction.setSigners( - // fee payed by the wallet owner - wallet.publicKey, - ...signers.map(s => s.publicKey), + // fee payed by the wallet owner + wallet.publicKey, + ...signers.map(s => s.publicKey), ); } - + if (signers.length > 0) { transaction.partialSign(...signers); } if (!includesFeePayer) { transaction = await wallet.signTransaction(transaction); } - - const rawTransaction = transaction.serialize(); - let options = { - skipPreflight: true, - commitment, - }; - - const txid = await connection.sendRawTransaction(rawTransaction, options); - let slot = 0; - - if (awaitConfirmation) { - const confirmation = await awaitTransactionSignatureConfirmation( + } + + const rawTransaction = transaction.serialize(); + let options = { + skipPreflight: true, + commitment, + }; + + const txid = await connection.sendRawTransaction(rawTransaction, options); + let slot = 0; + + if (awaitConfirmation) { + const confirmation = await awaitTransactionSignatureConfirmation( txid, DEFAULT_TIMEOUT, connection, commitment, - ); - - if (!confirmation) - throw new Error('Timed out awaiting confirmation on transaction'); - slot = confirmation?.slot || 0; - - if (confirmation?.err) { - const errors = await getErrorForTransaction(connection, txid); - - console.log(errors); - throw new Error(`Raw transaction ${txid} failed`); - } + ); + + if (!confirmation) + throw new Error('Timed out awaiting confirmation on transaction'); + slot = confirmation?.slot || 0; + + if (confirmation?.err) { + const errors = await getErrorForTransaction(connection, txid); + + console.log(errors); + throw new Error(`Raw transaction ${txid} failed`); } - - return { txid, slot }; - }; - - export const sendTransactionWithRetry = async ( + } + + return { txid, slot }; +}; + +export const sendTransactionWithRetry = async ( connection: Connection, wallet: any, instructions: TransactionInstruction[], @@ -297,185 +306,184 @@ import { includesFeePayer: boolean = false, block?: BlockhashAndFeeCalculator, beforeSend?: () => void, - ) => { - if (!wallet.publicKey) throw new WalletNotConnectedError(); - - let transaction = new Transaction(); - instructions.forEach(instruction => transaction.add(instruction)); - transaction.recentBlockhash = ( +) => { + if (!wallet.publicKey) throw new WalletNotConnectedError(); + + let transaction = new Transaction(); + instructions.forEach(instruction => transaction.add(instruction)); + transaction.recentBlockhash = ( block || (await connection.getRecentBlockhash(commitment)) - ).blockhash; - - if (includesFeePayer) { - transaction.setSigners(...signers.map(s => s.publicKey)); - } else { - transaction.setSigners( + ).blockhash; + + if (includesFeePayer) { + transaction.setSigners(...signers.map(s => s.publicKey)); + } else { + transaction.setSigners( // fee payed by the wallet owner wallet.publicKey, ...signers.map(s => s.publicKey), - ); - } - - if (signers.length > 0) { - transaction.partialSign(...signers); - } - if (!includesFeePayer) { - transaction = await wallet.signTransaction(transaction); - } - - if (beforeSend) { - beforeSend(); - } - - const { txid, slot } = await sendSignedTransaction({ - connection, - signedTransaction: transaction, - }); - - return { txid, slot }; - }; - - export const getUnixTs = () => { - return new Date().getTime() / 1000; - }; - - const DEFAULT_TIMEOUT = 100000; - - export async function sendSignedTransaction({ - signedTransaction, + ); + } + + if (signers.length > 0) { + transaction.partialSign(...signers); + } + if (!includesFeePayer) { + transaction = await wallet.signTransaction(transaction); + } + + if (beforeSend) { + beforeSend(); + } + + const { txid, slot } = await sendSignedTransaction({ connection, - timeout = DEFAULT_TIMEOUT, - }: { - signedTransaction: Transaction; - connection: Connection; - sendingMessage?: string; - sentMessage?: string; - successMessage?: string; - timeout?: number; - }): Promise<{ txid: string; slot: number }> { - const rawTransaction = signedTransaction.serialize(); - const startTime = getUnixTs(); - let slot = 0; - const txid: TransactionSignature = await connection.sendRawTransaction( + signedTransaction: transaction, + }); + + return { txid, slot }; +}; + +export const getUnixTs = () => { + return new Date().getTime() / 1000; +}; + +export async function sendSignedTransaction({ + signedTransaction, + connection, + timeout = DEFAULT_TIMEOUT, + }: { + signedTransaction: Transaction; + connection: Connection; + sendingMessage?: string; + sentMessage?: string; + successMessage?: string; + timeout?: number; +}): Promise<{ txid: string; slot: number }> { + const rawTransaction = signedTransaction.serialize(); + + const startTime = getUnixTs(); + let slot = 0; + const txid: TransactionSignature = await connection.sendRawTransaction( rawTransaction, { skipPreflight: true, }, - ); - - console.log('Started awaiting confirmation for', txid); - - let done = false; - (async () => { - while (!done && getUnixTs() - startTime < timeout) { - connection.sendRawTransaction(rawTransaction, { - skipPreflight: true, - }); - await sleep(500); - } - })(); - try { - const confirmation = await awaitTransactionSignatureConfirmation( + ); + + console.log('Started awaiting confirmation for', txid); + + let done = false; + (async () => { + while (!done && getUnixTs() - startTime < timeout) { + connection.sendRawTransaction(rawTransaction, { + skipPreflight: true, + }); + await sleep(500); + } + })(); + try { + const confirmation = await awaitTransactionSignatureConfirmation( txid, timeout, connection, 'recent', true, - ); - - if (!confirmation) - throw new Error('Timed out awaiting confirmation on transaction'); - - if (confirmation.err) { - console.error(confirmation.err); - throw new Error('Transaction failed: Custom instruction error'); - } - - slot = confirmation?.slot || 0; - } catch (err: any) { - console.error('Timeout Error caught', err); - if (err.timeout) { - throw new Error('Timed out awaiting confirmation on transaction'); - } - let simulateResult: SimulatedTransactionResponse | null = null; - try { - simulateResult = ( + ); + + if (!confirmation) + throw new Error('Timed out awaiting confirmation on transaction'); + + if (confirmation.err) { + console.error(confirmation.err); + throw new Error('Transaction failed: Custom instruction error'); + } + + slot = confirmation?.slot || 0; + } catch (err: any) { + console.error('Timeout Error caught', err); + if (err.timeout) { + throw new Error('Timed out awaiting confirmation on transaction'); + } + let simulateResult: SimulatedTransactionResponse | null = null; + try { + simulateResult = ( await simulateTransaction(connection, signedTransaction, 'single') - ).value; - } catch (e) { } - if (simulateResult && simulateResult.err) { - if (simulateResult.logs) { - for (let i = simulateResult.logs.length - 1; i >= 0; --i) { - const line = simulateResult.logs[i]; - if (line.startsWith('Program log: ')) { - throw new Error( + ).value; + } catch (e) {} + if (simulateResult && simulateResult.err) { + if (simulateResult.logs) { + for (let i = simulateResult.logs.length - 1; i >= 0; --i) { + const line = simulateResult.logs[i]; + if (line.startsWith('Program log: ')) { + throw new Error( 'Transaction failed: ' + line.slice('Program log: '.length), - ); - } + ); } } - throw new Error(JSON.stringify(simulateResult.err)); } - // throw new Error('Transaction failed'); - } finally { - done = true; + throw new Error(JSON.stringify(simulateResult.err)); } - - console.log('Latency', txid, getUnixTs() - startTime); - return { txid, slot }; + // throw new Error('Transaction failed'); + } finally { + done = true; } - - async function simulateTransaction( + + console.log('Latency', txid, getUnixTs() - startTime); + return { txid, slot }; +} + +async function simulateTransaction( connection: Connection, transaction: Transaction, commitment: Commitment, - ): Promise> { - // @ts-ignore - transaction.recentBlockhash = await connection._recentBlockhash( +): Promise> { + // @ts-ignore + transaction.recentBlockhash = await connection._recentBlockhash( // @ts-ignore connection._disableBlockhashCaching, - ); - - const signData = transaction.serializeMessage(); - // @ts-ignore - const wireTransaction = transaction._serialize(signData); - const encodedTransaction = wireTransaction.toString('base64'); - const config: any = { encoding: 'base64', commitment }; - const args = [encodedTransaction, config]; - - // @ts-ignore - const res = await connection._rpcRequest('simulateTransaction', args); - if (res.error) { - throw new Error('failed to simulate transaction: ' + res.error.message); - } - return res.result; + ); + + const signData = transaction.serializeMessage(); + // @ts-ignore + const wireTransaction = transaction._serialize(signData); + const encodedTransaction = wireTransaction.toString('base64'); + const config: any = { encoding: 'base64', commitment }; + const args = [encodedTransaction, config]; + + // @ts-ignore + const res = await connection._rpcRequest('simulateTransaction', args); + if (res.error) { + throw new Error('failed to simulate transaction: ' + res.error.message); } - - async function awaitTransactionSignatureConfirmation( + return res.result; +} + +async function awaitTransactionSignatureConfirmation( txid: TransactionSignature, timeout: number, connection: Connection, commitment: Commitment = 'recent', queryStatus = false, - ): Promise { - let done = false; - let status: SignatureStatus | null | void = { - slot: 0, - confirmations: 0, - err: null, - }; - let subId = 0; - status = await new Promise(async (resolve, reject) => { - setTimeout(() => { - if (done) { - return; - } - done = true; - console.log('Rejecting for timeout...'); - reject({ timeout: true }); - }, timeout); - try { - subId = connection.onSignature( +): Promise { + let done = false; + let status: SignatureStatus | null | void = { + slot: 0, + confirmations: 0, + err: null, + }; + let subId = 0; + status = await new Promise(async (resolve, reject) => { + setTimeout(() => { + if (done) { + return; + } + done = true; + console.log('Rejecting for timeout...'); + reject({ timeout: true }); + }, timeout); + try { + subId = connection.onSignature( txid, (result, context) => { done = true; @@ -493,52 +501,49 @@ import { } }, commitment, - ); - } catch (e) { - done = true; - console.error('WS error in setup', txid, e); - } - while (!done && queryStatus) { - // eslint-disable-next-line no-loop-func - (async () => { - try { - const signatureStatuses = await connection.getSignatureStatuses([ - txid, - ]); - status = signatureStatuses && signatureStatuses.value[0]; - if (!done) { - if (!status) { - console.log('REST null result for', txid, status); - } else if (status.err) { - console.log('REST error for', txid, status); - done = true; - reject(status.err); - } else if (!status.confirmations) { - console.log('REST no confirmations for', txid, status); - } else { - console.log('REST confirmation for', txid, status); - done = true; - resolve(status); - } - } - } catch (e) { - if (!done) { - console.log('REST connection error: txid', txid, e); + ); + } catch (e) { + done = true; + console.error('WS error in setup', txid, e); + } + while (!done && queryStatus) { + // eslint-disable-next-line no-loop-func + (async () => { + try { + const signatureStatuses = await connection.getSignatureStatuses([ + txid, + ]); + status = signatureStatuses && signatureStatuses.value[0]; + if (!done) { + if (!status) { + console.log('REST null result for', txid, status); + } else if (status.err) { + console.log('REST error for', txid, status); + done = true; + reject(status.err); + } else if (!status.confirmations) { + console.log('REST no confirmations for', txid, status); + } else { + console.log('REST confirmation for', txid, status); + done = true; + resolve(status); } } - })(); - await sleep(2000); - } - }); - - //@ts-ignore - if (connection._signatureSubscriptions[subId]) - connection.removeSignatureListener(subId); - done = true; - console.log('Returning status', status); - return status; - } - export function sleep(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); - } - \ No newline at end of file + } catch (e) { + if (!done) { + console.log('REST connection error: txid', txid, e); + } + } + })(); + await sleep(2000); + } + }); + + + done = true; + console.log('Returning status', status); + return status; +} +export function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/src/utils.ts b/src/utils.ts index 59eeff7..5c438bf 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -11,6 +11,7 @@ export interface AlertState { open: boolean; message: string; severity: 'success' | 'info' | 'warning' | 'error' | undefined; + hideDuration?: number | null; } export const toDate = (value?: anchor.BN) => { @@ -45,51 +46,51 @@ export const formatNumber = { }; export const SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID = - new anchor.web3.PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'); + new anchor.web3.PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'); export const CIVIC = new anchor.web3.PublicKey( - 'gatem74V238djXdzWnJf94Wo1DcnuGkfijbf3AuBhfs', + 'gatem74V238djXdzWnJf94Wo1DcnuGkfijbf3AuBhfs', ); export const getAtaForMint = async ( - mint: anchor.web3.PublicKey, - buyer: anchor.web3.PublicKey, + mint: anchor.web3.PublicKey, + buyer: anchor.web3.PublicKey, ): Promise<[anchor.web3.PublicKey, number]> => { return await anchor.web3.PublicKey.findProgramAddress( - [buyer.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()], - SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID, + [buyer.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()], + SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID, ); }; export const getNetworkExpire = async ( - gatekeeperNetwork: anchor.web3.PublicKey, + gatekeeperNetwork: anchor.web3.PublicKey, ): Promise<[anchor.web3.PublicKey, number]> => { return await anchor.web3.PublicKey.findProgramAddress( - [gatekeeperNetwork.toBuffer(), Buffer.from('expire')], - CIVIC, + [gatekeeperNetwork.toBuffer(), Buffer.from('expire')], + CIVIC, ); }; export const getNetworkToken = async ( - wallet: anchor.web3.PublicKey, - gatekeeperNetwork: anchor.web3.PublicKey, + wallet: anchor.web3.PublicKey, + gatekeeperNetwork: anchor.web3.PublicKey, ): Promise<[anchor.web3.PublicKey, number]> => { return await anchor.web3.PublicKey.findProgramAddress( - [ - wallet.toBuffer(), - Buffer.from('gateway'), - Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), - gatekeeperNetwork.toBuffer(), - ], - CIVIC, + [ + wallet.toBuffer(), + Buffer.from('gateway'), + Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), + gatekeeperNetwork.toBuffer(), + ], + CIVIC, ); }; export function createAssociatedTokenAccountInstruction( - associatedTokenAddress: anchor.web3.PublicKey, - payer: anchor.web3.PublicKey, - walletAddress: anchor.web3.PublicKey, - splTokenMintAddress: anchor.web3.PublicKey, + associatedTokenAddress: anchor.web3.PublicKey, + payer: anchor.web3.PublicKey, + walletAddress: anchor.web3.PublicKey, + splTokenMintAddress: anchor.web3.PublicKey, ) { const keys = [ {