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 + + + + + + + + + + + +
+ +
+
+
+

@metamask/assets-controller — Component Map

+
+
+ + + + + + + +
+ + @metamask/assets-controller — Component Map + An architecture diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Client + Controllers · extension · mobile · Architecture component + + + + Client + Controllers + extension · mobile + + + + Messenger · actions · events · Architecture component + + + + Messenger + actions · events + + + + Published Events · stateChange · balanceChanged · Architecture component + + + + Published Events + stateChange · balanceChanged + + + + AssetsController · BaseController · controller core · core + + + + AssetsController + BaseController + core + + + + State Commit · assetsInfo · assetsBalance · controller core + + + + State Commit + assetsInfo · assetsBalance + + + + Middleware Pipeline · composed with reduceRight · a throwing stage is caught and skipped · controller core + + + + Middleware Pipeline + composed with reduceRight · a throwing stage is caught and skipped + + + + AccountActivity · push only · never fetched · balance sources · first-pick chain assignment · priority 1 + + + + AccountActivity + push only · never fetched + priority 1 + + + + AccountsApi · poll 30s · timeout 15s · balance sources · first-pick chain assignment · priority 2 + + + + AccountsApi + poll 30s · timeout 15s + priority 2 + + + + Snap · keyring events · balance sources · first-pick chain assignment · priority 3 + + + + Snap + keyring events + priority 3 + + + + Rpc · eip155 only · poll 30s · balance sources · first-pick chain assignment · priority 4 + + + + Rpc + eip155 only · poll 30s + priority 4 + + + + StakedBalance · mainnet + Hoodi · Architecture component · no claims + + + + StakedBalance + mainnet + Hoodi + no claims + + + + Token · batch 50 · timeout 15s · enrichment · one parallel stage + + + + Token + batch 50 · timeout 15s + + + + Price · poll 60s · deduped · enrichment · one parallel stage + + + + Price + poll 60s · deduped + + + + Activity Service · WebSocket · Architecture component + + + + Activity Service + WebSocket + + + + Accounts API · v5 / v6 balances · Architecture component + + + + Accounts API + v5 / v6 balances + + + + Keyring Snaps · Solana · Bitcoin · Architecture component + + + + Keyring Snaps + Solana · Bitcoin + + + + EVM JSON-RPC · Multicall3 · staking vault · Architecture component + + + + EVM JSON-RPC + Multicall3 · staking vault + + + + Tokens + Price API · metadata · spot prices · Architecture component + + + + Tokens + Price API + metadata · spot prices + + + + + + getAssets + + + + action handlers + + + + merge + normalize + + + + on commit + + + + #executeMiddlewares + + + + onAssetsUpdate push + + + + balance · pLimit 3 + + + + background lane + + + + fallback lane + + + + extra source + + + + metadata + + + + price + + + + subscribe + + + + balances + + + + keyring RPC + + + + eth_call + + + + getShares + + + + v3 assets + + + + spot prices + + + + + + controller core + + + + + balance sources · first-pick chain assignment + + + + + enrichment · one parallel stage + + + + + +

+ + + + + + + + + +
+ + +
+
+
+
+

Chain assignment

+
+
    +
  • • 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
  • +
+
+ +
+
+
+

Two ways data arrives

+
+
    +
  • • Pull: getAssets drives the pipeline, then commits and publishes
  • +
  • • Push: a subscribed source calls onAssetsUpdate for a shorter pipeline
  • +
+
+ +
+
+
+

Basic functionality off

