diff --git a/apps/demo_web/src/components/oko_provider/use_oko.ts b/apps/demo_web/src/components/oko_provider/use_oko.ts index 6ffe24c3a..35e54980a 100644 --- a/apps/demo_web/src/components/oko_provider/use_oko.ts +++ b/apps/demo_web/src/components/oko_provider/use_oko.ts @@ -7,17 +7,19 @@ import { useSDKState } from "@oko-wallet-demo-web/state/sdk"; export function useInitOko() { const initOkoCosmos = useSDKState((state) => state.initOkoCosmos); const initOkoEth = useSDKState((state) => state.initOkoEth); - // const initOkoSol = useSDKState((state) => state.initOkoSol); + const initOkoSol = useSDKState((state) => state.initOkoSol); const isInitialized = useSDKState( - (state) => state.oko_cosmos !== null && state.oko_eth !== null, - // state.oko_sol !== null, + (state) => + state.oko_cosmos !== null && + state.oko_eth !== null && + state.oko_sol !== null, ); useEffect(() => { initOkoCosmos(); initOkoEth(); - // initOkoSol(); + initOkoSol(); }, []); return { isInitialized }; diff --git a/apps/demo_web/src/components/preview_panel/preview_panel.tsx b/apps/demo_web/src/components/preview_panel/preview_panel.tsx index ba4207e74..f167a8678 100644 --- a/apps/demo_web/src/components/preview_panel/preview_panel.tsx +++ b/apps/demo_web/src/components/preview_panel/preview_panel.tsx @@ -12,18 +12,17 @@ import { CosmosOnchainSignWidget } from "@oko-wallet-demo-web/components/widgets import { CosmosOffChainSignWidget } from "@oko-wallet-demo-web/components/widgets/cosmos_offchain_sign_widget/cosmos_offchain_sign_widget"; import { EthereumOnchainSignWidget } from "@oko-wallet-demo-web/components/widgets/ethereum_onchain_sign_widget/ethereum_onchain_sign_widget"; import { EthereumOffchainSignWidget } from "@oko-wallet-demo-web/components/widgets/ethereum_offchain_sign_widget/ethereum_offchain_sign_widget"; -// import { SolanaOffchainSignWidget } from "@oko-wallet-demo-web/components/widgets/solana_offchain_sign_widget/solana_offchain_sign_widget"; -// TODO: refactor this @chemonoworld @Ryz0nd -// import { SolanaOnchainSignWidget } from "@oko-wallet-demo-web/components/widgets/solana_onchain_sign_widget/solana_onchain_sign_widget"; +import { SolanaOffchainSignWidget } from "@oko-wallet-demo-web/components/widgets/solana_offchain_sign_widget/solana_offchain_sign_widget"; +import { SolanaOnchainSignWidget } from "@oko-wallet-demo-web/components/widgets/solana_onchain_sign_widget/solana_onchain_sign_widget"; import { useUserInfoState } from "@oko-wallet-demo-web/state/user_info"; import { useSDKState } from "@oko-wallet-demo-web/state/sdk"; export const PreviewPanel: FC = () => { const isLazyInitialized = useSDKState( - (st) => st.isCosmosLazyInitialized && st.isEthLazyInitialized, - // TODO: refactor this @chemonoworld @Ryz0nd - // && - // st.isSolLazyInitialized, + (st) => + st.isCosmosLazyInitialized && + st.isEthLazyInitialized && + st.isSolLazyInitialized, ); const isSignedIn = useUserInfoState((state) => state.isSignedIn); @@ -51,9 +50,8 @@ export const PreviewPanel: FC = () => { )} {isSignedIn && (
- {/* TODO: refactor this @chemonoworld @Ryz0nd */} - {/* */} - {/* */} + +
)} diff --git a/apps/demo_web/src/components/widgets/account_widget/account_info_widget.tsx b/apps/demo_web/src/components/widgets/account_widget/account_info_widget.tsx index 02ba46e62..1b9074421 100644 --- a/apps/demo_web/src/components/widgets/account_widget/account_info_widget.tsx +++ b/apps/demo_web/src/components/widgets/account_widget/account_info_widget.tsx @@ -16,7 +16,8 @@ import type { LoginMethod } from "@oko-wallet-demo-web/types/login"; export type AccountInfoWidgetProps = { type: LoginMethod; email: string; - publicKey: string; + publicKeySecp256k1: string; + publicKeyEd25519: string | null; name: string | null; onSignOut: () => void; }; @@ -24,7 +25,8 @@ export type AccountInfoWidgetProps = { export const AccountInfoWidget: FC = ({ type, email, - publicKey, + publicKeySecp256k1, + publicKeyEd25519, name, onSignOut, }) => { @@ -56,13 +58,36 @@ export const AccountInfoWidget: FC = ({ color="tertiary" className={styles.label} > - Public Key + Public Key (secp256k1) - {publicKey} + {publicKeySecp256k1} + {publicKeyEd25519 && ( + <> + +
+ + Public Key (ed25519) + + + {publicKeyEd25519} + +
+ + )} +
diff --git a/apps/demo_web/src/components/widgets/account_widget/account_widget.tsx b/apps/demo_web/src/components/widgets/account_widget/account_widget.tsx index 8a3b6f87b..b59e42adf 100644 --- a/apps/demo_web/src/components/widgets/account_widget/account_widget.tsx +++ b/apps/demo_web/src/components/widgets/account_widget/account_widget.tsx @@ -28,7 +28,10 @@ export const AccountWidget: FC = () => { status: "ready", }); const email = useUserInfoState((state) => state.email); - const publicKey = useUserInfoState((state) => state.publicKey); + const publicKeySecp256k1 = useUserInfoState( + (state) => state.publicKeySecp256k1, + ); + const publicKeyEd25519 = useUserInfoState((state) => state.publicKeyEd25519); const name = useUserInfoState((state) => state.name); const authType = useUserInfoState((state) => state.authType); const isSignedIn = useUserInfoState((state) => state.isSignedIn); @@ -118,7 +121,8 @@ export const AccountWidget: FC = () => { diff --git a/apps/demo_web/src/components/widgets/address_widget/address_row.tsx b/apps/demo_web/src/components/widgets/address_widget/address_row.tsx index 344f3bed1..d49bd7755 100644 --- a/apps/demo_web/src/components/widgets/address_widget/address_row.tsx +++ b/apps/demo_web/src/components/widgets/address_widget/address_row.tsx @@ -4,10 +4,18 @@ import { Tooltip } from "@oko-wallet/oko-common-ui/tooltip"; import styles from "./address_row.module.scss"; +const chainConfig: Record< + AddressRowProps["chain"], + { label: string; prefix: string } +> = { + ethereum: { label: "Ethereum", prefix: "0x" }, + cosmos: { label: "Cosmos Hub", prefix: "cosmos1" }, + solana: { label: "Solana", prefix: "" }, +}; + export const AddressRow: FC = ({ icon, chain, address }) => { const isLoggedIn = !!address; - const label = chain === "ethereum" ? "Ethereum" : "Cosmos Hub"; - const prefix = chain === "ethereum" ? "0x" : "cosmos1"; + const { label, prefix } = chainConfig[chain]; const renderChainLabel = () => (
@@ -63,6 +71,6 @@ export const AddressRow: FC = ({ icon, chain, address }) => { export interface AddressRowProps { icon: ReactElement; - chain: "ethereum" | "cosmos"; + chain: "ethereum" | "cosmos" | "solana"; address?: string; } diff --git a/apps/demo_web/src/components/widgets/address_widget/address_widget.tsx b/apps/demo_web/src/components/widgets/address_widget/address_widget.tsx index 213f9e256..5f4414357 100644 --- a/apps/demo_web/src/components/widgets/address_widget/address_widget.tsx +++ b/apps/demo_web/src/components/widgets/address_widget/address_widget.tsx @@ -1,6 +1,7 @@ import { useState, type FC } from "react"; import { CosmosIcon } from "@oko-wallet/oko-common-ui/icons/cosmos_icon"; import { EthereumBlueIcon } from "@oko-wallet/oko-common-ui/icons/ethereum_blue_icon"; +import { SolanaIcon } from "@oko-wallet/oko-common-ui/icons/solana_icon"; import { WalletIcon } from "@oko-wallet/oko-common-ui/icons/wallet"; import { Spacing } from "@oko-wallet/oko-common-ui/spacing"; import { Typography } from "@oko-wallet/oko-common-ui/typography"; @@ -15,7 +16,7 @@ import { useGetChainInfos } from "@oko-wallet-demo-web/hooks/use_get_chain_infos export const AddressWidget: FC = ({}) => { const [showModal, setShowModal] = useState(false); - const { cosmosAddress, ethAddress } = useAddresses(); + const { cosmosAddress, ethAddress, solanaAddress } = useAddresses(); const { data: chains } = useGetChainInfos(); @@ -57,6 +58,13 @@ export const AddressWidget: FC = ({}) => { /> + } + chain="solana" + address={formatAddress(solanaAddress)} + /> + +
diff --git a/apps/demo_web/src/components/widgets/solana_offchain_sign_widget/solana_offchain_sign_widget.tsx b/apps/demo_web/src/components/widgets/solana_offchain_sign_widget/solana_offchain_sign_widget.tsx index 76e280e64..4373584e3 100644 --- a/apps/demo_web/src/components/widgets/solana_offchain_sign_widget/solana_offchain_sign_widget.tsx +++ b/apps/demo_web/src/components/widgets/solana_offchain_sign_widget/solana_offchain_sign_widget.tsx @@ -1,38 +1,36 @@ -// TODO: refactor this widget @chemonoworld @Ryz0nd - -// import { SolanaIcon } from "@oko-wallet/oko-common-ui/icons/solana_icon"; - -// import { SignWidget } from "@oko-wallet-demo-web/components/widgets/sign_widget/sign_widget"; -// import { useSDKState } from "@oko-wallet-demo-web/state/sdk"; - -// export const SolanaOffchainSignWidget = () => { -// const okoSol = useSDKState((state) => state.oko_sol); - -// const handleClickSolOffchainSign = async () => { -// if (okoSol === null) { -// throw new Error("okoSol is not initialized"); -// } - -// // Connect if not already connected -// if (!okoSol.connected) { -// await okoSol.connect(); -// } - -// const message = "Welcome to Oko! Try generating an Ed25519 MPC signature."; -// const messageBytes = new TextEncoder().encode(message); - -// const signature = await okoSol.signMessage(messageBytes); - -// // Log signature for demo purposes -// console.log("Solana signature:", Buffer.from(signature).toString("hex")); -// }; - -// return ( -// } -// signType="offchain" -// signButtonOnClick={handleClickSolOffchainSign} -// /> -// ); -// }; +import { SolanaIcon } from "@oko-wallet/oko-common-ui/icons/solana_icon"; + +import { SignWidget } from "@oko-wallet-demo-web/components/widgets/sign_widget/sign_widget"; +import { useSDKState } from "@oko-wallet-demo-web/state/sdk"; + +export const SolanaOffchainSignWidget = () => { + const okoSol = useSDKState((state) => state.oko_sol); + + const handleClickSolOffchainSign = async () => { + if (okoSol === null) { + throw new Error("okoSol is not initialized"); + } + + // Connect if not already connected + if (!okoSol.connected) { + await okoSol.connect(); + } + + const message = "Welcome to Oko! Try generating an Ed25519 MPC signature."; + const messageBytes = new TextEncoder().encode(message); + + const signature = await okoSol.signMessage(messageBytes); + + // Log signature for demo purposes + console.log("Solana signature:", Buffer.from(signature).toString("hex")); + }; + + return ( + } + signType="offchain" + signButtonOnClick={handleClickSolOffchainSign} + /> + ); +}; diff --git a/apps/demo_web/src/components/widgets/solana_onchain_sign_widget/solana_onchain_sign_widget.tsx b/apps/demo_web/src/components/widgets/solana_onchain_sign_widget/solana_onchain_sign_widget.tsx index ac630f71a..5273de680 100644 --- a/apps/demo_web/src/components/widgets/solana_onchain_sign_widget/solana_onchain_sign_widget.tsx +++ b/apps/demo_web/src/components/widgets/solana_onchain_sign_widget/solana_onchain_sign_widget.tsx @@ -1,130 +1,128 @@ -// TODO: refactor this widget @chemonoworld @Ryz0nd - -// import { useCallback, useState } from "react"; -// import { -// Connection, -// PublicKey, -// SystemProgram, -// Transaction, -// TransactionMessage, -// VersionedTransaction, -// LAMPORTS_PER_SOL, -// } from "@solana/web3.js"; -// import { SolanaIcon } from "@oko-wallet/oko-common-ui/icons/solana_icon"; -// import { Checkbox } from "@oko-wallet/oko-common-ui/checkbox"; - -// import styles from "./solana_onchain_sign_widget.module.scss"; -// import { SignWidget } from "@oko-wallet-demo-web/components/widgets/sign_widget/sign_widget"; -// import { useSDKState } from "@oko-wallet-demo-web/state/sdk"; - -// const SOLANA_RPC_URL = "https://api.devnet.solana.com"; - -// export const SolanaOnchainSignWidget = () => { -// const okoSol = useSDKState((state) => state.oko_sol); -// const [isLegacy, setIsLegacy] = useState(false); - -// const handleClickSolOnchainSignV0 = useCallback(async () => { -// if (okoSol === null) { -// throw new Error("okoSol is not initialized"); -// } - -// if (!okoSol.connected) { -// await okoSol.connect(); -// } - -// if (!okoSol.publicKey) { -// throw new Error("No public key available"); -// } - -// const connection = new Connection(SOLANA_RPC_URL); - -// const toAddress = new PublicKey("11111111111111111111111111111111"); - -// const { blockhash } = await connection.getLatestBlockhash(); - -// const instructions = [ -// SystemProgram.transfer({ -// fromPubkey: okoSol.publicKey, -// toPubkey: toAddress, -// lamports: 0.001 * LAMPORTS_PER_SOL, -// }), -// ]; - -// const messageV0 = new TransactionMessage({ -// payerKey: okoSol.publicKey, -// recentBlockhash: blockhash, -// instructions, -// }).compileToV0Message(); - -// const versionedTransaction = new VersionedTransaction(messageV0); - -// const signedTransaction = -// await okoSol.signTransaction(versionedTransaction); - -// console.log( -// "Solana v0 signed transaction:", -// Buffer.from(signedTransaction.signatures[0]).toString("hex"), -// ); -// }, [okoSol]); - -// const handleClickSolOnchainSignLegacy = useCallback(async () => { -// if (okoSol === null) { -// throw new Error("okoSol is not initialized"); -// } - -// if (!okoSol.connected) { -// await okoSol.connect(); -// } - -// if (!okoSol.publicKey) { -// throw new Error("No public key available"); -// } - -// const connection = new Connection(SOLANA_RPC_URL); - -// const toAddress = new PublicKey("11111111111111111111111111111111"); - -// const transaction = new Transaction().add( -// SystemProgram.transfer({ -// fromPubkey: okoSol.publicKey, -// toPubkey: toAddress, -// lamports: 0.001 * LAMPORTS_PER_SOL, -// }), -// ); - -// const { blockhash } = await connection.getLatestBlockhash(); -// transaction.recentBlockhash = blockhash; -// transaction.feePayer = okoSol.publicKey; - -// const signedTransaction = await okoSol.signTransaction(transaction); - -// console.log( -// "Solana legacy signed transaction:", -// signedTransaction.signatures.map((sig) => -// sig.signature ? Buffer.from(sig.signature).toString("hex") : null, -// ), -// ); -// }, [okoSol]); - -// return ( -// } -// signType="onchain" -// signButtonOnClick={ -// isLegacy ? handleClickSolOnchainSignLegacy : handleClickSolOnchainSignV0 -// } -// renderBottom={() => ( -//
-// setIsLegacy((prevState) => !prevState)} -// label="Legacy Transaction" -// /> -//
-// )} -// /> -// ); -// }; +import { useCallback, useState } from "react"; +import { + Connection, + PublicKey, + SystemProgram, + Transaction, + TransactionMessage, + VersionedTransaction, + LAMPORTS_PER_SOL, +} from "@solana/web3.js"; +import { SolanaIcon } from "@oko-wallet/oko-common-ui/icons/solana_icon"; +import { Checkbox } from "@oko-wallet/oko-common-ui/checkbox"; + +import styles from "./solana_onchain_sign_widget.module.scss"; +import { SignWidget } from "@oko-wallet-demo-web/components/widgets/sign_widget/sign_widget"; +import { useSDKState } from "@oko-wallet-demo-web/state/sdk"; + +const SOLANA_RPC_URL = "https://api.devnet.solana.com"; + +export const SolanaOnchainSignWidget = () => { + const okoSol = useSDKState((state) => state.oko_sol); + const [isLegacy, setIsLegacy] = useState(false); + + const handleClickSolOnchainSignV0 = useCallback(async () => { + if (okoSol === null) { + throw new Error("okoSol is not initialized"); + } + + if (!okoSol.connected) { + await okoSol.connect(); + } + + if (!okoSol.publicKey) { + throw new Error("No public key available"); + } + + const connection = new Connection(SOLANA_RPC_URL); + + const toAddress = new PublicKey("11111111111111111111111111111111"); + + const { blockhash } = await connection.getLatestBlockhash(); + + const instructions = [ + SystemProgram.transfer({ + fromPubkey: okoSol.publicKey, + toPubkey: toAddress, + lamports: 0.001 * LAMPORTS_PER_SOL, + }), + ]; + + const messageV0 = new TransactionMessage({ + payerKey: okoSol.publicKey, + recentBlockhash: blockhash, + instructions, + }).compileToV0Message(); + + const versionedTransaction = new VersionedTransaction(messageV0); + + const signedTransaction = + await okoSol.signTransaction(versionedTransaction); + + console.log( + "Solana v0 signed transaction:", + Buffer.from(signedTransaction.signatures[0]).toString("hex"), + ); + }, [okoSol]); + + const handleClickSolOnchainSignLegacy = useCallback(async () => { + if (okoSol === null) { + throw new Error("okoSol is not initialized"); + } + + if (!okoSol.connected) { + await okoSol.connect(); + } + + if (!okoSol.publicKey) { + throw new Error("No public key available"); + } + + const connection = new Connection(SOLANA_RPC_URL); + + const toAddress = new PublicKey("11111111111111111111111111111111"); + + const transaction = new Transaction().add( + SystemProgram.transfer({ + fromPubkey: okoSol.publicKey, + toPubkey: toAddress, + lamports: 0.001 * LAMPORTS_PER_SOL, + }), + ); + + const { blockhash } = await connection.getLatestBlockhash(); + transaction.recentBlockhash = blockhash; + transaction.feePayer = okoSol.publicKey; + + const signedTransaction = await okoSol.signTransaction(transaction); + + console.log( + "Solana legacy signed transaction:", + signedTransaction.signatures.map((sig) => + sig.signature ? Buffer.from(sig.signature).toString("hex") : null, + ), + ); + }, [okoSol]); + + return ( + } + signType="onchain" + signButtonOnClick={ + isLegacy ? handleClickSolOnchainSignLegacy : handleClickSolOnchainSignV0 + } + renderBottom={() => ( +
+ setIsLegacy((prevState) => !prevState)} + label="Legacy Transaction" + /> +
+ )} + /> + ); +}; diff --git a/apps/demo_web/src/hooks/wallet.ts b/apps/demo_web/src/hooks/wallet.ts index 2bfd50035..e59f7ef79 100644 --- a/apps/demo_web/src/hooks/wallet.ts +++ b/apps/demo_web/src/hooks/wallet.ts @@ -1,4 +1,6 @@ import { useEffect, useRef, useState } from "react"; +import type { OkoSolWalletInterface } from "@oko-wallet/oko-sdk-sol"; +import type { Result } from "@oko-wallet/stdlib-js"; import { COSMOS_CHAIN_ID } from "@oko-wallet-demo-web/constants/cosmos"; import { useSDKState } from "@oko-wallet-demo-web/state/sdk"; @@ -7,12 +9,14 @@ import { useUserInfoState } from "@oko-wallet-demo-web/state/user_info"; export function useAddresses() { const okoCosmos = useSDKState((state) => state.oko_cosmos); const okoEth = useSDKState((state) => state.oko_eth); + const okoSol = useSDKState((state) => state.oko_sol); const isSignedIn = useUserInfoState((state) => state.isSignedIn); const isSignedRef = useRef(isSignedIn); isSignedRef.current = isSignedIn; const [cosmosAddress, setCosmosAddress] = useState(null); const [ethAddress, setEthAddress] = useState(null); + const [solanaAddress, setSolanaAddress] = useState(null); useEffect(() => { if (!isSignedIn) { @@ -23,12 +27,16 @@ export function useAddresses() { if (ethAddress) { setEthAddress(null); } + + if (solanaAddress) { + setSolanaAddress(null); + } return; } const loadAddresses = async () => { try { - const promises = []; + const promises: Promise[] = []; if (okoCosmos) { promises.push( @@ -50,6 +58,16 @@ export function useAddresses() { ); } + if (okoSol) { + promises.push( + new Promise((resolve, reject) => { + connectSol(okoSol, isSignedRef.current, setSolanaAddress) + .then(resolve) + .catch(reject); + }), + ); + } + await Promise.all(promises); } catch (err) { console.error("Failed to load addresses:", err); @@ -59,7 +77,37 @@ export function useAddresses() { if (isSignedIn) { loadAddresses(); } - }, [isSignedIn, okoCosmos, okoEth]); + }, [ + isSignedIn, + okoCosmos, + okoEth, + okoSol, + cosmosAddress, + ethAddress, + solanaAddress, + ]); + + return { cosmosAddress, ethAddress, solanaAddress }; +} + +async function connectSol( + okoSol: OkoSolWalletInterface, + isSignedRef: boolean, + setSolanaAddress: (pk: string) => void, +): Promise> { + try { + // this might have been done in lazyInit() + if (!okoSol.connected) { + await okoSol.connect(); + } + + if (okoSol.publicKey && isSignedRef) { + setSolanaAddress(okoSol.publicKey.toBase58()); + } - return { cosmosAddress, ethAddress }; + return { success: true, data: void 0 }; + } catch (err: any) { + console.error("Failed to get Solana address:", err); + return { success: false, err }; + } } diff --git a/apps/demo_web/src/state/sdk.ts b/apps/demo_web/src/state/sdk.ts index 23f14d6a7..62f8f8220 100644 --- a/apps/demo_web/src/state/sdk.ts +++ b/apps/demo_web/src/state/sdk.ts @@ -8,10 +8,10 @@ import { OkoEthWallet, type OkoEthWalletInterface, } from "@oko-wallet/oko-sdk-eth"; -// import { -// OkoSolWallet, -// type OkoSolWalletInterface, -// } from "@oko-wallet/oko-sdk-sol"; +import { + OkoSolWallet, + type OkoSolWalletInterface, +} from "@oko-wallet/oko-sdk-sol"; import { create } from "zustand"; import { combine } from "zustand/middleware"; @@ -20,7 +20,7 @@ import { useUserInfoState } from "@oko-wallet-demo-web/state/user_info"; interface SDKState { oko_eth: OkoEthWalletInterface | null; oko_cosmos: OkoCosmosWalletInterface | null; - // oko_sol: OkoSolWalletInterface | null; + oko_sol: OkoSolWalletInterface | null; isEthInitializing: boolean; isEthLazyInitialized: boolean; @@ -35,13 +35,13 @@ interface SDKState { interface SDKActions { initOkoEth: () => Promise; initOkoCosmos: () => Promise; - // initOkoSol: () => Promise; + initOkoSol: () => Promise; } const initialState: SDKState = { oko_eth: null, oko_cosmos: null, - // oko_sol: null, + oko_sol: null, isEthInitializing: false, isEthLazyInitialized: false, @@ -145,10 +145,10 @@ export const useSDKState = create( initOkoSol: async () => { const state = get(); - // if (state.oko_sol || state.isSolInitializing) { - // console.log("Sol SDK already initialized or initializing, skipping..."); - // return state.oko_sol; - // } + if (state.oko_sol || state.isSolInitializing) { + console.log("Sol SDK already initialized or initializing, skipping..."); + return state.oko_sol; + } try { console.log("Initializing Sol SDK..."); @@ -156,39 +156,41 @@ export const useSDKState = create( isSolInitializing: true, }); - // const initRes = OkoSolWallet.init({ - // api_key: - // "72bd2afd04374f86d563a40b814b7098e5ad6c7f52d3b8f84ab0c3d05f73ac6c", - // sdk_endpoint: process.env.NEXT_PUBLIC_OKO_SDK_ENDPOINT, - // }); - - // if (initRes.success) { - // console.log("Sol SDK initialized"); - - // const okoSol = initRes.data; - // set({ - // // oko_sol: okoSol, - // isSolInitializing: false, - // }); - - // try { - // await okoSol.waitUntilInitialized; - // console.log("Sol SDK lazy initialized"); - // set({ - // isSolLazyInitialized: true, - // }); - // } catch (e) { - // console.error("Sol SDK lazy init failed:", e); - // set({ isSolLazyInitialized: true }); // Still mark as done to not block - // } - - // return okoSol; - // } else { - // console.error("Sol sdk init fail, err: %s", initRes.err); - // set({ isSolInitializing: false, isSolLazyInitialized: true }); - - // return null; - // } + const initRes = OkoSolWallet.init({ + api_key: + "72bd2afd04374f86d563a40b814b7098e5ad6c7f52d3b8f84ab0c3d05f73ac6c", + sdk_endpoint: process.env.NEXT_PUBLIC_OKO_SDK_ENDPOINT, + }); + + if (initRes.success) { + console.log("Sol SDK initialized"); + + const okoSol = initRes.data; + setupSolListener(okoSol); + + set({ + oko_sol: okoSol, + isSolInitializing: false, + }); + + try { + await okoSol.waitUntilInitialized; + console.log("Sol SDK lazy initialized"); + set({ + isSolLazyInitialized: true, + }); + } catch (e) { + console.error("Sol SDK lazy init failed:", e); + set({ isSolLazyInitialized: true }); // Still mark as done to not block + } + + return okoSol; + } else { + console.error("Sol sdk init fail, err: %s", initRes.err); + set({ isSolInitializing: false, isSolLazyInitialized: true }); + + return null; + } } catch (e) { console.error("Sol SDK init error:", e); set({ isSolInitializing: false, isSolLazyInitialized: true }); @@ -222,3 +224,16 @@ function setupCosmosListener(cosmosSDK: OkoCosmosWalletInterface) { }); } } + +function setupSolListener(solSDK: OkoSolWalletInterface) { + const setPublicKeyEd25519 = useUserInfoState.getState().setPublicKeyEd25519; + + console.log("[Demo] Setting up Sol accountChanged listener"); + solSDK.on("accountChanged", () => { + const ed25519Key = solSDK.state.publicKeyRaw; + console.log("[Demo] Sol accountChanged event received:", { + ed25519Key: ed25519Key ? "exists" : "null", + }); + setPublicKeyEd25519(ed25519Key); + }); +} diff --git a/apps/demo_web/src/state/user_info.ts b/apps/demo_web/src/state/user_info.ts index e6bdfd843..d2a10da4d 100644 --- a/apps/demo_web/src/state/user_info.ts +++ b/apps/demo_web/src/state/user_info.ts @@ -5,7 +5,8 @@ import { combine } from "zustand/middleware"; interface UserInfoState { authType: AuthType | null; email: string | null; - publicKey: string | null; + publicKeySecp256k1: string | null; + publicKeyEd25519: string | null; name: string | null; isSignedIn: boolean; } @@ -15,8 +16,10 @@ interface UserInfoActions { authType: AuthType | null; email: string | null; publicKey: string | null; + publicKeyEd25519?: string | null; name?: string | null; }) => void; + setPublicKeyEd25519: (publicKeyEd25519: string | null) => void; clearUserInfo: () => void; } @@ -25,7 +28,8 @@ export const useUserInfoState = create( { authType: null, email: null, - publicKey: null, + publicKeySecp256k1: null, + publicKeyEd25519: null, name: null, isSignedIn: false, }, @@ -34,16 +38,21 @@ export const useUserInfoState = create( set({ authType: info.authType, email: info.email ?? null, - publicKey: info.publicKey, + publicKeySecp256k1: info.publicKey, + publicKeyEd25519: info.publicKeyEd25519 ?? null, name: info.name ?? null, isSignedIn: !!info.publicKey, }); }, + setPublicKeyEd25519: (publicKeyEd25519) => { + set({ publicKeyEd25519 }); + }, clearUserInfo: () => { set({ authType: null, email: null, - publicKey: null, + publicKeySecp256k1: null, + publicKeyEd25519: null, name: null, isSignedIn: false, }); diff --git a/backend/tss_api/src/api/sign_ed25519/index.ts b/backend/tss_api/src/api/sign_ed25519/index.ts index 7d49e0ccc..d727fe0b9 100644 --- a/backend/tss_api/src/api/sign_ed25519/index.ts +++ b/backend/tss_api/src/api/sign_ed25519/index.ts @@ -54,39 +54,24 @@ export async function runSignEd25519Round1( } const wallet = validateWalletEmailAndCurveTypeRes.data; - const encryptedShare = wallet.enc_tss_share.toString("utf-8"); - const decryptedShare = await decryptDataAsync( - encryptedShare, + // Decrypt and reconstruct key package + const storedShares = await decryptEd25519WalletShares( + wallet.enc_tss_share, encryptionSecret, ); - const storedShares = JSON.parse(decryptedShare) as { - signing_share: number[]; - verifying_share: number[]; - }; - - // Reconstruct key_package from stored shares - const serverIdentifier = participantToIdentifier(Participant.P1); - const verifyingKey = Array.from(wallet.public_key); - const minSigners = 2; - - let keyPackageBytes: Uint8Array; - try { - keyPackageBytes = reconstructKeyPackageEd25519( - new Uint8Array(storedShares.signing_share), - new Uint8Array(storedShares.verifying_share), - new Uint8Array(serverIdentifier), - new Uint8Array(verifyingKey), - minSigners, - ); - } catch (error) { + const keyPackageRes = reconstructServerKeyPackageEd25519( + storedShares, + wallet.public_key, + ); + if (!keyPackageRes.success) { return { success: false, code: "UNKNOWN_ERROR", - msg: `Failed to reconstruct key_package: ${error instanceof Error ? error.message : String(error)}`, + msg: keyPackageRes.error, }; } - const round1Result = runSignRound1Ed25519(keyPackageBytes); + const round1Result = runSignRound1Ed25519(keyPackageRes.data); // Create TSS session const sessionRes = await createTssSession(db, { @@ -212,35 +197,20 @@ export async function runSignEd25519Round2( }; } - const encryptedShare = wallet.enc_tss_share.toString("utf-8"); - const decryptedShare = await decryptDataAsync( - encryptedShare, + // Decrypt and reconstruct key package + const storedShares = await decryptEd25519WalletShares( + wallet.enc_tss_share, encryptionSecret, ); - const storedShares = JSON.parse(decryptedShare) as { - signing_share: number[]; - verifying_share: number[]; - }; - - // Reconstruct key_package from stored shares - const serverIdentifier = participantToIdentifier(Participant.P1); - const verifyingKey = Array.from(wallet.public_key); - const minSigners = 2; - - let keyPackageBytes: Uint8Array; - try { - keyPackageBytes = reconstructKeyPackageEd25519( - new Uint8Array(storedShares.signing_share), - new Uint8Array(storedShares.verifying_share), - new Uint8Array(serverIdentifier), - new Uint8Array(verifyingKey), - minSigners, - ); - } catch (error) { + const keyPackageRes = reconstructServerKeyPackageEd25519( + storedShares, + wallet.public_key, + ); + if (!keyPackageRes.success) { return { success: false, code: "UNKNOWN_ERROR", - msg: `Failed to reconstruct key_package: ${error instanceof Error ? error.message : String(error)}`, + msg: keyPackageRes.error, }; } @@ -258,7 +228,7 @@ export async function runSignEd25519Round2( const round2Result = runSignRound2Ed25519( new Uint8Array(msg), - keyPackageBytes, + keyPackageRes.data, new Uint8Array(nonces), allCommitments, ); @@ -301,3 +271,51 @@ export async function runSignEd25519Round2( }; } } + +interface Ed25519StoredShares { + signing_share: number[]; + verifying_share: number[]; +} + +/** + * Decrypt ed25519 wallet shares from encrypted storage. + */ +async function decryptEd25519WalletShares( + encTssShare: Buffer, + encryptionSecret: string, +): Promise { + const encryptedShare = encTssShare.toString("utf-8"); + const decryptedShare = await decryptDataAsync( + encryptedShare, + encryptionSecret, + ); + return JSON.parse(decryptedShare) as Ed25519StoredShares; +} + +/** + * Reconstruct server's key package from stored shares. + * Returns the key package bytes or an error response. + */ +function reconstructServerKeyPackageEd25519( + storedShares: Ed25519StoredShares, + publicKey: Buffer, +): { success: true; data: Uint8Array } | { success: false; error: string } { + const serverIdentifier = participantToIdentifier(Participant.P1); + const verifyingKey = Array.from(publicKey); + + try { + const keyPackageBytes = reconstructKeyPackageEd25519( + new Uint8Array(storedShares.signing_share), + new Uint8Array(storedShares.verifying_share), + new Uint8Array(serverIdentifier), + new Uint8Array(verifyingKey), + 2, + ); + return { success: true, data: keyPackageBytes }; + } catch (error) { + return { + success: false, + error: `Failed to reconstruct key_package: ${error instanceof Error ? error.message : String(error)}`, + }; + } +} diff --git a/backend/tss_api/src/api/v2/keygen/index.ts b/backend/tss_api/src/api/v2/keygen/index.ts index 95c18c802..c7e4f6e2a 100644 --- a/backend/tss_api/src/api/v2/keygen/index.ts +++ b/backend/tss_api/src/api/v2/keygen/index.ts @@ -153,26 +153,22 @@ export async function runKeygenV2( } // 3. Validate ed25519 public key - const ed25519PublicKeyUint8 = new Uint8Array(keygen_2_ed25519.public_key); - const ed25519PublicKeyHex = Buffer.from(ed25519PublicKeyUint8).toString( - "hex", - ); - - const ed25519PublicKeyRes = Bytes.fromUint8Array(ed25519PublicKeyUint8, 32); - if (ed25519PublicKeyRes.success === false) { + const ed25519PkValidation = validateEd25519PublicKey(keygen_2_ed25519.public_key); + if (!ed25519PkValidation.success) { return { success: false, code: "UNKNOWN_ERROR", - msg: `ed25519 publicKeyRes error: ${ed25519PublicKeyRes.err}`, + msg: ed25519PkValidation.error, }; } - const ed25519PublicKeyBytes = ed25519PublicKeyRes.data; + const { + publicKeyHex: ed25519PublicKeyHex, + publicKeyBytes: ed25519PublicKeyBytes, + publicKeyBuffer: ed25519PublicKeyBuffer, + } = ed25519PkValidation.data; // Check for duplicate ed25519 public key - const ed25519WalletByPkRes = await getWalletByPublicKey( - db, - Buffer.from(ed25519PublicKeyBytes.toUint8Array()), - ); + const ed25519WalletByPkRes = await getWalletByPublicKey(db, ed25519PublicKeyBuffer); if (ed25519WalletByPkRes.success === false) { return { success: false, @@ -245,24 +241,10 @@ export async function runKeygenV2( ); // 7. Encrypt ed25519 share (extract signing_share and verifying_share) - const ed25519KeyPackageShares = extractKeyPackageSharesEd25519( - new Uint8Array(keygen_2_ed25519.key_package), - ); - const ed25519SharesData = { - signing_share: ed25519KeyPackageShares.signing_share, - verifying_share: ed25519KeyPackageShares.verifying_share, - }; - const ed25519EncryptedShare = await encryptDataAsync( - JSON.stringify(ed25519SharesData), - encryptionSecret, - ); - const ed25519EncryptedShareBuffer = Buffer.from( - ed25519EncryptedShare, - "utf-8", - ); - const serverVerifyingShareEd25519Hex = Buffer.from( - ed25519KeyPackageShares.verifying_share, - ).toString("hex"); + const { + encryptedShareBuffer: ed25519EncryptedShareBuffer, + serverVerifyingShareHex: serverVerifyingShareEd25519Hex, + } = await encryptEd25519KeyPackageShares(keygen_2_ed25519.key_package, encryptionSecret); // 8. Get SSS threshold const getKeyshareNodeMetaRes = await getKeyShareNodeMeta(db); @@ -468,37 +450,33 @@ export async function runKeygenEd25519( }; } - const ed25519PublicKeyUint8 = new Uint8Array(keygen_2.public_key); - const ed25519PublicKeyHex = Buffer.from(ed25519PublicKeyUint8).toString( - "hex", - ); - - const ed25519PublicKeyRes = Bytes.fromUint8Array(ed25519PublicKeyUint8, 32); - if (ed25519PublicKeyRes.success === false) { + const ed25519PkValidation = validateEd25519PublicKey(keygen_2.public_key); + if (!ed25519PkValidation.success) { return { success: false, code: "UNKNOWN_ERROR", - msg: `ed25519 publicKeyRes error: ${ed25519PublicKeyRes.err}`, + msg: ed25519PkValidation.error, }; } - const ed25519PublicKeyBytes = ed25519PublicKeyRes.data; + const { + publicKeyHex: ed25519PublicKeyHex, + publicKeyBytes: ed25519PublicKeyBytes, + publicKeyBuffer: ed25519PublicKeyBuffer, + } = ed25519PkValidation.data; - const walletByPublicKeyRes = await getWalletByPublicKey( - db, - Buffer.from(ed25519PublicKeyBytes.toUint8Array()), - ); + const walletByPublicKeyRes = await getWalletByPublicKey(db, ed25519PublicKeyBuffer); if (walletByPublicKeyRes.success === false) { return { success: false, code: "UNKNOWN_ERROR", - msg: `getWalletByPublicKey error: ${walletByPublicKeyRes.err}`, + msg: `getWalletByPublicKey (ed25519) error: ${walletByPublicKeyRes.err}`, }; } if (walletByPublicKeyRes.data !== null) { return { success: false, code: "DUPLICATE_PUBLIC_KEY", - msg: `Duplicate public key: ${ed25519PublicKeyHex}`, + msg: `Duplicate ed25519 public key: ${ed25519PublicKeyHex}`, }; } @@ -536,25 +514,11 @@ export async function runKeygenEd25519( } const ksNodeIds = ed25519KsNodeIds; - // Extract signing_share and verifying_share from key_package - const ed25519KeyPackageShares = extractKeyPackageSharesEd25519( - new Uint8Array(keygen_2.key_package), - ); - - // Store only signing_share and verifying_share (64 bytes total) - const sharesData = { - signing_share: ed25519KeyPackageShares.signing_share, - verifying_share: ed25519KeyPackageShares.verifying_share, - }; - - const encryptedShare = await encryptDataAsync( - JSON.stringify(sharesData), - encryptionSecret, - ); - const encryptedShareBuffer = Buffer.from(encryptedShare, "utf-8"); - const serverVerifyingShareEd25519Hex = Buffer.from( - ed25519KeyPackageShares.verifying_share, - ).toString("hex"); + // Extract and encrypt ed25519 key package shares + const { + encryptedShareBuffer, + serverVerifyingShareHex: serverVerifyingShareEd25519Hex, + } = await encryptEd25519KeyPackageShares(keygen_2.key_package, encryptionSecret); const getKeyshareNodeMetaRes = await getKeyShareNodeMeta(db); if (getKeyshareNodeMetaRes.success === false) { @@ -650,3 +614,68 @@ export async function runKeygenEd25519( }; } } + +interface Ed25519PublicKeyValidationResult { + publicKeyHex: string; + publicKeyBytes: Bytes<32>; + publicKeyBuffer: Buffer; +} + +/** + * Validate and convert ed25519 public key from number array to various formats. + */ +function validateEd25519PublicKey( + publicKeyArray: number[], +): { success: true; data: Ed25519PublicKeyValidationResult } | { success: false; error: string } { + const publicKeyUint8 = new Uint8Array(publicKeyArray); + const publicKeyHex = Buffer.from(publicKeyUint8).toString("hex"); + + const publicKeyRes = Bytes.fromUint8Array(publicKeyUint8, 32); + if (publicKeyRes.success === false) { + return { + success: false, + error: `ed25519 publicKeyRes error: ${publicKeyRes.err}`, + }; + } + + return { + success: true, + data: { + publicKeyHex, + publicKeyBytes: publicKeyRes.data, + publicKeyBuffer: Buffer.from(publicKeyRes.data.toUint8Array()), + }, + }; +} + +interface Ed25519EncryptedShareResult { + encryptedShareBuffer: Buffer; + serverVerifyingShareHex: string; +} + +/** + * Extract shares from ed25519 key package and encrypt for storage. + */ +async function encryptEd25519KeyPackageShares( + keyPackage: number[], + encryptionSecret: string, +): Promise { + const keyPackageShares = extractKeyPackageSharesEd25519( + new Uint8Array(keyPackage), + ); + + const sharesData = { + signing_share: keyPackageShares.signing_share, + verifying_share: keyPackageShares.verifying_share, + }; + + const encryptedShare = await encryptDataAsync( + JSON.stringify(sharesData), + encryptionSecret, + ); + + return { + encryptedShareBuffer: Buffer.from(encryptedShare, "utf-8"), + serverVerifyingShareHex: Buffer.from(keyPackageShares.verifying_share).toString("hex"), + }; +} diff --git a/crypto/tecdsa/api_lib/src/index.ts b/crypto/tecdsa/api_lib/src/index.ts index b232c1810..02cc87ae7 100644 --- a/crypto/tecdsa/api_lib/src/index.ts +++ b/crypto/tecdsa/api_lib/src/index.ts @@ -51,7 +51,7 @@ import { type SignInResponse, type SignInResponseV2, } from "@oko-wallet/oko-types/user"; -import type { KeygenBodyV2 } from "@oko-wallet/oko-types/tss"; +import type { KeygenRequestBodyV2 } from "@oko-wallet/oko-types/tss"; import { type ErrorCode, type OkoApiErrorResponse, @@ -198,7 +198,7 @@ export async function reqKeygen( export async function reqKeygenV2( endpoint: string, - payload: KeygenBodyV2, + payload: KeygenRequestBodyV2, authToken: string, ) { const resp: OkoApiResponse = await makePostRequest( diff --git a/crypto/teddsa/api_lib/src/index.ts b/crypto/teddsa/api_lib/src/index.ts index a4209962d..450547207 100644 --- a/crypto/teddsa/api_lib/src/index.ts +++ b/crypto/teddsa/api_lib/src/index.ts @@ -1,5 +1,5 @@ import type { - KeygenEd25519Body, + KeygenEd25519RequestBody, SignEd25519Round1Body, SignEd25519Round1Response, SignEd25519Round2Body, @@ -118,7 +118,7 @@ async function makePostRequest( export async function reqKeygenEd25519( endpoint: string, - payload: KeygenEd25519Body, + payload: KeygenEd25519RequestBody, authToken: string, ) { const resp: OkoApiResponse = await makePostRequest( diff --git a/embed/oko_attached/src/window_msgs/oauth_info_pass/user_v2.ts b/embed/oko_attached/src/window_msgs/oauth_info_pass/user_v2.ts index ffe2d57d1..9639c1f3d 100644 --- a/embed/oko_attached/src/window_msgs/oauth_info_pass/user_v2.ts +++ b/embed/oko_attached/src/window_msgs/oauth_info_pass/user_v2.ts @@ -39,6 +39,7 @@ import { runTeddsaKeygen, serializeKeyPackage, serializePublicKeyPackage, + type TeddsaKeygenOutputBytes, } from "@oko-wallet/teddsa-hooks"; import { reqKeygenEd25519 } from "@oko-wallet/teddsa-api-lib"; import { reqKeygenV2 } from "@oko-wallet/api-lib"; @@ -83,16 +84,17 @@ export async function handleNewUserV2( const { keygen_1: secp256k1Keygen1, keygen_2: secp256k1Keygen2 } = secp256k1KeygenRes.data; - // 2. ed25519 keygen - const ed25519KeygenRes = await runTeddsaKeygen(); - if (ed25519KeygenRes.success === false) { - return { - success: false, - err: { type: "sign_in_request_fail", error: ed25519KeygenRes.err }, - }; + // 2. ed25519 keygen and split + const ed25519KeygenSplitRes = + await runEd25519KeygenAndSplit(keyshareNodeMeta); + if (ed25519KeygenSplitRes.success === false) { + return { success: false, err: ed25519KeygenSplitRes.err }; } - const { keygen_1: ed25519Keygen1, keygen_2: ed25519Keygen2 } = - ed25519KeygenRes.data; + const { + keygen1: ed25519Keygen1, + keygen2: ed25519Keygen2, + userKeyShares: ed25519UserKeyShares, + } = ed25519KeygenSplitRes.data; // 3. secp256k1 key share split const splitUserKeySharesRes = await splitUserKeyShares( @@ -107,28 +109,6 @@ export async function handleNewUserV2( } const secp256k1UserKeyShares = splitUserKeySharesRes.data; - // 4. ed25519 key share split - const ed25519SigningShareRes = extractSigningShare( - ed25519Keygen1.key_package, - ); - if (ed25519SigningShareRes.success === false) { - return { - success: false, - err: { type: "sign_in_request_fail", error: ed25519SigningShareRes.err }, - }; - } - const ed25519SplitRes = await splitTeddsaSigningShare( - ed25519SigningShareRes.data, - keyshareNodeMeta, - ); - if (ed25519SplitRes.success === false) { - return { - success: false, - err: { type: "sign_in_request_fail", error: ed25519SplitRes.err }, - }; - } - const ed25519UserKeyShares = ed25519SplitRes.data; - // 5. Send key shares by both curves to ks nodes using V2 API const registerKeySharesResults: Result[] = await Promise.all( secp256k1UserKeyShares.map((keyShareByNode, index) => @@ -161,6 +141,7 @@ export async function handleNewUserV2( const reqKeygenV2Res = await reqKeygenV2( TSS_V2_ENDPOINT, { + auth_type: authType, keygen_2_secp256k1: { public_key: secp256k1Keygen1.public_key.toHex(), private_share: secp256k1Keygen2.tss_private_share.toHex(), @@ -229,38 +210,11 @@ export async function handleExistingUserV2( authType: AuthType, ): Promise> { // 1. Sign in to API server - const signInRes = await makeAuthorizedOkoApiRequest( - "user/signin", - idToken, - { - auth_type: authType, - }, - TSS_V2_ENDPOINT, - ); - if (!signInRes.success) { - console.error("[attached] sign in failed, err: %s", signInRes.err); - return { - success: false, - err: { type: "sign_in_request_fail", error: signInRes.err.toString() }, - }; - } - - const apiResponse = signInRes.data; - if (!apiResponse.success) { - console.error( - "[attached] sign in request failed, err: %s", - apiResponse.msg, - ); - return { - success: false, - err: { - type: "sign_in_request_fail", - error: `code: ${apiResponse.code}`, - }, - }; + const signInResult = await signInV2(idToken, authType); + if (!signInResult.success) { + return { success: false, err: signInResult.err }; } - - const signInResp = apiResponse.data; + const signInResp = signInResult.data; // 2. Request secp256k1 and ed25519 shares from KS nodes using V2 API const requestSharesRes = await requestKeySharesV2( @@ -302,37 +256,16 @@ export async function handleExistingUserV2( }; } - // 3. Combine secp256k1 shares (convert hex strings to Point256) - const secp256k1SharesByNode: UserKeySharePointByNode[] = []; - for (const item of requestSharesRes.data) { - const shareHex = item.shares.secp256k1; - if (!shareHex) { - return { - success: false, - err: { - type: "key_share_combine_fail", - error: `secp256k1 share missing from node: ${item.node.name}`, - }, - }; - } - const point256Res = decodeKeyShareStringToPoint256(shareHex); - if (point256Res.success === false) { - return { - success: false, - err: { - type: "key_share_combine_fail", - error: `secp256k1 decode err: ${point256Res.err}`, - }, - }; - } - secp256k1SharesByNode.push({ - node: item.node, - share: point256Res.data, - }); + // 3. Decode and combine secp256k1 shares + const secp256k1DecodeRes = await decodeSecp256k1SharesByNode( + requestSharesRes.data, + ); + if (!secp256k1DecodeRes.success) { + return { success: false, err: secp256k1DecodeRes.err }; } const keyshare1Secp256k1Res = await combineUserShares( - secp256k1SharesByNode, + secp256k1DecodeRes.data, keyshareNodeMetaSecp256k1.threshold, ); if (keyshare1Secp256k1Res.success === false) { @@ -507,40 +440,20 @@ export async function handleExistingUserNeedsEd25519Keygen( keyshareNodeMetaEd25519: KeyShareNodeMetaWithNodeStatusInfo, authType: AuthType, ): Promise> { - // 1. ed25519 keygen - const ed25519KeygenRes = await runTeddsaKeygen(); - if (ed25519KeygenRes.success === false) { - return { - success: false, - err: { type: "sign_in_request_fail", error: ed25519KeygenRes.err }, - }; - } - const { keygen_1: ed25519Keygen1, keygen_2: ed25519Keygen2 } = - ed25519KeygenRes.data; - - // 2. ed25519 key share split - const ed25519SigningShareRes = extractSigningShare( - ed25519Keygen1.key_package, - ); - if (ed25519SigningShareRes.success === false) { - return { - success: false, - err: { type: "sign_in_request_fail", error: ed25519SigningShareRes.err }, - }; - } - const ed25519SplitRes = await splitTeddsaSigningShare( - ed25519SigningShareRes.data, + // 1. ed25519 keygen and split + const ed25519KeygenSplitRes = await runEd25519KeygenAndSplit( keyshareNodeMetaEd25519, ); - if (ed25519SplitRes.success === false) { - return { - success: false, - err: { type: "sign_in_request_fail", error: ed25519SplitRes.err }, - }; + if (ed25519KeygenSplitRes.success === false) { + return { success: false, err: ed25519KeygenSplitRes.err }; } - const ed25519UserKeyShares = ed25519SplitRes.data; + const { + keygen1: ed25519Keygen1, + keygen2: ed25519Keygen2, + userKeyShares: ed25519UserKeyShares, + } = ed25519KeygenSplitRes.data; - // 3. Send ed25519 key shares to ks nodes using V2 API + // 2. Send ed25519 key shares to ks nodes using V2 API const registerEd25519Results: Result[] = await Promise.all( keyshareNodeMetaEd25519.nodes.map((node, index) => registerKeyShareEd25519V2( @@ -569,6 +482,7 @@ export async function handleExistingUserNeedsEd25519Keygen( const reqKeygenEd25519Res = await reqKeygenEd25519( TSS_V2_ENDPOINT, { + auth_type: authType, keygen_2: { key_package: serializeKeyPackage(ed25519Keygen2.key_package), public_key_package: serializePublicKeyPackage( @@ -628,37 +542,16 @@ export async function handleExistingUserNeedsEd25519Keygen( }; } - // 7. Combine secp256k1 shares (convert hex strings to Point256) - const secp256k1SharesByNode: UserKeySharePointByNode[] = []; - for (const item of requestSharesRes.data) { - const shareHex = item.shares.secp256k1; - if (!shareHex) { - return { - success: false, - err: { - type: "key_share_combine_fail", - error: `secp256k1 share missing from node: ${item.node.name}`, - }, - }; - } - const point256Res = decodeKeyShareStringToPoint256(shareHex); - if (point256Res.success === false) { - return { - success: false, - err: { - type: "key_share_combine_fail", - error: `secp256k1 decode err: ${point256Res.err}`, - }, - }; - } - secp256k1SharesByNode.push({ - node: item.node, - share: point256Res.data, - }); + // 6. Decode and combine secp256k1 shares + const secp256k1DecodeRes = await decodeSecp256k1SharesByNode( + requestSharesRes.data, + ); + if (!secp256k1DecodeRes.success) { + return { success: false, err: secp256k1DecodeRes.err }; } const keyshare1Secp256k1Res = await combineUserShares( - secp256k1SharesByNode, + secp256k1DecodeRes.data, keyshareNodeMetaSecp256k1.threshold, ); if (keyshare1Secp256k1Res.success === false) { @@ -671,7 +564,7 @@ export async function handleExistingUserNeedsEd25519Keygen( }; } - // 8. Convert ed25519 keygen1 to hex format for storage + // 7. Convert ed25519 keygen1 to hex format for storage const keyPackageEd25519Hex = teddsaKeygenToHex(ed25519Keygen1); return { @@ -721,38 +614,11 @@ export async function handleReshareV2( ed25519NeedsReshare: boolean, ): Promise> { // 1. Sign in to API server to get public keys and server verifying share - const signInRes = await makeAuthorizedOkoApiRequest( - "user/signin", - idToken, - { - auth_type: authType, - }, - TSS_V2_ENDPOINT, - ); - if (!signInRes.success) { - console.error("[attached] sign in failed, err: %s", signInRes.err); - return { - success: false, - err: { type: "sign_in_request_fail", error: signInRes.err.toString() }, - }; - } - - const apiResponse = signInRes.data; - if (!apiResponse.success) { - console.error( - "[attached] sign in request failed, err: %s", - apiResponse.msg, - ); - return { - success: false, - err: { - type: "sign_in_request_fail", - error: `code: ${apiResponse.code}`, - }, - }; + const signInResult = await signInV2(idToken, authType); + if (!signInResult.success) { + return { success: false, err: signInResult.err }; } - - const signInResp = apiResponse.data; + const signInResp = signInResult.data; // Parse public keys const publicKeySecp256k1Res = Bytes.fromHexString( @@ -868,80 +734,28 @@ export async function handleReshareAndEd25519Keygen( }; } - // 2. ed25519 keygen (new) - const ed25519KeygenRes = await runTeddsaKeygen(); - if (ed25519KeygenRes.success === false) { - return { - success: false, - err: { type: "sign_in_request_fail", error: ed25519KeygenRes.err }, - }; - } - const { keygen_1: ed25519Keygen1, keygen_2: ed25519Keygen2 } = - ed25519KeygenRes.data; - - // 3. ed25519 key share split - const ed25519SigningShareRes = extractSigningShare( - ed25519Keygen1.key_package, - ); - if (ed25519SigningShareRes.success === false) { - return { - success: false, - err: { type: "sign_in_request_fail", error: ed25519SigningShareRes.err }, - }; - } - const ed25519SplitRes = await splitTeddsaSigningShare( - ed25519SigningShareRes.data, + // 2. ed25519 keygen and split + const ed25519KeygenSplitRes = await runEd25519KeygenAndSplit( keyshareNodeMetaEd25519, ); - if (ed25519SplitRes.success === false) { - return { - success: false, - err: { type: "sign_in_request_fail", error: ed25519SplitRes.err }, - }; + if (ed25519KeygenSplitRes.success === false) { + return { success: false, err: ed25519KeygenSplitRes.err }; } - const ed25519UserKeyShares = ed25519SplitRes.data; - - // 4. Request secp256k1 shares from ACTIVE nodes - const requestSharesRes = await requestKeySharesV2( - idToken, - activeNodes, - keyshareNodeMetaSecp256k1.threshold, - authType, - { - secp256k1: undefined, // Will be filled after sign-in - }, - ); - - // We need to sign in first to get the public key - const signInRes = await makeAuthorizedOkoApiRequest( - "user/signin", - idToken, - { - auth_type: authType, - }, - TSS_V2_ENDPOINT, - ); - if (!signInRes.success) { - return { - success: false, - err: { type: "sign_in_request_fail", error: signInRes.err.toString() }, - }; + const { + keygen1: ed25519Keygen1, + keygen2: ed25519Keygen2, + userKeyShares: ed25519UserKeyShares, + } = ed25519KeygenSplitRes.data; + + // 3. Sign in to get the public key + const signInResult = await signInV2(idToken, authType); + if (!signInResult.success) { + return { success: false, err: signInResult.err }; } - - const apiResponse = signInRes.data; - if (!apiResponse.success) { - return { - success: false, - err: { - type: "sign_in_request_fail", - error: `code: ${apiResponse.code}`, - }, - }; - } - const signInResp = apiResponse.data; + const signInResp = signInResult.data; const secp256k1PublicKey = signInResp.user.public_key_secp256k1; - // 5. Request secp256k1 shares with public key + // 4. Request secp256k1 shares with public key const requestSecp256k1SharesRes = await requestKeySharesV2( idToken, activeNodes, @@ -961,31 +775,25 @@ export async function handleReshareAndEd25519Keygen( }; } - // 6. secp256k1 expand - const secp256k1SharesByNode: UserKeySharePointByNode[] = []; - for (const item of requestSecp256k1SharesRes.data) { - const shareHex = item.shares.secp256k1; - if (!shareHex) { - continue; - } - const point256Res = decodeKeyShareStringToPoint256(shareHex); - if (!point256Res.success) { - return { - success: false, - err: { - type: "reshare_fail", - error: `secp256k1 decode err: ${point256Res.err}`, - }, - }; - } - secp256k1SharesByNode.push({ - node: item.node, - share: point256Res.data, - }); + // 5. Decode secp256k1 shares (filter out items without shares for reshare case) + const itemsWithSecp256k1 = requestSecp256k1SharesRes.data.filter( + (item) => item.shares.secp256k1, + ); + const secp256k1DecodeRes = + await decodeSecp256k1SharesByNode(itemsWithSecp256k1); + if (!secp256k1DecodeRes.success) { + return { + success: false, + err: { + type: "reshare_fail", + error: secp256k1DecodeRes.err.error, + }, + }; } + // 6. Expand secp256k1 shares to additional nodes const secp256k1ExpandRes = await runExpandShares( - secp256k1SharesByNode, + secp256k1DecodeRes.data, additionalNodes, keyshareNodeMetaSecp256k1.threshold, ); @@ -1075,6 +883,7 @@ export async function handleReshareAndEd25519Keygen( const reqKeygenEd25519Res = await reqKeygenEd25519( TSS_V2_ENDPOINT, { + auth_type: authType, keygen_2: { key_package: serializeKeyPackage(ed25519Keygen2.key_package), public_key_package: serializePublicKeyPackage( @@ -1165,3 +974,152 @@ async function saveReferralV2( throw new Error(`Save referral V2 API error: ${apiResponse.msg}`); } } + +/** + * Decode secp256k1 shares from hex strings to Point256 format. + * Used by multiple handlers that need to process shares from KS nodes. + */ +async function decodeSecp256k1SharesByNode( + sharesData: Array<{ + node: { name: string; endpoint: string }; + shares: { secp256k1?: string }; + }>, +): Promise< + Result< + UserKeySharePointByNode[], + { type: "key_share_combine_fail"; error: string } + > +> { + const sharesByNode: UserKeySharePointByNode[] = []; + + for (const item of sharesData) { + const shareHex = item.shares.secp256k1; + if (!shareHex) { + return { + success: false, + err: { + type: "key_share_combine_fail", + error: `secp256k1 share missing from node: ${item.node.name}`, + }, + }; + } + const point256Res = decodeKeyShareStringToPoint256(shareHex); + if (point256Res.success === false) { + return { + success: false, + err: { + type: "key_share_combine_fail", + error: `secp256k1 decode err: ${point256Res.err}`, + }, + }; + } + sharesByNode.push({ + node: item.node, + share: point256Res.data, + }); + } + + return { success: true, data: sharesByNode }; +} + +interface Ed25519KeygenSplitResult { + keygen1: TeddsaKeygenOutputBytes; + keygen2: TeddsaKeygenOutputBytes; + userKeyShares: TeddsaKeyShareByNode[]; +} + +/** + * Run ed25519 keygen and split the signing share for distribution to KS nodes. + * Used by handlers that need to create new ed25519 wallets. + */ +async function runEd25519KeygenAndSplit( + keyshareNodeMeta: KeyShareNodeMetaWithNodeStatusInfo, +): Promise< + Result< + Ed25519KeygenSplitResult, + { type: "sign_in_request_fail"; error: string } + > +> { + // 1. ed25519 keygen + const ed25519KeygenRes = await runTeddsaKeygen(); + if (ed25519KeygenRes.success === false) { + return { + success: false, + err: { type: "sign_in_request_fail", error: ed25519KeygenRes.err }, + }; + } + const { keygen_1: keygen1, keygen_2: keygen2 } = ed25519KeygenRes.data; + + // 2. Extract signing share from key package + const signingShareRes = extractSigningShare(keygen1.key_package); + if (signingShareRes.success === false) { + return { + success: false, + err: { type: "sign_in_request_fail", error: signingShareRes.err }, + }; + } + + // 3. Split signing share for distribution + const splitRes = await splitTeddsaSigningShare( + signingShareRes.data, + keyshareNodeMeta, + ); + if (splitRes.success === false) { + return { + success: false, + err: { type: "sign_in_request_fail", error: splitRes.err }, + }; + } + + return { + success: true, + data: { + keygen1, + keygen2, + userKeyShares: splitRes.data, + }, + }; +} + +interface SignInRequestV2 { + auth_type: AuthType; +} + +/** + * Sign in to API server and return user data. + * Used by handlers that need to authenticate before requesting shares. + */ +async function signInV2( + idToken: string, + authType: AuthType, +): Promise> { + const signInRes = await makeAuthorizedOkoApiRequest< + SignInRequestV2, + SignInResponseV2 + >("user/signin", idToken, { auth_type: authType }, TSS_V2_ENDPOINT); + + if (!signInRes.success) { + console.error("[attached] sign in failed, err: %s", signInRes.err); + return { + success: false, + err: { type: "sign_in_request_fail", error: signInRes.err.toString() }, + }; + } + + const apiResponse = signInRes.data; + if (!apiResponse.success) { + console.error( + "[attached] sign in request failed, err: %s", + apiResponse.msg, + ); + return { + success: false, + err: { + type: "sign_in_request_fail", + error: `code: ${apiResponse.code}`, + }, + }; + } + + return { success: true, data: apiResponse.data }; +} diff --git a/internals/ci/src/cmds/deploy.ts b/internals/ci/src/cmds/deploy.ts index 3ebdd7b7a..a31c8c9e1 100644 --- a/internals/ci/src/cmds/deploy.ts +++ b/internals/ci/src/cmds/deploy.ts @@ -30,6 +30,9 @@ const APP_CONFIGS = { "oko-sandbox-evm": { path: path.join(paths.root, "sandbox/sandbox_evm"), }, + "oko-sandbox-sol": { + path: path.join(paths.root, "sandbox/sandbox_sol"), + }, }; function listDeployableApps(): void { diff --git a/internals/docker/oko_api_server.Dockerfile b/internals/docker/oko_api_server.Dockerfile index cecbcea76..82e685a49 100644 --- a/internals/docker/oko_api_server.Dockerfile +++ b/internals/docker/oko_api_server.Dockerfile @@ -71,16 +71,16 @@ RUN yarn workspaces focus @oko-wallet/tecdsa-interface WORKDIR /home/node/oko/crypto/tecdsa/tecdsa_interface RUN yarn run build -# Build teddsa-interface +# Build oko-types (depends on stdlib-js, bytes, tecdsa-interface) WORKDIR /home/node/oko -RUN yarn workspaces focus @oko-wallet/teddsa-interface -WORKDIR /home/node/oko/crypto/teddsa/teddsa_interface +RUN yarn workspaces focus @oko-wallet/oko-types +WORKDIR /home/node/oko/common/oko_types RUN yarn run build -# Build oko-types (depends on stdlib-js, tecdsa-interface, teddsa-interface) +# Build teddsa-interface (depends on bytes, oko-types/teddsa) WORKDIR /home/node/oko -RUN yarn workspaces focus @oko-wallet/oko-types -WORKDIR /home/node/oko/common/oko_types +RUN yarn workspaces focus @oko-wallet/teddsa-interface +WORKDIR /home/node/oko/crypto/teddsa/teddsa_interface RUN yarn run build # Install dependencies for crypto-js diff --git a/sandbox/sandbox_sol/src/hooks/use_oko_sol.ts b/sandbox/sandbox_sol/src/hooks/use_oko_sol.ts index 40a3ed835..7d27701b3 100644 --- a/sandbox/sandbox_sol/src/hooks/use_oko_sol.ts +++ b/sandbox/sandbox_sol/src/hooks/use_oko_sol.ts @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState, useCallback } from "react"; +import type { PublicKey } from "@solana/web3.js"; import { OkoSolWallet, registerOkoWallet } from "@oko-wallet/oko-sdk-sol"; import { useSdkStore } from "@/store/sdk"; @@ -22,7 +23,9 @@ export function useOkoSol() { // Initialize SDK useEffect(() => { - if (isInitialized || isInitializing) return; + if (isInitialized || isInitializing) { + return; + } const initSdk = async () => { setIsInitializing(true); @@ -63,9 +66,10 @@ export function useOkoSol() { await solWallet.okoWallet.getPublicKeyEd25519(); if (existingEd25519Pubkey) { await solWallet.connect(); - const pk = solWallet.publicKey?.toBase58() ?? null; - setConnected(true, pk); - console.log("[sandbox_sol] Reconnected:", pk); + console.log( + "[sandbox_sol] Reconnected:", + solWallet.publicKey?.toBase58(), + ); } } catch (err) { const message = err instanceof Error ? err.message : String(err); @@ -83,9 +87,40 @@ export function useOkoSol() { setOkoWallet, setOkoSolWallet, setInitialized, - setConnected, ]); + useEffect(() => { + if (!okoSolWallet) { + return; + } + + const handleAccountChanged = (pk: PublicKey | null) => { + const pubkeyStr = pk?.toBase58() ?? null; + setConnected(!!pk, pubkeyStr); + console.log("[sandbox_sol] accountChanged event:", pubkeyStr); + }; + + const handleConnect = (pk: PublicKey) => { + setConnected(true, pk.toBase58()); + console.log("[sandbox_sol] connect event:", pk.toBase58()); + }; + + const handleDisconnect = () => { + setConnected(false, null); + console.log("[sandbox_sol] disconnect event"); + }; + + okoSolWallet.on("accountChanged", handleAccountChanged); + okoSolWallet.on("connect", handleConnect); + okoSolWallet.on("disconnect", handleDisconnect); + + return () => { + okoSolWallet.off("accountChanged", handleAccountChanged); + okoSolWallet.off("connect", handleConnect); + okoSolWallet.off("disconnect", handleDisconnect); + }; + }, [okoSolWallet, setConnected]); + const connect = useCallback(async () => { if (!okoSolWallet) { setError("SDK not initialized"); @@ -104,30 +139,32 @@ export function useOkoSol() { // connect() internally handles Ed25519 key creation if needed await okoSolWallet.connect(); - const pk = okoSolWallet.publicKey?.toBase58() ?? null; - setConnected(true, pk); - console.log("[sandbox_sol] Connected:", pk); + console.log( + "[sandbox_sol] Connected:", + okoSolWallet.publicKey?.toBase58(), + ); } catch (err) { const message = err instanceof Error ? err.message : String(err); setError(message); console.error("[sandbox_sol] Failed to connect:", message); } - }, [okoSolWallet, setConnected]); + }, [okoSolWallet]); // Disconnect wallet const disconnect = useCallback(async () => { - if (!okoSolWallet) return; + if (!okoSolWallet) { + return; + } try { await okoSolWallet.disconnect(); - setConnected(false, null); console.log("[sandbox_sol] Disconnected"); } catch (err) { const message = err instanceof Error ? err.message : String(err); setError(message); console.error("[sandbox_sol] Failed to disconnect:", message); } - }, [okoSolWallet, setConnected]); + }, [okoSolWallet]); return { okoWallet, diff --git a/sandbox/sandbox_sol/src/lib/connection.ts b/sandbox/sandbox_sol/src/lib/connection.ts index 1d3efd242..5352382fc 100644 --- a/sandbox/sandbox_sol/src/lib/connection.ts +++ b/sandbox/sandbox_sol/src/lib/connection.ts @@ -1,6 +1,8 @@ import { Connection } from "@solana/web3.js"; +const DEFAULT_RPC_URL = "https://api.devnet.solana.com"; + export const DEVNET_CONNECTION = new Connection( - "https://api.devnet.solana.com", + process.env.NEXT_PUBLIC_SOLANA_RPC_URL || DEFAULT_RPC_URL, "confirmed", ); diff --git a/sdk/oko_sdk_core/src/methods/sign_in/index.ts b/sdk/oko_sdk_core/src/methods/sign_in/index.ts index 15c2cd4c6..9baf573cd 100644 --- a/sdk/oko_sdk_core/src/methods/sign_in/index.ts +++ b/sdk/oko_sdk_core/src/methods/sign_in/index.ts @@ -33,7 +33,6 @@ export async function signIn(this: OkoWalletInterface, type: SignInType) { throw new Error(`Sign in error, err: ${err}`); } - // @TODO: required for ed25519 support const walletInfo = await this.getWalletInfo(); if (!walletInfo) {