-
Notifications
You must be signed in to change notification settings - Fork 147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: session key demo: improve timers, styles, and error handling #1368
Changes from 4 commits
b78421c
06f4c50
59e7208
2334c37
dab13e0
f408ede
ea450f8
e4482f2
1b6942c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
import { useState } from "react"; | ||
import { useEffect, useState } from "react"; | ||
import type { BatchUserOperationCallData } from "@aa-sdk/core"; | ||
import { | ||
encodeFunctionData, | ||
|
@@ -24,6 +24,7 @@ import { erc20MintableAbi } from "./7702/dca/abi/erc20Mintable"; | |
import { genEntityId } from "./7702/genEntityId"; | ||
import { odyssey, splitOdysseyTransport } from "./7702/transportSetup"; | ||
import { SESSION_KEY_VALIDITY_TIME_SECONDS } from "./7702/constants"; | ||
import { useToast } from "@/hooks/useToast"; | ||
|
||
export type CardStatus = "initial" | "setup" | "active" | "done"; | ||
|
||
|
@@ -32,11 +33,13 @@ export type TransactionType = { | |
state: TransactionStages; | ||
buyAmountUsdc: number; | ||
externalLink?: string; | ||
timeToBuy?: number; // timestamp when the txn will initiate | ||
secUntilBuy?: number; // seconds until the txn will initiate | ||
}; | ||
|
||
export const initialTransactions: TransactionType[] = [ | ||
{ | ||
state: "initiating", | ||
state: "initial", | ||
buyAmountUsdc: 4000, | ||
}, | ||
{ | ||
|
@@ -87,22 +90,69 @@ export const useRecurringTransactions = ({ | |
}, | ||
}); | ||
|
||
const handleTransaction = async (transactionIndex: number) => { | ||
setTransactions((prev) => { | ||
const newState = [...prev]; | ||
newState[transactionIndex].state = "initiating"; | ||
if (transactionIndex + 1 < newState.length) { | ||
newState[transactionIndex + 1].state = "next"; | ||
} | ||
return newState; | ||
const { setToast } = useToast(); | ||
|
||
const handleError = (error: Error) => { | ||
console.error(error); | ||
setCardStatus("initial"); | ||
setTransactions(initialTransactions); | ||
setToast({ | ||
type: "error", | ||
text: "Something went wrong. Please try again.", | ||
open: true, | ||
}); | ||
}; | ||
|
||
if (!sessionKeyClient) { | ||
console.error("no session key client"); | ||
setCardStatus("initial"); | ||
// Handle the ticking of the timers. | ||
useEffect(() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. related question re: timestamp and interval - could the reveal be handled in the 10s countdown promise? Not super high pri, just curious if we could simplify There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not totally following what you mean. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, we could probably just give each of them the time they should fire at the beginning and set the timeouts to 0, 10, and 20 sec right away. But doing it how it is now also results in waiting extra time before starting the next txn if the current txn takes longer than 10 sec, which I think is nice to have. |
||
if ( | ||
transactions.every( | ||
(it) => it.state === "complete" || it.state === "initial" | ||
) | ||
) { | ||
return; | ||
} | ||
|
||
const interval = setInterval(() => { | ||
setTransactions((prev) => | ||
prev.map((txn) => | ||
txn.state === "next" && txn.timeToBuy | ||
? { | ||
...txn, | ||
secUntilBuy: Math.ceil((txn.timeToBuy - Date.now()) / 1000), | ||
} | ||
: txn | ||
) | ||
); | ||
|
||
if (transactions.every((it) => it.state === "complete")) { | ||
clearInterval(interval); | ||
} | ||
}, 250); | ||
|
||
return () => clearInterval(interval); | ||
}, [transactions]); | ||
|
||
const handleTransaction = async (transactionIndex: number) => { | ||
if (!sessionKeyClient) { | ||
return handleError(new Error("no session key client")); | ||
} | ||
|
||
setTransactions((prev) => | ||
prev.map((txn, idx) => | ||
idx === transactionIndex | ||
? { ...txn, state: "initiating" } | ||
: idx === transactionIndex + 1 | ||
? { | ||
...txn, | ||
state: "next", | ||
timeToBuy: Date.now() + 10_000, | ||
secUntilBuy: 10, | ||
} | ||
: txn | ||
) | ||
); | ||
|
||
const usdcInAmount = transactions[transactionIndex].buyAmountUsdc; | ||
|
||
const uoHash = await sessionKeyClient.sendUserOperation({ | ||
|
@@ -119,46 +169,36 @@ export const useRecurringTransactions = ({ | |
const txnHash = await sessionKeyClient | ||
.waitForUserOperationTransaction(uoHash) | ||
.catch((e) => { | ||
console.log(e); | ||
console.error(e); | ||
}); | ||
|
||
if (!txnHash) { | ||
setCardStatus("initial"); | ||
return; | ||
return handleError(new Error("missing swap txn hash")); | ||
} | ||
|
||
setTransactions((prev) => { | ||
const newState = [...prev]; | ||
newState[transactionIndex].state = "complete"; | ||
newState[transactionIndex].externalLink = odyssey.blockExplorers | ||
? `${odyssey.blockExplorers?.default.url}/tx/${txnHash}` | ||
: undefined; | ||
return newState; | ||
}); | ||
setTransactions((prev) => | ||
prev.map((txn, idx) => | ||
idx === transactionIndex | ||
? { | ||
...txn, | ||
state: "complete", | ||
externalLink: odyssey.blockExplorers | ||
? `${odyssey.blockExplorers?.default.url}/tx/${txnHash}` | ||
: undefined, | ||
} | ||
: txn | ||
) | ||
); | ||
}; | ||
|
||
// Mock method to fire transactions for 7702 | ||
// Mock method to fire transactions | ||
const handleTransactions = async () => { | ||
if (!client) { | ||
console.error("no client"); | ||
return; | ||
} | ||
|
||
// initial state as referenced by `const initialTransactions` is mutated, so we need to re-create it. | ||
setTransactions([ | ||
{ | ||
state: "initiating", | ||
buyAmountUsdc: 4000, | ||
}, | ||
{ | ||
state: "initial", | ||
buyAmountUsdc: 3500, | ||
}, | ||
{ | ||
state: "initial", | ||
buyAmountUsdc: 4200, | ||
}, | ||
]); | ||
setTransactions(initialTransactions); | ||
adamegyed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
setCardStatus("setup"); | ||
|
||
// Start by minting the required USDC amount, and installing the session key, if not already installed. | ||
|
@@ -257,19 +297,23 @@ export const useRecurringTransactions = ({ | |
const txnHash = await client | ||
.waitForUserOperationTransaction(uoHash) | ||
.catch((e) => { | ||
console.log(e); | ||
console.error(e); | ||
}); | ||
|
||
if (!txnHash) { | ||
setCardStatus("initial"); | ||
return; | ||
return handleError(new Error("missing batch txn hash")); | ||
} | ||
|
||
setSessionKeyAdded(true); | ||
setCardStatus("active"); | ||
|
||
for (let i = 0; i < transactions.length; i++) { | ||
await handleTransaction(i); | ||
await Promise.all([ | ||
handleTransaction(i), | ||
...(i < transactions.length - 1 | ||
? [new Promise((resolve) => setTimeout(resolve, 10_000))] | ||
: []), | ||
]); | ||
} | ||
|
||
setCardStatus("done"); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
noob question: why do we need the timestamp here in addition to the countdown? could the countdown also serve as the time to reveal the transaction component's contents?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
timeToBuy
is used to calculate the number of seconds remaining each time the interval ticks. At first I was just trying to only have thetimeToBuy
and NOT include thesecToBuy
here, but I ran into a lot of annoying edge cases where it would be off by 1 due to re-renders, i.e. sometimes it would start from 11 sec and other times from 9 sec. This solution resulted in the cleanest UX, since we explicitly start the timer at 10 sec.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think i found another good solution! will push shortly.