Dos contratos Soroban desplegados en Testnet, conectados entre sí:
- MyToken (
my-token): token fungible SEP-0041 con mint gobernado por owner, sell con burn, pausable. - PromptMarketplace (
prompt-marketplace): marketplace donde admins registran prompts, usuarios compran (quemando tokens), y admins pueden re-mintear.
| Contrato | ID | Wasm Hash | Tamaño |
|---|---|---|---|---|
| MyToken | CCHAUOEVX6TQD56VFZY2GI3N3HNF5W6QSRRKAGKDXV6S53T4BKD5PYQD | 6613593d... | 9.7 KB |
| PromptMarketplace | CA6RRLV4IBLKRRLPUDCVXZFDKRE77YHBNJFSXEFFLXV6EUAVVVS6HJUQ | f31af684... | 5.6 KB |
Arquitectura: el marketplace almacena el ID del token en __constructor. buy_prompt llama a my_token::sell_forwarded via env.invoke_contract (cross-contract). remint llama a my_token::mint_forwarded. Ambos contratos emiten eventos #[contractevent] (PromptRegistered, PromptPriceUpdated, PromptRemoved, PromptPurchased, TokensReminted).
⚠️ Cross-contract auth forwarding: las funcionessell_forwardedymint_forwardedno verificanrequire_auth()internamente. Confían en que la root invocation (marketplace) ya verificó la autorización. Esto evita el errorError(Auth, ExistingValue)de Soroban cuando se llamarequire_auth()para la misma address en root + sub-invocation.
# Todos los tests (29)
SOROBAN_SDK_BUILD_SYSTEM_SUPPORTS_SPEC_SHAKING_V2=1 cargo test --workspace --target aarch64-apple-darwin
# Por paquete
SOROBAN_SDK_BUILD_SYSTEM_SUPPORTS_SPEC_SHAKING_V2=1 cargo test -p my-token --target aarch64-apple-darwin
SOROBAN_SDK_BUILD_SYSTEM_SUPPORTS_SPEC_SHAKING_V2=1 cargo test -p prompt-marketplace --target aarch64-apple-darwinEn Linux/CI usa
--target x86_64-unknown-linux-gnuen vez deaarch64-apple-darwin. El target por defecto del workspace (.cargo/config.toml) eswasm32v1-none, que no soportacargo test— siempre hay que pasar--targetexplícito para correr tests.
Soroban v25's mock auth no puede satisfacer un SEGUNDO require_auth() para la MISMA address dentro de un mismo árbol de invocación: si la invocación raíz llama buyer.require_auth() y luego una sub-invocación (marketplace → token vía invoke_contract) también llama require_auth() para buyer, el host rechaza con Error(Auth, ExistingValue) — mock auth no tiene forma de representar "esta address ya autorizó más arriba en el árbol".
Mitigación adoptada en este código: sell_forwarded y mint_forwarded (contracts/tokens/src/contract.rs) NO llaman require_auth() — confían en que la invocación raíz (buy_prompt / remint) ya autorizó la address correspondiente. Como resultado, sí es posible escribir tests con mock_auths() que mockeen solo el único require_auth() de la raíz y ejerzan la llamada cross-contract real de punta a punta (balances, eventos incluidos) — ver test_buy_prompt_cross_contract, test_remint_cross_contract, test_buy_prompt_emits_event, test_has_access_after_buy en contracts/marketplace/src/tests.rs. No se necesitan sub_invokes porque no hay un require_auth() anidado del lado del token.
El riesgo que esto sigue implicando: sell_forwarded / mint_forwarded no verifican autorización en absoluto — si algo pudiera invocarlos directamente (sin pasar por el marketplace), podría mintear o quemar balances arbitrarios. Ese límite de confianza se ejercita explícitamente, SIN ningún mock_auths(), en contracts/tokens/src/tests.rs (test_sell_forwarded_updates_balance, test_mint_forwarded_mints_tokens) — para probar que esas funciones realmente no requieren autorización, no solo el happy path.
scripts/integration-test.sh ejercita el mismo flujo de punta a punta contra testnet real con firmas genuinas (Soroban CLI), que es el único lugar donde un require_auth() anidado genuino (si se reintrodujera por error) sería detectado.
Prueba el flujo completo contra testnet real: mint → register → buy (cross-contract) → verify → remint → verify.
# Con los contracts deployados actuales
TOKEN="CCHAUOEVX6TQD56VFZY2GI3N3HNF5W6QSRRKAGKDXV6S53T4BKD5PYQD" \
MKT="CA6RRLV4IBLKRRLPUDCVXZFDKRE77YHBNJFSXEFFLXV6EUAVVVS6HJUQ" \
bash scripts/integration-test.sh| Test | Qué cubre |
|---|---|
test_token_mint_and_balance |
Mint vía storage API + balance + total_supply |
test_token_metadata |
name, symbol, decimals |
test_mint_multiple_same_user |
Mint acumulativo a la misma address |
test_mint_to_different_users |
Mint a múltiples usuarios, supply tracking |
test_mint_overflow_panics |
i128::MAX + 1 debe panic |
test_zero_balance_default |
Balance por defecto es 0 |
test_sell_forwarded_updates_balance |
sell_forwarded quema tokens y emite SellEvent, invocado SIN mock_auths() |
test_mint_forwarded_mints_tokens |
mint_forwarded mintea tokens y emite MintEvent, invocado SIN mock_auths() |
| Test | Qué cubre |
|---|---|
test_register_and_query_prompt |
Happy path: register → get_price / get_owner |
test_duplicate_registration_panics |
Mismo ID no se puede registrar dos veces |
test_update_price |
Admin cambia precio |
test_remove_prompt |
Admin elimina prompt (idempotente) |
test_multiple_prompts_independent |
Prompts distintos no interfieren |
test_get_price_unregistered_panics |
Consultar precio de prompt inexistente |
test_non_admin_cannot_register |
No-admin no puede registrar |
test_non_admin_cannot_update_price |
No-admin no puede cambiar precio |
test_non_admin_cannot_remove |
No-admin no puede eliminar |
test_register_zero_price_panics |
Precio 0 es inválido |
test_update_price_zero_panics |
Actualizar a precio 0 es inválido |
test_register_max_price |
i128::MAX funciona como precio |
test_update_unregistered_prompt_panics |
Actualizar precio de prompt inexistente |
test_register_after_remove |
Re-registrar mismo ID post-eliminación |
test_has_access_unregistered |
has_access sin compra devuelve false |
test_token_mint_and_balance |
Mint vía storage en contexto del token |
test_buy_prompt_cross_contract |
E2E: register → buy_prompt (cross-contract real vía invoke_contract) → balance quemado |
test_has_access_after_buy |
has_access es false antes de comprar y true después de buy_prompt |
test_buy_prompt_emits_event |
buy_prompt emite PromptPurchased con buyer/prompt_id/price correctos |
test_remint_cross_contract |
E2E: remint (cross-contract real vía invoke_contract) → balance minteado |
test_remint_emits_event |
remint emite TokensReminted con admin/to/amount correctos |
SOROBAN_SDK_BUILD_SYSTEM_SUPPORTS_SPEC_SHAKING_V2=1 stellar contract build --package my-token
SOROBAN_SDK_BUILD_SYSTEM_SUPPORTS_SPEC_SHAKING_V2=1 stellar contract build --package prompt-marketplaceRequiere el target wasm32v1-none y la env var SOROBAN_SDK_BUILD_SYSTEM_SUPPORTS_SPEC_SHAKING_V2=1 para Spec Shaking v2. Alternativamente, para mainnet se usa cargo build --target wasm32v1-none --release (ver sección Deploy en Mainnet).
# Token
stellar contract deploy \
--wasm target/wasm32v1-none/release/my_token.wasm \
--source default \
--network testnet \
--alias my_token \
-- \
--owner "$(stellar keys address default)" \
--name "PromptToken" \
--symbol "PRMPT" \
--decimals 7
# Marketplace (usar el ID del token deployado)
stellar contract deploy \
--wasm target/wasm32v1-none/release/prompt_marketplace.wasm \
--source default \
--network testnet \
--alias prompt_marketplace \
-- \
--admin "$(stellar keys address default)" \
--token "CCHAUOEVX6TQD56VFZY2GI3N3HNF5W6QSRRKAGKDXV6S53T4BKD5PYQD"
⚠️ ADVERTENCIA DE SEGURIDAD — LEER ANTES DE EJECUTARMainnet implica valor real y transacciones irreversibles. Antes de deployar:
- No hardcodees ni hagas
sourcede claves privadas. Usa un secrets manager (1Password CLI, AWS Secrets Manager, HashiCorp Vault, GitHub encrypted secrets, etc.) para inyectarMAINNET_DEPLOYER_SOURCEyMAINNET_ADMIN_SOURCEen runtime.- Verifica el código y el WASM antes de deployar. Recompila desde una fuente confiable, calcula el hash SHA-256 y comparalo con el artefacto que vas a subir.
- Preferí cuentas multisig o hardware wallets para la cuenta admin (
MAINNET_ADMIN_ADDR). El deployer y el admin pueden ser distintas cuentas.- Revisa los parámetros de constructor. Un error en
owner/admino en eltokendel marketplace puede dejar los contratos incontrolables.- Tené XLM suficiente en la cuenta deployer para cubrir el rent/storage de ambos contratos en mainnet.
- Stellar CLI configurado para mainnet (
stellar network add mainnet ...o variables de entorno equivalentes). - Acceso a un RPC endpoint de mainnet (Stellar public RPC, Blockdaemon, etc.).
- Cuenta con XLM real para pagar fees y rent.
- Cuenta admin separada (recomendado) con su par de claves seguras.
- Target de compilación instalado:
rustup target add wasm32v1-none. - Variable de entorno para Spec Shaking V2:
export SOROBAN_SDK_BUILD_SYSTEM_SUPPORTS_SPEC_SHAKING_V2=1
Build release, calcula hashes, deploya, inicializa y valida ambos contratos. Emite un resumen JSON en deploy-artifacts/.
MAINNET_DEPLOYER_SOURCE="deployer" \
MAINNET_ADMIN_SOURCE="admin" \
MAINNET_ADMIN_ADDR="G..." \
bash scripts/deploy-mainnet.shTambién podés usar el target de Makefile:
MAINNET_DEPLOYER_SOURCE="deployer" \
MAINNET_ADMIN_SOURCE="admin" \
MAINNET_ADMIN_ADDR="G..." \
make deploy-mainnetVariables opcionales:
| Variable | Descripción | Default |
|---|---|---|
MAINNET_TOKEN_NAME |
Nombre del token | AgentVerse Token |
MAINNET_TOKEN_SYMBOL |
Símbolo del token | AVT |
MAINNET_TOKEN_DECIMALS |
Decimales | 7 |
MAINNET_DEPLOY_OUT_DIR |
Carpeta de artefactos | ./deploy-artifacts |
El script:
- Construye ambos contratos con
cargo build --target wasm32v1-none --release. - Calcula el hash SHA-256 de cada WASM.
- Deploya
MyTokenyPromptMarketplacedesdeMAINNET_DEPLOYER_SOURCE. - Inicializa el token con el admin, nombre, símbolo y decimales configurados.
- Inicializa el marketplace con el admin y el
contract_iddel token recién deployado. - Valida post-deploy:
- Metadata del token (
name,symbol,decimals). - Supply inicial igual a
0. - Owner del token igual a
MAINNET_ADMIN_ADDR. - Admin del marketplace igual a
MAINNET_ADMIN_ADDR. - Token del marketplace igual al
contract_iddel token deployado. - Hash WASM on-chain (fetcheado) coincide con el artefacto local.
- Metadata del token (
- Escribe
deploy-artifacts/mainnet-deploy-summary-<timestamp>.json.
Verifica un par de contratos ya deployados sin re-deployar. Útil para CI o validaciones periódicas.
MAINNET_TOKEN_ID="C..." \
MAINNET_MARKETPLACE_ID="C..." \
MAINNET_ADMIN_ADDR="G..." \
bash scripts/verify-mainnet.shO vía Makefile:
MAINNET_TOKEN_ID="C..." \
MAINNET_MARKETPLACE_ID="C..." \
MAINNET_ADMIN_ADDR="G..." \
make verify-mainnetVerifica:
- Hash WASM on-chain vs. artefacto local.
- Admin/owner de ambos contratos.
- Link token ↔ marketplace.
- Supply inicial (
0). - Metadata del token.
Escribe deploy-artifacts/mainnet-verify-report-<timestamp>.json.
La v1 actual soporta cuentas multisig configuradas en Stellar CLI (el CLI pedirá/co-firmará las transacciones). Para una v2 se documentará como mejora:
- Flujo de firmas separadas: el deployer crea la transacción, múltiples signers la firman off-line, y alguien la publica.
- DAuthorization: delegar privilegios admin a un módulo de gobernanza on-chain.
# Leer nombre del token
stellar contract invoke --source default --network testnet \
--id CCHAUOEVX6TQD56VFZY2GI3N3HNF5W6QSRRKAGKDXV6S53T4BKD5PYQD \
-- name
# Registrar prompt
stellar contract invoke --source default --network testnet \
--id CA6RRLV4IBLKRRLPUDCVXZFDKRE77YHBNJFSXEFFLXV6EUAVVVS6HJUQ \
--send=yes \
-- register_prompt --prompt_id "alpha" --price 500 \
--owner "$(stellar keys address default)"
# Consultar precio
stellar contract invoke --source default --network testnet \
--id CA6RRLV4IBLKRRLPUDCVXZFDKRE77YHBNJFSXEFFLXV6EUAVVVS6HJUQ \
-- get_price --prompt_id "alpha"contracts/
├── tokens/ # MyToken (token fungible)
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs # module exports, re-export público
│ ├── contract.rs # #[contract] MyToken — API pública + traits
│ ├── events.rs # #[contractevent] structs (MintEvent, SellEvent, etc.)
│ ├── core/
│ │ └── token.rs # TokenManager — lógica de negocio
│ ├── storage/
│ │ └── types.rs # DataKey, contracttype structs
│ └── tests.rs # 8 tests
└── marketplace/ # PromptMarketplace
├── Cargo.toml
└── src/
├── lib.rs # module exports
├── contract.rs # #[contract] PromptMarketplace — API pública
├── storage/
│ └── types.rs # DataKey, Prompt struct, errors
└── tests.rs # 21 tests
Cargo.toml # workspace: tokens + marketplace
- Soroban SDK:
25.3.0 - OZ Stellar Contracts:
v0.7.1(fungible token, ownable, pausable) - Stellar CLI:
26.1.0 - Target:
wasm32v1-none(release),aarch64-apple-darwin(tests)
soroban-sdk v25 tiene una limitación: require_auth() para una misma dirección solo puede ejecutarse UNA VEZ por árbol de llamadas. Llamarlo en la root invocation y luego en una sub-invocación (via invoke_contract) falla con Error(Auth, ExistingValue).
Patrón adoptado:
- La función raíz (ej.
buy_prompt,remint) llamarequire_auth()para la/s dirección/es involucradas. - Las sub-invocaciones al token usan variantes
*_forwarded(sell_forwarded,mint_forwarded) que no verificanrequire_auth()internamente. - Para operaciones directas (sin marketplace), el token expone
sell(conrequire_auth) ymint(con#[only_owner]).
Esto aplica también a TokenManager::sell que usa Base::update en vez de Base::burn para evitar el doble require_auth de Base::burn.
Ver Brecha de mock auth en Soroban v25 para cómo este patrón se prueba (y se vigila como riesgo de seguridad) en los tests.