PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/service_market

Realm
Open in gnoweb ↗

Overview

Kind
Realm (renderable)
Name
service_market
Namespace
g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39
Files
3 (gnomod.toml)
Exported functions
39
Module
gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/service_market
gno
0.9

Files (3)

  • gnomod.tomltoml
  • service_market.gnogno
  • service_market_test.gnogno
service_market.gnogno
1// Realm service_market is a custodial GNOT marketplace for SERVICES:2// providers publish a standing offer (title, description, price,3// delivery window), a customer purchases it by paying the exact price4// into escrow, the provider marks the work delivered, and the escrow is5// released to the provider only once the customer accepts — or once the6// acceptance window lapses.7//8// COMPOSITION (per the recorded DISCOVERY / REUSE ANALYSIS in9// DISCOVERY.md): balance accounting is feeledger, coin movement is10// coinio, free-text render safety is p/nt/markdown/sanitize/v0. This11// realm owns only the two-level offer/order state machine, which the12// discovery gate found nowhere else: every escrow realm in the searched13// sources escrows FUNDER-FIRST (the party who will later decide escrows14// first, then solicits work), while a service marketplace must escrow15// CUSTOMER-FIRST against a standing offer.16//17// LIFECYCLE. A service is a reusable offer; an order is one purchase of18// it. Order terminal states are frozen and reached exactly once.19//20//	RegisterService (anyone, no coins) : status Active. The fee bps is21//	                                     SNAPSHOTTED under the22//	                                     provider's own maxFeeBps23//	                                     ceiling.24//	RetireService (provider only)      : Active -> Retired. Blocks NEW25//	                                     orders only; orders already in26//	                                     flight are untouched.27//	PurchaseService (EOA + -send)      : order Purchased, amount into28//	                                     escrow. Sets the delivery29//	                                     deadline.30//	MarkDelivered (provider only)      : Purchased -> Delivered. Starts31//	                                     the acceptance window.32//	AcceptDelivery (customer only)     : Delivered -> Released. THE33//	                                     RESOLUTION. Escrow becomes the34//	                                     provider's claimable balance35//	                                     minus the snapshotted fee.36//	ReleaseTimeout (anyone)            : Delivered -> Released after37//	                                     AcceptanceBlocks. Deemed38//	                                     acceptance.39//	DeclineOrder (provider only)       : Purchased -> Refunded, fee-free.40//	ClaimRefund (anyone)               : Purchased -> Refunded after the41//	                                     delivery deadline, fee-free.42//	Claim / ClaimAll (anyone)          : pays out the caller's own43//	                                     claimable balance.44//45// EVERY VALUE EXIT IS A PULL. Release and refund only move numbers46// between the escrow pool and a claimable balance; the beneficiary47// later calls Claim. Nothing in this realm ever pushes coins to a third48// party, which is what lets both timeout valves stay permissionless: a49// stranger triggering ReleaseTimeout or ClaimRefund performs no send, so50// a beneficiary that cannot receive a send can never brick the valve.51//52// THE THREE REQUIRED PROTECTIONS.53//54// Unauthorized settlement: every identity derives from the crossing55// entrypoint's cur.Previous().Address(); no function takes a caller56// identity as a parameter (the designation-forgery shape that57// disqualified most of the escrow realms found in discovery). The payee58// is COPIED INTO THE ORDER at purchase, so neither retiring the service59// nor any later edit can redirect an in-flight order's payment. There is60// no admin path to any order's escrow: the admin sets the fee for FUTURE61// registrations and nothing else.62//63// Double payment: an order's status is checked and driven terminal in64// the same transaction that moves its value, and escrowTotal is65// decremented in lockstep with the credit. Release and refund both66// require a non-terminal status, so at most one of them can ever67// succeed for a given order, and neither can succeed twice.68//69// Stuck funds: both counterparties have a permissionless valve against70// the other's inaction. A provider who never delivers loses the escrow71// back to the customer at the delivery deadline (ClaimRefund); a72// customer who never accepts loses it to the provider at the acceptance73// deadline (ReleaseTimeout). Neither valve trusts its caller.74//75// ACCEPTANCE IS A PROTOCOL CONSTANT, NOT A PROVIDER SETTING. The76// provider chooses the delivery window (their own commitment, and their77// own risk), but AcceptanceBlocks is fixed. Were it provider-chosen, a78// provider would set it to zero, mark work delivered, and auto-release79// in the same block — settlement without resolution, wearing the80// costume of a timeout.81//82// MONETARY INVARIANT (conservation). With H = ugnot held at the realm83// address, E = escrowTotal (Σ amount over Purchased and Delivered84// orders), U = claimable balances, F = the fee pot, S >= 0 out-of-band85// surplus:86//87//	H == E + U + F + S88//89// PurchaseService raises H and E by exactly the price. Release moves90// amount from E to U+F (feeledger guarantees credited + fee == amount);91// refund moves amount from E to U at zero fee. Claim*/WithdrawFees debit92// the ledger before coinio.Payout moves the identical amount out. Any93// panic aborts the whole transaction; this realm never issues or removes94// coins. Surplus is recoverable only via SweepDenom (fee recipient),95// which reserves Liabilities() = E + U + F.96//97// LIMITATION — NO DISPUTE ARBITRATION, DELIBERATELY. This realm resolves98// on acceptance or on the acceptance timeout. It does NOT adjudicate99// whether delivered work was good. A customer who considers the work100// inadequate has no lever here beyond declining to accept, and the101// timeout will still pay the provider. That is a real limitation and it102// is the deliberate price of the property above: an arbiter empowered to103// redirect escrow is a party who can seize funds, and the closest realm104// found in discovery (r/samcrew/escrow_v3) carries exactly that shape —105// a single hardcoded admin key, with no rotation function, that is106// simultaneously sole arbiter, a unilateral release path, and a107// permanent pause switch over every fund path. Adding arbitration here108// would change what this application IS and expand its security model,109// so it is refused rather than smuggled in. A deployment that needs110// adjudicated disputes needs that designed, audited and authorized as111// its own application.112//113// REALM-CALLER CAVEAT: assertNoSend reads the ORIGIN transaction's send114// envelope, not this realm's receipt, so a realm caller is refused by115// every non-payable function whenever the SAME transaction attached a116// -send anywhere — even though this realm received nothing. It fails117// closed, it is not third-party triggerable, and the workaround is to118// isolate the call in its own -send-free transaction.119//120// Named concretely, because the generic phrasing understates where it121// lands. It applies to every exported function except PurchaseService,122// the only payable one, but four of them carry the weight:123// MarkDelivered, without which no order against a realm provider can124// ever reach a release; ReleaseTimeout and ClaimRefund, the two valves125// this design deliberately leaves permissionless; and Claim / ClaimAll,126// the only paths that extract a credited balance. A realm-based keeper127// bot that batches a valve call into a transaction carrying coins for128// some other purpose is therefore silently unusable, and a realm129// provider must budget a dedicated transaction both to deliver and to130// collect.131// Requirement 3 (no stuck funds) survives this: both valves are132// permissionless and any EOA can pull them, so no order depends on a133// realm caller to unstick.134package service_market135136import (137	"chain"138	"chain/runtime"139	"chain/runtime/unsafe"140	"strconv"141142	"gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/coinio"143	"gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/feeledger"144	"gno.land/p/nt/avl/v0"145	"gno.land/p/nt/markdown/sanitize/v0"146)147148// Denom is the only asset this realm accepts.149const Denom = "ugnot"150151// MaxFeeBps is the hard protocol-fee cap: 1000 bps = 10%.152const MaxFeeBps = int64(1000)153154// AcceptanceBlocks is how long a customer has to accept a delivery155// before anyone may release it to the provider. It is a PROTOCOL156// constant on purpose — see the header. ~7 days at pearl's observed157// ~4.2s blocks.158const AcceptanceBlocks = int64(140000)159160// Service status values.161const (162	ServiceActive  = "active"163	ServiceRetired = "retired"164)165166// Order status values. Released and Refunded are terminal.167const (168	OrderPurchased = "purchased"169	OrderDelivered = "delivered"170	OrderReleased  = "released"171	OrderRefunded  = "refunded"172)173174// Input bounds.175const (176	MaxTitleLen = 80177	MaxDescLen  = 2000178	MaxURILen   = 400179	MinPrice    = int64(1)180181	// MinDeliveryBlocks/MaxDeliveryBlocks bound the provider's own182	// turnaround commitment: ~1 hour to ~60 days.183	MinDeliveryBlocks = int64(850)184	MaxDeliveryBlocks = int64(1200000)185186	// MaxServicesPerProvider bounds catalog monopolization by a single187	// address. The same finding was RED in both permission_registry and188	// service_registry; it is cheaper to carry the counter from the189	// start than to discover it in an audit.190	MaxServicesPerProvider = 20191192	// RenderLimit bounds the home page. An unbounded Render was YELLOW193	// in three prior audits.194	RenderLimit = 20195)196197type service struct {198	id             int64199	provider       address200	title          string201	description    string202	price          int64203	deliveryBlocks int64204	feeBps         int64 // snapshotted at registration, charged at release205	status         string206	orders         int64 // lifetime count, never decremented207}208209type order struct {210	id        int64211	serviceID int64212	customer  address213	provider  address // copied at purchase; never re-read from the service214	amount    int64215	feeBps    int64 // copied at purchase from the service's snapshot216	status    string217218	deliveryDeadline   int64 // absolute height; meaningful while Purchased219	acceptanceDeadline int64 // absolute height; set at MarkDelivered220	deliveryURI        string221}222223var (224	admin        address // may set fee, fee recipient, stage successor225	pendingAdmin address // staged by TransferAdmin, completes via AcceptAdmin226	feeRecipient address // may withdraw fees and sweep surplus227	feeBps       int64   // fee snapshotted into NEW services228229	self        address // this realm's address, captured at deploy230	nextService int64231	nextOrder   int64232	escrowTotal int64 // == Σ amount over Purchased and Delivered orders233234	services    = avl.NewTree() // padID(id)   -> *service235	orders      = avl.NewTree() // padID(id)   -> *order236	providerNum = avl.NewTree() // address     -> *int64, services per provider237	ledger      = feeledger.MustNew(MaxFeeBps)238)239240func init() {241	admin = unsafe.OriginCaller()242	feeRecipient = admin243	self = unsafe.CurrentRealm().Address()244}245246// --- provider side ---247248// RegisterService publishes a standing offer and returns its id. No249// coins are accepted; the storage deposit the caller pays is the250// anti-spam. The current protocol fee is snapshotted into the service251// and must not exceed maxFeeBps, the ceiling the provider signed for —252// pass MaxFeeBps to accept any legal fee.253//254// Providers may be EOAs or realms, but a realm provider takes on three255// obligations that this realm cannot check for it. Realm bytes are256// immutable after addpkg, so a realm deployed without them cannot257// acquire them later: failing (1) means it never earns, failing (2) or258// (3) means what it earned is permanently stranded.259//260//  1. It must already expose a crossing entrypoint that calls261//     MarkDelivered. That is the only transition to OrderDelivered, and262//     both release paths require it, so a provider realm that cannot263//     call it never earns a credit at all — every order against it runs264//     to ClaimRefund instead. This obligation binds before the next one.265//  2. It must already expose a crossing entrypoint that calls Claim or266//     ClaimAll on this realm. A credited balance is a pull, never a267//     push; nothing here can reach into a provider realm to deliver it.268//  3. It must make both calls in transactions whose ORIGIN carried no269//     -send, per the REALM-CALLER CAVEAT in the package header.270//271// The customer side is not symmetric: PurchaseService is payable and272// routes through coinio.Receive, which requires an EOA calling directly273// via MsgCall. A realm can therefore sell a service here but cannot buy274// one — and "can sell" is exactly as strong as the three obligations275// above, no stronger. A customer is structurally always an EOA, which276// is why the permissionless valves are always pullable.277func RegisterService(cur realm, title, description string, price, deliveryBlocks, maxFeeBps int64) int64 {278	assertNoSend()279	provider := cur.Previous().Address()280281	assertValidTitle(title)282	if len(description) > MaxDescLen {283		panic("description must be at most " + strconv.Itoa(MaxDescLen) + " characters")284	}285	if price < MinPrice {286		panic("price must be at least " + itoa(MinPrice) + Denom)287	}288	if deliveryBlocks < MinDeliveryBlocks || deliveryBlocks > MaxDeliveryBlocks {289		panic("deliveryBlocks must be " + itoa(MinDeliveryBlocks) + "-" + itoa(MaxDeliveryBlocks))290	}291	if maxFeeBps < 0 || maxFeeBps > MaxFeeBps {292		panic("maxFeeBps must be 0-" + itoa(MaxFeeBps))293	}294	if feeBps > maxFeeBps {295		panic("current fee " + itoa(feeBps) + " bps exceeds your ceiling of " + itoa(maxFeeBps) + " bps")296	}297	if countFor(provider) >= MaxServicesPerProvider {298		panic("at most " + strconv.Itoa(MaxServicesPerProvider) + " services per provider")299	}300301	id := nextService302	nextService++303	services.Set(padID(id), &service{304		id:             id,305		provider:       provider,306		title:          title,307		description:    description,308		price:          price,309		deliveryBlocks: deliveryBlocks,310		feeBps:         feeBps,311		status:         ServiceActive,312	})313	bumpFor(provider, 1)314315	chain.Emit("ServiceRegistered",316		"id", itoa(id),317		"provider", provider.String(),318		"price", itoa(price),319		"feeBps", itoa(feeBps),320	)321	return id322}323324// RetireService stops a service accepting NEW orders. Orders already in325// flight keep their own copies of provider, amount and fee, and run to326// their own terminal states unaffected. Provider only, and terminal —327// there is no un-retire, so a customer reading "active" cannot have it328// flicker underneath them.329func RetireService(cur realm, id int64) {330	assertNoSend()331	caller := cur.Previous().Address()332	s := mustGetService(id)333	if caller != s.provider {334		panic("only the provider may retire this service")335	}336	if s.status != ServiceActive {337		panic("service is not active")338	}339	s.status = ServiceRetired340	bumpFor(s.provider, -1)341342	chain.Emit("ServiceRetired", "id", itoa(id), "provider", s.provider.String())343}344345// --- customer side ---346347// PurchaseService escrows EXACTLY the listed price and opens an order.348// The caller must be an EOA (coinio.Receive is the receipt-guaranteed349// shape) and must attach exactly the price in ugnot; any mismatch aborts350// and the coins revert with the transaction. Self-purchase is rejected.351func PurchaseService(cur realm, serviceID int64) int64 {352	customer, amount := coinio.Receive(0, cur, Denom)353	s := mustGetService(serviceID)354	if s.status != ServiceActive {355		panic("service is not active")356	}357	if customer == s.provider {358		panic("the provider cannot purchase their own service")359	}360	if amount != s.price {361		panic("send exactly the listed price: " + itoa(s.price) + Denom)362	}363364	newEscrow, ok := checkedAdd(escrowTotal, amount)365	if !ok {366		panic("escrow total would overflow")367	}368369	id := nextOrder370	nextOrder++371	orders.Set(padID(id), &order{372		id:               id,373		serviceID:        serviceID,374		customer:         customer,375		provider:         s.provider,376		amount:           amount,377		feeBps:           s.feeBps,378		status:           OrderPurchased,379		deliveryDeadline: runtime.ChainHeight() + s.deliveryBlocks,380	})381	escrowTotal = newEscrow382	s.orders++383384	chain.Emit("ServicePurchased",385		"orderId", itoa(id),386		"serviceId", itoa(serviceID),387		"customer", customer.String(),388		"provider", s.provider.String(),389		"amount", itoa(amount),390	)391	return id392}393394// MarkDelivered records that the work is done and starts the acceptance395// window. Provider only. The uri is an opaque reference to the396// deliverable; this realm neither fetches nor interprets it, and it is397// sanitized on the way out to Render.398//399// Delivering AFTER the delivery deadline is allowed as long as nobody400// has yet called ClaimRefund — late work the customer still wants should401// not be destroyed by a deadline that exists to protect the customer.402// The customer's remedy is unchanged: they simply do not accept, and the403// refund valve stays open right up until this transition lands.404func MarkDelivered(cur realm, orderID int64, uri string) {405	assertNoSend()406	caller := cur.Previous().Address()407	o := mustGetOrder(orderID)408	if caller != o.provider {409		panic("only the provider may mark this order delivered")410	}411	if o.status != OrderPurchased {412		panic("order is not awaiting delivery")413	}414	if len(uri) > MaxURILen {415		panic("uri must be at most " + strconv.Itoa(MaxURILen) + " characters")416	}417418	o.status = OrderDelivered419	o.deliveryURI = uri420	o.acceptanceDeadline = runtime.ChainHeight() + AcceptanceBlocks421422	chain.Emit("OrderDelivered",423		"orderId", itoa(orderID),424		"provider", o.provider.String(),425		"acceptanceDeadline", itoa(o.acceptanceDeadline),426	)427}428429// AcceptDelivery is THE RESOLUTION: the customer accepts the delivered430// work and the escrow becomes the provider's claimable balance, minus431// the fee snapshotted into the order at purchase. Customer only.432func AcceptDelivery(cur realm, orderID int64) {433	assertNoSend()434	caller := cur.Previous().Address()435	o := mustGetOrder(orderID)436	if caller != o.customer {437		panic("only the customer may accept this delivery")438	}439	if o.status != OrderDelivered {440		panic("order is not awaiting acceptance")441	}442	release(o, "accepted")443}444445// ReleaseTimeout releases a delivered order to the provider once the446// acceptance window has lapsed. Permissionless: a customer who stops447// responding must not be able to strand a provider's completed work.448// This is deemed acceptance, and it is the only path by which escrow449// reaches a provider without the customer's explicit act.450func ReleaseTimeout(cur realm, orderID int64) {451	assertNoSend()452	o := mustGetOrder(orderID)453	if o.status != OrderDelivered {454		panic("order is not awaiting acceptance")455	}456	if runtime.ChainHeight() < o.acceptanceDeadline {457		panic("acceptance window has not lapsed: " +458			itoa(o.acceptanceDeadline-runtime.ChainHeight()) + " blocks remain")459	}460	release(o, "timeout")461}462463// DeclineOrder returns the escrow to the customer before delivery.464// Provider only, fee-free — a provider who cannot fulfil should be able465// to hand the money straight back rather than wait out a deadline. Safe466// to allow unilaterally because it only ever moves value AWAY from the467// caller.468func DeclineOrder(cur realm, orderID int64) {469	assertNoSend()470	caller := cur.Previous().Address()471	o := mustGetOrder(orderID)472	if caller != o.provider {473		panic("only the provider may decline this order")474	}475	if o.status != OrderPurchased {476		panic("order is not awaiting delivery")477	}478	refund(o, "declined")479}480481// ClaimRefund returns the escrow to the customer once the provider has482// missed the delivery deadline. Permissionless: a provider who abandons483// an order must not be able to strand the customer's money.484//485// It is fee-free. Charging a protocol fee on a refund would mean the486// marketplace profits from provider non-performance.487func ClaimRefund(cur realm, orderID int64) {488	assertNoSend()489	o := mustGetOrder(orderID)490	if o.status != OrderPurchased {491		panic("order is not awaiting delivery")492	}493	if runtime.ChainHeight() < o.deliveryDeadline {494		panic("delivery window has not lapsed: " +495			itoa(o.deliveryDeadline-runtime.ChainHeight()) + " blocks remain")496	}497	refund(o, "expired")498}499500// --- payouts ---501502// Claim sends amount ugnot of the caller's claimable balance back to the503// caller.504func Claim(cur realm, amount int64) {505	assertNoSend()506	caller := cur.Previous().Address()507	if err := ledger.Withdraw(caller.String(), amount); err != nil {508		panic(err)509	}510	coinio.Payout(0, cur, caller, Denom, amount)511	chain.Emit("Claim", "to", caller.String(), "amount", itoa(amount))512}513514// ClaimAll sends the caller's entire claimable balance back to the515// caller. Fails if there is nothing to claim.516func ClaimAll(cur realm) {517	assertNoSend()518	caller := cur.Previous().Address()519	amount, err := ledger.WithdrawAll(caller.String())520	if err != nil {521		panic(err)522	}523	if amount == 0 {524		panic("nothing to claim")525	}526	coinio.Payout(0, cur, caller, Denom, amount)527	chain.Emit("Claim", "to", caller.String(), "amount", itoa(amount))528}529530// WithdrawFees sends the accrued fee pot to the fee recipient. Only the531// fee recipient may call it.532func WithdrawFees(cur realm) {533	assertNoSend()534	caller := cur.Previous().Address()535	if caller != feeRecipient {536		panic("only the fee recipient may withdraw fees")537	}538	if ledger.FeesAccrued() == 0 {539		panic("no fees accrued")540	}541	amount := ledger.WithdrawFees()542	coinio.Payout(0, cur, feeRecipient, Denom, amount)543	chain.Emit("FeesWithdrawn", "to", feeRecipient.String(), "amount", itoa(amount))544}545546// SweepDenom sends the surplus of a single denomination to the fee547// recipient. For ugnot only the excess over Liabilities() moves — which548// reserves live escrow as well as claimable balances and the fee pot —549// and other denoms move wholly. Only the fee recipient may call it.550func SweepDenom(cur realm, denom string) {551	assertNoSend()552	caller := cur.Previous().Address()553	if caller != feeRecipient {554		panic("only the fee recipient may sweep surplus")555	}556	reserve := int64(0)557	if denom == Denom {558		reserve = Liabilities()559	}560	swept := coinio.Sweep(0, cur, feeRecipient, denom, reserve)561	chain.Emit("SurplusSwept", "to", feeRecipient.String(), "coins", itoa(swept)+denom)562}563564// --- administration ---565566// SetFeeBps sets the protocol fee snapshotted into FUTURE services.567// Existing services, and every order already placed against them, keep568// the fee they were created under. Admin only; bounded by [0, MaxFeeBps].569func SetFeeBps(cur realm, bps int64) {570	assertNoSend()571	assertAdmin(cur.Previous().Address())572	if bps < 0 || bps > MaxFeeBps {573		panic("fee must be 0-" + itoa(MaxFeeBps) + " bps")574	}575	feeBps = bps576	chain.Emit("FeeChanged", "bps", itoa(bps))577}578579// SetFeeRecipient sets the address that may withdraw fees and sweep580// surplus. Admin only.581func SetFeeRecipient(cur realm, next address) {582	assertNoSend()583	assertAdmin(cur.Previous().Address())584	if !next.IsValid() {585		panic("invalid fee recipient address")586	}587	feeRecipient = next588	chain.Emit("FeeRecipientChanged", "to", next.String())589}590591// TransferAdmin stages a successor. Two-step: the successor must call592// AcceptAdmin, so a typo cannot orphan the realm. Admin only. Passing593// the zero address cancels a pending nomination; any other value must be594// a well-formed address, for parity with SetFeeRecipient. The two-step595// handover already made a malformed nominee harmless — it could never596// call AcceptAdmin — so this check rejects the typo at the point it is597// made rather than leaving it staged and readable as a real nomination.598func TransferAdmin(cur realm, next address) {599	assertNoSend()600	assertAdmin(cur.Previous().Address())601	if next != "" && !next.IsValid() {602		panic("invalid admin address")603	}604	pendingAdmin = next605	chain.Emit("AdminNominated", "to", next.String())606}607608// AcceptAdmin completes a staged handover. Only the nominee may call it.609func AcceptAdmin(cur realm) {610	assertNoSend()611	caller := cur.Previous().Address()612	if pendingAdmin == "" || caller != pendingAdmin {613		panic("only the nominated admin may accept")614	}615	admin = caller616	pendingAdmin = ""617	chain.Emit("AdminTransferred", "to", admin.String())618}619620// --- views ---621622func ServiceInfo(id int64) (provider address, title string, price, deliveryBlocks, feeBps int64, status string, orderCount int64) {623	s := mustGetService(id)624	return s.provider, s.title, s.price, s.deliveryBlocks, s.feeBps, s.status, s.orders625}626627func ServiceDescription(id int64) string { return mustGetService(id).description }628629func OrderInfo(id int64) (serviceID int64, customer, provider address, amount, feeBps int64, status string) {630	o := mustGetOrder(id)631	return o.serviceID, o.customer, o.provider, o.amount, o.feeBps, o.status632}633634// OrderDeadlines returns the two absolute heights that govern an order.635// acceptanceDeadline is 0 until the order is delivered.636func OrderDeadlines(id int64) (deliveryDeadline, acceptanceDeadline int64) {637	o := mustGetOrder(id)638	return o.deliveryDeadline, o.acceptanceDeadline639}640641func OrderURI(id int64) string { return mustGetOrder(id).deliveryURI }642643// Quote reports what a purchase of this service would cost and what the644// provider would receive on resolution, at the service's snapshotted fee.645func Quote(id int64) (price, fee, toProvider int64) {646	s := mustGetService(id)647	f, err := feeledger.FeeFor(s.price, s.feeBps)648	if err != nil {649		panic(err)650	}651	return s.price, f, s.price - f652}653654func Admin() address             { return admin }655func PendingAdmin() address      { return pendingAdmin }656func FeeRecipient() address      { return feeRecipient }657func FeeBps() int64              { return feeBps }658func NumServices() int64         { return nextService }659func NumOrders() int64           { return nextOrder }660func ServicesOf(a address) int64 { return countFor(a) }661662func BalanceOf(addr address) int64 { return ledger.BalanceOf(addr.String()) }663func UsersTotal() int64            { return ledger.UsersTotal() }664func FeesAccrued() int64           { return ledger.FeesAccrued() }665666// EscrowTotal is the live escrow pool E: the sum of every order still in667// Purchased or Delivered.668func EscrowTotal() int64 { return escrowTotal }669670// Liabilities is everything this realm owes: live escrow, claimable671// balances and the fee pot. SweepDenom reserves exactly this.672func Liabilities() int64 { return escrowTotal + ledger.Liabilities() }673674func Held() int64      { return coinio.HeldAt(self, Denom) }675func Surplus() int64   { return Held() - Liabilities() }676func Address() address { return self }677func Height() int64    { return runtime.ChainHeight() }678679// --- render ---680681func Render(path string) string {682	if len(path) > 2 && path[:2] == "s/" {683		return renderService(path[2:])684	}685	if len(path) > 2 && path[:2] == "o/" {686		return renderOrder(path[2:])687	}688	held := Held()689	liab := Liabilities()690	status := "OK"691	if held < liab {692		status = "VIOLATED"693	}694	out := "# Service marketplace\n\n"695	out += "Providers publish services; customers purchase into escrow; payment is\n"696	out += "released only on acceptance, or when the acceptance window lapses.\n\n"697	out += "## Configuration\n\n"698	out += "- fee for new services: " + itoa(feeBps) + " bps (cap " + itoa(MaxFeeBps) +699		" bps; each service keeps the fee snapshotted at registration)\n"700	out += "- acceptance window: " + itoa(AcceptanceBlocks) + " blocks\n"701	out += "- fee recipient: " + feeRecipient.String() + "\n"702	out += "- admin: " + admin.String() + "\n\n"703	out += "## Accounting (H == E + U + F + S)\n\n"704	out += "- escrow held for open orders (E): " + itoa(escrowTotal) + Denom + "\n"705	out += "- claimable balances (U): " + itoa(ledger.UsersTotal()) + Denom + "\n"706	out += "- fees accrued (F): " + itoa(ledger.FeesAccrued()) + Denom + "\n"707	out += "- held (H): " + itoa(held) + Denom + "\n"708	out += "- conservation: " + status + "\n\n"709	out += "## Latest services\n\n"710	if nextService == 0 {711		out += "No services yet.\n"712		return out713	}714	shown := 0715	services.ReverseIterate("", "", func(_ string, v any) bool {716		s := v.(*service)717		out += "- [#" + itoa(s.id) + "](" + realmPath + ":s/" + itoa(s.id) + ") [" +718			s.status + "] " + s.title + " — " + itoa(s.price) + Denom + "\n"719		shown++720		return shown >= RenderLimit721	})722	return out723}724725func renderService(arg string) string {726	id, err := strconv.ParseInt(arg, 10, 64)727	if err != nil {728		return "> [!WARNING]\n> invalid service id\n"729	}730	v := services.Get(padID(id))731	if v == nil {732		return "> [!WARNING]\n> unknown service id\n"733	}734	s := v.(*service)735	out := "# Service #" + itoa(s.id) + ": " + s.title + "\n\n"736	out += "- status: " + s.status + "\n"737	out += "- provider: " + s.provider.String() + "\n"738	out += "- price: " + itoa(s.price) + Denom + "\n"739	out += "- delivery window: " + itoa(s.deliveryBlocks) + " blocks\n"740	out += "- fee (snapshot): " + itoa(s.feeBps) + " bps\n"741	out += "- orders placed: " + itoa(s.orders) + "\n"742	out += "\n## Description\n\n" + sanitize.InlineText(s.description) + "\n"743	return out744}745746func renderOrder(arg string) string {747	id, err := strconv.ParseInt(arg, 10, 64)748	if err != nil {749		return "> [!WARNING]\n> invalid order id\n"750	}751	v := orders.Get(padID(id))752	if v == nil {753		return "> [!WARNING]\n> unknown order id\n"754	}755	o := v.(*order)756	out := "# Order #" + itoa(o.id) + "\n\n"757	out += "- status: " + o.status + "\n"758	out += "- service: [#" + itoa(o.serviceID) + "](" + realmPath + ":s/" + itoa(o.serviceID) + ")\n"759	out += "- customer: " + o.customer.String() + "\n"760	out += "- provider: " + o.provider.String() + "\n"761	out += "- amount: " + itoa(o.amount) + Denom + "\n"762	out += "- fee (snapshot): " + itoa(o.feeBps) + " bps\n"763	out += "- delivery deadline: height " + itoa(o.deliveryDeadline) + "\n"764	if o.acceptanceDeadline != 0 {765		out += "- acceptance deadline: height " + itoa(o.acceptanceDeadline) + "\n"766	}767	if o.deliveryURI != "" {768		out += "\n## Delivery\n\n" + sanitize.InlineText(o.deliveryURI) + "\n"769	}770	return out771}772773// --- internals ---774775const realmPath = "/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/service_market"776777// release moves an order's escrow into the provider's claimable balance778// at the order's snapshotted fee, and drives the order terminal. The779// status write and the escrow decrement happen together with the credit,780// so no order can be released twice and E stays exactly Σ open amounts.781func release(o *order, reason string) {782	credited, fee, err := ledger.Deposit(o.provider.String(), o.amount, o.feeBps)783	if err != nil {784		panic(err)785	}786	o.status = OrderReleased787	escrowTotal -= o.amount // >= 0: escrowTotal == Σ open amounts >= o.amount788789	chain.Emit("OrderReleased",790		"orderId", itoa(o.id),791		"provider", o.provider.String(),792		"amount", itoa(o.amount),793		"credited", itoa(credited),794		"fee", itoa(fee),795		"reason", reason,796	)797}798799// refund moves an order's escrow into the customer's claimable balance800// at ZERO fee and drives the order terminal.801func refund(o *order, reason string) {802	credited, _, err := ledger.Deposit(o.customer.String(), o.amount, 0)803	if err != nil {804		panic(err)805	}806	o.status = OrderRefunded807	escrowTotal -= o.amount808809	chain.Emit("OrderRefunded",810		"orderId", itoa(o.id),811		"customer", o.customer.String(),812		"amount", itoa(credited),813		"reason", reason,814	)815}816817// assertNoSend refuses coins on non-payable entrypoints: an accidental818// -send would otherwise strand at the realm as sweep-only surplus.819// PurchaseService is the only payable function. NOTE: this reads the820// ORIGIN envelope — see the realm-provider caveat in the header.821func assertNoSend() {822	if len(unsafe.OriginSend()) != 0 {823		panic("this function does not accept coins")824	}825}826827func assertAdmin(caller address) {828	if caller != admin {829		panic("admin only")830	}831}832833func mustGetService(id int64) *service {834	v := services.Get(padID(id))835	if v == nil {836		panic("unknown service id")837	}838	return v.(*service)839}840841func mustGetOrder(id int64) *order {842	v := orders.Get(padID(id))843	if v == nil {844		panic("unknown order id")845	}846	return v.(*order)847}848849// countFor reports how many ACTIVE services an address holds.850func countFor(a address) int64 {851	v := providerNum.Get(a.String())852	if v == nil {853		return 0854	}855	return *(v.(*int64))856}857858// bumpFor adjusts the active-service counter, removing the entry at zero859// so a provider who retires everything stops paying for the slot.860func bumpFor(a address, delta int64) {861	key := a.String()862	v := providerNum.Get(key)863	if v == nil {864		if delta <= 0 {865			return866		}867		n := delta868		providerNum.Set(key, &n)869		return870	}871	p := v.(*int64)872	*p += delta873	if *p <= 0 {874		providerNum.Remove(key)875	}876}877878// assertValidTitle bounds length and restricts the charset so titles are879// list-safe in Render without escaping.880func assertValidTitle(title string) {881	if len(title) == 0 || len(title) > MaxTitleLen {882		panic("title must be 1-" + strconv.Itoa(MaxTitleLen) + " characters")883	}884	for i := 0; i < len(title); i++ {885		c := title[i]886		switch {887		case c >= 'a' && c <= 'z':888		case c >= 'A' && c <= 'Z':889		case c >= '0' && c <= '9':890		case c == ' ' || c == '_' || c == '-':891		default:892			panic("title may only contain letters, digits, space, _ and -")893		}894	}895}896897// padID renders an id as a fixed-width key so avl iteration order is898// numeric order.899func padID(id int64) string {900	s := strconv.FormatInt(id, 10)901	for len(s) < 12 {902		s = "0" + s903	}904	return s905}906907func checkedAdd(a, b int64) (int64, bool) {908	c := a + b909	if (b > 0 && c < a) || (b < 0 && c > a) {910		return 0, false911	}912	return c, true913}914915func itoa(n int64) string {916	return strconv.FormatInt(n, 10)917}918

