Demo dApp diseñada para charlas y talleres de Stellar/Soroban. Implementa una "vaca" (caja comunitaria LATAM) como contrato Soroban + frontend Next.js. Los asistentes conectan Freighter y aportan XLM en vivo; la barra de progreso sube en tiempo real y, al alcanzarse la meta, el speaker demuestra el retiro restringido a admin. La instancia pre-expirada sirve para mostrar el flujo de refund sin tener que esperar al deadline.
- Especificación completa:
CROWDFUNDING.md. - Playbook listo para una charla en vivo:
DEMO.md.
crowdfunding-dapp/
├── contracts/crowdfunding/ ← Soroban smart contract (Rust)
│ ├── src/contract.rs ← Lógica pública: contribute, refund, withdraw…
│ ├── src/storage.rs ← Helpers de Instance + Persistent storage
│ ├── src/events.rs ← Eventos tipados (#[contractevent])
│ └── src/test.rs ← 21 tests unitarios
├── packages/crowdfunding-client/← Bindings TypeScript autogenerados (no editar)
├── frontend/ ← Next.js 15 + Tailwind
│ ├── app/page.tsx ← Pantalla pública
│ ├── app/admin/page.tsx ← Panel admin
│ └── lib/ ← cliente, formato, hooks, eventos
├── scripts/
│ ├── deploy.sh ← build + upload + deploy + regenera bindings
│ └── init-crowdfunding.sh ← invoca initialize con los args
├── Cargo.toml ← Cargo workspace
├── package.json ← npm workspaces (frontend + bindings)
└── CROWDFUNDING.md ← Spec original
| Herramienta | Versión mínima | Verifica con |
|---|---|---|
stellar CLI |
26.0.0 | stellar --version |
| Rust + cargo | 1.85+ | cargo --version |
| Target WASM | wasm32v1-none |
rustup target list --installed |
| Node | 20+ | node --version |
| npm | 10+ | npm --version |
| Freighter | última | https://www.freighter.app |
Si te falta el target WASM:
rustup target add wasm32v1-nonegit clone … crowdfunding-dapp
cd crowdfunding-dapp
npm install # instala frontend + bindings packageCrea (o asegura) una identity de Stellar en testnet, fondeada por Friendbot:
stellar keys generate speaker --network testnet --fund
stellar keys address speaker # cópiala, será el admin(Si ya tienes una identity, úsala — sus comandos abajo asumen que se llama speaker.)
npm run contract:test21 tests cubriendo: initialize, contribute (éxito + edge cases), withdraw (admin + no-admin), refund (Failed + casos inválidos), check_expiration, extend_deadline. Salida esperada:
test result: ok. 21 passed; 0 failed; 0 ignored
scripts/deploy.sh speakerEl script:
- Compila el contrato a WASM con
stellar contract build. - Lo sube a testnet con
stellar contract upload. - Crea una instancia con
stellar contract deploy. - Regenera los bindings TypeScript con
stellar contract bindings typescript. - Imprime el
CONTRACT_IDfinal.
Cuando cambies el contrato, repite
scripts/deploy.sh: el paso 4 reescribe el paquetepackages/crowdfunding-clientcon el spec nuevo y un contract ID fresco.
Pega el ID en frontend/.env.local:
cp frontend/.env.local.example frontend/.env.local
# edita el archivo y pega NEXT_PUBLIC_CONTRACT_ID=C…Una vez desplegado, llama initialize con los parámetros que quieras:
# Vaca activa: meta 50 XLM, deadline en 60 minutos
scripts/init-crowdfunding.sh "Vaca de la charla" 50 3600 \
C…CONTRACT_ID… speakerSi quieres una segunda vaca pre-expirada para la demo de refund, despliega otro contrato (vuelve a correr deploy.sh) e inicialízalo con un deadline corto:
scripts/init-crowdfunding.sh "Vaca expirada" 50 1 \
C…SEGUNDO_CONTRACT_ID speaker
# después de 1s ya está expirado on-chainAgrega el segundo ID a frontend/.env.local:
NEXT_PUBLIC_CONTRACT_ID=C…activo
NEXT_PUBLIC_CONTRACT_ID_EXPIRED=C…expiradoEl frontend mostrará un selector entre ambas vacas en la parte superior.
Nota sobre la vaca expirada: como el deadline ya pasó, antes de mostrar el refund alguien tiene que llamar
check_expirationpara mover el estado aFailed. El panel admin tiene el botón listo, o puedes hacerlo desde la CLI:stellar contract invoke --id C…EXP --source speaker --network testnet -- check_expiration
npm run devAbre http://localhost:3000.
- Pantalla principal (
/): barra de progreso, countdown, formulario para aportar, lista de últimos 10 contribuyentes (víagetEventsdel RPC), refresh automático cada 5s. - Pantalla admin (
/admin): visible solo si la wallet conectada coincide con el admin del contrato; botón "Retirar fondos" habilitado solo cuando la meta esté alcanzada; botón "Marcar Failed" para vacas expiradas.
El repo ya trae vercel.json con la configuración de build para el monorepo.
Lo único que tienes que hacer es importar el repo en Vercel una vez.
-
Entra a https://vercel.com/new y selecciona "Import Git Repository".
-
Elige el repo (
QuillaBlocks/crowdfunding-dapp). -
Importante — Root Directory: Vercel va a sugerirte
frontendautomáticamente porque ahí detecta Next.js. No lo aceptes. Click "Edit" al lado de Root Directory y déjalo en./(la raíz del repo). Si no lo haces, npm workspaces no resuelvecrowdfunding-clienty el build falla conCannot find module 'crowdfunding-client'o con la ruta duplicadafrontend/frontend/.next not found. -
Vercel lee
vercel.jsony autoconfigura:- Framework: Next.js
- Install Command:
npm install - Build Command:
npm run build --workspace=crowdfunding-client && npm run build --workspace=crowdfunding-frontend - Output Directory:
frontend/.next
No cambies nada de esto.
-
Environment Variables — agrega estas tres (todas con scope "Production" + "Preview" + "Development"):
Key Value NEXT_PUBLIC_CONTRACT_IDEl contract ID activo (empieza con C…)NEXT_PUBLIC_CONTRACT_ID_EXPIRED(opcional) Contract ID de la vaca pre-expirada NEXT_PUBLIC_RPC_URLhttps://soroban-testnet.stellar.org -
Click Deploy.
Al terminar tienes una URL https://crowdfunding-dapp-<hash>.vercel.app. La promueves a un dominio custom desde el dashboard si quieres.
- Push a
main→ deploy de producción (la URL principal). - Push a cualquier otra rama / PR → deploy de preview con su propia URL temporal (útil para probar cambios sin tocar la URL pública).
- Pull request comments: Vercel deja un link al preview en cada PR.
Cada vez que corres scripts/deploy.sh localmente sale un CONTRACT_ID nuevo. Para que la app en Vercel apunte al contrato nuevo:
- Settings → Environment Variables → edita
NEXT_PUBLIC_CONTRACT_ID. - Re-despliega: Deployments → ⋯ → Redeploy (o haz un push vacío a
main).
Los bindings de TypeScript se commitean al repo cada vez que corres deploy.sh, así que el build de Vercel siempre usa los del último deploy.
| Error | Causa | Fix |
|---|---|---|
Cannot find module 'crowdfunding-client' |
Faltó el paso tsc de los bindings, o Root Directory = frontend (workspaces no resuelven) |
Settings → Root Directory = ./ |
frontend/frontend/.next not found |
Root Directory = frontend y vercel.json duplica la ruta |
Settings → Root Directory = ./ y Redeploy |
Module not found: ./dist/index.js |
El paquete de bindings no se compiló | Verifica que npm run build --workspace=crowdfunding-client corra primero |
Build exceeded maximum duration |
Stellar SDK + Next es pesado | Sube el plan o usa output: 'standalone' en next.config.js |
| La página muestra "Falta configurar el contract ID" | NEXT_PUBLIC_CONTRACT_ID no está seteada en Vercel |
Settings → Environment Variables |
- Abre la dApp con la vaca activa (proyector).
- Conecta tu Freighter (admin). Aparece el link al panel admin.
- Pide a los asistentes que abran la URL, conecten Freighter y fondeen con Friendbot.
- Cada quien aporta. En pantalla: barra subiendo, contribuyentes apareciendo, countdown bajando.
- Cuando se alcanza la meta: confeti, badge "Meta alcanzada", botón admin habilitado.
- Como admin, abre
/admin→ "Retirar fondos". Muestra que solo a ti te deja firmar. - Verifica la transacción en Stellar Expert (link inline).
- Cambia al selector "Vaca expirada". Conecta una wallet que haya aportado allí (o monta la demo con dos contribuciones previas). Pulsa "Reclamar mi aporte".
- Contrato: Soroban SDK 26, sin librerías externas. Usa
#[contractevent],i128,token::Clientpara transferencias del SAC del XLM nativo. - Bindings: generados por
stellar contract bindings typescript, instalados como dependencia local del frontend vía npm workspaces. - Frontend: Next.js 15 (App Router), React 19, Tailwind 3,
@stellar/stellar-sdk,@stellar/freighter-api. Poppins + JetBrains Mono. Paleta QuillaBlocks. - Estado en vivo: polling al RPC cada 5s (
useCrowdfunding) + lectura de eventos víaServer.getEventspara la lista de contribuyentes.
ledger protocol version too old for host al correr tests
Asegúrate de no estar seteando LedgerInfo manualmente con protocol_version: 23. Usa env.ledger().with_mut(|li| li.timestamp = …) para mantener el default del SDK.
El frontend no encuentra crowdfunding-client
Corre npm install desde la raíz para que workspaces enlace el paquete. Si los bindings no existen aún, scripts/deploy.sh los genera; de lo contrario puedes generar un placeholder con
stellar contract bindings typescript --wasm target/wasm32v1-none/release/crowdfunding.wasm --output-dir packages/crowdfunding-client --overwrite.
Error: NotInitialized al cargar la página
Faltó correr scripts/init-crowdfunding.sh después de deploy.sh. El contrato existe pero no tiene admin/goal/deadline.
error: DeadlineInPast al inicializar
El parámetro DEADLINE_SECONDS se cuenta desde ahora. Si el reloj de tu RPC y el de tu shell están descoordinados, súbelo a unos minutos.
El admin no aparece como tal en el panel
Asegúrate de conectar la misma wallet que firmó initialize. La dirección admin se ve en cualquier momento con:
stellar contract invoke --id C… --network testnet -- adminTus aportes no aparecen en "Últimos aportes"
La RPC pública mantiene eventos ~24h. Si llevas más de un día sin actividad, baja lookback en lib/events.ts o redespliega.
Freighter pide cambiar de red La dApp habla con testnet. Cambia la red en Freighter antes de aportar.
MIT, salvo donde se indique lo contrario.