Tiempo para completar: 50-60 minutos

En esta guía, crearemos una aplicación web3 construida en React, aprovechando herramientas de Sequence Stack para mintear loot generado por IA desde cofres del tesoro usando Embedded Wallet para autenticación y Cloudflare Worker para transacciones fáciles y sin confirmación.

Esto está integrado en un juego dungeon crawler para mostrar estas funciones en un entorno de juego donde puede jugar y ganar loot.

El código completo está disponible en los siguientes repositorios:

Estas herramientas le permitirán realizar:

  1. Registro en Sequence Builder Console y creación de proyecto: Cree un proyecto con el Builder
  2. Gestión de claves de acceso: Reclame una clave de acceso pública, una clave secreta y una clave de configuración waas para interactuar con Sequence Stack
  3. Integración de Embedded Wallet: Integre un Embedded Wallet en la aplicación
  4. Despliegue de contrato y patrocinio de gas: Despliegue un contrato de ítems y patrocine el gas
  5. Despliegue de un Cloudflare Worker: Despliegue un Cloudflare Worker para transacciones sin gas y sin confirmación
  6. Generación de prompts e imágenes con IA: Cree prompts de IA a partir de una API y genere imágenes para cargar
  7. Almacenar medios en el servicio de metadatos de Sequence: Suba metadatos de colección y tokens a Sequence
  8. Asegure su Cloudflare Worker: Evite solicitudes externas a su Cloudflare Worker restringiendo la URL de referencia
  9. (Opcional) Restricción de minteo nativo por wallet: Limite el minteo diario por wallet

1. Registro en Sequence Builder Console y creación de proyecto

Primero, siga esta guía para aprender cómo registrarse en el Sequence Builder Console y crear un proyecto.

Para usar ciertas funciones, como Gas Sponsoring y el uso de la Transactions API, deberá actualizar su plan de proyecto a Developer siguiendo esta guía

2. Gestión de claves de acceso

Ahora que tiene un proyecto, necesitará obtener 3 claves de acceso diferentes para su proyecto y así autenticar su aplicación con Sequence Stack:

  1. Waas Config Key utilizada para Embedded Wallet, puede aprender más aquí
  2. Public Access Key utilizada para Embedded Wallet y Transactions API, puede obtenerla aquí
  3. Secret Access Key utilizada para el servicio de metadatos, siguiendo estos pasos

Creación de clave de acceso secreta

1

Acceder a configuración

Primero, acceda a la configuración y seleccione las claves de API

2

Agregar cuenta de servicio

Desplácese hacia abajo y seleccione + Add Service Account

3

Seleccionar permiso de escritura

Luego cambie el acceso a Write y Confirm

Por último, copie la clave y guárdela en un lugar seguro, ya que no podrá acceder a ella nuevamente desde el Builder Console.

3. Integración de Embedded Wallet

Puede ver y clonar el repositorio de la plantilla aquí

Comenzaremos desde cero construyendo el proyecto con los componentes necesarios para habilitar el uso de un Sequence Embedded Wallet, lo que permite que los usuarios se incorporen a su aplicación usando proveedores de autenticación web2.

Primero, cree una carpeta de proyecto con mkdir <project>, luego cd <project> y cree un proyecto vite usando React:

pnpm create vite



# or 

yarn create vite



# or 

npm create vite

A continuación, instalaremos el paquete correcto de Wallet-as-a-Service (Waas) para usar Embedded Wallet:

pnpm install @0xsequence/waas



# or

npm install @0xsequence/waas



# or

yarn add @0xsequence/waas

Todos los archivos nuevos creados en los siguientes pasos deben crearse en /src

Primero, cree un archivo llamado algo como SequenceEmbeddedWallet.ts con el siguiente código de inicialización:

import { SequenceWaaS } from '@0xsequence/waas'



const sequence = new SequenceWaaS({

    projectAccessKey: import.meta.env.VITE_PROJECT_ACCESS_KEY!,

    waasConfigKey:  import.meta.env.VITE_WAAS_CONFIG_KEY!,

    network: 'arbitrum-nova'

})



export default sequence;

Luego cree otro archivo llamado useSessionHash.ts que genere un hash de sesión único para el usuario desde el SDK:

import sequence from './SequenceEmbeddedWallet.ts'

import { useEffect, useState } from "react";



