Skip to content

Commit a6dfee4

Browse files
atepemclaude
andcommitted
feat(history): support pearl-transactions v0.0.7 schema per chain
Base's transactions proxy pins subgraph v0.0.7, whose schema is incompatible with the queries Pearl sends (FundsMovement.bondType removed, bond rows moved to a separate bondMovements ledger, OLAS sweeps pre-split into AGENT_OLAS_TO_MASTER, Service.id reshaped to registry bytes with the numeric id in serviceId) — every request failed GraphQL validation, so Base users saw the error state. Adds a per-chain schema-revision map beside the subgraph URL map (Base → v2; absent → v1), v2 query documents + Zod schemas verified against the live Base deployment, and service-boundary normalization back to the v1-shaped domain types (bond rows merged into fundsMovements, sweep rows dropped, service.id mapped from serviceId). Hooks and components are untouched and revision-agnostic; migrating Gnosis/Optimism to v0.0.7 later is a one-line map flip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e896b8d commit a6dfee4

10 files changed

Lines changed: 856 additions & 17 deletions

File tree

docs/features/transaction-history.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ pearl-transactions subgraph (Gnosis | Polygon | Optimism | Base)
3636
└── <HistoryTab /> in BalancesAndAssets
3737
```
3838

39+
## Subgraph schema revisions (v1 / v2)
40+
41+
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):
42+
43+
| | v1 (v0.0.6) | v2 (v0.0.7) |
44+
|---|---|---|
45+
| Bond rows | `fundsMovements` with `bondType` | separate `bondMovements` ledger (carries `bondType`) |
46+
| OLAS reward sweeps | `AGENT_TO_MASTER`, hidden client-side (`isOlasAgentToMaster`) | pre-split `AGENT_OLAS_TO_MASTER`, excluded by the query's category filter |
47+
| `Service.id` | numeric string | registry Bytes (`"0x7802"`); numeric id in new `serviceId` field |
48+
49+
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.
50+
3951
## New files
4052

4153
| File | Purpose |

frontend/constants/urls.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { TransactionHistorySchemaRevision } from '@/types/TransactionHistory';
2+
13
import {
24
EvmChainId,
35
EvmChainIdMap,
@@ -44,6 +46,22 @@ export const TRANSACTION_HISTORY_SUBGRAPH_URLS_BY_EVM_CHAIN: Partial<
4446
[EvmChainIdMap.Base]: 'https://transactions-base.subgraph.autonolas.tech/',
4547
};
4648

49+
// Which pearl-transactions schema each chain's deployment serves (see
50+
// TransactionHistorySchemaRevision). Gnosis/Optimism proxies pin subgraph
51+
// v0.0.6 (v1); Base pins v0.0.7 (v2) — its indexers no longer serve v0.0.6.
52+
// When a chain's proxy migrates to v0.0.7, flip its entry here; absent
53+
// entries default to v1.
54+
export const TRANSACTION_HISTORY_SUBGRAPH_SCHEMA_BY_EVM_CHAIN: Partial<
55+
Record<EvmChainId, TransactionHistorySchemaRevision>
56+
> = {
57+
[EvmChainIdMap.Base]: 'v2',
58+
};
59+
60+
export const getTransactionHistorySchemaRevision = (
61+
chainId: EvmChainId,
62+
): TransactionHistorySchemaRevision =>
63+
TRANSACTION_HISTORY_SUBGRAPH_SCHEMA_BY_EVM_CHAIN[chainId] ?? 'v1';
64+
4765
// telegram
4866
export const SUPPORT_URL: Url = 'https://t.me/olaschat';
4967

frontend/service/AgentTransactionHistory.ts

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import { gql, request } from 'graphql-request';
22

33
import { EvmChainId } from '@/constants/chains';
4-
import { TRANSACTION_HISTORY_SUBGRAPH_URLS_BY_EVM_CHAIN } from '@/constants/urls';
4+
import {
5+
getTransactionHistorySchemaRevision,
6+
TRANSACTION_HISTORY_SUBGRAPH_URLS_BY_EVM_CHAIN,
7+
} from '@/constants/urls';
58
import { Address } from '@/types/Address';
69
import {
710
AgentTransactionHistoryResponse,
811
AgentTransactionHistoryResponseSchema,
12+
AgentTransactionHistoryResponseV2Schema,
913
} from '@/types/TransactionHistory';
14+
// Deep import (not the '@/utils' barrel): config/chains pulls the barrel at
15+
// module init, so barrel-importing here forms a cycle that breaks test loads.
16+
import { normalizeAgentTransactionHistoryResponseV2 } from '@/utils/transactionHistory';
1017

1118
const FETCH_AGENT_TRANSACTION_HISTORY_QUERY = gql`
1219
query GetAgentTransactionHistory(
@@ -56,6 +63,59 @@ const FETCH_AGENT_TRANSACTION_HISTORY_QUERY = gql`
5663
}
5764
`;
5865

66+
// v2 (subgraph v0.0.7) variant: no `bondType` on FundsMovement, service refs
67+
// carry the numeric id in `serviceId`. The category filter is unchanged — on
68+
// v2 OLAS reward sweeps are categorized AGENT_OLAS_TO_MASTER, so the same
69+
// filter now excludes them server-side (v1 hides them client-side).
70+
const FETCH_AGENT_TRANSACTION_HISTORY_QUERY_V2 = gql`
71+
query GetAgentTransactionHistoryV2(
72+
$agentSafe: Bytes!
73+
$first: Int!
74+
$skip: Int!
75+
) {
76+
fundsMovements(
77+
where: {
78+
agentSafe: $agentSafe
79+
category_in: [MASTER_TO_AGENT, AGENT_TO_MASTER]
80+
}
81+
orderBy: blockTimestamp
82+
orderDirection: desc
83+
first: $first
84+
skip: $skip
85+
) {
86+
id
87+
category
88+
source
89+
token
90+
amount
91+
from
92+
to
93+
blockTimestamp
94+
transactionHash
95+
agentSafe {
96+
id
97+
service {
98+
id
99+
serviceId
100+
agentIds
101+
}
102+
}
103+
service {
104+
id
105+
serviceId
106+
agentIds
107+
}
108+
}
109+
_meta {
110+
block {
111+
number
112+
timestamp
113+
}
114+
hasIndexingErrors
115+
}
116+
}
117+
`;
118+
59119
type GetAgentTransactionHistoryParams = {
60120
chainId: EvmChainId;
61121
agentSafe: Address;
@@ -78,12 +138,24 @@ const get = async ({
78138
);
79139
}
80140

81-
const raw = await request(url, FETCH_AGENT_TRANSACTION_HISTORY_QUERY, {
82-
agentSafe: agentSafe.toLowerCase(),
83-
first,
84-
skip,
85-
});
141+
const variables = { agentSafe: agentSafe.toLowerCase(), first, skip };
142+
143+
if (getTransactionHistorySchemaRevision(chainId) === 'v2') {
144+
const raw = await request(
145+
url,
146+
FETCH_AGENT_TRANSACTION_HISTORY_QUERY_V2,
147+
variables,
148+
);
149+
return normalizeAgentTransactionHistoryResponseV2(
150+
AgentTransactionHistoryResponseV2Schema.parse(raw),
151+
);
152+
}
86153

154+
const raw = await request(
155+
url,
156+
FETCH_AGENT_TRANSACTION_HISTORY_QUERY,
157+
variables,
158+
);
87159
return AgentTransactionHistoryResponseSchema.parse(raw);
88160
};
89161

frontend/service/TransactionHistory.ts

Lines changed: 157 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import { gql, request } from 'graphql-request';
22

33
import { EvmChainId } from '@/constants/chains';
4-
import { TRANSACTION_HISTORY_SUBGRAPH_URLS_BY_EVM_CHAIN } from '@/constants/urls';
4+
import {
5+
getTransactionHistorySchemaRevision,
6+
TRANSACTION_HISTORY_SUBGRAPH_URLS_BY_EVM_CHAIN,
7+
} from '@/constants/urls';
58
import { Address } from '@/types/Address';
69
import {
710
TransactionHistoryResponse,
811
TransactionHistoryResponseSchema,
12+
TransactionHistoryResponseV2Schema,
913
} from '@/types/TransactionHistory';
14+
// Deep import (not the '@/utils' barrel): config/chains pulls the barrel at
15+
// module init, so barrel-importing here forms a cycle that breaks test loads.
16+
import { normalizeTransactionHistoryResponseV2 } from '@/utils/transactionHistory';
1017

1118
const FETCH_TRANSACTION_HISTORY_QUERY = gql`
1219
query GetTransactionHistory($masterSafe: Bytes!, $first: Int!, $skip: Int!) {
@@ -103,6 +110,142 @@ const FETCH_TRANSACTION_HISTORY_QUERY = gql`
103110
}
104111
`;
105112

113+
// v2 (subgraph v0.0.7) variant. Differences from v1: FundsMovement has no
114+
// `bondType` and no SERVICE_BOND_* rows — bonds live on the separate
115+
// `bondMovements` ledger (queried alongside and merged during normalization);
116+
// OLAS reward sweeps arrive pre-split as AGENT_OLAS_TO_MASTER (excluded by the
117+
// category filter, matching v1's client-side hiding); service refs carry the
118+
// numeric id in `serviceId`.
119+
const FETCH_TRANSACTION_HISTORY_QUERY_V2 = gql`
120+
query GetTransactionHistoryV2(
121+
$masterSafe: Bytes!
122+
$first: Int!
123+
$skip: Int!
124+
) {
125+
masterSafe(id: $masterSafe) {
126+
id
127+
masterEoa
128+
owners
129+
threshold
130+
historyFloorBlock
131+
historyFloorTimestamp
132+
}
133+
fundsMovements(
134+
where: {
135+
masterSafe: $masterSafe
136+
category_in: [
137+
MASTER_FUNDING_IN
138+
AGENT_TO_MASTER
139+
MASTER_TO_AGENT
140+
MASTER_WITHDRAWAL
141+
]
142+
}
143+
orderBy: blockTimestamp
144+
orderDirection: desc
145+
first: $first
146+
skip: $skip
147+
) {
148+
id
149+
category
150+
source
151+
token
152+
amount
153+
from
154+
to
155+
blockTimestamp
156+
transactionHash
157+
agentSafe {
158+
id
159+
service {
160+
id
161+
serviceId
162+
agentIds
163+
}
164+
}
165+
service {
166+
id
167+
serviceId
168+
agentIds
169+
}
170+
}
171+
bondMovements(
172+
where: { masterSafe: $masterSafe }
173+
orderBy: blockTimestamp
174+
orderDirection: desc
175+
first: $first
176+
skip: $skip
177+
) {
178+
id
179+
category
180+
source
181+
bondType
182+
token
183+
amount
184+
from
185+
to
186+
blockTimestamp
187+
transactionHash
188+
agentSafe {
189+
id
190+
service {
191+
id
192+
serviceId
193+
agentIds
194+
}
195+
}
196+
service {
197+
id
198+
serviceId
199+
agentIds
200+
}
201+
}
202+
agentFundingEvents(
203+
where: { masterSafe: $masterSafe }
204+
orderBy: blockTimestamp
205+
orderDirection: desc
206+
first: $first
207+
skip: $skip
208+
) {
209+
id
210+
txHash
211+
blockTimestamp
212+
totalNativeAmount
213+
totalOlasAmount
214+
transfers {
215+
id
216+
category
217+
source
218+
token
219+
amount
220+
from
221+
to
222+
blockTimestamp
223+
transactionHash
224+
agentSafe {
225+
id
226+
service {
227+
id
228+
serviceId
229+
agentIds
230+
}
231+
}
232+
service {
233+
id
234+
serviceId
235+
agentIds
236+
}
237+
}
238+
}
239+
_meta {
240+
block {
241+
number
242+
timestamp
243+
}
244+
hasIndexingErrors
245+
}
246+
}
247+
`;
248+
106249
const DEFAULT_PAGE_SIZE = 100;
107250

108251
type GetTransactionHistoryParams = {
@@ -125,12 +268,20 @@ const get = async ({
125268
);
126269
}
127270

128-
const raw = await request(url, FETCH_TRANSACTION_HISTORY_QUERY, {
129-
masterSafe: masterSafe.toLowerCase(),
130-
first,
131-
skip,
132-
});
271+
const variables = { masterSafe: masterSafe.toLowerCase(), first, skip };
272+
273+
if (getTransactionHistorySchemaRevision(chainId) === 'v2') {
274+
const raw = await request(
275+
url,
276+
FETCH_TRANSACTION_HISTORY_QUERY_V2,
277+
variables,
278+
);
279+
return normalizeTransactionHistoryResponseV2(
280+
TransactionHistoryResponseV2Schema.parse(raw),
281+
);
282+
}
133283

284+
const raw = await request(url, FETCH_TRANSACTION_HISTORY_QUERY, variables);
134285
return TransactionHistoryResponseSchema.parse(raw);
135286
};
136287

0 commit comments

Comments
 (0)