Skip to main content

Service layer

Pages never talk to contracts, RPC endpoints or server routes directly. They call services: small classes in src/services/ that each own one concern (credentials, status, documents, enrolment and so on). All services are built in one place, the composition root createServices(config) in src/services/index.ts, and handed to React through ServicesProvider. This keeps the wiring in one file, lets tests replace any dependency with a fake, and means a page cannot accidentally read through the wrong network or the wrong contract set.

Layers​

LayerFolderJob
Walletsrc/services/wallet/Talks to the injected EIP-1193 wallet: accounts, signatures, transactions, chain switching. See Wallet and network.
Gatewayssrc/services/contracts/, src/services/credentials/Turn addresses and ABIs into ethers Contract objects. A gateway offers a reader (public RPC, no wallet) and a writer (wallet signer, after a chain check).
Indexessrc/services/indexing/Rebuild lists from contract events with bounded log scans and caches. See Reading the chain.
Domain servicessrc/services/domain/The operations pages use: issue, check, revoke, enrol, share, disclose, export, prove access.
Routers and fan-outssrc/services/domain/Send a call to the contract set that holds a credential, or run a read in every set. See Contract-set routing.
React gluesrc/services/react/ServicesProvider, useServices, useWalletSession, usePlatformPaused.
Server clientssrc/services/upload.ts, src/services/document-api.tsBrowser side of sign-in, upload, document lookup and removal. See Upload and documents.

The composition root​

createServices receives a ServicesConfig:

FieldTypeMeaning
getEthereum() => WalletEthereum or nullReturns the injected wallet. The browser passes window.ethereum; tests pass a fake or null.
addressBookAddressBookacademicRecords (v0.2 address), chainId (from NEXT_PUBLIC_NETWORK_ID, null if not a number), rpcUrls (public endpoints), startBlock (from NEXT_PUBLIC_START_BLOCK). Built by loadAddressBook() in src/services/contracts/address-book.ts.
contractFactoryoptionalReplaces new Contract(...) for the v0.2 gateway in tests.
documentGatewaysoptional string[]IPFS gateways for reading documents, best first. The browser passes gatewayList(NEXT_PUBLIC_PINATA_GATEWAY_URL): the configured gateway first, then https://ipfs.io and https://dweb.link.

createBrowserServices() in src/services/react/browser-services.ts fills these from the browser and the NEXT_PUBLIC_* variables.

Inside, the root builds, in order:

  1. One WalletService.
  2. A chainGuard that throws ChainConfigError when no chain id is configured and otherwise calls assertCorrectChain(wallet.network(), chainId). Every writer runs it before sending a transaction.
  3. Three gateways, all with createDefaultReadRunner as their read runner (a failover RPC provider with an 8 second per-request timeout):
    • createContractGateway for the v0.2 AcademicRecords contract.
    • createCredentialGateway for the v0.3 registries. Addresses always come from the bundled deployment manifest (ADR-0040), never from an environment variable.
    • createAccessGateway for the zero-knowledge access gate, from src/contracts/zk/deployment.<chainId>.json.
  4. The per-set services of the active contract set, then the routers and fan-outs that reach the other sets.
  5. The v0.2 services, the chain-wide v0.3 services, the document service and the access services.

Per-set services​

A credential lives in exactly one contract set, and every check of it must use that set's registries. createSetServices(gateway, wallet) builds one bundle of services on a gateway scoped to a single set. rememberPerGateway caches the bundle in a WeakMap keyed by the scoped gateway, so asking twice for the same set returns the same objects and their caches.

Field of SetServicesClassBuilt from
credentialsCredentialServicegateway, wallet
statusStatusServicegateway
recordTypesRecordTypeServicegateway, index
disclosureDisclosureServicegateway, credentials, wallet as identity
vcVcServicegateway, credentials, and status list support from createVcLists
presentationPresentationServicegateway, credentials, disclosure
issuedIssuedCredentialsServiceindex, status, recordTypes
inboxInboxServicegateway, index, status, recordTypes