+
+
    +
  • • 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 @@ + + @metamask/assets-controller — Component Map + An architecture diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Client + Controllers · extension · mobile · Architecture component + + + + Client + Controllers + extension · mobile + + + + Messenger · actions · events · Architecture component + + + + Messenger + actions · events + + + + Published Events · stateChange · balanceChanged · Architecture component + + + + Published Events + stateChange · balanceChanged + + + + AssetsController · BaseController · controller core · core + + + + AssetsController + BaseController + core + + + + State Commit · assetsInfo · assetsBalance · controller core + + + + State Commit + assetsInfo · assetsBalance + + + + Middleware Pipeline · composed with reduceRight · a throwing stage is caught and skipped · controller core + + + + Middleware Pipeline + composed with reduceRight · a throwing stage is caught and skipped + + + + AccountActivity · push only · never fetched · balance sources · first-pick chain assignment · priority 1 + + + + AccountActivity + push only · never fetched + priority 1 + + + + AccountsApi · poll 30s · timeout 15s · balance sources · first-pick chain assignment · priority 2 + + + + AccountsApi + poll 30s · timeout 15s + priority 2 + + + + Snap · keyring events · balance sources · first-pick chain assignment · priority 3 + + + + Snap + keyring events + priority 3 + + + + Rpc · eip155 only · poll 30s · balance sources · first-pick chain assignment · priority 4 + + + + Rpc + eip155 only · poll 30s + priority 4 + + + + StakedBalance · mainnet + Hoodi · Architecture component · no claims + + + + StakedBalance + mainnet + Hoodi + no claims + + + + Token · batch 50 · timeout 15s · enrichment · one parallel stage + + + + Token + batch 50 · timeout 15s + + + + Price · poll 60s · deduped · enrichment · one parallel stage + + + + Price + poll 60s · deduped + + + + Activity Service · WebSocket · Architecture component + + + + Activity Service + WebSocket + + + + Accounts API · v5 / v6 balances · Architecture component + + + + Accounts API + v5 / v6 balances + + + + Keyring Snaps · Solana · Bitcoin · Architecture component + + + + Keyring Snaps + Solana · Bitcoin + + + + EVM JSON-RPC · Multicall3 · staking vault · Architecture component + + + + EVM JSON-RPC + Multicall3 · staking vault + + + + Tokens + Price API · metadata · spot prices · Architecture component + + + + Tokens + Price API + metadata · spot prices + + + + + + getAssets + + + + action handlers + + + + merge + normalize + + + + on commit + + + + #executeMiddlewares + + + + onAssetsUpdate push + + + + balance · pLimit 3 + + + + background lane + + + + fallback lane + + + + extra source + + + + metadata + + + + price + + + + subscribe + + + + balances + + + + keyring RPC + + + + eth_call + + + + getShares + + + + v3 assets + + + + spot prices + + + + + + controller core + + + + + balance sources · first-pick chain assignment + + + + + enrichment · one parallel stage + + + + + \ 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 + + + + + + + + + + + +
+ +
+
+
+

getAssets — Fast Lane and Background Lane

+
+
+ + + + + + + +
+ + getAssets — Fast Lane and Background Lane + A workflow diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + 01 / Caller + + + 02 / Fast lane · awaited + + + + EX / Background lane · fire and forget + + + 04 / State + + + + + Gather balances + + + Reconcile + + + Enrich and commit + + + + detect then enrich + + same shape, deferred + + + + + + + + + + + + + + + + + + getAssets · forceUpdate: true · Caller › Gather balances · blocking + + + + getAssets + forceUpdate: true + blocking + + + + Read from state · balance · metadata · price · Caller › Enrich and commit + + + + Read from state + balance · metadata · price + + + + Parallel balance · AccountsApi + Staked · Fast lane · awaited › Gather balances · pLimit 3 + + + + Parallel balance + AccountsApi + Staked + pLimit 3 + + + + Graduation · retire custom assets · Fast lane · awaited › Gather balances + + + + Graduation + retire custom assets + + + + RPC fallback · errored + stale-zero · Fast lane · awaited › Reconcile + + + + RPC fallback + errored + stale-zero + + + + Detection · assets with no metadata · Fast lane · awaited › detect then enrich › Reconcile + + + + Detection + assets with no metadata + + + + Parallel enrich · Token + Price · Fast lane · awaited › detect then enrich › Enrich and commit · pLimit 2 + + + + Parallel enrich + Token + Price + pLimit 2 + + + + Leftover chains · errored, or not covered · Background lane · fire and forget › same shape, deferred › Gather balances · after fast commit + + + + Leftover chains + errored, or not covered + after fast commit + + + + Parallel balance · Snap + Rpc · Background lane · fire and forget › same shape, deferred › Gather balances · pLimit 3 + + + + Parallel balance + Snap + Rpc + pLimit 3 + + + + Detection · assets with no metadata · Background lane · fire and forget › same shape, deferred › Reconcile + + + + Detection + assets with no metadata + + + + Parallel enrich · Token + Price · Background lane · fire and forget › same shape, deferred › Enrich and commit + + + + Parallel enrich + Token + Price + + + + #updateState · merge · normalize · publish · State › Enrich and commit + + + + #updateState + merge · normalize · publish + + + + + + + accounts + chainIds + + + + resolve caller + + + + + enriched response + + + + + must precede fallback + + + + + second commit + + + + + + + + Legend + + + Caller + + + + Pipeline stage + + + + Handoff + + + + State + + + +

+ + + + + + + + + +
+ + +
+
+
+
+

Two lanes, one pipeline shape

+
+
    +
  • • 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
  • +
+
+ +
+
+
+

Failure is absorbed, not propagated

