Per-chain ledger view inside Pearl Wallet. Source: the proposed pearl-transactions subgraph (PR #129, currently plan-only). Mirrors the rewards-history stack: Service (GraphQL) → Hook (React Query) → Component.
Linear: VLOP-73.
All Pearl agent types on Gnosis, Polygon, Optimism, Base (Mode is deprecated, not deployed). Per @Tanya-atatakai's review, the subgraph drops the agentId allowlist and indexes all services from ServiceRegistryL2; consumers filter by Pearl agent IDs at query time. So PredictTrader, Polystrat, Modius (after deprecation-revisit), Optimus, AgentsFun, PettAi all get history on the chains they run on.
Acceptance criteria from VLOP-73:
- Pearl wallet shows Master Safe balance + history per chain
- Pre-Safe wallet (no Master Safe yet on selected chain) shows EOA only, no history tab
- First post-Safe entry is "Setup complete"
- Confirmed txs only; chronological; no filtering; token amount as primary value
- Subgraph-lag indicator when indexing is behind
frontend/components/PearlWallet/ is step-based (PearlWallet.tsx:47-67). The PEARL_WALLET_SCREEN step renders BalancesAndAssets.tsx. Plan:
- Convert
BalancesAndAssetscontent into a 2-tab layout: Balances | History. - Tabs only render when
masterSafeAddressis defined for the selected chain (PearlWalletProvider.tsx:191-194already derives this). When null → render existing EOA-only balances view, no tabs, no History tab. - Chain switching stays on the existing
Segmentedcontrol (BalancesAndAssets.tsx:140-159). The History tab re-keys its query onwalletChainId. - For a chain with no Pearl service yet (e.g. user has only run PredictTrader, then switches to Base before deploying AgentsFun) — empty-state copy "No transactions yet."
pearl-transactions subgraph (Gnosis | Polygon | Optimism | Base)
└── TransactionHistoryService (graphql-request, gql query + Zod parse)
└── useTransactionHistory(chainId, masterSafeAddress)
├── useTransactionHistoryByMonth (group + sort transformer)
└── useSubgraphLag (queries _meta, returns stale flag)
└── <HistoryTab /> in BalancesAndAssets
Live deployments serve two incompatible schema revisions (2026-07): Gnosis/Optimism proxies pin subgraph v0.0.6 (v1), Base pins v0.0.7 (v2) — its indexers no longer serve v0.0.6. Differences (verified against the live Base deployment, not the subgraph README):
| v1 (v0.0.6) | v2 (v0.0.7) | |
|---|---|---|
| Bond rows | fundsMovements with bondType |
separate bondMovements ledger (carries bondType) |
| OLAS reward sweeps | AGENT_TO_MASTER, hidden client-side (isOlasAgentToMaster) |
pre-split AGENT_OLAS_TO_MASTER, excluded by the query's category filter |
Service.id |
numeric string | registry Bytes ("0x7802"); numeric id in new serviceId field |
Mechanism: TRANSACTION_HISTORY_SUBGRAPH_SCHEMA_BY_EVM_CHAIN (frontend/constants/urls.ts) maps chain → revision (absent = v1). Both services pick the matching query document, Zod-parse with the matching schema, and — for v2 — normalize to the v1-shaped domain types via normalize*V2 in frontend/utils/transactionHistory.ts (bond rows merged into fundsMovements, sweep rows dropped, service.id mapped from serviceId). Hooks and components are revision-agnostic. Migrating a chain to v0.0.7 = flip its map entry.
| File | Purpose |
|---|---|
frontend/types/TransactionHistory.ts |
Zod schemas: FundsMovement, AgentFundingEvent, FundsCategory enum, Meta. Mirror Autonolas.ts |
frontend/service/TransactionHistory.ts |
graphql-request queries (getTransactionHistory, getMeta). Mirror service/FundRecovery.ts |
frontend/hooks/useTransactionHistory.ts |
useQuery keyed ['transactionHistory', chainId, masterSafe]; transformer to categorized + month-grouped rows |
frontend/hooks/useSubgraphLag.ts |
Lag detector: latestL1Block - _meta.block.number > LAG_THRESHOLD_BLOCKS |
frontend/components/PearlWallet/History/ |
HistoryTab.tsx, HistoryRow.tsx, MonthGroup.tsx, StaleIndicator.tsx, EmptyState.tsx |
| File | Change |
|---|---|
frontend/constants/urls.ts |
Add TRANSACTION_HISTORY_SUBGRAPH_URLS_BY_EVM_CHAIN for Gnosis + Polygon + Optimism + Base |
frontend/constants/reactQueryKeys.ts |
Add TRANSACTION_HISTORY_KEY, SUBGRAPH_LAG_KEY |
frontend/constants/intervals.ts |
Add TRANSACTION_HISTORY_REFETCH_INTERVAL (15s candidate) |
frontend/components/PearlWallet/Withdraw/BalancesAndAssets/BalancesAndAssets.tsx |
Wrap content in tabs when masterSafeAddress truthy |
frontend/components/PearlWallet/types.ts |
No change (steps unchanged) |
VLOP-73's 7 displayed entry types map to subgraph categories/entities:
| UI label | Subgraph source |
|---|---|
| "Setup complete" | FundsMovement.category = SAFE_DEPLOYED (emitted on first sighting of a Master Safe — see open Q on pre-stake) |
| "Deposit" | FundsMovement.category = MASTER_FUNDING_IN (inbound from non-masterEoa addresses) |
| "Funded <agent>: <amount> <token>" | AgentFundingEvent entity (one per txHash, lists constituent FundsMovements — collapses multi-token + AgentSafe/AgentEOA in one tx server-side) |
| "Withdrawal from <agent>" | FundsMovement.category = AGENT_TO_MASTER |
| "Withdrawal to external wallet" | FundsMovement.category = MASTER_WITHDRAWAL |
| "OLAS staking" | Still open — see Q3. Best-case: STAKING_DEPOSIT synthesized at ServiceStaked using minStakingDeposit × numAgentInstances |
| "OLAS unstaking" | FundsMovement.category = UNSTAKE_REWARD (Phase 1) |
| (excluded from UI) | STAKING_REWARD_CLAIM, SERVICE_EVICTED |
The "Setup complete" anchor uses Tanya's proposed SAFE_SETUP_TRANSFER category (first inbound from masterEoa) IF the subgraph adopts historical-startBlock template spawning per our remaining open ask. Otherwise the row anchors on SAFE_DEPLOYED (which only marks Safe sighting, no amount).
Frontend filter on source = RAW_TRANSFER for funding rows to avoid the semantic/raw double-count on STAKING_REWARD_CLAIM rows that Phase 2a reconciles.
query GetTransactionHistory($masterSafe: Bytes!, $first: Int!, $skip: Int!) {
masterSafe(id: $masterSafe) {
id
masterEoa
owners
threshold
}
fundsMovements(
where: {
masterSafe: $masterSafe
category_in: [
SAFE_DEPLOYED, SAFE_SETUP_TRANSFER, MASTER_FUNDING_IN,
AGENT_TO_MASTER, MASTER_WITHDRAWAL, UNSTAKE_REWARD
]
}
orderBy: blockTimestamp
orderDirection: desc
first: $first
skip: $skip
) {
id
category
source
token
amount
from
to
blockTimestamp
transactionHash
}
agentFundingEvents(
where: { masterSafe: $masterSafe }
orderBy: blockTimestamp
orderDirection: desc
first: $first
skip: $skip
) {
id
blockTimestamp
transactionHash
agentSafe { id service { id agentIds } }
transfers { token amount from to }
}
_meta { block { number timestamp } hasIndexingErrors }
}Two lists merged client-side into one chronological stream. Pagination: cursor by blockTimestamp_lt rather than skip once history grows. v1 uses first: 100, skip: 0 — adequate per "history scope: from Pearl install onwards".
AgentFundingEvent.agentSafe.service.id (subgraph) ↔ chain_data.token (frontend Service type, frontend/types/Service.ts:59) ↔ serviceConfigId. Lookup agentType from ServicesProvider by matching chain_data.multisig === agentSafe.id. Resolves to a display name via AGENT_CONFIG[agentType].displayName.
useSubgraphLag returns { isStale: boolean, lagBlocks: number }. Polls _meta every 30s. isStale when lagBlocks > 50 (Gnosis ≈ 4 min, Polygon ≈ 2 min). Renders inline banner above the list: "Refresh in progress…". Reuses no existing pattern — none exists today.
Reuse existing masterSafeAddress null check (PearlWalletProvider.tsx:191-194). When null:
- Tabs not rendered (only Balances content shown)
useTransactionHistorynot called (gated byenabled: !!masterSafeAddress)
VLOP-73: "Primary value: token amount only" (no USD). Use existing TokenAmount formatting from @/components/ui if present, else formatUnits(amount, decimals) with token lookup via TOKEN_CONFIG[chainId][symbol] (frontend/config/tokens.ts). Token symbol resolved by matching FundsMovement.token address against TOKEN_CONFIG[chainId].
For native transfers (Phase 2a Safe.SafeReceived / ExecutionSuccess) the subgraph's token field will be null or zero address — render as chain-native (xDAI / POL).
Per frontend/tests/TEST_PLAN.md conventions:
service/TransactionHistory.test.ts— mockgraphql-request, assert query/variables, Zod parse coveragehooks/useTransactionHistory.test.ts— categorization, month grouping, sort order, empty statehooks/useSubgraphLag.test.ts— stale threshold transitionscomponents/PearlWallet/History/HistoryTab.test.tsx— render states (loading, empty, populated, stale, pre-Safe), tab gating, chain switch refetch- Add to
tests/helpers/factories.ts:makeFundsMovement,makeMetaResponse— all hex via existing address factories
- Phase 1 — types + service + hook + subgraph URLs. No UI. Tests.
- Phase 2 —
HistoryTab+ tab wrapping insideBalancesAndAssets. Tests. - Phase 3 — lag indicator + empty/error states polish. Tests.
- Phase 4 — multi-instance support (post-VLOP-73 if
lastSelectedServiceConfigIdlands first; seedocs/features/multi-instance-agents.md).
Each phase = separate PR, with /review-implementation between.
- Blocker: pearl-transactions subgraph PR #129 must merge AND Studio deployments must exist for Gnosis, Polygon, Optimism, Base before any frontend code can integrate. Plan PR has no code yet.
- Subgraph review state: Tanya's review (drop agent-ID gate, derive
masterEoa, addSAFE_DEPLOYED/SAFE_SETUP_TRANSFER,AgentFundingEvent, networks extension, Phase 2b product framing) is the source of truth this doc tracks. - Our remaining asks in issuecomment-4533199835: pre-stake transfer capture, OLAS staking bond visibility. Open questions 1 & 2 below mirror these.
Reflects Tanya's accepted changes: no agentId gate; Master EOA derived via getOwners(); AgentFundingEvent aggregation; SAFE_DEPLOYED / SAFE_SETUP_TRANSFER categories. Red = the two remaining open asks.
flowchart LR
subgraph OFF["Off-chain (Pearl)"]
U1["1\. Install Pearl<br/>Master EOA created"]
U2["2\. Deploy Master Safe"]
U3["3\. Fund Master Safe<br/>30 xDAI"]
U4["4\. Create service<br/>+ bond + stake"]
U5["5\. Pearl funds Agent Safe + Agent EOA<br/>1 xDAI + 0.2 xDAI (one tx)"]
U6["6\. Daily reward"]
U7["7\. Reward sweep<br/>Agent→Master"]
U8["8\. External withdrawal"]
U9["9\. Unstake"]
end
subgraph CHAIN["On-chain events"]
E1[/"RegisterInstance<br/>(any agentId)"/]
E2[/"CreateMultisigWithAgents<br/>multisig=AgentSafe"/]
E3[/"ERC721.Transfer<br/>mint → MasterSafe"/]
E4[/"ERC721.Transfer<br/>MasterSafe → StakingProxy"/]
E5[/"ServiceStaked<br/>owner=MasterSafe<br/>multisig=AgentSafe"/]
ECALL[/"eth_call<br/>GnosisSafe.getOwners()"/]
E6[/"Safe.SafeReceived /<br/>ExecutionSuccess (MultiSend)"/]
E7[/"OLAS.Transfer<br/>StakingProxy → AgentSafe"/]
E8[/"OLAS.Transfer<br/>AgentSafe → MasterSafe"/]
E9[/"OLAS.Transfer<br/>MasterSafe → external"/]
E10[/"ServiceUnstaked<br/>reward=3 OLAS"/]
E11[/"RewardClaimed<br/>2 OLAS daily"/]
end
subgraph HND["Subgraph handlers"]
H1["handleRegisterInstance"]
H2["handleCreateMultisigWithAgents"]
H3["handleServiceNftTransfer"]
H4["handleServiceStaked<br/>+ getOwners() call"]
H5["handleSafeNative<br/>(Phase 2a template)"]
H6["handleOlasTransfer<br/>+ classifyTransfer<br/>+ AgentFundingEvent aggregator"]
H7["handleRewardClaimed"]
H8["handleServiceUnstaked"]
end
subgraph ENT["Entities written"]
N1["PendingRegistration"]
N2["Service + AgentSafe<br/>+ ServiceIndex"]
N3["Service.nftCustodian<br/>+ ServiceNftCustodyChange"]
N4["MasterSafe{masterEoa, owners, threshold}<br/>+ TrackedSafe(MASTER/AGENT/AGENT_EOA)<br/>+ FundsMovement(SAFE_DEPLOYED)<br/>+ spawn Safe templates"]
N5["AgentFundingEvent(txHash)<br/>+ child FundsMovements"]
N6["FundsMovement<br/>STAKING_REWARD_CLAIM<br/>+ DailyServiceFunds"]
N7["FundsMovement<br/>AGENT_TO_MASTER"]
N8["FundsMovement<br/>MASTER_WITHDRAWAL"]
N9["FundsMovement<br/>UNSTAKE_REWARD"]
N10["FundsMovement<br/>SAFE_SETUP_TRANSFER (first inbound from masterEoa)"]
end
GAP_A["OPEN ASK 1<br/>Pre-stake transfer to Master Safe<br/>→ Safe template not yet spawned<br/>→ SAFE_SETUP_TRANSFER may not fire<br/>(needs createWithContext historical startBlock)"]
GAP_B["OPEN ASK 2<br/>OLAS bond → ServiceRegistryTokenUtility<br/>not indexed<br/>→ no STAKING_DEPOSIT row"]
U1 -.no event.-> CHAIN
U2 -.no event.-> CHAIN
U3 --> GAP_A
U4 --> E1 --> H1 --> N1
U4 --> E2 --> H2 --> N2
U4 --> E3 --> H3 --> N3
U4 --> GAP_B
U4 --> E4 --> H3
U4 --> E5 --> H4
H4 --> ECALL --> N4
N4 -.enables.-> H5
N4 -.enables.-> H6
U5 --> E6 --> H5 --> N5
U5 --> E7 -.via classify.-> H6
U3 -.if backfilled.-> H5 --> N10
U6 --> E11 --> H7 --> N6
U6 --> E7 --> H6
U7 --> E8 --> H6 --> N7
U8 --> E9 --> H6 --> N8
U9 --> E10 --> H8 --> N9
style GAP_A fill:#ff6b6b,color:#fff
style GAP_B fill:#ff6b6b,color:#fff
style N4 fill:#a8e6cf
style N5 fill:#a8e6cf
style N10 fill:#a8e6cf
Reflects Tanya's accepted additions: masterEoa / owners / threshold on MasterSafe; AgentFundingEvent aggregator; expanded FundsCategory and TrackedSafe.role enums.
erDiagram
MasterSafe ||--o{ Service : owns
MasterSafe ||--o{ AgentSafe : "owns (via Service)"
Service ||--|| AgentSafe : "has one"
Service }o--|| StakingContract : "currentStakingContract"
Service ||--o{ ServiceNftCustodyChange : "nft trail"
Service ||--o{ DailyServiceFunds : "daily rollup"
MasterSafe ||--o{ FundsMovement : "from / to"
AgentSafe ||--o{ FundsMovement : "from / to"
Service ||--o{ FundsMovement : "indexes"
StakingContract ||--o{ FundsMovement : "stakingContract"
AgentFundingEvent ||--o{ FundsMovement : "groups (txHash)"
MasterSafe ||--o{ AgentFundingEvent : "funded from"
AgentSafe ||--o{ AgentFundingEvent : "funded into"
TrackedSafe }o--|| Service : "scoped to"
TokenBalance }o--|| Token : "of"
TrackedSafe ||--o{ TokenBalance : "balances"
MasterSafe {
Bytes id PK "Master Safe address"
String network
Bytes masterEoa "owners[0] at first sighting"
BytesList owners "kept current via Added/RemovedOwner"
BigInt threshold
BigInt totalOlasRewardsClaimed
BigInt firstSeenTimestamp
BigInt lastActivityTimestamp
}
AgentSafe {
Bytes id PK "Agent Safe (multisig)"
Bytes masterSafe FK
ID service FK
BigInt createdTimestamp
}
Service {
ID id PK "serviceId"
BigInt serviceId
IntList agentIds "consumer-filterable cohort"
BytesList operators
Bytes masterSafe FK
Bytes agentSafe FK
String state "REGISTERED|DEPLOYED|STAKED|UNSTAKED|TERMINATED"
Bytes nftCustodian
Bytes currentStakingContract FK
BigInt totalOlasRewardsClaimed
}
StakingContract {
Bytes id PK "staking proxy address"
Bytes implementation
BigInt minStakingDeposit
BigInt numAgentInstances
}
FundsMovement {
Bytes id PK "txHash.logIndex"
ID service FK
Bytes masterSafe FK
Bytes agentSafe FK
String category "SAFE_DEPLOYED|SAFE_SETUP_TRANSFER|STAKING_REWARD_CLAIM|UNSTAKE_REWARD|MASTER_FUNDING_IN|MASTER_TO_AGENT|AGENT_TO_MASTER|MASTER_WITHDRAWAL|AGENT_TO_APP|APP_TO_AGENT|OTHER"
String source "SEMANTIC|RAW_TRANSFER"
Bytes token "null = native or NFT"
BigInt amount
Bytes from
Bytes to
Bytes stakingContract FK
ID agentFundingEvent FK "non-null if part of agent-funding tx"
BigInt blockTimestamp
Bytes transactionHash
}
AgentFundingEvent {
ID id PK "txHash"
Bytes masterSafe FK
Bytes agentSafe FK
ID service FK
BigInt blockTimestamp
Bytes transactionHash
}
ServiceNftCustodyChange {
Bytes id PK
ID service FK
Bytes from
Bytes to
BigInt blockTimestamp
}
DailyServiceFunds {
ID id PK "serviceId-dayTimestamp"
ID service FK
BigInt dayTimestamp
BigInt olasRewardsClaimed
BigInt cumulativeOlasRewardsClaimed
}
Token {
Bytes id PK "token address"
String symbol
Int decimals
}
TrackedSafe {
Bytes id PK "Safe / EOA address"
String role "MASTER | AGENT | AGENT_EOA"
ID service FK
}
TokenBalance {
Bytes id PK "safe.token"
Bytes safe FK
Bytes token FK
BigInt balance
BigInt lastUpdatedTimestamp
}
PendingRegistration {
Bytes id PK "serviceId (transient)"
IntList agentIds
BytesList operators
}
ServiceIndex {
Bytes id PK "serviceId (transient)"
Bytes multisig
}
Sequence view assuming Tanya's review is adopted. Only the pre-stake transfer (Open Ask 1) and the OLAS bond (Open Ask 2) remain unresolved.
sequenceDiagram
autonumber
actor User
participant Pearl
participant Chain as Gnosis chain
participant SG as Subgraph (graph-node)
participant DB as Subgraph DB
User->>Pearl: Install + create Master EOA
Pearl->>Pearl: Deploy Master Safe (off-chain to subgraph)
Note over SG,DB: Subgraph state: empty for this user
User->>Chain: Fund Master Safe (30 xDAI)
Chain-->>SG: SafeReceived(MasterSafe) — no Safe template yet
Note over SG,DB: ⚠️ OPEN ASK 1<br/>Captured only if template uses<br/>createWithContext(historical startBlock)
Pearl->>Chain: RegisterInstance(any agentId)
Chain-->>SG: event
SG->>DB: write PendingRegistration
Pearl->>Chain: CreateMultisigWithAgents
Chain-->>SG: event
SG->>DB: create Service + AgentSafe<br/>(no agentId gate — all services indexed)
Pearl->>Chain: Post bond to ServiceRegistryTokenUtility (20 OLAS)
Note over SG,DB: ⚠️ OPEN ASK 2<br/>Contract not indexed → no STAKING_DEPOSIT row
Chain-->>SG: ERC721.Transfer (NFT mint → Master Safe)
SG->>DB: update Service.nftCustodian
Pearl->>Chain: Stake (NFT → StakingProxy)
Chain-->>SG: ERC721.Transfer + ServiceStaked
SG->>Chain: eth_call GnosisSafe.getOwners() on MasterSafe
Chain-->>SG: [masterEoa, backupOwner]
SG->>DB: ✅ create MasterSafe{masterEoa, owners, threshold}<br/>✅ FundsMovement(SAFE_DEPLOYED)<br/>✅ TrackedSafe(MASTER, AGENT, AGENT_EOA)<br/>✅ spawn Safe template
Note over SG,DB: Tracking starts HERE (forward)<br/>Backfilled IF createWithContext is used
Pearl->>Chain: Fund Agent Safe + Agent EOA (1 xDAI + 0.2 xDAI, one tx)
Chain-->>SG: SafeReceived(AgentSafe) + native transfer to AgentEOA
SG->>DB: FundsMovement(MASTER_TO_AGENT) × 2<br/>+ AgentFundingEvent(txHash) groups them
- OLAS staking entry source. No on-chain event surfaces the staking bond — it moves through
ServiceRegistryTokenUtilitywhich isn't indexed. Per our open ask 2, options: (a) subgraph synthesizesSTAKING_DEPOSITatServiceStakedusingminStakingDeposit × numAgentInstances, (b) subgraph indexesServiceRegistryTokenUtility, (c) UI skips the entry. Awaiting subgraph response. - Pre-stake "Setup complete" anchor. Per our open ask 1,
SAFE_SETUP_TRANSFERrequires theSafetemplate to spawn with a historicalstartBlock(createWithContext) so the pre-stake xDAI funding is captured. If not adopted, "Setup complete" can only anchor onSAFE_DEPLOYED(no amount) — confirm with design whether that's acceptable. - Stablecoin data path (Phase 2b). Per Tanya's product-impact framing of §6.3: Polystrat is USDC/USDC.e-denominated, so if 2b is punted off-chain (Dune/RPC), the Polygon wallet view cannot satisfy "Each included transaction type renders correctly" without a second data source. Needs product decision before frontend work proceeds for Polygon.
- Multi-instance interaction. Per
docs/features/multi-instance-agents.md, services are migrating to per-service selection. History keyed on Master Safe is per-chain, not per-service — multi-instance doesn't break v1, but if user wants per-agent-instance history, that's a v2. - Lag threshold values. 50 blocks proposed for both chains. Confirm with backend/DevOps what's realistic for each of Gnosis/Polygon/Optimism/Base (different block times).
- Pagination strategy. v1 ships
first: 100, skip: 0(matches rewards-history precedent). Cursor pagination byblockTimestamp_ltis the right call long-term; confirm scope here.