Comment construire votre propre DAO en utilisant Next.js, thirdweb, MetaMask & Alchemy

Apprenez à créer un DAO, à créer des NFT, des jetons de largage aérien et à ajouter une fonctionnalité de vote en utilisant uniquement JavaScript dans ce didacticiel complet.

Qu'est-ce qu'un DAO ?

DAO signifie Organisation Autonome Décentralisée. Comme son nom l'indique, un DAO est une organisation sans chef unique; au lieu de cela, les règles sont encodées dans la blockchain. Pour cette raison, un DAO est complètement transparent et tous ceux qui y participent ont un intérêt. Les décisions importantes sont prises par vote parmi ceux qui possèdent des jetons non fongibles (NFT) du DAO, qui accordent l'adhésion.

Aujourd'hui, nous allons créer notre propre DAO en utilisant Next.js, thirdweb, MetaMask et Alchemy. Il permettra aux utilisateurs de frapper le NFT de votre DAO, de recevoir de la crypto-monnaie via des parachutages et de participer aux sondages du DAO. Ce didacticiel sera écrit uniquement avec JavaScript, vous n'avez donc pas besoin de connaître Solidity.

Conditions préalables

Pour comprendre et suivre ce didacticiel, vous devez disposer des éléments suivants :

  • Connaissance pratique de JavaScript, Next.js et de la blockchain
  • Un portefeuille MetaMask
  • Un compte chez Alchemy

Contenu

  • Installer
    • Créer une application Alchemy
    • Obtenir la clé privée du portefeuille
    • Ajouter .envdes variables
  • Ajout de la fonctionnalité de connexion à l'aide de MetaMask
    • Ajout du troisième fournisseur Web
    • Ajout du composant de connexion
    • Initialisation du SDK thirdweb
  • Ajout de fonctionnalités pour créer un NFT
    • Créer et configurer un NFT
    • Vérifier si l'utilisateur a déjà un NFT
    • Création d'un bouton pour créer des NFT
    • Ajouter des styles
  • Créer et déployer un jeton de gouvernance
    • Frappe de jetons
    • Jetons de largage aérien
    • Autoriser les utilisateurs à voter
    • Affichage des propositions sur le site Web
    • Obtenir les propositions
    • Rendre les propositions
  • Conclusion

Installer

Nous allons commencer par configurer une application Next.js avec la commande suivante :

npx créer-next-app mon-dao

Maintenant, nous allons créer un nouveau projet dans thirdweb . Allez sur thirdweb et connectez votre portefeuille MetaMask. Après avoir connecté votre portefeuille, cliquez sur Créer un projet et choisissez le réseau Rinkeby.

Donnez un nom et une description à votre projet, puis cliquez sur Créer . Si vous n'avez pas assez d'ETH pour payer l'essence, procurez-vous en à ce robinet .

Créer une application Alchemy

Ensuite, rendez-vous sur Alchemy, connectez-vous, cliquez sur Créer une application et fournissez les détails requis. Assurez-vous d'utiliser la même chaîne que celle que vous avez utilisée dans thirdweb - dans notre cas, il s'agit de la chaîne Ethereum et du réseau Rinkeby.

Création d'une application d'alchimie

Une fois l'application créée, copiez la clé d'API HTTP.

Obtenir la clé privée du portefeuille

Afin de créer des NFT et d'exécuter certains scripts, nous allons avoir besoin de la clé privée du portefeuille.

 

Pour y accéder, ouvrez l'extension de navigateur MetaMask et cliquez sur Détails du compte . Vous devriez voir votre clé privée ici; exportez-le et copiez-le dans un endroit sûr.

Ajouter .envdes variables

Ajoutons ces variables dans un .envfichier afin de pouvoir y accéder plus tard :

PRIVATE_KEY=<wallet_private_key>
ALCHEMY_API_URL=<alchemy_http_key>
WALLET_ADDRESS=<public_wallet_address>

Parce que nous ne voulons pas les pousser vers GitHub, assurez-vous de les ajouter dans .gitignore

Ajout de la fonctionnalité de connexion à l'aide de MetaMask

Dans DApps, MetaMask est le portefeuille le plus utilisé, nous ajouterons donc la connexion MetaMask avec thirdweb.

Nous allons avoir besoin de deux packages à partir de l'installation :

npm i @3rdweb/sdk @3rdweb/hooks # npm

yarn add @3rdweb/sdk @3rdweb/hooks # yarn

Ajout du troisième fournisseur Web

Nous devons encapsuler l'ensemble de notre application dans un troisième fournisseur Web afin d'accéder aux informations de connexion et aux autres informations requises pour les composants :

import { thirdwebWeb3Provider } from "@3rdweb/hooks";
import "../styles/globals.css";

function MyApp({ Component, pageProps }) {
  return (
    <thirdwebWeb3Provider>
      <Component {...pageProps} />
    </thirdwebWeb3Provider>
  );
}

export default MyApp; 

À des fins d'authentification, nous devons également spécifier le type d'authentification et les ID de chaîne pris en charge. Nous utilisons MetaMask et la chaîne Rinkeby, alors ajoutez également ce qui suit :

const supportedChainIds = [4];

const connectors = {
  injected: {},
};

Enfin, transmettez-les en tant qu'accessoires dans le fournisseur comme suit :

  <thirdwebWeb3Provider
      connectors={connectors}
      supportedChainIds={supportedChainIds}
    >
      <Component {...pageProps} />
  </thirdwebWeb3Provider>

Ajout du composant de connexion

Créez un nouveau dossier appelé componentsà la racine du projet et ajoutez-y un Login.jsfichier :

import { useWeb3 } from "@3rdweb/hooks";
const Login = () => {
  const { connectWallet } = useWeb3();
  return (
    <div>
      <button onClick={() => connectWallet("injected")}>
        Sign in using MetaMask
      </button>
    </div>
  );
};
export default Login;

Heureusement, thirdweb fournit une connectWalletfonction que nous pouvons utiliser pour ajouter une authentification !

Rendu du composant de connexion

À l' intérieur index.jsde , affichez l'écran de connexion s'il n'y a pas d'adresse (si l'utilisateur n'est pas connecté) :

const { address } = useWeb3();
if (!address) {
  return <Login />;
}

Cela permettra à nos utilisateurs de se connecter, mais ensuite, un écran vide s'affichera. Donc, dans l'autre bloc de retour, montrons à l'utilisateur son adresse :

export default function Home() {
  const { address } = useWeb3();
  if (!address) {
    return <Login />;
  }
  return (
    <div className={styles.container}>
      <h2>You are signed in as {address}</h2>
    </div>
  );
}

La connexion fonctionne mais cela ne semble pas bon pour le moment. Créez donc un nouveau fichier Login.module.cssdans le stylesdossier et ajoutez ce qui suit :

.container {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: #7449bb;
}
.button {
  color: #7449bb;
  background-color: white;
  border: none;
  border-radius: 5px;
  padding: 10px;
  font-size: 16px;
  margin: 4px 2px;
  cursor: pointer;
  font-weight: 500;
}

Ensuite, ajoutez les classes suivantes àLogin.js :

<div className={styles.container}>
  <button className={styles.button} onClick={() => connectWallet("injected")}>
    Sign in using MetaMask
  </button>
</div>

Et enfin, importez les styles :

import styles from "../styles/Login.module.css";

Cela nous donnera un écran de connexion simple mais beau.

métamasque de connexion

Initialisation du SDK thirdweb

Nous devons maintenant initialiser le SDK thirdweb pour les différents scripts que nous allons exécuter. Commencez par créer un nouveau dossier appelé scriptsavec un initialize-sdk.jsfichier à l'intérieur.

Ajoutez le code suivant au fichier :

import { thirdwebSDK } from "@3rdweb/sdk";
import ethers from "ethers";
import dotenv from "dotenv";

dotenv.config();

if (!process.env.PRIVATE_KEY || process.env.PRIVATE_KEY == "") {
  console.log(" Private key not found.");
}
if (!process.env.ALCHEMY_API_URL || process.env.ALCHEMY_API_URL == "") {
  console.log(" Alchemy API URL not found.");
}
if (!process.env.WALLET_ADDRESS || process.env.WALLET_ADDRESS == "") {
  console.log(" Wallet Address not found.");
}
const sdk = new thirdwebSDK(
  new ethers.Wallet(
    process.env.PRIVATE_KEY,
    ethers.getDefaultProvider(process.env.ALCHEMY_API_URL)
  )
);
(async () => {
  try {
    const apps = await sdk.getApps();
    console.log("app address:", apps[0].address);
  } catch (err) {
    console.error("Failed to get apps from the sdk", err);
    process.exit(1);
  }
})();
export default sdk;

Cela initialisera le SDK thirdweb, et comme vous pouvez le voir, nous devons installer certains packages :

npm i ethers dotenv # npm

yarn add ethers dotenv # yarn

Nous utilisons ici des importations modulaires, alors créez un nouveau package.jsonfichier dans le scriptsdossier et ajoutez simplement ce qui suit :

{
  "name": "scripts",
  "type": "module"
}

Enfin, exécutez le script :

node scripts/initialize-sdk.js

L'exécution du script peut prendre un certain temps, mais après un certain temps, vous obtiendrez l'adresse de votre application.

Adresse de l'application Thirdweb

Nous en aurons besoin dans les prochaines étapes, alors stockez-le dans un endroit sûr.

Ajout de fonctionnalités pour créer un NFT

Pour cette étape, nous allons avoir besoin d'ETH de test, alors allez à un robinet comme celui -ci et obtenez-en.

Créer et configurer un NFT

Créez un nouveau fichier appelé deploy-drop.jsà l'intérieur du scriptsdossier. Ici, ajoutez le script suivant :

import { ethers } from "ethers";
import sdk from "./initialize-sdk.js";
import { readFileSync } from "fs";

const app = sdk.getAppModule("APP_ADDRESS");

(async () => {
  try {
    const bundleDropModule = await app.deployBundleDropModule({
      name: "LogRocket DAO", // Name of NFT Collection for DAO
      description: "A DAO for all the LogRocket readers.", // Description
      image: readFileSync("scripts/assets/rocket.png"), // Image for NFT collection
      primarySaleRecipientAddress: ethers.constants.AddressZero,
    });

    console.log(
      " Successfully deployed bundleDrop module, address:",
      bundleDropModule.address,
    );
    console.log(
      " bundleDrop metadata:",
      await bundleDropModule.getMetadata(),
    );
  } catch (error) {
    console.log("failed to deploy bundleDrop module", error);
  }
})()

Vous devrez mettre à jour quelques éléments ici :

  • Mettez à jour l'adresse de l'application avec la nouvelle adresse de l'application que vous avez obtenue en exécutant le script précédent
  • Mettre à jour le nom du dépôt NFT pour le DAO et sa description
  • Ajoutez une image pour le dépôt NFT en créant un nouveau dossier appelé assetset en y ajoutant l'image pour votre NFT

Après avoir mis à jour les détails, exécutez le script suivant :

node scripts/deploy-drop.js

Attendez que le script s'exécute et vous devriez obtenir une adresse et les métadonnées.

Adresse NFT et métadonnées

Vous pouvez même vérifier la transaction sur Rinkeby Etherscan

Configurons notre NFT maintenant ! Créez un nouveau config-nft.jsfichier dans le scriptsdossier et ajoutez ce qui suit :

import sdk from "./initialize-sdk.js";
import { readFileSync } from "fs";
const bundleDrop = sdk.getBundleDropModule(
  "BUNDLE_DROP_ADDRESS"
);
(async () => {
  try {
    await bundleDrop.createBatch([
      {
        name: "LogRocket DAO", // Name of NFT Collection for DAO
        description: "A DAO for all the LogRocket readers.", // Description
        image: readFileSync("scripts/assets/rocket.png"), // Image for NFT collection
      },
    ]);
    console.log(" Successfully created a new NFT in the drop!");
  } catch (error) {
    console.error("failed to create the new NFT", error);
  }
})();

Vous devez mettre à jour l'adresse de dépôt du bundle et les détails dans l'objet à l'intérieur de createBatch. Ces détails vont être utilisés pour le NFT !

Une fois que vous les avez tous mis à jour, exécutez le script suivant :

node scripts/config-nft.js

Cela devrait vous donner une sortie comme celle-ci.

