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}918AcceptAdmin(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.
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.