From 55919dff42db7d98cacd8789e22e55a93bea6a28 Mon Sep 17 00:00:00 2001 From: dankelleher Date: Fri, 22 Oct 2021 11:20:40 +0200 Subject: [PATCH] Support permissioned candymachines --- .gitignore | 1 + package.json | 1 + src/components/recaptcha-button.tsx | 1 + src/hooks/use-candy-machine.ts | 29 +++++++++++++++----- src/pages/index.tsx | 21 +++++++++++---- src/utils/candy-machine.ts | 42 +++++++++++++++++++++-------- yarn.lock | 9 +++++++ 7 files changed, 82 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 22b18997..e2bef181 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ # production /build +/out # misc .DS_Store diff --git a/package.json b/package.json index f52dc9b6..24f0230c 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/react-fontawesome": "^0.1.15", "@headlessui/react": "^1.4.1", + "@identity.com/solana-gateway-ts": "^0.3.3", "@metaplex/js": "^1.1.1", "@project-serum/anchor": "0.17.1-beta.1", "@solana/spl-token": "^0.1.8", diff --git a/src/components/recaptcha-button.tsx b/src/components/recaptcha-button.tsx index 84886af2..de712f95 100644 --- a/src/components/recaptcha-button.tsx +++ b/src/components/recaptcha-button.tsx @@ -18,6 +18,7 @@ export const RecaptchaButton = ({ const handleReCaptchaVerify = useCallback(async () => { if (!executeRecaptcha) { console.debug('Execute recaptcha not yet available'); + await onClick() return; } setValidating(true) diff --git a/src/hooks/use-candy-machine.ts b/src/hooks/use-candy-machine.ts index 27ebff6a..769cff5d 100644 --- a/src/hooks/use-candy-machine.ts +++ b/src/hooks/use-candy-machine.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import {useEffect, useState} from "react"; import * as anchor from "@project-serum/anchor"; import { awaitTransactionSignatureConfirmation, CandyMachine, getCandyMachineState, mintOneToken, mintMultipleToken } from "../utils/candy-machine"; import { useWallet } from "@solana/wallet-adapter-react"; @@ -6,6 +6,7 @@ import toast from 'react-hot-toast'; import useWalletBalance from "./use-wallet-balance"; import { LAMPORTS_PER_SOL } from "@solana/web3.js"; import { sleep } from "../utils/utility"; +import {findGatewayToken, GatewayToken } from "@identity.com/solana-gateway-ts"; const MINT_PRICE_SOL = Number(process.env.NEXT_MINT_PRICE_SOL) @@ -38,6 +39,19 @@ export default function useCandyMachine() { const [isMinting, setIsMinting] = useState(false); const [isSoldOut, setIsSoldOut] = useState(false); const [mintStartDate, setMintStartDate] = useState(new Date(parseInt(process.env.NEXT_PUBLIC_CANDY_START_DATE!, 10))); + const [gatekeeperNetwork, setGatekeeperNetwork] = useState(); + const [gatewayToken, setGatewayToken] = useState(); + + // a wallet is allowed to mint if the candymachine is not permissioned, or if there is a gateway token present + const walletPermissioned = gatekeeperNetwork ? !!gatewayToken : undefined; + + useEffect(() => { + (async () => { + if (!gatekeeperNetwork || !candyMachine || !wallet || !wallet.publicKey) return; + const foundToken = await findGatewayToken(candyMachine.connection, wallet.publicKey, gatekeeperNetwork); + setGatewayToken(foundToken ? foundToken : undefined) + })(); + }, [gatekeeperNetwork, setGatewayToken, candyMachine, wallet]) useEffect(() => { (async () => { @@ -55,7 +69,7 @@ export default function useCandyMachine() { signAllTransactions: wallet.signAllTransactions, signTransaction: wallet.signTransaction, } as anchor.Wallet; - const { candyMachine, goLiveDate, itemsRemaining } = + const { candyMachine, goLiveDate, itemsRemaining, gatekeeperNetwork } = await getCandyMachineState( anchorWallet, candyMachineId, @@ -65,6 +79,7 @@ export default function useCandyMachine() { setIsSoldOut(itemsRemaining === 0); setMintStartDate(goLiveDate); setCandyMachine(candyMachine); + setGatekeeperNetwork(gatekeeperNetwork); })(); }, [wallet, candyMachineId, connection]); @@ -97,7 +112,7 @@ export default function useCandyMachine() { signAllTransactions: wallet.signAllTransactions, signTransaction: wallet.signTransaction, } as anchor.Wallet; - const { candyMachine } = + const { candyMachine, gatekeeperNetwork } = await getCandyMachineState( anchorWallet, candyMachineId, @@ -109,7 +124,8 @@ export default function useCandyMachine() { candyMachine, config, wallet.publicKey, - treasury + treasury, + gatekeeperNetwork ); const status = await awaitTransactionSignatureConfirmation( @@ -161,7 +177,7 @@ export default function useCandyMachine() { signAllTransactions: wallet.signAllTransactions, signTransaction: wallet.signTransaction, } as anchor.Wallet; - const { candyMachine } = + const { candyMachine, gatekeeperNetwork } = await getCandyMachineState( anchorWallet, candyMachineId, @@ -176,6 +192,7 @@ export default function useCandyMachine() { config, wallet.publicKey, treasury, + gatekeeperNetwork, quantity ); @@ -249,5 +266,5 @@ export default function useCandyMachine() { }; - return { isSoldOut, mintStartDate, isMinting, nftsData, onMint, onMintMultiple } + return { isSoldOut, mintStartDate, isMinting, nftsData, onMint, onMintMultiple, walletPermissioned } } \ No newline at end of file diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 07fe216b..b8cc81f9 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -10,14 +10,16 @@ import useWalletBalance from '../hooks/use-wallet-balance'; import { shortenAddress } from '../utils/candy-machine'; import Countdown from 'react-countdown'; import { RecaptchaButton } from '../components/recaptcha-button'; +import {faCheckCircle, faTimesCircle} from "@fortawesome/free-solid-svg-icons"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; const Home = () => { const [balance] = useWalletBalance() const [isActive, setIsActive] = useState(false); const wallet = useWallet(); - const { isSoldOut, mintStartDate, isMinting, onMint, onMintMultiple, nftsData } = useCandyMachine() - + const { isSoldOut, mintStartDate, isMinting, onMint, onMintMultiple, nftsData, walletPermissioned } = useCandyMachine() + return (
@@ -47,7 +49,14 @@ const Home = () => { } {wallet.connected && -

Address: {shortenAddress(wallet.publicKey?.toBase58() || "")}

+
+ { walletPermissioned !== undefined && ( + walletPermissioned ? + : + + )} +

Address: {shortenAddress(wallet.publicKey?.toBase58() || "")}

+
} {wallet.connected && @@ -61,7 +70,8 @@ const Home = () => { {wallet.connected && {isSoldOut ? ( @@ -81,7 +91,8 @@ const Home = () => { {wallet.connected && onMintMultiple(5)} > {isSoldOut ? ( diff --git a/src/utils/candy-machine.ts b/src/utils/candy-machine.ts index a024527b..18a915e3 100644 --- a/src/utils/candy-machine.ts +++ b/src/utils/candy-machine.ts @@ -1,17 +1,15 @@ import * as anchor from "@project-serum/anchor"; -import { - MintLayout, - TOKEN_PROGRAM_ID, - Token, -} from "@solana/spl-token"; -import { Metadata } from '@metaplex/js'; +import {MintLayout, Token, TOKEN_PROGRAM_ID,} from "@solana/spl-token"; +import {Metadata} from '@metaplex/js'; import axios from "axios"; -import { sendTransactions } from "./utility"; -import { fetchHashTable } from "../hooks/use-hash-table"; +import {sendTransactions} from "./utility"; +import {fetchHashTable} from "../hooks/use-hash-table"; +import {findGatewayToken} from "@identity.com/solana-gateway-ts"; export const CANDY_MACHINE_PROGRAM = new anchor.web3.PublicKey( - "cndyAnrLdpjq1Ssp1z8xxDsB8dxe7u4HL5Nxi2K5WXZ" + // "cndyAnrLdpjq1Ssp1z8xxDsB8dxe7u4HL5Nxi2K5WXZ" + "gcmJfhh9k7hiEbKYb4ehHEQJGrdtCrmvxw1bgiB56Vb" ); const SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID = new anchor.web3.PublicKey( @@ -34,6 +32,7 @@ interface CandyMachineState { itemsRedeemed: number; itemsRemaining: number; goLiveDate: Date, + gatekeeperNetwork?: anchor.web3.PublicKey, } export const awaitTransactionSignatureConfirmation = async ( @@ -168,7 +167,7 @@ export const getCandyMachineState = async ( CANDY_MACHINE_PROGRAM, provider ); - + const program = new anchor.Program(idl!, CANDY_MACHINE_PROGRAM, provider); const candyMachine = { id: candyMachineId, @@ -176,6 +175,7 @@ export const getCandyMachineState = async ( program, } const state: any = await program.account.candyMachine.fetch(candyMachineId); + const itemsAvailable = state.data.itemsAvailable.toNumber(); const itemsRedeemed = state.itemsRedeemed.toNumber(); const itemsRemaining = itemsAvailable - itemsRedeemed; @@ -183,12 +183,15 @@ export const getCandyMachineState = async ( let goLiveDate = state.data.goLiveDate.toNumber(); goLiveDate = new Date(goLiveDate * 1000); + const gatekeeperNetwork = state.data.gatekeeperNetwork; + return { candyMachine, itemsAvailable, itemsRedeemed, itemsRemaining, goLiveDate, + gatekeeperNetwork }; } @@ -272,11 +275,23 @@ export async function getNftsForOwner(connection: anchor.web3.Connection, ownerA return allTokens } +const gatewayTokenToRemainingAccounts = (gatewayToken: anchor.web3.PublicKey | undefined) => (gatewayToken ? [{ + pubkey: gatewayToken, + isWritable: false, + isSigner: false, +}] : []) + +async function getRemainingAccounts(candyMachine: CandyMachine, payer: anchor.web3.PublicKey, gatekeeperNetwork: anchor.web3.PublicKey | undefined) { + const gatewayToken = gatekeeperNetwork ? await findGatewayToken(candyMachine.connection, payer, gatekeeperNetwork) : undefined; + return gatewayTokenToRemainingAccounts(gatewayToken?.publicKey); +} + export const mintOneToken = async ( candyMachine: CandyMachine, config: anchor.web3.PublicKey, payer: anchor.web3.PublicKey, treasury: anchor.web3.PublicKey, + gatekeeperNetwork?: anchor.web3.PublicKey, ): Promise => { const mint = anchor.web3.Keypair.generate(); const token = await getTokenWallet(payer, mint.publicKey); @@ -286,6 +301,7 @@ export const mintOneToken = async ( const rent = await connection.getMinimumBalanceForRentExemption( MintLayout.span ); + const remainingAccounts = await getRemainingAccounts(candyMachine, payer, gatekeeperNetwork); return await program.rpc.mintNft({ accounts: { @@ -304,6 +320,7 @@ export const mintOneToken = async ( rent: anchor.web3.SYSVAR_RENT_PUBKEY, clock: anchor.web3.SYSVAR_CLOCK_PUBKEY, }, + remainingAccounts, signers: [mint], instructions: [ anchor.web3.SystemProgram.createAccount({ @@ -351,10 +368,12 @@ export const mintMultipleToken = async ( config: anchor.web3.PublicKey, payer: anchor.web3.PublicKey, treasury: anchor.web3.PublicKey, + gatekeeperNetwork: anchor.web3.PublicKey | undefined, quantity: number = 2 ) => { const signersMatrix = [] const instructionsMatrix = [] + const remainingAccounts = await getRemainingAccounts(candyMachine, payer, gatekeeperNetwork); for (let index = 0; index < quantity; index++) { const mint = anchor.web3.Keypair.generate(); @@ -413,7 +432,8 @@ export const mintMultipleToken = async ( systemProgram: anchor.web3.SystemProgram.programId, rent: anchor.web3.SYSVAR_RENT_PUBKEY, clock: anchor.web3.SYSVAR_CLOCK_PUBKEY, - } + }, + remainingAccounts, }), ); const signers: anchor.web3.Keypair[] = [mint]; diff --git a/yarn.lock b/yarn.lock index ee61c643..6220b78e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -282,6 +282,15 @@ resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz" integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== +"@identity.com/solana-gateway-ts@^0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@identity.com/solana-gateway-ts/-/solana-gateway-ts-0.3.3.tgz#bb9ab03c57d39cd10f4a1f0a214e9b230f21cb91" + integrity sha512-lpFQZ8WDbPecorA0iNrsGVwPkkp1F4j56r51qKHEfARNKpeeRcNnutBfDxVVIWpV6oKL9yRPsbpwpa41tQ8YcQ== + dependencies: + "@solana/web3.js" "^1.22.0" + bn.js "^5.2.0" + borsh "^0.4.0" + "@json-rpc-tools/provider@^1.5.5": version "1.7.6" resolved "https://registry.npmjs.org/@json-rpc-tools/provider/-/provider-1.7.6.tgz"