Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/features/transaction-history.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ pearl-transactions subgraph (Gnosis | Polygon | Optimism | Base)
└── <HistoryTab /> in BalancesAndAssets
```

## Subgraph schema revisions (v1 / v2)

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.

## New files

| File | Purpose |
Expand Down
20 changes: 19 additions & 1 deletion frontend/constants/urls.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { TransactionHistorySchemaRevision } from '@/types/TransactionHistory';

import {
EvmChainId,
EvmChainIdMap,
Expand Down Expand Up @@ -41,9 +43,25 @@ export const TRANSACTION_HISTORY_SUBGRAPH_URLS_BY_EVM_CHAIN: Partial<
[EvmChainIdMap.Polygon]: undefined,
[EvmChainIdMap.Optimism]:
'https://transactions-optimism.subgraph.autonolas.tech/',
[EvmChainIdMap.Base]: undefined,
[EvmChainIdMap.Base]: 'https://transactions-base.subgraph.autonolas.tech/',
};

// Which pearl-transactions schema each chain's deployment serves (see
// TransactionHistorySchemaRevision). Gnosis/Optimism proxies pin subgraph
// v0.0.6 (v1); Base pins v0.0.7 (v2) — its indexers no longer serve v0.0.6.
// When a chain's proxy migrates to v0.0.7, flip its entry here; absent
// entries default to v1.
export const TRANSACTION_HISTORY_SUBGRAPH_SCHEMA_BY_EVM_CHAIN: Partial<

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather extend TRANSACTION_HISTORY_SUBGRAPH_URLS_BY_EVM_CHAIN and rename, e.g. ```
TRANSACTION_HISTORY_SUBGRAPH_BY_EVM_CHAIN = {[EvmChainIdMap.Base]: {url: '...', version: 'v2'}}

Record<EvmChainId, TransactionHistorySchemaRevision>
> = {
[EvmChainIdMap.Base]: 'v2',
};

export const getTransactionHistorySchemaRevision = (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't we just use object to know?

chainId: EvmChainId,
): TransactionHistorySchemaRevision =>
TRANSACTION_HISTORY_SUBGRAPH_SCHEMA_BY_EVM_CHAIN[chainId] ?? 'v1';

// telegram
export const SUPPORT_URL: Url = 'https://t.me/olaschat';

Expand Down
4 changes: 2 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"lodash-es": "4.18.1",
"minimatch": "9.0.9",
"picomatch": "4.0.4",
"postcss": "8.5.10",
"postcss": "8.5.12",
"sharp": "0.35.3",
"tar": "7.5.16",
"ws": "8.21.0"
Expand All @@ -37,7 +37,7 @@
"graphql-request": "6.1.0",
"lodash": "4.18.1",
"lottie-react": "2.4.1",
"next": "15.5.18",
"next": "15.5.21",
"react": "18.3.1",
"react-canvas-confetti": "1.2.1",
"react-dom": "18.3.1",
Expand Down
84 changes: 78 additions & 6 deletions frontend/service/AgentTransactionHistory.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { gql, request } from 'graphql-request';

import { EvmChainId } from '@/constants/chains';
import { TRANSACTION_HISTORY_SUBGRAPH_URLS_BY_EVM_CHAIN } from '@/constants/urls';
import {
getTransactionHistorySchemaRevision,
TRANSACTION_HISTORY_SUBGRAPH_URLS_BY_EVM_CHAIN,
} from '@/constants/urls';
import { Address } from '@/types/Address';
import {
AgentTransactionHistoryResponse,
AgentTransactionHistoryResponseSchema,
AgentTransactionHistoryResponseV2Schema,
} from '@/types/TransactionHistory';
// Deep import (not the '@/utils' barrel): config/chains pulls the barrel at
// module init, so barrel-importing here forms a cycle that breaks test loads.
import { normalizeAgentTransactionHistoryResponseV2 } from '@/utils/transactionHistory';