NFT-configs-sortie

Si vous voyez le module dans le tableau de bord thirdweb, vous verrez qu'un NFT a été créé !🥳

Tableau de bord Thirdweb

Enfin, ajoutons une condition de réclamation à notre NFT

La définition d'une condition de réclamation nous permettra de fixer une limite pour les NFT et d'autoriser une limite maximale spécifique par transaction. Nous définirons une condition de réclamation à partir du tableau de bord lui-même, alors cliquez sur le bouton Paramètres et vous pourrez mettre à jour les données en fonction de vos besoins.

revendiquer les paramètres de condition

Une fois la mise à jour terminée, cliquez sur Enregistrer et confirmez la petite transaction.

Vérifier si l'utilisateur a un NFT

Avant de créer un bouton de menthe qui permet aux utilisateurs de frapper des NFT, vérifions si l'utilisateur a déjà un NFT. Nous ne voulons pas que les utilisateurs frappent plusieurs NFT !

Commencez par ajouter deux nouvelles variables, sdket bundleDropModule, comme ceci avant notre composant fonctionnel :

const sdk = new thirdwebSDK("rinkeby");
const bundleDropModule = sdk.getBundleDropModule(
  "BUNDLE_DROP_ADDRESS"
);

Vous devrez également importer thirdwebSDK:

import { thirdwebSDK } from "@3rdweb/sdk";

Maintenant, créons un état pour hasClaimedNFT:

const [hasClaimedNFT, setHasClaimedNFT] = useState(false);

Nous devons également créer un useEffectcrochet pour vérifier si l'utilisateur a le NFT :

  useEffect(() => {
    const handler = async () => {
      if (!address) {
        return;
      }
      try {
        const balance = await bundleDropModule.balanceOf(address, "0");
        if (balance.gt(0)) {
          setHasClaimedNFT(true);
          console.log(" You have an NFT!");
        } else {
          setHasClaimedNFT(false);
          console.log(" You don't have an NFT.");
        }
      } catch (error) {
        setHasClaimedNFT(false);
        console.error("failed to nft balance", error);
      }
    };
    handler();
  }, [address]);

Tout d'abord, il vérifiera si l'utilisateur est connecté. Si l'utilisateur n'est pas connecté, il ne renverra rien. Ensuite, cela vérifie si l'utilisateur a le NFT avec l'ID de jeton 0dans le contrat de dépôt que nous avons importé en haut.

Si vous ouvrez la console sur le site Web, cela devrait indiquer que vous n'avez pas de NFT.

Vous n'avez pas de message NFT

Création d'un bouton pour créer des NFT

Créons le bouton pour créer des NFT ! Créez une nouvelle fonction appelée mintNftcomme suit :

const mintNft = async () => {
  setIsClaiming(true);
  try {
    await bundleDropModule.claim("0", 1);
    setHasClaimedNFT(true);
    console.log(" Successfully Minted the NFT!");
  } catch (error) {
    console.error("failed to claim", error);
  } finally {
    setIsClaiming(false);
  }
};

Nous appellerons cette fonction lorsqu'un bouton est cliqué pour frapper le NFT sur le portefeuille de l'utilisateur. Mais d'abord, ajoutons les deux états requis :

const [hasClaimedNFT, setHasClaimedNFT] = useState(false);
const [isClaiming, setIsClaiming] = useState(false);

Nous devons également appeler la providerSignerfonction, donc obtenez d'abord le fournisseur de useWeb3:

const { address, provider } = useWeb3();

Et ensuite appelez la fonction :

const signer = provider ? provider.getSigner() : undefined;

useEffect(() => {
  sdk.setProviderOrSigner(signer);
}, [signer]);

Créons le bouton maintenant ! Dans le bloc de retour final, ajoutez ce qui suit :

<div>
  <h1>Mint your free LogRocket DAO Membership NFT </h1>
  <button disabled={isClaiming} onClick={() => mintNft()}>
    {isClaiming ? "Minting..." : "Mint your nft (FREE)"}
  </button>
</div>

Maintenant, après nous être connectés, il devrait nous montrer un écran comme celui-ci.

Bouton d'écran Mint NFT

Vous pouvez même visualiser votre NFT sur Opensea Testnets ; allez simplement sur https://testnets.opensea.io/assets//0

Enfin, juste au-dessus du bloc de retour final, ajoutez cette vérification pour voir si l'utilisateur a déjà réclamé le NFT :

if (hasClaimedNFT) {
  return (
    <div>
      <h1>You have the DAO Membership NFT!</h1>
    </div>
  );
}

Ajouter des styles

Nous avons terminé la construction de la fonctionnalité NFT de frappe, mais elle a l'air moche, alors ajoutons quelques styles de base. À l' intérieur, Home.module.cssajoutez ce qui suit :

.container {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: #7449bb;
}
.container > h1 {
  font-size: 3rem;
  color: #fff;
  font-weight: bold;
}
.button {
  color: #7449bb;
  background-color: white;
  border: none;
  border-radius: 5px;
  padding: 10px;
  font-size: 16px;
  margin: 4px 2px;
  cursor: pointer;
  font-weight: 500;
}

Il faut aussi ajouter le classNames:

 if (hasClaimedNFT) {
    return (
      <div className={styles.container}>
        <h1>You have the DAO Membership NFT!</h1>
      </div>
    );
  }
  return (
    <div className={styles.container}>
      <h1>Mint your free LogRocket DAO Membership NFT </h1>
      <button
        className={styles.button}
        disabled={isClaiming}
        onClick={() => mintNft()}
      >
        {isClaiming ? "Minting..." : "Mint your NFT (FREE)"}
      </button>
    </div>
  );
};

Cela nous donne un meilleur écran de menthe.

Écran de frappe NFT stylé

Créer et déployer un jeton de gouvernance

Créez un nouveau fichier appelé deploy-token.jsdans le scriptsdossier. Ajoutez-y ce qui suit :

import sdk from "./initialize-sdk.js";
const app = sdk.getAppModule("YOUR_APP_ADDRESS");

(async () => {
  try {
    const tokenModule = await app.deployTokenModule({
      name: "LogRocket Token", // name of the token
      symbol: "LR", // symbol
    });
    console.log(
      " Successfully deployed token module, address:",
      tokenModule.address
    );
  } catch (error) {
    console.error("failed to deploy token module", error);
  }
})();

Ce script créera un nouveau module de jeton avec un nom et un symbole. Vous devrez mettre à jour manuellement l'adresse de l'application, le nom du jeton et le symbole vous-même.

Après la mise à jour, exécutez le script.

Vous pouvez vérifier ce jeton par l'adresse sur Rinkeby Etherscan, et également l'ajouter sur votre portefeuille MetaMask en cliquant sur Importer des jetons .

jetons d'importation de métamasque

Après l'importation, vous devriez voir le jeton sous vos actifs.

Nombre de jetons 0 LR

Il est actuellement nul, alors frappons quelques jetons !

Frappe de jetons

Créez un nouveau fichier appelé mint-token.jsdans le scriptsdossier et ajoutez ce qui suit :

import { ethers } from "ethers";
import sdk from "./initialize-sdk.js";
const tokenModule = sdk.getTokenModule(
  "TOKEN_MODULE_ADDRESS"
);

(async () => {
  try {
    const amount = 1_000_000;
    const amountWith18Decimals = ethers.utils.parseUnits(amount.toString(), 18);
    await tokenModule.mint(amountWith18Decimals);
    const totalSupply = await tokenModule.totalSupply();
    console.log(
      " There now is",
      ethers.utils.formatUnits(totalSupply, 18),
      "$LR in circulation"
    );
  } catch (error) {
    console.error("Failed to mint tokens", error);
  }
})();

Mettez à jour l'adresse du module de jeton avec l'adresse que vous avez obtenue dans le dernier script, et vous pouvez mettre à jour le montant que vous souhaitez frapper.

Une fois que vous êtes prêt à frapper, exécutez le script :

node scripts/mint-token.js

Vous devriez maintenant voir le nombre de jetons que vous avez émis sur votre portefeuille MetaMask !🎉

nombre de jetons 1 million

Jetons de largage aérien

Nous pourrions souhaiter diffuser les jetons à nos détenteurs de NFT, alors créons un script pour cela. Créez un nouveau airdrop.jsfichier à l'intérieur scriptset ajoutez ce qui suit :

import { ethers } from "ethers";
import sdk from "./initialize-sdk.js";
const bundleDropModule = sdk.getBundleDropModule(
  "BUNDLE_DROP_ADDRESS"
);
const tokenModule = sdk.getTokenModule(
  "TOKEN_MODULE_ADDRESS"
);
(async () => {
  try {
    const walletAddresses = await bundleDropModule.getAllClaimerAddresses("0");
    if (walletAddresses.length === 0) {
      console.log(
        "No NFTs have been claimed yet, ask your friends to mint some free ones :P!"
      );
      process.exit(0);
    }
    const airdropTargets = walletAddresses.map((address) => {
      const randomAmount = Math.floor(
        Math.random() * (10000 - 1000 + 1) + 1000
      );
      console.log(" Going to airdrop", randomAmount, "tokens to", address);
      return {
        address,
        amount: ethers.utils.parseUnits(randomAmount.toString(), 18),
      };
    });
    console.log(" Starting airdrop...");
    await tokenModule.transferBatch(airdropTargets);
    console.log(
      " Successfully airdropped tokens to all the holders of the NFT!"
    );
  } catch (err) {
    console.error("Failed to airdrop tokens", err);
  }
})();

Après avoir exécuté le script, vous devriez obtenir quelque chose comme ça.

Message de réussite du largage

Actuellement, vous seul avez créé un NFT, il n'enverra donc pas le jeton à quelqu'un d'autre. Mais cela peut être utilisé pour l'envoyer ultérieurement à d'autres détenteurs de NFT.

Autoriser les utilisateurs à voter

Créez un nouveau deploy-vote.jsfichier dans le scriptsdossier et ajoutez ce qui suit :

import sdk from "./initialize-sdk.js";
const appModule = sdk.getAppModule(
  "APP_MODULE_ADDRESS"
);
(async () => {
  try {
    const voteModule = await appModule.deployVoteModule({
      name: "LR Dao's Proposals",
      votingTokenAddress: "0x6fb07DCBC53Fd8390de38CDBfCc267b5A72761ca",
      proposalStartWaitTimeInSeconds: 0,
      proposalVotingTimeInSeconds: 24 * 60 * 60,
      votingQuorumFraction: 0,
      minimumNumberOfTokensNeededToPropose: "0",
    });
    console.log(
      " Successfully deployed vote module, address:",
      voteModule.address
    );
  } catch (err) {
    console.error("Failed to deploy vote module", err);
  }
})();

Mettez à jour l'adresse de l'application, le nom et l'adresse du jeton de vote, puis exécutez le script :

node scripts/deploy-vote.js

Nous devons également configurer un module de vote, alors créez un nouveau script appelé setup-vote.jset ajoutez ce qui suit :

import { ethers } from "ethers";
import sdk from "./initialize-sdk.js";
const voteModule = sdk.getVoteModule(
  "0xA57A03B9e117723b6873100742116A01140C43f4"
);
const tokenModule = sdk.getTokenModule(
  "0x6fb07DCBC53Fd8390de38CDBfCc267b5A72761ca"
);
(async () => {
  try {
    await tokenModule.grantRole("minter", voteModule.address);
    console.log(
      "Successfully gave vote module permissions to act on token module"
    );
  } catch (error) {
    console.error(
      "failed to grant vote module permissions on token module",
      error
    );
    process.exit(1);
  }
  try {
    const ownedTokenBalance = await tokenModule.balanceOf(
      process.env.WALLET_ADDRESS
    );
    const ownedAmount = ethers.BigNumber.from(ownedTokenBalance.value);
    const percent90 = ownedAmount.div(100).mul(90);
    await tokenModule.transfer(voteModule.address, percent90);
    console.log(" Successfully transferred tokens to vote module");
  } catch (err) {
    console.error("failed to transfer tokens to vote module", err);
  }
})();

Vous devrez exécuter ce script pour le terminer :

node scripts/setup-vote.js

Maintenant que notre module de vote est prêt, créons quelques propositions !