Functions

  • AcceptAdmin(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})

  • AcceptDelivery(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, orderID int64)

  • Address() string

  • Admin() string

  • BalanceOf(addr string) int64

  • Claim(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, amount int64)

  • ClaimAll(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})

  • ClaimRefund(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, orderID int64)

  • DeclineOrder(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, orderID int64)

  • EscrowTotal() int64

  • FeeBps() int64

  • FeeRecipient() string

  • FeesAccrued() int64

  • Height() int64

  • Held() int64

  • Liabilities() int64

  • MarkDelivered(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, orderID int64, uri string)

  • NumOrders() int64

  • NumServices() int64

  • OrderDeadlines(id int64) (deliveryDeadline int64, acceptanceDeadline int64)

  • OrderInfo(id int64) (serviceID int64, customer string, provider string, amount int64, feeBps int64, status string)

  • OrderURI(id int64) string

  • PendingAdmin() string

  • PurchaseService(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, serviceID int64) int64

  • Quote(id int64) (price int64, fee int64, toProvider int64)

  • RegisterService(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, title string, description string, price int64, deliveryBlocks int64, maxFeeBps int64) int64

  • ReleaseTimeout(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, orderID int64)

  • Render(path string) string

  • RetireService(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int64)

  • ServiceDescription(id int64) string

  • ServiceInfo(id int64) (provider string, title string, price int64, deliveryBlocks int64, feeBps int64, status string, orderCount int64)

  • ServicesOf(a string) int64

  • SetFeeBps(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, bps int64)

  • SetFeeRecipient(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, next string)

  • Surplus() int64

  • SweepDenom(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, denom string)

  • TransferAdmin(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, next string)

  • UsersTotal() int64

  • WithdrawFees(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})

Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.

Rendered

RenderedRawgnoweb ↗

vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.