const FETCH_AGENT_TRANSACTION_HISTORY_QUERY = gql`
query GetAgentTransactionHistory(
Expand Down Expand Up @@ -56,6 +63,59 @@ const FETCH_AGENT_TRANSACTION_HISTORY_QUERY = gql`
}
`;

// v2 (subgraph v0.0.7) variant: no `bondType` on FundsMovement, service refs
// carry the numeric id in `serviceId`. The category filter is unchanged — on
// v2 OLAS reward sweeps are categorized AGENT_OLAS_TO_MASTER, so the same
// filter now excludes them server-side (v1 hides them client-side).
const FETCH_AGENT_TRANSACTION_HISTORY_QUERY_V2 = gql`
query GetAgentTransactionHistoryV2(
$agentSafe: Bytes!
$first: Int!
$skip: Int!
) {
fundsMovements(
where: {
agentSafe: $agentSafe
category_in: [MASTER_TO_AGENT, AGENT_TO_MASTER]
}
orderBy: blockTimestamp
orderDirection: desc
first: $first
skip: $skip
) {
id
category
source
token
amount
from
to
blockTimestamp
transactionHash
agentSafe {
id
service {
id
serviceId
agentIds
}
}
service {
id
serviceId
agentIds
}
}
_meta {
block {
number
timestamp
}
hasIndexingErrors
}
}
`;

type GetAgentTransactionHistoryParams = {
chainId: EvmChainId;
agentSafe: Address;
Expand All @@ -78,12 +138,24 @@ const get = async ({
);
}

const raw = await request(url, FETCH_AGENT_TRANSACTION_HISTORY_QUERY, {
agentSafe: agentSafe.toLowerCase(),
first,
skip,
});
const variables = { agentSafe: agentSafe.toLowerCase(), first, skip };

if (getTransactionHistorySchemaRevision(chainId) === 'v2') {
const raw = await request(
url,
FETCH_AGENT_TRANSACTION_HISTORY_QUERY_V2,
variables,
);
return normalizeAgentTransactionHistoryResponseV2(
AgentTransactionHistoryResponseV2Schema.parse(raw),
);
}

const raw = await request(
url,
FETCH_AGENT_TRANSACTION_HISTORY_QUERY,
variables,
);
return AgentTransactionHistoryResponseSchema.parse(raw);
};

Expand Down
163 changes: 157 additions & 6 deletions frontend/service/TransactionHistory.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { gql, request } from 'graphql-request';

import { EvmChainId } from '@/constants/chains';
import { TRANSACTION_HISTORY_SUBGRAPH_URLS_BY_EVM_CHAIN } from '@/constants/urls';
import {
getTransactionHistorySchemaRevision,
TRANSACTION_HISTORY_SUBGRAPH_URLS_BY_EVM_CHAIN,
} from '@/constants/urls';
import { Address } from '@/types/Address';
import {
TransactionHistoryResponse,
TransactionHistoryResponseSchema,
TransactionHistoryResponseV2Schema,
} from '@/types/TransactionHistory';
// Deep import (not the '@/utils' barrel): config/chains pulls the barrel at
// module init, so barrel-importing here forms a cycle that breaks test loads.
import { normalizeTransactionHistoryResponseV2 } from '@/utils/transactionHistory';

const FETCH_TRANSACTION_HISTORY_QUERY = gql`
query GetTransactionHistory($masterSafe: Bytes!, $first: Int!, $skip: Int!) {
Expand Down Expand Up @@ -103,6 +110,142 @@ const FETCH_TRANSACTION_HISTORY_QUERY = gql`
}
`;

// v2 (subgraph v0.0.7) variant. Differences from v1: FundsMovement has no
// `bondType` and no SERVICE_BOND_* rows — bonds live on the separate
// `bondMovements` ledger (queried alongside and merged during normalization);
// OLAS reward sweeps arrive pre-split as AGENT_OLAS_TO_MASTER (excluded by the
// category filter, matching v1's client-side hiding); service refs carry the
// numeric id in `serviceId`.
const FETCH_TRANSACTION_HISTORY_QUERY_V2 = gql`
query GetTransactionHistoryV2(
$masterSafe: Bytes!
$first: Int!
$skip: Int!
) {
masterSafe(id: $masterSafe) {
id
masterEoa
owners
threshold
historyFloorBlock
historyFloorTimestamp
}
fundsMovements(
where: {
masterSafe: $masterSafe
category_in: [
MASTER_FUNDING_IN
AGENT_TO_MASTER
MASTER_TO_AGENT
MASTER_WITHDRAWAL
]
}
orderBy: blockTimestamp
orderDirection: desc
first: $first
skip: $skip
) {
id
category
source
token
amount
from
to
blockTimestamp
transactionHash
agentSafe {
id
service {
id
serviceId
agentIds
}
}
service {
id
serviceId
agentIds
}
}
bondMovements(
where: { masterSafe: $masterSafe }
orderBy: blockTimestamp
orderDirection: desc
first: $first
skip: $skip
) {
id
category
source
bondType
token
amount
from
to
blockTimestamp
transactionHash
agentSafe {
id
service {
id
serviceId
agentIds
}
}
service {
id
serviceId
agentIds
}
}
agentFundingEvents(
where: { masterSafe: $masterSafe }
orderBy: blockTimestamp
orderDirection: desc
first: $first
skip: $skip
) {
id
txHash
blockTimestamp
totalNativeAmount
totalOlasAmount
transfers {
id
category
source
token
amount
from
to
blockTimestamp
transactionHash
agentSafe {
id
service {
id
serviceId
agentIds
}
}
service {
id
serviceId
agentIds
}
}
}
_meta {
block {
number
timestamp
}
hasIndexingErrors
}
}
`;

const DEFAULT_PAGE_SIZE = 100;

type GetTransactionHistoryParams = {
Expand All @@ -125,12 +268,20 @@ const get = async ({
);
}

const raw = await request(url, FETCH_TRANSACTION_HISTORY_QUERY, {
masterSafe: masterSafe.toLowerCase(),
first,
skip,
});
const variables = { masterSafe: masterSafe.toLowerCase(), first, skip };

if (getTransactionHistorySchemaRevision(chainId) === 'v2') {
const raw = await request(
url,
FETCH_TRANSACTION_HISTORY_QUERY_V2,
variables,
);
return normalizeTransactionHistoryResponseV2(
TransactionHistoryResponseV2Schema.parse(raw),
);
}

const raw = await request(url, FETCH_TRANSACTION_HISTORY_QUERY, variables);
return TransactionHistoryResponseSchema.parse(raw);
};

Expand Down
Loading
Loading