Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ build/
.env.local
.env.*.local
.env.docker
.env.bak

# IDE
.idea/
Expand Down
9 changes: 7 additions & 2 deletions atp-indexer/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Database URL
DATABASE_URL=postgresql://user:password@localhost:5432/ponder
# Database (NOTE: Use POSTGRES_CONNECTION_STRING, not DATABASE_URL)
POSTGRES_CONNECTION_STRING=postgresql://user:password@localhost:5432/ponder

# RPC URL
RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY
Expand All @@ -10,11 +10,16 @@ CHAIN_ID=1
# Contract Addresses
ATP_FACTORY_ADDRESS=0x...
ATP_FACTORY_AUCTION_ADDRESS=0x...
ATP_FACTORY_EMPLOYEE_ADDRESS=0x...
ATP_FACTORY_INVESTOR_ADDRESS=0x...
STAKING_REGISTRY_ADDRESS=0x...
ROLLUP_ADDRESS=0x...

# Indexer Settings
START_BLOCK=0
# Per-factory start blocks (optional, for efficiency - set to deployment block of each factory)
EMPLOYEE_FACTORY_START_BLOCK=0
INVESTOR_FACTORY_START_BLOCK=0

# Application
NODE_ENV=development
Expand Down
9 changes: 9 additions & 0 deletions atp-indexer/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ get_contract_addresses() {
ATP_REGISTRY_AUCTION_ADDRESS=$(cat $contract_addresses_file | jq -r '.atpRegistryAuction')
ATP_FACTORY_AUCTION_ADDRESS=$(cat $contract_addresses_file | jq -r '.atpFactoryAuction')

# new factories
ATP_FACTORY_EMPLOYEE_ADDRESS=$(cat $contract_addresses_file | jq -r '.atpFactoryEmployee')
ATP_FACTORY_INVESTOR_ADDRESS=$(cat $contract_addresses_file | jq -r '.atpFactoryInvestor')

# other
STAKING_REGISTRY_ADDRESS=$(cat $contract_addresses_file | jq -r '.stakingRegistry')
ROLLUP_ADDRESS=$(cat $contract_addresses_file | jq -r '.rollupAddress')
Expand Down Expand Up @@ -150,6 +154,8 @@ CHAIN_ID=${CHAIN_ID}
# Contract addresses
ATP_FACTORY_ADDRESS=${ATP_FACTORY_ADDRESS}
ATP_FACTORY_AUCTION_ADDRESS=${ATP_FACTORY_AUCTION_ADDRESS}
ATP_FACTORY_EMPLOYEE_ADDRESS=${ATP_FACTORY_EMPLOYEE_ADDRESS}
ATP_FACTORY_INVESTOR_ADDRESS=${ATP_FACTORY_INVESTOR_ADDRESS}
STAKING_REGISTRY_ADDRESS=${STAKING_REGISTRY_ADDRESS}
ROLLUP_ADDRESS=${ROLLUP_ADDRESS}

Expand Down Expand Up @@ -187,6 +193,8 @@ CHAIN_ID=${CHAIN_ID}
# Contract addresses
ATP_FACTORY_ADDRESS=${ATP_FACTORY_ADDRESS}
ATP_FACTORY_AUCTION_ADDRESS=${ATP_FACTORY_AUCTION_ADDRESS}
ATP_FACTORY_EMPLOYEE_ADDRESS=${ATP_FACTORY_EMPLOYEE_ADDRESS}
ATP_FACTORY_INVESTOR_ADDRESS=${ATP_FACTORY_INVESTOR_ADDRESS}
STAKING_REGISTRY_ADDRESS=${STAKING_REGISTRY_ADDRESS}
ROLLUP_ADDRESS=${ROLLUP_ADDRESS}

Expand Down Expand Up @@ -470,6 +478,7 @@ case $ACTION in
echo ""
echo " Required contract address variables:"
echo " ATP_FACTORY_ADDRESS, ATP_FACTORY_AUCTION_ADDRESS"
echo " ATP_FACTORY_EMPLOYEE_ADDRESS, ATP_FACTORY_INVESTOR_ADDRESS"
echo " ATP_REGISTRY_ADDRESS, ATP_REGISTRY_AUCTION_ADDRESS"
echo " STAKING_REGISTRY_ADDRESS, ROLLUP_ADDRESS"
echo " START_BLOCK (optional, defaults to 0)"
Expand Down
43 changes: 38 additions & 5 deletions atp-indexer/ponder.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ const ATPCreatedEvent = parseAbiItem(
"event ATPCreated(address indexed beneficiary, address indexed atp, uint256 allocation)"
);

// Per-factory start blocks for efficient indexing
const FACTORY_START_BLOCKS = {
genesis: config.START_BLOCK || 0,
auction: config.START_BLOCK || 0,
employee: config.EMPLOYEE_FACTORY_START_BLOCK || config.START_BLOCK || 0,
investor: config.INVESTOR_FACTORY_START_BLOCK || config.START_BLOCK || 0,
};


let databaseConfig: DatabaseConfig | undefined;

Expand Down Expand Up @@ -48,14 +56,14 @@ export default createConfig({
},
contracts: {
/**
* ATP Factory - Main contract
* ATP Factory - Genesis Sale contract
* Emits ATPCreated events when new ATP positions are created
*/
ATPFactory: {
chain: config.networkName,
abi: ATP_ABI,
address: config.ATP_FACTORY_ADDRESS as `0x${string}`,
startBlock: config.START_BLOCK,
startBlock: FACTORY_START_BLOCKS.genesis,
},

/**
Expand All @@ -66,7 +74,29 @@ export default createConfig({
chain: config.networkName,
abi: ATP_ABI,
address: config.ATP_FACTORY_AUCTION_ADDRESS as `0x${string}`,
startBlock: config.START_BLOCK,
startBlock: FACTORY_START_BLOCKS.auction,
},

/**
* ATP Factory - Employee contract
* Issues MATPs to employees
*/
ATPFactoryEmployee: {
chain: config.networkName,
abi: ATP_ABI,
address: config.ATP_FACTORY_EMPLOYEE_ADDRESS as `0x${string}`,
startBlock: FACTORY_START_BLOCKS.employee,
},

/**
* ATP Factory - Investor contract
* Issues LATPs and MATPs to investors
*/
ATPFactoryInvestor: {
chain: config.networkName,
abi: ATP_ABI,
address: config.ATP_FACTORY_INVESTOR_ADDRESS as `0x${string}`,
startBlock: FACTORY_START_BLOCKS.investor,
},

/**
Expand Down Expand Up @@ -94,7 +124,7 @@ export default createConfig({
/**
* Dynamic ATP Contracts
* Created by factory events, tracks operator updates
* Uses factory pattern to only index ATPs created by our factories
* Uses factory pattern to only index ATPs created by all 4 factories
*/
ATP: {
chain: config.networkName,
Expand All @@ -103,11 +133,14 @@ export default createConfig({
address: [
config.ATP_FACTORY_ADDRESS as `0x${string}`,
config.ATP_FACTORY_AUCTION_ADDRESS as `0x${string}`,
config.ATP_FACTORY_EMPLOYEE_ADDRESS as `0x${string}`,
config.ATP_FACTORY_INVESTOR_ADDRESS as `0x${string}`,
],
event: ATPCreatedEvent,
parameter: "atp",
}),
startBlock: config.START_BLOCK,
// Use earliest factory start block to capture all ATP contracts
startBlock: Math.min(...Object.values(FACTORY_START_BLOCKS)),
},

/**
Expand Down
2 changes: 2 additions & 0 deletions atp-indexer/ponder.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const atpPosition = onchainTable("atp_position", (t) => ({
type: atpType("type").notNull(),
stakerAddress: t.hex().notNull(),
operatorAddress: t.hex(),
factoryAddress: t.hex().notNull(), // Factory that created this ATP
blockNumber: t.bigint().notNull(),
txHash: t.hex().notNull(),
logIndex: t.integer().notNull(),
Expand All @@ -23,6 +24,7 @@ export const atpPosition = onchainTable("atp_position", (t) => ({
addressIdx: index().on(table.address),
beneficiaryIdx: index().on(table.beneficiary),
stakerAddressIdx: index().on(table.stakerAddress),
factoryAddressIdx: index().on(table.factoryAddress),
}));

export const atpPositionRelations = relations(atpPosition, ({ many }) => ({
Expand Down
1 change: 1 addition & 0 deletions atp-indexer/src/api/handlers/atp/beneficiary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export async function handleATPByBeneficiary(c: Context): Promise<Response> {
allocation: pos.allocation.toString(),
type: pos.type,
stakerAddress: checksumAddress(pos.stakerAddress),
factoryAddress: checksumAddress(pos.factoryAddress),
sequentialNumber: index + 1,
timestamp: Number(pos.timestamp),
totalWithdrawn: (withdrawalMap.get(normalizedAddress) ?? 0n).toString(),
Expand Down
1 change: 1 addition & 0 deletions atp-indexer/src/api/types/atp.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export interface ATPPosition {
allocation: string;
type: string;
stakerAddress: string;
factoryAddress: string; // Factory that created this ATP
sequentialNumber: number;
timestamp: number;
totalWithdrawn?: string;
Expand Down
4 changes: 4 additions & 0 deletions atp-indexer/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,15 @@ const configSchema = z.object({
// Contract addresses
ATP_FACTORY_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'),
ATP_FACTORY_AUCTION_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'),
ATP_FACTORY_EMPLOYEE_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'),
ATP_FACTORY_INVESTOR_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'),
STAKING_REGISTRY_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'),
ROLLUP_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'),

// Indexer settings
START_BLOCK: z.string().transform(Number).refine(n => n >= 0, 'START_BLOCK must be non-negative').default('0'),
EMPLOYEE_FACTORY_START_BLOCK: z.string().transform(Number).refine(n => n >= 0, 'EMPLOYEE_FACTORY_START_BLOCK must be non-negative').optional(),
INVESTOR_FACTORY_START_BLOCK: z.string().transform(Number).refine(n => n >= 0, 'INVESTOR_FACTORY_START_BLOCK must be non-negative').optional(),

// Application
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
Expand Down
14 changes: 12 additions & 2 deletions atp-indexer/src/events/atp-factory/atp-created.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ async function handleATPCreated({ event, context }: IndexingFunctionArgs<'ATPFac

const atpType = await determineATPType(atp, client);
const stakerAddress = await getStakerAddress(atp, client);
const factoryAddress = event.log.address; // Factory contract that emitted the event

await db.insert(atpPosition).values({
id: normalizeAddress(atp),
Expand All @@ -71,19 +72,28 @@ async function handleATPCreated({ event, context }: IndexingFunctionArgs<'ATPFac
type: atpType,
stakerAddress: normalizeAddress(stakerAddress) as `0x${string}`,
operatorAddress: null,
factoryAddress: normalizeAddress(factoryAddress) as `0x${string}`,
blockNumber: event.block.number,
txHash: event.transaction.hash,
logIndex: event.log.logIndex,
timestamp: event.block.timestamp,
})

console.log(`${atpType} created (${source}): ${atp}`);
console.log(`${atpType} created (${source}): ${atp} from factory ${factoryAddress}`);
}

ponder.on("ATPFactory:ATPCreated", async (params) => {
await handleATPCreated(params, "factory");
await handleATPCreated(params, "genesis");
});

ponder.on("ATPFactoryAuction:ATPCreated", async (params) => {
await handleATPCreated(params, "auction");
});

ponder.on("ATPFactoryEmployee:ATPCreated", async (params) => {
await handleATPCreated(params, "employee");
});

ponder.on("ATPFactoryInvestor:ATPCreated", async (params) => {
await handleATPCreated(params, "investor");
});
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ interface ATPDetailsDelegationItemProps {
providerRewardsRecipient: string
}) => void
onWithdrawSuccess?: () => void
// ATP context for milestone validation
atpType?: string
registryAddress?: Address
milestoneId?: bigint
}

/**
Expand All @@ -45,7 +49,10 @@ export const ATPDetailsDelegationItem = ({
stakerAddress,
rollupVersion,
onClaimClick,
onWithdrawSuccess
onWithdrawSuccess,
atpType,
registryAddress,
milestoneId
}: ATPDetailsDelegationItemProps) => {
const [isExpanded, setIsExpanded] = useState(false)
const { symbol, decimals } = useStakingAssetTokenDetails()
Expand Down Expand Up @@ -488,6 +495,9 @@ export const ATPDetailsDelegationItem = ({
refetchStatus()
onWithdrawSuccess?.()
}}
atpType={atpType}
registryAddress={registryAddress}
milestoneId={milestoneId}
/>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,17 @@ interface ATPDetailsDirectStakeItemProps {
atp: ATPData
onClaimSuccess?: () => void
onWithdrawSuccess?: () => void
// ATP context for milestone validation
atpType?: string
registryAddress?: Address
milestoneId?: bigint
}

/**
* Individual self stake item component
* Displays sequencer address, transaction info, and links to explorers
*/
export const ATPDetailsDirectStakeItem = ({ stake, stakerAddress, rollupVersion, atp, onClaimSuccess, onWithdrawSuccess }: ATPDetailsDirectStakeItemProps) => {
export const ATPDetailsDirectStakeItem = ({ stake, stakerAddress, rollupVersion, atp, onClaimSuccess, onWithdrawSuccess, atpType, registryAddress, milestoneId }: ATPDetailsDirectStakeItemProps) => {
const [isExpanded, setIsExpanded] = useState(false)
const [isClaimModalOpen, setIsClaimModalOpen] = useState(false)
const { symbol, decimals } = useStakingAssetTokenDetails()
Expand Down Expand Up @@ -393,6 +397,9 @@ export const ATPDetailsDirectStakeItem = ({ stake, stakerAddress, rollupVersion,
refetchStatus()
onWithdrawSuccess?.()
}}
atpType={atpType}
registryAddress={registryAddress}
milestoneId={milestoneId}
/>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { ClaimAllProvider } from "@/contexts/ClaimAllContext"
import { ClaimAllDelegationRewardsButton } from "@/components/ClaimAllDelegationRewardsButton"
import { ClaimDelegationRewardsModal, type DelegationModalData } from "@/components/ClaimDelegationRewardsModal"
import type { ATPData } from "@/hooks/atp"
import { isMATPData } from "@/hooks/atp/matp/matpTypes"
import type { Address } from "viem"

interface ATPDetailsModalProps {
Expand Down Expand Up @@ -254,6 +255,11 @@ export const ATPDetailsModal = ({ atp, isOpen, onClose, onWithdrawSuccess, onRef

const { messages: alertMessages, type: alertType } = alertData

// Extract ATP context for milestone validation
const atpType = atp.typeString; // "MATP", "LATP", "NCATP"
const registryAddress = atp.registry as Address;
const milestoneId = isMATPData(atp) ? atp.milestoneId : undefined;

return createPortal(
<ClaimAllProvider>
<div
Expand Down Expand Up @@ -358,6 +364,9 @@ export const ATPDetailsModal = ({ atp, isOpen, onClose, onWithdrawSuccess, onRef
rollupVersion={rollupVersion}
atp={atp}
onWithdrawSuccess={handleWithdrawSuccess}
atpType={atpType}
registryAddress={registryAddress}
milestoneId={milestoneId}
/>
))}
</div>
Expand Down Expand Up @@ -448,6 +457,9 @@ export const ATPDetailsModal = ({ atp, isOpen, onClose, onWithdrawSuccess, onRef
rollupVersion={rollupVersion}
onClaimClick={handleDelegationClaimClick}
onWithdrawSuccess={handleWithdrawSuccess}
atpType={atpType}
registryAddress={registryAddress}
milestoneId={milestoneId}
/>
))}
</div>
Expand Down
Loading
Loading