Esta guía cubre la creación de un marketplace personalizado usando herramientas del stack de Sequence. Incluye pasos para mintear tokens, autenticación de wallet, consultas a la blockchain, tipos de wallets múltiples, creación de solicitudes, aceptación de órdenes y la integración opcional de un wallet integrado.
En esta guía repasaremos el proceso de crear un marketplace personalizado usando algunas herramientas simples del stack de Sequence.Estas herramientas le permitirán realizar:
Minteo: Minteo de tokens a su wallet desde el Sequence Builder
Vea un ejemplo de dapp de marketplace simplificado que permite a los usuarios mintear coleccionables, venderlos con el Sequence Marketplace Protocol y realizar compras con USDC en base-sepolia obteniendo la mejor orden del Marketplace.El código se puede encontrar aquí
El primer paso es crear un coleccionable desde el Sequence Builder y mintear algunos tokens, lo cual puede lograrse con esta guía y usar el tokenId que minteó en los siguientes pasos para consultar y cumplir órdenes.
Para su proyecto, necesitará una forma de autenticar a su usuario con un wallet.Su opción dentro del stack de Sequence es usar un Embedded Wallet para una experiencia headless y similar a web2, o un Ecosystem Wallet con Web SDK para llegar a más tipos de wallets.Para esta guía usaremos un Universal Sequence Wallet con conector Web SDK (con opción de un Embedded Wallet), que puede autenticar usuarios usando Google o Apple, además de wallets traídos por el usuario como Coinbase o Metamask.
Puede crear un proyecto vanilla js/html/css desde un template como este para una configuración rápida, o aquí le mostraremos cómo usar React desde cero.Comience creando un proyecto en una carpeta con el nombre que prefiera:
Una vez que tenga uno o varios coleccionables minteados, puede consultar los datos desde la dirección del contrato de su despliegue, que puede encontrar aquí:
Puede consultar datos usando el indexer, con este código donde una dirección de cuenta y la dirección del contrato (obtenidas del contrato desplegado desde el Sequence Builder) se ingresan en la API del indexer.Esto será importante cuando esté determinando un tokenID para crear una solicitud en el marketplace; para este demo, asumiremos que está trabajando con un solo tokenID.
Copiar
// Works in both a Webapp (browser) or Node.js:import { SequenceIndexer } from "@0xsequence/indexer";const indexer = new SequenceIndexer( "https://arbitrum-sepolia-indexer.sequence.app", "<access-key>");// try any contract and account address you'd like :), as an exampleconst contractAddress = "<your_deploy_contract_address"; // "0x1693ffc74edbb50d6138517fe5cd64fd1c917709";const accountAddress = address; // "0xc2be9cf6d9ee4fd211f88620760e829792659b16";// query Sequence Indexer for all nft balances of the account on Polygonconst nftBalances = await indexer.getTokenBalances({ contractAddress: contractAddress, accountAddress: accountAddress, includeMetadata: true,});console.log("collection of items:", nftBalances);
Donde la respuesta de la llamada al indexador arroja los siguientes datos:
Respuesta de ejemplo
contractType (string): el tipo de contrato (por ejemplo, ERC20, ERC721 o ERC1155)
contractAddress (string): la dirección del contrato del token
accountAddress (string): la dirección de la cuenta que desplegó el contrato
tokenID (string): el tokenID del token (siempre 0 si es ERC20)
balance (string): el balance del token
blockHash (string): el hash de merkle de la transacción del bloque donde se desplegó el token
blockNumber (number): el número de bloque donde se desplegó el token
chainId (number): el id de la red del token
contractType
chainId (number): el id de la red del token
address (string): la dirección del token
name (string): nombre a nivel de contrato del token
type (string): el tipo de contrato (por ejemplo, ERC20, ERC721 o ERC1155)
symbol (string): el símbolo del token
decimals (number): la cantidad de decimales que tiene el token
logoURI (string): el logo del token mostrado en sequence.app
deployed (boolean): si el token está desplegado
bytecodeHash (string): hash del bytecode de un smart contract desplegado en la blockchain
extensions
link (string): el sitio web asociado para enlazar al proyecto
description (string): la descripción de metadatos del token
ogImage (string): la imagen de banner del token, mostrada en sequence.app
originChainId (number): el id de la red de origen que representa el token
originAddress (string): la dirección del contrato de origen que representa el token
verified (boolean): si el token está verificado y es confiable
verifiedBy (string): la fuente de verificación que indica por qué esto no es spam
updatedAt (date): la última vez que se actualizó el indexador
tokenMetadata
tokenId (string): el tokenID del token (siempre 0 si es ERC20)
contractAddress (string): la dirección del contrato del token
name (string): nombre a nivel de token
description (string): la descripción del token
image (string): la imagen como url del token
decimals (string): la cantidad de decimales del token
properties (object): un objeto que contiene las propiedades de los metadatos del token
external_url (string): una url externa donde encontrar el token o más detalles
updatedAt (date): la última vez que se actualizaron los metadatos del token
Debido a que en este ejemplo usamos Web SDK, que le permite usar una Sequence wallet además de su propia EOA wallet, el envío de transacciones a la blockchain será diferente. Con una Sequence wallet puede enviar transacciones en lote para optimizar el costo de gas, mientras que con wagmi usando una EOA solo puede enviar una transacción a la vez.Para lograr esto, seguimos algunos pasos para crear una variable de estado local que verifica cuál wallet está autorizada.
Para este ejemplo, usaremos Arbitrum Sepolia USDC del faucet de la comunidadVaya allí primero para obtener algunos tokens, así podrá crear una listing con su request.Luego, para crear un request para el orderbook, primero debemos asegurarnos de habilitar el contrato del orderbook del marketplace con aprobación para transferir sus tokens.Primero, verificamos que el marketplace esté aprobado para el contrato, con algo de lógica.
Copiar
const ERC1155Contract = '0x1693ffc74edbb50d6138517fe5cd64fd1c917709'const MarketPlaceContract = '0xfdb42A198a932C8D3B506Ffa5e855bC4b348a712'function App() { async function checkERC1155Approval(ownerAddress: string, operatorAddress: string) { const abi = [ "function isApprovedForAll(address account, address operator) external view returns (bool)" ]; const provider = new ethers.providers.JsonRpcProvider(`https://nodes.sequence.app/arbitrum-sepolia/${process.env.REACT_APP_PROJECT_ACCESSKEY}`); const contract = new ethers.Contract(ERC1155Contract, abi, provider); return await contract.isApprovedForAll(ownerAddress, operatorAddress); } const createRequest = async () => { ... if(await checkERC1155Approval(address!,MarketPlaceContract)){ // is approved and only requires a single transaction ... } else { // is not approved, so requires multiple transactions if(isSequence) { .. perform multi-batch transactions ... } else { // is not a sequence wallet ... } } };}
Después, necesitaremos armar la transacción con el ABI correcto para generar el calldata esperado según los distintos caminos: no estar aprobado versus estar aprobado, y si es una Sequence wallet o no.
Copiar
const [requestData, setRequestData] = useState<any>(null);const createRequest = async () => { const sequenceMarketInterface = new ethers.Interface([ "function createRequest(tuple(bool isListing, bool isERC1155, address tokenContract, uint256 tokenId, uint256 quantity, uint96 expiry, address currency, uint256 pricePerToken)) external nonReentrant returns (uint256 requestId)", ]); const amountBigNumber = ethers.parseUnits(String("0.01"), 6); // ensure to use the proper decimals const request = { isListing: true, isERC1155: true, tokenContract: ERC1155Contract, tokenId: 1, quantity: 1, expiry: Date.now() + 7 * 24 * 60 * 60 * 1000, // 1 day currency: ArbSepoliaUSDCContract, pricePerToken: amountBigNumber, }; const data = sequenceMarketInterface.encodeFunctionData("createRequest", [ request, ]); setRequestData(data); // we'll need this in the next step if (await checkERC1155Approval(address!, MarketPlaceContract)) { // is approved and only requires a single transaction sendTransaction({ to: MarketPlaceContract, data: `0x${data.slice(2, data.length)}`, gas: null, }); } else { // is not approved, so requires multiple transactions const erc1155Interface = new ethers.Interface([ "function setApprovalForAll(address _operator, bool _approved) returns ()", ]); // is not approved const dataApprove = erc1155Interface.encodeFunctionData( "setApprovalForAll", ["0xfdb42A198a932C8D3B506Ffa5e855bC4b348a712", true] ); const txApprove = { to: ERC1155Contract, data: dataApprove, }; const tx = { to: MarketPlaceContract, data: data, }; if (isSequence) { const wallet = sequence.getWallet(); const signer = wallet.getSigner(421614); try { const res = signer.sendTransaction([txApprove, tx]); console.log(res); } catch (err) { console.log(err); console.log("user closed the wallet, or, an error occured"); } } else { // is not a sequence wallet // todo: implement mutex sendTransaction({ to: ERC1155Contract, data: `0x${dataApprove.slice(2, data.length)}`, gas: null, }); // still need to send acceptRequest transaction } }};
Finalmente, en el caso donde la transacción no se realiza desde una Sequence wallet y no está aprobada, debemos enviar una transacción una vez que haya un recibo de transacción del hook useSendTransaction usando un mutex para confirmar de qué transacción proviene el hash. Esto se hace en una función useEffect de React.
En programación, una exclusión mutua (mutex) es un objeto de programa que
previene que múltiples hilos accedan al mismo recurso compartido
simultáneamente.
Copiar
import { useSendTransaction } from 'wagmi'import { useMutex } from 'react-context-mutex';function App() { ... const [requestData, setRequestData] = useState<any>(null) const { data: hash, sendTransaction } = useSendTransaction() const MutexRunner = useMutex(); const mutexApproveERC1155 = new MutexRunner('sendApproveERC1155'); const createRequest = async () => { ... if(await checkERC1155Approval(address!,MarketPlaceContract)){ ... } else { if (isSequence) { // is a sequence wallet ... } else { // is not a sequence wallet mutexApproveERC1155.lock() sendTransaction({ to: ERC1155Contract, data: `0x${dataApprove.slice(2,data.length)}`, gas: null }) } } }; useEffect(() => { if (mutexApproveERC1155.isLocked() && hash) { sendTransaction({ to: MarketPlaceContract, data: `0x${requestData.slice(2, requestData.length)}`, gas: null, }); mutexApproveERC1155.unlock(); } }, [requestData, hash]);
¡Listo! Ya terminó de crear requests para el protocolo Sequence Market, ahora puede implementar un botón y probar el flujo.
Usaremos el indexador para consultar el balance y ver si el usuario tiene suficientes tokens para pagar la orden. Esto se puede hacer con el siguiente código:
Debe asegurarse de que al realizar una comparación de igualdad en la dirección del contrato del token, la dirección esté en minúsculas.
Copiar
import { SequenceIndexer } from '@0xsequence/indexer'...const checkERC20Balance = async (requiredAmount: any) => { const indexer = new SequenceIndexer('https://arbitrum-sepolia-indexer.sequence.app', process.env.REACT_APP_PROJECT_ACCESSKEY) const contractAddress = ArbSepoliaUSDCContract const accountAddress = address const tokenBalances = await indexer.getTokenBalances({ contractAddress: contractAddress, accountAddress: accountAddress, }) let hasEnoughBalance = false tokenBalances.balances.map((token) => { const tokenBalanceBN = ethers.BigNumber.from(token.balance); const requiredAmountBN = ethers.BigNumber.from(requiredAmount); if(token.contractAddress == ArbSepoliaUSDCContract && tokenBalanceBN.gte(requiredAmountBN)){ hasEnoughBalance = true } }) return hasEnoughBalance}const acceptOrder = async () => { const tokenID = '1' const topOrder: any = await getTopOrder(tokenID) const requiredAmount = topOrder.pricePerToken ... if(await checkERC20Balance(requiredAmount)){ ... } else { ... // provide prompt on screen that user does not have balance }}
Finalmente, completaremos la lógica necesaria enviando realmente una transacción a la blockchain.Comenzamos con el mismo flujo de antes, considerando el envío de transacciones en lote si es una Sequence wallet y no está aprobada, o, si el Marketplace ya está aprobado para gastar sus tokens, solo enviando una transacción.
Copiar
... const mutexApproveERC20 = new MutexRunner('sendApproveERC20'); ... const acceptOrder = async () => { const topOrder: any = await getTopOrder('1') const requiredAmount = topOrder.pricePerToken const sequenceMarketInterface = new ethers.Interface([ "function acceptRequest(uint256 requestId, uint256 quantity, address recipient, uint256[] calldata additionalFees, address[] calldata additionalFeeRecipients)", ]); const quantity = 1 const data = sequenceMarketInterface.encodeFunctionData( "acceptRequest", [topOrder.orderId, quantity, address, [], []], ); setAcceptData(data) // we'll need this later, only for Web SDK enabled transactions const tx = { to: MarketPlaceContract, // 0xfdb42A198a932C8D3B506Ffa5e855bC4b348a712 data: data } if(await checkERC20Balance(requiredAmount)){ if((await checkERC20Approval(address!,MarketPlaceContract,ArbSepoliaUSDCContract,requiredAmount))){ sendTransaction({ to: MarketPlaceContract, data: `0x${data.slice(2,data.length)}`, gas: null }) } else { ... const erc20Interface = new ethers.Interface([ "function approve(address spender, uint256 amount) external returns (bool)" ]); const spenderAddress = "0xfdb42A198a932C8D3B506Ffa5e855bC4b348a712"; const maxUint256 = ethers.constants.MaxUint256; const dataApprove = erc20Interface.encodeFunctionData("approve", [spenderAddress, maxUint256]); if(isSequence){ const wallet = sequence.getWallet() const signer = wallet.getSigner(421614) const txApprove = { to: ArbSepoliaUSDCContract, // The contract address of the ERC-20 token, replace with actual contract address data: dataApprove }; try { const res = await signer.sendTransaction([txApprove, tx]) console.log(res) } catch (err) { console.log(err) console.log('user closed the wallet, or, an error occured') } } else { mutexApproveERC20.lock() sendTransaction({ to: ArbSepoliaUSDCContract, data: `0x${dataApprove.slice(2,dataApprove.length)}`, gas: null }) } } }
Luego, en el flujo donde no es una Sequence wallet y requiere aprobación, incluiremos otro useEffect con la verificación del mutex como antes.
Para que su conector de Web SDK sea compatible con Embedded Wallet, necesitaremos instalar algunas versiones de paquetes y actualizar nuestro config.ts que usamos al inicio de la guía.La función Embedded Wallet permite transacciones sin confirmación, lo que puede crear una experiencia de usuario más fluida.
Copiar
pnpm i @0xsequence/kit@2.0.5-beta.9 @0xsequence/kit-connectors@2.0.5-beta.9
El último paso es asegurarse de actualizar a nuestro equipo con las URLs autorizadas de Google y Apple (por ejemplo, http://localhost:3000) para poder llamar al flujo de inicio de sesión de Embedded Wallet.