+
+
    +
  • • 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 @@ + + getAssets — Fast Lane and Background Lane + A workflow diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + 01 / Caller + + + 02 / Fast lane · awaited + + + + EX / Background lane · fire and forget + + + 04 / State + + + + + Gather balances + + + Reconcile + + + Enrich and commit + + + + detect then enrich + + same shape, deferred + + + + + + + + + + + + + + + + + + getAssets · forceUpdate: true · Caller › Gather balances · blocking + + + + getAssets + forceUpdate: true + blocking + + + + Read from state · balance · metadata · price · Caller › Enrich and commit + + + + Read from state + balance · metadata · price + + + + Parallel balance · AccountsApi + Staked · Fast lane · awaited › Gather balances · pLimit 3 + + + + Parallel balance + AccountsApi + Staked + pLimit 3 + + + + Graduation · retire custom assets · Fast lane · awaited › Gather balances + + + + Graduation + retire custom assets + + + + RPC fallback · errored + stale-zero · Fast lane · awaited › Reconcile + + + + RPC fallback + errored + stale-zero + + + + Detection · assets with no metadata · Fast lane · awaited › detect then enrich › Reconcile + + + + Detection + assets with no metadata + + + + Parallel enrich · Token + Price · Fast lane · awaited › detect then enrich › Enrich and commit · pLimit 2 + + + + Parallel enrich + Token + Price + pLimit 2 + + + + Leftover chains · errored, or not covered · Background lane · fire and forget › same shape, deferred › Gather balances · after fast commit + + + + Leftover chains + errored, or not covered + after fast commit + + + + Parallel balance · Snap + Rpc · Background lane · fire and forget › same shape, deferred › Gather balances · pLimit 3 + + + + Parallel balance + Snap + Rpc + pLimit 3 + + + + Detection · assets with no metadata · Background lane · fire and forget › same shape, deferred › Reconcile + + + + Detection + assets with no metadata + + + + Parallel enrich · Token + Price · Background lane · fire and forget › same shape, deferred › Enrich and commit + + + + Parallel enrich + Token + Price + + + + #updateState · merge · normalize · publish · State › Enrich and commit + + + + #updateState + merge · normalize · publish + + + + + + + accounts + chainIds + + + + resolve caller + + + + + enriched response + + + + + must precede fallback + + + + + second commit + + + + + + + + Legend + + + Caller + + + + Pipeline stage + + + + Handoff + + + + State + + + \ 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 + + + + + + + + + + + +
+ +
+
+
+

Balance Fallback Cascade — One EVM Chain, Every Layer Degrading

+
+
+ + + + + + + +
+ + Balance Fallback Cascade — One EVM Chain, Every Layer Degrading + A sequence diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fetch claimed chains + + + + + + + + multi-account balances + + + + + + + + no response in 15s + + + + + + + + errors[chainId], no throw + + + + + + + + next(), error still set + + + + + + + + errored + stale-zero chains + + + + + + + + aggregate3, batch 300 + + + + + + + + 3 attempts exhausted + + + + + + + + one eth_call per asset + + + + + + + + balances + + + + + + + + balances by chain + + + + + + + + merged, error cleared + + + + + + + Primary attempt + + + + Chain marked failed + + + + RPC fallback cascade + + + + Merge back + + + + + ParallelBalance · partitions chains · Sequence participant + + + + ParallelBalance + partitions chains + + + + AccountsApi · priority 2 · Sequence participant + + + + AccountsApi + priority 2 + + + + Accounts API · HTTPS · Sequence participant + + + + Accounts API + HTTPS + + + + RpcFallback · middleware · Sequence participant + + + + RpcFallback + middleware + + + + RpcDataSource · eip155 only · Sequence participant + + + + RpcDataSource + eip155 only + + + + EVM JSON-RPC · Multicall3 · Sequence participant + + + + EVM JSON-RPC + Multicall3 + + + + + Legend + + + main path + + + + response + + + + failure + + + + degraded + + + + call + + + +

+ + + + + + + + + +
+ + +
+
+
+
+

Every layer degrades once

+
+
    +
  • • 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
  • +
+
+ +
+
+
+

Where the retries actually live

+
+
    +
  • • 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 @@ + + Balance Fallback Cascade — One EVM Chain, Every Layer Degrading + A sequence diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fetch claimed chains + + + + + + + + multi-account balances + + + + + + + + no response in 15s + + + + + + + + errors[chainId], no throw + + + + + + + + next(), error still set + + + + + + + + errored + stale-zero chains + + + + + + + + aggregate3, batch 300 + + + + + + + + 3 attempts exhausted + + + + + + + + one eth_call per asset + + + + + + + + balances + + + + + + + + balances by chain + + + + + + + + merged, error cleared + + + + + + + Primary attempt + + + + Chain marked failed + + + + RPC fallback cascade + + + + Merge back + + + + + ParallelBalance · partitions chains · Sequence participant + + + + ParallelBalance + partitions chains + + + + AccountsApi · priority 2 · Sequence participant + + + + AccountsApi + priority 2 + + + + Accounts API · HTTPS · Sequence participant + + + + Accounts API + HTTPS + + + + RpcFallback · middleware · Sequence participant + + + + RpcFallback + middleware + + + + RpcDataSource · eip155 only · Sequence participant + + + + RpcDataSource + eip155 only + + + + EVM JSON-RPC · Multicall3 · Sequence participant + + + + EVM JSON-RPC + Multicall3 + + + + + Legend + + + main path + + + + response + + + + failure + + + + degraded + + + + call + + + \ 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 + + + + + + + + + + + +
