diff --git a/.oxfmtrc.json b/.oxfmtrc.json
index b5e76ee140b..7fcf346d8cc 100644
--- a/.oxfmtrc.json
+++ b/.oxfmtrc.json
@@ -16,5 +16,9 @@
},
"tabWidth": 2,
"trailingComma": "all",
- "ignorePatterns": [".yarnrc.yml", "merged-packages/**"]
+ "ignorePatterns": [
+ ".yarnrc.yml",
+ "merged-packages/**",
+ "packages/*/docs-temp/*.html"
+ ]
}
diff --git a/packages/assets-controller/docs-temp/01-component-map.architecture.json b/packages/assets-controller/docs-temp/01-component-map.architecture.json
new file mode 100644
index 00000000000..2ab6df6f473
--- /dev/null
+++ b/packages/assets-controller/docs-temp/01-component-map.architecture.json
@@ -0,0 +1,383 @@
+{
+ "schema_version": 1,
+ "diagram_type": "architecture",
+ "meta": {
+ "title": "@metamask/assets-controller \u2014 Component Map",
+ "quality_profile": "showcase",
+ "viewBox": [1330, 716],
+ "views": [
+ {
+ "id": "entry",
+ "label": "How callers reach the controller",
+ "focus": ["consumers", "messenger", "controller", "state", "events"],
+ "note": "Reads and refreshes arrive as registered messenger actions; commits publish events back."
+ },
+ {
+ "id": "balance-priority",
+ "label": "Balance sources by priority",
+ "focus": ["pipeline", "activity", "accounts_api", "snap_ds", "rpc_ds"],
+ "note": "Four sources claim chains in a fixed order, so later ones absorb what is left."
+ },
+ {
+ "id": "enrichment",
+ "label": "Enrichment and egress",
+ "focus": ["token_ds", "price_ds", "token_price_api", "evm_rpc"],
+ "note": "Metadata and price run together as one parallel stage after balances land."
+ }
+ ]
+ },
+ "components": [
+ {
+ "id": "consumers",
+ "type": "frontend",
+ "label": "Client + Controllers",
+ "sublabel": "extension \u00b7 mobile",
+ "pos": [405, 40],
+ "size": [190, 60]
+ },
+ {
+ "id": "messenger",
+ "type": "messagebus",
+ "label": "Messenger",
+ "sublabel": "actions \u00b7 events",
+ "pos": [661, 40],
+ "size": [180, 60]
+ },
+ {
+ "id": "events",
+ "type": "messagebus",
+ "label": "Published Events",
+ "sublabel": "stateChange \u00b7 balanceChanged",
+ "pos": [961, 40],
+ "size": [220, 60]
+ },
+ {
+ "id": "controller",
+ "type": "backend",
+ "label": "AssetsController",
+ "sublabel": "BaseController",
+ "tag": "core",
+ "pos": [661, 168],
+ "size": [180, 64]
+ },
+ {
+ "id": "state",
+ "type": "database",
+ "label": "State Commit",
+ "sublabel": "assetsInfo \u00b7 assetsBalance",
+ "pos": [961, 168],
+ "size": [220, 64]
+ },
+ {
+ "id": "pipeline",
+ "type": "backend",
+ "label": "Middleware Pipeline",
+ "sublabel": "composed with reduceRight \u00b7 a throwing stage is caught and skipped",
+ "pos": [232, 300],
+ "size": [1038, 72]
+ },
+ {
+ "id": "activity",
+ "type": "messagebus",
+ "label": "AccountActivity",
+ "sublabel": "push only \u00b7 never fetched",
+ "tag": "priority 1",
+ "pos": [60, 470],
+ "size": [150, 68]
+ },
+ {
+ "id": "accounts_api",
+ "type": "backend",
+ "label": "AccountsApi",
+ "sublabel": "poll 30s \u00b7 timeout 15s",
+ "tag": "priority 2",
+ "pos": [232, 470],
+ "size": [150, 68]
+ },
+ {
+ "id": "snap_ds",
+ "type": "backend",
+ "label": "Snap",
+ "sublabel": "keyring events",
+ "tag": "priority 3",
+ "pos": [404, 470],
+ "size": [150, 68]
+ },
+ {
+ "id": "rpc_ds",
+ "type": "backend",
+ "label": "Rpc",
+ "sublabel": "eip155 only \u00b7 poll 30s",
+ "tag": "priority 4",
+ "pos": [576, 470],
+ "size": [150, 68]
+ },
+ {
+ "id": "staked_ds",
+ "type": "backend",
+ "label": "StakedBalance",
+ "sublabel": "mainnet + Hoodi",
+ "tag": "no claims",
+ "pos": [762, 470],
+ "size": [150, 68]
+ },
+ {
+ "id": "token_ds",
+ "type": "backend",
+ "label": "Token",
+ "sublabel": "batch 50 \u00b7 timeout 15s",
+ "pos": [948, 470],
+ "size": [150, 68]
+ },
+ {
+ "id": "price_ds",
+ "type": "backend",
+ "label": "Price",
+ "sublabel": "poll 60s \u00b7 deduped",
+ "pos": [1120, 470],
+ "size": [150, 68]
+ },
+ {
+ "id": "activity_ws",
+ "type": "cloud",
+ "label": "Activity Service",
+ "sublabel": "WebSocket",
+ "pos": [60, 632],
+ "size": [150, 60]
+ },
+ {
+ "id": "accounts_api_svc",
+ "type": "cloud",
+ "label": "Accounts API",
+ "sublabel": "v5 / v6 balances",
+ "pos": [232, 632],
+ "size": [150, 60]
+ },
+ {
+ "id": "snaps",
+ "type": "external",
+ "label": "Keyring Snaps",
+ "sublabel": "Solana \u00b7 Bitcoin",
+ "pos": [404, 632],
+ "size": [150, 60]
+ },
+ {
+ "id": "evm_rpc",
+ "type": "external",
+ "label": "EVM JSON-RPC",
+ "sublabel": "Multicall3 \u00b7 staking vault",
+ "pos": [576, 632],
+ "size": [336, 60]
+ },
+ {
+ "id": "token_price_api",
+ "type": "cloud",
+ "label": "Tokens + Price API",
+ "sublabel": "metadata \u00b7 spot prices",
+ "pos": [948, 632],
+ "size": [322, 60]
+ }
+ ],
+ "boundaries": [
+ {
+ "kind": "region",
+ "label": "controller core",
+ "wraps": ["controller", "state", "pipeline"],
+ "pad": 20
+ },
+ {
+ "kind": "security-group",
+ "label": "balance sources \u00b7 first-pick chain assignment",
+ "wraps": ["activity", "accounts_api", "snap_ds", "rpc_ds"],
+ "pad": 14
+ },
+ {
+ "kind": "security-group",
+ "label": "enrichment \u00b7 one parallel stage",
+ "wraps": ["token_ds", "price_ds"],
+ "pad": 14
+ }
+ ],
+ "connections": [
+ {
+ "from": "consumers",
+ "to": "messenger",
+ "label": "getAssets",
+ "variant": "emphasis",
+ "fromSide": "right",
+ "toSide": "left"
+ },
+ {
+ "from": "messenger",
+ "to": "controller",
+ "label": "action handlers",
+ "variant": "emphasis",
+ "fromSide": "bottom",
+ "toSide": "top",
+ "labelDy": 58
+ },
+ {
+ "from": "controller",
+ "to": "state",
+ "label": "merge + normalize",
+ "fromSide": "right",
+ "toSide": "left"
+ },
+ {
+ "from": "state",
+ "to": "events",
+ "label": "on commit",
+ "variant": "dashed",
+ "fromSide": "top",
+ "toSide": "bottom"
+ },
+ {
+ "from": "controller",
+ "to": "pipeline",
+ "label": "#executeMiddlewares",
+ "variant": "emphasis",
+ "fromSide": "bottom",
+ "toSide": "top",
+ "labelDy": 45
+ },
+ {
+ "from": "activity",
+ "to": "controller",
+ "label": "onAssetsUpdate push",
+ "variant": "dashed",
+ "fromSide": "top",
+ "toSide": "left"
+ },
+ {
+ "from": "pipeline",
+ "to": "accounts_api",
+ "label": "balance \u00b7 pLimit 3",
+ "fromSide": "bottom",
+ "toSide": "top",
+ "labelSegment": 2
+ },
+ {
+ "from": "pipeline",
+ "to": "snap_ds",
+ "label": "background lane",
+ "fromSide": "bottom",
+ "toSide": "top",
+ "labelSegment": 2
+ },
+ {
+ "from": "pipeline",
+ "to": "rpc_ds",
+ "label": "fallback lane",
+ "fromSide": "bottom",
+ "toSide": "top",
+ "labelSegment": 2
+ },
+ {
+ "from": "pipeline",
+ "to": "staked_ds",
+ "label": "extra source",
+ "fromSide": "bottom",
+ "toSide": "top",
+ "labelSegment": 2
+ },
+ {
+ "from": "pipeline",
+ "to": "token_ds",
+ "label": "metadata",
+ "fromSide": "bottom",
+ "toSide": "top",
+ "labelSegment": 2
+ },
+ {
+ "from": "pipeline",
+ "to": "price_ds",
+ "label": "price",
+ "fromSide": "bottom",
+ "toSide": "top",
+ "labelSegment": 2
+ },
+ {
+ "from": "activity",
+ "to": "activity_ws",
+ "label": "subscribe",
+ "variant": "dashed",
+ "fromSide": "bottom",
+ "toSide": "top",
+ "labelDy": 58
+ },
+ {
+ "from": "accounts_api",
+ "to": "accounts_api_svc",
+ "label": "balances",
+ "fromSide": "bottom",
+ "toSide": "top",
+ "labelDy": 58
+ },
+ {
+ "from": "snap_ds",
+ "to": "snaps",
+ "label": "keyring RPC",
+ "fromSide": "bottom",
+ "toSide": "top",
+ "labelDy": 58
+ },
+ {
+ "from": "rpc_ds",
+ "to": "evm_rpc",
+ "label": "eth_call",
+ "fromSide": "bottom",
+ "toSide": "top",
+ "labelSegment": 0
+ },
+ {
+ "from": "staked_ds",
+ "to": "evm_rpc",
+ "label": "getShares",
+ "fromSide": "bottom",
+ "toSide": "top",
+ "labelSegment": 0
+ },
+ {
+ "from": "token_ds",
+ "to": "token_price_api",
+ "label": "v3 assets",
+ "fromSide": "bottom",
+ "toSide": "top",
+ "labelSegment": 0
+ },
+ {
+ "from": "price_ds",
+ "to": "token_price_api",
+ "label": "spot prices",
+ "fromSide": "bottom",
+ "toSide": "top",
+ "labelSegment": 0
+ }
+ ],
+ "cards": [
+ {
+ "dot": "cyan",
+ "title": "Chain assignment",
+ "items": [
+ "Priority sources are walked in order; each claims the active chains it can serve",
+ "A chain is claimed once, so lower-priority sources only see the leftovers"
+ ]
+ },
+ {
+ "dot": "emerald",
+ "title": "Two ways data arrives",
+ "items": [
+ "Pull: getAssets drives the pipeline, then commits and publishes",
+ "Push: a subscribed source calls onAssetsUpdate for a shorter pipeline"
+ ]
+ },
+ {
+ "dot": "violet",
+ "title": "Basic functionality off",
+ "items": [
+ "Only Rpc and StakedBalance stay reachable for balances",
+ "Metadata and price types are stripped, so those stages skip themselves"
+ ]
+ }
+ ]
+}
diff --git a/packages/assets-controller/docs-temp/01-component-map.html b/packages/assets-controller/docs-temp/01-component-map.html
new file mode 100644
index 00000000000..42e497ef4da
--- /dev/null
+++ b/packages/assets-controller/docs-temp/01-component-map.html
@@ -0,0 +1,15001 @@
+
+
+
+
+
+
+ @metamask/assets-controller — Component Map Diagram
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Guided views
+
+
+
+
+
Explore this system
+
Step through curated paths without changing the source diagram.
+
+
+ Beat
+
+
+
+ Next
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ready
+ Chapter 01 / 01
+
+
+ Guided chapter
+
+
+
+
+
+
+
+
+ Diagram guide
+ Explore this system
+
+
+
+
Inspecting compiled semantics
+
+
+
+
+
+
+
+
+
+ E ExportT ThemeS Style0 Reset+ Zoom in- Zoom outEsc Close
+
+
+
+
+
+ Find a node
+
+
+
+ ⌕
+
+ /
+
+
+
No matching nodes
+
+
+
+
+
+
Semantic passport
+
+
+
+
+
+
+
+
+
+
+
+
+
Authored reach
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Route probe
+ Choose a start node
+
+
+
+
+
+
+
+
+ Pick two semantic nodes on the diagram
+
+
+
+
+
+
+
+
Choose the source, then the destination. Direction matters.
+
+
+
+
+ Semantic lens
+ Compare system roles
+
+
+
+
Choose up to two semantic kinds. One reveals its real traffic; two compare only direct authored relationships.
+
+
Choose a kind to inspect its nodes and touching relationships.
+
+
+
+
+
+
+
+
+
+ Semantic radar
+ Building overview
+
+
+
+
+
+
Click nodeDrag to pan
+
+
Semantic radar needs more MAP space.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - • Priority sources are walked in order; each claims the active chains it can serve
+ - • A chain is claimed once, so lower-priority sources only see the leftovers
+
+
+
+
+
+
+ - • Pull: getAssets drives the pipeline, then commits and publishes
+ - • Push: a subscribed source calls onAssetsUpdate for a shorter pipeline
+
+
+
+
+
+
+ - • Only Rpc and StakedBalance stay reachable for balances
+ - • Metadata and price types are stripped, so those stages skip themselves
+
+
+
+
+
+
+
+
+
diff --git a/packages/assets-controller/docs-temp/01-component-map.png b/packages/assets-controller/docs-temp/01-component-map.png
new file mode 100644
index 00000000000..6b579c13578
Binary files /dev/null and b/packages/assets-controller/docs-temp/01-component-map.png differ
diff --git a/packages/assets-controller/docs-temp/01-component-map.svg b/packages/assets-controller/docs-temp/01-component-map.svg
new file mode 100644
index 00000000000..fe62c26977e
--- /dev/null
+++ b/packages/assets-controller/docs-temp/01-component-map.svg
@@ -0,0 +1,656 @@
+
\ No newline at end of file
diff --git a/packages/assets-controller/docs-temp/02-fetch-pipeline.html b/packages/assets-controller/docs-temp/02-fetch-pipeline.html
new file mode 100644
index 00000000000..89d924bcc48
--- /dev/null
+++ b/packages/assets-controller/docs-temp/02-fetch-pipeline.html
@@ -0,0 +1,14888 @@
+
+
+
+
+
+
+ getAssets — Fast Lane and Background Lane Diagram
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Guided views
+
+
+
+
+
Explore this system
+
Step through curated paths without changing the source diagram.
+
+
+ Beat
+
+
+
+ Next
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ready
+ Chapter 01 / 01
+
+
+ Guided chapter
+
+
+
+
+
+
+
+
+ Diagram guide
+ Explore this system
+
+
+
+
Inspecting compiled semantics
+
+
+
+
+
+
+
+
+
+ E ExportT ThemeS Style0 Reset+ Zoom in- Zoom outEsc Close
+
+
+
+
+
+ Find a node
+
+
+
+ ⌕
+
+ /
+
+
+
No matching nodes
+
+
+
+
+
+
Semantic passport
+
+
+
+
+
+
+
+
+
+
+
+
+
Authored reach
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Route probe
+ Choose a start node
+
+
+
+
+
+
+
+
+ Pick two semantic nodes on the diagram
+
+
+
+
+
+
+
+
Choose the source, then the destination. Direction matters.
+
+
+
+
+ Semantic lens
+ Compare system roles
+
+
+
+
Choose up to two semantic kinds. One reveals its real traffic; two compare only direct authored relationships.
+
+
Choose a kind to inspect its nodes and touching relationships.
+
+
+
+
+
+
+
+
+
+ Semantic radar
+ Building overview
+
+
+
+
+
+
Click nodeDrag to pan
+
+
Semantic radar needs more MAP space.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - • The caller only waits for the fast lane; the background lane is never awaited
+ - • A chain is retried in the background if it errored or Accounts API does not cover it
+
+
+
+
+
+
+ - • A stage that throws is caught by name and the chain continues with the next one
+ - • Every stage still calls next, so no stage can silently truncate the pipeline
+
+
+
+
+
+
+
+
+
diff --git a/packages/assets-controller/docs-temp/02-fetch-pipeline.png b/packages/assets-controller/docs-temp/02-fetch-pipeline.png
new file mode 100644
index 00000000000..78c46fbc7d5
Binary files /dev/null and b/packages/assets-controller/docs-temp/02-fetch-pipeline.png differ
diff --git a/packages/assets-controller/docs-temp/02-fetch-pipeline.svg b/packages/assets-controller/docs-temp/02-fetch-pipeline.svg
new file mode 100644
index 00000000000..5e6a3f61d37
--- /dev/null
+++ b/packages/assets-controller/docs-temp/02-fetch-pipeline.svg
@@ -0,0 +1,554 @@
+
\ No newline at end of file
diff --git a/packages/assets-controller/docs-temp/02-fetch-pipeline.workflow.json b/packages/assets-controller/docs-temp/02-fetch-pipeline.workflow.json
new file mode 100644
index 00000000000..2025dea719f
--- /dev/null
+++ b/packages/assets-controller/docs-temp/02-fetch-pipeline.workflow.json
@@ -0,0 +1,341 @@
+{
+ "schema_version": 2,
+ "diagram_type": "workflow",
+ "meta": {
+ "title": "getAssets \u2014 Fast Lane and Background Lane",
+ "quality_profile": "showcase",
+ "views": [
+ {
+ "id": "fast",
+ "label": "What the caller waits for",
+ "focus": [
+ "caller",
+ "fast_balance",
+ "graduation",
+ "rpc_fallback",
+ "detect_fast",
+ "enrich_fast",
+ "commit",
+ "result"
+ ],
+ "note": "The fast lane is the only part of the work the caller blocks on."
+ },
+ {
+ "id": "leftovers",
+ "label": "Chains the fast lane could not serve",
+ "focus": ["leftover", "slow_balance", "detect_slow", "enrich_slow"],
+ "note": "Errored chains and chains outside Accounts API coverage are retried in the background."
+ },
+ {
+ "id": "ordering",
+ "label": "Why graduation precedes fallback",
+ "focus": ["fast_balance", "graduation", "rpc_fallback"],
+ "note": "Graduation must only ever see Accounts API and websocket balances, never RPC ones."
+ }
+ ],
+ "legend": {
+ "entries": {
+ "frontend": {
+ "label": "Caller"
+ },
+ "backend": {
+ "label": "Pipeline stage"
+ },
+ "messagebus": {
+ "label": "Handoff"
+ },
+ "database": {
+ "label": "State"
+ }
+ }
+ }
+ },
+ "lanes": [
+ {
+ "id": "caller",
+ "label": "Caller"
+ },
+ {
+ "id": "fast",
+ "label": "Fast lane \u00b7 awaited"
+ },
+ {
+ "id": "slow",
+ "label": "Background lane \u00b7 fire and forget",
+ "variant": "exception"
+ },
+ {
+ "id": "store",
+ "label": "State"
+ }
+ ],
+ "phases": [
+ {
+ "id": "gather",
+ "label": "Gather balances",
+ "fromCol": 0,
+ "toCol": 1
+ },
+ {
+ "id": "reconcile",
+ "label": "Reconcile",
+ "fromCol": 2,
+ "toCol": 3,
+ "variant": "emphasis"
+ },
+ {
+ "id": "finish",
+ "label": "Enrich and commit",
+ "fromCol": 4,
+ "toCol": 5,
+ "variant": "dashed"
+ }
+ ],
+ "groups": [
+ {
+ "id": "enrich_group",
+ "label": "detect then enrich",
+ "lane": "fast",
+ "fromCol": 3,
+ "toCol": 4,
+ "variant": "emphasis"
+ },
+ {
+ "id": "slow_group",
+ "label": "same shape, deferred",
+ "lane": "slow",
+ "fromCol": 0,
+ "toCol": 4,
+ "variant": "dashed"
+ }
+ ],
+ "mainPath": [
+ "caller",
+ "fast_balance",
+ "graduation",
+ "rpc_fallback",
+ "detect_fast",
+ "enrich_fast",
+ "commit",
+ "result"
+ ],
+ "semanticChecks": {
+ "allowedRoots": ["caller"],
+ "allowedTerminals": ["result"]
+ },
+ "nodes": [
+ {
+ "id": "caller",
+ "lane": "caller",
+ "col": 0,
+ "type": "frontend",
+ "label": "getAssets",
+ "sublabel": "forceUpdate: true",
+ "tag": "blocking",
+ "width": 146
+ },
+ {
+ "id": "result",
+ "lane": "caller",
+ "col": 5,
+ "type": "frontend",
+ "label": "Read from state",
+ "sublabel": "balance \u00b7 metadata \u00b7 price",
+ "width": 160
+ },
+ {
+ "id": "fast_balance",
+ "lane": "fast",
+ "col": 0,
+ "type": "backend",
+ "label": "Parallel balance",
+ "sublabel": "AccountsApi + Staked",
+ "tag": "pLimit 3",
+ "width": 152
+ },
+ {
+ "id": "graduation",
+ "lane": "fast",
+ "col": 1,
+ "type": "backend",
+ "label": "Graduation",
+ "sublabel": "retire custom assets",
+ "width": 150
+ },
+ {
+ "id": "rpc_fallback",
+ "lane": "fast",
+ "col": 2,
+ "type": "backend",
+ "label": "RPC fallback",
+ "sublabel": "errored + stale-zero",
+ "width": 150
+ },
+ {
+ "id": "detect_fast",
+ "lane": "fast",
+ "col": 3,
+ "type": "backend",
+ "label": "Detection",
+ "sublabel": "assets with no metadata",
+ "width": 158
+ },
+ {
+ "id": "enrich_fast",
+ "lane": "fast",
+ "col": 4,
+ "type": "backend",
+ "label": "Parallel enrich",
+ "sublabel": "Token + Price",
+ "tag": "pLimit 2",
+ "width": 150
+ },
+ {
+ "id": "leftover",
+ "lane": "slow",
+ "col": 0,
+ "type": "messagebus",
+ "label": "Leftover chains",
+ "sublabel": "errored, or not covered",
+ "tag": "after fast commit",
+ "width": 158
+ },
+ {
+ "id": "slow_balance",
+ "lane": "slow",
+ "col": 1,
+ "type": "backend",
+ "label": "Parallel balance",
+ "sublabel": "Snap + Rpc",
+ "tag": "pLimit 3",
+ "width": 150
+ },
+ {
+ "id": "detect_slow",
+ "lane": "slow",
+ "col": 3,
+ "type": "backend",
+ "label": "Detection",
+ "sublabel": "assets with no metadata",
+ "width": 158
+ },
+ {
+ "id": "enrich_slow",
+ "lane": "slow",
+ "col": 4,
+ "type": "backend",
+ "label": "Parallel enrich",
+ "sublabel": "Token + Price",
+ "width": 150
+ },
+ {
+ "id": "commit",
+ "lane": "store",
+ "col": 5,
+ "type": "database",
+ "label": "#updateState",
+ "sublabel": "merge \u00b7 normalize \u00b7 publish",
+ "width": 166
+ }
+ ],
+ "edges": [
+ {
+ "id": "call",
+ "from": "caller",
+ "to": "fast_balance",
+ "label": "accounts + chainIds",
+ "variant": "emphasis"
+ },
+ {
+ "id": "balance-graduate",
+ "from": "fast_balance",
+ "to": "graduation",
+ "variant": "emphasis"
+ },
+ {
+ "id": "graduate-fallback",
+ "from": "graduation",
+ "to": "rpc_fallback",
+ "label": "must precede fallback",
+ "variant": "emphasis"
+ },
+ {
+ "id": "fallback-detect",
+ "from": "rpc_fallback",
+ "to": "detect_fast",
+ "variant": "emphasis"
+ },
+ {
+ "id": "detect-enrich",
+ "from": "detect_fast",
+ "to": "enrich_fast",
+ "variant": "emphasis"
+ },
+ {
+ "id": "enrich-commit",
+ "from": "enrich_fast",
+ "to": "commit",
+ "label": "enriched response",
+ "variant": "emphasis"
+ },
+ {
+ "id": "commit-read",
+ "from": "commit",
+ "to": "result",
+ "label": "resolve caller",
+ "role": "return",
+ "width": 1.2
+ },
+ {
+ "id": "spill",
+ "from": "fast_balance",
+ "to": "leftover",
+ "variant": "dashed",
+ "role": "branch"
+ },
+ {
+ "id": "slow-balance",
+ "from": "leftover",
+ "to": "slow_balance",
+ "variant": "dashed"
+ },
+ {
+ "id": "slow-detect",
+ "from": "slow_balance",
+ "to": "detect_slow",
+ "variant": "dashed"
+ },
+ {
+ "id": "slow-enrich",
+ "from": "detect_slow",
+ "to": "enrich_slow",
+ "variant": "dashed"
+ },
+ {
+ "id": "slow-commit",
+ "from": "enrich_slow",
+ "to": "commit",
+ "label": "second commit",
+ "variant": "dashed",
+ "role": "async"
+ }
+ ],
+ "cards": [
+ {
+ "dot": "cyan",
+ "title": "Two lanes, one pipeline shape",
+ "items": [
+ "The caller only waits for the fast lane; the background lane is never awaited",
+ "A chain is retried in the background if it errored or Accounts API does not cover it"
+ ]
+ },
+ {
+ "dot": "rose",
+ "title": "Failure is absorbed, not propagated",
+ "items": [
+ "A stage that throws is caught by name and the chain continues with the next one",
+ "Every stage still calls next, so no stage can silently truncate the pipeline"
+ ]
+ }
+ ]
+}
diff --git a/packages/assets-controller/docs-temp/03-fallback-cascade.html b/packages/assets-controller/docs-temp/03-fallback-cascade.html
new file mode 100644
index 00000000000..49471265e20
--- /dev/null
+++ b/packages/assets-controller/docs-temp/03-fallback-cascade.html
@@ -0,0 +1,14884 @@
+
+
+
+
+
+
+ Balance Fallback Cascade — One EVM Chain, Every Layer Degrading Diagram
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Guided views
+
+
+
+
+
Explore this system
+
Step through curated paths without changing the source diagram.
+
+
+ Beat
+
+
+
+ Next
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ready
+ Chapter 01 / 01
+
+
+ Guided chapter
+
+
+
+
+
+
+
+
+ Diagram guide
+ Explore this system
+
+
+
+
Inspecting compiled semantics
+
+
+
+
+
+
+
+
+
+ E ExportT ThemeS Style0 Reset+ Zoom in- Zoom outEsc Close
+
+
+
+
+
+ Find a node
+
+
+
+ ⌕
+
+ /
+
+
+
No matching nodes
+
+
+
+
+
+
Semantic passport
+
+
+
+
+
+
+
+
+
+
+
+
+
Authored reach
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Route probe
+ Choose a start node
+
+
+
+
+
+
+
+
+ Pick two semantic nodes on the diagram
+
+
+
+
+
+
+
+
Choose the source, then the destination. Direction matters.
+
+
+
+
+ Semantic lens
+ Compare system roles
+
+
+
+
Choose up to two semantic kinds. One reveals its real traffic; two compare only direct authored relationships.
+
+
Choose a kind to inspect its nodes and touching relationships.
+
+
+
+
+
+
+
+
+
+ Semantic radar
+ Building overview
+
+
+
+
+
+
Click nodeDrag to pan
+
+
Semantic radar needs more MAP space.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - • A timeout becomes a per-chain error entry, so the pipeline keeps running
+ - • The merge only clears an error for a chain RPC actually returned balances for
+
+
+
+
+
+
+ - • Only the batched multicall retries: three attempts under a service policy
+ - • The per-asset eth_call is the last resort and never retries on its own
+
+
+
+
+
+
+
+
+
diff --git a/packages/assets-controller/docs-temp/03-fallback-cascade.png b/packages/assets-controller/docs-temp/03-fallback-cascade.png
new file mode 100644
index 00000000000..b2dace13cd9
Binary files /dev/null and b/packages/assets-controller/docs-temp/03-fallback-cascade.png differ
diff --git a/packages/assets-controller/docs-temp/03-fallback-cascade.sequence.json b/packages/assets-controller/docs-temp/03-fallback-cascade.sequence.json
new file mode 100644
index 00000000000..c07d746d48b
--- /dev/null
+++ b/packages/assets-controller/docs-temp/03-fallback-cascade.sequence.json
@@ -0,0 +1,263 @@
+{
+ "schema_version": 1,
+ "diagram_type": "sequence",
+ "meta": {
+ "title": "Balance Fallback Cascade \u2014 One EVM Chain, Every Layer Degrading",
+ "quality_profile": "showcase",
+ "column_fit": "spread",
+ "viewBox": [1080, 575],
+ "views": [
+ {
+ "id": "primary",
+ "label": "Primary source fails",
+ "focus": ["balance_stage", "accounts_api", "api"],
+ "note": "A timeout is recorded as a per-chain error instead of being thrown."
+ },
+ {
+ "id": "handoff",
+ "label": "Fallback picks the chain up",
+ "focus": ["balance_stage", "fallback", "rpc_ds"],
+ "note": "Only chains that errored or hold a suspicious zero are handed to RPC."
+ },
+ {
+ "id": "multicall",
+ "label": "Inside the RPC attempt",
+ "focus": ["rpc_ds", "node"],
+ "note": "One batched multicall with three attempts, then one call per asset."
+ }
+ ],
+ "legend": {
+ "entries": {
+ "default": {
+ "label": "call"
+ },
+ "emphasis": {
+ "label": "main path"
+ },
+ "security": {
+ "label": "failure"
+ },
+ "dashed": {
+ "label": "degraded"
+ },
+ "return": {
+ "label": "response"
+ }
+ }
+ }
+ },
+ "participants": [
+ {
+ "id": "balance_stage",
+ "type": "backend",
+ "label": "ParallelBalance",
+ "sublabel": "partitions chains"
+ },
+ {
+ "id": "accounts_api",
+ "type": "backend",
+ "label": "AccountsApi",
+ "sublabel": "priority 2"
+ },
+ {
+ "id": "api",
+ "type": "cloud",
+ "label": "Accounts API",
+ "sublabel": "HTTPS"
+ },
+ {
+ "id": "fallback",
+ "type": "backend",
+ "label": "RpcFallback",
+ "sublabel": "middleware"
+ },
+ {
+ "id": "rpc_ds",
+ "type": "backend",
+ "label": "RpcDataSource",
+ "sublabel": "eip155 only"
+ },
+ {
+ "id": "node",
+ "type": "external",
+ "label": "EVM JSON-RPC",
+ "sublabel": "Multicall3"
+ }
+ ],
+ "segments": [
+ {
+ "from": 155,
+ "to": 240,
+ "label": "Primary attempt"
+ },
+ {
+ "from": 246,
+ "to": 296,
+ "label": "Chain marked failed"
+ },
+ {
+ "from": 302,
+ "to": 436,
+ "label": "RPC fallback cascade"
+ },
+ {
+ "from": 442,
+ "to": 492,
+ "label": "Merge back"
+ }
+ ],
+ "messages": [
+ {
+ "id": "fetch",
+ "from": "balance_stage",
+ "to": "accounts_api",
+ "y": 170,
+ "label": "fetch claimed chains",
+ "variant": "emphasis"
+ },
+ {
+ "id": "http",
+ "from": "accounts_api",
+ "to": "api",
+ "y": 198,
+ "label": "multi-account balances",
+ "variant": "default"
+ },
+ {
+ "id": "timeout",
+ "from": "api",
+ "to": "accounts_api",
+ "y": 226,
+ "label": "no response in 15s",
+ "variant": "security"
+ },
+ {
+ "id": "mark",
+ "from": "accounts_api",
+ "to": "balance_stage",
+ "y": 254,
+ "label": "errors[chainId], no throw",
+ "variant": "return"
+ },
+ {
+ "id": "carry",
+ "from": "balance_stage",
+ "to": "fallback",
+ "y": 282,
+ "label": "next(), error still set",
+ "variant": "emphasis"
+ },
+ {
+ "id": "select",
+ "from": "fallback",
+ "to": "rpc_ds",
+ "y": 310,
+ "label": "errored + stale-zero chains",
+ "variant": "emphasis"
+ },
+ {
+ "id": "aggregate",
+ "from": "rpc_ds",
+ "to": "node",
+ "y": 338,
+ "label": "aggregate3, batch 300",
+ "variant": "emphasis"
+ },
+ {
+ "id": "aggregate-fail",
+ "from": "node",
+ "to": "rpc_ds",
+ "y": 366,
+ "label": "3 attempts exhausted",
+ "variant": "security"
+ },
+ {
+ "id": "single",
+ "from": "rpc_ds",
+ "to": "node",
+ "y": 394,
+ "label": "one eth_call per asset",
+ "variant": "dashed"
+ },
+ {
+ "id": "single-ok",
+ "from": "node",
+ "to": "rpc_ds",
+ "y": 422,
+ "label": "balances",
+ "variant": "return"
+ },
+ {
+ "id": "hand-back",
+ "from": "rpc_ds",
+ "to": "fallback",
+ "y": 450,
+ "label": "balances by chain",
+ "variant": "return"
+ },
+ {
+ "id": "merge",
+ "from": "fallback",
+ "to": "balance_stage",
+ "y": 478,
+ "label": "merged, error cleared",
+ "variant": "return"
+ }
+ ],
+ "activations": [
+ {
+ "participant": "balance_stage",
+ "from": 165,
+ "to": 484,
+ "type": "backend"
+ },
+ {
+ "participant": "accounts_api",
+ "from": 176,
+ "to": 260,
+ "type": "backend"
+ },
+ {
+ "participant": "api",
+ "from": 193,
+ "to": 232,
+ "type": "cloud"
+ },
+ {
+ "participant": "fallback",
+ "from": 277,
+ "to": 482,
+ "type": "backend"
+ },
+ {
+ "participant": "rpc_ds",
+ "from": 305,
+ "to": 456,
+ "type": "backend"
+ },
+ {
+ "participant": "node",
+ "from": 333,
+ "to": 428,
+ "type": "external"
+ }
+ ],
+ "cards": [
+ {
+ "dot": "rose",
+ "title": "Every layer degrades once",
+ "items": [
+ "A timeout becomes a per-chain error entry, so the pipeline keeps running",
+ "The merge only clears an error for a chain RPC actually returned balances for"
+ ]
+ },
+ {
+ "dot": "amber",
+ "title": "Where the retries actually live",
+ "items": [
+ "Only the batched multicall retries: three attempts under a service policy",
+ "The per-asset eth_call is the last resort and never retries on its own"
+ ]
+ }
+ ]
+}
diff --git a/packages/assets-controller/docs-temp/03-fallback-cascade.svg b/packages/assets-controller/docs-temp/03-fallback-cascade.svg
new file mode 100644
index 00000000000..ded9f87cad0
--- /dev/null
+++ b/packages/assets-controller/docs-temp/03-fallback-cascade.svg
@@ -0,0 +1,550 @@
+
\ No newline at end of file
diff --git a/packages/assets-controller/docs-temp/04-tracking-lifecycle.html b/packages/assets-controller/docs-temp/04-tracking-lifecycle.html
new file mode 100644
index 00000000000..dedf1455cdd
--- /dev/null
+++ b/packages/assets-controller/docs-temp/04-tracking-lifecycle.html
@@ -0,0 +1,14847 @@
+
+
+
+
+
+
+ AssetsController — Tracking Lifecycle Diagram
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Guided views
+
+
+
+
+
Explore this system
+
Step through curated paths without changing the source diagram.
+
+
+ Beat
+
+
+
+ Next
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ready
+ Chapter 01 / 01
+
+
+ Guided chapter
+
+
+
+
+
+
+
+
+ Diagram guide
+ Explore this system
+
+
+
+
Inspecting compiled semantics
+
+
+
+
+
+
+
+
+
+ E ExportT ThemeS Style0 Reset+ Zoom in- Zoom outEsc Close
+
+
+
+
+
+ Find a node
+
+
+
+ ⌕
+
+ /
+
+
+
No matching nodes
+
+
+
+
+
+
Semantic passport
+
+
+
+
+
+
+
+
+
+
+
+
+
Authored reach
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Route probe
+ Choose a start node
+
+
+
+
+
+
+
+
+ Pick two semantic nodes on the diagram
+
+
+
+
+
+
+
+
Choose the source, then the destination. Direction matters.
+
+
+
+
+ Semantic lens
+ Compare system roles
+
+
+
+
Choose up to two semantic kinds. One reveals its real traffic; two compare only direct authored relationships.
+
+
Choose a kind to inspect its nodes and touching relationships.
+
+
+
+
+
+
+
+
+
+ Semantic radar
+ Building overview
+
+
+
+
+
+
Click nodeDrag to pan
+
+
Semantic radar needs more MAP space.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - • UI open, keyring unlocked, account tree initialised
+ - • Every one of those signals re-runs the same check
+ - • Start is skipped when no account or no chain is enabled
+ - • src/AssetsController.ts · #updateActive
+
+
+
+
+
+
+ - • A mutex serialises the forced fetch against later refreshes
+ - • Native zeros and default tracked assets are seeded next
+ - • Sources subscribe with skipInitialFetch, avoiding a re-poll
+ - • src/AssetsController.ts · #runStartupRefresh
+
+
+
+
+
+
+ - • Chain claims are recomputed, so every source re-subscribes
+ - • Stop unsubscribes each ds: key so polling timers really die
+ - • Messenger handlers stay registered, so the gate can reopen
+ - • src/AssetsController.ts · #stop, destroy
+
+
+
+
+
+
+
+
+
diff --git a/packages/assets-controller/docs-temp/04-tracking-lifecycle.lifecycle.json b/packages/assets-controller/docs-temp/04-tracking-lifecycle.lifecycle.json
new file mode 100644
index 00000000000..5faa116c6c6
--- /dev/null
+++ b/packages/assets-controller/docs-temp/04-tracking-lifecycle.lifecycle.json
@@ -0,0 +1,269 @@
+{
+ "schema_version": 1,
+ "diagram_type": "lifecycle",
+ "meta": {
+ "title": "AssetsController \u2014 Tracking Lifecycle",
+ "quality_profile": "showcase",
+ "viewBox": [980, 630],
+ "views": [
+ {
+ "id": "steady",
+ "label": "Reaching steady state",
+ "focus": ["constructed", "signals", "gate", "refresh", "tracking"],
+ "note": "Construction wires everything but touches no network; tracking only begins once the gate opens."
+ },
+ {
+ "id": "closed-gate",
+ "label": "Why nothing is happening",
+ "focus": ["signals", "gate", "dormant"],
+ "note": "One false condition is enough to tear every subscription down and go back to waiting."
+ },
+ {
+ "id": "rescope",
+ "label": "Re-scoping while live",
+ "focus": ["tracking", "rescope", "refresh"],
+ "note": "Account or network changes re-enter the fetch path instead of restarting the lifecycle."
+ },
+ {
+ "id": "teardown",
+ "label": "Teardown",
+ "focus": ["dormant", "destroyed"],
+ "note": "Dormant keeps its messenger handlers; destroy is the only step that releases them."
+ }
+ ],
+ "legend": {
+ "entries": {
+ "start": {
+ "label": "Constructor"
+ },
+ "neutral": {
+ "label": "Waiting on messenger"
+ },
+ "decision": {
+ "label": "Activation gate"
+ },
+ "active": {
+ "label": "Work in flight"
+ },
+ "success": {
+ "label": "Steady state"
+ },
+ "waiting": {
+ "label": "Paused"
+ },
+ "failure": {
+ "label": "Terminal"
+ }
+ }
+ }
+ },
+ "lanes": [
+ {
+ "id": "main",
+ "label": "Activation path"
+ },
+ {
+ "id": "interrupts",
+ "label": "Pauses and re-scope"
+ },
+ {
+ "id": "terminal",
+ "label": "Terminal exit"
+ }
+ ],
+ "states": [
+ {
+ "id": "constructed",
+ "type": "start",
+ "label": "Constructed",
+ "sublabel": "no network I/O",
+ "lane": "main",
+ "col": 0,
+ "step": "01",
+ "tag": "ctor"
+ },
+ {
+ "id": "signals",
+ "type": "neutral",
+ "label": "Signals",
+ "sublabel": "unlock \u00b7 UI \u00b7 tree",
+ "lane": "main",
+ "col": 1,
+ "step": "02"
+ },
+ {
+ "id": "gate",
+ "type": "decision",
+ "label": "Gate Check",
+ "sublabel": "all three true?",
+ "lane": "main",
+ "col": 2,
+ "step": "03"
+ },
+ {
+ "id": "refresh",
+ "type": "active",
+ "label": "Refresh",
+ "sublabel": "forced getAssets",
+ "lane": "main",
+ "col": 3,
+ "step": "04"
+ },
+ {
+ "id": "tracking",
+ "type": "success",
+ "label": "Tracking",
+ "sublabel": "subscribed \u00b7 live",
+ "lane": "main",
+ "col": 4,
+ "step": "05",
+ "tag": "steady"
+ },
+ {
+ "id": "dormant",
+ "type": "waiting",
+ "label": "Dormant",
+ "sublabel": "subscriptions cleared",
+ "lane": "interrupts",
+ "col": 0,
+ "tag": "stop"
+ },
+ {
+ "id": "rescope",
+ "type": "active",
+ "label": "Re-scope",
+ "sublabel": "accounts or chains",
+ "lane": "interrupts",
+ "col": 2
+ },
+ {
+ "id": "destroyed",
+ "type": "failure",
+ "label": "Destroyed",
+ "sublabel": "handlers released",
+ "lane": "terminal",
+ "col": 0
+ }
+ ],
+ "transitions": [
+ {
+ "id": "wire-up",
+ "from": "constructed",
+ "to": "signals",
+ "variant": "emphasis",
+ "route": "straight"
+ },
+ {
+ "id": "evaluate",
+ "from": "signals",
+ "to": "gate",
+ "variant": "emphasis",
+ "route": "straight"
+ },
+ {
+ "id": "gate-open",
+ "from": "gate",
+ "to": "refresh",
+ "variant": "emphasis",
+ "route": "straight"
+ },
+ {
+ "id": "seed-subscribe",
+ "from": "refresh",
+ "to": "tracking",
+ "variant": "emphasis",
+ "route": "straight"
+ },
+ {
+ "id": "gate-closed",
+ "from": "gate",
+ "to": "dormant",
+ "label": "any gate false",
+ "note": "locked or UI closed",
+ "variant": "security",
+ "route": "straight",
+ "labelDy": 48
+ },
+ {
+ "id": "await-signal",
+ "from": "dormant",
+ "to": "signals",
+ "label": "wait for next signal",
+ "variant": "dashed",
+ "fromSide": "left",
+ "toSide": "bottom",
+ "via": [
+ [320, 307],
+ [320, 230],
+ [248, 230]
+ ],
+ "labelSegment": 2
+ },
+ {
+ "id": "scope-changed",
+ "from": "tracking",
+ "to": "rescope",
+ "label": "scope changed",
+ "variant": "default",
+ "route": "straight",
+ "labelDy": 48
+ },
+ {
+ "id": "resubscribe",
+ "from": "rescope",
+ "to": "refresh",
+ "label": "re-fetch, re-subscribe",
+ "variant": "default",
+ "fromSide": "left",
+ "toSide": "bottom",
+ "via": [
+ [630, 307],
+ [630, 230],
+ [556, 230]
+ ],
+ "labelSegment": 2
+ },
+ {
+ "id": "teardown",
+ "from": "dormant",
+ "to": "destroyed",
+ "label": "destroy()",
+ "note": "callable from any phase",
+ "variant": "security",
+ "route": "straight",
+ "labelDy": 58
+ }
+ ],
+ "cards": [
+ {
+ "dot": "emerald",
+ "title": "Gate \u2014 all three, or nothing",
+ "items": [
+ "UI open, keyring unlocked, account tree initialised",
+ "Every one of those signals re-runs the same check",
+ "Start is skipped when no account or no chain is enabled",
+ "src/AssetsController.ts \u00b7 #updateActive"
+ ]
+ },
+ {
+ "dot": "cyan",
+ "title": "Step 04 \u2014 refresh before subscribe",
+ "items": [
+ "A mutex serialises the forced fetch against later refreshes",
+ "Native zeros and default tracked assets are seeded next",
+ "Sources subscribe with skipInitialFetch, avoiding a re-poll",
+ "src/AssetsController.ts \u00b7 #runStartupRefresh"
+ ]
+ },
+ {
+ "dot": "rose",
+ "title": "Re-scope, pause, destroy",
+ "items": [
+ "Chain claims are recomputed, so every source re-subscribes",
+ "Stop unsubscribes each ds: key so polling timers really die",
+ "Messenger handlers stay registered, so the gate can reopen",
+ "src/AssetsController.ts \u00b7 #stop, destroy"
+ ]
+ }
+ ]
+}
diff --git a/packages/assets-controller/docs-temp/04-tracking-lifecycle.png b/packages/assets-controller/docs-temp/04-tracking-lifecycle.png
new file mode 100644
index 00000000000..d093c1159e9
Binary files /dev/null and b/packages/assets-controller/docs-temp/04-tracking-lifecycle.png differ
diff --git a/packages/assets-controller/docs-temp/04-tracking-lifecycle.svg b/packages/assets-controller/docs-temp/04-tracking-lifecycle.svg
new file mode 100644
index 00000000000..9016c269b2f
--- /dev/null
+++ b/packages/assets-controller/docs-temp/04-tracking-lifecycle.svg
@@ -0,0 +1,496 @@
+
\ No newline at end of file
diff --git a/packages/assets-controller/docs-temp/05-evm-rpc-dataflow.dataflow.json b/packages/assets-controller/docs-temp/05-evm-rpc-dataflow.dataflow.json
new file mode 100644
index 00000000000..eb317b9a0f9
--- /dev/null
+++ b/packages/assets-controller/docs-temp/05-evm-rpc-dataflow.dataflow.json
@@ -0,0 +1,309 @@
+{
+ "schema_version": 1,
+ "diagram_type": "dataflow",
+ "meta": {
+ "title": "EVM RPC Read Path \u2014 State to Decoded Balances",
+ "quality_profile": "showcase",
+ "viewBox": [1070, 505],
+ "views": [
+ {
+ "id": "happy",
+ "label": "One batched call per chain",
+ "focus": ["tracked", "requests", "multicall", "decode", "state"],
+ "note": "The whole tracked set for one account and chain collapses into a single aggregate3 call."
+ },
+ {
+ "id": "degraded",
+ "label": "When the batch cannot be used",
+ "focus": ["requests", "multicall", "per_asset", "failed"],
+ "note": "Two different reasons drop to per-asset calls, and both are opt-in per caller."
+ },
+ {
+ "id": "scope",
+ "label": "What gets asked for",
+ "focus": ["tracked", "custom", "requests"],
+ "note": "RPC only reads assets already known to state, so it never discovers new tokens here."
+ },
+ {
+ "id": "staked",
+ "label": "Pooled staking side channel",
+ "focus": ["stake_scope", "staked_req", "staking_c", "stake_assets"],
+ "note": "Staked balances need two ordered calls and never share the balance batch."
+ }
+ ],
+ "legend": {
+ "entries": {
+ "emphasis": {
+ "label": "Primary read path"
+ },
+ "dashed": {
+ "label": "Degraded to single calls"
+ },
+ "security": {
+ "label": "Dropped result"
+ },
+ "database": {
+ "label": "Controller state"
+ },
+ "default": {
+ "label": "Supporting read"
+ }
+ }
+ }
+ },
+ "stages": [
+ {
+ "label": "Scope"
+ },
+ {
+ "label": "Plan"
+ },
+ {
+ "label": "Call"
+ },
+ {
+ "label": "Decode"
+ },
+ {
+ "label": "Commit"
+ }
+ ],
+ "nodes": [
+ {
+ "id": "tracked",
+ "type": "database",
+ "label": "Tracked Assets",
+ "sublabel": "assetsBalance",
+ "stage": 0,
+ "row": 0
+ },
+ {
+ "id": "custom",
+ "type": "database",
+ "label": "Custom Assets",
+ "sublabel": "RPC is sole source",
+ "stage": 0,
+ "row": 1
+ },
+ {
+ "id": "stake_scope",
+ "type": "database",
+ "label": "Staking Chains",
+ "sublabel": "known contracts",
+ "stage": 0,
+ "row": 2
+ },
+ {
+ "id": "requests",
+ "type": "backend",
+ "label": "Balance Requests",
+ "sublabel": "deduped by address",
+ "stage": 1,
+ "row": 0,
+ "tag": "per account"
+ },
+ {
+ "id": "staked_req",
+ "type": "backend",
+ "label": "Staking Reads",
+ "sublabel": "one per account",
+ "stage": 1,
+ "row": 2
+ },
+ {
+ "id": "multicall",
+ "type": "backend",
+ "label": "Multicall3",
+ "sublabel": "300 calls per batch",
+ "stage": 2,
+ "row": 0,
+ "tag": "aggregate3"
+ },
+ {
+ "id": "per_asset",
+ "type": "backend",
+ "label": "Per-asset Calls",
+ "sublabel": "one eth_call each",
+ "stage": 2,
+ "row": 1
+ },
+ {
+ "id": "staking_c",
+ "type": "external",
+ "label": "Staking Contract",
+ "sublabel": "two ordered calls",
+ "stage": 2,
+ "row": 2
+ },
+ {
+ "id": "decode",
+ "type": "backend",
+ "label": "Decode",
+ "sublabel": "uint256 to string",
+ "stage": 3,
+ "row": 0
+ },
+ {
+ "id": "failed",
+ "type": "backend",
+ "label": "Failed Addresses",
+ "sublabel": "no value written",
+ "stage": 3,
+ "row": 1
+ },
+ {
+ "id": "stake_assets",
+ "type": "backend",
+ "label": "Staked Balance",
+ "sublabel": "synthetic asset id",
+ "stage": 3,
+ "row": 2
+ },
+ {
+ "id": "state",
+ "type": "database",
+ "label": "assetsBalance",
+ "sublabel": "committed",
+ "stage": 4,
+ "row": 0,
+ "tag": "commit"
+ }
+ ],
+ "flows": [
+ {
+ "id": "tracked-in",
+ "from": "tracked",
+ "to": "requests",
+ "label": "tracked ids",
+ "variant": "emphasis",
+ "route": "straight",
+ "labelDy": -32
+ },
+ {
+ "id": "custom-in",
+ "from": "custom",
+ "to": "requests",
+ "label": "custom ids"
+ },
+ {
+ "id": "stake-scope-in",
+ "from": "stake_scope",
+ "to": "staked_req",
+ "label": "eligible chains",
+ "route": "straight"
+ },
+ {
+ "id": "plan-batch",
+ "from": "requests",
+ "to": "multicall",
+ "label": "balanceOf",
+ "variant": "emphasis",
+ "route": "straight",
+ "classification": "batched",
+ "labelDy": -32
+ },
+ {
+ "id": "no-multicall",
+ "from": "requests",
+ "to": "per_asset",
+ "label": "no Multicall3",
+ "variant": "dashed",
+ "classification": "opt-in"
+ },
+ {
+ "id": "batch-threw",
+ "from": "multicall",
+ "to": "per_asset",
+ "label": "3 tries, then drop",
+ "variant": "dashed",
+ "route": "straight",
+ "labelDy": 24
+ },
+ {
+ "id": "return-data",
+ "from": "multicall",
+ "to": "decode",
+ "label": "returnData",
+ "variant": "emphasis",
+ "route": "straight",
+ "classification": "success flag",
+ "labelDy": -32
+ },
+ {
+ "id": "single-results",
+ "from": "per_asset",
+ "to": "decode",
+ "label": "one result each"
+ },
+ {
+ "id": "dropped",
+ "from": "decode",
+ "to": "failed",
+ "label": "success = false",
+ "variant": "security",
+ "labelDy": 24
+ },
+ {
+ "id": "staking-call",
+ "from": "staked_req",
+ "to": "staking_c",
+ "label": "getShares",
+ "route": "straight"
+ },
+ {
+ "id": "staking-convert",
+ "from": "staking_c",
+ "to": "stake_assets",
+ "label": "convertToAssets",
+ "route": "straight"
+ },
+ {
+ "id": "commit",
+ "from": "decode",
+ "to": "state",
+ "label": "balances",
+ "variant": "emphasis",
+ "route": "straight",
+ "classification": "per asset",
+ "labelDy": -32
+ },
+ {
+ "id": "staked-commit",
+ "from": "stake_assets",
+ "to": "state",
+ "label": "staked balance"
+ }
+ ],
+ "cards": [
+ {
+ "dot": "emerald",
+ "title": "Scope comes from state, not discovery",
+ "items": [
+ "Only assets already in assetsBalance are re-read",
+ "Custom assets are read even with no balance entry yet",
+ "Requests are deduplicated by lowercase token address",
+ "src/.../evm-rpc-services/services/BalanceFetcher.ts"
+ ]
+ },
+ {
+ "dot": "cyan",
+ "title": "One call, then two ways down",
+ "items": [
+ "aggregate3 carries up to 300 calls; batches run serially",
+ "Three attempts total before the batch is abandoned",
+ "A chain with no Multicall3 batches 50 single calls in parallel",
+ "src/.../evm-rpc-services/clients/MulticallClient.ts"
+ ]
+ },
+ {
+ "dot": "rose",
+ "title": "Failures stay out of state",
+ "items": [
+ "A per-call success flag decides balance versus failure",
+ "Failed addresses are returned, never written as zero",
+ "Single-call fallback is opt-in, so callers can refuse it",
+ "Staked reads use their own poll and their own contract"
+ ]
+ }
+ ]
+}
diff --git a/packages/assets-controller/docs-temp/05-evm-rpc-dataflow.html b/packages/assets-controller/docs-temp/05-evm-rpc-dataflow.html
new file mode 100644
index 00000000000..1ec1b608bd3
--- /dev/null
+++ b/packages/assets-controller/docs-temp/05-evm-rpc-dataflow.html
@@ -0,0 +1,14917 @@
+
+
+
+
+
+
+ EVM RPC Read Path — State to Decoded Balances Diagram
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Guided views
+
+
+
+
+
Explore this system
+
Step through curated paths without changing the source diagram.
+
+
+ Beat
+
+
+
+ Next
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ready
+ Chapter 01 / 01
+
+
+ Guided chapter
+
+
+
+
+
+
+
+
+ Diagram guide
+ Explore this system
+
+
+
+
Inspecting compiled semantics
+
+
+
+
+
+
+
+
+
+ E ExportT ThemeS Style0 Reset+ Zoom in- Zoom outEsc Close
+
+
+
+
+
+ Find a node
+
+
+
+ ⌕
+
+ /
+
+
+
No matching nodes
+
+
+
+
+
+
Semantic passport
+
+
+
+
+
+
+
+
+
+
+
+
+
Authored reach
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Route probe
+ Choose a start node
+
+
+
+
+
+
+
+
+ Pick two semantic nodes on the diagram
+
+
+
+
+
+
+
+
Choose the source, then the destination. Direction matters.
+
+
+
+
+ Semantic lens
+ Compare system roles
+
+
+
+
Choose up to two semantic kinds. One reveals its real traffic; two compare only direct authored relationships.
+
+
Choose a kind to inspect its nodes and touching relationships.
+
+
+
+
+
+
+
+
+
+ Semantic radar
+ Building overview
+
+
+
+
+
+
Click nodeDrag to pan
+
+
Semantic radar needs more MAP space.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - • Only assets already in assetsBalance are re-read
+ - • Custom assets are read even with no balance entry yet
+ - • Requests are deduplicated by lowercase token address
+ - • src/.../evm-rpc-services/services/BalanceFetcher.ts
+
+
+
+
+
+
+ - • aggregate3 carries up to 300 calls; batches run serially
+ - • Three attempts total before the batch is abandoned
+ - • A chain with no Multicall3 batches 50 single calls in parallel
+ - • src/.../evm-rpc-services/clients/MulticallClient.ts
+
+
+
+
+
+
+ - • A per-call success flag decides balance versus failure
+ - • Failed addresses are returned, never written as zero
+ - • Single-call fallback is opt-in, so callers can refuse it
+ - • Staked reads use their own poll and their own contract
+
+
+
+
+
+
+
+
+
diff --git a/packages/assets-controller/docs-temp/05-evm-rpc-dataflow.png b/packages/assets-controller/docs-temp/05-evm-rpc-dataflow.png
new file mode 100644
index 00000000000..b325c7808b3
Binary files /dev/null and b/packages/assets-controller/docs-temp/05-evm-rpc-dataflow.png differ
diff --git a/packages/assets-controller/docs-temp/05-evm-rpc-dataflow.svg b/packages/assets-controller/docs-temp/05-evm-rpc-dataflow.svg
new file mode 100644
index 00000000000..0a6a903784d
--- /dev/null
+++ b/packages/assets-controller/docs-temp/05-evm-rpc-dataflow.svg
@@ -0,0 +1,566 @@
+
\ No newline at end of file
diff --git a/packages/assets-controller/docs-temp/06-event-stack.html b/packages/assets-controller/docs-temp/06-event-stack.html
new file mode 100644
index 00000000000..c2075629847
--- /dev/null
+++ b/packages/assets-controller/docs-temp/06-event-stack.html
@@ -0,0 +1,14837 @@
+
+
+
+
+
+
+ handleAssetsUpdate — One Stack Composed Per Source Diagram
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Guided views
+
+
+
+
+
Explore this system
+
Step through curated paths without changing the source diagram.
+
+
+ Beat
+
+
+
+ Next
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ready
+ Chapter 01 / 01
+
+
+ Guided chapter
+
+
+
+
+
+
+
+
+ Diagram guide
+ Explore this system
+
+
+
+
Inspecting compiled semantics
+
+
+
+
+
+
+
+
+
+ E ExportT ThemeS Style0 Reset+ Zoom in- Zoom outEsc Close
+
+
+
+
+
+ Find a node
+
+
+
+ ⌕
+
+ /
+
+
+
No matching nodes
+
+
+
+
+
+
Semantic passport
+
+
+
+
+
+
+
+
+
+
+
+
+
Authored reach
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Route probe
+ Choose a start node
+
+
+
+
+
+
+
+
+ Pick two semantic nodes on the diagram
+
+
+
+
+
+
+
+
Choose the source, then the destination. Direction matters.
+
+
+
+
+ Semantic lens
+ Compare system roles
+
+
+
+
Choose up to two semantic kinds. One reveals its real traffic; two compare only direct authored relationships.
+
+
Choose a kind to inspect its nodes and touching relationships.
+
+
+
+
+
+
+
+
+
+ Semantic radar
+ Building overview
+
+
+
+
+
+
Click nodeDrag to pan
+
+
Semantic radar needs more MAP space.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - • Graduation must see only API and websocket balances, never RPC ones
+ - • The occurrence floor runs before detection so spam is never persisted
+ - • Detection is the only stage present for every source
+
+
+
+
+
+
+ - • With it off, metadata and price are removed from the request data types
+ - • The parallel Token and Price stage is then never appended to the stack
+ - • The occurrence filter also needs it, so websocket spam filtering stops too
+
+
+
+
+
+
+ - • Rpc already reads chains directly, so a fallback would be circular
+ - • Snap covers non-EVM chains the RPC fallback cannot serve
+ - • src/AssetsController.ts · handleAssetsUpdate
+
+
+
+
+
+
+
+
+
diff --git a/packages/assets-controller/docs-temp/06-event-stack.png b/packages/assets-controller/docs-temp/06-event-stack.png
new file mode 100644
index 00000000000..67f9b25d2bf
Binary files /dev/null and b/packages/assets-controller/docs-temp/06-event-stack.png differ
diff --git a/packages/assets-controller/docs-temp/06-event-stack.svg b/packages/assets-controller/docs-temp/06-event-stack.svg
new file mode 100644
index 00000000000..7ff21e6bf83
--- /dev/null
+++ b/packages/assets-controller/docs-temp/06-event-stack.svg
@@ -0,0 +1,489 @@
+
\ No newline at end of file
diff --git a/packages/assets-controller/docs-temp/06-event-stack.workflow.json b/packages/assets-controller/docs-temp/06-event-stack.workflow.json
new file mode 100644
index 00000000000..dd4d3c220aa
--- /dev/null
+++ b/packages/assets-controller/docs-temp/06-event-stack.workflow.json
@@ -0,0 +1,259 @@
+{
+ "schema_version": 2,
+ "diagram_type": "workflow",
+ "meta": {
+ "title": "handleAssetsUpdate \u2014 One Stack Composed Per Source",
+ "quality_profile": "showcase",
+ "views": [
+ {
+ "id": "who-gets-what",
+ "label": "Which stages a source gets",
+ "focus": ["update", "graduate", "occurrence", "fallback", "detect"],
+ "note": "The stack is rebuilt for every update; only detection is in it unconditionally."
+ },
+ {
+ "id": "spam",
+ "label": "Spam is dropped before detection",
+ "focus": ["occurrence", "detect"],
+ "note": "A websocket airdrop is checked against Token API occurrences before anything persists it."
+ },
+ {
+ "id": "why-fallback",
+ "label": "Why only AccountsApi gets fallback",
+ "focus": ["graduate", "fallback", "detect"],
+ "note": "Other sources either already read RPC or cover chains the fallback cannot serve."
+ },
+ {
+ "id": "basic-off",
+ "label": "Basic functionality off",
+ "focus": ["detect", "enrich", "commit"],
+ "note": "Metadata and price are stripped from the request, so the parallel stage is never added."
+ }
+ ],
+ "legend": {
+ "entries": {
+ "messagebus": {
+ "label": "Update handoff"
+ },
+ "backend": {
+ "label": "Middleware stage"
+ },
+ "database": {
+ "label": "State"
+ }
+ }
+ }
+ },
+ "lanes": [
+ {
+ "id": "push",
+ "label": "Websocket only",
+ "variant": "exception"
+ },
+ {
+ "id": "prefix",
+ "label": "AccountsApi and Activity"
+ },
+ {
+ "id": "common",
+ "label": "Every source"
+ }
+ ],
+ "phases": [
+ {
+ "id": "prefix",
+ "label": "Source-specific stages",
+ "fromCol": 0,
+ "toCol": 2
+ },
+ {
+ "id": "tail",
+ "label": "Shared tail",
+ "fromCol": 3,
+ "toCol": 4,
+ "variant": "emphasis"
+ },
+ {
+ "id": "write",
+ "label": "Commit",
+ "fromCol": 5,
+ "toCol": 5,
+ "variant": "dashed"
+ }
+ ],
+ "mainPath": ["update", "graduate", "fallback", "detect", "enrich", "commit"],
+ "semanticChecks": {
+ "allowedRoots": ["update", "others"],
+ "allowedTerminals": ["commit"]
+ },
+ "nodes": [
+ {
+ "id": "update",
+ "lane": "prefix",
+ "col": 0,
+ "type": "messagebus",
+ "label": "Graduating sources",
+ "sublabel": "AccountsApi \u00b7 Activity",
+ "tag": "full prefix",
+ "width": 148
+ },
+ {
+ "id": "others",
+ "lane": "common",
+ "col": 0,
+ "type": "messagebus",
+ "label": "Other sources",
+ "sublabel": "Snap \u00b7 Rpc \u00b7 Price \u00b7 Staked",
+ "tag": "no prefix",
+ "width": 148
+ },
+ {
+ "id": "graduate",
+ "lane": "prefix",
+ "col": 1,
+ "type": "backend",
+ "label": "Graduation",
+ "sublabel": "retire custom assets",
+ "width": 150
+ },
+ {
+ "id": "occurrence",
+ "lane": "push",
+ "col": 2,
+ "type": "backend",
+ "label": "Occurrence Floor",
+ "sublabel": "drop below-floor tokens",
+ "width": 160
+ },
+ {
+ "id": "fallback",
+ "lane": "prefix",
+ "col": 2,
+ "type": "backend",
+ "label": "RPC Fallback",
+ "sublabel": "errored + stale-zero",
+ "width": 150
+ },
+ {
+ "id": "detect",
+ "lane": "common",
+ "col": 3,
+ "type": "backend",
+ "label": "Detection",
+ "sublabel": "assets with no metadata",
+ "tag": "always",
+ "width": 158
+ },
+ {
+ "id": "enrich",
+ "lane": "common",
+ "col": 4,
+ "type": "backend",
+ "label": "Token \u2225 Price",
+ "sublabel": "basic functionality on",
+ "width": 150
+ },
+ {
+ "id": "commit",
+ "lane": "common",
+ "col": 5,
+ "type": "database",
+ "label": "#updateState",
+ "sublabel": "merge \u00b7 normalize \u00b7 publish",
+ "width": 166
+ }
+ ],
+ "edges": [
+ {
+ "id": "prefix-in",
+ "from": "update",
+ "to": "graduate",
+ "label": "graduate first",
+ "variant": "emphasis"
+ },
+ {
+ "id": "to-occurrence",
+ "from": "graduate",
+ "to": "occurrence",
+ "label": "websocket push",
+ "role": "branch",
+ "variant": "dashed"
+ },
+ {
+ "id": "to-fallback",
+ "from": "graduate",
+ "to": "fallback",
+ "label": "http poll",
+ "variant": "emphasis"
+ },
+ {
+ "id": "occurrence-detect",
+ "from": "occurrence",
+ "to": "detect",
+ "variant": "dashed"
+ },
+ {
+ "id": "fallback-detect",
+ "from": "fallback",
+ "to": "detect",
+ "variant": "emphasis"
+ },
+ {
+ "id": "detect-enrich",
+ "from": "detect",
+ "to": "enrich",
+ "variant": "emphasis"
+ },
+ {
+ "id": "enrich-commit",
+ "from": "enrich",
+ "to": "commit",
+ "variant": "emphasis"
+ },
+ {
+ "id": "skip-enrich",
+ "from": "detect",
+ "to": "commit",
+ "label": "RPC-only mode",
+ "role": "branch",
+ "variant": "dashed",
+ "route": "bottom-channel"
+ },
+ {
+ "id": "others-detect",
+ "from": "others",
+ "to": "detect",
+ "label": "detection only",
+ "role": "branch"
+ }
+ ],
+ "cards": [
+ {
+ "dot": "cyan",
+ "title": "Order is a correctness constraint",
+ "items": [
+ "Graduation must see only API and websocket balances, never RPC ones",
+ "The occurrence floor runs before detection so spam is never persisted",
+ "Detection is the only stage present for every source"
+ ]
+ },
+ {
+ "dot": "amber",
+ "title": "Basic functionality gates two stages",
+ "items": [
+ "With it off, metadata and price are removed from the request data types",
+ "The parallel Token and Price stage is then never appended to the stack",
+ "The occurrence filter also needs it, so websocket spam filtering stops too"
+ ]
+ },
+ {
+ "dot": "rose",
+ "title": "Only AccountsApi triggers fallback here",
+ "items": [
+ "Rpc already reads chains directly, so a fallback would be circular",
+ "Snap covers non-EVM chains the RPC fallback cannot serve",
+ "src/AssetsController.ts \u00b7 handleAssetsUpdate"
+ ]
+ }
+ ]
+}
diff --git a/packages/assets-controller/docs-temp/07-token-filtering.html b/packages/assets-controller/docs-temp/07-token-filtering.html
new file mode 100644
index 00000000000..6a84e90903e
--- /dev/null
+++ b/packages/assets-controller/docs-temp/07-token-filtering.html
@@ -0,0 +1,14955 @@
+
+
+
+
+
+
+ Where A Spam Token Gets Filtered Out Diagram
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Guided views
+
+
+
+
+
Explore this system
+
Step through curated paths without changing the source diagram.
+
+
+ Beat
+
+
+
+ Next
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ready
+ Chapter 01 / 01
+
+
+ Guided chapter
+
+
+
+
+
+
+
+
+ Diagram guide
+ Explore this system
+
+
+
+
Inspecting compiled semantics
+
+
+
+
+
+
+
+
+
+ E ExportT ThemeS Style0 Reset+ Zoom in- Zoom outEsc Close
+
+
+
+
+
+ Find a node
+
+
+
+ ⌕
+
+ /
+
+
+
No matching nodes
+
+
+
+
+
+
Semantic passport
+
+
+
+
+
+
+
+
+
+
+
+
+
Authored reach
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Route probe
+ Choose a start node
+
+
+
+
+
+
+
+
+ Pick two semantic nodes on the diagram
+
+
+
+
+
+
+
+
Choose the source, then the destination. Direction matters.
+
+
+
+
+ Semantic lens
+ Compare system roles
+
+
+
+
Choose up to two semantic kinds. One reveals its real traffic; two compare only direct authored relationships.
+
+
Choose a kind to inspect its nodes and touching relationships.
+
+
+
+
+
+
+
+
+
+ Semantic radar
+ Building overview
+
+
+
+
+
+
Click nodeDrag to pan
+
+
Semantic radar needs more MAP space.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - • EVM ERC-20: occurrence count at or above its chain's suggested floor, default 3
+ - • Non-EVM token: a clean Blockaid verdict from PhishingController bulkScanTokens
+ - • Native assets, user-imported custom assets and mUSD are never judged at all
+
+
+
+
+
+
+ - • Both pipeline filters fail open: a Token API error keeps every candidate
+ - • The unlock sweep fails the other way: a failed call deletes nothing
+ - • Deleting a real holding is worse than briefly showing one that is spam
+
+
+
+
+
+
+ - • The pipeline filters only see assets moving through them right now
+ - • Tokens saved by older builds, a loosened API or a fail-open never get re-judged
+ - • Gated behind assetsUnifyState.useUnlockCleanup so it can be turned off remotely
+
+
+
+
+
+
+
+
+
diff --git a/packages/assets-controller/docs-temp/07-token-filtering.png b/packages/assets-controller/docs-temp/07-token-filtering.png
new file mode 100644
index 00000000000..bcac96e03bf
Binary files /dev/null and b/packages/assets-controller/docs-temp/07-token-filtering.png differ
diff --git a/packages/assets-controller/docs-temp/07-token-filtering.svg b/packages/assets-controller/docs-temp/07-token-filtering.svg
new file mode 100644
index 00000000000..dfef801b7e0
--- /dev/null
+++ b/packages/assets-controller/docs-temp/07-token-filtering.svg
@@ -0,0 +1,607 @@
+
\ No newline at end of file
diff --git a/packages/assets-controller/docs-temp/07-token-filtering.workflow.json b/packages/assets-controller/docs-temp/07-token-filtering.workflow.json
new file mode 100644
index 00000000000..7a1ca24b46c
--- /dev/null
+++ b/packages/assets-controller/docs-temp/07-token-filtering.workflow.json
@@ -0,0 +1,350 @@
+{
+ "schema_version": 2,
+ "diagram_type": "workflow",
+ "meta": {
+ "title": "Where A Spam Token Gets Filtered Out",
+ "quality_profile": "showcase",
+ "views": [
+ {
+ "id": "three-moments",
+ "label": "Three separate moments",
+ "focus": ["ws_cut", "en_cut", "un_cut"],
+ "note": "Two cuts edit an in-flight response; only the unlock cleanup edits state that was already saved."
+ },
+ {
+ "id": "websocket",
+ "label": "A websocket airdrop",
+ "focus": ["ws_in", "ws_pick", "ws_judge", "ws_cut"],
+ "note": "Runs before DetectionMiddleware, so a below-floor airdrop is never detected, enriched, priced or persisted."
+ },
+ {
+ "id": "enrichment",
+ "label": "The enrichment stage",
+ "focus": ["en_in", "en_pick", "en_judge", "en_cut"],
+ "note": "Lives in the parallel Token and Price stage, so it is the one filter both the fetch and event stacks share."
+ },
+ {
+ "id": "unlock",
+ "label": "The unlock sweep",
+ "focus": ["un_in", "un_gate", "un_pick", "un_judge", "un_cut"],
+ "note": "Repairs state that older builds, a loosened API or a websocket gap already let through."
+ }
+ ],
+ "legend": {
+ "entries": {
+ "messagebus": {
+ "label": "Trigger"
+ },
+ "backend": {
+ "label": "In-process step"
+ },
+ "cloud": {
+ "label": "Token API call"
+ },
+ "database": {
+ "label": "State"
+ }
+ }
+ }
+ },
+ "lanes": [
+ {
+ "id": "push",
+ "label": "On a websocket push"
+ },
+ {
+ "id": "enrich",
+ "label": "In the enrichment stage"
+ },
+ {
+ "id": "unlock",
+ "label": "On keyring unlock",
+ "variant": "exception"
+ }
+ ],
+ "phases": [
+ {
+ "id": "arrive",
+ "label": "What arrives",
+ "fromCol": 0,
+ "toCol": 1
+ },
+ {
+ "id": "judge",
+ "label": "What gets judged",
+ "fromCol": 2,
+ "toCol": 3,
+ "variant": "emphasis"
+ },
+ {
+ "id": "cut",
+ "label": "What is removed",
+ "fromCol": 4,
+ "toCol": 5,
+ "variant": "security"
+ }
+ ],
+ "mainPath": ["en_in", "en_pick", "en_judge", "en_cut", "state"],
+ "semanticChecks": {
+ "allowedRoots": ["ws_in", "en_in", "un_in"],
+ "allowedTerminals": ["state"]
+ },
+ "nodes": [
+ {
+ "id": "ws_in",
+ "lane": "push",
+ "col": 0,
+ "type": "messagebus",
+ "label": "AccountActivity",
+ "sublabel": "balance push",
+ "width": 136
+ },
+ {
+ "id": "ws_gate",
+ "lane": "push",
+ "col": 1,
+ "type": "backend",
+ "label": "Basic functionality",
+ "sublabel": "off means no filter",
+ "width": 136
+ },
+ {
+ "id": "ws_pick",
+ "lane": "push",
+ "col": 2,
+ "type": "backend",
+ "label": "New ERC-20s",
+ "sublabel": "unknown to state",
+ "width": 136
+ },
+ {
+ "id": "ws_judge",
+ "lane": "push",
+ "col": 3,
+ "type": "cloud",
+ "label": "Occurrence floor",
+ "sublabel": "fail open",
+ "width": 136
+ },
+ {
+ "id": "ws_cut",
+ "lane": "push",
+ "col": 4,
+ "type": "backend",
+ "label": "Cut pre-detection",
+ "sublabel": "never persisted",
+ "width": 136
+ },
+ {
+ "id": "en_in",
+ "lane": "enrich",
+ "col": 0,
+ "type": "messagebus",
+ "label": "Metadata wanted",
+ "sublabel": "both pipelines",
+ "width": 136
+ },
+ {
+ "id": "en_pick",
+ "lane": "enrich",
+ "col": 2,
+ "type": "backend",
+ "label": "Detected only",
+ "sublabel": "heals are exempt",
+ "width": 136
+ },
+ {
+ "id": "en_judge",
+ "lane": "enrich",
+ "col": 3,
+ "type": "cloud",
+ "label": "Floor or Blockaid",
+ "sublabel": "fail open",
+ "width": 136
+ },
+ {
+ "id": "en_cut",
+ "lane": "enrich",
+ "col": 4,
+ "type": "backend",
+ "label": "Cut from response",
+ "sublabel": "stub metadata too",
+ "width": 136
+ },
+ {
+ "id": "un_in",
+ "lane": "unlock",
+ "col": 0,
+ "type": "messagebus",
+ "label": "Keyring unlock",
+ "sublabel": "before tracking",
+ "width": 136
+ },
+ {
+ "id": "un_gate",
+ "lane": "unlock",
+ "col": 1,
+ "type": "backend",
+ "label": "Three gates",
+ "sublabel": "all must pass",
+ "width": 136
+ },
+ {
+ "id": "un_pick",
+ "lane": "unlock",
+ "col": 2,
+ "type": "backend",
+ "label": "Saved ERC-20s",
+ "sublabel": "Accounts API chains",
+ "width": 136
+ },
+ {
+ "id": "un_judge",
+ "lane": "unlock",
+ "col": 3,
+ "type": "cloud",
+ "label": "Occurrence floor",
+ "sublabel": "no delete on error",
+ "width": 136
+ },
+ {
+ "id": "un_cut",
+ "lane": "unlock",
+ "col": 4,
+ "type": "backend",
+ "label": "Cut from state",
+ "sublabel": "info, balance, price",
+ "width": 136
+ },
+ {
+ "id": "state",
+ "lane": "enrich",
+ "col": 5,
+ "type": "database",
+ "label": "Assets state",
+ "sublabel": "what the UI reads",
+ "width": 136
+ }
+ ],
+ "edges": [
+ {
+ "id": "ws-1",
+ "from": "ws_in",
+ "to": "ws_gate",
+ "label": "new balance"
+ },
+ {
+ "id": "ws-2",
+ "from": "ws_gate",
+ "to": "ws_pick",
+ "label": "on"
+ },
+ {
+ "id": "ws-3",
+ "from": "ws_pick",
+ "to": "ws_judge",
+ "label": "candidates"
+ },
+ {
+ "id": "ws-4",
+ "from": "ws_judge",
+ "to": "ws_cut",
+ "label": "below floor",
+ "role": "error"
+ },
+ {
+ "id": "ws-5",
+ "from": "ws_cut",
+ "to": "state",
+ "label": "survivors"
+ },
+ {
+ "id": "en-1",
+ "from": "en_in",
+ "to": "en_pick",
+ "label": "detected assets",
+ "variant": "emphasis"
+ },
+ {
+ "id": "en-2",
+ "from": "en_pick",
+ "to": "en_judge",
+ "label": "candidates",
+ "variant": "emphasis"
+ },
+ {
+ "id": "en-3",
+ "from": "en_judge",
+ "to": "en_cut",
+ "label": "spam",
+ "role": "error"
+ },
+ {
+ "id": "en-4",
+ "from": "en_cut",
+ "to": "state",
+ "label": "survivors",
+ "variant": "emphasis"
+ },
+ {
+ "id": "un-1",
+ "from": "un_in",
+ "to": "un_gate",
+ "label": "unlocked"
+ },
+ {
+ "id": "un-2",
+ "from": "un_gate",
+ "to": "un_pick",
+ "label": "all pass"
+ },
+ {
+ "id": "un-3",
+ "from": "un_pick",
+ "to": "un_judge",
+ "label": "batches of 50"
+ },
+ {
+ "id": "un-4",
+ "from": "un_judge",
+ "to": "un_cut",
+ "label": "below floor",
+ "role": "error"
+ },
+ {
+ "id": "un-5",
+ "from": "un_cut",
+ "to": "state",
+ "label": "rewritten"
+ }
+ ],
+ "cards": [
+ {
+ "dot": "cyan",
+ "title": "What a token has to clear",
+ "items": [
+ "EVM ERC-20: occurrence count at or above its chain's suggested floor, default 3",
+ "Non-EVM token: a clean Blockaid verdict from PhishingController bulkScanTokens",
+ "Native assets, user-imported custom assets and mUSD are never judged at all"
+ ]
+ },
+ {
+ "dot": "amber",
+ "title": "The two failure modes differ on purpose",
+ "items": [
+ "Both pipeline filters fail open: a Token API error keeps every candidate",
+ "The unlock sweep fails the other way: a failed call deletes nothing",
+ "Deleting a real holding is worse than briefly showing one that is spam"
+ ]
+ },
+ {
+ "dot": "violet",
+ "title": "Why a third pass exists",
+ "items": [
+ "The pipeline filters only see assets moving through them right now",
+ "Tokens saved by older builds, a loosened API or a fail-open never get re-judged",
+ "Gated behind assetsUnifyState.useUnlockCleanup so it can be turned off remotely"
+ ]
+ }
+ ]
+}
diff --git a/packages/assets-controller/docs-temp/08-spam-verdict.dataflow.json b/packages/assets-controller/docs-temp/08-spam-verdict.dataflow.json
new file mode 100644
index 00000000000..471bed0ba03
--- /dev/null
+++ b/packages/assets-controller/docs-temp/08-spam-verdict.dataflow.json
@@ -0,0 +1,209 @@
+{
+ "schema_version": 1,
+ "diagram_type": "dataflow",
+ "meta": {
+ "title": "Spam Verdict — How One Token Is Judged",
+ "quality_profile": "showcase",
+ "viewBox": [1070, 505],
+ "views": [
+ {
+ "id": "evm",
+ "label": "An unknown EVM token",
+ "focus": ["candidate", "exempt", "evm", "verdict", "dropped"],
+ "note": "The whole EVM decision is one number against one threshold: aggregator occurrences against the chain's floor."
+ },
+ {
+ "id": "nonevm",
+ "label": "A non-EVM token",
+ "focus": ["candidate", "exempt", "nonevm", "verdict"],
+ "note": "Solana and other non-EVM mints have no occurrence count, so Blockaid decides instead."
+ },
+ {
+ "id": "exempt",
+ "label": "What is never judged",
+ "focus": ["candidate", "exempt", "unjudged"],
+ "note": "Each of the three filtering passes carries its own skip list; this one belongs to the enrichment stage."
+ },
+ {
+ "id": "effect",
+ "label": "What a drop removes",
+ "focus": ["verdict", "kept", "dropped"],
+ "note": "Stub metadata goes too, so the token cannot look known and slip through as a heal next time."
+ }
+ ],
+ "legend": {
+ "entries": {
+ "emphasis": { "label": "EVM decision path" },
+ "security": { "label": "Removal" },
+ "database": { "label": "Survives" },
+ "default": { "label": "Non-EVM path" }
+ }
+ }
+ },
+ "stages": [
+ { "label": "Candidate" },
+ { "label": "Exemptions" },
+ { "label": "Rule" },
+ { "label": "Verdict" },
+ { "label": "Effect" }
+ ],
+ "nodes": [
+ {
+ "id": "candidate",
+ "type": "messagebus",
+ "label": "One asset id",
+ "sublabel": "in a response or state",
+ "stage": 0,
+ "row": 0
+ },
+ {
+ "id": "exempt",
+ "type": "backend",
+ "label": "Exemption check",
+ "sublabel": "four ways to skip",
+ "stage": 1,
+ "row": 0
+ },
+ {
+ "id": "evm",
+ "type": "cloud",
+ "label": "Occurrence floor",
+ "sublabel": "Token API v3 count",
+ "stage": 2,
+ "row": 0
+ },
+ {
+ "id": "nonevm",
+ "type": "external",
+ "label": "Blockaid scan",
+ "sublabel": "bulkScanTokens",
+ "stage": 2,
+ "row": 1
+ },
+ {
+ "id": "verdict",
+ "type": "backend",
+ "label": "Verdict",
+ "sublabel": "keep or drop",
+ "stage": 3,
+ "row": 0
+ },
+ {
+ "id": "kept",
+ "type": "database",
+ "label": "Kept",
+ "sublabel": "metadata persisted",
+ "stage": 4,
+ "row": 0
+ },
+ {
+ "id": "dropped",
+ "type": "security",
+ "label": "Dropped",
+ "sublabel": "balance, info, detected",
+ "stage": 4,
+ "row": 1
+ },
+ {
+ "id": "unjudged",
+ "type": "database",
+ "label": "Kept unjudged",
+ "sublabel": "no scan can remove it",
+ "stage": 4,
+ "row": 2
+ }
+ ],
+ "flows": [
+ {
+ "id": "in",
+ "from": "candidate",
+ "to": "exempt",
+ "label": "asset id",
+ "variant": "emphasis",
+ "route": "straight"
+ },
+ {
+ "id": "to-evm",
+ "from": "exempt",
+ "to": "evm",
+ "label": "eip155 erc20",
+ "variant": "emphasis",
+ "route": "straight"
+ },
+ {
+ "id": "to-nonevm",
+ "from": "exempt",
+ "to": "nonevm",
+ "label": "token namespace"
+ },
+ {
+ "id": "skip",
+ "from": "exempt",
+ "to": "unjudged",
+ "label": "exempt, never scanned",
+ "route": "bottom-channel",
+ "toSide": "bottom",
+ "labelSegment": 1,
+ "labelDx": -190
+ },
+ {
+ "id": "evm-out",
+ "from": "evm",
+ "to": "verdict",
+ "label": "count vs floor",
+ "variant": "emphasis",
+ "route": "straight"
+ },
+ {
+ "id": "nonevm-out",
+ "from": "nonevm",
+ "to": "verdict",
+ "label": "clean or flagged"
+ },
+ {
+ "id": "keep",
+ "from": "verdict",
+ "to": "kept",
+ "label": "passed",
+ "variant": "emphasis",
+ "route": "straight"
+ },
+ {
+ "id": "drop",
+ "from": "verdict",
+ "to": "dropped",
+ "label": "failed",
+ "variant": "security"
+ }
+ ],
+ "cards": [
+ {
+ "dot": "emerald",
+ "title": "What is never scanned",
+ "items": [
+ "Native assets — a chain's own coin is always kept",
+ "Custom assets — a user who imports a token gets to keep it",
+ "Balance-only heals — already held, only the metadata is missing",
+ "Staking contracts and mUSD, both exempted by address"
+ ]
+ },
+ {
+ "dot": "cyan",
+ "title": "Where the floor comes from",
+ "items": [
+ "GET /v1/suggestedOccurrenceFloors returns one floor per chain",
+ "Chains the response omits fall back to a floor of 3",
+ "No occurrence data is read as zero, so an unlisted token fails"
+ ]
+ },
+ {
+ "dot": "amber",
+ "title": "A drop is more than a balance delete",
+ "items": [
+ "Its entry leaves assetsBalance, assetsInfo and the detected list",
+ "Any stub name and symbol seeded by the websocket is deleted too",
+ "Leaving the stub would make the token look known and exempt it as a heal"
+ ]
+ }
+ ]
+}
diff --git a/packages/assets-controller/docs-temp/08-spam-verdict.html b/packages/assets-controller/docs-temp/08-spam-verdict.html
new file mode 100644
index 00000000000..56b0c1fa80a
--- /dev/null
+++ b/packages/assets-controller/docs-temp/08-spam-verdict.html
@@ -0,0 +1,14837 @@
+
+
+
+
+
+
+ Spam Verdict — How One Token Is Judged Diagram
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Guided views
+
+
+
+
+
Explore this system
+
Step through curated paths without changing the source diagram.
+
+
+ Beat
+
+
+
+ Next
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ready
+ Chapter 01 / 01
+
+
+ Guided chapter
+
+
+
+
+
+
+
+
+ Diagram guide
+ Explore this system
+
+
+
+
Inspecting compiled semantics
+
+
+
+
+
+
+
+
+
+ E ExportT ThemeS Style0 Reset+ Zoom in- Zoom outEsc Close
+
+
+
+
+
+ Find a node
+
+
+
+ ⌕
+
+ /
+
+
+
No matching nodes
+
+
+
+
+
+
Semantic passport
+
+
+
+
+
+
+
+
+
+
+
+
+
Authored reach
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Route probe
+ Choose a start node
+
+
+
+
+
+
+
+
+ Pick two semantic nodes on the diagram
+
+
+
+
+
+
+
+
Choose the source, then the destination. Direction matters.
+
+
+
+
+ Semantic lens
+ Compare system roles
+
+
+
+
Choose up to two semantic kinds. One reveals its real traffic; two compare only direct authored relationships.
+
+
Choose a kind to inspect its nodes and touching relationships.
+
+
+
+
+
+
+
+
+
+ Semantic radar
+ Building overview
+
+
+
+
+
+
Click nodeDrag to pan
+
+
Semantic radar needs more MAP space.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - • Native assets — a chain's own coin is always kept
+ - • Custom assets — a user who imports a token gets to keep it
+ - • Balance-only heals — already held, only the metadata is missing
+ - • Staking contracts and mUSD, both exempted by address
+
+
+
+
+
+
+ - • GET /v1/suggestedOccurrenceFloors returns one floor per chain
+ - • Chains the response omits fall back to a floor of 3
+ - • No occurrence data is read as zero, so an unlisted token fails
+
+
+
+
+
+
+ - • Its entry leaves assetsBalance, assetsInfo and the detected list
+ - • Any stub name and symbol seeded by the websocket is deleted too
+ - • Leaving the stub would make the token look known and exempt it as a heal
+
+
+
+
+
+
+
+
+
diff --git a/packages/assets-controller/docs-temp/08-spam-verdict.png b/packages/assets-controller/docs-temp/08-spam-verdict.png
new file mode 100644
index 00000000000..fbad1a74707
Binary files /dev/null and b/packages/assets-controller/docs-temp/08-spam-verdict.png differ
diff --git a/packages/assets-controller/docs-temp/08-spam-verdict.svg b/packages/assets-controller/docs-temp/08-spam-verdict.svg
new file mode 100644
index 00000000000..6879e0cecae
--- /dev/null
+++ b/packages/assets-controller/docs-temp/08-spam-verdict.svg
@@ -0,0 +1,488 @@
+
\ No newline at end of file
diff --git a/packages/assets-controller/docs-temp/09-visibility-gate.html b/packages/assets-controller/docs-temp/09-visibility-gate.html
new file mode 100644
index 00000000000..655b5cee5d9
--- /dev/null
+++ b/packages/assets-controller/docs-temp/09-visibility-gate.html
@@ -0,0 +1,14846 @@
+
+
+
+
+
+
+ Read Path — Why A Stored Token Is Not Shown Diagram
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Guided views
+
+
+
+
+
Explore this system
+
Step through curated paths without changing the source diagram.
+
+
+ Beat
+
+
+
+ Next
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ready
+ Chapter 01 / 01
+
+
+ Guided chapter
+
+
+
+
+
+
+
+
+ Diagram guide
+ Explore this system
+
+
+
+
Inspecting compiled semantics
+
+
+
+
+
+
+
+
+
+ E ExportT ThemeS Style0 Reset+ Zoom in- Zoom outEsc Close
+
+
+
+
+
+ Find a node
+
+
+
+ ⌕
+
+ /
+
+
+
No matching nodes
+
+
+
+
+
+
Semantic passport
+
+
+
+
+
+
+
+
+
+
+
+
+
Authored reach
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Route probe
+ Choose a start node
+
+
+
+
+
+
+
+
+ Pick two semantic nodes on the diagram
+
+
+
+
+
+
+
+
Choose the source, then the destination. Direction matters.
+
+
+
+
+ Semantic lens
+ Compare system roles
+
+
+
+
Choose up to two semantic kinds. One reveals its real traffic; two compare only direct authored relationships.
+
+
Choose a kind to inspect its nodes and touching relationships.
+
+
+
+
+
+
+
+
+
+ Semantic radar
+ Building overview
+
+
+
+
+
+
Click nodeDrag to pan
+
+
Semantic radar needs more MAP space.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - • Nothing here deletes anything; the asset stays in state and keeps refreshing
+ - • Spam filtering decides what is written, these gates decide what is returned
+ - • So a row can disappear from the UI while its balance is still being polled
+
+
+
+
+
+
+ - • Balance and metadata must both exist, or there is no symbol or decimals to show
+ - • hideAsset writes assetPreferences.hidden; re-adding the asset as custom clears it
+ - • Chain scope covers disabled networks and chains that have no native token at all
+
+
+
+
+
+
+ - • getAccountAssets takes optional chain and asset-type filters on top of the gates
+ - • Those are per-call and never persisted, unlike the hidden preference
+ - • Aggregate balance selectors apply the same hidden and enabled-network rules
+
+
+
+
+
+
+
+
+
diff --git a/packages/assets-controller/docs-temp/09-visibility-gate.png b/packages/assets-controller/docs-temp/09-visibility-gate.png
new file mode 100644
index 00000000000..de0b373623a
Binary files /dev/null and b/packages/assets-controller/docs-temp/09-visibility-gate.png differ
diff --git a/packages/assets-controller/docs-temp/09-visibility-gate.svg b/packages/assets-controller/docs-temp/09-visibility-gate.svg
new file mode 100644
index 00000000000..537f4980b37
--- /dev/null
+++ b/packages/assets-controller/docs-temp/09-visibility-gate.svg
@@ -0,0 +1,498 @@
+
\ No newline at end of file
diff --git a/packages/assets-controller/docs-temp/09-visibility-gate.workflow.json b/packages/assets-controller/docs-temp/09-visibility-gate.workflow.json
new file mode 100644
index 00000000000..cf3c3894c09
--- /dev/null
+++ b/packages/assets-controller/docs-temp/09-visibility-gate.workflow.json
@@ -0,0 +1,267 @@
+{
+ "schema_version": 2,
+ "diagram_type": "workflow",
+ "meta": {
+ "title": "Read Path \u2014 Why A Stored Token Is Not Shown",
+ "quality_profile": "showcase",
+ "views": [
+ {
+ "id": "gates",
+ "label": "The four read gates",
+ "focus": ["has_bal", "has_meta", "not_hidden", "chain_ok"],
+ "note": "Every gate is re-evaluated per asset on every read, and none of them writes anything back."
+ },
+ {
+ "id": "incomplete",
+ "label": "Waiting on data",
+ "focus": ["has_bal", "has_meta", "incomplete"],
+ "note": "A balance with no metadata has no symbol or decimals to render, so it waits for enrichment to catch up."
+ },
+ {
+ "id": "hidden",
+ "label": "The gate the user controls",
+ "focus": ["not_hidden", "withheld"],
+ "note": "hideAsset only sets a preference, so the balance keeps refreshing while the row is gone."
+ },
+ {
+ "id": "scope",
+ "label": "Scope, not spam",
+ "focus": ["chain_ok", "withheld", "visible"],
+ "note": "Disabled networks and chains with no native token drop rows the caller never asked for."
+ }
+ ],
+ "legend": {
+ "entries": {
+ "database": {
+ "label": "State"
+ },
+ "backend": {
+ "label": "Read gate"
+ },
+ "frontend": {
+ "label": "Returned to the caller"
+ },
+ "security": {
+ "label": "Not returned"
+ }
+ }
+ }
+ },
+ "lanes": [
+ {
+ "id": "shown",
+ "label": "Returned by a read"
+ },
+ {
+ "id": "held",
+ "label": "Kept in state, not returned",
+ "variant": "exception"
+ }
+ ],
+ "phases": [
+ {
+ "id": "source",
+ "label": "What is stored",
+ "fromCol": 0,
+ "toCol": 0
+ },
+ {
+ "id": "gate",
+ "label": "Four gates, every read",
+ "fromCol": 1,
+ "toCol": 4,
+ "variant": "emphasis"
+ },
+ {
+ "id": "out",
+ "label": "What the caller sees",
+ "fromCol": 5,
+ "toCol": 5
+ }
+ ],
+ "mainPath": [
+ "stored",
+ "has_bal",
+ "has_meta",
+ "not_hidden",
+ "chain_ok",
+ "visible"
+ ],
+ "semanticChecks": {
+ "allowedRoots": ["stored"],
+ "allowedTerminals": ["visible", "incomplete", "withheld"]
+ },
+ "nodes": [
+ {
+ "id": "stored",
+ "lane": "shown",
+ "col": 0,
+ "type": "database",
+ "label": "Asset in state",
+ "sublabel": "survived every filter",
+ "width": 136
+ },
+ {
+ "id": "has_bal",
+ "lane": "shown",
+ "col": 1,
+ "type": "backend",
+ "label": "Has a balance",
+ "sublabel": "assetsBalance",
+ "width": 136
+ },
+ {
+ "id": "has_meta",
+ "lane": "shown",
+ "col": 2,
+ "type": "backend",
+ "label": "Has metadata",
+ "sublabel": "assetsInfo",
+ "width": 136
+ },
+ {
+ "id": "not_hidden",
+ "lane": "shown",
+ "col": 3,
+ "type": "backend",
+ "label": "Not hidden",
+ "sublabel": "assetPreferences",
+ "width": 136
+ },
+ {
+ "id": "chain_ok",
+ "lane": "shown",
+ "col": 4,
+ "type": "backend",
+ "label": "Chain in scope",
+ "sublabel": "enabled, has native",
+ "width": 136
+ },
+ {
+ "id": "visible",
+ "lane": "shown",
+ "col": 5,
+ "type": "frontend",
+ "label": "Rendered",
+ "sublabel": "balance and fiat",
+ "width": 136
+ },
+ {
+ "id": "incomplete",
+ "lane": "held",
+ "col": 2,
+ "type": "security",
+ "label": "Not renderable",
+ "sublabel": "waiting on data",
+ "width": 136
+ },
+ {
+ "id": "withheld",
+ "lane": "held",
+ "col": 5,
+ "type": "security",
+ "label": "Withheld",
+ "sublabel": "still tracked and fetched",
+ "width": 136
+ }
+ ],
+ "edges": [
+ {
+ "id": "s-1",
+ "from": "stored",
+ "to": "has_bal",
+ "label": "per asset",
+ "variant": "emphasis",
+ "route": "straight"
+ },
+ {
+ "id": "s-2",
+ "from": "has_bal",
+ "to": "has_meta",
+ "label": "yes",
+ "variant": "emphasis",
+ "route": "straight"
+ },
+ {
+ "id": "s-3",
+ "from": "has_meta",
+ "to": "not_hidden",
+ "label": "yes",
+ "variant": "emphasis",
+ "route": "straight"
+ },
+ {
+ "id": "s-4",
+ "from": "not_hidden",
+ "to": "chain_ok",
+ "label": "yes",
+ "variant": "emphasis",
+ "route": "straight"
+ },
+ {
+ "id": "s-5",
+ "from": "chain_ok",
+ "to": "visible",
+ "label": "yes",
+ "variant": "emphasis",
+ "route": "straight"
+ },
+ {
+ "id": "b-1",
+ "from": "has_bal",
+ "to": "incomplete",
+ "label": "no balance",
+ "role": "error"
+ },
+ {
+ "id": "b-2",
+ "from": "has_meta",
+ "to": "incomplete",
+ "label": "no metadata",
+ "role": "error"
+ },
+ {
+ "id": "b-3",
+ "from": "not_hidden",
+ "to": "withheld",
+ "label": "user hid it",
+ "role": "error"
+ },
+ {
+ "id": "b-4",
+ "from": "chain_ok",
+ "to": "withheld",
+ "label": "out of scope",
+ "role": "error"
+ }
+ ],
+ "cards": [
+ {
+ "dot": "cyan",
+ "title": "A read filter is not a spam filter",
+ "items": [
+ "Nothing here deletes anything; the asset stays in state and keeps refreshing",
+ "Spam filtering decides what is written, these gates decide what is returned",
+ "So a row can disappear from the UI while its balance is still being polled"
+ ]
+ },
+ {
+ "dot": "violet",
+ "title": "What each gate actually reads",
+ "items": [
+ "Balance and metadata must both exist, or there is no symbol or decimals to show",
+ "hideAsset writes assetPreferences.hidden; re-adding the asset as custom clears it",
+ "Chain scope covers disabled networks and chains that have no native token at all"
+ ]
+ },
+ {
+ "dot": "amber",
+ "title": "Callers can narrow it further",
+ "items": [
+ "getAccountAssets takes optional chain and asset-type filters on top of the gates",
+ "Those are per-call and never persisted, unlike the hidden preference",
+ "Aggregate balance selectors apply the same hidden and enabled-network rules"
+ ]
+ }
+ ]
+}
diff --git a/packages/assets-controller/docs-temp/README.md b/packages/assets-controller/docs-temp/README.md
new file mode 100644
index 00000000000..092bfa80314
--- /dev/null
+++ b/packages/assets-controller/docs-temp/README.md
@@ -0,0 +1,46 @@
+# Assets controller architecture diagrams
+
+Scratch space for diagrams that explain how this package actually behaves. Nothing here
+is built, published, or imported by the package.
+
+Each diagram exists in four forms:
+
+- `*..json` — the source of truth, authored against the
+ [Archify](https://tt-a1i.github.io/archify/) schemas.
+- `*.html` — a self-contained interactive viewer: guided views, light and dark themes,
+ hover focus, and an export menu.
+- `*.png` — a flat raster of the diagram alone, for pasting into issues and docs.
+- `*.svg` — the same, as self-contained vector output.
+
+| Diagram | Kind | Question it answers |
+| ------------------------------------------------------ | ------------ | --------------------------------------------------------------------------- |
+| [`01-component-map`](./01-component-map.png) | architecture | What exists, how callers reach it, and which data source claims which chain |
+| [`02-fetch-pipeline`](./02-fetch-pipeline.png) | workflow | What `getAssets` awaits, and what it defers to a background lane |
+| [`03-fallback-cascade`](./03-fallback-cascade.png) | sequence | How one EVM chain degrades from Accounts API to RPC to per-asset calls |
+| [`04-tracking-lifecycle`](./04-tracking-lifecycle.png) | lifecycle | Why tracking is or is not running, and what stop and destroy each release |
+| [`05-evm-rpc-dataflow`](./05-evm-rpc-dataflow.png) | dataflow | How tracked state becomes one `aggregate3` call and then decoded balances |
+| [`06-event-stack`](./06-event-stack.png) | workflow | How `handleAssetsUpdate` composes a different middleware stack per source |
+| [`07-token-filtering`](./07-token-filtering.png) | workflow | The three moments a spam token can be filtered out, and what each removes |
+| [`08-spam-verdict`](./08-spam-verdict.png) | dataflow | How one candidate token turns into a keep or drop verdict |
+| [`09-visibility-gate`](./09-visibility-gate.png) | workflow | Why a token that is in state still does not come back from a read |
+
+Diagrams 02 and 06 are the two halves of the middleware architecture: 02 is the fetch
+stack that a caller drives, 06 is the event stack that an incoming update drives.
+
+Diagrams 07, 08 and 09 cover token filtering. 07 is _when_ a token is judged as spam and
+08 is _how_ that verdict is reached — both are write-path filters that decide what
+reaches state. 09 is the separate read-path filter that decides what a caller gets back
+out of state, and it deletes nothing.
+
+## Regenerating
+
+```bash
+archify deliver --quality showcase
+archify visual-check --json
+```
+
+All nine pass `deliver` at the `showcase` quality profile. All except
+`04-tracking-lifecycle` also pass `visual-check` containment at 1440×900, 1600×1000,
+1920×1080, and 2048×1320 in both themes; the lifecycle renderer's band geometry has a
+fixed vertical budget that makes the page scroll vertically below 1920px wide, which the
+bundled Archify lifecycle example does too.