export function useSessionHash() {

    const [sessionHash, setSessionHash] = useState("")

    const [error, setError] = useState<any>(undefined)



    useEffect(() => {

        const handler = async () => {

            try {

                setSessionHash(await sequence.getSessionHash())

            } catch (error) {

                console.error(error)

                setError(error)

            }

        }

        handler()

        return sequence.onSessionStateChanged(handler)

    }, [setSessionHash, setError])



    return {

        sessionHash,

        error,

        loading: !!sessionHash,

    }

}

Por último, para implementar la autenticación de Google, necesitará el GoogleOAuthProvider para envolver su aplicación. El siguiente comando instalará esto y el inicio de sesión con Apple, que se usará más adelante:

pnpm i @react-oauth/google react-apple-signin-auth

Luego, el código inicial se implementa con los archivos importados previamente, en el siguiente código dentro del archivo main.tsx:

import React from 'react'

import ReactDOM from 'react-dom/client'

import './index.css'

import App from './App.tsx'

import { useSessionHash } from "./useSessionHash.ts";



import { ThemeProvider } from '@0xsequence/design-system'

import { GoogleOAuthProvider } from '@react-oauth/google'





function Dapp() {

  const { sessionHash } = useSessionHash()



  return (

	<GoogleOAuthProvider clientId="<GOOGLE_CLIENT_ID>" nonce={sessionHash} key={sessionHash}>

		<App />

	</GoogleOAuthProvider>

  );

}



ReactDOM.createRoot(document.getElementById('root')!).render(

  <React.StrictMode>

    <Dapp />

  </React.StrictMode>

)

Una vez que su main.tsx esté listo, creemos los botones de inicio de sesión, que se verán así:

En App.tsx use el siguiente código que verifica si un usuario está conectado y muestra una dirección de wallet según el usuario autenticado, con los distintos botones y manejadores de inicio de sesión con redes sociales:

import { useState, useEffect } from 'react'

import './App.css'

import sequence from './SequenceEmbeddedWallet'

import { useSessionHash } from './useSessionHash'

import { CredentialResponse, GoogleLogin } from '@react-oauth/google';

import AppleSignin from 'react-apple-signin-auth';

import playImage from './assets/play.svg'



function LoginScreen () {

  const { sessionHash } = useSessionHash()



  const [wallet, setWallet] = useState<any>(null)



  const handleGoogleLogin = async (tokenResponse: CredentialResponse) => {

    const res = await sequence.signIn({

      idToken: tokenResponse.credential! // inputted id credential from google

    }, "template")

    setWallet(res.wallet)

  }



  const handleAppleLogin = async (response: any) => {

    const res = await sequence.signIn({

      idToken: response.authorization.id_token! // inputted id token from apple

    }, "template")

 

    setWallet(res.wallet)

  }



  // checks to see if there is a logged in user

  useEffect(() => {

    setTimeout(async () => {

      if(await sequence.isSignedIn()){

        setWallet(await sequence.getAddress())

      }

    }, 0)

  }, [])



  useEffect(() => {



  }, [wallet])



  const signOut = async () => {

    try {

      const sessions = await sequence.listSessions()



      for(let i = 0; i < sessions.length; i++){

        await sequence.dropSession({ sessionId: sessions[i].id })

      }

    }catch(err){

      console.log(err)

    }

  }



  return (

    <>

      {

        !wallet 

      ? 

        <>

          <span className='sign-in-via'>SIGN IN VIA</span>

          <br/>

          <br/>

          <br/>

          <div className="login-container">

          <div className='dashed-box-google'>

              <p className='content'>

                <div className='gmail-login' style={{overflow: 'hidden', opacity: '0', width: '90px', position: 'absolute', zIndex: 1, height: '100px'}}>

                  <GoogleLogin 

                    nonce={sessionHash}

                    key={sessionHash}

                    onSuccess={handleGoogleLogin} shape="circle" width={230} />

                  </div>

                  <span className='gmail-login'>Gmail</span>

              </p>

          </div>

          <div className='dashed-box-apple'>

            <p className='content' 

            style={{position:'relative'}}>

                <span className='apple-login'>

                  {/* @ts-ignore */}

                  <AppleSignin

                    key={sessionHash}

                    authOptions={{

                      clientId: '<replce with com. bundle id>',

                      scope: 'openid email',

                      redirectURI: '<must be a deployed URL>',

                      usePopup: true,

                      nonce: sessionHash

                    }}

                    onError={(error: any) => console.error(error)}

                    onSuccess={handleAppleLogin}

                  />Apple

                </span>

            </p>

            </div>

          </div>

        </>

      : 

        <>

          <div className="login-container">

          <p style={{cursor: 'pointer'}} onClick={() =>signOut()}>sign out</p>

          &nbsp;&nbsp;&nbsp;

          <span >{wallet}</span>

          </div>

        </>

      }

    </>

  )

}