+ +
+
+
+

AssetsController — Tracking Lifecycle

+
+
+ + + + + + + +
+ + AssetsController — Tracking Lifecycle + A lifecycle diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + 01 / Activation path + + 02 / Pauses and re-scope + + 03 / Terminal exit + + + + + + + + + + + + + + + + + + Constructed · no network I/O · Activation path · ctor + + + + 01 + Constructed + no network I/O + ctor + + + + Signals · unlock · UI · tree · Activation path + + + + 02 + Signals + unlock · UI · tree + + + + Gate Check · all three true? · Activation path + + + + 03 + Gate Check + all three true? + + + + Refresh · forced getAssets · Activation path + + + + 04 + Refresh + forced getAssets + + + + Tracking · subscribed · live · Activation path · steady + + + + 05 + Tracking + subscribed · live + steady + + + + Dormant · subscriptions cleared · Pauses and re-scope · stop + + + + Dormant + subscriptions cleared + stop + + + + Re-scope · accounts or chains · Pauses and re-scope + + + + Re-scope + accounts or chains + + + + Destroyed · handlers released · Terminal exit + + + + Destroyed + handlers released + + + + + + + + + + any gate false + locked or UI closed + + + + wait for next signal + + + + scope changed + + + + re-fetch, re-subscribe + + + + destroy() + callable from any phase + + + + + Legend + + + Constructor + + + + Work in flight + + + + Paused + + + + Activation gate + + + + Steady state + + + + Terminal + + + + Waiting on messenger + + + +

+ + + + + + + + + +
+ + +
+
+
+
+

Gate — all three, or nothing

+
+
    +
  • • 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
  • +
+
+ +
+
+
+

Step 04 — refresh before subscribe

+
+
    +
  • • 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
  • +
+
+ +
+
+
+

Re-scope, pause, destroy

+
+
    +
  • • 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 @@ + + AssetsController — Tracking Lifecycle + A lifecycle diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + 01 / Activation path + + 02 / Pauses and re-scope + + 03 / Terminal exit + + + + + + + + + + + + + + + + + + Constructed · no network I/O · Activation path · ctor + + + + 01 + Constructed + no network I/O + ctor + + + + Signals · unlock · UI · tree · Activation path + + + + 02 + Signals + unlock · UI · tree + + + + Gate Check · all three true? · Activation path + + + + 03 + Gate Check + all three true? + + + + Refresh · forced getAssets · Activation path + + + + 04 + Refresh + forced getAssets + + + + Tracking · subscribed · live · Activation path · steady + + + + 05 + Tracking + subscribed · live + steady + + + + Dormant · subscriptions cleared · Pauses and re-scope · stop + + + + Dormant + subscriptions cleared + stop + + + + Re-scope · accounts or chains · Pauses and re-scope + + + + Re-scope + accounts or chains + + + + Destroyed · handlers released · Terminal exit + + + + Destroyed + handlers released + + + + + + + + + + any gate false + locked or UI closed + + + + wait for next signal + + + + scope changed + + + + re-fetch, re-subscribe + + + + destroy() + callable from any phase + + + + + Legend + + + Constructor + + + + Work in flight + + + + Paused + + + + Activation gate + + + + Steady state + + + + Terminal + + + + Waiting on messenger + + + \ 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 + + + + + + + + + + + +
+ +
+
+
+

EVM RPC Read Path — State to Decoded Balances

