service_registry
gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/service_registry on gno.land mainnet
realmpipeline-applicationdiscoverylive on mainnet
Register a service under a name others can resolve.
Lets realms find each other by name instead of by hard-coded address, with each name held by the account that registered it.
In detail curated
Name -> package-path service directory with typed entries, two-step ownership transfer, deregistration and a 90-day name reservation period. GitHub-pipeline application #5, and the portfolio's on-chain discovery surface.
Use it
A realm holds state. Reading it is a free query; changing it is a transaction you sign yourself.
Read it — no key, no account, no GNOT
gnokey query vm/qeval \
--data 'gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/service_registry.ListServices()' \
--remote https://rpc.gno.land:443ListServices is read-only, so this is a query rather than a transaction: it costs nothing, signs nothing and needs no funded account. Every exported function is listed under the API below; swap the name and the arguments to read any of the others.
Change its state
Every state change is a transaction you sign with your own key — this site never asks for one and holds none. You do not have to hand-write the transaction: gnoweb generates the exact gnokey maketx call command for any of this realm's 6 crossing functions, with the argument names filled in.
The gno.land mainnet builder spends real GNOT. Start with the read above, and size -gas-fee from a simulation rather than from any example.
Read the known limitations further down this page first. They record what was not verified on chain, which is usually the part that matters.
Import it from another realm
import "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/service_registry"The path is the address: there is no package manager, nothing to add to a manifest and no version to pin, so the package you compile against is the one the chain runs. It differs per chain because the namespace in it is the deploying account's.
Deployments
gno.land mainnet gnoland-1
RenderedSourceCall builderState
chain-attested| Import path | gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/service_registry |
|---|---|
| Namespace | g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39 |
| Deployed at height | 336,712 |
| Deploy transaction | bd83a5558c3494f8273f0b37f764f7197d3db2c02902f9502a7a2345038118f6 look it up on the RPC |
| Deployer | g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39 |
| Gas used | 61,268,850 |
| Storage | not attested per package on this chain — see the limitations below |
| Files on chain | gnomod.toml service_registry.gno service_registry_test.gno |
| Deployed bytes | service_registry.gno — 19,781 bytes |
| sha256 | ead72c9ea14a6358a1d259177483e91a4f048f1a85050a803c3828d0531ee996 |
| Realm address | g1xgsqs4nnnhdp6zmdd6q87nhhrdtem4q0t6q9ww derived, never confirmed against the realm |
Do not take the hash above on trust. $download returns the bytes gnoland-1 is actually running; this command fetches them and prints their digest, which should equal the one in the table:
curl -sS 'https://gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/service_registry$download&file=service_registry.gno' | shasum -a 256Expected: ead72c9ea14a6358a1d259177483e91a4f048f1a85050a803c3828d0531ee996 — 19,781 bytes. Use curl: the gno.land edges answer Python's default user-agent with HTTP 403.
API
chain-derived 15 exported functions, 2 types.
Every function below deep-links to gnoweb's call builder, which generates a ready-to-run gnokey maketx call for it:
AcceptOwnershipCancelOwnershipTransferDeregisterGetPendingOwnerGetServiceListByTypeListServicesOwnerServiceCountRegisterServiceRenderResolveServiceCountTransferOwnershipTryResolveUpdateService
Overview
This package carries no package doc comment on chain, so there is nothing for vm/qdoc to return and gnoweb's $help Overview is empty. Deployed bytes are immutable, so this cannot be repaired in place — see [an unpublished record] §2.4.
Imports
chainchain/runtime/unsafegno.land/p/nt/markdown/sanitize/v0strconvstringstime
Constants and variables
const (
// MaxServices is a pure state bound, not an anti-squat defense —
// MaxServicesPerOwner is what makes monopolization expensive
// (pearl audit R1). Kept at 1000 rather than raised further so the
// linear scans over `names` (Deregister, ListServices, ListByType)
// stay bounded at a size one transaction can comfortably pay for.
MaxServices = 1000
// MaxServicesPerOwner caps how many names one address may hold at
// once. Enforced at registration and, for a handoff, at the moment
// the recipient CONSENTS (see AcceptOwnership).
MaxServicesPerOwner = 20
MaxNameLen = 64
MaxTypeLen = 32
MaxPkgPathLen = 128
MaxDescriptionLen = 500
MaxMetadataLen = 2000
pkgPathPrefix = "gno.land/"
// ReservationPeriod is how long a deregistered name stays reserved.
// Long enough for integrators to notice the deregistration; finite
// so tombstones cannot lock the namespace forever.
ReservationPeriod = int64(90 * 24 * 3600) // 90 days
// MaxRenderServices bounds the gnoweb table. Render is reachable by
// any viewer, so its cost lands on third parties rather than on
// whoever grew the state (pearl audit Y3). Complete data comes from
// the bounded queries named in the truncation notice.
MaxRenderServices = 25
// renderDescLen is the per-row description budget, applied to the
// RAW text before escaping so an escape sequence is never split.
renderDescLen = 60
)
Types
type Reservation
type Reservation struct {
Owner address
Registrant address
Expires time.Time
}
Reservation holds a deregistered name for its former owner AND its original registrant, and EXPIRES (re-audit 2026-09-02): an eternal reservation let an attacker cycle register/deregister to lock the whole namespace forever, and a hostile transferee could strand a name against its original registrant permanently.
type Service
type Service struct {
Name string
Owner address
Registrant address // ORIGINAL registrant; immutable through transfers
PkgPath string // the realm the service lives at — what integrators resolve
Description string
ServiceType string // e.g. "token", "dex", "oracle", "dao", "nft", "bridge"
Metadata string // freeform key=value pairs or JSON blob
}
Service represents a registered on-chain service.
| Exported field | Type | Doc |
|---|---|---|
Name | string | |
Owner | address | |
Registrant | address | ORIGINAL registrant; immutable through transfers |
PkgPath | string | the realm the service lives at — what integrators resolve |
Description | string | |
ServiceType | string | e.g. "token", "dex", "oracle", "dao", "nft", "bridge" |
Metadata | string | freeform key=value pairs or JSON blob |
Functions
AcceptOwnership
func AcceptOwnership(cur realm, name string)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asAcceptOwnership(cross(cur), ...).
AcceptOwnership completes a nominated handoff. Only the nominee can accept, and the nominee's own quota is checked HERE — at consent — so a nomination can never push an account past MaxServicesPerOwner without that account agreeing to it.
CancelOwnershipTransfer
func CancelOwnershipTransfer(cur realm, name string)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asCancelOwnershipTransfer(cross(cur), ...).
CancelOwnershipTransfer withdraws a pending nomination. Owner-only.
Deregister
func Deregister(cur realm, name string)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asDeregister(cross(cur), ...).
Deregister removes a service from the registry. Only the owner can deregister. The name stays reserved for the former owner and the original registrant for ReservationPeriod — it cannot be re-registered by anyone else in that window, so integrators who still resolve it can never be silently redirected by a squatter.
GetPendingOwner
func GetPendingOwner(name string) string
GetPendingOwner returns the nominated-but-not-yet-accepted owner of a service, or "none" when no handoff is open.
GetService
func GetService(name string) string
GetService returns a formatted summary of a registered service. Free text is escaped for a single-line markdown slot.
ListByType
func ListByType(serviceType string) string
ListByType returns all service names matching a given type. Same cost note as ListServices.
ListServices
func ListServices() string
ListServices returns all registered service names as a comma-separated string in registration order.
COST NOTE: this is O(MaxServices) and is deliberately NOT truncated — an integrator enumerating the registry needs the complete set, and the caller pays for its own read. Render, whose cost lands on third-party viewers instead, IS bounded.
OwnerServiceCount
func OwnerServiceCount(owner address) (count, limit int)
OwnerServiceCount returns how many services an address currently holds and the per-owner cap.
RegisterService
func RegisterService(cur realm, name, pkgPath, description, serviceType, metadata string)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asRegisterService(cross(cur), ...).
RegisterService adds a new service to the registry. The caller becomes the owner. Name must be unique, lowercase alphanumeric/underscores. pkgPath is the realm the service lives at — the field integrators resolve — and must look like a gno.land package path. A name that was deregistered stays reserved for its former owner and its original registrant for ReservationPeriod.
NOTE: this realm does NOT and cannot verify that pkgPath exists or that the caller controls it. See the INTEGRATOR CONTRACT on Resolve.
Render
func Render(path string) string
Render returns a markdown overview, bounded to MaxRenderServices rows (pearl audit Y3). Never panics. All free text goes through the ecosystem sanitizer rather than a bespoke escaper: the hand-rolled replacement of backticks and pipes it replaces left `[`, `]`, `(`, `)` and `!` live, so any registrant could inject a working markdown link or image into a table cell and phish every viewer of this page.
Resolve
func Resolve(name string) string
Resolve returns the pkgpath a service name points to — the primary integration query. Panics on unknown names so a consumer can never silently integrate against a missing entry.
INTEGRATOR CONTRACT — read this before trusting a resolution:
- A resolution is an ATTESTATION, NOT A PROOF. This realm records that some address claimed a name for some package path. It does NOT verify that the path exists, that it is deployed, or that the registrant controls it. Contrast r/demo/defi/grc20reg, which proves control by requiring the registered token object to originate from the calling realm; no equivalent proof exists for a bare path string, and requiring one would mean only realms — never their operators — could ever register a name, which is not this registry's model.
- A NAME IS NOT AN AUTHORIZATION. Never grant a privilege, route a payment, or admit a caller because Resolve returned its path. Resolution answers "where does this name point", never "may this caller act". Derive authority from your own crossing entrypoint's cur.Previous(), or from an explicit access-control realm.
- THE TARGET CAN CHANGE. The owner may repoint a name at any time via UpdateService, and ownership itself is transferable. Treat a resolution as valid only for the transaction that read it; cache it and you inherit whatever the name points at later. The ServiceUpdated and OwnershipTransferred events exist so movement is detectable.
ServiceCount
func ServiceCount() (count, limit int)
ServiceCount returns how many services are registered and the global cap, so a caller can check headroom without pulling the whole list.
TransferOwnership
func TransferOwnership(cur realm, name string, newOwner address)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asTransferOwnership(cross(cur), ...).
TransferOwnership NOMINATES a new owner for a service entry; the nominee must call AcceptOwnership to take control (pearl audit Y4).
The one-step form this replaces was a permanent-brick hazard: address.IsValid() only checks bech32 form, so a well-formed but unowned destination passed the check and committed immediately, after which the entry could never again be updated, transferred or deregistered — and because it could never be deregistered it could never enter the reservation window either, so the NAME became a permanent hole in a shared global namespace.
Nomination changes nothing: the sitting owner keeps full control until the nominee consents. Passing "" clears a pending nomination.
TryResolve
func TryResolve(name string) (string, bool)
TryResolve is the non-panicking variant for consumers that need to degrade gracefully when a name disappears (re-audit: a panicking-only read path bricks any consumer realm that calls it inline). The INTEGRATOR CONTRACT documented on Resolve applies here identically.
UpdateService
func UpdateService(cur realm, name, pkgPath, description, serviceType, metadata string)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asUpdateService(cross(cur), ...).
UpdateService modifies a service's pkgpath, description, type, and metadata. Only the registered owner can update. Name cannot change.
An update MAY REPOINT the name at a different package path. That is a deliberate capability (services move, and versions supersede), but it also means a name an integrator trusts today can point elsewhere tomorrow. The ServiceUpdated event carries both the old and the new path specifically so a repoint is observable in the transaction log rather than something a consumer has to poll for.
Doc text is reproduced as vm/qdoc returns it. The node markdown-escapes doc comments, so a bracket or angle bracket may carry a backslash the committed source does not have. The source itself is at source and in this repository.
Dependencies
chain-attested| Imports | chain, chain/runtime/unsafe, gno.land/p/nt/markdown/sanitize/v0, strconv, strings, time |
|---|---|
| First-party dependencies | none |
Known limitations
Recorded by the people who built and deployed it. This list is deliberately not empty where honesty costs something.
curated- No state-changing call was made against this realm on gnoland-1. The live evidence is byte custody plus a successful vm/qrender; every state transition rests on the committed test suite and the pearl-1 audit, not on mainnet behaviour ([an unpublished record], Live verification).
- storage_bytes and storage_deposit are null because gnoland-1 emits no /tm.StorageDepositEvent ON ADDPKG -- a scope that is load-bearing, because it DOES emit one on MsgCall (measured 2026-10-17, AL62); see the chain's storage_deposit_events field. The deposit actually charged was 2,867,500 ugnot, measured from the signer's balance and recorded in [an unpublished record], Cost; it is not chain-attested per package by this manifest.
- The deployed file set includes service_registry_test.gno. The pearl-1 deployment of the same realm carried only the source and gnomod.toml, so the mainnet package is three files where pearl's is two -- the test file is stored on chain and paid for in the deposit. It declares no crossing function and so adds no callable surface, but it is part of the immutable package.
- MaxServices 1000 and MaxServicesPerOwner 20 are fixed on an immutable realm.
- Sybil saturation residual beyond the resolved R1 quota: ~50 cheap keys x 20 fill MaxServices=1000 permanently (Deregister is owner-only, freed names reservation-locked 90 days). The source concedes the cap is a state bound, not an anti-squat defense. Recorded so the 'R1 resolved' line is not over-read. Audit 2026-11-04, disclosed per the operator's disclose-and-queue-successor rule; details and severity in [an unpublished record].
Source
| Source file | mainnet/r/service_registry/service_registry.gno at commit c2e10d17ffbf in the project repository (not public — the digest command above is the check that needs no repository) |
|---|---|
| Matches the deployed bytes | yes — byte-identical |