function App() {

  return (

    <LoginScreen/>

  )

}



export default App

Luego, incluya un archivo .env en la raíz de su proyecto, agréguelo a .gitignore y actualícelo con los siguientes valores obtenidos desde el Sequence Builder:

VITE_PROJECT_ACCESS_KEY=

VITE_WAAS_CONFIG_KEY=

Todas las variables de entorno deben comenzar con VITE_ para ser incluidas en el entorno de una aplicación vite

Ejecute su código con el siguiente comando en la carpeta raíz y pruébelo:

pnpm run dev

4. Despliegue un contrato y patrocine gas

Vamos a desplegar un contrato de token para poder vincular las imágenes generadas por IA con los metadatos de cualquier token. Al desplegar su contrato, se recomienda usar un ERC1155 en lugar de un ERC721. Los beneficios de usar un ERC1155 son:

  • Semi-fungible: ideal para activos de juegos que pueden tener múltiples copias del mismo objeto.
  • Ahorro de gas: para proyectos que requieren varios tokens, ya que un solo ERC1155 puede contener muchas variedades diferentes.

En cuanto al beneficio de ahorro de gas, en vez de desplegar un nuevo contrato para cada tipo de token, un solo contrato ERC1155 puede mantener el estado completo del sistema, reduciendo costos y complejidad en el despliegue.

Para desplegar un contrato, puedes seguir esta guía para desplegar tu ERC1155 y actualizar tu wrangler.toml con el CONTRACT_ADDRESS.

Luego, para que la función de minteo funcione de manera programática y las transacciones sean sin gas para su relayer, necesitará que la Transactions API utilice los créditos de su cuenta en su plan de facturación actualizado patrocinando la dirección de su contrato inteligente desplegado.

Para permitir que la Transactions API retransmita transacciones sin comisión, patrocine el gas siguiendo esta guía para el contrato desplegado.

Todos los testnets de Sequence son gratuitos

5. Despliegue la Transactions API en un Cloudflare Worker

Siguiendo el paso anterior, la Transactions API de Sequence puedes implementar en un Cloudflare Worker sin servidor para que la interacción del usuario en el juego o la app sea fluida, sin que sea necesario firmar una confirmación ni pagar gas. En este caso, el Worker aprovechará la Transactions API de Sequence para mintear tokens a la dirección del usuario. Además, te beneficias de no tener que preocuparte por la velocidad de las transacciones, el rendimiento o los reorgs, y disfrutas de escalabilidad automática con Cloudflare.

Minteo de un token

Si deseas aprender cómo desplegar un Cloudflare Worker desde cero, puedes seguir esta guía sobre cómo crear un servicio de minteo de NFT sin servidor con tu contrato ERC1155 desplegado o simplemente clonar la plantilla específica para esta guía.

Asegúrese de que, si está usando un Sequence Standard ERC1155 Items Contract, otorgue el MINTER_ROLE a la dirección de su cuenta relayer.

Una vez configurado, llamaremos al endpoint de la instancia de Cloudflare Worker para mintear nuestros NFTs en un paso posterior.

6. Generación de prompts e imágenes con IA

Al comenzar su experiencia en la generación de imágenes con IA, necesitará una fuente de prompts de modelos de IA para producir contenido. Para esta guía y demo, hemos obtenido prompts de los objetos incluidos en el juego Diablo.

En la plantilla, hemos incluido código para llamar a una API ya desplegada y código para analizar la respuesta.

Con esta API, mostraremos cómo generar imágenes usando el prompt de la API de Diablo desplegada dentro de la función generate en el Cloudflare worker:

const generate = async () => {

	const url = 'https://flask-production-2641.up.railway.app/'; // External API endpoint

	

	const init = {

		method: 'GET',

		headers: {

		'Content-Type': 'application/json',

		},

	};



	const response = await fetch(url, init); // Fetch data from external API

	const data: any= await response.json(); 

	const defend = Math.random() >= 0.5 ? true : false

	const attributes = []

	// parse the data to create the attributes

	

	return {loot: data[defend ? 'armor' : 'weapon'], attributes: attributes}

}

Luego complete la función getInferenceWithItem para obtener la referencia de inferencia instanciada desde la Scenario API y pasarle un prompt que es el nombre y tipo del loot generado, así como algunos parámetros adicionales del modelo, los cuales pueden personalizarse según la documentación de la Scenario API:

Para esta guía, elegimos el tipo de scheduler EulerDiscreteScheduler de la Scenario API por su calidad y tiempo, pero si desea experimentar con otros schedulers, puede usar este CLI local personalizado y revisar los resultados en el dashboard de Scenario.gg

const getInferenceWithItem = async (env: Env, prompt: any) => {

	try {

		const res: any = await fetch(`https://api.cloud.scenario.com/v1/models/${env.SCENARIO_MODEL_ID}/inferences`, {

			method: 'POST',

			headers: {

				'Authorization': `Basic ${env.SCENARIO_API_KEY}`,

				'accept': 'application/json',

				'content-type': 'application/json'

			},

			body: JSON.stringify({

						"parameters": {

						"numSamples": 1,

						"qualityBoostScale": 4,

						"qualityBoost": false,

						"type": "txt2img",

						"disableMerging": false,

						"hideResults": false,

						"referenceAdain": false,

						"intermediateImages": false,

						"scheduler": 'EulerDiscreteScheduler',

						"referenceAttn": false,

						"prompt": prompt + ' single object on black background no people'

					}

				})

		})



		const data = await res.json()

		console.log(data)

		return {inferenceId: data.inference.id}

	}catch(err){

		console.log(err)

		return {inferenceId: null, err: "ERROR"}

	}

}

Luego simplemente implementamos las funciones anteriores en el código de react:

	...

	if(mint){

		...

	} else {

		const loot = await generate()

		const inferenceId = await getInferenceWithItem(env, loot.loot.name + " " + loot.loot.type)

		...

	}

	...

Una vez que tengamos el inferenceId podemos consultar el estado de la inferencia y devolver el resultado cuando esté completo, lo cual se indica con el descriptor de estado succeeded:

const getInferenceObjectWithPolling = async (env: Env, id: any) => {

	console.log('getting inference status for: ', id.inferenceId)

	const inferenceId = id.inferenceId



	const headers = {

		'Authorization': `Basic ${env.SCENARIO_API_KEY}`,

		'accept': 'application/json',

		'content-type': 'application/json'

	}



	// Function to poll the inference status

	const pollInferenceStatus = async () => {

		let status = '';

		let inferenceData: any = null;

		while (!['succeeded', 'failed'].includes(status)) {

			// Fetch the inference details

			try {

				const inferenceResponse = await fetch(`https://api.cloud.scenario.com/v1/models/${env.SCENARIO_MODEL_ID}/inferences/${inferenceId}`, {

					method: 'GET',

					headers

				})

				if (inferenceResponse.ok) {

					console.log(inferenceResponse.statusText)

					inferenceData = await inferenceResponse.json();

				}

			}catch(err){

				console.log(err)

			}

			status = inferenceData.inference.status;

			console.log(`Inference status: ${status}`);



			// Wait for a certain interval before polling again

			await new Promise(resolve => setTimeout(resolve, 5000)); // Polling every 5 seconds

		}

		// Handle the final status

		if (status === 'succeeded') {

			console.log('Inference succeeded!');

			console.log(inferenceData); // Print inference data

			return inferenceData

		} else {

			console.log('Inference failed!');

			console.log(inferenceData); // Print inference data

			throw new Error("Scenario API Failed")

		}

	};



	// Start polling the inference status

	return await pollInferenceStatus();

}

Nuevamente, agregamos la función anterior al código de React y pasamos el inferenceId. Cuando recibas la respuesta, puedes obtener la url de la imagen con resObject.inference.images[0].url:

	...

	if(mint){

		...

	} else {

		const loot = await generate()

		const inferenceId = await getInferenceWithItem(env, loot.loot.name + " " + loot.loot.type)

		const resObject = await getInferenceObjectWithPolling(env, inferenceId)

		console.log(resObject.inference.images[0].url) // prints image url

		...

	}

	...

