Skip to main content

Raffle app

The raffle app is the reference build for a Gora app with both off-chain and on-chain components. The off-chain app picks one winner from registered participants across Base, Solana, and Algorand. The winner is paid on the same chain they registered on. If the winner is on Algorand, the Algorand app treasury pays ALGO. If the winner is on Solana, the Solana treasury pays SOL. If the winner is on Base, the Base contract pays ETH.
participant registers on a chain

operator runs the next raffle round

Gora executes the off-chain drawer

drawer returns winner + attestation hash inputs

operator settles on the winner's chain

winner-chain treasury pays 5% to the winner

What you are building

The reference app lives in examples/apps/raffle-app.
PartFile or folderPurpose
Off-chain appsrc/index.tsDeterministically chooses the winner from round number and participant registry.
Manifestgora.app.jsonDeclares app id, chains, actions, contracts, and iOS display metadata.
HTML UIui/index.htmlRenders the mobile app screen in a WebView and calls window.Gora.
App interfacegora.app.json -> interfaceDeclares generic views and actions so the bridge/mobile app do not hardcode raffle logic.
Policypolicy.jsonRestricts allowed chains and actions.
Base contractcontracts/base/GoraBaseRaffle.solStores Base participants and pays ETH from the contract treasury.
Solana programcontracts/solana/programStores Solana state and pays SOL from the treasury account.
Algorand appcontracts/algorandUses app opt-in as registration and pays ALGO from the app account.
Demo CLIscripts/raffle.mjsDeploys, registers, funds, draws, and settles for local demos.

HTML UI

The mobile wallet does not render raffle-specific Swift UI. It loads the app’s HTML from:
curl https://gora-bridge-dev.ngrok.app/apps/raffle_app/ui
The Raffle HTML uses the injected WebView bridge:
await window.Gora.context()
await window.Gora.view("participants")
await window.Gora.view("next_round")
await window.Gora.action("register", { chain: "base" })
await window.Gora.action("run_round", { chain: "base" })
The HTML owns layout and display. Native iOS only provides wallet context, bridge requests, and the signer sheet.

App interface

The Raffle app declares its app-specific surface in gora.app.json under interface. The important views are:
ViewWhat it returnsHow it is evaluated
participantsparticipants grouped by chainBase uses participantCount() + participants(index). Algorand uses app opt-ins from the dev wallet registry.
next_roundthe next round numberBase, Solana, and Algorand expose per-chain round counters; the bridge reduces them with max.
The important actions are:
ActionRoleWhat it declares
registerparticipantEVM register(), Solana register instruction, Algorand app opt-in.
run_roundoperatorBuild off-chain input from participants + next_round, invoke Gora, then settle.
settle_winneroperatorMap app output fields into the winner-chain settlement transaction.
The Dev Bridge evaluates these declarations through generic endpoints:
curl https://gora-bridge-dev.ngrok.app/apps/raffle_app/views/participants
curl https://gora-bridge-dev.ngrok.app/apps/raffle_app/views/next_round
This is the main rule for new examples: app-specific UI belongs in HTML, and app-specific chain reads/action shapes belong in the app interface, not in bridge or native mobile code. For mobile signing, the interface declares the full transaction shape. Base actions declare selectors and ABI argument paths. Solana actions declare ordered accounts, signer/writable flags, and instruction data. Algorand actions declare on_complete, app args, foreign accounts, and the minimum fee needed for inner transactions.

Prerequisites

You need:
  • a checkout of gora-v2
  • Node and npm
  • Rust/Cargo for the Gora CLI
  • the VM Devnet running Base, Solana, Algorand, Gora API, Dev Bridge, and ngrok
  • the iOS app only if you want to test the mobile flow after the CLI flow works
Start Devnet on the VM:
cargo run -p protocol-core --bin gora -- devnet up \
  --chains base,solana,algorand \
  --mobile-signing true
By default the Devnet script seeds and deploys the raffle-app example. To point the same flow at a different example later, set GORA_DEV_EXAMPLE_APP=<folder-name> or GORA_DEV_EXAMPLE_APP_DIR=/absolute/path/to/app. Expected public endpoints:
Gora API:     https://gora-dev.ngrok.app
Dev bridge:   https://gora-bridge-dev.ngrok.app
Base RPC:     https://base-dev.ngrok.app
Solana RPC:   https://solana-dev.ngrok.app
Algorand RPC: https://algorand-dev.ngrok.app
Check the bridge:
curl https://gora-bridge-dev.ngrok.app/health
The health response should include:
GET /apps
GET /apps/:id/ui
GET /mobile/signing-requests
GET /chains/algorand/transactions/params
POST /chains/algorand/transactions

Create the app workspace

For the reference demo, copy or open the existing example:
cd examples/apps/raffle-app
npm install
npm run setup:wallets
setup:wallets writes:
test-wallets/wallets.json
test-wallets/secrets.json
wallets.json contains public dev addresses. secrets.json contains dev-only private keys and mnemonics. Do not use these outside local/dev demos. For a remote VM demo, the VM only needs wallets.json to pre-fund generated Solana and Algorand accounts. Your local machine needs secrets.json if you want to import those wallets into the iOS simulator.

Deploy the chain components

The raffle has one chain component per chain. Each deploy writes a generic receipt to:
.gora/contracts/<chain>/latest.json
The raffle script also writes a legacy copy to:
.gora/contracts/<chain>/raffle.json