+
+
+ + + + + + + +
+ + EVM RPC Read Path — State to Decoded Balances + A data-flow diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + 01 / Scope + + + 02 / Plan + + + 03 / Call + + + 04 / Decode + + + 05 / Commit + + + + + + + + + + + + + + + + + + + Tracked Assets · assetsBalance · 01 / Scope + + + + Tracked Assets + assetsBalance + + + + Custom Assets · RPC is sole source · 01 / Scope + + + + Custom Assets + RPC is sole source + + + + Staking Chains · known contracts · 01 / Scope + + + + Staking Chains + known contracts + + + + Balance Requests · deduped by address · 02 / Plan · per account + + + + Balance Requests + deduped by address + per account + + + + Staking Reads · one per account · 02 / Plan + + + + Staking Reads + one per account + + + + Multicall3 · 300 calls per batch · 03 / Call · aggregate3 + + + + Multicall3 + 300 calls per batch + aggregate3 + + + + Per-asset Calls · one eth_call each · 03 / Call + + + + Per-asset Calls + one eth_call each + + + + Staking Contract · two ordered calls · 03 / Call + + + + Staking Contract + two ordered calls + + + + Decode · uint256 to string · 04 / Decode + + + + Decode + uint256 to string + + + + Failed Addresses · no value written · 04 / Decode + + + + Failed Addresses + no value written + + + + Staked Balance · synthetic asset id · 04 / Decode + + + + Staked Balance + synthetic asset id + + + + assetsBalance · committed · 05 / Commit · commit + + + + assetsBalance + committed + commit + + + + + + tracked ids + + + + custom ids + + + + eligible chains + + + + balanceOf + batched + + + + no Multicall3 + opt-in + + + + 3 tries, then drop + + + + returnData + success flag + + + + one result each + + + + success = false + + + + getShares + + + + convertToAssets + + + + balances + per asset + + + + staked balance + + + + + Legend + + + Primary read path + + + + Dropped result + + + + Degraded to single calls + + + + Controller state + + + + Supporting read + + + +

+ + + + + + + + + +
+ + +
+
+
+
+

Scope comes from state, not discovery

+
+
    +
  • • 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
  • +
+
+ +
+
+
+

One call, then two ways down

+
+
    +
  • • 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
  • +
+
+ +
+
+
+

Failures stay out of state

+
+
    +
  • • 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 @@ + + EVM RPC Read Path — State to Decoded Balances + A data-flow diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + 01 / Scope + + + 02 / Plan + + + 03 / Call + + + 04 / Decode + + + 05 / Commit + + + + + + + + + + + + + + + + + + + Tracked Assets · assetsBalance · 01 / Scope + + + + Tracked Assets + assetsBalance + + + + Custom Assets · RPC is sole source · 01 / Scope + + + + Custom Assets + RPC is sole source + + + + Staking Chains · known contracts · 01 / Scope + + + + Staking Chains + known contracts + + + + Balance Requests · deduped by address · 02 / Plan · per account + + + + Balance Requests + deduped by address + per account + + + + Staking Reads · one per account · 02 / Plan + + + + Staking Reads + one per account + + + + Multicall3 · 300 calls per batch · 03 / Call · aggregate3 + + + + Multicall3 + 300 calls per batch + aggregate3 + + + + Per-asset Calls · one eth_call each · 03 / Call + + + + Per-asset Calls + one eth_call each + + + + Staking Contract · two ordered calls · 03 / Call + + + + Staking Contract + two ordered calls + + + + Decode · uint256 to string · 04 / Decode + + + + Decode + uint256 to string + + + + Failed Addresses · no value written · 04 / Decode + + + + Failed Addresses + no value written + + + + Staked Balance · synthetic asset id · 04 / Decode + + + + Staked Balance + synthetic asset id + + + + assetsBalance · committed · 05 / Commit · commit + + + + assetsBalance + committed + commit + + + + + + tracked ids + + + + custom ids + + + + eligible chains + + + + balanceOf + batched + + + + no Multicall3 + opt-in + + + + 3 tries, then drop + + + + returnData + success flag + + + + one result each + + + + success = false + + + + getShares + + + + convertToAssets + + + + balances + per asset + + + + staked balance + + + + + Legend + + + Primary read path + + + + Dropped result + + + + Degraded to single calls + + + + Controller state + + + + Supporting read + + + \ 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 + + + + + + + + + + + +
+ +
+
+
+

handleAssetsUpdate — One Stack Composed Per Source

+
+
+ + + + + + + +
+ + handleAssetsUpdate — One Stack Composed Per Source + A workflow diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + + EX / Websocket only + + + 02 / AccountsApi and Activity + + + 03 / Every source + + + + + Source-specific stages + + + Shared tail + + + Commit + + + + + + + + + + + + + + + + + + Occurrence Floor · drop below-floor tokens · Websocket only › Source-specific stages + + + + Occurrence Floor + drop below-floor tokens + + + + Graduating sources · AccountsApi · Activity · AccountsApi and Activity › Source-specific stages · full prefix + + + + Graduating sources + AccountsApi · Activity + full prefix + + + + Graduation · retire custom assets · AccountsApi and Activity › Source-specific stages + + + + Graduation + retire custom assets + + + + RPC Fallback · errored + stale-zero · AccountsApi and Activity › Source-specific stages + + + + RPC Fallback + errored + stale-zero + + + + Other sources · Snap · Rpc · Price · Staked · Every source › Source-specific stages · no prefix + + + + Other sources + Snap · Rpc · Price · Staked + no prefix + + + + Detection · assets with no metadata · Every source › Shared tail · always + + + + Detection + assets with no metadata + always + + + + Token ∥ Price · basic functionality on · Every source › Shared tail + + + + Token ∥ Price + basic functionality on + + + + #updateState · merge · normalize · publish · Every source › Commit + + + + #updateState + merge · normalize · publish + + + + + + + + + + detection only + + + + graduate first + + + + RPC-only mode + + + + http poll + + + + websocket push + + + + + Legend + + + Middleware stage + + + + Update handoff + + + + State + + + +

