johnandersen777.bsky.social/atlibs-market-status
socialweb.computer bidders' market state as an atlibs module
0
0
use abi::repo::{get_record, put_record};
use abi::{AtUri, Call, Event, Handle, Run, entries};
use serde::Deserialize;
use serde_json::{Value, json};
// A bidder's market state, all by getRecord at computed keys (no listRecords):
// offering/self and heartbeat/self in the bidder's repo (and the offering the beat names, if another);
// badgeBlueKeys/association: the operator it claims; the operator's market.association
// bidder_associate-<bidder DID, ':' -> '-'> confirms it.
// The bidders an account follows here: one computer.socialweb.bidders/self record {bidders: [DID]}.
const OFFERING: &str = "com.publicdomainrelay.temp.market.offering";
const HEARTBEAT: &str = "com.publicdomainrelay.temp.market.heartbeat";
const BADGE: &str = "com.publicdomainrelay.temp.badgeBlueKeys";
const ASSOCIATION: &str = "com.publicdomainrelay.temp.market.association";
const INDEX: &str = "computer.socialweb.bidders";
const MAX: usize = 50;
#[derive(Deserialize)]
struct Args {
#[serde(default)]
did: String,
#[serde(default)]
now: String,
#[serde(default)]
dids: Vec<String>,
#[serde(default)]
of: Option<String>,
#[serde(default)]
bidder: Option<String>,
}
fn read(id: String, repo: &str, collection: &str, rkey: &str) -> Call {
get_record(&AtUri::new(repo, collection, rkey)).id(id).service(repo)
}
// A record, or null when it is not there (any failed read: the row says what it could see).
fn found(result: &abi::CallResult) -> Value {
result.output().ok().filter(|o| o["value"].is_object()).map(|o| json!({ "uri": o["uri"], "cid": o["cid"], "value": o["value"] })).unwrap_or(Value::Null)
}
fn endpoints(offering: &Value) -> Value {
let list = offering["value"]["endpoints"].as_array().cloned().unwrap_or_default();
let mut out: Vec<Value> = list.iter().filter_map(|e| Some(json!({ "kind": e["kind"].as_str()?, "address": e["address"] }))).collect();
if let Some(url) = offering["value"]["endpointUrl"].as_str() {
out.push(json!({ "kind": "https", "address": url }));
}
json!(out)
}
// bidders {dids}: one row per DID: {did, offering, heartbeat, beatAt, endpoints, appliesTo, withdrawn, operator, claimedOperator}.
fn bidders_handler(run: Run<Args>) -> Result<Handle, String> {
match run.event {
Event::Start => {
let mut dids: Vec<String> = Vec::new();
for d in run.args.dids.iter().map(|d| d.trim()) {
if !d.starts_with("did:") {
return Err(format!("not a DID: {d}"));
}
if !dids.iter().any(|x| x == d) {
dids.push(d.to_string());
}
}
if dids.len() > MAX {
return Err(format!("at most {MAX} DIDs"));
}
if dids.is_empty() {
return Ok(Handle::new().result(json!({ "bidders": [] })).done());
}
let mut h = Handle::new();
for (i, d) in dids.iter().enumerate() {
h = h.call(read(format!("o:{i}"), d, OFFERING, "self")).call(read(format!("h:{i}"), d, HEARTBEAT, "self")).call(read(format!("b:{i}"), d, BADGE, "association"));
}
let rows: Vec<Value> = dids.iter().map(|d| json!({ "did": d, "offering": null, "heartbeat": null, "operator": null, "claimedOperator": null })).collect();
Ok(h.state(json!({ "rows": rows, "pending": dids.len() * 3 })))
}
Event::Result { id, result, .. } => {
let mut s = run.state;
let (kind, i) = id.split_once(':').ok_or("bad id")?;
let i: usize = i.parse().map_err(|_| "bad id")?;
let did = s["rows"][i]["did"].as_str().unwrap_or_default().to_string();
let mut h = Handle::new();
let mut more = 0;
let record = found(&result);
match kind {
"o" | "n" => {
if !record.is_null() {
s["rows"][i]["offering"] = record;
}
}
"h" => {
let named = record["value"]["offering"].as_str().unwrap_or_default().to_string();
s["rows"][i]["heartbeat"] = record;
let own = format!("at://{did}/{OFFERING}/");
if let Some(rkey) = named.strip_prefix(&own).filter(|r| *r != "self" && !r.is_empty()) {
h = h.call(read(format!("n:{i}"), &did, OFFERING, rkey));
more += 1;
}
}
"b" => {
let v = &record["value"];
let op = v["challenge"].as_str().unwrap_or_default();
if op.starts_with("did:") && op != did && v["keyId"] == did.as_str() && v["service"] == "bidder_associate" {
s["rows"][i]["claimedOperator"] = json!(op);
let rkey = format!("bidder_associate-{}", did.replace(':', "-"));
h = h.call(read(format!("a:{i}"), op, ASSOCIATION, &rkey)).call(read(format!("c:{i}"), op, BADGE, &rkey));
more += 2;
}
}
// the operator's side: a standing association, or the same claim in its own badgeBlueKeys
"a" | "c" => {
let op = s["rows"][i]["claimedOperator"].clone();
let v = &record["value"];
let held = if kind == "a" {
v["subject"] == did.as_str() && v["service"] == "bidder_associate" && v.get("contract").is_none() && v.get("expiresAt").is_none()
} else {
v["challenge"] == op && v["keyId"] == did.as_str() && v["service"] == "bidder_associate"
};
if held {
s["rows"][i]["operator"] = op;
}
}
_ => {}
}
let pending = s["pending"].as_u64().unwrap_or(1) - 1 + more;
s["pending"] = json!(pending);
if pending > 0 {
return Ok(h.state(s));
}
let rows: Vec<Value> = s["rows"].as_array().cloned().unwrap_or_default().into_iter().map(finish).collect();
Ok(Handle::new().result(json!({ "bidders": rows, "at": run.args.now })).done())
}
_ => Ok(Handle::new()),
}
}
fn finish(mut row: Value) -> Value {
let offering = row["offering"].clone();
row["endpoints"] = endpoints(&offering);
row["appliesTo"] = offering["value"]["appliesTo"].clone();
row["withdrawn"] = json!(offering["value"]["withdrawnAt"].is_string());
row["beatAt"] = row["heartbeat"]["value"]["createdAt"].clone();
row
}
fn index_of(run: &Run<Args>) -> Result<String, String> {
let of = run.args.of.clone().filter(|s| !s.is_empty()).unwrap_or(run.args.did.clone());
of.starts_with("did:").then_some(of).ok_or("of: a DID (or sign in)".into())
}
fn listed(result: &abi::CallResult) -> Vec<String> {
found(result)["value"]["bidders"].as_array().into_iter().flatten().filter_map(|d| d.as_str().map(str::to_string)).collect()
}
// known {of?}: the bidders an account follows here (default: you).
fn known_handler(run: Run<Args>) -> Result<Handle, String> {
let of = index_of(&run)?;
Ok(match run.event {
Event::Start => Handle::new().call(read("index".into(), &of, INDEX, "self")),
Event::Result { id, result, .. } if id == "index" => Handle::new().result(json!({ "of": of, "bidders": listed(&result) })).done(),
_ => Handle::new(),
})
}
fn edit(run: Run<Args>, add: bool) -> Result<Handle, String> {
if !run.args.did.starts_with("did:") {
return Err("sign in first".into());
}
let bidder = run.args.bidder.clone().map(|b| b.trim().to_string()).filter(|b| b.starts_with("did:")).ok_or("bidder: a DID")?;
let me = run.args.did.clone();
Ok(match run.event {
Event::Start => Handle::new().call(read("index".into(), &me, INDEX, "self")),
Event::Result { id, result, .. } if id == "index" => {
let mut list = listed(&result);
list.retain(|d| *d != bidder);
if add {
if list.len() >= MAX {
return Err(format!("at most {MAX} bidders"));
}
list.push(bidder.clone());
}
let record = json!({ "$type": INDEX, "bidders": list, "updatedAt": run.args.now });
Handle::new().call(put_record(&AtUri::new(&me, INDEX, "self"), record).id("put")).state(json!({ "bidders": list }))
}
Event::Result { id, result, .. } if id == "put" => {
result.output()?;
Handle::new().result(json!({ "bidders": run.state["bidders"] })).done()
}
_ => Handle::new(),
})
}
fn track_handler(run: Run<Args>) -> Result<Handle, String> {
edit(run, true)
}
fn untrack_handler(run: Run<Args>) -> Result<Handle, String> {
edit(run, false)
}
entries!(
bidders => bidders_handler,
known => known_handler,
track => track_handler,
untrack => untrack_handler
);
#[cfg(test)]
mod tests {
use serde_json::{Value, json};
type H = fn(abi::Run<super::Args>) -> Result<abi::Handle, String>;
fn step(f: H, args: &Value, event: Value, state: &Value) -> Value {
let mut input = args.clone();
input["event"] = event;
input["state"] = state.clone();
serde_json::to_value(f(serde_json::from_value(input).unwrap()).unwrap()).unwrap()
}
fn res(id: &str, output: Value) -> Value {
json!({ "type": "result", "id": id, "ok": true, "output": output })
}
fn miss(id: &str) -> Value {
json!({ "type": "result", "id": id, "ok": false, "error": "RecordNotFound" })
}
const B: &str = "did:plc:bidder";
const OP: &str = "did:plc:op";
#[test]
fn a_bidder_with_a_named_offering_and_a_confirmed_operator() {
let args = json!({ "did": "did:plc:me", "now": "2026-11-10T00:00:00Z", "dids": [B, B] });
let s = step(super::bidders_handler, &args, json!({ "type": "start" }), &Value::Null);
assert_eq!(s["calls"].as_array().unwrap().len(), 3, "a repeated DID is read once");
let mut state = s["state"].clone();
let s = step(super::bidders_handler, &args, miss("o:0"), &state);
state = s["state"].clone();
let named = format!("at://{B}/com.publicdomainrelay.temp.market.offering/3abc");
let s = step(super::bidders_handler, &args, res("h:0", json!({ "uri": "h", "cid": "c", "value": { "offering": named, "createdAt": "2026-11-09T00:00:00Z" } })), &state);
assert_eq!(s["calls"][0]["params"]["rkey"], "3abc");
state = s["state"].clone();
let s = step(super::bidders_handler, &args, res("b:0", json!({ "uri": "b", "cid": "c", "value": { "challenge": OP, "keyId": B, "service": "bidder_associate" } })), &state);
assert_eq!(s["calls"][0]["params"]["rkey"], "bidder_associate-did-plc-bidder");
assert_eq!(s["calls"][0]["service"], OP);
state = s["state"].clone();
let s = step(super::bidders_handler, &args, miss("c:0"), &state);
state = s["state"].clone();
let s = step(super::bidders_handler, &args, res("n:0", json!({ "uri": named, "cid": "c", "value": { "appliesTo": ["x"], "endpoints": [{ "kind": "iroh", "address": "t" }] } })), &state);
state = s["state"].clone();
let done = step(super::bidders_handler, &args, res("a:0", json!({ "uri": "a", "cid": "c", "value": { "subject": B, "service": "bidder_associate" } })), &state);
let row = &done["result"]["bidders"][0];
assert_eq!(row["operator"], OP);
assert_eq!(row["beatAt"], "2026-11-09T00:00:00Z");
assert_eq!(row["endpoints"], json!([{ "kind": "iroh", "address": "t" }]));
assert_eq!(row["withdrawn"], false);
}
#[test]
fn track_and_untrack_rewrite_the_one_index_record() {
let args = json!({ "did": "did:plc:me", "now": "n", "bidder": B });
let s = step(super::track_handler, &args, json!({ "type": "start" }), &Value::Null);
assert_eq!(s["calls"][0]["params"]["collection"], "computer.socialweb.bidders");
let s = step(super::track_handler, &args, res("index", json!({ "uri": "u", "cid": "c", "value": { "bidders": ["did:plc:x", B] } })), &Value::Null);
assert_eq!(s["calls"][0]["input"]["record"]["bidders"], json!(["did:plc:x", B]));
let s = step(super::untrack_handler, &args, miss("index"), &Value::Null);
assert_eq!(s["calls"][0]["input"]["record"]["bidders"], json!([]));
}
}