Base

npm run deploy:base

Algorand

npm run deploy:algorand
The Algorand app account is the raffle treasury. Fund that app account before drawing an Algorand winner:
npm run fund -- algorand 100

Solana

Build and deploy the Solana program where the Solana toolchain is installed:
cargo build-sbf --manifest-path contracts/solana/program/Cargo.toml
PROGRAM_KEYPAIR=contracts/solana/program/target/deploy/gora_solana_raffle-keypair.json
SO_FILE=contracts/solana/program/target/deploy/gora_solana_raffle.so
solana program deploy "$SO_FILE" --program-id "$PROGRAM_KEYPAIR" --url http://127.0.0.1:8899
export SOLANA_PROGRAM_ID=$(solana address -k "$PROGRAM_KEYPAIR")
npm run deploy:solana
If you are running these scripts on the VM, keep transaction submissions on the local RPC and publish the ngrok URL in the contract receipt:
export SOLANA_RPC_URL=http://127.0.0.1:8899
export SOLANA_PUBLIC_RPC_URL=https://solana-dev.ngrok.app
Fund the Solana treasury before drawing a Solana winner:
npm run fund -- solana 25

Deploy the off-chain Gora app

Point the CLI at Devnet and deploy:
gora config set node.url https://gora-dev.ngrok.app
gora build
gora validate
gora deploy
Confirm the app is visible:
gora inspect app raffle_app
curl https://gora-bridge-dev.ngrok.app/apps
The app entry should include iOS metadata, chain RPCs, owner wallets, and contract records for the deployed chains.

Register participants

Register one or more participants per chain:
npm run register -- base 0xC224C62A6AEaafc76D3beE9ccf6bAcBF73a75F11
npm run register -- solana <SOLANA_PARTICIPANT_ADDRESS>
npm run register -- algorand <ALGORAND_PARTICIPANT_ADDRESS>
Base registration calls register(). Solana registration sends the raffle register instruction. Algorand registration is an application opt-in. If an Algorand address cannot register twice, that is expected; opt-in is the registration.

Fund treasuries

Each chain has its own treasury. The winner is paid by the treasury on the winner’s chain.
npm run fund -- base 1
npm run fund -- solana 25
npm run fund -- algorand 100
For Algorand, fund the app address from the deployment receipt:
cat .gora/contracts/algorand/latest.json
Use the app_address value if you want to fund manually from the mobile wallet.

Draw and settle

Run one round:
npm run draw
Run later rounds:
npm run draw -- --round 2
npm run draw -- --round 3
The output includes:
  • winner chain
  • winner address
  • registry hash
  • attestation hash
  • settlement transaction
There is no separate claim step. Settlement pays the winner in the same transaction.

Test from iOS

Use the iOS app after the CLI path works.
  1. Open the app and set the bridge URL to https://gora-bridge-dev.ngrok.app.
  2. Import the operator wallet for the chain you want to operate from.
  3. Import participant wallets in other simulators.
  4. Pull to refresh balances.
  5. Open Apps.
  6. Open Raffle.
  7. As participants, register on Base, Solana, or Algorand.
  8. As operator, run the next round.
The iOS app should show the active chains and the current participants. The operator wallet is detected from owner_wallets in the app manifest.

Troubleshooting

Apps fail with cannot reach gora node

The bridge is running but points at a stale Gora API port. Restart Devnet or kill the old bridge process on port 8787, then start Devnet again:
lsof -nP -iTCP:8787 -sTCP:LISTEN
kill <PID>

Algorand balance does not update

Pull to refresh in the iOS wallet. The app should query the bridge dynamically. If the balance is still stale, check the bridge Algorand RPC URL and token:
curl https://gora-bridge-dev.ngrok.app/health
curl https://gora-bridge-dev.ngrok.app/balances/algorand/<ADDRESS>

Algorand settle says app does not exist

The mobile app or bridge is using an old Algorand app id. Refresh Apps and verify:
cat .gora/contracts/algorand/latest.json
Redeploy the Gora app after redeploying the Algorand app so the manifest has the current app id.

Algorand settle says would result negative

The Algorand app account does not have enough ALGO to pay the 5% payout and fees. Fund the app treasury:
npm run fund -- algorand 100

Algorand settle says unavailable Account

The winner address must be included in the Algorand transaction’s foreign account list. This is handled by the current iOS and raffle script code. Rebuild the iOS app if you see this from mobile.

Second round fails with assert failed

The round submitted to the chain is stale. Rebuild the iOS app and refresh Apps so it queries the live next round from the bridge before invoking the off-chain app.

Solana airdrops hang

The Devnet script funds Solana accounts through JSON-RPC. If funding is slow, restart the Solana local validator and rerun Devnet. The funding log is:
target/devnet-chains/logs/raffle-app-solana-wallet-funding.log

Demo checklist

Before livestreaming:
  1. Start Devnet cleanly.
  2. Confirm bridge health.
  3. Deploy Base and Algorand components.
  4. Deploy Solana only if the VM toolchain is ready.
  5. Fund all treasuries.
  6. Register at least one participant per chain.
  7. Run one CLI draw.
  8. Rebuild iOS.
  9. Repeat the same flow from mobile.
If you are building a new example, keep this rule: app-specific state readers and transaction builders should move into manifest-declared schemas or reusable bridge/mobile primitives, not into Gora core.