+ + + + + + + + + +
+ + +
+
+
+
+

Order is a correctness constraint

+
+
    +
  • • 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
  • +
+
+ +
+
+
+

Basic functionality gates two stages

+
+
    +
  • • 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
  • +
+
+ +
+
+
+

Only AccountsApi triggers fallback here

+
+
    +
  • • 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 @@ + + handleAssetsUpdate — One Stack Composed Per Source + A workflow diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + + EX / Websocket only + + + 02 / AccountsApi and Activity + + + 03 / Every source + + + + + Source-specific stages + + + Shared tail + + + Commit + + + + + + + + + + + + + + + + + + Occurrence Floor · drop below-floor tokens · Websocket only › Source-specific stages + + + + Occurrence Floor + drop below-floor tokens + + + + Graduating sources · AccountsApi · Activity · AccountsApi and Activity › Source-specific stages · full prefix + + + + Graduating sources + AccountsApi · Activity + full prefix + + + + Graduation · retire custom assets · AccountsApi and Activity › Source-specific stages + + + + Graduation + retire custom assets + + + + RPC Fallback · errored + stale-zero · AccountsApi and Activity › Source-specific stages + + + + RPC Fallback + errored + stale-zero + + + + Other sources · Snap · Rpc · Price · Staked · Every source › Source-specific stages · no prefix + + + + Other sources + Snap · Rpc · Price · Staked + no prefix + + + + Detection · assets with no metadata · Every source › Shared tail · always + + + + Detection + assets with no metadata + always + + + + Token ∥ Price · basic functionality on · Every source › Shared tail + + + + Token ∥ Price + basic functionality on + + + + #updateState · merge · normalize · publish · Every source › Commit + + + + #updateState + merge · normalize · publish + + + + + + + + + + detection only + + + + graduate first + + + + RPC-only mode + + + + http poll + + + + websocket push + + + + + Legend + + + Middleware stage + + + + Update handoff + + + + State + + + \ 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 + + + + + + + + + + + +
+ +
+
+
+

Where A Spam Token Gets Filtered Out

+
+
+ + + + + + + +
+ + Where A Spam Token Gets Filtered Out + A workflow diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + 01 / On a websocket push + + + 02 / In the enrichment stage + + + + EX / On keyring unlock + + + + + What arrives + + + What gets judged + + + What is removed + + + + + + + + + + + + + + + + + + + + + + + AccountActivity · balance push · On a websocket push › What arrives + + + + AccountActivity + balance push + + + + Basic functionality · off means no filter · On a websocket push › What arrives + + + + Basic functionality + off means no filter + + + + New ERC-20s · unknown to state · On a websocket push › What gets judged + + + + New ERC-20s + unknown to state + + + + Occurrence floor · fail open · On a websocket push › What gets judged + + + + Occurrence floor + fail open + + + + Cut pre-detection · never persisted · On a websocket push › What is removed + + + + Cut pre-detection + never persisted + + + + Metadata wanted · both pipelines · In the enrichment stage › What arrives + + + + Metadata wanted + both pipelines + + + + Detected only · heals are exempt · In the enrichment stage › What gets judged + + + + Detected only + heals are exempt + + + + Floor or Blockaid · fail open · In the enrichment stage › What gets judged + + + + Floor or Blockaid + fail open + + + + Cut from response · stub metadata too · In the enrichment stage › What is removed + + + + Cut from response + stub metadata too + + + + Assets state · what the UI reads · In the enrichment stage › What is removed + + + + Assets state + what the UI reads + + + + Keyring unlock · before tracking · On keyring unlock › What arrives + + + + Keyring unlock + before tracking + + + + Three gates · all must pass · On keyring unlock › What arrives + + + + Three gates + all must pass + + + + Saved ERC-20s · Accounts API chains · On keyring unlock › What gets judged + + + + Saved ERC-20s + Accounts API chains + + + + Occurrence floor · no delete on error · On keyring unlock › What gets judged + + + + Occurrence floor + no delete on error + + + + Cut from state · info, balance, price · On keyring unlock › What is removed + + + + Cut from state + info, balance, price + + + + + + detected assets + + + + candidates + + + + spam + + + + survivors + + + + unlocked + + + + all pass + + + + batches of 50 + + + + below floor + + + + rewritten + + + + new balance + + + + on + + + + candidates + + + + below floor + + + + survivors + + + + + Legend + + + In-process step + + + + Trigger + + + + State + + + + Token API call + + + +

