Guía - Trigger con NextJS
Cómo Funciona Sherry - Entendiendo el Flujo
Antes de empezar a construir, es importante entender cómo Sherry Links conecta usuarios, plataformas y tu backend de mini app. El siguiente diagrama ilustra el flujo completo:

Flujo Paso a Paso
-
Sherry hace petición GET y recibe metadata
- La plataforma descubre tu mini app y solicita su configuración
- Tu backend responde con metadata que define la UI y funcionalidad
-
Back-End retorna metadata
- La metadata incluye campos de formulario, botones de acción, descripciones y endpoints
- Esto le dice a Sherry cómo renderizar la interfaz de tu mini app
-
Sherry renderiza mini-app
- La plataforma crea un formulario amigable basado en tu metadata
- Los usuarios pueden ver campos de entrada, descripciones y botones de acción
-
Usuario ejecuta mini-app
- El usuario llena el formulario con sus parámetros deseados
- El usuario hace clic en el botón de acción para enviar su solicitud
-
Sherry hace petición POST y recibe transacción serializada desde Back-End
- La plataforma envía la entrada del usuario a tu backend para procesamiento
- Tu backend aplica lógica personalizada y retorna una transacción blockchain lista para firmar
-
Usuario confirma transacción
- El usuario revisa los detalles de la transacción en su wallet
- El usuario firma y envía la transacción a la blockchain
Tu mini app actúa como una fábrica inteligente de transacciones - toma la entrada del usuario, aplica tu lógica de negocio única, y produce transacciones blockchain listas para firmar. Esto es lo que hace poderoso a Sherry Links: combinar gran UX con funcionalidad blockchain personalizada.
Requisitos Previos
Antes de comenzar, asegúrate de tener lo siguiente instalado y configurado:
- Node.js: Versión 18.x o superior
- Gestor de Paquetes: npm, yarn, o pnpm
- Conocimiento del Framework: Conceptos básicos de Next.js y TypeScript
- Conceptos Básicos de Blockchain: Entendimiento de contratos inteligentes y ABI
Configuración Inicial
1. Crear Proyecto Next.js
- npm
- yarn
- pnpm
npx create-next-app@latest mi-sherry-app --typescript --eslint --tailwind --src-dir --app --import-alias "@/*"
cd mi-sherry-app
yarn create next-app mi-sherry-app --typescript --eslint --tailwind --src-dir --app --import-alias "@/*"
cd mi-sherry-app
pnpm create next-app mi-sherry-app --typescript --eslint --tailwind --src-dir --app --import-alias "@/*"
cd mi-sherry-app
2. Instalar Dependencias
- npm
- yarn
- pnpm
npm install @sherrylinks/sdk viem wagmi
yarn add @sherrylinks/sdk viem wagmi
pnpm add @sherrylinks/sdk viem wagmi
3. Configurar Next.js (Opcional)
Para evitar errores de build con ESLint, puedes deshabilitarlo en next.config.js
:
/** @type {import('next').NextConfig} */
const nextConfig = {
eslint: {
ignoreDuringBuilds: true,
},
};
module.exports = nextConfig;
Creando el Endpoint GET - Metadata
El endpoint GET es el corazón de tu mini app. Aquí defines toda la información y estructura que las plataformas necesitan para renderizar tu aplicación.
La metadata le dice a las plataformas cómo renderizar tu mini app, qué inputs mostrar a los usuarios, y dónde enviar los datos cuando los usuarios interactúan con ella.
1. Crear el Archivo de Ruta
Crea el archivo app/api/mi-app/route.ts
:
import { NextRequest, NextResponse } from 'next/server';
import { createMetadata, Metadata, ValidatedMetadata } from '@sherrylinks/sdk';
2. Configurar la Estructura Básica del Handler GET
export async function GET(req: NextRequest) {
try {
// Obtener información de la URL del servidor
const host = req.headers.get('host') || 'localhost:3000';
const protocol = req.headers.get('x-forwarded-proto') || 'http';
// Construir la URL base
const serverUrl = `${protocol}://${host}`;
// Construiremos el objeto metadata paso a paso abajo
} catch (error) {
console.error('Error creando metadata:', error);
return NextResponse.json({ error: 'Error al crear metadata' }, { status: 500 });
}
}
Esto detecta automáticamente si estás ejecutando localmente (http://localhost:3000
) o en
producción (https://tudominio.com
) y construye la URL base correcta para tu mini app.
3. Definir Información Básica de la App
export async function GET(req: NextRequest) {
try {
const host = req.headers.get('host') || 'localhost:3000';
const protocol = req.headers.get('x-forwarded-proto') || 'http';
const serverUrl = `${protocol}://${host}`;
const metadata: Metadata = {
url: 'https://sherry.social',
icon: 'https://avatars.githubusercontent.com/u/117962315',
title: 'Mensaje con Timestamp',
baseUrl: serverUrl,
description:
'Almacena un mensaje con un timestamp optimizado calculado por nuestro algoritmo',
// Las acciones se agregarán en el siguiente paso
};
} catch (error) {
// Manejo de errores...
}
}
Entendiendo Cada Campo
Campo | Descripción | Ejemplo |
---|---|---|
url | URL principal del sitio web de tu proyecto | "https://sherry.social" |
icon | URL de imagen públicamente accesible (200x200px recomendado) | "https://ejemplo.com/icon.png" |
title | Nombre corto y descriptivo para tu mini app | "Mensaje con Timestamp" |
baseUrl | URL del servidor donde tu mini app está alojada (auto-detectada) | "https://tudominio.com" |
description | Explicación clara de qué hace tu mini app | "Almacenar mensajes con timestamps optimizados" |
4. Agregar Array de Acciones
Las acciones definen qué pueden hacer los usuarios con tu mini app. Cada acción representa un botón con funcionalidad específica.
const metadata: Metadata = {
url: 'https://sherry.social',
icon: 'https://avatars.githubusercontent.com/u/117962315',
title: 'Mensaje con Timestamp',
baseUrl: serverUrl,
description: 'Almacena un mensaje con un timestamp optimizado calculado por nuestro algoritmo',
actions: [
{
type: 'dynamic',
label: 'Almacenar Mensaje',
description:
'Almacena tu mensaje con un timestamp personalizado calculado para almacenamiento óptimo',
chains: { source: 43113 },
path: `/api/mi-app`,
// Los parámetros se agregarán en el siguiente paso
},
],
};
Entendiendo las Propiedades de Acción
- type: Siempre usa
"dynamic"
para mini apps complejas que necesitan lógica personalizada - label: El texto que aparecerá en el botón de acción
- description: Explicación de qué hace esta acción específica
- chains.source: La blockchain donde se ejecutará la transacción (chain ID)
43113
= Avalanche Fuji Testnet43114
= Avalanche Mainnet
- path: El endpoint de API que manejará la petición POST
5. Configurar Parámetros de Entrada del Usuario
const metadata: Metadata = {
url: 'https://sherry.social',
icon: 'https://avatars.githubusercontent.com/u/117962315',
title: 'Mensaje con Timestamp',
baseUrl: serverUrl,
description: 'Almacena un mensaje con un timestamp optimizado calculado por nuestro algoritmo',
actions: [
{
type: 'dynamic',
label: 'Almacenar Mensaje',
description:
'Almacena tu mensaje con un timestamp personalizado calculado para almacenamiento óptimo',
chains: { source: 43113 },
path: `/api/mi-app`,
params: [
{
name: 'mensaje',
label: 'Tu Mensaje',
type: 'text',
required: true,
description: 'Ingresa el mensaje que quieres almacenar en la blockchain',
},
],
},
],
};
"text"
: Entrada de texto de una línea -"textarea"
: Entrada de texto multilínea -"number"
: Entrada numérica con validación -"email"
: Entrada de email con validación -"url"
: Entrada de URL con validación
6. Validar y Retornar la Metadata
export async function GET(req: NextRequest) {
try {
const host = req.headers.get('host') || 'localhost:3000';
const protocol = req.headers.get('x-forwarded-proto') || 'http';
const serverUrl = `${protocol}://${host}`;
const metadata: Metadata = {
url: 'https://sherry.social',
icon: 'https://avatars.githubusercontent.com/u/117962315',
title: 'Mensaje con Timestamp',
baseUrl: serverUrl,
description:
'Almacena un mensaje con un timestamp optimizado calculado por nuestro algoritmo',
actions: [
{
type: 'dynamic',
label: 'Almacenar Mensaje',
description:
'Almacena tu mensaje con un timestamp personalizado calculado para almacenamiento óptimo',
chains: { source: 43113 },
path: `/api/mi-app`,
params: [
{
name: 'mensaje',
label: 'Tu Mensaje',
type: 'text',
required: true,
description: 'Ingresa el mensaje que quieres almacenar en la blockchain',
},
],
},
],
};
// Validar metadata usando el SDK
const validated: ValidatedMetadata = createMetadata(metadata);
// Retornar con headers CORS para acceso cross-origin
return NextResponse.json(validated, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
},
});
} catch (error) {
console.error('Error creando metadata:', error);
return NextResponse.json({ error: 'Error al crear metadata' }, { status: 500 });
}
}
La función createMetadata()
valida la estructura de tu metadata y asegura que cumple con los
requisitos del Sherry SDK antes de retornarla a la plataforma.
Ejemplo 1: Transferencia Simple (Solo Aprendizaje)
Este ejemplo es para aprender los conceptos básicos solamente. Transferencias simples como esta ya están soportadas por nuestro TransferAction incorporado y NO serán aceptadas en envíos al minithon. Úsalo para entender el flujo, luego pasa al Ejemplo 2 para funcionalidad lista para minithon.
Entendiendo el Flujo de Petición POST
Cuando un usuario completa el formulario de tu mini app y hace clic en el botón de acción, la plataforma enviará una petición POST a tu endpoint con la entrada del usuario como parámetros de URL.
1. Configurar la Estructura del Handler POST
Agrega estas importaciones al inicio de tu archivo:
import { NextRequest, NextResponse } from 'next/server';
import { avalancheFuji } from 'viem/chains';
import { createMetadata, Metadata, ValidatedMetadata, ExecutionResponse } from '@sherrylinks/sdk';
import { serialize } from 'wagmi';
Ahora construyamos el handler POST:
- Estructura Básica
- Con Validación
export async function POST(req: NextRequest) {
try {
// Paso 1: Extraer parámetros de la URL
const { searchParams } = new URL(req.url);
const mensaje = searchParams.get('mensaje');
// Agregaremos validación y creación de transacción abajo
} catch (error) {
console.error('Error en petición POST:', error);
return NextResponse.json({ error: 'Error Interno del Servidor' }, { status: 500 });
}
}
export async function POST(req: NextRequest) {
try {
// Extraer parámetros
const { searchParams } = new URL(req.url);
const mensaje = searchParams.get('mensaje');
// Validar parámetros requeridos
if (!mensaje) {
return NextResponse.json(
{ error: 'El parámetro mensaje es requerido' },
{
status: 400,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
},
);
}
// La creación de transacción se agregará después
} catch (error) {
// Manejo de errores...
}
}
2. Crear el Objeto de Transacción
// Crear una transacción de transferencia simple
const tx = {
to: '0x5ee75a1B1648C023e885E58bD3735Ae273f2cc52',
value: BigInt(1000000), // 1000000 wei = 0.000001 AVAX
chainId: avalancheFuji.id,
};
Entendiendo las Propiedades de Transacción
- to: La dirección de destino que recibirá la transferencia
- value: La cantidad a transferir en wei (unidad más pequeña). Usa
BigInt()
para números grandes - chainId: El ID numérico de la blockchain (avalancheFuji.id = 43113)
3. Ejemplo Completo de Transferencia Simple
export async function POST(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const mensaje = searchParams.get('mensaje');
if (!mensaje) {
return NextResponse.json(
{ error: 'El parámetro mensaje es requerido' },
{
status: 400,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
},
);
}
const tx = {
to: '0x5ee75a1B1648C023e885E58bD3735Ae273f2cc52',
value: BigInt(1000000),
chainId: avalancheFuji.id,
};
// Serializar la transacción para la blockchain
const serialized = serialize(tx);
// Crear el objeto de respuesta que Sherry espera
const resp: ExecutionResponse = {
serializedTransaction: serialized,
chainId: avalancheFuji.id, // Usar el ID de la chain
};
// Retornar la respuesta con headers CORS
return NextResponse.json(resp, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
} catch (error) {
console.error('Error en petición POST:', error);
return NextResponse.json({ error: 'Error Interno del Servidor' }, { status: 500 });
}
}
Este ejemplo solo realiza una transferencia simple de tokens. Dado que nuestra plataforma ya proporciona TransferAction para esta funcionalidad exacta, los envíos al minithon necesitan demostrar funcionalidad más compleja y con valor agregado.
Ejemplo 2: Interacción con Contratos Inteligentes (Listo para Minithon)
Este ejemplo demuestra interacción con contratos inteligentes con lógica de negocio personalizada, que es lo que buscamos en los envíos al minithon.
1. Entendiendo el Contrato Inteligente
Primero, entendamos qué hace nuestro contrato inteligente:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
/**
* @title TimestampedMessage
* @dev Stores messages with an optimized timestamp
*/
contract TimestampedMessage {
event MessageStored(address indexed sender, string message, uint256 timestamp, uint256 optimizedTimestamp);
struct MessageData {
address sender;
string message;
uint256 timestamp;
uint256 optimizedTimestamp;
}
// Array to store all messages
MessageData[] public messages;
// Mapping from address to their message count
mapping(address => uint256) public userMessageCount;
/**
* @dev Store a message with an optimized timestamp
* @param message The message to store
* @param optimizedTimestamp A timestamp calculated off-chain
*/
function storeMessage(string memory message, uint256 optimizedTimestamp) public {
// Store message with the current block timestamp and the optimized timestamp
messages.push(MessageData({
sender: msg.sender,
message: message,
timestamp: block.timestamp,
optimizedTimestamp: optimizedTimestamp
}));
// Increment message count for the sender
userMessageCount[msg.sender]++;
// Emit event
emit MessageStored(msg.sender, message, block.timestamp, optimizedTimestamp);
}
/**
* @dev Get the count of all messages
*/
function getMessageCount() public view returns (uint256) {
return messages.length;
}
/**
* @dev Get a message by index
*/
function getMessage(uint256 index) public view returns (
address sender,
string memory message,
uint256 timestamp,
uint256 optimizedTimestamp
) {
require(index < messages.length, "Index out of bounds");
MessageData memory data = messages[index];
return (data.sender, data.message, data.timestamp, data.optimizedTimestamp);
}
/**
* @dev Get all messages from a specific sender
*/
function getMessagesBySender(address sender) public view returns (
string[] memory messageTexts,
uint256[] memory timestamps,
uint256[] memory optimizedTimestamps
) {
uint256 count = userMessageCount[sender];
messageTexts = new string[](count);
timestamps = new uint256[](count);
optimizedTimestamps = new uint256[](count);
uint256 currentIndex = 0;
for (uint256 i = 0; i < messages.length; i++) {
if (messages[i].sender == sender) {
messageTexts[currentIndex] = messages[i].message;
timestamps[currentIndex] = messages[i].timestamp;
optimizedTimestamps[currentIndex] = messages[i].optimizedTimestamp;
currentIndex++;
}
}
return (messageTexts, timestamps, optimizedTimestamps);
}
}
- Lógica de negocio personalizada (cálculo de timestamp optimizado) - Gestión de estado de contrato inteligente - Emisión de eventos para seguimiento - Estructuras de datos complejas - Va más allá de transferencias simples
2. Configurar el ABI del Contrato
El ABI (Application Binary Interface) es esencial para interactuar con contratos inteligentes. Define las funciones disponibles, sus parámetros y tipos de retorno. Sin el ABI correcto, no podrás codificar las llamadas a funciones del contrato.
Crea el archivo blockchain/abi.ts
en tu proyecto:
export const abi = [
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: 'address',
name: 'sender',
type: 'address',
},
{
indexed: false,
internalType: 'string',
name: 'message',
type: 'string',
},
{
indexed: false,
internalType: 'uint256',
name: 'timestamp',
type: 'uint256',
},
{
indexed: false,
internalType: 'uint256',
name: 'optimizedTimestamp',
type: 'uint256',
},
],
name: 'MessageStored',
type: 'event',
},
{
inputs: [
{
internalType: 'string',
name: 'message',
type: 'string',
},
{
internalType: 'uint256',
name: 'optimizedTimestamp',
type: 'uint256',
},
],
name: 'storeMessage',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{
internalType: 'uint256',
name: 'index',
type: 'uint256',
},
],
name: 'getMessage',
outputs: [
{
internalType: 'address',
name: 'sender',
type: 'address',
},
{
internalType: 'string',
name: 'message',
type: 'string',
},
{
internalType: 'uint256',
name: 'timestamp',
type: 'uint256',
},
{
internalType: 'uint256',
name: 'optimizedTimestamp',
type: 'uint256',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'getMessageCount',
outputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
internalType: 'address',
name: 'sender',
type: 'address',
},
],
name: 'getMessagesBySender',
outputs: [
{
internalType: 'string[]',
name: 'messageTexts',
type: 'string[]',
},
{
internalType: 'uint256[]',
name: 'timestamps',
type: 'uint256[]',
},
{
internalType: 'uint256[]',
name: 'optimizedTimestamps',
type: 'uint256[]',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
],
name: 'messages',
outputs: [
{
internalType: 'address',
name: 'sender',
type: 'address',
},
{
internalType: 'string',
name: 'message',
type: 'string',
},
{
internalType: 'uint256',
name: 'timestamp',
type: 'uint256',
},
{
internalType: 'uint256',
name: 'optimizedTimestamp',
type: 'uint256',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
internalType: 'address',
name: '',
type: 'address',
},
],
name: 'userMessageCount',
outputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
],
stateMutability: 'view',
type: 'function',
},
] as const;
- Remix: Después de compilar, ve a la pestaña
solidity compiler
y busca el textoABI
al final. - Hardhat: Se genera automáticamente en
artifacts/contracts/
- Foundry: Usa
forge inspect <ContractName> abi
- Explorador de Blockchain: Si el contrato está verificado, puedes copiar el ABI
3. Actualizar las Importaciones de tu Archivo de Ruta
import { NextRequest, NextResponse } from 'next/server';
import { avalancheFuji } from 'viem/chains';
import { createMetadata, Metadata, ValidatedMetadata, ExecutionResponse } from '@sherrylinks/sdk';
import { serialize } from 'wagmi';
import { encodeFunctionData, TransactionSerializable } from 'viem';
import { abi } from '@/blockchain/abi';
// Dirección del contrato en Avalanche Fuji Testnet
const CONTRACT_ADDRESS = '0xTuContratoInteligenteDirecciónAquí';
4. Construir el Handler POST del Contrato Inteligente
- Paso 1: Validación
- Paso 2: Lógica Personalizada
- Paso 3: Transacción del Contrato
export async function POST(req: NextRequest) {
try {
// Extraer entrada del usuario
const { searchParams } = new URL(req.url);
const message = searchParams.get('mensaje');
// Validar parámetros requeridos
if (!message) {
return NextResponse.json(
{ error: 'Message parameter is required' },
{
status: 400,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
},
);
}
// La lógica personalizada y creación de transacción se agregarán abajo
} catch (error) {
console.error('Error en petición POST:', error);
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
export async function POST(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const message = searchParams.get('mensaje');
if (!message) {
// Error de validación...
}
// LÓGICA DE NEGOCIO PERSONALIZADA: Calcular timestamp optimizado
// Esto es lo que hace tu mini app única y valiosa
const optimizedTimestamp = calculateOptimizedTimestamp(message);
console.log(`Procesando mensaje: "${message}"`);
console.log(`Timestamp optimizado: ${optimizedTimestamp}`);
// La creación de transacción se agregará después
} catch (error) {
// Manejo de errores...
}
}
export async function POST(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const message = searchParams.get('mensaje');
if (!message) {
// Validación...
}
const optimizedTimestamp = calculateOptimizedTimestamp(message);
// Codificar los datos de la función del contrato
const data = encodeFunctionData({
abi: abi,
functionName: 'storeMessage',
args: [message, BigInt(optimizedTimestamp)],
});
// Crear transacción de interacción con contrato inteligente
const tx: TransactionSerializable = {
to: CONTRACT_ADDRESS,
data: data,
chainId: avalancheFuji.id,
type: 'legacy',
};
// La serialización y respuesta se agregarán después
} catch (error) {
// Manejo de errores...
}
}
Entendiendo las Propiedades de Transacción de Contrato Inteligente
- to: La dirección del contrato desplegado en la blockchain
- data: Los datos codificados de la función usando
encodeFunctionData
- chainId: El ID numérico de la blockchain (avalancheFuji.id = 43113)
- type: El tipo de transacción ('legacy' para compatibilidad amplia)
5. Handler Completo de Contrato Inteligente
export async function POST(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const message = searchParams.get('mensaje');
if (!message) {
return NextResponse.json(
{ error: 'Message parameter is required' },
{
status: 400,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
},
);
}
// Calcular timestamp optimizado usando algoritmo personalizado
const optimizedTimestamp = calculateOptimizedTimestamp(message);
// Codificar los datos de la función del contrato
const data = encodeFunctionData({
abi: abi,
functionName: 'storeMessage',
args: [message, BigInt(optimizedTimestamp)],
});
// Crear transacción de contrato inteligente
const tx: TransactionSerializable = {
to: CONTRACT_ADDRESS,
data: data,
chainId: avalancheFuji.id,
type: 'legacy',
};
// Serializar transacción
const serialized = serialize(tx);
// Crear respuesta
const resp: ExecutionResponse = {
serializedTransaction: serialized,
chainId: avalancheFuji.id,
};
return NextResponse.json(resp, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
} catch (error) {
console.error('Error en petición POST:', error);
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
6. Implementar el Algoritmo Personalizado
// Algoritmo personalizado para calcular timestamp optimizado basado en el contenido del mensaje
function calculateOptimizedTimestamp(message: string): number {
const currentTimestamp = Math.floor(Date.now() / 1000);
let offset = 0;
for (let i = 0; i < message.length; i++) {
offset += message.charCodeAt(i) * (i + 1);
}
const maxOffset = 3600;
offset = offset % maxOffset;
return currentTimestamp + offset;
}
Manejo de CORS
Es fundamental configurar CORS antes de probar o desplegar tu mini app. Sin la configuración CORS correcta, tu mini app no funcionará cuando sea accedida desde plataformas externas como el debugger de Sherry o aplicaciones de terceros.
Para permitir que tu mini app sea usada desde diferentes dominios y plataformas, necesitas manejar peticiones preflight de CORS:
export async function OPTIONS(request: NextRequest) {
return new NextResponse(null, {
status: 204, // Sin Contenido
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers':
'Content-Type, Authorization, X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Date, X-Api-Version',
},
});
}
Cuando las plataformas intentan usar tu mini app desde un dominio diferente, los navegadores primero envían una petición OPTIONS para verificar si se permiten peticiones cross-origin. Este handler le dice al navegador que tu API acepta peticiones de cualquier dominio.
Headers CORS en Todas las Respuestas
Asegúrate de incluir los headers CORS en todas las respuestas de tu API:
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
Probando tu Trigger
Opción 1: App Sherry Social
Ve a
- En el campo de dirección, ingresa la URL de tu endpoint GET
Ejemplo:
http://localhost:3000/api/mi-app
(para desarrollo local)- La plataforma renderizará automáticamente tu mini app
Opción 2: Debugger de Sherry (Recomendado para Desarrollo)
El debugger está específicamente diseñado para probar y debuggear mini apps durante el desarrollo con múltiples métodos de entrada.
Para probar tu mini app en el debugger, primero debes desplegarla en un proveedor de hosting como Vercel. El debugger necesita una URL pública accesible para poder cargar tu mini app. Si quieres probar localmente, usa el método JSON copiando la respuesta de tu endpoint GET local y pegándola en el debugger.
- Prueba URL (Recomendado)
- Prueba JSON (Para Testing Local)
- Prueba TypeScript
- Despliega tu mini app en Vercel:
vercel --prod
- Prueba URL:
- Pega la URL de tu endpoint GET desplegado
- Ejemplo:
https://mi-sherry-app.vercel.app/api/mi-app
- Haz clic en "Cargar" para renderizar tu mini app
- Ve a https://app.sherry.social/debugger
- Para testing local únicamente:
- Visita tu endpoint GET directamente:
http://localhost:3000/api/mi-app
- Copia toda la respuesta JSON
- Pégala en la entrada JSON del debugger
- Esto es útil para probar metadata durante desarrollo
- Visita tu endpoint GET directamente:
- Copia tu objeto metadata de tu código TypeScript
- Pégalo directamente en el debugger
- Bueno para iteración rápida en la estructura de metadata
Proceso de Pruebas Paso a Paso
-
Desarrollar Localmente
- Ejecutar
npm run dev
- Probar en
localhost:3000/api/mi-app
- Ejecutar
-
Desplegar en Vercel
vercel --prod
- Obtener URL pública
-
Probar en Debugger
- Usar URL pública del despliegue
- Verificar renderizado completo
-
Probar Funcionalidad
- Completar formulario y probar POST
- Verificar transacción generada
Guías del Minithon
Qué Hace una Presentación Ganadora del Minithon
- Requisitos Técnicos
- Factores de Creatividad
- Buenas Ideas
Interacción con Contratos Inteligentes
Funcionalidad significativa más allá de transferencias simples
Lógica de Negocio Personalizada
Algoritmos únicos que agregan valor real
Múltiples Parámetros
Manejo sofisticado de datos
Manejo de Errores
Mensajes de error amigables para el usuario
Casos de Uso Únicos
Que resuelvan problemas reales
Algoritmos Innovadores
O procesamiento de datos
Combinaciones de Parámetros Interesantes
Que aporten funcionalidad útil
Aplicaciones Prácticas
Para usuarios reales
Herramientas DeFi
Calculadoras de rendimiento personalizadas, rebalanceo de portafolio, estrategias automatizadas
Gaming
Mecánicas de juego on-chain, interacciones NFT, sistemas de puntuación
Social
Mensajería descentralizada, sistemas de reputación, herramientas comunitarias
Productividad
Gestión de tareas con recompensas de tokens, herramientas colaborativas
Qué No Será Aceptado en el Minithon
Transferencias Simples de Tokens
Usa nuestro TransferAction en su lugar
Ejemplos Copiados y Pegados
Sin modificaciones significativas
Sin Contratos Inteligentes
Las mini apps deben interactuar con contratos
Implementaciones Rotas
Deben funcionar de extremo a extremo
Resolución de Problemas
- Problemas del Endpoint GET
- Problemas del Endpoint POST
- Problemas de Contratos Inteligentes
Error: "Falló la validación de metadata"
Síntomas: La mini app no carga en el debugger
- Verifica que todos los campos requeridos estén presentes en la metadata - Confirma que los tipos
de datos coincidan con el formato esperado - Usa
createMetadata()
para validar la estructura - Revisa la consola para errores específicos de validación
try {
const validated = createMetadata(metadata);
console.log('Validación de metadata exitosa:', validated);
} catch (error) {
console.error('Falló la validación de metadata:', error);
}
Error: "La política CORS bloquea la petición"
Síntomas: "El acceso a fetch ha sido bloqueado por la política CORS"
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
}
Error: "Parámetro requerido"
Síntomas: Error sobre parámetros faltantes
console.log('Todos los parámetros:', Object.fromEntries(searchParams.entries()));
console.log('Parámetro mensaje:', searchParams.get('mensaje'));
Error: "Falló la serialización de transacción"
Síntomas: Error durante la llamada serialize()
- Usa
BigInt(1000000)
no1000000
para números grandes - Verifica que chainId sea correcto para la red objetivo - Asegúrate de que todos los campos requeridos de transacción estén presentes - Para llamadas a contratos, verifica que el ABI coincida con el contrato desplegado
Error: "Función ABI no encontrada"
Síntomas: Error sobre función faltante o incorrecta
- Verifica que el nombre de la función coincida exactamente con el contrato (sensible a mayúsculas) - Confirma que la función sea pública en el contrato inteligente - Asegúrate de que el ABI esté completo e incluya la función - Verifica que los tipos de parámetros coincidan con la firma de la función del contrato
Error: "Dirección de contrato inválida"
Síntomas: La transacción falla con errores relacionados a la dirección
- Verifica que el contrato esté desplegado en la red correcta (Fuji testnet)
- Confirma que la dirección esté correctamente formateada (comienza con 0x)
- Asegúrate de tener la dirección correcta del contrato para tu red
- Verifica que el contrato esté verificado en el explorador de blockchain
Ejemplo de Código Completo
Aquí tienes un archivo de ruta completo, listo para minithon que demuestra todos los conceptos:
import { NextRequest, NextResponse } from 'next/server';
import { avalancheFuji } from 'viem/chains';
import { createMetadata, Metadata, ValidatedMetadata, ExecutionResponse } from '@sherrylinks/sdk';
import { serialize } from 'wagmi';
import { encodeFunctionData, TransactionSerializable } from 'viem';
import { abi } from '@/blockchain/abi';
const CONTRACT_ADDRESS = '0xTuContratoInteligenteEnFuji';
export async function GET(req: NextRequest) {
try {
const host = req.headers.get('host') || 'localhost:3000';
const protocol = req.headers.get('x-forwarded-proto') || 'http';
const serverUrl = `${protocol}://${host}`;
const metadata: Metadata = {
url: 'https://sherry.social',
title: 'Mensaje con Timestamp',
baseUrl: serverUrl,
description:
'Almacena un mensaje con un timestamp optimizado calculado por nuestro algoritmo',
actions: [
{
type: 'dynamic',
label: 'Almacenar Mensaje',
description:
'Almacena tu mensaje con un timestamp personalizado calculado para almacenamiento óptimo',
chains: { source: 43113 },
path: `/api/mi-app`,
params: [
{
name: 'mensaje',
label: 'Tu Mensaje',
type: 'text',
required: true,
description: 'Ingresa el mensaje que quieres almacenar en la blockchain',
},
],
},
],
};
const validated: ValidatedMetadata = createMetadata(metadata);
return NextResponse.json(validated, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
},
});
} catch (error) {
return NextResponse.json({ error: 'Error al crear metadata' }, { status: 500 });
}
}
export async function POST(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const message = searchParams.get('mensaje');
if (!message) {
return NextResponse.json(
{ error: 'Message parameter is required' },
{
status: 400,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
},
);
}
// Lógica de negocio personalizada
const optimizedTimestamp = calculateOptimizedTimestamp(message);
// Interacción con contrato inteligente
const data = encodeFunctionData({
abi: abi,
functionName: 'storeMessage',
args: [message, BigInt(optimizedTimestamp)],
});
const tx: TransactionSerializable = {
to: CONTRACT_ADDRESS,
data: data,
chainId: avalancheFuji.id,
type: 'legacy',
};
const serialized = serialize(tx);
const resp: ExecutionResponse = {
serializedTransaction: serialized,
chainId: avalancheFuji.id,
};
return NextResponse.json(resp, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
} catch (error) {
console.error('Error en petición POST:', error);
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
export async function OPTIONS(request: NextRequest) {
return new NextResponse(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers':
'Content-Type, Authorization, X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Date, X-Api-Version',
},
});
}
// Algoritmo personalizado - aquí es donde agregas tu valor único
function calculateOptimizedTimestamp(message: string): number {
const currentTimestamp = Math.floor(Date.now() / 1000);
let offset = 0;
for (let i = 0; i < message.length; i++) {
offset += message.charCodeAt(i) * (i + 1);
}
const maxOffset = 3600;
offset = offset % maxOffset;
return currentTimestamp + offset;
}
Repositorio de Ejemplos
Puedes encontrar ejemplos completos de trabajo en nuestro repositorio: SherryLabs/sherry-example
Este repositorio contiene:
- Tanto ejemplos de transferencia simple como de contrato inteligente
- Código completo de contrato inteligente con fuente Solidity
- Scripts de despliegue y configuración
- Archivos ABI e instrucciones de configuración
- Ejemplos de múltiples parámetros
- Utilidades avanzadas de prueba
- Documentación completa
Próximos Pasos
- Para Aprender
- Para Participantes del Minithon
- Consideraciones de Despliegue
- Comienza con el ejemplo de transferencia simple para entender el flujo
- Pasa al ejemplo de contrato inteligente una vez que te sientas cómodo
- Experimenta con diferentes tipos de parámetros y validaciones
- Prueba minuciosamente usando el debugger
- Omite la transferencia simple - ve directamente a la interacción con contratos inteligentes
- Diseña tu algoritmo único - ¿qué problema vas a resolver?
- Planifica tu contrato inteligente - ¿qué funciones y datos necesitas?
- Crea múltiples parámetros - hazlo sofisticado y útil
- Prueba extensivamente - asegúrate de que todo funcione perfectamente
- Documenta tu innovación - explica por qué tu solución es única
- Desarrollo Local: Usa
http://localhost:3000
para pruebas - Producción: Despliega en Vercel con
vercel --prod
- Dominios Personalizados: Actualiza las URLs de tu metadata para coincidir con tu dominio
- Variables de Entorno: Almacena datos sensibles como claves privadas de forma segura
- Redes de Prueba: Comienza con Fuji testnet, luego pasa a mainnet