11const { gql } = require ( "@apollo/client/core" ) ;
2- const { parseUnits } = require ( "ethers" ) ;
2+ const { Contract , parseUnits } = require ( "ethers" ) ;
33
44const { baseWithdrawAmount } = require ( "./liquidityAutomation" ) ;
55const {
@@ -8,13 +8,19 @@ const {
88 requestBaseAssetWithdrawal,
99 resolveArmBase,
1010} = require ( "../utils/arm" ) ;
11+ const addresses = require ( "../utils/addresses" ) ;
1112const { createApolloClient } = require ( "../utils/apollo" ) ;
1213const { logTxDetails } = require ( "../utils/txLogger" ) ;
1314
1415const log = require ( "../utils/logger" ) ( "task:etherfiQueue" ) ;
1516
1617const uri = "https://origin.squids.live/ops-squid/graphql" ;
1718
19+ const ETHERFI_WITHDRAWAL_NFT_ABI = [
20+ "function isFinalized(uint256 requestId) view returns (bool)" ,
21+ "function getRequest(uint256 requestId) view returns (tuple(uint96 amountOfEEth, uint96 shareOfEEth, bool isValid, uint32 feeGwei))" ,
22+ ] ;
23+
1824const requestEtherFiWithdrawals = async ( options ) => {
1925 const { signer, amount } = options ;
2026 const baseContext = await resolveArmBase ( options ) ;
@@ -43,7 +49,7 @@ const claimEtherFiWithdrawals = async (options) => {
4349 ? // If an id is provided, just claim that one
4450 [ id ]
4551 : // Get the outstanding EtherFi withdrawal requests for the ARM
46- await claimableEtherFiRequests ( ) ;
52+ await claimableEtherFiRequests ( signer ) ;
4753
4854 if ( baseContext . version === "legacy" ) {
4955 if ( requestIds . length > 0 ) {
@@ -84,7 +90,32 @@ const claimEtherFiWithdrawals = async (options) => {
8490 await logTxDetails ( tx , "claim EtherFi withdraws" ) ;
8591} ;
8692
87- const claimableEtherFiRequests = async ( ) => {
93+ // Read the on-chain finalized/valid state of each subgraph-reported request.
94+ // EtherFi never reverts on these views (isFinalized is a numeric comparison and
95+ // getRequest returns a zeroed struct for burnt/unknown ids), so one stale id
96+ // can't break the batch.
97+ const etherFiRequestStatuses = async ( withdrawalNFT , requestIds ) =>
98+ Promise . all (
99+ requestIds . map ( async ( requestId ) => {
100+ const [ isFinalized , request ] = await Promise . all ( [
101+ withdrawalNFT . isFinalized ( requestId ) ,
102+ withdrawalNFT . getRequest ( requestId ) ,
103+ ] ) ;
104+ return { requestId, isFinalized, isValid : request . isValid } ;
105+ } ) ,
106+ ) ;
107+
108+ // Only finalized requests whose withdrawal NFT still exists (isValid) can be
109+ // claimed. EtherFi's isFinalized() stays true after a claim and the ops-squid
110+ // subgraph can lag on its `claimed` flag, so without this on-chain gate an
111+ // already-claimed request keeps coming back and claimEtherFiWithdrawals reverts
112+ // with "ERC721: invalid token ID" (the burnt NFT), wedging the action.
113+ const selectClaimableEtherFiRequests = ( statuses ) =>
114+ statuses
115+ . filter ( ( { isFinalized, isValid } ) => isFinalized && isValid )
116+ . map ( ( { requestId } ) => requestId ) ;
117+
118+ const claimableEtherFiRequests = async ( signer ) => {
88119 const client = createApolloClient ( uri ) ;
89120
90121 log ( `About to get claimable EtherFi withdrawal requests` ) ;
@@ -100,28 +131,47 @@ const claimableEtherFiRequests = async () => {
100131 }
101132 ` ;
102133
134+ let candidateIds ;
103135 try {
104136 const { data } = await client . query ( {
105137 query,
106138 } ) ;
107-
108- const claimableRequests = data . etherfiWithdrawalRequests . map (
139+ candidateIds = data . etherfiWithdrawalRequests . map (
109140 ( request ) => request . requestId ,
110141 ) ;
111-
112- log (
113- `Found ${ claimableRequests . length } claimable withdrawal requests: ${ claimableRequests } ` ,
114- ) ;
115-
116- return claimableRequests ;
117142 } catch ( error ) {
118143 const msg = `Failed to get claimable EtherFi withdrawal requests` ;
119144 console . error ( msg ) ;
120145 throw Error ( msg , { cause : error } ) ;
121146 }
147+
148+ const withdrawalNFT = new Contract (
149+ addresses . mainnet . etherfiWithdrawalQueue ,
150+ ETHERFI_WITHDRAWAL_NFT_ABI ,
151+ signer ,
152+ ) ;
153+ const statuses = await etherFiRequestStatuses ( withdrawalNFT , candidateIds ) ;
154+ const claimableRequests = selectClaimableEtherFiRequests ( statuses ) ;
155+
156+ const skipped = statuses
157+ . filter ( ( { isFinalized, isValid } ) => ! ( isFinalized && isValid ) )
158+ . map ( ( { requestId } ) => requestId ) ;
159+ if ( skipped . length > 0 ) {
160+ log (
161+ `Skipping ${ skipped . length } subgraph requests not claimable on-chain (already claimed or not finalized): ${ skipped } ` ,
162+ ) ;
163+ }
164+
165+ log (
166+ `Found ${ claimableRequests . length } claimable withdrawal requests: ${ claimableRequests } ` ,
167+ ) ;
168+
169+ return claimableRequests ;
122170} ;
123171
124172module . exports = {
125173 requestEtherFiWithdrawals,
126174 claimEtherFiWithdrawals,
175+ etherFiRequestStatuses,
176+ selectClaimableEtherFiRequests,
127177} ;
0 commit comments