Créez un nouveau fichier appelé vote-proposals.jsdans le scriptsdossier et ajoutez ce qui suit :

import { ethers } from "ethers";
import sdk from "./initialize-sdk.js";

const voteModule = sdk.getVoteModule(
  "VOTE_MODULE"
);
const tokenModule = sdk.getTokenModule(
  "TOKEN_MODULE"
);
(async () => {
  try {
    const amount = 10000;
    await voteModule.propose(
      "Should the DAO mint an additional " +
        amount +
        " tokens into the treasury?",
      [
        {
          nativeTokenValue: 0,
          transactionData: tokenModule.contract.interface.encodeFunctionData(
            "mint",
            [voteModule.address, ethers.utils.parseUnits(amount.toString(), 18)]
          ),
          toAddress: tokenModule.address,
        },
      ]
    );
    console.log(" Successfully created proposal to mint tokens");
  } catch (error) {
    console.error("failed to create first proposal", error);
    process.exit(1);
  }
})();

Vous devez mettre à jour les adresses des modules, et si vous souhaitez mettre à jour le message de la proposition, vous pouvez également le mettre à jour.

Enfin, exécutez le script. Cela devrait vous donner quelque chose comme ça.

proposition réussie de frapper un message de jetons

Si vous vérifiez maintenant le tableau de bord thirdweb, la proposition a été créée.🎉

proposition approuvée par thirdweb

Affichage des propositions sur le site Web

Tout d'abord, importez le module jeton et vote :

const voteModule = sdk.getVoteModule(
  "0xf738973379b8B6444e429D2fd6C8B1f223247390"
);
const tokenModule = sdk.getTokenModule(
  "0x8C35288de335070dd1C00d68d71383d81437472A"
);

Nous allons avoir besoin de trois useStates, comme ceci :

const [proposals, setProposals] = useState([]);
const [isVoting, setIsVoting] = useState(false);
const [hasVoted, setHasVoted] = useState(false); 

Obtenir les propositions

Nous devons récupérer les propositions pour les afficher à l'écran, alors créez ceci useEffect:

useEffect(() => {
    if (!hasClaimedNFT) {
      return;
    }
    voteModule
      .getAll()
      .then((proposals) => {
        setProposals(proposals);
      })
      .catch((err) => {
        console.error("failed to get proposals", err);
      });
  }, [hasClaimedNFT]);

Ensuite, créez une nouvelle handleFormSubmitfonction :

  const handleFormSubmit = async (e) => {
    e.preventDefault();
    e.stopPropagation();
    setIsVoting(true);
    const votes = proposals.map((proposal) => {
      let voteResult = {
        proposalId: proposal.proposalId,
        vote: 2,
      };
      proposal.votes.forEach((vote) => {
        const elem = document.getElementById(
          proposal.proposalId + "-" + vote.type
        );
        if (elem.checked) {
          voteResult.vote = vote.type;
          return;
        }
      });
      return voteResult;
    });
    try {
      const delegation = await tokenModule.getDelegationOf(address);
      if (delegation === ethers.constants.AddressZero) {
        await tokenModule.delegateTo(address);
      }
      try {
        await Promise.all(
          votes.map(async (vote) => {
            const proposal = await voteModule.get(vote.proposalId);
            if (proposal.state === 1) {
              return voteModule.vote(vote.proposalId, vote.vote);
            }
            return;
          })
        );
        try {
          await Promise.all(
            votes.map(async (vote) => {
              const proposal = await voteModule.get(vote.proposalId);
              if (proposal.state === 4) {
                return voteModule.execute(vote.proposalId);
              }
            })
          );
          setHasVoted(true);
        } catch (err) {
          console.error("failed to execute votes", err);
        }
      } catch (err) {
        console.error("failed to vote", err);
      }
    } catch (err) {
      console.error("failed to delegate tokens", err);
    } finally {
      setIsVoting(false);
    }
  };

Cette fonction va collecter le vote.

Rendre les propositions

Remplacez le if (hasClaimedNFT)bloc par ceci :

if (hasClaimedNFT) {
    return (
      <div className={styles.container}>
          <h2>Active Proposals</h2>
          <form onSubmit={handleFormSubmit}>
            {proposals.map((proposal) => (
              <Proposal
                key={proposal.proposalId}
                votes={proposal.votes}
                description={proposal.description}
                proposalId={proposal.proposalId}
              />
            ))}
            <button
              onClick={handleFormSubmit}
              type="submit"
              className={styles.button}
            >
              {isVoting
                ? "Voting..."
                "Submit Votes"}
            </button>
          </form>
        </div>
    );
  }

Nous créons un composant distinct pour la proposition afin de garder les choses propres. Créez donc un nouveau fichier appelé Proposal.jsdans le componentsdossier et ajoutez ce qui suit :

import styles from "../styles/Proposal.module.css";

const Proposal = ({ description, votes, proposalId }) => {
  return (
    <div className={styles.proposal}>
      <h5 className={styles.description}>{description}</h5>
      <div className={styles.options}>
        {votes.map((vote) => (
          <div key={vote.type}>
            <input
              type="radio"
              id={proposalId + "-" + vote.type}
              name={proposalId}
              value={vote.type}
              defaultChecked={vote.type === 2}
            />
            <label htmlFor={proposalId + "-" + vote.type}>{vote.label}</label>
          </div>
        ))}
      </div>
    </div>
  );
};
export default Proposal;

J'ai également ajouté un style de base, alors créez un nouveau Proposal.module.cssfichier dans le stylesdossier :

.proposal {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: #fafafa;
  border-radius: 10px;
  box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
  padding: 20px;
  margin: 20px;
}
.options {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: flex-start;
  width: 100%;
  margin-top: 1rem;
}

Pour centrer les éléments, j'ai également ajouté les styles suivantsHome.module.css :

.container > form {
  display: flex;
  flex-direction: column;
  align-items: center;
}

Vous arriverez sur cet écran où vous pourrez soumettre vos votes.🎉

Écran de vote de proposition

Enfin, créons une fonction pour vérifier si la personne a déjà voté.

Tout d'abord, créez un nouveau useEffect:

  useEffect(() => {
    if (!hasClaimedNFT) {
      return;
    }
    if (!proposals.length) {
      return;
    }
    voteModule
      .hasVoted(proposals[0].proposalId, address)
      .then((hasVoted) => {
        setHasVoted(hasVoted);
        if (hasVoted) {
        } else {
        }
      })
      .catch((err) => {
        console.error("failed to check if wallet has voted", err);
      });
  }, [hasClaimedNFT, address, proposals]);

Et remplacez le bouton par ceci :

 <button
  onClick={handleFormSubmit}
  type="submit"
  disabled={isVoting || hasVoted}
  className={styles.button}
>
  {isVoting ? "Voting..." : hasVoted ? "You Already Voted" : "Submit Votes"}
</button>

Après avoir voté, il devrait afficher le message Vous avez déjà voté :

Vous avez déjà voté message

Conclusion

C'était tout pour ce tutoriel, j'espère que vous l'avez aimé et que vous pourrez l'utiliser pour créer votre propre DAO ! Vous pouvez toujours mettre à jour le DAO et ajouter plus de fonctionnalités si vous le souhaitez.

Vous pouvez trouver le référentiel GitHub pour ce projet ici .

Source de l'article original sur https://blog.logrocket.com

What is GEEK

Buddha Community

Enoch Barcenas

Enoch Barcenas

1648523737

How to Build Your Own DAO using Next.js, Thirdweb, MetaMask & Alchemy

Learn how to build a DAO, mint NFTs, airdrop tokens, and add voting functionality using only JavaScript in this complete tutorial.

What is a DAO?

DAO stands for Decentralized Autonomous Organization. As it says in the name, a DAO is an organization without a single leader; instead, rules are encoded in the blockchain. Because of this, a DAO is completely transparent and everyone who participates has a stake. Large decisions are made via voting amongst those who own non-fungible tokens (NFTs) from the DAO, which grant membership.

Today, we are going to build our very own DAO using Next.js, thirdweb, MetaMask, and Alchemy. It will allow users to mint your DAO’s NFT, receive cryptocurrency via airdrops, and participate in the DAO’s polls. This tutorial will be written with just JavaScript, so you don’t need to know any Solidity.

Prerequisites

To understand and follow along with this tutorial, you should have the following:

  • Working knowledge of JavaScript, Next.js, and the blockchain
  • A MetaMask wallet
  • An account with Alchemy

Contents

  • Setup
    • Creating an Alchemy app
    • Getting the wallet’s private key
    • Adding .env variables
  • Adding sign in functionality using MetaMask
    • Adding the thirdweb provider
    • Adding the sign in component
    • Initializing the thirdweb SDK
  • Adding features to mint an NFT
    • Creating and configuring an NFT
    • Checking if the user already has an NFT
    • Creating a button to mint NFTs
    • Adding styles
  • Creating and deploying a governance token
    • Minting tokens
    • Airdropping tokens
    • Allowing users to vote
    • Showing proposals on the website
    • Getting the proposals
    • Rendering the proposals
  • Conclusion

Setup

We will begin by setting up a Next.js app with the following command:

npx create-next-app my-dao

Creating an Alchemy app

Next, head to Alchemy, sign in, click on Create App, and provide the required details. Make sure to use the same chain as the one you used in thirdweb – in our case, it is the Ethereum chain and the Rinkeby network.

Creating alchemy app

After the app is created, copy the HTTP API key.

Getting the wallet’s private key

In order to mint NFTs and perform certain scripts, we are going to need the wallet’s private key.

To access it, open the MetaMask browser extension and click on Account Details. You should see your private key here; export it and copy it somewhere safe.

 

Adding .env variables

Let’s add these variables in a .env file so we can access them later:

PRIVATE_KEY=<wallet_private_key>
ALCHEMY_API_URL=<alchemy_http_key>
WALLET_ADDRESS=<public_wallet_address>

Because we don’t want to push these to GitHub, be sure to add them in gitignore

Adding sign in functionality using MetaMask

In DApps, MetaMask is the most popular wallet used, so we will add MetaMask sign in with thirdweb.

We are going to need two packages from install:

npm i @thirdweb-dev/react @thirdweb-dev/sdk ethers # npm

yarn add @thirdweb-dev/react @thirdweb-dev/sdk ethers # yarn

Adding the thirdweb provider

We need to wrap our whole app in a thirdweb provider in order to access the login details and other information required for the components:

import "../styles/globals.css";
import {ThirdwebProvider } from "@thirdweb-dev/react";

function MyApp({ Component, pageProps }) {
  return (
    <ThirdwebProvider desiredChainId={activeChainId}>
      <Component {...pageProps} />
    </ThirdwebProvider>
  );
}
export default MyApp;

For authentication purposes, we also have to specify the type of authentication and the supported chain IDs. We are using MetaMask and the Rinkeby chain, so add the following as well:

import { ChainId, ThirdwebProvider } from "@thirdweb-dev/react";
const activeChainId = ChainId.Rinkeby;

Finally, pass these as props in the provider like so:

 <ThirdwebProvider desiredChainId={activeChainId}>
      <Component {...pageProps} />
  </ThirdwebProvider>

Adding the sign in component

Create a new folder called components in the root of the project and add a Login.js file to it:

import { useMetamask } from "@thirdweb-dev/react";
const Login = () => {
  const connectWithMetamask = useMetamask();
  return (
    <div>
      <button onClick={connectWithMetamask}>Sign in using MetaMask</button>
    </div>
  );
};
export default Login;

Thankfully, thirdweb provides a connectWallet function which we can use to add authentication!

Rendering the sign in component

Inside index.js, render the login screen if there is no address (if the user is not signed in):

const address = useAddress();
if (!address) {
  return ;
}

This will allow our users to sign in, but afterwards it just shows a blank screen. So, in the other return block, let’s show the user her address:

export default function Home() {
  const { address } = useWeb3();
  if (!address) {
    return <Login />;
  }
  return (
    <div className={styles.container}>
      <h2>You are signed in as {address}</h2>
    </div>
  );
}

The login works but it doesn’t look good right now. So, create a new file Login.module.css in the styles folder and add the following:

.container {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: #7449bb;
}
.button {
  color: #7449bb;
  background-color: white;
  border: none;
  border-radius: 5px;
  padding: 10px;
  font-size: 16px;
  margin: 4px 2px;
  cursor: pointer;
  font-weight: 500;
}