+ + + + + + + + + +
+ + +
+
+
+
+

What a token has to clear

+
+
    +
  • • 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
  • +
+
+ +
+
+
+

The two failure modes differ on purpose

+
+
    +
  • • 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
  • +
+
+ +
+
+
+

Why a third pass exists

+
+
    +
  • • 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 @@ + + Where A Spam Token Gets Filtered Out + A workflow diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + 01 / On a websocket push + + + 02 / In the enrichment stage + + + + EX / On keyring unlock + + + + + What arrives + + + What gets judged + + + What is removed + + + + + + + + + + + + + + + + + + + + + + + AccountActivity · balance push · On a websocket push › What arrives + + + + AccountActivity + balance push + + + + Basic functionality · off means no filter · On a websocket push › What arrives + + + + Basic functionality + off means no filter + + + + New ERC-20s · unknown to state · On a websocket push › What gets judged + + + + New ERC-20s + unknown to state + + + + Occurrence floor · fail open · On a websocket push › What gets judged + + + + Occurrence floor + fail open + + + + Cut pre-detection · never persisted · On a websocket push › What is removed + + + + Cut pre-detection + never persisted + + + + Metadata wanted · both pipelines · In the enrichment stage › What arrives + + + + Metadata wanted + both pipelines + + + + Detected only · heals are exempt · In the enrichment stage › What gets judged + + + + Detected only + heals are exempt + + + + Floor or Blockaid · fail open · In the enrichment stage › What gets judged + + + + Floor or Blockaid + fail open + + + + Cut from response · stub metadata too · In the enrichment stage › What is removed + + + + Cut from response + stub metadata too + + + + Assets state · what the UI reads · In the enrichment stage › What is removed + + + + Assets state + what the UI reads + + + + Keyring unlock · before tracking · On keyring unlock › What arrives + + + + Keyring unlock + before tracking + + + + Three gates · all must pass · On keyring unlock › What arrives + + + + Three gates + all must pass + + + + Saved ERC-20s · Accounts API chains · On keyring unlock › What gets judged + + + + Saved ERC-20s + Accounts API chains + + + + Occurrence floor · no delete on error · On keyring unlock › What gets judged + + + + Occurrence floor + no delete on error + + + + Cut from state · info, balance, price · On keyring unlock › What is removed + + + + Cut from state + info, balance, price + + + + + + detected assets + + + + candidates + + + + spam + + + + survivors + + + + unlocked + + + + all pass + + + + batches of 50 + + + + below floor + + + + rewritten + + + + new balance + + + + on + + + + candidates + + + + below floor + + + + survivors + + + + + Legend + + + In-process step + + + + Trigger + + + + State + + + + Token API call + + + \ 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 + + + + + + + + + + + +
+ +
+
+
+

Spam Verdict — How One Token Is Judged

+
+
+ + + + + + + +
+ + Spam Verdict — How One Token Is Judged + A data-flow diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + 01 / Candidate + + + 02 / Exemptions + + + 03 / Rule + + + 04 / Verdict + + + 05 / Effect + + + + + + + + + + + + + + One asset id · in a response or state · 01 / Candidate + + + + One asset id + in a response or state + + + + Exemption check · four ways to skip · 02 / Exemptions + + + + Exemption check + four ways to skip + + + + Occurrence floor · Token API v3 count · 03 / Rule + + + + Occurrence floor + Token API v3 count + + + + Blockaid scan · bulkScanTokens · 03 / Rule + + + + Blockaid scan + bulkScanTokens + + + + Verdict · keep or drop · 04 / Verdict + + + + Verdict + keep or drop + + + + Kept · metadata persisted · 05 / Effect + + + + Kept + metadata persisted + + + + Dropped · balance, info, detected · 05 / Effect + + + + Dropped + balance, info, detected + + + + Kept unjudged · no scan can remove it · 05 / Effect + + + + Kept unjudged + no scan can remove it + + + + + + asset id + + + + eip155 erc20 + + + + token namespace + + + + exempt, never scanned + + + + count vs floor + + + + clean or flagged + + + + passed + + + + failed + + + + + Legend + + + EVM decision path + + + + Removal + + + + Survives + + + + Non-EVM path + + + +

+ + + + + + + + + +
+ + +
+
+
+
+

What is never scanned

+
+
    +
  • • 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
  • +
+
+ +
+
+
+

Where the floor comes from

+
+
    +
  • • 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
  • +
+
+ +
+
+
+

A drop is more than a balance delete