createVcLists returns status list support only when both NEXT_PUBLIC_STATUS_LIST_BASE_URL and NEXT_PUBLIC_STATUS_LIST_PUBLISHER are set. It builds a StatusListChecker that recomputes a credential's list position from the issuer's issuance order, pins the publisher address, and counts an issuance as stable only after 64 confirmations (STATUS_LIST_CONFIRMATIONS). Without both variables, exported VCs carry the chain status alone. See Status lists.

The Services object​

These are the fields pages read through useServices().

FieldClass (file in src/services/domain/ unless noted)Depends onWhat pages use it forVersion
walletWalletService (src/services/wallet/wallet-service.ts)injected walletConnect, address, sign, switch chain, change eventsboth
rolesRoleService (role-service.ts)v0.2 gatewayhasRole, isRegisteredUniversityv0.2
universitiesUniversityService (university-service.ts)v0.2 gatewayAdd, remove, rename universitiesv0.2
studentsStudentService (student-service.ts)v0.2 gatewayRegister students, migrate walletsv0.2
recordsRecordService (record-service.ts)v0.2 gatewayAdd and read records, verify a recordv0.2
sharingSharingService (sharing-service.ts)v0.2 gateway, LogShareIndexShare and unshare, "shared with me" listsv0.2
governanceGovernanceService (governance-service.ts)v0.2 gatewayAdmins, custom types, pausev0.2
credentialsCredentialService (credential-service.ts)active setIssue, read, check a document or package, signatures, issuer lineagev0.3
locatorCredentialLocator (credential-locator.ts)credential gateway, per-set servicesFind the set that holds a credential idv0.3
anchorscreateRoutedCredentials (routed-credentials.ts)locatorgetCredential from whichever set holds itv0.3
documentsDocumentService (document-service.ts)credential gateway, wallet, anchors, document API, IPFS gatewaysEncrypt and upload, open as issuer or holder, grants, capability links, key replacement, removal of old copiesv0.3
statusStatusService (status-service.ts)active setSuspend, reinstate, revoke, supersedev0.3
enrolmentEnrolmentService (enrolment-service.ts)credential gateway, walletInvitations, redeeming, bindingsv0.3
inboxAllSetsInbox (all-sets-lists.ts)SetFanOut"My credentials" across every setv0.3
issuedAllSetsIssued (all-sets-lists.ts)SetFanOutAn issuer's credentials across every setv0.3
recordTypesRecordTypeService (record-type-service.ts)active setRegister, list, resolve record typesv0.3
issuerAccessIssuerAccessService (issuer-access-service.ts)version, roles, credential gatewayRole checks for navigation and page gatesboth
platformPlatformGovernanceService (platform-governance-service.ts)credential gateway, its own LogQueryIndexPause state, admin snapshot, timelock view and queue, retirementsv0.3
disclosureDisclosureRouter (package-routers.ts)active and routed DisclosureServiceDisclosure trees and disclosure filesv0.3
presentationPresentationRouter (presentation-router.ts)active and routed PresentationServicePhone codes and challengesv0.3
vcVcRouter (package-routers.ts)active and routed VcServiceVC and Open Badge checks and export optionsv0.3
badgesBadgeService (badge-service.ts)credential gateway, walletStandard eddsa-rdfc-2022 proof for Open Badgesv0.3
accessAccessService (access-service.ts)access gatewayAdmit, remove, read receipts, submit proofsv0.3
requesterAccessRequesterService (access-requester-service.ts)wallet, credential gateway, access, worker proverMake an access identity and a proof in a Web Worker, loading circuit files from /zkv0.3

The v0.2 services exist on a v0.3 build too (they are cheap to construct), but v0.3 pages do not call them, and the reverse holds on a v0.2 build.

