@@ -13,6 +13,7 @@ import { queryKeysFactory } from 'src/ui-config/queries';
1313import { getProvider } from 'src/utils/marketsAndNetworksConfig' ;
1414
1515import { TxActionsWrapper } from '../TxActionsWrapper' ;
16+ import { pollVoteStatus , RelayError , submitRelayVote } from './temporary/voteRelayClient' ;
1617import { VotingMachineService } from './temporary/VotingMachineService' ;
1718
1819export const baseSlots = {
@@ -180,27 +181,6 @@ const getVotingBalanceProofs = (
180181 ) ;
181182} ;
182183
183- const GELATO_TASK_STATUS_URL = 'https://api.gelato.digital/tasks/status' ;
184-
185- // Poll Gelato's public task status until the sponsored vote is mined. This endpoint
186- // needs no key — only the relay call itself is authenticated (server-side).
187- const waitForRelayedTx = async ( taskId : string ) : Promise < string > => {
188- const maxAttempts = 40 ; // ~2 min at 3s intervals
189- for ( let attempt = 0 ; attempt < maxAttempts ; attempt ++ ) {
190- await new Promise ( ( resolve ) => setTimeout ( resolve , 3000 ) ) ;
191- const res = await fetch ( `${ GELATO_TASK_STATUS_URL } /${ taskId } ` ) ;
192- if ( ! res . ok ) continue ;
193- const { task } = await res . json ( ) ;
194- if ( task ?. taskState === 'ExecSuccess' && task . transactionHash ) {
195- return task . transactionHash as string ;
196- }
197- if ( task ?. taskState === 'ExecReverted' || task ?. taskState === 'Cancelled' ) {
198- throw new Error ( `Relayed vote ${ task . taskState } ` ) ;
199- }
200- }
201- throw new Error ( 'Timed out waiting for the relayed vote' ) ;
202- } ;
203-
204184export const GovVoteActions = ( {
205185 isWrongNetwork,
206186 blocked,
@@ -221,7 +201,7 @@ export const GovVoteActions = ({
221201 const votingMachineAddress =
222202 governanceV3Config . votingChainConfig [ votingChainId ] . votingMachineAddress ;
223203
224- const withGelatoRelayer = process . env . NEXT_PUBLIC_ENABLE_GASLESS_VOTING === 'true' ;
204+ const withGaslessVoting = process . env . NEXT_PUBLIC_ENABLE_GASLESS_VOTING === 'true' ;
225205
226206 const assets : Array < { underlyingAsset : string ; isWithDelegatedPower : boolean } > = [ ] ;
227207
@@ -246,84 +226,93 @@ export const GovVoteActions = ({
246226 } ) ;
247227 }
248228
229+ // Self-paid vote: the connected wallet sends `submitVote` and pays gas. Also the
230+ // fallback when the sponsored relay is unavailable.
231+ const submitSelfPaidVote = async ( proofs : Awaited < ReturnType < typeof getVotingBalanceProofs > > ) => {
232+ const votingMachineService = new VotingMachineService ( votingMachineAddress ) ;
233+ const tx = await votingMachineService . generateSubmitVoteTxData (
234+ user ,
235+ proposalId ,
236+ support ,
237+ proofs
238+ ) ;
239+
240+ const txWithEstimatedGas = await estimateGasLimit ( tx , votingChainId ) ;
241+
242+ const response = await sendTx ( txWithEstimatedGas ) ;
243+ await response . wait ( 1 ) ;
244+ setMainTxState ( {
245+ txHash : response . hash ,
246+ loading : false ,
247+ success : true ,
248+ } ) ;
249+
250+ queryClient . invalidateQueries ( { queryKey : queryKeysFactory . governanceCache } ) ;
251+ } ;
252+
249253 const action = async ( ) => {
250254 setMainTxState ( { ...mainTxState , loading : true } ) ;
251255 try {
252256 const proofs = await getVotingBalanceProofs ( user , assets , ChainId . mainnet , blockHash ) ;
253257
254- const votingMachineService = new VotingMachineService ( votingMachineAddress ) ;
255-
256- if ( withGelatoRelayer ) {
257- const toSign = generateSubmitVoteSignature (
258- votingChainId ,
259- votingMachineAddress ,
260- proposalId ,
261- user ,
262- support ,
263- assets . map ( ( elem ) => ( {
264- underlyingAsset : elem . underlyingAsset ,
265- slot : getVoteBalanceSlot (
266- elem . underlyingAsset ,
267- elem . isWithDelegatedPower ,
268- governanceV3Config . votingAssets . aAaveTokenAddress ,
269- assetsBalanceSlots
270- ) ,
271- } ) )
272- ) ;
273- const signature = await signTxData ( toSign ) ;
274-
275- const tx = await votingMachineService . generateSubmitVoteBySignatureTxData (
276- user ,
277- proposalId ,
278- support ,
279- proofs ,
280- signature . toString ( )
281- ) ;
282-
283- const relayResponse = await fetch ( '/api/gelato/relay' , {
284- method : 'POST' ,
285- headers : { 'Content-Type' : 'application/json' } ,
286- body : JSON . stringify ( {
258+ if ( withGaslessVoting ) {
259+ try {
260+ // Sign over the assets + slots only; the proof bytes are sent but not signed.
261+ const toSign = generateSubmitVoteSignature (
262+ votingChainId ,
263+ votingMachineAddress ,
264+ proposalId ,
265+ user ,
266+ support ,
267+ assets . map ( ( elem ) => ( {
268+ underlyingAsset : elem . underlyingAsset ,
269+ slot : getVoteBalanceSlot (
270+ elem . underlyingAsset ,
271+ elem . isWithDelegatedPower ,
272+ governanceV3Config . votingAssets . aAaveTokenAddress ,
273+ assetsBalanceSlots
274+ ) ,
275+ } ) )
276+ ) ;
277+ const signature = await signTxData ( toSign ) ;
278+
279+ // The relay encodes `submitVoteBySignature` itself — send raw proofs + signature.
280+ const accepted = await submitRelayVote ( {
287281 chainId : votingChainId ,
288- target : votingMachineAddress ,
289- data : tx . data ,
290- } ) ,
291- } ) ;
292-
293- if ( ! relayResponse . ok ) {
294- throw new Error ( 'Relay request failed' ) ;
282+ proposalId,
283+ voter : user ,
284+ support,
285+ votingBalanceProofs : proofs ,
286+ signature : signature . toString ( ) ,
287+ } ) ;
288+
289+ const txHash = await pollVoteStatus ( accepted . transactionId , accepted . transactionHash ) ;
290+
291+ setMainTxState ( {
292+ txHash,
293+ loading : false ,
294+ success : true ,
295+ } ) ;
296+
297+ // The relay indexes the VoteEmitted event asynchronously, so this immediate
298+ // refetch usually races ahead of the indexer. Refetch again after a short
299+ // delay to pick up the freshly-cast vote.
300+ queryClient . invalidateQueries ( { queryKey : queryKeysFactory . governanceCache } ) ;
301+ setTimeout ( ( ) => {
302+ queryClient . invalidateQueries ( { queryKey : queryKeysFactory . governanceCache } ) ;
303+ } , 8000 ) ;
304+ return ;
305+ } catch ( err ) {
306+ // Relayer temporarily down — fall back to a self-paid vote. Any other relay
307+ // error (bad signature, already voted, simulation reverted, vote in flight)
308+ // is surfaced to the user rather than silently retried.
309+ if ( ! ( err instanceof RelayError && err . code === 'RELAYER_UNAVAILABLE' ) ) {
310+ throw err ;
311+ }
295312 }
296-
297- const { taskId } = await relayResponse . json ( ) ;
298- const txHash = await waitForRelayedTx ( taskId ) ;
299-
300- setMainTxState ( {
301- txHash,
302- loading : false ,
303- success : true ,
304- } ) ;
305-
306- queryClient . invalidateQueries ( { queryKey : queryKeysFactory . governanceCache } ) ;
307- } else {
308- const tx = await votingMachineService . generateSubmitVoteTxData (
309- user ,
310- proposalId ,
311- support ,
312- proofs
313- ) ;
314-
315- const txWithEstimatedGas = await estimateGasLimit ( tx , votingChainId ) ;
316-
317- const response = await sendTx ( txWithEstimatedGas ) ;
318- await response . wait ( 1 ) ;
319- setMainTxState ( {
320- txHash : response . hash ,
321- loading : false ,
322- success : true ,
323- } ) ;
324-
325- queryClient . invalidateQueries ( { queryKey : queryKeysFactory . governanceCache } ) ;
326313 }
314+
315+ await submitSelfPaidVote ( proofs ) ;
327316 } catch ( err ) {
328317 setTxError ( getErrorTextFromError ( err as Error , TxAction . MAIN_ACTION , false ) ) ;
329318 setMainTxState ( {
0 commit comments