Next, add the following classes to Login.js:

<div className={styles.container}>
  <button className={styles.button} onClick={() => connectWallet("injected")}>
    Sign in using MetaMask
  </button>
</div>

And finally, import the styles:

import styles from "../styles/Login.module.css";

This will give us a simple, but good-looking login screen.

metamask sign in

Initializing the thirdweb SDK

Now we need to initialize the thirdweb SDK for the various scripts we are going to run. Start by creating a new folder called scripts with an initialize-sdk.js file inside of it.

Add the following code to the file:

import { ThirdwebSDK } from "@thirdweb-dev/sdk";
import ethers from "ethers";
import dotenv from "dotenv";
dotenv.config();
if (!process.env.PRIVATE_KEY || process.env.PRIVATE_KEY === "") {
  console.log("🛑 Private key not found.");
}
if (!process.env.ALCHEMY_API_URL || process.env.ALCHEMY_API_URL === "") {
  console.log("🛑 Alchemy API URL not found.");
}
if (!process.env.WALLET_ADDRESS || process.env.WALLET_ADDRESS === "") {
  console.log("🛑 Wallet Address not found.");
}
const sdk = new ThirdwebSDK(
  new ethers.Wallet(
    process.env.PRIVATE_KEY,
    ethers.getDefaultProvider(process.env.ALCHEMY_API_URL)
  )
);
(async () => {
  try {
    const address = await sdk.getSigner().getAddress();
    console.log("SDK initialized by address:", address);
  } catch (err) {
    console.error("Failed to get the address", err);
    process.exit(1);
  }
})();
export default sdk;

This will initialize the thirdweb SDK, and as you can see, we need to install some packages:

npm i dotenv # npm

yarn add dotenv # yarn

We are using modular imports here, so create a new package.json file inside the scripts folder and simply add the following:

{
  "name": "scripts",
  "type": "module"
}

Finally, run the script:

node scripts/initialize-sdk.js

The script may take some time to run, but after some time you will get your app address.

We are going to need this in the next steps, so store it somewhere safe.

Adding features to mint an NFT

For this step, we are going to need some test ETH, so go to a faucet like this and get some.

Creating and configuring an NFT

Create a new file called deploy-drop.js inside the scripts folder. In here, add the following script:

import { ethers } from "ethers";
import sdk from "./initialize-sdk.js";

(async () => {
  try {
    const editionDropAddress = await sdk.deployer.deployEditionDrop({
      name: "LogRocket DAO", // Name of NFT Collection for DAO
      description: "A DAO for all the LogRocket readers.", // Description
      image: "image_Address", // PFP for NFT collection
      primary_sale_recipient: ethers.constants.AddressZero,
    });
    const editionDrop = sdk.getEditionDrop(editionDropAddress);
    const metadata = await editionDrop.metadata.get();
    console.log(
      "✅ Successfully deployed editionDrop contract, address:",
      editionDropAddress
    );
    console.log("✅ editionDrop metadata:", metadata);
  } catch (error) {
    console.log("failed to deploy editionDrop contract", error);
  }
})();

You will need to update a few things here:

  • Update the app address with the new app address that you got by running the previous script
  • Update the name of the NFT drop for the DAO, and its description
  • Add an image for the NFT drop by creating a new folder called assets, and adding the image for your NFT there

After you have updated the details, run the following script:

node scripts/deploy-drop.js

Wait for the script to run, and you should get an address and the metadata.

NFT address and metadata

This will create a new edition drop contract for us! You can even check out the transaction on Rinkeby Etherscan.

Let’s configure our NFT now! Create a new config-nft.js file inside the scripts folder and add the following:

import sdk from "./initialize-sdk.js";

const editionDrop = sdk.getEditionDrop("EDITION_DROP_ADDDRESS");

(async () => {
  try {
    await editionDrop.createBatch([
      {
        name: "LogRocket DAO", // Name of NFT Collection for DAO
        description: "A DAO for all the LogRocket readers.", // Description
        image: "image_address", // Image for NFT
      },
    ]);
    console.log("✅ Successfully created a new NFT in the drop!");
  } catch (error) {
    console.error("failed to create the new NFT", error);
  }
})();

You need to update the bundle drop address and the details in the object inside createBatch. These details are going to be used for the NFT!

Once, you have updated all of them, run the following script:

node scripts/config-nft.js

It should give you an output like this.

NFT-configs-output

If you see the module in the thirdweb dashboard, you will see that an NFT has been created! 🥳

Thirdweb dashboard

Finally, let’s add a claim condition to our NFT.

Setting a claim condition will allow us to set a limit for the NFTs and allow a specific max limit per transaction. We will set a claim condition from the dashboard itself, so click on the Settings button and create a new claim phase.

claim condition settings

After you are done updating, click Update claim phase and confirm the small transaction.

Checking if the user has an NFT

Before creating a mint button that allows the users to mint NFTs, let’s check if the user has an NFT already. We don’t want the users to mint multiple NFTs!

Start by adding two new variables, sdk and bundleDropModule, like this before our functional component:

  const editionDrop = useEditionDrop(
    "0x2f66A5A2BCB272FFC9EB873E3482A539BEB6f02a"
  );

You will also need to import useEditionDrop:

import { useAddress, useEditionDrop } from "@thirdweb-dev/react";

Now, let’s create a state for hasClaimedNFT:

const [hasClaimedNFT, setHasClaimedNFT] = useState(false);

We also need to create a useEffect Hook to check if the user has the NFT:

useEffect(() => {
    if (!address) {
      return;
    }
    const checkBalance = async () => {
      try {
        const balance = await editionDrop.balanceOf(address, 0);
        if (balance.gt(0)) {
          setHasClaimedNFT(true);
          console.log("🎉 You have an NFT!");
        } else {
          setHasClaimedNFT(false);
          console.log("🤷‍♂️ You don't have an NFT.");
        }
      } catch (error) {
        setHasClaimedNFT(false);
        console.error("Failed to get nft balance", error);
      }
    };
    checkBalance();
  }, [address, editionDrop]);

Firstly, it will check if the user is signed in. If the user is not signed in, it will return nothing. Then, this checks if the user has the NFT with the token ID 0 in the drop contract that we imported at the top.

If you, open the console in the website, it should show that you don’t have an NFT.

You dont have an NFT message

Creating a button to mint NFTs

Let’s create the button to mint NFTs! Create a new function called mintNft like so:

const mintNft = async () => {
  setIsClaiming(true);
  try {
    await bundleDropModule.claim("0", 1);
    setHasClaimedNFT(true);
    console.log("🌊 Successfully Minted the NFT!");
  } catch (error) {
    console.error("failed to claim", error);
  } finally {
    setIsClaiming(false);
  }
};

We will call this function when a button is clicked to mint the NFT to the user’s wallet. But first, let’s add the isClaiming state:

const [isClaiming, setIsClaiming] = useState(false);

Let’s create the button now! Inside the final return block add the following:

<div>
  <h1>Mint your free LogRocket DAO Membership NFT 💳</h1>
  <button disabled={isClaiming} onClick={() => mintNft()}>
    {isClaiming ? "Minting..." : "Mint your nft (FREE)"}
  </button>
</div>

Now, after we sign in, it should show us a screen like this.

Mint NFT screen button

If you try the Mint your nft (FREE) button, it should pop up your MetaMask screen to complete the transaction. In the console, should should see the following.

successfully minted nft

Finally, just above the final return block, add this check to see if the user has claimed the NFT already:

if (hasClaimedNFT) {
  return (
    <div>
      <h1>You have the DAO Membership NFT!</h1>
    </div>
  );
}

Adding styles

We have completed building the minting NFT functionality, but it looks ugly, so let’s add some basic stylings. Inside Home.module.css add the following:

.container {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: #7449bb;
}
.container > h1 {
  font-size: 3rem;
  color: #fff;
  font-weight: bold;
}
.button {
  color: #7449bb;
  background-color: white;
  border: none;
  border-radius: 5px;
  padding: 10px;
  font-size: 16px;
  margin: 4px 2px;
  cursor: pointer;
  font-weight: 500;
}

We also need to add the classNames:

  if (hasClaimedNFT) {
    return (
      <div className={styles.container}>
        <h1>You have the DAO Membership NFT!</h1>
      </div>
    );
  }
  return (
    <div className={styles.container}>
      <h1>Mint your free LogRocket DAO Membership NFT 💳</h1>
      <button
        className={styles.button}
        disabled={isClaiming}
        onClick={() => mintNft()}
      >
        {isClaiming ? "Minting..." : "Mint your NFT (FREE)"}
      </button>
    </div>
  );
};

This gives us a better mint screen.

Styled NFT minting screen

Creating and deploying a governance token

Create a new file called deploy-token.js in the scripts folder. Add the following to it:

import { AddressZero } from "@ethersproject/constants";
import sdk from "./initialize-sdk.js";
(async () => {
  try {
    const tokenAddress = await sdk.deployer.deployToken({
      name: "LogRocket Token", // name of the token
      symbol: "LR", // symbol
      primary_sale_recipient: AddressZero, // 0x0000000000000000000000000000000000000000
    });
    console.log(
      "✅ Successfully deployed token module, address:",
      tokenAddress
    );
  } catch (error) {
    console.error("failed to deploy token module", error);
  }
})();

This script will create a new token module with a name and symbol. You will need to manually update the app address, token name, and symbol yourself.

After updating, run the script.

You can check this token by the address on Rinkeby Etherscan, and also add it on your MetaMask wallet by clicking Import tokens.

metamask import tokens

After importing, you should see the token under your assets.

Token count 0 LR

It is currently zero, so let’s mint some tokens!

Minting tokens

Create a new file called mint-token.js inside the scripts folder and add the following:

import { ethers } from "ethers";
import sdk from "./initialize-sdk.js";
const tokenModule = sdk.getTokenModule(
  "TOKEN_MODULE_ADDRESS"
);

(async () => {
  try {
    const amount = 1_000_000;
    const amountWith18Decimals = ethers.utils.parseUnits(amount.toString(), 18);
    await tokenModule.mint(amountWith18Decimals);
    const totalSupply = await tokenModule.totalSupply();
    console.log(
      "✅ There now is",
      ethers.utils.formatUnits(totalSupply, 18),
      "$LR in circulation"
    );
  } catch (error) {
    console.error("Failed to mint tokens", error);
  }
})();

Update the token module address with the address you got in the last script, and you can update the amount you want to mint.

After you are ready to mint, run the script:

node scripts/mint-token.js

You should now see the amount of tokens you minted on your MetaMask wallet! 🎉

token count 1 million

Airdropping tokens

We might want to airdrop the tokens to our NFT holders, so let’s build a script for that. Create a new airdrop.js file inside scripts and add the following:

import sdk from "./initialize-sdk.js";
const editionDrop = sdk.getEditionDrop(
  "EDITION_ADDRESS"
);
const token = sdk.getToken("TOKEN_ADDRESS");
(async () => {
  try {
    const walletAddresses = await editionDrop.history.getAllClaimerAddresses(0);
    if (walletAddresses.length === 0) {
      console.log(
        "No NFTs have been claimed yet, ask yourfriends to claim some free NFTs!"
      );
      process.exit(0);
    }
    const airdropTargets = walletAddresses.map((address) => {
      const randomAmount = Math.floor(
        Math.random() * (10000 - 1000 + 1) + 1000
      );
      console.log("✅ Going to airdrop", randomAmount, "tokens to", address);
      const airdropTarget = {
        toAddress: address,
        amount: randomAmount,
      };
      return airdropTarget;
    });
    console.log("🌈 Starting airdrop...");
    await token.transferBatch(airdropTargets);
    console.log(
      "✅ Successfully airdropped tokens to all the holders of the NFT!"
    );
  } catch (err) {
    console.error("Failed to airdrop tokens", err);
  }
})();

After you run the script you should get something like this.

Airdrop success message

Currently, only you have minted an NFT, so it won’t send the token to someone else. But this can be used to send it to other NFT holders later on.

Allowing users to vote

Create a new deploy-vote.js file in the scripts folder and add the following:

import sdk from "./initialize-sdk.js";
(async () => {
  try {
    const voteContractAddress = await sdk.deployer.deployVote({
      name: "LR Dao's Proposals",
      voting_token_address: "TOKEN_ADDRESS",
      voting_delay_in_blocks: 0,
      voting_period_in_blocks: 6570,
      voting_quorum_fraction: 0,
      proposal_token_threshold: 0,
    });
    console.log(
      "✅ Successfully deployed vote contract, address:",
      voteContractAddress
    );
  } catch (err) {
    console.error("Failed to deploy vote contract", err);
  }
})();

Update the app address, the name, and the voting token address, then run the script:

node scripts/deploy-vote.js

We also need to set up a vote module, so create a new script called setup-vote.js and add the following:

import sdk from "./initialize-sdk.js";
const vote = sdk.getVote("VOTE_ADDRESS");
const token = sdk.getToken("TOKEN_ADDRESS");

(async () => {
  try {
    await token.roles.grant("minter", vote.getAddress());
    console.log(
      "Successfully gave vote contract permissions to act on token contract"
    );
  } catch (error) {
    console.error(
      "failed to grant vote contract permissions on token contract",
      error
    );
    process.exit(1);
  }
  try {
    const ownedTokenBalance = await token.balanceOf(process.env.WALLET_ADDRESS);
    const ownedAmount = ownedTokenBalance.displayValue;
    const percent90 = (Number(ownedAmount) / 100) * 90;
    await token.transfer(vote.getAddress(), percent90);
    console.log(
      "✅ Successfully transferred " + percent90 + " tokens to vote contract"
    );
  } catch (err) {
    console.error("failed to transfer tokens to vote contract", err);
  }
})();

You will need to run this script to finish it up:

node scripts/setup-vote.js

Now that we have our vote module ready, let’s create some proposals!

Create a new file called vote-proposals.js inside the scripts folder and add the following:

import sdk from "./initialize-sdk.js";
import { ethers } from "ethers";
const vote = sdk.getVote("0x31c5840b31A1F97745bDCbB1E46954b686828E0F");
const token = sdk.getToken("0x6eefd78C9C73505AA71A13FeE31D9718775c9086");
(async () => {
  try {
    const amount = 420_000;
    const description =
      "Should the DAO mint an additional " +
      amount +
      " tokens into the treasury?";
    const executions = [
      {
        toAddress: token.getAddress(),
        nativeTokenValue: 0,
        transactionData: token.encoder.encode("mintTo", [
          vote.getAddress(),
          ethers.utils.parseUnits(amount.toString(), 18),
        ]),
      },
    ];
    await vote.propose(description, executions);
    console.log("✅ Successfully created proposal to mint tokens");
  } catch (error) {
    console.error("failed to create first proposal", error);
    process.exit(1);
  }
})();

You need to update the module addresses, and if you want to update the message of the proposal, you can update that as well.

Finally, run the script. It should give you something like this.

successful proposal to mint tokens message

If you now check the thirdweb dashboard, the proposal has been created. 🎉

thirdweb cleared proposal

Showing proposals on the website

First, import the token and vote module:

const token = useToken("TOKEN_ADDRESS");
const vote = useVote("VOTE_ADDRESS");

We are going to need three useStates, like so:

const [proposals, setProposals] = useState([]);
const [isVoting, setIsVoting] = useState(false);
const [hasVoted, setHasVoted] = useState(false); 

Getting the proposals

We need to get the proposals to display them on the screen, so create this useEffect:

useEffect(() => {
    if (!hasClaimedNFT) {
      return;
    }
    const getAllProposals = async () => {
      try {
        const proposals = await vote.getAll();
        setProposals(proposals);
        console.log("📋 Proposals:", proposals);
      } catch (error) {
        console.log("failed to get proposals", error);
      }
    };
    getAllProposals();
  }, [hasClaimedNFT, vote]);

Then, create a new handleFormSubmit function:

const handleFormSubmit = async (e) => {
    e.preventDefault();
    e.stopPropagation();
    setIsVoting(true);
    const votes = proposals.map((proposal) => {
      const voteResult = {
        proposalId: proposal.proposalId,
        vote: 2,
      };
      proposal.votes.forEach((vote) => {
        const elem = document.getElementById(
          proposal.proposalId + "-" + vote.type
        );
        if (elem.checked) {
          voteResult.vote = vote.type;
          return;
        }
      });
      return voteResult;
    });
    try {
      const delegation = await token.getDelegationOf(address);
      if (delegation === AddressZero) {
        await token.delegateTo(address);
      }
      try {
        await Promise.all(
          votes.map(async ({ proposalId, vote: _vote }) => {
            const proposal = await vote.get(proposalId);
            if (proposal.state === 1) {
              return vote.vote(proposalId, _vote);
            }
            return;
          })
        );
        try {
          await Promise.all(
            votes.map(async ({ proposalId }) => {
              const proposal = await vote.get(proposalId);
              if (proposal.state === 4) {
                return vote.execute(proposalId);
              }
            })
          );
          setHasVoted(true);
          console.log("successfully voted");
        } catch (err) {
          console.error("failed to execute votes", err);
        }
      } catch (err) {
        console.error("failed to vote", err);
      }
    } catch (err) {
      console.error("failed to delegate tokens");
    } finally {
      setIsVoting(false);
    }
  };

This function is going to collect the vote.

Rendering the proposals

Replace the if (hasClaimedNFT) block with this:

if (hasClaimedNFT) {
    return (
      <div className={styles.container}>
          <h2>Active Proposals</h2>
          <form onSubmit={handleFormSubmit}>
            {proposals.map((proposal) => (
              <Proposal
                key={proposal.proposalId}
                votes={proposal.votes}
                description={proposal.description}
                proposalId={proposal.proposalId}
              />
            ))}
            <button
              onClick={handleFormSubmit}
              type="submit"
              className={styles.button}
            >
              {isVoting
                ? "Voting..."
                "Submit Votes"}
            </button>
          </form>
        </div>
    );
  }

We are creating a separate component for the proposal to keep things clean. So, create a new file called Proposal.js in the components folder and add the following:

import styles from "../styles/Proposal.module.css";

const Proposal = ({ description, votes, proposalId }) => {
  return (
    <div className={styles.proposal}>
      <h5 className={styles.description}>{description}</h5>
      <div className={styles.options}>
        {votes.map((vote) => (
          <div key={vote.type}>
            <input
              type="radio"
              id={proposalId + "-" + vote.type}
              name={proposalId}
              value={vote.type}
              defaultChecked={vote.type === 2}
            />
            <label htmlFor={proposalId + "-" + vote.type}>{vote.label}</label>
          </div>
        ))}
      </div>
    </div>
  );
};
export default Proposal;

I also added basic styling, so create a new Proposal.module.css file in the styles folder:

.proposal {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: #fafafa;
  border-radius: 10px;
  box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
  padding: 20px;
  margin: 20px;
}
.options {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: flex-start;
  width: 100%;
  margin-top: 1rem;
}

To center the items, I have added the following styles in Home.module.css as well:

.container > form {
  display: flex;
  flex-direction: column;
  align-items: center;
}

You will get to this screen where you can submit your votes. 🎉

Proposal vote screen

Finally, let’s make a function to check if the person has already voted.

First, create a new useEffect:

useEffect(() => {
    if (!hasClaimedNFT) {
      return;
    }
    if (!proposals.length) {
      return;
    }
    const checkIfUserHasVoted = async () => {
      try {
        const hasVoted = await vote.hasVoted(proposals[0].proposalId, address);
        setHasVoted(hasVoted);
      } catch (error) {
        console.error("Failed to check if wallet has voted", error);
      }
    };
    checkIfUserHasVoted();
  }, [hasClaimedNFT, proposals, address, vote]);

And replace the button with this:

 <button
  onClick={handleFormSubmit}
  type="submit"
  disabled={isVoting || hasVoted}
  className={styles.button}
>
  {isVoting ? "Voting..." : hasVoted ? "You Already Voted" : "Submit Votes"}
</button>

After you have voted, it should show the message You Already Voted:

You already voted message

Conclusion

This was it for this tutorial, hope you liked it and can use it to make your own DAO! You can always update the DAO and add more features if you like.

You can find the GitHub repo for this project here.

Original article source at https://blog.logrocket.com

#dao #next #nextjs #javascript #thirdweb #nft #metamask #alchemy #blockchain 

NBB: Ad-hoc CLJS Scripting on Node.js

Nbb

Not babashka. Node.js babashka!?

Ad-hoc CLJS scripting on Node.js.

Status

Experimental. Please report issues here.

Goals and features

Nbb's main goal is to make it easy to get started with ad hoc CLJS scripting on Node.js.

Additional goals and features are:

  • Fast startup without relying on a custom version of Node.js.
  • Small artifact (current size is around 1.2MB).
  • First class macros.
  • Support building small TUI apps using Reagent.
  • Complement babashka with libraries from the Node.js ecosystem.

Requirements

Nbb requires Node.js v12 or newer.

How does this tool work?

CLJS code is evaluated through SCI, the same interpreter that powers babashka. Because SCI works with advanced compilation, the bundle size, especially when combined with other dependencies, is smaller than what you get with self-hosted CLJS. That makes startup faster. The trade-off is that execution is less performant and that only a subset of CLJS is available (e.g. no deftype, yet).

Usage

Install nbb from NPM:

$ npm install nbb -g

Omit -g for a local install.

Try out an expression:

$ nbb -e '(+ 1 2 3)'
6

And then install some other NPM libraries to use in the script. E.g.:

$ npm install csv-parse shelljs zx

Create a script which uses the NPM libraries:

(ns script
  (:require ["csv-parse/lib/sync$default" :as csv-parse]
            ["fs" :as fs]
            ["path" :as path]
            ["shelljs$default" :as sh]
            ["term-size$default" :as term-size]
            ["zx$default" :as zx]
            ["zx$fs" :as zxfs]
            [nbb.core :refer [*file*]]))

(prn (path/resolve "."))

(prn (term-size))

(println (count (str (fs/readFileSync *file*))))

(prn (sh/ls "."))

(prn (csv-parse "foo,bar"))

(prn (zxfs/existsSync *file*))

