Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: Support permissioned candymachines - Basic #47

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

# production
/build
/out

# misc
.DS_Store
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/components/recaptcha-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
29 changes: 23 additions & 6 deletions src/hooks/use-candy-machine.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
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";
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)

Expand Down Expand Up @@ -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<anchor.web3.PublicKey | undefined>();
const [gatewayToken, setGatewayToken] = useState<GatewayToken | undefined>();

// 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 () => {
Expand All @@ -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,
Expand All @@ -65,6 +79,7 @@ export default function useCandyMachine() {
setIsSoldOut(itemsRemaining === 0);
setMintStartDate(goLiveDate);
setCandyMachine(candyMachine);
setGatekeeperNetwork(gatekeeperNetwork);
})();
}, [wallet, candyMachineId, connection]);

Expand Down Expand Up @@ -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,
Expand All @@ -109,7 +124,8 @@ export default function useCandyMachine() {
candyMachine,
config,
wallet.publicKey,
treasury
treasury,
gatekeeperNetwork
);

const status = await awaitTransactionSignatureConfirmation(
Expand Down Expand Up @@ -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,
Expand All @@ -176,6 +192,7 @@ export default function useCandyMachine() {
config,
wallet.publicKey,
treasury,
gatekeeperNetwork,
quantity
);

Expand Down Expand Up @@ -249,5 +266,5 @@ export default function useCandyMachine() {
};


return { isSoldOut, mintStartDate, isMinting, nftsData, onMint, onMintMultiple }
return { isSoldOut, mintStartDate, isMinting, nftsData, onMint, onMintMultiple, walletPermissioned }
}
21 changes: 16 additions & 5 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<main className="p-5">
<Toaster />
Expand Down Expand Up @@ -47,7 +49,14 @@ const Home = () => {
</span>}

{wallet.connected &&
<p className="text-gray-800 font-bold text-lg cursor-default">Address: {shortenAddress(wallet.publicKey?.toBase58() || "")}</p>
<div className="inline-flex" title={walletPermissioned ? 'Wallet is permitted to mint' : 'Wallet is not permitted to mint'}>
{ walletPermissioned !== undefined && (
walletPermissioned ?
<FontAwesomeIcon icon={faCheckCircle} className="w-4" color="green" /> :
<FontAwesomeIcon icon={faTimesCircle} className="w-4" color="red" />
)}
<p className="text-gray-800 font-bold text-lg cursor-default">Address: {shortenAddress(wallet.publicKey?.toBase58() || "")}</p>
</div>
}

{wallet.connected &&
Expand All @@ -61,7 +70,8 @@ const Home = () => {
{wallet.connected &&
<RecaptchaButton
actionName="mint"
disabled={isSoldOut || isMinting || !isActive}
// checking explicitly for walletPermissioned === false as undefined means "not needed"
disabled={isSoldOut || isMinting || !isActive || (walletPermissioned === false)}
onClick={onMint}
>
{isSoldOut ? (
Expand All @@ -81,7 +91,8 @@ const Home = () => {
{wallet.connected &&
<RecaptchaButton
actionName="mint5"
disabled={isSoldOut || isMinting || !isActive}
// checking explicitly for walletPermissioned === false as undefined means "not needed"
disabled={isSoldOut || isMinting || !isActive || (walletPermissioned === false)}
onClick={() => onMintMultiple(5)}
>
{isSoldOut ? (
Expand Down
42 changes: 31 additions & 11 deletions src/utils/candy-machine.ts
Original file line number Diff line number Diff line change
@@ -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"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented code should not be pushed to GitHub.

"gcmJfhh9k7hiEbKYb4ehHEQJGrdtCrmvxw1bgiB56Vb"
);

const SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID = new anchor.web3.PublicKey(
Expand All @@ -34,6 +32,7 @@ interface CandyMachineState {
itemsRedeemed: number;
itemsRemaining: number;
goLiveDate: Date,
gatekeeperNetwork?: anchor.web3.PublicKey,
}

export const awaitTransactionSignatureConfirmation = async (
Expand Down Expand Up @@ -168,27 +167,31 @@ export const getCandyMachineState = async (
CANDY_MACHINE_PROGRAM,
provider
);

const program = new anchor.Program(idl!, CANDY_MACHINE_PROGRAM, provider);
const candyMachine = {
id: candyMachineId,
connection,
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;

let goLiveDate = state.data.goLiveDate.toNumber();
goLiveDate = new Date(goLiveDate * 1000);

const gatekeeperNetwork = state.data.gatekeeperNetwork;

return {
candyMachine,
itemsAvailable,
itemsRedeemed,
itemsRemaining,
goLiveDate,
gatekeeperNetwork
};
}

Expand Down Expand Up @@ -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<string> => {
const mint = anchor.web3.Keypair.generate();
const token = await getTokenWallet(payer, mint.publicKey);
Expand All @@ -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: {
Expand All @@ -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({
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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];
Expand Down
9 changes: 9 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down