package requester import ( "bytes" "context" "encoding/json" "errors" "fmt" "log/slog" "net/http" "net/url" "slices" "strconv" "strings" "sync" "sync/atomic" "time" "github.com/bluesky-social/indigo/atproto/atcrypto" "github.com/bluesky-social/indigo/atproto/identity" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/market" "github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/serviceauth" "github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/xrpc" "github.com/johnandersen777/socialweb-computer-kcp/internal/contract/tunnel" "github.com/johnandersen777/socialweb-computer-kcp/internal/kit/opt" "github.com/johnandersen777/socialweb-computer-kcp/internal/kit/ttlcache" "github.com/johnandersen777/socialweb-computer-kcp/internal/market/participant" ) const ( marketReplyLimit = 4 << 20 bidderDiscoveryTimeout = 10 * time.Second defaultMarketServiceID = market.DefaultMarketServiceID maxBidderServiceRefHops = 8 ) var ErrBidderUnreachable = errors.New("requester: the bidder's market endpoint is not reachable yet") const bidderSuspendedError = "Suspended" type Bidder struct { DID string Endpoint string Audience string } type BidRecord struct { DID string URI string CID string Free bool Reason string Cost float64 Record map[string]any ArrivedAt time.Time } type SubmitRFPRequest struct { URI string CID string } type AcceptReply struct { URI string CID string SubmitEvent string Ingress []market.StrongRef Status int Body map[string]any } type SubmitEventRequest struct { URI string CID string Record map[string]any } type MarketEvent struct { Event market.ResolvedRecord Payload market.ResolvedRecord } const ( dropDenied = "denied by the requester" dropContractDenied = "denied by the contract" dropNoHeartbeat = "no fresh heartbeat" dropNoOffering = "no compute.vm offering" dropNoEndpoint = "the offering names no endpoint" dropOfferingsRead = "offerings read failed: " dropEndpoint = "offering endpoint refused: " maxDropsInMessage = 5 ) type BidderDrop struct { DID string Reason string } type Discovery struct { Restricted bool Candidates []string Bidders []Bidder Dropped []BidderDrop } func (d Discovery) clone() Discovery { d.Candidates = slices.Clone(d.Candidates) d.Bidders = slices.Clone(d.Bidders) d.Dropped = slices.Clone(d.Dropped) return d } func (d Discovery) Without(deny []string) Discovery { if len(deny) == 0 { return d } out := d.clone() out.Bidders = out.Bidders[:0] denied := map[string]bool{} for _, bidder := range d.Bidders { if !slices.Contains(deny, bidder.DID) { out.Bidders = append(out.Bidders, bidder) continue } if !denied[bidder.DID] { denied[bidder.DID] = true out.Dropped = append(out.Dropped, BidderDrop{DID: bidder.DID, Reason: dropContractDenied}) } } return out } func (d Discovery) DroppedText() []string { out := make([]string, 0, len(d.Dropped)) for _, drop := range d.Dropped { out = append(out, drop.DID+": "+drop.Reason) } return out } func (d Discovery) Why() string { if len(d.Dropped) == 0 { if d.Restricted { return "the contract's bidderDids name no bidder" } return "no candidate bidder DIDs: the requester has no bidderDids and no relay found an offering" } parts := d.DroppedText() more := "" if len(parts) > maxDropsInMessage { more = fmt.Sprintf(" and %d more", len(parts)-maxDropsInMessage) parts = parts[:maxDropsInMessage] } return "no bidder with a market endpoint among " + strconv.Itoa(len(d.Dropped)) + " candidates: " + strings.Join(parts, "; ") + more } func BidderDIDList(dids []string) []string { out := make([]string, 0, len(dids)) for _, did := range dids { if did = strings.TrimSpace(did); did != "" { out = append(out, did) } } slices.Sort(out) return slices.Compact(out) } type discoveryKey struct { only string } type ServiceAuthSource interface { ServiceAuth(ctx context.Context, audience, lxm string) (string, error) } type MarketClient interface { Discover(ctx context.Context, now time.Time, only []string) (Discovery, error) SubmitRFP(ctx context.Context, bidder Bidder, rfp SubmitRFPRequest) ([]BidRecord, error) SubmitAccept(ctx context.Context, bidder Bidder, acceptURI, acceptCID string) (*AcceptReply, error) SubmitEvent(ctx context.Context, bidder Bidder, event SubmitEventRequest) (int, error) FindReceipt(ctx context.Context, bidderDID, acceptURI, acceptCID string) (*market.ResolvedRecord, error) FindTermination(ctx context.Context, bidderDID, receiptURI string) (*MarketEvent, error) } type HTTPMarket struct { Directory identity.Directory Reader *market.RepoStore RepoDID string Signer *market.Signer ServiceKey atcrypto.PrivateKey ServiceAuth ServiceAuthSource RelayURLs []string BidderDIDs []string OperatorDID string DenyBidderDIDs []string Ingress tunnel.Service IngressRef func(ctx context.Context) *market.RecordRef IngressWritable func(ctx context.Context) bool IngressSTUN []string HTTPClient *http.Client Timeout time.Duration Log *slog.Logger StartedAt time.Time firstDiscovery atomic.Bool discoveryOnce sync.Once discovered *ttlcache.Cache[discoveryKey, Discovery] } var _ MarketClient = (*HTTPMarket)(nil) func (m *HTTPMarket) client() *http.Client { if m.HTTPClient != nil { return m.HTTPClient } return &http.Client{Timeout: DefaultHTTPTimeout} } func (m *HTTPMarket) log() *slog.Logger { if m.Log != nil { return m.Log } return slog.New(slog.DiscardHandler) } func (m *HTTPMarket) Discover(ctx context.Context, now time.Time, only []string) (Discovery, error) { if m == nil || m.Reader == nil { return Discovery{}, errors.New("requester: the market has no record reader") } only = BidderDIDList(only) m.discoveryOnce.Do(func() { m.discovered = ttlcache.New[discoveryKey, Discovery](ttlcache.Options{TTL: 20 * time.Second, MaxEntries: 256}) }) found, err := m.discovered.GetOrLoad(ctx, discoveryKey{only: strings.Join(only, ",")}, func(ctx context.Context) (Discovery, time.Duration, error) { found := m.discover(ctx, now, only) if len(found.Bidders) == 0 { return found, -1, nil } return found, 0, nil }) if err != nil { return Discovery{}, err } return found.clone(), nil } func (m *HTTPMarket) discover(ctx context.Context, now time.Time, only []string) Discovery { discoveryStart := time.Now() found := Discovery{Restricted: len(only) > 0} seen := map[string]bool{} explicit := map[string]bool{} add := func(did string, named bool) { did = strings.TrimSpace(did) if did == "" || seen[did] { return } seen[did] = true if slices.Contains(m.DenyBidderDIDs, did) { found.Dropped = append(found.Dropped, BidderDrop{DID: did, Reason: dropDenied}) return } explicit[did] = named found.Candidates = append(found.Candidates, did) } source, counts := "contract", "" if found.Restricted { for _, did := range only { add(did, true) } } else { source = "requester" sources, auto := m.candidateSources(ctx) for _, candidates := range sources { for _, did := range candidates.dids { add(did, candidates.named) } } counts = sourceCounts(sources, auto) } unnamed := []string{} for _, did := range found.Candidates { if !explicit[did] { unnamed = append(unnamed, did) } } fresh := []string{} if len(unnamed) > 0 { fresh = market.NewHeartbeatCache(m.Reader).FreshDIDs(ctx, unnamed, now) } seenEndpoint := map[string]bool{} for _, did := range found.Candidates { if !explicit[did] && !slices.Contains(fresh, did) { found.Dropped = append(found.Dropped, BidderDrop{DID: did, Reason: dropNoHeartbeat}) continue } bidders, reason := m.offeredBidders(ctx, did) added := 0 for _, bidder := range bidders { key := did + "::" + bidder.Endpoint if seenEndpoint[key] { continue } seenEndpoint[key] = true found.Bidders = append(found.Bidders, bidder) added++ } if added == 0 { found.Dropped = append(found.Dropped, BidderDrop{DID: did, Reason: reason}) } } bidderDIDs := make([]string, 0, len(found.Bidders)) for _, bidder := range found.Bidders { bidderDIDs = append(bidderDIDs, bidder.DID) } discoveryMs := time.Since(discoveryStart).Milliseconds() m.log().Info("requester_bidder_discovery", "source", source, "sources", counts, "candidates", len(found.Candidates), "bidders", bidderDIDs, "dropped", len(found.Dropped), "discoveryMs", discoveryMs) if !m.StartedAt.IsZero() && !m.firstDiscovery.Swap(true) { m.log().Info("requester_start_timing", "step", "firstBidderDiscovery", "sinceStartMs", time.Since(m.StartedAt).Milliseconds(), "discoveryMs", discoveryMs, "bidders", len(found.Bidders)) } return found } func (m *HTTPMarket) offeredBidders(ctx context.Context, did string) ([]Bidder, string) { offering, _, err := market.ReadOffering(ctx, m.Reader, did) if errors.Is(err, market.ErrNotFound) { return nil, dropNoOffering } if err != nil { m.log().Warn("requester_offerings_failed", "did", did, "err", err) return nil, dropOfferingsRead + err.Error() } if !market.OfferingApplies(offering, market.ComputeVMNSID) { return nil, dropNoOffering } endpoint, audience, err := resolveBidderEndpoint(ctx, m.Directory, offering.EndpointURL) if err != nil { m.log().Warn("requester_bidder_endpoint_refused", "did", did, "endpoint", offering.EndpointURL, "err", err) return nil, dropEndpoint + err.Error() } if endpoint == "" { return nil, dropNoEndpoint } return []Bidder{{DID: did, Endpoint: endpoint, Audience: audience}}, dropNoOffering } func resolveBidderEndpoint(ctx context.Context, dir identity.Directory, raw string) (string, string, error) { raw = strings.TrimSpace(raw) if raw == "" { return "", "", nil } return resolveBidderServiceRef(ctx, dir, raw) } func resolveBidderServiceRef(ctx context.Context, dir identity.Directory, raw string) (string, string, error) { if dir == nil { return "", "", fmt.Errorf("offering endpoint %q needs an identity directory", raw) } seen := map[string]bool{} current := strings.TrimSpace(raw) for range maxBidderServiceRefHops { if !strings.HasPrefix(current, "did:") { u, err := url.Parse(current) if err != nil || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") { return "", "", fmt.Errorf("offering endpoint %q is not an absolute http or https URL or a DID service ref", current) } base := strings.TrimRight(current, "/") if !strings.HasSuffix(base, "/xrpc") { base += "/xrpc" } return base, market.ServiceDIDForHost(u.Host), nil } didText := current fragment := defaultMarketServiceID if before, after, found := strings.Cut(current, "#"); found { didText = before if strings.TrimSpace(after) != "" { fragment = strings.TrimSpace(after) } } key := didText + "#" + fragment if seen[key] { return "", "", fmt.Errorf("offering endpoint %q repeats the service reference %s", raw, key) } seen[key] = true did, err := syntax.ParseDID(strings.TrimSpace(didText)) if err != nil { return "", "", fmt.Errorf("offering endpoint %q: %w", raw, err) } doc, err := dir.LookupDID(ctx, did) if err != nil || doc == nil { return "", "", fmt.Errorf("offering endpoint %q cannot be resolved", raw) } service, ok := doc.Services[fragment] if !ok || strings.TrimSpace(service.URL) == "" { return "", "", fmt.Errorf("offering endpoint %q names no service %s", raw, fragment) } current = strings.TrimSpace(service.URL) } return "", "", fmt.Errorf("offering endpoint %q exceeds %d service reference hops", raw, maxBidderServiceRefHops) } func (m *HTTPMarket) token(ctx context.Context, audience string, lxm string) (string, error) { if m.ServiceAuth != nil { return m.ServiceAuth.ServiceAuth(ctx, audience, lxm) } if m.ServiceKey == nil { return "", errors.New("requester: no service auth key") } if m.RepoDID == "" { return "", errors.New("requester: no repo DID for service auth") } nsid, err := syntax.ParseNSID(lxm) if err != nil { return "", err } issuer, err := syntax.ParseDID(m.RepoDID) if err != nil { return "", err } return serviceauth.Sign(issuer, audience, nsid, m.ServiceKey, ServiceAuthTTL) } func (m *HTTPMarket) post(ctx context.Context, endpoint, lxm, audience string, body any, timeout time.Duration) (int, map[string]any, error) { return m.postWithClient(ctx, m.client(), endpoint, lxm, audience, body, timeout) } func (m *HTTPMarket) postWithClient(ctx context.Context, client *http.Client, endpoint, lxm, audience string, body any, timeout time.Duration) (int, map[string]any, error) { if strings.TrimSpace(endpoint) == "" { return 0, nil, fmt.Errorf("requester: no inbound endpoint is advertised for %s", lxm) } timeout = opt.Positive(timeout, DefaultReceiptTimeout) cctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() auth := xrpc.NewServiceAuth(xrpc.MinterFunc(m.token), audience, lxm) reply, err := xrpc.Procedure[any, xrpc.Reply](cctx, &xrpc.Client{Host: endpoint, HTTP: client, Auth: auth, Limit: marketReplyLimit}, lxm, body) if xerr, ok := errors.AsType[*xrpc.Error](err); ok { reply = xrpc.Reply{Status: xerr.Status, Body: xerr.Body} } else if err != nil { return 0, nil, err } out := map[string]any{} if len(bytes.TrimSpace(reply.Body)) > 0 { _ = json.Unmarshal(reply.Body, &out) } return reply.Status, out, nil } func (m *HTTPMarket) SubmitRFP(ctx context.Context, bidder Bidder, rfp SubmitRFPRequest) ([]BidRecord, error) { body := map[string]any{ "rfpUri": rfp.URI, "rfpCid": rfp.CID, } var offer tunnel.Offer offered := false if m.Ingress != nil && (m.IngressWritable == nil || m.IngressWritable(ctx)) { current, err := m.Ingress.Offer(ctx, m.RepoDID, bidder.DID) if err != nil { m.log().Warn("requester_webrtc_offer_failed", "bidder", bidder.DID, "err", err.Error()) } else if ref, err := m.createIngressOffer(ctx, current, bidder.DID); err != nil { m.log().Warn("requester_webrtc_offer_record_failed", "bidder", bidder.DID, "err", err.Error()) } else { body["ingress"] = []map[string]any{ref.Map()} offer = current offered = true } } status, reply, err := m.post(ctx, bidder.Endpoint, market.SubmitRFPNSID, bidder.Audience, body, m.Timeout) if errors.Is(err, xrpc.ErrTooLarge) { return nil, err } if err != nil { return nil, fmt.Errorf("%w: %w", ErrBidderUnreachable, err) } if status < 200 || status > 299 { detail := stringOf(reply["error"]) if message := stringOf(reply["message"]); message != "" { detail = strings.TrimSpace(detail + ": " + message) } err := fmt.Errorf("requester: submitRfp to %s (aud %s) answered HTTP %d", bidder.Endpoint, bidder.Audience, status) if detail != "" { err = fmt.Errorf("requester: submitRfp to %s (aud %s) answered HTTP %d: %s", bidder.Endpoint, bidder.Audience, status, detail) } if stringOf(reply["error"]) == bidderSuspendedError { return nil, err } if status == http.StatusNotFound || status == http.StatusTooManyRequests || status >= 500 { return nil, fmt.Errorf("%w: %w", ErrBidderUnreachable, err) } return nil, err } if offered { m.acceptIngress(ctx, bidder.DID, offer, reply) } return bidsFromReply(bidder.DID, reply), nil } func (m *HTTPMarket) ingressList(ctx context.Context) []market.StrongRef { if m.IngressRef == nil { return nil } ref := m.IngressRef(ctx) if ref == nil || ref.URI == "" { return nil } return []market.StrongRef{market.NewStrongRef(ref.URI, ref.CID)} } func (m *HTTPMarket) createIngressOffer(ctx context.Context, offer tunnel.Offer, bidderDID string) (market.StrongRef, error) { if m.Reader == nil { return market.StrongRef{}, errors.New("requester: no record store for the ingress offer") } var descriptor *market.StrongRef if m.IngressRef != nil { if ref := m.IngressRef(ctx); ref != nil && ref.URI != "" { strong := market.NewStrongRef(ref.URI, ref.CID) descriptor = &strong } } value, err := market.EncodeRecord(market.IngressWebRTC{ Kind: tunnel.KindWebRTC, Mode: market.IngressModeOffer, ID: offer.ID, Peer: bidderDID, Session: offer.Session, Sdp: offer.SDP, Descriptor: descriptor, StunUrls: m.IngressSTUN, CreatedAt: market.NowISO(), }) if err != nil { return market.StrongRef{}, err } return m.Reader.CreateRecord(ctx, m.RepoDID, market.IngressWebRTCNSID, value) } func (m *HTTPMarket) acceptIngress(ctx context.Context, bidderDID string, offer tunnel.Offer, body map[string]any) { if m.Ingress == nil || m.Reader == nil || body == nil { return } raw, ok := body["ingress"] if !ok { return } refs, err := ingressRefsOf(raw) if err != nil { m.log().Warn("requester_webrtc_answer_invalid", "bidder", bidderDID, "err", err.Error()) return } for _, ref := range refs { if market.ATURIAuthority(ref.URI) != bidderDID || market.NSIDFromURI(ref.URI) != market.IngressWebRTCNSID { continue } resolved, err := m.Reader.Resolve(ctx, market.RecordRef{URI: ref.URI, CID: ref.CID}) if err != nil { m.log().Warn("requester_webrtc_answer_unresolved", "bidder", bidderDID, "answer", ref.URI, "err", err.Error()) continue } record, err := market.ParseIngressWebRTC(resolved.Value) if err != nil { m.log().Warn("requester_webrtc_answer_invalid", "bidder", bidderDID, "err", err.Error()) continue } if err := record.Validate(); err != nil { m.log().Warn("requester_webrtc_answer_invalid", "bidder", bidderDID, "err", err.Error()) continue } if record.Mode != market.IngressModeAnswer { continue } if record.ID != offer.ID || record.Session != offer.Session || record.Peer != m.RepoDID { m.log().Warn("requester_webrtc_answer_mismatch", "bidder", bidderDID, "answer", ref.URI) continue } answer := tunnel.Answer{Kind: tunnel.KindWebRTC, ID: record.ID, Session: record.Session, Peer: bidderDID, Type: tunnel.SignalAnswer, SDP: record.Sdp} if err := m.Ingress.Accept(ctx, answer); err != nil { m.log().Warn("requester_webrtc_accept_failed", "bidder", bidderDID, "err", err.Error()) continue } m.log().Info("requester_webrtc_connected", "bidder", bidderDID, "id", answer.ID, "session", answer.Session) return } m.log().Warn("requester_webrtc_answer_missing", "bidder", bidderDID, "session", offer.Session) } func ingressRefsOf(raw any) ([]market.StrongRef, error) { encoded, err := json.Marshal(raw) if err != nil { return nil, err } var refs []market.StrongRef if err := json.Unmarshal(encoded, &refs); err != nil { return nil, err } out := make([]market.StrongRef, 0, len(refs)) for _, ref := range refs { if ref.URI == "" || ref.CID == "" { continue } out = append(out, ref) } if len(out) == 0 { return nil, errors.New("the ingress list names no record") } return out, nil } func bidsFromReply(did string, body map[string]any) []BidRecord { if body == nil { return nil } candidates := []any{body["bid"], body["bids"], body["record"], body} out := []BidRecord{} for _, candidate := range candidates { switch value := candidate.(type) { case map[string]any: if rec := bidRecordOf(did, value); rec != nil { out = append(out, *rec) } case []any: for _, item := range value { if obj, ok := item.(map[string]any); ok { if rec := bidRecordOf(did, obj); rec != nil { out = append(out, *rec) } } } } } return dedupeBids(out) } func bidRecordOf(did string, value map[string]any) *BidRecord { uri, _ := value["uri"].(string) cid, _ := value["cid"].(string) record, _ := value["record"].(map[string]any) if record == nil { record = value } if uri == "" { return nil } if _, ok := record["rfp"]; !ok { return nil } return &BidRecord{DID: did, URI: uri, CID: cid, Record: record, ArrivedAt: time.Now()} } func (m *HTTPMarket) SubmitAccept(ctx context.Context, bidder Bidder, acceptURI, acceptCID string) (*AcceptReply, error) { client := *m.client() client.Timeout = 0 request := map[string]any{ "acceptUri": acceptURI, "acceptCid": acceptCID, } if refs := m.ingressList(ctx); len(refs) > 0 { request["ingress"] = refs } status, body, err := m.postWithClient(ctx, &client, bidder.Endpoint, market.SubmitAcceptNSID, bidder.Audience, request, submitAcceptProvisionTimeout) if err != nil { return nil, err } reply := &AcceptReply{Status: status, Body: body} if body != nil { reply.URI, _ = body["uri"].(string) reply.CID, _ = body["cid"].(string) reply.SubmitEvent, _ = body["submitEvent"].(string) if raw, ok := body["ingress"]; ok { reply.Ingress, _ = ingressRefsOf(raw) } } return reply, nil } func (m *HTTPMarket) SubmitEvent(ctx context.Context, bidder Bidder, event SubmitEventRequest) (int, error) { request := map[string]any{ "uri": event.URI, "cid": event.CID, "record": event.Record, } if refs := m.ingressList(ctx); len(refs) > 0 { request["ingress"] = refs } status, body, err := m.post(ctx, bidder.Endpoint, market.SubmitEventNSID, bidder.Audience, request, m.Timeout) if err == nil && (status < 200 || status > 299) { err = &participant.CallError{Status: status, Err: &xrpc.Error{Status: status, Name: stringOf(body["error"]), Message: stringOf(body["message"])}} } return status, err } func (m *HTTPMarket) FindReceipt(ctx context.Context, bidderDID, acceptURI, acceptCID string) (*market.ResolvedRecord, error) { if m == nil || m.Reader == nil { return nil, errors.New("requester: the market has no record reader") } if strings.TrimSpace(acceptCID) == "" { return nil, fmt.Errorf("requester: the accept %s names no cid, so its receipt rkey is unknown", acceptURI) } record, err := m.Reader.GetRecord(ctx, bidderDID, market.ReceiptNSID, market.ReceiptRKey(acceptURI, acceptCID)) if errors.Is(err, market.ErrNotFound) { return nil, nil } if err != nil { return nil, err } accept, _ := record.Value["accept"].(map[string]any) if accept == nil || stringOf(accept["uri"]) != acceptURI { m.log().Warn("requester_receipt_accept_mismatch", "receipt", record.URI, "accept", acceptURI) return nil, nil } return &record, nil } func (m *HTTPMarket) FindTermination(ctx context.Context, bidderDID, receiptURI string) (*MarketEvent, error) { if m == nil || m.Reader == nil { return nil, errors.New("requester: the market has no record reader") } _, eventRKey := market.EventRKeys(market.TerminationKey(receiptURI)) record, err := m.Reader.GetRecord(ctx, bidderDID, market.EventNSID, eventRKey) if errors.Is(err, market.ErrNotFound) { return nil, nil } if err != nil { return nil, err } receipt, _ := record.Value["receipt"].(map[string]any) payload, _ := record.Value["payload"].(map[string]any) var ref market.RecordRef if payload != nil { ref = market.RecordRef{URI: stringOf(payload["uri"]), CID: stringOf(payload["cid"])} } if receipt == nil || stringOf(receipt["uri"]) != receiptURI || market.NSIDFromURI(ref.URI) != market.ComputeEventsVMTerminatedNSID { m.log().Warn("requester_termination_event_mismatch", "event", record.URI, "receipt", receiptURI, "payload", ref.URI) return nil, nil } resolved, err := m.Reader.Resolve(ctx, ref) if errors.Is(err, market.ErrNotFound) { m.log().Warn("requester_termination_payload_missing", "event", record.URI, "payload", ref.URI) return nil, nil } if err != nil { return nil, fmt.Errorf("requester: the termination payload %s: %w", ref.URI, err) } return &MarketEvent{Event: record, Payload: resolved}, nil } func listAll(ctx context.Context, lister market.RecordLister, repoDID, collection string, limit int) ([]market.ListedRecord, error) { out := []market.ListedRecord{} cursor := "" seen := map[string]bool{} for range 20 { found, err := lister.ListRecords(ctx, repoDID, collection, limit, cursor) if err != nil { return out, err } out = append(out, found.Records...) if found.Cursor == "" || seen[found.Cursor] { return out, nil } seen[found.Cursor] = true cursor = found.Cursor } return out, nil } func dedupeBids(in []BidRecord) []BidRecord { seen := map[string]bool{} out := make([]BidRecord, 0, len(in)) for _, bid := range in { if bid.URI == "" || seen[bid.URI] { continue } seen[bid.URI] = true out = append(out, bid) } return out } func stringOf(value any) string { switch v := value.(type) { case string: return v case json.Number: return v.String() case nil: return "" default: return fmt.Sprint(v) } } func numberValue(value any) float64 { switch v := value.(type) { case float64: return v case float32: return float64(v) case int: return float64(v) case int64: return float64(v) case json.Number: f, _ := v.Float64() return f case string: f, err := strconv.ParseFloat(strings.TrimSpace(v), 64) if err == nil { return f } } return 0 } func payloadRef(value map[string]any) market.RecordRef { ref, _ := value["payload"].(map[string]any) if ref == nil { return market.RecordRef{} } return market.RecordRef{URI: stringOf(ref["uri"]), CID: stringOf(ref["cid"])} } var ErrParticipationSuspended = errors.New("requester: participation is suspended; no new RFP or accept is published")