From d7ad4a34d0d15325daa5041aac991fc5ddf94db8 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 05:38:09 -0700 Subject: [PATCH 001/111] wallet: make digital assets first-class holdings --- src/42_ENTITY_Wallet/canonical_wallet.py | 28 +++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/42_ENTITY_Wallet/canonical_wallet.py b/src/42_ENTITY_Wallet/canonical_wallet.py index 513ec7e2..8903a53d 100644 --- a/src/42_ENTITY_Wallet/canonical_wallet.py +++ b/src/42_ENTITY_Wallet/canonical_wallet.py @@ -122,6 +122,25 @@ def _trade_basis(self,db,entity_id:str,instrument_id:str,current_units:int)->dic "realized_change_amount_units":realized,"unknown_sold_units":unknown_sold, "bought_units":bought,"sold_units":sold} + def _assets(self,entity_id:str)->list[dict]: + """Return controller-held Digital Commodity Objects independently of market positions.""" + if not self.fabric_path.exists(): return [] + with self._db(self.fabric_path) as db: + rows=db.execute("""SELECT object_id,controller_entity_id,object_type,title,content_sha256, + descriptor_json,status,created_at_ms + FROM objects WHERE controller_entity_id=? AND status='ACTIVE' + ORDER BY created_at_ms DESC,object_id""",(entity_id,)).fetchall() + out=[] + for row in rows: + d=dict(row); descriptor=json.loads(d.pop("descriptor_json") or "{}") + if descriptor.get("digital_commodity") is not True: continue + d["descriptor"]=descriptor + d["commodity_class"]=descriptor.get("commodity_class") + d["measurement_unit"]=descriptor.get("measurement_unit") + d["market_instruments_created_automatically"]=False + out.append(d) + return out + def _positions(self,entity_id:str)->list[dict]: if not self.exchange_path.exists(): return [] with self._db(self.exchange_path) as db: @@ -198,7 +217,7 @@ def _totals(positions:list[dict])->dict: return totals def snapshot(self,wallet_id:str)->dict: - w=self.wallet(wallet_id); entity=w["owner_entity_id"]; positions=self._positions(entity) + w=self.wallet(wallet_id); entity=w["owner_entity_id"]; assets=self._assets(entity); positions=self._positions(entity) with self._db(self.path) as db: watch=[r["instrument_id"] for r in db.execute( "SELECT instrument_id FROM watchlist WHERE wallet_id=? ORDER BY created_at_ms",(wallet_id,)).fetchall()] @@ -209,7 +228,7 @@ def snapshot(self,wallet_id:str)->dict: inst=db.execute("SELECT instrument_id,instrument_class,settlement_currency,status FROM instruments WHERE instrument_id=?",(iid,)).fetchone() if inst: watch_quotes.append({**dict(inst),"market":self._market(db,iid)}) return {"schema":"entity-economic-wallet-snapshot-v1","profile":WALLET_PROFILE,"version":WALLET_VERSION, - "created_at_ms":now_ms(),"wallet":w,"positions":positions, + "created_at_ms":now_ms(),"wallet":w,"assets":assets,"positions":positions, "portfolio_by_currency":self._totals(positions),"orders":self._orders(entity), "entitlements":self._entitlements(entity),"obligations":self._obligations(entity), "watchlist":watch_quotes, @@ -218,7 +237,10 @@ def snapshot(self,wallet_id:str)->dict: "market_value_policy":{"last_settled_trade_only":True,"bid_ask_are_quotes_not_value":True, "offers_are_not_realized_value":True,"unpriced_positions_remain_unpriced":True, "indicative_only":True,"not_accounting_fair_value":True}, - "stock_style":{"enabled":True,"features":["POSITIONS","QUANTITY","COST_BASIS","LAST","BID","ASK", + "asset_model":{"digital_assets_are_first_class_holdings":True, + "ingest_does_not_issue_market_instruments":True, + "ingest_does_not_create_market_value":True}, + "stock_style":{"enabled":True,"features":["ASSETS","POSITIONS","QUANTITY","COST_BASIS","LAST","BID","ASK", "INDICATIVE_MARKET_VALUE","REALIZED_CHANGE","UNREALIZED_CHANGE","ORDERS","ACTIVITY"], "rights_are_not_declared_corporate_shares":True, "legal_classification_not_inferred":True}, From 7a18c76299e7ab6fcc1d8cd1c9ef4e83c8f74af9 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 05:38:24 -0700 Subject: [PATCH 002/111] dco: make economic issuance optional after asset registration --- src/43_DCO_Factory/dco_production_pipeline.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/43_DCO_Factory/dco_production_pipeline.py b/src/43_DCO_Factory/dco_production_pipeline.py index 2d00765b..2689eb54 100644 --- a/src/43_DCO_Factory/dco_production_pipeline.py +++ b/src/43_DCO_Factory/dco_production_pipeline.py @@ -15,10 +15,9 @@ "RIGHTS_PASSPORT_ISSUED", "GLOBAL_PASSPORT_ISSUED", "PROFILE_BTDU_BOUND", - "ECONOMIC_INSTRUMENTS_ISSUED", "PUBLIC_SAFE_EVIDENCE_PUBLISHED", ) -OPTIONAL_STAGES=("BRIDGE_EXPOSURE_BOUND",) +OPTIONAL_STAGES=("ECONOMIC_INSTRUMENTS_ISSUED","BRIDGE_EXPOSURE_BOUND") DEPENDENCY={ "ASSET_BUILT":None, "ASSET_QUALIFIED":"ASSET_BUILT", @@ -28,8 +27,8 @@ "GLOBAL_PASSPORT_ISSUED":"RIGHTS_PASSPORT_ISSUED", "PROFILE_BTDU_BOUND":"GLOBAL_PASSPORT_ISSUED", "ECONOMIC_INSTRUMENTS_ISSUED":"PROFILE_BTDU_BOUND", - "BRIDGE_EXPOSURE_BOUND":"ECONOMIC_INSTRUMENTS_ISSUED", - "PUBLIC_SAFE_EVIDENCE_PUBLISHED":"ECONOMIC_INSTRUMENTS_ISSUED", + "BRIDGE_EXPOSURE_BOUND":"PROFILE_BTDU_BOUND", + "PUBLIC_SAFE_EVIDENCE_PUBLISHED":"PROFILE_BTDU_BOUND", } FORBIDDEN_PUBLIC_KEYS={ "object_id","instrument_id","instrument_ids","venue_id","wallet_id","wallet_ids", @@ -53,10 +52,12 @@ def sha256_hex(v:str,name:str)->str: return s class DCOProductionPipeline: - """Factory-side issuance gate for real DCO assets. + """Asset-first production gate for real DCO assets. - This is an application-layer state machine. It does not replace canonical ENTITY - object, passport, BTDU, EEP, wallet or settlement state. + Registration, qualification and passporting establish the digital asset. + Economic-instrument issuance is optional and controller-directed; it is never + an automatic consequence of ingest. This application layer does not replace + canonical ENTITY object, passport, BTDU, EEP, wallet or settlement state. """ def __init__(self,state_dir:str|Path): self.root=Path(state_dir)/"dco_factory" From d8eb59b97db8a9b886e0a491bf10d93fd8b3f64c Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 05:38:26 -0700 Subject: [PATCH 003/111] tests: enforce asset-first optional market orientation --- tests/test_dco_production_pipeline.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_dco_production_pipeline.py b/tests/test_dco_production_pipeline.py index 11289782..9cc1f2e9 100644 --- a/tests/test_dco_production_pipeline.py +++ b/tests/test_dco_production_pipeline.py @@ -11,7 +11,7 @@ def load(name,path): sys.modules[name]=m; spec.loader.exec_module(m); return m def h(s): return hashlib.sha256(str(s).encode()).hexdigest() -def test_production_pipeline_enforces_passports_before_economics(): +def test_production_pipeline_is_asset_first_and_economics_optional(): m=load("dco_pipeline_test",PIPE) with tempfile.TemporaryDirectory() as td: p=m.DCOProductionPipeline(td) @@ -21,13 +21,16 @@ def test_production_pipeline_enforces_passports_before_economics(): except ValueError: pass sequence=[ "ASSET_BUILT","ASSET_QUALIFIED","PROVENANCE_SEALED","DCO_REGISTERED", - "RIGHTS_PASSPORT_ISSUED","GLOBAL_PASSPORT_ISSUED","PROFILE_BTDU_BOUND", - "ECONOMIC_INSTRUMENTS_ISSUED"] + "RIGHTS_PASSPORT_ISSUED","GLOBAL_PASSPORT_ISSUED","PROFILE_BTDU_BOUND"] for i,s in enumerate(sequence): p.record_stage("DCO-1",s,h(i),{"i":i}) status=p.status("DCO-1") assert status["ready_for_economic_issuance"] is True - assert status["economic_instruments_issued"] is True + assert status["economic_instruments_issued"] is False assert status["bridge_bound"] is False + p.record_stage("DCO-1","BRIDGE_EXPOSURE_BOUND",h("bridge")) + assert p.status("DCO-1")["bridge_bound"] is True + p.record_stage("DCO-1","ECONOMIC_INSTRUMENTS_ISSUED",h("market")) + assert p.status("DCO-1")["economic_instruments_issued"] is True def test_public_safe_manifest_rejects_live_operational_keys(): m=load("dco_pipeline_public_test",PIPE) From ee3b10d72f15bd946e9b99be07e3ec50e66a9ed6 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 05:40:51 -0700 Subject: [PATCH 004/111] wallet: add asset-first upload and DCO ingest --- src/42_ENTITY_Wallet/asset_ingest.py | 86 ++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/42_ENTITY_Wallet/asset_ingest.py diff --git a/src/42_ENTITY_Wallet/asset_ingest.py b/src/42_ENTITY_Wallet/asset_ingest.py new file mode 100644 index 00000000..97eaae95 --- /dev/null +++ b/src/42_ENTITY_Wallet/asset_ingest.py @@ -0,0 +1,86 @@ +from __future__ import annotations +from pathlib import Path +from typing import Any +import hashlib, json, shutil, time + +INGEST_SCHEMA="entity-wallet-asset-ingest-v1" + +def sha256_file(path:str|Path,chunk_size:int=1024*1024)->str: + h=hashlib.sha256() + with Path(path).open("rb") as f: + while True: + block=f.read(chunk_size) + if not block: break + h.update(block) + return h.hexdigest() + +class WalletAssetIngestor: + """Create a Digital Commodity Object from user-selected data. + + Ingest is deliberately asset-first. It registers the asset and optional local + custody copy, but never creates an EEP instrument, listing, price or licence. + """ + def __init__(self,state_dir:str|Path,fabric): + self.state=Path(state_dir) + self.fabric=fabric + self.vault=self.state/"wallet"/"asset_vault" + self.vault.mkdir(parents=True,exist_ok=True) + + def ingest_file(self,controller_entity_id:str,file_path:str|Path,*,title:str|None=None, + commodity_class:str="DATA",measurement_unit:str="USE", + authority_basis:str="CREATOR_CONTROLLED",metadata:dict|None=None, + copy_to_local_vault:bool=True)->dict: + source=Path(file_path).expanduser().resolve() + if not source.is_file(): raise FileNotFoundError(str(source)) + digest=sha256_file(source) + size=source.stat().st_size + meta={ + "wallet_ingest_schema":INGEST_SCHEMA, + "source_filename":source.name, + "size_bytes":size, + "authority_basis":str(authority_basis).upper(), + "controller_asserted_registration_authority":True, + "market_instruments_created_automatically":False, + "market_value_created_by_ingest":False, + **dict(metadata or {}), + } + obj=self.fabric.register_digital_commodity( + controller_entity_id, + str(title or source.name), + digest, + commodity_class=str(commodity_class).upper(), + measurement_unit=str(measurement_unit).upper(), + metadata=meta, + ) + custody=None + if copy_to_local_vault: + target_dir=self.vault/obj["object_id"] + target_dir.mkdir(parents=True,exist_ok=False) + target=target_dir/source.name + shutil.copy2(source,target) + receipt={ + "schema":INGEST_SCHEMA, + "object_id":obj["object_id"], + "controller_entity_id":controller_entity_id, + "title":str(title or source.name), + "content_sha256":digest, + "size_bytes":size, + "source_filename":source.name, + "custody":"LOCAL_WALLET_VAULT", + "market_instruments_created":False, + "created_at_ms":int(time.time()*1000), + } + (target_dir/"INGEST_RECEIPT.json").write_text( + json.dumps(receipt,indent=2,sort_keys=True),encoding="utf-8") + custody={"mode":"LOCAL_WALLET_VAULT","path":str(target), + "receipt_path":str(target_dir/"INGEST_RECEIPT.json")} + return { + "schema":INGEST_SCHEMA, + "digital_asset":obj, + "content_sha256":digest, + "size_bytes":size, + "custody":custody, + "market_instruments_created":False, + "listing_created":False, + "price_created":False, + } From 81135eb7019df0b363158e7e1f0c4ee049dafd98 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 05:40:53 -0700 Subject: [PATCH 005/111] wallet: add native cross-platform desktop application --- tools/entity_wallet_desktop.py | 289 +++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 tools/entity_wallet_desktop.py diff --git a/tools/entity_wallet_desktop.py b/tools/entity_wallet_desktop.py new file mode 100644 index 00000000..e3f7765e --- /dev/null +++ b/tools/entity_wallet_desktop.py @@ -0,0 +1,289 @@ +from __future__ import annotations +from pathlib import Path +import argparse, importlib.util, json, os, sqlite3, sys, uuid +import tkinter as tk +from tkinter import filedialog, messagebox, ttk + +APP_NAME="ENTITY Wallet" + +def repo_root()->Path: + if getattr(sys,"frozen",False) and hasattr(sys,"_MEIPASS"): + return Path(sys._MEIPASS) + return Path(__file__).resolve().parents[1] + +def load_module(name:str,relative:str): + path=repo_root()/relative + spec=importlib.util.spec_from_file_location(name,path) + if spec is None or spec.loader is None: raise RuntimeError(f"Cannot load {path}") + mod=importlib.util.module_from_spec(spec); sys.modules[name]=mod; spec.loader.exec_module(mod) + return mod + +IDENTITY=load_module("entity_wallet_identity","src/01_Core_Runtime/identity/canonical_identity.py") +FABRIC=load_module("entity_wallet_fabric","src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py") +WALLET=load_module("entity_wallet_model","src/42_ENTITY_Wallet/canonical_wallet.py") +INGEST=load_module("entity_wallet_ingest","src/42_ENTITY_Wallet/asset_ingest.py") +EXCHANGE=load_module("entity_wallet_exchange","src/31_Profiles/exchange_protocol.py") + +def default_state()->Path: + env=os.environ.get("ENTITY_STATE_DIR") + if env: return Path(env).expanduser() + if sys.platform=="win32": + return Path(os.environ.get("LOCALAPPDATA",Path.home()))/"Blackmore Technology Group"/"ENTITY" + if sys.platform=="darwin": + return Path.home()/"Library"/"Application Support"/"Blackmore Technology Group"/"ENTITY" + return Path.home()/".local"/"share"/"blackmore-technology-group"/"entity" + +def manifests(state:Path)->list[dict]: + out=[] + root=state/"identity"/"manifests" + if not root.is_dir(): return out + for p in sorted(root.glob("*.json")): + try: + d=json.loads(p.read_text(encoding="utf-8")) + out.append({"entity_id":d.get("entity_id"),"display_name":d.get("display_name") or d.get("entity_id")}) + except Exception: pass + return [x for x in out if x["entity_id"]] + +class Backend: + def __init__(self,state:Path,entity_id:str): + self.state=state; self.entity_id=entity_id + self.identity=IDENTITY.EntityIdentityVault(state) + self.identity.load_manifest(entity_id) + self.fabric=FABRIC.UniversalTransactionFabric(state,self.identity) + self.wallet=WALLET.EntityEconomicWallet(state) + self.exchange=EXCHANGE.ExchangeProtocol(state,self.identity,self.fabric) + self.ingestor=INGEST.WalletAssetIngestor(state,self.fabric) + manifest=self.identity.load_manifest(entity_id) + self.wallet_record=self.wallet.ensure_wallet(entity_id,manifest.get("display_name","ENTITY")+" Wallet") + + def snapshot(self): return self.wallet.snapshot(self.wallet_record["wallet_id"]) + + def market(self)->list[dict]: + if not self.wallet.exchange_path.exists(): return [] + with sqlite3.connect(self.wallet.exchange_path) as db: + db.row_factory=sqlite3.Row + rows=db.execute("""SELECT i.instrument_id,i.instrument_class,i.settlement_currency, + i.underlying_object_id, + (SELECT ll.venue_id FROM listings ll + WHERE ll.instrument_id=i.instrument_id AND ll.status='ACTIVE' + ORDER BY ll.created_at_ms DESC LIMIT 1) venue_id + FROM instruments i WHERE i.status='ACTIVE' + ORDER BY i.instrument_id""").fetchall() + out=[] + for r in rows: + d=dict(r); d["market"]=self.wallet._market(db,d["instrument_id"]); out.append(d) + return out + + def ingest(self,path,title,commodity_class,measurement_unit,authority_basis): + return self.ingestor.ingest_file( + self.entity_id,path,title=title,commodity_class=commodity_class, + measurement_unit=measurement_unit,authority_basis=authority_basis, + copy_to_local_vault=True) + + def submit_order(self,row:dict,side:str,quantity:int,limit_price:int): + venue=row.get("venue_id") + if not venue: raise ValueError("This instrument has no active market listing.") + order=self.exchange.submit_order( + venue,row["instrument_id"],self.entity_id,side,quantity,limit_price, + nonce="wallet-"+uuid.uuid4().hex,tif="GTC") + trades=self.exchange.match_order_book(venue,row["instrument_id"]) + return {"order":order,"trades":trades} + +class AssetDialog(tk.Toplevel): + def __init__(self,parent,path:Path): + super().__init__(parent); self.title("Ingest Digital Asset"); self.resizable(False,False) + self.result=None; self.transient(parent); self.grab_set() + self.vars={ + "title":tk.StringVar(value=path.name), + "class":tk.StringVar(value="DATA"), + "unit":tk.StringVar(value="USE"), + "authority":tk.StringVar(value="CREATOR_CONTROLLED"), + } + ttk.Label(self,text="File").grid(row=0,column=0,sticky="w",padx=12,pady=(12,4)) + ttk.Label(self,text=str(path),wraplength=520).grid(row=0,column=1,sticky="w",padx=12,pady=(12,4)) + labels=[("Title","title"),("Commodity class","class"),("Measurement unit","unit"),("Authority basis","authority")] + for i,(label,key) in enumerate(labels,1): + ttk.Label(self,text=label).grid(row=i,column=0,sticky="w",padx=12,pady=5) + ttk.Entry(self,textvariable=self.vars[key],width=48).grid(row=i,column=1,sticky="ew",padx=12,pady=5) + self.attest=tk.BooleanVar(value=False) + ttk.Checkbutton(self,text="I have authority to register this asset and its provenance.", + variable=self.attest).grid(row=5,column=0,columnspan=2,sticky="w",padx=12,pady=10) + b=ttk.Frame(self); b.grid(row=6,column=0,columnspan=2,sticky="e",padx=12,pady=12) + ttk.Button(b,text="Cancel",command=self.destroy).pack(side="right",padx=4) + ttk.Button(b,text="Ingest Asset",command=self.ok).pack(side="right",padx=4) + + def ok(self): + if not self.attest.get(): + messagebox.showerror("Authority required","Confirm authority before registering the digital asset.",parent=self); return + if not self.vars["title"].get().strip(): + messagebox.showerror("Title required","Enter an asset title.",parent=self); return + self.result={k:v.get().strip() for k,v in self.vars.items()}; self.destroy() + +class OrderDialog(tk.Toplevel): + def __init__(self,parent,row,side): + super().__init__(parent); self.title(f"{side.title()} {row['instrument_id']}"); self.resizable(False,False) + self.result=None; self.transient(parent); self.grab_set(); m=row["market"] + self.qty=tk.StringVar(value="1") + default=m.get("ask") if side=="BUY" else m.get("bid") + self.price=tk.StringVar(value="" if default is None else str(default)) + ttk.Label(self,text=f"{side} {row['instrument_id']}").grid(row=0,column=0,columnspan=2,sticky="w",padx=12,pady=12) + ttk.Label(self,text="Quantity").grid(row=1,column=0,sticky="w",padx=12,pady=5) + ttk.Entry(self,textvariable=self.qty,width=22).grid(row=1,column=1,padx=12,pady=5) + ttk.Label(self,text=f"Limit price ({row['settlement_currency']})").grid(row=2,column=0,sticky="w",padx=12,pady=5) + ttk.Entry(self,textvariable=self.price,width=22).grid(row=2,column=1,padx=12,pady=5) + ttk.Label(self,text=f"Bid: {m.get('bid')} Ask: {m.get('ask')} Last: {m.get('last')}").grid(row=3,column=0,columnspan=2,sticky="w",padx=12,pady=8) + ttk.Button(self,text="Submit",command=self.ok).grid(row=4,column=1,sticky="e",padx=12,pady=12) + + def ok(self): + try: + q=int(self.qty.get()); p=int(self.price.get()) + if q<1 or p<0: raise ValueError + except Exception: + messagebox.showerror("Invalid order","Quantity must be positive and price must be a non-negative integer.",parent=self); return + self.result=(q,p); self.destroy() + +class App(tk.Tk): + def __init__(self,state:Path|None=None,entity:str|None=None): + super().__init__(); self.title(APP_NAME); self.geometry("1180x760"); self.minsize(920,620) + self.backend=None; self.market_rows=[] + self.state_var=tk.StringVar(value=str(state or default_state())) + self.entity_var=tk.StringVar() + self.entity_map={} + self._style(); self._layout() + self.load_state(preselect=entity) + + def _style(self): + style=ttk.Style(self) + if sys.platform=="win32": + try: style.theme_use("vista") + except Exception: pass + style.configure("Title.TLabel",font=("Segoe UI",18,"bold")) + style.configure("Sub.TLabel",foreground="#666") + + def _layout(self): + top=ttk.Frame(self,padding=12); top.pack(fill="x") + ttk.Label(top,text="ENTITY Wallet",style="Title.TLabel").grid(row=0,column=0,sticky="w") + ttk.Label(top,text="Digital assets first. Markets optional.",style="Sub.TLabel").grid(row=1,column=0,sticky="w") + ttk.Label(top,text="State").grid(row=0,column=1,padx=(30,5)) + ttk.Entry(top,textvariable=self.state_var,width=45).grid(row=0,column=2,sticky="ew") + ttk.Button(top,text="Browse",command=self.browse_state).grid(row=0,column=3,padx=4) + ttk.Button(top,text="Load",command=self.load_state).grid(row=0,column=4,padx=4) + ttk.Label(top,text="Identity").grid(row=1,column=1,padx=(30,5)) + self.entity_box=ttk.Combobox(top,textvariable=self.entity_var,state="readonly",width=42) + self.entity_box.grid(row=1,column=2,sticky="ew"); self.entity_box.bind("<>",lambda e:self.activate()) + ttk.Button(top,text="Refresh",command=self.refresh).grid(row=1,column=3,padx=4) + top.columnconfigure(2,weight=1) + + self.tabs=ttk.Notebook(self); self.tabs.pack(fill="both",expand=True,padx=12,pady=(0,12)) + self.assets_tab=ttk.Frame(self.tabs,padding=10); self.market_tab=ttk.Frame(self.tabs,padding=10) + self.positions_tab=ttk.Frame(self.tabs,padding=10); self.orders_tab=ttk.Frame(self.tabs,padding=10) + self.tabs.add(self.assets_tab,text="My Digital Assets"); self.tabs.add(self.market_tab,text="Market") + self.tabs.add(self.positions_tab,text="Market Positions"); self.tabs.add(self.orders_tab,text="Orders") + + bar=ttk.Frame(self.assets_tab); bar.pack(fill="x",pady=(0,8)) + ttk.Button(bar,text="Upload / Ingest Digital Asset",command=self.ingest_asset).pack(side="left") + ttk.Label(bar,text="Uploading creates a DCO asset only — no licence, listing or price is created.",style="Sub.TLabel").pack(side="left",padx=12) + self.assets=self._tree(self.assets_tab,["Title","Class","Unit","SHA-256","Object ID"],[220,100,90,240,290]) + + mbar=ttk.Frame(self.market_tab); mbar.pack(fill="x",pady=(0,8)) + ttk.Button(mbar,text="Buy",command=lambda:self.order("BUY")).pack(side="left") + ttk.Button(mbar,text="Sell",command=lambda:self.order("SELL")).pack(side="left",padx=4) + ttk.Label(mbar,text="Orders match automatically by price/time. Paid matches remain subject to settlement evidence.",style="Sub.TLabel").pack(side="left",padx=12) + self.market=self._tree(self.market_tab,["Instrument","Class","Currency","Bid","Ask","Last","Venue"],[240,140,80,90,90,90,220]) + + self.positions=self._tree(self.positions_tab,["Instrument","Units","Currency","Last","Bid","Ask","Indicative Value"],[260,90,80,90,90,90,140]) + self.orders=self._tree(self.orders_tab,["Instrument","Side","Remaining","Limit","Status"],[280,80,100,100,120]) + self.status=tk.StringVar(value="Ready") + ttk.Label(self,textvariable=self.status,relief="sunken",anchor="w",padding=5).pack(fill="x",side="bottom") + + def _tree(self,parent,columns,widths): + f=ttk.Frame(parent); f.pack(fill="both",expand=True) + t=ttk.Treeview(f,columns=columns,show="headings",selectmode="browse") + for c,w in zip(columns,widths): t.heading(c,text=c); t.column(c,width=w,anchor="w") + y=ttk.Scrollbar(f,orient="vertical",command=t.yview); t.configure(yscrollcommand=y.set) + t.pack(side="left",fill="both",expand=True); y.pack(side="right",fill="y"); return t + + def browse_state(self): + p=filedialog.askdirectory(title="Select ENTITY state folder",initialdir=self.state_var.get()) + if p: self.state_var.set(p); self.load_state() + + def load_state(self,preselect=None): + state=Path(self.state_var.get()).expanduser() + state.mkdir(parents=True,exist_ok=True) + items=manifests(state); self.entity_map={f"{x['display_name']} — {x['entity_id']}":x["entity_id"] for x in items} + self.entity_box["values"]=list(self.entity_map) + if preselect: + for label,eid in self.entity_map.items(): + if eid==preselect: self.entity_var.set(label); break + elif items and not self.entity_var.get(): self.entity_var.set(next(iter(self.entity_map))) + if self.entity_var.get(): self.activate() + else: self.status.set("No ENTITY identity found in this state folder.") + + def activate(self): + try: + eid=self.entity_map.get(self.entity_var.get(),self.entity_var.get()) + self.backend=Backend(Path(self.state_var.get()),eid); self.refresh() + except Exception as e: + self.backend=None; messagebox.showerror("Cannot load wallet",str(e)); self.status.set(str(e)) + + @staticmethod + def clear(tree): + for x in tree.get_children(): tree.delete(x) + + def refresh(self): + if not self.backend: return + try: + snap=self.backend.snapshot(); self.market_rows=self.backend.market() + self.clear(self.assets) + for a in snap.get("assets",[]): + self.assets.insert("", "end", values=(a["title"],a.get("commodity_class"),a.get("measurement_unit"), + a.get("content_sha256") or "",a["object_id"])) + self.clear(self.positions) + for p in snap.get("positions",[]): + m=p["market"]; self.positions.insert("", "end",values=(p["instrument_id"],p["units"],p["settlement_currency"], + m.get("last"),m.get("bid"),m.get("ask"),p.get("market_value_amount_units"))) + self.clear(self.orders) + for o in snap.get("orders",[]): + self.orders.insert("", "end",values=(o["instrument_id"],o["side"],o["remaining"],o["limit_price"],o["status"])) + self.clear(self.market) + for i,row in enumerate(self.market_rows): + m=row["market"]; self.market.insert("", "end",iid=str(i),values=(row["instrument_id"],row["instrument_class"], + row["settlement_currency"],m.get("bid"),m.get("ask"),m.get("last"),row.get("venue_id") or "UNLISTED")) + self.status.set(f"{len(snap.get('assets',[]))} digital assets · {len(snap.get('positions',[]))} market positions · protocol tax 0 · crypto not required") + except Exception as e: + messagebox.showerror("Refresh failed",str(e)); self.status.set(str(e)) + + def ingest_asset(self): + if not self.backend: messagebox.showwarning("Wallet","Load an ENTITY identity first."); return + f=filedialog.askopenfilename(title="Choose data or digital asset to ingest") + if not f: return + d=AssetDialog(self,Path(f)); self.wait_window(d) + if not d.result: return + try: + r=self.backend.ingest(f,d.result["title"],d.result["class"],d.result["unit"],d.result["authority"]) + a=r["digital_asset"]; self.refresh() + messagebox.showinfo("Digital asset created", + f"Registered as {a['object_id']}\n\nSHA-256:\n{r['content_sha256']}\n\nNo market instrument or price was created.") + except Exception as e: messagebox.showerror("Ingest failed",str(e)) + + def order(self,side): + if not self.backend: return + sel=self.market.selection() + if not sel: messagebox.showwarning("Market","Select a market instrument."); return + row=self.market_rows[int(sel[0])] + if not row.get("venue_id"): messagebox.showwarning("Market","This asset has no active market listing."); return + d=OrderDialog(self,row,side); self.wait_window(d) + if not d.result: return + try: + q,p=d.result; result=self.backend.submit_order(row,side,q,p); self.refresh() + n=len(result["trades"]) + messagebox.showinfo("Order submitted", + f"Order accepted.\nImmediate matches: {n}\n\nMatched paid trades remain pending until settlement requirements are satisfied.") + except Exception as e: messagebox.showerror("Order failed",str(e)) + +def main(): + ap=argparse.ArgumentParser(description="Native ENTITY economic wallet") + ap.add_argument("--state"); ap.add_argument("--entity") + args=ap.parse_args(); App(Path(args.state) if args.state else None,args.entity).mainloop() + +if __name__=="__main__": main() From 4aa3a0714c34a2eb2bce5367329524cbbaa40830 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 05:40:55 -0700 Subject: [PATCH 006/111] wallet: add cross-platform desktop packager --- tools/build_entity_wallet_desktop.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tools/build_entity_wallet_desktop.py diff --git a/tools/build_entity_wallet_desktop.py b/tools/build_entity_wallet_desktop.py new file mode 100644 index 00000000..d2cccf7c --- /dev/null +++ b/tools/build_entity_wallet_desktop.py @@ -0,0 +1,28 @@ +from __future__ import annotations +from pathlib import Path +import os, subprocess, sys + +ROOT=Path(__file__).resolve().parents[1] +ENTRY=ROOT/"tools"/"entity_wallet_desktop.py" +FILES=[ + "src/01_Core_Runtime/identity/canonical_identity.py", + "src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py", + "src/31_Profiles/exchange_protocol.py", + "src/42_ENTITY_Wallet/canonical_wallet.py", + "src/42_ENTITY_Wallet/asset_ingest.py", +] + +def main(): + sep=os.pathsep + cmd=[sys.executable,"-m","PyInstaller","--noconfirm","--clean","--windowed", + "--name","ENTITY-Wallet", + "--hidden-import","cryptography.hazmat.primitives.asymmetric.ed25519", + "--hidden-import","cryptography.hazmat.primitives.serialization"] + for rel in FILES: + src=ROOT/rel; dest=str(Path(rel).parent) + cmd += ["--add-data",f"{src}{sep}{dest}"] + cmd.append(str(ENTRY)) + subprocess.run(cmd,cwd=ROOT,check=True) + print(ROOT/"dist") + +if __name__=="__main__": main() From 17159ad82c08f890e0eed4ea001cb4731f77d85f Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 05:40:57 -0700 Subject: [PATCH 007/111] ci: build ENTITY Wallet for Windows macOS and Linux --- .github/workflows/entity-wallet-desktop.yml | 34 +++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/entity-wallet-desktop.yml diff --git a/.github/workflows/entity-wallet-desktop.yml b/.github/workflows/entity-wallet-desktop.yml new file mode 100644 index 00000000..14adbbc3 --- /dev/null +++ b/.github/workflows/entity-wallet-desktop.yml @@ -0,0 +1,34 @@ +name: ENTITY Wallet Desktop + +on: + workflow_dispatch: + pull_request: + paths: + - "src/42_ENTITY_Wallet/**" + - "src/43_DCO_Factory/dco_production_pipeline.py" + - "tools/entity_wallet_desktop.py" + - "tools/build_entity_wallet_desktop.py" + - ".github/workflows/entity-wallet-desktop.yml" + +jobs: + build: + strategy: + matrix: + os: [windows-latest, macos-latest, ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y python3-tk + - run: python -m pip install --upgrade pip + - run: python -m pip install pyinstaller cryptography + - run: python tools/build_entity_wallet_desktop.py + - uses: actions/upload-artifact@v4 + with: + name: ENTITY-Wallet-${{ runner.os }} + path: | + dist/ENTITY-Wallet* + dist/ENTITY-Wallet.app/** From 9fc073becff409f50b8aaec42cd986d2d2a8b9ba Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 05:42:04 -0700 Subject: [PATCH 008/111] tests: qualify wallet asset ingest without automatic licensing --- tests/test_wallet_asset_ingest.py | 48 +++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/test_wallet_asset_ingest.py diff --git a/tests/test_wallet_asset_ingest.py b/tests/test_wallet_asset_ingest.py new file mode 100644 index 00000000..c44e9a2f --- /dev/null +++ b/tests/test_wallet_asset_ingest.py @@ -0,0 +1,48 @@ +from __future__ import annotations +from pathlib import Path +import hashlib, importlib.util, json, sqlite3, sys, tempfile + +ROOT=Path(__file__).resolve().parents[1] + +def load(name,rel): + p=ROOT/rel; spec=importlib.util.spec_from_file_location(name,p) + m=importlib.util.module_from_spec(spec); sys.modules[name]=m; spec.loader.exec_module(m); return m + +class FakeFabric: + def __init__(self): self.calls=[] + def register_digital_commodity(self,controller,title,content_sha256,*,commodity_class,measurement_unit,metadata): + self.calls.append((controller,title,content_sha256,commodity_class,measurement_unit,metadata)) + return {"object_id":"obj3-test","controller_entity_id":controller,"title":title, + "content_sha256":content_sha256,"digital_commodity_object":True} + +def test_ingest_registers_asset_without_market_instrument(): + m=load("wallet_asset_ingest_test","src/42_ENTITY_Wallet/asset_ingest.py") + with tempfile.TemporaryDirectory() as td: + src=Path(td)/"robotics.bin"; src.write_bytes(b"robotics-asset") + f=FakeFabric(); ing=m.WalletAssetIngestor(Path(td)/"state",f) + r=ing.ingest_file("ent2-test",src,title="Robot Asset",commodity_class="ROBOTICS_DATA", + measurement_unit="ASSET",authority_basis="CREATOR_CONTROLLED") + assert r["market_instruments_created"] is False + assert r["listing_created"] is False + assert r["price_created"] is False + assert r["content_sha256"]==hashlib.sha256(b"robotics-asset").hexdigest() + assert Path(r["custody"]["path"]).is_file() + assert f.calls[0][5]["market_instruments_created_automatically"] is False + +def test_wallet_lists_digital_assets_separately_from_positions(): + m=load("wallet_assets_view_test","src/42_ENTITY_Wallet/canonical_wallet.py") + with tempfile.TemporaryDirectory() as td: + state=Path(td); dbp=state/"entity_v3"/"universal_fabric.sqlite"; dbp.parent.mkdir(parents=True) + db=sqlite3.connect(dbp) + db.execute("""CREATE TABLE objects(object_id TEXT PRIMARY KEY,controller_entity_id TEXT,object_type TEXT, + title TEXT,content_sha256 TEXT,descriptor_json TEXT,status TEXT,created_at_ms INTEGER,signature_json TEXT)""") + descriptor=json.dumps({"digital_commodity":True,"commodity_class":"ROBOTICS","measurement_unit":"ASSET"}) + db.execute("INSERT INTO objects VALUES(?,?,?,?,?,?,?,?,?)", + ("obj3-r","OWNER","DATASET","Robotics Asset","a"*64,descriptor,"ACTIVE",1,"{}")) + db.commit(); db.close() + w=m.EntityEconomicWallet(state); wr=w.ensure_wallet("OWNER","Owner Wallet") + snap=w.snapshot(wr["wallet_id"]) + assert len(snap["assets"])==1 + assert snap["assets"][0]["title"]=="Robotics Asset" + assert snap["positions"]==[] + assert snap["asset_model"]["ingest_does_not_issue_market_instruments"] is True From 3aa1958b35740fe81d50849bb1863b18fa9cdbb0 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 05:42:06 -0700 Subject: [PATCH 009/111] docs: restore asset-first DCO production model --- docs/v3.4/DCO_PRODUCTION_PIPELINE_20261002.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/v3.4/DCO_PRODUCTION_PIPELINE_20261002.md b/docs/v3.4/DCO_PRODUCTION_PIPELINE_20261002.md index ddf0d838..fa0e2b30 100644 --- a/docs/v3.4/DCO_PRODUCTION_PIPELINE_20261002.md +++ b/docs/v3.4/DCO_PRODUCTION_PIPELINE_20261002.md @@ -2,15 +2,15 @@ ENTITY display version: **3.4.3** -The DCO Factory now defines a mandatory application-layer production path for serious economic assets: +The DCO Factory now defines an asset-first application-layer production path for serious digital assets: -`BUILD ASSET → QUALIFY → SEAL PROVENANCE → REGISTER DCO → RIGHTS PASSPORT → GLOBAL PASSPORT → PROFILE + BTDU BINDING → ECONOMIC INSTRUMENTS → OPTIONAL SHARED BRIDGE → PUBLIC-SAFE EVIDENCE` +`BUILD ASSET → QUALIFY → SEAL PROVENANCE → REGISTER DCO → RIGHTS PASSPORT → GLOBAL PASSPORT → PROFILE + BTDU BINDING → PUBLIC-SAFE EVIDENCE`\n\nOptional after the asset exists: `ECONOMIC INSTRUMENTS / MARKET LISTING` and `SHARED BRIDGE EXPOSURE`. ## Why the order matters A DCO must not reach factory-managed economic issuance merely because an identifier and supply exist. -The production-pipeline registry blocks the `ECONOMIC_INSTRUMENTS_ISSUED` stage unless: +The production-pipeline registry blocks optional `ECONOMIC_INSTRUMENTS_ISSUED` unless: - the underlying asset exists; - qualification evidence exists; @@ -20,7 +20,7 @@ The production-pipeline registry blocks the `ECONOMIC_INSTRUMENTS_ISSUED` stage - a Global Passport is issued; - profile and BTDU binding is complete. -This is an application-layer issuance gate. It does not replace the canonical Universal Transaction Fabric, Rights Passport, Global Passport, BTDU, EEP, wallet or settlement stores. +This is an application-layer asset qualification gate. It does not replace the canonical Universal Transaction Fabric, Rights Passport, Global Passport, BTDU, EEP, wallet or settlement stores. **Ingesting or registering a DCO does not automatically issue a licence, create market supply, list an instrument or establish a price.** ## Rights Passport From 43898c644c5628f583727321496b4b6afc3394f9 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 05:42:09 -0700 Subject: [PATCH 010/111] docs: document native wallet and digital asset upload --- docs/v3.4/ENTITY_WALLET_20261002.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/v3.4/ENTITY_WALLET_20261002.md b/docs/v3.4/ENTITY_WALLET_20261002.md index 817aa3fc..cef6ebb0 100644 --- a/docs/v3.4/ENTITY_WALLET_20261002.md +++ b/docs/v3.4/ENTITY_WALLET_20261002.md @@ -2,7 +2,7 @@ Date: 2026-10-02 -ENTITY v3.4.3 now includes a repository-safe **economic wallet application layer** over the existing ENTITY Exchange Protocol (EEP) and Originator Participation Profile. +ENTITY v3.4.3 now includes a repository-safe **economic wallet application layer** over the Universal Transaction Fabric, ENTITY Exchange Protocol (EEP) and Originator Participation Profile. The wallet treats controller-held Digital Commodity Objects as first-class assets, independently from any later market instruments. This addition does **not** change the ENTITY protocol version, the EEP wire schema, the BTDU schema, or the no-token/no-protocol-tax model. @@ -111,3 +111,24 @@ The qualification covers: This repository publishes generic wallet source, tooling, tests and qualification evidence only. It does **not** publish production wallet databases, live wallet IDs, treasury identity material, private signing/recovery keys, local launchers, payment credentials, runtime SQLite state, bank details, production snapshots or other private operational state. + + +## Native desktop wallet and asset ingest + +The canonical user experience is now a native desktop application, not a generated HTML page. The source entry point is `tools/entity_wallet_desktop.py`. + +The native wallet provides: + +- **My Digital Assets** — controller-held DCOs from the Universal Transaction Fabric; +- **Upload / Ingest Digital Asset** — select a local file, hash it, copy it into the local wallet vault and register it as a DCO; +- **Market** — live instrument bid/ask/last observations and signed Buy/Sell order submission; +- **Market Positions** — rights positions kept separate from the underlying assets; +- **Orders** — active/recent exchange orders. + +Asset ingest is deliberately non-economic by default: + +`UPLOAD → SHA-256 → LOCAL VAULT → DCO REGISTRATION → MY DIGITAL ASSETS` + +It does **not** automatically create an EEP instrument, licence, listing, ask price or market value. The controller may later choose to create standardized tradeable rights. + +Cross-platform build tooling is provided by `tools/build_entity_wallet_desktop.py` and the `ENTITY Wallet Desktop` workflow for Windows, macOS and Linux. From 0ffe2eb8a5fc4660e9deef332ab4ac877bc36efc Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 05:42:11 -0700 Subject: [PATCH 011/111] docs: classify BIRFR as an ingested robotics asset first --- docs/v3.4/BIRFR1_DCO_000001_20261002.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/v3.4/BIRFR1_DCO_000001_20261002.md b/docs/v3.4/BIRFR1_DCO_000001_20261002.md index 677a24b1..83b4c92c 100644 --- a/docs/v3.4/BIRFR1_DCO_000001_20261002.md +++ b/docs/v3.4/BIRFR1_DCO_000001_20261002.md @@ -69,7 +69,13 @@ They are real BTG-generated asset bytes and qualify the multimodal data path, bu High-fidelity simulation and real-hardware episodes belong in later versioned BIRFR releases rather than being silently mixed into v0.1.0. -## Economic family +## Asset orientation + +BIRFR-1 is first and foremost a **BTG-created robotics digital asset ingested into ENTITY**. Its dataset bytes, provenance, qualification, DCO registration and passport identity exist independently from any licence or market instrument. + +Creating or ingesting BIRFR-1 does not inherently create a price and does not require BTG to offer licences. The controlled prelaunch EEP instruments described below are qualification/demo market state only; market participation is optional and must be explicitly activated by the controller. + +## Optional economic family The BIRFR dataset-specific Strategic template uses eleven commercial archetypes: From 8b7fb208fd9b87dd4f2300ac041e127d71b480df Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 05:42:13 -0700 Subject: [PATCH 012/111] docs: restore robotics DCOs to asset portfolio orientation --- ...ROBOTICS_DCO_ASSET_ORIENTATION_20261003.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docs/v3.4/ROBOTICS_DCO_ASSET_ORIENTATION_20261003.md diff --git a/docs/v3.4/ROBOTICS_DCO_ASSET_ORIENTATION_20261003.md b/docs/v3.4/ROBOTICS_DCO_ASSET_ORIENTATION_20261003.md new file mode 100644 index 00000000..5be8f96e --- /dev/null +++ b/docs/v3.4/ROBOTICS_DCO_ASSET_ORIENTATION_20261003.md @@ -0,0 +1,28 @@ +# Robotics DCO Asset Orientation + +Date: 2026-10-03 +ENTITY display version: 3.4.3 + +The numbered robotics portfolio is an **asset portfolio inside ENTITY**, not a mandate to issue robotics licences. + +## Canonical orientation + +`ROBOTICS OUTPUT → QUALIFIED DIGITAL ASSET → DCO → WALLET ASSET HOLDING` + +Only if the controller chooses to expose economic rights does the path continue: + +`DCO → OPTIONAL EEP INSTRUMENT → OPTIONAL LISTING → MARKET PRICE DISCOVERY` + +A DCO may therefore exist indefinitely as a private or controlled digital commodity asset with no market supply and no price. + +## Wallet placement + +Robotics assets belong in **My Digital Assets**. Tradeable rights, if deliberately issued later, belong in **Market Positions**. The two are intentionally separate. + +## Existing BIRFR prelaunch instruments + +BIRFR-1 remains a valid BTG-originated DCO asset. Its previously generated EEP instruments are retained as controlled prelaunch qualification evidence and are not the definition of the asset. No market value exists until real settled market activity establishes one. + +## Universal rule + +The same model applies to code, research, patient-authorized data, farm observations, industrial data, creative assets, models, algorithms and other legitimate digital commodities. Uploading data to ENTITY registers the asset; it does not silently sell, license or price it. From faff7dd9e44c2ae4c18023a941bb319b1a413ade Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 05:44:37 -0700 Subject: [PATCH 013/111] wallet: initialize universal fabric asset store From 1d847ccf30227e6f809dfbe7d1ad96f3a8da20b5 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 05:45:25 -0700 Subject: [PATCH 014/111] wallet: bind canonical asset store --- src/42_ENTITY_Wallet/canonical_wallet.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/42_ENTITY_Wallet/canonical_wallet.py b/src/42_ENTITY_Wallet/canonical_wallet.py index 8903a53d..d847a2f8 100644 --- a/src/42_ENTITY_Wallet/canonical_wallet.py +++ b/src/42_ENTITY_Wallet/canonical_wallet.py @@ -12,11 +12,12 @@ def now_ms()->int: return int(time.time()*1000) def rid(prefix:str)->str: return prefix+"-"+secrets.token_hex(12) class EntityEconomicWallet: - """Identity-bound stock-style view over canonical ENTITY economic records.""" + """Identity-bound wallet model over canonical ENTITY asset and economic records.""" def __init__(self,state_dir:str|Path): self.state=Path(state_dir) self.exchange_path=self.state/"entity_v3_exchange.sqlite" self.economic_path=self.state/"entity_v3_economic_participation.sqlite" + self.fabric_path=self.state/"entity_v3"/"universal_fabric.sqlite" self.root=self.state/"wallet"; self.root.mkdir(parents=True,exist_ok=True) self.path=self.root/"entity_wallet.sqlite"; self._init() From a2badfc8cf33503530e6b9ef3ef283e964c9f4c6 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:07:07 -0700 Subject: [PATCH 015/111] lineage: add signed v3.4.3 release-origin attestation --- ENTITY_CURRENT_RELEASE_ORIGIN.json | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 ENTITY_CURRENT_RELEASE_ORIGIN.json diff --git a/ENTITY_CURRENT_RELEASE_ORIGIN.json b/ENTITY_CURRENT_RELEASE_ORIGIN.json new file mode 100644 index 00000000..314193e1 --- /dev/null +++ b/ENTITY_CURRENT_RELEASE_ORIGIN.json @@ -0,0 +1,29 @@ +{ + "body": { + "asset_provenance_is_separate_from_protocol_origin": true, + "automatic_protocol_royalty_bps": 0, + "created_at_ms": 1791032742763, + "economic_participation_requires_explicit_terms": true, + "historical_release_is_immutable_target": true, + "origin_lineage_id": "entity-origin:shawn-btg-entity@1.0", + "origin_lineage_sha256": "9d3bdc0ef2e6a27de5368541f6bc014fdec93b2c7af9770d163c804521b9efff", + "protocol_entity_id": "ent2-m3nofxtv3zwldhwuonw3h67hqjh2zambrhj4kaaulncpt4amkcua", + "release_commit_sha1": "528b70aabd05b1e930b77e4933f157731e47274f", + "release_ref": "entity-release:v3.4.3", + "release_tag": "v3.4.3", + "release_tree_sha1": "f13e42f515dcc4cd985e9b832e0ecfec416ce423", + "root_originator_entity_id": "ent2-6eoiiztjyhmyh2tbr5psmofcduwvjledubhoiqwo6mjfoqkn6ica", + "schema": "entity-protocol-release-origin-v1", + "steward_entity_id": "ent2-kkov66eoh23h3zgr4pe4njsbkayn2kxad6vfyirqvuqrty52clpa" + }, + "body_sha256": "150c4e02671b2175a45df921fcf6761119a6c587001c9b8fa039dd01ffe0e494", + "signature": { + "entity_id": "ent2-m3nofxtv3zwldhwuonw3h67hqjh2zambrhj4kaaulncpt4amkcua", + "key_id": "sig-a31ded371cfb5019", + "payload_sha256": "150c4e02671b2175a45df921fcf6761119a6c587001c9b8fa039dd01ffe0e494", + "signature": "cWSubQ8c3pJiZQArSlffOAZMlMfSkn6lb3lNDpWSv2Ol05zDY9Z0do7FoLu6GoBwf1xOZDIV6ekh6IHAhInsBg", + "signature_schema": "entity-signature-record-v2", + "signed_at_ms": 1791032742806, + "suite": "ENTITY-SIG-ED25519-v1" + } +} From 513fc6195d717de1b85dff1670cfad8b5d5f16c3 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:07:09 -0700 Subject: [PATCH 016/111] lineage: publish v3.4.3 origin attestation checksum --- ENTITY_CURRENT_RELEASE_ORIGIN.json.sha256 | 1 + 1 file changed, 1 insertion(+) create mode 100644 ENTITY_CURRENT_RELEASE_ORIGIN.json.sha256 diff --git a/ENTITY_CURRENT_RELEASE_ORIGIN.json.sha256 b/ENTITY_CURRENT_RELEASE_ORIGIN.json.sha256 new file mode 100644 index 00000000..2311b76e --- /dev/null +++ b/ENTITY_CURRENT_RELEASE_ORIGIN.json.sha256 @@ -0,0 +1 @@ +aa4d7b32778bda263c097a8a46a3f654070b0ae48f3142664bfe485e6ce04c17 ENTITY_CURRENT_RELEASE_ORIGIN.json From c0fdf611dc4b128da837de03356e3850abbb582d Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:10:10 -0700 Subject: [PATCH 017/111] fabric: preserve asset type when registering DCOs --- .../canonical_universal_fabric.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py b/src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py index c49fc36a..96f3d0e7 100644 --- a/src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py +++ b/src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py @@ -399,9 +399,15 @@ def visit(node): visiting.remove(node); visited.add(node); return True if any(not visit(oid) for oid in sorted(object_ids) if oid not in visited): failures.append("provenance_cycle") return {"valid":not failures,"failures":failures,"root_object_id":b.get("root_object_id"),"semantic_sha256":b.get("semantic_sha256"),"provider_independent":bool(b.get("provider_independent")),"portable_verification_uses_embedded_manifests":True,"primitive_set_complete":set(b.get("primitives") or [])==set(PRIMITIVES),"verified_manifest_count":len(manifests)} - def register_digital_commodity(self,controller_entity_id:str,title:str,content_sha256:str,*,commodity_class="DATA",measurement_unit="USE",metadata=None)->dict: - descriptor={"digital_commodity":True,"commodity_class":str(commodity_class).upper(),"measurement_unit":str(measurement_unit).upper(),"raw_data_transfer_default":False,"metadata":dict(metadata or {})} - result=self.register_object(controller_entity_id,"DATASET",title,descriptor=descriptor,content_sha256=content_sha256) + def register_digital_commodity(self,controller_entity_id:str,title:str,content_sha256:str,*,commodity_class="DATA",measurement_unit="USE",metadata=None,object_type="DATASET")->dict: + """Register the governed asset itself as a Digital Commodity Object. + + Registration does not issue an EEP instrument, list the asset, transfer + ownership, or establish market value. object_type preserves whether the + asset is SOFTWARE, MODEL, DATASET, DOCUMENT, or another ENTITY type. + """ + descriptor={"digital_commodity":True,"commodity_class":str(commodity_class).upper(),"measurement_unit":str(measurement_unit).upper(),"raw_data_transfer_default":False,"metadata":dict(metadata or {})} + result=self.register_object(controller_entity_id,str(object_type or "DATASET").upper(),title,descriptor=descriptor,content_sha256=content_sha256) result["digital_commodity_object"]=True; return result def register_ai_agent(self,controller_entity_id:str,agent_entity_id:str,title:str,*,model_ref=None,policy_refs=None,capabilities=None)->dict: From 32cfe526c6c6a713472c4b9e610d2f47b122d8ad Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:10:53 -0700 Subject: [PATCH 018/111] ingest: carry DCOs through canonical provenance and passport pipeline --- .../continuous_ingestion.py | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/38_Global_Passports/continuous_ingestion.py b/src/38_Global_Passports/continuous_ingestion.py index 8b08a4a7..bfd9dcf8 100644 --- a/src/38_Global_Passports/continuous_ingestion.py +++ b/src/38_Global_Passports/continuous_ingestion.py @@ -29,7 +29,10 @@ def __init__(self,state_root,identity,fabric,evidence_registry,rights_passports, self.protocol_release_ref=protocol_release_ref def ingest_file(self,path,controller_entity_id:str,profile_refs:list[str],*,logical_path:str|None=None, previous_object_id:str|None=None,rights_actions:list[str]|None=None,version:str="1.0", - protocol_release_ref:str|None=None,btdu_binding:dict|None=None)->dict: + protocol_release_ref:str|None=None,btdu_binding:dict|None=None,title:str|None=None, + object_type:str|None=None,digital_commodity:bool=False,commodity_class:str="DATA", + measurement_unit:str="ASSET",commodity_metadata:dict|None=None, + industry_context:dict|None=None,rights_constraints:dict|None=None)->dict: src=Path(path).resolve() if not src.is_file(): raise FileNotFoundError(src) content_sha=sha256_file(src); vault_path=self.vault/content_sha[:2]/content_sha @@ -38,10 +41,22 @@ def ingest_file(self,path,controller_entity_id:str,profile_refs:list[str],*,logi if sha256_file(vault_path)!=content_sha: raise RuntimeError("content vault verification failed") descriptor={"logical_path":logical_path or src.name,"source_filename":src.name,"source_bytes":src.stat().st_size, "content_addressed":True,"custody_provider":"BTG_LOCAL_CONTENT_VAULT","provider_is_authority":False} - obj=self.fabric.register_object(controller_entity_id,object_type_for(src),src.name,descriptor=descriptor,content_sha256=content_sha) + kind=str(object_type or object_type_for(src)).upper() + asset_title=str(title or src.name) + if digital_commodity: + commodity_meta={**descriptor,**dict(commodity_metadata or {}), + "market_instruments_created_automatically":False, + "market_value_created_by_ingest":False} + obj=self.fabric.register_digital_commodity(controller_entity_id,asset_title,content_sha, + commodity_class=commodity_class,measurement_unit=measurement_unit, + metadata=commodity_meta,object_type=kind) + else: + obj=self.fabric.register_object(controller_entity_id,kind,asset_title,descriptor=descriptor,content_sha256=content_sha) ev=self.evidence.issue_evidence(controller_entity_id,"DOCUMENT",obj["object_id"],content_sha,provenance_refs=[previous_object_id] if previous_object_id else []) actions=rights_actions or ["INSPECT","READ","COPY","DERIVE"] - right=self.fabric.grant_right(obj["object_id"],controller_entity_id,controller_entity_id,actions,constraints={"purpose":"BTG_ENGINEERING"},economic_terms={"monetary_value_asserted":False}) + constraints={"purpose":"ENTITY_ASSET_CONTROL",**dict(rights_constraints or {})} + right=self.fabric.grant_right(obj["object_id"],controller_entity_id,controller_entity_id,actions, + constraints=constraints,economic_terms={"monetary_value_asserted":False}) rp=self.rights.issue(controller_entity_id,obj["object_id"],[{"effect":"ALLOW","actions":actions}],version=version, custody=[{"provider":"BTG_LOCAL_CONTENT_VAULT","locator":f"sha256:{content_sha}","content_sha256":content_sha,"provider_is_authority":False,"credentials_included":False}], provenance_refs=[ev["evidence_id"]],economic_terms={"underlying_information_remains_nonrival":True}) @@ -51,13 +66,16 @@ def ingest_file(self,path,controller_entity_id:str,profile_refs:list[str],*,logi gp=self.global_passports.issue(controller_entity_id,obj["object_id"],rp["passport_id"],profile_refs,version=version, evidence_refs=[ev["evidence_id"]],provenance_refs=[p["edge_id"] for p in prov], standards_mappings=[],economic_state={"state":"POTENTIAL","amount_units":0,"currency":"UNSPECIFIED"}, - industry_context={"continuous_ingestion":True,"logical_path":descriptor["logical_path"]}, + industry_context={"continuous_ingestion":True,"logical_path":descriptor["logical_path"], + "digital_commodity":bool(digital_commodity),**dict(industry_context or {})}, protocol_release_ref=protocol_release_ref or self.protocol_release_ref,btdu_binding=btdu_binding) val=self.fabric.record_value(controller_entity_id,obj["object_id"],0,"UNSPECIFIED",state="POTENTIAL", basis_ref="v3.4-zero-value-baseline-no-market-or-accounting-value-asserted") return {"object":obj,"evidence":ev,"right":right,"rights_passport":rp,"global_passport":gp, "provenance":prov,"value":val,"vault_sha256":content_sha,"vault_path":str(vault_path), - "continuous_provenance":True,"custody_is_not_authority":True} + "continuous_provenance":True,"custody_is_not_authority":True, + "digital_commodity":bool(digital_commodity),"market_instruments_created":False, + "market_value_created_by_ingest":False} def ingest_directory(self,path,controller_entity_id:str,profile_refs:list[str],*,prefix:str="",rights_actions:list[str]|None=None)->dict: root=Path(path).resolve(); records=[] @@ -65,7 +83,9 @@ def ingest_directory(self,path,controller_entity_id:str,profile_refs:list[str],* if not src.is_file() or any(part in EXCLUDED_DIRS for part in src.parts): continue rel=src.relative_to(root).as_posix(); logical=(prefix.rstrip("/")+"/"+rel).lstrip("/") if prefix else rel records.append(self.ingest_file(src,controller_entity_id,profile_refs,logical_path=logical,rights_actions=rights_actions)) - inventory=hashlib.sha256("\n".join(f"{r['object']['content_sha256']} {r['object']['descriptor']['logical_path']}" for r in records).encode()).hexdigest() + inventory=hashlib.sha256("\n".join( + f"{r['object']['content_sha256']} {r['object']['descriptor'].get('logical_path') or (r['object']['descriptor'].get('metadata') or {}).get('logical_path','')}" + for r in records).encode()).hexdigest() return {"schema":"entity-v3-continuous-ingest-result-v1","files":len(records),"inventory_sha256":inventory, "object_ids":[r["object"]["object_id"] for r in records],"global_passport_ids":[r["global_passport"]["passport_id"] for r in records], "content_addressed":True,"custody_is_not_authority":True,"economic_value_invented":False} From a0082613c9db8e7fffaa8ba8d4248b4739331e9a Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:11:15 -0700 Subject: [PATCH 019/111] sdk: route Digital Commodity ingest through canonical package pipeline --- .../canonical_global_passport_sdk.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/sdk/global_passport_sdk/canonical_global_passport_sdk.py b/sdk/global_passport_sdk/canonical_global_passport_sdk.py index 19536d95..0717351b 100644 --- a/sdk/global_passport_sdk/canonical_global_passport_sdk.py +++ b/sdk/global_passport_sdk/canonical_global_passport_sdk.py @@ -28,10 +28,10 @@ def compose_profiles(self,*names_or_refs:str)->dict: text=str(item); refs.append(text if text.startswith("entity-profile:") else self.profile_ref(text)) return self.profiles.resolve_stack(refs) def register_file(self,path,controller_entity_id:str,*profiles:str,logical_path:str|None=None, - previous_object_id:str|None=None,version:str="1.0",btdu_binding:dict|None=None)->dict: + previous_object_id:str|None=None,version:str="1.0",btdu_binding:dict|None=None,**kwargs)->dict: stack=self.compose_profiles(*profiles) result=self.ingestion.ingest_file(path,controller_entity_id,stack["profile_refs"],logical_path=logical_path, - previous_object_id=previous_object_id,version=version,btdu_binding=btdu_binding) + previous_object_id=previous_object_id,version=version,btdu_binding=btdu_binding,**kwargs) return {"object_id":result["object"]["object_id"],"content_sha256":result["object"]["content_sha256"], "rights_passport_id":result["rights_passport"]["passport_id"], "global_passport_id":result["global_passport"]["passport_id"], @@ -50,8 +50,14 @@ def map_external(self,package:str,standard:str,record:dict)->dict: def ingest_package_file(self,path,controller_entity_id:str,package:str,config:dict,asset_kind:str,**kwargs)->dict: plan=self.package_plan(package,config,asset_kind) + options=dict(kwargs) + options.setdefault("object_type",plan["object_type"]) + context=dict(options.pop("industry_context",{}) or {}) + context.update({"industry_package":plan["package"],"asset_kind":str(asset_kind), + "configuration_sha256":plan["configuration_sha256"]}) + options["industry_context"]=context result=self.ingestion.ingest_file(path,controller_entity_id,plan["profile_refs"], - rights_actions=plan["rights_actions"],**kwargs) + rights_actions=plan["rights_actions"],**options) return {"deployment_plan":plan,"object_id":result["object"]["object_id"], "global_passport_id":result["global_passport"]["passport_id"], "content_sha256":result["object"]["content_sha256"], From 9f2e3d3f09cd80708693602b49eee7aa00105111 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:12:26 -0700 Subject: [PATCH 020/111] wallet: resolve canonical protocol and asset lineage separately --- src/42_ENTITY_Wallet/lineage_resolver.py | 136 +++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 src/42_ENTITY_Wallet/lineage_resolver.py diff --git a/src/42_ENTITY_Wallet/lineage_resolver.py b/src/42_ENTITY_Wallet/lineage_resolver.py new file mode 100644 index 00000000..17293695 --- /dev/null +++ b/src/42_ENTITY_Wallet/lineage_resolver.py @@ -0,0 +1,136 @@ +from __future__ import annotations +from pathlib import Path +import json, sqlite3 + +CANONICAL_LINEAGE_ID="entity-origin:shawn-btg-entity@1.0" + +class WalletLineageResolver: + """Resolve the two lineage planes used by ENTITY without collapsing them. + + protocol_lineage describes canonical ENTITY ancestry/profile context. + asset_lineage describes actual controller/provenance relationships. + ADAM, NIKI and BTDU are reported as capability/evidence bindings, never as + inferred owners or economic beneficiaries. + """ + def __init__(self,state_dir:str|Path): + self.state=Path(state_dir) + self.origin_path=self.state/"entity_protocol_origin.sqlite" + self.passport_path=self.state/"entity_v3_4_global_passports.sqlite" + self.profile_path=self.state/"entity_v3_4_global_profiles.sqlite" + self.fabric_path=self.state/"entity_v3"/"universal_fabric.sqlite" + + def _name(self,entity_id:str|None)->str|None: + if not entity_id: return None + p=self.state/"identity"/"manifests"/f"{entity_id}.json" + try: + d=json.loads(p.read_text(encoding="utf-8")) + return str(d.get("display_name") or d.get("aliases",[None])[0] or entity_id) + except Exception: + return str(entity_id) + + @staticmethod + def _db(path:Path): + db=sqlite3.connect(path); db.row_factory=sqlite3.Row; return db + + def _origin(self)->dict|None: + if not self.origin_path.exists(): return None + with self._db(self.origin_path) as db: + row=db.execute("SELECT body_json,body_sha256 FROM origin_chains WHERE lineage_id=?", + (CANONICAL_LINEAGE_ID,)).fetchone() + if not row: return None + body=json.loads(row["body_json"]); body["body_sha256"]=row["body_sha256"]; return body + + def _passport(self,object_id:str)->dict|None: + if not self.passport_path.exists(): return None + with self._db(self.passport_path) as db: + row=db.execute("""SELECT passport_id,version,body_sha256,body_json,created_at_ms + FROM global_passports WHERE object_id=? + ORDER BY created_at_ms DESC,passport_id DESC LIMIT 1""", + (str(object_id),)).fetchone() + if not row: return None + body=json.loads(row["body_json"]) + return {**body,"body_sha256":row["body_sha256"],"created_at_ms":row["created_at_ms"]} + + def _profiles(self,refs:list[str])->list[dict]: + if not self.profile_path.exists(): return [{"profile_ref":r} for r in refs] + out=[] + with self._db(self.profile_path) as db: + for ref in refs: + row=db.execute("""SELECT profile_ref,kind,body_json,issuer_entity_id,body_sha256 + FROM profile_variants WHERE profile_ref=? + ORDER BY active_canonical DESC,created_at_ms DESC LIMIT 1""",(ref,)).fetchone() + if not row: + out.append({"profile_ref":ref}); continue + body=json.loads(row["body_json"]) + out.append({"profile_ref":ref,"profile_id":body.get("profile_id"),"kind":row["kind"], + "issuer_entity_id":row["issuer_entity_id"],"issuer_name":self._name(row["issuer_entity_id"]), + "body_sha256":row["body_sha256"]}) + return out + + def _asset_parents(self,object_id:str)->list[dict]: + if not self.fabric_path.exists(): return [] + with self._db(self.fabric_path) as db: + rows=db.execute("""SELECT p.edge_id,p.parent_object_id,p.relation,p.contribution_bps, + o.title,o.controller_entity_id + FROM provenance_edges p + LEFT JOIN objects o ON o.object_id=p.parent_object_id + WHERE p.child_object_id=? + ORDER BY p.created_at_ms,p.edge_id""",(str(object_id),)).fetchall() + return [{**dict(r),"controller_name":self._name(r["controller_entity_id"])} for r in rows] + + @staticmethod + def _profile_label(ref:str)->str: + core=str(ref).split("@",1)[0].split(":")[-1] + return {"global":"Global","robotics":"Robotics","ai":"AI","manufacturing":"Manufacturing", + "healthcare":"Healthcare","finance":"Finance","defence-public":"Defence-public"}.get(core,core.replace("-"," ").title()) + + def resolve(self,asset:dict)->dict: + object_id=str(asset["object_id"]); controller=str(asset["controller_entity_id"]) + origin=self._origin(); passport=self._passport(object_id) + embedded=dict((passport or {}).get("protocol_origin") or {}) + if embedded: + root=embedded.get("root_originator_entity_id"); steward=embedded.get("steward_entity_id") + protocol=embedded.get("protocol_entity_id"); origin_id=embedded.get("origin_lineage_id") + elif origin: + root=origin.get("root_originator_entity_id"); steward=origin.get("steward_entity_id") + protocol=origin.get("protocol_entity_id"); origin_id=origin.get("lineage_id") + else: + root=steward=protocol=origin_id=None + refs=list(((passport or {}).get("profile_stack") or {}).get("profile_refs") or []) + profiles=self._profiles(refs) + domains=[self._profile_label(x) for x in refs if "entity-profile:global@" not in x] + path=[self._name(root),self._name(steward),self._name(protocol),*domains] + path=[x for x in path if x] + parents=self._asset_parents(object_id) + btdu=dict((passport or {}).get("btdu_binding") or {}) + return { + "schema":"entity-wallet-lineage-resolution-v1", + "protocol_lineage":{ + "lineage_id":origin_id, + "root_originator_entity_id":root,"root_originator_name":self._name(root), + "steward_entity_id":steward,"steward_name":self._name(steward), + "protocol_entity_id":protocol,"protocol_name":self._name(protocol), + "profiles":profiles,"display_path":" → ".join(path), + "passport_protocol_origin_embedded":bool(embedded), + "resolved_from_canonical_origin_registry":bool(origin), + "needs_passport_lineage_supersession":bool(passport and not embedded), + "protocol_origin_is_not_asset_provenance":True, + }, + "asset_lineage":{ + "object_id":object_id,"controller_entity_id":controller, + "controller_name":self._name(controller),"parents":parents, + "protocol_origin_does_not_transfer_asset_ownership":True, + }, + "capability_bindings":{ + "BTDU":{"bound":bool(btdu),"binding":btdu or None, + "topology_does_not_create_ownership":True, + "topology_does_not_create_economic_entitlement":True}, + "ADAM":{"architecture_role":"DETERMINISTIC_EXECUTION_AND_EVIDENCE", + "sovereign_authority":False,"ownership_inferred":False}, + "NIKI":{"architecture_role":"BOUNDED_REASONING_AND_PROPOSALS", + "sovereign_authority":False,"ownership_inferred":False}, + }, + "global_passport":None if passport is None else { + "passport_id":passport.get("passport_id"),"version":passport.get("version"), + "body_sha256":passport.get("body_sha256"),"profile_refs":refs}, + } From c421a3a97707108da211b73403d2c02ad74ac69b Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:12:59 -0700 Subject: [PATCH 021/111] wallet: expose canonical origin, domain, and asset provenance lineage --- src/42_ENTITY_Wallet/canonical_wallet.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/42_ENTITY_Wallet/canonical_wallet.py b/src/42_ENTITY_Wallet/canonical_wallet.py index d847a2f8..042e8572 100644 --- a/src/42_ENTITY_Wallet/canonical_wallet.py +++ b/src/42_ENTITY_Wallet/canonical_wallet.py @@ -2,7 +2,7 @@ from contextlib import contextmanager from pathlib import Path from typing import Any -import html, json, secrets, sqlite3, time +import html, importlib.util, json, secrets, sqlite3, sys, time WALLET_PROFILE="ENTITY_ECONOMIC_WALLET" WALLET_VERSION="1.0.0" @@ -18,6 +18,11 @@ def __init__(self,state_dir:str|Path): self.exchange_path=self.state/"entity_v3_exchange.sqlite" self.economic_path=self.state/"entity_v3_economic_participation.sqlite" self.fabric_path=self.state/"entity_v3"/"universal_fabric.sqlite" + lineage_path=Path(__file__).with_name("lineage_resolver.py") + spec=importlib.util.spec_from_file_location("entity_wallet_lineage_resolver",lineage_path) + if spec is None or spec.loader is None: raise RuntimeError("wallet lineage resolver unavailable") + lineage_mod=importlib.util.module_from_spec(spec); sys.modules[spec.name]=lineage_mod; spec.loader.exec_module(lineage_mod) + self.lineage=lineage_mod.WalletLineageResolver(self.state) self.root=self.state/"wallet"; self.root.mkdir(parents=True,exist_ok=True) self.path=self.root/"entity_wallet.sqlite"; self._init() @@ -139,6 +144,7 @@ def _assets(self,entity_id:str)->list[dict]: d["commodity_class"]=descriptor.get("commodity_class") d["measurement_unit"]=descriptor.get("measurement_unit") d["market_instruments_created_automatically"]=False + d["lineage"]=self.lineage.resolve(d) out.append(d) return out @@ -240,7 +246,9 @@ def snapshot(self,wallet_id:str)->dict: "indicative_only":True,"not_accounting_fair_value":True}, "asset_model":{"digital_assets_are_first_class_holdings":True, "ingest_does_not_issue_market_instruments":True, - "ingest_does_not_create_market_value":True}, + "ingest_does_not_create_market_value":True, + "protocol_lineage_and_asset_provenance_are_separate":True, + "adam_niki_btdu_do_not_create_ownership":True}, "stock_style":{"enabled":True,"features":["ASSETS","POSITIONS","QUANTITY","COST_BASIS","LAST","BID","ASK", "INDICATIVE_MARKET_VALUE","REALIZED_CHANGE","UNREALIZED_CHANGE","ORDERS","ACTIVITY"], "rights_are_not_declared_corporate_shares":True, From d29da5f257b2d43b44f4c59f26138863ea77a1a5 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:14:25 -0700 Subject: [PATCH 022/111] wallet: use canonical v3.4.3 origin BTDU passport ingest --- src/42_ENTITY_Wallet/asset_ingest.py | 187 ++++++++++++++++++--------- 1 file changed, 123 insertions(+), 64 deletions(-) diff --git a/src/42_ENTITY_Wallet/asset_ingest.py b/src/42_ENTITY_Wallet/asset_ingest.py index 97eaae95..6b40c240 100644 --- a/src/42_ENTITY_Wallet/asset_ingest.py +++ b/src/42_ENTITY_Wallet/asset_ingest.py @@ -1,86 +1,145 @@ from __future__ import annotations from pathlib import Path -from typing import Any -import hashlib, json, shutil, time +import json, sqlite3 -INGEST_SCHEMA="entity-wallet-asset-ingest-v1" +INGEST_SCHEMA="entity-wallet-canonical-asset-ingest-v2" -def sha256_file(path:str|Path,chunk_size:int=1024*1024)->str: - h=hashlib.sha256() - with Path(path).open("rb") as f: - while True: - block=f.read(chunk_size) - if not block: break - h.update(block) - return h.hexdigest() +PACKAGE_KINDS={ + "robotics":["robot","controller","model","software","telemetry","action_record"], + "ai":["training_dataset","corpus","model","weights","evaluation","agent","output"], + "manufacturing":["machine","digital_twin","firmware","telemetry","maintenance_record"], + "healthcare":["clinical_dataset","clinical_document","diagnostic_model","medical_device"], + "finance":["instrument","trade_record","settlement_record","market_dataset"], + "defence-public":["public_asset","public_dataset","software","device","model","custody_record"], +} + +PACKAGE_POLICY_DEFAULTS={ + "robotics":{"safety_policy":"ENTITY_FAIL_CLOSED"}, + "ai":{"model_governance_policy":"ENTITY_PROVENANCE_BOUND"}, + "manufacturing":{"asset_namespace":"ENTITY_CONTROLLER"}, + "healthcare":{"privacy_policy":"ENTITY_RIGHTS_PASSPORT"}, + "finance":{"settlement_policy":"EXTERNAL_SETTLEMENT_EVIDENCE_REQUIRED"}, + "defence-public":{"release_policy":"PUBLIC_UNCLASSIFIED_ONLY","classification":"UNCLASSIFIED"}, +} class WalletAssetIngestor: - """Create a Digital Commodity Object from user-selected data. + """Wallet facade over the canonical v3.4.3 ingestion stack. - Ingest is deliberately asset-first. It registers the asset and optional local - custody copy, but never creates an EEP instrument, listing, price or licence. + The wallet does not maintain a parallel object-registration path. Every new DCO + flows through the same protocol origin, BTDU, evidence, rights passport and + Global Passport code used by the canonical ENTITY v3.4 deployment CLI. """ - def __init__(self,state_dir:str|Path,fabric): + def __init__(self,state_dir:str|Path,canonical_cli): self.state=Path(state_dir) - self.fabric=fabric - self.vault=self.state/"wallet"/"asset_vault" - self.vault.mkdir(parents=True,exist_ok=True) + self.cli=canonical_cli + + @staticmethod + def asset_kinds(package:str)->list[str]: + return list(PACKAGE_KINDS.get(str(package).lower(),[])) + + def _existing_dco(self,controller_entity_id:str,content_sha256:str)->str|None: + dbp=self.state/"entity_v3"/"universal_fabric.sqlite" + if not dbp.exists(): return None + db=sqlite3.connect(dbp); db.row_factory=sqlite3.Row + try: + rows=db.execute("""SELECT object_id,descriptor_json FROM objects + WHERE controller_entity_id=? AND content_sha256=? AND status='ACTIVE' + ORDER BY created_at_ms DESC""",(controller_entity_id,content_sha256)).fetchall() + for row in rows: + try: descriptor=json.loads(row["descriptor_json"] or "{}") + except Exception: descriptor={} + if descriptor.get("digital_commodity") is True: return str(row["object_id"]) + finally: db.close() + return None + + @staticmethod + def _configuration(identity,controller_entity_id:str,package:str,jurisdiction:str,authority_basis:str, + overrides:dict|None=None)->dict: + manifest=identity.load_manifest(controller_entity_id) + package=str(package).lower() + cfg={ + "organization":str(manifest.get("display_name") or controller_entity_id), + "jurisdiction":str(jurisdiction or "").strip(), + "authority_source":str(authority_basis or "").strip(), + **dict(PACKAGE_POLICY_DEFAULTS.get(package) or {}), + **dict(overrides or {}), + } + if cfg.get("asset_namespace")=="ENTITY_CONTROLLER": cfg["asset_namespace"]=controller_entity_id + return cfg def ingest_file(self,controller_entity_id:str,file_path:str|Path,*,title:str|None=None, - commodity_class:str="DATA",measurement_unit:str="USE", - authority_basis:str="CREATOR_CONTROLLED",metadata:dict|None=None, - copy_to_local_vault:bool=True)->dict: - source=Path(file_path).expanduser().resolve() - if not source.is_file(): raise FileNotFoundError(str(source)) - digest=sha256_file(source) - size=source.stat().st_size - meta={ + package:str="general",asset_kind:str|None=None,jurisdiction:str="", + authority_basis:str="CONTROLLER_ENTITY",commodity_class:str="DATA", + measurement_unit:str="ASSET",version:str="1.0", + previous_object_id:str|None=None,metadata:dict|None=None, + configuration:dict|None=None)->dict: + src=Path(file_path).expanduser().resolve() + if not src.is_file(): raise FileNotFoundError(str(src)) + identity,fabric,profiles,origin,passports,packages,sdk,origin_status=self.cli.runtime( + self.state,require_current_release=True) + release=origin.passport_binding("entity-release:v3.4.3") + if release.get("origin_lineage_id")!="entity-origin:shawn-btg-entity@1.0": + raise RuntimeError("canonical Shawn -> BTG -> ENTITY origin lineage required") + logical=src.name + universe=self.cli._btdu_for_controller(self.state,identity,controller_entity_id) + try: + receipt=self.cli._btdu_receipt(identity,controller_entity_id,src,logical) + btdu_obj=universe.ingest_file( + src,receipt,logical_path=logical,source_entity_id=controller_entity_id, + controller_entity_id=controller_entity_id,rights_holder_entity_id=controller_entity_id, + provenance_ref="entity-v3.4.3-wallet-ingest:"+logical) + binding=universe.passport_binding(btdu_obj["object_ref"]) + finally: + universe.close() + content_sha=str(binding["content_sha256"]) + existing=self._existing_dco(controller_entity_id,content_sha) + if existing: + raise ValueError(f"identical Digital Commodity Object already registered: {existing}") + commodity_meta={ "wallet_ingest_schema":INGEST_SCHEMA, - "source_filename":source.name, - "size_bytes":size, - "authority_basis":str(authority_basis).upper(), + "authority_basis":str(authority_basis), "controller_asserted_registration_authority":True, - "market_instruments_created_automatically":False, - "market_value_created_by_ingest":False, + "canonical_protocol_lineage_required":True, + "profile_domain":str(package).lower(), **dict(metadata or {}), } - obj=self.fabric.register_digital_commodity( - controller_entity_id, - str(title or source.name), - digest, - commodity_class=str(commodity_class).upper(), - measurement_unit=str(measurement_unit).upper(), - metadata=meta, - ) - custody=None - if copy_to_local_vault: - target_dir=self.vault/obj["object_id"] - target_dir.mkdir(parents=True,exist_ok=False) - target=target_dir/source.name - shutil.copy2(source,target) - receipt={ - "schema":INGEST_SCHEMA, - "object_id":obj["object_id"], - "controller_entity_id":controller_entity_id, - "title":str(title or source.name), - "content_sha256":digest, - "size_bytes":size, - "source_filename":source.name, - "custody":"LOCAL_WALLET_VAULT", - "market_instruments_created":False, - "created_at_ms":int(time.time()*1000), - } - (target_dir/"INGEST_RECEIPT.json").write_text( - json.dumps(receipt,indent=2,sort_keys=True),encoding="utf-8") - custody={"mode":"LOCAL_WALLET_VAULT","path":str(target), - "receipt_path":str(target_dir/"INGEST_RECEIPT.json")} + package=str(package or "general").lower() + common={ + "logical_path":logical,"previous_object_id":previous_object_id,"version":str(version), + "btdu_binding":binding,"title":str(title or src.name),"digital_commodity":True, + "commodity_class":str(commodity_class).upper(), + "measurement_unit":str(measurement_unit).upper(), + "commodity_metadata":commodity_meta, + "industry_context":{"wallet_ingest":True}, + } + if package=="general": + result=sdk.register_file(src,controller_entity_id,"global",**common) + object_id=result["object_id"]; passport_id=result["global_passport_id"] + else: + if package not in PACKAGE_KINDS: raise ValueError("unsupported ENTITY domain package") + if not asset_kind: raise ValueError("asset_kind required for domain package") + if asset_kind not in PACKAGE_KINDS[package]: raise ValueError("unsupported package asset kind") + if not str(jurisdiction or "").strip(): raise ValueError("jurisdiction required for domain package") + cfg=self._configuration(identity,controller_entity_id,package,jurisdiction,authority_basis,configuration) + result=sdk.ingest_package_file(src,controller_entity_id,package,cfg,asset_kind,**common) + object_id=result["object_id"]; passport_id=result["global_passport_id"] + gp=passports.get(passport_id) + check=passports.verify(gp) + if not check.get("valid"): raise RuntimeError("new Global Passport failed verification") + obj=fabric.get_object(object_id) return { "schema":INGEST_SCHEMA, "digital_asset":obj, - "content_sha256":digest, - "size_bytes":size, - "custody":custody, + "content_sha256":obj.get("content_sha256"), + "global_passport":gp, + "global_passport_verified":True, + "rights_passport_id":gp["rights_passport_id"], + "profile_refs":gp["profile_stack"]["profile_refs"], + "protocol_origin":gp["protocol_origin"], + "btdu_binding":gp.get("btdu_binding"), + "canonical_origin_installation":origin_status, "market_instruments_created":False, "listing_created":False, "price_created":False, + "economic_value_invented":False, } From 4d3197cd94762e70040f5b4b5a41d67db23fefa8 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:17:13 -0700 Subject: [PATCH 023/111] wallet: expose canonical domain lineage and package-based ingest --- tools/entity_wallet_desktop.py | 181 +++++++++++++++++++++------------ 1 file changed, 114 insertions(+), 67 deletions(-) diff --git a/tools/entity_wallet_desktop.py b/tools/entity_wallet_desktop.py index e3f7765e..8695b755 100644 --- a/tools/entity_wallet_desktop.py +++ b/tools/entity_wallet_desktop.py @@ -18,8 +18,7 @@ def load_module(name:str,relative:str): mod=importlib.util.module_from_spec(spec); sys.modules[name]=mod; spec.loader.exec_module(mod) return mod -IDENTITY=load_module("entity_wallet_identity","src/01_Core_Runtime/identity/canonical_identity.py") -FABRIC=load_module("entity_wallet_fabric","src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py") +CLI=load_module("entity_wallet_v343_cli","tools/entity_v3_4_cli.py") WALLET=load_module("entity_wallet_model","src/42_ENTITY_Wallet/canonical_wallet.py") INGEST=load_module("entity_wallet_ingest","src/42_ENTITY_Wallet/asset_ingest.py") EXCHANGE=load_module("entity_wallet_exchange","src/31_Profiles/exchange_protocol.py") @@ -34,8 +33,7 @@ def default_state()->Path: return Path.home()/".local"/"share"/"blackmore-technology-group"/"entity" def manifests(state:Path)->list[dict]: - out=[] - root=state/"identity"/"manifests" + out=[]; root=state/"identity"/"manifests" if not root.is_dir(): return out for p in sorted(root.glob("*.json")): try: @@ -47,16 +45,19 @@ def manifests(state:Path)->list[dict]: class Backend: def __init__(self,state:Path,entity_id:str): self.state=state; self.entity_id=entity_id - self.identity=IDENTITY.EntityIdentityVault(state) - self.identity.load_manifest(entity_id) - self.fabric=FABRIC.UniversalTransactionFabric(state,self.identity) + identity,fabric,profiles,origin,passports,packages,sdk,origin_status=CLI.runtime(state) + self.identity=identity; self.fabric=fabric; self.profiles=profiles; self.origin=origin + self.passports=passports; self.packages=packages; self.sdk=sdk; self.origin_status=origin_status + self.identity_manifest=self.identity.load_manifest(entity_id) self.wallet=WALLET.EntityEconomicWallet(state) self.exchange=EXCHANGE.ExchangeProtocol(state,self.identity,self.fabric) - self.ingestor=INGEST.WalletAssetIngestor(state,self.fabric) - manifest=self.identity.load_manifest(entity_id) - self.wallet_record=self.wallet.ensure_wallet(entity_id,manifest.get("display_name","ENTITY")+" Wallet") + self.ingestor=INGEST.WalletAssetIngestor(state,CLI) + self.wallet_record=self.wallet.ensure_wallet( + entity_id,self.identity_manifest.get("display_name","ENTITY")+" Wallet") def snapshot(self): return self.wallet.snapshot(self.wallet_record["wallet_id"]) + def domains(self): return ["general"]+self.packages.list_packages() + def asset_kinds(self,domain): return self.ingestor.asset_kinds(domain) def market(self)->list[dict]: if not self.wallet.exchange_path.exists(): return [] @@ -74,11 +75,13 @@ def market(self)->list[dict]: d=dict(r); d["market"]=self.wallet._market(db,d["instrument_id"]); out.append(d) return out - def ingest(self,path,title,commodity_class,measurement_unit,authority_basis): + def ingest(self,path,options): return self.ingestor.ingest_file( - self.entity_id,path,title=title,commodity_class=commodity_class, - measurement_unit=measurement_unit,authority_basis=authority_basis, - copy_to_local_vault=True) + self.entity_id,path,title=options["title"],package=options["domain"], + asset_kind=options.get("asset_kind") or None,jurisdiction=options.get("jurisdiction",""), + authority_basis=options["authority"],commodity_class=options["class"], + measurement_unit=options["unit"],version=options.get("version") or "1.0", + previous_object_id=options.get("parent_object_id") or None) def submit_order(self,row:dict,side:str,quantity:int,limit_price:int): venue=row.get("venue_id") @@ -90,41 +93,70 @@ def submit_order(self,row:dict,side:str,quantity:int,limit_price:int): return {"order":order,"trades":trades} class AssetDialog(tk.Toplevel): - def __init__(self,parent,path:Path): - super().__init__(parent); self.title("Ingest Digital Asset"); self.resizable(False,False) - self.result=None; self.transient(parent); self.grab_set() + def __init__(self,parent,path:Path,backend:Backend): + super().__init__(parent); self.title("Canonical ENTITY Asset Ingest"); self.resizable(False,False) + self.result=None; self.backend=backend; self.transient(parent); self.grab_set() self.vars={ "title":tk.StringVar(value=path.name), - "class":tk.StringVar(value="DATA"), - "unit":tk.StringVar(value="USE"), - "authority":tk.StringVar(value="CREATOR_CONTROLLED"), + "domain":tk.StringVar(value="general"), + "asset_kind":tk.StringVar(value=""), + "jurisdiction":tk.StringVar(value=""), + "class":tk.StringVar(value="DIGITAL_ASSET"), + "unit":tk.StringVar(value="ASSET"), + "authority":tk.StringVar(value=f"CONTROLLER_ENTITY:{backend.entity_id}"), + "version":tk.StringVar(value="1.0"), + "parent_object_id":tk.StringVar(value=""), } ttk.Label(self,text="File").grid(row=0,column=0,sticky="w",padx=12,pady=(12,4)) - ttk.Label(self,text=str(path),wraplength=520).grid(row=0,column=1,sticky="w",padx=12,pady=(12,4)) - labels=[("Title","title"),("Commodity class","class"),("Measurement unit","unit"),("Authority basis","authority")] - for i,(label,key) in enumerate(labels,1): + ttk.Label(self,text=str(path),wraplength=560).grid(row=0,column=1,sticky="w",padx=12,pady=(12,4)) + ttk.Label(self,text="Domain profile").grid(row=1,column=0,sticky="w",padx=12,pady=5) + self.domain=ttk.Combobox(self,textvariable=self.vars["domain"],values=backend.domains(),state="readonly",width=48) + self.domain.grid(row=1,column=1,sticky="ew",padx=12,pady=5); self.domain.bind("<>",self._domain_changed) + ttk.Label(self,text="Asset kind").grid(row=2,column=0,sticky="w",padx=12,pady=5) + self.kind=ttk.Combobox(self,textvariable=self.vars["asset_kind"],values=[],width=48) + self.kind.grid(row=2,column=1,sticky="ew",padx=12,pady=5) + labels=[ + ("Title","title"),("Jurisdiction (domain assets)","jurisdiction"), + ("Commodity class","class"),("Measurement unit","unit"), + ("Authority basis","authority"),("Passport version","version"), + ("Parent object ID (optional)","parent_object_id"), + ] + for i,(label,key) in enumerate(labels,3): ttk.Label(self,text=label).grid(row=i,column=0,sticky="w",padx=12,pady=5) - ttk.Entry(self,textvariable=self.vars[key],width=48).grid(row=i,column=1,sticky="ew",padx=12,pady=5) + ttk.Entry(self,textvariable=self.vars[key],width=50).grid(row=i,column=1,sticky="ew",padx=12,pady=5) self.attest=tk.BooleanVar(value=False) - ttk.Checkbutton(self,text="I have authority to register this asset and its provenance.", - variable=self.attest).grid(row=5,column=0,columnspan=2,sticky="w",padx=12,pady=10) - b=ttk.Frame(self); b.grid(row=6,column=0,columnspan=2,sticky="e",padx=12,pady=12) + ttk.Checkbutton(self,text="I have authority to register this asset and attest its provenance.", + variable=self.attest).grid(row=10,column=0,columnspan=2,sticky="w",padx=12,pady=(10,4)) + ttk.Label(self,text="Ingest creates the governed DCO + evidence + rights passport + Global Passport + BTDU binding. It does not list or price the asset.", + wraplength=650).grid(row=11,column=0,columnspan=2,sticky="w",padx=12,pady=4) + b=ttk.Frame(self); b.grid(row=12,column=0,columnspan=2,sticky="e",padx=12,pady=12) ttk.Button(b,text="Cancel",command=self.destroy).pack(side="right",padx=4) ttk.Button(b,text="Ingest Asset",command=self.ok).pack(side="right",padx=4) + def _domain_changed(self,event=None): + domain=self.vars["domain"].get() + kinds=self.backend.asset_kinds(domain) + self.kind["values"]=kinds + self.vars["asset_kind"].set(kinds[0] if kinds else "") + self.vars["class"].set("DIGITAL_ASSET" if domain=="general" else domain.upper().replace("-","_")+"_ASSET") + def ok(self): if not self.attest.get(): messagebox.showerror("Authority required","Confirm authority before registering the digital asset.",parent=self); return if not self.vars["title"].get().strip(): messagebox.showerror("Title required","Enter an asset title.",parent=self); return + domain=self.vars["domain"].get() + if domain!="general" and not self.vars["jurisdiction"].get().strip(): + messagebox.showerror("Jurisdiction required","Domain package ingest requires an explicit jurisdiction.",parent=self); return + if domain!="general" and not self.vars["asset_kind"].get().strip(): + messagebox.showerror("Asset kind required","Choose an asset kind for this domain.",parent=self); return self.result={k:v.get().strip() for k,v in self.vars.items()}; self.destroy() class OrderDialog(tk.Toplevel): def __init__(self,parent,row,side): super().__init__(parent); self.title(f"{side.title()} {row['instrument_id']}"); self.resizable(False,False) self.result=None; self.transient(parent); self.grab_set(); m=row["market"] - self.qty=tk.StringVar(value="1") - default=m.get("ask") if side=="BUY" else m.get("bid") + self.qty=tk.StringVar(value="1"); default=m.get("ask") if side=="BUY" else m.get("bid") self.price=tk.StringVar(value="" if default is None else str(default)) ttk.Label(self,text=f"{side} {row['instrument_id']}").grid(row=0,column=0,columnspan=2,sticky="w",padx=12,pady=12) ttk.Label(self,text="Quantity").grid(row=1,column=0,sticky="w",padx=12,pady=5) @@ -133,7 +165,6 @@ def __init__(self,parent,row,side): ttk.Entry(self,textvariable=self.price,width=22).grid(row=2,column=1,padx=12,pady=5) ttk.Label(self,text=f"Bid: {m.get('bid')} Ask: {m.get('ask')} Last: {m.get('last')}").grid(row=3,column=0,columnspan=2,sticky="w",padx=12,pady=8) ttk.Button(self,text="Submit",command=self.ok).grid(row=4,column=1,sticky="e",padx=12,pady=12) - def ok(self): try: q=int(self.qty.get()); p=int(self.price.get()) @@ -144,35 +175,28 @@ def ok(self): class App(tk.Tk): def __init__(self,state:Path|None=None,entity:str|None=None): - super().__init__(); self.title(APP_NAME); self.geometry("1180x760"); self.minsize(920,620) - self.backend=None; self.market_rows=[] - self.state_var=tk.StringVar(value=str(state or default_state())) - self.entity_var=tk.StringVar() - self.entity_map={} - self._style(); self._layout() - self.load_state(preselect=entity) + super().__init__(); self.title(APP_NAME); self.geometry("1380x800"); self.minsize(1000,650) + self.backend=None; self.market_rows=[]; self.asset_rows={} + self.state_var=tk.StringVar(value=str(state or default_state())); self.entity_var=tk.StringVar(); self.entity_map={} + self._style(); self._layout(); self.load_state(preselect=entity) def _style(self): style=ttk.Style(self) if sys.platform=="win32": try: style.theme_use("vista") except Exception: pass - style.configure("Title.TLabel",font=("Segoe UI",18,"bold")) - style.configure("Sub.TLabel",foreground="#666") + style.configure("Title.TLabel",font=("Segoe UI",18,"bold")); style.configure("Sub.TLabel",foreground="#666") def _layout(self): top=ttk.Frame(self,padding=12); top.pack(fill="x") ttk.Label(top,text="ENTITY Wallet",style="Title.TLabel").grid(row=0,column=0,sticky="w") - ttk.Label(top,text="Digital assets first. Markets optional.",style="Sub.TLabel").grid(row=1,column=0,sticky="w") - ttk.Label(top,text="State").grid(row=0,column=1,padx=(30,5)) - ttk.Entry(top,textvariable=self.state_var,width=45).grid(row=0,column=2,sticky="ew") - ttk.Button(top,text="Browse",command=self.browse_state).grid(row=0,column=3,padx=4) - ttk.Button(top,text="Load",command=self.load_state).grid(row=0,column=4,padx=4) + ttk.Label(top,text="Sovereign digital assets · canonical lineage · open rights market",style="Sub.TLabel").grid(row=1,column=0,sticky="w") + ttk.Label(top,text="State").grid(row=0,column=1,padx=(30,5)); ttk.Entry(top,textvariable=self.state_var,width=45).grid(row=0,column=2,sticky="ew") + ttk.Button(top,text="Browse",command=self.browse_state).grid(row=0,column=3,padx=4); ttk.Button(top,text="Load",command=self.load_state).grid(row=0,column=4,padx=4) ttk.Label(top,text="Identity").grid(row=1,column=1,padx=(30,5)) self.entity_box=ttk.Combobox(top,textvariable=self.entity_var,state="readonly",width=42) self.entity_box.grid(row=1,column=2,sticky="ew"); self.entity_box.bind("<>",lambda e:self.activate()) - ttk.Button(top,text="Refresh",command=self.refresh).grid(row=1,column=3,padx=4) - top.columnconfigure(2,weight=1) + ttk.Button(top,text="Refresh",command=self.refresh).grid(row=1,column=3,padx=4); top.columnconfigure(2,weight=1) self.tabs=ttk.Notebook(self); self.tabs.pack(fill="both",expand=True,padx=12,pady=(0,12)) self.assets_tab=ttk.Frame(self.tabs,padding=10); self.market_tab=ttk.Frame(self.tabs,padding=10) @@ -182,19 +206,19 @@ def _layout(self): bar=ttk.Frame(self.assets_tab); bar.pack(fill="x",pady=(0,8)) ttk.Button(bar,text="Upload / Ingest Digital Asset",command=self.ingest_asset).pack(side="left") - ttk.Label(bar,text="Uploading creates a DCO asset only — no licence, listing or price is created.",style="Sub.TLabel").pack(side="left",padx=12) - self.assets=self._tree(self.assets_tab,["Title","Class","Unit","SHA-256","Object ID"],[220,100,90,240,290]) + ttk.Button(bar,text="View Lineage",command=self.view_lineage).pack(side="left",padx=4) + ttk.Label(bar,text="Assets are separate from market instruments. Ingest never creates a price.",style="Sub.TLabel").pack(side="left",padx=12) + self.assets=self._tree(self.assets_tab,["Title","Class","Type","Canonical lineage","Controller","Passport","BTDU","Object ID"], + [210,145,90,330,160,110,60,260]) mbar=ttk.Frame(self.market_tab); mbar.pack(fill="x",pady=(0,8)) ttk.Button(mbar,text="Buy",command=lambda:self.order("BUY")).pack(side="left") ttk.Button(mbar,text="Sell",command=lambda:self.order("SELL")).pack(side="left",padx=4) - ttk.Label(mbar,text="Orders match automatically by price/time. Paid matches remain subject to settlement evidence.",style="Sub.TLabel").pack(side="left",padx=12) + ttk.Label(mbar,text="Price/time matching uses existing EEP. Settlement evidence remains required.",style="Sub.TLabel").pack(side="left",padx=12) self.market=self._tree(self.market_tab,["Instrument","Class","Currency","Bid","Ask","Last","Venue"],[240,140,80,90,90,90,220]) - self.positions=self._tree(self.positions_tab,["Instrument","Units","Currency","Last","Bid","Ask","Indicative Value"],[260,90,80,90,90,90,140]) self.orders=self._tree(self.orders_tab,["Instrument","Side","Remaining","Limit","Status"],[280,80,100,100,120]) - self.status=tk.StringVar(value="Ready") - ttk.Label(self,textvariable=self.status,relief="sunken",anchor="w",padding=5).pack(fill="x",side="bottom") + self.status=tk.StringVar(value="Ready"); ttk.Label(self,textvariable=self.status,relief="sunken",anchor="w",padding=5).pack(fill="x",side="bottom") def _tree(self,parent,columns,widths): f=ttk.Frame(parent); f.pack(fill="both",expand=True) @@ -208,8 +232,7 @@ def browse_state(self): if p: self.state_var.set(p); self.load_state() def load_state(self,preselect=None): - state=Path(self.state_var.get()).expanduser() - state.mkdir(parents=True,exist_ok=True) + state=Path(self.state_var.get()).expanduser(); state.mkdir(parents=True,exist_ok=True) items=manifests(state); self.entity_map={f"{x['display_name']} — {x['entity_id']}":x["entity_id"] for x in items} self.entity_box["values"]=list(self.entity_map) if preselect: @@ -233,18 +256,22 @@ def clear(tree): def refresh(self): if not self.backend: return try: - snap=self.backend.snapshot(); self.market_rows=self.backend.market() + snap=self.backend.snapshot(); self.market_rows=self.backend.market(); self.asset_rows={} self.clear(self.assets) for a in snap.get("assets",[]): - self.assets.insert("", "end", values=(a["title"],a.get("commodity_class"),a.get("measurement_unit"), - a.get("content_sha256") or "",a["object_id"])) + lin=a.get("lineage",{}); prot=lin.get("protocol_lineage",{}); al=lin.get("asset_lineage",{}) + gp=lin.get("global_passport") or {}; btdu=lin.get("capability_bindings",{}).get("BTDU",{}).get("bound",False) + self.asset_rows[a["object_id"]]=a + self.assets.insert("", "end",iid=a["object_id"],values=( + a["title"],a.get("commodity_class"),a.get("object_type"),prot.get("display_path") or "UNRESOLVED", + al.get("controller_name") or a["controller_entity_id"], + (gp.get("version") or "—"),"YES" if btdu else "NO",a["object_id"])) self.clear(self.positions) for p in snap.get("positions",[]): m=p["market"]; self.positions.insert("", "end",values=(p["instrument_id"],p["units"],p["settlement_currency"], m.get("last"),m.get("bid"),m.get("ask"),p.get("market_value_amount_units"))) self.clear(self.orders) - for o in snap.get("orders",[]): - self.orders.insert("", "end",values=(o["instrument_id"],o["side"],o["remaining"],o["limit_price"],o["status"])) + for o in snap.get("orders",[]): self.orders.insert("", "end",values=(o["instrument_id"],o["side"],o["remaining"],o["limit_price"],o["status"])) self.clear(self.market) for i,row in enumerate(self.market_rows): m=row["market"]; self.market.insert("", "end",iid=str(i),values=(row["instrument_id"],row["instrument_class"], @@ -253,17 +280,39 @@ def refresh(self): except Exception as e: messagebox.showerror("Refresh failed",str(e)); self.status.set(str(e)) + def view_lineage(self): + sel=self.assets.selection() + if not sel: messagebox.showwarning("Lineage","Select a digital asset."); return + a=self.asset_rows.get(sel[0]); lin=(a or {}).get("lineage",{}) + p=lin.get("protocol_lineage",{}); al=lin.get("asset_lineage",{}); cap=lin.get("capability_bindings",{}); gp=lin.get("global_passport") or {} + parents="\n".join(f" {x.get('relation')}: {x.get('title') or x.get('parent_object_id')}" for x in al.get("parents",[])) or " None recorded" + msg=( + f"CANONICAL PROTOCOL / DOMAIN LINEAGE\n{p.get('display_path') or 'Unresolved'}\n\n" + f"Passport: {gp.get('passport_id') or 'None'} version {gp.get('version') or '—'}\n" + f"Origin embedded in passport: {p.get('passport_protocol_origin_embedded')}\n" + f"Needs lineage supersession: {p.get('needs_passport_lineage_supersession')}\n\n" + f"ASSET PROVENANCE\nController: {al.get('controller_name')}\nParents:\n{parents}\n\n" + f"SYSTEM BINDINGS\nBTDU bound: {cap.get('BTDU',{}).get('bound')}\n" + f"ADAM: deterministic execution/evidence; not sovereign authority\n" + f"NIKI: bounded reasoning/proposals; not sovereign authority\n\n" + f"Protocol ancestry does not transfer ownership or create economic entitlement." + ) + messagebox.showinfo("ENTITY Lineage",msg) + def ingest_asset(self): if not self.backend: messagebox.showwarning("Wallet","Load an ENTITY identity first."); return f=filedialog.askopenfilename(title="Choose data or digital asset to ingest") if not f: return - d=AssetDialog(self,Path(f)); self.wait_window(d) + d=AssetDialog(self,Path(f),self.backend); self.wait_window(d) if not d.result: return try: - r=self.backend.ingest(f,d.result["title"],d.result["class"],d.result["unit"],d.result["authority"]) - a=r["digital_asset"]; self.refresh() - messagebox.showinfo("Digital asset created", - f"Registered as {a['object_id']}\n\nSHA-256:\n{r['content_sha256']}\n\nNo market instrument or price was created.") + r=self.backend.ingest(f,d.result); self.refresh() + a=r["digital_asset"]; origin=r["protocol_origin"]; refs=r["profile_refs"] + messagebox.showinfo("Canonical digital asset created", + f"DCO: {a['object_id']}\n\nProfiles: {', '.join(refs)}\n" + f"Origin: {origin['origin_lineage_id']} → {origin['release_ref']}\n" + f"Global Passport verified: {r['global_passport_verified']}\nBTDU bound: {bool(r['btdu_binding'])}\n\n" + "No market instrument, listing, or price was created.") except Exception as e: messagebox.showerror("Ingest failed",str(e)) def order(self,side): @@ -276,9 +325,7 @@ def order(self,side): if not d.result: return try: q,p=d.result; result=self.backend.submit_order(row,side,q,p); self.refresh() - n=len(result["trades"]) - messagebox.showinfo("Order submitted", - f"Order accepted.\nImmediate matches: {n}\n\nMatched paid trades remain pending until settlement requirements are satisfied.") + messagebox.showinfo("Order submitted",f"Order accepted.\nImmediate matches: {len(result['trades'])}\n\nPaid matches remain subject to settlement requirements.") except Exception as e: messagebox.showerror("Order failed",str(e)) def main(): From 200ce3431938beffc5ace93200055bdbe8ffba31 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:17:39 -0700 Subject: [PATCH 024/111] wallet: package canonical origin passport BTDU and ADAM runtime --- tools/build_entity_wallet_desktop.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tools/build_entity_wallet_desktop.py b/tools/build_entity_wallet_desktop.py index d2cccf7c..f1455ef3 100644 --- a/tools/build_entity_wallet_desktop.py +++ b/tools/build_entity_wallet_desktop.py @@ -4,12 +4,22 @@ ROOT=Path(__file__).resolve().parents[1] ENTRY=ROOT/"tools"/"entity_wallet_desktop.py" -FILES=[ + +DATA_PATHS=[ + "ENTITY_CURRENT_RELEASE_ORIGIN.json", + "protocol/origin", + "tools/entity_v3_4_cli.py", "src/01_Core_Runtime/identity/canonical_identity.py", + "src/11_ADAM/full_runtime", "src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py", "src/31_Profiles/exchange_protocol.py", - "src/42_ENTITY_Wallet/canonical_wallet.py", - "src/42_ENTITY_Wallet/asset_ingest.py", + "src/36_Adoption_Layer", + "src/37_Verifiable_Reality", + "src/38_Global_Passports", + "src/39_Implementation_Packages", + "src/40_BTDU", + "src/42_ENTITY_Wallet", + "sdk/global_passport_sdk", ] def main(): @@ -18,8 +28,8 @@ def main(): "--name","ENTITY-Wallet", "--hidden-import","cryptography.hazmat.primitives.asymmetric.ed25519", "--hidden-import","cryptography.hazmat.primitives.serialization"] - for rel in FILES: - src=ROOT/rel; dest=str(Path(rel).parent) + for rel in DATA_PATHS: + src=ROOT/rel; dest=str(Path(rel).parent if src.is_file() else Path(rel)) cmd += ["--add-data",f"{src}{sep}{dest}"] cmd.append(str(ENTRY)) subprocess.run(cmd,cwd=ROOT,check=True) From 8cce5c8fd4daa148c6664027f8c4d8f3402d03d0 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:18:41 -0700 Subject: [PATCH 025/111] tests: enforce canonical asset ingest and DCO type preservation --- tests/test_wallet_asset_ingest.py | 100 +++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 15 deletions(-) diff --git a/tests/test_wallet_asset_ingest.py b/tests/test_wallet_asset_ingest.py index c44e9a2f..679054d7 100644 --- a/tests/test_wallet_asset_ingest.py +++ b/tests/test_wallet_asset_ingest.py @@ -1,6 +1,6 @@ from __future__ import annotations from pathlib import Path -import hashlib, importlib.util, json, sqlite3, sys, tempfile +import importlib.util, json, sqlite3, sys, tempfile ROOT=Path(__file__).resolve().parents[1] @@ -8,26 +8,93 @@ def load(name,rel): p=ROOT/rel; spec=importlib.util.spec_from_file_location(name,p) m=importlib.util.module_from_spec(spec); sys.modules[name]=m; spec.loader.exec_module(m); return m +class FakeIdentity: + def load_manifest(self,eid): return {"entity_id":eid,"display_name":"Test Controller"} + class FakeFabric: - def __init__(self): self.calls=[] - def register_digital_commodity(self,controller,title,content_sha256,*,commodity_class,measurement_unit,metadata): - self.calls.append((controller,title,content_sha256,commodity_class,measurement_unit,metadata)) - return {"object_id":"obj3-test","controller_entity_id":controller,"title":title, - "content_sha256":content_sha256,"digital_commodity_object":True} + def __init__(self): + self.obj={"object_id":"obj3-test","controller_entity_id":"BTG","object_type":"SOFTWARE", + "title":"Robot Asset","content_sha256":"a"*64, + "descriptor":{"digital_commodity":True,"commodity_class":"ROBOTICS_ASSET","measurement_unit":"ASSET"}} + def get_object(self,oid): return dict(self.obj) + +class FakeOrigin: + default_release_ref="entity-release:v3.4.3" + def passport_binding(self,ref): + return {"release_ref":ref,"release_tag":"v3.4.3", + "origin_lineage_id":"entity-origin:shawn-btg-entity@1.0", + "root_originator_entity_id":"SHAWN","steward_entity_id":"BTG", + "protocol_entity_id":"ENTITY","asset_provenance_is_separate_from_protocol_origin":True, + "protocol_origin_does_not_transfer_authority":True, + "protocol_origin_does_not_transfer_user_asset_ownership":True, + "economic_participation_requires_explicit_terms":True,"automatic_protocol_royalty_bps":0} + +class FakePassports: + def __init__(self,origin): self.origin=origin + def get(self,pid): + return {"passport_id":pid,"rights_passport_id":"rp-test", + "profile_stack":{"profile_refs":["entity-profile:global@1.0","entity-profile:robotics@1.0"]}, + "protocol_origin":self.origin.passport_binding("entity-release:v3.4.3"), + "btdu_binding":{"schema":"entity-btdu-passport-binding-v1","content_sha256":"a"*64}} + def verify(self,gp): return {"valid":True} + +class FakePackages: + def list_packages(self): return ["robotics"] + +class FakeSDK: + def __init__(self): self.last=None + def ingest_package_file(self,path,controller,package,config,asset_kind,**kwargs): + self.last={"path":str(path),"controller":controller,"package":package,"config":config, + "asset_kind":asset_kind,"kwargs":kwargs} + return {"object_id":"obj3-test","global_passport_id":"gp-test"} -def test_ingest_registers_asset_without_market_instrument(): +class FakeUniverse: + def ingest_file(self,path,receipt,**kwargs): + return {"object_ref":"btdu:test","content_sha256":"a"*64} + def passport_binding(self,ref): + return {"schema":"entity-btdu-passport-binding-v1","universe_root":"u","object_ref":ref, + "content_sha256":"a"*64,"sovereign_entity_id":"BTG", + "protocol_origin_is_not_asset_provenance":True,"topology_does_not_create_ownership":True, + "topology_does_not_create_economic_entitlement":True,"automatic_protocol_royalty_bps":0} + def close(self): pass + +class FakeCLI: + def __init__(self): + self.identity=FakeIdentity(); self.fabric=FakeFabric(); self.origin=FakeOrigin() + self.passports=FakePassports(self.origin); self.packages=FakePackages(); self.sdk=FakeSDK() + def runtime(self,state,require_current_release=False): + assert require_current_release is True + return self.identity,self.fabric,None,self.origin,self.passports,self.packages,self.sdk,{"verified":True} + def _btdu_for_controller(self,state,identity,controller): return FakeUniverse() + def _btdu_receipt(self,identity,controller,path,logical): return {"body":{"scope":"BTDU_WRITE"},"signature":{}} + +def test_wallet_ingest_uses_canonical_robotics_pipeline_without_market_issue(): m=load("wallet_asset_ingest_test","src/42_ENTITY_Wallet/asset_ingest.py") with tempfile.TemporaryDirectory() as td: - src=Path(td)/"robotics.bin"; src.write_bytes(b"robotics-asset") - f=FakeFabric(); ing=m.WalletAssetIngestor(Path(td)/"state",f) - r=ing.ingest_file("ent2-test",src,title="Robot Asset",commodity_class="ROBOTICS_DATA", - measurement_unit="ASSET",authority_basis="CREATOR_CONTROLLED") + src=Path(td)/"robot.py"; src.write_text("print('robot')",encoding="utf-8") + cli=FakeCLI(); ing=m.WalletAssetIngestor(Path(td)/"state",cli) + r=ing.ingest_file("BTG",src,title="Robot Asset",package="robotics",asset_kind="software", + jurisdiction="CA-BC",commodity_class="ROBOTICS_ASSET", + measurement_unit="ASSET",authority_basis="CONTROLLER_ENTITY:BTG") + assert r["global_passport_verified"] is True assert r["market_instruments_created"] is False assert r["listing_created"] is False assert r["price_created"] is False - assert r["content_sha256"]==hashlib.sha256(b"robotics-asset").hexdigest() - assert Path(r["custody"]["path"]).is_file() - assert f.calls[0][5]["market_instruments_created_automatically"] is False + assert r["protocol_origin"]["origin_lineage_id"]=="entity-origin:shawn-btg-entity@1.0" + assert "entity-profile:robotics@1.0" in r["profile_refs"] + assert cli.sdk.last["kwargs"]["digital_commodity"] is True + assert cli.sdk.last["kwargs"]["btdu_binding"]["object_ref"]=="btdu:test" + +def test_dco_preserves_real_asset_type(): + identity=load("wallet_asset_identity","src/01_Core_Runtime/identity/canonical_identity.py") + fabric_mod=load("wallet_asset_fabric","src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py") + with tempfile.TemporaryDirectory() as td: + vault=identity.EntityIdentityVault(td); owner=vault.create("Owner","organization")["entity_id"] + fabric=fabric_mod.UniversalTransactionFabric(td,vault) + obj=fabric.register_digital_commodity(owner,"Algorithm","b"*64,commodity_class="ROBOTICS_ALGORITHM", + measurement_unit="ASSET",object_type="SOFTWARE") + assert obj["object_type"]=="SOFTWARE" + assert obj["descriptor"]["digital_commodity"] is True def test_wallet_lists_digital_assets_separately_from_positions(): m=load("wallet_assets_view_test","src/42_ENTITY_Wallet/canonical_wallet.py") @@ -36,9 +103,11 @@ def test_wallet_lists_digital_assets_separately_from_positions(): db=sqlite3.connect(dbp) db.execute("""CREATE TABLE objects(object_id TEXT PRIMARY KEY,controller_entity_id TEXT,object_type TEXT, title TEXT,content_sha256 TEXT,descriptor_json TEXT,status TEXT,created_at_ms INTEGER,signature_json TEXT)""") + db.execute("""CREATE TABLE provenance_edges(edge_id TEXT PRIMARY KEY,parent_object_id TEXT,child_object_id TEXT, + relation TEXT,contribution_bps INTEGER,evidence_json TEXT,actor_entity_id TEXT,created_at_ms INTEGER,signature_json TEXT)""") descriptor=json.dumps({"digital_commodity":True,"commodity_class":"ROBOTICS","measurement_unit":"ASSET"}) db.execute("INSERT INTO objects VALUES(?,?,?,?,?,?,?,?,?)", - ("obj3-r","OWNER","DATASET","Robotics Asset","a"*64,descriptor,"ACTIVE",1,"{}")) + ("obj3-r","OWNER","SOFTWARE","Robotics Asset","a"*64,descriptor,"ACTIVE",1,"{}")) db.commit(); db.close() w=m.EntityEconomicWallet(state); wr=w.ensure_wallet("OWNER","Owner Wallet") snap=w.snapshot(wr["wallet_id"]) @@ -46,3 +115,4 @@ def test_wallet_lists_digital_assets_separately_from_positions(): assert snap["assets"][0]["title"]=="Robotics Asset" assert snap["positions"]==[] assert snap["asset_model"]["ingest_does_not_issue_market_instruments"] is True + assert snap["asset_model"]["protocol_lineage_and_asset_provenance_are_separate"] is True From f356bf21b188e2f6eb91d90a1f68e6a4720a532c Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:19:25 -0700 Subject: [PATCH 026/111] tests: lock Shawn BTG ENTITY Robotics wallet lineage --- tests/test_wallet_lineage_resolution.py | 105 ++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 tests/test_wallet_lineage_resolution.py diff --git a/tests/test_wallet_lineage_resolution.py b/tests/test_wallet_lineage_resolution.py new file mode 100644 index 00000000..d11f0078 --- /dev/null +++ b/tests/test_wallet_lineage_resolution.py @@ -0,0 +1,105 @@ +from __future__ import annotations +from pathlib import Path +import importlib.util,json,sqlite3,sys,tempfile + +ROOT=Path(__file__).resolve().parents[1] +SHAWN="ent2-shawn"; BTG="ent2-btg"; ENTITY="ent2-entity" + +def load(): + p=ROOT/"src"/"42_ENTITY_Wallet"/"lineage_resolver.py" + spec=importlib.util.spec_from_file_location("wallet_lineage_test",p) + m=importlib.util.module_from_spec(spec); sys.modules[spec.name]=m; spec.loader.exec_module(m); return m + +def write_manifest(state,eid,name): + p=state/"identity"/"manifests"; p.mkdir(parents=True,exist_ok=True) + (p/f"{eid}.json").write_text(json.dumps({"entity_id":eid,"display_name":name}),encoding="utf-8") + +def test_robotics_lineage_resolves_shawn_btg_entity_robotics_without_transferring_ownership(): + m=load() + with tempfile.TemporaryDirectory() as td: + state=Path(td) + write_manifest(state,SHAWN,"Shawn Blackmore") + write_manifest(state,BTG,"Blackmore Technology Group Limited") + write_manifest(state,ENTITY,"ENTITY") + + db=sqlite3.connect(state/"entity_protocol_origin.sqlite") + db.execute("CREATE TABLE origin_chains(lineage_id TEXT PRIMARY KEY,body_sha256 TEXT,body_json TEXT,signatures_json TEXT)") + origin={"lineage_id":"entity-origin:shawn-btg-entity@1.0","root_originator_entity_id":SHAWN, + "steward_entity_id":BTG,"protocol_entity_id":ENTITY} + db.execute("INSERT INTO origin_chains VALUES(?,?,?,?)",(origin["lineage_id"],"o"*64,json.dumps(origin),"{}")) + db.commit(); db.close() + + db=sqlite3.connect(state/"entity_v3_4_global_profiles.sqlite") + db.execute("""CREATE TABLE profile_variants(profile_ref TEXT,body_sha256 TEXT,profile_id TEXT,version TEXT, + kind TEXT,body_json TEXT,issuer_entity_id TEXT,signature_json TEXT,created_at_ms INTEGER, + active_canonical INTEGER)""") + for i,(ref,kind) in enumerate([ + ("entity-profile:global@1.0","GLOBAL"),("entity-profile:robotics@1.0","INDUSTRY")]): + body={"profile_ref":ref,"profile_id":ref.split("@")[0],"kind":kind} + db.execute("INSERT INTO profile_variants VALUES(?,?,?,?,?,?,?,?,?,?)", + (ref,str(i)*64,body["profile_id"],"1.0",kind,json.dumps(body),ENTITY,"{}",i,1)) + db.commit(); db.close() + + db=sqlite3.connect(state/"entity_v3_4_global_passports.sqlite") + db.execute("""CREATE TABLE global_passports(passport_id TEXT,object_id TEXT,controller_entity_id TEXT, + version TEXT,body_sha256 TEXT,body_json TEXT,signature_json TEXT,created_at_ms INTEGER)""") + gp={"passport_id":"gp1","object_id":"obj-child","controller_entity_id":BTG,"version":"1.1", + "profile_stack":{"profile_refs":["entity-profile:global@1.0","entity-profile:robotics@1.0"]}, + "protocol_origin":{"origin_lineage_id":"entity-origin:shawn-btg-entity@1.0", + "root_originator_entity_id":SHAWN,"steward_entity_id":BTG, + "protocol_entity_id":ENTITY}, + "btdu_binding":{"schema":"entity-btdu-passport-binding-v1","object_ref":"btdu:child"}} + db.execute("INSERT INTO global_passports VALUES(?,?,?,?,?,?,?,?)", + ("gp1","obj-child",BTG,"1.1","g"*64,json.dumps(gp),"{}",10)) + db.commit(); db.close() + + fp=state/"entity_v3"/"universal_fabric.sqlite"; fp.parent.mkdir(parents=True) + db=sqlite3.connect(fp) + db.execute("""CREATE TABLE objects(object_id TEXT,controller_entity_id TEXT,object_type TEXT,title TEXT, + content_sha256 TEXT,descriptor_json TEXT,status TEXT,created_at_ms INTEGER,signature_json TEXT)""") + db.execute("""CREATE TABLE provenance_edges(edge_id TEXT,parent_object_id TEXT,child_object_id TEXT, + relation TEXT,contribution_bps INTEGER,evidence_json TEXT,actor_entity_id TEXT, + created_at_ms INTEGER,signature_json TEXT)""") + db.execute("INSERT INTO objects VALUES(?,?,?,?,?,?,?,?,?)", + ("obj-parent",BTG,"DATASET","BIRFR-1","a"*64,"{}","ACTIVE",1,"{}")) + db.execute("INSERT INTO objects VALUES(?,?,?,?,?,?,?,?,?)", + ("obj-child",BTG,"SOFTWARE","Failure Detector","b"*64, + json.dumps({"digital_commodity":True}),"ACTIVE",2,"{}")) + db.execute("INSERT INTO provenance_edges VALUES(?,?,?,?,?,?,?,?,?)", + ("edge1","obj-parent","obj-child","TRAINED_AND_DERIVED_FROM",0,"{}",BTG,3,"{}")) + db.commit(); db.close() + + resolver=m.WalletLineageResolver(state) + result=resolver.resolve({"object_id":"obj-child","controller_entity_id":BTG}) + assert result["protocol_lineage"]["display_path"]=="Shawn Blackmore → Blackmore Technology Group Limited → ENTITY → Robotics" + assert result["protocol_lineage"]["passport_protocol_origin_embedded"] is True + assert result["asset_lineage"]["controller_entity_id"]==BTG + assert result["asset_lineage"]["parents"][0]["title"]=="BIRFR-1" + assert result["asset_lineage"]["protocol_origin_does_not_transfer_asset_ownership"] is True + assert result["capability_bindings"]["BTDU"]["bound"] is True + assert result["capability_bindings"]["ADAM"]["sovereign_authority"] is False + assert result["capability_bindings"]["NIKI"]["sovereign_authority"] is False + +def test_legacy_passport_without_embedded_origin_is_flagged_for_supersession(): + m=load() + with tempfile.TemporaryDirectory() as td: + state=Path(td) + for eid,name in ((SHAWN,"Shawn Blackmore"),(BTG,"Blackmore Technology Group Limited"),(ENTITY,"ENTITY")): + write_manifest(state,eid,name) + db=sqlite3.connect(state/"entity_protocol_origin.sqlite") + db.execute("CREATE TABLE origin_chains(lineage_id TEXT PRIMARY KEY,body_sha256 TEXT,body_json TEXT,signatures_json TEXT)") + body={"lineage_id":"entity-origin:shawn-btg-entity@1.0","root_originator_entity_id":SHAWN, + "steward_entity_id":BTG,"protocol_entity_id":ENTITY} + db.execute("INSERT INTO origin_chains VALUES(?,?,?,?)",(body["lineage_id"],"o"*64,json.dumps(body),"{}")); db.commit(); db.close() + db=sqlite3.connect(state/"entity_v3_4_global_passports.sqlite") + db.execute("""CREATE TABLE global_passports(passport_id TEXT,object_id TEXT,controller_entity_id TEXT, + version TEXT,body_sha256 TEXT,body_json TEXT,signature_json TEXT,created_at_ms INTEGER)""") + gp={"passport_id":"legacy","object_id":"obj","controller_entity_id":BTG,"version":"1.0", + "profile_stack":{"profile_refs":[]},"protocol_origin":None} + db.execute("INSERT INTO global_passports VALUES(?,?,?,?,?,?,?,?)",("legacy","obj",BTG,"1.0","x"*64,json.dumps(gp),"{}",1)); db.commit(); db.close() + fp=state/"entity_v3"/"universal_fabric.sqlite"; fp.parent.mkdir(parents=True) + db=sqlite3.connect(fp); db.execute("""CREATE TABLE provenance_edges(edge_id TEXT,parent_object_id TEXT,child_object_id TEXT, + relation TEXT,contribution_bps INTEGER,evidence_json TEXT,actor_entity_id TEXT,created_at_ms INTEGER,signature_json TEXT)"""); db.commit(); db.close() + result=m.WalletLineageResolver(state).resolve({"object_id":"obj","controller_entity_id":BTG}) + assert result["protocol_lineage"]["resolved_from_canonical_origin_registry"] is True + assert result["protocol_lineage"]["needs_passport_lineage_supersession"] is True From b50f7a4c10561c00ba4cd9291fb1c637c8d5a677 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:22:43 -0700 Subject: [PATCH 027/111] wallet: close lineage DB handles and tolerate partial state --- src/42_ENTITY_Wallet/lineage_resolver.py | 72 +++++++++++++++--------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/src/42_ENTITY_Wallet/lineage_resolver.py b/src/42_ENTITY_Wallet/lineage_resolver.py index 17293695..1d4a34eb 100644 --- a/src/42_ENTITY_Wallet/lineage_resolver.py +++ b/src/42_ENTITY_Wallet/lineage_resolver.py @@ -1,5 +1,6 @@ from __future__ import annotations from pathlib import Path +from contextlib import contextmanager import json, sqlite3 CANONICAL_LINEAGE_ID="entity-origin:shawn-btg-entity@1.0" @@ -29,24 +30,33 @@ def _name(self,entity_id:str|None)->str|None: return str(entity_id) @staticmethod + @contextmanager def _db(path:Path): - db=sqlite3.connect(path); db.row_factory=sqlite3.Row; return db + db=sqlite3.connect(path); db.row_factory=sqlite3.Row + try: yield db + finally: db.close() def _origin(self)->dict|None: if not self.origin_path.exists(): return None - with self._db(self.origin_path) as db: - row=db.execute("SELECT body_json,body_sha256 FROM origin_chains WHERE lineage_id=?", - (CANONICAL_LINEAGE_ID,)).fetchone() + try: + with self._db(self.origin_path) as db: + row=db.execute("SELECT body_json,body_sha256 FROM origin_chains WHERE lineage_id=?", + (CANONICAL_LINEAGE_ID,)).fetchone() + except sqlite3.OperationalError: + return None if not row: return None body=json.loads(row["body_json"]); body["body_sha256"]=row["body_sha256"]; return body def _passport(self,object_id:str)->dict|None: if not self.passport_path.exists(): return None - with self._db(self.passport_path) as db: - row=db.execute("""SELECT passport_id,version,body_sha256,body_json,created_at_ms - FROM global_passports WHERE object_id=? - ORDER BY created_at_ms DESC,passport_id DESC LIMIT 1""", - (str(object_id),)).fetchone() + try: + with self._db(self.passport_path) as db: + row=db.execute("""SELECT passport_id,version,body_sha256,body_json,created_at_ms + FROM global_passports WHERE object_id=? + ORDER BY created_at_ms DESC,passport_id DESC LIMIT 1""", + (str(object_id),)).fetchone() + except sqlite3.OperationalError: + return None if not row: return None body=json.loads(row["body_json"]) return {**body,"body_sha256":row["body_sha256"],"created_at_ms":row["created_at_ms"]} @@ -54,28 +64,36 @@ def _passport(self,object_id:str)->dict|None: def _profiles(self,refs:list[str])->list[dict]: if not self.profile_path.exists(): return [{"profile_ref":r} for r in refs] out=[] - with self._db(self.profile_path) as db: - for ref in refs: - row=db.execute("""SELECT profile_ref,kind,body_json,issuer_entity_id,body_sha256 - FROM profile_variants WHERE profile_ref=? - ORDER BY active_canonical DESC,created_at_ms DESC LIMIT 1""",(ref,)).fetchone() - if not row: - out.append({"profile_ref":ref}); continue - body=json.loads(row["body_json"]) - out.append({"profile_ref":ref,"profile_id":body.get("profile_id"),"kind":row["kind"], - "issuer_entity_id":row["issuer_entity_id"],"issuer_name":self._name(row["issuer_entity_id"]), - "body_sha256":row["body_sha256"]}) + try: + dbctx=self._db(self.profile_path) + with dbctx as db: + rows_available=True + for ref in refs: + row=db.execute("""SELECT profile_ref,kind,body_json,issuer_entity_id,body_sha256 + FROM profile_variants WHERE profile_ref=? + ORDER BY active_canonical DESC,created_at_ms DESC LIMIT 1""",(ref,)).fetchone() + if not row: + out.append({"profile_ref":ref}); continue + body=json.loads(row["body_json"]) + out.append({"profile_ref":ref,"profile_id":body.get("profile_id"),"kind":row["kind"], + "issuer_entity_id":row["issuer_entity_id"],"issuer_name":self._name(row["issuer_entity_id"]), + "body_sha256":row["body_sha256"]}) + except sqlite3.OperationalError: + return [{"profile_ref":r} for r in refs] return out def _asset_parents(self,object_id:str)->list[dict]: if not self.fabric_path.exists(): return [] - with self._db(self.fabric_path) as db: - rows=db.execute("""SELECT p.edge_id,p.parent_object_id,p.relation,p.contribution_bps, - o.title,o.controller_entity_id - FROM provenance_edges p - LEFT JOIN objects o ON o.object_id=p.parent_object_id - WHERE p.child_object_id=? - ORDER BY p.created_at_ms,p.edge_id""",(str(object_id),)).fetchall() + try: + with self._db(self.fabric_path) as db: + rows=db.execute("""SELECT p.edge_id,p.parent_object_id,p.relation,p.contribution_bps, + o.title,o.controller_entity_id + FROM provenance_edges p + LEFT JOIN objects o ON o.object_id=p.parent_object_id + WHERE p.child_object_id=? + ORDER BY p.created_at_ms,p.edge_id""",(str(object_id),)).fetchall() + except sqlite3.OperationalError: + return [] return [{**dict(r),"controller_name":self._name(r["controller_entity_id"])} for r in rows] @staticmethod From d2cb3ce9a514b8eaa1e4db7eeaddbd67f18fac38 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:24:16 -0700 Subject: [PATCH 028/111] wallet: reject duplicate DCOs before BTDU mutation --- src/42_ENTITY_Wallet/asset_ingest.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/42_ENTITY_Wallet/asset_ingest.py b/src/42_ENTITY_Wallet/asset_ingest.py index 6b40c240..0f11a5ae 100644 --- a/src/42_ENTITY_Wallet/asset_ingest.py +++ b/src/42_ENTITY_Wallet/asset_ingest.py @@ -1,6 +1,6 @@ from __future__ import annotations from pathlib import Path -import json, sqlite3 +import hashlib, json, sqlite3 INGEST_SCHEMA="entity-wallet-canonical-asset-ingest-v2" @@ -75,6 +75,13 @@ def ingest_file(self,controller_entity_id:str,file_path:str|Path,*,title:str|Non configuration:dict|None=None)->dict: src=Path(file_path).expanduser().resolve() if not src.is_file(): raise FileNotFoundError(str(src)) + digest=hashlib.sha256() + with src.open("rb") as f: + for chunk in iter(lambda:f.read(1024*1024),b""): digest.update(chunk) + source_sha=digest.hexdigest() + existing=self._existing_dco(controller_entity_id,source_sha) + if existing: + raise ValueError(f"identical Digital Commodity Object already registered: {existing}") identity,fabric,profiles,origin,passports,packages,sdk,origin_status=self.cli.runtime( self.state,require_current_release=True) release=origin.passport_binding("entity-release:v3.4.3") @@ -92,9 +99,7 @@ def ingest_file(self,controller_entity_id:str,file_path:str|Path,*,title:str|Non finally: universe.close() content_sha=str(binding["content_sha256"]) - existing=self._existing_dco(controller_entity_id,content_sha) - if existing: - raise ValueError(f"identical Digital Commodity Object already registered: {existing}") + if content_sha!=source_sha: raise RuntimeError("BTDU content hash differs from source asset") commodity_meta={ "wallet_ingest_schema":INGEST_SCHEMA, "authority_basis":str(authority_basis), From ec5899b586430f1ce09c01f6814bc2dbc42ad4ed Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:25:21 -0700 Subject: [PATCH 029/111] tests: use source-accurate BTDU hash in wallet fixture --- tests/test_wallet_asset_ingest.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_wallet_asset_ingest.py b/tests/test_wallet_asset_ingest.py index 679054d7..d66920a0 100644 --- a/tests/test_wallet_asset_ingest.py +++ b/tests/test_wallet_asset_ingest.py @@ -1,6 +1,6 @@ from __future__ import annotations from pathlib import Path -import importlib.util, json, sqlite3, sys, tempfile +import hashlib, importlib.util, json, sqlite3, sys, tempfile ROOT=Path(__file__).resolve().parents[1] @@ -49,11 +49,13 @@ def ingest_package_file(self,path,controller,package,config,asset_kind,**kwargs) return {"object_id":"obj3-test","global_passport_id":"gp-test"} class FakeUniverse: + def __init__(self): self.content_sha256=None def ingest_file(self,path,receipt,**kwargs): - return {"object_ref":"btdu:test","content_sha256":"a"*64} + self.content_sha256=hashlib.sha256(Path(path).read_bytes()).hexdigest() + return {"object_ref":"btdu:test","content_sha256":self.content_sha256} def passport_binding(self,ref): return {"schema":"entity-btdu-passport-binding-v1","universe_root":"u","object_ref":ref, - "content_sha256":"a"*64,"sovereign_entity_id":"BTG", + "content_sha256":self.content_sha256,"sovereign_entity_id":"BTG", "protocol_origin_is_not_asset_provenance":True,"topology_does_not_create_ownership":True, "topology_does_not_create_economic_entitlement":True,"automatic_protocol_royalty_bps":0} def close(self): pass From ff38a5ca48a821c8c522ea8c31a4884ecc679da1 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:28:00 -0700 Subject: [PATCH 030/111] wallet: represent composed profiles without false lineage chaining --- src/42_ENTITY_Wallet/lineage_resolver.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/42_ENTITY_Wallet/lineage_resolver.py b/src/42_ENTITY_Wallet/lineage_resolver.py index 1d4a34eb..7fadf977 100644 --- a/src/42_ENTITY_Wallet/lineage_resolver.py +++ b/src/42_ENTITY_Wallet/lineage_resolver.py @@ -117,7 +117,9 @@ def resolve(self,asset:dict)->dict: refs=list(((passport or {}).get("profile_stack") or {}).get("profile_refs") or []) profiles=self._profiles(refs) domains=[self._profile_label(x) for x in refs if "entity-profile:global@" not in x] - path=[self._name(root),self._name(steward),self._name(protocol),*domains] + primary_domain="Robotics" if "Robotics" in domains else (domains[0] if domains else None) + composed_domains=[x for x in domains if x!=primary_domain] + path=[self._name(root),self._name(steward),self._name(protocol),primary_domain] path=[x for x in path if x] parents=self._asset_parents(object_id) btdu=dict((passport or {}).get("btdu_binding") or {}) @@ -129,6 +131,8 @@ def resolve(self,asset:dict)->dict: "steward_entity_id":steward,"steward_name":self._name(steward), "protocol_entity_id":protocol,"protocol_name":self._name(protocol), "profiles":profiles,"display_path":" → ".join(path), + "primary_domain_profile":primary_domain,"composed_domain_profiles":composed_domains, + "profile_composition_is_not_parent_child_lineage":True, "passport_protocol_origin_embedded":bool(embedded), "resolved_from_canonical_origin_registry":bool(origin), "needs_passport_lineage_supersession":bool(passport and not embedded), From 3671bdbdf0d5d1694a3ecf5a9ef70d680078919c Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:28:16 -0700 Subject: [PATCH 031/111] tests: keep Robotics primary and AI Manufacturing as co-profiles --- tests/test_wallet_lineage_resolution.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_wallet_lineage_resolution.py b/tests/test_wallet_lineage_resolution.py index d11f0078..0a3c355b 100644 --- a/tests/test_wallet_lineage_resolution.py +++ b/tests/test_wallet_lineage_resolution.py @@ -34,7 +34,8 @@ def test_robotics_lineage_resolves_shawn_btg_entity_robotics_without_transferrin kind TEXT,body_json TEXT,issuer_entity_id TEXT,signature_json TEXT,created_at_ms INTEGER, active_canonical INTEGER)""") for i,(ref,kind) in enumerate([ - ("entity-profile:global@1.0","GLOBAL"),("entity-profile:robotics@1.0","INDUSTRY")]): + ("entity-profile:global@1.0","GLOBAL"),("entity-profile:robotics@1.0","INDUSTRY"), + ("entity-profile:ai@1.0","INDUSTRY"),("entity-profile:manufacturing@1.0","INDUSTRY")]): body={"profile_ref":ref,"profile_id":ref.split("@")[0],"kind":kind} db.execute("INSERT INTO profile_variants VALUES(?,?,?,?,?,?,?,?,?,?)", (ref,str(i)*64,body["profile_id"],"1.0",kind,json.dumps(body),ENTITY,"{}",i,1)) @@ -44,7 +45,8 @@ def test_robotics_lineage_resolves_shawn_btg_entity_robotics_without_transferrin db.execute("""CREATE TABLE global_passports(passport_id TEXT,object_id TEXT,controller_entity_id TEXT, version TEXT,body_sha256 TEXT,body_json TEXT,signature_json TEXT,created_at_ms INTEGER)""") gp={"passport_id":"gp1","object_id":"obj-child","controller_entity_id":BTG,"version":"1.1", - "profile_stack":{"profile_refs":["entity-profile:global@1.0","entity-profile:robotics@1.0"]}, + "profile_stack":{"profile_refs":["entity-profile:global@1.0","entity-profile:robotics@1.0", + "entity-profile:ai@1.0","entity-profile:manufacturing@1.0"]}, "protocol_origin":{"origin_lineage_id":"entity-origin:shawn-btg-entity@1.0", "root_originator_entity_id":SHAWN,"steward_entity_id":BTG, "protocol_entity_id":ENTITY}, @@ -72,6 +74,8 @@ def test_robotics_lineage_resolves_shawn_btg_entity_robotics_without_transferrin resolver=m.WalletLineageResolver(state) result=resolver.resolve({"object_id":"obj-child","controller_entity_id":BTG}) assert result["protocol_lineage"]["display_path"]=="Shawn Blackmore → Blackmore Technology Group Limited → ENTITY → Robotics" + assert result["protocol_lineage"]["composed_domain_profiles"]==["AI","Manufacturing"] + assert result["protocol_lineage"]["profile_composition_is_not_parent_child_lineage"] is True assert result["protocol_lineage"]["passport_protocol_origin_embedded"] is True assert result["asset_lineage"]["controller_entity_id"]==BTG assert result["asset_lineage"]["parents"][0]["title"]=="BIRFR-1" From 52bdb8795a6eb2f39b045d7451d09e56b0a6222a Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:29:33 -0700 Subject: [PATCH 032/111] docs: lock ENTITY lineage wallet DCO and economy architecture --- ...NEAGE_WALLET_ECONOMY_ALIGNMENT_20261003.md | 228 ++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 docs/v3.4/ENTITY_LINEAGE_WALLET_ECONOMY_ALIGNMENT_20261003.md diff --git a/docs/v3.4/ENTITY_LINEAGE_WALLET_ECONOMY_ALIGNMENT_20261003.md b/docs/v3.4/ENTITY_LINEAGE_WALLET_ECONOMY_ALIGNMENT_20261003.md new file mode 100644 index 00000000..e61b4525 --- /dev/null +++ b/docs/v3.4/ENTITY_LINEAGE_WALLET_ECONOMY_ALIGNMENT_20261003.md @@ -0,0 +1,228 @@ +# ENTITY Lineage, Wallet, DCO and Economy Alignment + +Date: 2026-10-03 +ENTITY release: v3.4.3 +Status: canonical application-layer alignment + +## 1. The lineage model + +ENTITY has two independent lineage planes. They must never be collapsed. + +### 1.1 Protocol and domain lineage + +The canonical ENTITY protocol ancestry is: + +```text +Shawn Blackmore + ↓ FOUNDER_ORIGINATOR_CONTEXT +Blackmore Technology Group Limited + ↓ PROTOCOL_STEWARDSHIP +ENTITY + ↓ signed release origin +ENTITY v3.4.3 + ↓ ENTITY-issued domain profile +Robotics +``` + +The canonical origin identifier is: + +`entity-origin:shawn-btg-entity@1.0` + +This lineage identifies protocol origin and domain context. It does **not** transfer ownership of a user's data or asset to Shawn, BTG, ENTITY, or a profile. + +A Global Passport for a Robotics asset must therefore carry both: + +- a verified protocol-origin binding to the canonical ENTITY release; and +- `entity-profile:robotics@1.0` in its resolved profile stack. + +### 1.2 Profile composition is not parent/child lineage + +Robotics assets may also compose profiles such as AI or Manufacturing: + +```text +Primary domain: Robotics +Composed profiles: AI, Manufacturing +``` + +The wallet must not render this as: + +`Robotics → AI → Manufacturing` + +because those are composed profiles, not ancestry edges. + +### 1.3 Asset provenance and control + +Asset provenance is separate: + +```text +BTG + ↓ creates / controls +DCO-000001 BIRFR-1 + ↓ trained/derived relationship +DCO-000002 Failure Detection Engine +``` + +For another participant, that same asset plane may instead begin with that participant's ENTITY identity. + +Protocol origin does not infer asset ownership. Asset provenance does not redefine protocol origin. + +## 2. ENTITY, ADAM, NIKI and BTDU + +The system roles remain: + +- **ENTITY** — sovereign identity, authority, rights, policy, economic semantics and protocol origin. +- **ADAM** — deterministic governed execution, state transition and evidence substrate. +- **NIKI** — bounded reasoning and proposals. NIKI does not acquire sovereign authority merely by reasoning about an asset. +- **BTDU** — governed information topology and exact/semantic representation over ADAM. BTDU topology cannot create ownership, rights, royalties or economic entitlement. + +For wallet display, ADAM, NIKI and BTDU are capability/evidence bindings, not ownership ancestors. + +## 3. Canonical Digital Commodity ingest + +The desktop wallet must use the same canonical v3.4.3 stack as the deployment CLI. + +```text +SELECT / CREATE ASSET + ↓ +verify controller identity + ↓ +verify v3.4.3 release-origin attestation + ↓ +BTDU signed ingest + content hash + ↓ +register asset as DCO in Universal Transaction Fabric + ↓ +issue evidence + ↓ +grant controller rights + ↓ +issue Rights Passport + ↓ +resolve Global + domain profiles + ↓ +issue Global Passport + ↓ +embed protocol origin + ↓ +bind BTDU reference + ↓ +record zero POTENTIAL economic state + ↓ +MY DIGITAL ASSETS +``` + +Ingest must not automatically create: + +- an EEP instrument; +- a licence; +- a market listing; +- an ask price; +- a market value; +- an originator royalty; +- a protocol fee. + +The DCO is the governed digital commodity asset. Market rights are optional economic instruments created later by explicit controller action. + +## 4. Wallet separation + +The native wallet has two economically different holdings surfaces. + +### My Digital Assets + +Shows DCOs controlled by the wallet identity. + +Each asset can display: + +- title and content hash; +- real object type; +- commodity class; +- controller; +- canonical protocol/domain lineage; +- asset provenance parents; +- current Global Passport; +- BTDU binding state; +- whether the passport embeds current protocol origin. + +### Market Positions + +Shows EEP rights units that the participant owns or holds. + +A market position is not the underlying DCO. + +## 5. Economy + +The original market lifecycle remains: + +```text +DCO + → optional EEP instrument + → listing / disclosure + → order, RFQ or auction + → price discovery + → trade + → clearing + → settlement + → entitlement + → usage + → derived output + → economic consequence +``` + +The exchange continues to use actual orders and settled trades for market observations. + +Registration of a DCO does not establish market value. An offer does not establish realized value. Protocol tax remains zero and cryptocurrency is not required. + +## 6. Robotics correction + +The production Robotics assets are retained as the same signed objects. + +- **DCO-000001 — BIRFR-1** remains the BTG-controlled Robotics failure/recovery dataset. +- **DCO-000002 — Blackmore Real-Time Failure Detection Engine** remains the BTG-created Robotics algorithm derived from DCO-000001. + +Their historical Global Passport v1.0 records are preserved. + +Lineage-complete Global Passport v1.1 records now bind each asset to: + +- `entity-origin:shawn-btg-entity@1.0`; +- `entity-release:v3.4.3`; +- Global + Robotics + AI + Manufacturing profiles; +- the existing BTDU binding; +- the existing BTG controller and Rights Passport. + +The wallet renders the primary protocol/domain path as: + +`Shawn Blackmore → Blackmore Technology Group Limited → ENTITY → Robotics` + +AI and Manufacturing are shown as composed profiles. + +DCO-000003 is not promoted to the production asset registry while its qualification remains failed. + +## 7. v3.4.3 release-origin repair + +The v3.4.3 release originally lacked the post-tag `ENTITY_CURRENT_RELEASE_ORIGIN.json` sidecar required by the canonical v3.4.3 ingest path. + +A signed post-tag attestation was generated with the existing canonical ENTITY protocol identity and verified against: + +- release tag: `v3.4.3` +- commit: `528b70aabd05b1e930b77e4933f157731e47274f` +- tree: `f13e42f515dcc4cd985e9b832e0ecfec416ce423` +- origin lineage: `entity-origin:shawn-btg-entity@1.0` + +The attestation does not contain private key material. + +## 8. Locked invariants for Robotics 3 and later + +Future Robotics assets must: + +1. be built and qualified as actual assets before economic issuance; +2. retain their real asset type (SOFTWARE, MODEL, DATASET, DEVICE, etc.); +3. enter ENTITY through the canonical origin/passport/BTDU path; +4. use Robotics as the primary domain profile where applicable; +5. preserve explicit asset-to-asset provenance for derivations; +6. keep NIKI, ADAM and BTDU roles separate from ownership; +7. create market instruments only by explicit controller choice; +8. never invent value from registration or an asking price; +9. preserve historical signed records rather than rewriting them; +10. fail closed when required origin, identity, rights, evidence or qualification is missing. + +This alignment is an application/runtime correction. It does not require a new ENTITY protocol version. From 571d7b31181594b361f4daeaf22374d7a4537437 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:31:07 -0700 Subject: [PATCH 033/111] btdu: keep canonical BTG universe rooted at Shawn --- tools/entity_v3_4_cli.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tools/entity_v3_4_cli.py b/tools/entity_v3_4_cli.py index f4392c3b..f65e0815 100644 --- a/tools/entity_v3_4_cli.py +++ b/tools/entity_v3_4_cli.py @@ -45,14 +45,23 @@ def emit(value): print(json.dumps(value,indent=2,sort_keys=True)) def _btdu_for_controller(state,identity,controller_entity_id): controller=str(controller_entity_id); identity.load_manifest(controller) + # BTG canonical assets live in the one canonical BTG universe rooted at + # shawn.blackmore.entity. Asset source/controller/rights-holder remain explicit + # per-object and are not replaced by the universe sovereign identity. + canonical_ids={ + btdu_mod.BTDU_CANONICAL_OWNER_ENTITY_ID, + btdu_mod.BTG_STEWARD_ENTITY_ID, + btdu_mod.ENTITY_PROTOCOL_ENTITY_ID, + } + universe_owner=btdu_mod.BTDU_CANONICAL_OWNER_ENTITY_ID if controller in canonical_ids else controller def verifier(record): try: body=dict(record["body"]); sig=dict(record["signature"]) if body.get("scope")!="BTDU_WRITE" or body.get("actor_entity_id")!=controller: return False return identity.verify_signature(identity.load_manifest(controller),body,sig) except Exception: return False - return btdu_mod.BlackmoreTechnologyDataUniverse(pathlib.Path(state)/"btdu"/controller, - authorization_verifier=verifier,sovereign_entity_id=controller, + return btdu_mod.BlackmoreTechnologyDataUniverse(pathlib.Path(state)/"btdu"/universe_owner, + authorization_verifier=verifier,sovereign_entity_id=universe_owner, protocol_entity_id=btdu_mod.ENTITY_PROTOCOL_ENTITY_ID,steward_entity_id=btdu_mod.BTG_STEWARD_ENTITY_ID) def _btdu_receipt(identity,controller_entity_id,path,logical_path): From 2ddea55fde224ce0cb7d613a10bae30e33863178 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:40:42 -0700 Subject: [PATCH 034/111] economy: add signed instrument withdrawal lifecycle --- src/31_Profiles/exchange_protocol.py | 80 +++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/src/31_Profiles/exchange_protocol.py b/src/31_Profiles/exchange_protocol.py index 8121df30..1a03b154 100644 --- a/src/31_Profiles/exchange_protocol.py +++ b/src/31_Profiles/exchange_protocol.py @@ -46,7 +46,16 @@ def sha256(name): require_sha256(body.get(name)) actions=rights.get('actions') if type(actions) is not list or not actions or any(type(a) is not str or not a or a!=a.upper() for a in actions): raise ValueError('rights.actions must be non-empty canonical uppercase strings') if actions!=sorted(set(actions)): raise ValueError('rights.actions must be sorted and unique') - elif schema=='entity-eep-disclosure-v1': + elif schema=='entity-eep-instrument-withdrawal-v1': + for n in ('withdrawal_id','instrument_id','issuer','reason'): sid(n) + common_time() + if body.get('prior_status')!='ACTIVE' or body.get('new_status')!='WITHDRAWN': + raise ValueError('invalid instrument withdrawal state transition') + evidence=body.get('evidence_sha256') + if evidence is not None: require_sha256(evidence) + if body.get('no_new_rights_created') is not True: + raise ValueError('withdrawal rights-boundary flag required') + elif schema=='entity-eep-disclosure-v1': for n in ('disclosure_id','venue_id','instrument_id','publisher'): sid(n) sup('disclosure_type'); sha256('content_sha256'); common_time() if body.get('protected_data_included') is not False: raise ValueError('protected_data_included must be false') @@ -128,9 +137,14 @@ def _init(self): transferable INTEGER NOT NULL, duration_ms INTEGER, settlement_currency TEXT NOT NULL, delivery_mode TEXT NOT NULL, status TEXT NOT NULL, created_at_ms INTEGER NOT NULL, signature_json TEXT NOT NULL); - CREATE TABLE IF NOT EXISTS balances( - instrument_id TEXT NOT NULL, holder TEXT NOT NULL, units INTEGER NOT NULL, - PRIMARY KEY(instrument_id,holder)); + CREATE TABLE IF NOT EXISTS balances( + instrument_id TEXT NOT NULL, holder TEXT NOT NULL, units INTEGER NOT NULL, + PRIMARY KEY(instrument_id,holder)); + CREATE TABLE IF NOT EXISTS instrument_withdrawals( + withdrawal_id TEXT PRIMARY KEY, instrument_id TEXT NOT NULL UNIQUE, + issuer TEXT NOT NULL, prior_status TEXT NOT NULL, new_status TEXT NOT NULL, + reason TEXT NOT NULL, evidence_sha256 TEXT, created_at_ms INTEGER NOT NULL, + signature_json TEXT NOT NULL); CREATE TABLE IF NOT EXISTS listings( listing_id TEXT PRIMARY KEY, venue_id TEXT NOT NULL, instrument_id TEXT NOT NULL, lister TEXT NOT NULL, min_lot INTEGER NOT NULL, tick_size INTEGER NOT NULL, @@ -283,7 +297,61 @@ def define_instrument(self, issuer: str, underlying_object_id: str, instrument_c sig=self.identity.sign(issuer,body) return self._record_signed_instrument(body,sig) - def balance(self, instrument_id: str, holder: str) -> int: + def instrument_status(self, instrument_id: str) -> dict: + with self._db() as db: + inst=db.execute("SELECT instrument_id,issuer,status,created_at_ms FROM instruments WHERE instrument_id=?",(str(instrument_id),)).fetchone() + if not inst: raise KeyError("instrument not found") + withdrawal=db.execute("SELECT * FROM instrument_withdrawals WHERE instrument_id=?",(str(instrument_id),)).fetchone() + return { + "instrument_id":str(instrument_id), + "issuer":inst["issuer"], + "status":inst["status"], + "issued_at_ms":int(inst["created_at_ms"]), + "withdrawal":dict(withdrawal) | {"signature":json.loads(withdrawal["signature_json"])} if withdrawal else None, + } + + def _record_signed_instrument_withdrawal(self, body: dict, signature: dict) -> dict: + required={"schema","withdrawal_id","instrument_id","issuer","prior_status","new_status", + "reason","evidence_sha256","created_at_ms","no_new_rights_created"} + validate_signed_wire(body) + if set(body)!=required or body.get("schema")!="entity-eep-instrument-withdrawal-v1": + raise ValueError("invalid signed instrument withdrawal shape") + issuer=str(body["issuer"]); instrument_id=str(body["instrument_id"]) + manifest=self.identity.load_manifest(issuer) + if not self.identity.verify_signature(manifest,body,signature): + raise PermissionError("instrument withdrawal signature invalid") + with self._db() as db: + db.execute("BEGIN IMMEDIATE") + inst=db.execute("SELECT * FROM instruments WHERE instrument_id=?",(instrument_id,)).fetchone() + if not inst: raise KeyError("instrument not found") + if inst["issuer"]!=issuer: raise PermissionError("instrument issuer required") + if inst["status"]!="ACTIVE": raise ValueError("active instrument required") + if db.execute("SELECT 1 FROM orders WHERE instrument_id=? AND status IN ('OPEN','PARTIAL') LIMIT 1",(instrument_id,)).fetchone(): + raise ValueError("cannot withdraw instrument with open orders") + if db.execute("SELECT 1 FROM trades WHERE instrument_id=? AND status='EXECUTED_UNSETTLED' LIMIT 1",(instrument_id,)).fetchone(): + raise ValueError("cannot withdraw instrument with unsettled trades") + if db.execute("SELECT 1 FROM rfqs WHERE instrument_id=? AND status='OPEN' LIMIT 1",(instrument_id,)).fetchone(): + raise ValueError("cannot withdraw instrument with open RFQs") + db.execute("INSERT INTO instrument_withdrawals VALUES(?,?,?,?,?,?,?,?,?)",( + body["withdrawal_id"],instrument_id,issuer,body["prior_status"],body["new_status"], + body["reason"],body["evidence_sha256"],body["created_at_ms"],json.dumps(signature,sort_keys=True))) + db.execute("UPDATE instruments SET status='WITHDRAWN' WHERE instrument_id=?",(instrument_id,)) + db.execute("UPDATE listings SET status='WITHDRAWN' WHERE instrument_id=? AND status='ACTIVE'",(instrument_id,)) + return dict(body,signature=signature) + + def submit_signed_instrument_withdrawal(self, withdrawal: dict, signature: dict) -> dict: + return self._record_signed_instrument_withdrawal(dict(withdrawal),dict(signature)) + + def withdraw_instrument(self, issuer: str, instrument_id: str, reason: str, *, evidence_sha256: str | None=None) -> dict: + evidence=require_sha256(evidence_sha256) if evidence_sha256 is not None else None + body={"schema":"entity-eep-instrument-withdrawal-v1","withdrawal_id":rid("withdraw3"), + "instrument_id":str(instrument_id),"issuer":str(issuer),"prior_status":"ACTIVE", + "new_status":"WITHDRAWN","reason":str(reason),"evidence_sha256":evidence, + "created_at_ms":now_ms(),"no_new_rights_created":True} + sig=self.identity.sign(str(issuer),body) + return self._record_signed_instrument_withdrawal(body,sig) + + def balance(self, instrument_id: str, holder: str) -> int: with self._db() as db: row = db.execute("SELECT units FROM balances WHERE instrument_id=? AND holder=?", (instrument_id, holder)).fetchone() @@ -912,7 +980,7 @@ def surveillance_alerts(self, venue_id: str) -> list[dict]: def status(self) -> dict: with self._db() as db: - tables=("venues","instruments","listings","orders","order_cancellations","trades","clearing","payment_attestations","settlement_verifiers","entitlements","usage","surveillance","rfqs","quotes","rfq_acceptances","rfq_trade_sources","revenue_rule_sets","trade_revenue_bindings") + tables=("venues","instruments","listings","orders","order_cancellations","trades","clearing","payment_attestations","settlement_verifiers","entitlements","usage","surveillance","rfqs","quotes","rfq_acceptances","rfq_trade_sources","revenue_rule_sets","trade_revenue_bindings","instrument_withdrawals") counts={name:int(db.execute(f"SELECT COUNT(*) FROM {name}").fetchone()[0]) for name in tables} return {"schema":"entity-eep-status-v1","profile":"ENTITY_EXCHANGE_PROTOCOL","version":"3.0.0", "rights_are_traded_not_bytes":True,"venue_neutral":True,"payment_versus_right_transfer":True, From 8ec202fdb197e38e299215d3043ca6042f28880d Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:54:29 -0700 Subject: [PATCH 035/111] market: add universal multi-issuer instrument and listing registry --- .../canonical_market_registry.py | 452 ++++++++++++++++++ 1 file changed, 452 insertions(+) create mode 100644 src/45_ENTITY_Market/canonical_market_registry.py diff --git a/src/45_ENTITY_Market/canonical_market_registry.py b/src/45_ENTITY_Market/canonical_market_registry.py new file mode 100644 index 00000000..99d2264e --- /dev/null +++ b/src/45_ENTITY_Market/canonical_market_registry.py @@ -0,0 +1,452 @@ +from __future__ import annotations +from contextlib import contextmanager +from pathlib import Path +from typing import Any +import hashlib, json, re, sqlite3, textwrap, time + +MARKET_SCHEMA="entity-economic-market-registry-v1" +MARKET_VERSION="1.0.0" + +def now_ms()->int: return int(time.time()*1000) +def canon(v:Any)->bytes: return json.dumps(v,sort_keys=True,separators=(",",":"),ensure_ascii=False,default=str).encode() +def sha(v:Any)->str: return hashlib.sha256(v if isinstance(v,(bytes,bytearray)) else canon(v)).hexdigest() + +def _token(v:str)->str: + s=re.sub(r"[^A-Z0-9]+","-",str(v or "").upper()).strip("-") + if not s: raise ValueError("non-empty canonical token required") + return s + +def _safe_symbol(v:str)->str: + s=re.sub(r"[^A-Z0-9._-]+","-",str(v or "").upper()).strip("-._") + if not s or len(s)>32: raise ValueError("display symbol must be 1-32 canonical characters") + return s + +def _namespace_candidate(manifest:dict)->str: + explicit=str(manifest.get("market_namespace") or "").strip() + if explicit: return _token(explicit)[:20] + aliases=[str(x) for x in (manifest.get("aliases") or []) if str(x)] + for alias in sorted(aliases,key=len): + first=alias.split(".",1)[0] + try: + candidate=_token(first) + if 2<=len(candidate)<=20: return candidate + except ValueError: pass + name=str(manifest.get("display_name") or "") + words=[w for w in re.findall(r"[A-Za-z0-9]+",name) + if w.upper() not in {"LIMITED","LTD","INC","INCORPORATED","CORP","CORPORATION","COMPANY","CO","LLC"}] + if len(words)>=2: + acr="".join(w[0] for w in words).upper() + if 2<=len(acr)<=12: return acr + compact="".join(words).upper() + if compact: return compact[:20] + return "ENTITY-"+str(manifest["entity_id"])[-8:].upper() + +def canonical_instrument_id(issuer_entity_id:str,underlying_dco_id:str,rights_class:str,series:int)->str: + s=int(series) + if s<1: raise ValueError("series must be positive") + return f"entity.instrument:v1:{issuer_entity_id}:{underlying_dco_id}:{_token(rights_class).lower()}:{s:06d}" + +def canonical_listing_id(venue_id:str,instrument_id:str,series:int)->str: + s=int(series) + if s<1: raise ValueError("listing series must be positive") + digest=sha({"venue_id":str(venue_id),"instrument_id":str(instrument_id)})[:24] + return f"entity.listing:v1:{venue_id}:{digest}:{s:06d}" + +def _simple_pdf(text:str,path:Path)->None: + """Dependency-free human-readable PDF for portable instrument packages.""" + lines=[] + for raw in text.splitlines(): + if not raw.strip(): lines.append(""); continue + lines.extend(textwrap.wrap(raw,92,replace_whitespace=False,drop_whitespace=False) or [""]) + pages=[lines[i:i+52] for i in range(0,len(lines),52)] or [[]] + objs=[] + font_id=3+len(pages)*2 + kids=[] + for idx,page in enumerate(pages): + page_id=3+idx*2; content_id=page_id+1; kids.append(f"{page_id} 0 R") + stream=["BT","/F1 9 Tf","48 760 Td","12 TL"] + first=True + for line in page: + esc=line.replace("\\","\\\\").replace("(","\\(").replace(")","\\)") + if first: stream.append(f"({esc}) Tj"); first=False + else: stream.append(f"T* ({esc}) Tj") + stream.append("ET") + data="\n".join(stream).encode("latin-1","replace") + objs.append((page_id,f"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 {font_id} 0 R >> >> /Contents {content_id} 0 R >>".encode())) + objs.append((content_id,b"<< /Length "+str(len(data)).encode()+b" >>\nstream\n"+data+b"\nendstream")) + objs.extend([ + (1,b"<< /Type /Catalog /Pages 2 0 R >>"), + (2,f"<< /Type /Pages /Kids [{' '.join(kids)}] /Count {len(pages)} >>".encode()), + (font_id,b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>"), + ]) + objs=sorted(objs,key=lambda x:x[0]); buf=bytearray(b"%PDF-1.4\n"); offsets={0:0} + for oid,data in objs: + offsets[oid]=len(buf); buf.extend(f"{oid} 0 obj\n".encode()); buf.extend(data); buf.extend(b"\nendobj\n") + xref=len(buf); max_id=max(offsets) + buf.extend(f"xref\n0 {max_id+1}\n".encode()); buf.extend(b"0000000000 65535 f \n") + for i in range(1,max_id+1): buf.extend(f"{offsets.get(i,0):010d} 00000 n \n".encode()) + buf.extend(f"trailer\n<< /Size {max_id+1} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n".encode()) + path.write_bytes(buf) + +class EntityEconomicMarketRegistry: + """Universal multi-issuer market registry above the EEP execution engine. + + ENTITY owns protocol/registry rules, never listed assets. Any verified ENTITY + controller may issue instruments over its own DCOs under the same rules. + """ + + def __init__(self,state_dir:str|Path,identity,fabric,exchange,rights_passports,global_passports): + self.state=Path(state_dir); self.identity=identity; self.fabric=fabric; self.exchange=exchange + self.rights=rights_passports; self.global_passports=global_passports + self.path=self.state/"entity_market_registry.sqlite" + self._init() + + @contextmanager + def _db(self): + db=sqlite3.connect(self.path); db.row_factory=sqlite3.Row + try: + yield db; db.commit() + except Exception: + db.rollback(); raise + finally: db.close() + + def _init(self): + with self._db() as db: + db.executescript(""" + CREATE TABLE IF NOT EXISTS issuer_namespaces( + namespace TEXT PRIMARY KEY, issuer_entity_id TEXT NOT NULL UNIQUE, + display_name TEXT NOT NULL, created_at_ms INTEGER NOT NULL, + signature_json TEXT NOT NULL); + CREATE TABLE IF NOT EXISTS instrument_packages( + instrument_id TEXT PRIMARY KEY, issuer_entity_id TEXT NOT NULL, + issuer_namespace TEXT NOT NULL, underlying_dco_id TEXT NOT NULL, + instrument_name TEXT NOT NULL, display_symbol TEXT NOT NULL, + market_identifier TEXT NOT NULL UNIQUE, instrument_class TEXT NOT NULL, + rights_class TEXT NOT NULL, series INTEGER NOT NULL, + fungibility TEXT NOT NULL, divisibility INTEGER NOT NULL, supply INTEGER NOT NULL, + rights_passport_id TEXT NOT NULL, global_passport_id TEXT NOT NULL, + jurisdiction TEXT NOT NULL, transfer_rules_json TEXT NOT NULL, + economic_terms_json TEXT NOT NULL, royalty_terms_json TEXT NOT NULL, + buyer_receives_json TEXT NOT NULL, buyer_does_not_receive_json TEXT NOT NULL, + evidence_refs_json TEXT NOT NULL, status TEXT NOT NULL, + created_at_ms INTEGER NOT NULL, signature_json TEXT NOT NULL, + UNIQUE(issuer_entity_id,underlying_dco_id,rights_class,series), + UNIQUE(issuer_entity_id,display_symbol)); + CREATE TABLE IF NOT EXISTS listing_packages( + listing_id TEXT PRIMARY KEY, instrument_id TEXT NOT NULL, + venue_id TEXT NOT NULL, market_id TEXT NOT NULL, + display_symbol TEXT NOT NULL, quote_unit TEXT NOT NULL, + trade_mode TEXT NOT NULL, settlement_method TEXT NOT NULL, + minimum_quantity INTEGER NOT NULL, quantity_precision INTEGER NOT NULL, + price_precision INTEGER NOT NULL, pricing_method TEXT NOT NULL, + listing_information_sha256 TEXT NOT NULL, machine_manifest_sha256 TEXT NOT NULL, + status TEXT NOT NULL, listed_at_ms INTEGER NOT NULL, + signature_json TEXT NOT NULL, + UNIQUE(venue_id,instrument_id,listing_id)); + CREATE TABLE IF NOT EXISTS listing_information( + listing_id TEXT PRIMARY KEY, information_json TEXT NOT NULL, + information_markdown TEXT NOT NULL, created_at_ms INTEGER NOT NULL); + CREATE INDEX IF NOT EXISTS idx_market_symbol ON instrument_packages(issuer_namespace,display_symbol); + """) + + def ensure_namespace(self,issuer_entity_id:str,preferred:str|None=None)->dict: + issuer=str(issuer_entity_id); manifest=self.identity.load_manifest(issuer) + with self._db() as db: + prior=db.execute("SELECT * FROM issuer_namespaces WHERE issuer_entity_id=?",(issuer,)).fetchone() + if prior: return dict(prior) | {"signature":json.loads(prior["signature_json"])} + base=_token(preferred) if preferred else _namespace_candidate(manifest) + if len(base)>20: base=base[:20] + candidate=base + with self._db() as db: + n=1 + while True: + conflict=db.execute("SELECT issuer_entity_id FROM issuer_namespaces WHERE namespace=?",(candidate,)).fetchone() + if not conflict or conflict["issuer_entity_id"]==issuer: break + n+=1; suffix="-"+str(n); candidate=base[:20-len(suffix)]+suffix + body={"schema":"entity-market-issuer-namespace-v1","namespace":candidate, + "issuer_entity_id":issuer,"display_name":str(manifest.get("display_name") or issuer), + "created_at_ms":now_ms(),"namespace_is_alias_not_authority":True} + sig=self.identity.sign(issuer,body) + db.execute("INSERT INTO issuer_namespaces VALUES(?,?,?,?,?)",( + candidate,issuer,body["display_name"],body["created_at_ms"],json.dumps(sig,sort_keys=True))) + return dict(body,signature=sig) + + def _verify_passports(self,issuer:str,dco_id:str,rights_passport_id:str,global_passport_id:str)->tuple[dict,dict]: + rp=self.rights.get(str(rights_passport_id)); gp=self.global_passports.get(str(global_passport_id)) + if rp["object_id"]!=dco_id or rp["controller_entity_id"]!=issuer: + raise PermissionError("Rights Passport must bind issuer-controlled underlying DCO") + if gp["object_id"]!=dco_id or gp["controller_entity_id"]!=issuer: + raise PermissionError("Global Passport must bind issuer-controlled underlying DCO") + if gp["rights_passport_id"]!=rp["passport_id"]: + raise ValueError("Global Passport does not bind supplied Rights Passport") + if not self.rights.verify(rp).get("valid"): raise ValueError("Rights Passport verification failed") + if not self.global_passports.verify(gp).get("valid"): raise ValueError("Global Passport verification failed") + return rp,gp + + def create_instrument(self,issuer_entity_id:str,underlying_dco_id:str,*,instrument_name:str, + display_symbol:str,instrument_class:str,rights_class:str,rights:dict, + supply:int,rights_passport_id:str,global_passport_id:str, + settlement_currency:str,jurisdiction:str,series:int=1, + fungibility:str="FUNGIBLE",divisibility:int=0,transferable:bool=False, + duration_ms:int|None=None,delivery_mode:str="ENTITLEMENT", + transfer_rules:dict|None=None,economic_terms:dict|None=None, + royalty_terms:dict|None=None,buyer_receives:list[str]|None=None, + buyer_does_not_receive:list[str]|None=None,evidence_refs:list[str]|None=None, + namespace:str|None=None)->dict: + issuer=str(issuer_entity_id); dco=str(underlying_dco_id) + obj=self.fabric.get_object(dco) + if obj["controller_entity_id"]!=issuer: + raise PermissionError("only the controlled DCO issuer may create an instrument") + if not (obj.get("descriptor") or {}).get("digital_commodity"): + raise ValueError("underlying object must be a Digital Commodity Object") + rp,gp=self._verify_passports(issuer,dco,rights_passport_id,global_passport_id) + ns=self.ensure_namespace(issuer,namespace) + symbol=_safe_symbol(display_symbol); rc=_token(rights_class); series=int(series) + instrument_id=canonical_instrument_id(issuer,dco,rc,series) + market_identifier=f"{ns['namespace']}:{symbol}" + supply=int(supply); div=int(divisibility) + if supply<1: raise ValueError("positive supply required") + if div<0 or div>9: raise ValueError("divisibility must be between 0 and 9") + fung=_token(fungibility) + if fung not in {"FUNGIBLE","NON-FUNGIBLE","SERIES-FUNGIBLE"}: raise ValueError("unsupported fungibility") + actions=sorted({_token(a) for a in (rights.get("actions") or [])}) + if not actions: raise ValueError("instrument rights actions required") + rights_doc={**dict(rights),"actions":actions, + "rights_passport_id":rp["passport_id"],"global_passport_id":gp["passport_id"], + "rights_class":rc,"market_identifier":market_identifier} + eep_body={"schema":"entity-eep-instrument-v1","instrument_id":instrument_id,"issuer":issuer, + "underlying_object_id":dco,"instrument_class":_token(instrument_class), + "rights":rights_doc,"total_units":supply,"transferable":bool(transferable), + "duration_ms":int(duration_ms) if duration_ms is not None else None, + "settlement_currency":_token(settlement_currency),"delivery_mode":_token(delivery_mode), + "status":"ACTIVE","created_at_ms":now_ms(),"bytes_are_not_the_traded_scarcity":True} + sig=self.identity.sign(issuer,eep_body) + self.exchange.submit_signed_instrument(eep_body,sig) + package={"schema":"entity-economic-instrument-package-v1","instrument_id":instrument_id, + "issuer_entity_id":issuer,"issuer_namespace":ns["namespace"],"underlying_dco_id":dco, + "instrument_name":str(instrument_name),"display_symbol":symbol, + "market_identifier":market_identifier,"instrument_class":eep_body["instrument_class"], + "rights_class":rc,"series":series,"fungibility":fung,"divisibility":div,"supply":supply, + "rights_passport_id":rp["passport_id"],"global_passport_id":gp["passport_id"], + "jurisdiction":str(jurisdiction).upper(),"transfer_rules":dict(transfer_rules or {}), + "economic_terms":dict(economic_terms or {}),"royalty_terms":dict(royalty_terms or {}), + "buyer_receives":[str(x) for x in (buyer_receives or [])], + "buyer_does_not_receive":[str(x) for x in (buyer_does_not_receive or [])], + "evidence_refs":sorted({str(x) for x in (evidence_refs or [])}), + "status":"ACTIVE","created_at_ms":eep_body["created_at_ms"], + "entity_owns_protocol_not_asset":True,"namespace_is_alias_not_authority":True, + "canonical_identity_is_instrument_id":True} + psig=self.identity.sign(issuer,package) + with self._db() as db: + db.execute("""INSERT INTO instrument_packages VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",( + instrument_id,issuer,ns["namespace"],dco,package["instrument_name"],symbol,market_identifier, + package["instrument_class"],rc,series,fung,div,supply,rp["passport_id"],gp["passport_id"], + package["jurisdiction"],json.dumps(package["transfer_rules"],sort_keys=True), + json.dumps(package["economic_terms"],sort_keys=True),json.dumps(package["royalty_terms"],sort_keys=True), + json.dumps(package["buyer_receives"],sort_keys=True),json.dumps(package["buyer_does_not_receive"],sort_keys=True), + json.dumps(package["evidence_refs"],sort_keys=True),"ACTIVE",package["created_at_ms"],json.dumps(psig,sort_keys=True))) + return dict(package,signature=psig,eep_instrument=dict(eep_body,signature=sig)) + + def instrument(self,instrument_id:str)->dict: + with self._db() as db: + r=db.execute("SELECT * FROM instrument_packages WHERE instrument_id=?",(str(instrument_id),)).fetchone() + if not r: raise KeyError("instrument package not found") + d=dict(r) + for src,dst in [("transfer_rules_json","transfer_rules"),("economic_terms_json","economic_terms"), + ("royalty_terms_json","royalty_terms"),("buyer_receives_json","buyer_receives"), + ("buyer_does_not_receive_json","buyer_does_not_receive"),("evidence_refs_json","evidence_refs")]: + d[dst]=json.loads(d.pop(src) or ("[]" if "buyer_" in dst or dst=="evidence_refs" else "{}")) + d["signature"]=json.loads(d.pop("signature_json")); return d + + def resolve_symbol(self,issuer_namespace:str,display_symbol:str)->dict: + ns=_token(issuer_namespace); sym=_safe_symbol(display_symbol) + with self._db() as db: + r=db.execute("""SELECT instrument_id FROM instrument_packages + WHERE issuer_namespace=? AND display_symbol=? AND status='ACTIVE'""",(ns,sym)).fetchone() + if not r: raise KeyError("active market identifier not found") + return self.instrument(r["instrument_id"]) + + def _information(self,instrument:dict,listing:dict,asset:dict,issuer_manifest:dict)->tuple[dict,str]: + info={ + "schema":"entity-listing-information-sheet-v1", + "what_is_this":f"{instrument['market_identifier']} represents {instrument['instrument_name']}.", + "instrument_name":instrument["instrument_name"],"display_symbol":instrument["display_symbol"], + "market_identifier":instrument["market_identifier"],"instrument_id":instrument["instrument_id"], + "issuer":str(issuer_manifest.get("display_name") or instrument["issuer_entity_id"]), + "issuer_entity_id":instrument["issuer_entity_id"],"underlying_dco_id":instrument["underlying_dco_id"], + "asset_name":asset["title"],"asset_type":asset["object_type"], + "instrument_class":instrument["instrument_class"],"rights_class":instrument["rights_class"], + "buyer_receives":instrument["buyer_receives"], + "buyer_does_not_receive":instrument["buyer_does_not_receive"], + "supply":instrument["supply"],"divisibility":instrument["divisibility"], + "fungibility":instrument["fungibility"],"jurisdiction":instrument["jurisdiction"], + "transfer_rules":instrument["transfer_rules"],"economic_terms":instrument["economic_terms"], + "royalty_terms":instrument["royalty_terms"],"rights_passport_id":instrument["rights_passport_id"], + "global_passport_id":instrument["global_passport_id"],"evidence_refs":instrument["evidence_refs"], + "listing_id":listing["listing_id"],"venue_id":listing["venue_id"],"market_id":listing["market_id"], + "quote_unit":listing["quote_unit"],"trade_mode":listing["trade_mode"], + "settlement_method":listing["settlement_method"],"minimum_quantity":listing["minimum_quantity"], + "quantity_precision":listing["quantity_precision"],"price_precision":listing["price_precision"], + "pricing_method":listing["pricing_method"],"status":listing["status"], + "instrument_created_at_ms":instrument["created_at_ms"],"listed_at_ms":listing["listed_at_ms"], + "sheet_does_not_replace_rights_passport":True, + "external_publication_does_not_create_new_instrument":True, + } + md=f"""# ENTITY Listing Information Sheet + +## WHAT IS THIS? + +**{info['market_identifier']} — {info['instrument_name']}** + +Underlying asset: **{info['asset_name']}** +Issuer: **{info['issuer']}** +Canonical Instrument ID: `{info['instrument_id']}` +Canonical Listing ID: `{info['listing_id']}` + +### Purchasing this instrument grants + +{chr(10).join('- '+x for x in info['buyer_receives']) or '- See Rights Passport.'} + +### Purchasing this instrument does NOT grant + +{chr(10).join('- '+x for x in info['buyer_does_not_receive']) or '- No rights beyond the Rights Passport.'} + +## Rights and market terms + +- Instrument class: {info['instrument_class']} +- Rights class: {info['rights_class']} +- Supply: {info['supply']} +- Divisibility: {info['divisibility']} +- Fungibility: {info['fungibility']} +- Jurisdiction: {info['jurisdiction']} +- Quote unit: {info['quote_unit']} +- Trade mode: {info['trade_mode']} +- Settlement: {info['settlement_method']} +- Minimum quantity: {info['minimum_quantity']} +- Pricing method: {info['pricing_method']} + +## Canonical references + +- Issuer ENTITY ID: `{info['issuer_entity_id']}` +- Underlying DCO: `{info['underlying_dco_id']}` +- Rights Passport: `{info['rights_passport_id']}` +- Global Passport: `{info['global_passport_id']}` +- Venue: `{info['venue_id']}` + +This sheet is a buyer-facing disclosure layer. It does **not** replace the Rights Passport, Global Passport, canonical instrument, or canonical listing record. +""" + info["verification_hash"]=sha(info) + return info,md + + def create_listing(self,issuer_entity_id:str,instrument_id:str,venue_id:str,*,market_id:str, + quote_unit:str,trade_mode:str="ORDER_BOOK",settlement_method:str="PAYMENT_VERSUS_RIGHT", + minimum_quantity:int=1,quantity_precision:int=0,price_precision:int=0, + pricing_method:str="ORDER_BOOK",listing_series:int=1,tick_size:int=1)->dict: + instrument=self.instrument(instrument_id); issuer=str(issuer_entity_id) + if instrument["issuer_entity_id"]!=issuer: raise PermissionError("instrument issuer required") + listing_id=canonical_listing_id(str(venue_id),instrument_id,int(listing_series)) + draft={"listing_id":listing_id,"venue_id":str(venue_id),"market_id":str(market_id), + "quote_unit":_token(quote_unit),"trade_mode":_token(trade_mode), + "settlement_method":_token(settlement_method),"minimum_quantity":int(minimum_quantity), + "quantity_precision":int(quantity_precision),"price_precision":int(price_precision), + "pricing_method":_token(pricing_method),"status":"ACTIVE","listed_at_ms":now_ms()} + if draft["minimum_quantity"]<1: raise ValueError("minimum quantity must be positive") + asset=self.fabric.get_object(instrument["underlying_dco_id"]) + manifest=self.identity.load_manifest(issuer) + info,md=self._information(instrument,draft,asset,manifest) + info_hash=sha(info) + machine={"protocol":"ENTITY-INSTRUMENT-1","instrument_symbol":instrument["display_symbol"], + "market_identifier":instrument["market_identifier"],"instrument_name":instrument["instrument_name"], + "instrument_id":instrument_id,"issuer_entity_id":issuer, + "underlying_dco_id":instrument["underlying_dco_id"],"listing_id":listing_id, + "rights_passport":instrument["rights_passport_id"],"global_passport":instrument["global_passport_id"], + "listing_information_sha256":info_hash} + machine["verification_hash"]=sha(machine); machine_hash=sha(machine) + disclosure=self.exchange.publish_disclosure(str(venue_id),instrument_id,issuer,"LISTING_INFORMATION",machine_hash) + listing_body={"schema":"entity-eep-listing-v1","listing_id":listing_id,"venue_id":str(venue_id), + "instrument_id":instrument_id,"lister":issuer,"min_lot":draft["minimum_quantity"], + "tick_size":int(tick_size),"disclosure_sha256":machine_hash, + "status":"ACTIVE","created_at_ms":draft["listed_at_ms"]} + lsig=self.identity.sign(issuer,listing_body) + self.exchange.submit_signed_listing(listing_body,lsig) + package={**draft,"schema":"entity-market-listing-package-v1","instrument_id":instrument_id, + "display_symbol":instrument["display_symbol"],"listing_information_sha256":info_hash, + "machine_manifest_sha256":machine_hash} + psig=self.identity.sign(issuer,package) + with self._db() as db: + db.execute("INSERT INTO listing_packages VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",( + listing_id,instrument_id,str(venue_id),draft["market_id"],instrument["display_symbol"], + draft["quote_unit"],draft["trade_mode"],draft["settlement_method"],draft["minimum_quantity"], + draft["quantity_precision"],draft["price_precision"],draft["pricing_method"],info_hash,machine_hash, + "ACTIVE",draft["listed_at_ms"],json.dumps(psig,sort_keys=True))) + db.execute("INSERT INTO listing_information VALUES(?,?,?,?)",( + listing_id,json.dumps({"information":info,"machine_manifest":machine},sort_keys=True), + md,draft["listed_at_ms"])) + return dict(package,signature=psig,information=info,machine_manifest=machine, + eep_listing=dict(listing_body,signature=lsig),disclosure=disclosure) + + def listing(self,listing_id:str)->dict: + with self._db() as db: + r=db.execute("SELECT * FROM listing_packages WHERE listing_id=?",(str(listing_id),)).fetchone() + i=db.execute("SELECT * FROM listing_information WHERE listing_id=?",(str(listing_id),)).fetchone() + if not r or not i: raise KeyError("listing package not found") + payload=json.loads(i["information_json"]) + return dict(r) | {"signature":json.loads(r["signature_json"]), + "information":payload["information"],"machine_manifest":payload["machine_manifest"], + "information_markdown":i["information_markdown"]} + + def export_portable_package(self,listing_id:str,destination:str|Path)->dict: + listing=self.listing(listing_id); instrument=self.instrument(listing["instrument_id"]) + rp=self.rights.get(instrument["rights_passport_id"]); gp=self.global_passports.get(instrument["global_passport_id"]) + asset=self.fabric.get_object(instrument["underlying_dco_id"]) + out=Path(destination); out.mkdir(parents=True,exist_ok=True) + files={ + "LISTING_INFORMATION.md":listing["information_markdown"].encode(), + "instrument.json":json.dumps(instrument,indent=2,sort_keys=True).encode(), + "entity-instrument.json":json.dumps(listing["machine_manifest"],indent=2,sort_keys=True).encode(), + "listing.json":json.dumps({k:v for k,v in listing.items() if k not in {"information_markdown"}},indent=2,sort_keys=True,default=str).encode(), + "rights-passport.json":json.dumps(rp,indent=2,sort_keys=True).encode(), + "global-passport.json":json.dumps(gp,indent=2,sort_keys=True).encode(), + "provenance.json":json.dumps({"underlying_dco":asset,"evidence_refs":instrument["evidence_refs"]},indent=2,sort_keys=True).encode(), + } + for name,data in files.items(): (out/name).write_bytes(data) + _simple_pdf(listing["information_markdown"],out/"LISTING_INFORMATION.pdf") + verification={"schema":"entity-portable-instrument-verification-v1","instrument_id":instrument["instrument_id"], + "listing_id":listing_id,"market_identifier":instrument["market_identifier"], + "canonical_identity_is_instrument_id":True, + "external_publication_does_not_create_new_instrument":True, + "manifest_sha256":listing["machine_manifest_sha256"]} + (out/"verification.json").write_text(json.dumps(verification,indent=2,sort_keys=True),encoding="utf-8") + names=sorted([p for p in out.iterdir() if p.is_file() and p.name!="SHA256SUMS"]) + sums="\n".join(f"{hashlib.sha256(p.read_bytes()).hexdigest()} {p.name}" for p in names)+"\n" + (out/"SHA256SUMS").write_text(sums,encoding="ascii") + badge=f"[ENTITY VERIFIED | {instrument['market_identifier']}]" + (out/"ENTITY_INSTRUMENT.md").write_text( + f"# {instrument['instrument_name']}\n\n{badge}\n\nInstrument: `{instrument['instrument_id']}`\n\n" + f"Underlying DCO: `{instrument['underlying_dco_id']}`\n\nIssuer: `{instrument['issuer_entity_id']}`\n\n" + f"Listing: `{listing_id}`\n\nRights Passport: `{instrument['rights_passport_id']}`\n\n" + f"Global Passport: `{instrument['global_passport_id']}`\n\nManifest SHA-256: `{listing['machine_manifest_sha256']}`\n", + encoding="utf-8") + return {"schema":"entity-portable-instrument-package-v1","destination":str(out), + "instrument_id":instrument["instrument_id"],"listing_id":listing_id, + "market_identifier":instrument["market_identifier"], + "files":sorted(p.name for p in out.iterdir() if p.is_file())} + + def active_market(self)->list[dict]: + with self._db() as db: + rows=db.execute("""SELECT l.*,i.instrument_name,i.market_identifier,i.issuer_entity_id, + i.underlying_dco_id,i.rights_class,i.instrument_class + FROM listing_packages l JOIN instrument_packages i ON i.instrument_id=l.instrument_id + WHERE l.status='ACTIVE' AND i.status='ACTIVE' + ORDER BY i.market_identifier,l.listed_at_ms""").fetchall() + return [dict(r) for r in rows] + + def status(self)->dict: + with self._db() as db: + counts={t:int(db.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0]) + for t in ("issuer_namespaces","instrument_packages","listing_packages","listing_information")} + return {"schema":MARKET_SCHEMA,"version":MARKET_VERSION,"multi_issuer":True, + "entity_owns_protocol_not_assets":True,"canonical_instrument_identity":True, + "symbols_are_non_authoritative_aliases":True,"listing_information_required":True, + "portable_publication_supported":True,"protocol_tax_bps":0, + "cryptocurrency_required":False,"counts":counts} From b9745c71387838b7ceb48d333248543256409ff2 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:55:33 -0700 Subject: [PATCH 036/111] tests: qualify universal multi-issuer market and listing package --- tests/test_entity_market_registry.py | 107 +++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 tests/test_entity_market_registry.py diff --git a/tests/test_entity_market_registry.py b/tests/test_entity_market_registry.py new file mode 100644 index 00000000..78a16a4d --- /dev/null +++ b/tests/test_entity_market_registry.py @@ -0,0 +1,107 @@ +from __future__ import annotations +from pathlib import Path +import importlib.util,sys,tempfile + +ROOT=Path(__file__).resolve().parents[1] + +def load(name,rel): + p=ROOT/rel; spec=importlib.util.spec_from_file_location(name,p) + m=importlib.util.module_from_spec(spec); sys.modules[name]=m; spec.loader.exec_module(m); return m + +class FakePassports: + def __init__(self,kind): self.kind=kind; self.records={} + def bind(self,pid,object_id,controller,rp=None): + if self.kind=="rights": + self.records[pid]={"passport_id":pid,"object_id":object_id,"controller_entity_id":controller} + else: + self.records[pid]={"passport_id":pid,"object_id":object_id,"controller_entity_id":controller, + "rights_passport_id":rp} + def get(self,pid): return dict(self.records[pid]) + def verify(self,p): return {"valid":True} + +def test_multi_issuer_symbols_canonical_ids_listing_sheet_and_portable_package(): + ident=load("market_test_identity","src/01_Core_Runtime/identity/canonical_identity.py") + fabmod=load("market_test_fabric","src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py") + exmod=load("market_test_exchange","src/31_Profiles/exchange_protocol.py") + marketmod=load("market_test_registry","src/45_ENTITY_Market/canonical_market_registry.py") + with tempfile.TemporaryDirectory() as td: + state=Path(td) + vault=ident.EntityIdentityVault(state) + acme=vault.create("Acme Corp","organization")["entity_id"] + jane=vault.create("Jane Smith","person")["entity_id"] + venue_owner=vault.create("Independent Market","organization")["entity_id"] + fabric=fabmod.UniversalTransactionFabric(state,vault) + ex=exmod.ExchangeProtocol(state,vault,fabric) + venue=ex.create_venue(venue_owner,"ENTITY Test Market","CA",["ORDER_BOOK"],"a"*64) + rights=FakePassports("rights"); gps=FakePassports("global") + reg=marketmod.EntityEconomicMarketRegistry(state,vault,fabric,ex,rights,gps) + + a=fabric.register_digital_commodity(acme,"Vision Model 07","1"*64, + commodity_class="AI_MODEL",measurement_unit="ASSET",object_type="MODEL") + j=fabric.register_digital_commodity(jane,"Photography Collection 2026","2"*64, + commodity_class="CREATIVE_COLLECTION",measurement_unit="ASSET",object_type="COLLECTION") + rights.bind("rp-acme",a["object_id"],acme); gps.bind("gp-acme",a["object_id"],acme,"rp-acme") + rights.bind("rp-jane",j["object_id"],jane); gps.bind("gp-jane",j["object_id"],jane,"rp-jane") + + ia=reg.create_instrument(acme,a["object_id"],instrument_name="Vision Model 07 Commercial Training Rights", + display_symbol="SHARED-TRN",instrument_class="SPOT_LICENSE",rights_class="TRAINING", + rights={"actions":["TRAIN","EVALUATE"]},supply=100,rights_passport_id="rp-acme", + global_passport_id="gp-acme",settlement_currency="CAD",jurisdiction="CA", + namespace="ACME",transferable=True,buyer_receives=["commercial training right"], + buyer_does_not_receive=["ownership of the model","copyright ownership"]) + ij=reg.create_instrument(jane,j["object_id"],instrument_name="Photography Collection Commercial Rights", + display_symbol="SHARED-TRN",instrument_class="SPOT_LICENSE",rights_class="COMMERCIAL", + rights={"actions":["COMMERCIALIZE"]},supply=25,rights_passport_id="rp-jane", + global_passport_id="gp-jane",settlement_currency="CAD",jurisdiction="CA", + namespace="JSMITH",transferable=True,buyer_receives=["defined commercial-use right"], + buyer_does_not_receive=["copyright ownership"]) + + assert ia["market_identifier"]=="ACME:SHARED-TRN" + assert ij["market_identifier"]=="JSMITH:SHARED-TRN" + assert ia["instrument_id"]!=ij["instrument_id"] + assert ia["instrument_id"].startswith("entity.instrument:v1:") + assert reg.resolve_symbol("ACME","SHARED-TRN")["instrument_id"]==ia["instrument_id"] + assert reg.resolve_symbol("JSMITH","SHARED-TRN")["instrument_id"]==ij["instrument_id"] + + listing=reg.create_listing(acme,ia["instrument_id"],venue["venue_id"], + market_id="ENTITY-PRIMARY",quote_unit="CAD",trade_mode="ORDER_BOOK", + settlement_method="PAYMENT_VERSUS_RIGHT",minimum_quantity=1,pricing_method="ORDER_BOOK") + assert listing["listing_id"].startswith("entity.listing:v1:") + assert listing["information"]["sheet_does_not_replace_rights_passport"] is True + assert "ownership of the model" in listing["information"]["buyer_does_not_receive"] + assert listing["machine_manifest"]["instrument_id"]==ia["instrument_id"] + assert ex.instrument_status(ia["instrument_id"])["status"]=="ACTIVE" + + pkg=reg.export_portable_package(listing["listing_id"],state/"portable") + required={"LISTING_INFORMATION.pdf","LISTING_INFORMATION.md","entity-instrument.json", + "instrument.json","listing.json","rights-passport.json","global-passport.json", + "provenance.json","verification.json","SHA256SUMS","ENTITY_INSTRUMENT.md"} + assert required.issubset(set(pkg["files"])) + assert (state/"portable"/"LISTING_INFORMATION.pdf").read_bytes().startswith(b"%PDF-1.4") + assert "ACME:SHARED-TRN" in (state/"portable"/"ENTITY_INSTRUMENT.md").read_text(encoding="utf-8") + status=reg.status() + assert status["multi_issuer"] is True + assert status["entity_owns_protocol_not_assets"] is True + assert status["listing_information_required"] is True + +def test_instrument_requires_controlled_dco(): + ident=load("market_test_identity2","src/01_Core_Runtime/identity/canonical_identity.py") + fabmod=load("market_test_fabric2","src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py") + exmod=load("market_test_exchange2","src/31_Profiles/exchange_protocol.py") + marketmod=load("market_test_registry2","src/45_ENTITY_Market/canonical_market_registry.py") + with tempfile.TemporaryDirectory() as td: + state=Path(td); vault=ident.EntityIdentityVault(state) + owner=vault.create("Asset Owner","person")["entity_id"]; attacker=vault.create("Other Issuer","person")["entity_id"] + fabric=fabmod.UniversalTransactionFabric(state,vault); ex=exmod.ExchangeProtocol(state,vault,fabric) + rights=FakePassports("rights"); gps=FakePassports("global") + reg=marketmod.EntityEconomicMarketRegistry(state,vault,fabric,ex,rights,gps) + obj=fabric.register_digital_commodity(owner,"Asset","3"*64,commodity_class="DATA",measurement_unit="ASSET") + rights.bind("rp",obj["object_id"],owner); gps.bind("gp",obj["object_id"],owner,"rp") + try: + reg.create_instrument(attacker,obj["object_id"],instrument_name="Impersonation", + display_symbol="FAKE",instrument_class="SPOT_LICENSE",rights_class="COMMERCIAL", + rights={"actions":["COMMERCIALIZE"]},supply=1,rights_passport_id="rp", + global_passport_id="gp",settlement_currency="CAD",jurisdiction="CA") + assert False,"non-controller issuance must fail" + except PermissionError: + pass From 798a893a7c0b3ebfd70c473f9144058ab07ffd42 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:57:53 -0700 Subject: [PATCH 037/111] fabric: support universal DCO asset categories --- .../canonical_universal_fabric.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py b/src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py index 96f3d0e7..ce113a70 100644 --- a/src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py +++ b/src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py @@ -6,7 +6,7 @@ import hashlib, json, secrets, sqlite3, time PRIMITIVES=("ENTITY","AUTHORITY","RIGHT","EVENT","VALUE") -OBJECT_TYPES={"PERSON","ORGANIZATION","AI_AGENT","DATASET","DOCUMENT","MODEL","SOFTWARE","API","DEVICE","VEHICLE","BUILDING","RESEARCH","DIGITAL_TWIN","FINANCIAL_INSTRUMENT","PHYSICAL_ASSET","OTHER"} +OBJECT_TYPES={"PERSON","ORGANIZATION","AI_AGENT","DATASET","DOCUMENT","MODEL","SOFTWARE","API","DEVICE","VEHICLE","BUILDING","RESEARCH","DIGITAL_TWIN","FINANCIAL_INSTRUMENT","PHYSICAL_ASSET","COLLECTION","CREATIVE_ASSET","ALGORITHM","INVENTION","DESIGN","MEDIA","KNOWLEDGE_ASSET","ROBOTICS_ASSET","OTHER"} RIGHT_ACTIONS={"INSPECT","READ","COPY","DERIVE","TRAIN","INFER","EXECUTE","MODIFY","REDISTRIBUTE","COMMERCIALIZE","CONTROL","TRANSFER"} AUTHORITY_ACTIONS={"ACT","SIGN","ACCESS_DATA","EXECUTE","PURCHASE","LICENSE","SETTLE","PUBLISH","RESOLVE","DELEGATE","CONTROL"} VALUE_STATES=("POTENTIAL","OFFER","CONTRACTED","ACCRUED","SETTLED","REALIZED") From 7e9a06209676e8fc11568417fad9788d9011c584 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:57:55 -0700 Subject: [PATCH 038/111] tests: use supported creative asset object type --- tests/test_entity_market_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_entity_market_registry.py b/tests/test_entity_market_registry.py index 78a16a4d..6175d1b7 100644 --- a/tests/test_entity_market_registry.py +++ b/tests/test_entity_market_registry.py @@ -39,7 +39,7 @@ def test_multi_issuer_symbols_canonical_ids_listing_sheet_and_portable_package() a=fabric.register_digital_commodity(acme,"Vision Model 07","1"*64, commodity_class="AI_MODEL",measurement_unit="ASSET",object_type="MODEL") j=fabric.register_digital_commodity(jane,"Photography Collection 2026","2"*64, - commodity_class="CREATIVE_COLLECTION",measurement_unit="ASSET",object_type="COLLECTION") + commodity_class="CREATIVE_COLLECTION",measurement_unit="ASSET",object_type="DOCUMENT") rights.bind("rp-acme",a["object_id"],acme); gps.bind("gp-acme",a["object_id"],acme,"rp-acme") rights.bind("rp-jane",j["object_id"],jane); gps.bind("gp-jane",j["object_id"],jane,"rp-jane") From d3ec00e9c37c0b4e1197394b32a76018c4103b80 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:57:57 -0700 Subject: [PATCH 039/111] wallet: exclude withdrawn instruments from active positions --- src/42_ENTITY_Wallet/canonical_wallet.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/42_ENTITY_Wallet/canonical_wallet.py b/src/42_ENTITY_Wallet/canonical_wallet.py index 042e8572..1fe63596 100644 --- a/src/42_ENTITY_Wallet/canonical_wallet.py +++ b/src/42_ENTITY_Wallet/canonical_wallet.py @@ -155,7 +155,8 @@ def _positions(self,entity_id:str)->list[dict]: i.rights_json,i.total_units,i.transferable,i.duration_ms, i.settlement_currency,i.delivery_mode,i.status FROM balances b JOIN instruments i ON i.instrument_id=b.instrument_id - WHERE b.holder=? AND b.units>0 ORDER BY b.instrument_id""",(entity_id,)).fetchall() + WHERE b.holder=? AND b.units>0 AND i.status='ACTIVE' + ORDER BY b.instrument_id""",(entity_id,)).fetchall() out=[] for row in rows: p=dict(row); p["units"]=int(p["units"]); p["total_units"]=int(p["total_units"]) From f247989d8f14ac61118c724f7e78563381b4bcf2 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:58:23 -0700 Subject: [PATCH 040/111] wallet: exclude withdrawn instruments from active positions From fd319f4dcdc5d5c7659b644a20c0ceba0170bd6f Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:58:32 -0700 Subject: [PATCH 041/111] wallet: package universal ENTITY market registry --- tools/build_entity_wallet_desktop.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/build_entity_wallet_desktop.py b/tools/build_entity_wallet_desktop.py index f1455ef3..50de6513 100644 --- a/tools/build_entity_wallet_desktop.py +++ b/tools/build_entity_wallet_desktop.py @@ -19,6 +19,7 @@ "src/39_Implementation_Packages", "src/40_BTDU", "src/42_ENTITY_Wallet", + "src/45_ENTITY_Market", "sdk/global_passport_sdk", ] From b0eb7ff122f1623e1b91f17c4096fbcec672e060 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:59:02 -0700 Subject: [PATCH 042/111] market: preserve canonical EEP enum tokens --- src/45_ENTITY_Market/canonical_market_registry.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/45_ENTITY_Market/canonical_market_registry.py b/src/45_ENTITY_Market/canonical_market_registry.py index 99d2264e..6a2a98ee 100644 --- a/src/45_ENTITY_Market/canonical_market_registry.py +++ b/src/45_ENTITY_Market/canonical_market_registry.py @@ -16,6 +16,11 @@ def _token(v:str)->str: if not s: raise ValueError("non-empty canonical token required") return s +def _enum(v:str)->str: + s=str(v or "").upper().strip() + if not s or re.search(r"[^A-Z0-9_-]",s): raise ValueError("invalid canonical enum") + return s + def _safe_symbol(v:str)->str: s=re.sub(r"[^A-Z0-9._-]+","-",str(v or "").upper()).strip("-._") if not s or len(s)>32: raise ValueError("display symbol must be 1-32 canonical characters") @@ -215,10 +220,10 @@ def create_instrument(self,issuer_entity_id:str,underlying_dco_id:str,*,instrume "rights_passport_id":rp["passport_id"],"global_passport_id":gp["passport_id"], "rights_class":rc,"market_identifier":market_identifier} eep_body={"schema":"entity-eep-instrument-v1","instrument_id":instrument_id,"issuer":issuer, - "underlying_object_id":dco,"instrument_class":_token(instrument_class), + "underlying_object_id":dco,"instrument_class":_enum(instrument_class), "rights":rights_doc,"total_units":supply,"transferable":bool(transferable), "duration_ms":int(duration_ms) if duration_ms is not None else None, - "settlement_currency":_token(settlement_currency),"delivery_mode":_token(delivery_mode), + "settlement_currency":_enum(settlement_currency),"delivery_mode":_enum(delivery_mode), "status":"ACTIVE","created_at_ms":now_ms(),"bytes_are_not_the_traded_scarcity":True} sig=self.identity.sign(issuer,eep_body) self.exchange.submit_signed_instrument(eep_body,sig) @@ -346,10 +351,10 @@ def create_listing(self,issuer_entity_id:str,instrument_id:str,venue_id:str,*,ma if instrument["issuer_entity_id"]!=issuer: raise PermissionError("instrument issuer required") listing_id=canonical_listing_id(str(venue_id),instrument_id,int(listing_series)) draft={"listing_id":listing_id,"venue_id":str(venue_id),"market_id":str(market_id), - "quote_unit":_token(quote_unit),"trade_mode":_token(trade_mode), - "settlement_method":_token(settlement_method),"minimum_quantity":int(minimum_quantity), + "quote_unit":_enum(quote_unit),"trade_mode":_enum(trade_mode), + "settlement_method":_enum(settlement_method),"minimum_quantity":int(minimum_quantity), "quantity_precision":int(quantity_precision),"price_precision":int(price_precision), - "pricing_method":_token(pricing_method),"status":"ACTIVE","listed_at_ms":now_ms()} + "pricing_method":_enum(pricing_method),"status":"ACTIVE","listed_at_ms":now_ms()} if draft["minimum_quantity"]<1: raise ValueError("minimum quantity must be positive") asset=self.fabric.get_object(instrument["underlying_dco_id"]) manifest=self.identity.load_manifest(issuer) From b72fe54bfc139bd36a6f31cc28ca2eaf93ff98d4 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:59:10 -0700 Subject: [PATCH 043/111] dco factory: preserve and withdraw superseded issuance campaigns --- src/43_DCO_Factory/canonical_dco_factory.py | 40 +++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/src/43_DCO_Factory/canonical_dco_factory.py b/src/43_DCO_Factory/canonical_dco_factory.py index d3d453b2..adbe330b 100644 --- a/src/43_DCO_Factory/canonical_dco_factory.py +++ b/src/43_DCO_Factory/canonical_dco_factory.py @@ -62,6 +62,9 @@ def _init(self): issuance_id TEXT PRIMARY KEY,dco_id TEXT NOT NULL,plan_sha256 TEXT NOT NULL, physical_instruments INTEGER NOT NULL,total_right_units INTEGER NOT NULL, status TEXT NOT NULL,created_at_ms INTEGER NOT NULL); + CREATE TABLE IF NOT EXISTS issuance_withdrawals( + issuance_id TEXT PRIMARY KEY,dco_id TEXT NOT NULL,reason TEXT NOT NULL, + evidence_sha256 TEXT,created_at_ms INTEGER NOT NULL); """) @staticmethod @@ -225,13 +228,46 @@ def record_issuance(self,dco_id:str,plan:dict,status="QUALIFIED")->dict: body["total_right_units"],body["status"],body["created_at_ms"])) return body + def withdraw_issuance(self,issuance_id:str,reason:str,*,evidence_sha256:str|None=None)->dict: + issuance_id=str(issuance_id); reason=str(reason).strip() + if not reason: raise ValueError("withdrawal reason required") + evidence=sha256_hex(evidence_sha256,"evidence_sha256") if evidence_sha256 is not None else None + with self._db(True) as db: + row=db.execute("SELECT * FROM issuance_runs WHERE issuance_id=?",(issuance_id,)).fetchone() + if not row: raise KeyError("issuance run not found") + prior=db.execute("SELECT * FROM issuance_withdrawals WHERE issuance_id=?",(issuance_id,)).fetchone() + if prior: + return {"schema":"entity-dco-issuance-withdrawal-v1",**dict(prior),"history_preserved":True} + body={"schema":"entity-dco-issuance-withdrawal-v1","issuance_id":issuance_id, + "dco_id":row["dco_id"],"reason":reason,"evidence_sha256":evidence, + "created_at_ms":now_ms(),"history_preserved":True} + db.execute("INSERT INTO issuance_withdrawals VALUES(?,?,?,?,?)",( + issuance_id,row["dco_id"],reason,evidence,body["created_at_ms"])) + return body + + def active_issuance_runs(self,dco_id:str|None=None)->list[dict]: + sql="""SELECT r.* FROM issuance_runs r + LEFT JOIN issuance_withdrawals w ON w.issuance_id=r.issuance_id + WHERE r.status='QUALIFIED' AND w.issuance_id IS NULL""" + args=() + if dco_id is not None: + sql+=" AND r.dco_id=?"; args=(str(dco_id),) + sql+=" ORDER BY r.created_at_ms,r.issuance_id" + with self._db() as db: return [dict(x) for x in db.execute(sql,args)] + def portfolio_summary(self)->dict: with self._db() as db: total=int(db.execute("SELECT COUNT(*) FROM dco_masters").fetchone()[0]) active=int(db.execute("SELECT COUNT(*) FROM dco_masters WHERE lifecycle LIKE 'ACTIVE%'").fetchone()[0]) families={r["family"]:int(r["n"]) for r in db.execute("SELECT family,COUNT(*) n FROM dco_masters GROUP BY family")} - issued=db.execute("SELECT COALESCE(SUM(physical_instruments),0) i,COALESCE(SUM(total_right_units),0) u FROM issuance_runs WHERE status='QUALIFIED'").fetchone() + issued=db.execute("""SELECT COALESCE(SUM(r.physical_instruments),0) i, + COALESCE(SUM(r.total_right_units),0) u + FROM issuance_runs r + LEFT JOIN issuance_withdrawals w ON w.issuance_id=r.issuance_id + WHERE r.status='QUALIFIED' AND w.issuance_id IS NULL""").fetchone() + withdrawn=int(db.execute("SELECT COUNT(*) FROM issuance_withdrawals").fetchone()[0]) return {"schema":"entity-dco-factory-portfolio-summary-v1","total_dcos":total,"active_dcos":active, "families":families,"qualified_physical_instruments":int(issued["i"]), - "qualified_right_units":int(issued["u"]),"factory_version":FACTORY_VERSION, + "qualified_right_units":int(issued["u"]),"withdrawn_issuance_runs":withdrawn, + "factory_version":FACTORY_VERSION, "target_design_capacity_dcos":10000,"protocol_change_required":False} From acaa4977c84ed842134d5f87dbb0b69a2a4b15eb Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 06:59:54 -0700 Subject: [PATCH 044/111] market: fix canonical instrument package persistence --- src/45_ENTITY_Market/canonical_market_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/45_ENTITY_Market/canonical_market_registry.py b/src/45_ENTITY_Market/canonical_market_registry.py index 6a2a98ee..8393d996 100644 --- a/src/45_ENTITY_Market/canonical_market_registry.py +++ b/src/45_ENTITY_Market/canonical_market_registry.py @@ -243,7 +243,7 @@ def create_instrument(self,issuer_entity_id:str,underlying_dco_id:str,*,instrume "canonical_identity_is_instrument_id":True} psig=self.identity.sign(issuer,package) with self._db() as db: - db.execute("""INSERT INTO instrument_packages VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",( + db.execute("""INSERT INTO instrument_packages VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",( instrument_id,issuer,ns["namespace"],dco,package["instrument_name"],symbol,market_identifier, package["instrument_class"],rc,series,fung,div,supply,rp["passport_id"],gp["passport_id"], package["jurisdiction"],json.dumps(package["transfer_rules"],sort_keys=True), From 912808dbc4595ebc74c702a7d931b752281dd490 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:01:07 -0700 Subject: [PATCH 045/111] market: define universal multi-issuer instrument and listing standard --- .../v3/ENTITY_ECONOMIC_MARKET_PROTOCOL.md | 391 ++++++++++++++++++ 1 file changed, 391 insertions(+) create mode 100644 protocol/v3/ENTITY_ECONOMIC_MARKET_PROTOCOL.md diff --git a/protocol/v3/ENTITY_ECONOMIC_MARKET_PROTOCOL.md b/protocol/v3/ENTITY_ECONOMIC_MARKET_PROTOCOL.md new file mode 100644 index 00000000..32434e50 --- /dev/null +++ b/protocol/v3/ENTITY_ECONOMIC_MARKET_PROTOCOL.md @@ -0,0 +1,391 @@ +# ENTITY Economic Market Protocol — Universal Instrument and Listing Registry + +Status: v3.4.3 application/registry profile +Date: 2026-10-03 + +## Purpose + +ENTITY is a general-purpose digital asset economy. It is not a BTG-only marketplace. + +Any valid ENTITY identity may control a DCO and, where its Rights Passport permits, issue an economic instrument against that DCO under the same protocol rules used by every other participant. + +ENTITY owns the **protocol and registry rules**, not the assets listed through them. + +## Universal market stack + +```text +USER / ORGANIZATION / OTHER VALID ENTITY CONTROLLER + ↓ +ENTITY identity + ↓ +Owned / controlled DCO + ↓ +Rights Passport + ↓ +Global Passport / lineage / evidence + ↓ +Economic Instrument + ↓ +Instrument Registry + ↓ +Listing + Listing Information Sheet + ↓ +ENTITY-compatible Market / Venue + ↓ +Wallet-to-wallet trade + ↓ +Clearing / settlement / entitlement + ↓ +Economic lineage +``` + +The five universal market primitives are: + +1. **Issuer** +2. **Instrument** +3. **Listing** +4. **Trade** +5. **Settlement** + +EEP remains the execution, clearing, settlement and entitlement engine. This profile adds the universal issuer namespace, canonical instrument package, canonical listing package and buyer-facing disclosure layer above EEP. + +## Issuer neutrality + +BTG has no special structural market privilege. + +Examples: + +```text +shawn.blackmore.entity +└── btg.entity + └── robotics.entity + └── controller-001.entity + +acme-corp.entity +└── ai.entity + └── vision-model-07.entity + +jane.smith.entity +└── photography.entity + └── collection-2026.entity +``` + +These are examples of participant/domain/asset structures. Protocol-origin ancestry remains distinct from participant asset provenance and control. + +## Issuer namespaces + +Every issuer may register one signed market namespace alias derived from or chosen for its ENTITY identity. + +Examples: + +```text +BTG +ACME +JSMITH +``` + +A namespace is a convenience alias only. It is never authority. + +The canonical issuer remains the cryptographic ENTITY ID. + +A namespace is unique in one registry. If a preferred alias is already held by another verified issuer, a collision-safe alias is allocated. + +## Instrument identity + +A human-readable market symbol is not globally authoritative. + +Example: + +```text +Instrument Name: +Vision Model 07 Commercial Training Rights + +Display Symbol: +VM07-TRN + +Market Identifier: +ACME:VM07-TRN + +Canonical Instrument ID: +entity.instrument:v1:::training:000001 +``` + +The three identity layers are: + +```text +Friendly name + ↓ +Issuer namespace + display symbol + ↓ +Canonical immutable instrument ID +``` + +Two issuers may independently use the same display symbol. Resolution is issuer-scoped. + +```text +ACME:SHARED-TRN +JSMITH:SHARED-TRN +``` + +These resolve to different canonical instrument IDs. + +## Required instrument package + +Every universal instrument package records at least: + +- canonical instrument ID; +- issuer ENTITY ID; +- issuer namespace alias; +- underlying DCO ID; +- instrument name; +- display symbol; +- market identifier; +- EEP instrument class; +- rights class; +- series; +- fungibility; +- divisibility; +- supply; +- Rights Passport ID; +- Global Passport ID; +- jurisdiction; +- transfer rules; +- economic terms; +- royalty/participation terms; +- plain-language rights the buyer receives; +- plain-language rights the buyer does not receive; +- evidence references; +- creation time; +- current status; +- issuer signature. + +The instrument package is not the underlying DCO and does not silently transfer ownership of the underlying asset. + +## Listing separation + +An instrument and a listing are different objects. + +One instrument may be listed on more than one approved venue without creating a new instrument. + +```text +DCO + │ + └── Canonical Instrument + │ + ├── ENTITY primary market + ├── issuer private market + └── partner marketplace +``` + +Each listing records at least: + +- canonical listing ID; +- canonical instrument ID; +- venue ID; +- market ID; +- display symbol; +- quote unit; +- trade mode; +- settlement method; +- minimum quantity; +- quantity precision; +- price precision; +- pricing method; +- Listing Information Sheet hash; +- machine manifest hash; +- listing status; +- listing time; +- issuer/lister signature. + +## Listing Information Sheet — REQUIRED + +A buyer must not be expected to understand an instrument from a ticker alone. + +Each listing therefore requires a buyer-facing **Listing Information Sheet** bound by hash to the canonical instrument and listing. + +It must answer: + +- What is this? +- Who is the issuer? +- What DCO is underneath it? +- What does the buyer receive? +- What does the buyer **not** receive? +- What actions are granted? +- What restrictions apply? +- Is commercial use permitted? +- Is AI training permitted? +- Are derivatives permitted? +- Is redistribution permitted? +- What is the supply? +- What is the divisibility? +- What pricing mechanism is being used? +- What royalties or participation obligations apply? +- What is the duration? +- What territory/jurisdiction applies? +- What transfer restrictions apply? +- Which Rights Passport applies? +- Which Global Passport applies? +- Which provenance/evidence references apply? +- What are the canonical instrument and listing IDs? +- What is the current status/version? + +A prominent plain-language section must distinguish the underlying asset from the rights represented by the instrument. + +Example: + +```text +WHAT IS THIS? + +VM07-TRN represents a defined commercial training right +associated with Vision Model 07. + +Purchasing this instrument grants: +• the rights explicitly listed below + +Purchasing this instrument does not grant: +• ownership of the underlying model +• copyright ownership +• unrestricted source redistribution +``` + +The Listing Information Sheet is disclosure. It does **not** substitute for the Rights Passport, Global Passport, canonical instrument record or governing terms. + +## Portable publication + +An instrument may be published anywhere while retaining exactly one canonical identity. + +Approved publication surfaces may include: + +- GitHub; +- project websites; +- API documentation; +- package metadata; +- research publications; +- product pages; +- contracts/invoices; +- QR codes; +- partner marketplaces. + +External publication is a reference surface, not a minting surface. + +A portable instrument package may contain: + +```text +.entity-instrument/ +├── LISTING_INFORMATION.pdf +├── LISTING_INFORMATION.md +├── ENTITY_INSTRUMENT.md +├── entity-instrument.json +├── instrument.json +├── listing.json +├── rights-passport.json +├── global-passport.json +├── provenance.json +├── verification.json +└── SHA256SUMS +``` + +Rule: + +> An instrument can be published anywhere, but it has one canonical instrument identity. + +## Wallet issuance flow + +The native wallet may expose: + +```text +Create / ingest asset + ↓ +Create DCO + ↓ +Establish provenance + lineage + ↓ +Rights Passport + ↓ +Global Passport + ↓ +Create Economic Instrument + ↓ +Choose rights being offered + ↓ +Set supply / divisibility / transfer rules + ↓ +Create Listing Information Sheet + ↓ +Choose venue + price mechanism + ↓ +Create Listing + ↓ +LIST +``` + +Instrument creation and listing are always explicit controller actions. DCO ingest alone does not issue rights or create market value. + +## Buyer inspection + +Before purchase the wallet should expose: + +- issuer; +- issuer ENTITY ID; +- market identifier; +- canonical instrument ID; +- underlying DCO; +- lineage; +- provenance; +- Rights Passport; +- Global Passport; +- rights granted; +- rights excluded; +- restrictions; +- supply/available quantity; +- price/bid/ask or pricing method; +- royalty/participation terms; +- jurisdiction; +- evidence; +- trade history. + +## Trade flow + +```text +BUY + ↓ +validate canonical instrument + ↓ +validate active listing + ↓ +validate buyer eligibility where required + ↓ +execute trade + ↓ +create clearing obligation + ↓ +settle payment-versus-right + ↓ +update entitlement balances + ↓ +apply bound royalty/participation rules + ↓ +generate signed ENTITY receipt + ↓ +append economic lineage +``` + +External payment truth remains evidence-bound. ENTITY does not claim money moved merely because a trade record exists. + +## Invariants + +1. ENTITY registry rules are issuer-neutral. +2. BTG is an issuer like any other issuer. +3. Symbol aliases are never authority. +4. Canonical instrument IDs are immutable. +5. One canonical instrument may have multiple listings. +6. Listings do not duplicate instruments. +7. Listing Information Sheets are required buyer-facing disclosures. +8. Rights Passports remain authoritative for rights semantics. +9. Protocol origin does not transfer asset ownership. +10. DCO creation does not create market value. +11. Instruments are optional after DCO creation. +12. Protocol tax remains 0. +13. Cryptocurrency is not required. +14. Venue operators do not become protocol or asset authority. +15. Historical signed economic records are preserved when withdrawn/superseded. From 8ae1cb599eff951430dcfdd273cc2f8df17fb1b2 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:01:11 -0700 Subject: [PATCH 046/111] market: expose issuer instruments and listing resolution --- .../canonical_market_registry.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/45_ENTITY_Market/canonical_market_registry.py b/src/45_ENTITY_Market/canonical_market_registry.py index 8393d996..2d77c218 100644 --- a/src/45_ENTITY_Market/canonical_market_registry.py +++ b/src/45_ENTITY_Market/canonical_market_registry.py @@ -437,6 +437,22 @@ def export_portable_package(self,listing_id:str,destination:str|Path)->dict: "market_identifier":instrument["market_identifier"], "files":sorted(p.name for p in out.iterdir() if p.is_file())} + def issuer_instruments(self,issuer_entity_id:str,*,include_withdrawn:bool=False)->list[dict]: + sql="SELECT instrument_id FROM instrument_packages WHERE issuer_entity_id=?" + args=[str(issuer_entity_id)] + if not include_withdrawn: sql+=" AND status='ACTIVE'" + sql+=" ORDER BY created_at_ms,instrument_id" + with self._db() as db: ids=[r["instrument_id"] for r in db.execute(sql,args)] + return [self.instrument(i) for i in ids] + + def instrument_listings(self,instrument_id:str,*,active_only:bool=True)->list[dict]: + sql="SELECT listing_id FROM listing_packages WHERE instrument_id=?" + args=[str(instrument_id)] + if active_only: sql+=" AND status='ACTIVE'" + sql+=" ORDER BY listed_at_ms,listing_id" + with self._db() as db: ids=[r["listing_id"] for r in db.execute(sql,args)] + return [self.listing(i) for i in ids] + def active_market(self)->list[dict]: with self._db() as db: rows=db.execute("""SELECT l.*,i.instrument_name,i.market_identifier,i.issuer_entity_id, From 5d1129b0d7e22b05bebac8225196d091e18e24c0 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:02:10 -0700 Subject: [PATCH 047/111] market: require instrument actions to fit Rights Passport --- src/45_ENTITY_Market/canonical_market_registry.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/45_ENTITY_Market/canonical_market_registry.py b/src/45_ENTITY_Market/canonical_market_registry.py index 2d77c218..f732fb83 100644 --- a/src/45_ENTITY_Market/canonical_market_registry.py +++ b/src/45_ENTITY_Market/canonical_market_registry.py @@ -216,6 +216,17 @@ def create_instrument(self,issuer_entity_id:str,underlying_dco_id:str,*,instrume if fung not in {"FUNGIBLE","NON-FUNGIBLE","SERIES-FUNGIBLE"}: raise ValueError("unsupported fungibility") actions=sorted({_token(a) for a in (rights.get("actions") or [])}) if not actions: raise ValueError("instrument rights actions required") + allowed=set(); prohibited=set() + for rule in rp.get("rights") or []: + effect=str(rule.get("effect") or "").upper() + rule_actions={_token(a) for a in (rule.get("actions") or [])} + if effect=="ALLOW": allowed.update(rule_actions) + elif effect=="PROHIBIT": prohibited.update(rule_actions) + requested=set(actions) + if requested & prohibited: + raise PermissionError("instrument requests action prohibited by Rights Passport") + if not requested.issubset(allowed): + raise PermissionError("instrument actions exceed Rights Passport") rights_doc={**dict(rights),"actions":actions, "rights_passport_id":rp["passport_id"],"global_passport_id":gp["passport_id"], "rights_class":rc,"market_identifier":market_identifier} From 7cce33671d7da6a4d506741fef6460d6a919016f Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:04:41 -0700 Subject: [PATCH 048/111] wallet: add universal instrument issuance listing disclosure and export --- tools/entity_wallet_desktop.py | 401 ++++++++++++++++++++++----------- 1 file changed, 265 insertions(+), 136 deletions(-) diff --git a/tools/entity_wallet_desktop.py b/tools/entity_wallet_desktop.py index 8695b755..bdab6536 100644 --- a/tools/entity_wallet_desktop.py +++ b/tools/entity_wallet_desktop.py @@ -7,29 +7,26 @@ APP_NAME="ENTITY Wallet" def repo_root()->Path: - if getattr(sys,"frozen",False) and hasattr(sys,"_MEIPASS"): - return Path(sys._MEIPASS) + if getattr(sys,"frozen",False) and hasattr(sys,"_MEIPASS"): return Path(sys._MEIPASS) return Path(__file__).resolve().parents[1] def load_module(name:str,relative:str): path=repo_root()/relative spec=importlib.util.spec_from_file_location(name,path) if spec is None or spec.loader is None: raise RuntimeError(f"Cannot load {path}") - mod=importlib.util.module_from_spec(spec); sys.modules[name]=mod; spec.loader.exec_module(mod) - return mod + mod=importlib.util.module_from_spec(spec); sys.modules[name]=mod; spec.loader.exec_module(mod); return mod CLI=load_module("entity_wallet_v343_cli","tools/entity_v3_4_cli.py") WALLET=load_module("entity_wallet_model","src/42_ENTITY_Wallet/canonical_wallet.py") INGEST=load_module("entity_wallet_ingest","src/42_ENTITY_Wallet/asset_ingest.py") EXCHANGE=load_module("entity_wallet_exchange","src/31_Profiles/exchange_protocol.py") +MARKET=load_module("entity_wallet_market","src/45_ENTITY_Market/canonical_market_registry.py") def default_state()->Path: env=os.environ.get("ENTITY_STATE_DIR") if env: return Path(env).expanduser() - if sys.platform=="win32": - return Path(os.environ.get("LOCALAPPDATA",Path.home()))/"Blackmore Technology Group"/"ENTITY" - if sys.platform=="darwin": - return Path.home()/"Library"/"Application Support"/"Blackmore Technology Group"/"ENTITY" + if sys.platform=="win32": return Path(os.environ.get("LOCALAPPDATA",Path.home()))/"Blackmore Technology Group"/"ENTITY" + if sys.platform=="darwin": return Path.home()/"Library"/"Application Support"/"Blackmore Technology Group"/"ENTITY" return Path.home()/".local"/"share"/"blackmore-technology-group"/"entity" def manifests(state:Path)->list[dict]: @@ -48,32 +45,60 @@ def __init__(self,state:Path,entity_id:str): identity,fabric,profiles,origin,passports,packages,sdk,origin_status=CLI.runtime(state) self.identity=identity; self.fabric=fabric; self.profiles=profiles; self.origin=origin self.passports=passports; self.packages=packages; self.sdk=sdk; self.origin_status=origin_status + self.rights=self.sdk.ingestion.rights self.identity_manifest=self.identity.load_manifest(entity_id) self.wallet=WALLET.EntityEconomicWallet(state) self.exchange=EXCHANGE.ExchangeProtocol(state,self.identity,self.fabric) + self.market_registry=MARKET.EntityEconomicMarketRegistry( + state,self.identity,self.fabric,self.exchange,self.rights,self.passports) self.ingestor=INGEST.WalletAssetIngestor(state,CLI) - self.wallet_record=self.wallet.ensure_wallet( - entity_id,self.identity_manifest.get("display_name","ENTITY")+" Wallet") + self.wallet_record=self.wallet.ensure_wallet(entity_id,self.identity_manifest.get("display_name","ENTITY")+" Wallet") def snapshot(self): return self.wallet.snapshot(self.wallet_record["wallet_id"]) def domains(self): return ["general"]+self.packages.list_packages() def asset_kinds(self,domain): return self.ingestor.asset_kinds(domain) + def latest_global_passport(self,object_id:str)->dict: + dbp=self.state/"entity_v3_4_global_passports.sqlite" + db=sqlite3.connect(dbp); db.row_factory=sqlite3.Row + try: + row=db.execute("""SELECT passport_id FROM global_passports WHERE object_id=? + ORDER BY created_at_ms DESC,passport_id DESC LIMIT 1""",(str(object_id),)).fetchone() + finally: db.close() + if not row: raise KeyError("asset has no Global Passport") + return self.passports.get(row["passport_id"]) + + def instruments(self)->list[dict]: + if not self.market_registry.path.exists(): return [] + db=sqlite3.connect(self.market_registry.path); db.row_factory=sqlite3.Row + try: + rows=db.execute("""SELECT * FROM instrument_packages + WHERE issuer_entity_id=? AND status='ACTIVE' + ORDER BY created_at_ms,instrument_id""",(self.entity_id,)).fetchall() + finally: db.close() + return [self.market_registry.instrument(r["instrument_id"]) for r in rows] + + def venues(self)->list[dict]: + if not self.exchange.path.exists(): return [] + db=sqlite3.connect(self.exchange.path); db.row_factory=sqlite3.Row + try: rows=db.execute("SELECT venue_id,name,jurisdiction,status FROM venues WHERE status='ACTIVE' ORDER BY name").fetchall() + finally: db.close() + return [dict(r) for r in rows] + def market(self)->list[dict]: - if not self.wallet.exchange_path.exists(): return [] - with sqlite3.connect(self.wallet.exchange_path) as db: - db.row_factory=sqlite3.Row - rows=db.execute("""SELECT i.instrument_id,i.instrument_class,i.settlement_currency, - i.underlying_object_id, - (SELECT ll.venue_id FROM listings ll - WHERE ll.instrument_id=i.instrument_id AND ll.status='ACTIVE' - ORDER BY ll.created_at_ms DESC LIMIT 1) venue_id - FROM instruments i WHERE i.status='ACTIVE' - ORDER BY i.instrument_id""").fetchall() + rows=self.market_registry.active_market() + if not rows: return [] + db=sqlite3.connect(self.exchange.path); db.row_factory=sqlite3.Row + try: out=[] - for r in rows: - d=dict(r); d["market"]=self.wallet._market(db,d["instrument_id"]); out.append(d) + for row in rows: + e=db.execute("SELECT settlement_currency FROM instruments WHERE instrument_id=? AND status='ACTIVE'", + (row["instrument_id"],)).fetchone() + if not e: continue + d=dict(row); d["settlement_currency"]=e["settlement_currency"] + d["market"]=self.wallet._market(db,d["instrument_id"]); out.append(d) return out + finally: db.close() def ingest(self,path,options): return self.ingestor.ingest_file( @@ -83,100 +108,171 @@ def ingest(self,path,options): measurement_unit=options["unit"],version=options.get("version") or "1.0", previous_object_id=options.get("parent_object_id") or None) + def create_instrument(self,object_id:str,options:dict)->dict: + gp=self.latest_global_passport(object_id) + return self.market_registry.create_instrument( + self.entity_id,object_id,instrument_name=options["name"],display_symbol=options["symbol"], + instrument_class=options["instrument_class"],rights_class=options["rights_class"], + rights={"actions":options["actions"]},supply=options["supply"], + rights_passport_id=gp["rights_passport_id"],global_passport_id=gp["passport_id"], + settlement_currency=options["currency"],jurisdiction=options["jurisdiction"], + series=options["series"],fungibility=options["fungibility"],divisibility=options["divisibility"], + transferable=options["transferable"],duration_ms=options.get("duration_ms"), + transfer_rules={"wallet_defined":True},economic_terms={"market_value_created_by_issuance":False}, + royalty_terms={"automatic_protocol_royalty_bps":0}, + buyer_receives=options["buyer_receives"],buyer_does_not_receive=options["buyer_does_not_receive"], + evidence_refs=gp.get("evidence_refs") or [],namespace=options.get("namespace") or None) + + def create_listing(self,instrument_id:str,options:dict)->dict: + return self.market_registry.create_listing( + self.entity_id,instrument_id,options["venue_id"],market_id=options["market_id"], + quote_unit=options["quote_unit"],trade_mode=options["trade_mode"], + settlement_method=options["settlement_method"],minimum_quantity=options["minimum_quantity"], + quantity_precision=options["quantity_precision"],price_precision=options["price_precision"], + pricing_method=options["pricing_method"],listing_series=options["listing_series"], + tick_size=options["tick_size"]) + def submit_order(self,row:dict,side:str,quantity:int,limit_price:int): venue=row.get("venue_id") if not venue: raise ValueError("This instrument has no active market listing.") - order=self.exchange.submit_order( - venue,row["instrument_id"],self.entity_id,side,quantity,limit_price, - nonce="wallet-"+uuid.uuid4().hex,tif="GTC") - trades=self.exchange.match_order_book(venue,row["instrument_id"]) - return {"order":order,"trades":trades} + order=self.exchange.submit_order(venue,row["instrument_id"],self.entity_id,side,quantity,limit_price, + nonce="wallet-"+uuid.uuid4().hex,tif="GTC") + return {"order":order,"trades":self.exchange.match_order_book(venue,row["instrument_id"])} class AssetDialog(tk.Toplevel): def __init__(self,parent,path:Path,backend:Backend): super().__init__(parent); self.title("Canonical ENTITY Asset Ingest"); self.resizable(False,False) self.result=None; self.backend=backend; self.transient(parent); self.grab_set() - self.vars={ - "title":tk.StringVar(value=path.name), - "domain":tk.StringVar(value="general"), - "asset_kind":tk.StringVar(value=""), - "jurisdiction":tk.StringVar(value=""), - "class":tk.StringVar(value="DIGITAL_ASSET"), - "unit":tk.StringVar(value="ASSET"), - "authority":tk.StringVar(value=f"CONTROLLER_ENTITY:{backend.entity_id}"), - "version":tk.StringVar(value="1.0"), - "parent_object_id":tk.StringVar(value=""), - } + self.vars={k:tk.StringVar(value=v) for k,v in { + "title":path.name,"domain":"general","asset_kind":"","jurisdiction":"","class":"DIGITAL_ASSET", + "unit":"ASSET","authority":f"CONTROLLER_ENTITY:{backend.entity_id}","version":"1.0","parent_object_id":""}.items()} ttk.Label(self,text="File").grid(row=0,column=0,sticky="w",padx=12,pady=(12,4)) ttk.Label(self,text=str(path),wraplength=560).grid(row=0,column=1,sticky="w",padx=12,pady=(12,4)) ttk.Label(self,text="Domain profile").grid(row=1,column=0,sticky="w",padx=12,pady=5) self.domain=ttk.Combobox(self,textvariable=self.vars["domain"],values=backend.domains(),state="readonly",width=48) self.domain.grid(row=1,column=1,sticky="ew",padx=12,pady=5); self.domain.bind("<>",self._domain_changed) ttk.Label(self,text="Asset kind").grid(row=2,column=0,sticky="w",padx=12,pady=5) - self.kind=ttk.Combobox(self,textvariable=self.vars["asset_kind"],values=[],width=48) - self.kind.grid(row=2,column=1,sticky="ew",padx=12,pady=5) - labels=[ - ("Title","title"),("Jurisdiction (domain assets)","jurisdiction"), - ("Commodity class","class"),("Measurement unit","unit"), - ("Authority basis","authority"),("Passport version","version"), - ("Parent object ID (optional)","parent_object_id"), - ] + self.kind=ttk.Combobox(self,textvariable=self.vars["asset_kind"],values=[],width=48); self.kind.grid(row=2,column=1,sticky="ew",padx=12,pady=5) + labels=[("Title","title"),("Jurisdiction (domain assets)","jurisdiction"),("Commodity class","class"), + ("Measurement unit","unit"),("Authority basis","authority"),("Passport version","version"), + ("Parent object ID (optional)","parent_object_id")] for i,(label,key) in enumerate(labels,3): ttk.Label(self,text=label).grid(row=i,column=0,sticky="w",padx=12,pady=5) ttk.Entry(self,textvariable=self.vars[key],width=50).grid(row=i,column=1,sticky="ew",padx=12,pady=5) self.attest=tk.BooleanVar(value=False) - ttk.Checkbutton(self,text="I have authority to register this asset and attest its provenance.", - variable=self.attest).grid(row=10,column=0,columnspan=2,sticky="w",padx=12,pady=(10,4)) - ttk.Label(self,text="Ingest creates the governed DCO + evidence + rights passport + Global Passport + BTDU binding. It does not list or price the asset.", - wraplength=650).grid(row=11,column=0,columnspan=2,sticky="w",padx=12,pady=4) + ttk.Checkbutton(self,text="I have authority to register this asset and attest its provenance.",variable=self.attest).grid(row=10,column=0,columnspan=2,sticky="w",padx=12,pady=(10,4)) + ttk.Label(self,text="Creates DCO + evidence + Rights Passport + Global Passport + BTDU binding. It does not create an instrument, listing, or price.",wraplength=650).grid(row=11,column=0,columnspan=2,sticky="w",padx=12,pady=4) b=ttk.Frame(self); b.grid(row=12,column=0,columnspan=2,sticky="e",padx=12,pady=12) - ttk.Button(b,text="Cancel",command=self.destroy).pack(side="right",padx=4) - ttk.Button(b,text="Ingest Asset",command=self.ok).pack(side="right",padx=4) - + ttk.Button(b,text="Cancel",command=self.destroy).pack(side="right",padx=4); ttk.Button(b,text="Ingest Asset",command=self.ok).pack(side="right",padx=4) def _domain_changed(self,event=None): - domain=self.vars["domain"].get() - kinds=self.backend.asset_kinds(domain) - self.kind["values"]=kinds + domain=self.vars["domain"].get(); kinds=self.backend.asset_kinds(domain); self.kind["values"]=kinds self.vars["asset_kind"].set(kinds[0] if kinds else "") self.vars["class"].set("DIGITAL_ASSET" if domain=="general" else domain.upper().replace("-","_")+"_ASSET") - def ok(self): - if not self.attest.get(): - messagebox.showerror("Authority required","Confirm authority before registering the digital asset.",parent=self); return - if not self.vars["title"].get().strip(): - messagebox.showerror("Title required","Enter an asset title.",parent=self); return + if not self.attest.get(): messagebox.showerror("Authority required","Confirm authority before registering the digital asset.",parent=self); return + if not self.vars["title"].get().strip(): messagebox.showerror("Title required","Enter an asset title.",parent=self); return domain=self.vars["domain"].get() - if domain!="general" and not self.vars["jurisdiction"].get().strip(): - messagebox.showerror("Jurisdiction required","Domain package ingest requires an explicit jurisdiction.",parent=self); return - if domain!="general" and not self.vars["asset_kind"].get().strip(): - messagebox.showerror("Asset kind required","Choose an asset kind for this domain.",parent=self); return + if domain!="general" and not self.vars["jurisdiction"].get().strip(): messagebox.showerror("Jurisdiction required","Domain package ingest requires an explicit jurisdiction.",parent=self); return + if domain!="general" and not self.vars["asset_kind"].get().strip(): messagebox.showerror("Asset kind required","Choose an asset kind for this domain.",parent=self); return self.result={k:v.get().strip() for k,v in self.vars.items()}; self.destroy() +class InstrumentDialog(tk.Toplevel): + def __init__(self,parent,asset:dict): + super().__init__(parent); self.title("Create Economic Instrument"); self.resizable(False,False) + self.result=None; self.transient(parent); self.grab_set() + short=(asset.get("title") or "ASSET").upper().replace(" ","")[:12] + self.vars={k:tk.StringVar(value=v) for k,v in { + "name":f"{asset.get('title')} Commercial Rights","symbol":(short+"-COM")[:24],"namespace":"", + "instrument_class":"SPOT_LICENSE","rights_class":"COMMERCIAL","actions":"COMMERCIALIZE", + "supply":"1","currency":"CAD","jurisdiction":"CA","series":"1","fungibility":"FUNGIBLE", + "divisibility":"0","duration_ms":"","buyer_receives":"defined commercial-use right", + "buyer_does_not_receive":"ownership of the underlying DCO,copyright ownership,rights not stated in the Rights Passport"}.items()} + fields=[("Instrument name","name"),("Display symbol","symbol"),("Issuer namespace (optional)","namespace"), + ("EEP instrument class","instrument_class"),("Rights class","rights_class"),("Rights actions (comma separated)","actions"), + ("Supply","supply"),("Settlement currency","currency"),("Jurisdiction","jurisdiction"),("Series","series"), + ("Fungibility","fungibility"),("Divisibility precision","divisibility"),("Duration ms (optional)","duration_ms"), + ("Buyer receives (comma separated)","buyer_receives"),("Buyer does NOT receive","buyer_does_not_receive")] + for i,(label,key) in enumerate(fields): + ttk.Label(self,text=label).grid(row=i,column=0,sticky="w",padx=12,pady=4) + if key=="instrument_class": + w=ttk.Combobox(self,textvariable=self.vars[key],state="readonly", + values=["SPOT_LICENSE","SUBSCRIPTION","COMPUTE_TO_DATA","PROCUREMENT","CONTRIBUTION","SECONDARY_LICENSE"],width=50) + else: w=ttk.Entry(self,textvariable=self.vars[key],width=54) + w.grid(row=i,column=1,sticky="ew",padx=12,pady=4) + self.transferable=tk.BooleanVar(value=False) + ttk.Checkbutton(self,text="Transferable / secondary sale allowed",variable=self.transferable).grid(row=len(fields),column=0,columnspan=2,sticky="w",padx=12,pady=8) + ttk.Label(self,text="Instrument creation issues rights units but does not list them or establish market value.",wraplength=650).grid(row=len(fields)+1,column=0,columnspan=2,sticky="w",padx=12,pady=4) + b=ttk.Frame(self); b.grid(row=len(fields)+2,column=0,columnspan=2,sticky="e",padx=12,pady=12) + ttk.Button(b,text="Cancel",command=self.destroy).pack(side="right",padx=4); ttk.Button(b,text="Create Instrument",command=self.ok).pack(side="right",padx=4) + def ok(self): + try: + supply=int(self.vars["supply"].get()); series=int(self.vars["series"].get()); div=int(self.vars["divisibility"].get()) + duration=int(self.vars["duration_ms"].get()) if self.vars["duration_ms"].get().strip() else None + if supply<1 or series<1 or div<0: raise ValueError + except Exception: + messagebox.showerror("Invalid instrument","Supply/series must be positive integers and divisibility non-negative.",parent=self); return + self.result={k:v.get().strip() for k,v in self.vars.items()} + self.result.update({"supply":supply,"series":series,"divisibility":div,"duration_ms":duration, + "transferable":self.transferable.get(), + "actions":[x.strip().upper() for x in self.vars["actions"].get().split(",") if x.strip()], + "buyer_receives":[x.strip() for x in self.vars["buyer_receives"].get().split(",") if x.strip()], + "buyer_does_not_receive":[x.strip() for x in self.vars["buyer_does_not_receive"].get().split(",") if x.strip()]}) + self.destroy() + +class ListingDialog(tk.Toplevel): + def __init__(self,parent,backend:Backend,instrument:dict): + super().__init__(parent); self.title("Create Listing"); self.resizable(False,False) + self.result=None; self.backend=backend; self.transient(parent); self.grab_set() + venues=backend.venues(); self.venue_map={f"{v['name']} — {v['venue_id']}":v["venue_id"] for v in venues} + self.vars={k:tk.StringVar(value=v) for k,v in { + "venue":next(iter(self.venue_map),""), + "market_id":"ENTITY-MARKET","quote_unit":"CAD","trade_mode":"ORDER_BOOK", + "settlement_method":"PAYMENT_VERSUS_RIGHT","minimum_quantity":"1","quantity_precision":"0", + "price_precision":"0","pricing_method":"ORDER_BOOK","listing_series":"1","tick_size":"1"}.items()} + fields=[("Venue","venue"),("Market ID","market_id"),("Quote unit","quote_unit"),("Trade mode","trade_mode"), + ("Settlement method","settlement_method"),("Minimum quantity","minimum_quantity"), + ("Quantity precision","quantity_precision"),("Price precision","price_precision"), + ("Pricing method","pricing_method"),("Listing series","listing_series"),("Tick size","tick_size")] + for i,(label,key) in enumerate(fields): + ttk.Label(self,text=label).grid(row=i,column=0,sticky="w",padx=12,pady=5) + if key=="venue": w=ttk.Combobox(self,textvariable=self.vars[key],values=list(self.venue_map),state="readonly",width=54) + else: w=ttk.Entry(self,textvariable=self.vars[key],width=56) + w.grid(row=i,column=1,sticky="ew",padx=12,pady=5) + ttk.Label(self,text=f"Listing {instrument['market_identifier']} creates a required Listing Information Sheet and machine manifest.",wraplength=650).grid(row=len(fields),column=0,columnspan=2,sticky="w",padx=12,pady=6) + b=ttk.Frame(self); b.grid(row=len(fields)+1,column=0,columnspan=2,sticky="e",padx=12,pady=12) + ttk.Button(b,text="Cancel",command=self.destroy).pack(side="right",padx=4); ttk.Button(b,text="Create Listing",command=self.ok).pack(side="right",padx=4) + def ok(self): + if not self.venue_map: messagebox.showerror("No venue","No active ENTITY-compatible venue exists.",parent=self); return + try: + ints={k:int(self.vars[k].get()) for k in ("minimum_quantity","quantity_precision","price_precision","listing_series","tick_size")} + if ints["minimum_quantity"]<1 or ints["listing_series"]<1 or ints["tick_size"]<1: raise ValueError + except Exception: + messagebox.showerror("Invalid listing","Listing quantities/series/tick must be valid integers.",parent=self); return + self.result={k:v.get().strip() for k,v in self.vars.items()} + self.result["venue_id"]=self.venue_map[self.result.pop("venue")]; self.result.update(ints); self.destroy() + class OrderDialog(tk.Toplevel): def __init__(self,parent,row,side): - super().__init__(parent); self.title(f"{side.title()} {row['instrument_id']}"); self.resizable(False,False) + super().__init__(parent); self.title(f"{side.title()} {row.get('market_identifier') or row['instrument_id']}"); self.resizable(False,False) self.result=None; self.transient(parent); self.grab_set(); m=row["market"] self.qty=tk.StringVar(value="1"); default=m.get("ask") if side=="BUY" else m.get("bid") self.price=tk.StringVar(value="" if default is None else str(default)) - ttk.Label(self,text=f"{side} {row['instrument_id']}").grid(row=0,column=0,columnspan=2,sticky="w",padx=12,pady=12) - ttk.Label(self,text="Quantity").grid(row=1,column=0,sticky="w",padx=12,pady=5) - ttk.Entry(self,textvariable=self.qty,width=22).grid(row=1,column=1,padx=12,pady=5) - ttk.Label(self,text=f"Limit price ({row['settlement_currency']})").grid(row=2,column=0,sticky="w",padx=12,pady=5) - ttk.Entry(self,textvariable=self.price,width=22).grid(row=2,column=1,padx=12,pady=5) + ttk.Label(self,text=f"{side} {row.get('market_identifier') or row['instrument_id']}").grid(row=0,column=0,columnspan=2,sticky="w",padx=12,pady=12) + ttk.Label(self,text="Quantity").grid(row=1,column=0,sticky="w",padx=12,pady=5); ttk.Entry(self,textvariable=self.qty,width=22).grid(row=1,column=1,padx=12,pady=5) + ttk.Label(self,text=f"Limit price ({row['settlement_currency']})").grid(row=2,column=0,sticky="w",padx=12,pady=5); ttk.Entry(self,textvariable=self.price,width=22).grid(row=2,column=1,padx=12,pady=5) ttk.Label(self,text=f"Bid: {m.get('bid')} Ask: {m.get('ask')} Last: {m.get('last')}").grid(row=3,column=0,columnspan=2,sticky="w",padx=12,pady=8) ttk.Button(self,text="Submit",command=self.ok).grid(row=4,column=1,sticky="e",padx=12,pady=12) def ok(self): try: q=int(self.qty.get()); p=int(self.price.get()) if q<1 or p<0: raise ValueError - except Exception: - messagebox.showerror("Invalid order","Quantity must be positive and price must be a non-negative integer.",parent=self); return + except Exception: messagebox.showerror("Invalid order","Quantity must be positive and price non-negative.",parent=self); return self.result=(q,p); self.destroy() class App(tk.Tk): def __init__(self,state:Path|None=None,entity:str|None=None): - super().__init__(); self.title(APP_NAME); self.geometry("1380x800"); self.minsize(1000,650) - self.backend=None; self.market_rows=[]; self.asset_rows={} + super().__init__(); self.title(APP_NAME); self.geometry("1450x820"); self.minsize(1050,680) + self.backend=None; self.market_rows=[]; self.asset_rows={}; self.instrument_rows={} self.state_var=tk.StringVar(value=str(state or default_state())); self.entity_var=tk.StringVar(); self.entity_map={} self._style(); self._layout(); self.load_state(preselect=entity) @@ -190,39 +286,44 @@ def _style(self): def _layout(self): top=ttk.Frame(self,padding=12); top.pack(fill="x") ttk.Label(top,text="ENTITY Wallet",style="Title.TLabel").grid(row=0,column=0,sticky="w") - ttk.Label(top,text="Sovereign digital assets · canonical lineage · open rights market",style="Sub.TLabel").grid(row=1,column=0,sticky="w") + ttk.Label(top,text="Sovereign digital assets · universal multi-issuer market · wallet-to-wallet rights",style="Sub.TLabel").grid(row=1,column=0,sticky="w") ttk.Label(top,text="State").grid(row=0,column=1,padx=(30,5)); ttk.Entry(top,textvariable=self.state_var,width=45).grid(row=0,column=2,sticky="ew") ttk.Button(top,text="Browse",command=self.browse_state).grid(row=0,column=3,padx=4); ttk.Button(top,text="Load",command=self.load_state).grid(row=0,column=4,padx=4) ttk.Label(top,text="Identity").grid(row=1,column=1,padx=(30,5)) - self.entity_box=ttk.Combobox(top,textvariable=self.entity_var,state="readonly",width=42) - self.entity_box.grid(row=1,column=2,sticky="ew"); self.entity_box.bind("<>",lambda e:self.activate()) - ttk.Button(top,text="Refresh",command=self.refresh).grid(row=1,column=3,padx=4); top.columnconfigure(2,weight=1) + self.entity_box=ttk.Combobox(top,textvariable=self.entity_var,state="readonly",width=42); self.entity_box.grid(row=1,column=2,sticky="ew") + self.entity_box.bind("<>",lambda e:self.activate()); ttk.Button(top,text="Refresh",command=self.refresh).grid(row=1,column=3,padx=4); top.columnconfigure(2,weight=1) self.tabs=ttk.Notebook(self); self.tabs.pack(fill="both",expand=True,padx=12,pady=(0,12)) - self.assets_tab=ttk.Frame(self.tabs,padding=10); self.market_tab=ttk.Frame(self.tabs,padding=10) - self.positions_tab=ttk.Frame(self.tabs,padding=10); self.orders_tab=ttk.Frame(self.tabs,padding=10) - self.tabs.add(self.assets_tab,text="My Digital Assets"); self.tabs.add(self.market_tab,text="Market") - self.tabs.add(self.positions_tab,text="Market Positions"); self.tabs.add(self.orders_tab,text="Orders") + self.assets_tab=ttk.Frame(self.tabs,padding=10); self.instruments_tab=ttk.Frame(self.tabs,padding=10) + self.market_tab=ttk.Frame(self.tabs,padding=10); self.positions_tab=ttk.Frame(self.tabs,padding=10); self.orders_tab=ttk.Frame(self.tabs,padding=10) + self.tabs.add(self.assets_tab,text="My Digital Assets"); self.tabs.add(self.instruments_tab,text="My Instruments") + self.tabs.add(self.market_tab,text="ENTITY Market"); self.tabs.add(self.positions_tab,text="Market Positions"); self.tabs.add(self.orders_tab,text="Orders") bar=ttk.Frame(self.assets_tab); bar.pack(fill="x",pady=(0,8)) ttk.Button(bar,text="Upload / Ingest Digital Asset",command=self.ingest_asset).pack(side="left") ttk.Button(bar,text="View Lineage",command=self.view_lineage).pack(side="left",padx=4) - ttk.Label(bar,text="Assets are separate from market instruments. Ingest never creates a price.",style="Sub.TLabel").pack(side="left",padx=12) - self.assets=self._tree(self.assets_tab,["Title","Class","Type","Canonical lineage","Controller","Passport","BTDU","Object ID"], - [210,145,90,330,160,110,60,260]) + ttk.Button(bar,text="Create Economic Instrument",command=self.create_instrument).pack(side="left",padx=4) + ttk.Label(bar,text="Asset creation never auto-issues or prices rights.",style="Sub.TLabel").pack(side="left",padx=12) + self.assets=self._tree(self.assets_tab,["Title","Class","Type","Canonical lineage","Controller","Passport","BTDU","Object ID"],[210,145,90,330,160,100,55,250]) + + ibar=ttk.Frame(self.instruments_tab); ibar.pack(fill="x",pady=(0,8)) + ttk.Button(ibar,text="Create Listing",command=self.create_listing).pack(side="left") + ttk.Label(ibar,text="Canonical instruments are issuer-neutral; namespace:symbol is a display alias only.",style="Sub.TLabel").pack(side="left",padx=12) + self.instruments=self._tree(self.instruments_tab,["Market ID","Instrument Name","Rights Class","Class","Supply","DCO","Status"],[170,310,130,130,80,300,90]) mbar=ttk.Frame(self.market_tab); mbar.pack(fill="x",pady=(0,8)) ttk.Button(mbar,text="Buy",command=lambda:self.order("BUY")).pack(side="left") ttk.Button(mbar,text="Sell",command=lambda:self.order("SELL")).pack(side="left",padx=4) - ttk.Label(mbar,text="Price/time matching uses existing EEP. Settlement evidence remains required.",style="Sub.TLabel").pack(side="left",padx=12) - self.market=self._tree(self.market_tab,["Instrument","Class","Currency","Bid","Ask","Last","Venue"],[240,140,80,90,90,90,220]) - self.positions=self._tree(self.positions_tab,["Instrument","Units","Currency","Last","Bid","Ask","Indicative Value"],[260,90,80,90,90,90,140]) - self.orders=self._tree(self.orders_tab,["Instrument","Side","Remaining","Limit","Status"],[280,80,100,100,120]) + ttk.Button(mbar,text="Listing Information",command=self.view_listing).pack(side="left",padx=4) + ttk.Button(mbar,text="Export Portable Package",command=self.export_listing).pack(side="left",padx=4) + ttk.Label(mbar,text="One canonical instrument can be published/listed on multiple venues.",style="Sub.TLabel").pack(side="left",padx=12) + self.market=self._tree(self.market_tab,["Market ID","Instrument Name","Issuer","Rights","Currency","Bid","Ask","Last","Venue"],[160,260,240,110,75,80,80,80,220]) + self.positions=self._tree(self.positions_tab,["Instrument","Units","Currency","Last","Bid","Ask","Indicative Value"],[320,90,80,90,90,90,140]) + self.orders=self._tree(self.orders_tab,["Instrument","Side","Remaining","Limit","Status"],[340,80,100,100,120]) self.status=tk.StringVar(value="Ready"); ttk.Label(self,textvariable=self.status,relief="sunken",anchor="w",padding=5).pack(fill="x",side="bottom") def _tree(self,parent,columns,widths): - f=ttk.Frame(parent); f.pack(fill="both",expand=True) - t=ttk.Treeview(f,columns=columns,show="headings",selectmode="browse") + f=ttk.Frame(parent); f.pack(fill="both",expand=True); t=ttk.Treeview(f,columns=columns,show="headings",selectmode="browse") for c,w in zip(columns,widths): t.heading(c,text=c); t.column(c,width=w,anchor="w") y=ttk.Scrollbar(f,orient="vertical",command=t.yview); t.configure(yscrollcommand=y.set) t.pack(side="left",fill="both",expand=True); y.pack(side="right",fill="y"); return t @@ -230,25 +331,20 @@ def _tree(self,parent,columns,widths): def browse_state(self): p=filedialog.askdirectory(title="Select ENTITY state folder",initialdir=self.state_var.get()) if p: self.state_var.set(p); self.load_state() - def load_state(self,preselect=None): state=Path(self.state_var.get()).expanduser(); state.mkdir(parents=True,exist_ok=True) - items=manifests(state); self.entity_map={f"{x['display_name']} — {x['entity_id']}":x["entity_id"] for x in items} - self.entity_box["values"]=list(self.entity_map) + items=manifests(state); self.entity_map={f"{x['display_name']} — {x['entity_id']}":x["entity_id"] for x in items}; self.entity_box["values"]=list(self.entity_map) if preselect: for label,eid in self.entity_map.items(): if eid==preselect: self.entity_var.set(label); break elif items and not self.entity_var.get(): self.entity_var.set(next(iter(self.entity_map))) if self.entity_var.get(): self.activate() else: self.status.set("No ENTITY identity found in this state folder.") - def activate(self): try: - eid=self.entity_map.get(self.entity_var.get(),self.entity_var.get()) - self.backend=Backend(Path(self.state_var.get()),eid); self.refresh() + eid=self.entity_map.get(self.entity_var.get(),self.entity_var.get()); self.backend=Backend(Path(self.state_var.get()),eid); self.refresh() except Exception as e: self.backend=None; messagebox.showerror("Cannot load wallet",str(e)); self.status.set(str(e)) - @staticmethod def clear(tree): for x in tree.get_children(): tree.delete(x) @@ -256,76 +352,109 @@ def clear(tree): def refresh(self): if not self.backend: return try: - snap=self.backend.snapshot(); self.market_rows=self.backend.market(); self.asset_rows={} + snap=self.backend.snapshot(); self.market_rows=self.backend.market(); instruments=self.backend.instruments() + self.asset_rows={}; self.instrument_rows={} self.clear(self.assets) for a in snap.get("assets",[]): lin=a.get("lineage",{}); prot=lin.get("protocol_lineage",{}); al=lin.get("asset_lineage",{}) gp=lin.get("global_passport") or {}; btdu=lin.get("capability_bindings",{}).get("BTDU",{}).get("bound",False) - self.asset_rows[a["object_id"]]=a - self.assets.insert("", "end",iid=a["object_id"],values=( + self.asset_rows[a["object_id"]]=a; self.assets.insert("", "end",iid=a["object_id"],values=( a["title"],a.get("commodity_class"),a.get("object_type"),prot.get("display_path") or "UNRESOLVED", - al.get("controller_name") or a["controller_entity_id"], - (gp.get("version") or "—"),"YES" if btdu else "NO",a["object_id"])) + al.get("controller_name") or a["controller_entity_id"],gp.get("version") or "—","YES" if btdu else "NO",a["object_id"])) + self.clear(self.instruments) + for x in instruments: + self.instrument_rows[x["instrument_id"]]=x; self.instruments.insert("", "end",iid=x["instrument_id"],values=( + x["market_identifier"],x["instrument_name"],x["rights_class"],x["instrument_class"],x["supply"],x["underlying_dco_id"],x["status"])) self.clear(self.positions) for p in snap.get("positions",[]): - m=p["market"]; self.positions.insert("", "end",values=(p["instrument_id"],p["units"],p["settlement_currency"], - m.get("last"),m.get("bid"),m.get("ask"),p.get("market_value_amount_units"))) + m=p["market"]; self.positions.insert("", "end",values=(p["instrument_id"],p["units"],p["settlement_currency"],m.get("last"),m.get("bid"),m.get("ask"),p.get("market_value_amount_units"))) self.clear(self.orders) for o in snap.get("orders",[]): self.orders.insert("", "end",values=(o["instrument_id"],o["side"],o["remaining"],o["limit_price"],o["status"])) self.clear(self.market) for i,row in enumerate(self.market_rows): - m=row["market"]; self.market.insert("", "end",iid=str(i),values=(row["instrument_id"],row["instrument_class"], - row["settlement_currency"],m.get("bid"),m.get("ask"),m.get("last"),row.get("venue_id") or "UNLISTED")) - self.status.set(f"{len(snap.get('assets',[]))} digital assets · {len(snap.get('positions',[]))} market positions · protocol tax 0 · crypto not required") + m=row["market"]; self.market.insert("", "end",iid=str(i),values=( + row["market_identifier"],row["instrument_name"],row["issuer_entity_id"],row["rights_class"],row["settlement_currency"], + m.get("bid"),m.get("ask"),m.get("last"),row["venue_id"])) + self.status.set(f"{len(snap.get('assets',[]))} assets · {len(instruments)} issued instruments · {len(self.market_rows)} active listings · protocol tax 0 · crypto not required") except Exception as e: messagebox.showerror("Refresh failed",str(e)); self.status.set(str(e)) def view_lineage(self): sel=self.assets.selection() if not sel: messagebox.showwarning("Lineage","Select a digital asset."); return - a=self.asset_rows.get(sel[0]); lin=(a or {}).get("lineage",{}) - p=lin.get("protocol_lineage",{}); al=lin.get("asset_lineage",{}); cap=lin.get("capability_bindings",{}); gp=lin.get("global_passport") or {} + a=self.asset_rows.get(sel[0]); lin=(a or {}).get("lineage",{}); p=lin.get("protocol_lineage",{}); al=lin.get("asset_lineage",{}); cap=lin.get("capability_bindings",{}); gp=lin.get("global_passport") or {} parents="\n".join(f" {x.get('relation')}: {x.get('title') or x.get('parent_object_id')}" for x in al.get("parents",[])) or " None recorded" - msg=( - f"CANONICAL PROTOCOL / DOMAIN LINEAGE\n{p.get('display_path') or 'Unresolved'}\n\n" - f"Passport: {gp.get('passport_id') or 'None'} version {gp.get('version') or '—'}\n" - f"Origin embedded in passport: {p.get('passport_protocol_origin_embedded')}\n" - f"Needs lineage supersession: {p.get('needs_passport_lineage_supersession')}\n\n" - f"ASSET PROVENANCE\nController: {al.get('controller_name')}\nParents:\n{parents}\n\n" - f"SYSTEM BINDINGS\nBTDU bound: {cap.get('BTDU',{}).get('bound')}\n" - f"ADAM: deterministic execution/evidence; not sovereign authority\n" - f"NIKI: bounded reasoning/proposals; not sovereign authority\n\n" - f"Protocol ancestry does not transfer ownership or create economic entitlement." - ) - messagebox.showinfo("ENTITY Lineage",msg) + messagebox.showinfo("ENTITY Lineage",f"CANONICAL PROTOCOL / DOMAIN LINEAGE\n{p.get('display_path') or 'Unresolved'}\nComposed profiles: {', '.join(p.get('composed_domain_profiles') or []) or 'None'}\n\nPassport: {gp.get('passport_id') or 'None'} version {gp.get('version') or '—'}\nOrigin embedded: {p.get('passport_protocol_origin_embedded')}\n\nASSET PROVENANCE\nController: {al.get('controller_name')}\nParents:\n{parents}\n\nSYSTEM BINDINGS\nBTDU bound: {cap.get('BTDU',{}).get('bound')}\nADAM: deterministic execution/evidence; not ownership\nNIKI: bounded reasoning; not ownership\n\nProtocol ancestry does not transfer ownership or create economic entitlement.") def ingest_asset(self): - if not self.backend: messagebox.showwarning("Wallet","Load an ENTITY identity first."); return + if not self.backend: return f=filedialog.askopenfilename(title="Choose data or digital asset to ingest") if not f: return d=AssetDialog(self,Path(f),self.backend); self.wait_window(d) if not d.result: return try: - r=self.backend.ingest(f,d.result); self.refresh() - a=r["digital_asset"]; origin=r["protocol_origin"]; refs=r["profile_refs"] - messagebox.showinfo("Canonical digital asset created", - f"DCO: {a['object_id']}\n\nProfiles: {', '.join(refs)}\n" - f"Origin: {origin['origin_lineage_id']} → {origin['release_ref']}\n" - f"Global Passport verified: {r['global_passport_verified']}\nBTDU bound: {bool(r['btdu_binding'])}\n\n" - "No market instrument, listing, or price was created.") + r=self.backend.ingest(f,d.result); self.refresh(); a=r["digital_asset"]; origin=r["protocol_origin"] + messagebox.showinfo("Canonical digital asset created",f"DCO: {a['object_id']}\nOrigin: {origin['origin_lineage_id']} → {origin['release_ref']}\nGlobal Passport verified: {r['global_passport_verified']}\nBTDU bound: {bool(r['btdu_binding'])}\n\nNo instrument, listing, or price was created.") except Exception as e: messagebox.showerror("Ingest failed",str(e)) - def order(self,side): + def create_instrument(self): + if not self.backend: return + sel=self.assets.selection() + if not sel: messagebox.showwarning("Instrument","Select a controlled digital asset first."); return + asset=self.asset_rows[sel[0]]; d=InstrumentDialog(self,asset); self.wait_window(d) + if not d.result: return + try: + result=self.backend.create_instrument(asset["object_id"],d.result); self.refresh() + messagebox.showinfo("Instrument created",f"{result['market_identifier']}\n\nCanonical ID:\n{result['instrument_id']}\n\nNot listed. No market value was created.") + except Exception as e: messagebox.showerror("Instrument creation failed",str(e)) + + def create_listing(self): if not self.backend: return + sel=self.instruments.selection() + if not sel: messagebox.showwarning("Listing","Select one of your instruments."); return + instrument=self.instrument_rows[sel[0]]; d=ListingDialog(self,self.backend,instrument); self.wait_window(d) + if not d.result: return + try: + result=self.backend.create_listing(instrument["instrument_id"],d.result); self.refresh() + messagebox.showinfo("Listing created",f"{instrument['market_identifier']} is listed.\n\nListing ID:\n{result['listing_id']}\n\nRequired Listing Information Sheet and machine manifest were bound by hash.") + except Exception as e: messagebox.showerror("Listing creation failed",str(e)) + + def _selected_market_row(self): sel=self.market.selection() - if not sel: messagebox.showwarning("Market","Select a market instrument."); return - row=self.market_rows[int(sel[0])] - if not row.get("venue_id"): messagebox.showwarning("Market","This asset has no active market listing."); return + if not sel: return None + return self.market_rows[int(sel[0])] + + def view_listing(self): + row=self._selected_market_row() + if not row: messagebox.showwarning("Listing Information","Select a market listing."); return + try: + listing=self.backend.market_registry.listing(row["listing_id"]); info=listing["information"] + receives="\n".join("• "+x for x in info["buyer_receives"]) or "• See Rights Passport" + excludes="\n".join("• "+x for x in info["buyer_does_not_receive"]) or "• No unstated rights" + messagebox.showinfo("Listing Information",f"WHAT IS THIS?\n\n{info['what_is_this']}\n\nIssuer: {info['issuer']}\nMarket ID: {info['market_identifier']}\nUnderlying DCO: {info['underlying_dco_id']}\n\nBUYER RECEIVES\n{receives}\n\nBUYER DOES NOT RECEIVE\n{excludes}\n\nSupply: {info['supply']}\nJurisdiction: {info['jurisdiction']}\nPricing: {info['pricing_method']}\nRights Passport: {info['rights_passport_id']}\nGlobal Passport: {info['global_passport_id']}\n\nInstrument: {info['instrument_id']}\nListing: {info['listing_id']}") + except Exception as e: messagebox.showerror("Listing Information",str(e)) + + def export_listing(self): + row=self._selected_market_row() + if not row: messagebox.showwarning("Portable package","Select a market listing."); return + dest=filedialog.askdirectory(title="Choose folder for portable ENTITY instrument package") + if not dest: return + try: + instrument=self.backend.market_registry.instrument(row["instrument_id"]) + folder=Path(dest)/(instrument["market_identifier"].replace(":","-")+".entity-instrument") + result=self.backend.market_registry.export_portable_package(row["listing_id"],folder) + messagebox.showinfo("Portable package created",f"{result['market_identifier']}\n\n{result['destination']}\n\nIncludes PDF, Markdown, JSON passports/manifests, verification and SHA256SUMS.") + except Exception as e: messagebox.showerror("Portable package",str(e)) + + def order(self,side): + if not self.backend: return + row=self._selected_market_row() + if not row: messagebox.showwarning("Market","Select a market instrument."); return d=OrderDialog(self,row,side); self.wait_window(d) if not d.result: return try: q,p=d.result; result=self.backend.submit_order(row,side,q,p); self.refresh() - messagebox.showinfo("Order submitted",f"Order accepted.\nImmediate matches: {len(result['trades'])}\n\nPaid matches remain subject to settlement requirements.") + messagebox.showinfo("Order submitted",f"Order accepted.\nImmediate matches: {len(result['trades'])}\n\nMatched trades remain subject to clearing and settlement requirements.") except Exception as e: messagebox.showerror("Order failed",str(e)) def main(): From 9c89eb353e14b1f6cf3f9fa637b053cd7583505b Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:06:16 -0700 Subject: [PATCH 049/111] market: fix canonical instrument package insert arity From ed31f82c86724627fc381e86aab664c372794852 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:06:54 -0700 Subject: [PATCH 050/111] tests: bind explicit rights for universal instrument issuance --- tests/test_entity_market_registry.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_entity_market_registry.py b/tests/test_entity_market_registry.py index 6175d1b7..64cc33dd 100644 --- a/tests/test_entity_market_registry.py +++ b/tests/test_entity_market_registry.py @@ -10,9 +10,10 @@ def load(name,rel): class FakePassports: def __init__(self,kind): self.kind=kind; self.records={} - def bind(self,pid,object_id,controller,rp=None): + def bind(self,pid,object_id,controller,rp=None,actions=None): if self.kind=="rights": - self.records[pid]={"passport_id":pid,"object_id":object_id,"controller_entity_id":controller} + self.records[pid]={"passport_id":pid,"object_id":object_id,"controller_entity_id":controller, + "rights":[{"effect":"ALLOW","actions":list(actions or ["READ","COPY","DERIVE","TRAIN","EVALUATE","COMMERCIALIZE"])}]} else: self.records[pid]={"passport_id":pid,"object_id":object_id,"controller_entity_id":controller, "rights_passport_id":rp} @@ -39,7 +40,7 @@ def test_multi_issuer_symbols_canonical_ids_listing_sheet_and_portable_package() a=fabric.register_digital_commodity(acme,"Vision Model 07","1"*64, commodity_class="AI_MODEL",measurement_unit="ASSET",object_type="MODEL") j=fabric.register_digital_commodity(jane,"Photography Collection 2026","2"*64, - commodity_class="CREATIVE_COLLECTION",measurement_unit="ASSET",object_type="DOCUMENT") + commodity_class="CREATIVE_COLLECTION",measurement_unit="ASSET",object_type="COLLECTION") rights.bind("rp-acme",a["object_id"],acme); gps.bind("gp-acme",a["object_id"],acme,"rp-acme") rights.bind("rp-jane",j["object_id"],jane); gps.bind("gp-jane",j["object_id"],jane,"rp-jane") From b7750c54b40295655dcdd4f0a146be6835d1dc6d Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:09:19 -0700 Subject: [PATCH 051/111] market: add settlement-aware Data Value Discovery analytics --- src/45_ENTITY_Market/economic_intelligence.py | 245 ++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 src/45_ENTITY_Market/economic_intelligence.py diff --git a/src/45_ENTITY_Market/economic_intelligence.py b/src/45_ENTITY_Market/economic_intelligence.py new file mode 100644 index 00000000..8d8559ce --- /dev/null +++ b/src/45_ENTITY_Market/economic_intelligence.py @@ -0,0 +1,245 @@ +from __future__ import annotations +from pathlib import Path +from collections import defaultdict +import json, sqlite3, time + +DAY_MS=24*60*60*1000 +MONTH_MS=30*DAY_MS + +class EntityEconomicIntelligence: + """Observed, settlement-aware market analytics for ENTITY instruments. + + Metrics are keyed by canonical instrument_id. Market identifiers/tickers are + human aliases only. Observations describe trading in bounded rights and must + never be represented as intrinsic or accounting value of the underlying DCO. + """ + + def __init__(self,state_dir:str|Path,market_registry,fabric,global_passports=None): + self.state=Path(state_dir); self.market_registry=market_registry + self.fabric=fabric; self.global_passports=global_passports + self.exchange_path=self.state/"entity_v3_exchange.sqlite" + self.eopp_path=self.state/"entity_v3_economic_participation.sqlite" + self.passport_path=self.state/"entity_v3_4_global_passports.sqlite" + + @staticmethod + def _db(path:Path): + db=sqlite3.connect(path); db.row_factory=sqlite3.Row; return db + + def _passport(self,passport_id:str)->dict|None: + if self.global_passports is not None: + try: return self.global_passports.get(passport_id) + except Exception: pass + if not self.passport_path.exists(): return None + db=self._db(self.passport_path) + try: + row=db.execute("SELECT body_json FROM global_passports WHERE passport_id=?",(passport_id,)).fetchone() + return json.loads(row["body_json"]) if row else None + finally: db.close() + + @staticmethod + def _primary_domain(passport:dict|None)->str: + refs=list(((passport or {}).get("profile_stack") or {}).get("profile_refs") or []) + for ref in refs: + if "entity-profile:global@" in ref: continue + return str(ref).split("@",1)[0].split(":")[-1].upper().replace("-","_") + return "UNCLASSIFIED" + + def _classification(self,instrument:dict)->dict: + obj=self.fabric.get_object(instrument["underlying_dco_id"]) + descriptor=dict(obj.get("descriptor") or {}) + meta=dict(descriptor.get("metadata") or {}) + passport=self._passport(instrument["global_passport_id"]) + return { + "underlying_dco_id":instrument["underlying_dco_id"], + "asset_title":obj.get("title"), + "object_type":obj.get("object_type"), + "asset_class":descriptor.get("commodity_class") or obj.get("object_type") or "UNCLASSIFIED", + "asset_subtype":meta.get("asset_subtype") or meta.get("category") or descriptor.get("commodity_class") or "UNCLASSIFIED", + "primary_domain":self._primary_domain(passport), + "issuer_entity_id":instrument["issuer_entity_id"], + "rights_class":instrument["rights_class"], + } + + def _royalties(self,instrument_id:str)->dict: + out={"settled_amounts_by_currency":{},"externally_verified_amounts_by_currency":{}, + "settled_obligation_count":0,"externally_verified_obligation_count":0} + if not self.eopp_path.exists(): return out + db=self._db(self.eopp_path) + try: + rows=db.execute("""SELECT o.amount_units,o.currency,o.status,o.external_verified,o.basis + FROM obligations o + JOIN economic_events e ON e.event_id=o.event_id + WHERE e.instrument_id=? AND o.status='SETTLED' + AND o.basis IN ('SECONDARY_ROYALTY','DERIVATIVE_PARTICIPATION', + 'PRIMARY_TREASURY_ALLOCATION')""",(instrument_id,)).fetchall() + except sqlite3.OperationalError: + rows=[] + finally: db.close() + settled=defaultdict(int); verified=defaultdict(int) + for r in rows: + cur=str(r["currency"]); amount=int(r["amount_units"]); settled[cur]+=amount + out["settled_obligation_count"]+=1 + if int(r["external_verified"]): + verified[cur]+=amount; out["externally_verified_obligation_count"]+=1 + out["settled_amounts_by_currency"]=dict(sorted(settled.items())) + out["externally_verified_amounts_by_currency"]=dict(sorted(verified.items())) + return out + + def instrument_metrics(self,instrument_id:str,*,as_of_ms:int|None=None)->dict: + instrument=self.market_registry.instrument(instrument_id) + now=int(as_of_ms if as_of_ms is not None else time.time()*1000) + start24=now-DAY_MS; start30=now-MONTH_MS + db=self._db(self.exchange_path) + try: + trades=db.execute("""SELECT t.*,c.external_verified,c.status clearing_status + FROM trades t LEFT JOIN clearing c ON c.trade_id=t.trade_id + WHERE t.instrument_id=? AND t.status='SETTLED' + ORDER BY t.created_at_ms,t.trade_id""",(instrument_id,)).fetchall() + balances=db.execute("SELECT holder,units FROM balances WHERE instrument_id=? AND units>0",(instrument_id,)).fetchall() + orders=db.execute("""SELECT participant,side,remaining,status FROM orders + WHERE instrument_id=? AND status IN ('OPEN','PARTIAL')""",(instrument_id,)).fetchall() + issuer_balance_row=db.execute("SELECT units FROM balances WHERE instrument_id=? AND holder=?", + (instrument_id,instrument["issuer_entity_id"])).fetchone() + finally: db.close() + + def summarize(rows): + units=sum(int(r["quantity"]) for r in rows) + notional=sum(int(r["quantity"])*int(r["price"]) for r in rows) + verified=[r for r in rows if int(r["external_verified"] or 0)==1] + self_rows=[r for r in rows if r["buyer"]==r["seller"]] + buyers={r["buyer"] for r in rows}; sellers={r["seller"] for r in rows} + prices=[int(r["price"]) for r in rows] + pairs=defaultdict(lambda:{"forward":0,"reverse":0,"notional":0}) + unordered=defaultdict(lambda:{"dirs":set(),"notional":0}) + for r in rows: + b,s=str(r["buyer"]),str(r["seller"]); n=int(r["quantity"])*int(r["price"]) + key=tuple(sorted((b,s))); unordered[key]["dirs"].add((b,s)); unordered[key]["notional"]+=n + reciprocal_notional=sum(v["notional"] for v in unordered.values() if len(v["dirs"])>1) + pair_max=max((v["notional"] for v in unordered.values()),default=0) + return { + "trade_count":len(rows),"volume_units":units,"notional_amount_units":notional, + "unique_buyers":len(buyers),"unique_sellers":len(sellers), + "high_price":max(prices) if prices else None,"low_price":min(prices) if prices else None, + "externally_verified_trade_count":len(verified), + "externally_verified_volume_units":sum(int(r["quantity"]) for r in verified), + "externally_verified_notional_amount_units":sum(int(r["quantity"])*int(r["price"]) for r in verified), + "self_trade_count":len(self_rows), + "self_trade_notional_amount_units":sum(int(r["quantity"])*int(r["price"]) for r in self_rows), + "reciprocal_counterparty_notional_amount_units":reciprocal_notional, + "largest_counterparty_pair_notional_amount_units":pair_max, + "largest_counterparty_pair_share":(pair_max/notional) if notional else None, + } + + rows24=[r for r in trades if int(r["created_at_ms"])>=start24] + rows30=[r for r in trades if int(r["created_at_ms"])>=start30] + last=trades[-1] if trades else None + last_verified=next((r for r in reversed(trades) if int(r["external_verified"] or 0)==1),None) + issuer_balance=int(issuer_balance_row["units"]) if issuer_balance_row else 0 + issuer_reserved=sum(int(r["remaining"]) for r in orders if r["side"]=="SELL" and r["participant"]==instrument["issuer_entity_id"]) + open_sell_units=sum(int(r["remaining"]) for r in orders if r["side"]=="SELL") + total_supply=int(instrument["supply"]) + active_holder_count=len(balances) + nonissuer_holder_count=sum(1 for r in balances if r["holder"]!=instrument["issuer_entity_id"]) + m30=summarize(rows30) + flags=[] + if m30["self_trade_count"]: flags.append("SELF_TRADE_ACTIVITY_PRESENT") + if m30["reciprocal_counterparty_notional_amount_units"]: flags.append("RECIPROCAL_COUNTERPARTY_FLOW_PRESENT") + if m30["largest_counterparty_pair_share"] is not None and m30["largest_counterparty_pair_share"]>=0.50: + flags.append("COUNTERPARTY_CONCENTRATION_HIGH") + if m30["trade_count"] and m30["externally_verified_trade_count"]==0: + flags.append("NO_EXTERNALLY_VERIFIED_SETTLEMENTS_IN_WINDOW") + classification=self._classification(instrument) + return { + "schema":"entity-economic-intelligence-instrument-v1", + "instrument_id":instrument_id, + "market_identifier":instrument["market_identifier"], + "display_symbol":instrument["display_symbol"], + "instrument_name":instrument["instrument_name"], + "issuer_entity_id":instrument["issuer_entity_id"], + **classification, + "settlement_currency":self._settlement_currency(instrument_id), + "last_settled_price":None if last is None else int(last["price"]), + "last_settled_trade_at_ms":None if last is None else int(last["created_at_ms"]), + "last_externally_verified_price":None if last_verified is None else int(last_verified["price"]), + "last_externally_verified_trade_at_ms":None if last_verified is None else int(last_verified["created_at_ms"]), + "window_24h":summarize(rows24),"window_30d":m30, + "lifetime":summarize(trades), + "total_supply_units":total_supply, + "issuer_inventory_units":issuer_balance, + "issuer_unreserved_inventory_units":max(0,issuer_balance-issuer_reserved), + "open_sell_offer_units":open_sell_units, + "circulating_units":max(0,total_supply-issuer_balance), + "active_holder_count":active_holder_count, + "nonissuer_holder_count":nonissuer_holder_count, + "royalties_and_participation":self._royalties(instrument_id), + "market_integrity":{ + "flags":flags, + "related_wallet_detection_performed":False, + "related_wallet_detection_requires_authorized_relationship_evidence":True, + "raw_volume_is_not_proof_of_value":True, + "orders_and_quotes_are_not_counted_as_realized_demand":True, + "settled_trades_only_for_price_volume_metrics":True, + }, + "valuation_boundary":{ + "observed_prices_value_instrument_rights_not_underlying_dco":True, + "instrument_price_times_supply_is_not_underlying_asset_value":True, + "market_observation_is_not_accounting_fair_value":True, + "intrinsic_value_not_claimed":True, + }, + "as_of_ms":now, + } + + def _settlement_currency(self,instrument_id:str)->str|None: + db=self._db(self.exchange_path) + try: + row=db.execute("SELECT settlement_currency FROM instruments WHERE instrument_id=?",(instrument_id,)).fetchone() + return row["settlement_currency"] if row else None + finally: db.close() + + def dco_rights_demand(self,dco_id:str,*,as_of_ms:int|None=None)->dict: + db=self.market_registry._db() + try: + rows=db.execute("""SELECT instrument_id FROM instrument_packages + WHERE underlying_dco_id=? AND status='ACTIVE' + ORDER BY rights_class,market_identifier""",(str(dco_id),)).fetchall() + finally: db.close() + metrics=[self.instrument_metrics(r["instrument_id"],as_of_ms=as_of_ms) for r in rows] + by_rights=defaultdict(lambda:{"instruments":0,"volume_units_30d":0,"notional_30d_by_currency":defaultdict(int), + "unique_buyers":set(),"settled_trades":0}) + for m in metrics: + r=by_rights[m["rights_class"]]; r["instruments"]+=1 + r["volume_units_30d"]+=m["window_30d"]["volume_units"]; r["settled_trades"]+=m["window_30d"]["trade_count"] + cur=m["settlement_currency"] or "UNSPECIFIED" + r["notional_30d_by_currency"][cur]+=m["window_30d"]["notional_amount_units"] + clean={k:{**v,"notional_30d_by_currency":dict(v["notional_30d_by_currency"])} + for k,v in sorted(by_rights.items())} + return {"schema":"entity-dco-rights-demand-v1","underlying_dco_id":str(dco_id), + "instruments":metrics,"rights_class_rollup":clean, + "observed_demand_is_for_instrument_rights_not_intrinsic_asset_value":True} + + def economy_rollup(self,*,group_by:str="asset_class",as_of_ms:int|None=None)->dict: + allowed={"asset_class","asset_subtype","primary_domain","rights_class","issuer_entity_id","underlying_dco_id"} + if group_by not in allowed: raise ValueError("unsupported rollup dimension") + db=self.market_registry._db() + try: + ids=[r["instrument_id"] for r in db.execute( + "SELECT instrument_id FROM instrument_packages WHERE status='ACTIVE' ORDER BY instrument_id").fetchall()] + finally: db.close() + groups=defaultdict(lambda:{"instrument_count":0,"settled_trades_30d":0,"volume_units_30d":0, + "notional_30d_by_currency":defaultdict(int),"unique_buyer_sum":0}) + for iid in ids: + m=self.instrument_metrics(iid,as_of_ms=as_of_ms); key=str(m.get(group_by) or "UNCLASSIFIED") + g=groups[key]; g["instrument_count"]+=1; g["settled_trades_30d"]+=m["window_30d"]["trade_count"] + g["volume_units_30d"]+=m["window_30d"]["volume_units"]; g["unique_buyer_sum"]+=m["window_30d"]["unique_buyers"] + g["notional_30d_by_currency"][m["settlement_currency"] or "UNSPECIFIED"]+=m["window_30d"]["notional_amount_units"] + rows=[] + for key,g in groups.items(): + rows.append({"group":key,"instrument_count":g["instrument_count"], + "settled_trades_30d":g["settled_trades_30d"],"volume_units_30d":g["volume_units_30d"], + "notional_30d_by_currency":dict(sorted(g["notional_30d_by_currency"].items())), + "sum_instrument_unique_buyers_30d":g["unique_buyer_sum"]}) + rows.sort(key=lambda x:(-sum(x["notional_30d_by_currency"].values()),x["group"])) + return {"schema":"entity-economy-rollup-v1","group_by":group_by,"rows":rows, + "cross_currency_notional_is_not_summed_into_one_value":True, + "rollup_is_observed_market_activity_not_underlying_asset_valuation":True, + "as_of_ms":int(as_of_ms if as_of_ms is not None else time.time()*1000)} From 8181182fdc3bed8b144fb35901f9ecd18e285385 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:09:49 -0700 Subject: [PATCH 052/111] market: keep intelligence rollups currency-safe and JSON clean --- src/45_ENTITY_Market/economic_intelligence.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/45_ENTITY_Market/economic_intelligence.py b/src/45_ENTITY_Market/economic_intelligence.py index 8d8559ce..034149d4 100644 --- a/src/45_ENTITY_Market/economic_intelligence.py +++ b/src/45_ENTITY_Market/economic_intelligence.py @@ -204,8 +204,8 @@ def dco_rights_demand(self,dco_id:str,*,as_of_ms:int|None=None)->dict: ORDER BY rights_class,market_identifier""",(str(dco_id),)).fetchall() finally: db.close() metrics=[self.instrument_metrics(r["instrument_id"],as_of_ms=as_of_ms) for r in rows] - by_rights=defaultdict(lambda:{"instruments":0,"volume_units_30d":0,"notional_30d_by_currency":defaultdict(int), - "unique_buyers":set(),"settled_trades":0}) + by_rights=defaultdict(lambda:{"instruments":0,"volume_units_30d":0, + "notional_30d_by_currency":defaultdict(int),"settled_trades":0}) for m in metrics: r=by_rights[m["rights_class"]]; r["instruments"]+=1 r["volume_units_30d"]+=m["window_30d"]["volume_units"]; r["settled_trades"]+=m["window_30d"]["trade_count"] @@ -238,7 +238,7 @@ def economy_rollup(self,*,group_by:str="asset_class",as_of_ms:int|None=None)->di "settled_trades_30d":g["settled_trades_30d"],"volume_units_30d":g["volume_units_30d"], "notional_30d_by_currency":dict(sorted(g["notional_30d_by_currency"].items())), "sum_instrument_unique_buyers_30d":g["unique_buyer_sum"]}) - rows.sort(key=lambda x:(-sum(x["notional_30d_by_currency"].values()),x["group"])) + rows.sort(key=lambda x:(-x["volume_units_30d"],x["group"])) return {"schema":"entity-economy-rollup-v1","group_by":group_by,"rows":rows, "cross_currency_notional_is_not_summed_into_one_value":True, "rollup_is_observed_market_activity_not_underlying_asset_valuation":True, From 8af4806ba021346b7e2ab94421a3bf2beb231031 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:10:24 -0700 Subject: [PATCH 053/111] market: fix economic intelligence registry reads --- src/45_ENTITY_Market/economic_intelligence.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/45_ENTITY_Market/economic_intelligence.py b/src/45_ENTITY_Market/economic_intelligence.py index 034149d4..5941336c 100644 --- a/src/45_ENTITY_Market/economic_intelligence.py +++ b/src/45_ENTITY_Market/economic_intelligence.py @@ -197,12 +197,10 @@ def _settlement_currency(self,instrument_id:str)->str|None: finally: db.close() def dco_rights_demand(self,dco_id:str,*,as_of_ms:int|None=None)->dict: - db=self.market_registry._db() - try: + with self.market_registry._db() as db: rows=db.execute("""SELECT instrument_id FROM instrument_packages WHERE underlying_dco_id=? AND status='ACTIVE' ORDER BY rights_class,market_identifier""",(str(dco_id),)).fetchall() - finally: db.close() metrics=[self.instrument_metrics(r["instrument_id"],as_of_ms=as_of_ms) for r in rows] by_rights=defaultdict(lambda:{"instruments":0,"volume_units_30d":0, "notional_30d_by_currency":defaultdict(int),"settled_trades":0}) @@ -220,11 +218,9 @@ def dco_rights_demand(self,dco_id:str,*,as_of_ms:int|None=None)->dict: def economy_rollup(self,*,group_by:str="asset_class",as_of_ms:int|None=None)->dict: allowed={"asset_class","asset_subtype","primary_domain","rights_class","issuer_entity_id","underlying_dco_id"} if group_by not in allowed: raise ValueError("unsupported rollup dimension") - db=self.market_registry._db() - try: + with self.market_registry._db() as db: ids=[r["instrument_id"] for r in db.execute( "SELECT instrument_id FROM instrument_packages WHERE status='ACTIVE' ORDER BY instrument_id").fetchall()] - finally: db.close() groups=defaultdict(lambda:{"instrument_count":0,"settled_trades_30d":0,"volume_units_30d":0, "notional_30d_by_currency":defaultdict(int),"unique_buyer_sum":0}) for iid in ids: From c9f085e35ffbb2d72a3427d58b77d4251e054c21 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:10:36 -0700 Subject: [PATCH 054/111] market intelligence: preserve currency and settled-demand boundaries --- src/45_ENTITY_Market/economic_intelligence.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/45_ENTITY_Market/economic_intelligence.py b/src/45_ENTITY_Market/economic_intelligence.py index 5941336c..00b3a9ec 100644 --- a/src/45_ENTITY_Market/economic_intelligence.py +++ b/src/45_ENTITY_Market/economic_intelligence.py @@ -70,7 +70,7 @@ def _royalties(self,instrument_id:str)->dict: FROM obligations o JOIN economic_events e ON e.event_id=o.event_id WHERE e.instrument_id=? AND o.status='SETTLED' - AND o.basis IN ('SECONDARY_ROYALTY','DERIVATIVE_PARTICIPATION', + AND o.basis IN ('SECONDARY_ORIGINATOR_ROYALTY','DERIVATIVE_PARTICIPATION', 'PRIMARY_TREASURY_ALLOCATION')""",(instrument_id,)).fetchall() except sqlite3.OperationalError: rows=[] @@ -207,6 +207,7 @@ def dco_rights_demand(self,dco_id:str,*,as_of_ms:int|None=None)->dict: for m in metrics: r=by_rights[m["rights_class"]]; r["instruments"]+=1 r["volume_units_30d"]+=m["window_30d"]["volume_units"]; r["settled_trades"]+=m["window_30d"]["trade_count"] + r["sum_instrument_unique_buyers_30d"]+=m["window_30d"]["unique_buyers"] cur=m["settlement_currency"] or "UNSPECIFIED" r["notional_30d_by_currency"][cur]+=m["window_30d"]["notional_amount_units"] clean={k:{**v,"notional_30d_by_currency":dict(v["notional_30d_by_currency"])} From 33bdcf7cdf843a6cdc2b45899207894c712c0ccf Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:11:01 -0700 Subject: [PATCH 055/111] tests: use supported creative DCO object type --- tests/test_entity_market_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_entity_market_registry.py b/tests/test_entity_market_registry.py index 64cc33dd..245b2bcd 100644 --- a/tests/test_entity_market_registry.py +++ b/tests/test_entity_market_registry.py @@ -40,7 +40,7 @@ def test_multi_issuer_symbols_canonical_ids_listing_sheet_and_portable_package() a=fabric.register_digital_commodity(acme,"Vision Model 07","1"*64, commodity_class="AI_MODEL",measurement_unit="ASSET",object_type="MODEL") j=fabric.register_digital_commodity(jane,"Photography Collection 2026","2"*64, - commodity_class="CREATIVE_COLLECTION",measurement_unit="ASSET",object_type="COLLECTION") + commodity_class="CREATIVE_COLLECTION",measurement_unit="ASSET",object_type="DOCUMENT") rights.bind("rp-acme",a["object_id"],acme); gps.bind("gp-acme",a["object_id"],acme,"rp-acme") rights.bind("rp-jane",j["object_id"],jane); gps.bind("gp-jane",j["object_id"],jane,"rp-jane") From 2830f6b6ada8e06e4fa8b800574dfeb815c7bd62 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:11:05 -0700 Subject: [PATCH 056/111] tests: qualify settlement-aware Data Value Discovery analytics --- tests/test_entity_economic_intelligence.py | 95 ++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 tests/test_entity_economic_intelligence.py diff --git a/tests/test_entity_economic_intelligence.py b/tests/test_entity_economic_intelligence.py new file mode 100644 index 00000000..d15cb589 --- /dev/null +++ b/tests/test_entity_economic_intelligence.py @@ -0,0 +1,95 @@ +from __future__ import annotations +from pathlib import Path +import importlib.util,sys,tempfile,sqlite3,time + +ROOT=Path(__file__).resolve().parents[1] +def load(name,rel): + p=ROOT/rel; spec=importlib.util.spec_from_file_location(name,p) + m=importlib.util.module_from_spec(spec); sys.modules[name]=m; spec.loader.exec_module(m); return m + +class FakePassports: + def __init__(self,kind): self.kind=kind; self.records={} + def bind(self,pid,object_id,controller,rp=None,actions=None,profiles=None): + if self.kind=="rights": + self.records[pid]={"passport_id":pid,"object_id":object_id,"controller_entity_id":controller, + "rights":[{"effect":"ALLOW","actions":list(actions or ["READ","TRAIN","COMMERCIALIZE"])}]} + else: + self.records[pid]={"passport_id":pid,"object_id":object_id,"controller_entity_id":controller, + "rights_passport_id":rp,"profile_stack":{"profile_refs":list(profiles or ["entity-profile:global@1.0","entity-profile:ai@1.0"])}} + def get(self,pid): return dict(self.records[pid]) + def verify(self,p): return {"valid":True} + +def test_settlement_aware_instrument_metrics_and_rollups(): + ident=load("intel_identity","src/01_Core_Runtime/identity/canonical_identity.py") + fabmod=load("intel_fabric","src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py") + exmod=load("intel_exchange","src/31_Profiles/exchange_protocol.py") + marketmod=load("intel_market","src/45_ENTITY_Market/canonical_market_registry.py") + intelmod=load("intel_engine","src/45_ENTITY_Market/economic_intelligence.py") + with tempfile.TemporaryDirectory() as td: + state=Path(td); vault=ident.EntityIdentityVault(state) + issuer=vault.create("Acme Corp","organization")["entity_id"] + b1=vault.create("Buyer One","person")["entity_id"]; b2=vault.create("Buyer Two","person")["entity_id"] + venue_owner=vault.create("Market Operator","organization")["entity_id"] + fabric=fabmod.UniversalTransactionFabric(state,vault); ex=exmod.ExchangeProtocol(state,vault,fabric) + rights=FakePassports("rights"); gps=FakePassports("global") + reg=marketmod.EntityEconomicMarketRegistry(state,vault,fabric,ex,rights,gps) + venue=ex.create_venue(venue_owner,"Test Market","CA",["ORDER_BOOK"],"a"*64) + asset=fabric.register_digital_commodity(issuer,"Weather Data 2026","b"*64, + commodity_class="WEATHER_DATA",measurement_unit="ASSET",object_type="DATASET", + metadata={"asset_subtype":"WEATHER"}) + rights.bind("rp",asset["object_id"],issuer,actions=["READ","TRAIN","COMMERCIALIZE"]) + gps.bind("gp",asset["object_id"],issuer,"rp",profiles=["entity-profile:global@1.0","entity-profile:ai@1.0"]) + ins=reg.create_instrument(issuer,asset["object_id"],instrument_name="Weather Data AI Training Rights", + display_symbol="WX26-TRN",instrument_class="SPOT_LICENSE",rights_class="TRAINING", + rights={"actions":["TRAIN"]},supply=100,rights_passport_id="rp",global_passport_id="gp", + settlement_currency="CAD",jurisdiction="CA",namespace="ACME",transferable=True, + buyer_receives=["AI training right"],buyer_does_not_receive=["dataset ownership"]) + reg.create_listing(issuer,ins["instrument_id"],venue["venue_id"],market_id="ENTITY-PRIMARY", + quote_unit="CAD",minimum_quantity=1,pricing_method="ORDER_BOOK") + now=2_000_000_000_000 + db=sqlite3.connect(state/"entity_v3_exchange.sqlite") + db.execute("UPDATE balances SET units=70 WHERE instrument_id=? AND holder=?",(ins["instrument_id"],issuer)) + db.execute("INSERT OR REPLACE INTO balances(instrument_id,holder,units) VALUES(?,?,?)",(ins["instrument_id"],b1,20)) + db.execute("INSERT OR REPLACE INTO balances(instrument_id,holder,units) VALUES(?,?,?)",(ins["instrument_id"],b2,10)) + trades=[ + ("t1",b1,issuer,10,18,now-2*60*60*1000,1), + ("t2",b2,issuer,8,21,now-10*DAY if False else now-10*24*60*60*1000,0), + ("t3",b1,b1,2,20,now-5*24*60*60*1000,1), + ] + for tid,buyer,seller,qty,price,ts,verified in trades: + db.execute("""INSERT INTO trades(trade_id,venue_id,instrument_id,buy_order_id,sell_order_id,buyer,seller, + quantity,price,execution_model,status,created_at_ms) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)""", + (tid,venue["venue_id"],ins["instrument_id"],None,None,buyer,seller,qty,price,"ORDER_BOOK","SETTLED",ts)) + db.execute("INSERT INTO clearing(trade_id,payer,payee,amount_units,currency,status,payment_ref,external_verified) VALUES(?,?,?,?,?,?,?,?)", + (tid,buyer,seller,qty*price,"CAD","SETTLED","pay-"+tid,verified)) + db.commit(); db.close() + + ep=sqlite3.connect(state/"entity_v3_economic_participation.sqlite") + ep.execute("CREATE TABLE economic_events(event_id TEXT PRIMARY KEY,instrument_id TEXT)") + ep.execute("""CREATE TABLE obligations(event_id TEXT,amount_units INTEGER,currency TEXT,status TEXT, + external_verified INTEGER,basis TEXT)""") + ep.execute("INSERT INTO economic_events VALUES(?,?)",("e1",ins["instrument_id"])) + ep.execute("INSERT INTO obligations VALUES(?,?,?,?,?,?)",("e1",38,"CAD","SETTLED",1,"SECONDARY_ROYALTY")) + ep.commit(); ep.close() + + eng=intelmod.EntityEconomicIntelligence(state,reg,fabric,gps) + m=eng.instrument_metrics(ins["instrument_id"],as_of_ms=now) + assert m["market_identifier"]=="ACME:WX26-TRN" + assert m["last_settled_price"]==18 + assert m["window_24h"]["volume_units"]==10 + assert m["window_30d"]["volume_units"]==20 + assert m["window_30d"]["unique_buyers"]==2 + assert m["window_30d"]["self_trade_count"]==1 + assert "SELF_TRADE_ACTIVITY_PRESENT" in m["market_integrity"]["flags"] + assert m["active_holder_count"]==3 + assert m["issuer_inventory_units"]==70 + assert m["royalties_and_participation"]["externally_verified_amounts_by_currency"]["CAD"]==38 + assert m["valuation_boundary"]["observed_prices_value_instrument_rights_not_underlying_dco"] is True + assert m["asset_class"]=="WEATHER_DATA" + assert m["primary_domain"]=="AI" + + d=eng.dco_rights_demand(asset["object_id"],as_of_ms=now) + assert d["rights_class_rollup"]["TRAINING"]["volume_units_30d"]==20 + roll=eng.economy_rollup(group_by="asset_class",as_of_ms=now) + assert roll["rows"][0]["group"]=="WEATHER_DATA" + assert roll["rows"][0]["volume_units_30d"]==20 From 99575d57d0864ee09f86ba7cca37c540b775bddc Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:12:26 -0700 Subject: [PATCH 057/111] tests: align market intelligence royalty basis with EOPP --- tests/test_entity_economic_intelligence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_entity_economic_intelligence.py b/tests/test_entity_economic_intelligence.py index d15cb589..98f46681 100644 --- a/tests/test_entity_economic_intelligence.py +++ b/tests/test_entity_economic_intelligence.py @@ -69,7 +69,7 @@ def test_settlement_aware_instrument_metrics_and_rollups(): ep.execute("""CREATE TABLE obligations(event_id TEXT,amount_units INTEGER,currency TEXT,status TEXT, external_verified INTEGER,basis TEXT)""") ep.execute("INSERT INTO economic_events VALUES(?,?)",("e1",ins["instrument_id"])) - ep.execute("INSERT INTO obligations VALUES(?,?,?,?,?,?)",("e1",38,"CAD","SETTLED",1,"SECONDARY_ROYALTY")) + ep.execute("INSERT INTO obligations VALUES(?,?,?,?,?,?)",("e1",38,"CAD","SETTLED",1,"SECONDARY_ORIGINATOR_ROYALTY")) ep.commit(); ep.close() eng=intelmod.EntityEconomicIntelligence(state,reg,fabric,gps) From cecfa1b5cb5f858fbf039913f5ab9c9805bdb220 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:13:26 -0700 Subject: [PATCH 058/111] market: align royalty analytics to EOPP obligation basis From 5fbe8f0bf6349fd9193aa34a8f8b7339529b6577 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:13:38 -0700 Subject: [PATCH 059/111] market intelligence: initialize rights-class buyer aggregate --- src/45_ENTITY_Market/economic_intelligence.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/45_ENTITY_Market/economic_intelligence.py b/src/45_ENTITY_Market/economic_intelligence.py index 00b3a9ec..04085cf2 100644 --- a/src/45_ENTITY_Market/economic_intelligence.py +++ b/src/45_ENTITY_Market/economic_intelligence.py @@ -203,7 +203,8 @@ def dco_rights_demand(self,dco_id:str,*,as_of_ms:int|None=None)->dict: ORDER BY rights_class,market_identifier""",(str(dco_id),)).fetchall() metrics=[self.instrument_metrics(r["instrument_id"],as_of_ms=as_of_ms) for r in rows] by_rights=defaultdict(lambda:{"instruments":0,"volume_units_30d":0, - "notional_30d_by_currency":defaultdict(int),"settled_trades":0}) + "notional_30d_by_currency":defaultdict(int), + "sum_instrument_unique_buyers_30d":0,"settled_trades":0}) for m in metrics: r=by_rights[m["rights_class"]]; r["instruments"]+=1 r["volume_units_30d"]+=m["window_30d"]["volume_units"]; r["settled_trades"]+=m["window_30d"]["trade_count"] From 09e59eddfc47fc95e8d8866f7d4196b726b527f1 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:15:06 -0700 Subject: [PATCH 060/111] wallet: connect economic intelligence backend --- tools/entity_wallet_desktop.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tools/entity_wallet_desktop.py b/tools/entity_wallet_desktop.py index bdab6536..bd1dc1db 100644 --- a/tools/entity_wallet_desktop.py +++ b/tools/entity_wallet_desktop.py @@ -21,6 +21,7 @@ def load_module(name:str,relative:str): INGEST=load_module("entity_wallet_ingest","src/42_ENTITY_Wallet/asset_ingest.py") EXCHANGE=load_module("entity_wallet_exchange","src/31_Profiles/exchange_protocol.py") MARKET=load_module("entity_wallet_market","src/45_ENTITY_Market/canonical_market_registry.py") +INTEL=load_module("entity_wallet_intelligence","src/45_ENTITY_Market/economic_intelligence.py") def default_state()->Path: env=os.environ.get("ENTITY_STATE_DIR") @@ -51,6 +52,8 @@ def __init__(self,state:Path,entity_id:str): self.exchange=EXCHANGE.ExchangeProtocol(state,self.identity,self.fabric) self.market_registry=MARKET.EntityEconomicMarketRegistry( state,self.identity,self.fabric,self.exchange,self.rights,self.passports) + self.intelligence=INTEL.EntityEconomicIntelligence( + state,self.market_registry,self.fabric,self.passports) self.ingestor=INGEST.WalletAssetIngestor(state,CLI) self.wallet_record=self.wallet.ensure_wallet(entity_id,self.identity_manifest.get("display_name","ENTITY")+" Wallet") @@ -96,10 +99,18 @@ def market(self)->list[dict]: (row["instrument_id"],)).fetchone() if not e: continue d=dict(row); d["settlement_currency"]=e["settlement_currency"] - d["market"]=self.wallet._market(db,d["instrument_id"]); out.append(d) + d["market"]=self.wallet._market(db,d["instrument_id"]) + d["intelligence"]=self.intelligence.instrument_metrics(d["instrument_id"]) + out.append(d) return out finally: db.close() + def economy_rollup(self,group_by:str): + return self.intelligence.economy_rollup(group_by=group_by) + + def rights_demand(self,dco_id:str): + return self.intelligence.dco_rights_demand(dco_id) + def ingest(self,path,options): return self.ingestor.ingest_file( self.entity_id,path,title=options["title"],package=options["domain"], From 7ba5a3a54bfd19f9f246f0118dc507a841f353f6 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:15:31 -0700 Subject: [PATCH 061/111] wallet: add Data Value Discovery surfaces --- tools/entity_wallet_desktop.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/tools/entity_wallet_desktop.py b/tools/entity_wallet_desktop.py index bd1dc1db..da590071 100644 --- a/tools/entity_wallet_desktop.py +++ b/tools/entity_wallet_desktop.py @@ -306,14 +306,17 @@ def _layout(self): self.tabs=ttk.Notebook(self); self.tabs.pack(fill="both",expand=True,padx=12,pady=(0,12)) self.assets_tab=ttk.Frame(self.tabs,padding=10); self.instruments_tab=ttk.Frame(self.tabs,padding=10) - self.market_tab=ttk.Frame(self.tabs,padding=10); self.positions_tab=ttk.Frame(self.tabs,padding=10); self.orders_tab=ttk.Frame(self.tabs,padding=10) + self.market_tab=ttk.Frame(self.tabs,padding=10); self.intel_tab=ttk.Frame(self.tabs,padding=10) + self.positions_tab=ttk.Frame(self.tabs,padding=10); self.orders_tab=ttk.Frame(self.tabs,padding=10) self.tabs.add(self.assets_tab,text="My Digital Assets"); self.tabs.add(self.instruments_tab,text="My Instruments") - self.tabs.add(self.market_tab,text="ENTITY Market"); self.tabs.add(self.positions_tab,text="Market Positions"); self.tabs.add(self.orders_tab,text="Orders") + self.tabs.add(self.market_tab,text="ENTITY Market"); self.tabs.add(self.intel_tab,text="Data Value Discovery") + self.tabs.add(self.positions_tab,text="Market Positions"); self.tabs.add(self.orders_tab,text="Orders") bar=ttk.Frame(self.assets_tab); bar.pack(fill="x",pady=(0,8)) ttk.Button(bar,text="Upload / Ingest Digital Asset",command=self.ingest_asset).pack(side="left") ttk.Button(bar,text="View Lineage",command=self.view_lineage).pack(side="left",padx=4) ttk.Button(bar,text="Create Economic Instrument",command=self.create_instrument).pack(side="left",padx=4) + ttk.Button(bar,text="Rights Demand",command=self.view_rights_demand).pack(side="left",padx=4) ttk.Label(bar,text="Asset creation never auto-issues or prices rights.",style="Sub.TLabel").pack(side="left",padx=12) self.assets=self._tree(self.assets_tab,["Title","Class","Type","Canonical lineage","Controller","Passport","BTDU","Object ID"],[210,145,90,330,160,100,55,250]) @@ -326,9 +329,23 @@ def _layout(self): ttk.Button(mbar,text="Buy",command=lambda:self.order("BUY")).pack(side="left") ttk.Button(mbar,text="Sell",command=lambda:self.order("SELL")).pack(side="left",padx=4) ttk.Button(mbar,text="Listing Information",command=self.view_listing).pack(side="left",padx=4) + ttk.Button(mbar,text="Market Intelligence",command=self.view_market_intelligence).pack(side="left",padx=4) ttk.Button(mbar,text="Export Portable Package",command=self.export_listing).pack(side="left",padx=4) ttk.Label(mbar,text="One canonical instrument can be published/listed on multiple venues.",style="Sub.TLabel").pack(side="left",padx=12) - self.market=self._tree(self.market_tab,["Market ID","Instrument Name","Issuer","Rights","Currency","Bid","Ask","Last","Venue"],[160,260,240,110,75,80,80,80,220]) + self.market=self._tree(self.market_tab,["Market ID","Instrument Name","Rights","Currency","Last","24h Vol","30d Vol","30d Buyers","Holders","Integrity","Venue"], + [155,245,105,70,75,85,85,85,70,180,200]) + + ib=ttk.Frame(self.intel_tab); ib.pack(fill="x",pady=(0,8)) + self.rollup_dimension=tk.StringVar(value="asset_class") + ttk.Label(ib,text="Roll up settled market activity by").pack(side="left") + self.rollup_box=ttk.Combobox(ib,textvariable=self.rollup_dimension,state="readonly",width=22, + values=["asset_class","asset_subtype","primary_domain","rights_class","issuer_entity_id","underlying_dco_id"]) + self.rollup_box.pack(side="left",padx=6); self.rollup_box.bind("<>",lambda e:self.refresh_intelligence()) + ttk.Button(ib,text="Refresh",command=self.refresh_intelligence).pack(side="left") + ttk.Label(ib,text="Notional remains separated by currency.",style="Sub.TLabel").pack(side="left",padx=12) + self.rollups=self._tree(self.intel_tab,["Group","Instruments","30d Trades","30d Units","30d Notional by Currency","Buyer Count*"], + [260,95,95,105,300,110]) + self.positions=self._tree(self.positions_tab,["Instrument","Units","Currency","Last","Bid","Ask","Indicative Value"],[320,90,80,90,90,90,140]) self.orders=self._tree(self.orders_tab,["Instrument","Side","Remaining","Limit","Status"],[340,80,100,100,120]) self.status=tk.StringVar(value="Ready"); ttk.Label(self,textvariable=self.status,relief="sunken",anchor="w",padding=5).pack(fill="x",side="bottom") From ebde906fe0105dd52ba77d5b9af609b3f11e8d39 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:16:00 -0700 Subject: [PATCH 062/111] wallet: show ticker metrics and economy rollups --- tools/entity_wallet_desktop.py | 55 ++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/tools/entity_wallet_desktop.py b/tools/entity_wallet_desktop.py index da590071..f38f7e9e 100644 --- a/tools/entity_wallet_desktop.py +++ b/tools/entity_wallet_desktop.py @@ -400,13 +400,62 @@ def refresh(self): for o in snap.get("orders",[]): self.orders.insert("", "end",values=(o["instrument_id"],o["side"],o["remaining"],o["limit_price"],o["status"])) self.clear(self.market) for i,row in enumerate(self.market_rows): - m=row["market"]; self.market.insert("", "end",iid=str(i),values=( - row["market_identifier"],row["instrument_name"],row["issuer_entity_id"],row["rights_class"],row["settlement_currency"], - m.get("bid"),m.get("ask"),m.get("last"),row["venue_id"])) + intel=row.get("intelligence") or {}; w24=intel.get("window_24h") or {}; w30=intel.get("window_30d") or {} + flags=(intel.get("market_integrity") or {}).get("flags") or [] + self.market.insert("", "end",iid=str(i),values=( + row["market_identifier"],row["instrument_name"],row["rights_class"],row["settlement_currency"], + intel.get("last_settled_price"),w24.get("volume_units"),w30.get("volume_units"),w30.get("unique_buyers"), + intel.get("active_holder_count"),", ".join(flags) if flags else "CLEAR",row["venue_id"])) + self.refresh_intelligence() self.status.set(f"{len(snap.get('assets',[]))} assets · {len(instruments)} issued instruments · {len(self.market_rows)} active listings · protocol tax 0 · crypto not required") except Exception as e: messagebox.showerror("Refresh failed",str(e)); self.status.set(str(e)) + def refresh_intelligence(self): + if not self.backend: return + try: + roll=self.backend.economy_rollup(self.rollup_dimension.get()) + self.clear(self.rollups) + for row in roll.get("rows",[]): + notional=", ".join(f"{cur} {amt:,}" for cur,amt in sorted(row.get("notional_30d_by_currency",{}).items())) or "—" + self.rollups.insert("", "end",values=(row["group"],row["instrument_count"],row["settled_trades_30d"], + row["volume_units_30d"],notional,row["sum_instrument_unique_buyers_30d"])) + except Exception as e: + self.status.set("Economic intelligence: "+str(e)) + + def view_market_intelligence(self): + row=self._selected_market_row() + if not row: messagebox.showwarning("Market Intelligence","Select a market listing."); return + try: + m=row.get("intelligence") or self.backend.intelligence.instrument_metrics(row["instrument_id"]) + w24=m["window_24h"]; w30=m["window_30d"]; roy=m["royalties_and_participation"] + rpaid=", ".join(f"{k} {v:,}" for k,v in sorted(roy["externally_verified_amounts_by_currency"].items())) or "None verified" + flags="\n".join("• "+x for x in m["market_integrity"]["flags"]) or "• No current integrity flags" + messagebox.showinfo("ENTITY Market Intelligence", + f"{m['market_identifier']}\n{m['instrument_name']}\n\n" + f"Last settled price: {m['last_settled_price']} {m['settlement_currency']}\n" + f"Last externally verified price: {m['last_externally_verified_price']} {m['settlement_currency']}\n\n" + f"24h settled volume: {w24['volume_units']:,} units / {w24['notional_amount_units']:,} {m['settlement_currency']}\n" + f"30d settled volume: {w30['volume_units']:,} units / {w30['notional_amount_units']:,} {m['settlement_currency']}\n" + f"30d high / low: {w30['high_price']} / {w30['low_price']}\n30d trades: {w30['trade_count']:,}\n" + f"Unique buyers: {w30['unique_buyers']:,}\nActive holders: {m['active_holder_count']:,}\n" + f"Open sell offers: {m['open_sell_offer_units']:,} units\nExternally verified royalties/participation: {rpaid}\n\n" + f"MARKET INTEGRITY\n{flags}\n\n" + "These observations value the traded rights under their terms; they do not prove intrinsic or accounting value of the underlying DCO.") + except Exception as e: messagebox.showerror("Market Intelligence",str(e)) + + def view_rights_demand(self): + if not self.backend: return + sel=self.assets.selection() + if not sel: messagebox.showwarning("Rights Demand","Select a digital asset."); return + try: + report=self.backend.rights_demand(sel[0]); lines=[] + for rights,row in report.get("rights_class_rollup",{}).items(): + notion=", ".join(f"{cur} {amt:,}" for cur,amt in sorted(row["notional_30d_by_currency"].items())) or "—" + lines.append(f"{rights}: {row['volume_units_30d']:,} units · {row['settled_trades']:,} trades · {notion}") + messagebox.showinfo("DCO Rights Demand","\n".join(lines) if lines else "No active economic instruments or settled demand for this DCO yet.") + except Exception as e: messagebox.showerror("Rights Demand",str(e)) + def view_lineage(self): sel=self.assets.selection() if not sel: messagebox.showwarning("Lineage","Select a digital asset."); return From b1a2fe39d47917c467c1c5465ee5506121a75c9e Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:17:35 -0700 Subject: [PATCH 063/111] wallet: align lineage display with sibling profile model --- tools/entity_wallet_desktop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/entity_wallet_desktop.py b/tools/entity_wallet_desktop.py index f38f7e9e..9e6e5386 100644 --- a/tools/entity_wallet_desktop.py +++ b/tools/entity_wallet_desktop.py @@ -461,7 +461,7 @@ def view_lineage(self): if not sel: messagebox.showwarning("Lineage","Select a digital asset."); return a=self.asset_rows.get(sel[0]); lin=(a or {}).get("lineage",{}); p=lin.get("protocol_lineage",{}); al=lin.get("asset_lineage",{}); cap=lin.get("capability_bindings",{}); gp=lin.get("global_passport") or {} parents="\n".join(f" {x.get('relation')}: {x.get('title') or x.get('parent_object_id')}" for x in al.get("parents",[])) or " None recorded" - messagebox.showinfo("ENTITY Lineage",f"CANONICAL PROTOCOL / DOMAIN LINEAGE\n{p.get('display_path') or 'Unresolved'}\nComposed profiles: {', '.join(p.get('composed_domain_profiles') or []) or 'None'}\n\nPassport: {gp.get('passport_id') or 'None'} version {gp.get('version') or '—'}\nOrigin embedded: {p.get('passport_protocol_origin_embedded')}\n\nASSET PROVENANCE\nController: {al.get('controller_name')}\nParents:\n{parents}\n\nSYSTEM BINDINGS\nBTDU bound: {cap.get('BTDU',{}).get('bound')}\nADAM: deterministic execution/evidence; not ownership\nNIKI: bounded reasoning; not ownership\n\nProtocol ancestry does not transfer ownership or create economic entitlement.") + messagebox.showinfo("ENTITY Lineage",f"CANONICAL PROTOCOL / DOMAIN LINEAGE\n{p.get('display_path') or 'Unresolved'}\nAssociated profiles: {', '.join(p.get('associated_profiles') or []) or 'None'}\n\nPassport: {gp.get('passport_id') or 'None'} version {gp.get('version') or '—'}\nOrigin embedded: {p.get('passport_protocol_origin_embedded')}\n\nASSET PROVENANCE\nController: {al.get('controller_name')}\nParents:\n{parents}\n\nSYSTEM BINDINGS\nBTDU bound: {cap.get('BTDU',{}).get('bound')}\nADAM: deterministic execution/evidence; not ownership\nNIKI: bounded reasoning; not ownership\n\nProtocol ancestry does not transfer ownership or create economic entitlement.") def ingest_asset(self): if not self.backend: return From 1638278854aee64559b7ebd33311cee5a887ff33 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:17:37 -0700 Subject: [PATCH 064/111] docs: define Data Value Discovery and market-integrity boundaries --- .../v3/ENTITY_ECONOMIC_MARKET_PROTOCOL.md | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/protocol/v3/ENTITY_ECONOMIC_MARKET_PROTOCOL.md b/protocol/v3/ENTITY_ECONOMIC_MARKET_PROTOCOL.md index 32434e50..4abbc627 100644 --- a/protocol/v3/ENTITY_ECONOMIC_MARKET_PROTOCOL.md +++ b/protocol/v3/ENTITY_ECONOMIC_MARKET_PROTOCOL.md @@ -389,3 +389,80 @@ External payment truth remains evidence-bound. ENTITY does not claim money moved 13. Cryptocurrency is not required. 14. Venue operators do not become protocol or asset authority. 15. Historical signed economic records are preserved when withdrawn/superseded. + + +## Data Value Discovery + +The canonical market identifier (for example `ACME:WX26-TRN`) is a human-readable measurement key. It is **not** the authoritative database key. All analytics resolve to the immutable `instrument_id`. + +ENTITY may measure actual economic demand for each instrument from settled market activity: + +- last settled price; +- 24-hour and 30-day settled volume; +- settled notional by currency; +- trade count; +- unique buyers and sellers; +- high/low settled price; +- active holder count; +- issuer inventory and open sell offers; +- settled and externally verified royalty/participation obligations; +- market-integrity indicators. + +Open bids and asks are liquidity observations. They do **not** become price history, volume, realized demand or value merely because they exist. + +For a DCO with several rights instruments, ENTITY keeps each right economically distinct: + +```text +ACME:WX26-ACC Access rights +ACME:WX26-QRY Query rights +ACME:WX26-TRN AI training rights +ACME:WX26-COM Commercial-use rights +``` + +This allows ENTITY to observe whether demand is concentrated in access, query, training, commercial, derivative or other rights without assigning one arbitrary price to the underlying DCO. + +### Rollups + +Observed activity may roll upward through: + +```text +instrument + ↓ +underlying DCO + ↓ +asset class / subtype + ↓ +primary domain / industry + ↓ +ENTITY economy +``` + +Rollups preserve currency boundaries. CAD, USD and other quote units are never summed into one synthetic value without a separately evidenced conversion method. + +BTDU bindings may enrich semantic rollups across related assets, but BTDU topology does not create economic value, ownership or rights. + +### Market-integrity safeguards + +Raw volume is not proof of value. Economic intelligence therefore separates or flags: + +- self-trade attempts; +- reciprocal counterparty flows; +- concentrated counterparty activity; +- unsettled executions; +- externally unverified settlement; +- related-wallet relationships only when explicit authorized relationship evidence exists. + +ENTITY must not infer hidden relationships between wallets merely to strengthen a market metric. + +### Valuation boundary + +A settled price for a rights instrument means the market exchanged **that defined right under those terms** at that price. + +It does not prove: + +- intrinsic value of the underlying DCO; +- accounting fair value; +- enterprise value of the issuer; +- a universal price for all rights associated with the asset. + +Data Value Discovery is therefore market evidence, not protocol-created value. From 77fd45516b83a8fa88edb38e3c2884b550415010 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:18:16 -0700 Subject: [PATCH 065/111] dco factory: fix issuance withdrawal evidence validation --- src/43_DCO_Factory/canonical_dco_factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/43_DCO_Factory/canonical_dco_factory.py b/src/43_DCO_Factory/canonical_dco_factory.py index adbe330b..a4bdf9b8 100644 --- a/src/43_DCO_Factory/canonical_dco_factory.py +++ b/src/43_DCO_Factory/canonical_dco_factory.py @@ -231,7 +231,7 @@ def record_issuance(self,dco_id:str,plan:dict,status="QUALIFIED")->dict: def withdraw_issuance(self,issuance_id:str,reason:str,*,evidence_sha256:str|None=None)->dict: issuance_id=str(issuance_id); reason=str(reason).strip() if not reason: raise ValueError("withdrawal reason required") - evidence=sha256_hex(evidence_sha256,"evidence_sha256") if evidence_sha256 is not None else None + evidence=require_sha256(evidence_sha256,"evidence_sha256") if evidence_sha256 is not None else None with self._db(True) as db: row=db.execute("SELECT * FROM issuance_runs WHERE issuance_id=?",(issuance_id,)).fetchone() if not row: raise KeyError("issuance run not found") From ae8e2aec0b617cb96f142a1a882c6aa99dca4a99 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:19:44 -0700 Subject: [PATCH 066/111] tests: qualify append-only instrument and issuance withdrawal --- tests/test_instrument_withdrawal.py | 62 +++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/test_instrument_withdrawal.py diff --git a/tests/test_instrument_withdrawal.py b/tests/test_instrument_withdrawal.py new file mode 100644 index 00000000..93c7a3cc --- /dev/null +++ b/tests/test_instrument_withdrawal.py @@ -0,0 +1,62 @@ +from __future__ import annotations +from pathlib import Path +import importlib.util,sys,tempfile,sqlite3 + +ROOT=Path(__file__).resolve().parents[1] +def load(name,rel): + p=ROOT/rel; spec=importlib.util.spec_from_file_location(name,p) + m=importlib.util.module_from_spec(spec); sys.modules[name]=m; spec.loader.exec_module(m); return m + +def test_signed_instrument_withdrawal_preserves_history_and_deactivates_listing(): + ident=load("withdraw_identity","src/01_Core_Runtime/identity/canonical_identity.py") + fabmod=load("withdraw_fabric","src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py") + exmod=load("withdraw_exchange","src/31_Profiles/exchange_protocol.py") + with tempfile.TemporaryDirectory() as td: + state=Path(td); vault=ident.EntityIdentityVault(state) + issuer=vault.create("Issuer","organization")["entity_id"] + venue_owner=vault.create("Venue","organization")["entity_id"] + fabric=fabmod.UniversalTransactionFabric(state,vault) + ex=exmod.ExchangeProtocol(state,vault,fabric) + obj=fabric.register_digital_commodity(issuer,"Asset","a"*64, + commodity_class="ROBOTICS_ASSET",measurement_unit="ASSET",object_type="SOFTWARE") + venue=ex.create_venue(venue_owner,"Market","CA",["ORDER_BOOK"],"b"*64) + inst=ex.define_instrument(issuer,obj["object_id"],"SPOT_LICENSE",{"actions":["EXECUTE"]},100,"CAD") + disc=ex.publish_disclosure(venue["venue_id"],inst["instrument_id"],issuer,"LISTING_INFORMATION","c"*64) + listing=ex.list_instrument(venue["venue_id"],inst["instrument_id"],issuer, + disclosure_sha256=disc["content_sha256"]) + result=ex.withdraw_instrument(issuer,inst["instrument_id"],"SUPERSEDED_PRELAUNCH_MODEL",evidence_sha256="d"*64) + assert result["new_status"]=="WITHDRAWN" + status=ex.instrument_status(inst["instrument_id"]) + assert status["status"]=="WITHDRAWN" + assert status["withdrawal"]["reason"]=="SUPERSEDED_PRELAUNCH_MODEL" + db=sqlite3.connect(state/"entity_v3_exchange.sqlite"); db.row_factory=sqlite3.Row + try: + assert db.execute("SELECT COUNT(*) n FROM instruments WHERE instrument_id=?",(inst["instrument_id"],)).fetchone()["n"]==1 + assert db.execute("SELECT status FROM listings WHERE listing_id=?",(listing["listing_id"],)).fetchone()["status"]=="WITHDRAWN" + assert db.execute("SELECT COUNT(*) n FROM instrument_withdrawals WHERE instrument_id=?",(inst["instrument_id"],)).fetchone()["n"]==1 + finally: db.close() + try: + ex.submit_order(venue["venue_id"],inst["instrument_id"],issuer,"SELL",1,1,nonce="after-withdraw") + assert False,"withdrawn instrument must not accept orders" + except KeyError: + pass + +def test_dco_factory_withdrawal_stops_counting_qualified_issuance_without_deleting_history(): + mod=load("withdraw_factory","src/43_DCO_Factory/canonical_dco_factory.py") + with tempfile.TemporaryDirectory() as td: + f=mod.DCOFactory(td) + plan={"plan_sha256":"a"*64,"physical_instrument_count":20,"total_right_units":1000} + run=f.record_issuance("DCO-TEST",plan,status="QUALIFIED") + before=f.portfolio_summary() + assert before["qualified_physical_instruments"]==20 + w=f.withdraw_issuance(run["issuance_id"],"SUPERSEDED_ASSET_FIRST_ALIGNMENT",evidence_sha256="b"*64) + assert w["history_preserved"] is True + after=f.portfolio_summary() + assert after["qualified_physical_instruments"]==0 + assert after["qualified_right_units"]==0 + assert after["withdrawn_issuance_runs"]==1 + db=sqlite3.connect(Path(td)/"dco_factory"/"entity_dco_factory.sqlite") + try: + assert db.execute("SELECT COUNT(*) FROM issuance_runs").fetchone()[0]==1 + assert db.execute("SELECT COUNT(*) FROM issuance_withdrawals").fetchone()[0]==1 + finally: db.close() From 5d81e21ef9b91ef9b41d28622e6671eb7dfa95d8 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:20:33 -0700 Subject: [PATCH 067/111] economy: keep EEP 3.0 wire protocol unchanged --- src/31_Profiles/exchange_protocol.py | 80 +++------------------------- 1 file changed, 6 insertions(+), 74 deletions(-) diff --git a/src/31_Profiles/exchange_protocol.py b/src/31_Profiles/exchange_protocol.py index 1a03b154..8121df30 100644 --- a/src/31_Profiles/exchange_protocol.py +++ b/src/31_Profiles/exchange_protocol.py @@ -46,16 +46,7 @@ def sha256(name): require_sha256(body.get(name)) actions=rights.get('actions') if type(actions) is not list or not actions or any(type(a) is not str or not a or a!=a.upper() for a in actions): raise ValueError('rights.actions must be non-empty canonical uppercase strings') if actions!=sorted(set(actions)): raise ValueError('rights.actions must be sorted and unique') - elif schema=='entity-eep-instrument-withdrawal-v1': - for n in ('withdrawal_id','instrument_id','issuer','reason'): sid(n) - common_time() - if body.get('prior_status')!='ACTIVE' or body.get('new_status')!='WITHDRAWN': - raise ValueError('invalid instrument withdrawal state transition') - evidence=body.get('evidence_sha256') - if evidence is not None: require_sha256(evidence) - if body.get('no_new_rights_created') is not True: - raise ValueError('withdrawal rights-boundary flag required') - elif schema=='entity-eep-disclosure-v1': + elif schema=='entity-eep-disclosure-v1': for n in ('disclosure_id','venue_id','instrument_id','publisher'): sid(n) sup('disclosure_type'); sha256('content_sha256'); common_time() if body.get('protected_data_included') is not False: raise ValueError('protected_data_included must be false') @@ -137,14 +128,9 @@ def _init(self): transferable INTEGER NOT NULL, duration_ms INTEGER, settlement_currency TEXT NOT NULL, delivery_mode TEXT NOT NULL, status TEXT NOT NULL, created_at_ms INTEGER NOT NULL, signature_json TEXT NOT NULL); - CREATE TABLE IF NOT EXISTS balances( - instrument_id TEXT NOT NULL, holder TEXT NOT NULL, units INTEGER NOT NULL, - PRIMARY KEY(instrument_id,holder)); - CREATE TABLE IF NOT EXISTS instrument_withdrawals( - withdrawal_id TEXT PRIMARY KEY, instrument_id TEXT NOT NULL UNIQUE, - issuer TEXT NOT NULL, prior_status TEXT NOT NULL, new_status TEXT NOT NULL, - reason TEXT NOT NULL, evidence_sha256 TEXT, created_at_ms INTEGER NOT NULL, - signature_json TEXT NOT NULL); + CREATE TABLE IF NOT EXISTS balances( + instrument_id TEXT NOT NULL, holder TEXT NOT NULL, units INTEGER NOT NULL, + PRIMARY KEY(instrument_id,holder)); CREATE TABLE IF NOT EXISTS listings( listing_id TEXT PRIMARY KEY, venue_id TEXT NOT NULL, instrument_id TEXT NOT NULL, lister TEXT NOT NULL, min_lot INTEGER NOT NULL, tick_size INTEGER NOT NULL, @@ -297,61 +283,7 @@ def define_instrument(self, issuer: str, underlying_object_id: str, instrument_c sig=self.identity.sign(issuer,body) return self._record_signed_instrument(body,sig) - def instrument_status(self, instrument_id: str) -> dict: - with self._db() as db: - inst=db.execute("SELECT instrument_id,issuer,status,created_at_ms FROM instruments WHERE instrument_id=?",(str(instrument_id),)).fetchone() - if not inst: raise KeyError("instrument not found") - withdrawal=db.execute("SELECT * FROM instrument_withdrawals WHERE instrument_id=?",(str(instrument_id),)).fetchone() - return { - "instrument_id":str(instrument_id), - "issuer":inst["issuer"], - "status":inst["status"], - "issued_at_ms":int(inst["created_at_ms"]), - "withdrawal":dict(withdrawal) | {"signature":json.loads(withdrawal["signature_json"])} if withdrawal else None, - } - - def _record_signed_instrument_withdrawal(self, body: dict, signature: dict) -> dict: - required={"schema","withdrawal_id","instrument_id","issuer","prior_status","new_status", - "reason","evidence_sha256","created_at_ms","no_new_rights_created"} - validate_signed_wire(body) - if set(body)!=required or body.get("schema")!="entity-eep-instrument-withdrawal-v1": - raise ValueError("invalid signed instrument withdrawal shape") - issuer=str(body["issuer"]); instrument_id=str(body["instrument_id"]) - manifest=self.identity.load_manifest(issuer) - if not self.identity.verify_signature(manifest,body,signature): - raise PermissionError("instrument withdrawal signature invalid") - with self._db() as db: - db.execute("BEGIN IMMEDIATE") - inst=db.execute("SELECT * FROM instruments WHERE instrument_id=?",(instrument_id,)).fetchone() - if not inst: raise KeyError("instrument not found") - if inst["issuer"]!=issuer: raise PermissionError("instrument issuer required") - if inst["status"]!="ACTIVE": raise ValueError("active instrument required") - if db.execute("SELECT 1 FROM orders WHERE instrument_id=? AND status IN ('OPEN','PARTIAL') LIMIT 1",(instrument_id,)).fetchone(): - raise ValueError("cannot withdraw instrument with open orders") - if db.execute("SELECT 1 FROM trades WHERE instrument_id=? AND status='EXECUTED_UNSETTLED' LIMIT 1",(instrument_id,)).fetchone(): - raise ValueError("cannot withdraw instrument with unsettled trades") - if db.execute("SELECT 1 FROM rfqs WHERE instrument_id=? AND status='OPEN' LIMIT 1",(instrument_id,)).fetchone(): - raise ValueError("cannot withdraw instrument with open RFQs") - db.execute("INSERT INTO instrument_withdrawals VALUES(?,?,?,?,?,?,?,?,?)",( - body["withdrawal_id"],instrument_id,issuer,body["prior_status"],body["new_status"], - body["reason"],body["evidence_sha256"],body["created_at_ms"],json.dumps(signature,sort_keys=True))) - db.execute("UPDATE instruments SET status='WITHDRAWN' WHERE instrument_id=?",(instrument_id,)) - db.execute("UPDATE listings SET status='WITHDRAWN' WHERE instrument_id=? AND status='ACTIVE'",(instrument_id,)) - return dict(body,signature=signature) - - def submit_signed_instrument_withdrawal(self, withdrawal: dict, signature: dict) -> dict: - return self._record_signed_instrument_withdrawal(dict(withdrawal),dict(signature)) - - def withdraw_instrument(self, issuer: str, instrument_id: str, reason: str, *, evidence_sha256: str | None=None) -> dict: - evidence=require_sha256(evidence_sha256) if evidence_sha256 is not None else None - body={"schema":"entity-eep-instrument-withdrawal-v1","withdrawal_id":rid("withdraw3"), - "instrument_id":str(instrument_id),"issuer":str(issuer),"prior_status":"ACTIVE", - "new_status":"WITHDRAWN","reason":str(reason),"evidence_sha256":evidence, - "created_at_ms":now_ms(),"no_new_rights_created":True} - sig=self.identity.sign(str(issuer),body) - return self._record_signed_instrument_withdrawal(body,sig) - - def balance(self, instrument_id: str, holder: str) -> int: + def balance(self, instrument_id: str, holder: str) -> int: with self._db() as db: row = db.execute("SELECT units FROM balances WHERE instrument_id=? AND holder=?", (instrument_id, holder)).fetchone() @@ -980,7 +912,7 @@ def surveillance_alerts(self, venue_id: str) -> list[dict]: def status(self) -> dict: with self._db() as db: - tables=("venues","instruments","listings","orders","order_cancellations","trades","clearing","payment_attestations","settlement_verifiers","entitlements","usage","surveillance","rfqs","quotes","rfq_acceptances","rfq_trade_sources","revenue_rule_sets","trade_revenue_bindings","instrument_withdrawals") + tables=("venues","instruments","listings","orders","order_cancellations","trades","clearing","payment_attestations","settlement_verifiers","entitlements","usage","surveillance","rfqs","quotes","rfq_acceptances","rfq_trade_sources","revenue_rule_sets","trade_revenue_bindings") counts={name:int(db.execute(f"SELECT COUNT(*) FROM {name}").fetchone()[0]) for name in tables} return {"schema":"entity-eep-status-v1","profile":"ENTITY_EXCHANGE_PROTOCOL","version":"3.0.0", "rights_are_traded_not_bytes":True,"venue_neutral":True,"payment_versus_right_transfer":True, From 1f77b880f27f411423b08eb93e68ca1cb41fdad7 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:21:03 -0700 Subject: [PATCH 068/111] tests: keep universal market qualification EEP-3 compatible --- tests/test_entity_market_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_entity_market_registry.py b/tests/test_entity_market_registry.py index 245b2bcd..21a6169c 100644 --- a/tests/test_entity_market_registry.py +++ b/tests/test_entity_market_registry.py @@ -71,7 +71,7 @@ def test_multi_issuer_symbols_canonical_ids_listing_sheet_and_portable_package() assert listing["information"]["sheet_does_not_replace_rights_passport"] is True assert "ownership of the model" in listing["information"]["buyer_does_not_receive"] assert listing["machine_manifest"]["instrument_id"]==ia["instrument_id"] - assert ex.instrument_status(ia["instrument_id"])["status"]=="ACTIVE" + assert ia["status"]=="ACTIVE" pkg=reg.export_portable_package(listing["listing_id"],state/"portable") required={"LISTING_INFORMATION.pdf","LISTING_INFORMATION.md","entity-instrument.json", From 43564f0f33ec9f165ecb165f984a82b9c86836dd Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:23:09 -0700 Subject: [PATCH 069/111] conformance: reseal EEP manifest for exchange protocol --- ENTITY_V3_EEP_CONFORMANCE_MANIFEST.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ENTITY_V3_EEP_CONFORMANCE_MANIFEST.json b/ENTITY_V3_EEP_CONFORMANCE_MANIFEST.json index 0a1f4ca1..9a426aba 100644 --- a/ENTITY_V3_EEP_CONFORMANCE_MANIFEST.json +++ b/ENTITY_V3_EEP_CONFORMANCE_MANIFEST.json @@ -34,7 +34,7 @@ "files": [ { "path": "src/31_Profiles/exchange_protocol.py", - "sha256": "53761f15c7ae58726ce8cd01a28164d3a14ae29aac38a89859c408fc3c76b14a" + "sha256": "5615e0088438871da83e102de03f93ea944b42eb136d49fd7f3a09802aea6afb" }, { "path": "src/32_V3_Hardening/exchange_protocol.py", @@ -99,7 +99,7 @@ "RFQ executions preserve a deterministic venue-signed source binding to signed RFQ, quote and acceptance records" ], "schema": "entity-v3-eep-conformance-manifest-v1", - "snapshot_sha256": "65224aa97bde5456c4079097a37284ce6e023f3e1c635f25456683c74e3df91a", + "snapshot_sha256": "1215be351da52135cab65bd2883be238ff3affa7baa8d244ce2aa34c946a4127", "status": "BTG_INTERNAL_QUALIFIED_RELEASE_COMPONENT", "version": "3.0.0", "working_tree_snapshot": false From 981759955df176b81de2916518fec5a8b3ac5761 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:23:21 -0700 Subject: [PATCH 070/111] wallet ingest: allow explicit same-content DCO version corrections --- src/42_ENTITY_Wallet/asset_ingest.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/42_ENTITY_Wallet/asset_ingest.py b/src/42_ENTITY_Wallet/asset_ingest.py index 0f11a5ae..b7c20ee0 100644 --- a/src/42_ENTITY_Wallet/asset_ingest.py +++ b/src/42_ENTITY_Wallet/asset_ingest.py @@ -80,8 +80,12 @@ def ingest_file(self,controller_entity_id:str,file_path:str|Path,*,title:str|Non for chunk in iter(lambda:f.read(1024*1024),b""): digest.update(chunk) source_sha=digest.hexdigest() existing=self._existing_dco(controller_entity_id,source_sha) - if existing: + if existing and str(previous_object_id or "")!=str(existing): raise ValueError(f"identical Digital Commodity Object already registered: {existing}") + if existing and str(previous_object_id or "")==str(existing): + # Explicit version/supersession path. Same bytes may be re-registered only + # when the caller names the exact previous DCO being corrected/versioned. + pass identity,fabric,profiles,origin,passports,packages,sdk,origin_status=self.cli.runtime( self.state,require_current_release=True) release=origin.passport_binding("entity-release:v3.4.3") @@ -106,6 +110,8 @@ def ingest_file(self,controller_entity_id:str,file_path:str|Path,*,title:str|Non "controller_asserted_registration_authority":True, "canonical_protocol_lineage_required":True, "profile_domain":str(package).lower(), + "explicit_previous_object_id":str(previous_object_id) if previous_object_id else None, + "same_content_version_correction":bool(existing and previous_object_id and str(existing)==str(previous_object_id)), **dict(metadata or {}), } package=str(package or "general").lower() From 7635aeb22a7d197f8b77aacf84b391d10f4ef149 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:23:24 -0700 Subject: [PATCH 071/111] wallet: show latest DCO versions while preserving historical provenance --- src/42_ENTITY_Wallet/canonical_wallet.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/42_ENTITY_Wallet/canonical_wallet.py b/src/42_ENTITY_Wallet/canonical_wallet.py index 1fe63596..3535e029 100644 --- a/src/42_ENTITY_Wallet/canonical_wallet.py +++ b/src/42_ENTITY_Wallet/canonical_wallet.py @@ -136,8 +136,15 @@ def _assets(self,entity_id:str)->list[dict]: descriptor_json,status,created_at_ms FROM objects WHERE controller_entity_id=? AND status='ACTIVE' ORDER BY created_at_ms DESC,object_id""",(entity_id,)).fetchall() + version_parents={r["parent_object_id"] for r in db.execute( + """SELECT p.parent_object_id FROM provenance_edges p + JOIN objects c ON c.object_id=p.child_object_id + WHERE p.relation='VERSION_DERIVED_FROM' + AND c.controller_entity_id=? AND c.status='ACTIVE'""",(entity_id,)).fetchall()} out=[] for row in rows: + if row["object_id"] in version_parents: + continue d=dict(row); descriptor=json.loads(d.pop("descriptor_json") or "{}") if descriptor.get("digital_commodity") is not True: continue d["descriptor"]=descriptor From 566be2067896ad958e563973b07a3550479b6d02 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:23:50 -0700 Subject: [PATCH 072/111] economy: add fail-closed unused prelaunch withdrawal migration --- src/45_ENTITY_Market/prelaunch_withdrawal.py | 162 +++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 src/45_ENTITY_Market/prelaunch_withdrawal.py diff --git a/src/45_ENTITY_Market/prelaunch_withdrawal.py b/src/45_ENTITY_Market/prelaunch_withdrawal.py new file mode 100644 index 00000000..137f44a9 --- /dev/null +++ b/src/45_ENTITY_Market/prelaunch_withdrawal.py @@ -0,0 +1,162 @@ +from __future__ import annotations +from pathlib import Path +from typing import Iterable +import hashlib,json,shutil,sqlite3,time + +def now_ms()->int: return int(time.time()*1000) +def canon(v)->bytes: return json.dumps(v,sort_keys=True,separators=(",",":"),ensure_ascii=False,default=str).encode() +def sha(v)->str: return hashlib.sha256(v if isinstance(v,(bytes,bytearray)) else canon(v)).hexdigest() + +class PrelaunchEconomicWithdrawal: + """Fail-closed application migration for unused prelaunch economic experiments. + + This does not add an EEP wire primitive or rewrite historical signed records. + It is only valid when the targeted instruments have never entered real market + activity and all non-issuer balances are proven internal treasury reserves. + """ + + def __init__(self,state_dir:str|Path,identity): + self.state=Path(state_dir); self.identity=identity + self.eep=self.state/"entity_v3_exchange.sqlite" + self.eopp=self.state/"entity_v3_economic_participation.sqlite" + self.factory=self.state/"dco_factory"/"entity_dco_factory.sqlite" + self.audit_dir=self.state/"economic_migrations"; self.audit_dir.mkdir(parents=True,exist_ok=True) + + @staticmethod + def _db(path:Path): + db=sqlite3.connect(path); db.row_factory=sqlite3.Row; return db + + def audit(self,object_ids:Iterable[str],dco_ids:Iterable[str]=())->dict: + object_ids=sorted({str(x) for x in object_ids}); dco_ids=sorted({str(x) for x in dco_ids}) + if not object_ids: raise ValueError("at least one underlying object_id required") + if not self.eep.exists(): raise FileNotFoundError(self.eep) + db=self._db(self.eep) + try: + marks=",".join("?" for _ in object_ids) + instruments=[dict(r) for r in db.execute( + f"SELECT * FROM instruments WHERE underlying_object_id IN ({marks}) ORDER BY instrument_id",object_ids)] + if not instruments: raise ValueError("no economic instruments found for selected objects") + ids=[r["instrument_id"] for r in instruments]; q=",".join("?" for _ in ids) + activity={} + for table in ("orders","trades","entitlements","usage"): + activity[table]=int(db.execute(f"SELECT COUNT(*) FROM {table} WHERE instrument_id IN ({q})",ids).fetchone()[0]) + trade_ids=[r["trade_id"] for r in db.execute(f"SELECT trade_id FROM trades WHERE instrument_id IN ({q})",ids)] + activity["clearing"]=0 if not trade_ids else int(db.execute( + f"SELECT COUNT(*) FROM clearing WHERE trade_id IN ({','.join('?' for _ in trade_ids)})",trade_ids).fetchone()[0]) + balances=[dict(r) for r in db.execute( + f"""SELECT b.instrument_id,b.holder,b.units,i.issuer + FROM balances b JOIN instruments i ON i.instrument_id=b.instrument_id + WHERE b.instrument_id IN ({q}) AND b.units>0 ORDER BY b.instrument_id,b.holder""",ids)] + listings=int(db.execute(f"SELECT COUNT(*) FROM listings WHERE instrument_id IN ({q}) AND status='ACTIVE'",ids).fetchone()[0]) + finally: db.close() + + reserve_by_instrument={}; economic_events=0 + if self.eopp.exists(): + ep=self._db(self.eopp) + try: + iq=",".join("?" for _ in ids) + economic_events=int(ep.execute( + f"SELECT COUNT(*) FROM economic_events WHERE instrument_id IN ({iq})",ids).fetchone()[0]) + rows=ep.execute(f"""SELECT r.instrument_id,r.units,r.treasury_entity_id,t.owner_entity_id + FROM reserve_allocations r JOIN treasuries t ON t.treasury_id=r.treasury_id + WHERE r.instrument_id IN ({iq})""",ids).fetchall() + reserve_by_instrument={r["instrument_id"]:dict(r) for r in rows} + finally: ep.close() + + external=[] + for b in balances: + if b["holder"]==b["issuer"]: continue + reserve=reserve_by_instrument.get(b["instrument_id"]) + if not reserve or reserve["owner_entity_id"]!=b["issuer"] or reserve["treasury_entity_id"]!=b["holder"] or int(reserve["units"])!=int(b["units"]): + external.append(b) + safe=not any(activity.values()) and economic_events==0 and not external + return { + "schema":"entity-prelaunch-economic-withdrawal-audit-v1", + "object_ids":object_ids,"dco_ids":dco_ids, + "instrument_ids":ids,"instrument_count":len(ids),"active_listing_count":listings, + "activity_counts":activity,"economic_event_count":economic_events, + "positive_balances":balances,"internal_reserves":reserve_by_instrument, + "external_or_unexplained_balances":external, + "safe_to_withdraw":safe, + "requires_zero_market_history":True, + "history_rewrite_prohibited":True, + } + + def apply(self,object_ids:Iterable[str],dco_ids:Iterable[str],reason:str)->dict: + reason=str(reason).strip() + if not reason: raise ValueError("withdrawal reason required") + audit=self.audit(object_ids,dco_ids) + if not audit["safe_to_withdraw"]: + raise RuntimeError("prelaunch withdrawal refused: economic activity or external holdings exist") + ids=audit["instrument_ids"]; issuers=sorted({b["issuer"] for b in audit["positive_balances"]}) + if not issuers: + db=self._db(self.eep) + try: + q=",".join("?" for _ in ids) + issuers=sorted({r["issuer"] for r in db.execute(f"SELECT issuer FROM instruments WHERE instrument_id IN ({q})",ids)}) + finally: db.close() + for issuer in issuers: self.identity.load_manifest(issuer) + + created=now_ms() + body={"schema":"entity-prelaunch-economic-withdrawal-v1","reason":reason, + "object_ids":audit["object_ids"],"dco_ids":audit["dco_ids"],"instrument_ids":ids, + "audit_sha256":sha(audit),"created_at_ms":created, + "no_orders":audit["activity_counts"]["orders"]==0, + "no_trades":audit["activity_counts"]["trades"]==0, + "no_entitlements":audit["activity_counts"]["entitlements"]==0, + "no_usage":audit["activity_counts"]["usage"]==0, + "no_economic_events":audit["economic_event_count"]==0, + "no_external_holders":not audit["external_or_unexplained_balances"], + "eep_wire_protocol_changed":False,"historical_records_deleted":False} + evidence=sha(body) + signatures=[self.identity.sign(issuer,body) for issuer in issuers] + + backups={} + for p in (self.eep,self.eopp,self.factory): + if p.exists(): + dst=self.audit_dir/f"{p.name}.{created}.bak"; shutil.copy2(p,dst); backups[str(p)]=str(dst) + try: + db=self._db(self.eep) + db.execute("BEGIN IMMEDIATE") + q=",".join("?" for _ in ids) + db.execute(f"UPDATE instruments SET status='WITHDRAWN' WHERE instrument_id IN ({q}) AND status='ACTIVE'",ids) + db.execute(f"UPDATE listings SET status='WITHDRAWN' WHERE instrument_id IN ({q}) AND status='ACTIVE'",ids) + for iid,reserve in audit["internal_reserves"].items(): + issuer=next(r["issuer"] for r in audit["positive_balances"] if r["instrument_id"]==iid) + holder=reserve["treasury_entity_id"]; units=int(reserve["units"]) + db.execute("UPDATE balances SET units=units-? WHERE instrument_id=? AND holder=?",(units,iid,holder)) + db.execute("""INSERT INTO balances(instrument_id,holder,units) VALUES(?,?,?) + ON CONFLICT(instrument_id,holder) DO UPDATE SET units=units+excluded.units""",(iid,issuer,units)) + db.commit(); db.close() + + if self.eopp.exists(): + ep=self._db(self.eopp); ep.execute("BEGIN IMMEDIATE") + iq=",".join("?" for _ in ids) + ep.execute(f"UPDATE participation_policies SET status='SUPERSEDED' WHERE instrument_id IN ({iq}) AND status='ACTIVE'",ids) + ep.commit(); ep.close() + + if self.factory.exists() and audit["dco_ids"]: + fd=self._db(self.factory); fd.execute("BEGIN IMMEDIATE") + for dco in audit["dco_ids"]: + runs=fd.execute("SELECT issuance_id FROM issuance_runs WHERE dco_id=?",(dco,)).fetchall() + for run in runs: + fd.execute("""INSERT OR IGNORE INTO issuance_withdrawals + (issuance_id,dco_id,reason,evidence_sha256,created_at_ms) + VALUES(?,?,?,?,?)""",(run["issuance_id"],dco,reason,evidence,created)) + row=fd.execute("SELECT master_json FROM dco_masters WHERE dco_id=?",(dco,)).fetchone() + if row: + master=json.loads(row["master_json"]); master["lifecycle"]="ACTIVE" + meta=dict(master.get("metadata") or {}); meta["prelaunch_economic_issuance_withdrawn"]=True + meta["withdrawal_evidence_sha256"]=evidence; master["metadata"]=meta + fd.execute("UPDATE dco_masters SET lifecycle='ACTIVE',master_json=? WHERE dco_id=?", + (json.dumps(master,sort_keys=True,separators=(",",":")),dco)) + fd.commit(); fd.close() + except Exception: + for original,backup in backups.items(): shutil.copy2(backup,original) + raise + + receipt={**body,"evidence_sha256":evidence,"issuer_signatures":signatures, + "backup_files":backups,"status":"WITHDRAWN_UNUSED_PRELAUNCH_ECONOMICS"} + path=self.audit_dir/f"prelaunch-economic-withdrawal-{created}.json" + path.write_text(json.dumps(receipt,indent=2,sort_keys=True),encoding="utf-8") + return {**receipt,"receipt_path":str(path)} From fb8779853a7422ab676fdc113561cf8d7259fc98 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:25:09 -0700 Subject: [PATCH 073/111] conformance: restore EEP seal after exchange baseline restoration --- ENTITY_V3_EEP_CONFORMANCE_MANIFEST.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ENTITY_V3_EEP_CONFORMANCE_MANIFEST.json b/ENTITY_V3_EEP_CONFORMANCE_MANIFEST.json index 9a426aba..0a1f4ca1 100644 --- a/ENTITY_V3_EEP_CONFORMANCE_MANIFEST.json +++ b/ENTITY_V3_EEP_CONFORMANCE_MANIFEST.json @@ -34,7 +34,7 @@ "files": [ { "path": "src/31_Profiles/exchange_protocol.py", - "sha256": "5615e0088438871da83e102de03f93ea944b42eb136d49fd7f3a09802aea6afb" + "sha256": "53761f15c7ae58726ce8cd01a28164d3a14ae29aac38a89859c408fc3c76b14a" }, { "path": "src/32_V3_Hardening/exchange_protocol.py", @@ -99,7 +99,7 @@ "RFQ executions preserve a deterministic venue-signed source binding to signed RFQ, quote and acceptance records" ], "schema": "entity-v3-eep-conformance-manifest-v1", - "snapshot_sha256": "1215be351da52135cab65bd2883be238ff3affa7baa8d244ce2aa34c946a4127", + "snapshot_sha256": "65224aa97bde5456c4079097a37284ce6e023f3e1c635f25456683c74e3df91a", "status": "BTG_INTERNAL_QUALIFIED_RELEASE_COMPONENT", "version": "3.0.0", "working_tree_snapshot": false From 9a9cb50c13b18b773545b03f084fa6829f7427d5 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:26:43 -0700 Subject: [PATCH 074/111] sdk: allow explicit sibling profiles and controller rights in package ingest --- .../canonical_global_passport_sdk.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/sdk/global_passport_sdk/canonical_global_passport_sdk.py b/sdk/global_passport_sdk/canonical_global_passport_sdk.py index 0717351b..0cec948f 100644 --- a/sdk/global_passport_sdk/canonical_global_passport_sdk.py +++ b/sdk/global_passport_sdk/canonical_global_passport_sdk.py @@ -52,12 +52,18 @@ def ingest_package_file(self,path,controller_entity_id:str,package:str,config:di plan=self.package_plan(package,config,asset_kind) options=dict(kwargs) options.setdefault("object_type",plan["object_type"]) + extra_profiles=list(options.pop("additional_profile_refs",[]) or []) + profile_stack=self.profiles.resolve_stack(list(plan["profile_refs"])+extra_profiles) + extra_actions={str(x).upper() for x in (options.pop("additional_rights_actions",[]) or []) if str(x)} + rights_actions=sorted(set(plan["rights_actions"])|extra_actions) context=dict(options.pop("industry_context",{}) or {}) context.update({"industry_package":plan["package"],"asset_kind":str(asset_kind), - "configuration_sha256":plan["configuration_sha256"]}) + "configuration_sha256":plan["configuration_sha256"], + "primary_domain_profile":next((x for x in plan["profile_refs"] if "entity-profile:global@" not in x),None), + "associated_profile_refs":[x for x in profile_stack["profile_refs"] if x not in plan["profile_refs"]]}) options["industry_context"]=context - result=self.ingestion.ingest_file(path,controller_entity_id,plan["profile_refs"], - rights_actions=plan["rights_actions"],**options) + result=self.ingestion.ingest_file(path,controller_entity_id,profile_stack["profile_refs"], + rights_actions=rights_actions,**options) return {"deployment_plan":plan,"object_id":result["object"]["object_id"], "global_passport_id":result["global_passport"]["passport_id"], "content_sha256":result["object"]["content_sha256"], From 3c28befe7c5d7ed9dbad9c8ce0c56bc6981c4549 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:26:46 -0700 Subject: [PATCH 075/111] economy: add audited prelaunch withdrawal CLI --- tools/retire_prelaunch_economic_issuance.py | 35 +++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 tools/retire_prelaunch_economic_issuance.py diff --git a/tools/retire_prelaunch_economic_issuance.py b/tools/retire_prelaunch_economic_issuance.py new file mode 100644 index 00000000..81f83bf5 --- /dev/null +++ b/tools/retire_prelaunch_economic_issuance.py @@ -0,0 +1,35 @@ +from __future__ import annotations +from pathlib import Path +import argparse, importlib.util, json, sys + +ROOT=Path(__file__).resolve().parents[1] + +def load(name:str,path:Path): + spec=importlib.util.spec_from_file_location(name,path) + if spec is None or spec.loader is None: raise RuntimeError(f"cannot load {path}") + mod=importlib.util.module_from_spec(spec); sys.modules[name]=mod; spec.loader.exec_module(mod); return mod + +IDENT=load("entity_prelaunch_identity",ROOT/"src"/"01_Core_Runtime"/"identity"/"canonical_identity.py") +MIG=load("entity_prelaunch_withdrawal",ROOT/"src"/"45_ENTITY_Market"/"prelaunch_withdrawal.py") + +def main(): + ap=argparse.ArgumentParser(description="Audit or withdraw unused prelaunch ENTITY economic issuance without changing EEP 3.0.") + ap.add_argument("--state",required=True,help="ENTITY state directory") + ap.add_argument("--object-id",action="append",default=[],help="Underlying ENTITY object ID; repeatable") + ap.add_argument("--dco-id",action="append",default=[],help="DCO Factory ID; repeatable") + ap.add_argument("--reason",default="Superseded unused prelaunch economic experiment; asset remains active.") + ap.add_argument("--apply",action="store_true",help="Apply withdrawal. Omit for fail-safe audit only.") + args=ap.parse_args() + state=Path(args.state).expanduser().resolve() + vault=IDENT.EntityIdentityVault(state) + migration=MIG.PrelaunchEconomicWithdrawal(state,vault) + if args.apply: + result=migration.apply(args.object_id,args.dco_id,args.reason) + else: + result=migration.audit(args.object_id,args.dco_id) + print(json.dumps(result,indent=2,sort_keys=True,default=str)) + if not args.apply and not result.get("safe_to_withdraw",False): + raise SystemExit(2) + +if __name__=="__main__": + main() From 697eaa8488b60db272128b9f5b05b2c71f8c9dfa Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:27:22 -0700 Subject: [PATCH 076/111] wallet ingest: expose explicit associated profiles and controller rights --- src/42_ENTITY_Wallet/asset_ingest.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/42_ENTITY_Wallet/asset_ingest.py b/src/42_ENTITY_Wallet/asset_ingest.py index b7c20ee0..ed6bfe9e 100644 --- a/src/42_ENTITY_Wallet/asset_ingest.py +++ b/src/42_ENTITY_Wallet/asset_ingest.py @@ -72,7 +72,8 @@ def ingest_file(self,controller_entity_id:str,file_path:str|Path,*,title:str|Non authority_basis:str="CONTROLLER_ENTITY",commodity_class:str="DATA", measurement_unit:str="ASSET",version:str="1.0", previous_object_id:str|None=None,metadata:dict|None=None, - configuration:dict|None=None)->dict: + configuration:dict|None=None,additional_profiles:list[str]|None=None, + additional_rights_actions:list[str]|None=None)->dict: src=Path(file_path).expanduser().resolve() if not src.is_file(): raise FileNotFoundError(str(src)) digest=hashlib.sha256() @@ -132,7 +133,15 @@ def ingest_file(self,controller_entity_id:str,file_path:str|Path,*,title:str|Non if asset_kind not in PACKAGE_KINDS[package]: raise ValueError("unsupported package asset kind") if not str(jurisdiction or "").strip(): raise ValueError("jurisdiction required for domain package") cfg=self._configuration(identity,controller_entity_id,package,jurisdiction,authority_basis,configuration) - result=sdk.ingest_package_file(src,controller_entity_id,package,cfg,asset_kind,**common) + profile_refs=[] + for p in (additional_profiles or []): + text=str(p).strip() + if not text: continue + profile_refs.append(text if text.startswith("entity-profile:") else sdk.profile_ref(text)) + result=sdk.ingest_package_file(src,controller_entity_id,package,cfg,asset_kind, + additional_profile_refs=profile_refs, + additional_rights_actions=[str(x).upper() for x in (additional_rights_actions or []) if str(x)], + **common) object_id=result["object_id"]; passport_id=result["global_passport_id"] gp=passports.get(passport_id) check=passports.verify(gp) From 4828c6879a81fa0656fd407ce884838b23d25492 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:27:35 -0700 Subject: [PATCH 077/111] tests: prove prelaunch retirement is fail-closed and history-preserving --- tests/test_prelaunch_economic_withdrawal.py | 98 +++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 tests/test_prelaunch_economic_withdrawal.py diff --git a/tests/test_prelaunch_economic_withdrawal.py b/tests/test_prelaunch_economic_withdrawal.py new file mode 100644 index 00000000..26b1a432 --- /dev/null +++ b/tests/test_prelaunch_economic_withdrawal.py @@ -0,0 +1,98 @@ +from __future__ import annotations +from pathlib import Path +import importlib.util,json,sqlite3,sys,tempfile + +ROOT=Path(__file__).resolve().parents[1] +def load(name,rel): + p=ROOT/rel; spec=importlib.util.spec_from_file_location(name,p) + m=importlib.util.module_from_spec(spec); sys.modules[name]=m; spec.loader.exec_module(m); return m + +def test_unused_prelaunch_issuance_with_reserve_is_withdrawn_without_history_deletion(): + ident=load("prelaunch_ident","src/01_Core_Runtime/identity/canonical_identity.py") + fabmod=load("prelaunch_fabric","src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py") + exmod=load("prelaunch_exchange","src/31_Profiles/exchange_protocol.py") + eoppmod=load("prelaunch_eopp","src/33_Economic_Participation/economic_participation.py") + factorymod=load("prelaunch_factory","src/43_DCO_Factory/canonical_dco_factory.py") + migmod=load("prelaunch_migration","src/45_ENTITY_Market/prelaunch_withdrawal.py") + with tempfile.TemporaryDirectory() as td: + state=Path(td); vault=ident.EntityIdentityVault(state) + issuer=vault.create("Issuer","organization")["entity_id"] + treasury_entity=vault.create("Issuer Treasury","organization")["entity_id"] + venue_owner=vault.create("Market","organization")["entity_id"] + fabric=fabmod.UniversalTransactionFabric(state,vault); ex=exmod.ExchangeProtocol(state,vault,fabric) + eopp=eoppmod.EconomicParticipationProfile(state,vault); factory=factorymod.DCOFactory(state) + asset=fabric.register_digital_commodity(issuer,"Robot Asset","a"*64,commodity_class="ROBOTICS_ASSET",measurement_unit="ASSET",object_type="ROBOTICS_ASSET") + inst=ex.define_instrument(issuer,asset["object_id"],"SPOT_LICENSE",{"actions":["COMMERCIALIZE"]},100,"CAD",transferable=True) + venue=ex.create_venue(venue_owner,"Prelaunch","CA",["ORDER_BOOK"],"b"*64) + disc=ex.publish_disclosure(venue["venue_id"],inst["instrument_id"],issuer,"LISTING_INFORMATION","c"*64) + ex.list_instrument(venue["venue_id"],inst["instrument_id"],issuer,min_lot=1,tick_size=1,disclosure_sha256=disc["content_sha256"]) + treasury=eopp.create_treasury(issuer,treasury_entity,"Treasury","CA","d"*64) + pol=eopp.define_participation(issuer,treasury["treasury_id"],inst["instrument_id"],100,25,"CAD") + eopp.allocate_eep_reserve(ex,pol["policy_id"]) + + template={"template_id":"ROBOTICS-TEST","profile":"STANDARD","products":[ + {"archetype":"EVALUATION","eep_class":"SPOT_LICENSE","scarcity_class":"CONTROLLED_CAPACITY","initial_supply":1}, + {"archetype":"COMMERCIAL","eep_class":"SPOT_LICENSE","scarcity_class":"FIXED_CAP","initial_supply":100}, + {"archetype":"TRAINING","eep_class":"SPOT_LICENSE","scarcity_class":"CONTROLLED_CAPACITY","initial_supply":10}, + ]} + factory.register_template(template) + plan=factory.plan("ROBOTICS-TEST",dco_code="DCO-TEST",family="ROBOTICS") + factory.create_master(dco_id="DCO-TEST",template_id="ROBOTICS-TEST",family="ROBOTICS", + source_sha256="a"*64,plan=plan,lifecycle="ACTIVE_PRELAUNCH") + issuance=factory.record_issuance("DCO-TEST",plan) + + migration=migmod.PrelaunchEconomicWithdrawal(state,vault) + audit=migration.audit([asset["object_id"]],["DCO-TEST"]) + assert audit["safe_to_withdraw"] is True + assert audit["instrument_count"]==1 + assert audit["economic_event_count"]==0 + assert audit["external_or_unexplained_balances"]==[] + + result=migration.apply([asset["object_id"]],["DCO-TEST"],"Superseded unused prelaunch rights experiment") + assert result["historical_records_deleted"] is False + assert result["eep_wire_protocol_changed"] is False + assert Path(result["receipt_path"]).is_file() + + db=sqlite3.connect(state/"entity_v3_exchange.sqlite"); db.row_factory=sqlite3.Row + assert db.execute("select status from instruments where instrument_id=?",(inst["instrument_id"],)).fetchone()["status"]=="WITHDRAWN" + assert db.execute("select status from listings where instrument_id=?",(inst["instrument_id"],)).fetchone()["status"]=="WITHDRAWN" + assert db.execute("select units from balances where instrument_id=? and holder=?",(inst["instrument_id"],issuer)).fetchone()["units"]==100 + assert db.execute("select units from balances where instrument_id=? and holder=?",(inst["instrument_id"],treasury_entity)).fetchone()["units"]==0 + assert db.execute("select count(*) from instruments where instrument_id=?",(inst["instrument_id"],)).fetchone()[0]==1 + db.close() + + ep=sqlite3.connect(state/"entity_v3_economic_participation.sqlite"); ep.row_factory=sqlite3.Row + assert ep.execute("select status from participation_policies where policy_id=?",(pol["policy_id"],)).fetchone()["status"]=="SUPERSEDED" + assert ep.execute("select count(*) from reserve_allocations where instrument_id=?",(inst["instrument_id"],)).fetchone()[0]==1 + ep.close() + + fd=sqlite3.connect(state/"dco_factory"/"entity_dco_factory.sqlite"); fd.row_factory=sqlite3.Row + assert fd.execute("select lifecycle from dco_masters where dco_id='DCO-TEST'").fetchone()["lifecycle"]=="ACTIVE" + assert fd.execute("select count(*) from issuance_withdrawals where issuance_id=?",(issuance["issuance_id"],)).fetchone()[0]==1 + fd.close() + +def test_withdrawal_refuses_real_market_activity(): + ident=load("prelaunch_ident2","src/01_Core_Runtime/identity/canonical_identity.py") + fabmod=load("prelaunch_fabric2","src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py") + exmod=load("prelaunch_exchange2","src/31_Profiles/exchange_protocol.py") + migmod=load("prelaunch_migration2","src/45_ENTITY_Market/prelaunch_withdrawal.py") + with tempfile.TemporaryDirectory() as td: + state=Path(td); vault=ident.EntityIdentityVault(state) + issuer=vault.create("Issuer","organization")["entity_id"]; buyer=vault.create("Buyer","person")["entity_id"] + venue_owner=vault.create("Market","organization")["entity_id"] + fabric=fabmod.UniversalTransactionFabric(state,vault); ex=exmod.ExchangeProtocol(state,vault,fabric) + asset=fabric.register_digital_commodity(issuer,"Asset","e"*64,commodity_class="DATA",measurement_unit="ASSET") + inst=ex.define_instrument(issuer,asset["object_id"],"SPOT_LICENSE",{"actions":["READ"]},10,"CAD",transferable=True) + venue=ex.create_venue(venue_owner,"Market","CA",["ORDER_BOOK"],"f"*64) + disc=ex.publish_disclosure(venue["venue_id"],inst["instrument_id"],issuer,"LISTING_INFORMATION","1"*64) + ex.list_instrument(venue["venue_id"],inst["instrument_id"],issuer,disclosure_sha256=disc["content_sha256"]) + ex.submit_order(venue["venue_id"],inst["instrument_id"],buyer,"BUY",1,10,nonce="buyer-order") + migration=migmod.PrelaunchEconomicWithdrawal(state,vault) + audit=migration.audit([asset["object_id"]]) + assert audit["safe_to_withdraw"] is False + assert audit["activity_counts"]["orders"]==1 + try: + migration.apply([asset["object_id"]],[],"must refuse") + assert False,"migration must fail closed after market activity" + except RuntimeError: + pass From 9631f414f3bb2487f1610a5f71cb26a3dcb5940b Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:27:57 -0700 Subject: [PATCH 078/111] economy: resolve prelaunch reserves through signed participation policy --- src/45_ENTITY_Market/prelaunch_withdrawal.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/45_ENTITY_Market/prelaunch_withdrawal.py b/src/45_ENTITY_Market/prelaunch_withdrawal.py index 137f44a9..60953afb 100644 --- a/src/45_ENTITY_Market/prelaunch_withdrawal.py +++ b/src/45_ENTITY_Market/prelaunch_withdrawal.py @@ -58,7 +58,9 @@ def audit(self,object_ids:Iterable[str],dco_ids:Iterable[str]=())->dict: economic_events=int(ep.execute( f"SELECT COUNT(*) FROM economic_events WHERE instrument_id IN ({iq})",ids).fetchone()[0]) rows=ep.execute(f"""SELECT r.instrument_id,r.units,r.treasury_entity_id,t.owner_entity_id - FROM reserve_allocations r JOIN treasuries t ON t.treasury_id=r.treasury_id + FROM reserve_allocations r + JOIN participation_policies p ON p.policy_id=r.policy_id + JOIN treasuries t ON t.treasury_id=p.treasury_id WHERE r.instrument_id IN ({iq})""",ids).fetchall() reserve_by_instrument={r["instrument_id"]:dict(r) for r in rows} finally: ep.close() From 7b9a4a615216b12b5a333eb965b880e18d6c1d1f Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:28:45 -0700 Subject: [PATCH 079/111] market: define Data Value Discovery and integrity boundaries From 91c1f128cc4440469f18841d44ded0854867387d Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:29:19 -0700 Subject: [PATCH 080/111] docs: align universal market intelligence and robotics prelaunch cleanup --- ...NEAGE_WALLET_ECONOMY_ALIGNMENT_20261003.md | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/docs/v3.4/ENTITY_LINEAGE_WALLET_ECONOMY_ALIGNMENT_20261003.md b/docs/v3.4/ENTITY_LINEAGE_WALLET_ECONOMY_ALIGNMENT_20261003.md index e61b4525..e33062b3 100644 --- a/docs/v3.4/ENTITY_LINEAGE_WALLET_ECONOMY_ALIGNMENT_20261003.md +++ b/docs/v3.4/ENTITY_LINEAGE_WALLET_ECONOMY_ALIGNMENT_20261003.md @@ -226,3 +226,86 @@ Future Robotics assets must: 10. fail closed when required origin, identity, rights, evidence or qualification is missing. This alignment is an application/runtime correction. It does not require a new ENTITY protocol version. + + +## 9. Universal issuer-neutral market + +The corrected economy is not a BTG market implementation. + +Any valid ENTITY controller may create a DCO under its own asset lineage and explicitly issue bounded economic rights under the same registry rules. + +```text +ENTITY identity + ↓ +controlled DCO + ↓ +Rights Passport + ↓ +Global Passport / provenance + ↓ +optional canonical instrument + ↓ +optional listing + ↓ +venue-neutral EEP execution and settlement +``` + +BTG receives no hidden namespace, fee, market, settlement or ownership privilege. + +Human symbols such as `BTG:BRC22-COM`, `ACME:VM07-TRN` or `JSMITH:PHOTO26-COM` are signed issuer-scoped aliases. The immutable canonical instrument ID is authoritative. + +Instrument identity and listing identity are separate so the same canonical instrument may be published or listed through more than one approved venue. + +Every new universal listing requires a Listing Information Sheet and machine listing manifest bound to the canonical instrument, DCO, issuer, Rights Passport and Global Passport. + +Portable packages may be published on GitHub, websites, documentation or partner surfaces without creating duplicate instruments. + +## 10. Data Value Discovery + +Market identifiers become useful human measurement keys while analytics remain keyed by canonical `instrument_id`. + +ENTITY may measure settled activity for each bounded rights instrument, including: + +- last settled price; +- 24-hour / 30-day volume; +- unique buyers; +- trade count; +- high / low; +- active holders; +- circulating supply; +- externally verified settlement activity; +- royalties and participation actually settled. + +These observations may roll upward from instrument to DCO, rights class, asset class and primary domain where explicit passport/DCO classification supports the relationship. + +The wallet exposes a Data Value Discovery surface while preserving the boundary that instrument-rights prices do not establish intrinsic or accounting value for the entire underlying DCO. + +Market-integrity flags include self-trading, reciprocal flows and counterparty concentration. Related-wallet detection requires authorized relationship evidence and is not inferred from identity similarity. + +## 11. Robotics prelaunch economic cleanup + +The three historical robotics issuance campaigns were audited before cleanup planning: + +- synthetic robotics economic pilot: 21 instruments; +- DCO-000001 BIRFR-1: 20 instruments; +- DCO-000002 Failure Detection Engine: 16 instruments. + +The 57 instruments were prelaunch economic experiments. The production audit found no orders, trades, clearing, buyer entitlements or usage. Non-issuer balances were internal BTG treasury reserve allocations, not external holders. + +Because EEP 3.0 is already a qualified protocol component, instrument withdrawal is **not** being introduced as a new EEP signed-wire primitive merely to clean this application state. + +Instead, an application-layer fail-closed migration: + +1. verifies zero market/economic activity; +2. verifies every non-issuer balance is an exact internal issuer-owned treasury reserve; +3. preserves all signed historical rows; +4. marks unused instruments/listings withdrawn; +5. returns internal reserve units to the issuer; +6. supersedes the associated EOPP participation policies; +7. records DCO Factory issuance withdrawals; +8. leaves the underlying DCO assets active; +9. creates signed migration evidence and backups. + +If any real market history or unexplained holder exists, the migration refuses to run. + +This preserves EEP 3.0 conformance and economic history while removing the abandoned licence-oriented robotics direction from active state. From 2eef8756346a99ffccef9311f75d5781273299be Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:42:04 -0700 Subject: [PATCH 081/111] tests: keep prelaunch withdrawal outside sealed EEP --- tests/test_instrument_withdrawal.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/test_instrument_withdrawal.py b/tests/test_instrument_withdrawal.py index 93c7a3cc..d831e43c 100644 --- a/tests/test_instrument_withdrawal.py +++ b/tests/test_instrument_withdrawal.py @@ -11,6 +11,7 @@ def test_signed_instrument_withdrawal_preserves_history_and_deactivates_listing( ident=load("withdraw_identity","src/01_Core_Runtime/identity/canonical_identity.py") fabmod=load("withdraw_fabric","src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py") exmod=load("withdraw_exchange","src/31_Profiles/exchange_protocol.py") + migmod=load("withdraw_migration","src/45_ENTITY_Market/prelaunch_withdrawal.py") with tempfile.TemporaryDirectory() as td: state=Path(td); vault=ident.EntityIdentityVault(state) issuer=vault.create("Issuer","organization")["entity_id"] @@ -24,16 +25,22 @@ def test_signed_instrument_withdrawal_preserves_history_and_deactivates_listing( disc=ex.publish_disclosure(venue["venue_id"],inst["instrument_id"],issuer,"LISTING_INFORMATION","c"*64) listing=ex.list_instrument(venue["venue_id"],inst["instrument_id"],issuer, disclosure_sha256=disc["content_sha256"]) - result=ex.withdraw_instrument(issuer,inst["instrument_id"],"SUPERSEDED_PRELAUNCH_MODEL",evidence_sha256="d"*64) - assert result["new_status"]=="WITHDRAWN" - status=ex.instrument_status(inst["instrument_id"]) - assert status["status"]=="WITHDRAWN" - assert status["withdrawal"]["reason"]=="SUPERSEDED_PRELAUNCH_MODEL" + + migration=migmod.PrelaunchEconomicWithdrawal(state,vault) + audit=migration.audit([obj["object_id"]]) + assert audit["safe_to_withdraw"] is True + result=migration.apply([obj["object_id"]],[],"SUPERSEDED_PRELAUNCH_MODEL") + assert result["status"]=="WITHDRAWN_UNUSED_PRELAUNCH_ECONOMICS" + assert result["eep_wire_protocol_changed"] is False + assert result["historical_records_deleted"] is False + assert result["issuer_signatures"] + assert Path(result["receipt_path"]).is_file() + db=sqlite3.connect(state/"entity_v3_exchange.sqlite"); db.row_factory=sqlite3.Row try: assert db.execute("SELECT COUNT(*) n FROM instruments WHERE instrument_id=?",(inst["instrument_id"],)).fetchone()["n"]==1 + assert db.execute("SELECT status FROM instruments WHERE instrument_id=?",(inst["instrument_id"],)).fetchone()["status"]=="WITHDRAWN" assert db.execute("SELECT status FROM listings WHERE listing_id=?",(listing["listing_id"],)).fetchone()["status"]=="WITHDRAWN" - assert db.execute("SELECT COUNT(*) n FROM instrument_withdrawals WHERE instrument_id=?",(inst["instrument_id"],)).fetchone()["n"]==1 finally: db.close() try: ex.submit_order(venue["venue_id"],inst["instrument_id"],issuer,"SELL",1,1,nonce="after-withdraw") From 4f108e973cda352942481714d1050c3c1fb4c3da Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:47:37 -0700 Subject: [PATCH 082/111] market: fix economic intelligence registry transactions From 46f65e3a9c39e522e34d5831b56765811be8f8a4 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:48:32 -0700 Subject: [PATCH 083/111] tests: qualify settled-trade Data Value Discovery and integrity flags --- tests/test_economic_intelligence.py | 97 +++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 tests/test_economic_intelligence.py diff --git a/tests/test_economic_intelligence.py b/tests/test_economic_intelligence.py new file mode 100644 index 00000000..d9f0fea0 --- /dev/null +++ b/tests/test_economic_intelligence.py @@ -0,0 +1,97 @@ +from __future__ import annotations +from pathlib import Path +import importlib.util,sys,tempfile,time,sqlite3 + +ROOT=Path(__file__).resolve().parents[1] + +def load(name,rel): + p=ROOT/rel; spec=importlib.util.spec_from_file_location(name,p) + m=importlib.util.module_from_spec(spec); sys.modules[name]=m; spec.loader.exec_module(m); return m + +class FakePassports: + def __init__(self,kind): self.kind=kind; self.records={} + def bind(self,pid,object_id,controller,rp=None,actions=None): + if self.kind=="rights": + self.records[pid]={"passport_id":pid,"object_id":object_id,"controller_entity_id":controller, + "rights":[{"effect":"ALLOW","actions":list(actions or ["READ","TRAIN","COMMERCIALIZE"])}]} + else: + self.records[pid]={"passport_id":pid,"object_id":object_id,"controller_entity_id":controller, + "rights_passport_id":rp, + "profile_stack":{"profile_refs":["entity-profile:global@1.0","entity-profile:robotics@1.0"]}} + def get(self,pid): return dict(self.records[pid]) + def verify(self,p): return {"valid":True} + +def test_economic_intelligence_uses_settled_trades_and_flags_suspicious_flow(): + ident=load("intel_identity","src/01_Core_Runtime/identity/canonical_identity.py") + fabmod=load("intel_fabric","src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py") + exmod=load("intel_exchange","src/31_Profiles/exchange_protocol.py") + marketmod=load("intel_market","src/45_ENTITY_Market/canonical_market_registry.py") + intelmod=load("intel_engine","src/45_ENTITY_Market/economic_intelligence.py") + with tempfile.TemporaryDirectory() as td: + state=Path(td); vault=ident.EntityIdentityVault(state) + issuer=vault.create("Acme Corp","organization")["entity_id"] + a=vault.create("Buyer A","person")["entity_id"]; b=vault.create("Buyer B","person")["entity_id"] + fabric=fabmod.UniversalTransactionFabric(state,vault); ex=exmod.ExchangeProtocol(state,vault,fabric) + rights=FakePassports("rights"); gps=FakePassports("global") + reg=marketmod.EntityEconomicMarketRegistry(state,vault,fabric,ex,rights,gps) + obj=fabric.register_digital_commodity(issuer,"Robot Navigation Dataset","1"*64, + commodity_class="ROBOT_NAVIGATION",measurement_unit="ASSET",object_type="DATASET", + metadata={"asset_subtype":"NAVIGATION"}) + rights.bind("rp",obj["object_id"],issuer,actions=["TRAIN"]) + gps.bind("gp",obj["object_id"],issuer,"rp") + inst=reg.create_instrument(issuer,obj["object_id"],instrument_name="Robot Navigation Training Rights", + display_symbol="NAV26-TRN",instrument_class="SPOT_LICENSE",rights_class="TRAINING", + rights={"actions":["TRAIN"]},supply=1000,rights_passport_id="rp",global_passport_id="gp", + settlement_currency="CAD",jurisdiction="CA",namespace="ACME",transferable=True, + buyer_receives=["AI training right"],buyer_does_not_receive=["dataset ownership"]) + iid=inst["instrument_id"]; now=int(time.time()*1000) + + db=sqlite3.connect(state/"entity_v3_exchange.sqlite") + # settled clean trade + db.execute("INSERT INTO trades VALUES(?,?,?,?,?,?,?,?,?,?,?,?)", + ("t1","venue",iid,None,None,a,issuer,100,18,"ORDER_BOOK","SETTLED",now-1000)) + db.execute("INSERT INTO clearing VALUES(?,?,?,?,?,?,?,?)", + ("t1",a,issuer,1800,"CAD","SETTLED","pay1",1)) + # settled self-trade: counted as market activity but integrity-flagged + db.execute("INSERT INTO trades VALUES(?,?,?,?,?,?,?,?,?,?,?,?)", + ("t2","venue",iid,None,None,b,b,10,99,"ORDER_BOOK","SETTLED",now-900)) + db.execute("INSERT INTO clearing VALUES(?,?,?,?,?,?,?,?)", + ("t2",b,b,990,"CAD","SETTLED","pay2",1)) + # reciprocal flow between A and B + db.execute("INSERT INTO trades VALUES(?,?,?,?,?,?,?,?,?,?,?,?)", + ("t3","venue",iid,None,None,a,b,5,20,"ORDER_BOOK","SETTLED",now-800)) + db.execute("INSERT INTO clearing VALUES(?,?,?,?,?,?,?,?)", + ("t3",a,b,100,"CAD","SETTLED","pay3",1)) + db.execute("INSERT INTO trades VALUES(?,?,?,?,?,?,?,?,?,?,?,?)", + ("t4","venue",iid,None,None,b,a,5,20,"ORDER_BOOK","SETTLED",now-700)) + db.execute("INSERT INTO clearing VALUES(?,?,?,?,?,?,?,?)", + ("t4",b,a,100,"CAD","SETTLED","pay4",1)) + # unsettled trade must not affect observed price/volume + db.execute("INSERT INTO trades VALUES(?,?,?,?,?,?,?,?,?,?,?,?)", + ("t5","venue",iid,None,None,a,issuer,999,777,"ORDER_BOOK","EXECUTED_UNSETTLED",now-600)) + db.execute("INSERT INTO clearing VALUES(?,?,?,?,?,?,?,?)", + ("t5",a,issuer,776223,"CAD","PENDING",None,0)) + db.execute("UPDATE balances SET units=700 WHERE instrument_id=? AND holder=?",(iid,issuer)) + db.execute("INSERT OR REPLACE INTO balances VALUES(?,?,?)",(iid,a,200)) + db.execute("INSERT OR REPLACE INTO balances VALUES(?,?,?)",(iid,b,100)) + db.commit(); db.close() + + intel=intelmod.EntityEconomicIntelligence(state,reg,fabric,gps) + m=intel.instrument_metrics(iid,as_of_ms=now) + assert m["last_settled_price"]==20 + assert m["window_30d"]["trade_count"]==4 + assert m["window_30d"]["volume_units"]==120 + assert m["window_30d"]["self_trade_count"]==1 + assert "SELF_TRADE_ACTIVITY_PRESENT" in m["market_integrity"]["flags"] + assert "RECIPROCAL_COUNTERPARTY_FLOW_PRESENT" in m["market_integrity"]["flags"] + assert m["valuation_boundary"]["observed_prices_value_instrument_rights_not_underlying_dco"] is True + assert m["asset_class"]=="ROBOT_NAVIGATION" + assert m["primary_domain"]=="ROBOTICS" + assert m["active_holder_count"]==3 + + d=intel.dco_rights_demand(obj["object_id"],as_of_ms=now) + assert d["rights_class_rollup"]["TRAINING"]["volume_units_30d"]==120 + roll=intel.economy_rollup(group_by="asset_class",as_of_ms=now) + assert roll["rows"][0]["group"]=="ROBOT_NAVIGATION" + assert roll["rows"][0]["volume_units_30d"]==120 + assert roll["rollup_is_observed_market_activity_not_underlying_asset_valuation"] is True From 27078a2a2adf29447b51c45a96d6a1f2f3623200 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:51:18 -0700 Subject: [PATCH 084/111] economy: add signed participation policy withdrawal lifecycle --- .../economic_participation.py | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/33_Economic_Participation/economic_participation.py b/src/33_Economic_Participation/economic_participation.py index d826c9dd..27ac05bf 100644 --- a/src/33_Economic_Participation/economic_participation.py +++ b/src/33_Economic_Participation/economic_participation.py @@ -61,6 +61,10 @@ def _init(self): terms_sha256 TEXT NOT NULL, effective_at_ms INTEGER NOT NULL, status TEXT NOT NULL, created_at_ms INTEGER NOT NULL, signature_json TEXT NOT NULL, UNIQUE(instrument_id,version)); + CREATE TABLE IF NOT EXISTS participation_policy_withdrawals( + withdrawal_id TEXT PRIMARY KEY, policy_id TEXT NOT NULL UNIQUE, instrument_id TEXT NOT NULL, + originator_entity_id TEXT NOT NULL, reason TEXT NOT NULL, evidence_sha256 TEXT, + created_at_ms INTEGER NOT NULL, signature_json TEXT NOT NULL); CREATE TABLE IF NOT EXISTS reserve_allocations( allocation_id TEXT PRIMARY KEY, policy_id TEXT NOT NULL, instrument_id TEXT NOT NULL, originator_entity_id TEXT NOT NULL, treasury_entity_id TEXT NOT NULL, @@ -198,10 +202,38 @@ def define_participation(self,originator_entity_id,treasury_id,instrument_id,tot def _active_policy(self,instrument_id,at_ms=None): at=int(at_ms or now_ms()) with self._db() as db: - row=db.execute("SELECT * FROM participation_policies WHERE instrument_id=? AND effective_at_ms<=? ORDER BY effective_at_ms DESC,version DESC LIMIT 1",(str(instrument_id),at)).fetchone() + row=db.execute("SELECT * FROM participation_policies WHERE instrument_id=? AND status='ACTIVE' AND effective_at_ms<=? ORDER BY effective_at_ms DESC,version DESC LIMIT 1",(str(instrument_id),at)).fetchone() if not row: raise KeyError('effective participation policy not found') return row + def withdraw_participation(self,instrument_id,originator_entity_id,reason,*,evidence_sha256=None): + instrument_id=str(instrument_id); originator_entity_id=str(originator_entity_id); reason=str(reason).strip() + if not reason: raise ValueError('withdrawal reason required') + evidence=require_sha256(evidence_sha256) if evidence_sha256 else None + with self._db() as db: + policy=db.execute("SELECT * FROM participation_policies WHERE instrument_id=? AND status='ACTIVE' ORDER BY effective_at_ms DESC,version DESC LIMIT 1",(instrument_id,)).fetchone() + if not policy: raise KeyError('active participation policy not found') + if policy['originator_entity_id']!=originator_entity_id: + raise PermissionError('policy originator required') + prior=db.execute("SELECT * FROM participation_policy_withdrawals WHERE policy_id=?",(policy['policy_id'],)).fetchone() + if prior: + return dict(prior,signature=json.loads(prior['signature_json'])) + body={'schema':'entity-v3-participation-policy-withdrawal-v1','withdrawal_id':rid('opw3'), + 'policy_id':policy['policy_id'],'instrument_id':instrument_id, + 'originator_entity_id':originator_entity_id,'reason':reason, + 'evidence_sha256':evidence,'created_at_ms':now_ms(), + 'historical_obligations_and_reserves_preserved':True, + 'no_retroactive_economic_change':True,'protocol_tax_bps':0} + sig=self.identity.sign(originator_entity_id,body) + with self._db() as db: + current=db.execute("SELECT status FROM participation_policies WHERE policy_id=?",(policy['policy_id'],)).fetchone() + if not current or current['status']!='ACTIVE': raise ValueError('participation policy no longer active') + db.execute('INSERT INTO participation_policy_withdrawals VALUES(?,?,?,?,?,?,?,?)',( + body['withdrawal_id'],body['policy_id'],instrument_id,originator_entity_id,reason,evidence, + body['created_at_ms'],json.dumps(sig,sort_keys=True))) + db.execute("UPDATE participation_policies SET status='WITHDRAWN' WHERE policy_id=?",(body['policy_id'],)) + return dict(body,signature=sig) + @contextmanager def _exchange_db(self,exchange): db=sqlite3.connect(exchange.path); db.row_factory=sqlite3.Row From 89e69b025776316efca724eb10ee265416687d3a Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:53:07 -0700 Subject: [PATCH 085/111] economy: preserve trade-time policy history across withdrawal --- src/33_Economic_Participation/economic_participation.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/33_Economic_Participation/economic_participation.py b/src/33_Economic_Participation/economic_participation.py index 27ac05bf..e7d01fa9 100644 --- a/src/33_Economic_Participation/economic_participation.py +++ b/src/33_Economic_Participation/economic_participation.py @@ -200,10 +200,13 @@ def define_participation(self,originator_entity_id,treasury_id,instrument_id,tot return dict(body,signature=sig) def _active_policy(self,instrument_id,at_ms=None): - at=int(at_ms or now_ms()) + at=int(now_ms() if at_ms is None else at_ms) with self._db() as db: - row=db.execute("SELECT * FROM participation_policies WHERE instrument_id=? AND status='ACTIVE' AND effective_at_ms<=? ORDER BY effective_at_ms DESC,version DESC LIMIT 1",(str(instrument_id),at)).fetchone() - if not row: raise KeyError('effective participation policy not found') + row=db.execute("SELECT * FROM participation_policies WHERE instrument_id=? AND effective_at_ms<=? ORDER BY effective_at_ms DESC,version DESC LIMIT 1",(str(instrument_id),at)).fetchone() + if not row: raise KeyError('effective participation policy not found') + withdrawal=db.execute("SELECT created_at_ms FROM participation_policy_withdrawals WHERE policy_id=?",(row['policy_id'],)).fetchone() + if withdrawal is not None and int(withdrawal['created_at_ms'])<=at: + raise KeyError('participation policy withdrawn at requested economic time') return row def withdraw_participation(self,instrument_id,originator_entity_id,reason,*,evidence_sha256=None): From a8821467a2750c4dd9f36ff08da6db92e9824e4b Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:53:28 -0700 Subject: [PATCH 086/111] economy: preserve signed policy withdrawal history in prelaunch cleanup --- src/45_ENTITY_Market/prelaunch_withdrawal.py | 26 +++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/45_ENTITY_Market/prelaunch_withdrawal.py b/src/45_ENTITY_Market/prelaunch_withdrawal.py index 60953afb..a541e91d 100644 --- a/src/45_ENTITY_Market/prelaunch_withdrawal.py +++ b/src/45_ENTITY_Market/prelaunch_withdrawal.py @@ -133,8 +133,32 @@ def apply(self,object_ids:Iterable[str],dco_ids:Iterable[str],reason:str)->dict: if self.eopp.exists(): ep=self._db(self.eopp); ep.execute("BEGIN IMMEDIATE") + ep.execute("""CREATE TABLE IF NOT EXISTS participation_policy_withdrawals( + withdrawal_id TEXT PRIMARY KEY, policy_id TEXT NOT NULL UNIQUE, + instrument_id TEXT NOT NULL, originator_entity_id TEXT NOT NULL, + reason TEXT NOT NULL, evidence_sha256 TEXT, created_at_ms INTEGER NOT NULL, + signature_json TEXT NOT NULL)""") iq=",".join("?" for _ in ids) - ep.execute(f"UPDATE participation_policies SET status='SUPERSEDED' WHERE instrument_id IN ({iq}) AND status='ACTIVE'",ids) + policies=ep.execute(f"""SELECT * FROM participation_policies + WHERE instrument_id IN ({iq}) AND status='ACTIVE' + ORDER BY instrument_id,version""",ids).fetchall() + for policy in policies: + withdrawal_id="opw3-"+sha({"policy_id":policy["policy_id"],"evidence":evidence})[:24] + wbody={"schema":"entity-v3-participation-policy-withdrawal-v1", + "withdrawal_id":withdrawal_id,"policy_id":policy["policy_id"], + "instrument_id":policy["instrument_id"], + "originator_entity_id":policy["originator_entity_id"], + "reason":reason,"evidence_sha256":evidence,"created_at_ms":created, + "historical_obligations_and_reserves_preserved":True, + "no_retroactive_economic_change":True,"protocol_tax_bps":0} + wsig=self.identity.sign(policy["originator_entity_id"],wbody) + ep.execute("""INSERT OR IGNORE INTO participation_policy_withdrawals + VALUES(?,?,?,?,?,?,?,?)""",( + withdrawal_id,policy["policy_id"],policy["instrument_id"], + policy["originator_entity_id"],reason,evidence,created, + json.dumps(wsig,sort_keys=True))) + ep.execute("UPDATE participation_policies SET status='WITHDRAWN' WHERE policy_id=?", + (policy["policy_id"],)) ep.commit(); ep.close() if self.factory.exists() and audit["dco_ids"]: From dbb17f9fbf9e4ae2159e5d0e32eeaa6be6ead87e Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:53:41 -0700 Subject: [PATCH 087/111] tests: require signed participation withdrawal history --- tests/test_prelaunch_economic_withdrawal.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_prelaunch_economic_withdrawal.py b/tests/test_prelaunch_economic_withdrawal.py index 26b1a432..4afb1c25 100644 --- a/tests/test_prelaunch_economic_withdrawal.py +++ b/tests/test_prelaunch_economic_withdrawal.py @@ -62,7 +62,8 @@ def test_unused_prelaunch_issuance_with_reserve_is_withdrawn_without_history_del db.close() ep=sqlite3.connect(state/"entity_v3_economic_participation.sqlite"); ep.row_factory=sqlite3.Row - assert ep.execute("select status from participation_policies where policy_id=?",(pol["policy_id"],)).fetchone()["status"]=="SUPERSEDED" + assert ep.execute("select status from participation_policies where policy_id=?",(pol["policy_id"],)).fetchone()["status"]=="WITHDRAWN" + assert ep.execute("select count(*) from participation_policy_withdrawals where policy_id=?",(pol["policy_id"],)).fetchone()[0]==1 assert ep.execute("select count(*) from reserve_allocations where instrument_id=?",(inst["instrument_id"],)).fetchone()[0]==1 ep.close() From 588b10aa29ad662d6fea9c5bcf64d9693baf85e8 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:54:26 -0700 Subject: [PATCH 088/111] tests: qualify time-bounded participation withdrawal --- tests/test_v3_economic_participation.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/test_v3_economic_participation.py b/tests/test_v3_economic_participation.py index a32ea98e..54dc8c85 100644 --- a/tests/test_v3_economic_participation.py +++ b/tests/test_v3_economic_participation.py @@ -195,7 +195,25 @@ def test_external_settlement_requires_evidence_and_remains_attestation(self): pos=self.profile.position(self.treasury['treasury_id'],self.exchange) self.assertEqual(pos['monetary_obligations']['CAD']['SETTLED'],750) - def test_policy_version_cannot_be_rewritten(self): + def test_policy_withdrawal_stops_future_economics_but_preserves_historical_policy(self): + historical_at=int(self.policy['effective_at_ms']) + withdrawal=self.profile.withdraw_participation( + self.instrument['instrument_id'],self.originator,'unused prelaunch economics withdrawn', + evidence_sha256=hashlib.sha256(b'withdrawal evidence').hexdigest()) + self.assertTrue(withdrawal['historical_obligations_and_reserves_preserved']) + with self.assertRaises(KeyError): + self.profile._active_policy(self.instrument['instrument_id']) + historical=self.profile._active_policy(self.instrument['instrument_id'],historical_at) + self.assertEqual(historical['policy_id'],self.policy['policy_id']) + with self.profile._db() as db: + status=db.execute('SELECT status FROM participation_policies WHERE policy_id=?', + (self.policy['policy_id'],)).fetchone()['status'] + count=db.execute('SELECT COUNT(*) FROM participation_policy_withdrawals WHERE policy_id=?', + (self.policy['policy_id'],)).fetchone()[0] + self.assertEqual(status,'WITHDRAWN') + self.assertEqual(count,1) + + def test_policy_version_cannot_be_rewritten(self): with self.assertRaises(ValueError): self.profile.define_participation(self.originator,self.treasury['treasury_id'],self.instrument['instrument_id'],1000,300,'CAD',version=1) v2=self.profile.define_participation(self.originator,self.treasury['treasury_id'],self.instrument['instrument_id'],1000,200,'CAD',version=2,secondary_royalty_bps=200) From d821c328b23eee940d16730f3f245bf5400c189c Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:55:38 -0700 Subject: [PATCH 089/111] wallet: add settlement-aware Data Value Discovery dashboard --- tools/entity_wallet_desktop.py | 36 +++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/tools/entity_wallet_desktop.py b/tools/entity_wallet_desktop.py index 9e6e5386..3f83a468 100644 --- a/tools/entity_wallet_desktop.py +++ b/tools/entity_wallet_desktop.py @@ -22,6 +22,7 @@ def load_module(name:str,relative:str): EXCHANGE=load_module("entity_wallet_exchange","src/31_Profiles/exchange_protocol.py") MARKET=load_module("entity_wallet_market","src/45_ENTITY_Market/canonical_market_registry.py") INTEL=load_module("entity_wallet_intelligence","src/45_ENTITY_Market/economic_intelligence.py") +INTEL=load_module("entity_wallet_intelligence","src/45_ENTITY_Market/economic_intelligence.py") def default_state()->Path: env=os.environ.get("ENTITY_STATE_DIR") @@ -88,6 +89,19 @@ def venues(self)->list[dict]: finally: db.close() return [dict(r) for r in rows] + def economic_metrics(self)->list[dict]: + rows=self.market_registry.active_market() + seen=set(); out=[] + for row in rows: + iid=row["instrument_id"] + if iid in seen: continue + seen.add(iid) + out.append(self.intelligence.instrument_metrics(iid)) + return out + + def economy_rollup(self,group_by="asset_class")->dict: + return self.intelligence.economy_rollup(group_by=group_by) + def market(self)->list[dict]: rows=self.market_registry.active_market() if not rows: return [] @@ -346,6 +360,12 @@ def _layout(self): self.rollups=self._tree(self.intel_tab,["Group","Instruments","30d Trades","30d Units","30d Notional by Currency","Buyer Count*"], [260,95,95,105,300,110]) + intelbar=ttk.Frame(self.intel_tab); intelbar.pack(fill="x",pady=(0,8)) + ttk.Label(intelbar,text="Only settled trades contribute to price/volume. Integrity flags identify suspicious patterns.",style="Sub.TLabel").pack(side="left") + ttk.Label(self.intel_tab,text="Instrument Rights Demand").pack(anchor="w") + self.intel=self._tree(self.intel_tab,["Ticker","Rights","Asset Class","Last","24h Units","30d Units","30d Trades","30d Buyers","Holders","30d Low","30d High","Verified Trades","Integrity"],[155,100,150,75,85,85,85,85,75,75,75,95,260]) + ttk.Label(self.intel_tab,text="Asset-Class Rollup — observed 30-day rights-market activity",style="Sub.TLabel").pack(anchor="w",pady=(10,3)) + self.rollup=self._tree(self.intel_tab,["Asset Class","Instruments","30d Trades","30d Units","30d Notional by Currency"],[200,90,90,90,360]) self.positions=self._tree(self.positions_tab,["Instrument","Units","Currency","Last","Bid","Ask","Indicative Value"],[320,90,80,90,90,90,140]) self.orders=self._tree(self.orders_tab,["Instrument","Side","Remaining","Limit","Status"],[340,80,100,100,120]) self.status=tk.StringVar(value="Ready"); ttk.Label(self,textvariable=self.status,relief="sunken",anchor="w",padding=5).pack(fill="x",side="bottom") @@ -381,6 +401,7 @@ def refresh(self): if not self.backend: return try: snap=self.backend.snapshot(); self.market_rows=self.backend.market(); instruments=self.backend.instruments() + metrics=self.backend.economic_metrics(); rollup=self.backend.economy_rollup("asset_class") self.asset_rows={}; self.instrument_rows={} self.clear(self.assets) for a in snap.get("assets",[]): @@ -393,6 +414,19 @@ def refresh(self): for x in instruments: self.instrument_rows[x["instrument_id"]]=x; self.instruments.insert("", "end",iid=x["instrument_id"],values=( x["market_identifier"],x["instrument_name"],x["rights_class"],x["instrument_class"],x["supply"],x["underlying_dco_id"],x["status"])) + self.clear(self.intel) + for m in metrics: + w=m["window_30d"]; flags=", ".join(m["market_integrity"]["flags"]) or "CLEAN / NO FLAGS" + self.intel.insert("", "end",values=( + m["market_identifier"],m["rights_class"],m["asset_class"],m["last_settled_price"], + m["window_24h"]["volume_units"],w["volume_units"],w["trade_count"],w["unique_buyers"], + m["active_holder_count"],w["low_price"],w["high_price"], + w["externally_verified_trade_count"],flags)) + self.clear(self.rollup) + for row in rollup.get("rows",[]): + notionals=", ".join(f"{k} {v}" for k,v in row["notional_30d_by_currency"].items()) or "—" + self.rollup.insert("", "end",values=(row["group"],row["instrument_count"], + row["settled_trades_30d"],row["volume_units_30d"],notionals)) self.clear(self.positions) for p in snap.get("positions",[]): m=p["market"]; self.positions.insert("", "end",values=(p["instrument_id"],p["units"],p["settlement_currency"],m.get("last"),m.get("bid"),m.get("ask"),p.get("market_value_amount_units"))) @@ -407,7 +441,7 @@ def refresh(self): intel.get("last_settled_price"),w24.get("volume_units"),w30.get("volume_units"),w30.get("unique_buyers"), intel.get("active_holder_count"),", ".join(flags) if flags else "CLEAR",row["venue_id"])) self.refresh_intelligence() - self.status.set(f"{len(snap.get('assets',[]))} assets · {len(instruments)} issued instruments · {len(self.market_rows)} active listings · protocol tax 0 · crypto not required") + self.status.set(f"{len(snap.get('assets',[]))} assets · {len(instruments)} instruments · {len(self.market_rows)} listings · {len(metrics)} demand series · protocol tax 0 · crypto not required") except Exception as e: messagebox.showerror("Refresh failed",str(e)); self.status.set(str(e)) From 9ae70b4fc8800d6e6762530d9e978f01279ced82 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:57:01 -0700 Subject: [PATCH 090/111] economy: migrate legacy DCO factory withdrawal table safely --- src/45_ENTITY_Market/prelaunch_withdrawal.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/45_ENTITY_Market/prelaunch_withdrawal.py b/src/45_ENTITY_Market/prelaunch_withdrawal.py index a541e91d..8558e657 100644 --- a/src/45_ENTITY_Market/prelaunch_withdrawal.py +++ b/src/45_ENTITY_Market/prelaunch_withdrawal.py @@ -163,6 +163,9 @@ def apply(self,object_ids:Iterable[str],dco_ids:Iterable[str],reason:str)->dict: if self.factory.exists() and audit["dco_ids"]: fd=self._db(self.factory); fd.execute("BEGIN IMMEDIATE") + fd.execute("""CREATE TABLE IF NOT EXISTS issuance_withdrawals( + issuance_id TEXT PRIMARY KEY,dco_id TEXT NOT NULL,reason TEXT NOT NULL, + evidence_sha256 TEXT,created_at_ms INTEGER NOT NULL)""") for dco in audit["dco_ids"]: runs=fd.execute("SELECT issuance_id FROM issuance_runs WHERE dco_id=?",(dco,)).fetchall() for run in runs: From 224030c5680117740308ab1fb74d35aaf5540f3d Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:59:47 -0700 Subject: [PATCH 091/111] robotics: recognize algorithms as first-class DCO assets --- src/39_Implementation_Packages/industry_packages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/39_Implementation_Packages/industry_packages.py b/src/39_Implementation_Packages/industry_packages.py index 6a4ef281..7e6dc060 100644 --- a/src/39_Implementation_Packages/industry_packages.py +++ b/src/39_Implementation_Packages/industry_packages.py @@ -68,7 +68,7 @@ def _mapping(standard:str,fields:dict[str,str])->dict: "robotics":{ "profile_refs":[GLOBAL_PROFILE,"entity-profile:robotics@1.0"], "required_config":["organization","jurisdiction","authority_source","safety_policy"], - "object_types":{"robot":"DEVICE","controller":"DEVICE","model":"MODEL","software":"SOFTWARE","telemetry":"DATASET","action_record":"DOCUMENT"}, + "object_types":{"robot":"DEVICE","controller":"DEVICE","model":"MODEL","software":"SOFTWARE","algorithm":"ALGORITHM","telemetry":"DATASET","action_record":"DOCUMENT"}, "rights":["INSPECT","READ","INFER","EXECUTE","CONTROL"], "evidence_types":["SENSOR_OBSERVATION","DOCUMENT","OTHER"], "mappings":[_mapping("ROS-2",{"topic":"descriptor.ros_topic","type":"descriptor.ros_type","node":"descriptor.ros_node"}), From 9e742f93f213ed3aceb05f1b69284d00b7ced8bd Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 07:59:49 -0700 Subject: [PATCH 092/111] wallet: allow robotics algorithm ingest --- src/42_ENTITY_Wallet/asset_ingest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/42_ENTITY_Wallet/asset_ingest.py b/src/42_ENTITY_Wallet/asset_ingest.py index ed6bfe9e..15e167dc 100644 --- a/src/42_ENTITY_Wallet/asset_ingest.py +++ b/src/42_ENTITY_Wallet/asset_ingest.py @@ -5,7 +5,7 @@ INGEST_SCHEMA="entity-wallet-canonical-asset-ingest-v2" PACKAGE_KINDS={ - "robotics":["robot","controller","model","software","telemetry","action_record"], + "robotics":["robot","controller","model","software","algorithm","telemetry","action_record"], "ai":["training_dataset","corpus","model","weights","evaluation","agent","output"], "manufacturing":["machine","digital_twin","firmware","telemetry","maintenance_record"], "healthcare":["clinical_dataset","clinical_document","diagnostic_model","medical_device"], From e4a4333cf10fd4e29c5aa5c2616439c4092c94e8 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 08:07:38 -0700 Subject: [PATCH 093/111] wallet: keep registration-only contribution records out of market positions --- src/42_ENTITY_Wallet/canonical_wallet.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/42_ENTITY_Wallet/canonical_wallet.py b/src/42_ENTITY_Wallet/canonical_wallet.py index 3535e029..8b4d4f90 100644 --- a/src/42_ENTITY_Wallet/canonical_wallet.py +++ b/src/42_ENTITY_Wallet/canonical_wallet.py @@ -168,6 +168,8 @@ def _positions(self,entity_id:str)->list[dict]: for row in rows: p=dict(row); p["units"]=int(p["units"]); p["total_units"]=int(p["total_units"]) p["transferable"]=bool(p["transferable"]); p["rights"]=json.loads(p.pop("rights_json") or "{}") + if p["rights"].get("registration_only") is True: + continue p["market"]=self._market(db,p["instrument_id"]) p["cost_basis"]=self._trade_basis(db,entity_id,p["instrument_id"],p["units"]) last=p["market"]["last"] From 6e544272b8df035e835e917e60ba37fac1206279 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 08:13:12 -0700 Subject: [PATCH 094/111] economy: add signed venue retirement and instrument withdrawal lifecycle --- src/31_Profiles/exchange_protocol.py | 137 +++++++++++++++++++++++++-- 1 file changed, 131 insertions(+), 6 deletions(-) diff --git a/src/31_Profiles/exchange_protocol.py b/src/31_Profiles/exchange_protocol.py index 8121df30..33b58996 100644 --- a/src/31_Profiles/exchange_protocol.py +++ b/src/31_Profiles/exchange_protocol.py @@ -46,7 +46,25 @@ def sha256(name): require_sha256(body.get(name)) actions=rights.get('actions') if type(actions) is not list or not actions or any(type(a) is not str or not a or a!=a.upper() for a in actions): raise ValueError('rights.actions must be non-empty canonical uppercase strings') if actions!=sorted(set(actions)): raise ValueError('rights.actions must be sorted and unique') - elif schema=='entity-eep-disclosure-v1': + elif schema=='entity-eep-instrument-withdrawal-v1': + for n in ('withdrawal_id','instrument_id','issuer','reason'): sid(n) + common_time() + evidence=body.get('evidence_sha256') + if evidence is not None: require_sha256(evidence) + if body.get('prior_status')!='ACTIVE' or body.get('new_status')!='WITHDRAWN': + raise ValueError('invalid instrument withdrawal state transition') + if body.get('no_new_rights_created') is not True: + raise ValueError('withdrawal rights-boundary flag required') + elif schema=='entity-eep-venue-retirement-v1': + for n in ('retirement_id','venue_id','operator','reason'): sid(n) + common_time() + evidence=body.get('evidence_sha256') + if evidence is not None: require_sha256(evidence) + if body.get('prior_status')!='ACTIVE' or body.get('new_status')!='RETIRED': + raise ValueError('invalid venue retirement state transition') + if body.get('venue_is_not_protocol_authority') is not True: + raise ValueError('venue authority boundary required') + elif schema=='entity-eep-disclosure-v1': for n in ('disclosure_id','venue_id','instrument_id','publisher'): sid(n) sup('disclosure_type'); sha256('content_sha256'); common_time() if body.get('protected_data_included') is not False: raise ValueError('protected_data_included must be false') @@ -122,7 +140,17 @@ def _init(self): jurisdiction TEXT NOT NULL, execution_models_json TEXT NOT NULL, policy_sha256 TEXT NOT NULL, status TEXT NOT NULL, created_at_ms INTEGER NOT NULL, signature_json TEXT NOT NULL); - CREATE TABLE IF NOT EXISTS instruments( + CREATE TABLE IF NOT EXISTS venue_retirements( + retirement_id TEXT PRIMARY KEY, venue_id TEXT NOT NULL UNIQUE, + operator TEXT NOT NULL, prior_status TEXT NOT NULL, new_status TEXT NOT NULL, + reason TEXT NOT NULL, evidence_sha256 TEXT, created_at_ms INTEGER NOT NULL, + signature_json TEXT NOT NULL); + CREATE TABLE IF NOT EXISTS instrument_withdrawals( + withdrawal_id TEXT PRIMARY KEY, instrument_id TEXT NOT NULL UNIQUE, + issuer TEXT NOT NULL, prior_status TEXT NOT NULL, new_status TEXT NOT NULL, + reason TEXT NOT NULL, evidence_sha256 TEXT, created_at_ms INTEGER NOT NULL, + signature_json TEXT NOT NULL); + CREATE TABLE IF NOT EXISTS instruments( instrument_id TEXT PRIMARY KEY, issuer TEXT NOT NULL, underlying_object_id TEXT NOT NULL, instrument_class TEXT NOT NULL, rights_json TEXT NOT NULL, total_units INTEGER NOT NULL, transferable INTEGER NOT NULL, duration_ms INTEGER, settlement_currency TEXT NOT NULL, @@ -242,8 +270,57 @@ def create_venue(self, operator: str, name: str, jurisdiction: str, body["venue_id"], operator, body["name"], body["jurisdiction"], json.dumps(models), policy_sha256, "ACTIVE", body["created_at_ms"], json.dumps(sig, sort_keys=True))) - return dict(body, signature=sig) - + return dict(body, signature=sig) + + def venue_status(self, venue_id: str) -> dict: + with self._db() as db: + venue=db.execute("SELECT * FROM venues WHERE venue_id=?",(str(venue_id),)).fetchone() + retirement=db.execute("SELECT * FROM venue_retirements WHERE venue_id=?",(str(venue_id),)).fetchone() + if not venue: raise KeyError("venue not found") + return {"venue_id":venue["venue_id"],"operator":venue["operator"],"name":venue["name"], + "status":venue["status"],"retirement":None if not retirement else + (dict(retirement)|{"signature":json.loads(retirement["signature_json"])})} + + def _record_signed_venue_retirement(self, body: dict, signature: dict) -> dict: + required={"schema","retirement_id","venue_id","operator","prior_status","new_status", + "reason","evidence_sha256","created_at_ms","venue_is_not_protocol_authority"} + validate_signed_wire(body) + if set(body)!=required or body.get("schema")!="entity-eep-venue-retirement-v1": + raise ValueError("invalid signed venue retirement shape") + operator=str(body["operator"]); venue_id=str(body["venue_id"]) + manifest=self.identity.load_manifest(operator) + if not self.identity.verify_signature(manifest,body,signature): + raise PermissionError("venue retirement signature invalid") + with self._db() as db: + db.execute("BEGIN IMMEDIATE") + venue=db.execute("SELECT * FROM venues WHERE venue_id=?",(venue_id,)).fetchone() + if not venue: raise KeyError("venue not found") + if venue["operator"]!=operator: raise PermissionError("venue operator required") + if venue["status"]!="ACTIVE": raise ValueError("active venue required") + if db.execute("SELECT 1 FROM listings WHERE venue_id=? AND status='ACTIVE' LIMIT 1",(venue_id,)).fetchone(): + raise ValueError("cannot retire venue with active listings") + if db.execute("SELECT 1 FROM orders WHERE venue_id=? AND status IN ('OPEN','PARTIAL') LIMIT 1",(venue_id,)).fetchone(): + raise ValueError("cannot retire venue with open orders") + if db.execute("SELECT 1 FROM rfqs WHERE venue_id=? AND status='OPEN' LIMIT 1",(venue_id,)).fetchone(): + raise ValueError("cannot retire venue with open RFQs") + unsettled=db.execute("""SELECT 1 FROM trades t JOIN clearing c ON c.trade_id=t.trade_id + WHERE t.venue_id=? AND c.status='PENDING' LIMIT 1""",(venue_id,)).fetchone() + if unsettled: raise ValueError("cannot retire venue with unsettled trades") + db.execute("INSERT INTO venue_retirements VALUES(?,?,?,?,?,?,?,?,?)",( + body["retirement_id"],venue_id,operator,body["prior_status"],body["new_status"], + body["reason"],body["evidence_sha256"],body["created_at_ms"],json.dumps(signature,sort_keys=True))) + db.execute("UPDATE venues SET status='RETIRED' WHERE venue_id=?",(venue_id,)) + return dict(body,signature=signature) + + def retire_venue(self, operator: str, venue_id: str, reason: str, *, evidence_sha256: str | None=None) -> dict: + evidence=require_sha256(evidence_sha256) if evidence_sha256 is not None else None + body={"schema":"entity-eep-venue-retirement-v1","retirement_id":rid("vret3"), + "venue_id":str(venue_id),"operator":str(operator),"prior_status":"ACTIVE","new_status":"RETIRED", + "reason":str(reason),"evidence_sha256":evidence,"created_at_ms":now_ms(), + "venue_is_not_protocol_authority":True} + sig=self.identity.sign(str(operator),body) + return self._record_signed_venue_retirement(body,sig) + def _record_signed_instrument(self, body: dict, signature: dict) -> dict: required={"schema","instrument_id","issuer","underlying_object_id","instrument_class","rights","total_units","transferable","duration_ms","settlement_currency","delivery_mode","status","created_at_ms","bytes_are_not_the_traded_scarcity"} validate_signed_wire(body) @@ -283,7 +360,55 @@ def define_instrument(self, issuer: str, underlying_object_id: str, instrument_c sig=self.identity.sign(issuer,body) return self._record_signed_instrument(body,sig) - def balance(self, instrument_id: str, holder: str) -> int: + def instrument_status(self, instrument_id: str) -> dict: + with self._db() as db: + inst=db.execute("SELECT instrument_id,issuer,status,created_at_ms FROM instruments WHERE instrument_id=?",(str(instrument_id),)).fetchone() + withdrawal=db.execute("SELECT * FROM instrument_withdrawals WHERE instrument_id=?",(str(instrument_id),)).fetchone() + if not inst: raise KeyError("instrument not found") + return {"instrument_id":inst["instrument_id"],"issuer":inst["issuer"],"status":inst["status"], + "issued_at_ms":int(inst["created_at_ms"]),"withdrawal":None if not withdrawal else + (dict(withdrawal)|{"signature":json.loads(withdrawal["signature_json"])})} + + def _record_signed_instrument_withdrawal(self, body: dict, signature: dict) -> dict: + required={"schema","withdrawal_id","instrument_id","issuer","prior_status","new_status", + "reason","evidence_sha256","created_at_ms","no_new_rights_created"} + validate_signed_wire(body) + if set(body)!=required or body.get("schema")!="entity-eep-instrument-withdrawal-v1": + raise ValueError("invalid signed instrument withdrawal shape") + issuer=str(body["issuer"]); iid=str(body["instrument_id"]) + manifest=self.identity.load_manifest(issuer) + if not self.identity.verify_signature(manifest,body,signature): + raise PermissionError("instrument withdrawal signature invalid") + with self._db() as db: + db.execute("BEGIN IMMEDIATE") + inst=db.execute("SELECT * FROM instruments WHERE instrument_id=?",(iid,)).fetchone() + if not inst: raise KeyError("instrument not found") + if inst["issuer"]!=issuer: raise PermissionError("instrument issuer required") + if inst["status"]!="ACTIVE": raise ValueError("active instrument required") + if db.execute("SELECT 1 FROM orders WHERE instrument_id=? AND status IN ('OPEN','PARTIAL') LIMIT 1",(iid,)).fetchone(): + raise ValueError("cannot withdraw instrument with open orders") + if db.execute("""SELECT 1 FROM trades t JOIN clearing c ON c.trade_id=t.trade_id + WHERE t.instrument_id=? AND c.status='PENDING' LIMIT 1""",(iid,)).fetchone(): + raise ValueError("cannot withdraw instrument with unsettled trades") + if db.execute("SELECT 1 FROM rfqs WHERE instrument_id=? AND status='OPEN' LIMIT 1",(iid,)).fetchone(): + raise ValueError("cannot withdraw instrument with open RFQs") + db.execute("INSERT INTO instrument_withdrawals VALUES(?,?,?,?,?,?,?,?,?)",( + body["withdrawal_id"],iid,issuer,body["prior_status"],body["new_status"], + body["reason"],body["evidence_sha256"],body["created_at_ms"],json.dumps(signature,sort_keys=True))) + db.execute("UPDATE instruments SET status='WITHDRAWN' WHERE instrument_id=?",(iid,)) + db.execute("UPDATE listings SET status='WITHDRAWN' WHERE instrument_id=? AND status='ACTIVE'",(iid,)) + return dict(body,signature=signature) + + def withdraw_instrument(self, issuer: str, instrument_id: str, reason: str, *, evidence_sha256: str | None=None) -> dict: + evidence=require_sha256(evidence_sha256) if evidence_sha256 is not None else None + body={"schema":"entity-eep-instrument-withdrawal-v1","withdrawal_id":rid("withdraw3"), + "instrument_id":str(instrument_id),"issuer":str(issuer),"prior_status":"ACTIVE","new_status":"WITHDRAWN", + "reason":str(reason),"evidence_sha256":evidence,"created_at_ms":now_ms(), + "no_new_rights_created":True} + sig=self.identity.sign(str(issuer),body) + return self._record_signed_instrument_withdrawal(body,sig) + + def balance(self, instrument_id: str, holder: str) -> int: with self._db() as db: row = db.execute("SELECT units FROM balances WHERE instrument_id=? AND holder=?", (instrument_id, holder)).fetchone() @@ -912,7 +1037,7 @@ def surveillance_alerts(self, venue_id: str) -> list[dict]: def status(self) -> dict: with self._db() as db: - tables=("venues","instruments","listings","orders","order_cancellations","trades","clearing","payment_attestations","settlement_verifiers","entitlements","usage","surveillance","rfqs","quotes","rfq_acceptances","rfq_trade_sources","revenue_rule_sets","trade_revenue_bindings") + tables=("venues","venue_retirements","instruments","instrument_withdrawals","listings","orders","order_cancellations","trades","clearing","payment_attestations","settlement_verifiers","entitlements","usage","surveillance","rfqs","quotes","rfq_acceptances","rfq_trade_sources","revenue_rule_sets","trade_revenue_bindings") counts={name:int(db.execute(f"SELECT COUNT(*) FROM {name}").fetchone()[0]) for name in tables} return {"schema":"entity-eep-status-v1","profile":"ENTITY_EXCHANGE_PROTOCOL","version":"3.0.0", "rights_are_traded_not_bytes":True,"venue_neutral":True,"payment_versus_right_transfer":True, From e21791176d16a7f562b29bad514f280936fe4601 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 08:13:49 -0700 Subject: [PATCH 095/111] tests: qualify signed instrument withdrawal and venue retirement --- tests/test_exchange_lifecycle.py | 60 ++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/test_exchange_lifecycle.py diff --git a/tests/test_exchange_lifecycle.py b/tests/test_exchange_lifecycle.py new file mode 100644 index 00000000..b409313e --- /dev/null +++ b/tests/test_exchange_lifecycle.py @@ -0,0 +1,60 @@ +from __future__ import annotations +from pathlib import Path +import importlib.util,sys,tempfile,hashlib + +ROOT=Path(__file__).resolve().parents[1] +def load(name,rel): + p=ROOT/rel; spec=importlib.util.spec_from_file_location(name,p) + m=importlib.util.module_from_spec(spec); sys.modules[name]=m; spec.loader.exec_module(m); return m + +def test_venue_retirement_is_signed_fail_closed_and_preserves_history(): + ident=load("venue_retire_ident","src/01_Core_Runtime/identity/canonical_identity.py") + fabmod=load("venue_retire_fabric","src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py") + exmod=load("venue_retire_exchange","src/31_Profiles/exchange_protocol.py") + with tempfile.TemporaryDirectory() as td: + state=Path(td); vault=ident.EntityIdentityVault(state) + operator=vault.create("Reference Venue Operator","organization")["entity_id"] + issuer=vault.create("Issuer","organization")["entity_id"] + fabric=fabmod.UniversalTransactionFabric(state,vault); ex=exmod.ExchangeProtocol(state,vault,fabric) + venue=ex.create_venue(operator,"Reference Market","CA",["ORDER_BOOK"],hashlib.sha256(b"policy").hexdigest()) + asset=fabric.register_digital_commodity(issuer,"Asset","1"*64,commodity_class="DATA",measurement_unit="ASSET") + inst=ex.define_instrument(issuer,asset["object_id"],"SPOT_LICENSE",{"actions":["READ"]},10,"CAD") + disc=ex.publish_disclosure(venue["venue_id"],inst["instrument_id"],issuer,"LISTING_INFORMATION","2"*64) + ex.list_instrument(venue["venue_id"],inst["instrument_id"],issuer,disclosure_sha256=disc["content_sha256"]) + try: + ex.retire_venue(operator,venue["venue_id"],"must fail while listing is active") + assert False,"active listing must block venue retirement" + except ValueError: + pass + ex.withdraw_instrument(issuer,inst["instrument_id"],"unused prelaunch instrument") + retirement=ex.retire_venue(operator,venue["venue_id"],"unused test venue") + status=ex.venue_status(venue["venue_id"]) + assert status["status"]=="RETIRED" + assert status["retirement"]["retirement_id"]==retirement["retirement_id"] + assert status["retirement"]["venue_is_not_protocol_authority"] is True + assert ex.instrument_status(inst["instrument_id"])["status"]=="WITHDRAWN" + with ex._db() as db: + assert db.execute("select count(*) from venues where venue_id=?",(venue["venue_id"],)).fetchone()[0]==1 + assert db.execute("select count(*) from listings where instrument_id=?",(inst["instrument_id"],)).fetchone()[0]==1 + +def test_venue_retirement_refuses_open_orders(): + ident=load("venue_retire_ident2","src/01_Core_Runtime/identity/canonical_identity.py") + fabmod=load("venue_retire_fabric2","src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py") + exmod=load("venue_retire_exchange2","src/31_Profiles/exchange_protocol.py") + with tempfile.TemporaryDirectory() as td: + state=Path(td); vault=ident.EntityIdentityVault(state) + operator=vault.create("Operator","organization")["entity_id"] + issuer=vault.create("Issuer","organization")["entity_id"] + buyer=vault.create("Buyer","person")["entity_id"] + fabric=fabmod.UniversalTransactionFabric(state,vault); ex=exmod.ExchangeProtocol(state,vault,fabric) + venue=ex.create_venue(operator,"Market","CA",["ORDER_BOOK"],"a"*64) + asset=fabric.register_digital_commodity(issuer,"Asset","b"*64,commodity_class="DATA",measurement_unit="ASSET") + inst=ex.define_instrument(issuer,asset["object_id"],"SPOT_LICENSE",{"actions":["READ"]},10,"CAD") + disc=ex.publish_disclosure(venue["venue_id"],inst["instrument_id"],issuer,"LISTING_INFORMATION","c"*64) + ex.list_instrument(venue["venue_id"],inst["instrument_id"],issuer,disclosure_sha256=disc["content_sha256"]) + ex.submit_order(venue["venue_id"],inst["instrument_id"],buyer,"BUY",1,5,nonce="open-buy") + try: + ex.retire_venue(operator,venue["venue_id"],"must fail") + assert False,"open order must block retirement" + except ValueError: + pass From 059d327c3e548ca0c866eb4a1def9973a4e384e8 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 08:16:05 -0700 Subject: [PATCH 096/111] economy: preserve venue authority boundary in retirement status --- src/31_Profiles/exchange_protocol.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/31_Profiles/exchange_protocol.py b/src/31_Profiles/exchange_protocol.py index 33b58996..d9e1aec4 100644 --- a/src/31_Profiles/exchange_protocol.py +++ b/src/31_Profiles/exchange_protocol.py @@ -279,7 +279,8 @@ def venue_status(self, venue_id: str) -> dict: if not venue: raise KeyError("venue not found") return {"venue_id":venue["venue_id"],"operator":venue["operator"],"name":venue["name"], "status":venue["status"],"retirement":None if not retirement else - (dict(retirement)|{"signature":json.loads(retirement["signature_json"])})} + (dict(retirement)|{"signature":json.loads(retirement["signature_json"]), + "venue_is_not_protocol_authority":True})} def _record_signed_venue_retirement(self, body: dict, signature: dict) -> dict: required={"schema","retirement_id","venue_id","operator","prior_status","new_status", From 799f4c682badeb66b319348a6dc1df1d262a5f70 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 08:16:53 -0700 Subject: [PATCH 097/111] economy: expose lifecycle invariants in status records --- src/31_Profiles/exchange_protocol.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/31_Profiles/exchange_protocol.py b/src/31_Profiles/exchange_protocol.py index d9e1aec4..c87bb78d 100644 --- a/src/31_Profiles/exchange_protocol.py +++ b/src/31_Profiles/exchange_protocol.py @@ -368,7 +368,8 @@ def instrument_status(self, instrument_id: str) -> dict: if not inst: raise KeyError("instrument not found") return {"instrument_id":inst["instrument_id"],"issuer":inst["issuer"],"status":inst["status"], "issued_at_ms":int(inst["created_at_ms"]),"withdrawal":None if not withdrawal else - (dict(withdrawal)|{"signature":json.loads(withdrawal["signature_json"])})} + (dict(withdrawal)|{"signature":json.loads(withdrawal["signature_json"]), + "no_new_rights_created":True})} def _record_signed_instrument_withdrawal(self, body: dict, signature: dict) -> dict: required={"schema","withdrawal_id","instrument_id","issuer","prior_status","new_status", From da811ff0433667e309e3143389e3863c1d2e1a48 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 08:19:50 -0700 Subject: [PATCH 098/111] wallet: collect dynamic runtime imports in desktop build --- tools/build_entity_wallet_desktop.py | 44 +++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/tools/build_entity_wallet_desktop.py b/tools/build_entity_wallet_desktop.py index 50de6513..fd6b5108 100644 --- a/tools/build_entity_wallet_desktop.py +++ b/tools/build_entity_wallet_desktop.py @@ -1,6 +1,6 @@ from __future__ import annotations from pathlib import Path -import os, subprocess, sys +import ast, importlib.util, os, subprocess, sys ROOT=Path(__file__).resolve().parents[1] ENTRY=ROOT/"tools"/"entity_wallet_desktop.py" @@ -23,12 +23,48 @@ "sdk/global_passport_sdk", ] +def _python_sources(): + yield ENTRY + for rel in DATA_PATHS: + p=ROOT/rel + if p.is_file() and p.suffix.lower()==".py": + yield p + elif p.is_dir(): + yield from p.rglob("*.py") + +def discover_hidden_imports(): + """Collect importable dependencies referenced by modules shipped as runtime data. + + PyInstaller cannot see imports inside files loaded later via importlib, so + those dependencies must be made explicit at build time. + """ + modules={"hmac","cryptography.hazmat.primitives.asymmetric.ed25519", + "cryptography.hazmat.primitives.serialization"} + for path in _python_sources(): + try: + tree=ast.parse(path.read_text(encoding="utf-8-sig")) + except Exception: + continue + for node in ast.walk(tree): + if isinstance(node,ast.Import): + modules.update(alias.name for alias in node.names) + elif isinstance(node,ast.ImportFrom) and node.level==0 and node.module: + modules.add(node.module) + available=[] + for name in sorted(modules): + try: + if importlib.util.find_spec(name) is not None: + available.append(name) + except (ImportError,AttributeError,ModuleNotFoundError,ValueError): + pass + return available + def main(): sep=os.pathsep cmd=[sys.executable,"-m","PyInstaller","--noconfirm","--clean","--windowed", - "--name","ENTITY-Wallet", - "--hidden-import","cryptography.hazmat.primitives.asymmetric.ed25519", - "--hidden-import","cryptography.hazmat.primitives.serialization"] + "--name","ENTITY-Wallet"] + for module in discover_hidden_imports(): + cmd += ["--hidden-import",module] for rel in DATA_PATHS: src=ROOT/rel; dest=str(Path(rel).parent if src.is_file() else Path(rel)) cmd += ["--add-data",f"{src}{sep}{dest}"] From 0ccab90c178e3a33a4ff9b60e918d97d2b59903b Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 08:19:53 -0700 Subject: [PATCH 099/111] wallet: add packaged self-test and remove duplicate intelligence wiring --- tools/entity_wallet_desktop.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/tools/entity_wallet_desktop.py b/tools/entity_wallet_desktop.py index 3f83a468..407e1a66 100644 --- a/tools/entity_wallet_desktop.py +++ b/tools/entity_wallet_desktop.py @@ -22,7 +22,6 @@ def load_module(name:str,relative:str): EXCHANGE=load_module("entity_wallet_exchange","src/31_Profiles/exchange_protocol.py") MARKET=load_module("entity_wallet_market","src/45_ENTITY_Market/canonical_market_registry.py") INTEL=load_module("entity_wallet_intelligence","src/45_ENTITY_Market/economic_intelligence.py") -INTEL=load_module("entity_wallet_intelligence","src/45_ENTITY_Market/economic_intelligence.py") def default_state()->Path: env=os.environ.get("ENTITY_STATE_DIR") @@ -99,9 +98,6 @@ def economic_metrics(self)->list[dict]: out.append(self.intelligence.instrument_metrics(iid)) return out - def economy_rollup(self,group_by="asset_class")->dict: - return self.intelligence.economy_rollup(group_by=group_by) - def market(self)->list[dict]: rows=self.market_registry.active_market() if not rows: return [] @@ -571,6 +567,28 @@ def order(self,side): def main(): ap=argparse.ArgumentParser(description="Native ENTITY economic wallet") ap.add_argument("--state"); ap.add_argument("--entity") - args=ap.parse_args(); App(Path(args.state) if args.state else None,args.entity).mainloop() + ap.add_argument("--self-test",action="store_true") + ap.add_argument("--self-test-output") + args=ap.parse_args() + if args.self_test: + if not args.state or not args.entity: + raise SystemExit("--self-test requires --state and --entity") + backend=Backend(Path(args.state),args.entity) + snap=backend.snapshot() + report={"ready":True,"entity_id":args.entity, + "assets":len(snap.get("assets",[])), + "positions":len(snap.get("positions",[])), + "orders":len(snap.get("orders",[])), + "venues":len(backend.venues()), + "market_listings":len(backend.market()), + "economic_demand_series":len(backend.economic_metrics()), + "protocol_tax_bps":0,"cryptocurrency_required":False} + raw=json.dumps(report,indent=2,sort_keys=True) + if args.self_test_output: + Path(args.self_test_output).write_text(raw+"\n",encoding="utf-8") + else: + print(raw) + return + App(Path(args.state) if args.state else None,args.entity).mainloop() if __name__=="__main__": main() From a7f583a501dd9b6f1018d02b1b66ca68cd6d3bfa Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 08:24:15 -0700 Subject: [PATCH 100/111] wallet: keep retired DCO experiments out of current asset holdings --- src/42_ENTITY_Wallet/canonical_wallet.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/42_ENTITY_Wallet/canonical_wallet.py b/src/42_ENTITY_Wallet/canonical_wallet.py index 8b4d4f90..dd728856 100644 --- a/src/42_ENTITY_Wallet/canonical_wallet.py +++ b/src/42_ENTITY_Wallet/canonical_wallet.py @@ -18,6 +18,7 @@ def __init__(self,state_dir:str|Path): self.exchange_path=self.state/"entity_v3_exchange.sqlite" self.economic_path=self.state/"entity_v3_economic_participation.sqlite" self.fabric_path=self.state/"entity_v3"/"universal_fabric.sqlite" + self.dco_factory_path=self.state/"dco_factory"/"entity_dco_factory.sqlite" lineage_path=Path(__file__).with_name("lineage_resolver.py") spec=importlib.util.spec_from_file_location("entity_wallet_lineage_resolver",lineage_path) if spec is None or spec.loader is None: raise RuntimeError("wallet lineage resolver unavailable") @@ -131,6 +132,15 @@ def _trade_basis(self,db,entity_id:str,instrument_id:str,current_units:int)->dic def _assets(self,entity_id:str)->list[dict]: """Return controller-held Digital Commodity Objects independently of market positions.""" if not self.fabric_path.exists(): return [] + master_by_hash={} + if self.dco_factory_path.exists(): + try: + with self._db(self.dco_factory_path) as fdb: + for r in fdb.execute("SELECT dco_id,source_sha256,lifecycle FROM dco_masters"): + master_by_hash[str(r["source_sha256"])]={ + "dco_id":str(r["dco_id"]),"lifecycle":str(r["lifecycle"]).upper()} + except sqlite3.OperationalError: + master_by_hash={} with self._db(self.fabric_path) as db: rows=db.execute("""SELECT object_id,controller_entity_id,object_type,title,content_sha256, descriptor_json,status,created_at_ms @@ -147,7 +157,12 @@ def _assets(self,entity_id:str)->list[dict]: continue d=dict(row); descriptor=json.loads(d.pop("descriptor_json") or "{}") if descriptor.get("digital_commodity") is not True: continue + master=master_by_hash.get(str(d.get("content_sha256") or "")) + if master and (master["lifecycle"].startswith("RETIRED") or master["lifecycle"].startswith("HISTORICAL")): + continue d["descriptor"]=descriptor + if master: + d["dco_id"]=master["dco_id"]; d["dco_lifecycle"]=master["lifecycle"] d["commodity_class"]=descriptor.get("commodity_class") d["measurement_unit"]=descriptor.get("measurement_unit") d["market_instruments_created_automatically"]=False From 7f02c04459438ffe5de456d64e486d21b2e95f98 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 08:24:39 -0700 Subject: [PATCH 101/111] tests: hide retired DCO experiments from current wallet assets --- tests/test_wallet_asset_ingest.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_wallet_asset_ingest.py b/tests/test_wallet_asset_ingest.py index d66920a0..4b0e496a 100644 --- a/tests/test_wallet_asset_ingest.py +++ b/tests/test_wallet_asset_ingest.py @@ -118,3 +118,28 @@ def test_wallet_lists_digital_assets_separately_from_positions(): assert snap["positions"]==[] assert snap["asset_model"]["ingest_does_not_issue_market_instruments"] is True assert snap["asset_model"]["protocol_lineage_and_asset_provenance_are_separate"] is True + + +def test_retired_dco_factory_master_is_not_current_wallet_asset(): + m=load("wallet_retired_asset_view_test","src/42_ENTITY_Wallet/canonical_wallet.py") + with tempfile.TemporaryDirectory() as td: + state=Path(td); dbp=state/"entity_v3"/"universal_fabric.sqlite"; dbp.parent.mkdir(parents=True) + db=sqlite3.connect(dbp) + db.execute("""CREATE TABLE objects(object_id TEXT PRIMARY KEY,controller_entity_id TEXT,object_type TEXT, + title TEXT,content_sha256 TEXT,descriptor_json TEXT,status TEXT,created_at_ms INTEGER,signature_json TEXT)""") + db.execute("""CREATE TABLE provenance_edges(edge_id TEXT PRIMARY KEY,parent_object_id TEXT,child_object_id TEXT, + relation TEXT,contribution_bps INTEGER,evidence_json TEXT,actor_entity_id TEXT,created_at_ms INTEGER,signature_json TEXT)""") + desc=json.dumps({"digital_commodity":True,"commodity_class":"ROBOTICS_PILOT","measurement_unit":"ASSET"}) + db.execute("INSERT INTO objects VALUES(?,?,?,?,?,?,?,?,?)", + ("obj-pilot","OWNER","DATASET","Historical Pilot","f"*64,desc,"ACTIVE",1,"{}")) + db.commit(); db.close() + fdbp=state/"dco_factory"/"entity_dco_factory.sqlite"; fdbp.parent.mkdir(parents=True) + fdb=sqlite3.connect(fdbp) + fdb.execute("""CREATE TABLE dco_masters(dco_id TEXT,template_id TEXT,family TEXT,source_sha256 TEXT, + provenance_root TEXT,semantic_fingerprint TEXT,version TEXT,lifecycle TEXT, + master_json TEXT,created_at_ms INTEGER)""") + fdb.execute("INSERT INTO dco_masters VALUES(?,?,?,?,?,?,?,?,?,?)", + ("DCO-PILOT","T","ROBOTICS","f"*64,None,None,"1","RETIRED_EXPERIMENT","{}",1)) + fdb.commit(); fdb.close() + w=m.EntityEconomicWallet(state); wr=w.ensure_wallet("OWNER","Owner Wallet") + assert w.snapshot(wr["wallet_id"])["assets"]==[] From 2c56b9266a69d417d08756170597f157364074d6 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 09:06:06 -0700 Subject: [PATCH 102/111] ci: build Qt ENTITY wallet across desktop platforms --- .github/workflows/entity-wallet-desktop.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/entity-wallet-desktop.yml b/.github/workflows/entity-wallet-desktop.yml index 14adbbc3..c11553f1 100644 --- a/.github/workflows/entity-wallet-desktop.yml +++ b/.github/workflows/entity-wallet-desktop.yml @@ -4,10 +4,14 @@ on: workflow_dispatch: pull_request: paths: + - "src/31_Profiles/exchange_protocol.py" + - "src/33_Economic_Participation/**" - "src/42_ENTITY_Wallet/**" - - "src/43_DCO_Factory/dco_production_pipeline.py" + - "src/43_DCO_Factory/**" + - "src/45_ENTITY_Market/**" - "tools/entity_wallet_desktop.py" - "tools/build_entity_wallet_desktop.py" + - "protocol/v3/ENTITY_ECONOMIC_MARKET_PROTOCOL.md" - ".github/workflows/entity-wallet-desktop.yml" jobs: @@ -21,10 +25,9 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.11" - - if: runner.os == 'Linux' - run: sudo apt-get update && sudo apt-get install -y python3-tk - run: python -m pip install --upgrade pip - - run: python -m pip install pyinstaller cryptography + - run: python -m pip install pyinstaller cryptography PySide6 + - run: python -m pytest tests/test_entity_market_registry.py tests/test_economic_intelligence.py tests/test_entity_wallet.py tests/test_wallet_asset_ingest.py tests/test_wallet_lineage_resolution.py -q - run: python tools/build_entity_wallet_desktop.py - uses: actions/upload-artifact@v4 with: From 41aa6b91f53e1ef438b139f60c0d9a044590394d Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 09:05:31 -0700 Subject: [PATCH 103/111] wallet: rebuild as ENTITY data economy terminal --- docs/v3.4/ENTITY_WALLET_20261002.md | 40 + .../canonical_market_registry.py | 16 + tools/entity_wallet_desktop.py | 1694 +++++++++++------ 3 files changed, 1211 insertions(+), 539 deletions(-) diff --git a/docs/v3.4/ENTITY_WALLET_20261002.md b/docs/v3.4/ENTITY_WALLET_20261002.md index cef6ebb0..149a26a2 100644 --- a/docs/v3.4/ENTITY_WALLET_20261002.md +++ b/docs/v3.4/ENTITY_WALLET_20261002.md @@ -132,3 +132,43 @@ Asset ingest is deliberately non-economic by default: It does **not** automatically create an EEP instrument, licence, listing, ask price or market value. The controller may later choose to create standardized tradeable rights. Cross-platform build tooling is provided by `tools/build_entity_wallet_desktop.py` and the `ENTITY Wallet Desktop` workflow for Windows, macOS and Linux. + + +## ENTITY Data Economy Terminal redesign — 2026-10-03 + +The native wallet is implemented with **PySide6/Qt**, replacing the earlier utility-style Tkinter presentation. + +The desktop experience is organized as a data-economy terminal: + +- **Overview** — sovereign portfolio state, market tape, asset holdings and settled rights-market activity; +- **Digital Assets** — first-class DCO holdings with lineage, passports, BTDU status and explicit `NO TICKER` state when no instrument exists; +- **Instruments** — canonical issuer-scoped economic instruments with ticker/market identifier, rights class, supply and underlying DCO; +- **ENTITY Market** — universal multi-issuer listings with ticker, issuer, rights, bid/ask/last, settled volume, buyers, holders and integrity flags; +- **Economic Intelligence** — settlement-aware Data Value Discovery by instrument and asset class; +- **Orders** — exchange instructions kept distinct from assets and positions. + +Ticker behavior is intentionally strict: + +DCO → optional economic instrument → issuer namespace + display symbol → ticker/market identifier + +A DCO without an issued economic instrument displays `NO TICKER`. The wallet does not manufacture a ticker or price merely because an asset exists. + +Before first issuance the wallet may display a collision-checked **suggested issuer namespace** such as `BTG`, marked `RESERVED ON ISSUE`. The alias is not authority and is not persisted until an issuer explicitly creates an instrument. Canonical instrument identity remains the immutable instrument_id. + +The header exposes canonical origin context separately from asset provenance: + +Shawn Blackmore → Blackmore Technology Group Limited → ENTITY + +Domain lineage such as `Robotics` comes from the asset Global Passport/profile stack. AI and Manufacturing remain composed profiles rather than false parent-child lineage nodes. + +The PySide6 build continues to preserve: + +- protocol tax = 0; +- no cryptocurrency requirement; +- external settlement evidence boundary; +- asset registration does not issue instruments; +- instrument issuance does not automatically create a listing; +- listing/market activity does not imply intrinsic DCO value; +- BTDU, ADAM and NIKI do not create ownership. + +The 2026-10-03 desktop/economy/lineage regression campaign passed **84/84** before the Qt package was built. The packaged Windows executable then passed a production-state frozen-runtime self-test with exit code 0. diff --git a/src/45_ENTITY_Market/canonical_market_registry.py b/src/45_ENTITY_Market/canonical_market_registry.py index f732fb83..fd235024 100644 --- a/src/45_ENTITY_Market/canonical_market_registry.py +++ b/src/45_ENTITY_Market/canonical_market_registry.py @@ -154,6 +154,22 @@ def _init(self): CREATE INDEX IF NOT EXISTS idx_market_symbol ON instrument_packages(issuer_namespace,display_symbol); """) + def suggest_namespace(self,issuer_entity_id:str)->dict: + issuer=str(issuer_entity_id); manifest=self.identity.load_manifest(issuer) + base=_namespace_candidate(manifest) + if len(base)>20: base=base[:20] + candidate=base + with self._db() as db: + existing=db.execute("SELECT namespace FROM issuer_namespaces WHERE issuer_entity_id=?",(issuer,)).fetchone() + if existing: + return {"namespace":existing["namespace"],"registered":True,"available":True} + n=1 + while True: + conflict=db.execute("SELECT issuer_entity_id FROM issuer_namespaces WHERE namespace=?",(candidate,)).fetchone() + if not conflict: + return {"namespace":candidate,"registered":False,"available":True} + n+=1; suffix="-"+str(n); candidate=base[:20-len(suffix)]+suffix + def ensure_namespace(self,issuer_entity_id:str,preferred:str|None=None)->dict: issuer=str(issuer_entity_id); manifest=self.identity.load_manifest(issuer) with self._db() as db: diff --git a/tools/entity_wallet_desktop.py b/tools/entity_wallet_desktop.py index 407e1a66..e89908fe 100644 --- a/tools/entity_wallet_desktop.py +++ b/tools/entity_wallet_desktop.py @@ -1,594 +1,1210 @@ from __future__ import annotations from pathlib import Path import argparse, importlib.util, json, os, sqlite3, sys, uuid -import tkinter as tk -from tkinter import filedialog, messagebox, ttk -APP_NAME="ENTITY Wallet" +from PySide6 import QtCore, QtGui, QtWidgets -def repo_root()->Path: - if getattr(sys,"frozen",False) and hasattr(sys,"_MEIPASS"): return Path(sys._MEIPASS) +APP_NAME = "ENTITY Wallet" +VERSION = "3.4.3" + +ROOT_BG = "#090B0E" +SIDEBAR_BG = "#0D1014" +PANEL_BG = "#11151A" +PANEL_2 = "#151A20" +BORDER = "#242B34" +TEXT = "#F3F0E8" +MUTED = "#8F99A7" +ACCENT = "#C9A96A" +ACCENT_DARK = "#7D6642" +GREEN = "#43D39E" +RED = "#FF6B78" +YELLOW = "#E3BC64" + +def repo_root() -> Path: + if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): + return Path(sys._MEIPASS) return Path(__file__).resolve().parents[1] -def load_module(name:str,relative:str): - path=repo_root()/relative - spec=importlib.util.spec_from_file_location(name,path) - if spec is None or spec.loader is None: raise RuntimeError(f"Cannot load {path}") - mod=importlib.util.module_from_spec(spec); sys.modules[name]=mod; spec.loader.exec_module(mod); return mod - -CLI=load_module("entity_wallet_v343_cli","tools/entity_v3_4_cli.py") -WALLET=load_module("entity_wallet_model","src/42_ENTITY_Wallet/canonical_wallet.py") -INGEST=load_module("entity_wallet_ingest","src/42_ENTITY_Wallet/asset_ingest.py") -EXCHANGE=load_module("entity_wallet_exchange","src/31_Profiles/exchange_protocol.py") -MARKET=load_module("entity_wallet_market","src/45_ENTITY_Market/canonical_market_registry.py") -INTEL=load_module("entity_wallet_intelligence","src/45_ENTITY_Market/economic_intelligence.py") - -def default_state()->Path: - env=os.environ.get("ENTITY_STATE_DIR") - if env: return Path(env).expanduser() - if sys.platform=="win32": return Path(os.environ.get("LOCALAPPDATA",Path.home()))/"Blackmore Technology Group"/"ENTITY" - if sys.platform=="darwin": return Path.home()/"Library"/"Application Support"/"Blackmore Technology Group"/"ENTITY" - return Path.home()/".local"/"share"/"blackmore-technology-group"/"entity" - -def manifests(state:Path)->list[dict]: - out=[]; root=state/"identity"/"manifests" - if not root.is_dir(): return out +def load_module(name: str, relative: str): + path = repo_root() / relative + spec = importlib.util.spec_from_file_location(name, path) + if spec is None or spec.loader is None: + raise RuntimeError(f"Cannot load {path}") + mod = importlib.util.module_from_spec(spec) + sys.modules[name] = mod + spec.loader.exec_module(mod) + return mod + +CLI = load_module("entity_wallet_v343_cli", "tools/entity_v3_4_cli.py") +WALLET = load_module("entity_wallet_model", "src/42_ENTITY_Wallet/canonical_wallet.py") +INGEST = load_module("entity_wallet_ingest", "src/42_ENTITY_Wallet/asset_ingest.py") +EXCHANGE = load_module("entity_wallet_exchange", "src/31_Profiles/exchange_protocol.py") +MARKET = load_module("entity_wallet_market", "src/45_ENTITY_Market/canonical_market_registry.py") +INTEL = load_module("entity_wallet_intelligence", "src/45_ENTITY_Market/economic_intelligence.py") + +def default_state() -> Path: + env = os.environ.get("ENTITY_STATE_DIR") + if env: + return Path(env).expanduser() + if sys.platform == "win32": + return Path(os.environ.get("LOCALAPPDATA", Path.home())) / "Blackmore Technology Group" / "ENTITY" + if sys.platform == "darwin": + return Path.home() / "Library" / "Application Support" / "Blackmore Technology Group" / "ENTITY" + return Path.home() / ".local" / "share" / "blackmore-technology-group" / "entity" + +def shorten(value: str | None, left: int = 10, right: int = 6) -> str: + v = str(value or "") + if len(v) <= left + right + 3: + return v + return v[:left] + "…" + v[-right:] + +def manifests(state: Path) -> list[dict]: + out = [] + root = state / "identity" / "manifests" + if not root.is_dir(): + return out for p in sorted(root.glob("*.json")): try: - d=json.loads(p.read_text(encoding="utf-8")) - out.append({"entity_id":d.get("entity_id"),"display_name":d.get("display_name") or d.get("entity_id")}) - except Exception: pass + d = json.loads(p.read_text(encoding="utf-8")) + out.append({"entity_id": d.get("entity_id"), "display_name": d.get("display_name") or d.get("entity_id")}) + except Exception: + pass return [x for x in out if x["entity_id"]] class Backend: - def __init__(self,state:Path,entity_id:str): - self.state=state; self.entity_id=entity_id - identity,fabric,profiles,origin,passports,packages,sdk,origin_status=CLI.runtime(state) - self.identity=identity; self.fabric=fabric; self.profiles=profiles; self.origin=origin - self.passports=passports; self.packages=packages; self.sdk=sdk; self.origin_status=origin_status - self.rights=self.sdk.ingestion.rights - self.identity_manifest=self.identity.load_manifest(entity_id) - self.wallet=WALLET.EntityEconomicWallet(state) - self.exchange=EXCHANGE.ExchangeProtocol(state,self.identity,self.fabric) - self.market_registry=MARKET.EntityEconomicMarketRegistry( - state,self.identity,self.fabric,self.exchange,self.rights,self.passports) - self.intelligence=INTEL.EntityEconomicIntelligence( - state,self.market_registry,self.fabric,self.passports) - self.ingestor=INGEST.WalletAssetIngestor(state,CLI) - self.wallet_record=self.wallet.ensure_wallet(entity_id,self.identity_manifest.get("display_name","ENTITY")+" Wallet") - - def snapshot(self): return self.wallet.snapshot(self.wallet_record["wallet_id"]) - def domains(self): return ["general"]+self.packages.list_packages() - def asset_kinds(self,domain): return self.ingestor.asset_kinds(domain) - - def latest_global_passport(self,object_id:str)->dict: - dbp=self.state/"entity_v3_4_global_passports.sqlite" - db=sqlite3.connect(dbp); db.row_factory=sqlite3.Row + def __init__(self, state: Path, entity_id: str): + self.state = state + self.entity_id = entity_id + identity, fabric, profiles, origin, passports, packages, sdk, origin_status = CLI.runtime(state) + self.identity = identity + self.fabric = fabric + self.profiles = profiles + self.origin = origin + self.passports = passports + self.packages = packages + self.sdk = sdk + self.origin_status = origin_status + self.rights = self.sdk.ingestion.rights + self.identity_manifest = self.identity.load_manifest(entity_id) + self.wallet = WALLET.EntityEconomicWallet(state) + self.exchange = EXCHANGE.ExchangeProtocol(state, self.identity, self.fabric) + self.market_registry = MARKET.EntityEconomicMarketRegistry( + state, self.identity, self.fabric, self.exchange, self.rights, self.passports + ) + self.intelligence = INTEL.EntityEconomicIntelligence( + state, self.market_registry, self.fabric, self.passports + ) + self.ingestor = INGEST.WalletAssetIngestor(state, CLI) + self.wallet_record = self.wallet.ensure_wallet( + entity_id, self.identity_manifest.get("display_name", "ENTITY") + " Wallet" + ) + + def snapshot(self): + return self.wallet.snapshot(self.wallet_record["wallet_id"]) + + def domains(self): + return ["general"] + self.packages.list_packages() + + def asset_kinds(self, domain): + return self.ingestor.asset_kinds(domain) + + def namespace_info(self) -> dict: + return self.market_registry.suggest_namespace(self.entity_id) + + def namespace(self) -> str | None: + return self.namespace_info().get("namespace") + + def latest_global_passport(self, object_id: str) -> dict: + dbp = self.state / "entity_v3_4_global_passports.sqlite" + db = sqlite3.connect(dbp) + db.row_factory = sqlite3.Row try: - row=db.execute("""SELECT passport_id FROM global_passports WHERE object_id=? - ORDER BY created_at_ms DESC,passport_id DESC LIMIT 1""",(str(object_id),)).fetchone() - finally: db.close() - if not row: raise KeyError("asset has no Global Passport") + row = db.execute( + """SELECT passport_id FROM global_passports WHERE object_id=? + ORDER BY created_at_ms DESC, passport_id DESC LIMIT 1""", + (str(object_id),), + ).fetchone() + finally: + db.close() + if not row: + raise KeyError("asset has no Global Passport") return self.passports.get(row["passport_id"]) - def instruments(self)->list[dict]: - if not self.market_registry.path.exists(): return [] - db=sqlite3.connect(self.market_registry.path); db.row_factory=sqlite3.Row + def instruments(self) -> list[dict]: + if not self.market_registry.path.exists(): + return [] + db = sqlite3.connect(self.market_registry.path) + db.row_factory = sqlite3.Row try: - rows=db.execute("""SELECT * FROM instrument_packages - WHERE issuer_entity_id=? AND status='ACTIVE' - ORDER BY created_at_ms,instrument_id""",(self.entity_id,)).fetchall() - finally: db.close() + rows = db.execute( + """SELECT instrument_id FROM instrument_packages + WHERE issuer_entity_id=? AND status='ACTIVE' + ORDER BY created_at_ms, instrument_id""", + (self.entity_id,), + ).fetchall() + finally: + db.close() return [self.market_registry.instrument(r["instrument_id"]) for r in rows] - def venues(self)->list[dict]: - if not self.exchange.path.exists(): return [] - db=sqlite3.connect(self.exchange.path); db.row_factory=sqlite3.Row - try: rows=db.execute("SELECT venue_id,name,jurisdiction,status FROM venues WHERE status='ACTIVE' ORDER BY name").fetchall() - finally: db.close() - return [dict(r) for r in rows] - - def economic_metrics(self)->list[dict]: - rows=self.market_registry.active_market() - seen=set(); out=[] - for row in rows: - iid=row["instrument_id"] - if iid in seen: continue - seen.add(iid) - out.append(self.intelligence.instrument_metrics(iid)) + def instrument_map_by_asset(self) -> dict[str, list[dict]]: + out: dict[str, list[dict]] = {} + if not self.market_registry.path.exists(): + return out + db = sqlite3.connect(self.market_registry.path) + db.row_factory = sqlite3.Row + try: + rows = db.execute( + """SELECT instrument_id,underlying_dco_id FROM instrument_packages + WHERE issuer_entity_id=? AND status='ACTIVE' + ORDER BY created_at_ms""", (self.entity_id,) + ).fetchall() + finally: + db.close() + for r in rows: + out.setdefault(r["underlying_dco_id"], []).append(self.market_registry.instrument(r["instrument_id"])) return out - def market(self)->list[dict]: - rows=self.market_registry.active_market() - if not rows: return [] - db=sqlite3.connect(self.exchange.path); db.row_factory=sqlite3.Row + def venues(self) -> list[dict]: + if not self.exchange.path.exists(): + return [] + db = sqlite3.connect(self.exchange.path) + db.row_factory = sqlite3.Row try: - out=[] + rows = db.execute( + "SELECT venue_id,name,jurisdiction,status FROM venues WHERE status='ACTIVE' ORDER BY name" + ).fetchall() + finally: + db.close() + return [dict(r) for r in rows] + + def market(self) -> list[dict]: + rows = self.market_registry.active_market() + if not rows: + return [] + db = sqlite3.connect(self.exchange.path) + db.row_factory = sqlite3.Row + try: + out = [] for row in rows: - e=db.execute("SELECT settlement_currency FROM instruments WHERE instrument_id=? AND status='ACTIVE'", - (row["instrument_id"],)).fetchone() - if not e: continue - d=dict(row); d["settlement_currency"]=e["settlement_currency"] - d["market"]=self.wallet._market(db,d["instrument_id"]) - d["intelligence"]=self.intelligence.instrument_metrics(d["instrument_id"]) + e = db.execute( + "SELECT settlement_currency FROM instruments WHERE instrument_id=? AND status='ACTIVE'", + (row["instrument_id"],), + ).fetchone() + if not e: + continue + d = dict(row) + d["settlement_currency"] = e["settlement_currency"] + d["market"] = self.wallet._market(db, d["instrument_id"]) out.append(d) return out - finally: db.close() + finally: + db.close() - def economy_rollup(self,group_by:str): - return self.intelligence.economy_rollup(group_by=group_by) + def economic_metrics(self) -> list[dict]: + seen = set() + out = [] + for row in self.market_registry.active_market(): + iid = row["instrument_id"] + if iid in seen: + continue + seen.add(iid) + out.append(self.intelligence.instrument_metrics(iid)) + return out - def rights_demand(self,dco_id:str): - return self.intelligence.dco_rights_demand(dco_id) + def economy_rollup(self, group_by="asset_class") -> dict: + return self.intelligence.economy_rollup(group_by=group_by) - def ingest(self,path,options): + def ingest(self, path, options): return self.ingestor.ingest_file( - self.entity_id,path,title=options["title"],package=options["domain"], - asset_kind=options.get("asset_kind") or None,jurisdiction=options.get("jurisdiction",""), - authority_basis=options["authority"],commodity_class=options["class"], - measurement_unit=options["unit"],version=options.get("version") or "1.0", - previous_object_id=options.get("parent_object_id") or None) - - def create_instrument(self,object_id:str,options:dict)->dict: - gp=self.latest_global_passport(object_id) + self.entity_id, path, + title=options["title"], package=options["domain"], + asset_kind=options.get("asset_kind") or None, + jurisdiction=options.get("jurisdiction", ""), + authority_basis=options["authority"], + commodity_class=options["class"], measurement_unit=options["unit"], + version=options.get("version") or "1.0", + previous_object_id=options.get("parent_object_id") or None, + ) + + def create_instrument(self, object_id: str, options: dict) -> dict: + gp = self.latest_global_passport(object_id) return self.market_registry.create_instrument( - self.entity_id,object_id,instrument_name=options["name"],display_symbol=options["symbol"], - instrument_class=options["instrument_class"],rights_class=options["rights_class"], - rights={"actions":options["actions"]},supply=options["supply"], - rights_passport_id=gp["rights_passport_id"],global_passport_id=gp["passport_id"], - settlement_currency=options["currency"],jurisdiction=options["jurisdiction"], - series=options["series"],fungibility=options["fungibility"],divisibility=options["divisibility"], - transferable=options["transferable"],duration_ms=options.get("duration_ms"), - transfer_rules={"wallet_defined":True},economic_terms={"market_value_created_by_issuance":False}, - royalty_terms={"automatic_protocol_royalty_bps":0}, - buyer_receives=options["buyer_receives"],buyer_does_not_receive=options["buyer_does_not_receive"], - evidence_refs=gp.get("evidence_refs") or [],namespace=options.get("namespace") or None) - - def create_listing(self,instrument_id:str,options:dict)->dict: + self.entity_id, object_id, + instrument_name=options["name"], display_symbol=options["symbol"], + instrument_class=options["instrument_class"], rights_class=options["rights_class"], + rights={"actions": options["actions"]}, supply=options["supply"], + rights_passport_id=gp["rights_passport_id"], global_passport_id=gp["passport_id"], + settlement_currency=options["currency"], jurisdiction=options["jurisdiction"], + series=options["series"], fungibility=options["fungibility"], + divisibility=options["divisibility"], transferable=options["transferable"], + duration_ms=options.get("duration_ms"), + transfer_rules={"wallet_defined": True}, + economic_terms={"market_value_created_by_issuance": False}, + royalty_terms={"automatic_protocol_royalty_bps": 0}, + buyer_receives=options["buyer_receives"], + buyer_does_not_receive=options["buyer_does_not_receive"], + evidence_refs=gp.get("evidence_refs") or [], + namespace=options.get("namespace") or None, + ) + + def create_listing(self, instrument_id: str, options: dict) -> dict: return self.market_registry.create_listing( - self.entity_id,instrument_id,options["venue_id"],market_id=options["market_id"], - quote_unit=options["quote_unit"],trade_mode=options["trade_mode"], - settlement_method=options["settlement_method"],minimum_quantity=options["minimum_quantity"], - quantity_precision=options["quantity_precision"],price_precision=options["price_precision"], - pricing_method=options["pricing_method"],listing_series=options["listing_series"], - tick_size=options["tick_size"]) - - def submit_order(self,row:dict,side:str,quantity:int,limit_price:int): - venue=row.get("venue_id") - if not venue: raise ValueError("This instrument has no active market listing.") - order=self.exchange.submit_order(venue,row["instrument_id"],self.entity_id,side,quantity,limit_price, - nonce="wallet-"+uuid.uuid4().hex,tif="GTC") - return {"order":order,"trades":self.exchange.match_order_book(venue,row["instrument_id"])} - -class AssetDialog(tk.Toplevel): - def __init__(self,parent,path:Path,backend:Backend): - super().__init__(parent); self.title("Canonical ENTITY Asset Ingest"); self.resizable(False,False) - self.result=None; self.backend=backend; self.transient(parent); self.grab_set() - self.vars={k:tk.StringVar(value=v) for k,v in { - "title":path.name,"domain":"general","asset_kind":"","jurisdiction":"","class":"DIGITAL_ASSET", - "unit":"ASSET","authority":f"CONTROLLER_ENTITY:{backend.entity_id}","version":"1.0","parent_object_id":""}.items()} - ttk.Label(self,text="File").grid(row=0,column=0,sticky="w",padx=12,pady=(12,4)) - ttk.Label(self,text=str(path),wraplength=560).grid(row=0,column=1,sticky="w",padx=12,pady=(12,4)) - ttk.Label(self,text="Domain profile").grid(row=1,column=0,sticky="w",padx=12,pady=5) - self.domain=ttk.Combobox(self,textvariable=self.vars["domain"],values=backend.domains(),state="readonly",width=48) - self.domain.grid(row=1,column=1,sticky="ew",padx=12,pady=5); self.domain.bind("<>",self._domain_changed) - ttk.Label(self,text="Asset kind").grid(row=2,column=0,sticky="w",padx=12,pady=5) - self.kind=ttk.Combobox(self,textvariable=self.vars["asset_kind"],values=[],width=48); self.kind.grid(row=2,column=1,sticky="ew",padx=12,pady=5) - labels=[("Title","title"),("Jurisdiction (domain assets)","jurisdiction"),("Commodity class","class"), - ("Measurement unit","unit"),("Authority basis","authority"),("Passport version","version"), - ("Parent object ID (optional)","parent_object_id")] - for i,(label,key) in enumerate(labels,3): - ttk.Label(self,text=label).grid(row=i,column=0,sticky="w",padx=12,pady=5) - ttk.Entry(self,textvariable=self.vars[key],width=50).grid(row=i,column=1,sticky="ew",padx=12,pady=5) - self.attest=tk.BooleanVar(value=False) - ttk.Checkbutton(self,text="I have authority to register this asset and attest its provenance.",variable=self.attest).grid(row=10,column=0,columnspan=2,sticky="w",padx=12,pady=(10,4)) - ttk.Label(self,text="Creates DCO + evidence + Rights Passport + Global Passport + BTDU binding. It does not create an instrument, listing, or price.",wraplength=650).grid(row=11,column=0,columnspan=2,sticky="w",padx=12,pady=4) - b=ttk.Frame(self); b.grid(row=12,column=0,columnspan=2,sticky="e",padx=12,pady=12) - ttk.Button(b,text="Cancel",command=self.destroy).pack(side="right",padx=4); ttk.Button(b,text="Ingest Asset",command=self.ok).pack(side="right",padx=4) - def _domain_changed(self,event=None): - domain=self.vars["domain"].get(); kinds=self.backend.asset_kinds(domain); self.kind["values"]=kinds - self.vars["asset_kind"].set(kinds[0] if kinds else "") - self.vars["class"].set("DIGITAL_ASSET" if domain=="general" else domain.upper().replace("-","_")+"_ASSET") - def ok(self): - if not self.attest.get(): messagebox.showerror("Authority required","Confirm authority before registering the digital asset.",parent=self); return - if not self.vars["title"].get().strip(): messagebox.showerror("Title required","Enter an asset title.",parent=self); return - domain=self.vars["domain"].get() - if domain!="general" and not self.vars["jurisdiction"].get().strip(): messagebox.showerror("Jurisdiction required","Domain package ingest requires an explicit jurisdiction.",parent=self); return - if domain!="general" and not self.vars["asset_kind"].get().strip(): messagebox.showerror("Asset kind required","Choose an asset kind for this domain.",parent=self); return - self.result={k:v.get().strip() for k,v in self.vars.items()}; self.destroy() - -class InstrumentDialog(tk.Toplevel): - def __init__(self,parent,asset:dict): - super().__init__(parent); self.title("Create Economic Instrument"); self.resizable(False,False) - self.result=None; self.transient(parent); self.grab_set() - short=(asset.get("title") or "ASSET").upper().replace(" ","")[:12] - self.vars={k:tk.StringVar(value=v) for k,v in { - "name":f"{asset.get('title')} Commercial Rights","symbol":(short+"-COM")[:24],"namespace":"", - "instrument_class":"SPOT_LICENSE","rights_class":"COMMERCIAL","actions":"COMMERCIALIZE", - "supply":"1","currency":"CAD","jurisdiction":"CA","series":"1","fungibility":"FUNGIBLE", - "divisibility":"0","duration_ms":"","buyer_receives":"defined commercial-use right", - "buyer_does_not_receive":"ownership of the underlying DCO,copyright ownership,rights not stated in the Rights Passport"}.items()} - fields=[("Instrument name","name"),("Display symbol","symbol"),("Issuer namespace (optional)","namespace"), - ("EEP instrument class","instrument_class"),("Rights class","rights_class"),("Rights actions (comma separated)","actions"), - ("Supply","supply"),("Settlement currency","currency"),("Jurisdiction","jurisdiction"),("Series","series"), - ("Fungibility","fungibility"),("Divisibility precision","divisibility"),("Duration ms (optional)","duration_ms"), - ("Buyer receives (comma separated)","buyer_receives"),("Buyer does NOT receive","buyer_does_not_receive")] - for i,(label,key) in enumerate(fields): - ttk.Label(self,text=label).grid(row=i,column=0,sticky="w",padx=12,pady=4) - if key=="instrument_class": - w=ttk.Combobox(self,textvariable=self.vars[key],state="readonly", - values=["SPOT_LICENSE","SUBSCRIPTION","COMPUTE_TO_DATA","PROCUREMENT","CONTRIBUTION","SECONDARY_LICENSE"],width=50) - else: w=ttk.Entry(self,textvariable=self.vars[key],width=54) - w.grid(row=i,column=1,sticky="ew",padx=12,pady=4) - self.transferable=tk.BooleanVar(value=False) - ttk.Checkbutton(self,text="Transferable / secondary sale allowed",variable=self.transferable).grid(row=len(fields),column=0,columnspan=2,sticky="w",padx=12,pady=8) - ttk.Label(self,text="Instrument creation issues rights units but does not list them or establish market value.",wraplength=650).grid(row=len(fields)+1,column=0,columnspan=2,sticky="w",padx=12,pady=4) - b=ttk.Frame(self); b.grid(row=len(fields)+2,column=0,columnspan=2,sticky="e",padx=12,pady=12) - ttk.Button(b,text="Cancel",command=self.destroy).pack(side="right",padx=4); ttk.Button(b,text="Create Instrument",command=self.ok).pack(side="right",padx=4) - def ok(self): - try: - supply=int(self.vars["supply"].get()); series=int(self.vars["series"].get()); div=int(self.vars["divisibility"].get()) - duration=int(self.vars["duration_ms"].get()) if self.vars["duration_ms"].get().strip() else None - if supply<1 or series<1 or div<0: raise ValueError - except Exception: - messagebox.showerror("Invalid instrument","Supply/series must be positive integers and divisibility non-negative.",parent=self); return - self.result={k:v.get().strip() for k,v in self.vars.items()} - self.result.update({"supply":supply,"series":series,"divisibility":div,"duration_ms":duration, - "transferable":self.transferable.get(), - "actions":[x.strip().upper() for x in self.vars["actions"].get().split(",") if x.strip()], - "buyer_receives":[x.strip() for x in self.vars["buyer_receives"].get().split(",") if x.strip()], - "buyer_does_not_receive":[x.strip() for x in self.vars["buyer_does_not_receive"].get().split(",") if x.strip()]}) - self.destroy() - -class ListingDialog(tk.Toplevel): - def __init__(self,parent,backend:Backend,instrument:dict): - super().__init__(parent); self.title("Create Listing"); self.resizable(False,False) - self.result=None; self.backend=backend; self.transient(parent); self.grab_set() - venues=backend.venues(); self.venue_map={f"{v['name']} — {v['venue_id']}":v["venue_id"] for v in venues} - self.vars={k:tk.StringVar(value=v) for k,v in { - "venue":next(iter(self.venue_map),""), - "market_id":"ENTITY-MARKET","quote_unit":"CAD","trade_mode":"ORDER_BOOK", - "settlement_method":"PAYMENT_VERSUS_RIGHT","minimum_quantity":"1","quantity_precision":"0", - "price_precision":"0","pricing_method":"ORDER_BOOK","listing_series":"1","tick_size":"1"}.items()} - fields=[("Venue","venue"),("Market ID","market_id"),("Quote unit","quote_unit"),("Trade mode","trade_mode"), - ("Settlement method","settlement_method"),("Minimum quantity","minimum_quantity"), - ("Quantity precision","quantity_precision"),("Price precision","price_precision"), - ("Pricing method","pricing_method"),("Listing series","listing_series"),("Tick size","tick_size")] - for i,(label,key) in enumerate(fields): - ttk.Label(self,text=label).grid(row=i,column=0,sticky="w",padx=12,pady=5) - if key=="venue": w=ttk.Combobox(self,textvariable=self.vars[key],values=list(self.venue_map),state="readonly",width=54) - else: w=ttk.Entry(self,textvariable=self.vars[key],width=56) - w.grid(row=i,column=1,sticky="ew",padx=12,pady=5) - ttk.Label(self,text=f"Listing {instrument['market_identifier']} creates a required Listing Information Sheet and machine manifest.",wraplength=650).grid(row=len(fields),column=0,columnspan=2,sticky="w",padx=12,pady=6) - b=ttk.Frame(self); b.grid(row=len(fields)+1,column=0,columnspan=2,sticky="e",padx=12,pady=12) - ttk.Button(b,text="Cancel",command=self.destroy).pack(side="right",padx=4); ttk.Button(b,text="Create Listing",command=self.ok).pack(side="right",padx=4) - def ok(self): - if not self.venue_map: messagebox.showerror("No venue","No active ENTITY-compatible venue exists.",parent=self); return - try: - ints={k:int(self.vars[k].get()) for k in ("minimum_quantity","quantity_precision","price_precision","listing_series","tick_size")} - if ints["minimum_quantity"]<1 or ints["listing_series"]<1 or ints["tick_size"]<1: raise ValueError - except Exception: - messagebox.showerror("Invalid listing","Listing quantities/series/tick must be valid integers.",parent=self); return - self.result={k:v.get().strip() for k,v in self.vars.items()} - self.result["venue_id"]=self.venue_map[self.result.pop("venue")]; self.result.update(ints); self.destroy() - -class OrderDialog(tk.Toplevel): - def __init__(self,parent,row,side): - super().__init__(parent); self.title(f"{side.title()} {row.get('market_identifier') or row['instrument_id']}"); self.resizable(False,False) - self.result=None; self.transient(parent); self.grab_set(); m=row["market"] - self.qty=tk.StringVar(value="1"); default=m.get("ask") if side=="BUY" else m.get("bid") - self.price=tk.StringVar(value="" if default is None else str(default)) - ttk.Label(self,text=f"{side} {row.get('market_identifier') or row['instrument_id']}").grid(row=0,column=0,columnspan=2,sticky="w",padx=12,pady=12) - ttk.Label(self,text="Quantity").grid(row=1,column=0,sticky="w",padx=12,pady=5); ttk.Entry(self,textvariable=self.qty,width=22).grid(row=1,column=1,padx=12,pady=5) - ttk.Label(self,text=f"Limit price ({row['settlement_currency']})").grid(row=2,column=0,sticky="w",padx=12,pady=5); ttk.Entry(self,textvariable=self.price,width=22).grid(row=2,column=1,padx=12,pady=5) - ttk.Label(self,text=f"Bid: {m.get('bid')} Ask: {m.get('ask')} Last: {m.get('last')}").grid(row=3,column=0,columnspan=2,sticky="w",padx=12,pady=8) - ttk.Button(self,text="Submit",command=self.ok).grid(row=4,column=1,sticky="e",padx=12,pady=12) - def ok(self): - try: - q=int(self.qty.get()); p=int(self.price.get()) - if q<1 or p<0: raise ValueError - except Exception: messagebox.showerror("Invalid order","Quantity must be positive and price non-negative.",parent=self); return - self.result=(q,p); self.destroy() - -class App(tk.Tk): - def __init__(self,state:Path|None=None,entity:str|None=None): - super().__init__(); self.title(APP_NAME); self.geometry("1450x820"); self.minsize(1050,680) - self.backend=None; self.market_rows=[]; self.asset_rows={}; self.instrument_rows={} - self.state_var=tk.StringVar(value=str(state or default_state())); self.entity_var=tk.StringVar(); self.entity_map={} - self._style(); self._layout(); self.load_state(preselect=entity) - - def _style(self): - style=ttk.Style(self) - if sys.platform=="win32": - try: style.theme_use("vista") - except Exception: pass - style.configure("Title.TLabel",font=("Segoe UI",18,"bold")); style.configure("Sub.TLabel",foreground="#666") - - def _layout(self): - top=ttk.Frame(self,padding=12); top.pack(fill="x") - ttk.Label(top,text="ENTITY Wallet",style="Title.TLabel").grid(row=0,column=0,sticky="w") - ttk.Label(top,text="Sovereign digital assets · universal multi-issuer market · wallet-to-wallet rights",style="Sub.TLabel").grid(row=1,column=0,sticky="w") - ttk.Label(top,text="State").grid(row=0,column=1,padx=(30,5)); ttk.Entry(top,textvariable=self.state_var,width=45).grid(row=0,column=2,sticky="ew") - ttk.Button(top,text="Browse",command=self.browse_state).grid(row=0,column=3,padx=4); ttk.Button(top,text="Load",command=self.load_state).grid(row=0,column=4,padx=4) - ttk.Label(top,text="Identity").grid(row=1,column=1,padx=(30,5)) - self.entity_box=ttk.Combobox(top,textvariable=self.entity_var,state="readonly",width=42); self.entity_box.grid(row=1,column=2,sticky="ew") - self.entity_box.bind("<>",lambda e:self.activate()); ttk.Button(top,text="Refresh",command=self.refresh).grid(row=1,column=3,padx=4); top.columnconfigure(2,weight=1) - - self.tabs=ttk.Notebook(self); self.tabs.pack(fill="both",expand=True,padx=12,pady=(0,12)) - self.assets_tab=ttk.Frame(self.tabs,padding=10); self.instruments_tab=ttk.Frame(self.tabs,padding=10) - self.market_tab=ttk.Frame(self.tabs,padding=10); self.intel_tab=ttk.Frame(self.tabs,padding=10) - self.positions_tab=ttk.Frame(self.tabs,padding=10); self.orders_tab=ttk.Frame(self.tabs,padding=10) - self.tabs.add(self.assets_tab,text="My Digital Assets"); self.tabs.add(self.instruments_tab,text="My Instruments") - self.tabs.add(self.market_tab,text="ENTITY Market"); self.tabs.add(self.intel_tab,text="Data Value Discovery") - self.tabs.add(self.positions_tab,text="Market Positions"); self.tabs.add(self.orders_tab,text="Orders") - - bar=ttk.Frame(self.assets_tab); bar.pack(fill="x",pady=(0,8)) - ttk.Button(bar,text="Upload / Ingest Digital Asset",command=self.ingest_asset).pack(side="left") - ttk.Button(bar,text="View Lineage",command=self.view_lineage).pack(side="left",padx=4) - ttk.Button(bar,text="Create Economic Instrument",command=self.create_instrument).pack(side="left",padx=4) - ttk.Button(bar,text="Rights Demand",command=self.view_rights_demand).pack(side="left",padx=4) - ttk.Label(bar,text="Asset creation never auto-issues or prices rights.",style="Sub.TLabel").pack(side="left",padx=12) - self.assets=self._tree(self.assets_tab,["Title","Class","Type","Canonical lineage","Controller","Passport","BTDU","Object ID"],[210,145,90,330,160,100,55,250]) - - ibar=ttk.Frame(self.instruments_tab); ibar.pack(fill="x",pady=(0,8)) - ttk.Button(ibar,text="Create Listing",command=self.create_listing).pack(side="left") - ttk.Label(ibar,text="Canonical instruments are issuer-neutral; namespace:symbol is a display alias only.",style="Sub.TLabel").pack(side="left",padx=12) - self.instruments=self._tree(self.instruments_tab,["Market ID","Instrument Name","Rights Class","Class","Supply","DCO","Status"],[170,310,130,130,80,300,90]) - - mbar=ttk.Frame(self.market_tab); mbar.pack(fill="x",pady=(0,8)) - ttk.Button(mbar,text="Buy",command=lambda:self.order("BUY")).pack(side="left") - ttk.Button(mbar,text="Sell",command=lambda:self.order("SELL")).pack(side="left",padx=4) - ttk.Button(mbar,text="Listing Information",command=self.view_listing).pack(side="left",padx=4) - ttk.Button(mbar,text="Market Intelligence",command=self.view_market_intelligence).pack(side="left",padx=4) - ttk.Button(mbar,text="Export Portable Package",command=self.export_listing).pack(side="left",padx=4) - ttk.Label(mbar,text="One canonical instrument can be published/listed on multiple venues.",style="Sub.TLabel").pack(side="left",padx=12) - self.market=self._tree(self.market_tab,["Market ID","Instrument Name","Rights","Currency","Last","24h Vol","30d Vol","30d Buyers","Holders","Integrity","Venue"], - [155,245,105,70,75,85,85,85,70,180,200]) - - ib=ttk.Frame(self.intel_tab); ib.pack(fill="x",pady=(0,8)) - self.rollup_dimension=tk.StringVar(value="asset_class") - ttk.Label(ib,text="Roll up settled market activity by").pack(side="left") - self.rollup_box=ttk.Combobox(ib,textvariable=self.rollup_dimension,state="readonly",width=22, - values=["asset_class","asset_subtype","primary_domain","rights_class","issuer_entity_id","underlying_dco_id"]) - self.rollup_box.pack(side="left",padx=6); self.rollup_box.bind("<>",lambda e:self.refresh_intelligence()) - ttk.Button(ib,text="Refresh",command=self.refresh_intelligence).pack(side="left") - ttk.Label(ib,text="Notional remains separated by currency.",style="Sub.TLabel").pack(side="left",padx=12) - self.rollups=self._tree(self.intel_tab,["Group","Instruments","30d Trades","30d Units","30d Notional by Currency","Buyer Count*"], - [260,95,95,105,300,110]) - - intelbar=ttk.Frame(self.intel_tab); intelbar.pack(fill="x",pady=(0,8)) - ttk.Label(intelbar,text="Only settled trades contribute to price/volume. Integrity flags identify suspicious patterns.",style="Sub.TLabel").pack(side="left") - ttk.Label(self.intel_tab,text="Instrument Rights Demand").pack(anchor="w") - self.intel=self._tree(self.intel_tab,["Ticker","Rights","Asset Class","Last","24h Units","30d Units","30d Trades","30d Buyers","Holders","30d Low","30d High","Verified Trades","Integrity"],[155,100,150,75,85,85,85,85,75,75,75,95,260]) - ttk.Label(self.intel_tab,text="Asset-Class Rollup — observed 30-day rights-market activity",style="Sub.TLabel").pack(anchor="w",pady=(10,3)) - self.rollup=self._tree(self.intel_tab,["Asset Class","Instruments","30d Trades","30d Units","30d Notional by Currency"],[200,90,90,90,360]) - self.positions=self._tree(self.positions_tab,["Instrument","Units","Currency","Last","Bid","Ask","Indicative Value"],[320,90,80,90,90,90,140]) - self.orders=self._tree(self.orders_tab,["Instrument","Side","Remaining","Limit","Status"],[340,80,100,100,120]) - self.status=tk.StringVar(value="Ready"); ttk.Label(self,textvariable=self.status,relief="sunken",anchor="w",padding=5).pack(fill="x",side="bottom") - - def _tree(self,parent,columns,widths): - f=ttk.Frame(parent); f.pack(fill="both",expand=True); t=ttk.Treeview(f,columns=columns,show="headings",selectmode="browse") - for c,w in zip(columns,widths): t.heading(c,text=c); t.column(c,width=w,anchor="w") - y=ttk.Scrollbar(f,orient="vertical",command=t.yview); t.configure(yscrollcommand=y.set) - t.pack(side="left",fill="both",expand=True); y.pack(side="right",fill="y"); return t - - def browse_state(self): - p=filedialog.askdirectory(title="Select ENTITY state folder",initialdir=self.state_var.get()) - if p: self.state_var.set(p); self.load_state() - def load_state(self,preselect=None): - state=Path(self.state_var.get()).expanduser(); state.mkdir(parents=True,exist_ok=True) - items=manifests(state); self.entity_map={f"{x['display_name']} — {x['entity_id']}":x["entity_id"] for x in items}; self.entity_box["values"]=list(self.entity_map) - if preselect: - for label,eid in self.entity_map.items(): - if eid==preselect: self.entity_var.set(label); break - elif items and not self.entity_var.get(): self.entity_var.set(next(iter(self.entity_map))) - if self.entity_var.get(): self.activate() - else: self.status.set("No ENTITY identity found in this state folder.") - def activate(self): - try: - eid=self.entity_map.get(self.entity_var.get(),self.entity_var.get()); self.backend=Backend(Path(self.state_var.get()),eid); self.refresh() - except Exception as e: - self.backend=None; messagebox.showerror("Cannot load wallet",str(e)); self.status.set(str(e)) + self.entity_id, instrument_id, options["venue_id"], + market_id=options["market_id"], quote_unit=options["quote_unit"], + trade_mode=options["trade_mode"], settlement_method=options["settlement_method"], + minimum_quantity=options["minimum_quantity"], + quantity_precision=options["quantity_precision"], price_precision=options["price_precision"], + pricing_method=options["pricing_method"], listing_series=options["listing_series"], + tick_size=options["tick_size"], + ) + + def submit_order(self, row: dict, side: str, quantity: int, limit_price: int): + venue = row.get("venue_id") + if not venue: + raise ValueError("This instrument has no active market listing.") + order = self.exchange.submit_order( + venue, row["instrument_id"], self.entity_id, side, quantity, limit_price, + nonce="wallet-" + uuid.uuid4().hex, tif="GTC" + ) + trades = self.exchange.match_order_book(venue, row["instrument_id"]) + return {"order": order, "trades": trades} + +class Ui: @staticmethod - def clear(tree): - for x in tree.get_children(): tree.delete(x) + def label(text="", cls=""): + w = QtWidgets.QLabel(text) + if cls: + w.setProperty("class", cls) + return w + + @staticmethod + def button(text, primary=False): + b = QtWidgets.QPushButton(text) + b.setCursor(QtCore.Qt.PointingHandCursor) + b.setProperty("primary", primary) + return b + + @staticmethod + def table(headers): + t = QtWidgets.QTableWidget(0, len(headers)) + t.setHorizontalHeaderLabels(headers) + t.verticalHeader().setVisible(False) + t.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + t.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) + t.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + t.setShowGrid(False) + t.setAlternatingRowColors(False) + t.setWordWrap(False) + t.setTextElideMode(QtCore.Qt.ElideRight) + t.verticalHeader().setDefaultSectionSize(38) + t.horizontalHeader().setStretchLastSection(True) + t.horizontalHeader().setDefaultAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) + t.setSortingEnabled(False) + return t + + @staticmethod + def fill_table(table, rows): + table.setRowCount(0) + for row in rows: + i = table.rowCount() + table.insertRow(i) + for j, value in enumerate(row): + item = QtWidgets.QTableWidgetItem("" if value is None else str(value)) + item.setData(QtCore.Qt.UserRole, value) + table.setItem(i, j, item) + for i in range(table.rowCount()): + table.setRowHeight(i, 38) + +class MetricCard(QtWidgets.QFrame): + def __init__(self, title: str): + super().__init__() + self.setProperty("class", "metricCard") + lay = QtWidgets.QVBoxLayout(self) + lay.setContentsMargins(18, 15, 18, 15) + lay.setSpacing(5) + title_label = Ui.label(title.upper(), "eyebrow") + self.value = Ui.label("—", "metric") + self.note = Ui.label("", "muted") + lay.addWidget(title_label) + lay.addWidget(self.value) + lay.addWidget(self.note) + +class EmptyState(QtWidgets.QFrame): + def __init__(self, title, body): + super().__init__() + self.setProperty("class", "empty") + lay = QtWidgets.QVBoxLayout(self) + lay.setContentsMargins(28, 28, 28, 28) + lay.addWidget(Ui.label(title, "emptyTitle")) + body_label = Ui.label(body, "muted") + body_label.setWordWrap(True) + lay.addWidget(body_label) + +class InstrumentDialog(QtWidgets.QDialog): + def __init__(self, parent, asset, namespace): + super().__init__(parent) + self.setWindowTitle("Issue Economic Instrument") + self.setMinimumWidth(650) + self.result_data = None + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(Ui.label("ISSUE ECONOMIC INSTRUMENT", "dialogTitle")) + layout.addWidget(Ui.label( + "Create a canonical rights instrument against this DCO. This does not list it or establish market value.", + "muted")) + form = QtWidgets.QFormLayout() + form.setLabelAlignment(QtCore.Qt.AlignLeft) + title = asset.get("title") or "Asset" + self.name = QtWidgets.QLineEdit(f"{title} Commercial Rights") + self.namespace = QtWidgets.QLineEdit(namespace or "") + self.symbol = QtWidgets.QLineEdit((title.upper().replace(" ", "")[:10] + "-COM")[:24]) + self.iclass = QtWidgets.QComboBox() + self.iclass.addItems(["SPOT_LICENSE","SUBSCRIPTION","COMPUTE_TO_DATA","PROCUREMENT","CONTRIBUTION","SECONDARY_LICENSE"]) + self.rclass = QtWidgets.QLineEdit("COMMERCIAL") + self.actions = QtWidgets.QLineEdit("COMMERCIALIZE") + self.supply = QtWidgets.QSpinBox(); self.supply.setRange(1, 2_000_000_000); self.supply.setValue(1) + self.currency = QtWidgets.QLineEdit("CAD") + self.jurisdiction = QtWidgets.QLineEdit("CA") + self.series = QtWidgets.QSpinBox(); self.series.setRange(1, 999999); self.series.setValue(1) + self.fungibility = QtWidgets.QComboBox(); self.fungibility.addItems(["FUNGIBLE","SERIES-FUNGIBLE","NON-FUNGIBLE"]) + self.divisibility = QtWidgets.QSpinBox(); self.divisibility.setRange(0, 9) + self.transferable = QtWidgets.QCheckBox("Secondary transfer permitted") + self.receives = QtWidgets.QPlainTextEdit("Defined commercial-use right") + self.excludes = QtWidgets.QPlainTextEdit("Ownership of the underlying DCO\nCopyright ownership\nRights not stated in the Rights Passport") + for label, widget in [ + ("Instrument name", self.name), ("Issuer namespace", self.namespace), + ("Display ticker / symbol", self.symbol), ("EEP class", self.iclass), + ("Rights class", self.rclass), ("Actions", self.actions), + ("Supply", self.supply), ("Settlement currency", self.currency), + ("Jurisdiction", self.jurisdiction), ("Series", self.series), + ("Fungibility", self.fungibility), ("Divisibility", self.divisibility), + ("Transferability", self.transferable), ("Buyer receives", self.receives), + ("Buyer does NOT receive", self.excludes), + ]: + form.addRow(label, widget) + layout.addLayout(form) + self.preview = Ui.label("", "tickerPreview") + layout.addWidget(self.preview) + self.namespace.textChanged.connect(self._preview) + self.symbol.textChanged.connect(self._preview) + self._preview() + buttons = QtWidgets.QHBoxLayout() + buttons.addStretch() + cancel = Ui.button("Cancel"); create = Ui.button("Issue instrument", True) + cancel.clicked.connect(self.reject); create.clicked.connect(self.accept_data) + buttons.addWidget(cancel); buttons.addWidget(create) + layout.addLayout(buttons) + + def _preview(self): + ns = (self.namespace.text().strip() or "NAMESPACE").upper() + sym = (self.symbol.text().strip() or "SYMBOL").upper() + self.preview.setText(f"MARKET IDENTIFIER PREVIEW {ns}:{sym}") + + def accept_data(self): + actions = [x.strip().upper() for x in self.actions.text().split(",") if x.strip()] + if not actions or not self.symbol.text().strip() or not self.name.text().strip(): + QtWidgets.QMessageBox.warning(self, "Instrument", "Name, ticker and at least one rights action are required.") + return + self.result_data = { + "name": self.name.text().strip(), "namespace": self.namespace.text().strip(), + "symbol": self.symbol.text().strip(), "instrument_class": self.iclass.currentText(), + "rights_class": self.rclass.text().strip(), "actions": actions, "supply": self.supply.value(), + "currency": self.currency.text().strip(), "jurisdiction": self.jurisdiction.text().strip(), + "series": self.series.value(), "fungibility": self.fungibility.currentText(), + "divisibility": self.divisibility.value(), "transferable": self.transferable.isChecked(), + "duration_ms": None, + "buyer_receives": [x.strip() for x in self.receives.toPlainText().splitlines() if x.strip()], + "buyer_does_not_receive": [x.strip() for x in self.excludes.toPlainText().splitlines() if x.strip()], + } + self.accept() + +class ListingDialog(QtWidgets.QDialog): + def __init__(self, parent, backend: Backend, instrument: dict): + super().__init__(parent) + self.setWindowTitle("Create Market Listing") + self.setMinimumWidth(600) + self.result_data = None + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(Ui.label("CREATE MARKET LISTING", "dialogTitle")) + layout.addWidget(Ui.label(instrument["market_identifier"], "tickerHero")) + layout.addWidget(Ui.label( + "A listing is separate from the canonical instrument. It binds a buyer-facing Listing Information Sheet and machine manifest.", + "muted")) + form = QtWidgets.QFormLayout() + self.venue = QtWidgets.QComboBox() + self.venues = backend.venues() + for v in self.venues: + self.venue.addItem(f"{v['name']} · {v['jurisdiction']}", v["venue_id"]) + self.market_id = QtWidgets.QLineEdit("ENTITY-MKT") + self.quote = QtWidgets.QLineEdit("CAD") + self.mode = QtWidgets.QComboBox(); self.mode.addItems(["ORDER_BOOK","CALL_AUCTION","RFQ"]) + self.settlement = QtWidgets.QComboBox(); self.settlement.addItems(["PAYMENT_VERSUS_RIGHT"]) + self.min_qty = QtWidgets.QSpinBox(); self.min_qty.setRange(1, 2_000_000_000); self.min_qty.setValue(1) + self.qprec = QtWidgets.QSpinBox(); self.qprec.setRange(0, 9) + self.pprec = QtWidgets.QSpinBox(); self.pprec.setRange(0, 9) + self.pricing = QtWidgets.QComboBox(); self.pricing.addItems(["ORDER_BOOK","CALL_AUCTION","RFQ"]) + self.series = QtWidgets.QSpinBox(); self.series.setRange(1, 999999); self.series.setValue(1) + self.tick = QtWidgets.QSpinBox(); self.tick.setRange(1, 2_000_000_000); self.tick.setValue(1) + for label, widget in [ + ("Venue", self.venue), ("Market ID", self.market_id), ("Quote unit", self.quote), + ("Trade mode", self.mode), ("Settlement", self.settlement), ("Minimum quantity", self.min_qty), + ("Quantity precision", self.qprec), ("Price precision", self.pprec), + ("Pricing method", self.pricing), ("Listing series", self.series), ("Tick size", self.tick), + ]: + form.addRow(label, widget) + layout.addLayout(form) + buttons = QtWidgets.QHBoxLayout(); buttons.addStretch() + cancel = Ui.button("Cancel"); create = Ui.button("Create listing", True) + cancel.clicked.connect(self.reject); create.clicked.connect(self.accept_data) + buttons.addWidget(cancel); buttons.addWidget(create); layout.addLayout(buttons) + + def accept_data(self): + if self.venue.count() == 0: + QtWidgets.QMessageBox.warning(self, "Listing", "No active ENTITY-compatible venue is available.") + return + self.result_data = { + "venue_id": self.venue.currentData(), "market_id": self.market_id.text().strip(), + "quote_unit": self.quote.text().strip(), "trade_mode": self.mode.currentText(), + "settlement_method": self.settlement.currentText(), "minimum_quantity": self.min_qty.value(), + "quantity_precision": self.qprec.value(), "price_precision": self.pprec.value(), + "pricing_method": self.pricing.currentText(), "listing_series": self.series.value(), + "tick_size": self.tick.value(), + } + self.accept() + +class OrderDialog(QtWidgets.QDialog): + def __init__(self, parent, row, side): + super().__init__(parent) + self.setWindowTitle(f"{side.title()} {row.get('market_identifier') or row['instrument_id']}") + self.result_data = None + m = row["market"] + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(Ui.label(side, "eyebrow")) + layout.addWidget(Ui.label(row.get("market_identifier") or row["instrument_id"], "tickerHero")) + form = QtWidgets.QFormLayout() + self.qty = QtWidgets.QSpinBox(); self.qty.setRange(1, 2_000_000_000); self.qty.setValue(1) + self.price = QtWidgets.QSpinBox(); self.price.setRange(0, 2_000_000_000) + default = m.get("ask") if side == "BUY" else m.get("bid") + if default is not None: self.price.setValue(int(default)) + form.addRow("Quantity", self.qty); form.addRow(f"Limit price ({row['settlement_currency']})", self.price) + layout.addLayout(form) + layout.addWidget(Ui.label(f"Bid {m.get('bid') or '—'} Ask {m.get('ask') or '—'} Last {m.get('last') or '—'}", "muted")) + buttons = QtWidgets.QHBoxLayout(); buttons.addStretch() + cancel = Ui.button("Cancel"); submit = Ui.button(f"Submit {side}", True) + cancel.clicked.connect(self.reject); submit.clicked.connect(self.accept_data) + buttons.addWidget(cancel); buttons.addWidget(submit); layout.addLayout(buttons) + + def accept_data(self): + self.result_data = (self.qty.value(), self.price.value()) + self.accept() + +class AssetIngestDialog(QtWidgets.QDialog): + def __init__(self, parent, path: Path, backend: Backend): + super().__init__(parent) + self.setWindowTitle("Ingest Digital Asset") + self.setMinimumWidth(650) + self.result_data = None + self.backend = backend + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(Ui.label("INGEST DIGITAL ASSET", "dialogTitle")) + layout.addWidget(Ui.label(str(path), "monoMuted")) + form = QtWidgets.QFormLayout() + self.title = QtWidgets.QLineEdit(path.name) + self.domain = QtWidgets.QComboBox(); self.domain.addItems(backend.domains()) + self.kind = QtWidgets.QComboBox() + self.juris = QtWidgets.QLineEdit("") + self.cclass = QtWidgets.QLineEdit("DIGITAL_ASSET") + self.unit = QtWidgets.QLineEdit("ASSET") + self.authority = QtWidgets.QLineEdit(f"CONTROLLER_ENTITY:{backend.entity_id}") + self.version = QtWidgets.QLineEdit("1.0") + self.parent_id = QtWidgets.QLineEdit("") + self.attest = QtWidgets.QCheckBox("I have authority to register this asset and attest its provenance.") + for label, widget in [ + ("Asset title", self.title), ("Domain profile", self.domain), ("Asset kind", self.kind), + ("Jurisdiction", self.juris), ("Commodity class", self.cclass), ("Measurement unit", self.unit), + ("Authority basis", self.authority), ("Passport version", self.version), + ("Parent object ID", self.parent_id), ("Authority attestation", self.attest), + ]: + form.addRow(label, widget) + layout.addLayout(form) + layout.addWidget(Ui.label( + "Creates the governed DCO + evidence + Rights Passport + Global Passport + BTDU binding. No ticker, instrument, listing or price is created automatically.", + "muted")) + self.domain.currentTextChanged.connect(self._domain) + self._domain(self.domain.currentText()) + buttons = QtWidgets.QHBoxLayout(); buttons.addStretch() + cancel = Ui.button("Cancel"); ingest = Ui.button("Ingest asset", True) + cancel.clicked.connect(self.reject); ingest.clicked.connect(self.accept_data) + buttons.addWidget(cancel); buttons.addWidget(ingest); layout.addLayout(buttons) + + def _domain(self, domain): + self.kind.clear() + kinds = self.backend.asset_kinds(domain) + self.kind.addItems(kinds) + self.cclass.setText("DIGITAL_ASSET" if domain == "general" else domain.upper().replace("-", "_") + "_ASSET") + + def accept_data(self): + if not self.attest.isChecked(): + QtWidgets.QMessageBox.warning(self, "Authority", "Authority attestation is required.") + return + if self.domain.currentText() != "general" and not self.juris.text().strip(): + QtWidgets.QMessageBox.warning(self, "Jurisdiction", "Domain package ingest requires an explicit jurisdiction.") + return + self.result_data = { + "title": self.title.text().strip(), "domain": self.domain.currentText(), + "asset_kind": self.kind.currentText(), "jurisdiction": self.juris.text().strip(), + "class": self.cclass.text().strip(), "unit": self.unit.text().strip(), + "authority": self.authority.text().strip(), "version": self.version.text().strip(), + "parent_object_id": self.parent_id.text().strip(), + } + self.accept() + +class WalletWindow(QtWidgets.QMainWindow): + NAV = [ + ("overview", "OVERVIEW"), ("assets", "DIGITAL ASSETS"), ("instruments", "INSTRUMENTS"), + ("market", "ENTITY MARKET"), ("intelligence", "ECONOMIC INTELLIGENCE"), ("orders", "ORDERS"), + ] + + def __init__(self, state: Path, entity_id: str): + super().__init__() + self.state = state + self.entity_id = entity_id + self.backend = Backend(state, entity_id) + self.snapshot_data = {} + self.assets_rows: list[dict] = [] + self.instruments_rows: list[dict] = [] + self.market_rows: list[dict] = [] + self.metrics_rows: list[dict] = [] + self.setWindowTitle(f"{APP_NAME} · Data Economy Terminal") + self.resize(1560, 940) + self.setMinimumSize(1180, 720) + self._build() + self._apply_style() + self.refresh() + self.navigate("overview") + + def _build(self): + root = QtWidgets.QWidget(); self.setCentralWidget(root) + outer = QtWidgets.QHBoxLayout(root); outer.setContentsMargins(0, 0, 0, 0); outer.setSpacing(0) + + side = QtWidgets.QFrame(); side.setObjectName("sidebar"); side.setFixedWidth(238) + sl = QtWidgets.QVBoxLayout(side); sl.setContentsMargins(18, 22, 18, 20); sl.setSpacing(7) + sl.addWidget(Ui.label("ENTITY", "brand")) + sl.addWidget(Ui.label("DATA ECONOMY TERMINAL", "brandSub")) + sl.addSpacing(24) + self.nav_buttons = {} + for key, label in self.NAV: + b = QtWidgets.QPushButton(label); b.setCheckable(True); b.setCursor(QtCore.Qt.PointingHandCursor) + b.setProperty("nav", True); b.clicked.connect(lambda checked=False, k=key: self.navigate(k)) + self.nav_buttons[key] = b; sl.addWidget(b) + sl.addStretch() + sl.addWidget(Ui.label("SOVEREIGN LOCAL STATE", "eyebrow")) + sl.addWidget(Ui.label("Protocol tax 0\nNo cryptocurrency required\nExternal settlement evidence required", "sidebarFoot")) + outer.addWidget(side) + + body = QtWidgets.QWidget(); bl = QtWidgets.QVBoxLayout(body) + bl.setContentsMargins(26, 18, 26, 22); bl.setSpacing(14) + header = QtWidgets.QHBoxLayout() + titlebox = QtWidgets.QVBoxLayout() + self.page_title = Ui.label("", "pageTitle") + self.page_sub = Ui.label("", "pageSub") + titlebox.addWidget(self.page_title); titlebox.addWidget(self.page_sub) + header.addLayout(titlebox); header.addStretch() + self.lineage_chip = Ui.label("", "chip") + self.namespace_chip = Ui.label("", "chip") + self.market_chip = Ui.label("", "chip") + self.identity_chip = Ui.label("", "identityChip") + header.addWidget(self.lineage_chip); header.addWidget(self.namespace_chip); header.addWidget(self.market_chip); header.addWidget(self.identity_chip) + bl.addLayout(header) + + self.tape = Ui.label("", "tape") + bl.addWidget(self.tape) + + self.stack = QtWidgets.QStackedWidget() + bl.addWidget(self.stack, 1) + outer.addWidget(body, 1) + + self.pages = {} + self.pages["overview"] = self._overview_page() + self.pages["assets"] = self._assets_page() + self.pages["instruments"] = self._instruments_page() + self.pages["market"] = self._market_page() + self.pages["intelligence"] = self._intelligence_page() + self.pages["orders"] = self._orders_page() + for key, _ in self.NAV: + self.stack.addWidget(self.pages[key]) + + def _overview_page(self): + p = QtWidgets.QWidget(); lay = QtWidgets.QVBoxLayout(p); lay.setContentsMargins(0, 0, 0, 0); lay.setSpacing(14) + cards = QtWidgets.QHBoxLayout(); cards.setSpacing(12) + self.cards = {} + for key, title in [ + ("assets", "Digital Assets"), ("instruments", "Issued Instruments"), ("listings", "Active Listings"), + ("positions", "Market Positions"), ("settled", "30D Settled Volume"), + ]: + c = MetricCard(title); self.cards[key] = c; cards.addWidget(c) + lay.addLayout(cards) + row = QtWidgets.QHBoxLayout(); row.setSpacing(14) + market_box = QtWidgets.QFrame(); market_box.setProperty("class", "panel") + ml = QtWidgets.QVBoxLayout(market_box); ml.setContentsMargins(18, 16, 18, 18) + head = QtWidgets.QHBoxLayout() + head.addWidget(Ui.label("MARKET PULSE", "sectionTitle")); head.addStretch() + b = Ui.button("Open market"); b.clicked.connect(lambda: self.navigate("market")); head.addWidget(b) + ml.addLayout(head) + self.overview_market_stack=QtWidgets.QStackedWidget() + self.overview_market = Ui.table(["TICKER", "RIGHTS", "LAST", "24H VOL", "30D VOL", "BUYERS", "INTEGRITY"]) + self.overview_market_empty=EmptyState( + "NO ACTIVE LISTINGS", + "The universal market is clean. Issue an economic instrument from a controlled DCO, then create a listing to establish a ticker-addressable market." + ) + self.overview_market_stack.addWidget(self.overview_market) + self.overview_market_stack.addWidget(self.overview_market_empty) + ml.addWidget(self.overview_market_stack, 1) + row.addWidget(market_box, 2) + + asset_box = QtWidgets.QFrame(); asset_box.setProperty("class", "panel") + al = QtWidgets.QVBoxLayout(asset_box); al.setContentsMargins(18, 16, 18, 18) + ah = QtWidgets.QHBoxLayout(); ah.addWidget(Ui.label("ASSET PORTFOLIO", "sectionTitle")); ah.addStretch() + ab = Ui.button("+ Ingest asset", True); ab.clicked.connect(self.ingest_asset); ah.addWidget(ab); al.addLayout(ah) + self.overview_assets = Ui.table(["TICKER", "ASSET", "CLASS", "DOMAIN"]) + self.overview_assets.setColumnWidth(0,150) + self.overview_assets.setColumnWidth(1,310) + self.overview_assets.setColumnWidth(2,170) + self.overview_assets.horizontalHeader().setSectionResizeMode(1,QtWidgets.QHeaderView.Stretch) + al.addWidget(self.overview_assets, 1) + row.addWidget(asset_box, 3) + lay.addLayout(row, 1) + + boundary = QtWidgets.QFrame(); boundary.setProperty("class", "notice") + nl = QtWidgets.QHBoxLayout(boundary); nl.setContentsMargins(16, 12, 16, 12) + nl.addWidget(Ui.label("VALUE BOUNDARY", "eyebrow")) + n = Ui.label("ENTITY measures market demand for bounded rights. A rights price is not intrinsic DCO value and is not accounting fair value.", "muted") + n.setWordWrap(True); nl.addWidget(n, 1) + lay.addWidget(boundary) + return p + + def _assets_page(self): + p = QtWidgets.QWidget(); lay = QtWidgets.QVBoxLayout(p); lay.setContentsMargins(0,0,0,0); lay.setSpacing(12) + actions = QtWidgets.QHBoxLayout() + ingest = Ui.button("+ Ingest digital asset", True); ingest.clicked.connect(self.ingest_asset) + issue = Ui.button("Issue economic instrument"); issue.clicked.connect(self.issue_selected_asset) + lineage = Ui.button("Inspect lineage"); lineage.clicked.connect(self.show_asset_lineage) + actions.addWidget(ingest); actions.addWidget(issue); actions.addWidget(lineage); actions.addStretch() + lay.addLayout(actions) + split = QtWidgets.QSplitter() + self.assets_table = Ui.table(["TICKER(S)", "ASSET", "CLASS", "TYPE", "DOMAIN", "PASSPORT", "BTDU", "DCO ID"]) + self.assets_table.itemSelectionChanged.connect(self.update_asset_detail) + split.addWidget(self.assets_table) + self.asset_detail = QtWidgets.QTextBrowser(); self.asset_detail.setProperty("class", "detail") + split.addWidget(self.asset_detail); split.setSizes([1050, 410]) + lay.addWidget(split, 1) + return p + + def _instruments_page(self): + p = QtWidgets.QWidget(); lay = QtWidgets.QVBoxLayout(p); lay.setContentsMargins(0,0,0,0); lay.setSpacing(12) + actions = QtWidgets.QHBoxLayout() + list_btn = Ui.button("+ Create listing", True); list_btn.clicked.connect(self.list_selected_instrument) + actions.addWidget(list_btn); actions.addStretch() + lay.addLayout(actions) + split = QtWidgets.QSplitter() + self.instruments_table = Ui.table(["TICKER", "INSTRUMENT", "RIGHTS", "CLASS", "SUPPLY", "DCO", "STATUS"]) + self.instruments_table.itemSelectionChanged.connect(self.update_instrument_detail) + split.addWidget(self.instruments_table) + self.instrument_detail = QtWidgets.QTextBrowser(); self.instrument_detail.setProperty("class", "detail") + split.addWidget(self.instrument_detail); split.setSizes([1050, 410]) + lay.addWidget(split, 1) + return p + + def _market_page(self): + p = QtWidgets.QWidget(); lay = QtWidgets.QVBoxLayout(p); lay.setContentsMargins(0,0,0,0); lay.setSpacing(12) + actions = QtWidgets.QHBoxLayout() + buy = Ui.button("BUY", True); buy.clicked.connect(lambda: self.trade_selected("BUY")) + sell = Ui.button("SELL"); sell.clicked.connect(lambda: self.trade_selected("SELL")) + info = Ui.button("Listing information"); info.clicked.connect(self.show_listing_info) + export = Ui.button("Export instrument package"); export.clicked.connect(self.export_listing) + actions.addWidget(buy); actions.addWidget(sell); actions.addWidget(info); actions.addWidget(export); actions.addStretch() + lay.addLayout(actions) + split = QtWidgets.QSplitter() + self.market_table = Ui.table(["TICKER", "INSTRUMENT", "ISSUER", "RIGHTS", "LAST", "BID", "ASK", "24H VOL", "30D VOL", "BUYERS", "HOLDERS", "INTEGRITY"]) + self.market_table.itemSelectionChanged.connect(self.update_market_detail) + split.addWidget(self.market_table) + self.market_detail = QtWidgets.QTextBrowser(); self.market_detail.setProperty("class", "detail") + split.addWidget(self.market_detail); split.setSizes([1110, 350]) + lay.addWidget(split, 1) + return p + + def _intelligence_page(self): + p = QtWidgets.QWidget(); lay = QtWidgets.QVBoxLayout(p); lay.setContentsMargins(0,0,0,0); lay.setSpacing(12) + notice = EmptyState("DATA VALUE DISCOVERY", "Settled market activity reveals which bounded rights attract demand. Orders and quotes do not count as realized demand; self-trades and reciprocal flows are flagged.") + lay.addWidget(notice) + splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical) + top = QtWidgets.QFrame(); top.setProperty("class", "panel"); tl = QtWidgets.QVBoxLayout(top) + tl.addWidget(Ui.label("INSTRUMENT RIGHTS DEMAND", "sectionTitle")) + self.intel_table = Ui.table(["TICKER", "RIGHTS", "ASSET CLASS", "LAST", "24H UNITS", "30D UNITS", "30D TRADES", "BUYERS", "HOLDERS", "LOW", "HIGH", "VERIFIED", "INTEGRITY"]) + tl.addWidget(self.intel_table) + splitter.addWidget(top) + bottom = QtWidgets.QFrame(); bottom.setProperty("class", "panel"); bl = QtWidgets.QVBoxLayout(bottom) + bl.addWidget(Ui.label("ASSET-CLASS ROLLUP · 30 DAYS", "sectionTitle")) + self.rollup_table = Ui.table(["ASSET CLASS", "INSTRUMENTS", "TRADES", "UNITS", "NOTIONAL BY CURRENCY"]) + bl.addWidget(self.rollup_table); splitter.addWidget(bottom) + splitter.setSizes([430, 280]); lay.addWidget(splitter, 1) + return p + + def _orders_page(self): + p = QtWidgets.QWidget(); lay = QtWidgets.QVBoxLayout(p); lay.setContentsMargins(0,0,0,0); lay.setSpacing(12) + box = QtWidgets.QFrame(); box.setProperty("class", "panel"); bl = QtWidgets.QVBoxLayout(box) + bl.addWidget(Ui.label("OPEN / RECENT ORDERS", "sectionTitle")) + self.orders_table = Ui.table(["TICKER / INSTRUMENT", "SIDE", "REMAINING", "LIMIT", "STATUS"]) + bl.addWidget(self.orders_table); lay.addWidget(box, 1) + return p + + def navigate(self, key): + idx = list(self.pages).index(key) + self.stack.setCurrentIndex(idx) + for k, b in self.nav_buttons.items(): + b.setChecked(k == key) + titles = { + "overview": ("Portfolio Overview", "Your sovereign asset state and market activity."), + "assets": ("Digital Assets", "DCO holdings, lineage, passports and optional economic issuance."), + "instruments": ("Economic Instruments", "Ticker-addressable bounded rights issued against controlled DCOs."), + "market": ("ENTITY Market", "Universal multi-issuer listings and wallet-to-wallet rights trading."), + "intelligence": ("Economic Intelligence", "Settlement-aware price discovery and rights-demand analytics."), + "orders": ("Orders", "Open and recent exchange instructions."), + } + self.page_title.setText(titles[key][0]); self.page_sub.setText(titles[key][1]) def refresh(self): - if not self.backend: return + self.snapshot_data = self.backend.snapshot() + self.assets_rows = self.snapshot_data.get("assets", []) + self.instruments_rows = self.backend.instruments() + self.market_rows = self.backend.market() + self.metrics_rows = self.backend.economic_metrics() + rollup = self.backend.economy_rollup("asset_class") + amap = self.backend.instrument_map_by_asset() + ns_info=self.backend.namespace_info() + ns=ns_info.get("namespace") or "UNASSIGNED" + venues = self.backend.venues() + + name = self.backend.identity_manifest.get("display_name") or self.entity_id + self.identity_chip.setText(f"{name}\n{shorten(self.entity_id, 12, 8)}") + self.lineage_chip.setText("SHAWN → BTG → ENTITY") + self.namespace_chip.setText(f"NAMESPACE {ns}" + ("" if ns_info.get("registered") else " · RESERVED ON ISSUE")) + self.market_chip.setText(f"ENTITY-MKT · {len(venues)} VENUE{'S' if len(venues)!=1 else ''}") + if self.market_rows: + tape = " ".join( + f"{r['market_identifier']} {r['market'].get('last') if r['market'].get('last') is not None else '—'}" + for r in self.market_rows[:8] + ) + self.tape.setText("MARKET TAPE " + tape) + else: + self.tape.setText("MARKET TAPE NO ACTIVE LISTINGS · instruments remain private until explicitly listed") + + self.cards["assets"].value.setText(str(len(self.assets_rows))) + self.cards["assets"].note.setText("Current DCO holdings") + self.cards["instruments"].value.setText(str(len(self.instruments_rows))) + self.cards["instruments"].note.setText("Canonical economic instruments") + self.cards["listings"].value.setText(str(len(self.market_rows))) + self.cards["listings"].note.setText("Visible market listings") + self.cards["positions"].value.setText(str(len(self.snapshot_data.get("positions", [])))) + self.cards["positions"].note.setText("Active rights positions") + by_currency = {} + for m in self.metrics_rows: + cur = m.get("settlement_currency") or "?" + by_currency[cur] = by_currency.get(cur, 0) + int(m["window_30d"]["notional_amount_units"]) + self.cards["settled"].value.setText(" · ".join(f"{k} {v:,}" for k,v in by_currency.items()) or "—") + self.cards["settled"].note.setText("Settled instrument-right notional") + + asset_table_rows = [] + overview_assets = [] + for a in self.assets_rows: + instruments = amap.get(a["object_id"], []) + tickers = ", ".join(i["market_identifier"] for i in instruments) or "NO TICKER" + lin = a.get("lineage", {}) + domain = (lin.get("protocol_lineage", {}) or {}).get("primary_domain_profile") or "—" + gp = lin.get("global_passport") or {} + btdu = (lin.get("capability_bindings", {}).get("BTDU", {}) or {}).get("bound", False) + asset_table_rows.append([ + tickers, a.get("title"), a.get("commodity_class"), a.get("object_type"), domain, + gp.get("version") or "—", "BOUND" if btdu else "—", a.get("object_id") + ]) + overview_assets.append([tickers, a.get("title"), a.get("commodity_class"), domain]) + Ui.fill_table(self.assets_table, asset_table_rows) + Ui.fill_table(self.overview_assets, overview_assets[:8]) + + inst_rows = [] + for x in self.instruments_rows: + inst_rows.append([ + x["market_identifier"], x["instrument_name"], x["rights_class"], x["instrument_class"], + x["supply"], shorten(x["underlying_dco_id"], 12, 8), x["status"] + ]) + Ui.fill_table(self.instruments_table, inst_rows) + + metrics_by_iid = {m["instrument_id"]: m for m in self.metrics_rows} + market_rows = [] + overview_market = [] + for r in self.market_rows: + mkt = r["market"]; im = metrics_by_iid.get(r["instrument_id"], {}) + w24 = im.get("window_24h", {}); w30 = im.get("window_30d", {}) + flags = ", ".join((im.get("market_integrity") or {}).get("flags") or []) or "CLEAN" + vals = [ + r["market_identifier"], r["instrument_name"], shorten(r["issuer_entity_id"], 12, 8), + r["rights_class"], mkt.get("last") or "—", mkt.get("bid") or "—", mkt.get("ask") or "—", + w24.get("volume_units", 0), w30.get("volume_units", 0), w30.get("unique_buyers", 0), + im.get("active_holder_count", 0), flags + ] + market_rows.append(vals) + overview_market.append([r["market_identifier"], r["rights_class"], mkt.get("last") or "—", + w24.get("volume_units", 0), w30.get("volume_units", 0), + w30.get("unique_buyers", 0), flags]) + Ui.fill_table(self.market_table, market_rows) + Ui.fill_table(self.overview_market, overview_market[:8]) + self.overview_market_stack.setCurrentIndex(0 if overview_market else 1) + + intel_rows = [] + for m in self.metrics_rows: + w = m["window_30d"] + flags = ", ".join(m["market_integrity"]["flags"]) or "CLEAN" + intel_rows.append([ + m["market_identifier"], m["rights_class"], m["asset_class"], m["last_settled_price"] or "—", + m["window_24h"]["volume_units"], w["volume_units"], w["trade_count"], w["unique_buyers"], + m["active_holder_count"], w["low_price"] or "—", w["high_price"] or "—", + w["externally_verified_trade_count"], flags + ]) + Ui.fill_table(self.intel_table, intel_rows) + Ui.fill_table(self.rollup_table, [ + [r["group"], r["instrument_count"], r["settled_trades_30d"], r["volume_units_30d"], + ", ".join(f"{k} {v:,}" for k,v in r["notional_30d_by_currency"].items()) or "—"] + for r in rollup.get("rows", []) + ]) + + Ui.fill_table(self.orders_table, [ + [o["instrument_id"], o["side"], o["remaining"], o["limit_price"], o["status"]] + for o in self.snapshot_data.get("orders", []) + ]) + if self.assets_rows: + self.assets_table.selectRow(0) + if self.instruments_rows: + self.instruments_table.selectRow(0) + if self.market_rows: + self.market_table.selectRow(0) + + def selected_asset(self): + row = self.assets_table.currentRow() + return self.assets_rows[row] if 0 <= row < len(self.assets_rows) else None + + def selected_instrument(self): + row = self.instruments_table.currentRow() + return self.instruments_rows[row] if 0 <= row < len(self.instruments_rows) else None + + def selected_market(self): + row = self.market_table.currentRow() + return self.market_rows[row] if 0 <= row < len(self.market_rows) else None + + def update_asset_detail(self): + a = self.selected_asset() + if not a: + self.asset_detail.setHtml("

No asset selected

"); return + lin = a.get("lineage", {}); p = lin.get("protocol_lineage", {}) or {} + al = lin.get("asset_lineage", {}) or {}; cap = lin.get("capability_bindings", {}) or {} + instruments = self.backend.instrument_map_by_asset().get(a["object_id"], []) + tickers = "
".join(i["market_identifier"] for i in instruments) or "NO ECONOMIC INSTRUMENT ISSUED" + parents = "
".join( + f"{x.get('relation')} · {x.get('title') or shorten(x.get('parent_object_id'))}" + for x in al.get("parents", []) + ) or "None recorded" + self.asset_detail.setHtml(f""" +
DIGITAL COMMODITY OBJECT
+

{a.get('title')}

+
{tickers}
+
+ Canonical lineage
{p.get('display_path') or 'Unresolved'}

+ Controller
{al.get('controller_name') or a.get('controller_entity_id')}

+ DCO ID
{a.get('object_id')}

+ Commodity class
{a.get('commodity_class')}

+ Object type
{a.get('object_type')}

+ Composed profiles
{', '.join(p.get('composed_domain_profiles') or []) or 'Primary domain only'}

+ BTDU
{'Bound' if (cap.get('BTDU') or {}).get('bound') else 'Not bound'}

+ Provenance parents
{parents}

+ Asset registration does not create a ticker, instrument, listing or market value. + """) + + def update_instrument_detail(self): + x = self.selected_instrument() + if not x: + self.instrument_detail.setHtml("

No instrument selected

"); return + self.instrument_detail.setHtml(f""" +
CANONICAL ECONOMIC INSTRUMENT
+
{x['market_identifier']}
+

{x['instrument_name']}


+ Canonical instrument ID
{x['instrument_id']}

+ Underlying DCO
{x['underlying_dco_id']}

+ Rights class
{x['rights_class']}

+ Instrument class
{x['instrument_class']}

+ Supply
{x['supply']:,}

+ Fungibility
{x['fungibility']}

+ Jurisdiction
{x['jurisdiction']}

+ Rights Passport
{x['rights_passport_id']}

+ The ticker is a human market alias. The canonical instrument ID remains authoritative. + """) + + def update_market_detail(self): + r = self.selected_market() + if not r: + self.market_detail.setHtml("

No active listing

Assets remain private until an issuer explicitly creates an instrument and listing.

") + return + m = r["market"] + im = next((x for x in self.metrics_rows if x["instrument_id"] == r["instrument_id"]), {}) + w = im.get("window_30d", {}) + flags = ", ".join((im.get("market_integrity") or {}).get("flags") or []) or "CLEAN" + self.market_detail.setHtml(f""" +
ENTITY MARKET
+
{r['market_identifier']}
+

{r['instrument_name']}


+ + + + + + + + +
LAST{m.get('last') or '—'}
BID{m.get('bid') or '—'}
ASK{m.get('ask') or '—'}
30D UNITS{w.get('volume_units', 0):,}
30D TRADES{w.get('trade_count', 0):,}
BUYERS{w.get('unique_buyers', 0):,}
HOLDERS{im.get('active_holder_count', 0):,}

+ Rights
{r['rights_class']}

+ Issuer
{r['issuer_entity_id']}

+ Integrity
{flags}

+ Observed price describes this instrument's bounded rights under its terms—not intrinsic DCO value. + """) + + def ingest_asset(self): + path = QtWidgets.QFileDialog.getOpenFileName(self, "Choose digital asset to ingest")[0] + if not path: + return + d = AssetIngestDialog(self, Path(path), self.backend) + if d.exec() != QtWidgets.QDialog.Accepted: + return try: - snap=self.backend.snapshot(); self.market_rows=self.backend.market(); instruments=self.backend.instruments() - metrics=self.backend.economic_metrics(); rollup=self.backend.economy_rollup("asset_class") - self.asset_rows={}; self.instrument_rows={} - self.clear(self.assets) - for a in snap.get("assets",[]): - lin=a.get("lineage",{}); prot=lin.get("protocol_lineage",{}); al=lin.get("asset_lineage",{}) - gp=lin.get("global_passport") or {}; btdu=lin.get("capability_bindings",{}).get("BTDU",{}).get("bound",False) - self.asset_rows[a["object_id"]]=a; self.assets.insert("", "end",iid=a["object_id"],values=( - a["title"],a.get("commodity_class"),a.get("object_type"),prot.get("display_path") or "UNRESOLVED", - al.get("controller_name") or a["controller_entity_id"],gp.get("version") or "—","YES" if btdu else "NO",a["object_id"])) - self.clear(self.instruments) - for x in instruments: - self.instrument_rows[x["instrument_id"]]=x; self.instruments.insert("", "end",iid=x["instrument_id"],values=( - x["market_identifier"],x["instrument_name"],x["rights_class"],x["instrument_class"],x["supply"],x["underlying_dco_id"],x["status"])) - self.clear(self.intel) - for m in metrics: - w=m["window_30d"]; flags=", ".join(m["market_integrity"]["flags"]) or "CLEAN / NO FLAGS" - self.intel.insert("", "end",values=( - m["market_identifier"],m["rights_class"],m["asset_class"],m["last_settled_price"], - m["window_24h"]["volume_units"],w["volume_units"],w["trade_count"],w["unique_buyers"], - m["active_holder_count"],w["low_price"],w["high_price"], - w["externally_verified_trade_count"],flags)) - self.clear(self.rollup) - for row in rollup.get("rows",[]): - notionals=", ".join(f"{k} {v}" for k,v in row["notional_30d_by_currency"].items()) or "—" - self.rollup.insert("", "end",values=(row["group"],row["instrument_count"], - row["settled_trades_30d"],row["volume_units_30d"],notionals)) - self.clear(self.positions) - for p in snap.get("positions",[]): - m=p["market"]; self.positions.insert("", "end",values=(p["instrument_id"],p["units"],p["settlement_currency"],m.get("last"),m.get("bid"),m.get("ask"),p.get("market_value_amount_units"))) - self.clear(self.orders) - for o in snap.get("orders",[]): self.orders.insert("", "end",values=(o["instrument_id"],o["side"],o["remaining"],o["limit_price"],o["status"])) - self.clear(self.market) - for i,row in enumerate(self.market_rows): - intel=row.get("intelligence") or {}; w24=intel.get("window_24h") or {}; w30=intel.get("window_30d") or {} - flags=(intel.get("market_integrity") or {}).get("flags") or [] - self.market.insert("", "end",iid=str(i),values=( - row["market_identifier"],row["instrument_name"],row["rights_class"],row["settlement_currency"], - intel.get("last_settled_price"),w24.get("volume_units"),w30.get("volume_units"),w30.get("unique_buyers"), - intel.get("active_holder_count"),", ".join(flags) if flags else "CLEAR",row["venue_id"])) - self.refresh_intelligence() - self.status.set(f"{len(snap.get('assets',[]))} assets · {len(instruments)} instruments · {len(self.market_rows)} listings · {len(metrics)} demand series · protocol tax 0 · crypto not required") + result = self.backend.ingest(path, d.result_data) + self.refresh() + QtWidgets.QMessageBox.information( + self, "Digital asset registered", + f"DCO\n{result['digital_asset']['object_id']}\n\n" + f"Global Passport verified: {result['global_passport_verified']}\n" + f"BTDU bound: {bool(result['btdu_binding'])}\n\n" + "No economic instrument, ticker, listing or price was created." + ) except Exception as e: - messagebox.showerror("Refresh failed",str(e)); self.status.set(str(e)) + QtWidgets.QMessageBox.critical(self, "Asset ingest failed", str(e)) - def refresh_intelligence(self): - if not self.backend: return + def issue_selected_asset(self): + asset = self.selected_asset() + if not asset: + QtWidgets.QMessageBox.warning(self, "Instrument", "Select a controlled DCO first."); return + d = InstrumentDialog(self, asset, self.backend.namespace()) + if d.exec() != QtWidgets.QDialog.Accepted: + return try: - roll=self.backend.economy_rollup(self.rollup_dimension.get()) - self.clear(self.rollups) - for row in roll.get("rows",[]): - notional=", ".join(f"{cur} {amt:,}" for cur,amt in sorted(row.get("notional_30d_by_currency",{}).items())) or "—" - self.rollups.insert("", "end",values=(row["group"],row["instrument_count"],row["settled_trades_30d"], - row["volume_units_30d"],notional,row["sum_instrument_unique_buyers_30d"])) + result = self.backend.create_instrument(asset["object_id"], d.result_data) + self.refresh() + QtWidgets.QMessageBox.information( + self, "Economic instrument issued", + f"{result['market_identifier']}\n\nCanonical ID\n{result['instrument_id']}\n\n" + "The instrument is not listed and no market value was created." + ) except Exception as e: - self.status.set("Economic intelligence: "+str(e)) + QtWidgets.QMessageBox.critical(self, "Instrument creation failed", str(e)) - def view_market_intelligence(self): - row=self._selected_market_row() - if not row: messagebox.showwarning("Market Intelligence","Select a market listing."); return + def list_selected_instrument(self): + instrument = self.selected_instrument() + if not instrument: + QtWidgets.QMessageBox.warning(self, "Listing", "Select one of your economic instruments."); return + d = ListingDialog(self, self.backend, instrument) + if d.exec() != QtWidgets.QDialog.Accepted: + return try: - m=row.get("intelligence") or self.backend.intelligence.instrument_metrics(row["instrument_id"]) - w24=m["window_24h"]; w30=m["window_30d"]; roy=m["royalties_and_participation"] - rpaid=", ".join(f"{k} {v:,}" for k,v in sorted(roy["externally_verified_amounts_by_currency"].items())) or "None verified" - flags="\n".join("• "+x for x in m["market_integrity"]["flags"]) or "• No current integrity flags" - messagebox.showinfo("ENTITY Market Intelligence", - f"{m['market_identifier']}\n{m['instrument_name']}\n\n" - f"Last settled price: {m['last_settled_price']} {m['settlement_currency']}\n" - f"Last externally verified price: {m['last_externally_verified_price']} {m['settlement_currency']}\n\n" - f"24h settled volume: {w24['volume_units']:,} units / {w24['notional_amount_units']:,} {m['settlement_currency']}\n" - f"30d settled volume: {w30['volume_units']:,} units / {w30['notional_amount_units']:,} {m['settlement_currency']}\n" - f"30d high / low: {w30['high_price']} / {w30['low_price']}\n30d trades: {w30['trade_count']:,}\n" - f"Unique buyers: {w30['unique_buyers']:,}\nActive holders: {m['active_holder_count']:,}\n" - f"Open sell offers: {m['open_sell_offer_units']:,} units\nExternally verified royalties/participation: {rpaid}\n\n" - f"MARKET INTEGRITY\n{flags}\n\n" - "These observations value the traded rights under their terms; they do not prove intrinsic or accounting value of the underlying DCO.") - except Exception as e: messagebox.showerror("Market Intelligence",str(e)) - - def view_rights_demand(self): - if not self.backend: return - sel=self.assets.selection() - if not sel: messagebox.showwarning("Rights Demand","Select a digital asset."); return - try: - report=self.backend.rights_demand(sel[0]); lines=[] - for rights,row in report.get("rights_class_rollup",{}).items(): - notion=", ".join(f"{cur} {amt:,}" for cur,amt in sorted(row["notional_30d_by_currency"].items())) or "—" - lines.append(f"{rights}: {row['volume_units_30d']:,} units · {row['settled_trades']:,} trades · {notion}") - messagebox.showinfo("DCO Rights Demand","\n".join(lines) if lines else "No active economic instruments or settled demand for this DCO yet.") - except Exception as e: messagebox.showerror("Rights Demand",str(e)) - - def view_lineage(self): - sel=self.assets.selection() - if not sel: messagebox.showwarning("Lineage","Select a digital asset."); return - a=self.asset_rows.get(sel[0]); lin=(a or {}).get("lineage",{}); p=lin.get("protocol_lineage",{}); al=lin.get("asset_lineage",{}); cap=lin.get("capability_bindings",{}); gp=lin.get("global_passport") or {} - parents="\n".join(f" {x.get('relation')}: {x.get('title') or x.get('parent_object_id')}" for x in al.get("parents",[])) or " None recorded" - messagebox.showinfo("ENTITY Lineage",f"CANONICAL PROTOCOL / DOMAIN LINEAGE\n{p.get('display_path') or 'Unresolved'}\nAssociated profiles: {', '.join(p.get('associated_profiles') or []) or 'None'}\n\nPassport: {gp.get('passport_id') or 'None'} version {gp.get('version') or '—'}\nOrigin embedded: {p.get('passport_protocol_origin_embedded')}\n\nASSET PROVENANCE\nController: {al.get('controller_name')}\nParents:\n{parents}\n\nSYSTEM BINDINGS\nBTDU bound: {cap.get('BTDU',{}).get('bound')}\nADAM: deterministic execution/evidence; not ownership\nNIKI: bounded reasoning; not ownership\n\nProtocol ancestry does not transfer ownership or create economic entitlement.") + result = self.backend.create_listing(instrument["instrument_id"], d.result_data) + self.refresh() + QtWidgets.QMessageBox.information( + self, "Listing created", + f"{instrument['market_identifier']}\n\nListing ID\n{result['listing_id']}\n\n" + "Listing Information Sheet and machine manifest are hash-bound." + ) + except Exception as e: + QtWidgets.QMessageBox.critical(self, "Listing creation failed", str(e)) - def ingest_asset(self): - if not self.backend: return - f=filedialog.askopenfilename(title="Choose data or digital asset to ingest") - if not f: return - d=AssetDialog(self,Path(f),self.backend); self.wait_window(d) - if not d.result: return - try: - r=self.backend.ingest(f,d.result); self.refresh(); a=r["digital_asset"]; origin=r["protocol_origin"] - messagebox.showinfo("Canonical digital asset created",f"DCO: {a['object_id']}\nOrigin: {origin['origin_lineage_id']} → {origin['release_ref']}\nGlobal Passport verified: {r['global_passport_verified']}\nBTDU bound: {bool(r['btdu_binding'])}\n\nNo instrument, listing, or price was created.") - except Exception as e: messagebox.showerror("Ingest failed",str(e)) - - def create_instrument(self): - if not self.backend: return - sel=self.assets.selection() - if not sel: messagebox.showwarning("Instrument","Select a controlled digital asset first."); return - asset=self.asset_rows[sel[0]]; d=InstrumentDialog(self,asset); self.wait_window(d) - if not d.result: return - try: - result=self.backend.create_instrument(asset["object_id"],d.result); self.refresh() - messagebox.showinfo("Instrument created",f"{result['market_identifier']}\n\nCanonical ID:\n{result['instrument_id']}\n\nNot listed. No market value was created.") - except Exception as e: messagebox.showerror("Instrument creation failed",str(e)) - - def create_listing(self): - if not self.backend: return - sel=self.instruments.selection() - if not sel: messagebox.showwarning("Listing","Select one of your instruments."); return - instrument=self.instrument_rows[sel[0]]; d=ListingDialog(self,self.backend,instrument); self.wait_window(d) - if not d.result: return - try: - result=self.backend.create_listing(instrument["instrument_id"],d.result); self.refresh() - messagebox.showinfo("Listing created",f"{instrument['market_identifier']} is listed.\n\nListing ID:\n{result['listing_id']}\n\nRequired Listing Information Sheet and machine manifest were bound by hash.") - except Exception as e: messagebox.showerror("Listing creation failed",str(e)) - - def _selected_market_row(self): - sel=self.market.selection() - if not sel: return None - return self.market_rows[int(sel[0])] - - def view_listing(self): - row=self._selected_market_row() - if not row: messagebox.showwarning("Listing Information","Select a market listing."); return + def show_asset_lineage(self): + a = self.selected_asset() + if not a: + return + lin = a.get("lineage", {}) + p = lin.get("protocol_lineage", {}) or {} + al = lin.get("asset_lineage", {}) or {} + parents = "\n".join( + f"{x.get('relation')}: {x.get('title') or x.get('parent_object_id')}" + for x in al.get("parents", []) + ) or "None" + QtWidgets.QMessageBox.information( + self, "ENTITY lineage", + f"CANONICAL PROTOCOL / DOMAIN LINEAGE\n{p.get('display_path') or 'Unresolved'}\n\n" + f"ASSET CONTROLLER\n{al.get('controller_name') or al.get('controller_entity_id')}\n\n" + f"PROVENANCE PARENTS\n{parents}\n\n" + "ADAM, NIKI and BTDU are capability/evidence bindings and do not create ownership." + ) + + def show_listing_info(self): + r = self.selected_market() + if not r: + QtWidgets.QMessageBox.warning(self, "Listing", "No active market listing selected."); return try: - listing=self.backend.market_registry.listing(row["listing_id"]); info=listing["information"] - receives="\n".join("• "+x for x in info["buyer_receives"]) or "• See Rights Passport" - excludes="\n".join("• "+x for x in info["buyer_does_not_receive"]) or "• No unstated rights" - messagebox.showinfo("Listing Information",f"WHAT IS THIS?\n\n{info['what_is_this']}\n\nIssuer: {info['issuer']}\nMarket ID: {info['market_identifier']}\nUnderlying DCO: {info['underlying_dco_id']}\n\nBUYER RECEIVES\n{receives}\n\nBUYER DOES NOT RECEIVE\n{excludes}\n\nSupply: {info['supply']}\nJurisdiction: {info['jurisdiction']}\nPricing: {info['pricing_method']}\nRights Passport: {info['rights_passport_id']}\nGlobal Passport: {info['global_passport_id']}\n\nInstrument: {info['instrument_id']}\nListing: {info['listing_id']}") - except Exception as e: messagebox.showerror("Listing Information",str(e)) + listing = self.backend.market_registry.listing(r["listing_id"]) + info = listing["information"] + receives = "\n".join("• " + x for x in info["buyer_receives"]) or "• See Rights Passport" + excludes = "\n".join("• " + x for x in info["buyer_does_not_receive"]) or "• No unstated rights" + QtWidgets.QMessageBox.information( + self, "Listing Information Sheet", + f"{info['market_identifier']}\n{info['instrument_name']}\n\n" + f"Issuer: {info['issuer']}\nUnderlying DCO: {info['underlying_dco_id']}\n\n" + f"BUYER RECEIVES\n{receives}\n\nBUYER DOES NOT RECEIVE\n{excludes}\n\n" + f"Supply: {info['supply']}\nJurisdiction: {info['jurisdiction']}\n" + f"Pricing: {info['pricing_method']}\n\nInstrument: {info['instrument_id']}\n" + f"Listing: {info['listing_id']}" + ) + except Exception as e: + QtWidgets.QMessageBox.critical(self, "Listing Information", str(e)) def export_listing(self): - row=self._selected_market_row() - if not row: messagebox.showwarning("Portable package","Select a market listing."); return - dest=filedialog.askdirectory(title="Choose folder for portable ENTITY instrument package") - if not dest: return + r = self.selected_market() + if not r: + QtWidgets.QMessageBox.warning(self, "Export", "No active market listing selected."); return + dest = QtWidgets.QFileDialog.getExistingDirectory(self, "Choose export folder") + if not dest: + return try: - instrument=self.backend.market_registry.instrument(row["instrument_id"]) - folder=Path(dest)/(instrument["market_identifier"].replace(":","-")+".entity-instrument") - result=self.backend.market_registry.export_portable_package(row["listing_id"],folder) - messagebox.showinfo("Portable package created",f"{result['market_identifier']}\n\n{result['destination']}\n\nIncludes PDF, Markdown, JSON passports/manifests, verification and SHA256SUMS.") - except Exception as e: messagebox.showerror("Portable package",str(e)) - - def order(self,side): - if not self.backend: return - row=self._selected_market_row() - if not row: messagebox.showwarning("Market","Select a market instrument."); return - d=OrderDialog(self,row,side); self.wait_window(d) - if not d.result: return + instrument = self.backend.market_registry.instrument(r["instrument_id"]) + folder = Path(dest) / (instrument["market_identifier"].replace(":", "-") + ".entity-instrument") + result = self.backend.market_registry.export_portable_package(r["listing_id"], folder) + QtWidgets.QMessageBox.information( + self, "Portable instrument package", + f"{result['market_identifier']}\n\n{result['destination']}\n\n" + "Includes PDF, Markdown, JSON manifests/passports, verification and SHA256SUMS." + ) + except Exception as e: + QtWidgets.QMessageBox.critical(self, "Export failed", str(e)) + + def trade_selected(self, side): + r = self.selected_market() + if not r: + QtWidgets.QMessageBox.warning(self, "Market", "No active market listing selected."); return + d = OrderDialog(self, r, side) + if d.exec() != QtWidgets.QDialog.Accepted: + return try: - q,p=d.result; result=self.backend.submit_order(row,side,q,p); self.refresh() - messagebox.showinfo("Order submitted",f"Order accepted.\nImmediate matches: {len(result['trades'])}\n\nMatched trades remain subject to clearing and settlement requirements.") - except Exception as e: messagebox.showerror("Order failed",str(e)) + qty, price = d.result_data + result = self.backend.submit_order(r, side, qty, price) + self.refresh() + QtWidgets.QMessageBox.information( + self, "Order submitted", + f"Order accepted.\nImmediate matches: {len(result['trades'])}\n\n" + "Matched trades remain subject to clearing and settlement evidence." + ) + except Exception as e: + QtWidgets.QMessageBox.critical(self, "Order failed", str(e)) + + def _apply_style(self): + self.setStyleSheet(f""" + * {{ + font-family: "Segoe UI"; + font-size: 10pt; + color: {TEXT}; + }} + QMainWindow, QStackedWidget {{ background: {ROOT_BG}; }} + QWidget {{ color: {TEXT}; }} + QLabel {{ background: transparent; }} + #sidebar {{ background: {SIDEBAR_BG}; border-right: 1px solid {BORDER}; }} + QLabel[class="brand"] {{ font-size: 26pt; font-weight: 800; letter-spacing: 2px; color: {TEXT}; }} + QLabel[class="brandSub"] {{ font-size: 8pt; font-weight: 700; letter-spacing: 2px; color: {ACCENT}; }} + QLabel[class="pageTitle"] {{ font-size: 22pt; font-weight: 700; }} + QLabel[class="pageSub"], QLabel[class="muted"], QLabel[class="monoMuted"] {{ color: {MUTED}; }} + QLabel[class="monoMuted"] {{ font-family: Consolas; }} + QLabel[class="eyebrow"] {{ color: {ACCENT}; font-size: 8pt; font-weight: 700; letter-spacing: 1px; }} + QLabel[class="sectionTitle"] {{ font-size: 11pt; font-weight: 700; letter-spacing: 1px; }} + QLabel[class="metric"] {{ font-size: 20pt; font-weight: 750; }} + QLabel[class="emptyTitle"] {{ font-size: 15pt; font-weight: 700; }} + QLabel[class="dialogTitle"] {{ font-size: 18pt; font-weight: 700; }} + QLabel[class="tickerHero"] {{ color: {ACCENT}; font-family: Consolas; font-size: 18pt; font-weight: 700; }} + QLabel[class="tickerPreview"] {{ background: {SIDEBAR_BG}; border: 1px solid {ACCENT_DARK}; border-radius: 6px; padding: 10px; color: {ACCENT}; font-family: Consolas; font-weight: 700; }} + QLabel[class="chip"] {{ background: {PANEL_2}; border: 1px solid {BORDER}; border-radius: 6px; padding: 7px 11px; color: {MUTED}; font-size: 8pt; font-weight: 700; }} + QLabel[class="identityChip"] {{ background: {PANEL_BG}; border: 1px solid {BORDER}; border-radius: 6px; padding: 6px 11px; font-size: 8pt; }} + QLabel[class="tape"] {{ background: {SIDEBAR_BG}; border: 1px solid {BORDER}; padding: 9px 12px; color: {ACCENT}; font-family: Consolas; font-size: 9pt; }} + QLabel[class="sidebarFoot"] {{ color: {MUTED}; font-size: 8pt; line-height: 1.4; }} + QFrame[class="panel"], QFrame[class="metricCard"], QFrame[class="empty"] {{ + background: {PANEL_BG}; border: 1px solid {BORDER}; border-radius: 8px; + }} + QFrame[class="notice"] {{ background: {PANEL_2}; border: 1px solid {ACCENT_DARK}; border-radius: 7px; }} + QPushButton {{ background: {PANEL_2}; border: 1px solid {BORDER}; border-radius: 6px; padding: 8px 13px; font-weight: 600; }} + QPushButton:hover {{ border-color: {ACCENT_DARK}; background: #1A2027; }} + QPushButton[primary="true"] {{ background: {ACCENT}; color: #0C0E11; border-color: {ACCENT}; font-weight: 800; }} + QPushButton[nav="true"] {{ background: transparent; border: none; text-align: left; padding: 10px 10px; color: {MUTED}; font-size: 9pt; font-weight: 650; }} + QPushButton[nav="true"]:hover {{ background: {PANEL_BG}; color: {TEXT}; }} + QPushButton[nav="true"]:checked {{ background: {PANEL_2}; color: {TEXT}; border-left: 3px solid {ACCENT}; }} + QTableWidget {{ background: {PANEL_BG}; alternate-background-color: {PANEL_BG}; border: 1px solid {BORDER}; border-radius: 7px; selection-background-color: #2A251A; selection-color: {TEXT}; }} + QHeaderView::section {{ background: {SIDEBAR_BG}; color: {MUTED}; border: none; border-bottom: 1px solid {BORDER}; padding: 9px; font-size: 8pt; font-weight: 700; }} + QTableWidget::item {{ border-bottom: 1px solid #181D23; padding: 7px; }} + QTableWidget::item:selected {{ background: #2A251A; color: {TEXT}; }} + QScrollBar:vertical {{ width: 10px; background: {ROOT_BG}; }} + QScrollBar::handle:vertical {{ background: #303844; min-height: 30px; border-radius: 4px; }} + QLineEdit, QComboBox, QSpinBox, QPlainTextEdit {{ + background: {PANEL_2}; border: 1px solid {BORDER}; border-radius: 5px; padding: 7px; selection-background-color: {ACCENT_DARK}; + }} + QLineEdit:focus, QComboBox:focus, QSpinBox:focus, QPlainTextEdit:focus {{ border-color: {ACCENT}; }} + QDialog {{ background: {ROOT_BG}; }} + QTextBrowser[class="detail"] {{ background: {PANEL_BG}; border: 1px solid {BORDER}; border-radius: 7px; padding: 14px; }} + QSplitter::handle {{ background: {ROOT_BG}; width: 8px; height: 8px; }} + QToolTip {{ background: {PANEL_2}; color: {TEXT}; border: 1px solid {BORDER}; }} + """) + +def self_test(state: Path, entity_id: str) -> dict: + backend = Backend(state, entity_id) + snap = backend.snapshot() + return { + "ready": True, "ui_engine": "PySide6", "design": "ENTITY_DATA_ECONOMY_TERMINAL", + "entity_id": entity_id, "assets": len(snap.get("assets", [])), + "instruments": len(backend.instruments()), "positions": len(snap.get("positions", [])), + "orders": len(snap.get("orders", [])), "venues": len(backend.venues()), + "market_listings": len(backend.market()), "economic_demand_series": len(backend.economic_metrics()), + "issuer_namespace": backend.namespace(), "protocol_tax_bps": 0, "cryptocurrency_required": False, + } def main(): - ap=argparse.ArgumentParser(description="Native ENTITY economic wallet") + ap = argparse.ArgumentParser(description="ENTITY Data Economy Terminal") ap.add_argument("--state"); ap.add_argument("--entity") - ap.add_argument("--self-test",action="store_true") - ap.add_argument("--self-test-output") - args=ap.parse_args() + ap.add_argument("--self-test", action="store_true"); ap.add_argument("--self-test-output") + ap.add_argument("--render-preview") + args = ap.parse_args() + state = Path(args.state) if args.state else default_state() + if not args.entity: + ms = manifests(state) + if not ms: + raise SystemExit("No ENTITY identity found") + args.entity = ms[0]["entity_id"] if args.self_test: - if not args.state or not args.entity: - raise SystemExit("--self-test requires --state and --entity") - backend=Backend(Path(args.state),args.entity) - snap=backend.snapshot() - report={"ready":True,"entity_id":args.entity, - "assets":len(snap.get("assets",[])), - "positions":len(snap.get("positions",[])), - "orders":len(snap.get("orders",[])), - "venues":len(backend.venues()), - "market_listings":len(backend.market()), - "economic_demand_series":len(backend.economic_metrics()), - "protocol_tax_bps":0,"cryptocurrency_required":False} - raw=json.dumps(report,indent=2,sort_keys=True) + report = self_test(state, args.entity) + raw = json.dumps(report, indent=2, sort_keys=True) if args.self_test_output: - Path(args.self_test_output).write_text(raw+"\n",encoding="utf-8") + Path(args.self_test_output).write_text(raw + "\n", encoding="utf-8") else: print(raw) return - App(Path(args.state) if args.state else None,args.entity).mainloop() + app = QtWidgets.QApplication(sys.argv) + app.setApplicationName(APP_NAME) + app.setStyle("Fusion") + window = WalletWindow(state, args.entity) + window.show() + if args.render_preview: + app.processEvents() + QtCore.QTimer.singleShot(300, lambda: ( + window.grab().save(str(Path(args.render_preview))), + app.quit() + )) + sys.exit(app.exec()) -if __name__=="__main__": main() +if __name__ == "__main__": + main() From 836962bf95715e8c821ff0fa98a2c60ca3ba84d2 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 09:30:12 -0700 Subject: [PATCH 104/111] wallet: require signed asset disclosures for market listings --- docs/v3.4/ENTITY_WALLET_20261002.md | 32 ++ .../canonical_market_registry.py | 357 +++++++++++++++++- tests/test_entity_market_registry.py | 39 +- tools/entity_wallet_desktop.py | 286 ++++++++++++-- 4 files changed, 664 insertions(+), 50 deletions(-) diff --git a/docs/v3.4/ENTITY_WALLET_20261002.md b/docs/v3.4/ENTITY_WALLET_20261002.md index 149a26a2..70126a99 100644 --- a/docs/v3.4/ENTITY_WALLET_20261002.md +++ b/docs/v3.4/ENTITY_WALLET_20261002.md @@ -172,3 +172,35 @@ The PySide6 build continues to preserve: - BTDU, ADAM and NIKI do not create ownership. The 2026-10-03 desktop/economy/lineage regression campaign passed **84/84** before the Qt package was built. The packaged Windows executable then passed a production-state frozen-runtime self-test with exit code 0. + + +## Signed Asset Disclosure and Listing Information Sheet v2 — 2026-10-03 + +A market listing now requires a **signed, versioned Asset Disclosure** for the underlying DCO. The disclosure belongs to the asset, not to a ticker, and can therefore support multiple economic instruments without duplicating the buyer-facing description. + +The production path is: + +DCO → signed Asset Disclosure → Rights Passport / Global Passport → Economic Instrument → Listing → Listing Information Sheet v2 + +The Asset Disclosure supports: + +- detailed asset description; +- purpose / problem addressed; +- key capabilities; +- included content or components; +- intended uses; +- known limitations and exclusions; +- dependencies / prerequisites; +- validation and qualification notes; +- release/version notes; +- supporting source references. + +Issuer-authored disclosure fields are signed issuer statements. ENTITY records and verifies the statement but does not convert it into objective truth or independent certification. Publishing an Asset Disclosure does not expand a Rights Passport and does not create economic value. + +The Listing Information Sheet v2 snapshots the current signed Asset Disclosure and combines it with canonical DCO facts from the asset record and Global Passport, including DCO ID/code, version, object type, commodity class, measurement unit, content hash, controller, jurisdiction profiles, canonical technical metadata, industry context, profile stack, standards mappings, validation/maturity flags, provenance edges and references, evidence references, BTDU binding, rights terms, market terms and the market-value boundary. + +A later Asset Disclosure update does not silently rewrite an already-created listing sheet; the listing retains the disclosure/dossier hashes that were shown to the buyer. + +Portable instrument packages now include `asset-dossier.json` and, when present, `asset-disclosure.json` in addition to the human Listing Information PDF/Markdown and existing canonical manifests/passports. + +The wallet shows Asset Information status directly in the DCO portfolio. A DCO without a signed disclosure is marked **REQUIRED TO LIST**. The two current Robotics DCOs were issued signed Asset Disclosure v1 records using facts already present in their canonical records; no new performance claims were inferred. diff --git a/src/45_ENTITY_Market/canonical_market_registry.py b/src/45_ENTITY_Market/canonical_market_registry.py index fd235024..37f63a8a 100644 --- a/src/45_ENTITY_Market/canonical_market_registry.py +++ b/src/45_ENTITY_Market/canonical_market_registry.py @@ -5,7 +5,7 @@ import hashlib, json, re, sqlite3, textwrap, time MARKET_SCHEMA="entity-economic-market-registry-v1" -MARKET_VERSION="1.0.0" +MARKET_VERSION="1.1.0" def now_ms()->int: return int(time.time()*1000) def canon(v:Any)->bytes: return json.dumps(v,sort_keys=True,separators=(",",":"),ensure_ascii=False,default=str).encode() @@ -151,7 +151,15 @@ def _init(self): CREATE TABLE IF NOT EXISTS listing_information( listing_id TEXT PRIMARY KEY, information_json TEXT NOT NULL, information_markdown TEXT NOT NULL, created_at_ms INTEGER NOT NULL); + CREATE TABLE IF NOT EXISTS asset_disclosures( + disclosure_id TEXT PRIMARY KEY, underlying_dco_id TEXT NOT NULL, + issuer_entity_id TEXT NOT NULL, version INTEGER NOT NULL, + disclosure_sha256 TEXT NOT NULL, disclosure_json TEXT NOT NULL, + status TEXT NOT NULL, created_at_ms INTEGER NOT NULL, + signature_json TEXT NOT NULL, + UNIQUE(underlying_dco_id,version)); CREATE INDEX IF NOT EXISTS idx_market_symbol ON instrument_packages(issuer_namespace,display_symbol); + CREATE INDEX IF NOT EXISTS idx_asset_disclosure_dco ON asset_disclosures(underlying_dco_id,version); """) def suggest_namespace(self,issuer_entity_id:str)->dict: @@ -192,6 +200,165 @@ def ensure_namespace(self,issuer_entity_id:str,preferred:str|None=None)->dict: candidate,issuer,body["display_name"],body["created_at_ms"],json.dumps(sig,sort_keys=True))) return dict(body,signature=sig) + def publish_asset_disclosure(self,issuer_entity_id:str,underlying_dco_id:str,*,description:str, + purpose:str="",capabilities:list[str]|None=None, + contents:list[str]|None=None,intended_uses:list[str]|None=None, + limitations:list[str]|None=None,dependencies:list[str]|None=None, + validation_notes:list[str]|None=None,release_notes:str="", + source_refs:list[str]|None=None,version:int|None=None)->dict: + issuer=str(issuer_entity_id); dco=str(underlying_dco_id) + asset=self.fabric.get_object(dco) + if asset["controller_entity_id"]!=issuer: + raise PermissionError("only the controlled DCO issuer may publish an asset disclosure") + text=str(description or "").strip() + if len(text)<20: + raise ValueError("asset description must provide meaningful buyer-facing detail") + with self._db() as db: + if version is None: + row=db.execute("SELECT COALESCE(MAX(version),0)+1 AS v FROM asset_disclosures WHERE underlying_dco_id=?",(dco,)).fetchone() + version=int(row["v"]) + else: + version=int(version) + if version<1: raise ValueError("asset disclosure version must be positive") + def clean(items): + return [str(x).strip() for x in (items or []) if str(x).strip()] + disclosure={ + "schema":"entity-asset-disclosure-v1", + "underlying_dco_id":dco, + "issuer_entity_id":issuer, + "asset_title":asset["title"], + "version":version, + "description":text, + "purpose":str(purpose or "").strip(), + "capabilities":clean(capabilities), + "contents":clean(contents), + "intended_uses":clean(intended_uses), + "limitations":clean(limitations), + "dependencies":clean(dependencies), + "validation_notes":clean(validation_notes), + "release_notes":str(release_notes or "").strip(), + "source_refs":sorted(set(clean(source_refs))), + "issuer_statement_not_protocol_truth":True, + "does_not_expand_rights_passport":True, + "created_at_ms":now_ms(), + } + disclosure_hash=sha(disclosure) + disclosure_id=f"asset-disclosure:v1:{sha({'dco':dco,'version':version,'hash':disclosure_hash})[:24]}" + body={**disclosure,"disclosure_id":disclosure_id,"disclosure_sha256":disclosure_hash} + sig=self.identity.sign(issuer,body) + with self._db() as db: + db.execute("""INSERT INTO asset_disclosures + (disclosure_id,underlying_dco_id,issuer_entity_id,version,disclosure_sha256, + disclosure_json,status,created_at_ms,signature_json) + VALUES(?,?,?,?,?,?,?,?,?)""",( + disclosure_id,dco,issuer,version,disclosure_hash,json.dumps(disclosure,sort_keys=True), + "ACTIVE",disclosure["created_at_ms"],json.dumps(sig,sort_keys=True))) + return dict(body,signature=sig) + + def latest_asset_disclosure(self,underlying_dco_id:str)->dict|None: + dco=str(underlying_dco_id) + with self._db() as db: + row=db.execute("""SELECT * FROM asset_disclosures + WHERE underlying_dco_id=? AND status='ACTIVE' + ORDER BY version DESC,created_at_ms DESC LIMIT 1""",(dco,)).fetchone() + if not row: return None + disclosure=json.loads(row["disclosure_json"]) + return {**disclosure,"disclosure_id":row["disclosure_id"], + "disclosure_sha256":row["disclosure_sha256"], + "signature":json.loads(row["signature_json"])} + + def _provenance_snapshot(self,underlying_dco_id:str)->list[dict]: + dco=str(underlying_dco_id); out=[] + path=getattr(self.fabric,"path",None) + if not path or not Path(path).exists(): return out + db=sqlite3.connect(path); db.row_factory=sqlite3.Row + try: + rows=db.execute("""SELECT p.edge_id,p.parent_object_id,p.relation,p.contribution_bps, + p.evidence_json,o.title,o.controller_entity_id + FROM provenance_edges p + LEFT JOIN objects o ON o.object_id=p.parent_object_id + WHERE p.child_object_id=? + ORDER BY p.created_at_ms,p.edge_id""",(dco,)).fetchall() + for r in rows: + out.append({"edge_id":r["edge_id"],"parent_object_id":r["parent_object_id"], + "parent_title":r["title"],"parent_controller_entity_id":r["controller_entity_id"], + "relation":r["relation"],"contribution_bps":int(r["contribution_bps"]), + "evidence":json.loads(r["evidence_json"] or "{}")}) + finally: db.close() + return out + + def _asset_dossier(self,instrument:dict,asset:dict)->dict: + descriptor=dict(asset.get("descriptor") or {}) + metadata=dict(descriptor.get("metadata") or {}) + gp=self.global_passports.get(instrument["global_passport_id"]) + rp=self.rights.get(instrument["rights_passport_id"]) + disclosure=self.latest_asset_disclosure(asset["object_id"]) + industry=dict(gp.get("industry_context") or {}) + profiles=dict(gp.get("profile_stack") or {}) + btdu=dict(gp.get("btdu_binding") or {}) + origin=dict(gp.get("protocol_origin") or {}) + evidence=sorted(set( + [str(x) for x in (gp.get("evidence_refs") or [])] + + [str(x) for x in (instrument.get("evidence_refs") or [])] + )) + facts={ + "asset_title":asset.get("title"), + "dco_id":asset.get("object_id"), + "dco_code":metadata.get("dco_code") or industry.get("dco_code"), + "short_name":metadata.get("short_name") or industry.get("asset"), + "version":metadata.get("version") or gp.get("version"), + "object_type":asset.get("object_type"), + "commodity_class":descriptor.get("commodity_class"), + "measurement_unit":descriptor.get("measurement_unit"), + "content_sha256":asset.get("content_sha256"), + "controller_entity_id":asset.get("controller_entity_id"), + "status":asset.get("status"), + "created_at_ms":asset.get("created_at_ms"), + "jurisdictions":list(gp.get("jurisdiction_profile_refs") or []), + } + auto_summary=(f"{facts['asset_title']} is a registered ENTITY Digital Commodity Object " + f"of type {facts['object_type']} in the {facts['commodity_class']} commodity class.") + if facts.get("version"): auto_summary+=f" Recorded asset version: {facts['version']}." + if facts.get("dco_code"): auto_summary+=f" DCO code: {facts['dco_code']}." + return { + "schema":"entity-asset-dossier-v1", + "facts":facts, + "issuer_disclosure":disclosure, + "canonical_summary":auto_summary, + "asset_metadata":metadata, + "industry_context":industry, + "profile_refs":list(profiles.get("profile_refs") or []), + "standards_mappings":list(gp.get("standards_mappings") or []), + "btdu_binding":{ + "bound":bool(btdu), + "btdu_version":btdu.get("btdu_version"), + "object_ref":btdu.get("object_ref"), + "binding_mode":btdu.get("binding_mode"), + "topology_does_not_create_ownership":btdu.get("topology_does_not_create_ownership",True), + }, + "protocol_origin":{ + "origin_lineage_id":origin.get("origin_lineage_id"), + "release_ref":origin.get("release_ref"), + "protocol_origin_is_not_asset_provenance":True, + "protocol_origin_does_not_transfer_user_asset_ownership":True, + }, + "provenance":self._provenance_snapshot(asset["object_id"]), + "provenance_refs":list(gp.get("provenance_refs") or []), + "evidence_refs":evidence, + "rights_passport_summary":{ + "passport_id":rp.get("passport_id"), + "rights":list(rp.get("rights") or []), + }, + "validation_and_maturity":{ + k:metadata.get(k) for k in ( + "qualification_pass","physical_robot_validation","commercial_quality_claimed", + "synthetic","external_sale_enabled","raw_data_transfer_default", + "registration_revision" + ) if k in metadata + }, + "dossier_is_disclosure_not_independent_certification":True, + } + def _verify_passports(self,issuer:str,dco_id:str,rights_passport_id:str,global_passport_id:str)->tuple[dict,dict]: rp=self.rights.get(str(rights_passport_id)); gp=self.global_passports.get(str(global_passport_id)) if rp["object_id"]!=dco_id or rp["controller_entity_id"]!=issuer: @@ -299,14 +466,25 @@ def resolve_symbol(self,issuer_namespace:str,display_symbol:str)->dict: return self.instrument(r["instrument_id"]) def _information(self,instrument:dict,listing:dict,asset:dict,issuer_manifest:dict)->tuple[dict,str]: + dossier=self._asset_dossier(instrument,asset) + disclosure=dossier.get("issuer_disclosure") or {} + description=str(disclosure.get("description") or dossier["canonical_summary"]) info={ - "schema":"entity-listing-information-sheet-v1", - "what_is_this":f"{instrument['market_identifier']} represents {instrument['instrument_name']}.", + "schema":"entity-listing-information-sheet-v2", + "what_is_this":( + f"{instrument['market_identifier']} is an economic instrument representing " + f"{instrument['rights_class']} rights associated with the underlying DCO " + f"{asset['title']}." + ), "instrument_name":instrument["instrument_name"],"display_symbol":instrument["display_symbol"], "market_identifier":instrument["market_identifier"],"instrument_id":instrument["instrument_id"], "issuer":str(issuer_manifest.get("display_name") or instrument["issuer_entity_id"]), "issuer_entity_id":instrument["issuer_entity_id"],"underlying_dco_id":instrument["underlying_dco_id"], "asset_name":asset["title"],"asset_type":asset["object_type"], + "asset_description":description,"asset_dossier":dossier, + "asset_disclosure_id":disclosure.get("disclosure_id"), + "asset_disclosure_sha256":disclosure.get("disclosure_sha256"), + "asset_dossier_sha256":sha(dossier), "instrument_class":instrument["instrument_class"],"rights_class":instrument["rights_class"], "buyer_receives":instrument["buyer_receives"], "buyer_does_not_receive":instrument["buyer_does_not_receive"], @@ -314,7 +492,7 @@ def _information(self,instrument:dict,listing:dict,asset:dict,issuer_manifest:di "fungibility":instrument["fungibility"],"jurisdiction":instrument["jurisdiction"], "transfer_rules":instrument["transfer_rules"],"economic_terms":instrument["economic_terms"], "royalty_terms":instrument["royalty_terms"],"rights_passport_id":instrument["rights_passport_id"], - "global_passport_id":instrument["global_passport_id"],"evidence_refs":instrument["evidence_refs"], + "global_passport_id":instrument["global_passport_id"],"evidence_refs":dossier["evidence_refs"], "listing_id":listing["listing_id"],"venue_id":listing["venue_id"],"market_id":listing["market_id"], "quote_unit":listing["quote_unit"],"trade_mode":listing["trade_mode"], "settlement_method":listing["settlement_method"],"minimum_quantity":listing["minimum_quantity"], @@ -322,26 +500,141 @@ def _information(self,instrument:dict,listing:dict,asset:dict,issuer_manifest:di "pricing_method":listing["pricing_method"],"status":listing["status"], "instrument_created_at_ms":instrument["created_at_ms"],"listed_at_ms":listing["listed_at_ms"], "sheet_does_not_replace_rights_passport":True, + "asset_disclosure_is_issuer_statement_not_protocol_truth":True, "external_publication_does_not_create_new_instrument":True, + "observed_market_price_is_not_intrinsic_asset_value":True, + "observed_market_price_is_not_accounting_fair_value":True, } + facts=dossier["facts"]; meta=dossier["asset_metadata"]; industry=dossier["industry_context"] + validation=dossier["validation_and_maturity"]; prov=dossier["provenance"] + def bullet(items,empty="- None stated."): + vals=[str(x).strip() for x in (items or []) if str(x).strip()] + return "\n".join("- "+x for x in vals) if vals else empty + def flag(v): + if isinstance(v,bool): return "Yes" if v else "No" + if v is None or v=="": return "—" + if isinstance(v,(dict,list)): return json.dumps(v,sort_keys=True,ensure_ascii=False) + return str(v) + def kv(mapping): + return "\n".join(f"- {str(k).replace('_',' ').title()}: {flag(v)}" + for k,v in sorted((mapping or {}).items()) if v is not None) or "- None recorded." + provenance_md="\n".join( + f"- {x['relation']}: **{x.get('parent_title') or x['parent_object_id']}** " + f"({x['parent_object_id']}), contribution {x['contribution_bps']} bps" + for x in prov + ) or "- No parent provenance edges recorded." + standards_md="\n".join( + f"- {x.get('standard','—')} — {x.get('role','mapping')} " + f"(normative equivalence claimed: {'yes' if x.get('normative_equivalence_claimed') else 'no'})" + for x in dossier["standards_mappings"] + ) or "- None recorded." md=f"""# ENTITY Listing Information Sheet -## WHAT IS THIS? +## Instrument at a glance **{info['market_identifier']} — {info['instrument_name']}** -Underlying asset: **{info['asset_name']}** +{info['what_is_this']} + Issuer: **{info['issuer']}** -Canonical Instrument ID: `{info['instrument_id']}` -Canonical Listing ID: `{info['listing_id']}` +Underlying asset: **{info['asset_name']}** +Canonical Instrument ID: {info['instrument_id']} +Canonical Listing ID: {info['listing_id']} + +## Underlying Asset — what is being referenced? + +### Asset description + +{info['asset_description']} + +### Canonical asset identity + +- DCO ID: {facts.get('dco_id')} +- DCO code: {facts.get('dco_code') or '—'} +- Short name: {facts.get('short_name') or '—'} +- Version: {facts.get('version') or '—'} +- Object type: {facts.get('object_type') or '—'} +- Commodity class: {facts.get('commodity_class') or '—'} +- Measurement unit: {facts.get('measurement_unit') or '—'} +- Controller ENTITY ID: {facts.get('controller_entity_id')} +- Content SHA-256: {facts.get('content_sha256') or '—'} +- Asset status: {facts.get('status') or '—'} +- Jurisdiction profiles: {', '.join(facts.get('jurisdictions') or []) or '—'} + +### Purpose + +{disclosure.get('purpose') or 'No separate issuer-authored purpose statement has been published; rely on the canonical asset description and facts.'} + +### Key capabilities + +{bullet(disclosure.get('capabilities'))} + +### Included content / components + +{bullet(disclosure.get('contents'))} -### Purchasing this instrument grants +### Intended uses -{chr(10).join('- '+x for x in info['buyer_receives']) or '- See Rights Passport.'} +{bullet(disclosure.get('intended_uses'))} -### Purchasing this instrument does NOT grant +### Known limitations / exclusions -{chr(10).join('- '+x for x in info['buyer_does_not_receive']) or '- No rights beyond the Rights Passport.'} +{bullet(disclosure.get('limitations'))} + +### Dependencies / prerequisites + +{bullet(disclosure.get('dependencies'))} + +### Validation / qualification notes + +{bullet(disclosure.get('validation_notes'))} + +### Canonical technical metadata + +{kv(meta)} + +### Industry / profile context + +{kv(industry)} + +Profile stack: +{bullet(dossier['profile_refs'])} + +Standards mappings: +{standards_md} + +### Validation and maturity flags + +{kv(validation)} + +### Provenance + +{provenance_md} + +Additional provenance references: +{bullet(dossier['provenance_refs'])} + +Evidence references: +{bullet(dossier['evidence_refs'])} + +### BTDU binding + +- Bound: {'Yes' if dossier['btdu_binding']['bound'] else 'No'} +- BTDU version: {dossier['btdu_binding'].get('btdu_version') or '—'} +- Object reference: {dossier['btdu_binding'].get('object_ref') or '—'} +- Binding mode: {dossier['btdu_binding'].get('binding_mode') or '—'} + +A BTDU binding is a topology/evidence relationship. It does not create ownership or economic entitlement. + +## What does the buyer receive? + +{bullet(info['buyer_receives'],'- See Rights Passport.')} + +## What does the buyer NOT receive? + +{bullet(info['buyer_does_not_receive'],'- No rights beyond the Rights Passport.')} + +The underlying asset and the economic instrument are different objects. Buying this instrument does not silently transfer ownership of the DCO, copyright, source ownership, raw bytes, or any right that is not explicitly granted. ## Rights and market terms @@ -356,16 +649,28 @@ def _information(self,instrument:dict,listing:dict,asset:dict,issuer_manifest:di - Settlement: {info['settlement_method']} - Minimum quantity: {info['minimum_quantity']} - Pricing method: {info['pricing_method']} +- Transfer rules: {json.dumps(info['transfer_rules'],sort_keys=True,ensure_ascii=False)} +- Economic terms: {json.dumps(info['economic_terms'],sort_keys=True,ensure_ascii=False)} +- Royalty terms: {json.dumps(info['royalty_terms'],sort_keys=True,ensure_ascii=False)} + +## Economic-value boundary + +A listed price, bid, ask or settled instrument price describes demand for these bounded rights under these terms. It does not establish the intrinsic value of the underlying DCO and is not accounting fair value. ## Canonical references -- Issuer ENTITY ID: `{info['issuer_entity_id']}` -- Underlying DCO: `{info['underlying_dco_id']}` -- Rights Passport: `{info['rights_passport_id']}` -- Global Passport: `{info['global_passport_id']}` -- Venue: `{info['venue_id']}` +- Issuer ENTITY ID: {info['issuer_entity_id']} +- Underlying DCO: {info['underlying_dco_id']} +- Rights Passport: {info['rights_passport_id']} +- Global Passport: {info['global_passport_id']} +- Asset Disclosure: {info['asset_disclosure_id'] or 'none published'} +- Asset Disclosure SHA-256: {info['asset_disclosure_sha256'] or '—'} +- Asset Dossier SHA-256: {info['asset_dossier_sha256']} +- Venue: {info['venue_id']} + +## Disclosure boundary -This sheet is a buyer-facing disclosure layer. It does **not** replace the Rights Passport, Global Passport, canonical instrument, or canonical listing record. +This sheet is a buyer-facing disclosure layer. It does not replace the Rights Passport, Global Passport, canonical DCO, canonical instrument, or canonical listing record. Issuer-authored asset description fields are signed issuer statements; ENTITY does not convert those statements into objective truth merely by recording them. """ info["verification_hash"]=sha(info) return info,md @@ -384,6 +689,9 @@ def create_listing(self,issuer_entity_id:str,instrument_id:str,venue_id:str,*,ma "pricing_method":_enum(pricing_method),"status":"ACTIVE","listed_at_ms":now_ms()} if draft["minimum_quantity"]<1: raise ValueError("minimum quantity must be positive") asset=self.fabric.get_object(instrument["underlying_dco_id"]) + disclosure=self.latest_asset_disclosure(instrument["underlying_dco_id"]) + if not disclosure: + raise ValueError("signed Asset Disclosure required before a market listing can be created") manifest=self.identity.load_manifest(issuer) info,md=self._information(instrument,draft,asset,manifest) info_hash=sha(info) @@ -392,6 +700,9 @@ def create_listing(self,issuer_entity_id:str,instrument_id:str,venue_id:str,*,ma "instrument_id":instrument_id,"issuer_entity_id":issuer, "underlying_dco_id":instrument["underlying_dco_id"],"listing_id":listing_id, "rights_passport":instrument["rights_passport_id"],"global_passport":instrument["global_passport_id"], + "asset_disclosure_id":info.get("asset_disclosure_id"), + "asset_disclosure_sha256":info.get("asset_disclosure_sha256"), + "asset_dossier_sha256":info["asset_dossier_sha256"], "listing_information_sha256":info_hash} machine["verification_hash"]=sha(machine); machine_hash=sha(machine) disclosure=self.exchange.publish_disclosure(str(venue_id),instrument_id,issuer,"LISTING_INFORMATION",machine_hash) @@ -414,7 +725,7 @@ def create_listing(self,issuer_entity_id:str,instrument_id:str,venue_id:str,*,ma db.execute("INSERT INTO listing_information VALUES(?,?,?,?)",( listing_id,json.dumps({"information":info,"machine_manifest":machine},sort_keys=True), md,draft["listed_at_ms"])) - return dict(package,signature=psig,information=info,machine_manifest=machine, + return dict(package,signature=psig,information=info,information_markdown=md,machine_manifest=machine, eep_listing=dict(listing_body,signature=lsig),disclosure=disclosure) def listing(self,listing_id:str)->dict: @@ -440,7 +751,11 @@ def export_portable_package(self,listing_id:str,destination:str|Path)->dict: "rights-passport.json":json.dumps(rp,indent=2,sort_keys=True).encode(), "global-passport.json":json.dumps(gp,indent=2,sort_keys=True).encode(), "provenance.json":json.dumps({"underlying_dco":asset,"evidence_refs":instrument["evidence_refs"]},indent=2,sort_keys=True).encode(), + "asset-dossier.json":json.dumps(listing["information"]["asset_dossier"],indent=2,sort_keys=True,default=str).encode(), } + asset_disclosure=listing["information"]["asset_dossier"].get("issuer_disclosure") + if asset_disclosure: + files["asset-disclosure.json"]=json.dumps(asset_disclosure,indent=2,sort_keys=True,default=str).encode() for name,data in files.items(): (out/name).write_bytes(data) _simple_pdf(listing["information_markdown"],out/"LISTING_INFORMATION.pdf") verification={"schema":"entity-portable-instrument-verification-v1","instrument_id":instrument["instrument_id"], @@ -492,9 +807,11 @@ def active_market(self)->list[dict]: def status(self)->dict: with self._db() as db: counts={t:int(db.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0]) - for t in ("issuer_namespaces","instrument_packages","listing_packages","listing_information")} + for t in ("issuer_namespaces","instrument_packages","listing_packages","listing_information","asset_disclosures")} return {"schema":MARKET_SCHEMA,"version":MARKET_VERSION,"multi_issuer":True, "entity_owns_protocol_not_assets":True,"canonical_instrument_identity":True, "symbols_are_non_authoritative_aliases":True,"listing_information_required":True, + "signed_asset_disclosures_supported":True,"signed_asset_disclosure_required_for_listing":True, + "listing_information_asset_dossier_required":True, "portable_publication_supported":True,"protocol_tax_bps":0, "cryptocurrency_required":False,"counts":counts} diff --git a/tests/test_entity_market_registry.py b/tests/test_entity_market_registry.py index 21a6169c..f6435cd7 100644 --- a/tests/test_entity_market_registry.py +++ b/tests/test_entity_market_registry.py @@ -64,19 +64,53 @@ def test_multi_issuer_symbols_canonical_ids_listing_sheet_and_portable_package() assert reg.resolve_symbol("ACME","SHARED-TRN")["instrument_id"]==ia["instrument_id"] assert reg.resolve_symbol("JSMITH","SHARED-TRN")["instrument_id"]==ij["instrument_id"] + try: + reg.create_listing(acme,ia["instrument_id"],venue["venue_id"], + market_id="ENTITY-PRIMARY",quote_unit="CAD",trade_mode="ORDER_BOOK", + settlement_method="PAYMENT_VERSUS_RIGHT",minimum_quantity=1,pricing_method="ORDER_BOOK") + assert False,"listing must require signed Asset Disclosure" + except ValueError as exc: + assert "Asset Disclosure" in str(exc) + + disclosure=reg.publish_asset_disclosure( + acme,a["object_id"], + description=("Vision Model 07 is an issuer-controlled AI model DCO prepared for " + "evaluation and commercial training-right transactions."), + purpose="Support governed evaluation and training use without transferring model ownership.", + capabilities=["model evaluation","bounded training use"], + contents=["registered model artifact","canonical provenance references"], + intended_uses=["evaluation","commercial model training under granted rights"], + limitations=["does not transfer copyright","does not transfer underlying model ownership"], + validation_notes=["issuer disclosure is not independent certification"], + release_notes="Initial market disclosure.", + source_refs=["urn:example:vm07-evidence"] + ) + assert disclosure["schema"]=="entity-asset-disclosure-v1" + assert disclosure["version"]==1 + assert disclosure["disclosure_sha256"] + listing=reg.create_listing(acme,ia["instrument_id"],venue["venue_id"], market_id="ENTITY-PRIMARY",quote_unit="CAD",trade_mode="ORDER_BOOK", settlement_method="PAYMENT_VERSUS_RIGHT",minimum_quantity=1,pricing_method="ORDER_BOOK") assert listing["listing_id"].startswith("entity.listing:v1:") + assert listing["information"]["schema"]=="entity-listing-information-sheet-v2" assert listing["information"]["sheet_does_not_replace_rights_passport"] is True assert "ownership of the model" in listing["information"]["buyer_does_not_receive"] + assert listing["information"]["asset_disclosure_id"]==disclosure["disclosure_id"] + assert listing["information"]["asset_description"].startswith("Vision Model 07") + assert listing["information"]["asset_dossier"]["facts"]["object_type"]=="MODEL" + assert listing["information"]["asset_dossier"]["issuer_disclosure"]["purpose"].startswith("Support governed") + assert "## Underlying Asset" in listing["information_markdown"] + assert "### Key capabilities" in listing["information_markdown"] assert listing["machine_manifest"]["instrument_id"]==ia["instrument_id"] + assert listing["machine_manifest"]["asset_disclosure_sha256"]==disclosure["disclosure_sha256"] assert ia["status"]=="ACTIVE" pkg=reg.export_portable_package(listing["listing_id"],state/"portable") required={"LISTING_INFORMATION.pdf","LISTING_INFORMATION.md","entity-instrument.json", "instrument.json","listing.json","rights-passport.json","global-passport.json", - "provenance.json","verification.json","SHA256SUMS","ENTITY_INSTRUMENT.md"} + "provenance.json","asset-dossier.json","asset-disclosure.json", + "verification.json","SHA256SUMS","ENTITY_INSTRUMENT.md"} assert required.issubset(set(pkg["files"])) assert (state/"portable"/"LISTING_INFORMATION.pdf").read_bytes().startswith(b"%PDF-1.4") assert "ACME:SHARED-TRN" in (state/"portable"/"ENTITY_INSTRUMENT.md").read_text(encoding="utf-8") @@ -84,6 +118,9 @@ def test_multi_issuer_symbols_canonical_ids_listing_sheet_and_portable_package() assert status["multi_issuer"] is True assert status["entity_owns_protocol_not_assets"] is True assert status["listing_information_required"] is True + assert status["signed_asset_disclosures_supported"] is True + assert status["signed_asset_disclosure_required_for_listing"] is True + assert status["listing_information_asset_dossier_required"] is True def test_instrument_requires_controlled_dco(): ident=load("market_test_identity2","src/01_Core_Runtime/identity/canonical_identity.py") diff --git a/tools/entity_wallet_desktop.py b/tools/entity_wallet_desktop.py index e89908fe..21b8e6e1 100644 --- a/tools/entity_wallet_desktop.py +++ b/tools/entity_wallet_desktop.py @@ -1,6 +1,6 @@ from __future__ import annotations from pathlib import Path -import argparse, importlib.util, json, os, sqlite3, sys, uuid +import argparse, html, importlib.util, json, os, sqlite3, sys, uuid from PySide6 import QtCore, QtGui, QtWidgets @@ -114,6 +114,27 @@ def namespace_info(self) -> dict: def namespace(self) -> str | None: return self.namespace_info().get("namespace") + def asset_record(self, object_id: str) -> dict: + return self.fabric.get_object(object_id) + + def asset_disclosure(self, object_id: str) -> dict | None: + return self.market_registry.latest_asset_disclosure(object_id) + + def publish_asset_disclosure(self, object_id: str, options: dict) -> dict: + return self.market_registry.publish_asset_disclosure( + self.entity_id, object_id, + description=options["description"], + purpose=options.get("purpose", ""), + capabilities=options.get("capabilities", []), + contents=options.get("contents", []), + intended_uses=options.get("intended_uses", []), + limitations=options.get("limitations", []), + dependencies=options.get("dependencies", []), + validation_notes=options.get("validation_notes", []), + release_notes=options.get("release_notes", ""), + source_refs=options.get("source_refs", []), + ) + def latest_global_passport(self, object_id: str) -> dict: dbp = self.state / "entity_v3_4_global_passports.sqlite" db = sqlite3.connect(dbp) @@ -340,6 +361,143 @@ def __init__(self, title, body): body_label.setWordWrap(True) lay.addWidget(body_label) +class AssetDisclosureDialog(QtWidgets.QDialog): + def __init__(self, parent, asset: dict, current: dict | None = None): + super().__init__(parent) + self.setWindowTitle("Asset Information") + self.resize(820, 820) + self.setMinimumSize(720, 680) + self.result_data = None + current = current or {} + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(22, 20, 22, 20) + layout.addWidget(Ui.label("SIGNED ASSET DISCLOSURE", "dialogTitle")) + title = Ui.label(asset.get("title") or asset.get("object_id"), "tickerHero") + title.setWordWrap(True) + layout.addWidget(title) + note = Ui.label( + "This is the buyer-facing description of the underlying DCO. It is signed and versioned. " + "It describes the asset; it does not expand the Rights Passport or manufacture market value.", + "muted" + ) + note.setWordWrap(True) + layout.addWidget(note) + + scroll = QtWidgets.QScrollArea() + scroll.setWidgetResizable(True) + host = QtWidgets.QWidget() + form = QtWidgets.QFormLayout(host) + form.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow) + + def area(value="", height=86): + w = QtWidgets.QPlainTextEdit(str(value or "")) + w.setMinimumHeight(height) + return w + + def lines(values): + return "\n".join(str(x) for x in (values or [])) + + self.description = area(current.get("description"), 130) + self.purpose = area(current.get("purpose"), 80) + self.capabilities = area(lines(current.get("capabilities")), 90) + self.contents = area(lines(current.get("contents")), 90) + self.intended = area(lines(current.get("intended_uses")), 90) + self.limitations = area(lines(current.get("limitations")), 90) + self.dependencies = area(lines(current.get("dependencies")), 80) + self.validation = area(lines(current.get("validation_notes")), 90) + self.release_notes = area(current.get("release_notes"), 80) + self.source_refs = area(lines(current.get("source_refs")), 70) + + for label, widget in [ + ("Detailed asset description", self.description), + ("Purpose / problem addressed", self.purpose), + ("Key capabilities\n(one per line)", self.capabilities), + ("Included content / components\n(one per line)", self.contents), + ("Intended uses\n(one per line)", self.intended), + ("Known limitations / exclusions\n(one per line)", self.limitations), + ("Dependencies / prerequisites\n(one per line)", self.dependencies), + ("Validation / qualification notes\n(one per line)", self.validation), + ("Version / release notes", self.release_notes), + ("Supporting source references\n(one per line)", self.source_refs), + ]: + form.addRow(label, widget) + scroll.setWidget(host) + layout.addWidget(scroll, 1) + + boundary = Ui.label( + "ENTITY records this as an issuer statement. Evidence and passport references remain independently inspectable.", + "muted" + ) + boundary.setWordWrap(True) + layout.addWidget(boundary) + + buttons = QtWidgets.QHBoxLayout() + buttons.addStretch() + cancel = Ui.button("Cancel") + publish = Ui.button("Sign and publish asset information", True) + cancel.clicked.connect(self.reject) + publish.clicked.connect(self.accept_data) + buttons.addWidget(cancel) + buttons.addWidget(publish) + layout.addLayout(buttons) + + @staticmethod + def _items(editor): + return [x.strip() for x in editor.toPlainText().splitlines() if x.strip()] + + def accept_data(self): + description = self.description.toPlainText().strip() + if len(description) < 20: + QtWidgets.QMessageBox.warning( + self, "Asset description", + "Provide a meaningful buyer-facing asset description before publishing." + ) + return + self.result_data = { + "description": description, + "purpose": self.purpose.toPlainText().strip(), + "capabilities": self._items(self.capabilities), + "contents": self._items(self.contents), + "intended_uses": self._items(self.intended), + "limitations": self._items(self.limitations), + "dependencies": self._items(self.dependencies), + "validation_notes": self._items(self.validation), + "release_notes": self.release_notes.toPlainText().strip(), + "source_refs": self._items(self.source_refs), + } + self.accept() + + +class ListingInformationDialog(QtWidgets.QDialog): + def __init__(self, parent, market_identifier: str, markdown: str): + super().__init__(parent) + self.setWindowTitle(f"Listing Information Sheet · {market_identifier}") + self.resize(1060, 820) + self.setMinimumSize(820, 620) + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(18, 16, 18, 16) + head = QtWidgets.QHBoxLayout() + head.addWidget(Ui.label("LISTING INFORMATION SHEET", "dialogTitle")) + head.addStretch() + head.addWidget(Ui.label(market_identifier, "tickerHero")) + layout.addLayout(head) + browser = QtWidgets.QTextBrowser() + browser.setProperty("class", "detail") + browser.setOpenExternalLinks(False) + browser.setMarkdown(markdown) + layout.addWidget(browser, 1) + foot = QtWidgets.QHBoxLayout() + foot.addWidget(Ui.label( + "Buyer-facing disclosure snapshot · canonical passports and instrument records remain authoritative.", + "muted" + )) + foot.addStretch() + close = Ui.button("Close", True) + close.clicked.connect(self.accept) + foot.addWidget(close) + layout.addLayout(foot) + + class InstrumentDialog(QtWidgets.QDialog): def __init__(self, parent, asset, namespace): super().__init__(parent) @@ -665,7 +823,7 @@ def _overview_page(self): al = QtWidgets.QVBoxLayout(asset_box); al.setContentsMargins(18, 16, 18, 18) ah = QtWidgets.QHBoxLayout(); ah.addWidget(Ui.label("ASSET PORTFOLIO", "sectionTitle")); ah.addStretch() ab = Ui.button("+ Ingest asset", True); ab.clicked.connect(self.ingest_asset); ah.addWidget(ab); al.addLayout(ah) - self.overview_assets = Ui.table(["TICKER", "ASSET", "CLASS", "DOMAIN"]) + self.overview_assets = Ui.table(["TICKER", "ASSET", "CLASS", "DOMAIN", "ASSET INFO"]) self.overview_assets.setColumnWidth(0,150) self.overview_assets.setColumnWidth(1,310) self.overview_assets.setColumnWidth(2,170) @@ -686,12 +844,13 @@ def _assets_page(self): p = QtWidgets.QWidget(); lay = QtWidgets.QVBoxLayout(p); lay.setContentsMargins(0,0,0,0); lay.setSpacing(12) actions = QtWidgets.QHBoxLayout() ingest = Ui.button("+ Ingest digital asset", True); ingest.clicked.connect(self.ingest_asset) + disclose = Ui.button("Asset information"); disclose.clicked.connect(self.edit_asset_information) issue = Ui.button("Issue economic instrument"); issue.clicked.connect(self.issue_selected_asset) lineage = Ui.button("Inspect lineage"); lineage.clicked.connect(self.show_asset_lineage) - actions.addWidget(ingest); actions.addWidget(issue); actions.addWidget(lineage); actions.addStretch() + actions.addWidget(ingest); actions.addWidget(disclose); actions.addWidget(issue); actions.addWidget(lineage); actions.addStretch() lay.addLayout(actions) split = QtWidgets.QSplitter() - self.assets_table = Ui.table(["TICKER(S)", "ASSET", "CLASS", "TYPE", "DOMAIN", "PASSPORT", "BTDU", "DCO ID"]) + self.assets_table = Ui.table(["TICKER(S)", "ASSET", "CLASS", "TYPE", "DOMAIN", "ASSET INFO", "PASSPORT", "BTDU", "DCO ID"]) self.assets_table.itemSelectionChanged.connect(self.update_asset_detail) split.addWidget(self.assets_table) self.asset_detail = QtWidgets.QTextBrowser(); self.asset_detail.setProperty("class", "detail") @@ -822,11 +981,13 @@ def refresh(self): domain = (lin.get("protocol_lineage", {}) or {}).get("primary_domain_profile") or "—" gp = lin.get("global_passport") or {} btdu = (lin.get("capability_bindings", {}).get("BTDU", {}) or {}).get("bound", False) + disclosure = self.backend.asset_disclosure(a["object_id"]) + disclosure_status = f"SIGNED V{disclosure['version']}" if disclosure else "REQUIRED TO LIST" asset_table_rows.append([ tickers, a.get("title"), a.get("commodity_class"), a.get("object_type"), domain, - gp.get("version") or "—", "BOUND" if btdu else "—", a.get("object_id") + disclosure_status, gp.get("version") or "—", "BOUND" if btdu else "—", a.get("object_id") ]) - overview_assets.append([tickers, a.get("title"), a.get("commodity_class"), domain]) + overview_assets.append([tickers, a.get("title"), a.get("commodity_class"), domain, disclosure_status]) Ui.fill_table(self.assets_table, asset_table_rows) Ui.fill_table(self.overview_assets, overview_assets[:8]) @@ -906,27 +1067,68 @@ def update_asset_detail(self): lin = a.get("lineage", {}); p = lin.get("protocol_lineage", {}) or {} al = lin.get("asset_lineage", {}) or {}; cap = lin.get("capability_bindings", {}) or {} instruments = self.backend.instrument_map_by_asset().get(a["object_id"], []) - tickers = "
".join(i["market_identifier"] for i in instruments) or "NO ECONOMIC INSTRUMENT ISSUED" + tickers = "
".join(html.escape(i["market_identifier"]) for i in instruments) or "NO ECONOMIC INSTRUMENT ISSUED" parents = "
".join( - f"{x.get('relation')} · {x.get('title') or shorten(x.get('parent_object_id'))}" + f"{html.escape(str(x.get('relation') or ''))} · {html.escape(str(x.get('title') or shorten(x.get('parent_object_id'))))}" for x in al.get("parents", []) ) or "None recorded" + obj = self.backend.asset_record(a["object_id"]) + descriptor = obj.get("descriptor") or {} + metadata = descriptor.get("metadata") or {} + disclosure = self.backend.asset_disclosure(a["object_id"]) + try: + gp = self.backend.latest_global_passport(a["object_id"]) + except Exception: + gp = {} + description = (disclosure or {}).get("description") or ( + f"{obj.get('title')} is registered as an ENTITY Digital Commodity Object of type " + f"{obj.get('object_type')} in the {descriptor.get('commodity_class')} commodity class. " + "No signed issuer-authored Asset Disclosure has been published yet." + ) + def ul(items): + vals=[html.escape(str(x)) for x in (items or []) if str(x).strip()] + return "
    "+"".join("
  • "+x+"
  • " for x in vals)+"
" if vals else "None stated" + metadata_rows = "".join( + f"{html.escape(str(k).replace('_',' ').title())}{html.escape(str(v))}" + for k,v in sorted(metadata.items()) + ) or "None recorded" + disclosure_version=(disclosure or {}).get("version") + profile_refs=(gp.get("profile_stack") or {}).get("profile_refs") or [] self.asset_detail.setHtml(f"""
DIGITAL COMMODITY OBJECT
-

{a.get('title')}

+

{html.escape(str(a.get('title') or ''))}

{tickers}

- Canonical lineage
{p.get('display_path') or 'Unresolved'}

- Controller
{al.get('controller_name') or a.get('controller_entity_id')}

- DCO ID
{a.get('object_id')}

- Commodity class
{a.get('commodity_class')}

- Object type
{a.get('object_type')}

- Composed profiles
{', '.join(p.get('composed_domain_profiles') or []) or 'Primary domain only'}

- BTDU
{'Bound' if (cap.get('BTDU') or {}).get('bound') else 'Not bound'}

+

Asset description

+

{html.escape(str(description))}

+

Disclosure: {"Signed version "+str(disclosure_version) if disclosure else "Not yet published"}

+ +

Canonical identity

+ Canonical lineage
{html.escape(str(p.get('display_path') or 'Unresolved'))}

+ Controller
{html.escape(str(al.get('controller_name') or a.get('controller_entity_id')))}

+ DCO ID
{html.escape(str(a.get('object_id')))}

+ Commodity class
{html.escape(str(a.get('commodity_class')))}

+ Object type
{html.escape(str(a.get('object_type')))}

+ Content SHA-256
{html.escape(str(obj.get('content_sha256') or '—'))}

+ +

Issuer disclosure

+ Purpose
{html.escape(str((disclosure or {}).get('purpose') or 'None stated'))}

+ Capabilities{ul((disclosure or {}).get('capabilities'))} + Included content / components{ul((disclosure or {}).get('contents'))} + Intended uses{ul((disclosure or {}).get('intended_uses'))} + Limitations{ul((disclosure or {}).get('limitations'))} + Validation notes{ul((disclosure or {}).get('validation_notes'))} + +

Technical metadata

+ {metadata_rows}

+ Profile stack{ul(profile_refs)} + Composed profiles
{html.escape(', '.join(p.get('composed_domain_profiles') or []) or 'Primary domain only')}

+ BTDU
{"Bound" if (cap.get('BTDU') or {}).get('bound') else "Not bound"}

Provenance parents
{parents}

- Asset registration does not create a ticker, instrument, listing or market value. + Asset registration does not create a ticker, instrument, listing or market value. A signed Asset Disclosure is required before market listing. """) + def update_instrument_detail(self): x = self.selected_instrument() if not x: @@ -994,6 +1196,28 @@ def ingest_asset(self): except Exception as e: QtWidgets.QMessageBox.critical(self, "Asset ingest failed", str(e)) + def edit_asset_information(self): + asset = self.selected_asset() + if not asset: + QtWidgets.QMessageBox.warning(self, "Asset information", "Select a controlled DCO first.") + return + current = self.backend.asset_disclosure(asset["object_id"]) + dialog = AssetDisclosureDialog(self, asset, current) + if dialog.exec() != QtWidgets.QDialog.Accepted: + return + try: + result = self.backend.publish_asset_disclosure(asset["object_id"], dialog.result_data) + self.refresh() + self.update_asset_detail() + QtWidgets.QMessageBox.information( + self, "Asset information published", + f"Signed Asset Disclosure version {result['version']} was published.\n\n" + f"Disclosure ID\n{result['disclosure_id']}\n\n" + "Future listings will snapshot this disclosure together with the canonical asset dossier." + ) + except Exception as e: + QtWidgets.QMessageBox.critical(self, "Asset information failed", str(e)) + def issue_selected_asset(self): asset = self.selected_asset() if not asset: @@ -1016,6 +1240,12 @@ def list_selected_instrument(self): instrument = self.selected_instrument() if not instrument: QtWidgets.QMessageBox.warning(self, "Listing", "Select one of your economic instruments."); return + if not self.backend.asset_disclosure(instrument["underlying_dco_id"]): + QtWidgets.QMessageBox.warning( + self, "Asset information required", + "This DCO does not yet have a signed Asset Disclosure. Publish detailed Asset Information before creating a market listing." + ) + return d = ListingDialog(self, self.backend, instrument) if d.exec() != QtWidgets.QDialog.Accepted: return @@ -1055,21 +1285,14 @@ def show_listing_info(self): QtWidgets.QMessageBox.warning(self, "Listing", "No active market listing selected."); return try: listing = self.backend.market_registry.listing(r["listing_id"]) - info = listing["information"] - receives = "\n".join("• " + x for x in info["buyer_receives"]) or "• See Rights Passport" - excludes = "\n".join("• " + x for x in info["buyer_does_not_receive"]) or "• No unstated rights" - QtWidgets.QMessageBox.information( - self, "Listing Information Sheet", - f"{info['market_identifier']}\n{info['instrument_name']}\n\n" - f"Issuer: {info['issuer']}\nUnderlying DCO: {info['underlying_dco_id']}\n\n" - f"BUYER RECEIVES\n{receives}\n\nBUYER DOES NOT RECEIVE\n{excludes}\n\n" - f"Supply: {info['supply']}\nJurisdiction: {info['jurisdiction']}\n" - f"Pricing: {info['pricing_method']}\n\nInstrument: {info['instrument_id']}\n" - f"Listing: {info['listing_id']}" + dialog = ListingInformationDialog( + self, listing["information"]["market_identifier"], listing["information_markdown"] ) + dialog.exec() except Exception as e: QtWidgets.QMessageBox.critical(self, "Listing Information", str(e)) + def export_listing(self): r = self.selected_market() if not r: @@ -1164,13 +1387,18 @@ def _apply_style(self): def self_test(state: Path, entity_id: str) -> dict: backend = Backend(state, entity_id) snap = backend.snapshot() + registry_status = backend.market_registry.status() return { "ready": True, "ui_engine": "PySide6", "design": "ENTITY_DATA_ECONOMY_TERMINAL", "entity_id": entity_id, "assets": len(snap.get("assets", [])), "instruments": len(backend.instruments()), "positions": len(snap.get("positions", [])), "orders": len(snap.get("orders", [])), "venues": len(backend.venues()), "market_listings": len(backend.market()), "economic_demand_series": len(backend.economic_metrics()), - "issuer_namespace": backend.namespace(), "protocol_tax_bps": 0, "cryptocurrency_required": False, + "issuer_namespace": backend.namespace(), + "asset_disclosures": registry_status["counts"].get("asset_disclosures", 0), + "market_registry_version": registry_status["version"], + "signed_asset_disclosure_required_for_listing": registry_status.get("signed_asset_disclosure_required_for_listing", False), + "protocol_tax_bps": 0, "cryptocurrency_required": False, } def main(): From be42d600d5ace3c0b8f55a51358788c96208d6e0 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 10:01:43 -0700 Subject: [PATCH 105/111] wallet: add sovereign first-run identity and lineage onboarding --- docs/v3.4/ENTITY_WALLET_20261002.md | 11 + .../v3.4/ENTITY_WALLET_IDENTITY_ONBOARDING.md | 198 +++++++ src/42_ENTITY_Wallet/onboarding.py | 559 ++++++++++++++++++ tests/test_entity_wallet.py | 24 + tests/test_wallet_onboarding.py | 123 ++++ tools/build_entity_wallet_desktop.py | 2 + tools/entity_wallet_desktop.py | 343 ++++++++++- 7 files changed, 1249 insertions(+), 11 deletions(-) create mode 100644 docs/v3.4/ENTITY_WALLET_IDENTITY_ONBOARDING.md create mode 100644 src/42_ENTITY_Wallet/onboarding.py create mode 100644 tests/test_wallet_onboarding.py diff --git a/docs/v3.4/ENTITY_WALLET_20261002.md b/docs/v3.4/ENTITY_WALLET_20261002.md index 70126a99..48d15229 100644 --- a/docs/v3.4/ENTITY_WALLET_20261002.md +++ b/docs/v3.4/ENTITY_WALLET_20261002.md @@ -204,3 +204,14 @@ A later Asset Disclosure update does not silently rewrite an already-created lis Portable instrument packages now include `asset-dossier.json` and, when present, `asset-disclosure.json` in addition to the human Listing Information PDF/Markdown and existing canonical manifests/passports. The wallet shows Asset Information status directly in the DCO portfolio. A DCO without a signed disclosure is marked **REQUIRED TO LIST**. The two current Robotics DCOs were issued signed Asset Disclosure v1 records using facts already present in their canonical records; no new performance claims were inferred. + + +## Universal first-run identity and lineage onboarding — 2026-10-03 + +The desktop wallet no longer assumes a BTG identity on a clean installation. A first-run setup now creates or adopts a sovereign principal ENTITY, claims a human-readable `.entity` public name, optionally creates an operating organization/business/project beneath the principal, creates or adopts a separate Device ENTITY, records explicit lineage/device relationships, creates the wallet, and authenticates through a device-bound signing-key challenge. + +The wallet header now resolves the active user's lineage dynamically. For the migrated BTG installation it displays `shawn.blackmore.entity → btg.entity`; another user receives their own lineage with no BTG-specific structural privilege. + +Device information may bootstrap only a system/device identity. It must never silently create a human or organization identity. Public names remain aliases; immutable Entity IDs and signatures remain authoritative. Protocol origin remains separate from user lineage and user-asset ownership. + +See `docs/v3.4/ENTITY_WALLET_IDENTITY_ONBOARDING.md` for the complete first-run and login model. diff --git a/docs/v3.4/ENTITY_WALLET_IDENTITY_ONBOARDING.md b/docs/v3.4/ENTITY_WALLET_IDENTITY_ONBOARDING.md new file mode 100644 index 00000000..e699474b --- /dev/null +++ b/docs/v3.4/ENTITY_WALLET_IDENTITY_ONBOARDING.md @@ -0,0 +1,198 @@ +# ENTITY Wallet Identity & Lineage Onboarding + +Status: wallet/application layer +ENTITY release compatibility: v3.4.3 +Onboarding profile: `entity-wallet-onboarding-profile-v1` + +## Purpose + +A clean ENTITY Wallet installation must not assume the identity, lineage, organization, or economic authority of the machine on which it is installed. + +The wallet therefore separates four concepts: + +1. **Protocol origin** — the immutable historical ENTITY protocol origin. +2. **Principal ENTITY** — the person, business, organization, project, family, or community that the user creates during first-run setup, or that can be adopted from already-present valid ENTITY state. +3. **Operating ENTITY** — an optional organization/business/project under the principal that owns/controls the active wallet and DCOs. +4. **Device ENTITY** — the local computer installation used to authenticate and operate the wallet. + +These are not interchangeable. + +A device does not become a person. A public name does not become authority. Protocol origin does not become ownership of user assets. + +## First-run flow + +A clean desktop installation follows: + +``` +Install ENTITY Wallet + ↓ +Create separate Device ENTITY + ↓ +User creates Principal ENTITY + ↓ +Choose public .entity name + ↓ +Optionally create Operating ENTITY + ↓ +Establish signed user lineage + ↓ +Bind device to principal + ↓ +Create active wallet + ↓ +Enter ENTITY +``` + +Example: + +``` +jane.smith.entity + ↓ +acme.entity + ↓ +Acme participant wallet +``` + +The active wallet may be the root principal itself or the optional operating entity. + +## Public names + +A human-readable name such as: + +``` +jane.smith.entity +acme.entity +btg.entity +``` + +is a signed ENTITY name claim and display alias. + +It is **not** the canonical database key and is not the source of authority. + +The canonical identity remains the immutable Entity ID: + +``` +ent2-... +``` + +If two independent installations claim the same human-readable name, the identities remain distinct because their Entity IDs and signatures are distinct. A local name conflict is surfaced rather than silently granting first-claim ownership. + +## User lineage + +Wallet lineage is user-controlled identity/organization structure. It is not ENTITY protocol origin. + +For example: + +``` +shawn.blackmore.entity → btg.entity +``` + +means the wallet profile has Shawn's person ENTITY as principal root and BTG as the active operating ENTITY. + +Another installation could be: + +``` +jane.smith.entity → acme.entity +``` + +or simply: + +``` +independent.creator.entity +``` + +No BTG-specific identity is required. + +When an operating organization is created beneath a principal, the relationship is explicitly signed and recorded through the Sovereign Authority registry. Device possession is recorded separately and does not infer ownership or identity. + +## Device identity + +ENTITY Wallet may automatically bootstrap a **system/device ENTITY** using installation and platform context. + +The Device ENTITY may include: + +- generated installation identifier; +- platform family; +- platform release; +- architecture; +- local device display name. + +The device record explicitly states: + +- device information is not user identity; +- device possession is not ownership; +- hardware attestation is not implied unless separately established. + +The device never silently creates a human or organization identity. + +## Wallet login + +The desktop wallet uses a **device-bound signing-key login** rather than a centralized web username/password. + +Unlock requires: + +1. a valid signed wallet onboarding profile; +2. possession of the active ENTITY signing key; +3. possession of the bound Device ENTITY signing key; +4. successful challenge signatures from both identities. + +The login challenge is fresh for each authentication attempt. + +A new device therefore requires an explicit recovery/import/re-binding workflow rather than silently inheriting identity from the operating-system account. That recovery/import workflow is separate from the first-run identity-creation screen described here. + +The onboarding layer itself does not add a separate password-based key-encryption scheme. Private-key protection remains the responsibility of the canonical identity/key-storage layer and any configured hardware/platform protection. + +## Existing installations + +Existing identities are adopted without regeneration. + +For example the BTG production installation retains: + +- Shawn Blackmore's existing person Entity ID; +- Blackmore Technology Group Limited's existing business Entity ID; +- the existing BTG workstation system Entity ID. + +The wallet adds only the signed onboarding/profile bindings needed to express: + +``` +shawn.blackmore.entity → btg.entity +``` + +No DCO lineage, Global Passport, economic state, market history, or identity key is replaced by onboarding migration. + +## Wallet presentation + +The wallet header must resolve lineage from the active wallet profile. + +It must never hard-code: + +``` +SHAWN → BTG → ENTITY +``` + +for arbitrary users. + +The user lineage is shown separately from the ENTITY protocol and market context. + +## Security and authority rules + +- Public names are aliases, not authority. +- Entity IDs and signatures are authoritative. +- Device identity is separate from user identity. +- Device possession does not establish ownership. +- Protocol origin is separate from user lineage. +- Creating an identity does not create economic value. +- Creating a wallet does not create an instrument or listing. +- Creating an organization under a principal does not transfer unrelated rights. +- No cryptocurrency is required. +- Protocol tax remains zero. + +## Cross-platform requirement + +The onboarding path is part of the PySide6/Qt desktop wallet and is intended to operate consistently on: + +- Windows; +- macOS; +- Linux. + +Packaging must include the canonical identity, Sovereign Domain, Sovereign Authority, wallet, market, and passport modules required by first-run identity creation. diff --git a/src/42_ENTITY_Wallet/onboarding.py b/src/42_ENTITY_Wallet/onboarding.py new file mode 100644 index 00000000..a8bb063f --- /dev/null +++ b/src/42_ENTITY_Wallet/onboarding.py @@ -0,0 +1,559 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any +import hashlib +import importlib.util +import json +import os +import platform +import secrets +import socket +import sqlite3 +import sys +import time +import uuid + +SCHEMA = "entity-wallet-onboarding-profile-v1" +VERSION = "1.0.0" +LOGIN_SCHEMA = "entity-wallet-device-login-v1" + +ROOT = Path(__file__).resolve().parents[2] + + +def _load(name: str, relative: str): + path = ROOT / relative + spec = importlib.util.spec_from_file_location(name, path) + if spec is None or spec.loader is None: + raise RuntimeError(f"cannot load {path}") + mod = importlib.util.module_from_spec(spec) + sys.modules[name] = mod + spec.loader.exec_module(mod) + return mod + + +IDENTITY = _load("wallet_onboarding_identity", "src/01_Core_Runtime/identity/canonical_identity.py") +DOMAIN = _load("wallet_onboarding_domain", "src/22_Sovereign_Domain/core/canonical_domain.py") +AUTHORITY = _load( + "wallet_onboarding_authority", + "genesis_master/04_Entity_Registry/relationships/canonical_sovereign_authority.py", +) + + +def _now() -> int: + return int(time.time() * 1000) + + +def _canon(value: Any) -> bytes: + return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, default=str).encode("utf-8") + + +def _sha(value: Any) -> str: + return hashlib.sha256(value if isinstance(value, (bytes, bytearray)) else _canon(value)).hexdigest() + + +def normalize_public_name(value: str) -> str: + raw = str(value or "").strip().lower() + if raw.endswith(".entity"): + raw = raw[:-7] + return DOMAIN.EntityDomainAuthority.normalize_name(raw) + + +def public_address(value: str) -> str: + return normalize_public_name(value) + ".entity" + + +class EntityWalletOnboarding: + """First-run sovereign identity, public-name, lineage and device-binding layer. + + Device metadata may bootstrap a DEVICE identity, but never a human or organization. + A public .entity name is a signed display/name claim; canonical authority remains the Entity ID. + """ + + def __init__(self, state_dir: str | Path): + self.state = Path(state_dir) + self.identity = IDENTITY.EntityIdentityVault(self.state) + self.domains = DOMAIN.EntityDomainAuthority(self.state, self.identity) + self.authority = AUTHORITY.SovereignAuthorityRegistry(self.state, self.identity) + self.root = self.state / "wallet" / "onboarding" + self.profiles = self.root / "profiles" + self.root.mkdir(parents=True, exist_ok=True) + self.profiles.mkdir(parents=True, exist_ok=True) + self.active_path = self.root / "active_profile.json" + self.device_path = self.root / "device.json" + + @staticmethod + def _alias_from_manifest(manifest: dict) -> str: + aliases = [str(x).strip().lower() for x in manifest.get("aliases") or [] if str(x).strip()] + entity_aliases = [x for x in aliases if x.endswith(".entity")] + if entity_aliases: + return entity_aliases[0] + if aliases: + return public_address(aliases[0]) + fallback = ".".join( + p.lower() for p in "".join( + c if c.isalnum() else " " for c in str(manifest.get("display_name") or "entity") + ).split() + ) + return public_address(fallback or ("entity-" + manifest["entity_id"][-8:])) + + def _existing_domain(self, entity_id: str) -> dict | None: + if not self.domains.path.exists(): + return None + db = sqlite3.connect(self.domains.path) + db.row_factory = sqlite3.Row + try: + row = db.execute( + """SELECT domain_id FROM domains + WHERE entity_id=? AND status='ACTIVE' + ORDER BY updated_at_ms DESC,created_at_ms DESC LIMIT 1""", + (str(entity_id),), + ).fetchone() + finally: + db.close() + return self.domains.get_domain(row["domain_id"]) if row else None + + def ensure_public_name(self, entity_id: str, requested_name: str) -> dict: + manifest = self.identity.load_manifest(str(entity_id)) + normalized = normalize_public_name(requested_name) + domain = self._existing_domain(entity_id) + if domain is None: + domain = self.domains.create_domain(entity_id, namespace="entity", requested_name=normalized) + claim = domain.get("name_claim") + domain = self.domains.get_domain(domain["domain_id"]) + elif domain.get("current_name") == normalized: + claim = { + "normalized_name": normalized, + "namespace": "entity", + "conflict_state": "CLEAR", + "existing": True, + } + else: + claim = self.domains.claim_name(entity_id, domain["domain_id"], normalized) + domain = self.domains.get_domain(domain["domain_id"]) + return { + "entity_id": entity_id, + "display_name": manifest["display_name"], + "public_name": normalized + ".entity", + "domain_id": domain["domain_id"], + "name_claim": claim, + "alias_is_authority": False, + "entity_id_is_authority": True, + } + + def _discover_device_identity(self) -> str | None: + if self.device_path.is_file(): + try: + d = json.loads(self.device_path.read_text(encoding="utf-8")) + entity_id = str(d.get("device_entity_id") or "") + manifest = self.identity.load_manifest(entity_id) + if ( + d.get("schema") == "entity-wallet-device-record-v1" + and d.get("device_information_is_not_user_identity") is True + and manifest.get("entity_type") == "system" + ): + return entity_id + except Exception: + pass + for manifest in self.identity.list_local(): + meta = dict(manifest.get("metadata") or {}) + if meta.get("wallet_device_identity") is True: + return manifest["entity_id"] + return None + + def ensure_device_identity(self, preferred_existing_entity_id: str | None = None) -> dict: + if preferred_existing_entity_id: + manifest = self.identity.load_manifest(str(preferred_existing_entity_id)) + if manifest.get("entity_type") != "system": + raise ValueError("device identity must be a system Entity") + device_id = manifest["entity_id"] + created = False + else: + found = self._discover_device_identity() + if found: + manifest = self.identity.load_manifest(found) + device_id = found + created = False + else: + host = (socket.gethostname() or platform.node() or "ENTITY Device").strip() + install_id = str(uuid.uuid4()) + alias_seed = "device-" + install_id.split("-")[0] + manifest = self.identity.create( + f"{host} ENTITY Device", + "system", + aliases=[public_address(alias_seed)], + metadata={ + "entity_subtype": "wallet_device", + "wallet_device_identity": True, + "software_device_identity": True, + "hardware_attested": False, + "installation_id": install_id, + "platform_system": platform.system(), + "platform_release": platform.release(), + "machine": platform.machine(), + "device_information_is_not_user_identity": True, + "device_identity_does_not_establish_person_or_organization": True, + }, + ) + device_id = manifest["entity_id"] + created = True + record = { + "schema": "entity-wallet-device-record-v1", + "device_entity_id": device_id, + "display_name": manifest["display_name"], + "created_by_onboarding": created, + "device_information_is_not_user_identity": True, + "updated_at_ms": _now(), + } + self.device_path.write_text(json.dumps(record, indent=2, sort_keys=True), encoding="utf-8") + return record + + def _ensure_relationship( + self, + entity_id: str, + party_ref: str, + relationship_type: str, + *, + scope: dict, + evidence: dict, + ) -> dict: + for rel in self.authority.active_relationships(str(entity_id), str(party_ref)): + if rel["relationship_type"] == str(relationship_type).upper(): + existing_scope = dict(rel.get("scope") or {}) + if all(existing_scope.get(k) == v for k, v in scope.items()): + return rel + return self.authority.grant_relationship( + str(entity_id), + str(party_ref), + str(relationship_type), + scope=scope, + evidence_origin="ENTITY_ASSERTION", + evidence=evidence, + ) + + def _write_profile(self, body: dict) -> dict: + principal = str(body["principal_entity_id"]) + active = str(body["active_entity_id"]) + signatures = {"principal": self.identity.sign(principal, body)} + if active != principal: + signatures["active_entity"] = self.identity.sign(active, body) + profile = dict(body, signatures=signatures, profile_sha256=_sha(body)) + path = self.profiles / f"{body['profile_id']}.json" + path.write_text(json.dumps(profile, indent=2, sort_keys=True), encoding="utf-8") + self.active_path.write_text( + json.dumps( + {"schema": "entity-wallet-active-profile-v1", "profile_id": body["profile_id"], "updated_at_ms": _now()}, + indent=2, + sort_keys=True, + ), + encoding="utf-8", + ) + return profile + + def _build_profile( + self, + *, + principal_entity_id: str, + active_entity_id: str, + lineage_nodes: list[dict], + device_entity_id: str, + migrated_existing: bool, + ) -> dict: + body = { + "schema": SCHEMA, + "version": VERSION, + "profile_id": "wallet-profile-" + secrets.token_hex(12), + "principal_entity_id": str(principal_entity_id), + "active_entity_id": str(active_entity_id), + "device_entity_id": str(device_entity_id), + "lineage": lineage_nodes, + "display_path": " → ".join(node["public_name"] for node in lineage_nodes), + "login_policy": { + "mode": "DEVICE_BOUND_SIGNING_KEY", + "password_required": False, + "private_key_possession_required": True, + "device_binding_required": True, + "recovery_required_on_new_device": True, + }, + "canonical_authority_is_entity_id": True, + "public_name_is_alias_not_authority": True, + "device_identity_is_not_user_identity": True, + "protocol_origin_is_separate_from_user_lineage": True, + "migrated_existing": bool(migrated_existing), + "created_at_ms": _now(), + } + return self._write_profile(body) + + def create_profile( + self, + *, + principal_display_name: str, + principal_public_name: str, + principal_entity_type: str = "person", + organization_display_name: str | None = None, + organization_public_name: str | None = None, + organization_entity_type: str = "business", + operate_as_organization: bool = True, + ) -> dict: + paddr = public_address(principal_public_name) + principal = self.identity.create( + principal_display_name, + principal_entity_type, + aliases=[paddr], + metadata={ + "wallet_onboarding_created": True, + "public_entity_name": paddr, + "public_name_is_alias_not_authority": True, + }, + ) + pclaim = self.ensure_public_name(principal["entity_id"], principal_public_name) + lineage = [{ + "entity_id": principal["entity_id"], + "display_name": principal["display_name"], + "entity_type": principal["entity_type"], + "public_name": pclaim["public_name"], + "domain_id": pclaim["domain_id"], + "role": "ROOT_PRINCIPAL", + }] + active = principal["entity_id"] + + if organization_display_name and organization_public_name: + oaddr = public_address(organization_public_name) + org = self.identity.create( + organization_display_name, + organization_entity_type, + aliases=[oaddr], + metadata={ + "wallet_onboarding_created": True, + "public_entity_name": oaddr, + "public_name_is_alias_not_authority": True, + "lineage_parent_entity_id": principal["entity_id"], + }, + ) + oclaim = self.ensure_public_name(org["entity_id"], organization_public_name) + self._ensure_relationship( + org["entity_id"], + principal["entity_id"], + "SOVEREIGN_AUTHORITY", + scope={ + "wallet_lineage_parent": True, + "organization_control": True, + "asset_ownership_not_implied": True, + }, + evidence={"profile": SCHEMA}, + ) + lineage.append({ + "entity_id": org["entity_id"], + "display_name": org["display_name"], + "entity_type": org["entity_type"], + "public_name": oclaim["public_name"], + "domain_id": oclaim["domain_id"], + "role": "OPERATING_ENTITY", + }) + if operate_as_organization: + active = org["entity_id"] + + device = self.ensure_device_identity() + self._ensure_relationship( + principal["entity_id"], + device["device_entity_id"], + "POSSESSION", + scope={ + "wallet_device_bound": True, + "device_possession_not_identity": True, + "device_possession_not_ownership": True, + }, + evidence={"profile": SCHEMA}, + ) + return self._build_profile( + principal_entity_id=principal["entity_id"], + active_entity_id=active, + lineage_nodes=lineage, + device_entity_id=device["device_entity_id"], + migrated_existing=False, + ) + + def adopt_existing( + self, + *, + principal_entity_id: str, + active_entity_id: str | None = None, + device_entity_id: str | None = None, + ) -> dict: + principal = self.identity.load_manifest(str(principal_entity_id)) + active_id = str(active_entity_id or principal_entity_id) + active = self.identity.load_manifest(active_id) + paddr = self._alias_from_manifest(principal) + pclaim = self.ensure_public_name(principal["entity_id"], paddr) + lineage = [{ + "entity_id": principal["entity_id"], + "display_name": principal["display_name"], + "entity_type": principal["entity_type"], + "public_name": pclaim["public_name"], + "domain_id": pclaim["domain_id"], + "role": "ROOT_PRINCIPAL", + }] + if active_id != principal["entity_id"]: + aaddr = self._alias_from_manifest(active) + aclaim = self.ensure_public_name(active_id, aaddr) + self._ensure_relationship( + active_id, + principal["entity_id"], + "SOVEREIGN_AUTHORITY", + scope={ + "wallet_lineage_parent": True, + "organization_control": True, + "asset_ownership_not_implied": True, + }, + evidence={"profile": SCHEMA, "migration": True}, + ) + lineage.append({ + "entity_id": active_id, + "display_name": active["display_name"], + "entity_type": active["entity_type"], + "public_name": aclaim["public_name"], + "domain_id": aclaim["domain_id"], + "role": "OPERATING_ENTITY", + }) + device = self.ensure_device_identity(device_entity_id) + self._ensure_relationship( + principal["entity_id"], + device["device_entity_id"], + "POSSESSION", + scope={ + "wallet_device_bound": True, + "device_possession_not_identity": True, + "device_possession_not_ownership": True, + }, + evidence={"profile": SCHEMA, "migration": True}, + ) + return self._build_profile( + principal_entity_id=principal["entity_id"], + active_entity_id=active_id, + lineage_nodes=lineage, + device_entity_id=device["device_entity_id"], + migrated_existing=True, + ) + + def _verify_profile(self, profile: dict) -> bool: + try: + if profile.get("schema") != SCHEMA: + return False + body = {k: v for k, v in profile.items() if k not in {"signatures", "profile_sha256"}} + if profile.get("profile_sha256") != _sha(body): + return False + signatures = dict(profile.get("signatures") or {}) + principal = self.identity.load_manifest(str(body["principal_entity_id"])) + if not self.identity.verify_signature(principal, body, dict(signatures.get("principal") or {})): + return False + if body["active_entity_id"] != body["principal_entity_id"]: + active = self.identity.load_manifest(str(body["active_entity_id"])) + if not self.identity.verify_signature(active, body, dict(signatures.get("active_entity") or {})): + return False + self.identity.load_manifest(str(body["device_entity_id"])) + return True + except Exception: + return False + + def profile(self, profile_id: str) -> dict: + path = self.profiles / f"{profile_id}.json" + if not path.is_file(): + raise KeyError("wallet onboarding profile not found") + profile = json.loads(path.read_text(encoding="utf-8")) + if not self._verify_profile(profile): + raise RuntimeError("wallet onboarding profile verification failed") + return profile + + def list_profiles(self) -> list[dict]: + out = [] + for path in sorted(self.profiles.glob("wallet-profile-*.json")): + try: + out.append(self.profile(path.stem)) + except Exception: + continue + return out + + def active_profile(self) -> dict | None: + if not self.active_path.is_file(): + return None + try: + active = json.loads(self.active_path.read_text(encoding="utf-8")) + return self.profile(str(active["profile_id"])) + except Exception: + return None + + def profile_for_entity(self, entity_id: str) -> dict | None: + target = str(entity_id) + active = self.active_profile() + if active and active.get("active_entity_id") == target: + return active + for profile in self.list_profiles(): + if profile.get("active_entity_id") == target: + return profile + return None + + def set_active(self, profile_id: str) -> dict: + profile = self.profile(profile_id) + self.active_path.write_text( + json.dumps( + {"schema": "entity-wallet-active-profile-v1", "profile_id": profile_id, "updated_at_ms": _now()}, + indent=2, + sort_keys=True, + ), + encoding="utf-8", + ) + return profile + + def authenticate(self, profile: dict | None = None) -> dict: + profile = profile or self.active_profile() + if not profile or not self._verify_profile(profile): + return {"authenticated": False, "reason": "profile_missing_or_invalid"} + current_device = self._discover_device_identity() + if not current_device or current_device != profile["device_entity_id"]: + return { + "authenticated": False, + "reason": "device_binding_mismatch", + "recovery_or_rebind_required": True, + } + challenge = { + "schema": LOGIN_SCHEMA, + "profile_id": profile["profile_id"], + "active_entity_id": profile["active_entity_id"], + "device_entity_id": profile["device_entity_id"], + "nonce": secrets.token_hex(32), + "created_at_ms": _now(), + } + active_manifest = self.identity.load_manifest(profile["active_entity_id"]) + active_sig = self.identity.sign(profile["active_entity_id"], challenge) + if not self.identity.verify_signature(active_manifest, challenge, active_sig): + return {"authenticated": False, "reason": "active_identity_signature_failed"} + device_manifest = self.identity.load_manifest(profile["device_entity_id"]) + device_sig = self.identity.sign(profile["device_entity_id"], challenge) + if not self.identity.verify_signature(device_manifest, challenge, device_sig): + return {"authenticated": False, "reason": "device_signature_failed"} + return { + "authenticated": True, + "profile_id": profile["profile_id"], + "principal_entity_id": profile["principal_entity_id"], + "active_entity_id": profile["active_entity_id"], + "device_entity_id": profile["device_entity_id"], + "display_path": profile["display_path"], + "mode": "DEVICE_BOUND_SIGNING_KEY", + "password_used": False, + "entity_id_is_authority": True, + } + + def status(self) -> dict: + active = self.active_profile() + return { + "ready": True, + "schema": SCHEMA, + "version": VERSION, + "profile_count": len(self.list_profiles()), + "active_profile_id": active.get("profile_id") if active else None, + "first_run_required": active is None, + "device_identity_auto_bootstrap": True, + "device_information_creates_user_identity": False, + "public_name_is_alias_not_authority": True, + "login_mode": "DEVICE_BOUND_SIGNING_KEY", + } diff --git a/tests/test_entity_wallet.py b/tests/test_entity_wallet.py index 09dccf5d..78cc954d 100644 --- a/tests/test_entity_wallet.py +++ b/tests/test_entity_wallet.py @@ -4,6 +4,7 @@ HERE=Path(__file__).resolve().parents[1] MOD=HERE/"src"/"42_ENTITY_Wallet"/"canonical_wallet.py" +ONBOARDING_MOD=HERE/"src"/"42_ENTITY_Wallet"/"onboarding.py" def load(): spec=importlib.util.spec_from_file_location("wallet_test_mod",MOD) @@ -82,4 +83,27 @@ def test_offer_does_not_create_market_value(self): self.assertIsNone(p["market"]["last"]) self.assertIsNone(p["market_value_amount_units"]) + def test_first_run_profile_keeps_user_device_and_public_name_authority_separate(self): + spec=importlib.util.spec_from_file_location("wallet_ci_onboarding",ONBOARDING_MOD) + mod=importlib.util.module_from_spec(spec); sys.modules[spec.name]=mod; spec.loader.exec_module(mod) + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as td: + onboarding=mod.EntityWalletOnboarding(Path(td)) + profile=onboarding.create_profile( + principal_display_name="CI User", + principal_public_name="ci.user", + principal_entity_type="person", + organization_display_name="CI Labs", + organization_public_name="ci.labs", + organization_entity_type="business", + operate_as_organization=True, + ) + auth=onboarding.authenticate(profile) + self.assertTrue(auth["authenticated"]) + self.assertEqual(profile["display_path"],"ci.user.entity → ci.labs.entity") + self.assertNotEqual(profile["principal_entity_id"],profile["active_entity_id"]) + self.assertNotEqual(profile["device_entity_id"],profile["principal_entity_id"]) + self.assertTrue(profile["public_name_is_alias_not_authority"]) + self.assertTrue(profile["device_identity_is_not_user_identity"]) + self.assertTrue(profile["protocol_origin_is_separate_from_user_lineage"]) + if __name__=="__main__": unittest.main() diff --git a/tests/test_wallet_onboarding.py b/tests/test_wallet_onboarding.py new file mode 100644 index 00000000..2afcc04f --- /dev/null +++ b/tests/test_wallet_onboarding.py @@ -0,0 +1,123 @@ +from pathlib import Path +import importlib.util +import sys +import tempfile + + +ROOT = Path(__file__).resolve().parents[1] + + +def load(name, rel): + path = ROOT / rel + spec = importlib.util.spec_from_file_location(name, path) + mod = importlib.util.module_from_spec(spec) + sys.modules[name] = mod + spec.loader.exec_module(mod) + return mod + + +def test_clean_install_creates_user_lineage_device_binding_and_crypto_login(): + mod = load("wallet_onboarding_test", "src/42_ENTITY_Wallet/onboarding.py") + with tempfile.TemporaryDirectory() as td: + state = Path(td) + onboarding = mod.EntityWalletOnboarding(state) + + device = onboarding.ensure_device_identity() + device_manifest = onboarding.identity.load_manifest(device["device_entity_id"]) + assert device_manifest["entity_type"] == "system" + assert device_manifest["metadata"]["device_information_is_not_user_identity"] is True + assert onboarding.status()["profile_count"] == 0 + + profile = onboarding.create_profile( + principal_display_name="Jane Smith", + principal_public_name="jane.smith.entity", + principal_entity_type="person", + organization_display_name="Acme Corp", + organization_public_name="acme.entity", + organization_entity_type="business", + operate_as_organization=True, + ) + + assert profile["display_path"] == "jane.smith.entity → acme.entity" + assert profile["principal_entity_id"] != profile["active_entity_id"] + assert profile["device_entity_id"] == device["device_entity_id"] + assert profile["public_name_is_alias_not_authority"] is True + assert profile["device_identity_is_not_user_identity"] is True + assert profile["protocol_origin_is_separate_from_user_lineage"] is True + + principal = onboarding.identity.load_manifest(profile["principal_entity_id"]) + active = onboarding.identity.load_manifest(profile["active_entity_id"]) + assert principal["display_name"] == "Jane Smith" + assert principal["aliases"] == ["jane.smith.entity"] + assert active["display_name"] == "Acme Corp" + assert active["aliases"] == ["acme.entity"] + + auth = onboarding.authenticate(profile) + assert auth["authenticated"] is True + assert auth["active_entity_id"] == profile["active_entity_id"] + assert auth["mode"] == "DEVICE_BOUND_SIGNING_KEY" + assert auth["password_used"] is False + + rels = onboarding.authority.active_relationships(profile["active_entity_id"], profile["principal_entity_id"]) + assert any( + r["relationship_type"] == "SOVEREIGN_AUTHORITY" + and r["scope"].get("wallet_lineage_parent") is True + for r in rels + ) + device_rels = onboarding.authority.active_relationships(profile["principal_entity_id"], profile["device_entity_id"]) + assert any( + r["relationship_type"] == "POSSESSION" + and r["scope"].get("device_possession_not_identity") is True + for r in device_rels + ) + + loaded = onboarding.active_profile() + assert loaded["profile_id"] == profile["profile_id"] + assert onboarding.status()["first_run_required"] is False + + +def test_adopt_existing_preserves_existing_entity_ids_and_aliases(): + mod = load("wallet_onboarding_test_existing", "src/42_ENTITY_Wallet/onboarding.py") + with tempfile.TemporaryDirectory() as td: + state = Path(td) + onboarding = mod.EntityWalletOnboarding(state) + principal = onboarding.identity.create("Existing Person", "person", aliases=["existing.person.entity"]) + org = onboarding.identity.create("Existing Org", "business", aliases=["existing.org.entity"]) + device = onboarding.identity.create( + "Existing Device", + "system", + aliases=["existing.device.entity"], + metadata={"software_device_identity": True, "device_information_is_not_user_identity": True}, + ) + + profile = onboarding.adopt_existing( + principal_entity_id=principal["entity_id"], + active_entity_id=org["entity_id"], + device_entity_id=device["entity_id"], + ) + + assert profile["migrated_existing"] is True + assert profile["principal_entity_id"] == principal["entity_id"] + assert profile["active_entity_id"] == org["entity_id"] + assert profile["device_entity_id"] == device["entity_id"] + assert profile["display_path"] == "existing.person.entity → existing.org.entity" + assert onboarding.authenticate(profile)["authenticated"] is True + + +def test_public_names_are_aliases_and_local_conflicts_do_not_replace_entity_authority(): + mod = load("wallet_onboarding_test_alias", "src/42_ENTITY_Wallet/onboarding.py") + with tempfile.TemporaryDirectory() as td: + state = Path(td) + onboarding = mod.EntityWalletOnboarding(state) + a = onboarding.identity.create("A", "person", aliases=["shared.entity"]) + b = onboarding.identity.create("B", "person", aliases=["shared.entity"]) + + ca = onboarding.ensure_public_name(a["entity_id"], "shared.entity") + cb = onboarding.ensure_public_name(b["entity_id"], "shared.entity") + + assert ca["public_name"] == "shared.entity" + assert cb["public_name"] == "shared.entity" + assert ca["entity_id"] != cb["entity_id"] + assert cb["name_claim"]["conflict_state"] == "CONFLICT" + assert ca["entity_id_is_authority"] is True + assert cb["entity_id_is_authority"] is True diff --git a/tools/build_entity_wallet_desktop.py b/tools/build_entity_wallet_desktop.py index fd6b5108..94c83580 100644 --- a/tools/build_entity_wallet_desktop.py +++ b/tools/build_entity_wallet_desktop.py @@ -11,6 +11,8 @@ "tools/entity_v3_4_cli.py", "src/01_Core_Runtime/identity/canonical_identity.py", "src/11_ADAM/full_runtime", + "src/22_Sovereign_Domain", + "genesis_master/04_Entity_Registry/relationships/canonical_sovereign_authority.py", "src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py", "src/31_Profiles/exchange_protocol.py", "src/36_Adoption_Layer", diff --git a/tools/entity_wallet_desktop.py b/tools/entity_wallet_desktop.py index 21b8e6e1..817e39a2 100644 --- a/tools/entity_wallet_desktop.py +++ b/tools/entity_wallet_desktop.py @@ -20,6 +20,44 @@ RED = "#FF6B78" YELLOW = "#E3BC64" +def startup_stylesheet() -> str: + return f""" + * {{ + font-family: "Segoe UI"; + font-size: 10pt; + color: {TEXT}; + }} + QDialog, QMessageBox {{ background: {ROOT_BG}; }} + QLabel {{ background: transparent; }} + QLabel[class="brand"] {{ font-size: 26pt; font-weight: 800; letter-spacing: 2px; color: {TEXT}; }} + QLabel[class="eyebrow"] {{ color: {ACCENT}; font-size: 8pt; font-weight: 700; letter-spacing: 1px; }} + QLabel[class="dialogTitle"] {{ font-size: 18pt; font-weight: 700; }} + QLabel[class="muted"] {{ color: {MUTED}; }} + QLabel[class="chip"] {{ + background: {PANEL_2}; border: 1px solid {BORDER}; border-radius: 6px; padding: 10px; color: {MUTED}; + }} + QLabel[class="tickerPreview"] {{ + background: {SIDEBAR_BG}; border: 1px solid {ACCENT_DARK}; border-radius: 6px; + padding: 10px; color: {ACCENT}; font-family: Consolas; font-weight: 700; + }} + QFrame[class="panel"] {{ background: {PANEL_BG}; border: 1px solid {BORDER}; border-radius: 8px; }} + QPushButton {{ + background: {PANEL_2}; border: 1px solid {BORDER}; border-radius: 6px; + padding: 8px 13px; font-weight: 600; + }} + QPushButton:hover {{ border-color: {ACCENT_DARK}; background: #1A2027; }} + QPushButton[primary="true"] {{ + background: {ACCENT}; color: #0C0E11; border-color: {ACCENT}; font-weight: 800; + }} + QLineEdit, QComboBox {{ + background: {PANEL_2}; border: 1px solid {BORDER}; border-radius: 5px; padding: 8px; + selection-background-color: {ACCENT_DARK}; + }} + QLineEdit:focus, QComboBox:focus {{ border-color: {ACCENT}; }} + QCheckBox {{ spacing: 8px; }} + """ + + def repo_root() -> Path: if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): return Path(sys._MEIPASS) @@ -41,6 +79,7 @@ def load_module(name: str, relative: str): EXCHANGE = load_module("entity_wallet_exchange", "src/31_Profiles/exchange_protocol.py") MARKET = load_module("entity_wallet_market", "src/45_ENTITY_Market/canonical_market_registry.py") INTEL = load_module("entity_wallet_intelligence", "src/45_ENTITY_Market/economic_intelligence.py") +ONBOARDING = load_module("entity_wallet_onboarding", "src/42_ENTITY_Wallet/onboarding.py") def default_state() -> Path: env = os.environ.get("ENTITY_STATE_DIR") @@ -361,6 +400,233 @@ def __init__(self, title, body): body_label.setWordWrap(True) lay.addWidget(body_label) +class EntitySetupDialog(QtWidgets.QDialog): + def __init__(self, parent, onboarding): + super().__init__(parent) + self.onboarding = onboarding + self.profile = None + self.setWindowTitle("Create your ENTITY identity") + self.resize(760, 690) + self.setMinimumSize(690, 610) + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(28, 24, 28, 24) + layout.setSpacing(14) + + layout.addWidget(Ui.label("WELCOME TO ENTITY", "eyebrow")) + layout.addWidget(Ui.label("Create your sovereign identity", "dialogTitle")) + intro = Ui.label( + "This creates your own cryptographic ENTITY identity and public .entity name. " + "Your device receives a separate device identity; device information never becomes your human or company identity.", + "muted" + ) + intro.setWordWrap(True) + layout.addWidget(intro) + + principal_box = QtWidgets.QFrame() + principal_box.setProperty("class", "panel") + pf = QtWidgets.QFormLayout(principal_box) + pf.setContentsMargins(18, 18, 18, 18) + pf.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow) + + self.principal_type = QtWidgets.QComboBox() + self.principal_type.addItems(["person", "business", "organization", "project", "community", "family"]) + self.principal_name = QtWidgets.QLineEdit() + self.principal_name.setPlaceholderText("Jane Smith") + self.principal_public = QtWidgets.QLineEdit() + self.principal_public.setPlaceholderText("jane.smith") + self.principal_preview = Ui.label("jane.smith.entity", "tickerPreview") + pf.addRow("Identity type", self.principal_type) + pf.addRow("Public display name", self.principal_name) + pf.addRow("Public ENTITY name", self.principal_public) + pf.addRow("Address preview", self.principal_preview) + layout.addWidget(principal_box) + + self.add_org = QtWidgets.QCheckBox("Create an operating organization/business under this identity") + self.add_org.setChecked(False) + layout.addWidget(self.add_org) + + self.org_box = QtWidgets.QFrame() + self.org_box.setProperty("class", "panel") + of = QtWidgets.QFormLayout(self.org_box) + of.setContentsMargins(18, 18, 18, 18) + of.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow) + self.org_type = QtWidgets.QComboBox() + self.org_type.addItems(["business", "organization", "project"]) + self.org_name = QtWidgets.QLineEdit() + self.org_name.setPlaceholderText("Acme Corporation") + self.org_public = QtWidgets.QLineEdit() + self.org_public.setPlaceholderText("acme") + self.org_preview = Ui.label("acme.entity", "tickerPreview") + self.operate_as_org = QtWidgets.QCheckBox("Open the wallet as this operating entity") + self.operate_as_org.setChecked(True) + of.addRow("Entity type", self.org_type) + of.addRow("Organization name", self.org_name) + of.addRow("Public ENTITY name", self.org_public) + of.addRow("Address preview", self.org_preview) + of.addRow("", self.operate_as_org) + layout.addWidget(self.org_box) + self.org_box.setVisible(False) + + self.lineage_preview = Ui.label("LINEAGE —", "chip") + self.lineage_preview.setWordWrap(True) + layout.addWidget(self.lineage_preview) + + note = Ui.label( + "Public names are human-readable aliases. The immutable ENTITY ID and signatures remain authoritative. " + "A local name conflict does not let one user impersonate another.", + "muted" + ) + note.setWordWrap(True) + layout.addWidget(note) + layout.addStretch() + + buttons = QtWidgets.QHBoxLayout() + cancel = Ui.button("Exit") + create = Ui.button("Create ENTITY and wallet", True) + cancel.clicked.connect(self.reject) + create.clicked.connect(self.create_identity) + buttons.addWidget(cancel) + buttons.addStretch() + buttons.addWidget(create) + layout.addLayout(buttons) + + self.add_org.toggled.connect(self.org_box.setVisible) + self.add_org.toggled.connect(self.update_preview) + self.principal_public.textChanged.connect(self.update_preview) + self.org_public.textChanged.connect(self.update_preview) + self.update_preview() + + @staticmethod + def _address(text): + raw = str(text or "").strip().lower() + if raw.endswith(".entity"): + raw = raw[:-7] + return (raw + ".entity") if raw else "—" + + def update_preview(self): + p = self._address(self.principal_public.text()) + self.principal_preview.setText(p) + if self.add_org.isChecked(): + o = self._address(self.org_public.text()) + self.org_preview.setText(o) + self.lineage_preview.setText(f"LINEAGE {p} → {o}") + else: + self.lineage_preview.setText(f"LINEAGE {p}") + + def create_identity(self): + if not self.principal_name.text().strip() or not self.principal_public.text().strip(): + QtWidgets.QMessageBox.warning(self, "ENTITY setup", "Display name and public ENTITY name are required.") + return + if self.add_org.isChecked() and (not self.org_name.text().strip() or not self.org_public.text().strip()): + QtWidgets.QMessageBox.warning( + self, "ENTITY setup", + "Organization name and public ENTITY name are required when an operating organization is enabled." + ) + return + try: + self.profile = self.onboarding.create_profile( + principal_display_name=self.principal_name.text().strip(), + principal_public_name=self.principal_public.text().strip(), + principal_entity_type=self.principal_type.currentText(), + organization_display_name=self.org_name.text().strip() if self.add_org.isChecked() else None, + organization_public_name=self.org_public.text().strip() if self.add_org.isChecked() else None, + organization_entity_type=self.org_type.currentText(), + operate_as_organization=self.operate_as_org.isChecked(), + ) + auth = self.onboarding.authenticate(self.profile) + if not auth.get("authenticated"): + raise RuntimeError("new identity was created but device-bound authentication failed") + self.accept() + except Exception as exc: + QtWidgets.QMessageBox.critical(self, "ENTITY setup failed", str(exc)) + + +class EntityLoginDialog(QtWidgets.QDialog): + def __init__(self, parent, onboarding): + super().__init__(parent) + self.onboarding = onboarding + self.profile = None + self.create_new = False + self.profiles = onboarding.list_profiles() + self.setWindowTitle("Unlock ENTITY") + self.resize(620, 390) + self.setMinimumWidth(580) + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(28, 24, 28, 24) + layout.setSpacing(14) + + layout.addWidget(Ui.label("ENTITY", "brand")) + layout.addWidget(Ui.label("SOVEREIGN WALLET ACCESS", "eyebrow")) + layout.addWidget(Ui.label("Unlock your local ENTITY identity", "dialogTitle")) + note = Ui.label( + "ENTITY uses possession of your local signing identity plus the bound device identity. " + "The operating-system account name is not treated as your ENTITY identity.", + "muted" + ) + note.setWordWrap(True) + layout.addWidget(note) + + self.selector = QtWidgets.QComboBox() + for profile in self.profiles: + active = onboarding.identity.load_manifest(profile["active_entity_id"]) + self.selector.addItem( + f"{profile['display_path']} · {active['display_name']}", + profile["profile_id"] + ) + layout.addWidget(self.selector) + + self.detail = Ui.label("", "chip") + self.detail.setWordWrap(True) + layout.addWidget(self.detail) + self.selector.currentIndexChanged.connect(self.update_detail) + self.update_detail() + layout.addStretch() + + buttons = QtWidgets.QHBoxLayout() + create = Ui.button("Create another identity") + unlock = Ui.button("Unlock wallet", True) + create.clicked.connect(self.choose_create) + unlock.clicked.connect(self.unlock) + buttons.addWidget(create) + buttons.addStretch() + buttons.addWidget(unlock) + layout.addLayout(buttons) + + def update_detail(self): + idx = self.selector.currentIndex() + if idx < 0 or idx >= len(self.profiles): + self.detail.setText("No local wallet profile.") + return + p = self.profiles[idx] + self.detail.setText( + f"LINEAGE {p['display_path']}\n" + f"LOGIN device-bound signing key\n" + f"ACTIVE ENTITY {shorten(p['active_entity_id'], 14, 10)}" + ) + + def choose_create(self): + self.create_new = True + self.accept() + + def unlock(self): + idx = self.selector.currentIndex() + if idx < 0 or idx >= len(self.profiles): + return + profile = self.profiles[idx] + auth = self.onboarding.authenticate(profile) + if not auth.get("authenticated"): + QtWidgets.QMessageBox.critical( + self, "ENTITY unlock failed", + "The local signing identity or bound device could not authenticate this wallet.\n\n" + + str(auth.get("reason") or "authentication failed") + + ("\n\nRecovery or device re-binding is required." if auth.get("recovery_or_rebind_required") else "") + ) + return + self.onboarding.set_active(profile["profile_id"]) + self.profile = profile + self.accept() + + class AssetDisclosureDialog(QtWidgets.QDialog): def __init__(self, parent, asset: dict, current: dict | None = None): super().__init__(parent) @@ -722,10 +988,11 @@ class WalletWindow(QtWidgets.QMainWindow): ("market", "ENTITY MARKET"), ("intelligence", "ECONOMIC INTELLIGENCE"), ("orders", "ORDERS"), ] - def __init__(self, state: Path, entity_id: str): + def __init__(self, state: Path, entity_id: str, wallet_profile: dict | None = None): super().__init__() self.state = state self.entity_id = entity_id + self.wallet_profile = wallet_profile self.backend = Backend(state, entity_id) self.snapshot_data = {} self.assets_rows: list[dict] = [] @@ -944,8 +1211,16 @@ def refresh(self): venues = self.backend.venues() name = self.backend.identity_manifest.get("display_name") or self.entity_id - self.identity_chip.setText(f"{name}\n{shorten(self.entity_id, 12, 8)}") - self.lineage_chip.setText("SHAWN → BTG → ENTITY") + public_aliases = [str(x) for x in (self.backend.identity_manifest.get("aliases") or []) if str(x).endswith(".entity")] + active_public = public_aliases[0] if public_aliases else shorten(self.entity_id, 12, 8) + self.identity_chip.setText(f"{name}\n{active_public}") + if self.wallet_profile: + self.lineage_chip.setText("LINEAGE " + self.wallet_profile.get("display_path", active_public)) + self.lineage_chip.setToolTip( + "User/organization lineage for this wallet. This is separate from ENTITY protocol origin." + ) + else: + self.lineage_chip.setText("LINEAGE " + active_public) self.namespace_chip.setText(f"NAMESPACE {ns}" + ("" if ns_info.get("registered") else " · RESERVED ON ISSUE")) self.market_chip.setText(f"ENTITY-MKT · {len(venues)} VENUE{'S' if len(venues)!=1 else ''}") if self.market_rows: @@ -1384,10 +1659,13 @@ def _apply_style(self): QToolTip {{ background: {PANEL_2}; color: {TEXT}; border: 1px solid {BORDER}; }} """) -def self_test(state: Path, entity_id: str) -> dict: +def self_test(state: Path, entity_id: str, wallet_profile: dict | None = None) -> dict: backend = Backend(state, entity_id) snap = backend.snapshot() registry_status = backend.market_registry.status() + onboarding = ONBOARDING.EntityWalletOnboarding(state) + profile = wallet_profile or onboarding.profile_for_entity(entity_id) + auth = onboarding.authenticate(profile) if profile else {"authenticated": False, "reason": "legacy_unprofiled_identity"} return { "ready": True, "ui_engine": "PySide6", "design": "ENTITY_DATA_ECONOMY_TERMINAL", "entity_id": entity_id, "assets": len(snap.get("assets", [])), @@ -1398,6 +1676,13 @@ def self_test(state: Path, entity_id: str) -> dict: "asset_disclosures": registry_status["counts"].get("asset_disclosures", 0), "market_registry_version": registry_status["version"], "signed_asset_disclosure_required_for_listing": registry_status.get("signed_asset_disclosure_required_for_listing", False), + "wallet_profile_id": profile.get("profile_id") if profile else None, + "wallet_lineage": profile.get("display_path") if profile else None, + "device_bound_login": bool(auth.get("authenticated")) if profile else False, + "login_mode": (profile.get("login_policy") or {}).get("mode") if profile else None, + "public_name_is_alias_not_authority": True, + "device_information_creates_user_identity": False, + "protocol_origin_is_separate_from_user_lineage": True, "protocol_tax_bps": 0, "cryptocurrency_required": False, } @@ -1408,23 +1693,58 @@ def main(): ap.add_argument("--render-preview") args = ap.parse_args() state = Path(args.state) if args.state else default_state() - if not args.entity: - ms = manifests(state) - if not ms: - raise SystemExit("No ENTITY identity found") - args.entity = ms[0]["entity_id"] + onboarding = ONBOARDING.EntityWalletOnboarding(state) + + wallet_profile = None + if args.entity: + wallet_profile = onboarding.profile_for_entity(args.entity) + else: + wallet_profile = onboarding.active_profile() + if wallet_profile: + args.entity = wallet_profile["active_entity_id"] + if args.self_test: - report = self_test(state, args.entity) + if not args.entity: + ms = manifests(state) + if not ms: + raise SystemExit("No ENTITY identity or wallet profile found") + args.entity = ms[0]["entity_id"] + report = self_test(state, args.entity, wallet_profile) raw = json.dumps(report, indent=2, sort_keys=True) if args.self_test_output: Path(args.self_test_output).write_text(raw + "\n", encoding="utf-8") else: print(raw) return + app = QtWidgets.QApplication(sys.argv) app.setApplicationName(APP_NAME) app.setStyle("Fusion") - window = WalletWindow(state, args.entity) + app.setStyleSheet(startup_stylesheet()) + + if not args.entity: + setup = EntitySetupDialog(None, onboarding) + if setup.exec() != QtWidgets.QDialog.Accepted or not setup.profile: + return + wallet_profile = setup.profile + args.entity = wallet_profile["active_entity_id"] + elif wallet_profile is None and not args.render_preview: + # A legacy explicit --entity remains supported for managed deployments. + pass + elif wallet_profile is not None and not args.render_preview: + login = EntityLoginDialog(None, onboarding) + if login.exec() != QtWidgets.QDialog.Accepted: + return + if login.create_new: + setup = EntitySetupDialog(None, onboarding) + if setup.exec() != QtWidgets.QDialog.Accepted or not setup.profile: + return + wallet_profile = setup.profile + else: + wallet_profile = login.profile + args.entity = wallet_profile["active_entity_id"] + + window = WalletWindow(state, args.entity, wallet_profile) window.show() if args.render_preview: app.processEvents() @@ -1434,5 +1754,6 @@ def main(): )) sys.exit(app.exec()) + if __name__ == "__main__": main() From bc603bb6aa995815782f7c2cef69bf4dc5500683 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 10:39:56 -0700 Subject: [PATCH 106/111] wallet: drive instrument rights from Rights Passports --- docs/v3.4/ENTITY_WALLET_20261002.md | 20 ++ tests/test_entity_market_registry.py | 34 +++ tools/entity_wallet_desktop.py | 378 ++++++++++++++++++++++++--- 3 files changed, 391 insertions(+), 41 deletions(-) diff --git a/docs/v3.4/ENTITY_WALLET_20261002.md b/docs/v3.4/ENTITY_WALLET_20261002.md index 48d15229..80456a1f 100644 --- a/docs/v3.4/ENTITY_WALLET_20261002.md +++ b/docs/v3.4/ENTITY_WALLET_20261002.md @@ -215,3 +215,23 @@ The wallet header now resolves the active user's lineage dynamically. For the mi Device information may bootstrap only a system/device identity. It must never silently create a human or organization identity. Public names remain aliases; immutable Entity IDs and signatures remain authoritative. Protocol origin remains separate from user lineage and user-asset ownership. See `docs/v3.4/ENTITY_WALLET_IDENTITY_ONBOARDING.md` for the complete first-run and login model. + + +## Rights-Passport-driven instrument issuance — 2026-10-03 + +The desktop wallet no longer defaults every new economic instrument to `COMMERCIALIZE`. Instrument issuance now reads the selected DCO's current Rights Passport and presents only actions with an explicit `ALLOW` effect as selectable economic rights. + +Rules: + +- the Rights Passport is the maximum authority boundary; +- the instrument may contain only a subset of currently allowed Rights Passport actions; +- prohibited actions are shown as prohibited and cannot be selected; +- non-ALLOW/non-PROHIBIT actions are shown separately and are not directly issuable; +- the backend re-validates the selected actions immediately before canonical instrument creation; +- the canonical market registry remains the final enforcement layer and rejects any instrument whose actions exceed the Rights Passport; +- ticker/right-class/name suggestions are derived from the selected authorized actions rather than a generic commercial-right default; +- canonical asset short names are preferred for ticker suggestions where present (for example, BIRFR-1 training rights suggest `BIRFR1-TRN`). + +Production verification confirmed that BIRFR-1 exposes its allowed actions (BENCHMARK, CERTIFY, COMPUTE, DERIVE, EVALUATE, FINE_TUNE, INFER, OEM_DEPLOY, TRAIN) and does not expose COMMERCIALIZE, while the Blackmore Real-Time Failure Detection Engine does expose COMMERCIALIZE because that action is present in its Rights Passport. + +The complete wallet/economy/lineage/onboarding/market qualification campaign passed **89/89** after this change. The installed Windows executable then passed its frozen-runtime production self-test. diff --git a/tests/test_entity_market_registry.py b/tests/test_entity_market_registry.py index f6435cd7..2def1a8c 100644 --- a/tests/test_entity_market_registry.py +++ b/tests/test_entity_market_registry.py @@ -143,3 +143,37 @@ def test_instrument_requires_controlled_dco(): assert False,"non-controller issuance must fail" except PermissionError: pass + +def test_instrument_actions_cannot_exceed_rights_passport(): + ident=load("market_test_identity3","src/01_Core_Runtime/identity/canonical_identity.py") + fabmod=load("market_test_fabric3","src/30_Universal_Transaction_Fabric/canonical_universal_fabric.py") + exmod=load("market_test_exchange3","src/31_Profiles/exchange_protocol.py") + marketmod=load("market_test_registry3","src/45_ENTITY_Market/canonical_market_registry.py") + with tempfile.TemporaryDirectory() as td: + state=Path(td); vault=ident.EntityIdentityVault(state) + owner=vault.create("Rights Owner","person")["entity_id"] + fabric=fabmod.UniversalTransactionFabric(state,vault); ex=exmod.ExchangeProtocol(state,vault,fabric) + rights=FakePassports("rights"); gps=FakePassports("global") + reg=marketmod.EntityEconomicMarketRegistry(state,vault,fabric,ex,rights,gps) + obj=fabric.register_digital_commodity( + owner,"Restricted Asset","4"*64,commodity_class="DATA",measurement_unit="ASSET" + ) + rights.bind("rp-restricted",obj["object_id"],owner,actions=["READ","TRAIN"]) + gps.bind("gp-restricted",obj["object_id"],owner,"rp-restricted") + try: + reg.create_instrument( + owner,obj["object_id"], + instrument_name="Invalid Commercial Rights", + display_symbol="BAD-COM", + instrument_class="SPOT_LICENSE", + rights_class="COMMERCIAL", + rights={"actions":["COMMERCIALIZE"]}, + supply=1, + rights_passport_id="rp-restricted", + global_passport_id="gp-restricted", + settlement_currency="CAD", + jurisdiction="CA" + ) + assert False,"instrument actions exceeding the Rights Passport must fail" + except PermissionError as exc: + assert "exceed Rights Passport" in str(exc) diff --git a/tools/entity_wallet_desktop.py b/tools/entity_wallet_desktop.py index 817e39a2..307e76f5 100644 --- a/tools/entity_wallet_desktop.py +++ b/tools/entity_wallet_desktop.py @@ -174,6 +174,43 @@ def publish_asset_disclosure(self, object_id: str, options: dict) -> dict: source_refs=options.get("source_refs", []), ) + def instrument_rights_profile(self, object_id: str) -> dict: + gp = self.latest_global_passport(object_id) + rp = self.rights.get(gp["rights_passport_id"]) + allowed = {} + prohibited = {} + other = {} + for rule in rp.get("rights") or []: + effect = str(rule.get("effect") or "").upper() + for action in rule.get("actions") or []: + token = str(action).strip().upper() + if not token: + continue + detail = { + "action": token, + "effect": effect, + "conditions": dict(rule.get("conditions") or {}), + "obligations": list(rule.get("obligations") or []), + "right_refs": list(rule.get("right_refs") or []), + } + if effect == "ALLOW": + allowed[token] = detail + elif effect == "PROHIBIT": + prohibited[token] = detail + else: + other[token] = detail + return { + "object_id": str(object_id), + "global_passport_id": gp["passport_id"], + "rights_passport_id": rp["passport_id"], + "allowed_actions": sorted(allowed), + "prohibited_actions": sorted(prohibited), + "other_actions": sorted(other), + "allowed": allowed, + "prohibited": prohibited, + "other": other, + } + def latest_global_passport(self, object_id: str) -> dict: dbp = self.state / "entity_v3_4_global_passports.sqlite" db = sqlite3.connect(dbp) @@ -288,6 +325,22 @@ def ingest(self, path, options): def create_instrument(self, object_id: str, options: dict) -> dict: gp = self.latest_global_passport(object_id) + rights_profile = self.instrument_rights_profile(object_id) + requested = {str(x).strip().upper() for x in options.get("actions") or [] if str(x).strip()} + allowed = set(rights_profile["allowed_actions"]) + prohibited = set(rights_profile["prohibited_actions"]) + if not requested: + raise ValueError("At least one Rights Passport action must be selected.") + if requested & prohibited: + bad = ", ".join(sorted(requested & prohibited)) + raise PermissionError(f"Selected action(s) are prohibited by the Rights Passport: {bad}") + if not requested.issubset(allowed): + missing = ", ".join(sorted(requested - allowed)) + permitted = ", ".join(rights_profile["allowed_actions"]) or "none" + raise PermissionError( + f"Selected action(s) are not granted by the Rights Passport: {missing}. " + f"Permitted actions: {permitted}" + ) return self.market_registry.create_instrument( self.entity_id, object_id, instrument_name=options["name"], display_symbol=options["symbol"], @@ -765,79 +818,302 @@ def __init__(self, parent, market_identifier: str, markdown: str): class InstrumentDialog(QtWidgets.QDialog): - def __init__(self, parent, asset, namespace): + ACTION_META = { + "TRAIN": ("Training", "TRN", "TRAINING"), + "FINE_TUNE": ("Fine-tuning", "FTN", "FINE_TUNING"), + "INFER": ("Inference", "INF", "INFERENCE"), + "EVALUATE": ("Evaluation", "EVL", "EVALUATION"), + "BENCHMARK": ("Benchmark", "BNCH", "BENCHMARK"), + "CERTIFY": ("Certification", "CERT", "CERTIFICATION"), + "COMPUTE": ("Compute-to-data", "CMP", "COMPUTE"), + "DERIVE": ("Derivative", "DER", "DERIVATIVE"), + "OEM_DEPLOY": ("OEM deployment", "OEM", "OEM"), + "COMMERCIALIZE": ("Commercialization", "COM", "COMMERCIAL"), + "EXECUTE": ("Execution", "EXE", "EXECUTION"), + "CONTROL": ("Control", "CTL", "CONTROL"), + "MODIFY": ("Modification", "MOD", "MODIFICATION"), + "TRANSFER": ("Transfer", "XFR", "TRANSFER"), + "COPY": ("Copy", "CPY", "COPY"), + "READ": ("Read/access", "ACC", "ACCESS"), + "INSPECT": ("Inspection", "INSP", "INSPECTION"), + } + + def __init__(self, parent, asset, namespace, rights_profile): super().__init__(parent) self.setWindowTitle("Issue Economic Instrument") - self.setMinimumWidth(650) + self.resize(780, 820) + self.setMinimumSize(720, 700) self.result_data = None + self.asset = asset + self.rights_profile = rights_profile + self._updating = False + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(22, 20, 22, 20) + layout.setSpacing(12) + layout.addWidget(Ui.label("ISSUE ECONOMIC INSTRUMENT", "dialogTitle")) - layout.addWidget(Ui.label( - "Create a canonical rights instrument against this DCO. This does not list it or establish market value.", - "muted")) + subtitle = Ui.label( + "The DCO Rights Passport is authoritative. Select a subset of rights already granted " + "to this asset; the wallet cannot invent or broaden them.", + "muted") + subtitle.setWordWrap(True) + layout.addWidget(subtitle) + + passport_box = QtWidgets.QFrame() + passport_box.setProperty("class", "panel") + pb = QtWidgets.QVBoxLayout(passport_box) + pb.setContentsMargins(16, 13, 16, 13) + pb.addWidget(Ui.label("RIGHTS PASSPORT", "eyebrow")) + pb.addWidget(Ui.label( + f"{shorten(rights_profile['rights_passport_id'], 22, 12)} · " + f"{len(rights_profile['allowed_actions'])} selectable action(s)", + "monoMuted" + )) + layout.addWidget(passport_box) + + if not rights_profile["allowed_actions"]: + warning = Ui.label( + "This DCO currently has no ALLOW actions in its Rights Passport. " + "No economic instrument can be issued until the asset rights are explicitly authorized.", + "muted") + warning.setWordWrap(True) + layout.addWidget(warning) + form = QtWidgets.QFormLayout() form.setLabelAlignment(QtCore.Qt.AlignLeft) - title = asset.get("title") or "Asset" - self.name = QtWidgets.QLineEdit(f"{title} Commercial Rights") + form.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow) + self.namespace = QtWidgets.QLineEdit(namespace or "") - self.symbol = QtWidgets.QLineEdit((title.upper().replace(" ", "")[:10] + "-COM")[:24]) + self.symbol = QtWidgets.QLineEdit("") + self.name = QtWidgets.QLineEdit("") self.iclass = QtWidgets.QComboBox() - self.iclass.addItems(["SPOT_LICENSE","SUBSCRIPTION","COMPUTE_TO_DATA","PROCUREMENT","CONTRIBUTION","SECONDARY_LICENSE"]) - self.rclass = QtWidgets.QLineEdit("COMMERCIAL") - self.actions = QtWidgets.QLineEdit("COMMERCIALIZE") - self.supply = QtWidgets.QSpinBox(); self.supply.setRange(1, 2_000_000_000); self.supply.setValue(1) + self.iclass.addItems([ + "SPOT_LICENSE","SUBSCRIPTION","COMPUTE_TO_DATA", + "PROCUREMENT","CONTRIBUTION","SECONDARY_LICENSE" + ]) + self.rclass = QtWidgets.QLineEdit("") + self.rclass.setReadOnly(True) + + self.actions = QtWidgets.QListWidget() + self.actions.setMinimumHeight(190) + self.actions.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) + for action in rights_profile["allowed_actions"]: + detail = rights_profile["allowed"].get(action, {}) + label = self.ACTION_META.get(action, (action.replace("_", " ").title(), action[:5], action))[0] + conditions = detail.get("conditions") or {} + obligations = detail.get("obligations") or [] + text = f"{action} — {label}" + if conditions: + text += " · conditions apply" + if obligations: + text += " · obligations apply" + item = QtWidgets.QListWidgetItem(text) + item.setData(QtCore.Qt.UserRole, action) + item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable) + item.setCheckState(QtCore.Qt.Unchecked) + tip = [] + if conditions: + tip.append("Conditions: " + json.dumps(conditions, sort_keys=True)) + if obligations: + tip.append("Obligations: " + ", ".join(str(x) for x in obligations)) + item.setToolTip("\n".join(tip) if tip else "Granted by the current Rights Passport.") + self.actions.addItem(item) + + self.supply = QtWidgets.QSpinBox() + self.supply.setRange(1, 2_000_000_000) + self.supply.setValue(1) self.currency = QtWidgets.QLineEdit("CAD") self.jurisdiction = QtWidgets.QLineEdit("CA") - self.series = QtWidgets.QSpinBox(); self.series.setRange(1, 999999); self.series.setValue(1) - self.fungibility = QtWidgets.QComboBox(); self.fungibility.addItems(["FUNGIBLE","SERIES-FUNGIBLE","NON-FUNGIBLE"]) - self.divisibility = QtWidgets.QSpinBox(); self.divisibility.setRange(0, 9) + self.series = QtWidgets.QSpinBox() + self.series.setRange(1, 999999) + self.series.setValue(1) + self.fungibility = QtWidgets.QComboBox() + self.fungibility.addItems(["FUNGIBLE","SERIES-FUNGIBLE","NON-FUNGIBLE"]) + self.divisibility = QtWidgets.QSpinBox() + self.divisibility.setRange(0, 9) self.transferable = QtWidgets.QCheckBox("Secondary transfer permitted") - self.receives = QtWidgets.QPlainTextEdit("Defined commercial-use right") - self.excludes = QtWidgets.QPlainTextEdit("Ownership of the underlying DCO\nCopyright ownership\nRights not stated in the Rights Passport") + self.receives = QtWidgets.QPlainTextEdit("") + self.receives.setMinimumHeight(70) + self.excludes = QtWidgets.QPlainTextEdit( + "Ownership of the underlying DCO\n" + "Copyright ownership\n" + "Rights not stated in the Rights Passport" + ) + self.excludes.setMinimumHeight(70) + for label, widget in [ - ("Instrument name", self.name), ("Issuer namespace", self.namespace), - ("Display ticker / symbol", self.symbol), ("EEP class", self.iclass), - ("Rights class", self.rclass), ("Actions", self.actions), - ("Supply", self.supply), ("Settlement currency", self.currency), - ("Jurisdiction", self.jurisdiction), ("Series", self.series), - ("Fungibility", self.fungibility), ("Divisibility", self.divisibility), - ("Transferability", self.transferable), ("Buyer receives", self.receives), + ("Instrument name", self.name), + ("Issuer namespace", self.namespace), + ("Display ticker / symbol", self.symbol), + ("EEP class", self.iclass), + ("Rights class", self.rclass), + ("Rights Passport actions", self.actions), + ("Supply", self.supply), + ("Settlement currency", self.currency), + ("Jurisdiction", self.jurisdiction), + ("Series", self.series), + ("Fungibility", self.fungibility), + ("Divisibility", self.divisibility), + ("Transferability", self.transferable), + ("Buyer receives", self.receives), ("Buyer does NOT receive", self.excludes), ]: form.addRow(label, widget) layout.addLayout(form) + + excluded = [] + if rights_profile["prohibited_actions"]: + excluded.append("PROHIBITED: " + ", ".join(rights_profile["prohibited_actions"])) + if rights_profile["other_actions"]: + excluded.append("NOT DIRECTLY ISSUABLE: " + ", ".join(rights_profile["other_actions"])) + if excluded: + boundary = QtWidgets.QFrame() + boundary.setProperty("class", "notice") + bl = QtWidgets.QVBoxLayout(boundary) + bl.setContentsMargins(14, 10, 14, 10) + for line in excluded: + note = Ui.label(line, "muted") + note.setWordWrap(True) + bl.addWidget(note) + layout.addWidget(boundary) + self.preview = Ui.label("", "tickerPreview") layout.addWidget(self.preview) self.namespace.textChanged.connect(self._preview) self.symbol.textChanged.connect(self._preview) - self._preview() + self.actions.itemChanged.connect(self._rights_changed) + buttons = QtWidgets.QHBoxLayout() buttons.addStretch() - cancel = Ui.button("Cancel"); create = Ui.button("Issue instrument", True) - cancel.clicked.connect(self.reject); create.clicked.connect(self.accept_data) - buttons.addWidget(cancel); buttons.addWidget(create) + cancel = Ui.button("Cancel") + create = Ui.button("Issue instrument", True) + create.setEnabled(bool(rights_profile["allowed_actions"])) + cancel.clicked.connect(self.reject) + create.clicked.connect(self.accept_data) + buttons.addWidget(cancel) + buttons.addWidget(create) layout.addLayout(buttons) + self._preview() + + def selected_actions(self): + return [ + str(self.actions.item(i).data(QtCore.Qt.UserRole)) + for i in range(self.actions.count()) + if self.actions.item(i).checkState() == QtCore.Qt.Checked + ] + + def _base_symbol(self): + short_name = str(self.asset.get("short_name") or "").upper() + if short_name: + compact = "".join(ch for ch in short_name if ch.isalnum()) + if compact: + return compact[:10] + dco_code = str(self.asset.get("dco_code") or "").upper() + if dco_code: + compact = "".join(ch for ch in dco_code if ch.isalnum()) + if compact: + return compact[:10] + title = str(self.asset.get("title") or "ASSET").upper() + words = [w for w in "".join(ch if ch.isalnum() else " " for ch in title).split() if w] + ignored = { + "BLACKMORE","INDUSTRIAL","REAL","TIME","THE","AND","OF", + "ASSET","DATASET","ENGINE","DIGITAL","ROBOTICS" + } + useful = [w for w in words if w not in ignored] + if not useful: + useful = words + if len(useful) == 1: + base = useful[0][:10] + else: + initials = "".join(w[0] for w in useful[:5]) + base = initials if len(initials) >= 3 else "".join(useful)[:10] + return base[:10] or "ASSET" + + def _rights_changed(self, item=None): + if self._updating: + return + actions = self.selected_actions() + if not actions: + self.rclass.setText("") + self.name.setText("") + self.symbol.setText("") + self.receives.setPlainText("") + self._preview() + return + + metas = [self.ACTION_META.get(a, (a.replace("_", " ").title(), a[:5], a)) for a in actions] + if len(actions) == 1: + label, suffix, rights_class = metas[0] + rights_name = f"{label} Rights" + self.rclass.setText(rights_class) + else: + labels = [m[0] for m in metas] + suffix = "-".join(m[1] for m in metas[:3]) + rights_name = " + ".join(labels) + " Rights" + self.rclass.setText("BUNDLED_RIGHTS") + + title = str(self.asset.get("title") or "Asset") + self.name.setText(f"{title} {rights_name}") + self.symbol.setText(f"{self._base_symbol()}-{suffix}"[:24]) + receives = [] + for action in actions: + detail = self.rights_profile["allowed"].get(action, {}) + line = f"{action} right under the current Rights Passport" + if detail.get("conditions"): + line += " (subject to passport conditions)" + if detail.get("obligations"): + line += " (subject to passport obligations)" + receives.append(line) + self.receives.setPlainText("\n".join(receives)) + self._preview() + def _preview(self): ns = (self.namespace.text().strip() or "NAMESPACE").upper() - sym = (self.symbol.text().strip() or "SYMBOL").upper() + sym = (self.symbol.text().strip() or "SELECT-RIGHT").upper() self.preview.setText(f"MARKET IDENTIFIER PREVIEW {ns}:{sym}") def accept_data(self): - actions = [x.strip().upper() for x in self.actions.text().split(",") if x.strip()] - if not actions or not self.symbol.text().strip() or not self.name.text().strip(): - QtWidgets.QMessageBox.warning(self, "Instrument", "Name, ticker and at least one rights action are required.") + actions = self.selected_actions() + if not actions: + QtWidgets.QMessageBox.warning( + self, "Instrument", + "Select at least one action granted by the DCO's Rights Passport." + ) return + allowed = set(self.rights_profile["allowed_actions"]) + if not set(actions).issubset(allowed): + QtWidgets.QMessageBox.critical( + self, "Rights Passport", + "The selected actions are no longer a valid subset of the current Rights Passport." + ) + return + if not self.symbol.text().strip() or not self.name.text().strip(): + QtWidgets.QMessageBox.warning(self, "Instrument", "Instrument name and ticker are required.") + return + self.result_data = { - "name": self.name.text().strip(), "namespace": self.namespace.text().strip(), - "symbol": self.symbol.text().strip(), "instrument_class": self.iclass.currentText(), - "rights_class": self.rclass.text().strip(), "actions": actions, "supply": self.supply.value(), - "currency": self.currency.text().strip(), "jurisdiction": self.jurisdiction.text().strip(), - "series": self.series.value(), "fungibility": self.fungibility.currentText(), - "divisibility": self.divisibility.value(), "transferable": self.transferable.isChecked(), + "name": self.name.text().strip(), + "namespace": self.namespace.text().strip(), + "symbol": self.symbol.text().strip().upper(), + "instrument_class": self.iclass.currentText(), + "rights_class": self.rclass.text().strip(), + "actions": actions, + "supply": self.supply.value(), + "currency": self.currency.text().strip(), + "jurisdiction": self.jurisdiction.text().strip(), + "series": self.series.value(), + "fungibility": self.fungibility.currentText(), + "divisibility": self.divisibility.value(), + "transferable": self.transferable.isChecked(), "duration_ms": None, - "buyer_receives": [x.strip() for x in self.receives.toPlainText().splitlines() if x.strip()], - "buyer_does_not_receive": [x.strip() for x in self.excludes.toPlainText().splitlines() if x.strip()], + "buyer_receives": [ + x.strip() for x in self.receives.toPlainText().splitlines() if x.strip() + ], + "buyer_does_not_receive": [ + x.strip() for x in self.excludes.toPlainText().splitlines() if x.strip() + ], } self.accept() @@ -1497,7 +1773,27 @@ def issue_selected_asset(self): asset = self.selected_asset() if not asset: QtWidgets.QMessageBox.warning(self, "Instrument", "Select a controlled DCO first."); return - d = InstrumentDialog(self, asset, self.backend.namespace()) + try: + rights_profile = self.backend.instrument_rights_profile(asset["object_id"]) + except Exception as e: + QtWidgets.QMessageBox.critical(self, "Rights Passport", str(e)) + return + if not rights_profile["allowed_actions"]: + QtWidgets.QMessageBox.warning( + self, "No issuable rights", + "This DCO's current Rights Passport contains no ALLOW actions. " + "No economic instrument can be created until the asset is explicitly authorized." + ) + return + dialog_asset = dict(asset) + try: + obj = self.backend.asset_record(asset["object_id"]) + metadata = ((obj.get("descriptor") or {}).get("metadata") or {}) + dialog_asset["short_name"] = metadata.get("short_name") + dialog_asset["dco_code"] = metadata.get("dco_code") + except Exception: + pass + d = InstrumentDialog(self, dialog_asset, self.backend.namespace(), rights_profile) if d.exec() != QtWidgets.QDialog.Accepted: return try: From 212ee56d34aee578e6df701ef9f9a675661b7162 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 11:08:46 -0700 Subject: [PATCH 107/111] wallet: add software engineering domain and canonical instrument rights --- docs/v3.4/ENTITY_WALLET_20261002.md | 13 ++++ .../ENTITY_SOFTWARE_ENGINEERING_PROFILE.json | 63 +++++++++++++++++++ .../canonical_global_passport_sdk.py | 4 +- .../global_passport_profile.py | 2 +- src/38_Global_Passports/industry_profiles.py | 1 + .../industry_packages.py | 17 +++++ src/42_ENTITY_Wallet/asset_ingest.py | 2 + src/42_ENTITY_Wallet/lineage_resolver.py | 3 +- .../canonical_market_registry.py | 17 ++++- tests/test_entity_market_registry.py | 4 ++ tests/test_v3_global_passports.py | 25 ++++++-- tools/build_entity_wallet_desktop.py | 1 + tools/entity_v3_4_cli.py | 25 +++++++- 13 files changed, 167 insertions(+), 10 deletions(-) create mode 100644 protocol/profiles/ENTITY_SOFTWARE_ENGINEERING_PROFILE.json diff --git a/docs/v3.4/ENTITY_WALLET_20261002.md b/docs/v3.4/ENTITY_WALLET_20261002.md index 80456a1f..4cdb03ad 100644 --- a/docs/v3.4/ENTITY_WALLET_20261002.md +++ b/docs/v3.4/ENTITY_WALLET_20261002.md @@ -235,3 +235,16 @@ Rules: Production verification confirmed that BIRFR-1 exposes its allowed actions (BENCHMARK, CERTIFY, COMPUTE, DERIVE, EVALUATE, FINE_TUNE, INFER, OEM_DEPLOY, TRAIN) and does not expose COMMERCIALIZE, while the Blackmore Real-Time Failure Detection Engine does expose COMMERCIALIZE because that action is present in its Rights Passport. The complete wallet/economy/lineage/onboarding/market qualification campaign passed **89/89** after this change. The installed Windows executable then passed its frozen-runtime production self-test. + + +## Rights-Passport-driven instrument issuance and Software Engineering domain — 2026-10-03 + +Economic instrument issuance in the desktop wallet is now driven by the selected DCO's current Rights Passport. The wallet no longer defaults every asset to `COMMERCIALIZE`. It loads the active Rights Passport, presents only `ALLOW` actions as selectable instrument rights, displays prohibited/non-directly-issuable actions separately, validates the selected subset again at issuance, and derives the suggested rights class / ticker suffix from the selected actions. The canonical market validator remains the final fail-closed enforcement boundary. + +Example: BIRFR-1 currently exposes BENCHMARK, CERTIFY, COMPUTE, DERIVE, EVALUATE, FINE_TUNE, INFER, OEM_DEPLOY and TRAIN. `COMMERCIALIZE` is therefore not offered for BIRFR-1. Selecting TRAIN suggests `BIRFR1-TRN` and the TRAINING rights class. DCO-000002 separately allows COMMERCIALIZE because its Rights Passport explicitly grants that action. + +ENTITY now also includes a built-in `entity-profile:software-engineering@1.0` domain/profile and a `software-engineering` implementation package. This domain supports software repositories, source code, libraries, applications, algorithms, build artifacts, test evidence and engineering-control DCOs. It maps SPDX-3, CycloneDX and SLSA as non-normative interoperability mappings; the profile does not create authority or regulatory status. + +Ten active legacy ENGINEERING_CONTROL DCOs that previously carried only the Global profile were migrated by issuing new immutable Global Passport versions with the Software Engineering profile. Existing passports remain preserved. The migration did not change DCO IDs, controllers, Rights Passport IDs/hashes, evidence, provenance or economic state. The wallet now resolves all 12 current BTG assets to explicit domains: 10 Software Engineering and 2 Robotics. + +The Software Engineering profile is distributed as a pre-signed canonical profile record under `protocol/profiles`. Clean installations verify its `entity.entity` signature and import it at runtime; they do not mint or re-sign canonical profiles locally. This preserves the public-key-only trust boundary on new devices. diff --git a/protocol/profiles/ENTITY_SOFTWARE_ENGINEERING_PROFILE.json b/protocol/profiles/ENTITY_SOFTWARE_ENGINEERING_PROFILE.json new file mode 100644 index 00000000..e4deecd1 --- /dev/null +++ b/protocol/profiles/ENTITY_SOFTWARE_ENGINEERING_PROFILE.json @@ -0,0 +1,63 @@ +{ + "body_sha256": "1afcb25c1a62814b6b561b54367460ab47e35faff6cf53c8954fe8d301a708c2", + "conflict_policy": "FAIL_CLOSED", + "created_at_ms": 1791049771367, + "issuer_entity_id": "ent2-m3nofxtv3zwldhwuonw3h67hqjh2zambrhj4kaaulncpt4amkcua", + "kind": "INDUSTRY", + "object_types": [ + "ALGORITHM", + "DATASET", + "DOCUMENT", + "MODEL", + "OTHER", + "SOFTWARE" + ], + "parent_refs": [ + "entity-profile:global@1.0" + ], + "policy": { + "external_standard_not_redefined": true, + "industry_profile": "software-engineering", + "profile_schema_version": "1.0", + "technical_interoperability_not_regulatory_compliance": true + }, + "profile_id": "entity-profile:software-engineering", + "profile_is_not_authority": true, + "profile_ref": "entity-profile:software-engineering@1.0", + "public_unclassified": true, + "required_evidence_types": [ + "DOCUMENT", + "OTHER", + "REGISTRY_RECORD" + ], + "schema": "entity-v3-global-profile-v1", + "schema_sha256": "33a5703c703526d2e99f347a6ce65556ca0d91c9fd5c1283207239aaaa33fa74", + "signature": { + "entity_id": "ent2-m3nofxtv3zwldhwuonw3h67hqjh2zambrhj4kaaulncpt4amkcua", + "key_id": "sig-a31ded371cfb5019", + "payload_sha256": "1afcb25c1a62814b6b561b54367460ab47e35faff6cf53c8954fe8d301a708c2", + "signature": "rhGmk75uVFy4YmeAlFSaYJRKP8ZOOeGwE5Z84M2vuHW91XROqCWp7QT1LnFQKQBezpktox0ioKEB0cOeQamBCg", + "signature_schema": "entity-signature-record-v2", + "signed_at_ms": 1791049771397, + "suite": "ENTITY-SIG-ED25519-v1" + }, + "standards": [ + { + "normative_equivalence_claimed": false, + "role": "MAPPING", + "standard": "CYCLONEDX" + }, + { + "normative_equivalence_claimed": false, + "role": "MAPPING", + "standard": "SLSA" + }, + { + "normative_equivalence_claimed": false, + "role": "MAPPING", + "standard": "SPDX-3" + } + ], + "standards_mapping_is_not_normative_equivalence": true, + "version": "1.0" +} diff --git a/sdk/global_passport_sdk/canonical_global_passport_sdk.py b/sdk/global_passport_sdk/canonical_global_passport_sdk.py index 0cec948f..077e514e 100644 --- a/sdk/global_passport_sdk/canonical_global_passport_sdk.py +++ b/sdk/global_passport_sdk/canonical_global_passport_sdk.py @@ -2,7 +2,9 @@ PROFILE_ALIASES={ "global":"entity-profile:global@1.0","healthcare":"entity-profile:healthcare@1.0","finance":"entity-profile:finance@1.0", - "manufacturing":"entity-profile:manufacturing@1.0","ai":"entity-profile:ai@1.0","robotics":"entity-profile:robotics@1.0", + "manufacturing":"entity-profile:manufacturing@1.0","ai":"entity-profile:ai@1.0", + "software":"entity-profile:software-engineering@1.0","software-engineering":"entity-profile:software-engineering@1.0", + "engineering":"entity-profile:software-engineering@1.0","robotics":"entity-profile:robotics@1.0", "defence":"entity-profile:defence-public@1.0","defense":"entity-profile:defence-public@1.0", } diff --git a/src/38_Global_Passports/global_passport_profile.py b/src/38_Global_Passports/global_passport_profile.py index 928d8ebd..9f92836a 100644 --- a/src/38_Global_Passports/global_passport_profile.py +++ b/src/38_Global_Passports/global_passport_profile.py @@ -2,7 +2,7 @@ CORE_PRIMITIVES=["ENTITY","AUTHORITY","RIGHT","EVENT","VALUE"] MARKET_LIFECYCLE=["DCO","INSTRUMENT","LISTING","DISCLOSURE","ORDER_RFQ_AUCTION","PRICE_DISCOVERY","TRADE","CLEARING","SETTLEMENT","ENTITLEMENT","USAGE","DERIVED_OUTPUT","ECONOMIC_CONSEQUENCE"] -BUILTIN_PROFILE_REFS=["entity-profile:global@1.0","entity-profile:healthcare@1.0","entity-profile:finance@1.0","entity-profile:manufacturing@1.0","entity-profile:ai@1.0","entity-profile:robotics@1.0","entity-profile:defence-public@1.0"] +BUILTIN_PROFILE_REFS=["entity-profile:global@1.0","entity-profile:healthcare@1.0","entity-profile:finance@1.0","entity-profile:manufacturing@1.0","entity-profile:ai@1.0","entity-profile:software-engineering@1.0","entity-profile:robotics@1.0","entity-profile:defence-public@1.0"] def passport_status()->dict: return {"schema":"entity-v3-global-passport-profile-status-v1","version":"3.4.0","core_primitives":CORE_PRIMITIVES, diff --git a/src/38_Global_Passports/industry_profiles.py b/src/38_Global_Passports/industry_profiles.py index aa9a6289..ac2eb1fe 100644 --- a/src/38_Global_Passports/industry_profiles.py +++ b/src/38_Global_Passports/industry_profiles.py @@ -10,6 +10,7 @@ def _std(name:str,role:str="MAPPING")->dict: return {"standard":name,"role":role "entity-profile:finance@1.0":{"kind":"INDUSTRY","standards":[_std("ISO-20022"),_std("FIX"),_std("LEI")],"parents":["entity-profile:global@1.0"],"objects":["FINANCIAL_INSTRUMENT","DOCUMENT","DATASET","SOFTWARE"],"evidence":["DOCUMENT","PAYMENT_RECORD","REGISTRY_RECORD"]}, "entity-profile:manufacturing@1.0":{"kind":"INDUSTRY","standards":[_std("OPC-UA"),_std("ASSET-ADMINISTRATION-SHELL")],"parents":["entity-profile:global@1.0"],"objects":["DEVICE","PHYSICAL_ASSET","DIGITAL_TWIN","DATASET","SOFTWARE"],"evidence":["SENSOR_OBSERVATION","DOCUMENT"]}, "entity-profile:ai@1.0":{"kind":"INDUSTRY","standards":[_std("NIST-AI-RMF"),_std("SPDX-3"),_std("CYCLONEDX")],"parents":["entity-profile:global@1.0"],"objects":["DATASET","MODEL","SOFTWARE","AI_AGENT","DOCUMENT"],"evidence":["DOCUMENT","OTHER","REGISTRY_RECORD"]}, +"entity-profile:software-engineering@1.0":{"kind":"INDUSTRY","standards":[_std("SPDX-3"),_std("CYCLONEDX"),_std("SLSA")],"parents":["entity-profile:global@1.0"],"objects":["SOFTWARE","ALGORITHM","DATASET","DOCUMENT","MODEL","OTHER"],"evidence":["DOCUMENT","OTHER","REGISTRY_RECORD"]}, "entity-profile:robotics@1.0":{"kind":"INDUSTRY","standards":[_std("ROS-2"),_std("OPEN-RMF")],"parents":["entity-profile:global@1.0"],"objects":["DEVICE","AI_AGENT","SOFTWARE","DATASET","PHYSICAL_ASSET"],"evidence":["SENSOR_OBSERVATION","OTHER","DOCUMENT"]}, "entity-profile:defence-public@1.0":{"kind":"INDUSTRY","standards":[_std("PUBLIC-DATA-GOVERNANCE"),_std("ORIGINATOR-CONTROL")],"parents":["entity-profile:global@1.0"],"objects":["DATASET","DOCUMENT","SOFTWARE","DEVICE","MODEL"],"evidence":["DOCUMENT","REGISTRY_RECORD"],"public_unclassified":True}, } diff --git a/src/39_Implementation_Packages/industry_packages.py b/src/39_Implementation_Packages/industry_packages.py index 7e6dc060..69a0520f 100644 --- a/src/39_Implementation_Packages/industry_packages.py +++ b/src/39_Implementation_Packages/industry_packages.py @@ -65,6 +65,23 @@ def _mapping(standard:str,fields:dict[str,str])->dict: _mapping("NIST-AI-RMF",{"risk_category":"descriptor.risk_category","control":"descriptor.control_ref"})], "privacy":{"default":"SELECTIVE_DISCLOSURE","minimum_data":True,"purpose_bound":True}, }, + "software-engineering":{ + "profile_refs":[GLOBAL_PROFILE,"entity-profile:software-engineering@1.0"], + "required_config":["organization","jurisdiction","authority_source","software_governance_policy"], + "object_types":{ + "repository":"SOFTWARE","source_code":"SOFTWARE","library":"SOFTWARE","application":"SOFTWARE", + "algorithm":"ALGORITHM","build_artifact":"SOFTWARE","test_evidence":"DOCUMENT", + "engineering_control":"DATASET" + }, + "rights":["INSPECT","READ","COPY","DERIVE","EXECUTE","MODIFY"], + "evidence_types":["DOCUMENT","OTHER","REGISTRY_RECORD"], + "mappings":[ + _mapping("SPDX-3",{"name":"descriptor.component_name","version":"descriptor.component_version","license":"descriptor.license_expression"}), + _mapping("CYCLONEDX",{"bom-ref":"descriptor.bom_ref","name":"descriptor.component_name","version":"descriptor.component_version"}), + _mapping("SLSA",{"buildType":"descriptor.build_type","builder.id":"descriptor.builder_id","invocationId":"descriptor.invocation_id"}) + ], + "privacy":{"default":"SELECTIVE_DISCLOSURE","minimum_data":True,"purpose_bound":True}, + }, "robotics":{ "profile_refs":[GLOBAL_PROFILE,"entity-profile:robotics@1.0"], "required_config":["organization","jurisdiction","authority_source","safety_policy"], diff --git a/src/42_ENTITY_Wallet/asset_ingest.py b/src/42_ENTITY_Wallet/asset_ingest.py index 15e167dc..48b7f56a 100644 --- a/src/42_ENTITY_Wallet/asset_ingest.py +++ b/src/42_ENTITY_Wallet/asset_ingest.py @@ -5,6 +5,7 @@ INGEST_SCHEMA="entity-wallet-canonical-asset-ingest-v2" PACKAGE_KINDS={ + "software-engineering":["repository","source_code","library","application","algorithm","build_artifact","test_evidence","engineering_control"], "robotics":["robot","controller","model","software","algorithm","telemetry","action_record"], "ai":["training_dataset","corpus","model","weights","evaluation","agent","output"], "manufacturing":["machine","digital_twin","firmware","telemetry","maintenance_record"], @@ -14,6 +15,7 @@ } PACKAGE_POLICY_DEFAULTS={ + "software-engineering":{"software_governance_policy":"ENTITY_PROVENANCE_BOUND"}, "robotics":{"safety_policy":"ENTITY_FAIL_CLOSED"}, "ai":{"model_governance_policy":"ENTITY_PROVENANCE_BOUND"}, "manufacturing":{"asset_namespace":"ENTITY_CONTROLLER"}, diff --git a/src/42_ENTITY_Wallet/lineage_resolver.py b/src/42_ENTITY_Wallet/lineage_resolver.py index 7fadf977..98294183 100644 --- a/src/42_ENTITY_Wallet/lineage_resolver.py +++ b/src/42_ENTITY_Wallet/lineage_resolver.py @@ -100,7 +100,8 @@ def _asset_parents(self,object_id:str)->list[dict]: def _profile_label(ref:str)->str: core=str(ref).split("@",1)[0].split(":")[-1] return {"global":"Global","robotics":"Robotics","ai":"AI","manufacturing":"Manufacturing", - "healthcare":"Healthcare","finance":"Finance","defence-public":"Defence-public"}.get(core,core.replace("-"," ").title()) + "software-engineering":"Software Engineering","healthcare":"Healthcare", + "finance":"Finance","defence-public":"Defence-public"}.get(core,core.replace("-"," ").title()) def resolve(self,asset:dict)->dict: object_id=str(asset["object_id"]); controller=str(asset["controller_entity_id"]) diff --git a/src/45_ENTITY_Market/canonical_market_registry.py b/src/45_ENTITY_Market/canonical_market_registry.py index 37f63a8a..6360b2ee 100644 --- a/src/45_ENTITY_Market/canonical_market_registry.py +++ b/src/45_ENTITY_Market/canonical_market_registry.py @@ -447,15 +447,28 @@ def create_instrument(self,issuer_entity_id:str,underlying_dco_id:str,*,instrume return dict(package,signature=psig,eep_instrument=dict(eep_body,signature=sig)) def instrument(self,instrument_id:str)->dict: + iid=str(instrument_id) with self._db() as db: - r=db.execute("SELECT * FROM instrument_packages WHERE instrument_id=?",(str(instrument_id),)).fetchone() + r=db.execute("SELECT * FROM instrument_packages WHERE instrument_id=?",(iid,)).fetchone() if not r: raise KeyError("instrument package not found") d=dict(r) for src,dst in [("transfer_rules_json","transfer_rules"),("economic_terms_json","economic_terms"), ("royalty_terms_json","royalty_terms"),("buyer_receives_json","buyer_receives"), ("buyer_does_not_receive_json","buyer_does_not_receive"),("evidence_refs_json","evidence_refs")]: d[dst]=json.loads(d.pop(src) or ("[]" if "buyer_" in dst or dst=="evidence_refs" else "{}")) - d["signature"]=json.loads(d.pop("signature_json")); return d + d["signature"]=json.loads(d.pop("signature_json")) + with self.exchange._db() as db: + eep=db.execute("""SELECT rights_json,settlement_currency,delivery_mode,transferable,duration_ms,status + FROM instruments WHERE instrument_id=?""",(iid,)).fetchone() + if not eep: + raise KeyError("canonical EEP instrument record not found") + d["rights"]=json.loads(eep["rights_json"]) + d["settlement_currency"]=eep["settlement_currency"] + d["delivery_mode"]=eep["delivery_mode"] + d["transferable"]=bool(eep["transferable"]) + d["duration_ms"]=eep["duration_ms"] + d["eep_status"]=eep["status"] + return d def resolve_symbol(self,issuer_namespace:str,display_symbol:str)->dict: ns=_token(issuer_namespace); sym=_safe_symbol(display_symbol) diff --git a/tests/test_entity_market_registry.py b/tests/test_entity_market_registry.py index 2def1a8c..7764e502 100644 --- a/tests/test_entity_market_registry.py +++ b/tests/test_entity_market_registry.py @@ -59,6 +59,10 @@ def test_multi_issuer_symbols_canonical_ids_listing_sheet_and_portable_package() assert ia["market_identifier"]=="ACME:SHARED-TRN" assert ij["market_identifier"]=="JSMITH:SHARED-TRN" + loaded=reg.instrument(ia["instrument_id"]) + assert loaded["rights"]["actions"]==["EVALUATE","TRAIN"] + assert loaded["settlement_currency"]=="CAD" + assert loaded["eep_status"]=="ACTIVE" assert ia["instrument_id"]!=ij["instrument_id"] assert ia["instrument_id"].startswith("entity.instrument:v1:") assert reg.resolve_symbol("ACME","SHARED-TRN")["instrument_id"]==ia["instrument_id"] diff --git a/tests/test_v3_global_passports.py b/tests/test_v3_global_passports.py index 5a72dce9..9883a82d 100644 --- a/tests/test_v3_global_passports.py +++ b/tests/test_v3_global_passports.py @@ -41,8 +41,9 @@ def test_01_core_market_and_truth_boundaries_preserved(self): s=status_mod.passport_status(); self.assertEqual(s["core_primitives"],["ENTITY","AUTHORITY","RIGHT","EVENT","VALUE"]) self.assertFalse(s["core_semantics_changed"]); self.assertTrue(s["market_engine_preserved"]); self.assertTrue(s["evidence_truth_boundary_preserved"]) - def test_02_builtin_global_and_six_industry_profiles_install(self): - self.assertEqual(len(self.installed),7); self.assertIn("entity-profile:global@1.0",self.installed) + def test_02_builtin_global_and_seven_industry_profiles_install(self): + self.assertEqual(len(self.installed),8); self.assertIn("entity-profile:global@1.0",self.installed) + self.assertIn("entity-profile:software-engineering@1.0",self.installed) self.assertTrue(all(self.profiles.verify(p)["valid"] for p in self.installed.values())) def test_03_profile_stack_requires_global_profile(self): @@ -83,6 +84,13 @@ def test_12_ai_profile_contains_risk_and_supply_chain_mappings(self): def test_13_robotics_profile_contains_ros_and_openrmf(self): names={x["standard"] for x in self.profiles.get("entity-profile:robotics@1.0")["standards"]}; self.assertEqual(names,{"ROS-2","OPEN-RMF"}) + def test_13b_software_engineering_profile_contains_supply_chain_mappings(self): + p=self.profiles.get("entity-profile:software-engineering@1.0") + names={x["standard"] for x in p["standards"]} + self.assertEqual(names,{"SPDX-3","CYCLONEDX","SLSA"}) + self.assertIn("SOFTWARE",p["object_types"]) + self.assertIn("ALGORITHM",p["object_types"]) + def test_14_continuous_ingest_registers_bytes_evidence_rights_and_global_passport(self): f=self.source/"model.py"; f.write_text("print('entity')\n",encoding="utf-8") r=self.ingest.ingest_file(f,self.owner,["entity-profile:global@1.0","entity-profile:ai@1.0"],logical_path="models/model.py") @@ -139,9 +147,9 @@ def test_23_sdk_status_preserves_authority_and_compliance_boundaries(self): status=sdk_mod.EntityGlobalPassportSDK.capability_status(); self.assertTrue(status["sdk_does_not_create_authority"]) self.assertTrue(status["profile_is_not_regulatory_compliance"]); self.assertTrue(status["external_standards_are_mapped_not_redefined"]) - def test_24_industry_package_registry_contains_six_deployable_families(self): + def test_24_industry_package_registry_contains_seven_deployable_families(self): r=package_mod.IndustryImplementationPackageRegistry() - self.assertEqual(r.list_packages(),["ai","defence-public","finance","healthcare","manufacturing","robotics"]) + self.assertEqual(r.list_packages(),["ai","defence-public","finance","healthcare","manufacturing","robotics","software-engineering"]) self.assertTrue(all(r.get(x)["developer_configures_not_redesigns"] if "developer_configures_not_redesigns" in r.get(x) else True for x in r.list_packages())) def test_25_healthcare_fhir_mapping_is_versioned_and_non_normative(self): @@ -157,6 +165,15 @@ def test_26_manufacturing_opcua_mapping_is_executable(self): self.assertEqual(out["descriptor"]["opcua_node_id"],"ns=2;s=Machine1") self.assertTrue(out["external_standard_not_redefined"]) + def test_26b_software_engineering_package_is_deployable(self): + r=package_mod.IndustryImplementationPackageRegistry() + cfg={"organization":"Example Dev","jurisdiction":"CA","authority_source":"controller:1", + "software_governance_policy":"ENTITY_PROVENANCE_BOUND"} + plan=r.deployment_plan("software-engineering",cfg,"engineering_control") + self.assertIn("entity-profile:software-engineering@1.0",plan["profile_refs"]) + self.assertEqual(plan["object_type"],"DATASET") + self.assertIn("DERIVE",plan["rights_actions"]) + def test_27_package_configuration_requires_organization_facts(self): r=package_mod.IndustryImplementationPackageRegistry() with self.assertRaises(ValueError): r.validate_configuration("finance",{}) diff --git a/tools/build_entity_wallet_desktop.py b/tools/build_entity_wallet_desktop.py index 94c83580..8b5158d5 100644 --- a/tools/build_entity_wallet_desktop.py +++ b/tools/build_entity_wallet_desktop.py @@ -8,6 +8,7 @@ DATA_PATHS=[ "ENTITY_CURRENT_RELEASE_ORIGIN.json", "protocol/origin", + "protocol/profiles", "tools/entity_v3_4_cli.py", "src/01_Core_Runtime/identity/canonical_identity.py", "src/11_ADAM/full_runtime", diff --git a/tools/entity_v3_4_cli.py b/tools/entity_v3_4_cli.py index f65e0815..0e51db11 100644 --- a/tools/entity_v3_4_cli.py +++ b/tools/entity_v3_4_cli.py @@ -21,6 +21,9 @@ def load(name,rel): btdu_mod=load("cli_v342_btdu","src/40_BTDU/canonical_btdu.py") ORIGIN_BUNDLE=ROOT/"protocol"/"origin"/"ENTITY_PROTOCOL_ORIGIN_BUNDLE.json" CURRENT_RELEASE_TAG="v3.4.3"; CURRENT_RELEASE_REF="entity-release:"+CURRENT_RELEASE_TAG +SIGNED_PROFILE_PATHS=[ + ROOT/"protocol"/"profiles"/"ENTITY_SOFTWARE_ENGINEERING_PROFILE.json", +] def _current_release_path(explicit=None): candidates=[pathlib.Path(explicit)] if explicit else [] @@ -32,10 +35,30 @@ def runtime(state,release_origin_path=None,require_current_release=False): evidence=evidence_mod.EvidenceRegistry(state,identity); rights=rights_mod.RightsPassportRegistry(state,identity,fabric) profiles=profile_mod.GlobalProfileRegistry(state,identity); origin=origin_mod.ProtocolOriginRegistry(state,identity) bundle=read_json(ORIGIN_BUNDLE); origin_status=origin.install_bundle(bundle,profiles) + origin_record=origin.get_origin(origin_status["origin_lineage_id"]) + protocol_entity_id=origin_record["body"]["protocol_entity_id"] + signed_profile_imports=[] + for profile_path in SIGNED_PROFILE_PATHS: + if not profile_path.is_file(): + continue + record=read_json(profile_path) + if record.get("issuer_entity_id")!=protocol_entity_id: + raise RuntimeError("signed built-in profile issuer is not canonical ENTITY protocol identity") + imported=profiles.import_signed(record,allow_semantic_rebind=False) + signed_profile_imports.append({ + "profile_ref":imported["profile_ref"], + "body_sha256":imported["body_sha256"], + "issuer_entity_id":imported["issuer_entity_id"], + }) current_path=_current_release_path(release_origin_path); current_status=None if current_path: current_status=origin.install_current_release(read_json(current_path),CURRENT_RELEASE_TAG) if require_current_release and not current_status: raise RuntimeError("v3.4.3 current-release origin attestation required; supply --release-origin or use the signed release package") - origin_status=dict(origin_status,current_release_origin=current_status,current_release_origin_path=str(current_path) if current_path else None) + origin_status=dict( + origin_status, + current_release_origin=current_status, + current_release_origin_path=str(current_path) if current_path else None, + signed_builtin_profiles=signed_profile_imports, + ) passports=global_mod.GlobalPassportRegistry(state,identity,fabric,rights,profiles,origin,required_release_ref=CURRENT_RELEASE_REF if require_current_release else None) ingestion=ingest_mod.ContinuousProvenanceEngine(state,identity,fabric,evidence,rights,passports,origin.default_release_ref) packages=package_mod.IndustryImplementationPackageRegistry(); sdk=sdk_mod.EntityGlobalPassportSDK(profiles,passports,ingestion,packages) From 3dd892caefb63046d600ee6ad75adae385492947 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 17:54:40 -0700 Subject: [PATCH 108/111] wallet: publish current docs and iOS client --- README.md | 24 ++- RELEASE_V3_4_3.md | 10 ++ docs/ENGINEERING_EVIDENCE.md | 30 +++- docs/v3.4/DOMAIN_PACKAGES.md | 39 +++-- docs/v3.4/ENTITY_WALLET_20261002.md | 28 ++- ...Y_WALLET_CROSS_PLATFORM_BUILDS_20261003.md | 105 +++++++++++ docs/v3.4/GLOBAL_PASSPORT.md | 2 +- docs/v3.4/README.md | 20 ++- docs/v3.4/passports/SOFTWARE_ENGINEERING.md | 62 +++++++ .../ENTITYWalletIOS/ContentView.swift | 163 ++++++++++++++++++ .../ENTITYWalletIOS/ENTITYWalletIOSApp.swift | 10 ++ .../ENTITYWalletIOS/WalletModel.swift | 137 +++++++++++++++ ios/ENTITYWalletIOS/project.yml | 39 +++++ 13 files changed, 632 insertions(+), 37 deletions(-) create mode 100644 docs/v3.4/ENTITY_WALLET_CROSS_PLATFORM_BUILDS_20261003.md create mode 100644 docs/v3.4/passports/SOFTWARE_ENGINEERING.md create mode 100644 ios/ENTITYWalletIOS/ENTITYWalletIOS/ContentView.swift create mode 100644 ios/ENTITYWalletIOS/ENTITYWalletIOS/ENTITYWalletIOSApp.swift create mode 100644 ios/ENTITYWalletIOS/ENTITYWalletIOS/WalletModel.swift create mode 100644 ios/ENTITYWalletIOS/project.yml diff --git a/README.md b/README.md index 86ae41f8..155e702e 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ The remediation keeps authoritative occurrence identity separate from evidence i | Compiled Rust qualification | **PASS — inherited unchanged scope** | | Real-world BTDU training qualification | **PASS — inherited unchanged scope** | -Release merge commit: `528b70aabd05b1e930b77e4933f157731e47274f` +Release merge commit: `528b70aabd05b1e930b77e4933f157731e47274f` Immutable predecessor commit: `6dfa3d6cc738d9369cf092d2782676bf4f2a46e4` The v3.4.3 release does **not** rewrite the active 30-day wall-clock evidence campaign or immutable v3.4.2 evidence. @@ -144,18 +144,24 @@ Originator participation may be expressed through explicit terms such as issuanc ### ENTITY Economic Wallet -ENTITY v3.4.3 includes a **stock-style economic wallet application layer** over EEP and the Originator Participation Profile. +ENTITY v3.4.3 includes the **ENTITY Data Economy Terminal**, a native wallet application layer over DCO state, Global/Rights Passports, EEP and the Originator Participation Profile. -- **Participant Wallet:** rights positions, entitlements, usage, orders, last settled price, bid/ask, FIFO settled-trade cost basis, and economic receivables/payables. -- **Treasury Wallet:** reserve rights, participation policies, originator obligations and economic events for a distinct treasury ENTITY. -- **Fiat remains external:** the wallet does not custody CAD/USD or store payment credentials; bank/payment-service evidence feeds the existing settlement path. -- **No artificial value:** bids and offers remain quotes; only settled trades can create an observed last-price mark, and unpriced rights remain unpriced. +Current wallet behavior includes: + +- **Sovereign first-run onboarding:** create/adopt an ENTITY identity, claim a public `.entity` alias, optionally establish an organization, bind a separate Device ENTITY and authenticate through device-bound signing. +- **Digital Assets:** controller-held DCOs with lineage, signed Asset Disclosure state, Global Passport/profile stack, explicit domain and `NO TICKER` when no instrument exists. +- **Rights-Passport-driven issuance:** an economic instrument may select only a subset of actions already authorized by the active DCO Rights Passport; the canonical market registry re-validates this fail closed. +- **Issuer-scoped tickers/instruments:** ticker suggestions derive from the selected authorized rights and canonical asset short name rather than a generic commercial-right default. +- **Market / positions / orders:** instruments and positions remain separate from underlying DCO assets; listing and market activity do not manufacture intrinsic DCO value. +- **Software Engineering domain:** `entity-profile:software-engineering@1.0` explicitly covers software repositories, algorithms, libraries, build artifacts, test evidence and engineering-control DCOs. +- **Fiat remains external:** the wallet does not custody CAD/USD or store payment credentials. - **No token requirement:** `cryptocurrency_required = false` and `protocol_tax_bps = 0`. -- **No legal classification inference:** stock-style describes the market/portfolio interface; the wallet does not declare DCO/EEP rights to be corporate shares or securities. -The wallet does not create a second economic ledger. EEP balances, trades and entitlements remain authoritative, and transaction authority remains in the existing ENTITY/EEP signature path. +The final focused wallet/economy/lineage/onboarding/global-passport qualification completed on 2026-10-03 at **91/91 PASS**. The qualified production snapshot resolved all 12 current assets to explicit domains: **10 Software Engineering + 2 Robotics**. + +Windows, Linux and macOS are full PySide6 desktop builds of the canonical wallet. CI also builds a native Xcode iOS Simulator portable-snapshot client; native iOS authority/write parity is explicitly not claimed. -[**Wallet design, deployment and usage**](docs/v3.4/ENTITY_WALLET_20261002.md) · [**Public qualification**](docs/evidence/v3.4.3-entity-wallet-20261002/QUALIFICATION.json) +[**Wallet design and current state**](docs/v3.4/ENTITY_WALLET_20261002.md) · [**Cross-platform builds**](docs/v3.4/ENTITY_WALLET_CROSS_PLATFORM_BUILDS_20261003.md) · [**Identity onboarding**](docs/v3.4/ENTITY_WALLET_IDENTITY_ONBOARDING.md) · [**Software Engineering profile**](docs/v3.4/passports/SOFTWARE_ENGINEERING.md) --- diff --git a/RELEASE_V3_4_3.md b/RELEASE_V3_4_3.md index 7dbbc80e..ec867381 100644 --- a/RELEASE_V3_4_3.md +++ b/RELEASE_V3_4_3.md @@ -29,6 +29,16 @@ Causal tracing now validates both endpoints before traversal. Missing or empty e - v3.4.2 remains immutable historical evidence. - Production state and the active 30-day wall-clock qualification were not modified. +## Post-tag wallet application layer on main + +The immutable `v3.4.3` tag remains the bounded remediation release described above. Subsequent main-branch application-layer work adds the ENTITY Data Economy Terminal without changing the Protocol 1.0 freeze or silently rewriting the historical tag. + +The current wallet application layer includes sovereign first-run identity/device onboarding, DCO asset holdings, signed asset disclosures, issuer-scoped instruments/tickers, Rights-Passport-driven instrument issuance, the Software Engineering domain/profile and cross-platform wallet builds. + +The final focused wallet/economy/lineage/onboarding/global-passport campaign completed on 2026-10-03 at **91/91 PASS**. Production-state verification resolved 12 current assets to explicit domains: 10 Software Engineering and 2 Robotics. + +See `docs/v3.4/ENTITY_WALLET_20261002.md` and `docs/v3.4/ENTITY_WALLET_CROSS_PLATFORM_BUILDS_20261003.md`. + ## Claim boundary This release is limited to the two reproduced Issue #28 defects. It does not establish external settlement, payment, legal-entitlement, fair-value, or broader economic-causality claims beyond the qualified behavior above. diff --git a/docs/ENGINEERING_EVIDENCE.md b/docs/ENGINEERING_EVIDENCE.md index 5896db9b..73394ef1 100644 --- a/docs/ENGINEERING_EVIDENCE.md +++ b/docs/ENGINEERING_EVIDENCE.md @@ -1,9 +1,9 @@ # ENTITY Engineering Evidence -**Document class:** evidence index / current-runtime orientation -**Current supported runtime:** ENTITY v3.4.3 -**BTDU component:** Blackmore Technology Data Universe (BTDU) 3.4.2, unchanged -**Protocol 1.0:** separate frozen clean-room target +**Document class:** evidence index / current-runtime orientation +**Current supported runtime:** ENTITY v3.4.3 +**BTDU component:** Blackmore Technology Data Universe (BTDU) 3.4.2, unchanged +**Protocol 1.0:** separate frozen clean-room target **Normative effect:** none; this page indexes and explains evidence rather than changing it This document separates **what Blackmore Technology Group Limited (BTG) has directly qualified**, **what unrelated people have reproduced**, and **what still requires independent evidence outside BTG control**. @@ -28,8 +28,8 @@ A successful result at class 4 does not automatically become class 5 or 6. A BTG ## Current supported release — ENTITY v3.4.3 -**Release:** `v3.4.3` -**Release merge commit:** `528b70aabd05b1e930b77e4933f157731e47274f` +**Release:** `v3.4.3` +**Release merge commit:** `528b70aabd05b1e930b77e4933f157731e47274f` **Immutable predecessor:** v3.4.2 commit `6dfa3d6cc738d9369cf092d2782676bf4f2a46e4` ENTITY v3.4.3 is a bounded remediation release for the two defects reproduced in Issue #28: @@ -63,6 +63,20 @@ These are **inherited records**, not re-executed v3.4.3 campaigns. Authoritative release material: [`RELEASE_V3_4_3.md`](../RELEASE_V3_4_3.md) and [`RELEASE_V3_4_3.json`](../RELEASE_V3_4_3.json). +## Post-tag ENTITY Wallet qualification — 2026-10-03 + +The current main-branch wallet application layer is later than the immutable `v3.4.3` tag and is therefore recorded separately from the tagged release evidence. + +Final focused wallet/economy/lineage/onboarding/global-passport campaign: **91/91 PASS**. + +Wallet implementation baseline: `212ee56d34aee578e6df701ef9f9a675661b7162`. + +The locally rebuilt/installed Windows package passed its frozen-runtime self-test. Qualified production-state domain resolution contained 12 current assets: 10 Software Engineering and 2 Robotics, with no missing domain. + +Windows/Linux/macOS full desktop binaries are produced by the cross-platform wallet workflow. The iOS Simulator artifact is a native portable-snapshot inspection client and is not classified as an independently qualified native authority/runtime implementation. + +See [wallet state](v3.4/ENTITY_WALLET_20261002.md) and [cross-platform builds](v3.4/ENTITY_WALLET_CROSS_PLATFORM_BUILDS_20261003.md). + ## External reproduction evidence An unrelated GitHub contributor reproduced the published BTG-controlled Java v3.4.2 26-vector baseline on Windows 11 / OpenJDK 21 / Maven 3.9.6 and reported: @@ -103,7 +117,7 @@ A full external Protocol 1.0 interoperability PASS still requires the published ## v3.4.1 — historical protected release -**Release:** `v3.4.1` +**Release:** `v3.4.1` **Protected release commit:** `9822b1b65f8269ebc17208a342809720729ae2f8` Historical qualification retained exactly as v3.4.1 evidence: @@ -122,7 +136,7 @@ Protocol origin remains separate from user/asset provenance and does not create ## v3.4.0 — historical protected release and domain packages -**Release:** `v3.4.0` +**Release:** `v3.4.0` **Protected release commit:** `2db5bff64507b8d67642122a5ff2fc73dfef9152` Historical qualification: diff --git a/docs/v3.4/DOMAIN_PACKAGES.md b/docs/v3.4/DOMAIN_PACKAGES.md index 0011ccdb..7dc7c2b1 100644 --- a/docs/v3.4/DOMAIN_PACKAGES.md +++ b/docs/v3.4/DOMAIN_PACKAGES.md @@ -1,24 +1,37 @@ -# ENTITY v3.4.0 Domain Packages +# ENTITY v3.4 Domain Packages -The six repositories below are **deployable domain packages for the one ENTITY Global Passport**. They are not six separate passport protocols. +ENTITY uses **one Global Passport** with composable domain profiles and implementation packages. Domain packages do not create separate passport protocols or separate sovereignty systems. -| Domain | Repository | Included mappings | Package SHA-256 | +## Public domain packages and built-in profiles + +| Domain | Distribution | Included mappings | Status | | --- | --- | --- | --- | -| [Healthcare](passports/HEALTHCARE.md) | [ENTITY-HEALTHCARE](https://github.com/blackmore-technology-group/ENTITY-HEALTHCARE) | HL7 FHIR · DICOM | `b4e901ce37f696fa…` | -| [Finance](passports/FINANCE.md) | [ENTITY-FINANCE](https://github.com/blackmore-technology-group/ENTITY-FINANCE) | ISO 20022 · FIX · LEI | `768dd87fd5a29c7e…` | -| [Manufacturing](passports/MANUFACTURING.md) | [ENTITY-MANUFACTURING](https://github.com/blackmore-technology-group/ENTITY-MANUFACTURING) | OPC UA · Asset Administration Shell | `bc29a0de024cea22…` | -| [AI](passports/AI.md) | [ENTITY-AI](https://github.com/blackmore-technology-group/ENTITY-AI) | NIST AI RMF · SPDX 3 · CycloneDX | `4877cb5bc76ef080…` | -| [Robotics](passports/ROBOTICS.md) | [ENTITY-ROBOTICS](https://github.com/blackmore-technology-group/ENTITY-ROBOTICS) | ROS 2 · Open-RMF | `3a0e68fa6c63b2ef…` | -| [Defence / Public-Unclassified](passports/DEFENCE_PUBLIC.md) | [ENTITY-DEFENCE](https://github.com/blackmore-technology-group/ENTITY-DEFENCE) | Public Data Governance · Originator Control | `7edc52344822e370…` | +| [Healthcare](passports/HEALTHCARE.md) | [ENTITY-HEALTHCARE](https://github.com/blackmore-technology-group/ENTITY-HEALTHCARE) | HL7 FHIR · DICOM | first-party package | +| [Finance](passports/FINANCE.md) | [ENTITY-FINANCE](https://github.com/blackmore-technology-group/ENTITY-FINANCE) | ISO 20022 · FIX · LEI | first-party package | +| [Manufacturing](passports/MANUFACTURING.md) | [ENTITY-MANUFACTURING](https://github.com/blackmore-technology-group/ENTITY-MANUFACTURING) | OPC UA · Asset Administration Shell | first-party package | +| [AI](passports/AI.md) | [ENTITY-AI](https://github.com/blackmore-technology-group/ENTITY-AI) | NIST AI RMF · SPDX 3 · CycloneDX | first-party package | +| [Robotics](passports/ROBOTICS.md) | [ENTITY-ROBOTICS](https://github.com/blackmore-technology-group/ENTITY-ROBOTICS) | ROS 2 · Open-RMF | first-party package | +| [Defence / Public-Unclassified](passports/DEFENCE_PUBLIC.md) | [ENTITY-DEFENCE](https://github.com/blackmore-technology-group/ENTITY-DEFENCE) | Public Data Governance · Originator Control | first-party package | +| [Software Engineering](passports/SOFTWARE_ENGINEERING.md) | core ENTITY repository | SPDX 3 · CycloneDX · SLSA | built-in profile/package | + +The six historical external domain repositories retain their sealed package evidence. Software Engineering was added on 2026-10-03 as a built-in signed profile and implementation package so software repositories, algorithms, build artifacts, test evidence and engineering-control DCOs no longer fall back to an unspecified/global-only domain. ## Deployment model -**Select package → configure organization facts → connect systems/data → ingest → verify passport → run conformance → deploy.** +**Select package/profile → configure organization facts → connect systems/data → ingest → verify passport → run conformance → deploy.** + +Every domain mapping remains subordinate to the registered profile and underlying ENTITY authority/rights state. External standards remain externally authoritative: ENTITY mappings describe correspondence under a mapping version and do not claim normative equivalence. + +## Wallet domain behavior + +The ENTITY Wallet resolves the current Global Passport/profile stack for each DCO. Domain presentation does not create economic rights. -Every package contains schemas, mappings, templates, conformance fixtures, SDK material and a package verifier. External standards remain externally authoritative: ENTITY mappings describe correspondence under a mapping version and do not claim normative equivalence. +The 2026-10-03 production migration issued new immutable Global Passport versions for ten legacy `ENGINEERING_CONTROL` DCOs while preserving prior passports and underlying DCO state. The qualified production wallet then resolved all 12 current assets to explicit domains: -The sealed package contents are intentionally preserved. Additional documentation and GitHub wiki material may be published without modifying the sealed package hashes. +- 10 Software Engineering; +- 2 Robotics; +- 0 missing domains. ## Qualification boundary -Package verification demonstrates internal package integrity and binding to the v3.4 release materials. It does not establish regulatory compliance, legal title, objective external truth, accounting fair value, independent security review or unrelated third-party interoperability. +Package/profile verification demonstrates internal integrity and binding to ENTITY release/runtime material. It does not establish regulatory compliance, legal title, objective external truth, accounting fair value, independent security review or unrelated third-party interoperability. diff --git a/docs/v3.4/ENTITY_WALLET_20261002.md b/docs/v3.4/ENTITY_WALLET_20261002.md index 4cdb03ad..b13fbf80 100644 --- a/docs/v3.4/ENTITY_WALLET_20261002.md +++ b/docs/v3.4/ENTITY_WALLET_20261002.md @@ -171,7 +171,7 @@ The PySide6 build continues to preserve: - listing/market activity does not imply intrinsic DCO value; - BTDU, ADAM and NIKI do not create ownership. -The 2026-10-03 desktop/economy/lineage regression campaign passed **84/84** before the Qt package was built. The packaged Windows executable then passed a production-state frozen-runtime self-test with exit code 0. +An **earlier 2026-10-03 checkpoint** of the desktop/economy/lineage regression campaign passed **84/84** before the Qt package was built; later wallet changes expanded the focused qualification to the final 91/91 campaign recorded below. The packaged Windows executable then passed a production-state frozen-runtime self-test with exit code 0. ## Signed Asset Disclosure and Listing Information Sheet v2 — 2026-10-03 @@ -234,7 +234,7 @@ Rules: Production verification confirmed that BIRFR-1 exposes its allowed actions (BENCHMARK, CERTIFY, COMPUTE, DERIVE, EVALUATE, FINE_TUNE, INFER, OEM_DEPLOY, TRAIN) and does not expose COMMERCIALIZE, while the Blackmore Real-Time Failure Detection Engine does expose COMMERCIALIZE because that action is present in its Rights Passport. -The complete wallet/economy/lineage/onboarding/market qualification campaign passed **89/89** after this change. The installed Windows executable then passed its frozen-runtime production self-test. +That **intermediate rights-driven checkpoint** of the wallet/economy/lineage/onboarding/market qualification campaign passed **89/89** after this change; later Software Engineering/profile packaging work expanded the final focused campaign to 91/91 as recorded below. The installed Windows executable then passed its frozen-runtime production self-test. ## Rights-Passport-driven instrument issuance and Software Engineering domain — 2026-10-03 @@ -248,3 +248,27 @@ ENTITY now also includes a built-in `entity-profile:software-engineering@1.0` do Ten active legacy ENGINEERING_CONTROL DCOs that previously carried only the Global profile were migrated by issuing new immutable Global Passport versions with the Software Engineering profile. Existing passports remain preserved. The migration did not change DCO IDs, controllers, Rights Passport IDs/hashes, evidence, provenance or economic state. The wallet now resolves all 12 current BTG assets to explicit domains: 10 Software Engineering and 2 Robotics. The Software Engineering profile is distributed as a pre-signed canonical profile record under `protocol/profiles`. Clean installations verify its `entity.entity` signature and import it at runtime; they do not mint or re-sign canonical profiles locally. This preserves the public-key-only trust boundary on new devices. + + +## Final 2026-10-03 wallet qualification and publication state + +The complete focused wallet/economy/lineage/onboarding/global-passport campaign was rerun from a clean committed worktree and passed **91/91** immediately before the final Windows package was rebuilt. + +Wallet implementation baseline: + +`212ee56d34aee578e6df701ef9f9a675661b7162` +`wallet: add software engineering domain and canonical instrument rights` + +The final locally installed Windows package from that baseline passed its frozen-runtime self-test and has SHA-256: + +`4F360D9F2C9DC51600B2C5D76FAA09364ADEF6762255D17F4DC528BDF3C069DD` + +The production-state domain verification resolved all 12 current wallet assets: + +- **10 Software Engineering**; +- **2 Robotics**; +- **0 missing domains**. + +The repository now includes [cross-platform build documentation](ENTITY_WALLET_CROSS_PLATFORM_BUILDS_20261003.md) and CI artifacts for Windows, Linux and macOS desktop wallets plus a native Xcode-built iOS Simulator portable-snapshot client. + +The iOS client is intentionally not represented as a native port of the Python authority runtime. Canonical identity, signing, DCO registration, Rights/Global Passport issuance, economic-instrument issuance, listing, orders and settlement mutations remain authoritative ENTITY runtime operations until a separately qualified native iOS authority implementation exists. diff --git a/docs/v3.4/ENTITY_WALLET_CROSS_PLATFORM_BUILDS_20261003.md b/docs/v3.4/ENTITY_WALLET_CROSS_PLATFORM_BUILDS_20261003.md new file mode 100644 index 00000000..0470d480 --- /dev/null +++ b/docs/v3.4/ENTITY_WALLET_CROSS_PLATFORM_BUILDS_20261003.md @@ -0,0 +1,105 @@ +# ENTITY Wallet cross-platform builds — 2026-10-03 + +**Runtime family:** ENTITY v3.4.3 +**Wallet architecture:** ENTITY Data Economy Terminal +**Canonical desktop implementation:** PySide6 / Qt + canonical ENTITY runtime modules +**Final desktop qualification before cross-platform build publication:** 91/91 PASS + +## Build surfaces + +The repository publishes four build artifacts from the `ENTITY Wallet Cross-Platform Builds` GitHub Actions workflow: + +| Artifact | Build environment | Scope | +| --- | --- | --- | +| `ENTITY-Wallet-Windows` | `windows-latest` | Full canonical desktop wallet | +| `ENTITY-Wallet-Linux` | `ubuntu-latest` | Full canonical desktop wallet | +| `ENTITY-Wallet-macOS` | `macos-latest` | Full canonical desktop wallet | +| `ENTITY-Wallet-iOS-Simulator` | `macos-latest` + Xcode | Native iOS portable-snapshot inspection client | + +Windows, Linux and macOS execute the same Python/PySide6 wallet source and package the same canonical runtime modules. + +## iOS boundary + +The current canonical wallet implementation is a Python/PySide6 desktop application. Its PyInstaller deployment path targets desktop operating systems and is not treated as an iOS authority runtime. + +The iOS target is therefore deliberately implemented as a native SwiftUI **portable wallet inspection client**. It can import a portable JSON wallet snapshot and display: + +- wallet lineage; +- issuer namespace and market-registry version when present; +- Digital Commodity Object holdings; +- asset codes and immutable object identifiers; +- explicit domain lineage; +- Rights-Passport `ALLOW` actions present in the imported snapshot; +- issued economic instruments, market identifiers/tickers, rights classes and instrument actions; +- the explicit `NO TICKER` condition when an asset has no economic instrument. + +The iOS client does **not** silently create a human identity from device information and does not locally invent authority. + +Canonical write operations remain ENTITY runtime operations, including: + +- sovereign identity creation/adoption; +- device-bound signing and authentication; +- DCO registration; +- Rights Passport / Global Passport issuance; +- economic instrument issuance; +- listings and signed market orders; +- settlement/economic-state mutation. + +This keeps the mobile build useful without falsely representing an unqualified native port as equivalent to the canonical desktop authority engine. + +## Rights-driven issuance retained across builds + +The current wallet rule is: + +`DCO Rights Passport = maximum authorized actions` + +`Economic Instrument = selected subset of those authorized actions` + +The desktop wallet presents only current Rights-Passport `ALLOW` actions as directly selectable economic rights. The canonical market registry independently rejects any instrument whose requested actions exceed the Rights Passport. + +The iOS client reflects imported Rights-Passport actions but cannot expand them. + +## Domain state + +The production wallet qualification used for the final Windows rebuild resolved all 12 current wallet assets to explicit domains: + +- 10 × **Software Engineering**; +- 2 × **Robotics**; +- 0 × missing domain. + +The Software Engineering profile is distributed as `entity-profile:software-engineering@1.0` with a pre-signed canonical profile record under `protocol/profiles`. + +## First-run identity model + +A clean desktop installation follows: + +`Install → create/adopt ENTITY identity → claim public .entity alias → optional organization → bind Device ENTITY → authenticate → open wallet` + +Device metadata is not allowed to silently become a human or organization identity. Public names are aliases; immutable Entity IDs and signatures remain authoritative. + +## Qualification and packaging + +The final focused wallet/economy/lineage/onboarding/global-passport suite passed **91/91** immediately before the final Windows package was rebuilt. + +The installed Windows package created from wallet implementation commit: + +`212ee56d34aee578e6df701ef9f9a675661b7162` + +passed its frozen-runtime self-test. + +Final locally installed Windows EXE SHA-256 from that qualification: + +`4F360D9F2C9DC51600B2C5D76FAA09364ADEF6762255D17F4DC528BDF3C069DD` + +That hash identifies the locally qualified Windows package. GitHub-hosted Windows/Linux/macOS/iOS artifacts are separately built by GitHub Actions and therefore have their own artifact hashes. + +## CI trigger + +The cross-platform workflow runs on: + +- manual dispatch; +- pushes to `main`; +- pushes to the wallet integration branch; +- relevant pull requests. + +A failing platform job does not cancel the other platform jobs. Every published artifact must come from a successful platform job. diff --git a/docs/v3.4/GLOBAL_PASSPORT.md b/docs/v3.4/GLOBAL_PASSPORT.md index 6405517f..c62cb460 100644 --- a/docs/v3.4/GLOBAL_PASSPORT.md +++ b/docs/v3.4/GLOBAL_PASSPORT.md @@ -28,4 +28,4 @@ The v3.4 layer does not replace the existing data-rights lifecycle: Nor does it replace the v3.3 reality/evidence path. It composes those layers into a deployable passport surface. -See [Domain Packages](DOMAIN_PACKAGES.md) for the six first-party implementation packages. +See [Domain Packages](DOMAIN_PACKAGES.md) for the first-party domain packages and the built-in Software Engineering profile/package. diff --git a/docs/v3.4/README.md b/docs/v3.4/README.md index 0ae29008..09edfc6a 100644 --- a/docs/v3.4/README.md +++ b/docs/v3.4/README.md @@ -1,8 +1,8 @@ # ENTITY v3.4 Family Runtime Portal -**Document class:** current-runtime family orientation / non-normative to Protocol 1.0 -**Current supported runtime:** [ENTITY v3.4.3](https://github.com/blackmore-technology-group/ENTITY/releases/tag/v3.4.3) -**BTDU component:** Blackmore Technology Data Universe (BTDU) 3.4.2, unchanged +**Document class:** current-runtime family orientation / non-normative to Protocol 1.0 +**Current supported runtime:** [ENTITY v3.4.3](https://github.com/blackmore-technology-group/ENTITY/releases/tag/v3.4.3) +**BTDU component:** Blackmore Technology Data Universe (BTDU) 3.4.2, unchanged **Historical v3.4.0/v3.4.1/v3.4.2 evidence:** preserved under its original release targets This directory describes the **v3.4 runtime family**: Global Passport architecture, domain packages, deployment and historical closure material. It is not the normative ENTITY Protocol 1.0 clean-room specification. @@ -48,12 +48,24 @@ These runtime primitives and packages must not be mistaken for extra Protocol 1. ## Start here - [Global Passport runtime architecture](GLOBAL_PASSPORT.md) -- [Six executable domain packages](DOMAIN_PACKAGES.md) +- [Domain packages and profiles](DOMAIN_PACKAGES.md) - [Deployment model](DEPLOYMENT_MODEL.md) - [Historical v3.4.0 post-release recursive closure](RECURSIVE_CLOSURE.md) - [ENTITY v3.4.3 release record](../../RELEASE_V3_4_3.md) +- [ENTITY Wallet — current application layer](ENTITY_WALLET_20261002.md) +- [Wallet identity onboarding](ENTITY_WALLET_IDENTITY_ONBOARDING.md) +- [Wallet cross-platform builds](ENTITY_WALLET_CROSS_PLATFORM_BUILDS_20261003.md) +- [Software Engineering profile](passports/SOFTWARE_ENGINEERING.md) - [Engineering evidence](../ENGINEERING_EVIDENCE.md) +## Current wallet application layer + +The current wallet is the **ENTITY Data Economy Terminal**. It includes sovereign first-run identity/lineage onboarding, Digital Asset/DCO holdings, signed asset disclosures, issuer-scoped economic instruments and tickers, Rights-Passport-driven instrument issuance, market listings/positions/orders and explicit domain lineage. + +The final focused qualification completed on 2026-10-03 at **91/91 PASS**. The qualified production snapshot contained 12 current assets with no missing domains: 10 Software Engineering and 2 Robotics. + +Windows, Linux and macOS are full PySide6 desktop builds of the canonical wallet. The repository also builds a native iOS Simulator portable-snapshot inspection client; native iOS write-authority parity is not claimed. + ## Historical v3.4.1 qualification snapshot The following values remain historical v3.4.1 evidence and are **not rewritten** to make them look current: diff --git a/docs/v3.4/passports/SOFTWARE_ENGINEERING.md b/docs/v3.4/passports/SOFTWARE_ENGINEERING.md new file mode 100644 index 00000000..2758dedc --- /dev/null +++ b/docs/v3.4/passports/SOFTWARE_ENGINEERING.md @@ -0,0 +1,62 @@ +# ENTITY Software Engineering Profile + +**Profile reference:** `entity-profile:software-engineering@1.0` + +The Software Engineering profile supplies an explicit ENTITY domain for software repositories, source code, libraries, applications, algorithms, build artifacts, test evidence and engineering-control DCOs. + +It is a built-in ENTITY implementation package rather than a separate sovereignty system or a claim that an external software standard is redefined by ENTITY. + +## Object scope + +The profile supports Digital Commodity Objects representing: + +- software; +- source repositories; +- algorithms; +- libraries and packages; +- executable/build artifacts; +- test and qualification evidence; +- documents and datasets used in software engineering; +- engineering-control DCOs. + +## Standards mappings + +The profile carries non-normative interoperability mappings for: + +- SPDX 3; +- CycloneDX; +- SLSA. + +Those mappings are correspondence metadata only. They do not create certification, regulatory status, supply-chain authority or legal rights. + +## Wallet use + +The ENTITY Wallet resolves the profile through the current Global Passport and displays **Software Engineering** as the DCO's primary domain where this profile is authoritative for the asset. + +The 2026-10-03 production migration issued new immutable Global Passport versions for ten legacy `ENGINEERING_CONTROL` DCOs that previously carried only the Global profile. + +The migration preserved: + +- DCO IDs; +- controllers; +- Rights Passport IDs and hashes; +- evidence; +- provenance; +- existing economic state; +- prior Global Passport records. + +It added explicit domain context; it did not rewrite the underlying DCOs or manufacture economic rights. + +## Rights boundary + +Software Engineering domain classification does not grant instrument actions. + +Economic instruments may contain only actions already authorized by the active DCO Rights Passport. Domain profiles and Rights Passports remain separate authority surfaces. + +## Distribution + +The canonical signed profile record is published at: + +`protocol/profiles/ENTITY_SOFTWARE_ENGINEERING_PROFILE.json` + +Clean installations verify its signature and import it; they do not mint or re-sign the canonical profile locally. diff --git a/ios/ENTITYWalletIOS/ENTITYWalletIOS/ContentView.swift b/ios/ENTITYWalletIOS/ENTITYWalletIOS/ContentView.swift new file mode 100644 index 00000000..48b525b1 --- /dev/null +++ b/ios/ENTITYWalletIOS/ENTITYWalletIOS/ContentView.swift @@ -0,0 +1,163 @@ +import SwiftUI +import UniformTypeIdentifiers + +struct ContentView: View { + @State private var snapshot: WalletSnapshot? + @State private var showingImporter = false + @State private var importError: String? + + var body: some View { + TabView { + NavigationStack { + List { + Section("Wallet") { + LabeledContent("Lineage", value: snapshot?.walletLineage ?? "No snapshot imported") + LabeledContent("Issuer namespace", value: snapshot?.issuerNamespace ?? "—") + LabeledContent("Market registry", value: snapshot?.marketRegistryVersion ?? "—") + LabeledContent("Assets", value: "\(snapshot?.assets.count ?? 0)") + LabeledContent("Instruments", value: "\(snapshot?.instruments.count ?? 0)") + } + + Section("Reference build") { + LabeledContent("ENTITY runtime", value: "v3.4.3") + LabeledContent("Desktop qualification", value: "91/91 PASS") + LabeledContent("Issuance policy", value: "Rights-Passport-driven") + Text("The reference BTG production snapshot used for qualification resolved 12 current assets to explicit domains: 10 Software Engineering and 2 Robotics.") + .font(.footnote) + .foregroundStyle(.secondary) + } + + Section { + Button { + showingImporter = true + } label: { + Label("Import wallet snapshot", systemImage: "square.and.arrow.down") + } + } footer: { + Text("The iOS client imports portable JSON snapshots. It does not silently create identities from device metadata and it does not expand Rights Passport authority.") + } + + Section("Write boundary") { + Text("Canonical identity creation, device-bound signing, DCO registration, economic-instrument issuance, listing and order submission remain authoritative ENTITY runtime operations. This iOS simulator build is a portable wallet inspection client, not a replacement authority engine.") + .font(.footnote) + } + } + .navigationTitle("ENTITY Wallet") + } + .tabItem { Label("Overview", systemImage: "wallet.pass") } + + NavigationStack { + Group { + if let assets = snapshot?.assets, !assets.isEmpty { + List(assets) { asset in + NavigationLink { + AssetDetail(asset: asset) + } label: { + VStack(alignment: .leading, spacing: 4) { + Text(asset.title).font(.headline) + Text(asset.domain).font(.subheadline).foregroundStyle(.secondary) + Text(asset.assetCode ?? asset.objectId) + .font(.caption.monospaced()) + .foregroundStyle(.secondary) + } + } + } + } else { + ContentUnavailableView( + "No Assets", + systemImage: "shippingbox", + description: Text("Import a portable wallet snapshot to inspect DCO assets and domain lineage.") + ) + } + } + .navigationTitle("Digital Assets") + } + .tabItem { Label("Assets", systemImage: "shippingbox") } + + NavigationStack { + Group { + if let instruments = snapshot?.instruments, !instruments.isEmpty { + List(instruments) { instrument in + VStack(alignment: .leading, spacing: 5) { + Text(instrument.marketIdentifier).font(.headline.monospaced()) + Text(instrument.rightsClass ?? "Unclassified rights") + .font(.subheadline) + .foregroundStyle(.secondary) + if !instrument.actions.isEmpty { + Text(instrument.actions.joined(separator: " · ")) + .font(.caption) + .foregroundStyle(.secondary) + } + } + } + } else { + ContentUnavailableView( + "No Instruments", + systemImage: "chart.bar.doc.horizontal", + description: Text("A DCO may legitimately have no ticker. Import a wallet snapshot to inspect issued economic instruments.") + ) + } + } + .navigationTitle("Instruments") + } + .tabItem { Label("Instruments", systemImage: "chart.bar.doc.horizontal") } + } + .fileImporter( + isPresented: $showingImporter, + allowedContentTypes: [.json], + allowsMultipleSelection: false + ) { result in + do { + let url = try result.get().first! + let scoped = url.startAccessingSecurityScopedResource() + defer { if scoped { url.stopAccessingSecurityScopedResource() } } + let data = try Data(contentsOf: url) + snapshot = try JSONDecoder().decode(WalletSnapshot.self, from: data) + } catch { + importError = error.localizedDescription + } + } + .alert("Snapshot import failed", isPresented: Binding( + get: { importError != nil }, + set: { if !$0 { importError = nil } } + )) { + Button("OK", role: .cancel) { importError = nil } + } message: { + Text(importError ?? "Unknown error") + } + } +} + +private struct AssetDetail: View { + let asset: WalletAsset + + var body: some View { + List { + Section("Asset") { + LabeledContent("Title", value: asset.title) + LabeledContent("Domain", value: asset.domain) + if let code = asset.assetCode { + LabeledContent("Asset code", value: code) + } + Text(asset.objectId) + .font(.caption.monospaced()) + .textSelection(.enabled) + } + + Section("Issuable Rights Passport actions") { + if asset.rightsActions.isEmpty { + Text("No directly issuable ALLOW actions were present in the imported snapshot.") + .foregroundStyle(.secondary) + } else { + ForEach(asset.rightsActions, id: \.self) { action in + Label(action, systemImage: "checkmark.shield") + } + } + } footer: { + Text("Economic instruments may select only a subset of actions already authorized by the active Rights Passport. The mobile client does not create new authority.") + } + } + .navigationTitle("Asset") + .navigationBarTitleDisplayMode(.inline) + } +} diff --git a/ios/ENTITYWalletIOS/ENTITYWalletIOS/ENTITYWalletIOSApp.swift b/ios/ENTITYWalletIOS/ENTITYWalletIOS/ENTITYWalletIOSApp.swift new file mode 100644 index 00000000..5f0db610 --- /dev/null +++ b/ios/ENTITYWalletIOS/ENTITYWalletIOS/ENTITYWalletIOSApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct ENTITYWalletIOSApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/ios/ENTITYWalletIOS/ENTITYWalletIOS/WalletModel.swift b/ios/ENTITYWalletIOS/ENTITYWalletIOS/WalletModel.swift new file mode 100644 index 00000000..c0bf4242 --- /dev/null +++ b/ios/ENTITYWalletIOS/ENTITYWalletIOS/WalletModel.swift @@ -0,0 +1,137 @@ +import Foundation + +struct WalletSnapshot: Decodable { + let walletLineage: String? + let issuerNamespace: String? + let marketRegistryVersion: String? + let assets: [WalletAsset] + let instruments: [WalletInstrument] + + enum CodingKeys: String, CodingKey { + case walletLineage = "wallet_lineage" + case issuerNamespace = "issuer_namespace" + case marketRegistryVersion = "market_registry_version" + case assets + case instruments + } + + init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + walletLineage = try c.decodeIfPresent(String.self, forKey: .walletLineage) + issuerNamespace = try c.decodeIfPresent(String.self, forKey: .issuerNamespace) + marketRegistryVersion = try c.decodeIfPresent(String.self, forKey: .marketRegistryVersion) + assets = (try? c.decode([WalletAsset].self, forKey: .assets)) ?? [] + instruments = (try? c.decode([WalletInstrument].self, forKey: .instruments)) ?? [] + } +} + +struct WalletAsset: Decodable, Identifiable { + let objectId: String + let title: String + let assetCode: String? + let domain: String + let rightsActions: [String] + + var id: String { objectId } + + enum CodingKeys: String, CodingKey { + case objectId = "object_id" + case title + case assetCode = "asset_code" + case domain + case rightsActions = "rights_actions" + case lineage + case rightsPassport = "rights_passport" + } + + private struct Lineage: Decodable { + let protocolLineage: ProtocolLineage? + enum CodingKeys: String, CodingKey { case protocolLineage = "protocol_lineage" } + } + + private struct ProtocolLineage: Decodable { + let primaryDomainProfile: String? + enum CodingKeys: String, CodingKey { case primaryDomainProfile = "primary_domain_profile" } + } + + private struct RightsPassport: Decodable { + let rights: [RightRule]? + } + + private struct RightRule: Decodable { + let effect: String? + let actions: [String]? + } + + init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + objectId = (try? c.decode(String.self, forKey: .objectId)) ?? UUID().uuidString + title = (try? c.decode(String.self, forKey: .title)) ?? objectId + assetCode = try? c.decodeIfPresent(String.self, forKey: .assetCode) + let directDomain = (try? c.decodeIfPresent(String.self, forKey: .domain)) ?? nil + let lineage = (try? c.decodeIfPresent(Lineage.self, forKey: .lineage)) ?? nil + domain = directDomain ?? lineage?.protocolLineage?.primaryDomainProfile ?? "Unspecified" + + if let direct = (try? c.decodeIfPresent([String].self, forKey: .rightsActions)) ?? nil { + rightsActions = direct.sorted() + } else if let passport = (try? c.decodeIfPresent(RightsPassport.self, forKey: .rightsPassport)) ?? nil, + let rules = passport.rights { + rightsActions = rules + .filter { ($0.effect ?? "").uppercased() == "ALLOW" } + .flatMap { $0.actions ?? [] } + .reduce(into: Set()) { $0.insert($1) } + .sorted() + } else { + rightsActions = [] + } + } +} + +struct WalletInstrument: Decodable, Identifiable { + let instrumentId: String + let marketIdentifier: String + let rightsClass: String? + let actions: [String] + + var id: String { instrumentId } + + enum CodingKeys: String, CodingKey { + case instrumentId = "instrument_id" + case marketIdentifier = "market_identifier" + case displaySymbol = "display_symbol" + case rightsClass = "rights_class" + case actions + case rights + } + + private struct Rights: Decodable { + let actions: [String]? + let rightsClass: String? + let marketIdentifier: String? + + enum CodingKeys: String, CodingKey { + case actions + case rightsClass = "rights_class" + case marketIdentifier = "market_identifier" + } + } + + init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + instrumentId = (try? c.decode(String.self, forKey: .instrumentId)) ?? UUID().uuidString + let rights = (try? c.decodeIfPresent(Rights.self, forKey: .rights)) ?? nil + let symbol = (try? c.decodeIfPresent(String.self, forKey: .displaySymbol)) ?? nil + marketIdentifier = + ((try? c.decodeIfPresent(String.self, forKey: .marketIdentifier)) ?? nil) ?? + rights?.marketIdentifier ?? + symbol ?? + "NO TICKER" + rightsClass = + ((try? c.decodeIfPresent(String.self, forKey: .rightsClass)) ?? nil) ?? + rights?.rightsClass + actions = + ((try? c.decodeIfPresent([String].self, forKey: .actions)) ?? nil) ?? + rights?.actions ?? + [] + } +} diff --git a/ios/ENTITYWalletIOS/project.yml b/ios/ENTITYWalletIOS/project.yml new file mode 100644 index 00000000..5b08de82 --- /dev/null +++ b/ios/ENTITYWalletIOS/project.yml @@ -0,0 +1,39 @@ +name: ENTITYWalletIOS +options: + bundleIdPrefix: technology.blackmore + deploymentTarget: + iOS: "17.0" + +settings: + base: + SWIFT_VERSION: "5.0" + CURRENT_PROJECT_VERSION: "1" + MARKETING_VERSION: "3.4.3" + +targets: + ENTITYWalletIOS: + type: application + platform: iOS + sources: + - path: ENTITYWalletIOS + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: technology.blackmore.entitywallet + PRODUCT_NAME: ENTITYWalletIOS + GENERATE_INFOPLIST_FILE: YES + INFOPLIST_KEY_CFBundleDisplayName: ENTITY Wallet + INFOPLIST_KEY_UIApplicationSceneManifest_Generation: YES + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents: YES + INFOPLIST_KEY_UILaunchScreen_Generation: YES + TARGETED_DEVICE_FAMILY: "1,2" + CODE_SIGN_STYLE: Automatic + +schemes: + ENTITYWalletIOS: + build: + targets: + ENTITYWalletIOS: all + run: + config: Debug + archive: + config: Release From dedd47eb032b5a95d88f092cf3346298ca3011eb Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 17:55:27 -0700 Subject: [PATCH 109/111] ci: build ENTITY Wallet on Windows Linux macOS and iOS --- .github/workflows/entity-wallet-desktop.yml | 103 ++++++++++++++++++-- 1 file changed, 94 insertions(+), 9 deletions(-) diff --git a/.github/workflows/entity-wallet-desktop.yml b/.github/workflows/entity-wallet-desktop.yml index c11553f1..6d1b6ff5 100644 --- a/.github/workflows/entity-wallet-desktop.yml +++ b/.github/workflows/entity-wallet-desktop.yml @@ -1,24 +1,61 @@ -name: ENTITY Wallet Desktop +name: ENTITY Wallet Cross-Platform Builds on: workflow_dispatch: + push: + branches: + - main + - wallet-native-asset-ingest-20261003 + paths: + - "src/31_Profiles/exchange_protocol.py" + - "src/33_Economic_Participation/**" + - "src/38_Global_Passports/**" + - "src/39_Implementation_Packages/**" + - "src/42_ENTITY_Wallet/**" + - "src/43_DCO_Factory/**" + - "src/45_ENTITY_Market/**" + - "sdk/global_passport_sdk/**" + - "protocol/profiles/**" + - "protocol/v3/ENTITY_ECONOMIC_MARKET_PROTOCOL.md" + - "tools/entity_wallet_desktop.py" + - "tools/build_entity_wallet_desktop.py" + - "ios/ENTITYWalletIOS/**" + - ".github/workflows/entity-wallet-desktop.yml" pull_request: paths: - "src/31_Profiles/exchange_protocol.py" - "src/33_Economic_Participation/**" + - "src/38_Global_Passports/**" + - "src/39_Implementation_Packages/**" - "src/42_ENTITY_Wallet/**" - "src/43_DCO_Factory/**" - "src/45_ENTITY_Market/**" + - "sdk/global_passport_sdk/**" + - "protocol/profiles/**" - "tools/entity_wallet_desktop.py" - "tools/build_entity_wallet_desktop.py" - - "protocol/v3/ENTITY_ECONOMIC_MARKET_PROTOCOL.md" + - "ios/ENTITYWalletIOS/**" - ".github/workflows/entity-wallet-desktop.yml" +permissions: + contents: read + jobs: - build: + desktop: + name: Desktop · ${{ matrix.label }} strategy: + fail-fast: false matrix: - os: [windows-latest, macos-latest, ubuntu-latest] + include: + - os: windows-latest + label: Windows + artifact: ENTITY-Wallet-Windows + - os: ubuntu-latest + label: Linux + artifact: ENTITY-Wallet-Linux + - os: macos-latest + label: macOS + artifact: ENTITY-Wallet-macOS runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -26,12 +63,60 @@ jobs: with: python-version: "3.11" - run: python -m pip install --upgrade pip - - run: python -m pip install pyinstaller cryptography PySide6 - - run: python -m pytest tests/test_entity_market_registry.py tests/test_economic_intelligence.py tests/test_entity_wallet.py tests/test_wallet_asset_ingest.py tests/test_wallet_lineage_resolution.py -q - - run: python tools/build_entity_wallet_desktop.py + - run: python -m pip install pytest pyinstaller cryptography PySide6 + - name: Wallet qualification + run: >- + python -m pytest + tests/test_wallet_onboarding.py + tests/test_entity_market_registry.py + tests/test_economic_intelligence.py + tests/test_exchange_lifecycle.py + tests/test_v3_economic_participation.py + tests/test_prelaunch_economic_withdrawal.py + tests/test_entity_wallet.py + tests/test_wallet_asset_ingest.py + tests/test_wallet_lineage_resolution.py + tests/test_v3_protocol_origin_lineage.py + tests/test_v3_global_passports.py + -q + - name: Build desktop wallet + run: python tools/build_entity_wallet_desktop.py - uses: actions/upload-artifact@v4 with: - name: ENTITY-Wallet-${{ runner.os }} + name: ${{ matrix.artifact }} + if-no-files-found: error path: | - dist/ENTITY-Wallet* + dist/ENTITY-Wallet/** dist/ENTITY-Wallet.app/** + + ios-simulator: + name: iOS · Simulator client + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Install XcodeGen + run: brew install xcodegen + - name: Generate Xcode project + working-directory: ios/ENTITYWalletIOS + run: xcodegen generate + - name: Build iOS Simulator app + run: | + xcodebuild \ + -project ios/ENTITYWalletIOS/ENTITYWalletIOS.xcodeproj \ + -scheme ENTITYWalletIOS \ + -configuration Release \ + -sdk iphonesimulator \ + -derivedDataPath build/ios \ + CODE_SIGNING_ALLOWED=NO \ + build + - name: Package iOS Simulator app + run: | + mkdir -p dist + APP="build/ios/Build/Products/Release-iphonesimulator/ENTITYWalletIOS.app" + test -d "$APP" + ditto -c -k --sequesterRsrc --keepParent "$APP" dist/ENTITY-Wallet-iOS-Simulator.zip + - uses: actions/upload-artifact@v4 + with: + name: ENTITY-Wallet-iOS-Simulator + if-no-files-found: error + path: dist/ENTITY-Wallet-iOS-Simulator.zip From 87cc4d9f8760f3495881296633e36c11236f7780 Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 18:01:48 -0700 Subject: [PATCH 110/111] ci: refresh EEP seal and fix iOS wallet build --- ENTITY_V3_EEP_CONFORMANCE_MANIFEST.json | 212 +++++++++--------- .../ENTITYWalletIOS/ContentView.swift | 4 +- 2 files changed, 109 insertions(+), 107 deletions(-) diff --git a/ENTITY_V3_EEP_CONFORMANCE_MANIFEST.json b/ENTITY_V3_EEP_CONFORMANCE_MANIFEST.json index 0a1f4ca1..bded6ee1 100644 --- a/ENTITY_V3_EEP_CONFORMANCE_MANIFEST.json +++ b/ENTITY_V3_EEP_CONFORMANCE_MANIFEST.json @@ -1,106 +1,106 @@ -{ - "based_on_local_commit": "68179328a2b87408af93a5caa1c64312e70247be", - "claim_boundaries": [ - "development conformance evidence is not a released protocol qualification", - "signature cryptography remains governed by the ENTITY identity layer", - "stateful market authorization and signature verification are tested by executable campaigns in addition to schema vectors", - "external independent EEP clean-room interoperability remains pending" - ], - "conformance": { - "complete_active_tree": { - "passed": 90, - "total": 90 - }, - "json_schema_draft": "2020-12", - "record_definitions": 13, - "reference_mirror_convergence_tests": { - "passed": 16, - "total": 16 - }, - "schema_and_vector_tests": { - "passed": 4, - "total": 4 - }, - "signed_wire_schemas": 11, - "validator_chain": [ - "JSON_SCHEMA_DRAFT_2020_12", - "ENTITY_CANONICAL_SIGNED_WIRE_RULES" - ], - "vectors_expected_invalid": 14, - "vectors_expected_valid": 13, - "vectors_total": 27 - }, - "created_date": "2026-09-23", - "files": [ - { - "path": "src/31_Profiles/exchange_protocol.py", - "sha256": "53761f15c7ae58726ce8cd01a28164d3a14ae29aac38a89859c408fc3c76b14a" - }, - { - "path": "src/32_V3_Hardening/exchange_protocol.py", - "sha256": "767e2a83a7ab5a71256c4f9535b75d91e7d33c97aaa7b7cefc617ffa7874bf41" - }, - { - "path": "protocol/v3/ENTITY_EXCHANGE_PROTOCOL_DRAFT.md", - "sha256": "1733dcf2055a4353abd611344582438a04cbae08b358451290885697c665dbd7" - }, - { - "path": "protocol/v3/ENTITY_EXCHANGE_PROTOCOL.schema.json", - "sha256": "146932a4598797e295ad7d86ac3ee3ee80261fc1675b1bfb4724df0e05fa3218" - }, - { - "path": "protocol/v3/eep_vectors/VECTOR_MANIFEST.json", - "sha256": "08fe381309dc122e0efb8544490e23287e53b3d37cea868b5cd359cc736c4b53" - }, - { - "path": "protocol/v3/eep_vectors/SHA256SUMS.txt", - "sha256": "68989a573a74046e55d6b2713f9fdee8304d8b709d22dd5ccd17f32791d927b7" - }, - { - "path": "protocol/v3/eep_vectors/README.md", - "sha256": "ff734344b2be9e523109c677b4aa746b19eb7e505bd4ba01be8f11ef44ca030e" - }, - { - "path": "tests/test_v3_eep_conformance.py", - "sha256": "d832cc585662a8722bb3ac95763c6f487285983818794a314826792a6c11390f" - }, - { - "path": "tests/test_v3_exchange_convergence.py", - "sha256": "079767d481e2dd09582bbf8b5e97a0ea4334d3fe06acbcc32e99c03caddf29fd" - }, - { - "path": "tests/test_v3_hardening_exchange.py", - "sha256": "f7a7ecf9fa7d748d3953eb68c426945273c42fcf3a952048f21f0def773ecd17" - }, - { - "path": "requirements.txt", - "sha256": "ba36584e5af40e46435d63315f94fb6d600e3d18af53d123e321763f98f22668" - }, - { - "path": "tools/run_v3_regression.ps1", - "sha256": "0bc9f4acc6d1fb8b6b274c305bd89750b78e68ad5568887ee46a577385d5f5f9" - } - ], - "invariants": [ - "signed wire values are not silently normalized after signature verification", - "JSON integer fields reject numeric strings and booleans", - "enumerated tokens and usage actions are canonical uppercase", - "SHA-256 values are canonical lowercase hexadecimal", - "rights action lists are non-empty, unique, and lexicographically sorted", - "revenue allocations do not exceed 10000 basis points", - "RFQ and quote expiry follows signed creation time", - "entitlement expiry semantics converge across reference and qualification mirror", - "executed but unsettled sell quantities remain reserved until clearing completes", - "trade revenue allocation is bound at execution and is not changed by later rule-set mutation", - "sell admission rechecks unreserved entitlement atomically under concurrent submissions", - "concurrent matchers cannot create duplicate trades from the same executable orders", - "concurrent settlement attempts commit at most one entitlement transfer", - "revenue-event persistence is atomic with hardening-profile settlement state", - "RFQ executions preserve a deterministic venue-signed source binding to signed RFQ, quote and acceptance records" - ], - "schema": "entity-v3-eep-conformance-manifest-v1", - "snapshot_sha256": "65224aa97bde5456c4079097a37284ce6e023f3e1c635f25456683c74e3df91a", - "status": "BTG_INTERNAL_QUALIFIED_RELEASE_COMPONENT", - "version": "3.0.0", - "working_tree_snapshot": false -} +{ + "based_on_local_commit": "dedd47eb032b5a95d88f092cf3346298ca3011eb", + "claim_boundaries": [ + "development conformance evidence is not a released protocol qualification", + "signature cryptography remains governed by the ENTITY identity layer", + "stateful market authorization and signature verification are tested by executable campaigns in addition to schema vectors", + "external independent EEP clean-room interoperability remains pending" + ], + "conformance": { + "complete_active_tree": { + "passed": 90, + "total": 90 + }, + "json_schema_draft": "2020-12", + "record_definitions": 13, + "reference_mirror_convergence_tests": { + "passed": 16, + "total": 16 + }, + "schema_and_vector_tests": { + "passed": 4, + "total": 4 + }, + "signed_wire_schemas": 11, + "validator_chain": [ + "JSON_SCHEMA_DRAFT_2020_12", + "ENTITY_CANONICAL_SIGNED_WIRE_RULES" + ], + "vectors_expected_invalid": 14, + "vectors_expected_valid": 13, + "vectors_total": 27 + }, + "created_date": "2026-09-23", + "files": [ + { + "path": "src/31_Profiles/exchange_protocol.py", + "sha256": "fa3c040b31d9cfd7ab37daddeb2c904f52f03a4fc110d77bdd8067fa1b9d547b" + }, + { + "path": "src/32_V3_Hardening/exchange_protocol.py", + "sha256": "767e2a83a7ab5a71256c4f9535b75d91e7d33c97aaa7b7cefc617ffa7874bf41" + }, + { + "path": "protocol/v3/ENTITY_EXCHANGE_PROTOCOL_DRAFT.md", + "sha256": "1733dcf2055a4353abd611344582438a04cbae08b358451290885697c665dbd7" + }, + { + "path": "protocol/v3/ENTITY_EXCHANGE_PROTOCOL.schema.json", + "sha256": "146932a4598797e295ad7d86ac3ee3ee80261fc1675b1bfb4724df0e05fa3218" + }, + { + "path": "protocol/v3/eep_vectors/VECTOR_MANIFEST.json", + "sha256": "08fe381309dc122e0efb8544490e23287e53b3d37cea868b5cd359cc736c4b53" + }, + { + "path": "protocol/v3/eep_vectors/SHA256SUMS.txt", + "sha256": "68989a573a74046e55d6b2713f9fdee8304d8b709d22dd5ccd17f32791d927b7" + }, + { + "path": "protocol/v3/eep_vectors/README.md", + "sha256": "ff734344b2be9e523109c677b4aa746b19eb7e505bd4ba01be8f11ef44ca030e" + }, + { + "path": "tests/test_v3_eep_conformance.py", + "sha256": "d832cc585662a8722bb3ac95763c6f487285983818794a314826792a6c11390f" + }, + { + "path": "tests/test_v3_exchange_convergence.py", + "sha256": "079767d481e2dd09582bbf8b5e97a0ea4334d3fe06acbcc32e99c03caddf29fd" + }, + { + "path": "tests/test_v3_hardening_exchange.py", + "sha256": "f7a7ecf9fa7d748d3953eb68c426945273c42fcf3a952048f21f0def773ecd17" + }, + { + "path": "requirements.txt", + "sha256": "ba36584e5af40e46435d63315f94fb6d600e3d18af53d123e321763f98f22668" + }, + { + "path": "tools/run_v3_regression.ps1", + "sha256": "0bc9f4acc6d1fb8b6b274c305bd89750b78e68ad5568887ee46a577385d5f5f9" + } + ], + "invariants": [ + "signed wire values are not silently normalized after signature verification", + "JSON integer fields reject numeric strings and booleans", + "enumerated tokens and usage actions are canonical uppercase", + "SHA-256 values are canonical lowercase hexadecimal", + "rights action lists are non-empty, unique, and lexicographically sorted", + "revenue allocations do not exceed 10000 basis points", + "RFQ and quote expiry follows signed creation time", + "entitlement expiry semantics converge across reference and qualification mirror", + "executed but unsettled sell quantities remain reserved until clearing completes", + "trade revenue allocation is bound at execution and is not changed by later rule-set mutation", + "sell admission rechecks unreserved entitlement atomically under concurrent submissions", + "concurrent matchers cannot create duplicate trades from the same executable orders", + "concurrent settlement attempts commit at most one entitlement transfer", + "revenue-event persistence is atomic with hardening-profile settlement state", + "RFQ executions preserve a deterministic venue-signed source binding to signed RFQ, quote and acceptance records" + ], + "schema": "entity-v3-eep-conformance-manifest-v1", + "snapshot_sha256": "5d9ad374f4bfdce3181166a057d18b8bcca0ad966a1518f461967ad2a23ef18f", + "status": "BTG_INTERNAL_QUALIFIED_RELEASE_COMPONENT", + "version": "3.0.0", + "working_tree_snapshot": false +} diff --git a/ios/ENTITYWalletIOS/ENTITYWalletIOS/ContentView.swift b/ios/ENTITYWalletIOS/ENTITYWalletIOS/ContentView.swift index 48b525b1..8e0fcf62 100644 --- a/ios/ENTITYWalletIOS/ENTITYWalletIOS/ContentView.swift +++ b/ios/ENTITYWalletIOS/ENTITYWalletIOS/ContentView.swift @@ -144,7 +144,7 @@ private struct AssetDetail: View { .textSelection(.enabled) } - Section("Issuable Rights Passport actions") { + Section { if asset.rightsActions.isEmpty { Text("No directly issuable ALLOW actions were present in the imported snapshot.") .foregroundStyle(.secondary) @@ -153,6 +153,8 @@ private struct AssetDetail: View { Label(action, systemImage: "checkmark.shield") } } + } header: { + Text("Issuable Rights Passport actions") } footer: { Text("Economic instruments may select only a subset of actions already authorized by the active Rights Passport. The mobile client does not create new authority.") } From b16ee3834fc28c7498ed452f8a46c56877c7781c Mon Sep 17 00:00:00 2001 From: Shawn Blackmore Date: Sat, 3 Oct 2026 18:05:16 -0700 Subject: [PATCH 111/111] evidence: refresh market recovery parent seal --- ENTITY_V3_MARKET_RECOVERY_MANIFEST.json | 132 ++++++++++++------------ 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/ENTITY_V3_MARKET_RECOVERY_MANIFEST.json b/ENTITY_V3_MARKET_RECOVERY_MANIFEST.json index 6a1fb7a1..87deca75 100644 --- a/ENTITY_V3_MARKET_RECOVERY_MANIFEST.json +++ b/ENTITY_V3_MARKET_RECOVERY_MANIFEST.json @@ -1,66 +1,66 @@ -{ - "based_on_local_commit": "68179328a2b87408af93a5caa1c64312e70247be", - "claim_boundaries": [ - "market recovery does not export private signing keys", - "market recovery composes with but does not replace ENTITY Core identity/key recovery", - "external settlement remains attestation evidence", - "development evidence is not release qualification", - "external independent market-recovery reimplementation remains pending" - ], - "created_date": "2026-09-23", - "files": [ - { - "path": "src/34_Market_Recovery/market_recovery.py", - "sha256": "7a96f1e44b7910e7c1560074591c5a5dba208bd66d5e10702d828bd57b35e606" - }, - { - "path": "tests/test_v3_market_recovery.py", - "sha256": "e7a89fa2381421b21b37b6238217f72f8cc9cdf91259e82688128327b9b940ff" - }, - { - "path": "protocol/v3/ENTITY_MARKET_STATE_RECOVERY_PROFILE.md", - "sha256": "70be04b633a305b7c09d1873533370b1b059a2bc4227ae64c5b74320545df8c4" - }, - { - "path": "protocol/v3/ENTITY_V3_GAP_CLOSURE_MATRIX.md", - "sha256": "9b026fb2f29059fe8cb2bfac34f16845dae863dd420622738de224188828775c" - }, - { - "path": "ENTITY_V3_EEP_CONFORMANCE_MANIFEST.json", - "sha256": "aa7dd20a8d396084ead3b677eda0251912cb7ccaf8a857bd54d1df84ec7679d1" - }, - { - "path": "ENTITY_V3_EOPP_HARDENING_MANIFEST.json", - "sha256": "25292ef3320e31243c0155ddf297c5b6908b8a8c4b50993b1c66c3aff8295403" - } - ], - "implemented": [ - "provider_independent_logical_eep_state_export", - "provider_independent_logical_eopp_state_export", - "per_table_and_per_database_semantic_roots", - "embedded_public_signer_manifests", - "venue_operator_and_treasury_owner_root_attestations", - "linked_core_sovereign_bundles", - "empty_destination_fail_closed_restore", - "atomic_cross_database_restore", - "post_restore_exact_state_root_comparison", - "destructive_market_database_recovery", - "tamper_detection_after_hash_recomputation", - "required_trade_revenue_binding_recovery" - ], - "schema": "entity-v3-market-recovery-evidence-manifest-v1", - "snapshot_sha256": "947a0b9bf2aa60078ec990e28962392d428d4fd751a6e2cfebf4f26378abdfd3", - "status": "BTG_INTERNAL_QUALIFIED_RELEASE_COMPONENT", - "validation": { - "complete_active_tree": { - "passed": 90, - "total": 90 - }, - "targeted_market_recovery": { - "passed": 7, - "total": 7 - } - }, - "version": "3.0.0", - "working_tree_snapshot": false -} +{ + "based_on_local_commit": "87cc4d9f8760f3495881296633e36c11236f7780", + "claim_boundaries": [ + "market recovery does not export private signing keys", + "market recovery composes with but does not replace ENTITY Core identity/key recovery", + "external settlement remains attestation evidence", + "development evidence is not release qualification", + "external independent market-recovery reimplementation remains pending" + ], + "created_date": "2026-09-23", + "files": [ + { + "path": "src/34_Market_Recovery/market_recovery.py", + "sha256": "7a96f1e44b7910e7c1560074591c5a5dba208bd66d5e10702d828bd57b35e606" + }, + { + "path": "tests/test_v3_market_recovery.py", + "sha256": "e7a89fa2381421b21b37b6238217f72f8cc9cdf91259e82688128327b9b940ff" + }, + { + "path": "protocol/v3/ENTITY_MARKET_STATE_RECOVERY_PROFILE.md", + "sha256": "70be04b633a305b7c09d1873533370b1b059a2bc4227ae64c5b74320545df8c4" + }, + { + "path": "protocol/v3/ENTITY_V3_GAP_CLOSURE_MATRIX.md", + "sha256": "9b026fb2f29059fe8cb2bfac34f16845dae863dd420622738de224188828775c" + }, + { + "path": "ENTITY_V3_EEP_CONFORMANCE_MANIFEST.json", + "sha256": "9d5e55aebe5de480f779791a931aff787d2b048fc12a6090bf868250af57cf10" + }, + { + "path": "ENTITY_V3_EOPP_HARDENING_MANIFEST.json", + "sha256": "25292ef3320e31243c0155ddf297c5b6908b8a8c4b50993b1c66c3aff8295403" + } + ], + "implemented": [ + "provider_independent_logical_eep_state_export", + "provider_independent_logical_eopp_state_export", + "per_table_and_per_database_semantic_roots", + "embedded_public_signer_manifests", + "venue_operator_and_treasury_owner_root_attestations", + "linked_core_sovereign_bundles", + "empty_destination_fail_closed_restore", + "atomic_cross_database_restore", + "post_restore_exact_state_root_comparison", + "destructive_market_database_recovery", + "tamper_detection_after_hash_recomputation", + "required_trade_revenue_binding_recovery" + ], + "schema": "entity-v3-market-recovery-evidence-manifest-v1", + "snapshot_sha256": "b1622bcf6e909271b06f8d19de8a724a485edf4dea417ebb7e52bbc402398d1a", + "status": "BTG_INTERNAL_QUALIFIED_RELEASE_COMPONENT", + "validation": { + "complete_active_tree": { + "passed": 90, + "total": 90 + }, + "targeted_market_recovery": { + "passed": 7, + "total": 7 + } + }, + "version": "3.0.0", + "working_tree_snapshot": false +}