Cabe señalar que puede diseñar aplicaciones que devuelvan varias imágenes por prompt y permitir que el usuario, desde la interfaz de usuario, elija la generación correcta.

7. Almacenar medios en Sequence Metadata Service

Con la url del medio obtenida de la Scenario API, podemos pasar a almacenar el recurso en el Sequence Metadata Service. Esto le permite vincular la imagen generada por IA con los metadatos específicos del token, todo a través de llamadas REST-API.

Cada recompensa de cofre del Dungeon Minter sigue el mismo proceso, donde primero se almacenan los metadatos usando la Sequence Metadata API, y se devuelve al cliente la url y el tokenID generado aleatoriamente (lo que permite solicitudes en paralelo). Luego, el usuario da su consentimiento, tras inspeccionar el coleccionable, para mintear el token, donde el tokenID y la address del usuario se envían de vuelta al worker creado en el Paso 5.

Implementación

Complete e integre esta guía para construir su servicio de medios sin servidor aprovechando la Sequence Metadata API que utiliza Cloudflare workers, o simplemente clone nuestra plantilla de cloudflare para esta guía.

Una vez terminado, pase el tokenID y la url del medio almacenado al frontend para que se muestre y el usuario pueda mintear después de ver qué va a mintear:

    const randomTokenIDSpace = ethers.parseUnits(String('10000'), 18)

	...

	const jsonCreateAsset = await collectionsService.createAsset({...})

	...

	const response = await uploadAsset(env, projectID, collectionID, jsonCreateAsset.asset.id, String(randomTokenIDSpace), imageUrl)

	return new Response(JSON.stringify({tokenID: String(randomTokenIDSpace), image: response.url }), { status: 200 });

8. Minteo con su Cloudflare Worker

El último paso es finalmente mintear el tokenId correspondiente que vinculó previamente a los metadatos, a la dirección del usuario. Aquí enviamos una solicitud al Cloudflare Worker que creamos en el Paso 5, el cual minteará el token al usuario.

const data = {

	address: address,

	mint: true,

	tokenID: tokenID

};



const res = await fetch(ENDPOINT, {

	method: 'POST',

	headers: {

	'Content-Type': 'application/json',

	},

	body: JSON.stringify(data),

})



const json = await res.json()

Un punto importante es que probablemente querrá asegurarse de que sus Cloudflare workers solo procesen solicitudes desde un origen frontend específico; simplemente puede verificar el valor Referrer en los request.headers y compararlo con el CLIENT_URL en el wrangler.toml:

async function handleRequest(request: any, env: Env, ctx: ExecutionContext) {

	const originUrl = new URL(request.url);

	const referer = request.headers.get('Referer');



	if(referer.toString() != env.CLIENT_URL){

		return new Response('Bad Origin', { status: 500 }); // Handle errors

	} 



	...

}

Conclusión

Recapitulemos lo que hicimos durante este tutorial:

Vimos cómo crear un proyecto Sequence y obtener acceso a nuestra suite de APIs. Desplegamos y configuramos un wallet embebido para asegurar una experiencia de juego fluida en un ejemplo de dungeon crawler. Además, utilizamos la plataforma Sequence para desplegar un contrato y patrocinar gas para ese contrato, simplificando la experiencia del usuario. También desplegamos un minteador de NFT sin servidor usando la Sequence Transaction API, lo que permite que su juego escale a millones de jugadores y maneje interacciones blockchain complejas como reorgs. Asimismo, aprovechamos la API de scenario.gg para crear dinámicamente activos de juego como recompensas para los jugadores. Estas imágenes luego se vinculan a los metadatos de un NFT usando la Sequence Metadata API. Ahora debería entender cómo mintear arte de IA para su juego impulsado por Scenario.gg y Sequence.

Está claro cuántos componentes intervienen en la creación de un juego blockchain escalable, seguro y divertido, pero la plataforma Sequence junto con Scenario le brinda todo lo que necesita.

Por último, puede ver todos los pasos anteriores integrados en una experiencia completa con nuestro juego dungeon crawler para explorar el laberinto y obtener su propio botín.

¡Feliz construcción!

