Contract reference
Complete reference for the Stellaroid Earn Soroban contract — all 19 public functions with auth requirements, the full error table, emitted events, the credential status lifecycle, and how to invoke it from the Stellar CLI. Everything here runs on Stellar testnet.
The Stellaroid Earn contract is written in Rust with soroban-sdk and deployed to Stellar testnet. It exposes 19 public functions: a one-shot init, nine credential-surface write functions covering issuer trust, credential lifecycle, and payment, two read functions, and a seven-function milestone-escrow extension for funded paid trials (documented below). The frontend invokes it through @stellar/stellar-sdk — writes are signed by the connected wallet and submitted over Soroban RPC; reads run as free simulations.
Function surface
Auth requirements below are derived from the on-chain roles each function encodes: the admin wallet set at init, issuers in the approval queue, and the employer or student addresses passed as arguments. Unauthorized callers are rejected with error 3 Unauthorized.
| Function | Signature | Description | Auth required |
|---|---|---|---|
| init | init(admin, token) | One-shot bootstrap. Stores the admin and reward token in instance storage. | One-shot; a second call fails with AlreadyInitialized (error 1) |
| register_issuer | register_issuer(issuer, name, website, category) | Issuer self-registers on-chain and enters the pending trust queue. | Issuer (self) |
| approve_issuer | approve_issuer(admin, issuer) | Admin approves a pending issuer so it can issue and verify credentials. | Admin only |
| suspend_issuer | suspend_issuer(admin, issuer) | Admin suspends an issuer from future issue / verify operations. | Admin only |
| register_certificate | register_certificate(issuer, student, cert_hash, title, cohort, metadata_uri) | Binds the hash plus minimal proof metadata to a student wallet; rejects duplicates; emits cert_reg. | Approved issuer |
| verify_certificate | verify_certificate(verifier, cert_hash) | Trusted verification of a registered credential; emits cert_ver. | Approved issuer or admin |
| revoke_certificate | revoke_certificate(actor, cert_hash) | Marks a credential revoked so payment-linked actions are blocked. | Authorized actor |
| suspend_certificate | suspend_certificate(actor, cert_hash) | Temporarily suspends a credential without deleting its audit trail. | Authorized actor |
| reward_student | reward_student(student, cert_hash, amount) | Admin-triggered XLM reward via the configured Stellar Asset Contract. | Admin only |
| link_payment | link_payment(employer, student, cert_hash, amount) | Employer pays a verified student directly; emits payment. | Employer |
| get_certificate | get_certificate(cert_hash) | Read-only lookup of the certificate record. | Public read (free simulation) |
| get_issuer | get_issuer(issuer) | Read-only lookup of issuer trust status and profile metadata. | Public read (free simulation) |
Credential status lifecycle
A credential moves through five on-chain statuses. Actions attempted in the wrong state fail with error 10 InvalidStatus.
| Status | Meaning |
|---|---|
| issued | Initial state after register_certificate binds the hash to the student wallet. Not yet verified. |
| verified | Set by verify_certificate (approved issuer or admin). Unlocks payment via link_payment. |
| suspended | Set by suspend_certificate. A temporary hold — the record and its audit trail stay on-chain. Transitions are one-way on the current surface: no function re-verifies a suspended credential. |
| revoked | Set by revoke_certificate. Terminal for payment: revoked credentials can no longer unlock payment (error 11). |
| expired | Credential expired and must be reissued or renewed (error 12). |
Status transitions are one-way: verify_certificate requires an Issued credential (a suspended credential cannot be re-verified) and revocation is terminal. The frontend additionally defines an unknown status as a client-side fallback for records it cannot decode; it is not an on-chain state.
Error codes
All 12 contract errors, as surfaced to callers and normalized by the frontend error layer.
| Code | Name | Meaning |
|---|---|---|
| 1 | AlreadyInitialized | init called twice. |
| 2 | NotInitialized | Admin/token not set yet. |
| 3 | Unauthorized | Caller isn't allowed. |
| 4 | AlreadyExists | Duplicate cert hash. |
| 5 | NotFound | Hash isn't registered. |
| 6 | InvalidAmount | Amount must be > 0. |
| 7 | IssuerNotFound | Issuer hasn't registered on-chain. |
| 8 | IssuerNotApproved | Issuer still needs admin approval. |
| 9 | IssuerSuspended | Issuer has been suspended. |
| 10 | InvalidStatus | Credential is in the wrong lifecycle state for this action. |
| 11 | CredentialRevoked | Credential was revoked and can no longer unlock payment. |
| 12 | CredentialExpired | Credential expired and must be reissued or renewed. |
Events
The contract emits events as Soroban RPC events with a symbol topic. The app's activity feed decodes the following event kinds; anyone can audit them independently on a Stellar explorer such as stellar.expert.
| Event | Emitted by | Payload / decoded meaning |
|---|---|---|
init | init | Contract bootstrapped. |
iss_reg | register_issuer | Issuer registered. |
iss_appr | approve_issuer | Issuer approved. |
iss_susp | suspend_issuer | Issuer suspended. |
cert_reg | register_certificate | Certificate registered; payload carries the certificate hash. |
cert_ver | verify_certificate | Certificate verified; payload carries the certificate hash. |
| cert_rev | revoke_certificate | Credential revoked; payload is the certificate hash. Not decoded by the app's activity feed — inspect it on stellar.expert. |
| cert_sup | suspend_certificate | Credential suspended; payload is the certificate hash. Not decoded by the app's activity feed — inspect it on stellar.expert. |
reward | reward_student | Student reward sent; payload carries the amount, formatted with the configured asset decimals and code. |
payment | link_payment | Employer payment sent; payload carries the amount, formatted with the configured asset decimals and code. |
A cert_fail event kind also exists; the frontend activity feed intentionally filters it out of the public feed.
Calling the contract
Writes via Stellar CLI
Write functions require the caller's signature. With Stellar CLI (v21+, stellar contract invoke — the deprecated soroban CLI is not used) and a funded testnet identity, registering a certificate looks like this:
stellar contract invoke \
--id <CONTRACT_ID> \
--source my-key \
--network testnet \
-- \
register_certificate \
--issuer <ISSUER_G_ADDRESS> \
--student <STUDENT_G_ADDRESS> \
--cert_hash <64_CHAR_SHA256_HEX> \
--title "Soroban Bootcamp Certificate" \
--cohort "2026-Q2" \
--metadata_uri "https://example.com/credential.json"cert_hash is a 32-byte value supplied as 64 hexadecimal characters — the frontend converts the hex string to a bytes32 ScVal before invoking, and the same format applies from the CLI. The invoking issuer must already be registered and approved, or the call fails with error 7 IssuerNotFound or 8 IssuerNotApproved.
Reads are free simulations
get_certificate and get_issuer never touch the ledger. The frontend builds an unsigned transaction from a configured read address (with a hardcoded fallback account when none is set) and executes it via the RPC simulateTransaction method, decoding the return value with scValToNative. No wallet, no signature, and no fee is required — this is what powers the public proof pages.
How the frontend submits writes
- Builds an
invokeContractFunctionoperation withTransactionBuilderatBASE_FEE, pinned to the expected network passphrase from config. - Prepares the transaction against Soroban RPC (with a raw-simulation fallback for SDK XDR parse failures), then hands the XDR to the connected wallet — Freighter or Albedo — for signing.
- Submits via
sendTransactionand acceptsPENDINGorDUPLICATEas in-flight statuses, then polls the transaction result for up to 20 attempts at 1.2 s intervals. - Serializes arguments with
nativeToScVal; addresses asaddress, strings asstring, and certificate hashes as 32-byte values converted from hex.
Test coverage
The contract ships with twelve tests in contract/src/test.rs (t1–t12). Together they cover the happy path, the issuer trust layer, revocation gating, event emission, and the escrow lifecycle.
| Test | What it proves |
|---|---|
t1_happy_path_with_approved_issuer | The complete journey: init, register issuer, approve, register certificate, verify, pay. |
t2_unapproved_issuer_cannot_issue | A pending issuer cannot register certificates before admin approval. |
t3_suspended_issuer_cannot_issue | A suspended issuer is blocked from issuing at the contract level. |
t4_wrong_approved_issuer_cannot_verify | An approved issuer cannot verify a credential it did not issue — only the issuing organization or the admin can. |
t5_revoked_credential_blocks_payment | A revoked credential can no longer unlock payment. |
t6_issuer_events_emit | Issuer registration and approval emit auditable events. |
t7_opportunity_happy_path | The escrow journey: create, fund, submit, approve, release. |
t8_revoked_credential_blocks_opportunity | A revoked credential cannot anchor a new paid-trial opportunity. |
t9_refund_funded_opportunity | An employer can recover escrowed funds from a funded trial that never progressed. |
t10_invalid_status_transitions_fail | Out-of-order escrow moves (release before approval, refund after approval) fail with typed errors. |
t11_rejects_too_many_opportunity_milestones | Milestone counts are bounded at opportunity creation. |
t12_employer_can_refund_submitted_opportunity | The employer can still exit and recover escrow after a milestone is submitted but before approval. |
Escrow extension: funded paid trials
Beyond the credential core, the contract includes a milestone-escrow extension that lets an employer fund a paid trial tied to a verified credential. The escrowed amount is held by the contract and released as milestones are approved. These seven functions complete the 19-function public surface.
| Function | Signature | Description | Auth required |
|---|---|---|---|
| create_opportunity | create_opportunity(employer, candidate, cert_hash, title, amount, milestone_count) | Creates a paid-trial opportunity against the candidate's credential; rejects non-positive amounts, expired credentials, and a cert_hash not owned by the candidate; emits opp_crt | Employer |
| fund_opportunity | fund_opportunity(employer, opp_id) | Escrows the full amount into the contract; fails with AlreadyFunded (#14) if repeated; emits opp_fund | Employer (creator) |
| submit_milestone | submit_milestone(candidate, opp_id) | Candidate marks the next milestone as delivered; emits mile_sub | Candidate |
| approve_milestone | approve_milestone(employer, opp_id) | Employer approves the submitted milestone; emits mile_apr | Employer (creator) |
| release_payment | release_payment(employer, opp_id) | Releases the approved milestone share from escrow to the candidate; emits pay_rel | Employer (creator) |
| refund_opportunity | refund_opportunity(employer, opp_id) | Returns remaining escrowed funds to the employer; emits pay_ref | Employer (creator) |
| get_opportunity | get_opportunity(opp_id) | Read-only lookup of an opportunity record | None (public read) |
The extension adds five typed error codes on top of the credential-surface errors:
| Code | Name | Meaning |
|---|---|---|
| 13 | OpportunityNotFound | No opportunity exists for this id. |
| 14 | AlreadyFunded | The opportunity has already been funded. |
| 15 | InvalidMilestone | Milestone submission or approval is out of order for the current milestone count. |
| 16 | InvalidOpportunityStatus | The action is not allowed in the opportunity's current status. |
| 17 | PaymentLocked | Escrowed funds cannot be released in the current state. |
Escrow events mirror the flow: opp_crt, opp_fund, mile_sub, mile_apr, pay_rel, and pay_ref — all auditable on stellar.expert alongside the credential events.
Frequently asked questions
- Who can verify a credential on-chain?
- Only an approved issuer or the admin wallet can call verify_certificate. Unauthorized callers are rejected with typed errors (Unauthorized #3, IssuerNotApproved #8, IssuerSuspended #9), and the boundary is exercised by contract tests t2 through t4 — unapproved, suspended, and wrong-issuer callers all fail.
- Do read functions cost anything?
- No. get_certificate and get_issuer are read-only lookups executed as RPC simulations from a read address — no wallet connection, no signature, and no network fee. This is how the public proof pages work.
- What happens if the same certificate hash is registered twice?
- The contract rejects duplicate hashes with error 4 (AlreadyExists) before writing anything, so an existing credential can never be overwritten or re-minted — the guard is enforced in register_certificate itself.
- Is the contract deployed to mainnet?
- No. Stellaroid Earn is an early-access pilot and the contract is deployed to Stellar testnet only. No real funds are involved, and all documented flows target the testnet network.
- How is payment gated by verification?
- link_payment pays a verified student directly, and a revoked credential can no longer unlock payment — the contract returns error 11 (CredentialRevoked), a boundary exercised by the t5_revoked_credential_blocks_payment contract test.
Run it yourself on testnet
Everything documented here is live in the early-access pilot — free, testnet-only, and auditable on stellar.expert.