+
+
    +
  • • 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 @@ + + Spam Verdict — How One Token Is Judged + A data-flow diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + 01 / Candidate + + + 02 / Exemptions + + + 03 / Rule + + + 04 / Verdict + + + 05 / Effect + + + + + + + + + + + + + + One asset id · in a response or state · 01 / Candidate + + + + One asset id + in a response or state + + + + Exemption check · four ways to skip · 02 / Exemptions + + + + Exemption check + four ways to skip + + + + Occurrence floor · Token API v3 count · 03 / Rule + + + + Occurrence floor + Token API v3 count + + + + Blockaid scan · bulkScanTokens · 03 / Rule + + + + Blockaid scan + bulkScanTokens + + + + Verdict · keep or drop · 04 / Verdict + + + + Verdict + keep or drop + + + + Kept · metadata persisted · 05 / Effect + + + + Kept + metadata persisted + + + + Dropped · balance, info, detected · 05 / Effect + + + + Dropped + balance, info, detected + + + + Kept unjudged · no scan can remove it · 05 / Effect + + + + Kept unjudged + no scan can remove it + + + + + + asset id + + + + eip155 erc20 + + + + token namespace + + + + exempt, never scanned + + + + count vs floor + + + + clean or flagged + + + + passed + + + + failed + + + + + Legend + + + EVM decision path + + + + Removal + + + + Survives + + + + Non-EVM path + + + \ 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 + + + + + + + + + + + +
+ +
+
+
+

Read Path — Why A Stored Token Is Not Shown

+
+
+ + + + + + + +
+ + Read Path — Why A Stored Token Is Not Shown + A workflow diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + 01 / Returned by a read + + + + EX / Kept in state, not returned + + + + + What is stored + + + Four gates, every read + + + What the caller sees + + + + + + + + + + + + + + + + + + Asset in state · survived every filter · Returned by a read › What is stored + + + + Asset in state + survived every filter + + + + Has a balance · assetsBalance · Returned by a read › Four gates, every read + + + + Has a balance + assetsBalance + + + + Has metadata · assetsInfo · Returned by a read › Four gates, every read + + + + Has metadata + assetsInfo + + + + Not hidden · assetPreferences · Returned by a read › Four gates, every read + + + + Not hidden + assetPreferences + + + + Chain in scope · enabled, has native · Returned by a read › Four gates, every read + + + + Chain in scope + enabled, has native + + + + Rendered · balance and fiat · Returned by a read › What the caller sees + + + + Rendered + balance and fiat + + + + Not renderable · waiting on data · Kept in state, not returned › Four gates, every read + + + + Not renderable + waiting on data + + + + Withheld · still tracked and fetched · Kept in state, not returned › What the caller sees + + + + Withheld + still tracked and fetched + + + + + + no balance + + + + no metadata + + + + user hid it + + + + out of scope + + + + per asset + + + + yes + + + + yes + + + + yes + + + + yes + + + + + Legend + + + Returned to the caller + + + + Read gate + + + + Not returned + + + + State + + + +

+ + + + + + + + + +
+ + +
+
+
+
+

A read filter is not a spam filter

+
+
    +
  • • 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
  • +
+
+ +
+
+
+

What each gate actually reads

+
+
    +
  • • 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
  • +
+
+ +
+
+
+

Callers can narrow it further

+
+
    +
  • • 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 @@ + + Read Path — Why A Stored Token Is Not Shown + A workflow diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + 01 / Returned by a read + + + + EX / Kept in state, not returned + + + + + What is stored + + + Four gates, every read + + + What the caller sees + + + + + + + + + + + + + + + + + + Asset in state · survived every filter · Returned by a read › What is stored + + + + Asset in state + survived every filter + + + + Has a balance · assetsBalance · Returned by a read › Four gates, every read + + + + Has a balance + assetsBalance + + + + Has metadata · assetsInfo · Returned by a read › Four gates, every read + + + + Has metadata + assetsInfo + + + + Not hidden · assetPreferences · Returned by a read › Four gates, every read + + + + Not hidden + assetPreferences + + + + Chain in scope · enabled, has native · Returned by a read › Four gates, every read + + + + Chain in scope + enabled, has native + + + + Rendered · balance and fiat · Returned by a read › What the caller sees + + + + Rendered + balance and fiat + + + + Not renderable · waiting on data · Kept in state, not returned › Four gates, every read + + + + Not renderable + waiting on data + + + + Withheld · still tracked and fetched · Kept in state, not returned › What the caller sees + + + + Withheld + still tracked and fetched + + + + + + no balance + + + + no metadata + + + + user hid it + + + + out of scope + + + + per asset + + + + yes + + + + yes + + + + yes + + + + yes + + + + + Legend + + + Returned to the caller + + + + Read gate + + + + Not returned + + + + State + + + \ 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.