9. (Opcional) Restricción de minteo nativo por wallet

Como opción para evitar el uso excesivo del minteo de cofres desde wallets específicas, se puede establecer un parámetro llamado DAILY_MINT_RESTRICTION en el wrangler.toml como un máximo de minteos permitidos por día. Y, si lo considera necesario, puede agregar un ADMIN a su protocolo para poder mintear una cantidad infinita.

Estas funciones pueden implementarse en el código con los siguientes pasos:

async function handleRequest(request: any, env: Env, ctx: ExecutionContext) {

	... 

	const payload = await request.json()

	const { address, tokenID }: any = payload



	// check for admin

	if(address.toLowerCase() != env.ADMIN.toLowerCase() && !await hasDailyMintAllowance(env, address)){

		// check for daily mint allowance

		return new Response(JSON.stringify({limitExceeded: true}), { status: 400 })

	}

	...

}

Donde hasDailyMintAllowance se divide en 2 funciones:

  • fullPaginationDay de transacciones de la address del usuario
  • mintCount que corresponde a que el from sea la dirección 0x

Paginación completa del Indexer por un día

Como nota adicional, la pila de Sequence Indexer solo almacena 30 días de transacciones en este periodo, así que puede ampliar el rango de tiempo desde un día hasta este máximo.

Para usar el Sequence Indexer, necesitaremos ejecutar pnpm install @0xsequence/indexer

Luego, para implementarlo, usamos un bucle while que obtiene el primer lote de transacciones y el valor de page.after del indexer, y verifica continuamente si la marca de tiempo es menor a 24 horas, agregando a un arreglo temporal en cada pasada. Esto asegura que obtengamos todas las transacciones disponibles:

import { SequenceIndexer } from '@0xsequence/indexer'



const isLessThan24Hours = (isoDate: string) => {

    const dateProvided: any = new Date(isoDate);

    const currentDate: any = new Date();

    const twentyFourHours = 24 * 60 * 60 * 1000; // 24 hours in milliseconds



    // Calculate the difference in milliseconds

    const difference = currentDate - dateProvided;



    // Check if the difference is less than 24 hours

    return difference < twentyFourHours && difference > 0;

}



const fullPaginationDay = async (env: Env, address: string) => {

    const txs: any = []

	const indexer = new SequenceIndexer(`https://${env.CHAIN_HANDLE}-indexer.sequence.app`, env.PROJECT_ACCESS_KEY)



    const filter = {

        accountAddress: address,

    };



    // query Sequence Indexer for all token transaction history

	let txHistory: any

	let firstLoop = true;

    let finished = true;

    // if there are more transactions to log, proceed to paginate

    while(firstLoop || (!finished && txHistory.page.more)){  

		if(firstLoop){

			firstLoop = false

			txHistory = await indexer.getTransactionHistory({

				filter: filter,

				page: { pageSize: 50 }

			})



			for(let i = 0; i < txHistory.transactions.length; i++){

				if(!isLessThan24Hours(txHistory.transactions[i].timestamp)){

					finished = true

				}

				txs.push(txHistory.transactions[i])

			}

		}

        txHistory = await indexer.getTransactionHistory({

            filter: filter,

            page: { 

                pageSize: 50, 

                // use the after cursor from the previous indexer call

                after: txHistory!.page!.after! 

            }

        })

		for(let i = 0; i < txHistory.transactions.length; i++){

			if(!isLessThan24Hours(txHistory.transactions[i].timestamp)){

				finished = true

			}

			txs.push(txHistory.transactions[i])

		}

    }



    return txs

}

Conteo de minteos en un día

Todos los coleccionables minteados desde los contratos estándar Sequence ERC721 y ERC1155 provienen de la dirección 0x:

const mintCount = (env: Env, txs: any) => {

	let count = 0

	for(let i = 0; i < txs.length; i++){

		if(

			txs[i].transfers[0].from == '0x0000000000000000000000000000000000000000' 

			&& txs[i].transfers[0].contractAddress == env.CONTRACT_ADDRESS.toLowerCase()

		) count++

	}

	return count

}

Tiene asignación diaria de minteo

const hasDailyMintAllowance = async (env: Env, address: string) => {

	const txs = await fullPaginationDay(env, address)

	const count = mintCount(env, txs)

	return count < env.DAILY_MINT_RESTRICTION

}