Skip to content

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.

Testnet only. Stellaroid Earn is an early-access pilot; the contract is not deployed to mainnet and no real funds are involved. All examples below target the testnet network.

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.

FunctionSignatureDescriptionAuth required
initinit(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_issuerregister_issuer(issuer, name, website, category)Issuer self-registers on-chain and enters the pending trust queue.Issuer (self)
approve_issuerapprove_issuer(admin, issuer)Admin approves a pending issuer so it can issue and verify credentials.Admin only
suspend_issuersuspend_issuer(admin, issuer)Admin suspends an issuer from future issue / verify operations.Admin only
register_certificateregister_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_certificateverify_certificate(verifier, cert_hash)Trusted verification of a registered credential; emits cert_ver.Approved issuer or admin
revoke_certificaterevoke_certificate(actor, cert_hash)Marks a credential revoked so payment-linked actions are blocked.Authorized actor
suspend_certificatesuspend_certificate(actor, cert_hash)Temporarily suspends a credential without deleting its audit trail.Authorized actor
reward_studentreward_student(student, cert_hash, amount)Admin-triggered XLM reward via the configured Stellar Asset Contract.Admin only
link_paymentlink_payment(employer, student, cert_hash, amount)Employer pays a verified student directly; emits payment.Employer
get_certificateget_certificate(cert_hash)Read-only lookup of the certificate record.Public read (free simulation)
get_issuerget_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.

StatusMeaning
issuedInitial state after register_certificate binds the hash to the student wallet. Not yet verified.
verifiedSet by verify_certificate (approved issuer or admin). Unlocks payment via link_payment.
suspendedSet 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.
revokedSet by revoke_certificate. Terminal for payment: revoked credentials can no longer unlock payment (error 11).
expiredCredential 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.

CodeNameMeaning
1AlreadyInitializedinit called twice.
2NotInitializedAdmin/token not set yet.
3UnauthorizedCaller isn't allowed.
4AlreadyExistsDuplicate cert hash.
5NotFoundHash isn't registered.
6InvalidAmountAmount must be > 0.
7IssuerNotFoundIssuer hasn't registered on-chain.
8IssuerNotApprovedIssuer still needs admin approval.
9IssuerSuspendedIssuer has been suspended.
10InvalidStatusCredential is in the wrong lifecycle state for this action.
11CredentialRevokedCredential was revoked and can no longer unlock payment.
12CredentialExpiredCredential 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.

EventEmitted byPayload / decoded meaning
initinitContract bootstrapped.
iss_regregister_issuerIssuer registered.
iss_apprapprove_issuerIssuer approved.
iss_suspsuspend_issuerIssuer suspended.
cert_regregister_certificateCertificate registered; payload carries the certificate hash.
cert_ververify_certificateCertificate verified; payload carries the certificate hash.
cert_revrevoke_certificateCredential revoked; payload is the certificate hash. Not decoded by the app's activity feed — inspect it on stellar.expert.
cert_supsuspend_certificateCredential suspended; payload is the certificate hash. Not decoded by the app's activity feed — inspect it on stellar.expert.
rewardreward_studentStudent reward sent; payload carries the amount, formatted with the configured asset decimals and code.
paymentlink_paymentEmployer 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 invokeContractFunction operation with TransactionBuilder at BASE_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 sendTransaction and accepts PENDING or DUPLICATE as in-flight statuses, then polls the transaction result for up to 20 attempts at 1.2 s intervals.
  • Serializes arguments with nativeToScVal; addresses as address, strings as string, and certificate hashes as 32-byte values converted from hex.

Test coverage

The contract ships with twelve tests in contract/src/test.rs (t1t12). Together they cover the happy path, the issuer trust layer, revocation gating, event emission, and the escrow lifecycle.

TestWhat it proves
t1_happy_path_with_approved_issuerThe complete journey: init, register issuer, approve, register certificate, verify, pay.
t2_unapproved_issuer_cannot_issueA pending issuer cannot register certificates before admin approval.
t3_suspended_issuer_cannot_issueA suspended issuer is blocked from issuing at the contract level.
t4_wrong_approved_issuer_cannot_verifyAn approved issuer cannot verify a credential it did not issue — only the issuing organization or the admin can.
t5_revoked_credential_blocks_paymentA revoked credential can no longer unlock payment.
t6_issuer_events_emitIssuer registration and approval emit auditable events.
t7_opportunity_happy_pathThe escrow journey: create, fund, submit, approve, release.
t8_revoked_credential_blocks_opportunityA revoked credential cannot anchor a new paid-trial opportunity.
t9_refund_funded_opportunityAn employer can recover escrowed funds from a funded trial that never progressed.
t10_invalid_status_transitions_failOut-of-order escrow moves (release before approval, refund after approval) fail with typed errors.
t11_rejects_too_many_opportunity_milestonesMilestone counts are bounded at opportunity creation.
t12_employer_can_refund_submitted_opportunityThe 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.

FunctionSignatureDescriptionAuth required
create_opportunitycreate_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_crtEmployer
fund_opportunityfund_opportunity(employer, opp_id)Escrows the full amount into the contract; fails with AlreadyFunded (#14) if repeated; emits opp_fundEmployer (creator)
submit_milestonesubmit_milestone(candidate, opp_id)Candidate marks the next milestone as delivered; emits mile_subCandidate
approve_milestoneapprove_milestone(employer, opp_id)Employer approves the submitted milestone; emits mile_aprEmployer (creator)
release_paymentrelease_payment(employer, opp_id)Releases the approved milestone share from escrow to the candidate; emits pay_relEmployer (creator)
refund_opportunityrefund_opportunity(employer, opp_id)Returns remaining escrowed funds to the employer; emits pay_refEmployer (creator)
get_opportunityget_opportunity(opp_id)Read-only lookup of an opportunity recordNone (public read)

The extension adds five typed error codes on top of the credential-surface errors:

CodeNameMeaning
13OpportunityNotFoundNo opportunity exists for this id.
14AlreadyFundedThe opportunity has already been funded.
15InvalidMilestoneMilestone submission or approval is out of order for the current milestone count.
16InvalidOpportunityStatusThe action is not allowed in the opportunity's current status.
17PaymentLockedEscrowed 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.

Soroban contract reference — functions, errors, events | Stellaroid Earn