Domain service methods​

ServiceMethods
CredentialServiceaccountNonce, address, checkDocument, checkPackage, checkSignature, getCredential, isAvailable, issue, issuerLineage, signTypedData, supportsRevisions
StatusServicegetStatus, suspend, reinstate, revoke, supersede, supersedeToRegistry
EnrolmentServiceaddress, createInvitation, signTypedData, redeem, accept, enrol, holderOf, myEnrolment, rebind, unbind, revokeInvitations
DocumentServiceencryptForUpload, upload, openAsIssuer, openFromPackage, openGrant, openCapability, myShareCode, grantAccess, capabilityLink, rotate, removeOldCopies
RecordTypeServiceissuable, register, resolve, setActive, typesOf
DisclosureServiceaddress, attest, attestPackage, createDisclosure, isFullyVerified, newChallenge, signTypedData, verify
VcServicecheckFile, isFullyVerified, listOptions, verify, verifyBadge
PresentationServicecheckStatic, checkDisclosureCode, isAccepted
InboxServicemyCredentials (from the wallet's enrolment at each issuer)
IssuedCredentialsServiceissuedBy, issuedIds
PlatformGovernanceServiceisPaused, pause, queue, retirements, snapshot, timelockView
IssuerAccessServiceaccessOf, issuerName, isIssuer, retiredTo
AccessServiceadmit, isAvailable, overview, prepareProof, read, receipts, remove, servedRegistry, submitProof
AccessRequesterServiceidentityFromSeeds, newIdentity, prove, walletSeeds
BadgeServiceaddress, chainId, exportStandard, signTypedData
RecordService (v0.2)addRecord, getPublicRecord, getRecordForViewer, getStudentRecords, getStudentRecordsByAddress, getTotalRecords, getUniversityRecords, recordAccess, verifyRecord
SharingService (v0.2)shareRecord, unshareRecord, getSharedRecords, isRecordSharedWith, sharedWithMe, sharesOf
GovernanceService (v0.2)addAdmin, removeAdmin, getAllAdmins, addCustomRecordType, getTotalCustomTypes, listCustomTypes, isPaused, pauseContract, unpauseContract
UniversityService (v0.2)addUniversity, removeUniversity, getAllUniversities, getUniversityName, setUniversityName
StudentService (v0.2)getStudentId, registerStudent, migrateStudentWallet
RoleService (v0.2)hasRole, isRegisteredUniversity

The flows that use these methods are described in Flows; the methods' contract calls are in Smart contracts.

Gateways in more detail​

v0.2 contract gateway​

createContractGateway (src/services/contracts/contract-gateway.ts) offers:

  • reader(): an AcademicRecords contract on the public RPC when the address, RPC endpoints and chain id are all configured. Otherwise it falls back to the wallet's signer, and with no wallet it throws ChainConfigError.
  • connected(): the contract bound to the current wallet signer, rebuilt when the signer changes.
  • writer(): connected() after chainGuard().
  • studentRegistry(): the StudentManagement contract at the address AcademicRecords reports.

src/services/contracts/tx.ts holds sendAndWait(pending, onSent), which reports the transaction once it is sent and waits for the receipt, and readEventArg, which reads one argument of an event from a receipt.

v0.3 credential gateway​

createCredentialGateway (src/services/credentials/credential-gateway.ts) reads the bundled manifest for the configured chain (src/contracts/v03/manifest.ts). Its main members:

MemberWhat it does
isDeployed()True when a manifest exists for the chain
sets()Every listed set as { label, active, registry }
forRegistry(address)A gateway scoped to the set that owns that credential registry, cached; null for an unlisted address
registry(), deployment()Address, chain id, deployment block and all contract addresses of the set
reader(), writer(), statusReader(), statusWriter(), issuerReader(), issuerWriter(), holderReader(), holderWriter(), recordTypeReader(), recordTypeWriter()Contracts of the set; readers use the public RPC, writers the wallet after chainGuard
timelockReader()The chain-wide timelock from the manifest, or null
logSource()Event logs over the public RPC, for the indexes
accountNonce()The issuer's next signing nonce
isContractAccount(address)True when the address has code; an EIP-7702 delegation designator (0xef0100 followed by an address) counts as not a contract
isValidContractSignature(issuer, hash, signature)ERC-1271 check (magic value 0x1626ba7e); a call exception or bad data counts as false

A missing manifest raises CredentialRegistryNotDeployedError. Public reads need NEXT_PUBLIC_RPC_URLS or NEXT_PUBLIC_RPC_URL; without them the gateway throws "Public reads need NEXT_PUBLIC_RPC_URL or NEXT_PUBLIC_RPC_URLS; a wallet-backed read would follow whatever chain the wallet is on". It never falls back to the wallet for reads, because a wallet on another chain would answer for that chain.

Access gateway​

createAccessGateway (src/services/credentials/access-gateway.ts) reads the access gate and Semaphore addresses from src/contracts/zk/deployment.<chainId>.json. AccessService.isAvailable() is false when no file exists for the chain, which is how /access decides to show Not on this network yet.

Calling the server​

The document service is the only service that calls Certichain's own server. createDocumentService gives it three functions from src/services/document-api.ts (uploadContainer, lookupDocumentCids, removeOtherCopies) and a sign-in context of window.location.host, window.location.origin and the wallet's chain id. Each call goes through withSignIn (src/services/upload.ts): when the server answers 401 or 403, the browser signs in once with the wallet and repeats the request. See Sign-in and sessions.

Optional pieces​

PieceBuilt whenOtherwise
v0.2 share history (LogShareIndex)RPC endpoints, chain id, NEXT_PUBLIC_CONTRACT_ADDRESS and NEXT_PUBLIC_START_BLOCK are all setSharingService throws ShareHistoryNotConfiguredError for history lists, and the pages say so
VC status list supportNEXT_PUBLIC_STATUS_LIST_BASE_URL and NEXT_PUBLIC_STATUS_LIST_PUBLISHER are setVC exports carry the chain status only
Access servicesA zk manifest exists for the chain/access and the access panels say the gate is not on this network

Conventions​

  • One composition root. No other file constructs a service with its dependencies. Pages ask for services; they do not build them.
  • Errors are thrown, not hidden. A failed chain read throws. IssuerAccessService never turns a failed read into "no role", and SetFanOut fails the whole list if one set cannot be read.
  • Reads and writes are separate. Readers use the public RPC and work without a wallet. Writers need the wallet and always pass chainGuard first.
  • Test seam. ServicesProvider accepts a services prop, and createServices accepts a fake wallet getter, a fake contract factory and test gateways, so component tests run without a chain.

Source files​

  • src/services/index.ts
  • src/services/react/services-provider.tsx, src/services/react/browser-services.ts
  • src/services/contracts/address-book.ts, src/services/contracts/contract-gateway.ts, src/services/contracts/tx.ts
  • src/services/credentials/credential-gateway.ts, src/services/credentials/access-gateway.ts
  • src/services/domain/*.ts
  • src/services/upload.ts, src/services/document-api.ts
  • src/contracts/v03/manifest.ts

Tests​

  • src/services/index.test.ts, src/services/react/services-provider.test.tsx
  • src/services/contracts/address-book.test.ts, src/services/contracts/contract-gateway.test.ts, src/services/contracts/tx.test.ts
  • src/services/credentials/credential-gateway.test.ts, src/services/credentials/access-gateway.test.ts
  • One test file per domain service, for example src/services/domain/credential-service.test.ts, src/services/domain/status-service.test.ts, src/services/domain/document-service.test.ts, src/services/domain/issuer-access-service.test.ts
  • src/services/upload.test.ts, src/services/document-api.test.ts