From a1c2426153b3f498513d6006a7a586578126837c Mon Sep 17 00:00:00 2001 From: Martin Stefcek Date: Wed, 22 Jan 2025 10:05:43 +0100 Subject: [PATCH] feat: add zk login --- .env.example | 5 + .github/workflows/deploy.yml | 7 +- app/GlobalStateContext.tsx | 19 +- app/page.tsx | 1 + components/cloud-view.tsx | 572 ++++++++++++++++------------ components/dashboard.tsx | 37 +- components/login-register-modal.tsx | 146 ++++--- components/user-profile-icon.tsx | 5 +- lib/atoma.ts | 53 ++- lib/zklogin.ts | 281 ++++++++++++++ package-lock.json | 10 + package.json | 1 + pages/auth/google.ts | 27 ++ 13 files changed, 831 insertions(+), 333 deletions(-) create mode 100644 lib/zklogin.ts create mode 100644 pages/auth/google.ts diff --git a/.env.example b/.env.example index 3aed6a3..674bc04 100644 --- a/.env.example +++ b/.env.example @@ -2,3 +2,8 @@ ACME_EMAIL= NEXT_PUBLIC_PROXY_URL= NEXT_PUBLIC_PROXY_WALLET= NEXT_PUBLIC_USDC_TYPE= # e.g. for testnet '0xa1ec7fc00a6f40db9693ad1415d0c193ad3906494428cf252621037bd7117e29::usdc::USDC' +NEXT_PUBLIC_SUI_RPC_URL= +NEXT_PUBLIC_PROVER_URL= +NEXT_PUBLIC_GOOGLE_OAUTH_SCOPE="openid email profile" +NEXT_PUBLIC_GOOGLE_CLIENT_ID= +NEXT_PUBLIC_GOOGLE_REDIRECT_URI="/auth/google" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6c79fd7..e808e10 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -33,8 +33,13 @@ jobs: - name: Build env: NEXT_PUBLIC_PROXY_URL: "https://credentials.atoma.network" - NEXT_PUBLIC_USDC_TYPE: "0xa1ec7fc00a6f40db9693ad1415d0c193ad3906494428cf252621037bd7117e29::usdc::USDC" NEXT_PUBLIC_PROXY_WALLET: "0xec2c53f7c706e37518afedb71bbe46021fb5b1ab1c2a56754541120cac8d7a9e" + NEXT_PUBLIC_USDC_TYPE: "0xa1ec7fc00a6f40db9693ad1415d0c193ad3906494428cf252621037bd7117e29::usdc::USDC" + NEXT_PUBLIC_SUI_RPC_URL: "https://fullnode.testnet.sui.io:443" + NEXT_PUBLIC_PROVER_URL: "http://77.37.96.94:5001" + NEXT_PUBLIC_GOOGLE_OAUTH_SCOPE: "openid email profile" + NEXT_PUBLIC_GOOGLE_CLIENT_ID: "135471414073-41r9t89rejgfr6bc9aptjpm75o4oedk2.apps.googleusercontent.com" + NEXT_PUBLIC_GOOGLE_REDIRECT_URI: "https://cloud.atoma.network/auth/google" run: npm run build - name: Upload artifact diff --git a/app/GlobalStateContext.tsx b/app/GlobalStateContext.tsx index c9647ed..1d27e71 100644 --- a/app/GlobalStateContext.tsx +++ b/app/GlobalStateContext.tsx @@ -1,27 +1,34 @@ import { createContext, useContext, useEffect, useState, type ReactNode } from "react"; import ModalError from "./ModalError"; +import ZkLogin from "@/lib/zklogin"; + + +export type LoginState = 'loggedIn' | 'loggedOut' | 'loggingIn'; interface GlobalState { - isLoggedIn: boolean; - setIsLoggedIn: (isLoggedIn: boolean) => void; + logState: LoginState; + setLogState: (logState: LoginState) => void; error?: string; setError: (error: string) => void; + zkLogin: ZkLogin; } const GlobalStateContext = createContext(undefined); export const GlobalStateProvider = ({ children }: { children: ReactNode }) => { - const [isLoggedIn, setIsLoggedIn] = useState(false); + const [logState, setLogState] = useState('loggingIn'); const [error, setError] = useState(undefined); + const [zkLogin] = useState(new ZkLogin(setLogState)); useEffect(() => { - if (localStorage.getItem("access_token")) { - setIsLoggedIn(true); + if (localStorage.getItem("access_token") && !localStorage.getItem("id_token")) { + // This is when the user is logged in but not via zklogin. Zklogin will check the state of the user and set the state accordingly. + setLogState('loggedIn'); } }, []); return ( - + {children} {error && setError(undefined)}/>} diff --git a/app/page.tsx b/app/page.tsx index bbacf5e..52cba08 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -23,6 +23,7 @@ export default function Home() { + diff --git a/components/cloud-view.tsx b/components/cloud-view.tsx index 1851216..20e90f9 100644 --- a/components/cloud-view.tsx +++ b/components/cloud-view.tsx @@ -1,53 +1,60 @@ -"use client" - -import { useState, useMemo, useEffect} from "react" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Input } from "@/components/ui/input" -import { Button } from "@/components/ui/button" -import { Cloud, Key, CreditCardIcon, BookOpen, Calculator, Info, Copy, CheckCircle2, ArrowRight } from 'lucide-react' -import { Delete } from 'lucide-react' -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +"use client"; + +import { useState, useMemo, useEffect } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Cloud, Key, CreditCardIcon, BookOpen, Calculator, Info, Copy, CheckCircle2, ArrowRight } from "lucide-react"; +import { Delete } from "lucide-react"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; // import { Progress } from "@/components/ui/progress" -import { Label } from "@/components/ui/label" -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion" -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip" +import { Label } from "@/components/ui/label"; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; // import { Switch } from "@/components/ui/switch" -import { Slider } from "@/components/ui/slider" -import { LoginRegisterModal } from "./login-register-modal" +import { Slider } from "@/components/ui/slider"; +import { LoginRegisterModal } from "./login-register-modal"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { ComputeUnitsPayment } from "./compute-units-payment"; +import { + generateApiKey, + getAllStacks, + getBalance, + getSubscriptions, + getSuiAddress, + getTasks, + listApiKeys, + ModelModality, + payUSDC, + proofRequest, + revokeApiToken, + usdcPayment, + type NodeSubscription, + type Task, +} from "@/lib/atoma"; +import { useGlobalState } from "@/app/GlobalStateContext"; import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" -import { ComputeUnitsPayment } from "./compute-units-payment" -import { generateApiKey, getAllStacks, getBalance, getSubscriptions, getSuiAddress, getTasks, listApiKeys, ModelModality, payUSDC, proofRequest, revokeApiToken, usdcPayment, type NodeSubscription, type Task } from "@/lib/atoma" -import { useGlobalState } from "@/app/GlobalStateContext" -import { ConnectModal, useCurrentAccount, useCurrentWallet, useSignAndExecuteTransaction, useSignPersonalMessage, useSuiClient} from "@mysten/dapp-kit" -import { GetApiSample } from "@/components/ui/GetApiSample" -import Image from "next/image" -import { formatNumber, simplifyModelName } from "@/lib/utils" - -type TabType = 'compute' | 'models' | 'api' | 'billing' | 'docs' | 'calculator'; + ConnectModal, + useCurrentAccount, + useCurrentWallet, + useSignAndExecuteTransaction, + useSignPersonalMessage, + useSuiClient, +} from "@mysten/dapp-kit"; +import { toBase64 } from "@mysten/sui/utils"; +import { GetApiSample } from "@/components/ui/GetApiSample"; +import Image from "next/image"; +import { formatNumber, simplifyModelName } from "@/lib/utils"; + +type TabType = "compute" | "models" | "api" | "billing" | "docs" | "calculator"; const tabs = [ - { id: 'compute', icon: Cloud, label: 'Models' }, - { id: 'api', icon: Key, label: 'API Access' }, - { id: 'billing', icon: CreditCardIcon, label: 'Billing' }, - { id: 'docs', icon: BookOpen, label: 'Documentation' }, - { id: 'calculator', icon: Calculator, label: 'Cost Calculator' }, + { id: "compute", icon: Cloud, label: "Models" }, + { id: "api", icon: Key, label: "API Access" }, + { id: "billing", icon: CreditCardIcon, label: "Billing" }, + { id: "docs", icon: BookOpen, label: "Documentation" }, + { id: "calculator", icon: Calculator, label: "Cost Calculator" }, ] as const; interface IUsageHistory { @@ -59,57 +66,56 @@ interface IUsageHistory { model: string; } - const apiEndpoints = [ { name: "Chat Completions", endpoint: "/v1/chat/completions", method: "POST", - example: , + example: , }, { name: "Images Generations", endpoint: "/v1/images/generations", method: "POST", - example: , + example: , }, { name: "Embeddings", endpoint: "/v1/embeddings", method: "POST", - example: , + example: , }, ]; interface IModelOptions { - id: string; - name: string; - modality:ModelModality[] - features: string[]; - pricePer1MTokens: number; - status: string; + id: string; + name: string; + modality: ModelModality[]; + features: string[]; + pricePer1MTokens: number; + status: string; } -const modalityToFeatureName = (modality: ModelModality): string => { +const modalityToFeatureName = (modality: ModelModality): string => { switch (modality) { case ModelModality.ChatCompletions: - return 'Chat Completion'; + return "Chat Completion"; case ModelModality.ImagesGenerations: - return 'Image Generation'; + return "Image Generation"; case ModelModality.Embeddings: - return 'Embeddings'; + return "Embeddings"; default: return modality; } -} +}; export function CloudView() { - const [activeTab, setActiveTab] = useState('compute') + const [activeTab, setActiveTab] = useState("compute"); // const [privacyEnabled, setPrivacyEnabled] = useState(false) - const [isLoginModalOpen, setIsLoginModalOpen] = useState(false) - const [isComputeUnitsModalOpen, setIsComputeUnitsModalOpen] = useState(false) - const [selectedModelForPayment, setSelectedModelForPayment] = useState(null) - const [apiKeys, setApiKeys] = useState(); + const [isLoginModalOpen, setIsLoginModalOpen] = useState(false); + const [isComputeUnitsModalOpen, setIsComputeUnitsModalOpen] = useState(false); + const [selectedModelForPayment, setSelectedModelForPayment] = useState(null); + const [apiKeys, setApiKeys] = useState(); const [subscribers, setSubscribers] = useState(); const [tasks, setTasks] = useState(); const [modelOptions, setModelOptions] = useState([]); @@ -118,50 +124,64 @@ export function CloudView() { const [exampleUsage, setExampleUsage] = useState(apiEndpoints[0].example); const [modelModalities, setModelModalities] = useState>(new Map()); const [partialBalance, setPartialBalance] = useState(); - const { isLoggedIn, setIsLoggedIn } = useGlobalState(); + const { logState, setLogState } = useGlobalState(); - console.log('balance', balance) - console.log('partialBalance', partialBalance) + console.log("balance", balance); + console.log("partialBalance", partialBalance); useEffect(() => { - getTasks().then((tasks_with_modalities) => { - const tasks = tasks_with_modalities.map((task) => task[0]); - setModelModalities(new Map(tasks_with_modalities.filter(([task]) => task.model_name !== undefined).map(([task, modalities]) => [task.model_name!, modalities]))); - // setExampleModels(apiEndpoints.map((endpoint) => tasks_with_modalities.find(([task, modalities])=> modalities.includes(endpoint.name as ModelModality) && task.model_name)?.[0].model_name || "$MODEL_NAME")); - if (isLoggedIn) { - getAllStacks().then((stacks) => { - console.log('stacks', stacks) - let partialBalance = 0; - stacks.forEach(([stack]) => { - partialBalance += stack.already_computed_units / stack.num_compute_units * stack.price_per_one_million_compute_units; - }); - setPartialBalance(partialBalance); - setUsageHistory( - stacks.map(([stack, timestamp]) => { - return { - id: stack.stack_id, - date: new Date(timestamp), - tokens: stack.num_compute_units, - used_tokens: stack.already_computed_units, - cost: (stack.num_compute_units / 1000000) * (stack.price_per_one_million_compute_units / 1000000), - model: tasks.find((task) => task.task_small_id === stack.task_small_id)?.model_name || "Unknown", - }; + getTasks() + .then((tasks_with_modalities) => { + const tasks = tasks_with_modalities.map((task) => task[0]); + setModelModalities( + new Map( + tasks_with_modalities + .filter(([task]) => task.model_name !== undefined) + .map(([task, modalities]) => [task.model_name!, modalities]) + ) + ); + // setExampleModels(apiEndpoints.map((endpoint) => tasks_with_modalities.find(([task, modalities])=> modalities.includes(endpoint.name as ModelModality) && task.model_name)?.[0].model_name || "$MODEL_NAME")); + if (logState === "loggedIn") { + getAllStacks() + .then((stacks) => { + console.log("stacks", stacks); + let partialBalance = 0; + stacks.forEach(([stack]) => { + partialBalance += + (stack.already_computed_units / stack.num_compute_units) * stack.price_per_one_million_compute_units; + }); + setPartialBalance(partialBalance); + setUsageHistory( + stacks.map(([stack, timestamp]) => { + return { + id: stack.stack_id, + date: new Date(timestamp), + tokens: stack.num_compute_units, + used_tokens: stack.already_computed_units, + cost: (stack.num_compute_units / 1000000) * (stack.price_per_one_million_compute_units / 1000000), + model: tasks.find((task) => task.task_small_id === stack.task_small_id)?.model_name || "Unknown", + }; + }) + ); }) - ); - }).catch(() => { - setIsLoggedIn(false); - }); - } - }).catch((err:Response) => { - console.error(err); - }); + .catch(() => { + setLogState("loggedOut"); + }); + } + }) + .catch((err: Response) => { + console.error(err); + }); getSubscriptions().then((subscriptions) => { setSubscribers(subscriptions); }); getTasks().then((tasks_with_modalities) => { const tasks = tasks_with_modalities.map((task) => task[0]); - setTasks(tasks) + setTasks(tasks); }); - if (!isLoggedIn) { + }, [logState, setLogState]); + + useEffect(() => { + if (logState !== "loggedIn") { return; } getBalance() @@ -174,58 +194,67 @@ export function CloudView() { listApiKeys() .then((keys) => setApiKeys(keys)) .catch(() => { - setIsLoggedIn(false); + setLogState("loggedOut"); }); - }, [isLoggedIn, setIsLoggedIn]); + }, [logState, setLogState]); useEffect(() => { if (!subscribers || !tasks) return; - const availableModels : Record = {} + const availableModels: Record = {}; for (const task of tasks) { if (!task.model_name || task.is_deprecated) { continue; } - const subs_for_this_task = subscribers.filter((subscription) => subscription.task_small_id === task.task_small_id && subscription.valid); + const subs_for_this_task = subscribers.filter( + (subscription) => subscription.task_small_id === task.task_small_id && subscription.valid + ); if (subs_for_this_task.length === 0) { // No valid subscriptions for this task continue; } if (task.model_name in availableModels) { - availableModels[task.model_name] = subs_for_this_task.reduce((min, item) => item.price_per_one_million_compute_units < min.price_per_one_million_compute_units ? item : min, availableModels[task.model_name]) + availableModels[task.model_name] = subs_for_this_task.reduce( + (min, item) => + item.price_per_one_million_compute_units < min.price_per_one_million_compute_units ? item : min, + availableModels[task.model_name] + ); } else { - availableModels[task.model_name] = subs_for_this_task.reduce((min, item) => item.price_per_one_million_compute_units < min.price_per_one_million_compute_units ? item : min, subs_for_this_task[0]) + availableModels[task.model_name] = subs_for_this_task.reduce( + (min, item) => + item.price_per_one_million_compute_units < min.price_per_one_million_compute_units ? item : min, + subs_for_this_task[0] + ); } } - setModelOptions(Object.keys(availableModels).map((model) => ( - { + setModelOptions( + Object.keys(availableModels).map((model) => ({ id: model, name: model, modality: modelModalities.get(model) || [], features: modelModalities?.get(model)?.map((modality) => modalityToFeatureName(modality)) || [], pricePer1MTokens: availableModels[model].price_per_one_million_compute_units, - status: 'Available' - }) - )) - }, [subscribers, tasks, modelModalities]) + status: "Available", + })) + ); + }, [subscribers, tasks, modelModalities]); const addApiKey = () => { generateApiKey().then(() => listApiKeys().then((keys) => setApiKeys(keys))); - } + }; - const revokeToken = (token:string) => { + const revokeToken = (token: string) => { revokeApiToken(token).then(() => listApiKeys().then((keys) => setApiKeys(keys))); - } - + }; const getAdjustedPrice = (pricePerMillion: number) => { - return pricePerMillion / 1000000 // Convert from USDC (6 decimals) to USD + return pricePerMillion / 1000000; // Convert from USDC (6 decimals) to USD // return (privacyEnabled ? pricePerMillion * 1.05 : pricePerMillion) / 1000000 // Convert from USDC (6 decimals) to USD - } + }; - const handleStartUsing = (model:IModelOptions) => { - setSelectedModelForPayment(model) - setIsComputeUnitsModalOpen(true) - } + const handleStartUsing = (model: IModelOptions) => { + setSelectedModelForPayment(model); + setIsComputeUnitsModalOpen(true); + }; const ComputeTab = () => (
@@ -247,7 +276,10 @@ export function CloudView() {
{modelOptions.map((model) => ( - +
@@ -294,7 +326,7 @@ export function CloudView() {
Price: - ${getAdjustedPrice(model.pricePer1MTokens).toFixed(2)} / 1M {model.id === 'flux1' ? 'MP' : 'tokens'} + ${getAdjustedPrice(model.pricePer1MTokens).toFixed(2)} / 1M {model.id === "flux1" ? "MP" : "tokens"}
@@ -313,10 +345,10 @@ export function CloudView() { -
- ) + ); const ApiTab = () => (
@@ -345,44 +377,47 @@ export function CloudView() {
-

- API Key -

- {isLoggedIn? - Array.isArray(apiKeys) ? ( - <> - {apiKeys.map((apiKey) => { - return ( -
- - - + +
+ ); + })} + {apiKeys.length === 0 && ( + <> +
No API key generated yet
+ -
- ); - })} - {apiKeys.length === 0 && ( - <> -
No API key generated yet
- - - )} - + + )} + + ) : ( +
Loading
+ ) + ) : logState === "loggingIn" ? ( + "..." ) : ( -
Loading
- ):"N/A"} + "N/A" + )}
@@ -409,9 +444,7 @@ export function CloudView() {

Example Usage

-
-                {exampleUsage}
-            
+
{exampleUsage}
@@ -422,36 +455,37 @@ export function CloudView() { const now = new Date(); const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999); - const totalUsedTokens =isLoggedIn? usageHistory.reduce((sum, item) => sum + (item.date >= startOfMonth ? item.used_tokens : 0), 0) - : 0; + const totalUsedTokens = logState === 'loggedIn' + ? usageHistory.reduce((sum, item) => sum + (item.date >= startOfMonth ? item.used_tokens : 0), 0) + : 0; // const totalUsage = isLoggedIn // ? usageHistory.reduce((sum, item) => sum + (item.date >= startOfMonth ? item.tokens : 0), 0) // : 0; // const totalCost = isLoggedIn ? usageHistory.reduce((sum, item) => sum + item.cost, 0) : 0; // const [isNewCardModalOpen, setIsNewCardModalOpen] = useState(false) // const [isAutoReloadEnabled, setIsAutoReloadEnabled] = useState(false) - const [isAddFundsModalOpen, setIsAddFundsModalOpen] = useState(false) + const [isAddFundsModalOpen, setIsAddFundsModalOpen] = useState(false); // const [cardNumber, setCardNumber] = useState(null); - + const AddFundsModal = () => { const [amount, setAmount] = useState(10); const [step, setStep] = useState<"payment" | "amount" | "wallet" | "result" | "confirmed">("payment"); const [walletConfirmed, setWalletConfirmed] = useState(false); + const [handlingPayment, setHandlingPayment] = useState(false); const { connectionStatus } = useCurrentWallet(); - // const { isLoggedIn } = useGlobalState(); const { mutateAsync: signAndExecuteTransaction } = useSignAndExecuteTransaction(); const { mutateAsync: signPersonalMessage } = useSignPersonalMessage(); - const { setError } = useGlobalState(); + const { setError, zkLogin } = useGlobalState(); const suiClient = useSuiClient(); const account = useCurrentAccount(); - + useEffect(() => { - if (!isLoggedIn) { + if (logState !== 'loggedIn') { return; } getSuiAddress().then((suiAddress) => { // console.log('suiAddress', suiAddress) - setWalletConfirmed(suiAddress != null && suiAddress == account?.address) + setWalletConfirmed(suiAddress != null && suiAddress == account?.address); }); }, [account]); @@ -462,15 +496,20 @@ export function CloudView() { const access_token = localStorage.getItem("access_token"); let user_id; if (access_token) { - const base64Url = access_token.split('.')[1]; - const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); - const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) { - return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); - }).join('')); + const base64Url = access_token.split(".")[1]; + const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); + const jsonPayload = decodeURIComponent( + atob(base64) + .split("") + .map(function (c) { + return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); + }) + .join("") + ); const token_json = JSON.parse(jsonPayload); user_id = token_json.user_id; } - + signPersonalMessage({ account, message: new TextEncoder().encode( @@ -482,42 +521,81 @@ export function CloudView() { setStep("confirmed"); setWalletConfirmed(true); }) - .catch((error:Response) => { - setError(`${error.status} : ${error.statusText}`); + .catch((error: Response) => { + setError(`${error.status} : ${error.statusText}`); console.error(error); }); }); - } - + }; + const handleUSDCPayment = async (amount: number) => { + setHandlingPayment(true); + console.log("zklogin", zkLogin.isEnabled); + if (zkLogin.isEnabled) { + zkLogin + .payUSDC(amount * 1000000, suiClient) + .then((res) => { + console.log(res); + const txDigest = res.digest; + // const txDigest = "ASp9K5Ms1HS1sKW2H4oa4Q9q6Zz3kBqKUn3x9JbZcGsw"; + console.log("txDigest", txDigest); + zkLogin.signMessage(txDigest).then((proofSignature) => { + console.log("proofSignature", proofSignature); + setTimeout(() => { + usdcPayment(txDigest, proofSignature) + .then(() => { + setStep("result"); + }) + .catch((error: Response) => { + setError(`${error.status} : ${error.statusText}`); + console.error(error); + }); + }, 1000); + }); + }) + .catch((error) => { + console.error(error); + setError(`${error}`); + }); + setHandlingPayment(false); + return; + } if (account == null) { + setHandlingPayment(false); return; } - + try { const suiAddress = await getSuiAddress(); if (suiAddress == null || suiAddress != account?.address) { // We haven't proven the SUI address yet throw new Error("SUI address not found or not matching"); } - payUSDC(amount * 1000000, suiClient, signAndExecuteTransaction, account).then((res: unknown) => { - const txDigest = (res as { digest: string }).digest; - setTimeout(() => { - usdcPayment(txDigest).then(() => { - setStep("result"); - }).catch((error:Response) => { - setError(`${error.status} : ${error.statusText}`); - console.error(error) - }); - }, 1000); - }).catch((error) => { - console.error(error) - setError(`${error}`); - }); + payUSDC(amount * 1000000, suiClient, signAndExecuteTransaction, account) + .then((res: unknown) => { + const txDigest = (res as { digest: string }).digest; + setTimeout(() => { + usdcPayment(txDigest) + .then(() => { + setStep("result"); + }) + .catch((error: Response) => { + setError(`${error.status} : ${error.statusText}`); + console.error(error); + }); + }, 1000); + }) + .catch((error) => { + console.error(error); + setError(`${error}`); + }); } catch { handleConfirmWallet(); } - } + finally { + setHandlingPayment(false); + } + }; return ( @@ -582,6 +663,7 @@ export function CloudView() { @@ -600,6 +682,7 @@ export function CloudView() { @@ -632,17 +715,24 @@ export function CloudView() {

- {isLoggedIn - ? `$${balance !== undefined && partialBalance !== undefined ? ((balance+partialBalance) / 1000000).toFixed(2) : "Loading"}` + {logState === "loggedIn" + ? `$${ + balance !== undefined && partialBalance !== undefined + ? ((balance + partialBalance) / 1000000).toFixed(2) + : "Loading" + }` + : logState === "loggingIn" + ? "..." : "N/A"}

Remaining Balance

{/*
@@ -726,16 +816,18 @@ export function CloudView() { - {isLoggedIn && + {logState === 'loggedIn' && usageHistory.map((item) => ( - {item.date.toLocaleDateString(undefined, { dateStyle: "long" })} + + {item.date.toLocaleDateString(undefined, { dateStyle: "long" })} + {item.model} {item.used_tokens.toLocaleString()} - ${(item.cost*item.used_tokens/item.tokens).toFixed(2)} + ${((item.cost * item.used_tokens) / item.tokens).toFixed(2)} ))} @@ -781,22 +873,25 @@ export function CloudView() {
); - } + }; const DocsTab = () => ( -