(zx/$ #js ["ls"])

Call the script:

$ nbb script.cljs
"/private/tmp/test-script"
#js {:columns 216, :rows 47}
510
#js ["node_modules" "package-lock.json" "package.json" "script.cljs"]
#js [#js ["foo" "bar"]]
true
$ ls
node_modules
package-lock.json
package.json
script.cljs

Macros

Nbb has first class support for macros: you can define them right inside your .cljs file, like you are used to from JVM Clojure. Consider the plet macro to make working with promises more palatable:

(defmacro plet
  [bindings & body]
  (let [binding-pairs (reverse (partition 2 bindings))
        body (cons 'do body)]
    (reduce (fn [body [sym expr]]
              (let [expr (list '.resolve 'js/Promise expr)]
                (list '.then expr (list 'clojure.core/fn (vector sym)
                                        body))))
            body
            binding-pairs)))

Using this macro we can look async code more like sync code. Consider this puppeteer example:

(-> (.launch puppeteer)
      (.then (fn [browser]
               (-> (.newPage browser)
                   (.then (fn [page]
                            (-> (.goto page "https://clojure.org")
                                (.then #(.screenshot page #js{:path "screenshot.png"}))
                                (.catch #(js/console.log %))
                                (.then #(.close browser)))))))))

Using plet this becomes:

(plet [browser (.launch puppeteer)
       page (.newPage browser)
       _ (.goto page "https://clojure.org")
       _ (-> (.screenshot page #js{:path "screenshot.png"})
             (.catch #(js/console.log %)))]
      (.close browser))

See the puppeteer example for the full code.

Since v0.0.36, nbb includes promesa which is a library to deal with promises. The above plet macro is similar to promesa.core/let.

Startup time

$ time nbb -e '(+ 1 2 3)'
6
nbb -e '(+ 1 2 3)'   0.17s  user 0.02s system 109% cpu 0.168 total

The baseline startup time for a script is about 170ms seconds on my laptop. When invoked via npx this adds another 300ms or so, so for faster startup, either use a globally installed nbb or use $(npm bin)/nbb script.cljs to bypass npx.

Dependencies

NPM dependencies

Nbb does not depend on any NPM dependencies. All NPM libraries loaded by a script are resolved relative to that script. When using the Reagent module, React is resolved in the same way as any other NPM library.

Classpath

To load .cljs files from local paths or dependencies, you can use the --classpath argument. The current dir is added to the classpath automatically. So if there is a file foo/bar.cljs relative to your current dir, then you can load it via (:require [foo.bar :as fb]). Note that nbb uses the same naming conventions for namespaces and directories as other Clojure tools: foo-bar in the namespace name becomes foo_bar in the directory name.

To load dependencies from the Clojure ecosystem, you can use the Clojure CLI or babashka to download them and produce a classpath:

$ classpath="$(clojure -A:nbb -Spath -Sdeps '{:aliases {:nbb {:replace-deps {com.github.seancorfield/honeysql {:git/tag "v2.0.0-rc5" :git/sha "01c3a55"}}}}}')"

and then feed it to the --classpath argument:

$ nbb --classpath "$classpath" -e "(require '[honey.sql :as sql]) (sql/format {:select :foo :from :bar :where [:= :baz 2]})"
["SELECT foo FROM bar WHERE baz = ?" 2]

Currently nbb only reads from directories, not jar files, so you are encouraged to use git libs. Support for .jar files will be added later.

Current file

The name of the file that is currently being executed is available via nbb.core/*file* or on the metadata of vars:

(ns foo
  (:require [nbb.core :refer [*file*]]))

(prn *file*) ;; "/private/tmp/foo.cljs"

(defn f [])
(prn (:file (meta #'f))) ;; "/private/tmp/foo.cljs"

Reagent

Nbb includes reagent.core which will be lazily loaded when required. You can use this together with ink to create a TUI application:

$ npm install ink

ink-demo.cljs:

(ns ink-demo
  (:require ["ink" :refer [render Text]]
            [reagent.core :as r]))

(defonce state (r/atom 0))

(doseq [n (range 1 11)]
  (js/setTimeout #(swap! state inc) (* n 500)))

(defn hello []
  [:> Text {:color "green"} "Hello, world! " @state])

(render (r/as-element [hello]))

Promesa

Working with callbacks and promises can become tedious. Since nbb v0.0.36 the promesa.core namespace is included with the let and do! macros. An example:

(ns prom
  (:require [promesa.core :as p]))

(defn sleep [ms]
  (js/Promise.
   (fn [resolve _]
     (js/setTimeout resolve ms))))

(defn do-stuff
  []
  (p/do!
   (println "Doing stuff which takes a while")
   (sleep 1000)
   1))

(p/let [a (do-stuff)
        b (inc a)
        c (do-stuff)
        d (+ b c)]
  (prn d))
$ nbb prom.cljs
Doing stuff which takes a while
Doing stuff which takes a while
3

Also see API docs.

Js-interop

Since nbb v0.0.75 applied-science/js-interop is available:

(ns example
  (:require [applied-science.js-interop :as j]))

(def o (j/lit {:a 1 :b 2 :c {:d 1}}))

(prn (j/select-keys o [:a :b])) ;; #js {:a 1, :b 2}
(prn (j/get-in o [:c :d])) ;; 1

Most of this library is supported in nbb, except the following:

  • destructuring using :syms
  • property access using .-x notation. In nbb, you must use keywords.

See the example of what is currently supported.

Examples

See the examples directory for small examples.

Also check out these projects built with nbb:

API

See API documentation.

Migrating to shadow-cljs

See this gist on how to convert an nbb script or project to shadow-cljs.

Build

Prequisites:

  • babashka >= 0.4.0
  • Clojure CLI >= 1.10.3.933
  • Node.js 16.5.0 (lower version may work, but this is the one I used to build)

To build:

  • Clone and cd into this repo
  • bb release

Run bb tasks for more project-related tasks.

Download Details:
Author: borkdude
Download Link: Download The Source Code
Official Website: https://github.com/borkdude/nbb 
License: EPL-1.0

#node #javascript

Comment construire votre propre DAO en utilisant Next.js, thirdweb, MetaMask & Alchemy

Apprenez à créer un DAO, à créer des NFT, des jetons de largage aérien et à ajouter une fonctionnalité de vote en utilisant uniquement JavaScript dans ce didacticiel complet.

Qu'est-ce qu'un DAO ?

DAO signifie Organisation Autonome Décentralisée. Comme son nom l'indique, un DAO est une organisation sans chef unique; au lieu de cela, les règles sont encodées dans la blockchain. Pour cette raison, un DAO est complètement transparent et tous ceux qui y participent ont un intérêt. Les décisions importantes sont prises par vote parmi ceux qui possèdent des jetons non fongibles (NFT) du DAO, qui accordent l'adhésion.

Aujourd'hui, nous allons créer notre propre DAO en utilisant Next.js, thirdweb, MetaMask et Alchemy. Il permettra aux utilisateurs de frapper le NFT de votre DAO, de recevoir de la crypto-monnaie via des parachutages et de participer aux sondages du DAO. Ce didacticiel sera écrit uniquement avec JavaScript, vous n'avez donc pas besoin de connaître Solidity.

Conditions préalables

Pour comprendre et suivre ce didacticiel, vous devez disposer des éléments suivants :

  • Connaissance pratique de JavaScript, Next.js et de la blockchain
  • Un portefeuille MetaMask
  • Un compte chez Alchemy

Contenu

  • Installer
    • Créer une application Alchemy
    • Obtenir la clé privée du portefeuille
    • Ajouter .envdes variables
  • Ajout de la fonctionnalité de connexion à l'aide de MetaMask
    • Ajout du troisième fournisseur Web
    • Ajout du composant de connexion
    • Initialisation du SDK thirdweb
  • Ajout de fonctionnalités pour créer un NFT
    • Créer et configurer un NFT
    • Vérifier si l'utilisateur a déjà un NFT
    • Création d'un bouton pour créer des NFT
    • Ajouter des styles
  • Créer et déployer un jeton de gouvernance
    • Frappe de jetons
    • Jetons de largage aérien
    • Autoriser les utilisateurs à voter
    • Affichage des propositions sur le site Web
    • Obtenir les propositions
    • Rendre les propositions
  • Conclusion

Installer

Nous allons commencer par configurer une application Next.js avec la commande suivante :

npx créer-next-app mon-dao

Maintenant, nous allons créer un nouveau projet dans thirdweb . Allez sur thirdweb et connectez votre portefeuille MetaMask. Après avoir connecté votre portefeuille, cliquez sur Créer un projet et choisissez le réseau Rinkeby.

Donnez un nom et une description à votre projet, puis cliquez sur Créer . Si vous n'avez pas assez d'ETH pour payer l'essence, procurez-vous en à ce robinet .

Créer une application Alchemy

Ensuite, rendez-vous sur Alchemy, connectez-vous, cliquez sur Créer une application et fournissez les détails requis. Assurez-vous d'utiliser la même chaîne que celle que vous avez utilisée dans thirdweb - dans notre cas, il s'agit de la chaîne Ethereum et du réseau Rinkeby.

Création d'une application d'alchimie

Une fois l'application créée, copiez la clé d'API HTTP.

Obtenir la clé privée du portefeuille

Afin de créer des NFT et d'exécuter certains scripts, nous allons avoir besoin de la clé privée du portefeuille.

 

Pour y accéder, ouvrez l'extension de navigateur MetaMask et cliquez sur Détails du compte . Vous devriez voir votre clé privée ici; exportez-le et copiez-le dans un endroit sûr.

Ajouter .envdes variables

Ajoutons ces variables dans un .envfichier afin de pouvoir y accéder plus tard :

PRIVATE_KEY=<wallet_private_key>
ALCHEMY_API_URL=<alchemy_http_key>
WALLET_ADDRESS=<public_wallet_address>

Parce que nous ne voulons pas les pousser vers GitHub, assurez-vous de les ajouter dans .gitignore

Ajout de la fonctionnalité de connexion à l'aide de MetaMask

Dans DApps, MetaMask est le portefeuille le plus utilisé, nous ajouterons donc la connexion MetaMask avec thirdweb.

Nous allons avoir besoin de deux packages à partir de l'installation :

npm i @3rdweb/sdk @3rdweb/hooks # npm

yarn add @3rdweb/sdk @3rdweb/hooks # yarn

Ajout du troisième fournisseur Web

Nous devons encapsuler l'ensemble de notre application dans un troisième fournisseur Web afin d'accéder aux informations de connexion et aux autres informations requises pour les composants :

import { thirdwebWeb3Provider } from "@3rdweb/hooks";
import "../styles/globals.css";

function MyApp({ Component, pageProps }) {
  return (
    <thirdwebWeb3Provider>
      <Component {...pageProps} />
    </thirdwebWeb3Provider>
  );
}

export default MyApp; 

À des fins d'authentification, nous devons également spécifier le type d'authentification et les ID de chaîne pris en charge. Nous utilisons MetaMask et la chaîne Rinkeby, alors ajoutez également ce qui suit :

const supportedChainIds = [4];

const connectors = {
  injected: {},
};

Enfin, transmettez-les en tant qu'accessoires dans le fournisseur comme suit :

  <thirdwebWeb3Provider
      connectors={connectors}
      supportedChainIds={supportedChainIds}
    >
      <Component {...pageProps} />
  </thirdwebWeb3Provider>

Ajout du composant de connexion

Créez un nouveau dossier appelé componentsà la racine du projet et ajoutez-y un Login.jsfichier :

import { useWeb3 } from "@3rdweb/hooks";
const Login = () => {
  const { connectWallet } = useWeb3();
  return (
    <div>
      <button onClick={() => connectWallet("injected")}>
        Sign in using MetaMask
      </button>
    </div>
  );
};
export default Login;

Heureusement, thirdweb fournit une connectWalletfonction que nous pouvons utiliser pour ajouter une authentification !

Rendu du composant de connexion

À l' intérieur index.jsde , affichez l'écran de connexion s'il n'y a pas d'adresse (si l'utilisateur n'est pas connecté) :

const { address } = useWeb3();
if (!address) {
  return <Login />;
}

Cela permettra à nos utilisateurs de se connecter, mais ensuite, un écran vide s'affichera. Donc, dans l'autre bloc de retour, montrons à l'utilisateur son adresse :

export default function Home() {
  const { address } = useWeb3();
  if (!address) {
    return <Login />;
  }
  return (
    <div className={styles.container}>
      <h2>You are signed in as {address}</h2>
    </div>
  );
}

La connexion fonctionne mais cela ne semble pas bon pour le moment. Créez donc un nouveau fichier Login.module.cssdans le stylesdossier et ajoutez ce qui suit :

.container {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: #7449bb;
}
.button {
  color: #7449bb;
  background-color: white;
  border: none;
  border-radius: 5px;
  padding: 10px;
  font-size: 16px;
  margin: 4px 2px;
  cursor: pointer;
  font-weight: 500;
}

Ensuite, ajoutez les classes suivantes àLogin.js :

<div className={styles.container}>
  <button className={styles.button} onClick={() => connectWallet("injected")}>
    Sign in using MetaMask
  </button>
</div>

Et enfin, importez les styles :

import styles from "../styles/Login.module.css";

Cela nous donnera un écran de connexion simple mais beau.

métamasque de connexion

Initialisation du SDK thirdweb

Nous devons maintenant initialiser le SDK thirdweb pour les différents scripts que nous allons exécuter. Commencez par créer un nouveau dossier appelé scriptsavec un initialize-sdk.jsfichier à l'intérieur.

Ajoutez le code suivant au fichier :

import { thirdwebSDK } from "@3rdweb/sdk";
import ethers from "ethers";
import dotenv from "dotenv";

dotenv.config();

if (!process.env.PRIVATE_KEY || process.env.PRIVATE_KEY == "") {
  console.log(" Private key not found.");
}
if (!process.env.ALCHEMY_API_URL || process.env.ALCHEMY_API_URL == "") {
  console.log(" Alchemy API URL not found.");
}
if (!process.env.WALLET_ADDRESS || process.env.WALLET_ADDRESS == "") {
  console.log(" Wallet Address not found.");
}
const sdk = new thirdwebSDK(
  new ethers.Wallet(
    process.env.PRIVATE_KEY,
    ethers.getDefaultProvider(process.env.ALCHEMY_API_URL)
  )
);
(async () => {
  try {
    const apps = await sdk.getApps();
    console.log("app address:", apps[0].address);
  } catch (err) {
    console.error("Failed to get apps from the sdk", err);
    process.exit(1);
  }
})();
export default sdk;

Cela initialisera le SDK thirdweb, et comme vous pouvez le voir, nous devons installer certains packages :

npm i ethers dotenv # npm

yarn add ethers dotenv # yarn

Nous utilisons ici des importations modulaires, alors créez un nouveau package.jsonfichier dans le scriptsdossier et ajoutez simplement ce qui suit :

{
  "name": "scripts",
  "type": "module"
}

Enfin, exécutez le script :

node scripts/initialize-sdk.js

L'exécution du script peut prendre un certain temps, mais après un certain temps, vous obtiendrez l'adresse de votre application.

Adresse de l'application Thirdweb

Nous en aurons besoin dans les prochaines étapes, alors stockez-le dans un endroit sûr.

Ajout de fonctionnalités pour créer un NFT

Pour cette étape, nous allons avoir besoin d'ETH de test, alors allez à un robinet comme celui -ci et obtenez-en.

Créer et configurer un NFT

Créez un nouveau fichier appelé deploy-drop.jsà l'intérieur du scriptsdossier. Ici, ajoutez le script suivant :

import { ethers } from "ethers";
import sdk from "./initialize-sdk.js";
import { readFileSync } from "fs";

const app = sdk.getAppModule("APP_ADDRESS");

(async () => {
  try {
    const bundleDropModule = await app.deployBundleDropModule({
      name: "LogRocket DAO", // Name of NFT Collection for DAO
      description: "A DAO for all the LogRocket readers.", // Description
      image: readFileSync("scripts/assets/rocket.png"), // Image for NFT collection
      primarySaleRecipientAddress: ethers.constants.AddressZero,
    });

    console.log(
      " Successfully deployed bundleDrop module, address:",
      bundleDropModule.address,
    );
    console.log(
      " bundleDrop metadata:",
      await bundleDropModule.getMetadata(),
    );
  } catch (error) {
    console.log("failed to deploy bundleDrop module", error);
  }
})()

Vous devrez mettre à jour quelques éléments ici :

  • Mettez à jour l'adresse de l'application avec la nouvelle adresse de l'application que vous avez obtenue en exécutant le script précédent
  • Mettre à jour le nom du dépôt NFT pour le DAO et sa description
  • Ajoutez une image pour le dépôt NFT en créant un nouveau dossier appelé assetset en y ajoutant l'image pour votre NFT

Après avoir mis à jour les détails, exécutez le script suivant :

node scripts/deploy-drop.js

Attendez que le script s'exécute et vous devriez obtenir une adresse et les métadonnées.

Adresse NFT et métadonnées

Vous pouvez même vérifier la transaction sur Rinkeby Etherscan

Configurons notre NFT maintenant ! Créez un nouveau config-nft.jsfichier dans le scriptsdossier et ajoutez ce qui suit :

import sdk from "./initialize-sdk.js";
import { readFileSync } from "fs";
const bundleDrop = sdk.getBundleDropModule(
  "BUNDLE_DROP_ADDRESS"
);
(async () => {
  try {
    await bundleDrop.createBatch([
      {
        name: "LogRocket DAO", // Name of NFT Collection for DAO
        description: "A DAO for all the LogRocket readers.", // Description
        image: readFileSync("scripts/assets/rocket.png"), // Image for NFT collection
      },
    ]);
    console.log(" Successfully created a new NFT in the drop!");
  } catch (error) {
    console.error("failed to create the new NFT", error);
  }
})();

Vous devez mettre à jour l'adresse de dépôt du bundle et les détails dans l'objet à l'intérieur de createBatch. Ces détails vont être utilisés pour le NFT !

Une fois que vous les avez tous mis à jour, exécutez le script suivant :

node scripts/config-nft.js

Cela devrait vous donner une sortie comme celle-ci.

NFT-configs-sortie

Si vous voyez le module dans le tableau de bord thirdweb, vous verrez qu'un NFT a été créé !🥳

Tableau de bord Thirdweb

Enfin, ajoutons une condition de réclamation à notre NFT

La définition d'une condition de réclamation nous permettra de fixer une limite pour les NFT et d'autoriser une limite maximale spécifique par transaction. Nous définirons une condition de réclamation à partir du tableau de bord lui-même, alors cliquez sur le bouton Paramètres et vous pourrez mettre à jour les données en fonction de vos besoins.

revendiquer les paramètres de condition

Une fois la mise à jour terminée, cliquez sur Enregistrer et confirmez la petite transaction.

Vérifier si l'utilisateur a un NFT

Avant de créer un bouton de menthe qui permet aux utilisateurs de frapper des NFT, vérifions si l'utilisateur a déjà un NFT. Nous ne voulons pas que les utilisateurs frappent plusieurs NFT !

Commencez par ajouter deux nouvelles variables, sdket bundleDropModule, comme ceci avant notre composant fonctionnel :

const sdk = new thirdwebSDK("rinkeby");
const bundleDropModule = sdk.getBundleDropModule(
  "BUNDLE_DROP_ADDRESS"
);

Vous devrez également importer thirdwebSDK:

import { thirdwebSDK } from "@3rdweb/sdk";

Maintenant, créons un état pour hasClaimedNFT:

const [hasClaimedNFT, setHasClaimedNFT] = useState(false);

Nous devons également créer un useEffectcrochet pour vérifier si l'utilisateur a le NFT :

  useEffect(() => {
    const handler = async () => {
      if (!address) {
        return;
      }
      try {
        const balance = await bundleDropModule.balanceOf(address, "0");
        if (balance.gt(0)) {
          setHasClaimedNFT(true);
          console.log(" You have an NFT!");
        } else {
          setHasClaimedNFT(false);
          console.log(" You don't have an NFT.");
        }
      } catch (error) {
        setHasClaimedNFT(false);
        console.error("failed to nft balance", error);
      }
    };
    handler();
  }, [address]);

Tout d'abord, il vérifiera si l'utilisateur est connecté. Si l'utilisateur n'est pas connecté, il ne renverra rien. Ensuite, cela vérifie si l'utilisateur a le NFT avec l'ID de jeton 0dans le contrat de dépôt que nous avons importé en haut.

Si vous ouvrez la console sur le site Web, cela devrait indiquer que vous n'avez pas de NFT.

Vous n'avez pas de message NFT

Création d'un bouton pour créer des NFT

Créons le bouton pour créer des NFT ! Créez une nouvelle fonction appelée mintNftcomme suit :

const mintNft = async () => {
  setIsClaiming(true);
  try {
    await bundleDropModule.claim("0", 1);
    setHasClaimedNFT(true);
    console.log(" Successfully Minted the NFT!");
  } catch (error) {
    console.error("failed to claim", error);
  } finally {
    setIsClaiming(false);
  }
};

Nous appellerons cette fonction lorsqu'un bouton est cliqué pour frapper le NFT sur le portefeuille de l'utilisateur. Mais d'abord, ajoutons les deux états requis :

const [hasClaimedNFT, setHasClaimedNFT] = useState(false);
const [isClaiming, setIsClaiming] = useState(false);

Nous devons également appeler la providerSignerfonction, donc obtenez d'abord le fournisseur de useWeb3:

const { address, provider } = useWeb3();

Et ensuite appelez la fonction :

const signer = provider ? provider.getSigner() : undefined;

useEffect(() => {
  sdk.setProviderOrSigner(signer);
}, [signer]);

Créons le bouton maintenant ! Dans le bloc de retour final, ajoutez ce qui suit :

<div>
  <h1>Mint your free LogRocket DAO Membership NFT </h1>
  <button disabled={isClaiming} onClick={() => mintNft()}>
    {isClaiming ? "Minting..." : "Mint your nft (FREE)"}
  </button>
</div>

Maintenant, après nous être connectés, il devrait nous montrer un écran comme celui-ci.

Bouton d'écran Mint NFT

Vous pouvez même visualiser votre NFT sur Opensea Testnets ; allez simplement sur https://testnets.opensea.io/assets//0

Enfin, juste au-dessus du bloc de retour final, ajoutez cette vérification pour voir si l'utilisateur a déjà réclamé le NFT :

if (hasClaimedNFT) {
  return (
    <div>
      <h1>You have the DAO Membership NFT!</h1>
    </div>
  );
}

Ajouter des styles

Nous avons terminé la construction de la fonctionnalité NFT de frappe, mais elle a l'air moche, alors ajoutons quelques styles de base. À l' intérieur, Home.module.cssajoutez ce qui suit :

.container {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: #7449bb;
}
.container > h1 {
  font-size: 3rem;
  color: #fff;
  font-weight: bold;
}
.button {
  color: #7449bb;
  background-color: white;
  border: none;
  border-radius: 5px;
  padding: 10px;
  font-size: 16px;
  margin: 4px 2px;
  cursor: pointer;
  font-weight: 500;
}

Il faut aussi ajouter le classNames:

 if (hasClaimedNFT) {
    return (
      <div className={styles.container}>
        <h1>You have the DAO Membership NFT!</h1>
      </div>
    );
  }
  return (
    <div className={styles.container}>
      <h1>Mint your free LogRocket DAO Membership NFT </h1>
      <button
        className={styles.button}
        disabled={isClaiming}
        onClick={() => mintNft()}
      >
        {isClaiming ? "Minting..." : "Mint your NFT (FREE)"}
      </button>
    </div>
  );
};

Cela nous donne un meilleur écran de menthe.

Écran de frappe NFT stylé

Créer et déployer un jeton de gouvernance

Créez un nouveau fichier appelé deploy-token.jsdans le scriptsdossier. Ajoutez-y ce qui suit :

import sdk from "./initialize-sdk.js";
const app = sdk.getAppModule("YOUR_APP_ADDRESS");

(async () => {
  try {
    const tokenModule = await app.deployTokenModule({
      name: "LogRocket Token", // name of the token
      symbol: "LR", // symbol
    });
    console.log(
      " Successfully deployed token module, address:",
      tokenModule.address
    );
  } catch (error) {
    console.error("failed to deploy token module", error);
  }
})();

Ce script créera un nouveau module de jeton avec un nom et un symbole. Vous devrez mettre à jour manuellement l'adresse de l'application, le nom du jeton et le symbole vous-même.

Après la mise à jour, exécutez le script.

Vous pouvez vérifier ce jeton par l'adresse sur Rinkeby Etherscan, et également l'ajouter sur votre portefeuille MetaMask en cliquant sur Importer des jetons .

jetons d'importation de métamasque

Après l'importation, vous devriez voir le jeton sous vos actifs.

Nombre de jetons 0 LR

Il est actuellement nul, alors frappons quelques jetons !

Frappe de jetons

Créez un nouveau fichier appelé mint-token.jsdans le scriptsdossier et ajoutez ce qui suit :

import { ethers } from "ethers";
import sdk from "./initialize-sdk.js";
const tokenModule = sdk.getTokenModule(
  "TOKEN_MODULE_ADDRESS"
);

(async () => {
  try {
    const amount = 1_000_000;
    const amountWith18Decimals = ethers.utils.parseUnits(amount.toString(), 18);
    await tokenModule.mint(amountWith18Decimals);
    const totalSupply = await tokenModule.totalSupply();
    console.log(
      " There now is",
      ethers.utils.formatUnits(totalSupply, 18),
      "$LR in circulation"
    );
  } catch (error) {
    console.error("Failed to mint tokens", error);
  }
})();

Mettez à jour l'adresse du module de jeton avec l'adresse que vous avez obtenue dans le dernier script, et vous pouvez mettre à jour le montant que vous souhaitez frapper.

Une fois que vous êtes prêt à frapper, exécutez le script :

node scripts/mint-token.js

Vous devriez maintenant voir le nombre de jetons que vous avez émis sur votre portefeuille MetaMask !🎉

nombre de jetons 1 million

Jetons de largage aérien

Nous pourrions souhaiter diffuser les jetons à nos détenteurs de NFT, alors créons un script pour cela. Créez un nouveau airdrop.jsfichier à l'intérieur scriptset ajoutez ce qui suit :

import { ethers } from "ethers";
import sdk from "./initialize-sdk.js";
const bundleDropModule = sdk.getBundleDropModule(
  "BUNDLE_DROP_ADDRESS"
);
const tokenModule = sdk.getTokenModule(
  "TOKEN_MODULE_ADDRESS"
);
(async () => {
  try {
    const walletAddresses = await bundleDropModule.getAllClaimerAddresses("0");
    if (walletAddresses.length === 0) {
      console.log(
        "No NFTs have been claimed yet, ask your friends to mint some free ones :P!"
      );
      process.exit(0);
    }
    const airdropTargets = walletAddresses.map((address) => {
      const randomAmount = Math.floor(
        Math.random() * (10000 - 1000 + 1) + 1000
      );
      console.log(" Going to airdrop", randomAmount, "tokens to", address);
      return {
        address,
        amount: ethers.utils.parseUnits(randomAmount.toString(), 18),
      };
    });
    console.log(" Starting airdrop...");
    await tokenModule.transferBatch(airdropTargets);
    console.log(
      " Successfully airdropped tokens to all the holders of the NFT!"
    );
  } catch (err) {
    console.error("Failed to airdrop tokens", err);
  }
})();

Après avoir exécuté le script, vous devriez obtenir quelque chose comme ça.

Message de réussite du largage

Actuellement, vous seul avez créé un NFT, il n'enverra donc pas le jeton à quelqu'un d'autre. Mais cela peut être utilisé pour l'envoyer ultérieurement à d'autres détenteurs de NFT.

Autoriser les utilisateurs à voter

Créez un nouveau deploy-vote.jsfichier dans le scriptsdossier et ajoutez ce qui suit :

import sdk from "./initialize-sdk.js";
const appModule = sdk.getAppModule(
  "APP_MODULE_ADDRESS"
);
(async () => {
  try {
    const voteModule = await appModule.deployVoteModule({
      name: "LR Dao's Proposals",
      votingTokenAddress: "0x6fb07DCBC53Fd8390de38CDBfCc267b5A72761ca",
      proposalStartWaitTimeInSeconds: 0,
      proposalVotingTimeInSeconds: 24 * 60 * 60,
      votingQuorumFraction: 0,
      minimumNumberOfTokensNeededToPropose: "0",
    });
    console.log(
      " Successfully deployed vote module, address:",
      voteModule.address
    );
  } catch (err) {
    console.error("Failed to deploy vote module", err);
  }
})();

Mettez à jour l'adresse de l'application, le nom et l'adresse du jeton de vote, puis exécutez le script :

node scripts/deploy-vote.js

Nous devons également configurer un module de vote, alors créez un nouveau script appelé setup-vote.jset ajoutez ce qui suit :

import { ethers } from "ethers";
import sdk from "./initialize-sdk.js";
const voteModule = sdk.getVoteModule(
  "0xA57A03B9e117723b6873100742116A01140C43f4"
);
const tokenModule = sdk.getTokenModule(
  "0x6fb07DCBC53Fd8390de38CDBfCc267b5A72761ca"
);
(async () => {
  try {
    await tokenModule.grantRole("minter", voteModule.address);
    console.log(
      "Successfully gave vote module permissions to act on token module"
    );
  } catch (error) {
    console.error(
      "failed to grant vote module permissions on token module",
      error
    );
    process.exit(1);
  }
  try {
    const ownedTokenBalance = await tokenModule.balanceOf(
      process.env.WALLET_ADDRESS
    );
    const ownedAmount = ethers.BigNumber.from(ownedTokenBalance.value);
    const percent90 = ownedAmount.div(100).mul(90);
    await tokenModule.transfer(voteModule.address, percent90);
    console.log(" Successfully transferred tokens to vote module");
  } catch (err) {
    console.error("failed to transfer tokens to vote module", err);
  }
})();

Vous devrez exécuter ce script pour le terminer :

node scripts/setup-vote.js

Maintenant que notre module de vote est prêt, créons quelques propositions !

Créez un nouveau fichier appelé vote-proposals.jsdans le scriptsdossier et ajoutez ce qui suit :

import { ethers } from "ethers";
import sdk from "./initialize-sdk.js";

const voteModule = sdk.getVoteModule(
  "VOTE_MODULE"
);
const tokenModule = sdk.getTokenModule(
  "TOKEN_MODULE"
);
(async () => {
  try {
    const amount = 10000;
    await voteModule.propose(
      "Should the DAO mint an additional " +
        amount +
        " tokens into the treasury?",
      [
        {
          nativeTokenValue: 0,
          transactionData: tokenModule.contract.interface.encodeFunctionData(
            "mint",
            [voteModule.address, ethers.utils.parseUnits(amount.toString(), 18)]
          ),
          toAddress: tokenModule.address,
        },
      ]
    );
    console.log(" Successfully created proposal to mint tokens");
  } catch (error) {
    console.error("failed to create first proposal", error);
    process.exit(1);
  }
})();

Vous devez mettre à jour les adresses des modules, et si vous souhaitez mettre à jour le message de la proposition, vous pouvez également le mettre à jour.

Enfin, exécutez le script. Cela devrait vous donner quelque chose comme ça.

proposition réussie de frapper un message de jetons

Si vous vérifiez maintenant le tableau de bord thirdweb, la proposition a été créée.🎉

proposition approuvée par thirdweb

Affichage des propositions sur le site Web

Tout d'abord, importez le module jeton et vote :

const voteModule = sdk.getVoteModule(
  "0xf738973379b8B6444e429D2fd6C8B1f223247390"
);
const tokenModule = sdk.getTokenModule(
  "0x8C35288de335070dd1C00d68d71383d81437472A"
);

Nous allons avoir besoin de trois useStates, comme ceci :

const [proposals, setProposals] = useState([]);
const [isVoting, setIsVoting] = useState(false);
const [hasVoted, setHasVoted] = useState(false); 

Obtenir les propositions

Nous devons récupérer les propositions pour les afficher à l'écran, alors créez ceci useEffect:

useEffect(() => {
    if (!hasClaimedNFT) {
      return;
    }
    voteModule
      .getAll()
      .then((proposals) => {
        setProposals(proposals);
      })
      .catch((err) => {
        console.error("failed to get proposals", err);
      });
  }, [hasClaimedNFT]);

Ensuite, créez une nouvelle handleFormSubmitfonction :

  const handleFormSubmit = async (e) => {
    e.preventDefault();
    e.stopPropagation();
    setIsVoting(true);
    const votes = proposals.map((proposal) => {
      let voteResult = {
        proposalId: proposal.proposalId,
        vote: 2,
      };
      proposal.votes.forEach((vote) => {
        const elem = document.getElementById(
          proposal.proposalId + "-" + vote.type
        );
        if (elem.checked) {
          voteResult.vote = vote.type;
          return;
        }
      });
      return voteResult;
    });
    try {
      const delegation = await tokenModule.getDelegationOf(address);
      if (delegation === ethers.constants.AddressZero) {
        await tokenModule.delegateTo(address);
      }
      try {
        await Promise.all(
          votes.map(async (vote) => {
            const proposal = await voteModule.get(vote.proposalId);
            if (proposal.state === 1) {
              return voteModule.vote(vote.proposalId, vote.vote);
            }
            return;
          })
        );
        try {
          await Promise.all(
            votes.map(async (vote) => {
              const proposal = await voteModule.get(vote.proposalId);
              if (proposal.state === 4) {
                return voteModule.execute(vote.proposalId);
              }
            })
          );
          setHasVoted(true);
        } catch (err) {
          console.error("failed to execute votes", err);
        }
      } catch (err) {
        console.error("failed to vote", err);
      }
    } catch (err) {
      console.error("failed to delegate tokens", err);
    } finally {
      setIsVoting(false);
    }
  };

Cette fonction va collecter le vote.

Rendre les propositions

Remplacez le if (hasClaimedNFT)bloc par ceci :

if (hasClaimedNFT) {
    return (
      <div className={styles.container}>
          <h2>Active Proposals</h2>
          <form onSubmit={handleFormSubmit}>
            {proposals.map((proposal) => (
              <Proposal
                key={proposal.proposalId}
                votes={proposal.votes}
                description={proposal.description}
                proposalId={proposal.proposalId}
              />
            ))}
            <button
              onClick={handleFormSubmit}
              type="submit"
              className={styles.button}
            >
              {isVoting
                ? "Voting..."
                "Submit Votes"}
            </button>
          </form>
        </div>
    );
  }

Nous créons un composant distinct pour la proposition afin de garder les choses propres. Créez donc un nouveau fichier appelé Proposal.jsdans le componentsdossier et ajoutez ce qui suit :

import styles from "../styles/Proposal.module.css";

const Proposal = ({ description, votes, proposalId }) => {
  return (
    <div className={styles.proposal}>
      <h5 className={styles.description}>{description}</h5>
      <div className={styles.options}>
        {votes.map((vote) => (
          <div key={vote.type}>
            <input
              type="radio"
              id={proposalId + "-" + vote.type}
              name={proposalId}
              value={vote.type}
              defaultChecked={vote.type === 2}
            />
            <label htmlFor={proposalId + "-" + vote.type}>{vote.label}</label>
          </div>
        ))}
      </div>
    </div>
  );
};
export default Proposal;

J'ai également ajouté un style de base, alors créez un nouveau Proposal.module.cssfichier dans le stylesdossier :

.proposal {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: #fafafa;
  border-radius: 10px;
  box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
  padding: 20px;
  margin: 20px;
}
.options {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: flex-start;
  width: 100%;
  margin-top: 1rem;
}

Pour centrer les éléments, j'ai également ajouté les styles suivantsHome.module.css :

.container > form {
  display: flex;
  flex-direction: column;
  align-items: center;
}

Vous arriverez sur cet écran où vous pourrez soumettre vos votes.🎉

Écran de vote de proposition

Enfin, créons une fonction pour vérifier si la personne a déjà voté.

Tout d'abord, créez un nouveau useEffect:

  useEffect(() => {
    if (!hasClaimedNFT) {
      return;
    }
    if (!proposals.length) {
      return;
    }
    voteModule
      .hasVoted(proposals[0].proposalId, address)
      .then((hasVoted) => {
        setHasVoted(hasVoted);
        if (hasVoted) {
        } else {
        }
      })
      .catch((err) => {
        console.error("failed to check if wallet has voted", err);
      });
  }, [hasClaimedNFT, address, proposals]);

Et remplacez le bouton par ceci :

 <button
  onClick={handleFormSubmit}
  type="submit"
  disabled={isVoting || hasVoted}
  className={styles.button}
>
  {isVoting ? "Voting..." : hasVoted ? "You Already Voted" : "Submit Votes"}
</button>

Après avoir voté, il devrait afficher le message Vous avez déjà voté :

Vous avez déjà voté message

Conclusion

C'était tout pour ce tutoriel, j'espère que vous l'avez aimé et que vous pourrez l'utiliser pour créer votre propre DAO ! Vous pouvez toujours mettre à jour le DAO et ajouter plus de fonctionnalités si vous le souhaitez.

Vous pouvez trouver le référentiel GitHub pour ce projet ici .

Source de l'article original sur https://blog.logrocket.com

Eva  Murphy

Eva Murphy

1625674200

Google analytics Setup with Next JS, React JS using Router Events - 14

In this video, we are going to implement Google Analytics to our Next JS application. Tracking page views of an application is very important.

Google analytics will allow us to track analytics information.

Frontend: https://github.com/amitavroy/video-reviews
API: https://github.com/amitavdevzone/video-review-api
App link: https://video-reviews.vercel.app

You can find me on:
Twitter: https://twitter.com/amitavroy7​
Discord: https://discord.gg/Em4nuvQk

#next js #js #react js #react #next #google analytics

Eva  Murphy

Eva Murphy

1625751960

Laravel API and React Next JS frontend development - 28

In this video, I wanted to touch upon the functionality of adding Chapters inside a Course. The idea was to not think much and start the development and pick up things as they come.

There are places where I get stuck and trying to find answers to it up doing what every developer does - Google and get help. I hope this will help you understand the flow and also how developers debug while doing development.

App url: https://video-reviews.vercel.app
Github code links below:
Next JS App: https://github.com/amitavroy/video-reviews
Laravel API: https://github.com/amitavdevzone/video-review-api

You can find me on:
Twitter: https://twitter.com/amitavroy7​
Discord: https://discord.gg/Em4nuvQk

#next js #api #react next js #next #frontend #development