Records immutable audit events on the Stellar blockchain with on-chain queryable state.
| Function | Description |
|---|---|
record_ballot(ballot_id_hash) |
Register a ballot on-chain |
record_token(ballot_id_hash) |
Increment token issued count |
record_vote(ballot_id_hash) |
Increment vote cast count |
record_result(ballot_id_hash, result_hash) |
Publish result hash |
get_tokens_issued(ballot_id_hash) |
Read token count |
get_votes_cast(ballot_id_hash) |
Read vote count |
get_result_hash(ballot_id_hash) |
Read result hash |
is_consistent(ballot_id_hash) |
Check tokens == votes |
All inputs use SHA-256 hashes of ballot UUIDs — no raw IDs stored on-chain.
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Add WASM target
rustup target add wasm32-unknown-unknown
# Install Stellar CLI
cargo install --locked stellar-cli --features optcd contracts/anonvote
cargo build --target wasm32-unknown-unknown --releaseOutput: target/wasm32-unknown-unknown/release/anonvote.wasm
cd contracts/anonvote
cargo test- Rust with the
wasm32-unknown-unknowntarget - Stellar CLI
jqinstalled- A Stellar account funded with testnet/mainnet XLM
Copy .env.example to .env and fill in:
cp .env.example .env| Variable | Required | Description |
|---|---|---|---|
| STELLAR_SECRET_KEY | Yes | Admin secret key for signing the deploy transaction |
| SOROBAN_RPC_URL_TESTNET | No | Testnet RPC endpoint — defaults to https://soroban-testnet.stellar.org |
| SOROBAN_RPC_URL_MAINNET | No | Mainnet RPC endpoint — defaults to https://soroban-mainnet.stellar.org |
source .env
./deploy.sh testnetsource .env
./deploy.sh mainnet- Builds the WASM binary
- Deploys to the specified Stellar network
- Initializes the contract with the derived admin address
- Records contract ID, WASM hash, git commit, and timestamp in
deployments.json - Creates a git tag (
contract-testnet-v1.0.0orcontract-mainnet-v1.0.0) - Prints a summary with the contract ID and verification links
Check the contract on Stellar Explorer:
- Testnet:
https://stellar.expert/explorer/testnet/contract/<CONTRACT_ID> - Mainnet:
https://stellar.expert/explorer/mainnet/contract/<CONTRACT_ID>
After a successful deployment the contract ID is recorded in two places:
contracts/CONTRACT_ID— human-readable file, one line per network, committed to gitcontracts/deployments.json— full deployment metadata (contract ID, WASM hash, git commit, timestamp)
Both files are updated automatically by deploy.sh. Commit them so the deployed ID is always traceable in version history.
Set the contract ID in the backend before starting the server:
# contracts/.env (used by the TypeScript service layer)
SOROBAN_CONTRACT_ID=<CONTRACT_ID>
# backend/.env (used by the backend application)
SOROBAN_CONTRACT_ID=<CONTRACT_ID>The backend reads SOROBAN_CONTRACT_ID from the environment on startup and validates the format before accepting requests. If the variable is missing or malformed the server will refuse to start.
After deployment, push the tag and commit:
git push origin contract-testnet-v1.0.0
git push origin feat/contract-deployment-scriptOnce deployed, update backend/src/services/sorobanService.ts calls in:
backend/src/services/ballotEngine.ts— callinvokeContract(id, "record_ballot", [...])backend/src/services/identityManager.ts— callinvokeContract(id, "record_token", [...])backend/src/services/privacyEngine.ts— callinvokeContract(id, "record_vote", [...])backend/src/services/resultEngine.ts— callinvokeContract(id, "record_result", [...])
The ballot_id_hash argument should be hashIdentifier(ballotId) — the same SHA-256 function already used in the backend.
The preferred way to use the service is via the createSorobanService factory, which creates an object with all methods pre-bound to a config:
import { Keypair } from "stellar-sdk";
import { createSorobanService, createDefaultTestnetConfig } from "@anonvote/contracts/service";
const sourceKeypair = Keypair.fromSecret(process.env.STELLAR_SECRET_KEY!);
const config = createDefaultTestnetConfig({
contractId: process.env.SOROBAN_CONTRACT_ID!,
sourceKeypair,
});
const service = createSorobanService(config);
await service.sorobanRecordBallot("hash123");// Testnet — defaults to https://soroban-testnet.stellar.org
const testnetConfig = createDefaultTestnetConfig({ contractId, sourceKeypair });
// Mainnet — defaults to https://soroban-mainnet.stellar.org
const mainnetConfig = createDefaultMainnetConfig({ contractId, sourceKeypair });
// Override any field for custom RPC gateways or local dev nodes:
const customConfig = { ...testnetConfig, rpcUrl: "http://localhost:8000" };The module-level functions are still exported for callers who need to pass config dynamically:
import { SorobanServiceError, sorobanRecordBallot } from "@anonvote/contracts/service";
try {
await sorobanRecordBallot(config, ballotIdHash);
} catch (err) {
if (err instanceof SorobanServiceError) {
if (err.retryable) {
// Enqueue for retry with backoff
}
}
throw err;
}All service helpers throw SorobanServiceError on failure. Import it from service/index.ts and wrap every call:
import { SorobanServiceError } from "@anonvote/contracts/service";
try {
await service.sorobanRecordBallot(ballotIdHash);
} catch (err) {
if (err instanceof SorobanServiceError) {
if (err.retryable) {
// Enqueue for retry with backoff — NETWORK_ERROR and SIMULATION_FAILED
// are transient; retrying is safe and expected.
} else {
// CONTRACT_ERROR or TRANSACTION_FAILED — do not retry.
// CONTRACT_ERROR indicates a logic error (e.g. ballot already exists).
}
}
throw err;
}Error code retryability:
SorobanServiceErrorCode |
retryable |
When thrown |
|---|---|---|
NETWORK_ERROR |
true |
RPC endpoint unreachable, DNS failure, TCP reset |
SIMULATION_FAILED |
true |
RPC timeout, overloaded node, no error code |
TRANSACTION_FAILED |
false |
sendTransaction returned ERROR, or tx never confirmed after max retries |
CONTRACT_ERROR |
false |
On-chain logic error (BallotNotFound, BallotAlreadyExists, etc.) |
Full error details (raw RPC responses, contract diagnostics) are logged internally only and are never exposed in the thrown error message, so it is safe to surface err.message in structured logs. Do not include raw contract error details in API responses to clients.
AnonVote development is organized into three milestones. Each issue is tagged with which milestone it belongs to.
Everything works end-to-end on testnet. A real admin can create a ballot, upload voters, issue tokens, collect votes, tally, and verify the result on Stellar. No manual database steps.
Status: In progress Focus: Core voting flow, Soroban integration, vote encryption, public verification
The system is production-safe. Per-ballot encryption keys, rate limiting, error handling, retry queues, no raw identifiers anywhere, Soroban fully wired not stubbed.
Status: Planned Focus: Security hardening, production readiness, reliability, scalability
@anonvote/crypto published on npm, docs repo complete, contracts deployed on mainnet, third party developers can build on top of AnonVote using the JS SDK.
Status: Planned Focus: SDK release, third-party integrations, documentation
Issues are labeled with their corresponding milestone so you can see what stage of development they belong to.