diff --git a/apps/demo_web/package.json b/apps/demo_web/package.json
index 0029df80f..b33880ef5 100644
--- a/apps/demo_web/package.json
+++ b/apps/demo_web/package.json
@@ -23,6 +23,7 @@
"@oko-wallet/oko-sdk-core": "^0.0.6-rc.130",
"@oko-wallet/oko-sdk-cosmos": "^0.0.6-rc.157",
"@oko-wallet/oko-sdk-eth": "^0.0.6-rc.145",
+ "@oko-wallet/oko-sdk-sol": "workspace:*",
"@oko-wallet/stdlib-js": "^0.0.2-rc.44",
"@types/node": "^24.10.1",
"@types/react": "^19.2.7",
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 18bff523f..6e56d1b85 100644
--- a/apps/demo_web/src/components/oko_provider/use_oko.ts
+++ b/apps/demo_web/src/components/oko_provider/use_oko.ts
@@ -5,14 +5,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 isInitialized = useSDKState(
- (state) => state.oko_cosmos !== null && state.oko_eth !== null,
+ (state) =>
+ state.oko_cosmos !== null &&
+ state.oko_eth !== null &&
+ state.oko_sol !== null,
);
useEffect(() => {
initOkoCosmos();
initOkoEth();
+ 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 6c7a2d894..2d5f882fa 100644
--- a/apps/demo_web/src/components/preview_panel/preview_panel.tsx
+++ b/apps/demo_web/src/components/preview_panel/preview_panel.tsx
@@ -11,12 +11,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";
+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: React.FC = () => {
const isLazyInitialized = useSDKState(
- (st) => st.isCosmosLazyInitialized && st.isEthLazyInitialized,
+ (st) =>
+ st.isCosmosLazyInitialized &&
+ st.isEthLazyInitialized &&
+ st.isSolLazyInitialized,
);
const isSignedIn = useUserInfoState((state) => state.isSignedIn);
@@ -42,6 +47,12 @@ export const PreviewPanel: React.FC = () => {
)}
+ {isSignedIn && (
+
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
new file mode 100644
index 000000000..4373584e3
--- /dev/null
+++ b/apps/demo_web/src/components/widgets/solana_offchain_sign_widget/solana_offchain_sign_widget.tsx
@@ -0,0 +1,36 @@
+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.module.scss b/apps/demo_web/src/components/widgets/solana_onchain_sign_widget/solana_onchain_sign_widget.module.scss
new file mode 100644
index 000000000..3c0a9661a
--- /dev/null
+++ b/apps/demo_web/src/components/widgets/solana_onchain_sign_widget/solana_onchain_sign_widget.module.scss
@@ -0,0 +1,3 @@
+.checkboxContainer {
+ margin: -4px 0 20px 20px;
+}
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
new file mode 100644
index 000000000..a7477a144
--- /dev/null
+++ b/apps/demo_web/src/components/widgets/solana_onchain_sign_widget/solana_onchain_sign_widget.tsx
@@ -0,0 +1,132 @@
+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/state/sdk.ts b/apps/demo_web/src/state/sdk.ts
index ea30bdb7c..e3b62e6d0 100644
--- a/apps/demo_web/src/state/sdk.ts
+++ b/apps/demo_web/src/state/sdk.ts
@@ -6,6 +6,10 @@ import {
OkoEthWallet,
type OkoEthWalletInterface,
} from "@oko-wallet/oko-sdk-eth";
+import {
+ OkoSolWallet,
+ type OkoSolWalletInterface,
+} from "@oko-wallet/oko-sdk-sol";
import { create } from "zustand";
import { combine } from "zustand/middleware";
@@ -14,28 +18,37 @@ import { useUserInfoState } from "@oko-wallet-demo-web/state/user_info";
interface SDKState {
oko_eth: OkoEthWalletInterface | null;
oko_cosmos: OkoCosmosWalletInterface | null;
+ oko_sol: OkoSolWalletInterface | null;
isEthInitializing: boolean;
isEthLazyInitialized: boolean;
isCosmosInitializing: boolean;
isCosmosLazyInitialized: boolean;
+
+ isSolInitializing: boolean;
+ isSolLazyInitialized: boolean;
}
interface SDKActions {
initOkoEth: () => Promise
;
initOkoCosmos: () => Promise;
+ initOkoSol: () => Promise;
}
const initialState: SDKState = {
oko_eth: null,
oko_cosmos: null,
+ oko_sol: null,
isEthInitializing: false,
isEthLazyInitialized: false,
isCosmosInitializing: false,
isCosmosLazyInitialized: false,
+
+ isSolInitializing: false,
+ isSolLazyInitialized: false,
};
export const useSDKState = create(
@@ -127,6 +140,59 @@ export const useSDKState = create(
return null;
}
},
+ initOkoSol: async () => {
+ const state = get();
+
+ 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...");
+ set({
+ 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;
+ }
+ } catch (e) {
+ console.error("Sol SDK init error:", e);
+ set({ isSolInitializing: false, isSolLazyInitialized: true });
+ return null;
+ }
+ },
})),
);
@@ -134,9 +200,16 @@ function setupCosmosListener(cosmosSDK: OkoCosmosWalletInterface) {
const setUserInfo = useUserInfoState.getState().setUserInfo;
if (cosmosSDK) {
+ console.log("[Demo] Setting up Cosmos accountsChanged listener");
cosmosSDK.on({
type: "accountsChanged",
handler: ({ authType, email, publicKey, name }) => {
+ console.log("[Demo] accountsChanged event received:", {
+ authType,
+ email,
+ publicKey: publicKey ? "exists" : "null",
+ name,
+ });
setUserInfo({
authType: authType || null,
email: email || null,
diff --git a/apps/docs_web/docs/v0/sdk-usage/sdk-overview.md b/apps/docs_web/docs/v0/sdk-usage/sdk-overview.md
index aa66cdeb4..fae937c5b 100644
--- a/apps/docs_web/docs/v0/sdk-usage/sdk-overview.md
+++ b/apps/docs_web/docs/v0/sdk-usage/sdk-overview.md
@@ -23,6 +23,9 @@ npm install @oko-wallet/oko-sdk-cosmos
# For Ethereum/EVM chains
npm install @oko-wallet/oko-sdk-eth
+# For Solana
+npm install @oko-wallet/oko-sdk-sol
+
# Core SDK (for custom integration)
npm install @oko-wallet/oko-sdk-core
```
@@ -56,10 +59,25 @@ const ethWallet = initRes.data;
const provider = await ethWallet.getEthereumProvider();
```
+### Solana
+
+```typescript
+import { OkoSolWallet } from "@oko-wallet/oko-sdk-sol";
+
+const initRes = OkoSolWallet.init(config);
+if (!initRes.success) {
+ throw new Error(`Solana wallet initialization failed: ${initRes.err}`);
+}
+
+const solWallet = initRes.data;
+await solWallet.connect();
+```
+
## Next Steps
- **[Cosmos Integration](./cosmos-integration)** - Complete Cosmos setup
- **[Ethereum Integration](./ethereum-integration)** - Complete Ethereum setup
+- **[Solana Integration](./solana-integration)** - Complete Solana setup
- **[React Integration](./react-integration)** - React patterns
- **[RainbowKit Integration](./rainbow-kit-integration)** - RainbowKit
integration
diff --git a/apps/docs_web/docs/v0/sdk-usage/solana-integration.md b/apps/docs_web/docs/v0/sdk-usage/solana-integration.md
new file mode 100644
index 000000000..af76700e4
--- /dev/null
+++ b/apps/docs_web/docs/v0/sdk-usage/solana-integration.md
@@ -0,0 +1,198 @@
+---
+title: Solana Integration
+sidebar_position: 4
+---
+
+# Solana Integration
+
+Complete guide for integrating Oko with Solana.
+
+
+:::tip Wallet Standard Support
+Oko Solana SDK fully supports the [Wallet Standard](https://github.com/anza-xyz/wallet-standard), making it compatible with most Solana dApps automatically.
+:::
+
+## Installation
+
+```bash
+npm install @oko-wallet/oko-sdk-sol @solana/web3.js
+```
+
+## Basic Setup
+
+```typescript
+import { OkoSolWallet } from "@oko-wallet/oko-sdk-sol";
+
+// Initialize Solana wallet
+const initRes = OkoSolWallet.init({
+ api_key: "your-api-key",
+});
+
+if (!initRes.success) {
+ throw new Error(`Solana wallet initialization failed: ${initRes.err}`);
+}
+
+const wallet = initRes.data;
+
+// Connect to get public key
+await wallet.connect();
+console.log("Connected:", wallet.publicKey?.toBase58());
+```
+
+## On-chain Signing
+
+### Send Transaction
+
+```typescript
+import {
+ Connection,
+ Transaction,
+ SystemProgram,
+ PublicKey,
+ LAMPORTS_PER_SOL,
+} from "@solana/web3.js";
+
+const connection = new Connection("https://api.mainnet-beta.solana.com");
+const recipientAddress = new PublicKey("...");
+
+// Create transaction
+const transaction = new Transaction().add(
+ SystemProgram.transfer({
+ fromPubkey: wallet.publicKey!,
+ toPubkey: recipientAddress,
+ lamports: 0.1 * LAMPORTS_PER_SOL,
+ })
+);
+
+// Get recent blockhash
+const { blockhash } = await connection.getLatestBlockhash();
+transaction.recentBlockhash = blockhash;
+transaction.feePayer = wallet.publicKey!;
+
+// Sign and send
+const signature = await wallet.sendTransaction(transaction, connection);
+console.log("Transaction sent:", signature);
+```
+
+### Sign and Send Transaction (Phantom-compatible)
+
+```typescript
+// Alternative method that returns { signature }
+const { signature } = await wallet.signAndSendTransaction(
+ transaction,
+ connection
+);
+```
+
+### Sign Multiple Transactions
+
+```typescript
+const transactions = [transaction1, transaction2, transaction3];
+const signedTransactions = await wallet.signAllTransactions(transactions);
+
+// Send each signed transaction
+for (const signed of signedTransactions) {
+ const sig = await connection.sendRawTransaction(signed.serialize());
+ console.log("Sent:", sig);
+}
+```
+
+## Off-chain Signing
+
+### Sign Message
+
+```typescript
+const message = new TextEncoder().encode("Welcome to Oko!");
+const signature = await wallet.signMessage(message);
+
+console.log("Signature:", Buffer.from(signature).toString("base64"));
+```
+
+## Event Handling
+
+```typescript
+// Listen for connection events
+wallet.on("connect", (publicKey) => {
+ console.log("Connected:", publicKey.toBase58());
+});
+
+wallet.on("disconnect", () => {
+ console.log("Disconnected");
+});
+
+// Listen for account changes
+wallet.on("accountChanged", (publicKey) => {
+ if (publicKey) {
+ console.log("Account changed:", publicKey.toBase58());
+ } else {
+ console.log("Account disconnected");
+ }
+});
+
+// Remove listener
+const unsubscribe = wallet.on("connect", handler);
+unsubscribe(); // or wallet.off("connect", handler);
+```
+
+## Wallet Standard Integration
+
+Register Oko wallet for automatic discovery by dApps using `@solana/wallet-adapter`:
+
+```typescript
+import { OkoSolWallet, registerOkoWallet } from "@oko-wallet/oko-sdk-sol";
+
+const initRes = OkoSolWallet.init({ api_key: "your-api-key" });
+if (initRes.success) {
+ const wallet = initRes.data;
+
+ // Register with wallet-standard
+ registerOkoWallet(wallet);
+
+ // Now dApps using getWallets() will discover Oko automatically
+}
+```
+
+### Supported Features
+
+| Feature | Description |
+|---------|-------------|
+| `standard:connect` | Connect to wallet |
+| `standard:disconnect` | Disconnect from wallet |
+| `standard:events` | Subscribe to wallet events |
+| `solana:signMessage` | Sign arbitrary messages |
+| `solana:signTransaction` | Sign transactions |
+| `solana:signAndSendTransaction` | Sign and broadcast transactions |
+
+## Versioned Transactions
+
+Oko supports both legacy and versioned transactions:
+
+```typescript
+import { VersionedTransaction, TransactionMessage } from "@solana/web3.js";
+
+// Create versioned transaction
+const messageV0 = new TransactionMessage({
+ payerKey: wallet.publicKey!,
+ recentBlockhash: blockhash,
+ instructions: [transferInstruction],
+}).compileToV0Message();
+
+const versionedTx = new VersionedTransaction(messageV0);
+
+// Sign versioned transaction
+const signedTx = await wallet.signTransaction(versionedTx);
+```
+
+## Disconnect
+
+```typescript
+await wallet.disconnect();
+console.log("Wallet disconnected");
+```
+
+## Next Steps
+
+- **[Ethereum Integration](./ethereum-integration)** - Add Ethereum support
+- **[Cosmos Integration](./cosmos-integration)** - Add Cosmos support
+- **[React Integration](./react-integration)** - React patterns
+- **[Error Handling](./error-handling)** - Error handling
diff --git a/backend/tss_api/src/api/keygen_ed25519/index.test.ts b/backend/tss_api/src/api/keygen_ed25519/index.test.ts
index 3eb81146b..c748e8d24 100644
--- a/backend/tss_api/src/api/keygen_ed25519/index.test.ts
+++ b/backend/tss_api/src/api/keygen_ed25519/index.test.ts
@@ -4,8 +4,8 @@ import { Participant } from "@oko-wallet/teddsa-interface";
import { runKeygenCentralizedEd25519 } from "@oko-wallet/teddsa-addon/src/server";
import { createPgConn } from "@oko-wallet/postgres-lib";
import { insertKSNode } from "@oko-wallet/oko-pg-interface/ks_nodes";
-import { createUser } from "@oko-wallet/oko-pg-interface/ewallet_users";
-import { createWallet } from "@oko-wallet/oko-pg-interface/ewallet_wallets";
+import { createUser } from "@oko-wallet/oko-pg-interface/oko_users";
+import { createWallet } from "@oko-wallet/oko-pg-interface/oko_wallets";
import { insertKeyShareNodeMeta } from "@oko-wallet/oko-pg-interface/key_share_node_meta";
import type { WalletStatus } from "@oko-wallet/oko-types/wallets";
@@ -46,16 +46,17 @@ async function setUpKeyShareNodeMeta(pool: Pool): Promise {
function generateKeygenRequest(
keygenResult: ReturnType,
- email: string = TEST_EMAIL,
+ user_identifier: string = TEST_EMAIL,
): KeygenEd25519Request {
const serverKeygenOutput = keygenResult.keygen_outputs[Participant.P1];
return {
auth_type: "google",
- email,
+ user_identifier,
keygen_2: {
...serverKeygenOutput,
public_key: [...keygenResult.public_key],
},
+ email: user_identifier,
};
}
@@ -214,11 +215,12 @@ describe("Ed25519 Keygen", () => {
const request: KeygenEd25519Request = {
auth_type: "google",
- email: TEST_EMAIL,
+ user_identifier: TEST_EMAIL,
keygen_2: {
...serverKeygenOutput,
public_key: [...keygenResult.public_key],
},
+ email: TEST_EMAIL,
name: "Test User",
};
@@ -247,11 +249,12 @@ describe("Ed25519 Keygen", () => {
const request: KeygenEd25519Request = {
auth_type: authTypes[i],
- email: `authtype-test-${i}@test.com`,
+ user_identifier: `authtype-test-${i}@test.com`,
keygen_2: {
...serverKeygenOutput,
public_key: [...keygenResult.public_key],
},
+ email: `authtype-test-${i}@test.com`,
};
const result = await runKeygenEd25519(
diff --git a/backend/tss_api/src/api/keygen_ed25519/index.ts b/backend/tss_api/src/api/keygen_ed25519/index.ts
index d2842c336..650bcb609 100644
--- a/backend/tss_api/src/api/keygen_ed25519/index.ts
+++ b/backend/tss_api/src/api/keygen_ed25519/index.ts
@@ -2,7 +2,7 @@ import { Pool } from "pg";
import {
createUser,
getUserByEmailAndAuthType,
-} from "@oko-wallet/oko-pg-interface/ewallet_users";
+} from "@oko-wallet/oko-pg-interface/oko_users";
import type { Result } from "@oko-wallet/stdlib-js";
import { encryptDataAsync } from "@oko-wallet/crypto-js/node";
import { Bytes, type Bytes32 } from "@oko-wallet/bytes";
@@ -14,7 +14,7 @@ import {
createWallet,
getActiveWalletByUserIdAndCurveType,
getWalletByPublicKey,
-} from "@oko-wallet/oko-pg-interface/ewallet_wallets";
+} from "@oko-wallet/oko-pg-interface/oko_wallets";
import {
createWalletKSNodes,
getActiveKSNodes,
@@ -33,9 +33,13 @@ export async function runKeygenEd25519(
encryptionSecret: string,
): Promise> {
try {
- const { auth_type, email, keygen_2, name } = keygenRequest;
+ const { auth_type, user_identifier, keygen_2, email, name } = keygenRequest;
- const getUserRes = await getUserByEmailAndAuthType(db, email, auth_type);
+ const getUserRes = await getUserByEmailAndAuthType(
+ db,
+ user_identifier,
+ auth_type,
+ );
if (getUserRes.success === false) {
return {
success: false,
@@ -68,7 +72,7 @@ export async function runKeygenEd25519(
};
}
} else {
- const createUserRes = await createUser(db, email, auth_type);
+ const createUserRes = await createUser(db, user_identifier, auth_type);
if (createUserRes.success === false) {
return {
success: false,
@@ -202,7 +206,7 @@ export async function runKeygenEd25519(
const tokenResult = generateUserToken({
wallet_id: secp256k1WalletId,
wallet_id_ed25519: wallet.wallet_id,
- email: email,
+ email: user_identifier,
jwt_config: jwtConfig,
});
@@ -219,9 +223,10 @@ export async function runKeygenEd25519(
data: {
token: tokenResult.data.token,
user: {
- email: email,
wallet_id: wallet.wallet_id,
public_key: publicKeyHex,
+ user_identifier,
+ email: email ?? null,
name: name ?? null,
},
},
diff --git a/backend/tss_api/src/api/sign_ed25519/index.test.ts b/backend/tss_api/src/api/sign_ed25519/index.test.ts
index 3d37f99f2..9c99a7940 100644
--- a/backend/tss_api/src/api/sign_ed25519/index.test.ts
+++ b/backend/tss_api/src/api/sign_ed25519/index.test.ts
@@ -17,8 +17,8 @@ import {
import { createPgConn } from "@oko-wallet/postgres-lib";
import type { WalletStatus } from "@oko-wallet/oko-types/wallets";
import { insertKSNode } from "@oko-wallet/oko-pg-interface/ks_nodes";
-import { createWallet } from "@oko-wallet/oko-pg-interface/ewallet_wallets";
-import { createUser } from "@oko-wallet/oko-pg-interface/ewallet_users";
+import { createWallet } from "@oko-wallet/oko-pg-interface/oko_wallets";
+import { createUser } from "@oko-wallet/oko-pg-interface/oko_users";
import { insertKeyShareNodeMeta } from "@oko-wallet/oko-pg-interface/key_share_node_meta";
import { insertCustomer } from "@oko-wallet/oko-pg-interface/customers";
import { encryptDataAsync } from "@oko-wallet/crypto-js/node";
diff --git a/backend/tss_api/src/api/wallet_ed25519/index.ts b/backend/tss_api/src/api/wallet_ed25519/index.ts
index af7584618..bdb47a935 100644
--- a/backend/tss_api/src/api/wallet_ed25519/index.ts
+++ b/backend/tss_api/src/api/wallet_ed25519/index.ts
@@ -1,7 +1,7 @@
import { Pool } from "pg";
import { decryptDataAsync } from "@oko-wallet/crypto-js/node";
-import { getActiveWalletByUserIdAndCurveType } from "@oko-wallet/oko-pg-interface/ewallet_wallets";
-import { getUserByEmailAndAuthType } from "@oko-wallet/oko-pg-interface/ewallet_users";
+import { getActiveWalletByUserIdAndCurveType } from "@oko-wallet/oko-pg-interface/oko_wallets";
+import { getUserByEmailAndAuthType } from "@oko-wallet/oko-pg-interface/oko_users";
import type { OkoApiResponse } from "@oko-wallet/oko-types/api_response";
import type { AuthType } from "@oko-wallet/oko-types/auth";
import {
@@ -10,7 +10,7 @@ import {
} from "@oko-wallet/teddsa-interface";
export interface WalletEd25519PublicInfoRequest {
- email: string;
+ user_identifier: string;
auth_type: AuthType;
}
@@ -31,10 +31,10 @@ export async function getWalletEd25519PublicInfo(
request: WalletEd25519PublicInfoRequest,
): Promise> {
try {
- const { email, auth_type } = request;
+ const { user_identifier, auth_type } = request;
// Get user
- const getUserRes = await getUserByEmailAndAuthType(db, email, auth_type);
+ const getUserRes = await getUserByEmailAndAuthType(db, user_identifier, auth_type);
if (getUserRes.success === false) {
return {
success: false,
diff --git a/backend/tss_api/src/routes/keygen_ed25519.ts b/backend/tss_api/src/routes/keygen_ed25519.ts
index 0e634935e..be9ab8df4 100644
--- a/backend/tss_api/src/routes/keygen_ed25519.ts
+++ b/backend/tss_api/src/routes/keygen_ed25519.ts
@@ -91,11 +91,13 @@ export function setKeygenEd25519Routes(router: Router) {
const auth_type = oauthUser.type as AuthType;
const body = req.body;
- if (!oauthUser?.email) {
+ const user_identifier = oauthUser.user_identifier;
+
+ if (!user_identifier) {
res.status(401).json({
success: false,
code: "UNAUTHORIZED",
- msg: "User email not found",
+ msg: "User identifier not found",
});
return;
}
@@ -110,8 +112,9 @@ export function setKeygenEd25519Routes(router: Router) {
jwtConfig,
{
auth_type,
- email: oauthUser.email.toLowerCase(),
+ user_identifier,
keygen_2: body.keygen_2,
+ email: oauthUser.email,
name: oauthUser.name,
},
state.encryption_secret,
diff --git a/backend/tss_api/src/routes/wallet_ed25519.ts b/backend/tss_api/src/routes/wallet_ed25519.ts
index d5f29446b..92ba2cca1 100644
--- a/backend/tss_api/src/routes/wallet_ed25519.ts
+++ b/backend/tss_api/src/routes/wallet_ed25519.ts
@@ -82,11 +82,12 @@ export function setWalletEd25519Routes(router: Router) {
const state = req.app.locals;
const oauthUser = res.locals.oauth_user;
- if (!oauthUser?.email) {
+ const user_identifier = oauthUser?.user_identifier;
+ if (!user_identifier) {
res.status(401).json({
success: false,
code: "UNAUTHORIZED",
- msg: "User email not found",
+ msg: "User identifier not found",
});
return;
}
@@ -95,7 +96,7 @@ export function setWalletEd25519Routes(router: Router) {
state.db,
state.encryption_secret,
{
- email: oauthUser.email.toLowerCase(),
+ user_identifier,
auth_type: oauthUser.type,
},
);
diff --git a/common/oko_types/src/tss/auth.ts b/common/oko_types/src/tss/auth.ts
index cc2913138..4339602e3 100644
--- a/common/oko_types/src/tss/auth.ts
+++ b/common/oko_types/src/tss/auth.ts
@@ -1,5 +1,5 @@
export interface UserTokenPayload {
- email: string;
+ email?: string;
wallet_id: string;
wallet_id_ed25519?: string;
type: "user";
@@ -8,7 +8,7 @@ export interface UserTokenPayload {
export interface GenerateUserTokenArgs {
wallet_id: string;
wallet_id_ed25519?: string;
- email: string;
+ email?: string;
jwt_config: {
secret: string;
expires_in: string;
diff --git a/common/oko_types/src/tss/keygen_ed25519.ts b/common/oko_types/src/tss/keygen_ed25519.ts
index 31d969507..cd885fb4a 100644
--- a/common/oko_types/src/tss/keygen_ed25519.ts
+++ b/common/oko_types/src/tss/keygen_ed25519.ts
@@ -8,8 +8,9 @@ export interface TeddsaKeygenOutputWithPublicKey extends TeddsaKeygenOutput {
export interface KeygenEd25519Request {
auth_type: AuthType;
- email: string;
+ user_identifier: string;
keygen_2: TeddsaKeygenOutputWithPublicKey;
+ email?: string;
name?: string;
}
diff --git a/crypto/teddsa/api_lib/src/index.ts b/crypto/teddsa/api_lib/src/index.ts
index 51ef79981..f608e1ed8 100644
--- a/crypto/teddsa/api_lib/src/index.ts
+++ b/crypto/teddsa/api_lib/src/index.ts
@@ -6,6 +6,10 @@ import type {
SignEd25519Round2Response,
SignEd25519AggregateBody,
SignEd25519AggregateResponse,
+ PresignEd25519Body,
+ PresignEd25519Response,
+ SignEd25519Body,
+ SignEd25519Response,
} from "@oko-wallet/oko-types/tss";
import type { SignInResponse } from "@oko-wallet/oko-types/user";
import type {
@@ -75,6 +79,7 @@ async function makePostRequest(
path: string,
payload: T,
authToken?: string,
+ apiKey?: string,
): Promise {
const url = `${endpoint}/${path}`;
@@ -86,6 +91,10 @@ async function makePostRequest(
headers.Authorization = `Bearer ${authToken}`;
}
+ if (apiKey) {
+ headers["x-api-key"] = apiKey;
+ }
+
const ret = await fetch(url, {
headers,
method: "POST",
@@ -169,3 +178,33 @@ export async function reqSignEd25519Aggregate(
);
return resp;
}
+
+export async function reqPresignEd25519(
+ endpoint: string,
+ payload: PresignEd25519Body,
+ apiKey: string,
+ authToken: string,
+) {
+ const resp: OkoApiResponse = await makePostRequest(
+ endpoint,
+ "presign_ed25519",
+ payload,
+ authToken,
+ apiKey,
+ );
+ return resp;
+}
+
+export async function reqSignEd25519(
+ endpoint: string,
+ payload: SignEd25519Body,
+ authToken: string,
+) {
+ const resp: OkoApiResponse = await makePostRequest(
+ endpoint,
+ "sign_ed25519",
+ payload,
+ authToken,
+ );
+ return resp;
+}
diff --git a/crypto/teddsa/teddsa_hooks/package.json b/crypto/teddsa/teddsa_hooks/package.json
index 6da1116df..a0f6b6ee8 100644
--- a/crypto/teddsa/teddsa_hooks/package.json
+++ b/crypto/teddsa/teddsa_hooks/package.json
@@ -4,6 +4,7 @@
"version": "0.1.0",
"dependencies": {
"@oko-wallet/bytes": "^0.0.3-alpha.62",
+ "@oko-wallet/frost-ed25519-keplr-wasm": "workspace:*",
"@oko-wallet/stdlib-js": "^0.0.2-rc.42",
"@oko-wallet/teddsa-interface": "workspace:*",
"@oko-wallet/teddsa-wasm-mock": "workspace:*"
@@ -12,4 +13,4 @@
"@types/node": "^24.10.1",
"typescript": "^5.8.3"
}
-}
\ No newline at end of file
+}
diff --git a/crypto/teddsa/teddsa_hooks/src/keygen.ts b/crypto/teddsa/teddsa_hooks/src/keygen.ts
index 508c9b788..9dd76a808 100644
--- a/crypto/teddsa/teddsa_hooks/src/keygen.ts
+++ b/crypto/teddsa/teddsa_hooks/src/keygen.ts
@@ -1,4 +1,4 @@
-import { wasmModule } from "@oko-wallet/teddsa-wasm";
+import { wasmModule } from "@oko-wallet/frost-ed25519-keplr-wasm";
import { Bytes } from "@oko-wallet/bytes";
import type { Bytes32 } from "@oko-wallet/bytes";
import type { Result } from "@oko-wallet/stdlib-js";
diff --git a/crypto/teddsa/teddsa_hooks/src/sign.ts b/crypto/teddsa/teddsa_hooks/src/sign.ts
index 737c8ecbc..da69b159c 100644
--- a/crypto/teddsa/teddsa_hooks/src/sign.ts
+++ b/crypto/teddsa/teddsa_hooks/src/sign.ts
@@ -1,4 +1,4 @@
-import { wasmModule } from "@oko-wallet/teddsa-wasm";
+import { wasmModule } from "@oko-wallet/frost-ed25519-keplr-wasm";
import type { Result } from "@oko-wallet/stdlib-js";
import type {
TeddsaSignRound1Output,
diff --git a/embed/oko_attached/package.json b/embed/oko_attached/package.json
index 13fd27cc5..216ae9102 100644
--- a/embed/oko_attached/package.json
+++ b/embed/oko_attached/package.json
@@ -6,7 +6,7 @@
"scripts": {
"create_env": "tsx src/bin/create_env.ts",
"clean": "del-cli ./public/pkg",
- "copy_wasm": "yarn run clean && cp -R ../../crypto/tecdsa/cait_sith_keplr_wasm/pkg ./public",
+ "copy_wasm": "yarn run clean && cp -R ../../crypto/tecdsa/cait_sith_keplr_wasm/pkg ./public && cp ../../crypto/teddsa/frost_ed25519_keplr_wasm/pkg/frost_ed25519_keplr_wasm_bg.wasm ./public/pkg/",
"dev": "tsx ./src/bin/launch.ts",
"build": "vite build && tsc --noEmit",
"serve": "vite preview",
@@ -35,6 +35,7 @@
"@oko-wallet/cait-sith-keplr-wasm": "workspace:*",
"@oko-wallet/crypto-js": "^0.0.2-alpha.8",
"@oko-wallet/dotenv": "^0.0.2-alpha.29",
+ "@oko-wallet/frost-ed25519-keplr-wasm": "workspace:*",
"@oko-wallet/ksn-interface": "^0.0.2-alpha.60",
"@oko-wallet/oko-common-ui": "workspace:*",
"@oko-wallet/oko-sdk-core": "^0.0.6-rc.130",
@@ -42,12 +43,18 @@
"@oko-wallet/oko-sdk-eth": "^0.0.6-rc.145",
"@oko-wallet/stdlib-js": "^0.0.2-rc.44",
"@oko-wallet/tecdsa-interface": "0.0.2-alpha.22",
+ "@oko-wallet/teddsa-hooks": "workspace:*",
+ "@oko-wallet/teddsa-interface": "workspace:*",
+ "@solana/web3.js": "^1.98.0",
+ "@solanafm/explorer-kit": "^1.2.0",
+ "@solanafm/explorer-kit-idls": "^1.1.4",
"@tanstack/react-query": "^5.90.12",
"@tanstack/react-router": "^1.136.8",
"@tanstack/react-router-devtools": "^1.136.8",
"@tanstack/router-plugin": "^1.136.8",
"auth0-js": "^9.29.0",
"bitcoinjs-lib": "^6.1.7",
+ "bs58": "^6.0.0",
"chalk": "^5.5.0",
"cosmjs-types": "^0.9.0",
"jsonwebtoken": "^9.0.2",
diff --git a/embed/oko_attached/src/analytics/events.ts b/embed/oko_attached/src/analytics/events.ts
index 06426063c..eaff2fd4f 100644
--- a/embed/oko_attached/src/analytics/events.ts
+++ b/embed/oko_attached/src/analytics/events.ts
@@ -1,4 +1,5 @@
import { useEffect, useRef } from "react";
+import type { ParsedInstruction } from "@oko-wallet-attached/tx-parsers/sol";
import type { EthTxAction } from "@oko-wallet-attached/components/modal_variants/eth/tx_sig/actions/types";
import { trackEvent } from "./amplitude";
@@ -48,24 +49,30 @@ export function useTrackTxSummaryView(args: UseTrackTxSummaryViewArgs) {
}
export function trackTxButtonEvent(args: TrackTxButtonEventArgs) {
- const { hostOrigin, chainType, chainId, eventType } = args;
+ const { hostOrigin, chainType, eventType } = args;
+
+ let txTypes: string[];
+ let chainId: string | undefined;
- let txTypes;
switch (chainType) {
case "eth": {
- const { actions } = args;
+ const { actions, chainId: ethChainId } = args;
txTypes = classifyEthTxType(actions);
+ chainId = ethChainId;
break;
}
case "cosmos": {
- const { messages } = args;
+ const { messages, chainId: cosmosChainId } = args;
txTypes = classifyCosmosTxType(messages);
+ chainId = cosmosChainId;
break;
}
- default: {
- throw new Error("invalid chain type");
+ case "solana": {
+ const { instructions } = args;
+ txTypes = classifySolanaTxType(instructions);
+ break;
}
}
@@ -96,3 +103,11 @@ function classifyCosmosTxType(messages: CosmosMsgs) {
return messages.map((msg) => ("typeUrl" in msg ? msg.typeUrl : msg.type));
}
+
+function classifySolanaTxType(instructions: ParsedInstruction[] | null) {
+ if (instructions === null || instructions.length === 0) {
+ return ["unknown"];
+ }
+
+ return instructions.map((ix) => ix.instructionName);
+}
diff --git a/embed/oko_attached/src/analytics/types.ts b/embed/oko_attached/src/analytics/types.ts
index eac34608e..50354a249 100644
--- a/embed/oko_attached/src/analytics/types.ts
+++ b/embed/oko_attached/src/analytics/types.ts
@@ -1,13 +1,14 @@
import type { Msg } from "@keplr-wallet/types";
import type { Any } from "@keplr-wallet/proto-types/google/protobuf/any";
import type { AminoMsg } from "@cosmjs/amino";
+import type { ParsedInstruction } from "@oko-wallet-attached/tx-parsers/sol";
import type { EthTxAction } from "@oko-wallet-attached/components/modal_variants/eth/tx_sig/actions/types";
import type { UnpackedMsgForView } from "@oko-wallet-attached/types/cosmos_msg";
export type TxType = string;
-export type ChainType = "eth" | "cosmos";
+export type ChainType = "eth" | "cosmos" | "solana";
export type CosmosMsgs =
| Any[]
@@ -45,4 +46,10 @@ export type TrackTxButtonEventArgs =
chainType: "eth";
chainId: string;
actions: EthTxAction[];
+ }
+ | {
+ eventType: "approve" | "reject";
+ hostOrigin: string;
+ chainType: "solana";
+ instructions: ParsedInstruction[] | null;
};
diff --git a/embed/oko_attached/src/components/modal/modal_dialog.tsx b/embed/oko_attached/src/components/modal/modal_dialog.tsx
index 9908f757d..1040952b1 100644
--- a/embed/oko_attached/src/components/modal/modal_dialog.tsx
+++ b/embed/oko_attached/src/components/modal/modal_dialog.tsx
@@ -3,6 +3,7 @@ import { ErrorBoundary } from "react-error-boundary";
import { MakeSignatureCosmosModal } from "@oko-wallet-attached/components/modal_variants/cosmos/make_signature_cosmos_modal";
import { MakeSignatureEthModal } from "@oko-wallet-attached/components/modal_variants/eth/make_sig_eth_modal";
+import { MakeSignatureSolModal } from "@oko-wallet-attached/components/modal_variants/sol/make_signature_sol_modal";
import type { ModalRequest } from "@oko-wallet-attached/store/memory/types";
import { ErrorModal } from "@oko-wallet-attached/components/modal_variants/error/error_modal";
import { useMemoryState } from "@oko-wallet-attached/store/memory";
@@ -91,6 +92,17 @@ export const ModalDialog: FC = ({ modalRequest }) => {
break;
}
+ case "sol/make_signature": {
+ component = (
+
+ );
+ break;
+ }
+
// case "auth/email_login": {
// component = (
//
diff --git a/embed/oko_attached/src/components/modal_variants/sol/all_tx_sig/make_all_tx_sig_modal.tsx b/embed/oko_attached/src/components/modal_variants/sol/all_tx_sig/make_all_tx_sig_modal.tsx
new file mode 100644
index 000000000..7a7193fe6
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/all_tx_sig/make_all_tx_sig_modal.tsx
@@ -0,0 +1,94 @@
+import type { FC } from "react";
+import type { MakeSolAllTxSignData } from "@oko-wallet/oko-sdk-core";
+import { XCloseIcon } from "@oko-wallet/oko-common-ui/icons/x_close";
+import { Spacing } from "@oko-wallet/oko-common-ui/spacing";
+import { Button } from "@oko-wallet/oko-common-ui/button";
+
+import styles from "@oko-wallet-attached/components/modal_variants/common/make_signature/make_signature_modal.module.scss";
+import { CommonModal } from "@oko-wallet-attached/components/modal_variants/common/common_modal";
+import { DemoView } from "@oko-wallet-attached/components/modal_variants/common/make_signature/demo_view";
+import { SignWithOkoBox } from "@oko-wallet-attached/components/sign_with_oko_box/sign_with_oko_box";
+import { useAllTxSigModal } from "./use_all_tx_sig_modal";
+import { SolanaAllTxSignatureContent } from "./sol_all_tx_signature_content";
+
+export interface MakeAllTxSigModalProps {
+ getIsAborted: () => boolean;
+ modalId: string;
+ data: MakeSolAllTxSignData;
+}
+
+export const MakeAllTxSigModal: FC = ({
+ getIsAborted,
+ data,
+ modalId,
+}) => {
+ const {
+ onReject,
+ onApprove,
+ isLoading,
+ isApproveEnabled,
+ isDemo,
+ theme,
+ txCount,
+ signingProgress,
+ } = useAllTxSigModal({
+ getIsAborted,
+ data,
+ modalId,
+ });
+
+ function handleRejectClick() {
+ onReject();
+ }
+
+ function handleApproveClick() {
+ onApprove();
+ }
+
+ const buttonText = isLoading
+ ? `Signing ${signingProgress}/${txCount}...`
+ : "Approve All";
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {isDemo &&
}
+
+ );
+};
diff --git a/embed/oko_attached/src/components/modal_variants/sol/all_tx_sig/sol_all_tx_signature_content.tsx b/embed/oko_attached/src/components/modal_variants/sol/all_tx_sig/sol_all_tx_signature_content.tsx
new file mode 100644
index 000000000..c404f5b3d
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/all_tx_sig/sol_all_tx_signature_content.tsx
@@ -0,0 +1,104 @@
+import type { FC } from "react";
+import type { SolanaAllTxSignPayload } from "@oko-wallet/oko-sdk-core";
+import { Spacing } from "@oko-wallet/oko-common-ui/spacing";
+import { Typography } from "@oko-wallet/oko-common-ui/typography";
+
+import styles from "../common/signature_content.module.scss";
+import { Avatar } from "@oko-wallet-attached/components/avatar/avatar";
+import { SignerAddressOrEmail } from "@oko-wallet-attached/components/modal_variants/common/metadata_content/signer_address_or_email/signer_address_or_email";
+import { SOLANA_LOGO_URL } from "@oko-wallet-attached/constants/urls";
+
+interface SolanaAllTxSignatureContentProps {
+ payload: SolanaAllTxSignPayload;
+}
+
+function getFaviconUrl(origin: string): string {
+ if (!origin) return "";
+ try {
+ const parsed = new URL(origin);
+ return `https://www.google.com/s2/favicons?domain_url=${encodeURIComponent(
+ parsed.origin,
+ )}`;
+ } catch {
+ return "";
+ }
+}
+
+export const SolanaAllTxSignatureContent: FC<
+ SolanaAllTxSignatureContentProps
+> = ({ payload }) => {
+ const { origin, signer, data } = payload;
+ const faviconUrl = getFaviconUrl(origin);
+ const txCount = data.serialized_transactions.length;
+
+ return (
+
+
+

+
+
Solana
+
+
+
+
+
+ Sign {txCount} Solana Transactions
+
+
+
+
+
+
+ {faviconUrl && faviconUrl.length > 0 && (
+

+ )}
+
+ {origin.replace(/^https?:\/\//, "")}
+
+
+
+
+
+
+
+
+ requested your
+
+
+
+
+ Solana signatures
+
+
+
+
+
+
+
+
+
+
+
+
+ This request will sign {txCount} transactions at once. Please review
+ carefully before approving.
+
+
+
+ );
+};
diff --git a/embed/oko_attached/src/components/modal_variants/sol/all_tx_sig/use_all_tx_sig_modal.tsx b/embed/oko_attached/src/components/modal_variants/sol/all_tx_sig/use_all_tx_sig_modal.tsx
new file mode 100644
index 000000000..daa98918d
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/all_tx_sig/use_all_tx_sig_modal.tsx
@@ -0,0 +1,73 @@
+import { useState } from "react";
+import type { MakeSolAllTxSignData } from "@oko-wallet/oko-sdk-core";
+import { base64ToUint8Array } from "@oko-wallet-attached/utils/base64";
+import {
+ useSolSignatureBase,
+ signMessageToHex,
+} from "../use_sol_signature_base";
+
+export interface UseAllTxSigModalArgs {
+ modalId: string;
+ data: MakeSolAllTxSignData;
+ getIsAborted: () => boolean;
+}
+
+export function useAllTxSigModal(args: UseAllTxSigModalArgs) {
+ const { modalId, data, getIsAborted } = args;
+ const hostOrigin = data.payload.origin;
+
+ const base = useSolSignatureBase({ modalId, hostOrigin, getIsAborted });
+ const [signingProgress, setSigningProgress] = useState(0);
+
+ const txCount = data.payload.data.serialized_transactions.length;
+
+ async function onApprove() {
+ if (getIsAborted()) return;
+
+ const ctx = base.prepareSigningContext();
+ if (!ctx) return;
+
+ base.setIsLoading(true);
+ setSigningProgress(0);
+
+ try {
+ const signatures: string[] = [];
+ const messagesToSign = data.payload.data.messages_to_sign;
+
+ for (let i = 0; i < messagesToSign.length; i++) {
+ if (getIsAborted()) return;
+
+ const message = base64ToUint8Array(messagesToSign[i]);
+ const result = await signMessageToHex(message, ctx);
+
+ if (!result.success) {
+ base.emitError(result.error);
+ return;
+ }
+
+ signatures.push(result.signature);
+ setSigningProgress(i + 1);
+ }
+
+ base.closeWithSignatures(signatures);
+ } catch (error: unknown) {
+ const errorMessage =
+ error instanceof Error ? error.message : String(error);
+ base.emitUnknownError(errorMessage);
+ } finally {
+ base.setIsLoading(false);
+ }
+ }
+
+ return {
+ onReject: base.onReject,
+ onApprove,
+ isLoading: base.isLoading,
+ isApproveEnabled: base.isApproveEnabled,
+ isDemo: base.isDemo,
+ theme: base.theme,
+ data,
+ txCount,
+ signingProgress,
+ };
+}
diff --git a/embed/oko_attached/src/components/modal_variants/sol/common/signature_content.module.scss b/embed/oko_attached/src/components/modal_variants/sol/common/signature_content.module.scss
new file mode 100644
index 000000000..b32755231
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/common/signature_content.module.scss
@@ -0,0 +1,62 @@
+.signatureContent {
+ display: flex;
+ flex-direction: column;
+ padding: 0 16px;
+}
+
+.chainInfo {
+ display: flex;
+ align-items: center;
+}
+
+.chainLogo {
+ width: 32px;
+ height: 32px;
+ border-radius: 16px;
+}
+
+.chainName {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--fg-secondary);
+}
+
+.signTypeTitle {
+ font-size: 20px;
+ font-weight: 600;
+ color: var(--fg-primary);
+}
+
+.metadataWrapper {
+ display: flex;
+ flex-direction: column;
+}
+
+.originRow {
+ display: flex;
+ align-items: center;
+}
+
+.originFavicon {
+ width: 16px;
+ height: 16px;
+ margin-right: 8px;
+}
+
+.signInfoColumn {
+ display: flex;
+ flex-direction: column;
+}
+
+.chainInfoRow {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ flex-wrap: wrap;
+}
+
+.chainNameGroup {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
diff --git a/embed/oko_attached/src/components/modal_variants/sol/common/summary.module.scss b/embed/oko_attached/src/components/modal_variants/sol/common/summary.module.scss
new file mode 100644
index 000000000..dc13c2a7a
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/common/summary.module.scss
@@ -0,0 +1,34 @@
+.summaryContainer {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ align-self: stretch;
+ gap: 8px;
+}
+
+.summaryHeader {
+ display: flex;
+ align-items: center;
+ align-self: stretch;
+ justify-content: space-between;
+}
+
+.summaryHeaderRight {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 18px;
+ cursor: pointer;
+}
+
+.summaryHeaderRightIcon {
+ width: 16px;
+ height: 16px;
+ aspect-ratio: 1/1;
+}
+
+.divider {
+ width: 100%;
+ height: 1px;
+ background: var(--border-primary);
+}
diff --git a/embed/oko_attached/src/components/modal_variants/sol/make_signature_sol_modal.tsx b/embed/oko_attached/src/components/modal_variants/sol/make_signature_sol_modal.tsx
new file mode 100644
index 000000000..1cffa3d9f
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/make_signature_sol_modal.tsx
@@ -0,0 +1,48 @@
+import type { FC } from "react";
+import type { MakeSolanaSigData } from "@oko-wallet/oko-sdk-core";
+
+import { MakeTxSigModal } from "./tx_sig/make_tx_sig_modal";
+import { MakeAllTxSigModal } from "./all_tx_sig/make_all_tx_sig_modal";
+import { MakeMessageSigModal } from "./message_sig/make_message_sig_modal";
+
+export interface MakeSignatureSolModalProps {
+ getIsAborted: () => boolean;
+ modalId: string;
+ data: MakeSolanaSigData;
+}
+
+export const MakeSignatureSolModal: FC = ({
+ getIsAborted,
+ data,
+ modalId,
+}) => {
+ switch (data.sign_type) {
+ case "tx": {
+ return (
+
+ );
+ }
+ case "all_tx": {
+ return (
+
+ );
+ }
+ case "message": {
+ return (
+
+ );
+ }
+ }
+};
diff --git a/embed/oko_attached/src/components/modal_variants/sol/message_sig/make_message_sig_modal.tsx b/embed/oko_attached/src/components/modal_variants/sol/message_sig/make_message_sig_modal.tsx
new file mode 100644
index 000000000..a3b1eab16
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/message_sig/make_message_sig_modal.tsx
@@ -0,0 +1,74 @@
+import type { FC } from "react";
+import type { MakeSolMessageSignData } from "@oko-wallet/oko-sdk-core";
+import { XCloseIcon } from "@oko-wallet/oko-common-ui/icons/x_close";
+import { Spacing } from "@oko-wallet/oko-common-ui/spacing";
+import { Button } from "@oko-wallet/oko-common-ui/button";
+
+import styles from "@oko-wallet-attached/components/modal_variants/common/make_signature/make_signature_modal.module.scss";
+import { CommonModal } from "@oko-wallet-attached/components/modal_variants/common/common_modal";
+import { DemoView } from "@oko-wallet-attached/components/modal_variants/common/make_signature/demo_view";
+import { SignWithOkoBox } from "@oko-wallet-attached/components/sign_with_oko_box/sign_with_oko_box";
+import { useMessageSigModal } from "./use_message_sig_modal";
+import { SolanaMessageSignatureContent } from "./sol_message_signature_content";
+
+export interface MakeMessageSigModalProps {
+ getIsAborted: () => boolean;
+ modalId: string;
+ data: MakeSolMessageSignData;
+}
+
+export const MakeMessageSigModal: FC = ({
+ getIsAborted,
+ data,
+ modalId,
+}) => {
+ const { onReject, onApprove, isLoading, isApproveEnabled, isDemo, theme } =
+ useMessageSigModal({
+ getIsAborted,
+ data,
+ modalId,
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {isDemo &&
}
+
+ );
+};
diff --git a/embed/oko_attached/src/components/modal_variants/sol/message_sig/sol_message_signature_content.tsx b/embed/oko_attached/src/components/modal_variants/sol/message_sig/sol_message_signature_content.tsx
new file mode 100644
index 000000000..92599fa2e
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/message_sig/sol_message_signature_content.tsx
@@ -0,0 +1,91 @@
+import type { FC } from "react";
+import type { SolanaMessageSignPayload } from "@oko-wallet/oko-sdk-core";
+import { Spacing } from "@oko-wallet/oko-common-ui/spacing";
+import { Typography } from "@oko-wallet/oko-common-ui/typography";
+
+import styles from "../common/signature_content.module.scss";
+import { Avatar } from "@oko-wallet-attached/components/avatar/avatar";
+import { SignerAddressOrEmail } from "@oko-wallet-attached/components/modal_variants/common/metadata_content/signer_address_or_email/signer_address_or_email";
+import { SolanaMessageSummary } from "./sol_message_summary";
+import { SOLANA_LOGO_URL } from "@oko-wallet-attached/constants/urls";
+
+interface SolanaMessageSignatureContentProps {
+ payload: SolanaMessageSignPayload;
+}
+
+function getFaviconUrl(origin: string): string {
+ if (!origin) return "";
+ try {
+ const parsed = new URL(origin);
+ return `https://www.google.com/s2/favicons?domain_url=${encodeURIComponent(
+ parsed.origin,
+ )}`;
+ } catch {
+ return "";
+ }
+}
+
+export const SolanaMessageSignatureContent: FC<
+ SolanaMessageSignatureContentProps
+> = ({ payload }) => {
+ const { origin, signer } = payload;
+ const faviconUrl = getFaviconUrl(origin);
+
+ return (
+
+
+

+
+
Solana
+
+
+
+
+
Sign Message
+
+
+
+
+
+ {faviconUrl && faviconUrl.length > 0 && (
+

+ )}
+
+ {origin.replace(/^https?:\/\//, "")}
+
+
+
+
+
+
+
+
+ requested your
+
+
+
+
+ Solana signature
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/embed/oko_attached/src/components/modal_variants/sol/message_sig/sol_message_summary.tsx b/embed/oko_attached/src/components/modal_variants/sol/message_sig/sol_message_summary.tsx
new file mode 100644
index 000000000..d3fddf876
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/message_sig/sol_message_summary.tsx
@@ -0,0 +1,97 @@
+import { type FC, type ReactNode, useState, useMemo } from "react";
+import type { SolanaMessageSignPayload } from "@oko-wallet/oko-sdk-core";
+import { Typography } from "@oko-wallet/oko-common-ui/typography";
+import { ChevronRightIcon } from "@oko-wallet/oko-common-ui/icons/chevron_right";
+
+import styles from "../common/summary.module.scss";
+import { MakeSignatureRawCodeBlock } from "@oko-wallet-attached/components/modal_variants/common/make_signature/make_sig_modal_code_block";
+import { MakeSignatureRawCodeBlockContainer } from "@oko-wallet-attached/components/modal_variants/common/make_signature/make_sig_modal_code_block_container";
+import { TxContainer } from "@oko-wallet-attached/components/modal_variants/eth/tx_sig/actions/common/tx_container";
+import { TxRow } from "@oko-wallet-attached/components/modal_variants/common/tx_row";
+
+export interface SolanaMessageSummaryProps {
+ payload: SolanaMessageSignPayload;
+}
+
+export const SolanaMessageSummary: FC = ({
+ payload,
+}) => {
+ const [isRawView, setIsRawView] = useState(false);
+
+ const { rawData, smartViewContent } = useMemo(() => {
+ const msgData = payload.data;
+ let decoded = "";
+
+ try {
+ const hex = msgData.message.startsWith("0x")
+ ? msgData.message.slice(2)
+ : msgData.message;
+ const byteArray = new Uint8Array(hex.length / 2);
+ for (let i = 0; i < byteArray.length; i++) {
+ byteArray[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
+ }
+ decoded = new TextDecoder().decode(byteArray);
+ } catch {
+ decoded = msgData.message;
+ }
+
+ const content: ReactNode = (
+
+
+
+ {decoded.length > 200 ? decoded.slice(0, 200) + "..." : decoded}
+
+
+
+ );
+
+ return {
+ rawData: JSON.stringify({ message: msgData.message }, null, 2),
+ smartViewContent: content,
+ };
+ }, [payload.data]);
+
+ function handleToggleView() {
+ setIsRawView((prev) => !prev);
+ }
+
+ let content: ReactNode | null = null;
+
+ if (isRawView) {
+ content = (
+
+
+
+ );
+ } else {
+ content = smartViewContent;
+ }
+
+ return (
+
+
+
+ Message
+
+
+
+ {isRawView ? "Smart View" : "Raw View"}
+
+
+
+
+ {content}
+
+ );
+};
diff --git a/embed/oko_attached/src/components/modal_variants/sol/message_sig/use_message_sig_modal.tsx b/embed/oko_attached/src/components/modal_variants/sol/message_sig/use_message_sig_modal.tsx
new file mode 100644
index 000000000..2f90161fd
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/message_sig/use_message_sig_modal.tsx
@@ -0,0 +1,56 @@
+import type { MakeSolMessageSignData } from "@oko-wallet/oko-sdk-core";
+import { hexToUint8Array } from "@oko-wallet-attached/crypto/keygen_ed25519";
+import {
+ useSolSignatureBase,
+ signMessageToHex,
+} from "../use_sol_signature_base";
+
+export interface UseMessageSigModalArgs {
+ modalId: string;
+ data: MakeSolMessageSignData;
+ getIsAborted: () => boolean;
+}
+
+export function useMessageSigModal(args: UseMessageSigModalArgs) {
+ const { modalId, data, getIsAborted } = args;
+ const hostOrigin = data.payload.origin;
+
+ const base = useSolSignatureBase({ modalId, hostOrigin, getIsAborted });
+
+ async function onApprove() {
+ if (getIsAborted()) return;
+
+ const ctx = base.prepareSigningContext();
+ if (!ctx) return;
+
+ base.setIsLoading(true);
+
+ try {
+ const message = hexToUint8Array(data.payload.data.message);
+ const result = await signMessageToHex(message, ctx);
+
+ if (!result.success) {
+ base.emitError(result.error);
+ return;
+ }
+
+ base.closeWithSignature(result.signature);
+ } catch (error: unknown) {
+ const errorMessage =
+ error instanceof Error ? error.message : String(error);
+ base.emitUnknownError(errorMessage);
+ } finally {
+ base.setIsLoading(false);
+ }
+ }
+
+ return {
+ onReject: base.onReject,
+ onApprove,
+ isLoading: base.isLoading,
+ isApproveEnabled: base.isApproveEnabled,
+ isDemo: base.isDemo,
+ theme: base.theme,
+ data,
+ };
+}
diff --git a/embed/oko_attached/src/components/modal_variants/sol/tx_sig/make_tx_sig_modal.tsx b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/make_tx_sig_modal.tsx
new file mode 100644
index 000000000..e9e5ff260
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/make_tx_sig_modal.tsx
@@ -0,0 +1,117 @@
+import type { FC } from "react";
+import type { MakeSolTxSignData } from "@oko-wallet/oko-sdk-core";
+import { XCloseIcon } from "@oko-wallet/oko-common-ui/icons/x_close";
+import { Spacing } from "@oko-wallet/oko-common-ui/spacing";
+import { Button } from "@oko-wallet/oko-common-ui/button";
+
+import styles from "@oko-wallet-attached/components/modal_variants/common/make_signature/make_signature_modal.module.scss";
+import { CommonModal } from "@oko-wallet-attached/components/modal_variants/common/common_modal";
+import { DemoView } from "@oko-wallet-attached/components/modal_variants/common/make_signature/demo_view";
+import { SignWithOkoBox } from "@oko-wallet-attached/components/sign_with_oko_box/sign_with_oko_box";
+import { trackTxButtonEvent } from "@oko-wallet-attached/analytics/events";
+import { useTxSigModal } from "./use_tx_sig_modal";
+import { SolanaTxSignatureContent } from "./sol_tx_signature_content";
+import { SolanaTxFee } from "./sol_tx_fee";
+import { useParseTx } from "./use_parse_tx";
+
+export interface MakeTxSigModalProps {
+ getIsAborted: () => boolean;
+ modalId: string;
+ data: MakeSolTxSignData;
+}
+
+export const MakeTxSigModal: FC = ({
+ getIsAborted,
+ data,
+ modalId,
+}) => {
+ const { onReject, onApprove, isLoading, isApproveEnabled, isDemo, theme } =
+ useTxSigModal({
+ getIsAborted,
+ data,
+ modalId,
+ });
+
+ const {
+ parsedTx,
+ parseError,
+ isLoading: isParsing,
+ } = useParseTx(data.payload.data.serialized_transaction);
+
+ function handleRejectClick() {
+ trackTxButtonEvent({
+ eventType: "reject",
+ hostOrigin: data.payload.origin,
+ chainType: "solana",
+ instructions: parsedTx?.instructions ?? null,
+ });
+
+ onReject();
+ }
+
+ function handleApproveClick() {
+ trackTxButtonEvent({
+ eventType: "approve",
+ hostOrigin: data.payload.origin,
+ chainType: "solana",
+ instructions: parsedTx?.instructions ?? null,
+ });
+
+ onApprove();
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {isDemo &&
}
+
+ );
+};
diff --git a/embed/oko_attached/src/components/modal_variants/sol/tx_sig/msg/instructions.module.scss b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/msg/instructions.module.scss
new file mode 100644
index 000000000..4d9c93982
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/msg/instructions.module.scss
@@ -0,0 +1,40 @@
+.container {
+ display: flex;
+ padding: 16px;
+ flex-direction: column;
+ align-items: flex-start;
+ overflow-y: auto;
+ border-radius: 12px;
+ background: var(--bg-secondary);
+ width: 100%;
+ gap: 16px;
+}
+
+.tokenInfo {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: 100%;
+}
+
+.tokenAmount {
+ text-overflow: ellipsis;
+ word-break: break-all;
+}
+
+.address {
+ word-break: break-all;
+}
+
+.instructionsContainer {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ width: 100%;
+}
+
+.instructionDivider {
+ width: 100%;
+ height: 1px;
+ background: var(--border-primary);
+}
diff --git a/embed/oko_attached/src/components/modal_variants/sol/tx_sig/msg/instructions.tsx b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/msg/instructions.tsx
new file mode 100644
index 000000000..a73ddde40
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/msg/instructions.tsx
@@ -0,0 +1,111 @@
+import type { FC, ReactNode } from "react";
+import type { ParsedInstruction } from "@oko-wallet-attached/tx-parsers/sol";
+import { Skeleton } from "@oko-wallet/oko-common-ui/skeleton";
+
+import styles from "./instructions.module.scss";
+import { SolTransferPretty } from "./transfer/transfer";
+import { TokenTransferPretty } from "./transfer/token_transfer";
+import { UnknownInstruction } from "./unknown/unknown";
+
+function renderInstruction(
+ instruction: ParsedInstruction,
+ index: number,
+): ReactNode {
+ const { programId, instructionName, data, accounts } = instruction;
+
+ // System Program - SOL Transfer
+ if (programId === "11111111111111111111111111111111") {
+ if (instructionName === "transfer") {
+ const lamports = data.lamports as bigint | number | undefined;
+ const from = accounts[0]?.pubkey;
+ const to = accounts[1]?.pubkey;
+
+ if (lamports !== undefined) {
+ return (
+
+ );
+ }
+ }
+ }
+
+ // Token Program - Token Transfer
+ if (
+ programId === "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" ||
+ programId === "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
+ ) {
+ // transferChecked: source, mint, destination, owner
+ if (instructionName === "transferChecked") {
+ const amount = data.amount as bigint | number | undefined;
+ const decimals = data.decimals as number | undefined;
+ const from = accounts[0]?.pubkey;
+ const mint = accounts[1]?.pubkey;
+ const to = accounts[2]?.pubkey;
+
+ if (amount !== undefined) {
+ return (
+
+ );
+ }
+ }
+
+ // transfer: source, destination, owner (no mint in accounts)
+ if (instructionName === "transfer") {
+ const amount = data.amount as bigint | number | undefined;
+ const from = accounts[0]?.pubkey;
+ const to = accounts[1]?.pubkey;
+
+ if (amount !== undefined) {
+ return (
+
+ );
+ }
+ }
+
+ return ;
+ }
+
+ // Default: Unknown instruction
+ return ;
+}
+
+export interface InstructionsProps {
+ instructions: ParsedInstruction[];
+ isLoading?: boolean;
+}
+
+export const Instructions: FC = ({
+ instructions,
+ isLoading,
+}) => {
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+ {instructions.flatMap((ix, index) => [
+ index > 0 && (
+
+ ),
+ renderInstruction(ix, index),
+ ])}
+
+ );
+};
diff --git a/embed/oko_attached/src/components/modal_variants/sol/tx_sig/msg/transfer/token_transfer.tsx b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/msg/transfer/token_transfer.tsx
new file mode 100644
index 000000000..5e73c5f98
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/msg/transfer/token_transfer.tsx
@@ -0,0 +1,128 @@
+import type { FC } from "react";
+import { Typography } from "@oko-wallet/oko-common-ui/typography";
+import { Skeleton } from "@oko-wallet/oko-common-ui/skeleton";
+
+import { Avatar } from "@oko-wallet-attached/components/avatar/avatar";
+import { TxRow } from "@oko-wallet-attached/components/modal_variants/common/tx_row";
+import { useGetSolanaTokenMetadata } from "@oko-wallet-attached/web3/solana/queries";
+import styles from "../instructions.module.scss";
+
+function shortenAddress(address: string): string {
+ if (address.length <= 12) return address;
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
+}
+
+function formatTokenAmount(amount: bigint | number, decimals: number): string {
+ if (decimals === 0) {
+ return amount.toLocaleString();
+ }
+
+ // Use scientific notation to avoid floating-point precision issues
+ const formatter = new Intl.NumberFormat(undefined, {
+ minimumFractionDigits: 0,
+ maximumFractionDigits: decimals,
+ });
+
+ // Convert to scientific notation string: "123456E-6" for amount=123456, decimals=6
+ return formatter.format(`${amount}E-${decimals}` as unknown as number);
+}
+
+export interface TokenTransferPrettyProps {
+ amount: bigint | number;
+ decimals?: number;
+ mint?: string;
+ from?: string;
+ to?: string;
+}
+
+export const TokenTransferPretty: FC = ({
+ amount,
+ decimals: providedDecimals,
+ mint,
+ from,
+ to,
+}) => {
+ const { data: tokenMetadata, isLoading } = useGetSolanaTokenMetadata({
+ mintAddress: mint,
+ });
+
+ const decimals = tokenMetadata?.decimals ?? providedDecimals ?? 0;
+ const symbol = tokenMetadata?.symbol;
+ const name = tokenMetadata?.name;
+ const icon = tokenMetadata?.icon;
+ const hasMetadata = !!symbol;
+
+ const formattedAmount = formatTokenAmount(amount, decimals);
+
+ return (
+
+
+
+ {isLoading ? (
+
+ ) : hasMetadata ? (
+
+ ) : null}
+
+ {isLoading ? (
+
+ ) : hasMetadata ? (
+ `${formattedAmount} ${symbol}`
+ ) : mint ? (
+ `${formattedAmount} (${shortenAddress(mint)})`
+ ) : (
+ formattedAmount
+ )}
+
+
+
+ {hasMetadata && name && (
+
+
+ {name}
+
+
+ )}
+ {from && (
+
+
+ {shortenAddress(from)}
+
+
+ )}
+ {to && (
+
+
+ {shortenAddress(to)}
+
+
+ )}
+
+ );
+};
diff --git a/embed/oko_attached/src/components/modal_variants/sol/tx_sig/msg/transfer/transfer.tsx b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/msg/transfer/transfer.tsx
new file mode 100644
index 000000000..6e933914f
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/msg/transfer/transfer.tsx
@@ -0,0 +1,81 @@
+import type { FC } from "react";
+import { Typography } from "@oko-wallet/oko-common-ui/typography";
+
+import { Avatar } from "@oko-wallet-attached/components/avatar/avatar";
+import styles from "../instructions.module.scss";
+import { TxRow } from "@oko-wallet-attached/components/modal_variants/common/tx_row";
+import { SOLANA_LOGO_URL } from "@oko-wallet-attached/constants/urls";
+
+function shortenAddress(address: string): string {
+ if (address.length <= 12) return address;
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
+}
+
+function formatLamports(lamports: bigint | number): string {
+ // Use scientific notation to avoid floating-point precision issues
+ // SOL has 9 decimals
+ const formatter = new Intl.NumberFormat(undefined, {
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 9,
+ });
+ return `${formatter.format(`${lamports}E-9` as unknown as number)} SOL`;
+}
+
+export interface SolTransferPrettyProps {
+ lamports: bigint | number;
+ from?: string;
+ to?: string;
+}
+
+export const SolTransferPretty: FC = ({
+ lamports,
+ from,
+ to,
+}) => {
+ return (
+
+
+
+
+
+ {formatLamports(lamports)}
+
+
+
+ {from && (
+
+
+ {shortenAddress(from)}
+
+
+ )}
+ {to && (
+
+
+ {shortenAddress(to)}
+
+
+ )}
+
+ );
+};
diff --git a/embed/oko_attached/src/components/modal_variants/sol/tx_sig/msg/unknown/unknown.tsx b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/msg/unknown/unknown.tsx
new file mode 100644
index 000000000..3466ed581
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/msg/unknown/unknown.tsx
@@ -0,0 +1,43 @@
+import type { FC } from "react";
+import type { ParsedInstruction } from "@oko-wallet-attached/tx-parsers/sol";
+import { Typography } from "@oko-wallet/oko-common-ui/typography";
+
+import styles from "../instructions.module.scss";
+import { TxRow } from "@oko-wallet-attached/components/modal_variants/common/tx_row";
+
+export interface UnknownInstructionProps {
+ instruction: ParsedInstruction;
+}
+
+export const UnknownInstruction: FC = ({
+ instruction,
+}) => {
+ const { programName, instructionName, data } = instruction;
+
+ return (
+
+
+
+ {instructionName}
+
+
+ {Object.keys(data).length > 0 && data.raw === undefined && (
+
+
+ {JSON.stringify(data, null, 2)}
+
+
+ )}
+
+
+ {programName}
+
+
+
+ );
+};
diff --git a/embed/oko_attached/src/components/modal_variants/sol/tx_sig/sol_tx_fee.module.scss b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/sol_tx_fee.module.scss
new file mode 100644
index 000000000..4e41e3e80
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/sol_tx_fee.module.scss
@@ -0,0 +1,6 @@
+.feeContainer {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ justify-content: center;
+}
diff --git a/embed/oko_attached/src/components/modal_variants/sol/tx_sig/sol_tx_fee.tsx b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/sol_tx_fee.tsx
new file mode 100644
index 000000000..e99bf8dac
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/sol_tx_fee.tsx
@@ -0,0 +1,41 @@
+import type { FC } from "react";
+import { Typography } from "@oko-wallet/oko-common-ui/typography";
+
+import styles from "./sol_tx_fee.module.scss";
+import { useCalculateFee } from "@oko-wallet-attached/web3/solana/use_calculate_fee";
+
+const LAMPORTS_PER_SOL = 1_000_000_000;
+
+function formatFee(lamports: number): string {
+ const sol = lamports / LAMPORTS_PER_SOL;
+ if (sol < 0.000001) {
+ return `${lamports} lamports`;
+ }
+ return `${sol.toFixed(6)} SOL`;
+}
+
+export interface SolanaTxFeeProps {
+ serializedTransaction: string;
+ isVersioned: boolean;
+}
+
+export const SolanaTxFee: FC = ({
+ serializedTransaction,
+ isVersioned,
+}) => {
+ const { fee } = useCalculateFee({
+ serializedTransaction,
+ isVersioned,
+ });
+
+ return (
+
+
+ Fee
+
+
+ {fee !== null ? formatFee(fee) : "-"}
+
+
+ );
+};
diff --git a/embed/oko_attached/src/components/modal_variants/sol/tx_sig/sol_tx_signature_content.tsx b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/sol_tx_signature_content.tsx
new file mode 100644
index 000000000..053b36ae3
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/sol_tx_signature_content.tsx
@@ -0,0 +1,103 @@
+import type { FC } from "react";
+import type { SolanaTxSignPayload } from "@oko-wallet/oko-sdk-core";
+import { Spacing } from "@oko-wallet/oko-common-ui/spacing";
+import { Typography } from "@oko-wallet/oko-common-ui/typography";
+import type { ParsedTransaction } from "@oko-wallet-attached/tx-parsers/sol";
+
+import styles from "../common/signature_content.module.scss";
+import { Avatar } from "@oko-wallet-attached/components/avatar/avatar";
+import { SignerAddressOrEmail } from "@oko-wallet-attached/components/modal_variants/common/metadata_content/signer_address_or_email/signer_address_or_email";
+import { SolanaTxSummary } from "./sol_tx_summary";
+import { SOLANA_LOGO_URL } from "@oko-wallet-attached/constants/urls";
+
+interface SolanaTxSignatureContentProps {
+ payload: SolanaTxSignPayload;
+ parsedTx: ParsedTransaction | null;
+ parseError: string | null;
+ isLoading: boolean;
+}
+
+function getFaviconUrl(origin: string): string {
+ if (!origin) return "";
+ try {
+ const parsed = new URL(origin);
+ return `https://www.google.com/s2/favicons?domain_url=${encodeURIComponent(
+ parsed.origin,
+ )}`;
+ } catch {
+ return "";
+ }
+}
+
+export const SolanaTxSignatureContent: FC = ({
+ payload,
+ parsedTx,
+ parseError,
+ isLoading,
+}) => {
+ const { origin, signer } = payload;
+ const faviconUrl = getFaviconUrl(origin);
+
+ return (
+
+
+

+
+
Solana
+
+
+
+
+
Sign Solana Transaction
+
+
+
+
+
+ {faviconUrl && faviconUrl.length > 0 && (
+

+ )}
+
+ {origin.replace(/^https?:\/\//, "")}
+
+
+
+
+
+
+
+
+ requested your
+
+
+
+
+ Solana signature
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/embed/oko_attached/src/components/modal_variants/sol/tx_sig/sol_tx_summary.tsx b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/sol_tx_summary.tsx
new file mode 100644
index 000000000..04dfc7539
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/sol_tx_summary.tsx
@@ -0,0 +1,107 @@
+import { type FC, type ReactNode, useState, useMemo } from "react";
+import type { SolanaTxSignPayload } from "@oko-wallet/oko-sdk-core";
+import { Typography } from "@oko-wallet/oko-common-ui/typography";
+import { ChevronRightIcon } from "@oko-wallet/oko-common-ui/icons/chevron_right";
+import type { ParsedTransaction } from "@oko-wallet-attached/tx-parsers/sol";
+
+import styles from "../common/summary.module.scss";
+import { MakeSignatureRawCodeBlock } from "@oko-wallet-attached/components/modal_variants/common/make_signature/make_sig_modal_code_block";
+import { MakeSignatureRawCodeBlockContainer } from "@oko-wallet-attached/components/modal_variants/common/make_signature/make_sig_modal_code_block_container";
+import { TxContainer } from "@oko-wallet-attached/components/modal_variants/eth/tx_sig/actions/common/tx_container";
+import { TxRow } from "@oko-wallet-attached/components/modal_variants/common/tx_row";
+import { Instructions } from "./msg/instructions";
+
+export interface SolanaTxSummaryProps {
+ payload: SolanaTxSignPayload;
+ parsedTx: ParsedTransaction | null;
+ parseError: string | null;
+ isLoading: boolean;
+}
+
+export const SolanaTxSummary: FC = ({
+ payload,
+ parsedTx,
+ parseError,
+ isLoading,
+}) => {
+ const [isRawView, setIsRawView] = useState(false);
+
+ const txData = payload.data;
+
+ const { rawData, smartViewContent } = useMemo(() => {
+ let content: ReactNode;
+
+ if (parseError) {
+ content = (
+
+
+
+ {parseError}
+
+
+
+ );
+ } else if (isLoading || !parsedTx) {
+ content = (
+
+
+
+ Parsing...
+
+
+
+ );
+ } else {
+ content = ;
+ }
+
+ return {
+ rawData: JSON.stringify(
+ {
+ serialized_transaction: txData.serialized_transaction,
+ message_to_sign: txData.message_to_sign,
+ is_versioned: txData.is_versioned,
+ },
+ null,
+ 2,
+ ),
+ smartViewContent: content,
+ };
+ }, [txData, parsedTx, parseError, isLoading]);
+
+ function handleToggleView() {
+ setIsRawView((prev) => !prev);
+ }
+
+ let content: ReactNode | null = null;
+
+ if (isRawView) {
+ content = (
+
+
+
+ );
+ } else {
+ content = smartViewContent;
+ }
+
+ return (
+
+
+
+ Transaction
+
+
+
+ {isRawView ? "Smart View" : "Raw View"}
+
+
+
+
+ {content}
+
+ );
+};
diff --git a/embed/oko_attached/src/components/modal_variants/sol/tx_sig/use_parse_tx.ts b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/use_parse_tx.ts
new file mode 100644
index 000000000..ea57a7093
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/use_parse_tx.ts
@@ -0,0 +1,55 @@
+import { useState, useEffect } from "react";
+import {
+ deserializeTransaction,
+ parseTransaction,
+ type ParsedTransaction,
+} from "@oko-wallet-attached/tx-parsers/sol";
+
+import { base64ToUint8Array } from "@oko-wallet-attached/utils/base64";
+
+export interface UseParseTxResult {
+ parsedTx: ParsedTransaction | null;
+ parseError: string | null;
+ isLoading: boolean;
+}
+
+export function useParseTx(serializedTransaction: string): UseParseTxResult {
+ const [parsedTx, setParsedTx] = useState(null);
+ const [parseError, setParseError] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ async function parseTx() {
+ setIsLoading(true);
+ try {
+ const txBytes = base64ToUint8Array(serializedTransaction);
+ const deserializeResult = deserializeTransaction(txBytes);
+
+ if (!deserializeResult.success) {
+ setParseError(deserializeResult.error);
+ setIsLoading(false);
+ return;
+ }
+
+ const parseResult = await parseTransaction(deserializeResult.data);
+
+ if (!parseResult.success) {
+ setParseError(parseResult.error);
+ setIsLoading(false);
+ return;
+ }
+
+ setParsedTx(parseResult.data);
+ setParseError(null);
+ } catch (err) {
+ setParseError(String(err));
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ parseTx();
+ }, [serializedTransaction]);
+
+ return { parsedTx, parseError, isLoading };
+}
diff --git a/embed/oko_attached/src/components/modal_variants/sol/tx_sig/use_tx_sig_modal.tsx b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/use_tx_sig_modal.tsx
new file mode 100644
index 000000000..2203f7c79
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/tx_sig/use_tx_sig_modal.tsx
@@ -0,0 +1,56 @@
+import type { MakeSolTxSignData } from "@oko-wallet/oko-sdk-core";
+import { base64ToUint8Array } from "@oko-wallet-attached/utils/base64";
+import {
+ useSolSignatureBase,
+ signMessageToHex,
+} from "../use_sol_signature_base";
+
+export interface UseTxSigModalArgs {
+ modalId: string;
+ data: MakeSolTxSignData;
+ getIsAborted: () => boolean;
+}
+
+export function useTxSigModal(args: UseTxSigModalArgs) {
+ const { modalId, data, getIsAborted } = args;
+ const hostOrigin = data.payload.origin;
+
+ const base = useSolSignatureBase({ modalId, hostOrigin, getIsAborted });
+
+ async function onApprove() {
+ if (getIsAborted()) return;
+
+ const ctx = base.prepareSigningContext();
+ if (!ctx) return;
+
+ base.setIsLoading(true);
+
+ try {
+ const message = base64ToUint8Array(data.payload.data.message_to_sign);
+ const result = await signMessageToHex(message, ctx);
+
+ if (!result.success) {
+ base.emitError(result.error);
+ return;
+ }
+
+ base.closeWithSignature(result.signature);
+ } catch (error: unknown) {
+ const errorMessage =
+ error instanceof Error ? error.message : String(error);
+ base.emitUnknownError(errorMessage);
+ } finally {
+ base.setIsLoading(false);
+ }
+ }
+
+ return {
+ onReject: base.onReject,
+ onApprove,
+ isLoading: base.isLoading,
+ isApproveEnabled: base.isApproveEnabled,
+ isDemo: base.isDemo,
+ theme: base.theme,
+ data,
+ };
+}
diff --git a/embed/oko_attached/src/components/modal_variants/sol/use_sol_signature_base.tsx b/embed/oko_attached/src/components/modal_variants/sol/use_sol_signature_base.tsx
new file mode 100644
index 000000000..5d2e9671a
--- /dev/null
+++ b/embed/oko_attached/src/components/modal_variants/sol/use_sol_signature_base.tsx
@@ -0,0 +1,168 @@
+import { useState } from "react";
+import type {
+ MakeSolSigError,
+ OpenModalAckPayload,
+} from "@oko-wallet/oko-sdk-core";
+
+import { useAppState } from "@oko-wallet-attached/store/app";
+import { useMemoryState } from "@oko-wallet-attached/store/memory";
+import { DEMO_WEB_ORIGIN } from "@oko-wallet-attached/requests/endpoints";
+import {
+ makeSignOutputEd25519,
+ type KeyPackageEd25519,
+} from "@oko-wallet-attached/crypto/sign_ed25519";
+import { teddsaKeygenFromHex } from "@oko-wallet-attached/crypto/keygen_ed25519";
+
+export interface UseSolSignatureBaseArgs {
+ modalId: string;
+ hostOrigin: string;
+ getIsAborted: () => boolean;
+}
+
+export interface SigningContext {
+ keyPackage: KeyPackageEd25519;
+ apiKey: string;
+ authToken: string;
+ getIsAborted: () => boolean;
+}
+
+export type SigningResult =
+ | { success: true; signature: string }
+ | { success: true; signatures: string[] }
+ | { success: false };
+
+/**
+ * Sign a single message and return hex-encoded signature
+ */
+export async function signMessageToHex(
+ message: Uint8Array,
+ ctx: SigningContext,
+): Promise<{ success: true; signature: string } | { success: false; error: MakeSolSigError }> {
+ const signatureRes = await makeSignOutputEd25519(
+ message,
+ ctx.keyPackage,
+ ctx.apiKey,
+ ctx.authToken,
+ ctx.getIsAborted,
+ );
+
+ if (!signatureRes.success) {
+ return { success: false, error: signatureRes.err };
+ }
+
+ const signatureHex = Array.from(signatureRes.data)
+ .map((b) => b.toString(16).padStart(2, "0"))
+ .join("");
+
+ return { success: true, signature: signatureHex };
+}
+
+export function useSolSignatureBase(args: UseSolSignatureBaseArgs) {
+ const { modalId, hostOrigin, getIsAborted } = args;
+ const { closeModal, setError } = useMemoryState();
+
+ const theme = useAppState().getTheme(hostOrigin);
+ const apiKey = useAppState().getApiKey(hostOrigin);
+ const authToken = useAppState().getAuthToken(hostOrigin);
+ const keyPackageHex = useAppState().getKeyPackageEd25519(hostOrigin);
+
+ const [isLoading, setIsLoading] = useState(false);
+
+ const isDemo = !!hostOrigin && hostOrigin === DEMO_WEB_ORIGIN;
+ const isApproveEnabled = !!keyPackageHex && !!apiKey && !!authToken;
+
+ function onReject() {
+ const ack: OpenModalAckPayload = {
+ modal_type: "sol/make_signature",
+ modal_id: modalId,
+ type: "reject",
+ };
+ closeModal(ack);
+ }
+
+ function emitError(error: MakeSolSigError) {
+ setError({
+ modal_type: "sol/make_signature",
+ modal_id: modalId,
+ type: "error",
+ error,
+ });
+ }
+
+ function emitUnknownError(message: string) {
+ emitError({ type: "unknown_error", error: message });
+ }
+
+ function closeWithSignature(signature: string) {
+ const ack: OpenModalAckPayload = {
+ modal_type: "sol/make_signature",
+ modal_id: modalId,
+ type: "approve",
+ data: {
+ chain_type: "sol",
+ sig_result: { type: "signature", signature },
+ },
+ };
+ closeModal(ack);
+ }
+
+ function closeWithSignatures(signatures: string[]) {
+ const ack: OpenModalAckPayload = {
+ modal_type: "sol/make_signature",
+ modal_id: modalId,
+ type: "approve",
+ data: {
+ chain_type: "sol",
+ sig_result: { type: "signatures", signatures },
+ },
+ };
+ closeModal(ack);
+ }
+
+ /**
+ * Prepare signing context. Returns null if validation fails.
+ */
+ function prepareSigningContext(): SigningContext | null {
+ if (!keyPackageHex || !apiKey || !authToken) {
+ emitUnknownError("Missing key package, API key, or auth token");
+ return null;
+ }
+
+ const keyPackageRes = teddsaKeygenFromHex(keyPackageHex);
+ if (!keyPackageRes.success) {
+ emitUnknownError(keyPackageRes.err);
+ return null;
+ }
+
+ return {
+ keyPackage: {
+ keyPackage: keyPackageRes.data.key_package,
+ publicKeyPackage: keyPackageRes.data.public_key_package,
+ identifier: keyPackageRes.data.identifier,
+ },
+ apiKey,
+ authToken,
+ getIsAborted,
+ };
+ }
+
+ return {
+ // State
+ isLoading,
+ setIsLoading,
+ isApproveEnabled,
+ isDemo,
+ theme,
+
+ // Actions
+ onReject,
+ emitError,
+ emitUnknownError,
+ closeWithSignature,
+ closeWithSignatures,
+
+ // Signing
+ prepareSigningContext,
+ getIsAborted,
+ };
+}
diff --git a/embed/oko_attached/src/constants/urls.ts b/embed/oko_attached/src/constants/urls.ts
new file mode 100644
index 000000000..fe151f494
--- /dev/null
+++ b/embed/oko_attached/src/constants/urls.ts
@@ -0,0 +1,4 @@
+export const S3_BUCKET_URL =
+ "https://oko-wallet.s3.ap-northeast-2.amazonaws.com/icons";
+
+export const SOLANA_LOGO_URL = `${S3_BUCKET_URL}/solana.png`;
diff --git a/embed/oko_attached/src/crypto/hash.ts b/embed/oko_attached/src/crypto/hash.ts
index b45434c39..60ac3086c 100644
--- a/embed/oko_attached/src/crypto/hash.ts
+++ b/embed/oko_attached/src/crypto/hash.ts
@@ -2,6 +2,10 @@ import { Bytes, type Bytes32 } from "@oko-wallet/bytes";
import { sha256 } from "@oko-wallet/crypto-js";
import type { Result } from "@oko-wallet/stdlib-js";
+/**
+ * Hash keyshare node names to get identifiers for secp256k1 SSS.
+ * The hash is reduced to fit within the secp256k1 scalar field order.
+ */
export async function hashKeyshareNodeNames(
keyshareNodeNames: string[],
): Promise> {
@@ -35,3 +39,43 @@ export async function hashKeyshareNodeNames(
data: hashes,
};
}
+
+/**
+ * Hash keyshare node names to get identifiers for Ed25519 SSS.
+ * The hash is reduced to fit within the Ed25519 scalar field order (~2^252).
+ * Ed25519 uses little-endian byte order, so we reduce the most significant byte (byte 31).
+ */
+export async function hashKeyshareNodeNamesEd25519(
+ keyshareNodeNames: string[],
+): Promise> {
+ const hashes = [];
+ for (const name of keyshareNodeNames) {
+ const hashResult = sha256(name);
+ if (hashResult.success === false) {
+ return {
+ success: false,
+ err: hashResult.err,
+ };
+ }
+ const hash = hashResult.data;
+ const hashU8Arr = new Uint8Array(hash.toUint8Array());
+ // Ed25519 scalar order is ~2^252 (specifically: 2^252 + 27742317777372353535851937790883648493)
+ // In little-endian, byte 31 is the most significant byte.
+ // Set byte 31 to 0x0F or less to ensure value is < 2^252
+ // This gives us ~248 bits of entropy which is still cryptographically secure.
+ hashU8Arr[31] = hashU8Arr[31] & 0x0f;
+ const bytesRes = Bytes.fromUint8Array(hashU8Arr, 32);
+ if (bytesRes.success === false) {
+ return {
+ success: false,
+ err: bytesRes.err,
+ };
+ }
+ hashes.push(bytesRes.data);
+ }
+
+ return {
+ success: true,
+ data: hashes,
+ };
+}
diff --git a/embed/oko_attached/src/crypto/keygen_ed25519.ts b/embed/oko_attached/src/crypto/keygen_ed25519.ts
new file mode 100644
index 000000000..cc09eadbd
--- /dev/null
+++ b/embed/oko_attached/src/crypto/keygen_ed25519.ts
@@ -0,0 +1,68 @@
+import type { TeddsaKeygenOutputBytes } from "@oko-wallet/teddsa-hooks";
+import type { Result } from "@oko-wallet/stdlib-js";
+import { Bytes, type Bytes32 } from "@oko-wallet/bytes";
+
+export interface KeyPackageEd25519Hex {
+ keyPackage: string;
+ publicKeyPackage: string;
+ identifier: string;
+ publicKey: string;
+}
+
+export function hexToUint8Array(hex: string): Uint8Array {
+ const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
+ const bytes = new Uint8Array(cleanHex.length / 2);
+ for (let i = 0; i < bytes.length; i++) {
+ bytes[i] = parseInt(cleanHex.substr(i * 2, 2), 16);
+ }
+ return bytes;
+}
+
+export function uint8ArrayToHex(bytes: Uint8Array): string {
+ return Array.from(bytes)
+ .map((b) => b.toString(16).padStart(2, "0"))
+ .join("");
+}
+
+export function teddsaKeygenToHex(
+ keygen: TeddsaKeygenOutputBytes,
+): KeyPackageEd25519Hex {
+ return {
+ keyPackage: uint8ArrayToHex(keygen.key_package),
+ publicKeyPackage: uint8ArrayToHex(keygen.public_key_package),
+ identifier: uint8ArrayToHex(keygen.identifier),
+ publicKey: keygen.public_key.toHex(),
+ };
+}
+
+export function teddsaKeygenFromHex(
+ data: KeyPackageEd25519Hex,
+): Result {
+ try {
+ const publicKeyRes = Bytes.fromHexString(data.publicKey, 32);
+ if (!publicKeyRes.success) {
+ return { success: false, err: publicKeyRes.err };
+ }
+
+ return {
+ success: true,
+ data: {
+ key_package: hexToUint8Array(data.keyPackage),
+ public_key_package: hexToUint8Array(data.publicKeyPackage),
+ identifier: hexToUint8Array(data.identifier),
+ public_key: publicKeyRes.data,
+ },
+ };
+ } catch (error) {
+ return {
+ success: false,
+ err: error instanceof Error ? error.message : String(error),
+ };
+ }
+}
+
+export function getPublicKeyFromKeyPackage(
+ keyPackageHex: KeyPackageEd25519Hex,
+): Result {
+ return Bytes.fromHexString(keyPackageHex.publicKey, 32);
+}
diff --git a/embed/oko_attached/src/crypto/sign_ed25519.ts b/embed/oko_attached/src/crypto/sign_ed25519.ts
new file mode 100644
index 000000000..f0bf9c358
--- /dev/null
+++ b/embed/oko_attached/src/crypto/sign_ed25519.ts
@@ -0,0 +1,160 @@
+import {
+ teddsaSignRound1,
+ teddsaSignRound2,
+ teddsaAggregate,
+} from "@oko-wallet/teddsa-hooks";
+import type {
+ TeddsaCommitmentEntry,
+ TeddsaSignatureShareEntry,
+} from "@oko-wallet/teddsa-interface";
+import type { Result } from "@oko-wallet/stdlib-js";
+import type { MakeSignOutputError } from "@oko-wallet/oko-sdk-core";
+import {
+ reqPresignEd25519,
+ reqSignEd25519,
+} from "@oko-wallet/teddsa-api-lib";
+
+import { TSS_V1_ENDPOINT } from "@oko-wallet-attached/requests/oko_api";
+
+export interface KeyPackageEd25519 {
+ keyPackage: Uint8Array;
+ publicKeyPackage: Uint8Array;
+ identifier: Uint8Array;
+}
+
+/**
+ * Ed25519 signing using presign flow.
+ *
+ * Flow:
+ * 1. Server presign: Generate server nonces/commitments (requires apiKey)
+ * 2. Client round1: Generate client nonces/commitments
+ * 3. Server sign: Generate server signature share using presign (no apiKey needed)
+ * 4. Client round2: Generate client signature share
+ * 5. Aggregate: Combine signature shares into final signature
+ */
+export async function makeSignOutputEd25519(
+ message: Uint8Array,
+ keyPackage: KeyPackageEd25519,
+ apiKey: string,
+ authToken: string,
+ getIsAborted: () => boolean,
+): Promise> {
+ try {
+ if (getIsAborted()) {
+ return { success: false, err: { type: "aborted" } };
+ }
+
+ // 1. Server presign: Get server commitments (requires apiKey for session creation)
+ const presignRes = await reqPresignEd25519(TSS_V1_ENDPOINT, {}, apiKey, authToken);
+
+ if (!presignRes.success) {
+ return {
+ success: false,
+ err: { type: "sign_fail", error: { type: "error", msg: presignRes.msg } },
+ };
+ }
+
+ const { session_id: sessionId, commitments_0: serverCommitment } = presignRes.data;
+
+ if (getIsAborted()) {
+ return { success: false, err: { type: "aborted" } };
+ }
+
+ // 2. Client round1: Generate client nonces and commitments
+ const round1Result = teddsaSignRound1(keyPackage.keyPackage);
+ if (!round1Result.success) {
+ return {
+ success: false,
+ err: { type: "sign_fail", error: { type: "error", msg: round1Result.err } },
+ };
+ }
+
+ const clientCommitment: TeddsaCommitmentEntry = {
+ identifier: round1Result.data.identifier,
+ commitments: round1Result.data.commitments,
+ };
+
+ const allCommitments: TeddsaCommitmentEntry[] = [
+ clientCommitment,
+ serverCommitment,
+ ].sort((a, b) => (a.identifier[0] ?? 0) - (b.identifier[0] ?? 0));
+
+ if (getIsAborted()) {
+ return { success: false, err: { type: "aborted" } };
+ }
+
+ // 3. Server sign: Get server signature share using presign session (no apiKey needed)
+ const serverSignRes = await reqSignEd25519(
+ TSS_V1_ENDPOINT,
+ {
+ session_id: sessionId,
+ msg: [...message],
+ commitments_1: clientCommitment,
+ },
+ authToken,
+ );
+
+ if (!serverSignRes.success) {
+ return {
+ success: false,
+ err: { type: "sign_fail", error: { type: "error", msg: serverSignRes.msg } },
+ };
+ }
+
+ const serverSignatureShare: TeddsaSignatureShareEntry = serverSignRes.data.signature_share_0;
+
+ if (getIsAborted()) {
+ return { success: false, err: { type: "aborted" } };
+ }
+
+ // 4. Client round2: Generate client signature share
+ const round2Result = teddsaSignRound2(
+ message,
+ keyPackage.keyPackage,
+ new Uint8Array(round1Result.data.nonces),
+ allCommitments,
+ );
+
+ if (!round2Result.success) {
+ return {
+ success: false,
+ err: { type: "sign_fail", error: { type: "error", msg: round2Result.err } },
+ };
+ }
+
+ const clientSignatureShare: TeddsaSignatureShareEntry = {
+ identifier: round2Result.data.identifier,
+ signature_share: round2Result.data.signature_share,
+ };
+
+ const allSignatureShares: TeddsaSignatureShareEntry[] = [
+ clientSignatureShare,
+ serverSignatureShare,
+ ].sort((a, b) => (a.identifier[0] ?? 0) - (b.identifier[0] ?? 0));
+
+ // 5. Aggregate: Combine signature shares into final signature
+ const aggregateResult = teddsaAggregate(
+ message,
+ allCommitments,
+ allSignatureShares,
+ keyPackage.publicKeyPackage,
+ );
+
+ if (!aggregateResult.success) {
+ return {
+ success: false,
+ err: { type: "sign_fail", error: { type: "error", msg: aggregateResult.err } },
+ };
+ }
+
+ return { success: true, data: aggregateResult.data };
+ } catch (error) {
+ return {
+ success: false,
+ err: {
+ type: "sign_fail",
+ error: { type: "error", msg: error instanceof Error ? error.message : String(error) },
+ },
+ };
+ }
+}
diff --git a/embed/oko_attached/src/crypto/sss_ed25519.ts b/embed/oko_attached/src/crypto/sss_ed25519.ts
new file mode 100644
index 000000000..6a31c3006
--- /dev/null
+++ b/embed/oko_attached/src/crypto/sss_ed25519.ts
@@ -0,0 +1,239 @@
+import type { KeyShareNodeMetaWithNodeStatusInfo } from "@oko-wallet/oko-types/tss";
+import { wasmModule } from "@oko-wallet/frost-ed25519-keplr-wasm";
+import { Bytes, type Bytes32 } from "@oko-wallet/bytes";
+import type {
+ PointNumArr,
+ UserKeySharePointByNode,
+} from "@oko-wallet/oko-types/user_key_share";
+import type { TeddsaKeygenOutputBytes } from "@oko-wallet/teddsa-hooks";
+import type { Result } from "@oko-wallet/stdlib-js";
+
+import { hashKeyshareNodeNamesEd25519 } from "./hash";
+
+/**
+ * Data structure for Ed25519 key share backup on KS nodes.
+ * Contains the SSS split of signing_share along with public info needed for reconstruction.
+ */
+export interface Ed25519KeyShareBackup {
+ /** SSS split point (x: identifier, y: share) */
+ share: {
+ x: Bytes32;
+ y: Bytes32;
+ };
+ /** Serialized PublicKeyPackage (needed for reconstruction) */
+ publicKeyPackage: string; // hex string
+ /** Participant identifier (needed for reconstruction) */
+ identifier: string; // hex string
+ /** Ed25519 public key (for verification) */
+ publicKey: string; // hex string
+}
+
+/**
+ * Split Ed25519 key package for backup on Key Share Nodes.
+ *
+ * Extracts the signing_share from the key_package and splits it using SSS.
+ * Also includes the public information needed to reconstruct the key_package.
+ */
+export async function splitUserKeySharesEd25519(
+ keygen_1: TeddsaKeygenOutputBytes,
+ keyshareNodeMeta: KeyShareNodeMetaWithNodeStatusInfo,
+): Promise> {
+ try {
+ const keyPackageBytes = [...keygen_1.key_package];
+
+ // Extract signing_share from key_package (32-byte scalar)
+ const signingShareArr: number[] =
+ wasmModule.extract_signing_share(keyPackageBytes);
+
+ // Hash KS node names to get identifiers for SSS (Ed25519-compatible)
+ const keyshareNodeHashesRes = await hashKeyshareNodeNamesEd25519(
+ keyshareNodeMeta.nodes.map((meta) => meta.name),
+ );
+ if (keyshareNodeHashesRes.success === false) {
+ return {
+ success: false,
+ err: keyshareNodeHashesRes.err,
+ };
+ }
+ const keyshareNodeHashes = keyshareNodeHashesRes.data.map((bytes) => {
+ return [...bytes.toUint8Array()];
+ });
+
+ // Split signing_share using SSS
+ const splitPoints: PointNumArr[] = wasmModule.sss_split(
+ signingShareArr,
+ keyshareNodeHashes,
+ keyshareNodeMeta.threshold,
+ );
+
+ // Convert to UserKeySharePointByNode format
+ const shares: UserKeySharePointByNode[] = splitPoints.map(
+ (point: PointNumArr, index: number) => {
+ const xBytesRes = Bytes.fromUint8Array(
+ Uint8Array.from([...point.x]),
+ 32,
+ );
+ if (xBytesRes.success === false) {
+ throw new Error(xBytesRes.err);
+ }
+ const yBytesRes = Bytes.fromUint8Array(
+ Uint8Array.from([...point.y]),
+ 32,
+ );
+ if (yBytesRes.success === false) {
+ throw new Error(yBytesRes.err);
+ }
+ return {
+ node: {
+ name: keyshareNodeMeta.nodes[index].name,
+ endpoint: keyshareNodeMeta.nodes[index].endpoint,
+ },
+ share: {
+ x: xBytesRes.data,
+ y: yBytesRes.data,
+ },
+ };
+ },
+ );
+
+ return {
+ success: true,
+ data: shares,
+ };
+ } catch (error: any) {
+ return {
+ success: false,
+ err: `splitUserKeySharesEd25519 failed: ${String(error)}`,
+ };
+ }
+}
+
+/**
+ * Combine Ed25519 key shares to recover the signing_share.
+ */
+export async function combineUserSharesEd25519(
+ userKeySharePoints: UserKeySharePointByNode[],
+ threshold: number,
+): Promise> {
+ try {
+ if (threshold < 2) {
+ return {
+ success: false,
+ err: "Threshold must be at least 2",
+ };
+ }
+
+ if (userKeySharePoints.length < threshold) {
+ return {
+ success: false,
+ err: "Number of user key shares is less than threshold",
+ };
+ }
+
+ const points: PointNumArr[] = userKeySharePoints.map(
+ (userKeySharePoint) => ({
+ x: [...userKeySharePoint.share.x.toUint8Array()],
+ y: [...userKeySharePoint.share.y.toUint8Array()],
+ }),
+ );
+
+ // Combine shares to recover signing_share
+ const combinedSigningShare: number[] = wasmModule.sss_combine(
+ points,
+ threshold,
+ );
+
+ return {
+ success: true,
+ data: Uint8Array.from(combinedSigningShare),
+ };
+ } catch (e) {
+ return {
+ success: false,
+ err: `combineUserSharesEd25519 failed: ${String(e)}`,
+ };
+ }
+}
+
+/**
+ * Reconstruct a full KeyPackage from a recovered signing_share and public info.
+ */
+export async function reconstructKeyPackageEd25519(
+ signingShare: Uint8Array,
+ publicKeyPackage: Uint8Array,
+ identifier: Uint8Array,
+): Promise> {
+ try {
+ const keyPackageArr: number[] = wasmModule.reconstruct_key_package(
+ [...signingShare],
+ [...publicKeyPackage],
+ [...identifier],
+ );
+
+ return {
+ success: true,
+ data: Uint8Array.from(keyPackageArr),
+ };
+ } catch (e) {
+ return {
+ success: false,
+ err: `reconstructKeyPackageEd25519 failed: ${String(e)}`,
+ };
+ }
+}
+
+/**
+ * Full recovery of Ed25519 keygen output from KS node shares.
+ *
+ * Given the SSS shares from KS nodes plus the stored public info,
+ * reconstructs the complete TeddsaKeygenOutputBytes.
+ */
+export async function recoverEd25519Keygen(
+ userKeySharePoints: UserKeySharePointByNode[],
+ threshold: number,
+ publicKeyPackage: Uint8Array,
+ identifier: Uint8Array,
+ publicKey: Bytes32,
+): Promise> {
+ try {
+ // Combine shares to recover signing_share
+ const combineRes = await combineUserSharesEd25519(
+ userKeySharePoints,
+ threshold,
+ );
+ if (!combineRes.success) {
+ return {
+ success: false,
+ err: combineRes.err,
+ };
+ }
+
+ // Reconstruct key_package from signing_share + public info
+ const reconstructRes = await reconstructKeyPackageEd25519(
+ combineRes.data,
+ publicKeyPackage,
+ identifier,
+ );
+ if (!reconstructRes.success) {
+ return {
+ success: false,
+ err: reconstructRes.err,
+ };
+ }
+
+ return {
+ success: true,
+ data: {
+ key_package: reconstructRes.data,
+ public_key_package: publicKeyPackage,
+ identifier: identifier,
+ public_key: publicKey,
+ },
+ };
+ } catch (e) {
+ return {
+ success: false,
+ err: `recoverEd25519Keygen failed: ${String(e)}`,
+ };
+ }
+}
diff --git a/embed/oko_attached/src/requests/ks_node.ts b/embed/oko_attached/src/requests/ks_node.ts
index 0a3d7a86d..0141f5856 100644
--- a/embed/oko_attached/src/requests/ks_node.ts
+++ b/embed/oko_attached/src/requests/ks_node.ts
@@ -1,4 +1,5 @@
import { type GetKeyShareResponse } from "@oko-wallet/ksn-interface/key_share";
+import type { CurveType } from "@oko-wallet/ksn-interface/curve_type";
import type { NodeStatusInfo } from "@oko-wallet/oko-types/tss";
import type { AuthType } from "@oko-wallet/oko-types/auth";
import type {
@@ -9,7 +10,7 @@ import type { Result } from "@oko-wallet/stdlib-js";
import type { KSNodeApiResponse } from "@oko-wallet/ksn-interface/response";
import type { RequestSplitSharesError } from "../types/ks_node_request";
-import type { Bytes33 } from "@oko-wallet/bytes";
+import type { Bytes32, Bytes33 } from "@oko-wallet/bytes";
import {
decodeKeyShareStringToPoint256,
encodePoint256ToKeyShareString,
@@ -280,3 +281,230 @@ message(${response.statusText}) in ${ksNodeEndpoint}`,
};
}
}
+
+/**
+ * Send Ed25519 key share to a single KS node.
+ */
+export async function doSendUserKeySharesEd25519(
+ ksNodeEndpoint: string,
+ idToken: string,
+ publicKey: Bytes32,
+ serverShare: Point256,
+ authType: AuthType,
+): Promise> {
+ try {
+ const serverShareString = encodePoint256ToKeyShareString(serverShare);
+ const response = await fetch(`${ksNodeEndpoint}/keyshare/v1/register`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${idToken}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ auth_type: authType,
+ curve_type: "ed25519" as CurveType,
+ public_key: publicKey.toHex(),
+ share: serverShareString,
+ }),
+ });
+
+ if (!response.ok) {
+ try {
+ const data = (await response.json()) as KSNodeApiResponse;
+ if (!data.success && data.code === "DUPLICATE_PUBLIC_KEY") {
+ return { success: true, data: void 0 };
+ }
+ } catch (_) {}
+
+ return {
+ success: false,
+ err: `Failed to send Ed25519 key share: status(${response.status}) \
+message(${response.statusText}) in ${ksNodeEndpoint}`,
+ };
+ }
+
+ const data = (await response.json()) as { success: boolean };
+ if (data.success === false) {
+ return {
+ success: false,
+ err: `Failed to register Ed25519 key share in ${ksNodeEndpoint}`,
+ };
+ }
+ return {
+ success: true,
+ data: void 0,
+ };
+ } catch (e) {
+ return {
+ success: false,
+ err: `Failed to send Ed25519 key share in ${ksNodeEndpoint}: ${String(e)}`,
+ };
+ }
+}
+
+/**
+ * Request Ed25519 key shares from KS nodes for recovery.
+ */
+export async function requestSplitSharesEd25519(
+ publicKey: Bytes32,
+ idToken: string,
+ allNodes: NodeStatusInfo[],
+ threshold: number,
+ authType: AuthType = "google",
+): Promise> {
+ const shuffledNodes = [...allNodes];
+ for (let i = shuffledNodes.length - 1; i > 0; i -= 1) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [shuffledNodes[i], shuffledNodes[j]] = [shuffledNodes[j], shuffledNodes[i]];
+ }
+
+ const succeeded: UserKeySharePointByNode[] = [];
+ let nodesToTry = shuffledNodes.slice(0, threshold);
+ let backupNodes = shuffledNodes.slice(threshold);
+
+ while (succeeded.length < threshold && nodesToTry.length > 0) {
+ const results = await Promise.allSettled(
+ nodesToTry.map((node) =>
+ requestSplitShareEd25519(publicKey, idToken, node, authType),
+ ),
+ );
+
+ const failedNodes: NodeStatusInfo[] = [];
+
+ for (let i = 0; i < results.length; i++) {
+ const result = results[i];
+ const node = nodesToTry[i];
+
+ if (result.status === "fulfilled" && result.value.success) {
+ succeeded.push(result.value.data);
+ } else {
+ const errorCode =
+ result.status === "fulfilled" && !result.value.success
+ ? result.value.err
+ : null;
+
+ if (errorCode === "WALLET_NOT_FOUND") {
+ return {
+ success: false,
+ err: {
+ code: "WALLET_NOT_FOUND",
+ affectedNode: { name: node.name, endpoint: node.endpoint },
+ },
+ };
+ }
+
+ failedNodes.push(node);
+ }
+ }
+
+ if (succeeded.length >= threshold) {
+ return {
+ success: true,
+ data: succeeded.slice(0, threshold),
+ };
+ }
+
+ nodesToTry = [];
+ for (let i = 0; i < failedNodes.length && backupNodes.length > 0; i++) {
+ nodesToTry.push(backupNodes.shift()!);
+ }
+ }
+
+ return {
+ success: false,
+ err: {
+ code: "INSUFFICIENT_SHARES",
+ got: succeeded.length,
+ need: threshold,
+ },
+ };
+}
+
+/**
+ * Request Ed25519 key share from a single KS node.
+ */
+export async function requestSplitShareEd25519(
+ publicKey: Bytes32,
+ idToken: string,
+ node: NodeStatusInfo,
+ authType: AuthType,
+ maxRetries: number = 2,
+): Promise> {
+ let attempt = 0;
+ while (attempt < maxRetries) {
+ try {
+ const response = await fetch(`${node.endpoint}/keyshare/v1/`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${idToken}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ auth_type: authType,
+ curve_type: "ed25519" as CurveType,
+ public_key: publicKey.toHex(),
+ }),
+ });
+
+ if (!response.ok) {
+ let parsedCode: string | null = null;
+ try {
+ const data =
+ (await response.json()) as KSNodeApiResponse;
+ if (data.success === false) {
+ parsedCode = data.code || null;
+ const isNotFound =
+ data.code === "USER_NOT_FOUND" ||
+ data.code === "WALLET_NOT_FOUND";
+ if (isNotFound) {
+ return { success: false, err: "WALLET_NOT_FOUND" };
+ }
+ }
+ } catch (_) {}
+
+ if (attempt < maxRetries - 1) {
+ attempt = attempt + 1;
+ continue;
+ }
+ return { success: false, err: parsedCode ?? `HTTP_${response.status}` };
+ }
+
+ const data =
+ (await response.json()) as KSNodeApiResponse;
+
+ if (data.success === false) {
+ return { success: false, err: data.code || "UNKNOWN_ERROR" };
+ }
+
+ const sharePointRes = decodeKeyShareStringToPoint256(data.data.share);
+ if (sharePointRes.success === false) {
+ return { success: false, err: sharePointRes.err };
+ }
+
+ return {
+ success: true,
+ data: {
+ node: {
+ name: node.name,
+ endpoint: node.endpoint,
+ },
+ share: sharePointRes.data,
+ },
+ };
+ } catch (e) {
+ if (attempt < maxRetries - 1) {
+ attempt = attempt + 1;
+ continue;
+ }
+ return {
+ success: false,
+ err: `Failed to request Ed25519 split share: ${String(e)}`,
+ };
+ }
+ }
+
+ return {
+ success: false,
+ err: "Failed to request Ed25519 split share: max retries exceeded",
+ };
+}
diff --git a/embed/oko_attached/src/store/app.ts b/embed/oko_attached/src/store/app.ts
index d8519557c..9ded68879 100644
--- a/embed/oko_attached/src/store/app.ts
+++ b/embed/oko_attached/src/store/app.ts
@@ -13,10 +13,18 @@ interface WalletState {
name: string | null;
}
+interface KeyPackageEd25519State {
+ keyPackage: string;
+ publicKeyPackage: string;
+ identifier: string;
+ publicKey: string;
+}
+
interface PerOriginState {
theme: Theme | null;
apiKey: string | null;
keyshare_1: string | null;
+ keyPackageEd25519: KeyPackageEd25519State | null;
nonce: string | null;
codeVerifier: string | null;
authToken: string | null;
@@ -43,6 +51,12 @@ interface AppActions {
getKeyshare_1: (hostOrigin: string) => string | null;
setKeyshare_1: (hostOrigin: string, keyshare_1: string | null) => void;
+ getKeyPackageEd25519: (hostOrigin: string) => KeyPackageEd25519State | null;
+ setKeyPackageEd25519: (
+ hostOrigin: string,
+ keyPackage: KeyPackageEd25519State | null,
+ ) => void;
+
getApiKey: (hostOrigin: string) => string | null;
setApiKey: (hostOrigin: string, apiKey: string | null) => void;
@@ -103,6 +117,20 @@ export const useAppState = create(
},
});
},
+ setKeyPackageEd25519: (
+ hostOrigin: string,
+ keyPackageEd25519: KeyPackageEd25519State | null,
+ ) => {
+ set({
+ perOrigin: {
+ ...get().perOrigin,
+ [hostOrigin]: {
+ ...get().perOrigin[hostOrigin],
+ keyPackageEd25519,
+ },
+ },
+ });
+ },
setAuthToken: (hostOrigin: string, authToken: string | null) => {
set({
perOrigin: {
@@ -123,6 +151,7 @@ export const useAppState = create(
theme: null,
apiKey: null,
keyshare_1: null,
+ keyPackageEd25519: null,
nonce: null,
codeVerifier: null,
authToken: null,
@@ -167,6 +196,9 @@ export const useAppState = create(
getKeyshare_1: (hostOrigin: string) => {
return get().perOrigin[hostOrigin]?.keyshare_1;
},
+ getKeyPackageEd25519: (hostOrigin: string) => {
+ return get().perOrigin[hostOrigin]?.keyPackageEd25519;
+ },
getApiKey: (hostOrigin: string) => {
return get().perOrigin[hostOrigin]?.apiKey;
},
diff --git a/embed/oko_attached/src/tx-parsers/sol/index.ts b/embed/oko_attached/src/tx-parsers/sol/index.ts
new file mode 100644
index 000000000..1f24da61a
--- /dev/null
+++ b/embed/oko_attached/src/tx-parsers/sol/index.ts
@@ -0,0 +1,3 @@
+export * from "./types";
+export * from "./parser";
+export * from "./instruction";
diff --git a/embed/oko_attached/src/tx-parsers/sol/instruction.ts b/embed/oko_attached/src/tx-parsers/sol/instruction.ts
new file mode 100644
index 000000000..58be6d9a0
--- /dev/null
+++ b/embed/oko_attached/src/tx-parsers/sol/instruction.ts
@@ -0,0 +1,120 @@
+import {
+ SolanaFMParser,
+ ParserType,
+ type InstructionParserInterface,
+} from "@solanafm/explorer-kit";
+import { getProgramIdl } from "@solanafm/explorer-kit-idls";
+import type { ParsedInstruction, ParsedAccount, ParseResult } from "./types";
+
+const parserCache = new Map();
+
+const KNOWN_PROGRAMS: Record = {
+ "11111111111111111111111111111111": "System Program",
+ TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA: "Token Program",
+ TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb: "Token-2022",
+ ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL: "Associated Token Program",
+ JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4: "Jupiter v6",
+ whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc: "Orca Whirlpool",
+ "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin": "Serum DEX v3",
+};
+
+async function getParserForProgram(
+ programId: string,
+): Promise {
+ if (parserCache.has(programId)) {
+ return parserCache.get(programId) ?? null;
+ }
+
+ try {
+ const idl = await getProgramIdl(programId);
+ if (!idl) {
+ parserCache.set(programId, null);
+ return null;
+ }
+
+ const parser = new SolanaFMParser(idl, programId);
+ const instructionParser = parser.createParser(ParserType.INSTRUCTION);
+
+ if (
+ instructionParser &&
+ "parseInstructions" in instructionParser
+ ) {
+ parserCache.set(programId, instructionParser as InstructionParserInterface);
+ return instructionParser as InstructionParserInterface;
+ }
+ } catch {
+ parserCache.set(programId, null);
+ }
+
+ return null;
+}
+
+export async function parseInstruction(
+ programId: string,
+ instructionData: string,
+ accounts: { pubkey: string; isSigner: boolean; isWritable: boolean }[],
+): Promise> {
+ const programName = KNOWN_PROGRAMS[programId] ?? "Unknown Program";
+
+ const parser = await getParserForProgram(programId);
+
+ if (!parser) {
+ return {
+ success: true,
+ data: {
+ programId,
+ programName,
+ instructionName: "Unknown",
+ data: { raw: instructionData },
+ accounts: accounts.map((acc) => ({
+ pubkey: acc.pubkey,
+ isSigner: acc.isSigner,
+ isWritable: acc.isWritable,
+ })),
+ },
+ };
+ }
+
+ try {
+ const parsed = parser.parseInstructions(instructionData);
+
+ if (!parsed) {
+ return {
+ success: true,
+ data: {
+ programId,
+ programName,
+ instructionName: "Unknown",
+ data: { raw: instructionData },
+ accounts: accounts.map((acc) => ({
+ pubkey: acc.pubkey,
+ isSigner: acc.isSigner,
+ isWritable: acc.isWritable,
+ })),
+ },
+ };
+ }
+
+ const parsedAccounts: ParsedAccount[] = accounts.map((acc) => ({
+ pubkey: acc.pubkey,
+ isSigner: acc.isSigner,
+ isWritable: acc.isWritable,
+ }));
+
+ return {
+ success: true,
+ data: {
+ programId,
+ programName,
+ instructionName: parsed.name ?? "Unknown",
+ data: parsed.data ?? {},
+ accounts: parsedAccounts,
+ },
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: `Failed to parse instruction: ${error}`,
+ };
+ }
+}
diff --git a/embed/oko_attached/src/tx-parsers/sol/parser.ts b/embed/oko_attached/src/tx-parsers/sol/parser.ts
new file mode 100644
index 000000000..debfd67dd
--- /dev/null
+++ b/embed/oko_attached/src/tx-parsers/sol/parser.ts
@@ -0,0 +1,115 @@
+import {
+ Transaction,
+ VersionedTransaction,
+} from "@solana/web3.js";
+import { parseInstruction } from "./instruction";
+import type {
+ ParsedTransaction,
+ ParsedInstruction,
+ ParseResult,
+} from "./types";
+import bs58 from "bs58";
+
+export async function parseTransaction(
+ transaction: Transaction | VersionedTransaction,
+): Promise> {
+ try {
+ if (transaction instanceof Transaction) {
+ return await parseLegacyTransaction(transaction);
+ } else {
+ return await parseVersionedTransaction(transaction);
+ }
+ } catch (error) {
+ return {
+ success: false,
+ error: `Failed to parse transaction: ${error}`,
+ };
+ }
+}
+
+async function parseLegacyTransaction(
+ transaction: Transaction,
+): Promise> {
+ const instructions: ParsedInstruction[] = [];
+
+ for (const ix of transaction.instructions) {
+ const accounts = ix.keys.map((key) => ({
+ pubkey: key.pubkey.toBase58(),
+ isSigner: key.isSigner,
+ isWritable: key.isWritable,
+ }));
+
+ const result = await parseInstruction(
+ ix.programId.toBase58(),
+ bs58.encode(ix.data),
+ accounts,
+ );
+
+ if (result.success) {
+ instructions.push(result.data);
+ }
+ }
+
+ return {
+ success: true,
+ data: {
+ instructions,
+ feePayer: transaction.feePayer?.toBase58() ?? null,
+ recentBlockhash: transaction.recentBlockhash ?? null,
+ },
+ };
+}
+
+async function parseVersionedTransaction(
+ transaction: VersionedTransaction,
+): Promise> {
+ const message = transaction.message;
+ const instructions: ParsedInstruction[] = [];
+ const accountKeys = message.staticAccountKeys;
+
+ for (const ix of message.compiledInstructions) {
+ const programId = accountKeys[ix.programIdIndex].toBase58();
+
+ const accounts = ix.accountKeyIndexes.map((idx) => ({
+ pubkey: accountKeys[idx].toBase58(),
+ isSigner: message.isAccountSigner(idx),
+ isWritable: message.isAccountWritable(idx),
+ }));
+
+ const result = await parseInstruction(programId, bs58.encode(ix.data), accounts);
+
+ if (result.success) {
+ instructions.push(result.data);
+ }
+ }
+
+ return {
+ success: true,
+ data: {
+ instructions,
+ feePayer: accountKeys[0]?.toBase58() ?? null,
+ recentBlockhash: message.recentBlockhash ?? null,
+ },
+ };
+}
+
+export function deserializeTransaction(
+ serialized: Uint8Array | Buffer,
+): ParseResult {
+ try {
+ const buffer = Buffer.from(serialized);
+
+ try {
+ const versionedTx = VersionedTransaction.deserialize(buffer);
+ return { success: true, data: versionedTx };
+ } catch {
+ const legacyTx = Transaction.from(buffer);
+ return { success: true, data: legacyTx };
+ }
+ } catch (error) {
+ return {
+ success: false,
+ error: `Failed to deserialize transaction: ${error}`,
+ };
+ }
+}
diff --git a/embed/oko_attached/src/tx-parsers/sol/types.ts b/embed/oko_attached/src/tx-parsers/sol/types.ts
new file mode 100644
index 000000000..57bb9e6c5
--- /dev/null
+++ b/embed/oko_attached/src/tx-parsers/sol/types.ts
@@ -0,0 +1,24 @@
+export interface ParsedInstruction {
+ programId: string;
+ programName: string;
+ instructionName: string;
+ data: Record;
+ accounts: ParsedAccount[];
+}
+
+export interface ParsedAccount {
+ pubkey: string;
+ isSigner: boolean;
+ isWritable: boolean;
+ name?: string;
+}
+
+export interface ParsedTransaction {
+ instructions: ParsedInstruction[];
+ feePayer: string | null;
+ recentBlockhash: string | null;
+}
+
+export type ParseResult =
+ | { success: true; data: T }
+ | { success: false; error: string };
diff --git a/embed/oko_attached/src/utils/base64.ts b/embed/oko_attached/src/utils/base64.ts
new file mode 100644
index 000000000..725fa4d90
--- /dev/null
+++ b/embed/oko_attached/src/utils/base64.ts
@@ -0,0 +1,8 @@
+export function base64ToUint8Array(base64: string): Uint8Array {
+ const binaryString = atob(base64);
+ const bytes = new Uint8Array(binaryString.length);
+ for (let i = 0; i < binaryString.length; i++) {
+ bytes[i] = binaryString.charCodeAt(i);
+ }
+ return bytes;
+}
diff --git a/embed/oko_attached/src/wasm/index.ts b/embed/oko_attached/src/wasm/index.ts
index c5aa465d7..17d3beed0 100644
--- a/embed/oko_attached/src/wasm/index.ts
+++ b/embed/oko_attached/src/wasm/index.ts
@@ -1,11 +1,23 @@
-import * as wasmModule from "@oko-wallet/cait-sith-keplr-wasm/pkg/cait_sith_keplr_wasm";
-import { initWasm } from "@oko-wallet/cait-sith-keplr-wasm";
+import * as caitSithWasmModule from "@oko-wallet/cait-sith-keplr-wasm/pkg/cait_sith_keplr_wasm";
+import { initWasm as initCaitSithWasm } from "@oko-wallet/cait-sith-keplr-wasm";
+import * as frostWasmModule from "@oko-wallet/frost-ed25519-keplr-wasm/pkg/frost_ed25519_keplr_wasm";
+import { initWasm as initFrostWasm } from "@oko-wallet/frost-ed25519-keplr-wasm";
export async function initKeplrWasm() {
try {
- await initWasm(wasmModule, "/pkg/cait_sith_keplr_wasm_bg.wasm");
- console.log("[attached] WASM initialized");
+ await initCaitSithWasm(
+ caitSithWasmModule,
+ "/pkg/cait_sith_keplr_wasm_bg.wasm",
+ );
+ console.log("[attached] cait-sith WASM initialized");
} catch (err) {
- console.error("[attached] Error initializing WASM, err: %s", err);
+ console.error("[attached] Error initializing cait-sith WASM, err: %s", err);
+ }
+
+ try {
+ await initFrostWasm(frostWasmModule, "/pkg/frost_ed25519_keplr_wasm_bg.wasm");
+ console.log("[attached] frost-ed25519 WASM initialized");
+ } catch (err) {
+ console.error("[attached] Error initializing frost-ed25519 WASM, err: %s", err);
}
}
diff --git a/embed/oko_attached/src/web3/solana/queries/index.ts b/embed/oko_attached/src/web3/solana/queries/index.ts
new file mode 100644
index 000000000..09a0f0dc0
--- /dev/null
+++ b/embed/oko_attached/src/web3/solana/queries/index.ts
@@ -0,0 +1 @@
+export * from "./use_get_token_metadata";
diff --git a/embed/oko_attached/src/web3/solana/queries/use_get_token_metadata.ts b/embed/oko_attached/src/web3/solana/queries/use_get_token_metadata.ts
new file mode 100644
index 000000000..a42755806
--- /dev/null
+++ b/embed/oko_attached/src/web3/solana/queries/use_get_token_metadata.ts
@@ -0,0 +1,91 @@
+import { useQuery, type UseQueryOptions } from "@tanstack/react-query";
+
+const JUPITER_TOKEN_API_V2 = "https://api.jup.ag/tokens/v2";
+
+export interface JupiterToken {
+ id: string;
+ name: string;
+ symbol: string;
+ decimals: number;
+ icon: string | null;
+ tags: string[];
+ isVerified: boolean;
+}
+
+export type SolanaTokenMetadataResult = {
+ name?: string;
+ symbol?: string;
+ decimals?: number;
+ icon?: string;
+};
+
+export interface UseGetSolanaTokenMetadataProps {
+ mintAddress?: string;
+ options?: Partial>;
+}
+
+async function fetchTokenMetadata(
+ mintAddress: string,
+): Promise {
+ const url = `${JUPITER_TOKEN_API_V2}/search?query=${encodeURIComponent(mintAddress)}`;
+
+ const response = await fetch(url, {
+ method: "GET",
+ headers: { Accept: "application/json" },
+ });
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ return null;
+ }
+ throw new Error(
+ `Jupiter API error: ${response.status} ${response.statusText}`,
+ );
+ }
+
+ const tokens: JupiterToken[] = await response.json();
+
+ const token = tokens.find(
+ (t) => t.id.toLowerCase() === mintAddress.toLowerCase(),
+ );
+
+ return token ?? null;
+}
+
+export function useGetSolanaTokenMetadata({
+ mintAddress,
+ options,
+}: UseGetSolanaTokenMetadataProps) {
+ return useQuery({
+ queryKey: ["solana-token-metadata", mintAddress],
+ queryFn: async (): Promise => {
+ const defaultResult = {
+ name: undefined,
+ symbol: undefined,
+ decimals: undefined,
+ icon: undefined,
+ };
+
+ if (!mintAddress) {
+ return defaultResult;
+ }
+
+ try {
+ const token = await fetchTokenMetadata(mintAddress);
+ if (!token) {
+ return defaultResult;
+ }
+ return {
+ name: token.name,
+ symbol: token.symbol,
+ decimals: token.decimals,
+ icon: token.icon ?? undefined,
+ };
+ } catch {
+ return defaultResult;
+ }
+ },
+ ...options,
+ enabled: !!mintAddress && options?.enabled !== false,
+ });
+}
diff --git a/embed/oko_attached/src/web3/solana/use_calculate_fee.ts b/embed/oko_attached/src/web3/solana/use_calculate_fee.ts
new file mode 100644
index 000000000..659550ce6
--- /dev/null
+++ b/embed/oko_attached/src/web3/solana/use_calculate_fee.ts
@@ -0,0 +1,118 @@
+import { useMemo } from "react";
+import {
+ VersionedTransaction,
+ Transaction,
+ ComputeBudgetProgram,
+} from "@solana/web3.js";
+
+import { base64ToUint8Array } from "@oko-wallet-attached/utils/base64";
+
+const LAMPORTS_PER_SIGNATURE = 5000;
+const DEFAULT_COMPUTE_UNITS = 200_000;
+const COMPUTE_BUDGET_PROGRAM_ID = ComputeBudgetProgram.programId.toBase58();
+
+interface PriorityFeeInfo {
+ computeUnits: number;
+ microLamportsPerUnit: number;
+}
+
+function parsePriorityFee(
+ instructions: { programId: string; data: Uint8Array }[],
+): PriorityFeeInfo {
+ let computeUnits = DEFAULT_COMPUTE_UNITS;
+ let microLamportsPerUnit = 0;
+
+ for (const ix of instructions) {
+ if (ix.programId !== COMPUTE_BUDGET_PROGRAM_ID) continue;
+
+ const data = ix.data;
+ if (data.length === 0) continue;
+
+ const instructionType = data[0];
+
+ // SetComputeUnitLimit = 2
+ if (instructionType === 2 && data.length >= 5) {
+ computeUnits = new DataView(
+ data.buffer,
+ data.byteOffset + 1,
+ 4,
+ ).getUint32(0, true);
+ }
+
+ // SetComputeUnitPrice = 3
+ if (instructionType === 3 && data.length >= 9) {
+ microLamportsPerUnit = Number(
+ new DataView(data.buffer, data.byteOffset + 1, 8).getBigUint64(0, true),
+ );
+ }
+ }
+
+ return { computeUnits, microLamportsPerUnit };
+}
+
+export interface UseCalculateFeeProps {
+ serializedTransaction: string;
+ isVersioned: boolean;
+}
+
+export interface UseCalculateFeeResult {
+ fee: number | null;
+}
+
+export function useCalculateFee({
+ serializedTransaction,
+ isVersioned,
+}: UseCalculateFeeProps): UseCalculateFeeResult {
+ const fee = useMemo(() => {
+ if (!serializedTransaction) {
+ return null;
+ }
+
+ try {
+ const txBytes = base64ToUint8Array(serializedTransaction);
+
+ let numSignatures: number;
+ let priorityFeeInfo: PriorityFeeInfo;
+
+ if (isVersioned) {
+ const transaction = VersionedTransaction.deserialize(txBytes);
+ const message = transaction.message;
+
+ numSignatures = message.header.numRequiredSignatures;
+
+ const accountKeys = message.staticAccountKeys.map((k) => k.toBase58());
+ const instructions = message.compiledInstructions.map((ix) => ({
+ programId: accountKeys[ix.programIdIndex],
+ data: ix.data,
+ }));
+ priorityFeeInfo = parsePriorityFee(instructions);
+ } else {
+ const transaction = Transaction.from(txBytes);
+ const message = transaction.compileMessage();
+
+ numSignatures = message.header.numRequiredSignatures;
+
+ const instructions = transaction.instructions.map((ix) => ({
+ programId: ix.programId.toBase58(),
+ data: ix.data,
+ }));
+ priorityFeeInfo = parsePriorityFee(instructions);
+ }
+
+ // Base fee: 5000 lamports per signature
+ const baseFee = numSignatures * LAMPORTS_PER_SIGNATURE;
+
+ // Priority fee: microLamports × computeUnits / 1_000_000
+ const priorityFee = Math.floor(
+ (priorityFeeInfo.microLamportsPerUnit * priorityFeeInfo.computeUnits) /
+ 1_000_000,
+ );
+
+ return baseFee + priorityFee;
+ } catch {
+ return null;
+ }
+ }, [serializedTransaction, isVersioned]);
+
+ return { fee };
+}
diff --git a/embed/oko_attached/src/window_msgs/get_public_key_ed25519.ts b/embed/oko_attached/src/window_msgs/get_public_key_ed25519.ts
new file mode 100644
index 000000000..7029d1a3c
--- /dev/null
+++ b/embed/oko_attached/src/window_msgs/get_public_key_ed25519.ts
@@ -0,0 +1,31 @@
+import type { OkoWalletMsgGetPublicKeyEd25519Ack } from "@oko-wallet/oko-sdk-core";
+
+import { OKO_SDK_TARGET } from "./target";
+import { useAppState } from "@oko-wallet-attached/store/app";
+import type { MsgEventContext } from "./types";
+
+export async function handleGetPublicKeyEd25519(ctx: MsgEventContext) {
+ const { port, hostOrigin } = ctx;
+ const keyPackageEd25519 = useAppState.getState().getKeyPackageEd25519(hostOrigin);
+
+ let payload: OkoWalletMsgGetPublicKeyEd25519Ack["payload"];
+ if (keyPackageEd25519?.publicKey) {
+ payload = {
+ success: true,
+ data: keyPackageEd25519.publicKey,
+ };
+ } else {
+ payload = {
+ success: false,
+ err: "No Ed25519 public key found",
+ };
+ }
+
+ const ack: OkoWalletMsgGetPublicKeyEd25519Ack = {
+ target: OKO_SDK_TARGET,
+ msg_type: "get_public_key_ed25519_ack",
+ payload,
+ };
+
+ port.postMessage(ack);
+}
diff --git a/embed/oko_attached/src/window_msgs/index.ts b/embed/oko_attached/src/window_msgs/index.ts
index 440c2a161..870ffdad3 100644
--- a/embed/oko_attached/src/window_msgs/index.ts
+++ b/embed/oko_attached/src/window_msgs/index.ts
@@ -2,6 +2,7 @@ import type { OkoWalletMsg } from "@oko-wallet/oko-sdk-core";
import type { MsgEventContext } from "./types";
import { handleGetPublicKey } from "./get_public_key";
+import { handleGetPublicKeyEd25519 } from "./get_public_key_ed25519";
import { handleSetOAuthNonce } from "./set_oauth_nonce";
import { handleSetCodeVerifier } from "./set_code_verifier";
import { handleOpenModal } from "./open_modal";
@@ -58,6 +59,11 @@ export function makeMsgHandler() {
break;
}
+ case "get_public_key_ed25519": {
+ await handleGetPublicKeyEd25519(ctx);
+ break;
+ }
+
case "get_email": {
await handleGetEmail(ctx);
break;
diff --git a/embed/oko_attached/src/window_msgs/oauth_info_pass/index.ts b/embed/oko_attached/src/window_msgs/oauth_info_pass/index.ts
index a50ca087b..0216599c3 100644
--- a/embed/oko_attached/src/window_msgs/oauth_info_pass/index.ts
+++ b/embed/oko_attached/src/window_msgs/oauth_info_pass/index.ts
@@ -107,6 +107,7 @@ export async function handleOAuthInfoPass(
idToken,
userExists,
authType,
+ apiKey,
);
if (!handleUserSignInRes.success) {
await bail(message, handleUserSignInRes.err);
@@ -124,6 +125,11 @@ export async function handleOAuthInfoPass(
name: signInResult.name,
});
+ // Store Ed25519 key package if available (for Solana support)
+ if (signInResult.keyPackageEd25519) {
+ appState.setKeyPackageEd25519(hostOrigin, signInResult.keyPackageEd25519);
+ }
+
hasSignedIn = true;
isNewUser = signInResult.isNewUser;
@@ -166,6 +172,7 @@ export async function handleUserSignIn(
idToken: string,
userExists: CheckEmailResponse,
authType: AuthType,
+ apiKey: string,
): Promise> {
const meta = userExists.keyshare_node_meta;
@@ -176,6 +183,7 @@ export async function handleUserSignIn(
idToken,
meta,
authType,
+ apiKey,
referralInfo,
);
if (!signInRes.success) {
@@ -204,7 +212,7 @@ export async function handleUserSignIn(
}
// sign in flow
else {
- const signInRes = await handleExistingUser(idToken, meta, authType);
+ const signInRes = await handleExistingUser(idToken, meta, authType, apiKey);
if (!signInRes.success) {
throw signInRes.err;
}
diff --git a/embed/oko_attached/src/window_msgs/oauth_info_pass/user.ts b/embed/oko_attached/src/window_msgs/oauth_info_pass/user.ts
index 0624d8f37..ae1eb6edd 100644
--- a/embed/oko_attached/src/window_msgs/oauth_info_pass/user.ts
+++ b/embed/oko_attached/src/window_msgs/oauth_info_pass/user.ts
@@ -27,17 +27,102 @@ import { reshareUserKeyShares } from "@oko-wallet-attached/crypto/reshare";
import {
doSendUserKeyShares,
requestSplitShares,
+ requestSplitSharesEd25519,
} from "@oko-wallet-attached/requests/ks_node";
import { runKeygen } from "@oko-wallet/cait-sith-keplr-hooks";
+import { runTeddsaKeygen } from "@oko-wallet/teddsa-hooks";
import { reqKeygen } from "@oko-wallet/api-lib";
import { Bytes } from "@oko-wallet/bytes";
import type { ReferralInfo } from "@oko-wallet-attached/store/memory/types";
+import {
+ teddsaKeygenToHex,
+ type KeyPackageEd25519Hex,
+} from "@oko-wallet-attached/crypto/keygen_ed25519";
+import { recoverEd25519Keygen } from "@oko-wallet-attached/crypto/sss_ed25519";
+
+async function recoverEd25519KeyPackage(
+ idToken: string,
+ keyshareNodeMeta: KeyShareNodeMetaWithNodeStatusInfo,
+ authType: AuthType,
+): Promise {
+ const publicInfoRes = await fetch(
+ `${TSS_V1_ENDPOINT}/wallet_ed25519/public_info`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${idToken}`,
+ },
+ body: JSON.stringify({}),
+ },
+ );
+
+ if (!publicInfoRes.ok) {
+ console.warn(
+ "[attached] Ed25519 public info HTTP error:",
+ publicInfoRes.status,
+ );
+ return null;
+ }
+
+ const publicInfoData = await publicInfoRes.json();
+ if (!publicInfoData.success) {
+ console.warn(
+ "[attached] Ed25519 public info request failed:",
+ publicInfoData.msg,
+ );
+ return null;
+ }
+
+ const {
+ public_key: publicKeyHex,
+ public_key_package,
+ identifier,
+ } = publicInfoData.data;
+
+ const publicKeyBytesRes = Bytes.fromHexString(publicKeyHex, 32);
+ if (!publicKeyBytesRes.success) {
+ console.warn(
+ "[attached] Invalid Ed25519 public key:",
+ publicKeyBytesRes.err,
+ );
+ return null;
+ }
+
+ const sharesRes = await requestSplitSharesEd25519(
+ publicKeyBytesRes.data,
+ idToken,
+ keyshareNodeMeta.nodes,
+ keyshareNodeMeta.threshold,
+ authType,
+ );
+ if (!sharesRes.success) {
+ console.warn("[attached] Ed25519 shares request failed:", sharesRes.err);
+ return null;
+ }
+
+ const recoveryRes = await recoverEd25519Keygen(
+ sharesRes.data,
+ keyshareNodeMeta.threshold,
+ Uint8Array.from(public_key_package),
+ Uint8Array.from(identifier),
+ publicKeyBytesRes.data,
+ );
+ if (!recoveryRes.success) {
+ console.warn("[attached] Ed25519 key recovery failed:", recoveryRes.err);
+ return null;
+ }
+
+ console.log("[attached] Ed25519 key recovered successfully");
+ return teddsaKeygenToHex(recoveryRes.data);
+}
export async function handleExistingUser(
idToken: string,
keyshareNodeMeta: KeyShareNodeMetaWithNodeStatusInfo,
authType: AuthType,
+ apiKey: string,
): Promise> {
// 1. sign in to api server
const signInRes = await makeAuthorizedOkoApiRequest(
@@ -141,6 +226,7 @@ export async function handleExistingUser(
isNewUser: false,
email: signInResp.user.email ?? null,
name: signInResp.user.name ?? null,
+ keyPackageEd25519: null,
},
};
}
@@ -173,16 +259,313 @@ user pk: ${signInResp.user.public_key}`,
};
}
+ // 4. Ed25519 keygen for Solana support (if user doesn't have Ed25519 wallet yet)
+ // This is non-blocking - failure doesn't fail the entire sign-in
+ let keyPackageEd25519: KeyPackageEd25519Hex | null = null;
+ // Track updated JWT token (with wallet_id_ed25519) from Ed25519 keygen
+ let updatedJwtToken: string | null = null;
+ try {
+ const ed25519KeygenRes = await runTeddsaKeygen();
+ if (ed25519KeygenRes.success) {
+ const { keygen_1: ed25519Keygen1, keygen_2: ed25519Keygen2 } =
+ ed25519KeygenRes.data;
+
+ // Send Ed25519 keygen_2 to server
+ const serverRes = await fetch(`${TSS_V1_ENDPOINT}/keygen_ed25519`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "x-api-key": apiKey,
+ Authorization: `Bearer ${idToken}`,
+ },
+ body: JSON.stringify({
+ keygen_2: {
+ key_package: [...ed25519Keygen2.key_package],
+ public_key_package: [...ed25519Keygen2.public_key_package],
+ identifier: [...ed25519Keygen2.identifier],
+ public_key: [...ed25519Keygen2.public_key.toUint8Array()],
+ },
+ }),
+ });
+
+ if (serverRes.ok) {
+ const serverData = await serverRes.json();
+ if (serverData.success) {
+ keyPackageEd25519 = teddsaKeygenToHex(ed25519Keygen1);
+ // Capture new JWT that includes wallet_id_ed25519
+ updatedJwtToken = serverData.data.token;
+ console.log("[attached] Ed25519 keygen successful for existing user");
+
+ // SSS split Ed25519 key_package and send to KS nodes
+ try {
+ const { splitUserKeySharesEd25519 } = await import(
+ "@oko-wallet-attached/crypto/sss_ed25519"
+ );
+ const { doSendUserKeySharesEd25519 } = await import(
+ "@oko-wallet-attached/requests/ks_node"
+ );
+
+ const splitEd25519Res = await splitUserKeySharesEd25519(
+ ed25519Keygen1,
+ keyshareNodeMeta,
+ );
+
+ if (splitEd25519Res.success) {
+ const ed25519KeyShares = splitEd25519Res.data;
+
+ // Send Ed25519 shares to all KS nodes
+ const sendEd25519Results = await Promise.all(
+ ed25519KeyShares.map((keyShareByNode) =>
+ doSendUserKeySharesEd25519(
+ keyShareByNode.node.endpoint,
+ idToken,
+ ed25519Keygen1.public_key,
+ keyShareByNode.share,
+ authType,
+ ),
+ ),
+ );
+
+ const ed25519SendErrors = sendEd25519Results.filter(
+ (result) => result.success === false,
+ );
+ if (ed25519SendErrors.length > 0) {
+ console.warn(
+ "[attached] Some Ed25519 key shares failed to send:",
+ ed25519SendErrors.map((e) => e.err).join(", "),
+ );
+ } else {
+ console.log(
+ "[attached] Ed25519 key shares sent to KS nodes successfully",
+ );
+ }
+ } else {
+ console.warn(
+ "[attached] Ed25519 SSS split failed:",
+ splitEd25519Res.err,
+ );
+ }
+ } catch (sssErr) {
+ console.warn("[attached] Ed25519 SSS error:", sssErr);
+ }
+ } else if (serverData.code === "WALLET_ALREADY_EXISTS") {
+ // User already has Ed25519 wallet, recover from KS nodes
+ console.log(
+ "[attached] Ed25519 wallet already exists, recovering from KS nodes",
+ );
+ try {
+ // 1. Get Ed25519 public info from server
+ const publicInfoRes = await fetch(
+ `${TSS_V1_ENDPOINT}/wallet_ed25519/public_info`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${idToken}`,
+ },
+ body: JSON.stringify({}),
+ },
+ );
+
+ if (publicInfoRes.ok) {
+ const publicInfoData = await publicInfoRes.json();
+ if (publicInfoData.success) {
+ const {
+ public_key: publicKeyHex,
+ public_key_package,
+ identifier,
+ } = publicInfoData.data;
+
+ // 2. Request Ed25519 shares from KS nodes
+ const publicKeyBytesRes = Bytes.fromHexString(publicKeyHex, 32);
+ if (publicKeyBytesRes.success) {
+ const sharesRes = await requestSplitSharesEd25519(
+ publicKeyBytesRes.data,
+ idToken,
+ keyshareNodeMeta.nodes,
+ keyshareNodeMeta.threshold,
+ authType,
+ );
+
+ if (sharesRes.success) {
+ // 3. Recover Ed25519 keygen output
+ const recoveryRes = await recoverEd25519Keygen(
+ sharesRes.data,
+ keyshareNodeMeta.threshold,
+ Uint8Array.from(public_key_package),
+ Uint8Array.from(identifier),
+ publicKeyBytesRes.data,
+ );
+
+ if (recoveryRes.success) {
+ keyPackageEd25519 = teddsaKeygenToHex(recoveryRes.data);
+ console.log(
+ "[attached] Ed25519 key recovered successfully",
+ );
+ } else {
+ console.warn(
+ "[attached] Ed25519 key recovery failed:",
+ recoveryRes.err,
+ );
+ }
+ } else {
+ console.warn(
+ "[attached] Ed25519 shares request failed:",
+ sharesRes.err,
+ );
+ }
+ } else {
+ console.warn(
+ "[attached] Invalid Ed25519 public key:",
+ publicKeyBytesRes.err,
+ );
+ }
+ } else {
+ console.warn(
+ "[attached] Ed25519 public info request failed:",
+ publicInfoData.msg,
+ );
+ }
+ } else {
+ console.warn(
+ "[attached] Ed25519 public info HTTP error:",
+ publicInfoRes.status,
+ );
+ }
+ } catch (recoveryErr) {
+ console.warn("[attached] Ed25519 recovery error:", recoveryErr);
+ }
+ } else {
+ console.warn(
+ "[attached] Ed25519 server keygen failed:",
+ serverData.msg,
+ );
+ }
+ } else {
+ // Non-2xx response - check if it's WALLET_ALREADY_EXISTS (409)
+ try {
+ const errorData = await serverRes.json();
+ if (errorData.code === "WALLET_ALREADY_EXISTS") {
+ // User already has Ed25519 wallet, recover from KS nodes
+ console.log(
+ "[attached] Ed25519 wallet already exists (409), recovering from KS nodes",
+ );
+ try {
+ // 1. Get Ed25519 public info from server
+ const publicInfoRes = await fetch(
+ `${TSS_V1_ENDPOINT}/wallet_ed25519/public_info`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${idToken}`,
+ },
+ body: JSON.stringify({}),
+ },
+ );
+
+ if (publicInfoRes.ok) {
+ const publicInfoData = await publicInfoRes.json();
+ if (publicInfoData.success) {
+ const {
+ public_key: publicKeyHex,
+ public_key_package,
+ identifier,
+ } = publicInfoData.data;
+
+ // 2. Request Ed25519 shares from KS nodes
+ const publicKeyBytesRes = Bytes.fromHexString(publicKeyHex, 32);
+ if (publicKeyBytesRes.success) {
+ const sharesRes = await requestSplitSharesEd25519(
+ publicKeyBytesRes.data,
+ idToken,
+ keyshareNodeMeta.nodes,
+ keyshareNodeMeta.threshold,
+ authType,
+ );
+
+ if (sharesRes.success) {
+ // 3. Recover Ed25519 keygen output
+ const recoveryRes = await recoverEd25519Keygen(
+ sharesRes.data,
+ keyshareNodeMeta.threshold,
+ Uint8Array.from(public_key_package),
+ Uint8Array.from(identifier),
+ publicKeyBytesRes.data,
+ );
+
+ if (recoveryRes.success) {
+ keyPackageEd25519 = teddsaKeygenToHex(recoveryRes.data);
+ console.log(
+ "[attached] Ed25519 key recovered successfully",
+ );
+ } else {
+ console.warn(
+ "[attached] Ed25519 key recovery failed:",
+ recoveryRes.err,
+ );
+ }
+ } else {
+ console.warn(
+ "[attached] Ed25519 shares request failed:",
+ sharesRes.err,
+ );
+ }
+ } else {
+ console.warn(
+ "[attached] Invalid Ed25519 public key:",
+ publicKeyBytesRes.err,
+ );
+ }
+ } else {
+ console.warn(
+ "[attached] Ed25519 public info request failed:",
+ publicInfoData.msg,
+ );
+ }
+ } else {
+ console.warn(
+ "[attached] Ed25519 public info HTTP error:",
+ publicInfoRes.status,
+ );
+ }
+ } catch (recoveryErr) {
+ console.warn("[attached] Ed25519 recovery error:", recoveryErr);
+ }
+ } else {
+ console.warn(
+ "[attached] Ed25519 server keygen request failed:",
+ serverRes.status,
+ errorData,
+ );
+ }
+ } catch {
+ console.warn(
+ "[attached] Ed25519 server keygen request failed (non-JSON):",
+ serverRes.status,
+ );
+ }
+ }
+ } else {
+ console.warn("[attached] Ed25519 keygen failed:", ed25519KeygenRes.err);
+ }
+ } catch (err) {
+ // Log but don't fail sign-in if Ed25519 keygen fails
+ console.warn("[attached] Ed25519 keygen error:", err);
+ }
+
return {
success: true,
data: {
publicKey: signInResp.user.public_key,
walletId: signInResp.user.wallet_id,
- jwtToken: signInResp.token,
+ // Use updated JWT (with wallet_id_ed25519) if Ed25519 keygen succeeded
+ jwtToken: updatedJwtToken ?? signInResp.token,
keyshare_1: keyshare_1_res.data,
isNewUser: false,
email: signInResp.user.email ?? null,
name: signInResp.user.name ?? null,
+ keyPackageEd25519,
},
};
}
@@ -191,6 +574,7 @@ export async function handleNewUser(
idToken: string,
keyshareNodeMeta: KeyShareNodeMetaWithNodeStatusInfo,
authType: AuthType,
+ apiKey: string,
referralInfo?: ReferralInfo | null,
): Promise> {
// TODO: @jinwoo, (wip) importing secret key
@@ -273,16 +657,129 @@ export async function handleNewUser(
}
}
+ // Ed25519 keygen (for Solana support)
+ // This is non-blocking - failure doesn't fail the entire sign-in
+ let keyPackageEd25519: KeyPackageEd25519Hex | null = null;
+ // Track updated JWT token (with wallet_id_ed25519) from Ed25519 keygen
+ let updatedJwtToken: string | null = null;
+ try {
+ const ed25519KeygenRes = await runTeddsaKeygen();
+ if (ed25519KeygenRes.success) {
+ const { keygen_1: ed25519Keygen1, keygen_2: ed25519Keygen2 } =
+ ed25519KeygenRes.data;
+
+ // Send Ed25519 keygen_2 to server
+ const serverRes = await fetch(`${TSS_V1_ENDPOINT}/keygen_ed25519`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "x-api-key": apiKey,
+ Authorization: `Bearer ${idToken}`,
+ },
+ body: JSON.stringify({
+ keygen_2: {
+ key_package: [...ed25519Keygen2.key_package],
+ public_key_package: [...ed25519Keygen2.public_key_package],
+ identifier: [...ed25519Keygen2.identifier],
+ public_key: [...ed25519Keygen2.public_key.toUint8Array()],
+ },
+ }),
+ });
+
+ if (serverRes.ok) {
+ const serverData = await serverRes.json();
+ if (serverData.success) {
+ keyPackageEd25519 = teddsaKeygenToHex(ed25519Keygen1);
+ // Capture new JWT that includes wallet_id_ed25519
+ updatedJwtToken = serverData.data.token;
+ console.log("[attached] Ed25519 keygen successful");
+
+ // SSS split Ed25519 key_package and send to KS nodes
+ try {
+ const { splitUserKeySharesEd25519 } = await import(
+ "@oko-wallet-attached/crypto/sss_ed25519"
+ );
+ const { doSendUserKeySharesEd25519 } = await import(
+ "@oko-wallet-attached/requests/ks_node"
+ );
+
+ const splitEd25519Res = await splitUserKeySharesEd25519(
+ ed25519Keygen1,
+ keyshareNodeMeta,
+ );
+
+ if (splitEd25519Res.success) {
+ const ed25519KeyShares = splitEd25519Res.data;
+
+ // Send Ed25519 shares to all KS nodes
+ const sendEd25519Results = await Promise.all(
+ ed25519KeyShares.map((keyShareByNode) =>
+ doSendUserKeySharesEd25519(
+ keyShareByNode.node.endpoint,
+ idToken,
+ ed25519Keygen1.public_key,
+ keyShareByNode.share,
+ authType,
+ ),
+ ),
+ );
+
+ const ed25519SendErrors = sendEd25519Results.filter(
+ (result) => result.success === false,
+ );
+ if (ed25519SendErrors.length > 0) {
+ console.warn(
+ "[attached] Some Ed25519 key shares failed to send:",
+ ed25519SendErrors.map((e) => e.err).join(", "),
+ );
+ } else {
+ console.log(
+ "[attached] Ed25519 key shares sent to KS nodes successfully",
+ );
+ }
+ } else {
+ console.warn(
+ "[attached] Ed25519 SSS split failed:",
+ splitEd25519Res.err,
+ );
+ }
+ } catch (sssErr) {
+ console.warn("[attached] Ed25519 SSS error:", sssErr);
+ }
+ } else {
+ console.warn(
+ "[attached] Ed25519 server keygen failed:",
+ serverData.msg,
+ );
+ }
+ } else {
+ const errorBody = await serverRes.text();
+ console.warn(
+ "[attached] Ed25519 server keygen request failed:",
+ serverRes.status,
+ errorBody,
+ );
+ }
+ } else {
+ console.warn("[attached] Ed25519 keygen failed:", ed25519KeygenRes.err);
+ }
+ } catch (err) {
+ // Log but don't fail sign-in if Ed25519 keygen fails
+ console.warn("[attached] Ed25519 keygen error:", err);
+ }
+
return {
success: true,
data: {
publicKey: reqKeygenRes.data.user.public_key,
walletId: reqKeygenRes.data.user.wallet_id,
- jwtToken: reqKeygenRes.data.token,
+ // Use updated JWT (with wallet_id_ed25519) if Ed25519 keygen succeeded
+ jwtToken: updatedJwtToken ?? reqKeygenRes.data.token,
keyshare_1: keygen_1.tss_private_share.toHex(),
isNewUser: true,
email: reqKeygenRes.data.user.email ?? null,
name: reqKeygenRes.data.user.name ?? null,
+ keyPackageEd25519,
},
};
}
@@ -380,6 +877,12 @@ export async function handleReshare(
};
}
+ const keyPackageEd25519 = await recoverEd25519KeyPackage(
+ idToken,
+ keyshareNodeMeta,
+ authType,
+ );
+
return {
success: true,
data: {
@@ -390,6 +893,7 @@ export async function handleReshare(
isNewUser: false,
email: signInResp.user.email ?? null,
name: signInResp.user.name ?? null,
+ keyPackageEd25519,
},
};
}
diff --git a/sandbox/sandbox_sol/next-env.d.ts b/sandbox/sandbox_sol/next-env.d.ts
index 830fb594c..c4b7818fb 100644
--- a/sandbox/sandbox_sol/next-env.d.ts
+++ b/sandbox/sandbox_sol/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-///
+import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/sandbox/sandbox_sol/next.config.ts b/sandbox/sandbox_sol/next.config.ts
index 0d9807765..55c198504 100644
--- a/sandbox/sandbox_sol/next.config.ts
+++ b/sandbox/sandbox_sol/next.config.ts
@@ -2,6 +2,7 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactStrictMode: true,
+ turbopack: {},
webpack: (config) => {
config.resolve.fallback = {
...config.resolve.fallback,
diff --git a/sandbox/sandbox_sol/package.json b/sandbox/sandbox_sol/package.json
index 21c059446..94efcf1f8 100644
--- a/sandbox/sandbox_sol/package.json
+++ b/sandbox/sandbox_sol/package.json
@@ -11,6 +11,9 @@
},
"dependencies": {
"@oko-wallet/oko-sdk-sol": "workspace:*",
+ "@solana/wallet-adapter-base": "^0.9.23",
+ "@solana/wallet-adapter-react": "^0.15.35",
+ "@solana/wallet-adapter-react-ui": "^0.9.35",
"@solana/web3.js": "^1.98.0",
"bs58": "^6.0.0",
"next": "^16.1.1",
@@ -19,9 +22,11 @@
"zustand": "^5.0.0"
},
"devDependencies": {
+ "@tailwindcss/postcss": "^4",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
+ "tailwindcss": "^4.1.3",
"typescript": "^5.8.3"
}
}
diff --git a/sandbox/sandbox_sol/postcss.config.mjs b/sandbox/sandbox_sol/postcss.config.mjs
new file mode 100644
index 000000000..c7bcb4b1e
--- /dev/null
+++ b/sandbox/sandbox_sol/postcss.config.mjs
@@ -0,0 +1,5 @@
+const config = {
+ plugins: ["@tailwindcss/postcss"],
+};
+
+export default config;
diff --git a/sandbox/sandbox_sol/public/icon.png b/sandbox/sandbox_sol/public/icon.png
new file mode 100644
index 000000000..e52eddf95
Binary files /dev/null and b/sandbox/sandbox_sol/public/icon.png differ
diff --git a/sandbox/sandbox_sol/public/logo.png b/sandbox/sandbox_sol/public/logo.png
new file mode 100644
index 000000000..0db0fc81f
Binary files /dev/null and b/sandbox/sandbox_sol/public/logo.png differ
diff --git a/sandbox/sandbox_sol/src/app/globals.css b/sandbox/sandbox_sol/src/app/globals.css
index eb17b04b7..e5c0d342f 100644
--- a/sandbox/sandbox_sol/src/app/globals.css
+++ b/sandbox/sandbox_sol/src/app/globals.css
@@ -1,38 +1,31 @@
-:root {
- --bg-primary: #0a0a0a;
- --bg-secondary: #141414;
- --bg-tertiary: #1a1a1a;
- --text-primary: #ffffff;
- --text-secondary: #a0a0a0;
- --border: #2a2a2a;
- --accent: #9945ff;
- --accent-hover: #7c35d9;
- --success: #14f195;
- --error: #ff4444;
-}
+@import "tailwindcss";
-* {
- box-sizing: border-box;
- padding: 0;
- margin: 0;
-}
-
-html,
-body {
- max-width: 100vw;
- overflow-x: hidden;
- font-family:
- -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
- Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+:root {
+ --background: #0a0a0a;
+ --foreground: #ededed;
}
-body {
- background: var(--bg-primary);
- color: var(--text-primary);
- min-height: 100vh;
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --font-sans: var(--font-geist-sans);
+ --font-mono: var(--font-geist-mono);
+ /* UI tokens */
+ --color-widget: #24262a;
+ --color-widget-border: #2f3136;
+ --color-widget-field: #1f2125;
+ --color-widget-border-hover: #3a3f45;
+ /* Solana brand colors */
+ --color-solana-purple: #9945ff;
+ --color-solana-green: #14f195;
}
-a {
- color: inherit;
- text-decoration: none;
+@layer base {
+ body {
+ background: var(--background);
+ color: var(--foreground);
+ font-family: var(--font-geist-sans), Arial, Helvetica, sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
}
diff --git a/sandbox/sandbox_sol/src/app/layout.tsx b/sandbox/sandbox_sol/src/app/layout.tsx
index de24b58b1..199ec2c67 100644
--- a/sandbox/sandbox_sol/src/app/layout.tsx
+++ b/sandbox/sandbox_sol/src/app/layout.tsx
@@ -1,9 +1,23 @@
import type { Metadata } from "next";
+import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
+const geistSans = Geist({
+ variable: "--font-geist-sans",
+ subsets: ["latin"],
+});
+
+const geistMono = Geist_Mono({
+ variable: "--font-geist-mono",
+ subsets: ["latin"],
+});
+
export const metadata: Metadata = {
title: "Sandbox SOL - Oko Wallet",
description: "Solana SDK testing sandbox for Oko Wallet",
+ icons: {
+ icon: "/icon.png",
+ },
};
export default function RootLayout({
@@ -13,7 +27,11 @@ export default function RootLayout({
}) {
return (
- {children}
+
+ {children}
+
);
}
diff --git a/sandbox/sandbox_sol/src/app/page.module.css b/sandbox/sandbox_sol/src/app/page.module.css
deleted file mode 100644
index 4af6fbe32..000000000
--- a/sandbox/sandbox_sol/src/app/page.module.css
+++ /dev/null
@@ -1,41 +0,0 @@
-.main {
- min-height: 100vh;
- padding: 2rem;
-}
-
-.container {
- max-width: 800px;
- margin: 0 auto;
-}
-
-.header {
- text-align: center;
- margin-bottom: 2rem;
-}
-
-.title {
- font-size: 2.5rem;
- font-weight: 700;
- background: linear-gradient(90deg, #9945ff, #14f195);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
- margin-bottom: 0.5rem;
-}
-
-.subtitle {
- color: var(--text-secondary);
- font-size: 1.125rem;
-}
-
-.content {
- display: flex;
- flex-direction: column;
- gap: 1.5rem;
-}
-
-.widgets {
- display: flex;
- flex-direction: column;
- gap: 1.5rem;
-}
diff --git a/sandbox/sandbox_sol/src/app/page.tsx b/sandbox/sandbox_sol/src/app/page.tsx
index b2326c025..06b4ed41c 100644
--- a/sandbox/sandbox_sol/src/app/page.tsx
+++ b/sandbox/sandbox_sol/src/app/page.tsx
@@ -1,10 +1,8 @@
"use client";
import { useOkoSol } from "@/hooks/use_oko_sol";
-import { WalletStatus } from "@/components/wallet_status";
-import { SignMessageWidget } from "@/components/sign_message_widget";
-import { SignTransactionWidget } from "@/components/sign_transaction_widget";
-import styles from "./page.module.css";
+import LoginView from "@/components/LoginView";
+import ConnectedView from "@/components/ConnectedView";
export default function Home() {
const {
@@ -12,38 +10,21 @@ export default function Home() {
isInitializing,
isConnected,
publicKey,
- error,
connect,
disconnect,
} = useOkoSol();
return (
-
-
-
-
-
-
-
- {isConnected && (
-
-
-
-
- )}
-
-
+
+ {!isConnected ? (
+
+ ) : (
+
+ )}
);
}
diff --git a/sandbox/sandbox_sol/src/app/wallet-adapter/page.tsx b/sandbox/sandbox_sol/src/app/wallet-adapter/page.tsx
new file mode 100644
index 000000000..f56970076
--- /dev/null
+++ b/sandbox/sandbox_sol/src/app/wallet-adapter/page.tsx
@@ -0,0 +1,120 @@
+"use client";
+
+import { useEffect } from "react";
+import { useWallet } from "@solana/wallet-adapter-react";
+import {
+ WalletMultiButton,
+ WalletDisconnectButton,
+} from "@solana/wallet-adapter-react-ui";
+import { WalletAdapterProvider } from "@/components/WalletAdapterProvider";
+import { useOkoSol } from "@/hooks/use_oko_sol";
+
+function WalletAdapterContent() {
+ const { isInitialized, isInitializing } = useOkoSol();
+ const { publicKey, connected, wallet, wallets } = useWallet();
+
+ useEffect(() => {
+ console.log("[wallet-adapter] Available wallets:", wallets);
+ }, [wallets]);
+
+ if (isInitializing) {
+ return (
+
+
Initializing Oko SDK...
+
+ );
+ }
+
+ if (!isInitialized) {
+ return (
+
+
Failed to initialize SDK
+
+ );
+ }
+
+ return (
+
+
+
Wallet Adapter Test
+
+ Testing wallet-standard integration with @solana/wallet-adapter
+
+
+
+
+
+ {connected && }
+
+
+
+
Status
+
+
+
+ Connected:
+
+ {connected ? "Yes" : "No"}
+
+
+
+
+ Wallet:
+ {wallet?.adapter.name ?? "None"}
+
+
+
+ Public Key:
+
+ {publicKey?.toBase58().slice(0, 20)}...
+
+
+
+
+
+
+
Discovered Wallets
+
+ {wallets.length === 0 ? (
+
No wallets discovered
+ ) : (
+ wallets.map((w) => (
+
+ {w.adapter.icon && (
+

+ )}
+
{w.adapter.name}
+
({w.readyState})
+
+ ))
+ )}
+
+
+
+
+
+ );
+}
+
+export default function WalletAdapterPage() {
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/sandbox/sandbox_sol/src/components/AccountInfo.tsx b/sandbox/sandbox_sol/src/components/AccountInfo.tsx
new file mode 100644
index 000000000..36fde5564
--- /dev/null
+++ b/sandbox/sandbox_sol/src/components/AccountInfo.tsx
@@ -0,0 +1,235 @@
+"use client";
+
+import { useState, useEffect, useCallback } from "react";
+import Link from "next/link";
+import { PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
+import Button from "./Button";
+import { DEVNET_CONNECTION } from "@/lib/connection";
+
+interface AccountInfoProps {
+ publicKey: string;
+ onDisconnect: () => void;
+ className?: string;
+}
+
+export default function AccountInfo({
+ publicKey,
+ onDisconnect,
+ className,
+}: AccountInfoProps) {
+ const [balance, setBalance] = useState(null);
+ const [isLoadingBalance, setIsLoadingBalance] = useState(false);
+
+ const fetchBalance = useCallback(async () => {
+ if (!publicKey) {
+ setBalance(null);
+ return;
+ }
+
+ setIsLoadingBalance(true);
+ try {
+ const pubkey = new PublicKey(publicKey);
+ const lamports = await DEVNET_CONNECTION.getBalance(pubkey);
+ setBalance(lamports / LAMPORTS_PER_SOL);
+ } catch (err) {
+ console.error("[sandbox_sol] Failed to fetch balance:", err);
+ setBalance(null);
+ } finally {
+ setIsLoadingBalance(false);
+ }
+ }, [publicKey]);
+
+ useEffect(() => {
+ fetchBalance();
+ }, [fetchBalance]);
+
+ function formatAddress(address: string) {
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
+ }
+
+ const balanceDisplay = isLoadingBalance
+ ? "..."
+ : balance !== null
+ ? `${balance.toFixed(4)} SOL`
+ : "—";
+
+ return (
+
+
+
Account
+
+
+
+
+
+ );
+}
+
+function FieldLabel({ children }: { children: React.ReactNode }) {
+ return (
+
+ );
+}
+
+function CopyableAddress({
+ value,
+ format,
+}: {
+ value?: string;
+ format?: (value: string) => string;
+}) {
+ const [copied, setCopied] = useState(false);
+
+ return (
+
+
Wallet Address
+
+
+ );
+}
+
+function AvailableBalance({
+ value,
+ onRefresh,
+ isLoading,
+}: {
+ value?: string;
+ onRefresh: () => void;
+ isLoading: boolean;
+}) {
+ return (
+
+
+
Available Balance
+
+
+
+
+ {value ?? "..."}
+
+
+ Get Solana devnet SOL
+
+
+
+
+ );
+}
diff --git a/sandbox/sandbox_sol/src/components/Button.tsx b/sandbox/sandbox_sol/src/components/Button.tsx
new file mode 100644
index 000000000..d169ccaca
--- /dev/null
+++ b/sandbox/sandbox_sol/src/components/Button.tsx
@@ -0,0 +1,57 @@
+"use client";
+
+import React from "react";
+
+type ButtonVariant = "primary" | "ghost";
+type ButtonSize = "sm" | "md" | "lg";
+
+interface ButtonProps extends React.ButtonHTMLAttributes {
+ variant?: ButtonVariant;
+ size?: ButtonSize;
+ fullWidth?: boolean;
+ loading?: boolean;
+}
+
+const base =
+ "inline-flex items-center justify-center gap-2 font-semibold rounded-2xl transition-all duration-200 shadow-lg hover:shadow-xl focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-700/60 disabled:opacity-60 disabled:cursor-not-allowed disabled:shadow-none";
+
+const variants: Record = {
+ primary: "bg-white text-black hover:bg-gray-100 active:scale-[0.99]",
+ ghost:
+ "bg-transparent text-gray-200 border border-gray-700/60 hover:bg-gray-800/50",
+};
+
+const sizes: Record = {
+ sm: "h-9 px-4 text-sm",
+ md: "h-12 px-6 text-base",
+ lg: "h-14 px-8 text-lg",
+};
+
+export default function Button({
+ variant = "primary",
+ size = "md",
+ fullWidth,
+ loading = false,
+ className = "",
+ ...props
+}: ButtonProps) {
+ const widthClass = fullWidth ? "w-full" : "";
+ return (
+
+ );
+}
diff --git a/sandbox/sandbox_sol/src/components/ConnectedView.tsx b/sandbox/sandbox_sol/src/components/ConnectedView.tsx
new file mode 100644
index 000000000..be4b595d1
--- /dev/null
+++ b/sandbox/sandbox_sol/src/components/ConnectedView.tsx
@@ -0,0 +1,35 @@
+"use client";
+
+import AccountInfo from "./AccountInfo";
+import { SignMessageWidget } from "./sign_message_widget";
+import { SignTransactionWidget } from "./sign_transaction_widget";
+import { SplTokenTransferWidget } from "./spl_token_transfer_widget";
+import { TestTransactionsWidget } from "./test_transactions_widget";
+import { SiwsWidget } from "./siws_widget";
+
+interface ConnectedViewProps {
+ publicKey: string;
+ onDisconnect: () => void;
+}
+
+export default function ConnectedView({
+ publicKey,
+ onDisconnect,
+}: ConnectedViewProps) {
+ return (
+
+ );
+}
diff --git a/sandbox/sandbox_sol/src/components/LoginView.tsx b/sandbox/sandbox_sol/src/components/LoginView.tsx
new file mode 100644
index 000000000..713b227e6
--- /dev/null
+++ b/sandbox/sandbox_sol/src/components/LoginView.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import Image from "next/image";
+import Button from "./Button";
+
+interface LoginViewProps {
+ isInitialized: boolean;
+ isInitializing: boolean;
+ onConnect: () => void;
+}
+
+export default function LoginView({
+ isInitialized,
+ isInitializing,
+ onConnect,
+}: LoginViewProps) {
+ return (
+
+ );
+}
diff --git a/sandbox/sandbox_sol/src/components/WalletAdapterProvider.tsx b/sandbox/sandbox_sol/src/components/WalletAdapterProvider.tsx
new file mode 100644
index 000000000..58b44f657
--- /dev/null
+++ b/sandbox/sandbox_sol/src/components/WalletAdapterProvider.tsx
@@ -0,0 +1,32 @@
+"use client";
+
+import { useMemo } from "react";
+import {
+ ConnectionProvider,
+ WalletProvider,
+} from "@solana/wallet-adapter-react";
+import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
+import { clusterApiUrl } from "@solana/web3.js";
+
+import "@solana/wallet-adapter-react-ui/styles.css";
+
+interface WalletAdapterProviderProps {
+ children: React.ReactNode;
+}
+
+export function WalletAdapterProvider({
+ children,
+}: WalletAdapterProviderProps) {
+ const endpoint = useMemo(() => clusterApiUrl("devnet"), []);
+
+ // No wallets array needed - wallet-standard wallets are auto-discovered
+ const wallets = useMemo(() => [], []);
+
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/sandbox/sandbox_sol/src/components/sign_message_widget.tsx b/sandbox/sandbox_sol/src/components/sign_message_widget.tsx
index a3df63108..76ebfc0d2 100644
--- a/sandbox/sandbox_sol/src/components/sign_message_widget.tsx
+++ b/sandbox/sandbox_sol/src/components/sign_message_widget.tsx
@@ -3,7 +3,7 @@
import { useState } from "react";
import { useSdkStore } from "@/store/sdk";
import bs58 from "bs58";
-import styles from "./widget.module.css";
+import Button from "./Button";
export function SignMessageWidget() {
const { okoSolWallet } = useSdkStore();
@@ -36,16 +36,20 @@ export function SignMessageWidget() {
};
return (
-
-
Sign Message
-
+
+
+ Sign Message
+
+
Sign an arbitrary message with your Solana wallet
-
-
+
+
-
+ Sign Message
+
- {error &&
{error}
}
+ {error && (
+
+ {error}
+
+ )}
{signature && (
-
-
-
{signature}
+
+
+
+ {signature}
+
)}
diff --git a/sandbox/sandbox_sol/src/components/sign_transaction_widget.tsx b/sandbox/sandbox_sol/src/components/sign_transaction_widget.tsx
index 7964e6454..c572b7d17 100644
--- a/sandbox/sandbox_sol/src/components/sign_transaction_widget.tsx
+++ b/sandbox/sandbox_sol/src/components/sign_transaction_widget.tsx
@@ -1,55 +1,29 @@
"use client";
-import { useState, useEffect, useCallback } from "react";
+import { useState } from "react";
+import Link from "next/link";
import {
- Connection,
PublicKey,
SystemProgram,
Transaction,
+ TransactionMessage,
+ VersionedTransaction,
LAMPORTS_PER_SOL,
} from "@solana/web3.js";
import bs58 from "bs58";
import { useSdkStore } from "@/store/sdk";
-import styles from "./widget.module.css";
-
-const DEVNET_CONNECTION = new Connection(
- "https://api.devnet.solana.com",
- "confirmed",
-);
+import Button from "./Button";
+import { DEVNET_CONNECTION } from "@/lib/connection";
export function SignTransactionWidget() {
const { okoSolWallet, publicKey } = useSdkStore();
const [recipient, setRecipient] = useState("");
const [amount, setAmount] = useState("0.001");
+ const [useVersioned, setUseVersioned] = useState(false);
const [signature, setSignature] = useState
(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
- const [balance, setBalance] = useState(null);
- const [isLoadingBalance, setIsLoadingBalance] = useState(false);
-
- const fetchBalance = useCallback(async () => {
- if (!publicKey) {
- setBalance(null);
- return;
- }
-
- setIsLoadingBalance(true);
- try {
- const pubkey = new PublicKey(publicKey);
- const lamports = await DEVNET_CONNECTION.getBalance(pubkey);
- setBalance(lamports / LAMPORTS_PER_SOL);
- } catch (err) {
- console.error("[sandbox_sol] Failed to fetch balance:", err);
- setBalance(null);
- } finally {
- setIsLoadingBalance(false);
- }
- }, [publicKey]);
-
- useEffect(() => {
- fetchBalance();
- }, [fetchBalance]);
const handleSignTransaction = async () => {
if (!okoSolWallet || !publicKey) {
@@ -76,31 +50,46 @@ export function SignTransactionWidget() {
const fromPubkey = new PublicKey(publicKey);
const { blockhash } = await DEVNET_CONNECTION.getLatestBlockhash();
- const transaction = new Transaction({
- recentBlockhash: blockhash,
- feePayer: fromPubkey,
- }).add(
- SystemProgram.transfer({
- fromPubkey,
- toPubkey: recipientPubkey,
- lamports,
- }),
- );
+ const instruction = SystemProgram.transfer({
+ fromPubkey,
+ toPubkey: recipientPubkey,
+ lamports,
+ });
+
+ if (useVersioned) {
+ const messageV0 = new TransactionMessage({
+ payerKey: fromPubkey,
+ recentBlockhash: blockhash,
+ instructions: [instruction],
+ }).compileToV0Message();
- const signedTx = await okoSolWallet.signTransaction(transaction);
- const txSignature = signedTx.signature;
+ const transaction = new VersionedTransaction(messageV0);
+ const signedTx = await okoSolWallet.signTransaction(transaction);
+ const txSignature = signedTx.signatures[0];
- if (txSignature) {
- const signatureBase58 = bs58.encode(txSignature);
- setSignature(signatureBase58);
- console.log("[sandbox_sol] Transaction signed:", signatureBase58);
+ if (txSignature) {
+ setSignature(bs58.encode(txSignature));
+ } else {
+ setSignature("Transaction signed (no signature extracted)");
+ }
} else {
- setSignature("Transaction signed (no signature extracted)");
+ const transaction = new Transaction({
+ recentBlockhash: blockhash,
+ feePayer: fromPubkey,
+ }).add(instruction);
+
+ const signedTx = await okoSolWallet.signTransaction(transaction);
+ const txSignature = signedTx.signature;
+
+ if (txSignature) {
+ setSignature(bs58.encode(txSignature));
+ } else {
+ setSignature("Transaction signed (no signature extracted)");
+ }
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
setError(errorMessage);
- console.error("[sandbox_sol] Failed to sign transaction:", errorMessage);
} finally {
setIsLoading(false);
}
@@ -131,16 +120,28 @@ export function SignTransactionWidget() {
const fromPubkey = new PublicKey(publicKey);
const { blockhash } = await DEVNET_CONNECTION.getLatestBlockhash();
- const transaction = new Transaction({
- recentBlockhash: blockhash,
- feePayer: fromPubkey,
- }).add(
- SystemProgram.transfer({
- fromPubkey,
- toPubkey: recipientPubkey,
- lamports,
- }),
- );
+ const instruction = SystemProgram.transfer({
+ fromPubkey,
+ toPubkey: recipientPubkey,
+ lamports,
+ });
+
+ let transaction: Transaction | VersionedTransaction;
+
+ if (useVersioned) {
+ const messageV0 = new TransactionMessage({
+ payerKey: fromPubkey,
+ recentBlockhash: blockhash,
+ instructions: [instruction],
+ }).compileToV0Message();
+
+ transaction = new VersionedTransaction(messageV0);
+ } else {
+ transaction = new Transaction({
+ recentBlockhash: blockhash,
+ feePayer: fromPubkey,
+ }).add(instruction);
+ }
const txSignature = await okoSolWallet.sendTransaction(
transaction,
@@ -148,100 +149,129 @@ export function SignTransactionWidget() {
);
setSignature(txSignature);
- console.log("[sandbox_sol] Transaction sent:", txSignature);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
setError(errorMessage);
- console.error("[sandbox_sol] Failed to send transaction:", errorMessage);
} finally {
setIsLoading(false);
}
};
- const balanceLabel = isLoadingBalance
- ? "Loading..."
- : balance !== null
- ? `${balance.toFixed(4)} SOL`
- : "—";
-
return (
-
-
Sign Transaction
-
+
+
+ Send Transaction
+
+
Create and sign a SOL transfer transaction (Devnet)
- {publicKey && (
-
-
Balance:
-
{balanceLabel}
-
+
+
+
+ setRecipient(e.target.value)}
+ placeholder="Enter Solana address..."
+ />
- )}
-
-
- setRecipient(e.target.value)}
- placeholder="Enter Solana address..."
- />
-
+
+
+ setAmount(e.target.value)}
+ placeholder="0.001"
+ step="0.001"
+ min="0"
+ />
+
-
-
- setAmount(e.target.value)}
- placeholder="0.001"
- step="0.001"
- min="0"
- />
-
+
-
-
-
+
+
+
+
- {error &&
{error}
}
+ {error && (
+
+ {error}
+
+ )}
{signature && (
-
-
-
{signature}
+
)}
diff --git a/sandbox/sandbox_sol/src/components/siws_widget.tsx b/sandbox/sandbox_sol/src/components/siws_widget.tsx
new file mode 100644
index 000000000..f871e810c
--- /dev/null
+++ b/sandbox/sandbox_sol/src/components/siws_widget.tsx
@@ -0,0 +1,265 @@
+"use client";
+
+import { useState } from "react";
+import { useSdkStore } from "@/store/sdk";
+import {
+ OkoStandardWallet,
+ buildSignInMessage,
+} from "@oko-wallet/oko-sdk-sol";
+import bs58 from "bs58";
+import Button from "./Button";
+
+export function SiwsWidget() {
+ const { okoSolWallet } = useSdkStore();
+
+ // SIWS input fields
+ const [domain, setDomain] = useState("");
+ const [statement, setStatement] = useState("Sign in to this application");
+ const [nonce, setNonce] = useState("");
+ const [uri, setUri] = useState("");
+
+ // Result state
+ const [result, setResult] = useState<{
+ signedMessage: string;
+ signature: string;
+ address: string;
+ } | null>(null);
+ const [previewMessage, setPreviewMessage] = useState
(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ // Generate random nonce
+ const generateNonce = () => {
+ const array = new Uint8Array(16);
+ crypto.getRandomValues(array);
+ setNonce(bs58.encode(array));
+ };
+
+ // Auto-fill domain and URI from current page
+ const autoFillFromPage = () => {
+ if (typeof window !== "undefined") {
+ setDomain(window.location.host);
+ setUri(window.location.href);
+ }
+ };
+
+ // Preview the SIWS message
+ const handlePreview = () => {
+ if (!okoSolWallet?.publicKey) {
+ setError("Wallet not connected");
+ return;
+ }
+
+ const address = okoSolWallet.publicKey.toBase58();
+ const message = buildSignInMessage(
+ {
+ domain: domain || undefined,
+ statement: statement || undefined,
+ uri: uri || undefined,
+ nonce: nonce || undefined,
+ issuedAt: new Date().toISOString(),
+ },
+ address,
+ );
+ setPreviewMessage(message);
+ };
+
+ // Execute SIWS
+ const handleSignIn = async () => {
+ if (!okoSolWallet) {
+ setError("SDK not initialized");
+ return;
+ }
+
+ setIsLoading(true);
+ setError(null);
+ setResult(null);
+
+ try {
+ // Create StandardWallet wrapper
+ const standardWallet = new OkoStandardWallet(okoSolWallet);
+
+ // Call solana:signIn feature
+ const [signInResult] = await standardWallet.features[
+ "solana:signIn"
+ ].signIn({
+ domain: domain || undefined,
+ statement: statement || undefined,
+ uri: uri || undefined,
+ nonce: nonce || undefined,
+ issuedAt: new Date().toISOString(),
+ });
+
+ setResult({
+ signedMessage: new TextDecoder().decode(signInResult.signedMessage),
+ signature: bs58.encode(signInResult.signature),
+ address: signInResult.account.address,
+ });
+
+ console.log("[sandbox_sol] SIWS success:", signInResult);
+ } catch (err) {
+ const errorMessage = err instanceof Error ? err.message : String(err);
+ setError(errorMessage);
+ console.error("[sandbox_sol] SIWS failed:", errorMessage);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+ Sign In With Solana (SIWS)
+
+
+ Authenticate using the wallet-standard solana:signIn feature
+
+
+ {/* Input Fields */}
+
+ {/* Domain */}
+
+
+
+
+
+
setDomain(e.target.value)}
+ placeholder="example.com"
+ />
+
+
+ {/* Statement */}
+
+
+
+
+ {/* URI */}
+
+
+ setUri(e.target.value)}
+ placeholder="https://example.com/login"
+ />
+
+
+ {/* Nonce */}
+
+
+
+
+
+
setNonce(e.target.value)}
+ placeholder="Random nonce for replay protection"
+ />
+
+
+
+ {/* Action Buttons */}
+
+
+
+
+
+ {/* Preview */}
+ {previewMessage && (
+
+
+
+ {previewMessage}
+
+
+ )}
+
+ {/* Error */}
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Result */}
+ {result && (
+
+
+
+ Sign In Successful
+
+
+ Address: {result.address}
+
+
+
+
+
+
+ {result.signedMessage}
+
+
+
+
+
+
+ {result.signature}
+
+
+
+ )}
+
+ );
+}
diff --git a/sandbox/sandbox_sol/src/components/spl_token_transfer_widget.tsx b/sandbox/sandbox_sol/src/components/spl_token_transfer_widget.tsx
new file mode 100644
index 000000000..aefd69985
--- /dev/null
+++ b/sandbox/sandbox_sol/src/components/spl_token_transfer_widget.tsx
@@ -0,0 +1,343 @@
+"use client";
+
+import { useState } from "react";
+import Link from "next/link";
+import {
+ PublicKey,
+ Transaction,
+ TransactionInstruction,
+} from "@solana/web3.js";
+import bs58 from "bs58";
+
+import { useSdkStore } from "@/store/sdk";
+import Button from "./Button";
+import { DEVNET_CONNECTION } from "@/lib/connection";
+
+// Token Program ID
+const TOKEN_PROGRAM_ID = new PublicKey(
+ "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
+);
+
+// Well-known devnet tokens for testing
+const DEVNET_TOKENS = [
+ {
+ name: "USDC (Devnet)",
+ mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
+ decimals: 6,
+ },
+];
+
+/**
+ * Creates a transferChecked instruction manually (without @solana/spl-token dependency)
+ */
+function createTransferCheckedInstruction(
+ source: PublicKey,
+ mint: PublicKey,
+ destination: PublicKey,
+ owner: PublicKey,
+ amount: bigint,
+ decimals: number,
+): TransactionInstruction {
+ // TransferChecked instruction discriminator = 12
+ const data = Buffer.alloc(10);
+ data.writeUInt8(12, 0); // instruction discriminator
+ data.writeBigUInt64LE(amount, 1); // amount
+ data.writeUInt8(decimals, 9); // decimals
+
+ return new TransactionInstruction({
+ keys: [
+ { pubkey: source, isSigner: false, isWritable: true },
+ { pubkey: mint, isSigner: false, isWritable: false },
+ { pubkey: destination, isSigner: false, isWritable: true },
+ { pubkey: owner, isSigner: true, isWritable: false },
+ ],
+ programId: TOKEN_PROGRAM_ID,
+ data,
+ });
+}
+
+/**
+ * Derives Associated Token Account address
+ */
+function getAssociatedTokenAddress(
+ mint: PublicKey,
+ owner: PublicKey,
+): PublicKey {
+ const ASSOCIATED_TOKEN_PROGRAM_ID = new PublicKey(
+ "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
+ );
+
+ const [address] = PublicKey.findProgramAddressSync(
+ [owner.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()],
+ ASSOCIATED_TOKEN_PROGRAM_ID,
+ );
+
+ return address;
+}
+
+export function SplTokenTransferWidget() {
+ const { okoSolWallet, publicKey } = useSdkStore();
+ const [selectedToken, setSelectedToken] = useState(DEVNET_TOKENS[0]);
+ const [recipient, setRecipient] = useState("");
+ const [amount, setAmount] = useState("1");
+ const [signature, setSignature] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const handleSignTransaction = async () => {
+ if (!okoSolWallet || !publicKey) return;
+
+ setIsLoading(true);
+ setError(null);
+ setSignature(null);
+
+ try {
+ let recipientPubkey: PublicKey;
+ try {
+ recipientPubkey = new PublicKey(recipient);
+ } catch {
+ throw new Error("Invalid recipient address");
+ }
+
+ const tokenAmount = BigInt(
+ Math.floor(parseFloat(amount) * Math.pow(10, selectedToken.decimals)),
+ );
+ if (tokenAmount <= BigInt(0)) {
+ throw new Error("Amount must be greater than 0");
+ }
+
+ const ownerPubkey = new PublicKey(publicKey);
+ const mintPubkey = new PublicKey(selectedToken.mint);
+
+ // Derive token accounts
+ const sourceAta = getAssociatedTokenAddress(mintPubkey, ownerPubkey);
+ const destinationAta = getAssociatedTokenAddress(
+ mintPubkey,
+ recipientPubkey,
+ );
+
+ const { blockhash } = await DEVNET_CONNECTION.getLatestBlockhash();
+
+ const transaction = new Transaction({
+ recentBlockhash: blockhash,
+ feePayer: ownerPubkey,
+ }).add(
+ createTransferCheckedInstruction(
+ sourceAta,
+ mintPubkey,
+ destinationAta,
+ ownerPubkey,
+ tokenAmount,
+ selectedToken.decimals,
+ ),
+ );
+
+ const signedTx = await okoSolWallet.signTransaction(transaction);
+ const txSignature = signedTx.signature;
+
+ if (txSignature) {
+ setSignature(bs58.encode(txSignature));
+ } else {
+ setSignature("Transaction signed (no signature extracted)");
+ }
+ } catch (err) {
+ const errorMessage = err instanceof Error ? err.message : String(err);
+ setError(errorMessage);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleSendTransaction = async () => {
+ if (!okoSolWallet || !publicKey) return;
+
+ setIsLoading(true);
+ setError(null);
+ setSignature(null);
+
+ try {
+ let recipientPubkey: PublicKey;
+ try {
+ recipientPubkey = new PublicKey(recipient);
+ } catch {
+ throw new Error("Invalid recipient address");
+ }
+
+ const tokenAmount = BigInt(
+ Math.floor(parseFloat(amount) * Math.pow(10, selectedToken.decimals)),
+ );
+ if (tokenAmount <= BigInt(0)) {
+ throw new Error("Amount must be greater than 0");
+ }
+
+ const ownerPubkey = new PublicKey(publicKey);
+ const mintPubkey = new PublicKey(selectedToken.mint);
+
+ const sourceAta = getAssociatedTokenAddress(mintPubkey, ownerPubkey);
+ const destinationAta = getAssociatedTokenAddress(
+ mintPubkey,
+ recipientPubkey,
+ );
+
+ const { blockhash } = await DEVNET_CONNECTION.getLatestBlockhash();
+
+ const transaction = new Transaction({
+ recentBlockhash: blockhash,
+ feePayer: ownerPubkey,
+ }).add(
+ createTransferCheckedInstruction(
+ sourceAta,
+ mintPubkey,
+ destinationAta,
+ ownerPubkey,
+ tokenAmount,
+ selectedToken.decimals,
+ ),
+ );
+
+ const txSignature = await okoSolWallet.sendTransaction(
+ transaction,
+ DEVNET_CONNECTION,
+ );
+
+ setSignature(txSignature);
+ } catch (err) {
+ const errorMessage = err instanceof Error ? err.message : String(err);
+ setError(errorMessage);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+ SPL Token Transfer
+
+
+ Test SPL token transferChecked instruction (Devnet)
+
+
+
+
+
+
+
+ Mint: {selectedToken.mint}
+
+
+
+
+
+ setRecipient(e.target.value)}
+ placeholder="Enter Solana address..."
+ />
+
+
+
+
+ setAmount(e.target.value)}
+ placeholder="1"
+ step="0.000001"
+ min="0"
+ />
+
+
+
+
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {signature && (
+
+
+
+ {signature}
+
+ {signature.length > 50 && (
+
+ View on Solana Explorer
+
+
+ )}
+
+ )}
+
+ );
+}
diff --git a/sandbox/sandbox_sol/src/components/test_transactions_widget.tsx b/sandbox/sandbox_sol/src/components/test_transactions_widget.tsx
new file mode 100644
index 000000000..13ed17077
--- /dev/null
+++ b/sandbox/sandbox_sol/src/components/test_transactions_widget.tsx
@@ -0,0 +1,236 @@
+"use client";
+
+import { useState } from "react";
+import {
+ PublicKey,
+ SystemProgram,
+ Transaction,
+ LAMPORTS_PER_SOL,
+ TransactionMessage,
+ VersionedTransaction,
+} from "@solana/web3.js";
+import bs58 from "bs58";
+
+import { useSdkStore } from "@/store/sdk";
+import Button from "./Button";
+import { DEVNET_CONNECTION } from "@/lib/connection";
+
+type TestTxType =
+ | "sol_transfer"
+ | "multi_sol_transfer"
+ | "create_account"
+ | "versioned_tx";
+
+interface TestTxOption {
+ id: TestTxType;
+ name: string;
+ description: string;
+}
+
+const TEST_TX_OPTIONS: TestTxOption[] = [
+ {
+ id: "sol_transfer",
+ name: "SOL Transfer",
+ description: "Simple SOL transfer using System Program",
+ },
+ {
+ id: "multi_sol_transfer",
+ name: "Multi SOL Transfer",
+ description: "3 SOL transfers in one transaction",
+ },
+ {
+ id: "create_account",
+ name: "Create Account",
+ description: "System Program createAccount instruction",
+ },
+ {
+ id: "versioned_tx",
+ name: "Versioned Transaction",
+ description: "SOL transfer using VersionedTransaction (V0)",
+ },
+];
+
+export function TestTransactionsWidget() {
+ const { okoSolWallet, publicKey } = useSdkStore();
+ const [selectedTx, setSelectedTx] = useState("sol_transfer");
+ const [signature, setSignature] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const createTestTransaction = async (): Promise<
+ Transaction | VersionedTransaction
+ > => {
+ if (!publicKey) throw new Error("No public key");
+
+ const fromPubkey = new PublicKey(publicKey);
+ const { blockhash } =
+ await DEVNET_CONNECTION.getLatestBlockhash();
+
+ const testRecipient = new PublicKey(
+ "11111111111111111111111111111112",
+ );
+
+ switch (selectedTx) {
+ case "sol_transfer": {
+ return new Transaction({
+ recentBlockhash: blockhash,
+ feePayer: fromPubkey,
+ }).add(
+ SystemProgram.transfer({
+ fromPubkey,
+ toPubkey: testRecipient,
+ lamports: 0.001 * LAMPORTS_PER_SOL,
+ }),
+ );
+ }
+
+ case "multi_sol_transfer": {
+ const tx = new Transaction({
+ recentBlockhash: blockhash,
+ feePayer: fromPubkey,
+ });
+
+ for (let i = 0; i < 3; i++) {
+ tx.add(
+ SystemProgram.transfer({
+ fromPubkey,
+ toPubkey: new PublicKey(
+ `1111111111111111111111111111111${i + 2}`,
+ ),
+ lamports: (0.0001 + i * 0.0001) * LAMPORTS_PER_SOL,
+ }),
+ );
+ }
+
+ return tx;
+ }
+
+ case "create_account": {
+ const newAccount = PublicKey.unique();
+
+ return new Transaction({
+ recentBlockhash: blockhash,
+ feePayer: fromPubkey,
+ }).add(
+ SystemProgram.createAccount({
+ fromPubkey,
+ newAccountPubkey: newAccount,
+ lamports: 0.001 * LAMPORTS_PER_SOL,
+ space: 0,
+ programId: SystemProgram.programId,
+ }),
+ );
+ }
+
+ case "versioned_tx": {
+ const message = new TransactionMessage({
+ payerKey: fromPubkey,
+ recentBlockhash: blockhash,
+ instructions: [
+ SystemProgram.transfer({
+ fromPubkey,
+ toPubkey: testRecipient,
+ lamports: 0.001 * LAMPORTS_PER_SOL,
+ }),
+ ],
+ }).compileToV0Message();
+
+ return new VersionedTransaction(message);
+ }
+
+ default:
+ throw new Error("Unknown transaction type");
+ }
+ };
+
+ const handleSignTransaction = async () => {
+ if (!okoSolWallet || !publicKey) return;
+
+ setIsLoading(true);
+ setError(null);
+ setSignature(null);
+
+ try {
+ const tx = await createTestTransaction();
+
+ const signedTx = await okoSolWallet.signTransaction(tx);
+
+ if (signedTx instanceof VersionedTransaction) {
+ const sig = signedTx.signatures[0];
+ setSignature(sig ? bs58.encode(sig) : "Signed (no signature)");
+ } else {
+ const sig = signedTx.signature;
+ setSignature(sig ? bs58.encode(sig) : "Signed (no signature)");
+ }
+ } catch (err) {
+ const errorMessage = err instanceof Error ? err.message : String(err);
+ setError(errorMessage);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+ Test Transactions
+
+
+ Test various transaction types to verify parser
+
+
+
+ {TEST_TX_OPTIONS.map((option) => (
+
+ ))}
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {signature && (
+
+
+
+ {signature}
+
+
+ )}
+
+ );
+}
diff --git a/sandbox/sandbox_sol/src/components/wallet_status.module.css b/sandbox/sandbox_sol/src/components/wallet_status.module.css
deleted file mode 100644
index 13ccf2aed..000000000
--- a/sandbox/sandbox_sol/src/components/wallet_status.module.css
+++ /dev/null
@@ -1,133 +0,0 @@
-.card {
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: 16px;
- padding: 1.5rem;
-}
-
-.header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 1rem;
-}
-
-.title {
- font-size: 1.25rem;
- font-weight: 600;
-}
-
-.status {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.375rem 0.75rem;
- border-radius: 20px;
- font-size: 0.875rem;
- font-weight: 500;
-}
-
-.status.pending {
- background: rgba(160, 160, 160, 0.1);
- color: var(--text-secondary);
-}
-
-.status.ready {
- background: rgba(153, 69, 255, 0.1);
- color: var(--accent);
-}
-
-.status.success {
- background: rgba(20, 241, 149, 0.1);
- color: var(--success);
-}
-
-.dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background: currentColor;
-}
-
-.error {
- background: rgba(255, 68, 68, 0.1);
- border: 1px solid rgba(255, 68, 68, 0.3);
- border-radius: 8px;
- padding: 0.75rem 1rem;
- color: var(--error);
- font-size: 0.875rem;
- margin-bottom: 1rem;
-}
-
-.info {
- margin-bottom: 1rem;
-}
-
-.label {
- display: block;
- color: var(--text-secondary);
- font-size: 0.75rem;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- margin-bottom: 0.375rem;
-}
-
-.publicKey {
- display: block;
- background: var(--bg-tertiary);
- padding: 0.75rem 1rem;
- border-radius: 8px;
- font-size: 0.875rem;
- word-break: break-all;
- color: var(--text-primary);
-}
-
-.actions {
- display: flex;
- gap: 0.75rem;
-}
-
-.connectButton {
- flex: 1;
- background: linear-gradient(90deg, #9945ff, #14f195);
- color: white;
- border: none;
- padding: 0.875rem 1.5rem;
- border-radius: 12px;
- cursor: pointer;
- font-size: 1rem;
- font-weight: 600;
- transition:
- opacity 0.2s,
- transform 0.1s;
-}
-
-.connectButton:hover:not(:disabled) {
- opacity: 0.9;
-}
-
-.connectButton:active:not(:disabled) {
- transform: scale(0.98);
-}
-
-.connectButton:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-.disconnectButton {
- flex: 1;
- background: var(--bg-tertiary);
- color: var(--text-primary);
- border: 1px solid var(--border);
- padding: 0.875rem 1.5rem;
- border-radius: 12px;
- cursor: pointer;
- font-size: 1rem;
- font-weight: 500;
- transition: background 0.2s;
-}
-
-.disconnectButton:hover {
- background: var(--border);
-}
diff --git a/sandbox/sandbox_sol/src/components/wallet_status.tsx b/sandbox/sandbox_sol/src/components/wallet_status.tsx
deleted file mode 100644
index d2bb1cbaf..000000000
--- a/sandbox/sandbox_sol/src/components/wallet_status.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-"use client";
-
-import styles from "./wallet_status.module.css";
-
-interface WalletStatusProps {
- isInitialized: boolean;
- isInitializing: boolean;
- isConnected: boolean;
- publicKey: string | null;
- error: string | null;
- onConnect: () => void;
- onDisconnect: () => void;
-}
-
-export function WalletStatus({
- isInitialized,
- isInitializing,
- isConnected,
- publicKey,
- error,
- onConnect,
- onDisconnect,
-}: WalletStatusProps) {
- const getStatus = () => {
- if (isInitializing)
- return { text: "Initializing...", className: styles.pending };
- if (!isInitialized)
- return { text: "Not Initialized", className: styles.pending };
- if (isConnected) return { text: "Connected", className: styles.success };
- return { text: "Ready", className: styles.ready };
- };
-
- const status = getStatus();
-
- return (
-
-
-
Wallet Status
-
-
- {status.text}
-
-
-
- {error &&
{error}
}
-
- {isConnected && publicKey && (
-
-
- {publicKey}
-
- )}
-
-
- {!isConnected ? (
-
- ) : (
-
- )}
-
-
- );
-}
diff --git a/sandbox/sandbox_sol/src/components/widget.module.css b/sandbox/sandbox_sol/src/components/widget.module.css
deleted file mode 100644
index a18dd19d3..000000000
--- a/sandbox/sandbox_sol/src/components/widget.module.css
+++ /dev/null
@@ -1,197 +0,0 @@
-.card {
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: 16px;
- padding: 1.5rem;
-}
-
-.title {
- font-size: 1.125rem;
- font-weight: 600;
- margin-bottom: 0.25rem;
-}
-
-.description {
- color: var(--text-secondary);
- font-size: 0.875rem;
- margin-bottom: 1.25rem;
-}
-
-.field {
- margin-bottom: 1rem;
-}
-
-.label {
- display: block;
- color: var(--text-secondary);
- font-size: 0.75rem;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- margin-bottom: 0.375rem;
-}
-
-.input {
- width: 100%;
- background: var(--bg-tertiary);
- border: 1px solid var(--border);
- border-radius: 8px;
- padding: 0.75rem 1rem;
- color: var(--text-primary);
- font-size: 0.9375rem;
- transition: border-color 0.2s;
-}
-
-.input:focus {
- outline: none;
- border-color: var(--accent);
-}
-
-.input::placeholder {
- color: var(--text-secondary);
- opacity: 0.6;
-}
-
-.textarea {
- width: 100%;
- background: var(--bg-tertiary);
- border: 1px solid var(--border);
- border-radius: 8px;
- padding: 0.75rem 1rem;
- color: var(--text-primary);
- font-size: 0.9375rem;
- font-family: inherit;
- resize: vertical;
- min-height: 80px;
- transition: border-color 0.2s;
-}
-
-.textarea:focus {
- outline: none;
- border-color: var(--accent);
-}
-
-.textarea::placeholder {
- color: var(--text-secondary);
- opacity: 0.6;
-}
-
-.button {
- background: var(--bg-tertiary);
- color: var(--text-primary);
- border: 1px solid var(--border);
- padding: 0.75rem 1.5rem;
- border-radius: 8px;
- cursor: pointer;
- font-size: 0.9375rem;
- font-weight: 500;
- transition: background 0.2s;
-}
-
-.button:hover:not(:disabled) {
- background: var(--border);
-}
-
-.button:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-.button.primary {
- background: var(--accent);
- border-color: var(--accent);
-}
-
-.button.primary:hover:not(:disabled) {
- background: var(--accent-hover);
-}
-
-.buttonGroup {
- display: flex;
- gap: 0.75rem;
-}
-
-.buttonGroup .button {
- flex: 1;
-}
-
-.error {
- margin-top: 1rem;
- background: rgba(255, 68, 68, 0.1);
- border: 1px solid rgba(255, 68, 68, 0.3);
- border-radius: 8px;
- padding: 0.75rem 1rem;
- color: var(--error);
- font-size: 0.875rem;
-}
-
-.result {
- margin-top: 1rem;
- padding-top: 1rem;
- border-top: 1px solid var(--border);
-}
-
-.code {
- display: block;
- background: var(--bg-tertiary);
- padding: 0.75rem 1rem;
- border-radius: 8px;
- font-size: 0.8125rem;
- word-break: break-all;
- color: var(--success);
- font-family: "SF Mono", "Monaco", "Inconsolata", "Fira Mono", monospace;
-}
-
-.link {
- display: inline-block;
- margin-top: 0.75rem;
- color: var(--accent);
- text-decoration: none;
- font-size: 0.875rem;
-}
-
-.link:hover {
- text-decoration: underline;
-}
-
-.balanceRow {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- background: var(--bg-tertiary);
- padding: 0.75rem 1rem;
- border-radius: 8px;
- margin-bottom: 1.25rem;
-}
-
-.balanceLabel {
- color: var(--text-secondary);
- font-size: 0.875rem;
-}
-
-.balanceValue {
- font-size: 1rem;
- font-weight: 600;
- color: var(--text-primary);
- flex: 1;
-}
-
-.refreshButton {
- background: transparent;
- border: 1px solid var(--border);
- border-radius: 6px;
- padding: 0.25rem 0.5rem;
- cursor: pointer;
- font-size: 1rem;
- color: var(--text-secondary);
- transition: all 0.2s;
-}
-
-.refreshButton:hover:not(:disabled) {
- background: var(--border);
- color: var(--text-primary);
-}
-
-.refreshButton:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
diff --git a/sandbox/sandbox_sol/src/hooks/use_oko_sol.ts b/sandbox/sandbox_sol/src/hooks/use_oko_sol.ts
index b7060b33b..291fca00d 100644
--- a/sandbox/sandbox_sol/src/hooks/use_oko_sol.ts
+++ b/sandbox/sandbox_sol/src/hooks/use_oko_sol.ts
@@ -1,12 +1,9 @@
"use client";
import { useEffect, useState, useCallback } from "react";
-import { OkoSolWallet } from "@oko-wallet/oko-sdk-sol";
+import { OkoSolWallet, registerOkoWallet } from "@oko-wallet/oko-sdk-sol";
import { useSdkStore } from "@/store/sdk";
-const OKO_SDK_ENDPOINT = "http://localhost:3201";
-const OKO_API_KEY = "test_api_key";
-
export function useOkoSol() {
const {
okoWallet,
@@ -34,8 +31,8 @@ export function useOkoSol() {
try {
// Initialize OkoSolWallet (internally initializes OkoWallet)
const solWalletResult = OkoSolWallet.init({
- api_key: OKO_API_KEY,
- sdk_endpoint: OKO_SDK_ENDPOINT,
+ api_key: process.env.NEXT_PUBLIC_OKO_API_KEY!,
+ sdk_endpoint: process.env.NEXT_PUBLIC_OKO_SDK_ENDPOINT,
});
if (!solWalletResult.success) {
@@ -54,11 +51,17 @@ export function useOkoSol() {
// Wait for initialization
await solWallet.waitUntilInitialized;
+ // Register with wallet-standard for dApp discovery
+ registerOkoWallet(solWallet);
+ console.log("[sandbox_sol] Oko wallet registered with wallet-standard");
+
setInitialized(true);
console.log("[sandbox_sol] SDK initialized");
- const existingPubkey = await solWallet.okoWallet.getPublicKey();
- if (existingPubkey) {
+ // Check for existing Ed25519 key (Solana uses Ed25519, not secp256k1)
+ const existingEd25519Pubkey =
+ await solWallet.okoWallet.getPublicKeyEd25519();
+ if (existingEd25519Pubkey) {
await solWallet.connect();
const pk = solWallet.publicKey?.toBase58() ?? null;
setConnected(true, pk);
@@ -92,11 +95,14 @@ export function useOkoSol() {
try {
setError(null);
+ // Check if user is signed in
const existingPubkey = await okoSolWallet.okoWallet.getPublicKey();
if (!existingPubkey) {
+ // Not signed in - trigger OAuth sign in
await okoSolWallet.okoWallet.signIn("google");
}
+ // connect() internally handles Ed25519 key creation if needed
await okoSolWallet.connect();
const pk = okoSolWallet.publicKey?.toBase58() ?? null;
setConnected(true, pk);
diff --git a/sandbox/sandbox_sol/src/lib/connection.ts b/sandbox/sandbox_sol/src/lib/connection.ts
new file mode 100644
index 000000000..1d3efd242
--- /dev/null
+++ b/sandbox/sandbox_sol/src/lib/connection.ts
@@ -0,0 +1,6 @@
+import { Connection } from "@solana/web3.js";
+
+export const DEVNET_CONNECTION = new Connection(
+ "https://api.devnet.solana.com",
+ "confirmed",
+);
diff --git a/sandbox/sandbox_sol/tsconfig.json b/sandbox/sandbox_sol/tsconfig.json
index c1334095f..b575f7dac 100644
--- a/sandbox/sandbox_sol/tsconfig.json
+++ b/sandbox/sandbox_sol/tsconfig.json
@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "ES2017",
- "lib": ["dom", "dom.iterable", "esnext"],
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -11,7 +15,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
- "jsx": "preserve",
+ "jsx": "react-jsx",
"incremental": true,
"plugins": [
{
@@ -19,9 +23,19 @@
}
],
"paths": {
- "@/*": ["./src/*"]
+ "@/*": [
+ "./src/*"
+ ]
}
},
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
- "exclude": ["node_modules"]
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
}
diff --git a/sdk/oko_sdk_core/src/methods/get_public_key_ed25519.ts b/sdk/oko_sdk_core/src/methods/get_public_key_ed25519.ts
new file mode 100644
index 000000000..6c8af7a13
--- /dev/null
+++ b/sdk/oko_sdk_core/src/methods/get_public_key_ed25519.ts
@@ -0,0 +1,20 @@
+import type { OkoWalletInterface } from "@oko-wallet-sdk-core/types";
+import { OKO_ATTACHED_TARGET } from "@oko-wallet-sdk-core/window_msg/target";
+
+export async function getPublicKeyEd25519(
+ this: OkoWalletInterface,
+): Promise {
+ await this.waitUntilInitialized;
+
+ const res = await this.sendMsgToIframe({
+ target: OKO_ATTACHED_TARGET,
+ msg_type: "get_public_key_ed25519",
+ payload: null,
+ });
+
+ if (res.msg_type === "get_public_key_ed25519_ack" && res.payload.success) {
+ return res.payload.data;
+ }
+
+ return null;
+}
diff --git a/sdk/oko_sdk_core/src/oko.ts b/sdk/oko_sdk_core/src/oko.ts
index 85b426dd1..72ae547e9 100644
--- a/sdk/oko_sdk_core/src/oko.ts
+++ b/sdk/oko_sdk_core/src/oko.ts
@@ -4,6 +4,7 @@ import { openModal } from "./methods/open_modal";
import { signIn } from "./methods/sign_in";
import { signOut } from "./methods/sign_out";
import { getPublicKey } from "./methods/get_public_key";
+import { getPublicKeyEd25519 } from "./methods/get_public_key_ed25519";
import { getEmail } from "./methods/get_email";
import { getName } from "./methods/get_name";
import { getWalletInfo } from "./methods/get_wallet_info";
@@ -25,6 +26,7 @@ ptype.sendMsgToIframe = sendMsgToIframe;
ptype.signIn = signIn;
ptype.signOut = signOut;
ptype.getPublicKey = getPublicKey;
+ptype.getPublicKeyEd25519 = getPublicKeyEd25519;
ptype.getEmail = getEmail;
ptype.getName = getName;
ptype.getAuthType = getAuthType;
diff --git a/sdk/oko_sdk_core/src/types/modal/make_sig/index.ts b/sdk/oko_sdk_core/src/types/modal/make_sig/index.ts
index 9ad687734..c76a15797 100644
--- a/sdk/oko_sdk_core/src/types/modal/make_sig/index.ts
+++ b/sdk/oko_sdk_core/src/types/modal/make_sig/index.ts
@@ -8,10 +8,16 @@ import type {
MakeEthSigError,
MakeEthSigModalResult,
} from "./eth";
+import type {
+ MakeSolanaSigData,
+ MakeSolSigError,
+ MakeSolSigModalResult,
+} from "./sol";
export * from "./common";
export * from "./cosmos";
export * from "./eth";
+export * from "./sol";
export type MakeSigModalPayload =
| {
@@ -23,6 +29,11 @@ export type MakeSigModalPayload =
modal_type: "eth/make_signature";
modal_id: string;
data: MakeEthereumSigData;
+ }
+ | {
+ modal_type: "sol/make_signature";
+ modal_id: string;
+ data: MakeSolanaSigData;
};
export type MakeSigModalApproveAckPayload =
@@ -37,6 +48,12 @@ export type MakeSigModalApproveAckPayload =
modal_id: string;
type: "approve";
data: MakeEthSigModalResult;
+ }
+ | {
+ modal_type: "sol/make_signature";
+ modal_id: string;
+ type: "approve";
+ data: MakeSolSigModalResult;
};
export type MakeSigModalRejectAckPayload =
@@ -49,6 +66,11 @@ export type MakeSigModalRejectAckPayload =
modal_type: "cosmos/make_signature";
modal_id: string;
type: "reject";
+ }
+ | {
+ modal_type: "sol/make_signature";
+ modal_id: string;
+ type: "reject";
};
export type MakeSigModalErrorAckPayload =
@@ -63,4 +85,10 @@ export type MakeSigModalErrorAckPayload =
modal_id: string;
type: "error";
error: MakeCosmosSigError;
+ }
+ | {
+ modal_type: "sol/make_signature";
+ modal_id: string;
+ type: "error";
+ error: MakeSolSigError;
};
diff --git a/sdk/oko_sdk_core/src/types/modal/make_sig/sol.ts b/sdk/oko_sdk_core/src/types/modal/make_sig/sol.ts
new file mode 100644
index 000000000..baa5d2ce2
--- /dev/null
+++ b/sdk/oko_sdk_core/src/types/modal/make_sig/sol.ts
@@ -0,0 +1,68 @@
+import type { MakeSigError } from "./common";
+
+export interface MakeSolSigModalResult {
+ chain_type: "sol";
+ sig_result: MakeSolanaSigResult;
+}
+
+export type MakeSolanaSigData =
+ | MakeSolTxSignData
+ | MakeSolAllTxSignData
+ | MakeSolMessageSignData;
+
+export interface MakeSolTxSignData {
+ chain_type: "sol";
+ sign_type: "tx";
+ payload: SolanaTxSignPayload;
+}
+
+export interface MakeSolAllTxSignData {
+ chain_type: "sol";
+ sign_type: "all_tx";
+ payload: SolanaAllTxSignPayload;
+}
+
+export interface MakeSolMessageSignData {
+ chain_type: "sol";
+ sign_type: "message";
+ payload: SolanaMessageSignPayload;
+}
+
+export type MakeSolanaSigResult =
+ | { type: "signature"; signature: string }
+ | { type: "signatures"; signatures: string[] };
+
+export interface SolanaTxSignPayload {
+ origin: string;
+ signer: string;
+ data: {
+ serialized_transaction: string;
+ message_to_sign: string;
+ is_versioned: boolean;
+ };
+}
+
+export interface SolanaAllTxSignPayload {
+ origin: string;
+ signer: string;
+ data: {
+ serialized_transactions: string[];
+ messages_to_sign: string[];
+ is_versioned: boolean;
+ };
+}
+
+export interface SolanaMessageSignPayload {
+ origin: string;
+ signer: string;
+ data: {
+ message: string;
+ };
+}
+
+export type MakeSolSigError =
+ | {
+ type: "unknown_error";
+ error: any;
+ }
+ | MakeSigError;
diff --git a/sdk/oko_sdk_core/src/types/msg/index.ts b/sdk/oko_sdk_core/src/types/msg/index.ts
index 6e3b4abba..8a6c7f5ce 100644
--- a/sdk/oko_sdk_core/src/types/msg/index.ts
+++ b/sdk/oko_sdk_core/src/types/msg/index.ts
@@ -26,6 +26,18 @@ export type OkoWalletMsgGetPublicKeyAck = {
payload: Result;
};
+export type OkoWalletMsgGetPublicKeyEd25519 = {
+ target: "oko_attached";
+ msg_type: "get_public_key_ed25519";
+ payload: null;
+};
+
+export type OkoWalletMsgGetPublicKeyEd25519Ack = {
+ target: "oko_sdk";
+ msg_type: "get_public_key_ed25519_ack";
+ payload: Result;
+};
+
export type OkoWalletMsgSetOAuthNonce = {
target: "oko_attached";
msg_type: "set_oauth_nonce";
@@ -222,6 +234,8 @@ export type OkoWalletMsg =
| OkoWalletMsgInitAck
| OkoWalletMsgGetPublicKey
| OkoWalletMsgGetPublicKeyAck
+ | OkoWalletMsgGetPublicKeyEd25519
+ | OkoWalletMsgGetPublicKeyEd25519Ack
| OkoWalletMsgSetOAuthNonce
| OkoWalletMsgSetOAuthNonceAck
| OkoWalletMsgSetCodeVerifier
diff --git a/sdk/oko_sdk_core/src/types/oko_wallet.ts b/sdk/oko_sdk_core/src/types/oko_wallet.ts
index a9c24a1e0..d8a0c7767 100644
--- a/sdk/oko_sdk_core/src/types/oko_wallet.ts
+++ b/sdk/oko_sdk_core/src/types/oko_wallet.ts
@@ -40,6 +40,7 @@ export interface OkoWalletInterface {
signIn: (type: SignInType) => Promise;
signOut: () => Promise;
getPublicKey: () => Promise;
+ getPublicKeyEd25519: () => Promise;
getEmail: () => Promise;
getName: () => Promise;
getWalletInfo: () => Promise;
diff --git a/sdk/oko_sdk_sol/jest.config.js b/sdk/oko_sdk_sol/jest.config.js
new file mode 100644
index 000000000..87bb0a88e
--- /dev/null
+++ b/sdk/oko_sdk_sol/jest.config.js
@@ -0,0 +1,20 @@
+/** @type {import('ts-jest').JestConfigWithTsJest} */
+export default {
+ preset: "ts-jest/presets/default-esm",
+ extensionsToTreatAsEsm: [".ts"],
+ transform: {
+ "^.+\\.ts$": [
+ "ts-jest",
+ {
+ useESM: true,
+ tsconfig: "tsconfig.json",
+ },
+ ],
+ },
+ testEnvironment: "node",
+ modulePathIgnorePatterns: ["/dist/"],
+ testTimeout: 30000,
+ moduleNameMapper: {
+ "^@oko-wallet-sdk-sol/(.*)$": "/src/$1",
+ },
+};
diff --git a/sdk/oko_sdk_sol/package.json b/sdk/oko_sdk_sol/package.json
index 25058a7fa..29f7b0229 100644
--- a/sdk/oko_sdk_sol/package.json
+++ b/sdk/oko_sdk_sol/package.json
@@ -34,13 +34,18 @@
"clean": "del-cli dist tsconfig.rollup.tsbuildinfo",
"rollup-build": "rollup -c --configPlugin typescript",
"build": "yarn clean && yarn rollup-build && tsc-alias",
- "test": "echo \"Error: no test specified\" && exit 1",
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest",
"prepublishOnly": "yarn build"
},
"dependencies": {
"@oko-wallet/oko-sdk-core": "^0.0.6-rc.130",
"@oko-wallet/stdlib-js": "^0.0.2-rc.42",
+ "@solana/wallet-standard-features": "^1.3.0",
"@solana/web3.js": "^1.98.0",
+ "@wallet-standard/base": "^1.1.0",
+ "@wallet-standard/features": "^1.1.0",
+ "@wallet-standard/wallet": "^1.1.0",
+ "bs58": "^6.0.0",
"eventemitter3": "^5.0.1",
"uuid": "^9.0.0"
},
@@ -48,11 +53,15 @@
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-node-resolve": "^15.0.0",
"@rollup/plugin-typescript": "^11.0.0",
+ "@types/jest": "^29.5.14",
"@types/node": "^24.10.1",
+ "@types/uuid": "^10.0.0",
"del-cli": "^6.0.0",
+ "jest": "^30.1.3",
"rollup": "^4.0.0",
"rollup-plugin-dts": "^6.2.1",
"rollup-plugin-tsconfig-paths": "^1.5.2",
+ "ts-jest": "^29.4.5",
"tsc-alias": "^1.8.16",
"typescript": "^5.8.3"
}
diff --git a/sdk/oko_sdk_sol/src/constructor.ts b/sdk/oko_sdk_sol/src/constructor.ts
index 482c433a5..c6b8a0c3a 100644
--- a/sdk/oko_sdk_sol/src/constructor.ts
+++ b/sdk/oko_sdk_sol/src/constructor.ts
@@ -1,13 +1,16 @@
import type { OkoWalletInterface } from "@oko-wallet/oko-sdk-core";
+import { PublicKey } from "@solana/web3.js";
import type {
OkoSolWalletInterface,
+ OkoSolWalletInternal,
OkoSolWalletStaticInterface,
} from "./types";
+import { SolWalletEventEmitter } from "./emitter";
import { lazyInit } from "./private/lazy_init";
export const OkoSolWallet = function (
- this: OkoSolWalletInterface,
+ this: OkoSolWalletInternal,
okoWallet: OkoWalletInterface,
) {
this.okoWallet = okoWallet;
@@ -18,5 +21,46 @@ export const OkoSolWallet = function (
this.publicKey = null;
this.connecting = false;
this.connected = false;
- this.waitUntilInitialized = lazyInit(this).then();
+
+ this._emitter = new SolWalletEventEmitter();
+ this.on = this._emitter.on.bind(this._emitter) as OkoSolWalletInterface["on"];
+ this.off = this._emitter.off.bind(
+ this._emitter,
+ ) as OkoSolWalletInterface["off"];
+
+ this._accountsChangedHandler = async (payload: {
+ publicKey: string | null;
+ }) => {
+ if (payload.publicKey === null) {
+ this.state.publicKey = null;
+ this.state.publicKeyRaw = null;
+ this.publicKey = null;
+ this.connected = false;
+ this._emitter.emit("accountChanged", null);
+ } else {
+ // Get Ed25519 key for Solana (not secp256k1)
+ try {
+ const ed25519Key = await this.okoWallet.getPublicKeyEd25519();
+ if (ed25519Key && ed25519Key !== this.state.publicKeyRaw) {
+ const publicKeyBytes = Buffer.from(ed25519Key, "hex");
+ const newPublicKey = new PublicKey(publicKeyBytes);
+
+ this.state.publicKey = newPublicKey;
+ this.state.publicKeyRaw = ed25519Key;
+ this.publicKey = newPublicKey;
+ this.connected = true;
+ this._emitter.emit("accountChanged", newPublicKey);
+ }
+ } catch (e) {
+ console.warn("[Sol SDK] Failed to get Ed25519 key on account change:", e);
+ }
+ }
+ };
+
+ okoWallet.on({
+ type: "CORE__accountsChanged",
+ handler: this._accountsChangedHandler,
+ });
+
+ this.waitUntilInitialized = lazyInit(this);
} as unknown as OkoSolWalletStaticInterface;
diff --git a/sdk/oko_sdk_sol/src/emitter.ts b/sdk/oko_sdk_sol/src/emitter.ts
new file mode 100644
index 000000000..02d7391ff
--- /dev/null
+++ b/sdk/oko_sdk_sol/src/emitter.ts
@@ -0,0 +1,48 @@
+import { EventEmitter } from "eventemitter3";
+
+import type {
+ SolWalletEvent,
+ SolWalletEventHandler,
+ SolWalletEventMap,
+} from "./types";
+
+export class SolWalletEventEmitter extends EventEmitter {
+ on(event: K, handler: SolWalletEventHandler): this {
+ return super.on(event, handler as (...args: unknown[]) => void);
+ }
+
+ once(
+ event: K,
+ handler: SolWalletEventHandler,
+ ): this {
+ return super.once(event, handler as (...args: unknown[]) => void);
+ }
+
+ off(
+ event: K,
+ handler: SolWalletEventHandler,
+ ): this {
+ return super.off(event, handler as (...args: unknown[]) => void);
+ }
+
+ emit(
+ event: K,
+ ...args: SolWalletEventMap[K] extends void ? [] : [SolWalletEventMap[K]]
+ ): boolean {
+ return super.emit(event, ...args);
+ }
+
+ addListener(
+ event: K,
+ handler: SolWalletEventHandler,
+ ): this {
+ return this.on(event, handler);
+ }
+
+ removeListener(
+ event: K,
+ handler: SolWalletEventHandler,
+ ): this {
+ return this.off(event, handler);
+ }
+}
diff --git a/sdk/oko_sdk_sol/src/index.ts b/sdk/oko_sdk_sol/src/index.ts
index af0981873..b67bf0dcd 100644
--- a/sdk/oko_sdk_sol/src/index.ts
+++ b/sdk/oko_sdk_sol/src/index.ts
@@ -1,4 +1,5 @@
export { OkoSolWallet } from "./sol_wallet";
+export { SolWalletEventEmitter } from "./emitter";
export type {
OkoSolWalletState,
@@ -13,8 +14,9 @@ export type {
SolSignTransactionResult,
SolSignAllTransactionsResult,
SolSignMessageResult,
- OkoSolWalletEvent,
- OkoSolWalletEventHandler,
+ SolWalletEvent,
+ SolWalletEventMap,
+ SolWalletEventHandler,
} from "./types";
export type {
@@ -22,3 +24,22 @@ export type {
LazyInitError,
OkoSolWalletError,
} from "./errors";
+
+// Wallet Standard
+export {
+ registerOkoWallet,
+ OkoStandardWallet,
+ OKO_WALLET_NAME,
+ OkoSolanaWalletAccount,
+ OKO_ACCOUNT_FEATURES,
+ SOLANA_CHAINS,
+ SOLANA_MAINNET_CHAIN,
+ SOLANA_DEVNET_CHAIN,
+ SOLANA_TESTNET_CHAIN,
+ isSolanaChain,
+ OKO_ICON,
+ buildSignInMessage,
+ createSignInFeature,
+} from "./wallet-standard";
+
+export type { SolanaChain } from "./wallet-standard";
diff --git a/sdk/oko_sdk_sol/src/methods/connect.ts b/sdk/oko_sdk_sol/src/methods/connect.ts
index 71cd6fa41..74602ea2f 100644
--- a/sdk/oko_sdk_sol/src/methods/connect.ts
+++ b/sdk/oko_sdk_sol/src/methods/connect.ts
@@ -1,6 +1,9 @@
import { PublicKey } from "@solana/web3.js";
-import type { OkoSolWalletInterface } from "@oko-wallet-sdk-sol/types";
+import type {
+ OkoSolWalletInterface,
+ OkoSolWalletInternal,
+} from "@oko-wallet-sdk-sol/types";
export async function connect(this: OkoSolWalletInterface): Promise {
if (this.connected) {
@@ -12,10 +15,11 @@ export async function connect(this: OkoSolWalletInterface): Promise {
try {
await this.waitUntilInitialized;
- const publicKeyHex = await this.okoWallet.getPublicKey();
+ // Solana uses Ed25519, not secp256k1
+ const publicKeyHex = await this.okoWallet.getPublicKeyEd25519();
if (!publicKeyHex) {
- throw new Error("Not signed in");
+ throw new Error("No Ed25519 key found. Please sign in first.");
}
const publicKeyBytes = Buffer.from(publicKeyHex, "hex");
@@ -32,6 +36,9 @@ export async function connect(this: OkoSolWalletInterface): Promise {
this.state.publicKeyRaw = publicKeyHex;
this.publicKey = publicKey;
this.connected = true;
+
+ // Emit connect event
+ (this as OkoSolWalletInternal)._emitter.emit("connect", publicKey);
} finally {
this.connecting = false;
}
diff --git a/sdk/oko_sdk_sol/src/methods/disconnect.ts b/sdk/oko_sdk_sol/src/methods/disconnect.ts
index ecc6f6970..d78acc05b 100644
--- a/sdk/oko_sdk_sol/src/methods/disconnect.ts
+++ b/sdk/oko_sdk_sol/src/methods/disconnect.ts
@@ -1,8 +1,26 @@
-import type { OkoSolWalletInterface } from "@oko-wallet-sdk-sol/types";
+import type {
+ OkoSolWalletInterface,
+ OkoSolWalletInternal,
+} from "@oko-wallet-sdk-sol/types";
export async function disconnect(this: OkoSolWalletInterface): Promise {
+ const internal = this as OkoSolWalletInternal;
+
+ // Remove event listener from core wallet
+ this.okoWallet.eventEmitter.off({
+ type: "CORE__accountsChanged",
+ handler: internal._accountsChangedHandler,
+ });
+
+ // Clear session in iframe (clears stored auth token and public key)
+ await this.okoWallet.signOut();
+
+ // Clear local state
this.state.publicKey = null;
this.state.publicKeyRaw = null;
this.publicKey = null;
this.connected = false;
+
+ // Emit disconnect event
+ internal._emitter.emit("disconnect");
}
diff --git a/sdk/oko_sdk_sol/src/methods/make_signature.ts b/sdk/oko_sdk_sol/src/methods/make_signature.ts
index 633cf3ac9..0e8506349 100644
--- a/sdk/oko_sdk_sol/src/methods/make_signature.ts
+++ b/sdk/oko_sdk_sol/src/methods/make_signature.ts
@@ -68,7 +68,7 @@ function createMakeSignatureData(
// Serialize just the message bytes for signing
// This is what ed25519 actually signs
const messageBytes = isVersioned
- ? (tx as any).message.serialize()
+ ? tx.message.serialize()
: tx.serializeMessage();
const messageBase64 = Buffer.from(messageBytes).toString("base64");
@@ -88,13 +88,30 @@ function createMakeSignatureData(
}
case "sign_all_transactions": {
+ if (params.transactions.length === 0) {
+ throw new Error("No transactions to sign");
+ }
+
+ const hasVersioned = params.transactions.some((tx) => "version" in tx);
+ const hasLegacy = params.transactions.some((tx) => !("version" in tx));
+
+ if (hasVersioned && hasLegacy) {
+ throw new Error("Cannot mix legacy and versioned transactions");
+ }
+
+ const isVersioned = hasVersioned;
+
const serializedTxs = params.transactions.map((tx) =>
Buffer.from(tx.serialize({ requireAllSignatures: false })).toString(
"base64",
),
);
- const isVersioned =
- params.transactions.length > 0 && "version" in params.transactions[0];
+
+ const messagesToSign = params.transactions.map((tx) => {
+ const messageBytes =
+ "version" in tx ? tx.message.serialize() : tx.serializeMessage();
+ return Buffer.from(messageBytes).toString("base64");
+ });
return {
chain_type: "sol",
@@ -104,6 +121,7 @@ function createMakeSignatureData(
signer,
data: {
serialized_transactions: serializedTxs,
+ messages_to_sign: messagesToSign,
is_versioned: isVersioned,
},
},
@@ -123,13 +141,6 @@ function createMakeSignatureData(
},
};
}
-
- default: {
- throw new SolanaRpcError(
- SolanaRpcErrorCode.Internal,
- `Unknown sign method: ${(params as any).type}`,
- );
- }
}
}
@@ -195,9 +206,7 @@ async function handleSigningFlow(
}
default: {
- throw new Error(
- `unreachable response type: ${(openModalResp as any).type}`,
- );
+ throw new Error("unreachable response type");
}
}
} catch (error) {
@@ -298,12 +307,5 @@ function convertSigResultToOutput(
signature: signatureBytes,
};
}
-
- default: {
- throw new SolanaRpcError(
- SolanaRpcErrorCode.Internal,
- `Unknown params type`,
- );
- }
}
}
diff --git a/sdk/oko_sdk_sol/src/methods/sign_and_send_all_transactions.ts b/sdk/oko_sdk_sol/src/methods/sign_and_send_all_transactions.ts
new file mode 100644
index 000000000..c19634b33
--- /dev/null
+++ b/sdk/oko_sdk_sol/src/methods/sign_and_send_all_transactions.ts
@@ -0,0 +1,39 @@
+import type {
+ Connection,
+ SendOptions,
+ Transaction,
+ TransactionSignature,
+ VersionedTransaction,
+} from "@solana/web3.js";
+
+import type { OkoSolWalletInterface } from "@oko-wallet-sdk-sol/types";
+import { signAllTransactions } from "./sign_all_transactions";
+import { SolanaRpcError, SolanaRpcErrorCode } from "./make_signature";
+
+export async function signAndSendAllTransactions(
+ this: OkoSolWalletInterface,
+ transactions: (Transaction | VersionedTransaction)[],
+ connection: Connection,
+ options?: SendOptions,
+): Promise<{ signatures: TransactionSignature[] }> {
+ if (!this.connected || !this.publicKey) {
+ throw new SolanaRpcError(
+ SolanaRpcErrorCode.Internal,
+ "Wallet not connected",
+ );
+ }
+
+ const signedTransactions = await signAllTransactions.call(this, transactions);
+
+ const signatures: TransactionSignature[] = [];
+
+ for (const signedTx of signedTransactions) {
+ const signature = await connection.sendRawTransaction(
+ signedTx.serialize(),
+ options,
+ );
+ signatures.push(signature);
+ }
+
+ return { signatures };
+}
diff --git a/sdk/oko_sdk_sol/src/methods/sign_and_send_transaction.ts b/sdk/oko_sdk_sol/src/methods/sign_and_send_transaction.ts
new file mode 100644
index 000000000..769fd5f78
--- /dev/null
+++ b/sdk/oko_sdk_sol/src/methods/sign_and_send_transaction.ts
@@ -0,0 +1,34 @@
+import type {
+ Connection,
+ SendOptions,
+ Transaction,
+ TransactionSignature,
+ VersionedTransaction,
+} from "@solana/web3.js";
+
+import type { OkoSolWalletInterface } from "@oko-wallet-sdk-sol/types";
+import { signTransaction } from "./sign_transaction";
+import { SolanaRpcError, SolanaRpcErrorCode } from "./make_signature";
+
+export async function signAndSendTransaction(
+ this: OkoSolWalletInterface,
+ transaction: Transaction | VersionedTransaction,
+ connection: Connection,
+ options?: SendOptions,
+): Promise<{ signature: TransactionSignature }> {
+ if (!this.connected || !this.publicKey) {
+ throw new SolanaRpcError(
+ SolanaRpcErrorCode.Internal,
+ "Wallet not connected",
+ );
+ }
+
+ const signedTransaction = await signTransaction.call(this, transaction);
+
+ const signature = await connection.sendRawTransaction(
+ signedTransaction.serialize(),
+ options,
+ );
+
+ return { signature };
+}
diff --git a/sdk/oko_sdk_sol/src/private/lazy_init.ts b/sdk/oko_sdk_sol/src/private/lazy_init.ts
index 50c6cf402..0ea72fcdf 100644
--- a/sdk/oko_sdk_sol/src/private/lazy_init.ts
+++ b/sdk/oko_sdk_sol/src/private/lazy_init.ts
@@ -1,30 +1,46 @@
import type { Result } from "@oko-wallet/stdlib-js";
+import { PublicKey } from "@solana/web3.js";
import type {
- OkoSolWalletInterface,
+ OkoSolWalletInternal,
OkoSolWalletState,
} from "@oko-wallet-sdk-sol/types";
import type { LazyInitError } from "@oko-wallet-sdk-sol/errors";
export async function lazyInit(
- wallet: OkoSolWalletInterface,
+ wallet: OkoSolWalletInternal,
): Promise> {
- try {
- await wallet.okoWallet.waitUntilInitialized;
-
- // TODO
+ const coreStateRes = await wallet.okoWallet.waitUntilInitialized;
- return {
- success: true,
- data: wallet.state,
- };
- } catch (e) {
+ if (!coreStateRes.success) {
return {
success: false,
err: {
type: "lazy_init_error",
- msg: e instanceof Error ? e.message : String(e),
+ msg: "Core wallet initialization failed",
},
};
}
+
+ // Set initial state from core wallet if user is already logged in
+ // Use Ed25519 key for Solana (not secp256k1)
+ try {
+ const ed25519Key = await wallet.okoWallet.getPublicKeyEd25519();
+ if (ed25519Key) {
+ const publicKeyBytes = Buffer.from(ed25519Key, "hex");
+ const newPublicKey = new PublicKey(publicKeyBytes);
+
+ wallet.state.publicKey = newPublicKey;
+ wallet.state.publicKeyRaw = ed25519Key;
+ wallet.publicKey = newPublicKey;
+ wallet.connected = true;
+ }
+ } catch (e) {
+ console.warn("[Sol SDK] Failed to get Ed25519 key during init:", e);
+ }
+
+ return {
+ success: true,
+ data: wallet.state,
+ };
}
diff --git a/sdk/oko_sdk_sol/src/sol_wallet.ts b/sdk/oko_sdk_sol/src/sol_wallet.ts
index 0c16338f2..15f9aa4df 100644
--- a/sdk/oko_sdk_sol/src/sol_wallet.ts
+++ b/sdk/oko_sdk_sol/src/sol_wallet.ts
@@ -7,6 +7,8 @@ import { signTransaction } from "./methods/sign_transaction";
import { signAllTransactions } from "./methods/sign_all_transactions";
import { signMessage } from "./methods/sign_message";
import { sendTransaction } from "./methods/send_transaction";
+import { signAndSendTransaction } from "./methods/sign_and_send_transaction";
+import { signAndSendAllTransactions } from "./methods/sign_and_send_all_transactions";
OkoSolWallet.init = init;
@@ -18,5 +20,7 @@ ptype.signTransaction = signTransaction;
ptype.signAllTransactions = signAllTransactions;
ptype.signMessage = signMessage;
ptype.sendTransaction = sendTransaction;
+ptype.signAndSendTransaction = signAndSendTransaction;
+ptype.signAndSendAllTransactions = signAndSendAllTransactions;
export { OkoSolWallet };
diff --git a/sdk/oko_sdk_sol/src/tests/base.test.ts b/sdk/oko_sdk_sol/src/tests/base.test.ts
new file mode 100644
index 000000000..bcf33e479
--- /dev/null
+++ b/sdk/oko_sdk_sol/src/tests/base.test.ts
@@ -0,0 +1,125 @@
+import { PublicKey } from "@solana/web3.js";
+
+import { OkoSolWallet } from "@oko-wallet-sdk-sol/sol_wallet";
+import type { OkoSolWalletInterface } from "@oko-wallet-sdk-sol/types";
+import {
+ SolanaRpcError,
+ SolanaRpcErrorCode,
+} from "@oko-wallet-sdk-sol/methods/make_signature";
+import {
+ createMockOkoWallet,
+ createMockOkoWalletWithNoAccount,
+ MOCK_ED25519_PUBLIC_KEY,
+} from "./mock";
+
+describe("OkoSolWallet - Base Operations", () => {
+ describe("Constructor", () => {
+ it("should create wallet instance with initial state", () => {
+ const mockOkoWallet = createMockOkoWallet();
+ const wallet = new (OkoSolWallet as any)(
+ mockOkoWallet,
+ ) as OkoSolWalletInterface;
+
+ expect(wallet.okoWallet).toBe(mockOkoWallet);
+ expect(wallet.state.publicKey).toBeNull();
+ expect(wallet.state.publicKeyRaw).toBeNull();
+ expect(wallet.publicKey).toBeNull();
+ expect(wallet.connecting).toBe(false);
+ expect(wallet.connected).toBe(false);
+ });
+ });
+
+ describe("connect", () => {
+ it("should connect and set public key", async () => {
+ const mockOkoWallet = createMockOkoWallet();
+ const wallet = new (OkoSolWallet as any)(
+ mockOkoWallet,
+ ) as OkoSolWalletInterface;
+
+ await wallet.connect();
+
+ expect(wallet.connected).toBe(true);
+ expect(wallet.connecting).toBe(false);
+ expect(wallet.publicKey).toBeInstanceOf(PublicKey);
+ expect(wallet.state.publicKey).toBeInstanceOf(PublicKey);
+ expect(wallet.state.publicKeyRaw).toBe(MOCK_ED25519_PUBLIC_KEY);
+ });
+
+ it("should not reconnect if already connected", async () => {
+ const mockOkoWallet = createMockOkoWallet();
+ const wallet = new (OkoSolWallet as any)(
+ mockOkoWallet,
+ ) as OkoSolWalletInterface;
+
+ await wallet.connect();
+ const firstPublicKey = wallet.publicKey;
+
+ await wallet.connect();
+
+ expect(wallet.publicKey).toBe(firstPublicKey);
+ });
+
+ it("should throw error if no Ed25519 key found", async () => {
+ const mockOkoWallet = createMockOkoWalletWithNoAccount();
+ const wallet = new (OkoSolWallet as any)(
+ mockOkoWallet,
+ ) as OkoSolWalletInterface;
+
+ await expect(wallet.connect()).rejects.toThrow(
+ "No Ed25519 key found. Please sign in first.",
+ );
+ expect(wallet.connected).toBe(false);
+ expect(wallet.connecting).toBe(false);
+ });
+ });
+
+ describe("disconnect", () => {
+ it("should disconnect and clear state", async () => {
+ const mockOkoWallet = createMockOkoWallet();
+ const wallet = new (OkoSolWallet as any)(
+ mockOkoWallet,
+ ) as OkoSolWalletInterface;
+
+ await wallet.connect();
+ expect(wallet.connected).toBe(true);
+
+ await wallet.disconnect();
+
+ expect(wallet.connected).toBe(false);
+ expect(wallet.publicKey).toBeNull();
+ expect(wallet.state.publicKey).toBeNull();
+ expect(wallet.state.publicKeyRaw).toBeNull();
+ });
+ });
+
+ describe("signMessage", () => {
+ it("should throw error if not connected", async () => {
+ const mockOkoWallet = createMockOkoWallet();
+ const wallet = new (OkoSolWallet as any)(
+ mockOkoWallet,
+ ) as OkoSolWalletInterface;
+
+ const message = new TextEncoder().encode("Hello, Solana!");
+
+ await expect(wallet.signMessage(message)).rejects.toMatchObject({
+ code: SolanaRpcErrorCode.Internal,
+ message: "Wallet not connected",
+ });
+ });
+
+ it("should sign message when connected", async () => {
+ const mockOkoWallet = createMockOkoWallet();
+ const wallet = new (OkoSolWallet as any)(
+ mockOkoWallet,
+ ) as OkoSolWalletInterface;
+
+ await wallet.connect();
+
+ const message = new TextEncoder().encode("Hello, Solana!");
+ const signature = await wallet.signMessage(message);
+
+ expect(signature).toBeInstanceOf(Uint8Array);
+ expect(signature.length).toBe(64); // Ed25519 signature is 64 bytes
+ });
+ });
+});
diff --git a/sdk/oko_sdk_sol/src/tests/errors.test.ts b/sdk/oko_sdk_sol/src/tests/errors.test.ts
new file mode 100644
index 000000000..9587476c5
--- /dev/null
+++ b/sdk/oko_sdk_sol/src/tests/errors.test.ts
@@ -0,0 +1,34 @@
+import {
+ SolanaRpcError,
+ SolanaRpcErrorCode,
+} from "@oko-wallet-sdk-sol/methods/make_signature";
+
+describe("SolanaRpcError", () => {
+ it("should create error with code and message", () => {
+ const error = new SolanaRpcError(
+ SolanaRpcErrorCode.Internal,
+ "Internal error",
+ );
+
+ expect(error).toBeInstanceOf(Error);
+ expect(error).toBeInstanceOf(SolanaRpcError);
+ expect(error.code).toBe(SolanaRpcErrorCode.Internal);
+ expect(error.message).toBe("Internal error");
+ expect(error.name).toBe("SolanaRpcError");
+ });
+
+ it("should create user rejected error", () => {
+ const error = new SolanaRpcError(
+ SolanaRpcErrorCode.UserRejectedRequest,
+ "User rejected the request",
+ );
+
+ expect(error.code).toBe(4001);
+ expect(error.message).toBe("User rejected the request");
+ });
+
+ it("should have correct error codes", () => {
+ expect(SolanaRpcErrorCode.UserRejectedRequest).toBe(4001);
+ expect(SolanaRpcErrorCode.Internal).toBe(-32603);
+ });
+});
diff --git a/sdk/oko_sdk_sol/src/tests/mock/index.ts b/sdk/oko_sdk_sol/src/tests/mock/index.ts
new file mode 100644
index 000000000..831453165
--- /dev/null
+++ b/sdk/oko_sdk_sol/src/tests/mock/index.ts
@@ -0,0 +1,167 @@
+import type {
+ OkoWalletInterface,
+ OpenModalAckPayload,
+} from "@oko-wallet/oko-sdk-core";
+import type { Result } from "@oko-wallet/stdlib-js";
+import type { OpenModalError } from "@oko-wallet/oko-sdk-core";
+import { EventEmitter } from "eventemitter3";
+
+// Mock Ed25519 public key (32 bytes in hex)
+export const MOCK_ED25519_PUBLIC_KEY =
+ "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f";
+
+// Mock signature (64 bytes in hex)
+export const MOCK_SIGNATURE =
+ "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";
+
+export interface MockOkoWalletConfig {
+ publicKeyEd25519?: string | null;
+ publicKey?: string | null;
+ shouldRejectSignature?: boolean;
+ signatureResponse?: string;
+ signaturesResponse?: string[];
+}
+
+export function createMockOkoWallet(
+ config: MockOkoWalletConfig = {},
+): OkoWalletInterface {
+ const {
+ publicKeyEd25519 = MOCK_ED25519_PUBLIC_KEY,
+ publicKey = null,
+ shouldRejectSignature = false,
+ signatureResponse = MOCK_SIGNATURE,
+ signaturesResponse = [MOCK_SIGNATURE],
+ } = config;
+
+ const eventEmitter = new EventEmitter();
+
+ const mockWallet: OkoWalletInterface = {
+ state: {
+ authType: publicKeyEd25519 ? "google" : null,
+ email: publicKeyEd25519 ? "test@example.com" : null,
+ publicKey: publicKey,
+ name: publicKeyEd25519 ? "Test User" : null,
+ },
+ apiKey: "test-api-key",
+ iframe: {} as HTMLIFrameElement,
+ activePopupId: null,
+ activePopupWindow: null,
+ sdkEndpoint: "https://test.oko.wallet",
+ eventEmitter: eventEmitter as any,
+ origin: "https://test-dapp.com",
+ waitUntilInitialized: Promise.resolve({
+ success: true,
+ data: {
+ authType: publicKeyEd25519 ? "google" : null,
+ email: publicKeyEd25519 ? "test@example.com" : null,
+ publicKey: publicKey,
+ name: publicKeyEd25519 ? "Test User" : null,
+ },
+ } as Result),
+
+ openModal: async (
+ msg,
+ ): Promise> => {
+ if (shouldRejectSignature) {
+ return {
+ success: true,
+ data: {
+ modal_type: "sol/make_signature",
+ type: "reject",
+ } as OpenModalAckPayload,
+ };
+ }
+
+ // Determine response based on sign_type
+ const signType = (msg.payload.data as any)?.sign_type;
+
+ if (signType === "all_tx") {
+ return {
+ success: true,
+ data: {
+ modal_type: "sol/make_signature",
+ type: "approve",
+ data: {
+ chain_type: "sol",
+ sig_result: {
+ type: "signatures",
+ signatures: signaturesResponse,
+ },
+ },
+ } as OpenModalAckPayload,
+ };
+ }
+
+ return {
+ success: true,
+ data: {
+ modal_type: "sol/make_signature",
+ type: "approve",
+ data: {
+ chain_type: "sol",
+ sig_result: {
+ type: "signature",
+ signature: signatureResponse,
+ },
+ },
+ } as OpenModalAckPayload,
+ };
+ },
+
+ closeModal: () => {},
+
+ sendMsgToIframe: async (msg) => msg,
+
+ signIn: async () => {},
+
+ signOut: async () => {
+ mockWallet.state.authType = null;
+ mockWallet.state.email = null;
+ mockWallet.state.publicKey = null;
+ mockWallet.state.name = null;
+ },
+
+ getPublicKey: async () => publicKey,
+
+ getPublicKeyEd25519: async () => publicKeyEd25519,
+
+ getEmail: async () => (publicKeyEd25519 ? "test@example.com" : null),
+
+ getName: async () => (publicKeyEd25519 ? "Test User" : null),
+
+ getAuthType: async () => (publicKeyEd25519 ? "google" : null),
+
+ getWalletInfo: async () =>
+ publicKeyEd25519
+ ? {
+ authType: "google" as const,
+ publicKey: publicKey ?? "",
+ email: "test@example.com",
+ name: "Test User",
+ }
+ : null,
+
+ startEmailSignIn: async () => {},
+
+ completeEmailSignIn: async () => {},
+
+ on: (handlerDef) => {
+ // No-op for tests
+ },
+ };
+
+ return mockWallet;
+}
+
+export function createMockOkoWalletWithNoAccount(): OkoWalletInterface {
+ return createMockOkoWallet({
+ publicKeyEd25519: null,
+ publicKey: null,
+ });
+}
+
+export function createMockOkoWalletThatRejects(): OkoWalletInterface {
+ return createMockOkoWallet({
+ shouldRejectSignature: true,
+ });
+}
diff --git a/sdk/oko_sdk_sol/src/tests/wallet-standard.test.ts b/sdk/oko_sdk_sol/src/tests/wallet-standard.test.ts
new file mode 100644
index 000000000..448743ab8
--- /dev/null
+++ b/sdk/oko_sdk_sol/src/tests/wallet-standard.test.ts
@@ -0,0 +1,314 @@
+import { jest } from "@jest/globals";
+
+import { OkoSolWallet } from "@oko-wallet-sdk-sol/sol_wallet";
+import type { OkoSolWalletInterface } from "@oko-wallet-sdk-sol/types";
+import {
+ OkoStandardWallet,
+ OkoSolanaWalletAccount,
+ OKO_WALLET_NAME,
+ OKO_ACCOUNT_FEATURES,
+ SOLANA_CHAINS,
+ SOLANA_MAINNET_CHAIN,
+ SOLANA_DEVNET_CHAIN,
+ SOLANA_TESTNET_CHAIN,
+ isSolanaChain,
+ OKO_ICON,
+ buildSignInMessage,
+} from "@oko-wallet-sdk-sol/wallet-standard";
+import { createMockOkoWallet } from "./mock";
+
+describe("Wallet Standard", () => {
+ describe("OkoSolanaWalletAccount", () => {
+ it("should create account with correct properties", () => {
+ const address = "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f";
+ const publicKey = new Uint8Array(32).fill(0x7f);
+
+ const account = new OkoSolanaWalletAccount(address, publicKey);
+
+ expect(account.address).toBe(address);
+ expect(account.publicKey).toEqual(publicKey);
+ expect(account.chains).toEqual(SOLANA_CHAINS);
+ expect(account.features).toEqual(OKO_ACCOUNT_FEATURES);
+ });
+ });
+
+ describe("Chain utilities", () => {
+ it("should define correct chain constants", () => {
+ expect(SOLANA_MAINNET_CHAIN).toBe("solana:mainnet");
+ expect(SOLANA_DEVNET_CHAIN).toBe("solana:devnet");
+ expect(SOLANA_TESTNET_CHAIN).toBe("solana:testnet");
+ });
+
+ it("should validate Solana chains correctly", () => {
+ expect(isSolanaChain("solana:mainnet")).toBe(true);
+ expect(isSolanaChain("solana:devnet")).toBe(true);
+ expect(isSolanaChain("solana:testnet")).toBe(true);
+ expect(isSolanaChain("ethereum:mainnet")).toBe(false);
+ expect(isSolanaChain("bitcoin:mainnet")).toBe(false);
+ });
+
+ it("should include all supported chains", () => {
+ expect(SOLANA_CHAINS).toContain(SOLANA_MAINNET_CHAIN);
+ expect(SOLANA_CHAINS).toContain(SOLANA_DEVNET_CHAIN);
+ expect(SOLANA_CHAINS).toContain(SOLANA_TESTNET_CHAIN);
+ expect(SOLANA_CHAINS.length).toBe(3);
+ });
+ });
+
+ describe("OkoStandardWallet", () => {
+ let wallet: OkoSolWalletInterface;
+ let standardWallet: OkoStandardWallet;
+
+ beforeEach(async () => {
+ const mockOkoWallet = createMockOkoWallet();
+ wallet = new (OkoSolWallet as any)(
+ mockOkoWallet,
+ ) as OkoSolWalletInterface;
+ standardWallet = new OkoStandardWallet(wallet);
+ });
+
+ describe("Wallet properties", () => {
+ it("should have correct name", () => {
+ expect(standardWallet.name).toBe(OKO_WALLET_NAME);
+ expect(standardWallet.name).toBe("Oko");
+ });
+
+ it("should have correct version", () => {
+ expect(standardWallet.version).toBe("1.0.0");
+ });
+
+ it("should have correct icon", () => {
+ expect(standardWallet.icon).toBe(OKO_ICON);
+ expect(standardWallet.icon).toMatch(/^data:image\//);
+ });
+
+ it("should have correct chains", () => {
+ expect(standardWallet.chains).toEqual(SOLANA_CHAINS);
+ });
+
+ it("should have empty accounts when not connected", () => {
+ expect(standardWallet.accounts).toEqual([]);
+ });
+ });
+
+ describe("Features", () => {
+ it("should have standard:connect feature", () => {
+ const features = standardWallet.features;
+ expect(features["standard:connect"]).toBeDefined();
+ expect(features["standard:connect"].version).toBe("1.0.0");
+ expect(typeof features["standard:connect"].connect).toBe("function");
+ });
+
+ it("should have standard:disconnect feature", () => {
+ const features = standardWallet.features;
+ expect(features["standard:disconnect"]).toBeDefined();
+ expect(features["standard:disconnect"].version).toBe("1.0.0");
+ expect(typeof features["standard:disconnect"].disconnect).toBe(
+ "function",
+ );
+ });
+
+ it("should have standard:events feature", () => {
+ const features = standardWallet.features;
+ expect(features["standard:events"]).toBeDefined();
+ expect(features["standard:events"].version).toBe("1.0.0");
+ expect(typeof features["standard:events"].on).toBe("function");
+ });
+
+ it("should have solana:signMessage feature", () => {
+ const features = standardWallet.features;
+ expect(features["solana:signMessage"]).toBeDefined();
+ expect(features["solana:signMessage"].version).toBe("1.0.0");
+ });
+
+ it("should have solana:signTransaction feature", () => {
+ const features = standardWallet.features;
+ expect(features["solana:signTransaction"]).toBeDefined();
+ expect(features["solana:signTransaction"].version).toBe("1.0.0");
+ expect(
+ features["solana:signTransaction"].supportedTransactionVersions,
+ ).toContain("legacy");
+ expect(
+ features["solana:signTransaction"].supportedTransactionVersions,
+ ).toContain(0);
+ });
+
+ it("should have solana:signAndSendTransaction feature", () => {
+ const features = standardWallet.features;
+ expect(features["solana:signAndSendTransaction"]).toBeDefined();
+ expect(features["solana:signAndSendTransaction"].version).toBe("1.0.0");
+ expect(
+ features["solana:signAndSendTransaction"]
+ .supportedTransactionVersions,
+ ).toContain("legacy");
+ expect(
+ features["solana:signAndSendTransaction"]
+ .supportedTransactionVersions,
+ ).toContain(0);
+ });
+ });
+
+ describe("Connect", () => {
+ it("should connect and return accounts", async () => {
+ const features = standardWallet.features;
+ const result = await features["standard:connect"].connect();
+
+ expect(result.accounts).toHaveLength(1);
+ expect(result.accounts[0]).toBeInstanceOf(OkoSolanaWalletAccount);
+ });
+
+ it("should update accounts after connect", async () => {
+ expect(standardWallet.accounts).toHaveLength(0);
+
+ const features = standardWallet.features;
+ await features["standard:connect"].connect();
+
+ expect(standardWallet.accounts).toHaveLength(1);
+ });
+ });
+
+ describe("Disconnect", () => {
+ it("should disconnect successfully", async () => {
+ const features = standardWallet.features;
+ await features["standard:connect"].connect();
+ expect(standardWallet.accounts).toHaveLength(1);
+
+ await features["standard:disconnect"].disconnect();
+ expect(wallet.connected).toBe(false);
+ });
+ });
+
+ describe("Events", () => {
+ it("should register and unregister event listeners", () => {
+ const features = standardWallet.features;
+ const listener = jest.fn();
+
+ const unsubscribe = features["standard:events"].on("change", listener);
+
+ expect(typeof unsubscribe).toBe("function");
+
+ unsubscribe();
+ });
+
+ it("should emit change event on connect", async () => {
+ const features = standardWallet.features;
+ const listener = jest.fn();
+
+ features["standard:events"].on("change", listener);
+ await features["standard:connect"].connect();
+
+ expect(listener).toHaveBeenCalled();
+ expect(listener).toHaveBeenCalledWith(
+ expect.objectContaining({
+ accounts: expect.any(Array),
+ }),
+ );
+ });
+
+ it("should emit change event on disconnect", async () => {
+ const features = standardWallet.features;
+ const listener = jest.fn();
+
+ await features["standard:connect"].connect();
+
+ features["standard:events"].on("change", listener);
+ await features["standard:disconnect"].disconnect();
+
+ expect(listener).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe("Account features", () => {
+ it("should have correct feature identifiers", () => {
+ expect(OKO_ACCOUNT_FEATURES).toContain("solana:signIn");
+ expect(OKO_ACCOUNT_FEATURES).toContain("solana:signMessage");
+ expect(OKO_ACCOUNT_FEATURES).toContain("solana:signTransaction");
+ expect(OKO_ACCOUNT_FEATURES).toContain("solana:signAndSendTransaction");
+ });
+ });
+
+ describe("Sign In (SIWS)", () => {
+ describe("buildSignInMessage", () => {
+ it("should build basic SIWS message", () => {
+ const input = {
+ domain: "example.com",
+ statement: "Sign in to Example App",
+ };
+ const address = "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f";
+
+ const message = buildSignInMessage(input, address);
+
+ expect(message).toContain("example.com wants you to sign in");
+ expect(message).toContain(address);
+ expect(message).toContain("Sign in to Example App");
+ });
+
+ it("should include all optional fields", () => {
+ const input = {
+ domain: "example.com",
+ statement: "Sign in",
+ uri: "https://example.com",
+ version: "1",
+ chainId: "mainnet",
+ nonce: "abc123",
+ issuedAt: "2024-01-01T00:00:00Z",
+ expirationTime: "2024-01-02T00:00:00Z",
+ notBefore: "2024-01-01T00:00:00Z",
+ requestId: "req-123",
+ resources: ["https://api.example.com", "https://storage.example.com"],
+ };
+ const address = "abc123";
+
+ const message = buildSignInMessage(input, address);
+
+ expect(message).toContain("URI: https://example.com");
+ expect(message).toContain("Version: 1");
+ expect(message).toContain("Chain ID: mainnet");
+ expect(message).toContain("Nonce: abc123");
+ expect(message).toContain("Issued At: 2024-01-01T00:00:00Z");
+ expect(message).toContain("Expiration Time: 2024-01-02T00:00:00Z");
+ expect(message).toContain("Not Before: 2024-01-01T00:00:00Z");
+ expect(message).toContain("Request ID: req-123");
+ expect(message).toContain("Resources:");
+ expect(message).toContain("- https://api.example.com");
+ expect(message).toContain("- https://storage.example.com");
+ });
+
+ it("should omit optional fields when not provided", () => {
+ const input = {
+ domain: "example.com",
+ };
+ const address = "abc123";
+
+ const message = buildSignInMessage(input, address);
+
+ expect(message).not.toContain("URI:");
+ expect(message).not.toContain("Version:");
+ expect(message).not.toContain("Chain ID:");
+ expect(message).not.toContain("Nonce:");
+ expect(message).not.toContain("Resources:");
+ });
+ });
+
+ describe("signIn feature", () => {
+ let wallet: OkoSolWalletInterface;
+ let standardWallet: OkoStandardWallet;
+
+ beforeEach(() => {
+ const mockOkoWallet = createMockOkoWallet();
+ wallet = new (OkoSolWallet as any)(
+ mockOkoWallet,
+ ) as OkoSolWalletInterface;
+ standardWallet = new OkoStandardWallet(wallet);
+ });
+
+ it("should have solana:signIn feature", () => {
+ const features = standardWallet.features;
+ expect(features["solana:signIn"]).toBeDefined();
+ expect(features["solana:signIn"].version).toBe("1.0.0");
+ expect(typeof features["solana:signIn"].signIn).toBe("function");
+ });
+ });
+ });
+});
diff --git a/sdk/oko_sdk_sol/src/types/event.ts b/sdk/oko_sdk_sol/src/types/event.ts
index 4e273bbf5..0d98a3323 100644
--- a/sdk/oko_sdk_sol/src/types/event.ts
+++ b/sdk/oko_sdk_sol/src/types/event.ts
@@ -1,23 +1,14 @@
import type { PublicKey } from "@solana/web3.js";
-export type OkoSolWalletEvent = "connect" | "disconnect" | "error";
-
-export type OkoSolWalletConnectEvent = {
- type: "connect";
- handler: (publicKey: PublicKey) => void;
-};
-
-export type OkoSolWalletDisconnectEvent = {
- type: "disconnect";
- handler: () => void;
+export type SolWalletEventMap = {
+ connect: PublicKey;
+ disconnect: void;
+ accountChanged: PublicKey | null;
+ error: Error;
};
-export type OkoSolWalletErrorEvent = {
- type: "error";
- handler: (error: Error) => void;
-};
+export type SolWalletEvent = keyof SolWalletEventMap;
-export type OkoSolWalletEventHandler =
- | OkoSolWalletConnectEvent
- | OkoSolWalletDisconnectEvent
- | OkoSolWalletErrorEvent;
+export type SolWalletEventHandler = (
+ payload: SolWalletEventMap[K],
+) => void;
diff --git a/sdk/oko_sdk_sol/src/types/index.ts b/sdk/oko_sdk_sol/src/types/index.ts
index d5ea3133f..df19bd8a5 100644
--- a/sdk/oko_sdk_sol/src/types/index.ts
+++ b/sdk/oko_sdk_sol/src/types/index.ts
@@ -17,9 +17,9 @@ export type {
} from "./sign";
export type {
- OkoSolWalletEvent,
- OkoSolWalletEventHandler,
- OkoSolWalletConnectEvent,
- OkoSolWalletDisconnectEvent,
- OkoSolWalletErrorEvent,
+ SolWalletEvent,
+ SolWalletEventMap,
+ SolWalletEventHandler,
} from "./event";
+
+export type { OkoSolWalletInternal } from "./internal";
diff --git a/sdk/oko_sdk_sol/src/types/internal.ts b/sdk/oko_sdk_sol/src/types/internal.ts
new file mode 100644
index 000000000..3a6f70b62
--- /dev/null
+++ b/sdk/oko_sdk_sol/src/types/internal.ts
@@ -0,0 +1,11 @@
+import type { OkoSolWalletInterface } from "./sol_wallet";
+import type { SolWalletEventEmitter } from "../emitter";
+
+/**
+ * Internal interface that extends OkoSolWalletInterface with private emitter.
+ * Used internally by SDK methods that need access to the event emitter.
+ */
+export interface OkoSolWalletInternal extends OkoSolWalletInterface {
+ _emitter: SolWalletEventEmitter;
+ _accountsChangedHandler: (payload: { publicKey: string | null }) => void;
+}
diff --git a/sdk/oko_sdk_sol/src/types/sol_wallet.ts b/sdk/oko_sdk_sol/src/types/sol_wallet.ts
index 53affd694..cfc23f516 100644
--- a/sdk/oko_sdk_sol/src/types/sol_wallet.ts
+++ b/sdk/oko_sdk_sol/src/types/sol_wallet.ts
@@ -16,6 +16,7 @@ import type {
OkoSolWalletInitError,
LazyInitError,
} from "@oko-wallet-sdk-sol/errors";
+import type { SolWalletEvent, SolWalletEventHandler } from "./event";
export interface OkoSolWalletState {
publicKey: PublicKey | null;
@@ -32,17 +33,14 @@ export interface OkoSolWalletStaticInterface {
}
export interface OkoSolWalletInterface {
- // oko 패턴 속성
state: OkoSolWalletState;
okoWallet: OkoWalletInterface;
waitUntilInitialized: Promise>;
- // Solana Wallet Adapter 표준 속성
publicKey: PublicKey | null;
connecting: boolean;
connected: boolean;
- // Solana Wallet Adapter 표준 메서드
connect: () => Promise;
disconnect: () => Promise;
signTransaction: (
@@ -57,4 +55,24 @@ export interface OkoSolWalletInterface {
connection: Connection,
options?: SendOptions,
) => Promise;
+
+ signAndSendTransaction: (
+ transaction: Transaction | VersionedTransaction,
+ connection: Connection,
+ options?: SendOptions,
+ ) => Promise<{ signature: TransactionSignature }>;
+ signAndSendAllTransactions: (
+ transactions: (Transaction | VersionedTransaction)[],
+ connection: Connection,
+ options?: SendOptions,
+ ) => Promise<{ signatures: TransactionSignature[] }>;
+
+ on: (
+ event: K,
+ handler: SolWalletEventHandler,
+ ) => void;
+ off: (
+ event: K,
+ handler: SolWalletEventHandler,
+ ) => void;
}
diff --git a/sdk/oko_sdk_sol/src/wallet-standard/account.ts b/sdk/oko_sdk_sol/src/wallet-standard/account.ts
new file mode 100644
index 000000000..1cb5bf6f6
--- /dev/null
+++ b/sdk/oko_sdk_sol/src/wallet-standard/account.ts
@@ -0,0 +1,35 @@
+import type { IdentifierString, WalletAccount } from "@wallet-standard/base";
+import type {
+ SolanaSignAndSendTransactionFeature,
+ SolanaSignInFeature,
+ SolanaSignMessageFeature,
+ SolanaSignTransactionFeature,
+} from "@solana/wallet-standard-features";
+
+import { SOLANA_CHAINS, type SolanaChain } from "./chains";
+
+type SolanaFeatureKey = keyof (SolanaSignInFeature &
+ SolanaSignMessageFeature &
+ SolanaSignTransactionFeature &
+ SolanaSignAndSendTransactionFeature);
+
+export const OKO_ACCOUNT_FEATURES: readonly IdentifierString[] = [
+ "solana:signIn",
+ "solana:signMessage",
+ "solana:signTransaction",
+ "solana:signAndSendTransaction",
+] as const satisfies readonly SolanaFeatureKey[];
+
+export class OkoSolanaWalletAccount implements WalletAccount {
+ readonly address: string;
+ readonly publicKey: Uint8Array;
+ readonly chains: readonly SolanaChain[];
+ readonly features: readonly IdentifierString[];
+
+ constructor(address: string, publicKey: Uint8Array) {
+ this.address = address;
+ this.publicKey = publicKey;
+ this.chains = SOLANA_CHAINS;
+ this.features = OKO_ACCOUNT_FEATURES;
+ }
+}
diff --git a/sdk/oko_sdk_sol/src/wallet-standard/chains.ts b/sdk/oko_sdk_sol/src/wallet-standard/chains.ts
new file mode 100644
index 000000000..bf6d2b3e1
--- /dev/null
+++ b/sdk/oko_sdk_sol/src/wallet-standard/chains.ts
@@ -0,0 +1,17 @@
+import type { IdentifierString } from "@wallet-standard/base";
+
+export const SOLANA_MAINNET_CHAIN = "solana:mainnet" as IdentifierString;
+export const SOLANA_DEVNET_CHAIN = "solana:devnet" as IdentifierString;
+export const SOLANA_TESTNET_CHAIN = "solana:testnet" as IdentifierString;
+
+export const SOLANA_CHAINS = [
+ SOLANA_MAINNET_CHAIN,
+ SOLANA_DEVNET_CHAIN,
+ SOLANA_TESTNET_CHAIN,
+] as const;
+
+export type SolanaChain = (typeof SOLANA_CHAINS)[number];
+
+export function isSolanaChain(chain: string): chain is SolanaChain {
+ return SOLANA_CHAINS.includes(chain as SolanaChain);
+}
diff --git a/sdk/oko_sdk_sol/src/wallet-standard/features.ts b/sdk/oko_sdk_sol/src/wallet-standard/features.ts
new file mode 100644
index 000000000..7d57ab7e9
--- /dev/null
+++ b/sdk/oko_sdk_sol/src/wallet-standard/features.ts
@@ -0,0 +1,150 @@
+import {
+ Transaction,
+ VersionedTransaction,
+ Connection,
+ clusterApiUrl,
+} from "@solana/web3.js";
+import type {
+ SolanaSignAndSendTransactionFeature,
+ SolanaSignAndSendTransactionMethod,
+ SolanaSignAndSendTransactionOutput,
+ SolanaSignMessageFeature,
+ SolanaSignMessageMethod,
+ SolanaSignMessageOutput,
+ SolanaSignTransactionFeature,
+ SolanaSignTransactionMethod,
+ SolanaSignTransactionOutput,
+} from "@solana/wallet-standard-features";
+import bs58 from "bs58";
+
+import type { OkoSolWalletInterface } from "@oko-wallet-sdk-sol/types";
+import { isSolanaChain, SOLANA_MAINNET_CHAIN } from "./chains";
+
+// Support both legacy and versioned transactions
+const SUPPORTED_TRANSACTION_VERSIONS = ["legacy", 0] as const;
+
+function getConnection(chain?: string): Connection {
+ if (!chain || chain === SOLANA_MAINNET_CHAIN) {
+ return new Connection(clusterApiUrl("mainnet-beta"));
+ }
+ if (chain === "solana:devnet") {
+ return new Connection(clusterApiUrl("devnet"));
+ }
+ if (chain === "solana:testnet") {
+ return new Connection(clusterApiUrl("testnet"));
+ }
+ return new Connection(clusterApiUrl("mainnet-beta"));
+}
+
+function deserializeTransaction(
+ bytes: Uint8Array,
+): Transaction | VersionedTransaction {
+ try {
+ return VersionedTransaction.deserialize(bytes);
+ } catch {
+ return Transaction.from(bytes);
+ }
+}
+
+export function createSignMessageFeature(
+ wallet: OkoSolWalletInterface,
+): SolanaSignMessageFeature {
+ const signMessage: SolanaSignMessageMethod = async (
+ ...inputs
+ ): Promise => {
+ const outputs: SolanaSignMessageOutput[] = [];
+
+ for (const input of inputs) {
+ const signature = await wallet.signMessage(input.message);
+ outputs.push({
+ signedMessage: input.message,
+ signature,
+ });
+ }
+
+ return outputs;
+ };
+
+ return {
+ "solana:signMessage": {
+ version: "1.0.0",
+ signMessage,
+ },
+ };
+}
+
+export function createSignTransactionFeature(
+ wallet: OkoSolWalletInterface,
+): SolanaSignTransactionFeature {
+ const signTransaction: SolanaSignTransactionMethod = async (
+ ...inputs
+ ): Promise => {
+ const outputs: SolanaSignTransactionOutput[] = [];
+
+ for (const input of inputs) {
+ if (input.chain && !isSolanaChain(input.chain)) {
+ throw new Error(`Unsupported chain: ${input.chain}`);
+ }
+
+ const transaction = deserializeTransaction(input.transaction);
+ const signedTransaction = await wallet.signTransaction(transaction);
+ const signedBytes = signedTransaction.serialize();
+
+ outputs.push({
+ signedTransaction: signedBytes,
+ });
+ }
+
+ return outputs;
+ };
+
+ return {
+ "solana:signTransaction": {
+ version: "1.0.0",
+ supportedTransactionVersions: SUPPORTED_TRANSACTION_VERSIONS,
+ signTransaction,
+ },
+ };
+}
+
+export function createSignAndSendTransactionFeature(
+ wallet: OkoSolWalletInterface,
+): SolanaSignAndSendTransactionFeature {
+ const signAndSendTransaction: SolanaSignAndSendTransactionMethod = async (
+ ...inputs
+ ): Promise => {
+ const outputs: SolanaSignAndSendTransactionOutput[] = [];
+
+ for (const input of inputs) {
+ if (input.chain && !isSolanaChain(input.chain)) {
+ throw new Error(`Unsupported chain: ${input.chain}`);
+ }
+
+ const transaction = deserializeTransaction(input.transaction);
+ const connection = getConnection(input.chain);
+
+ const { signature } = await wallet.signAndSendTransaction(
+ transaction,
+ connection,
+ input.options,
+ );
+
+ // signature is base58 encoded, decode to Uint8Array
+ const signatureBytes = bs58.decode(signature);
+
+ outputs.push({
+ signature: signatureBytes,
+ });
+ }
+
+ return outputs;
+ };
+
+ return {
+ "solana:signAndSendTransaction": {
+ version: "1.0.0",
+ supportedTransactionVersions: SUPPORTED_TRANSACTION_VERSIONS,
+ signAndSendTransaction,
+ },
+ };
+}
diff --git a/sdk/oko_sdk_sol/src/wallet-standard/icon.ts b/sdk/oko_sdk_sol/src/wallet-standard/icon.ts
new file mode 100644
index 000000000..9433dee70
--- /dev/null
+++ b/sdk/oko_sdk_sol/src/wallet-standard/icon.ts
@@ -0,0 +1,5 @@
+import type { WalletIcon } from "@wallet-standard/base";
+
+// OKO wallet icon (SVG)
+export const OKO_ICON: WalletIcon =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAACxLAAAsSwGlPZapAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAkzSURBVHgB7Z09bFvXFcf/95GuyQSoWdSIvdihpxZNgTgB2qHuQAE1mk4yWiBBp1AJmqmAbLhwpzZytxoNIqHZiiT0lCZA0GhyigxhAbtDB0sFnKKd+iIvTpAgdAZLjvR4c84ln0zKlERRFO/X+QFPj6RICnrnf865H+fepzABdL1aWcd6NUN2WkNXisDjfNZQFfp1RdGh6djysSrCIt3uuYJO20g+pmvSKnQet0ooLatG2sIBo3AArNaPV+mfOUf/zJNk5BrCM+ZEIOMsk2OkdA0X6Wmz3LiTYsyMTQDs5atk+gSYpqc1CGOHBdGGWsAYxbBvAbDhv8K9WVLpeTwcxoWDIaW00aBUcXW/QtiXANbqx2bpNAcxvC04PVwmETQwIiMJgHM8KfBNSKh3BRbC1CjRIMEeYa8n4y9BjO8S1YRsQrY5v8fP7S0C0B94lU57/iPC5CDnnDvc+PTy8O8fkvv1x96kMFOH4DzcQCQRzAz33iEgz/8QEvJ9o1lqfDK125t2bQOw50OM7yO1ru12ZEcB0Be8LGHfX9h23XbbtmybArp9/HkIIXCB0sFAWw4UQLefz109GeAJADJyi4aQnxo0TpAM/oDmRp8YPxC0mXHVA9sDDwmAvL8Omb0LkdqggaK+FNAN/ez9VQjBwangGyif6q0z6IsACdrPQ4wfLJwK1rDWFwU2I4B4fxxsjQK9EaAGMX7wcBS4T+6eP98UAHn/LIRYmM4fmBTQDf//hxANNEp4iscF8ghQgxAV5PDn+Jx0fuhpCFGhoJ7ksxGAlsZfdGjoGp+VrleoVXj4CwjRcRjlbyVrKJ2GECW8Wiuh0T+Z9IkUXqpXzKCqey4N9gT1yDehvvMj4OgJJCefgKIDjxwBykfM73rRn98GPqPj3pdor9yCXvkI+jYd/FqgKF6nqcwCTY1QYIMnT/+Ujp9BffvE8J/j93bfX3jqmc3XWQDt//0T+ub7aC+9j8CoFhFAD0Cxh595FsnZlx7y7HF8d+Hoc8CZ56A5OixdQ/v6O9AkCt8h5z+ifK74ZW8vnrvYCfMThgWQXX8b7RvvwFfawKKXAmCvLL44b8XwW+EUsfHaC9RmuAX/UE2v2n8c3gvTF3Hoyr+cMD7DYjw09wEKLMijw7c53EBXOALwJFAVjqNOfh/FX7/h9EXmaJAt/smntJB6EQGSs78yXua6h3VS04KJUr7gvAAKv/wDinT4RGH6N14IlnFWAJzvD/32XRTI+33EpKxL7zovAhZAFY7BxjcXz5GG3qiYlOC2CNwcBTYXjTwoBFwXgXMC4JwfivFzjAioB4Mxj1KOA6cEwK1nX3P+bpg2wQsLcA1nBJDQBAy3nkMmefoZ5wTuhADMhItnXb1RMSnuhDspzgkBJBT6/RtGHZ3ii6/CFawLIKFp1gIdMcHtAVdGC60LwKdh03FSOPuSE70CqwJg748p9PfBM5sONAitCiBW789xIQpYE0DU3p/jQBSwJoDCj5+FYD8KWBEAe77vEz1jg4yf9FQhTxorAkgCHe4dFZvdYDsCEO/vw0x+WUoDExeACf+8Qkd4gFnBdAY2mHwEOCHGH0Ry8nuwweQFcO9LCAOwdF0mLgB9+1bQCy5HobPkzM66QysRYOPKL0QEXdj42euz1q4HLwyxtjSYxwJiHw3US9eMCGxhVQCCfULdG0IYEhFA5IgAIkcEEDkigMgRAUSOCCByRACRIwKIHBFA5BRhkXxXT8Xbt0aGvncX7Zt/t77hpLW5ALMUPPDVwMOQffAXZG/9HrawVhUsxu/A6wJ4m1tb2CkKPSNrAnpJvmuvSNaKALSUhfVhsw1kRwArH0F4QHbT3jb0dgTA++//1//t1seBuR/BjbdhC2vjAJnH26yPE9vdQGsCYNVLYSg5wuIrsInVkUDb/7xtXHACqwKIPQq44ADW5wI2Xj+PGGHjuyB+6wLgRlD7ZnB349qR/MYSLuDEbODGG7NRpQJeGeUKbkwH83KxSFKBK6E/x5l6AHMbtsB7BZzqXAn9OU4VhPDFCXWAyOR9SnWu4VxFUPbW74KbKzD3FuQV0Q5OgrlXEkYXaf3Kz4MRwabxHW3ksgBSuEZXBL53D103Ppy+byD3DF6bMSVTPsKN2o25nzjfvXW+Kpjr5XzrHbBo1//oZs7fihdl4dw7WL/0Q+e9iQ2+8ecZq0Wee4UF0IIHsPFZBK5GA57Y2rj0A2ubPY2GSr0RQE4eDWxW0fTCuZ7DPY9k+ljrqO7Xj/1NA+fgIZ3y8otm6/lJYxp5771ivaJnf6imWq0fbyjo5+ExuRAOetcx9nB946/IHFjRMw4UVKOooVsKfmP6293JJBYB34uAN2BWY9iW1hh96Rqy6zREzZtcBlTS3gY+5rWBKQLChOaud/JNqMHrD08+0REEP+cIUT7SeZx/ho26eteMPeiVW3S+i/bKfzrVywFPU1PkbxUpDFAjMMytAo1hqVWeedUynxwZCmlSQLYMIUoStFvJIaynEKKkhNJyohotHgdIIcTGsmqkLTMUrKH+ASEquAfA564AtLQDIoMa/+/xOel9IkRFk38YAZQbd9L8BSEKlrs2fzAdTDlhEUIUUJtvIX+8KYAy7jfg2cygMBIpeqL9pgC4O9irDCFMyMZX8/DP9FUElbA2D4kCIZPS0eh9oU8A3UGhyxCChLz/cq/3MwNngtfqxz6kUw1CSKSlxientr44sCiUlDIDSQUhwe27qUG/GCiAbpiQVBAOD4X+nG3LwilczNPYgPQKPIfzPttyu9/vWg22Wn+sQUPFXtcMxkq3y1ff6T1DlQNKo9BHVLPUuDO127uGWhlEIWSKZgyvQvAC9vxhjM8MvTSs3Pi0zvkEguss7Bb2e9nT2kD64jk6XYB0EV2EbXKBovWeNlsaaUnAav14VUFzu6AKwQFUUwMz23X1dvwk9gEJoU5CeBkiBFuYofudunm7se9FQRwN6FTvLi+rQpgEZua2hNI8F3ZiH4xtVVhXCDUSAm+FdRrCAaCabejFMrXG9mv4zW/EAdAjhml0ooIIYiRUSgZqZtD/5rrNUXL8rn8BE0DXq5U1rJ1O0K5kUNSAVBXqfjyu+1NGdcunqggKlfY/1+TBZllei5fn0TjLXRp6T/lxhmz5UTyajsvLd+JrStVjl2hIpw0AAAAASUVORK5CYII=";
diff --git a/sdk/oko_sdk_sol/src/wallet-standard/index.ts b/sdk/oko_sdk_sol/src/wallet-standard/index.ts
new file mode 100644
index 000000000..1ec4c17d1
--- /dev/null
+++ b/sdk/oko_sdk_sol/src/wallet-standard/index.ts
@@ -0,0 +1,13 @@
+export { registerOkoWallet } from "./register";
+export { OkoStandardWallet, OKO_WALLET_NAME } from "./wallet";
+export { OkoSolanaWalletAccount, OKO_ACCOUNT_FEATURES } from "./account";
+export {
+ SOLANA_CHAINS,
+ SOLANA_MAINNET_CHAIN,
+ SOLANA_DEVNET_CHAIN,
+ SOLANA_TESTNET_CHAIN,
+ isSolanaChain,
+ type SolanaChain,
+} from "./chains";
+export { OKO_ICON } from "./icon";
+export { buildSignInMessage, createSignInFeature } from "./sign-in";
diff --git a/sdk/oko_sdk_sol/src/wallet-standard/register.ts b/sdk/oko_sdk_sol/src/wallet-standard/register.ts
new file mode 100644
index 000000000..2df4c7c1e
--- /dev/null
+++ b/sdk/oko_sdk_sol/src/wallet-standard/register.ts
@@ -0,0 +1,16 @@
+import { registerWallet } from "@wallet-standard/wallet";
+
+import type { OkoSolWalletInterface } from "@oko-wallet-sdk-sol/types";
+import { OkoStandardWallet } from "./wallet";
+
+let registered = false;
+
+export function registerOkoWallet(wallet: OkoSolWalletInterface): void {
+ if (registered) {
+ return;
+ }
+
+ const standardWallet = new OkoStandardWallet(wallet);
+ registerWallet(standardWallet);
+ registered = true;
+}
diff --git a/sdk/oko_sdk_sol/src/wallet-standard/sign-in.ts b/sdk/oko_sdk_sol/src/wallet-standard/sign-in.ts
new file mode 100644
index 000000000..88c038c38
--- /dev/null
+++ b/sdk/oko_sdk_sol/src/wallet-standard/sign-in.ts
@@ -0,0 +1,133 @@
+import type {
+ SolanaSignInFeature,
+ SolanaSignInInput,
+ SolanaSignInMethod,
+ SolanaSignInOutput,
+} from "@solana/wallet-standard-features";
+
+import type { OkoSolWalletInterface } from "@oko-wallet-sdk-sol/types";
+import { OkoSolanaWalletAccount } from "./account";
+
+/**
+ * Build SIWS (Sign In With Solana) message following EIP-4361 format
+ * https://eips.ethereum.org/EIPS/eip-4361
+ */
+export function buildSignInMessage(
+ input: SolanaSignInInput,
+ address: string,
+): string {
+ const lines: string[] = [];
+
+ // Domain and address (required)
+ const domain = input.domain ?? window.location.host;
+ lines.push(`${domain} wants you to sign in with your Solana account:`);
+ lines.push(input.address ?? address);
+
+ // Statement (optional)
+ if (input.statement) {
+ lines.push("");
+ lines.push(input.statement);
+ }
+
+ lines.push("");
+
+ // URI
+ if (input.uri) {
+ lines.push(`URI: ${input.uri}`);
+ }
+
+ // Version
+ if (input.version) {
+ lines.push(`Version: ${input.version}`);
+ }
+
+ // Chain ID
+ if (input.chainId) {
+ lines.push(`Chain ID: ${input.chainId}`);
+ }
+
+ // Nonce
+ if (input.nonce) {
+ lines.push(`Nonce: ${input.nonce}`);
+ }
+
+ // Issued At
+ if (input.issuedAt) {
+ lines.push(`Issued At: ${input.issuedAt}`);
+ }
+
+ // Expiration Time
+ if (input.expirationTime) {
+ lines.push(`Expiration Time: ${input.expirationTime}`);
+ }
+
+ // Not Before
+ if (input.notBefore) {
+ lines.push(`Not Before: ${input.notBefore}`);
+ }
+
+ // Request ID
+ if (input.requestId) {
+ lines.push(`Request ID: ${input.requestId}`);
+ }
+
+ // Resources
+ if (input.resources && input.resources.length > 0) {
+ lines.push("Resources:");
+ for (const resource of input.resources) {
+ lines.push(`- ${resource}`);
+ }
+ }
+
+ return lines.join("\n");
+}
+
+export function createSignInFeature(
+ wallet: OkoSolWalletInterface,
+): SolanaSignInFeature {
+ const signIn: SolanaSignInMethod = async (
+ ...inputs
+ ): Promise => {
+ const outputs: SolanaSignInOutput[] = [];
+
+ for (const input of inputs) {
+ // Ensure connected
+ if (!wallet.connected || !wallet.publicKey) {
+ await wallet.connect();
+ }
+
+ const publicKey = wallet.publicKey;
+ if (!publicKey) {
+ throw new Error("Wallet not connected");
+ }
+
+ const address = publicKey.toBase58();
+
+ // Build SIWS message
+ const message = buildSignInMessage(input, address);
+ const messageBytes = new TextEncoder().encode(message);
+
+ // Sign the message
+ const signature = await wallet.signMessage(messageBytes);
+
+ // Create account for output
+ const account = new OkoSolanaWalletAccount(address, publicKey.toBytes());
+
+ outputs.push({
+ account,
+ signedMessage: messageBytes,
+ signature,
+ signatureType: "ed25519",
+ });
+ }
+
+ return outputs;
+ };
+
+ return {
+ "solana:signIn": {
+ version: "1.0.0",
+ signIn,
+ },
+ };
+}
diff --git a/sdk/oko_sdk_sol/src/wallet-standard/wallet.ts b/sdk/oko_sdk_sol/src/wallet-standard/wallet.ts
new file mode 100644
index 000000000..de6c683cb
--- /dev/null
+++ b/sdk/oko_sdk_sol/src/wallet-standard/wallet.ts
@@ -0,0 +1,163 @@
+import type {
+ IdentifierString,
+ Wallet,
+ WalletAccount,
+ WalletIcon,
+} from "@wallet-standard/base";
+import type {
+ StandardConnectFeature,
+ StandardDisconnectFeature,
+ StandardEventsFeature,
+ StandardEventsListeners,
+ StandardEventsNames,
+ StandardEventsOnMethod,
+} from "@wallet-standard/features";
+import type {
+ SolanaSignAndSendTransactionFeature,
+ SolanaSignInFeature,
+ SolanaSignMessageFeature,
+ SolanaSignTransactionFeature,
+} from "@solana/wallet-standard-features";
+
+import type { OkoSolWalletInterface } from "@oko-wallet-sdk-sol/types";
+import { OkoSolanaWalletAccount } from "./account";
+import { SOLANA_CHAINS } from "./chains";
+import {
+ createSignAndSendTransactionFeature,
+ createSignMessageFeature,
+ createSignTransactionFeature,
+} from "./features";
+import { createSignInFeature } from "./sign-in";
+import { OKO_ICON } from "./icon";
+
+export const OKO_WALLET_NAME = "Oko" as const;
+
+type OkoWalletFeatures = StandardConnectFeature &
+ StandardDisconnectFeature &
+ StandardEventsFeature &
+ SolanaSignInFeature &
+ SolanaSignMessageFeature &
+ SolanaSignTransactionFeature &
+ SolanaSignAndSendTransactionFeature;
+
+export class OkoStandardWallet implements Wallet {
+ readonly #wallet: OkoSolWalletInterface;
+ #accounts: WalletAccount[] = [];
+ #listeners: { [E in StandardEventsNames]?: StandardEventsListeners[E][] } =
+ {};
+
+ readonly version = "1.0.0" as const;
+ readonly name = OKO_WALLET_NAME;
+ readonly icon: WalletIcon = OKO_ICON;
+ readonly chains: readonly IdentifierString[] = SOLANA_CHAINS;
+
+ get accounts(): readonly WalletAccount[] {
+ return this.#accounts;
+ }
+
+ get features(): OkoWalletFeatures {
+ return {
+ "standard:connect": {
+ version: "1.0.0",
+ connect: async () => {
+ // Check if user is signed in
+ let existingKey =
+ await this.#wallet.okoWallet.getPublicKeyEd25519();
+
+ if (!existingKey) {
+ // Trigger OAuth sign-in
+ await this.#wallet.okoWallet.signIn("google");
+
+ // Re-check after sign-in
+ existingKey = await this.#wallet.okoWallet.getPublicKeyEd25519();
+ }
+
+ await this.#wallet.connect();
+
+ this.#updateAccounts();
+ return { accounts: this.#accounts };
+ },
+ },
+ "standard:disconnect": {
+ version: "1.0.0",
+ disconnect: async () => {
+ await this.#wallet.disconnect();
+ },
+ },
+ "standard:events": {
+ version: "1.0.0",
+ on: this.#on.bind(this) as StandardEventsOnMethod,
+ },
+ ...createSignInFeature(this.#wallet),
+ ...createSignMessageFeature(this.#wallet),
+ ...createSignTransactionFeature(this.#wallet),
+ ...createSignAndSendTransactionFeature(this.#wallet),
+ };
+ }
+
+ constructor(wallet: OkoSolWalletInterface) {
+ this.#wallet = wallet;
+
+ if (wallet.connected && wallet.publicKey) {
+ this.#updateAccounts();
+ }
+
+ wallet.on("connect", () => {
+ this.#updateAccounts();
+ this.#emit("change", { accounts: this.#accounts });
+ });
+
+ wallet.on("disconnect", () => {
+ this.#accounts = [];
+ this.#emit("change", { accounts: this.#accounts });
+ });
+
+ wallet.on("accountChanged", () => {
+ this.#updateAccounts();
+ this.#emit("change", { accounts: this.#accounts });
+ });
+ }
+
+ #on(
+ event: E,
+ listener: StandardEventsListeners[E],
+ ): () => void {
+ if (!this.#listeners[event]) {
+ this.#listeners[event] = [];
+ }
+ this.#listeners[event]!.push(listener);
+
+ return () => {
+ const listeners = this.#listeners[event];
+ if (listeners) {
+ const index = listeners.indexOf(listener);
+ if (index !== -1) {
+ listeners.splice(index, 1);
+ }
+ }
+ };
+ }
+
+ #emit(
+ event: E,
+ ...args: Parameters
+ ): void {
+ const listeners = this.#listeners[event];
+ if (listeners) {
+ for (const listener of listeners) {
+ (listener as (...a: unknown[]) => void)(...args);
+ }
+ }
+ }
+
+ #updateAccounts(): void {
+ const publicKey = this.#wallet.publicKey;
+ if (publicKey) {
+ this.#accounts = [
+ new OkoSolanaWalletAccount(publicKey.toBase58(), publicKey.toBytes()),
+ ];
+ } else {
+ this.#accounts = [];
+ }
+ }
+}
diff --git a/ui/oko_common_ui/src/icons/solana_icon.tsx b/ui/oko_common_ui/src/icons/solana_icon.tsx
new file mode 100644
index 000000000..41d171643
--- /dev/null
+++ b/ui/oko_common_ui/src/icons/solana_icon.tsx
@@ -0,0 +1,22 @@
+import React from "react";
+
+import { s3BucketURL } from "./paths";
+
+export const SolanaIcon: React.FC = ({
+ width = 16,
+ height = 16,
+}) => {
+ return (
+
+ );
+};
+
+export interface SolanaIconProps {
+ width?: number;
+ height?: number;
+}
diff --git a/yarn.lock b/yarn.lock
index 8a6d00ee4..53e0c6693 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2828,6 +2828,82 @@ __metadata:
languageName: node
linkType: hard
+"@coral-xyz/anchor-errors@npm:^0.30.1":
+ version: 0.30.1
+ resolution: "@coral-xyz/anchor-errors@npm:0.30.1"
+ checksum: 10c0/cb6749b68c12df54041d1be47df7a7b7f1d4ad2c54759bca7964c22a5dd892de0deb004100ac81cf70278e373d228a99d648013225613b5de85854530c41f501
+ languageName: node
+ linkType: hard
+
+"@coral-xyz/anchor-new@npm:@coral-xyz/anchor@^0.30.0":
+ version: 0.30.1
+ resolution: "@coral-xyz/anchor@npm:0.30.1"
+ dependencies:
+ "@coral-xyz/anchor-errors": "npm:^0.30.1"
+ "@coral-xyz/borsh": "npm:^0.30.1"
+ "@noble/hashes": "npm:^1.3.1"
+ "@solana/web3.js": "npm:^1.68.0"
+ bn.js: "npm:^5.1.2"
+ bs58: "npm:^4.0.1"
+ buffer-layout: "npm:^1.2.2"
+ camelcase: "npm:^6.3.0"
+ cross-fetch: "npm:^3.1.5"
+ crypto-hash: "npm:^1.3.0"
+ eventemitter3: "npm:^4.0.7"
+ pako: "npm:^2.0.3"
+ snake-case: "npm:^3.0.4"
+ superstruct: "npm:^0.15.4"
+ toml: "npm:^3.0.0"
+ checksum: 10c0/ed0e5903e72d11884015bd8c21199604f56c20a4570972323133011172677b7dcfa8c7054055d827f914b61ad5b70378343809a83a06f3f6010e9d08707fd4c5
+ languageName: node
+ linkType: hard
+
+"@coral-xyz/anchor@npm:^0.29.0":
+ version: 0.29.0
+ resolution: "@coral-xyz/anchor@npm:0.29.0"
+ dependencies:
+ "@coral-xyz/borsh": "npm:^0.29.0"
+ "@noble/hashes": "npm:^1.3.1"
+ "@solana/web3.js": "npm:^1.68.0"
+ bn.js: "npm:^5.1.2"
+ bs58: "npm:^4.0.1"
+ buffer-layout: "npm:^1.2.2"
+ camelcase: "npm:^6.3.0"
+ cross-fetch: "npm:^3.1.5"
+ crypto-hash: "npm:^1.3.0"
+ eventemitter3: "npm:^4.0.7"
+ pako: "npm:^2.0.3"
+ snake-case: "npm:^3.0.4"
+ superstruct: "npm:^0.15.4"
+ toml: "npm:^3.0.0"
+ checksum: 10c0/cc863ab22b23c126e83b0804a43d45fdb7836659b917db0a8895146ec8d48637db034a7ae7dbd55d5ec7a058a266dd06878e1f921950df1e899369e49fb36c93
+ languageName: node
+ linkType: hard
+
+"@coral-xyz/borsh@npm:^0.29.0":
+ version: 0.29.0
+ resolution: "@coral-xyz/borsh@npm:0.29.0"
+ dependencies:
+ bn.js: "npm:^5.1.2"
+ buffer-layout: "npm:^1.2.0"
+ peerDependencies:
+ "@solana/web3.js": ^1.68.0
+ checksum: 10c0/25afec48714d9f0c95a02d04215803181f7da0aad85fdd1b076d9f37ef54586342e01e24fc1b2128aba0b5155e970afd6a2c6f8b0343699d8733c0703655ce39
+ languageName: node
+ linkType: hard
+
+"@coral-xyz/borsh@npm:^0.30.1":
+ version: 0.30.1
+ resolution: "@coral-xyz/borsh@npm:0.30.1"
+ dependencies:
+ bn.js: "npm:^5.1.2"
+ buffer-layout: "npm:^1.2.0"
+ peerDependencies:
+ "@solana/web3.js": ^1.68.0
+ checksum: 10c0/eaad9c59647ba58faecc408a30416f17b0cab56bce907cc54654f7a0b151526954c71cedadce7ed38830b731244d918e87b7c3fb19c56a26c2b140c5ccef27dd
+ languageName: node
+ linkType: hard
+
"@cosmjs/amino@npm:0.36.1":
version: 0.36.1
resolution: "@cosmjs/amino@npm:0.36.1"
@@ -8557,6 +8633,71 @@ __metadata:
languageName: node
linkType: hard
+"@metaplex-foundation/umi-options@npm:^0.8.5, @metaplex-foundation/umi-options@npm:^0.8.9":
+ version: 0.8.9
+ resolution: "@metaplex-foundation/umi-options@npm:0.8.9"
+ checksum: 10c0/da0bcec5188ab5638ee4bbdbba22236b69f5e89f33bf279ef9ea584d5e5ed428ab6a7cddcc73d29b8bf1e8ea840d641b509e01b9778b470baded5ab30d18cc3d
+ languageName: node
+ linkType: hard
+
+"@metaplex-foundation/umi-public-keys@npm:^0.8.9":
+ version: 0.8.9
+ resolution: "@metaplex-foundation/umi-public-keys@npm:0.8.9"
+ dependencies:
+ "@metaplex-foundation/umi-serializers-encodings": "npm:^0.8.9"
+ checksum: 10c0/961281b83cb6b60eb7872fb6c818798ee3b81b5457fb5c31c9b96bc73010d77e12d9952659f7d16d0352eb219ac7644d59d1147f27817d8c46722ea6bad23d83
+ languageName: node
+ linkType: hard
+
+"@metaplex-foundation/umi-serializers-core@npm:^0.8.9":
+ version: 0.8.9
+ resolution: "@metaplex-foundation/umi-serializers-core@npm:0.8.9"
+ checksum: 10c0/b169b62b55b6fe7f529c94c8387e9bea25c66a7e9c66bc38027fc89d230f267192f0002a16a663a07dff787d3160424c26d090243edda0aefaaf7f0d874b9adb
+ languageName: node
+ linkType: hard
+
+"@metaplex-foundation/umi-serializers-encodings@npm:^0.8.9":
+ version: 0.8.9
+ resolution: "@metaplex-foundation/umi-serializers-encodings@npm:0.8.9"
+ dependencies:
+ "@metaplex-foundation/umi-serializers-core": "npm:^0.8.9"
+ checksum: 10c0/13bd3a58278284d640b941d26580c86e9bc52390dfc6ee145c3934cd50bf65b42838812f2c4d7adfc1aa93be5c5d3293dcabec2d170ed80406941fa252f0a3d9
+ languageName: node
+ linkType: hard
+
+"@metaplex-foundation/umi-serializers-numbers@npm:^0.8.9":
+ version: 0.8.9
+ resolution: "@metaplex-foundation/umi-serializers-numbers@npm:0.8.9"
+ dependencies:
+ "@metaplex-foundation/umi-serializers-core": "npm:^0.8.9"
+ checksum: 10c0/3dcea7c90ab5c97e631a20d1a73f7fec7f5767200187b79ea6c1ee9763f17ae14cb66f401409662e83f406e7c40b980db62c87506711b95c3be67dd52ab1d2bb
+ languageName: node
+ linkType: hard
+
+"@metaplex-foundation/umi-serializers@npm:^0.8.5, @metaplex-foundation/umi-serializers@npm:^0.8.9":
+ version: 0.8.9
+ resolution: "@metaplex-foundation/umi-serializers@npm:0.8.9"
+ dependencies:
+ "@metaplex-foundation/umi-options": "npm:^0.8.9"
+ "@metaplex-foundation/umi-public-keys": "npm:^0.8.9"
+ "@metaplex-foundation/umi-serializers-core": "npm:^0.8.9"
+ "@metaplex-foundation/umi-serializers-encodings": "npm:^0.8.9"
+ "@metaplex-foundation/umi-serializers-numbers": "npm:^0.8.9"
+ checksum: 10c0/5fd0ad628ca942b8c24860f6c7af76ffa5256c61171e4a2e50b08e56beab776870a6fb4e6f4110f256d84d2c2b74bd8cf0c95644c2851c018305e3b11f682a8a
+ languageName: node
+ linkType: hard
+
+"@metaplex-foundation/umi@npm:^0.8.6":
+ version: 0.8.10
+ resolution: "@metaplex-foundation/umi@npm:0.8.10"
+ dependencies:
+ "@metaplex-foundation/umi-options": "npm:^0.8.9"
+ "@metaplex-foundation/umi-public-keys": "npm:^0.8.9"
+ "@metaplex-foundation/umi-serializers": "npm:^0.8.9"
+ checksum: 10c0/3c70661a381fc07cff20da78f5aed8f85d4c969f8d652c922459ae2782c7bcca6333cc1dd8702f4163f3accaebdae1bb9c9c01a8b633ac0a3f8b0e2cf40a1714
+ languageName: node
+ linkType: hard
+
"@mjackson/node-fetch-server@npm:^0.2.0":
version: 0.2.0
resolution: "@mjackson/node-fetch-server@npm:0.2.0"
@@ -8762,7 +8903,7 @@ __metadata:
languageName: node
linkType: hard
-"@noble/curves@npm:1.9.7, @noble/curves@npm:^1.1.0, @noble/curves@npm:^1.4.2, @noble/curves@npm:^1.6.0, @noble/curves@npm:^1.7.0, @noble/curves@npm:^1.9.2, @noble/curves@npm:^1.9.7, @noble/curves@npm:~1.9.0":
+"@noble/curves@npm:1.9.7, @noble/curves@npm:^1.1.0, @noble/curves@npm:^1.4.2, @noble/curves@npm:^1.6.0, @noble/curves@npm:^1.7.0, @noble/curves@npm:^1.8.0, @noble/curves@npm:^1.9.2, @noble/curves@npm:^1.9.7, @noble/curves@npm:~1.9.0":
version: 1.9.7
resolution: "@noble/curves@npm:1.9.7"
dependencies:
@@ -10221,6 +10362,7 @@ __metadata:
"@oko-wallet/oko-sdk-core": "npm:^0.0.6-rc.130"
"@oko-wallet/oko-sdk-cosmos": "npm:^0.0.6-rc.157"
"@oko-wallet/oko-sdk-eth": "npm:^0.0.6-rc.145"
+ "@oko-wallet/oko-sdk-sol": "workspace:*"
"@oko-wallet/stdlib-js": "npm:^0.0.2-rc.44"
"@types/node": "npm:^24.10.1"
"@types/react": "npm:^19.2.7"
@@ -10311,7 +10453,7 @@ __metadata:
languageName: unknown
linkType: soft
-"@oko-wallet/frost-ed25519-keplr-wasm@workspace:crypto/teddsa/frost_ed25519_keplr_wasm":
+"@oko-wallet/frost-ed25519-keplr-wasm@workspace:*, @oko-wallet/frost-ed25519-keplr-wasm@workspace:crypto/teddsa/frost_ed25519_keplr_wasm":
version: 0.0.0-use.local
resolution: "@oko-wallet/frost-ed25519-keplr-wasm@workspace:crypto/teddsa/frost_ed25519_keplr_wasm"
dependencies:
@@ -10597,6 +10739,7 @@ __metadata:
"@oko-wallet/cait-sith-keplr-wasm": "workspace:*"
"@oko-wallet/crypto-js": "npm:^0.0.2-alpha.8"
"@oko-wallet/dotenv": "npm:^0.0.2-alpha.29"
+ "@oko-wallet/frost-ed25519-keplr-wasm": "workspace:*"
"@oko-wallet/ksn-interface": "npm:^0.0.2-alpha.60"
"@oko-wallet/oko-common-ui": "workspace:*"
"@oko-wallet/oko-sdk-core": "npm:^0.0.6-rc.130"
@@ -10604,6 +10747,11 @@ __metadata:
"@oko-wallet/oko-sdk-eth": "npm:^0.0.6-rc.145"
"@oko-wallet/stdlib-js": "npm:^0.0.2-rc.44"
"@oko-wallet/tecdsa-interface": "npm:0.0.2-alpha.22"
+ "@oko-wallet/teddsa-hooks": "workspace:*"
+ "@oko-wallet/teddsa-interface": "workspace:*"
+ "@solana/web3.js": "npm:^1.98.0"
+ "@solanafm/explorer-kit": "npm:^1.2.0"
+ "@solanafm/explorer-kit-idls": "npm:^1.1.4"
"@tanstack/react-query": "npm:^5.90.12"
"@tanstack/react-router": "npm:^1.136.8"
"@tanstack/react-router-devtools": "npm:^1.136.8"
@@ -10615,6 +10763,7 @@ __metadata:
"@vitejs/plugin-react": "npm:^5.0.3"
auth0-js: "npm:^9.29.0"
bitcoinjs-lib: "npm:^6.1.7"
+ bs58: "npm:^6.0.0"
chalk: "npm:^5.5.0"
cosmjs-types: "npm:^0.9.0"
del-cli: "npm:^6.0.0"
@@ -10880,13 +11029,22 @@ __metadata:
"@rollup/plugin-commonjs": "npm:^25.0.0"
"@rollup/plugin-node-resolve": "npm:^15.0.0"
"@rollup/plugin-typescript": "npm:^11.0.0"
+ "@solana/wallet-standard-features": "npm:^1.3.0"
"@solana/web3.js": "npm:^1.98.0"
+ "@types/jest": "npm:^29.5.14"
"@types/node": "npm:^24.10.1"
+ "@types/uuid": "npm:^10.0.0"
+ "@wallet-standard/base": "npm:^1.1.0"
+ "@wallet-standard/features": "npm:^1.1.0"
+ "@wallet-standard/wallet": "npm:^1.1.0"
+ bs58: "npm:^6.0.0"
del-cli: "npm:^6.0.0"
eventemitter3: "npm:^5.0.1"
+ jest: "npm:^30.1.3"
rollup: "npm:^4.0.0"
rollup-plugin-dts: "npm:^6.2.1"
rollup-plugin-tsconfig-paths: "npm:^1.5.2"
+ ts-jest: "npm:^29.4.5"
tsc-alias: "npm:^1.8.16"
typescript: "npm:^5.8.3"
uuid: "npm:^9.0.0"
@@ -11012,7 +11170,11 @@ __metadata:
resolution: "@oko-wallet/sandbox-sol@workspace:sandbox/sandbox_sol"
dependencies:
"@oko-wallet/oko-sdk-sol": "workspace:*"
+ "@solana/wallet-adapter-base": "npm:^0.9.23"
+ "@solana/wallet-adapter-react": "npm:^0.15.35"
+ "@solana/wallet-adapter-react-ui": "npm:^0.9.35"
"@solana/web3.js": "npm:^1.98.0"
+ "@tailwindcss/postcss": "npm:^4"
"@types/node": "npm:^22.0.0"
"@types/react": "npm:^19.0.0"
"@types/react-dom": "npm:^19.0.0"
@@ -11020,6 +11182,7 @@ __metadata:
next: "npm:^16.1.1"
react: "npm:^19.0.0"
react-dom: "npm:^19.0.0"
+ tailwindcss: "npm:^4.1.3"
typescript: "npm:^5.8.3"
zustand: "npm:^5.0.0"
languageName: unknown
@@ -11139,11 +11302,12 @@ __metadata:
languageName: unknown
linkType: soft
-"@oko-wallet/teddsa-hooks@workspace:crypto/teddsa/teddsa_hooks":
+"@oko-wallet/teddsa-hooks@workspace:*, @oko-wallet/teddsa-hooks@workspace:crypto/teddsa/teddsa_hooks":
version: 0.0.0-use.local
resolution: "@oko-wallet/teddsa-hooks@workspace:crypto/teddsa/teddsa_hooks"
dependencies:
"@oko-wallet/bytes": "npm:^0.0.3-alpha.62"
+ "@oko-wallet/frost-ed25519-keplr-wasm": "workspace:*"
"@oko-wallet/stdlib-js": "npm:^0.0.2-rc.42"
"@oko-wallet/teddsa-interface": "workspace:*"
"@oko-wallet/teddsa-wasm-mock": "workspace:*"
@@ -13111,6 +13275,17 @@ __metadata:
languageName: node
linkType: hard
+"@react-native-async-storage/async-storage@npm:^1.17.7":
+ version: 1.24.0
+ resolution: "@react-native-async-storage/async-storage@npm:1.24.0"
+ dependencies:
+ merge-options: "npm:^3.0.4"
+ peerDependencies:
+ react-native: ^0.0.0-0 || >=0.60 <1.0
+ checksum: 10c0/cad2098ef84251f2ab8ebc07b750e585a20ac7ca07f26e5441e957a76f2b66f01d10ef5fbddb63d675431377b31beb5208548093e1eb17d262d2184b51133f4d
+ languageName: node
+ linkType: hard
+
"@react-router/dev@npm:^7.4.0":
version: 7.11.0
resolution: "@react-router/dev@npm:7.11.0"
@@ -15364,6 +15539,69 @@ __metadata:
languageName: node
linkType: hard
+"@solana-mobile/mobile-wallet-adapter-protocol-web3js@npm:^2.2.5":
+ version: 2.2.5
+ resolution: "@solana-mobile/mobile-wallet-adapter-protocol-web3js@npm:2.2.5"
+ dependencies:
+ "@solana-mobile/mobile-wallet-adapter-protocol": "npm:^2.2.5"
+ bs58: "npm:^5.0.0"
+ js-base64: "npm:^3.7.5"
+ peerDependencies:
+ "@solana/web3.js": ^1.58.0
+ checksum: 10c0/04f870e56d28252961de63eb940211bfd8571f26e2b5cdacdc1fd4fb05322366f4528df03e5208c6b937dd1ba61f71e35eb7b9d665ae5e06b29fc9ab08fdb8eb
+ languageName: node
+ linkType: hard
+
+"@solana-mobile/mobile-wallet-adapter-protocol@npm:^2.2.5":
+ version: 2.2.5
+ resolution: "@solana-mobile/mobile-wallet-adapter-protocol@npm:2.2.5"
+ dependencies:
+ "@solana/codecs-strings": "npm:^4.0.0"
+ "@solana/wallet-standard": "npm:^1.1.2"
+ "@solana/wallet-standard-util": "npm:^1.1.1"
+ "@wallet-standard/core": "npm:^1.0.3"
+ js-base64: "npm:^3.7.5"
+ peerDependencies:
+ react-native: ">0.69"
+ checksum: 10c0/98d82eae9f613b383a64aa0430211bc5e76c336ab410255280ed441a01742d0982c198e8ec32d2c440869d29c7c8ffa7677134059516a6f42cf740db5c228049
+ languageName: node
+ linkType: hard
+
+"@solana-mobile/wallet-adapter-mobile@npm:^2.2.0":
+ version: 2.2.5
+ resolution: "@solana-mobile/wallet-adapter-mobile@npm:2.2.5"
+ dependencies:
+ "@react-native-async-storage/async-storage": "npm:^1.17.7"
+ "@solana-mobile/mobile-wallet-adapter-protocol-web3js": "npm:^2.2.5"
+ "@solana-mobile/wallet-standard-mobile": "npm:^0.4.3"
+ "@solana/wallet-adapter-base": "npm:^0.9.23"
+ "@solana/wallet-standard-features": "npm:^1.2.0"
+ js-base64: "npm:^3.7.5"
+ peerDependencies:
+ "@solana/web3.js": ^1.58.0
+ dependenciesMeta:
+ "@react-native-async-storage/async-storage":
+ optional: true
+ checksum: 10c0/20e0cf906b7cc657e1886b7cadc18c48ff1f54c7140a36639694a832da8ad4b59970fea479d8c04d28d31248429a942e1d8d60481bc6462bd6087c657d382c96
+ languageName: node
+ linkType: hard
+
+"@solana-mobile/wallet-standard-mobile@npm:^0.4.3":
+ version: 0.4.4
+ resolution: "@solana-mobile/wallet-standard-mobile@npm:0.4.4"
+ dependencies:
+ "@solana-mobile/mobile-wallet-adapter-protocol": "npm:^2.2.5"
+ "@solana/wallet-standard-chains": "npm:^1.1.0"
+ "@solana/wallet-standard-features": "npm:^1.2.0"
+ "@wallet-standard/base": "npm:^1.0.1"
+ "@wallet-standard/features": "npm:^1.0.3"
+ bs58: "npm:^5.0.0"
+ js-base64: "npm:^3.7.5"
+ qrcode: "npm:^1.5.4"
+ checksum: 10c0/aad8ca944a3edfd988b4cc6207107336f09df8f88d752550336473676e0b65ff18e664c1157a0ae5cd1763b3b13d6657fe8a51645d69f5ccb4a0772d28083243
+ languageName: node
+ linkType: hard
+
"@solana/buffer-layout@npm:^4.0.1":
version: 4.0.1
resolution: "@solana/buffer-layout@npm:4.0.1"
@@ -15384,6 +15622,29 @@ __metadata:
languageName: node
linkType: hard
+"@solana/codecs-core@npm:4.0.0":
+ version: 4.0.0
+ resolution: "@solana/codecs-core@npm:4.0.0"
+ dependencies:
+ "@solana/errors": "npm:4.0.0"
+ peerDependencies:
+ typescript: ">=5.3.3"
+ checksum: 10c0/2ca06ed1b57fc6d762cce784d4ef81a2774e47cfcf54815e9ddcb338452c1b2987227f51a7906cd87cd4d78331815ad2d19beeb295fe211419d7a87bc9ef29ab
+ languageName: node
+ linkType: hard
+
+"@solana/codecs-numbers@npm:4.0.0":
+ version: 4.0.0
+ resolution: "@solana/codecs-numbers@npm:4.0.0"
+ dependencies:
+ "@solana/codecs-core": "npm:4.0.0"
+ "@solana/errors": "npm:4.0.0"
+ peerDependencies:
+ typescript: ">=5.3.3"
+ checksum: 10c0/59346de9493431b2d0dada2851c7a1c0ad62369d81eb45a64db0a56d1723c2dcc40baee282874652ac10728a07ae30bb4169458efd47c88c4c6b2c9dc81e4747
+ languageName: node
+ linkType: hard
+
"@solana/codecs-numbers@npm:^2.1.0":
version: 2.3.0
resolution: "@solana/codecs-numbers@npm:2.3.0"
@@ -15396,6 +15657,20 @@ __metadata:
languageName: node
linkType: hard
+"@solana/codecs-strings@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "@solana/codecs-strings@npm:4.0.0"
+ dependencies:
+ "@solana/codecs-core": "npm:4.0.0"
+ "@solana/codecs-numbers": "npm:4.0.0"
+ "@solana/errors": "npm:4.0.0"
+ peerDependencies:
+ fastestsmallesttextencoderdecoder: ^1.0.22
+ typescript: ">=5.3.3"
+ checksum: 10c0/bfb9f8dc99595ce101891030be81b4e6408c6693de087af5a74a0d43232325c40426361c964e7d4df840488976c1cbba9f5ab961d4308612976d52d987fc9647
+ languageName: node
+ linkType: hard
+
"@solana/errors@npm:2.3.0":
version: 2.3.0
resolution: "@solana/errors@npm:2.3.0"
@@ -15410,7 +15685,179 @@ __metadata:
languageName: node
linkType: hard
-"@solana/web3.js@npm:^1.98.0, @solana/web3.js@npm:^1.98.4":
+"@solana/errors@npm:4.0.0":
+ version: 4.0.0
+ resolution: "@solana/errors@npm:4.0.0"
+ dependencies:
+ chalk: "npm:5.6.2"
+ commander: "npm:14.0.1"
+ peerDependencies:
+ typescript: ">=5.3.3"
+ bin:
+ errors: bin/cli.mjs
+ checksum: 10c0/a6315e0f86b3ac27ecb7f7381e244351daf15e0bbc93b6cf9eb6455ef603c3c933d3c644479d1ba5952b9a9d7557084c66f4a6e376035bf5cebcf7792ff58c85
+ languageName: node
+ linkType: hard
+
+"@solana/spl-type-length-value@npm:^0.1.0":
+ version: 0.1.0
+ resolution: "@solana/spl-type-length-value@npm:0.1.0"
+ dependencies:
+ buffer: "npm:^6.0.3"
+ checksum: 10c0/a8f2fd6308dffa27827799146857a778ff807380578e187023f8fe90ebf8a68ed1f9f74a0c196cde7b757ea188ff2af040a727c18bb3c86a82f62fe3ec4c43bb
+ languageName: node
+ linkType: hard
+
+"@solana/wallet-adapter-base-ui@npm:^0.1.6":
+ version: 0.1.6
+ resolution: "@solana/wallet-adapter-base-ui@npm:0.1.6"
+ dependencies:
+ "@solana/wallet-adapter-react": "npm:^0.15.39"
+ peerDependencies:
+ "@solana/web3.js": ^1.98.0
+ react: "*"
+ checksum: 10c0/c39a54bddaeab67a4866b4537b4e6238c3e92cba3e3a7883de54602f159e0c412dd485a54f9febcc8a9e247fbe2a0b0d6cd579abd97d9921d4a9d23598165535
+ languageName: node
+ linkType: hard
+
+"@solana/wallet-adapter-base@npm:^0.9.23, @solana/wallet-adapter-base@npm:^0.9.27":
+ version: 0.9.27
+ resolution: "@solana/wallet-adapter-base@npm:0.9.27"
+ dependencies:
+ "@solana/wallet-standard-features": "npm:^1.3.0"
+ "@wallet-standard/base": "npm:^1.1.0"
+ "@wallet-standard/features": "npm:^1.1.0"
+ eventemitter3: "npm:^5.0.1"
+ peerDependencies:
+ "@solana/web3.js": ^1.98.0
+ checksum: 10c0/b9df73f96f068bafb6cd021f61ab839303093f144a9c73a4a4f7a8de929038207153bb15e4e7fefa703ba37938049adc01311a724a83214a1e30c9b58ce4590b
+ languageName: node
+ linkType: hard
+
+"@solana/wallet-adapter-react-ui@npm:^0.9.35":
+ version: 0.9.39
+ resolution: "@solana/wallet-adapter-react-ui@npm:0.9.39"
+ dependencies:
+ "@solana/wallet-adapter-base": "npm:^0.9.27"
+ "@solana/wallet-adapter-base-ui": "npm:^0.1.6"
+ "@solana/wallet-adapter-react": "npm:^0.15.39"
+ peerDependencies:
+ "@solana/web3.js": ^1.98.0
+ react: "*"
+ react-dom: "*"
+ checksum: 10c0/95264c0c538d352e67eebc5c381d46b7dfb670f9d35b4a5eb69648d71363c6202755b93f10c03202b2503a9cd3a4862f7561eddadcce5f01489058a7c37edaa7
+ languageName: node
+ linkType: hard
+
+"@solana/wallet-adapter-react@npm:^0.15.35, @solana/wallet-adapter-react@npm:^0.15.39":
+ version: 0.15.39
+ resolution: "@solana/wallet-adapter-react@npm:0.15.39"
+ dependencies:
+ "@solana-mobile/wallet-adapter-mobile": "npm:^2.2.0"
+ "@solana/wallet-adapter-base": "npm:^0.9.27"
+ "@solana/wallet-standard-wallet-adapter-react": "npm:^1.1.4"
+ peerDependencies:
+ "@solana/web3.js": ^1.98.0
+ react: "*"
+ checksum: 10c0/55133c60383accf86aa76d608687013abcfaaef6fedf666ec6c1b8e27b43ee6b35b89eb50237b7852a12771107db671158692bd9d63255c6086b0b903d77859e
+ languageName: node
+ linkType: hard
+
+"@solana/wallet-standard-chains@npm:^1.1.0, @solana/wallet-standard-chains@npm:^1.1.1":
+ version: 1.1.1
+ resolution: "@solana/wallet-standard-chains@npm:1.1.1"
+ dependencies:
+ "@wallet-standard/base": "npm:^1.1.0"
+ checksum: 10c0/37aa21d56fab1851c3ff0dc434564a5b8e8608d95890db8126ae7723af58399ce69872a0bf8bc20704d03d837d8a53a8b70fb9229d0c1ffb2ed8ae8359ab4f21
+ languageName: node
+ linkType: hard
+
+"@solana/wallet-standard-core@npm:^1.1.2":
+ version: 1.1.2
+ resolution: "@solana/wallet-standard-core@npm:1.1.2"
+ dependencies:
+ "@solana/wallet-standard-chains": "npm:^1.1.1"
+ "@solana/wallet-standard-features": "npm:^1.3.0"
+ "@solana/wallet-standard-util": "npm:^1.1.2"
+ checksum: 10c0/708109bd34881f742697ec8b314d962a7a3b7d24ad31de831dee01e28805b219b835598787936f9314fba98ced078ca74f040ac289b33d25c6282b8a3b09cd8b
+ languageName: node
+ linkType: hard
+
+"@solana/wallet-standard-features@npm:^1.2.0, @solana/wallet-standard-features@npm:^1.3.0":
+ version: 1.3.0
+ resolution: "@solana/wallet-standard-features@npm:1.3.0"
+ dependencies:
+ "@wallet-standard/base": "npm:^1.1.0"
+ "@wallet-standard/features": "npm:^1.1.0"
+ checksum: 10c0/a71f797d81103697eb2878ce301a493a95a057dd2b3daa92faeb7b53f3c5e0655f41de6ea5260571b34b1c819a74a42c72213ada714edb4fbd27c6cf85d4036d
+ languageName: node
+ linkType: hard
+
+"@solana/wallet-standard-util@npm:^1.1.1, @solana/wallet-standard-util@npm:^1.1.2":
+ version: 1.1.2
+ resolution: "@solana/wallet-standard-util@npm:1.1.2"
+ dependencies:
+ "@noble/curves": "npm:^1.8.0"
+ "@solana/wallet-standard-chains": "npm:^1.1.1"
+ "@solana/wallet-standard-features": "npm:^1.3.0"
+ checksum: 10c0/c249d34a35f6817c24e33d672ed497b42b1abc578cc523d7a8327878e3fdd94c455746c629f42447cd86720f63fb7624a9c1e6fcc8d2f116ecbf8c5af33a8ca0
+ languageName: node
+ linkType: hard
+
+"@solana/wallet-standard-wallet-adapter-base@npm:^1.1.4":
+ version: 1.1.4
+ resolution: "@solana/wallet-standard-wallet-adapter-base@npm:1.1.4"
+ dependencies:
+ "@solana/wallet-adapter-base": "npm:^0.9.23"
+ "@solana/wallet-standard-chains": "npm:^1.1.1"
+ "@solana/wallet-standard-features": "npm:^1.3.0"
+ "@solana/wallet-standard-util": "npm:^1.1.2"
+ "@wallet-standard/app": "npm:^1.1.0"
+ "@wallet-standard/base": "npm:^1.1.0"
+ "@wallet-standard/features": "npm:^1.1.0"
+ "@wallet-standard/wallet": "npm:^1.1.0"
+ peerDependencies:
+ "@solana/web3.js": ^1.98.0
+ bs58: ^6.0.0
+ checksum: 10c0/5fee810ba4afb61f5ede743f07596327fe5e93cee645d9c2b780e458adc12760c0ebfb0900c9dcd1551759f2df8c08daaac37bc64d65a9ba598c0c27754ffada
+ languageName: node
+ linkType: hard
+
+"@solana/wallet-standard-wallet-adapter-react@npm:^1.1.4":
+ version: 1.1.4
+ resolution: "@solana/wallet-standard-wallet-adapter-react@npm:1.1.4"
+ dependencies:
+ "@solana/wallet-standard-wallet-adapter-base": "npm:^1.1.4"
+ "@wallet-standard/app": "npm:^1.1.0"
+ "@wallet-standard/base": "npm:^1.1.0"
+ peerDependencies:
+ "@solana/wallet-adapter-base": "*"
+ react: "*"
+ checksum: 10c0/0854a56e6a78f8a26e0ec5c00bcce326cba1ecaedd6f9e2dadaf92a51a65d148407b9a25c1f3b2eff527e374c058d15bfebcf864fb5b12cf34e2487ba87e627d
+ languageName: node
+ linkType: hard
+
+"@solana/wallet-standard-wallet-adapter@npm:^1.1.4":
+ version: 1.1.4
+ resolution: "@solana/wallet-standard-wallet-adapter@npm:1.1.4"
+ dependencies:
+ "@solana/wallet-standard-wallet-adapter-base": "npm:^1.1.4"
+ "@solana/wallet-standard-wallet-adapter-react": "npm:^1.1.4"
+ checksum: 10c0/1b3cc630b0a9fff0d50a5695a482c17620b40c04e5a6238bda3d0e49d365d8642ccbf33f411e91af5758ed161a30c7da5c7732d765639c45fc16c4d823cea118
+ languageName: node
+ linkType: hard
+
+"@solana/wallet-standard@npm:^1.1.2":
+ version: 1.1.4
+ resolution: "@solana/wallet-standard@npm:1.1.4"
+ dependencies:
+ "@solana/wallet-standard-core": "npm:^1.1.2"
+ "@solana/wallet-standard-wallet-adapter": "npm:^1.1.4"
+ checksum: 10c0/1963bb08d5463bb4f5cb20d2b4cb75f50d8b34a023513765e45a231650cfbb3d19c49ab2ffc10b56cec97b3390e21638b28ac81e148560f344610634b68cf9d1
+ languageName: node
+ linkType: hard
+
+"@solana/web3.js@npm:^1.68.0, @solana/web3.js@npm:^1.98.0, @solana/web3.js@npm:^1.98.4":
version: 1.98.4
resolution: "@solana/web3.js@npm:1.98.4"
dependencies:
@@ -15433,6 +15880,54 @@ __metadata:
languageName: node
linkType: hard
+"@solanafm/explorer-kit-idls@npm:^1.1.4":
+ version: 1.1.4
+ resolution: "@solanafm/explorer-kit-idls@npm:1.1.4"
+ dependencies:
+ "@coral-xyz/anchor": "npm:^0.29.0"
+ "@coral-xyz/anchor-new": "npm:@coral-xyz/anchor@^0.30.0"
+ "@solanafm/kinobi-lite": "npm:^0.12.3"
+ axios: "npm:^1.3.3"
+ checksum: 10c0/69d353cdee9cf6d4337a7839bb48fbaa5c93f632a57a35c33c4c3c71bf250f40781f53939a1feec4b42c8903567e82ffdaf0147585e7f39012276c7c4cd67db4
+ languageName: node
+ linkType: hard
+
+"@solanafm/explorer-kit@npm:^1.2.0":
+ version: 1.2.0
+ resolution: "@solanafm/explorer-kit@npm:1.2.0"
+ dependencies:
+ "@coral-xyz/anchor": "npm:^0.29.0"
+ "@coral-xyz/anchor-new": "npm:@coral-xyz/anchor@^0.30.0"
+ "@metaplex-foundation/umi": "npm:^0.8.6"
+ "@metaplex-foundation/umi-serializers": "npm:^0.8.5"
+ "@solana/spl-type-length-value": "npm:^0.1.0"
+ "@solanafm/kinobi-lite": "npm:^0.12.3"
+ "@solanafm/utils": "npm:^1.0.5"
+ checksum: 10c0/8996e23592238406a84bd25878ba79ab5b23ed312ddc53844a38bca1b27c291d1a49d47e4d1a2453f77c541986276ce451718e8afff8b53d3cbc334e8fe613d6
+ languageName: node
+ linkType: hard
+
+"@solanafm/kinobi-lite@npm:^0.12.3":
+ version: 0.12.4
+ resolution: "@solanafm/kinobi-lite@npm:0.12.4"
+ dependencies:
+ "@noble/hashes": "npm:^1.1.5"
+ checksum: 10c0/8fba806f2f72d27f5990cd61efa4302c11ecba8198e2f5c4c369931bfffe4c5d30c44053a58b7aec331947e1faa355c789b7cf64c8f767fd435f30c6d81450f4
+ languageName: node
+ linkType: hard
+
+"@solanafm/utils@npm:^1.0.5":
+ version: 1.1.0
+ resolution: "@solanafm/utils@npm:1.1.0"
+ dependencies:
+ "@metaplex-foundation/umi-options": "npm:^0.8.5"
+ bn.js: "npm:^5.2.1"
+ bs58: "npm:^5.0.0"
+ dayjs: "npm:^1.11.7"
+ checksum: 10c0/651453c9241c5015e26a3fd7ce30ecfb64c95cc2e48871ccd0c68246a2a7fc41e0abe79cc2062df6c48dbd5cebb20ec1e28556313d550e0e90dcb68191ccdd3c
+ languageName: node
+ linkType: hard
+
"@standard-schema/spec@npm:^1.0.0":
version: 1.1.0
resolution: "@standard-schema/spec@npm:1.1.0"
@@ -17100,6 +17595,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/uuid@npm:^10.0.0":
+ version: 10.0.0
+ resolution: "@types/uuid@npm:10.0.0"
+ checksum: 10c0/9a1404bf287164481cb9b97f6bb638f78f955be57c40c6513b7655160beb29df6f84c915aaf4089a1559c216557dc4d2f79b48d978742d3ae10b937420ddac60
+ languageName: node
+ linkType: hard
+
"@types/uuid@npm:^8.3.4":
version: 8.3.4
resolution: "@types/uuid@npm:8.3.4"
@@ -17701,6 +18203,65 @@ __metadata:
languageName: node
linkType: hard
+"@wallet-standard/app@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "@wallet-standard/app@npm:1.1.0"
+ dependencies:
+ "@wallet-standard/base": "npm:^1.1.0"
+ checksum: 10c0/04650f92d512493f4556cbf48e49626745a0fe55633b03a96d99698e415d5e66114733ba3cff25867b9f89ef607f5755b0ad964a914e8b43f94df508be6998d0
+ languageName: node
+ linkType: hard
+
+"@wallet-standard/base@npm:^1.0.1, @wallet-standard/base@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "@wallet-standard/base@npm:1.1.0"
+ checksum: 10c0/4cae344d5a74ba4b7d063b649b191f2267bd11ea9573ebb9e78874163c03b58e3ec531bb296d0a8d7941bc09231761d97afb4c6ca8c0dc399c81d39884b4e408
+ languageName: node
+ linkType: hard
+
+"@wallet-standard/core@npm:^1.0.3":
+ version: 1.1.1
+ resolution: "@wallet-standard/core@npm:1.1.1"
+ dependencies:
+ "@wallet-standard/app": "npm:^1.1.0"
+ "@wallet-standard/base": "npm:^1.1.0"
+ "@wallet-standard/errors": "npm:^0.1.1"
+ "@wallet-standard/features": "npm:^1.1.0"
+ "@wallet-standard/wallet": "npm:^1.1.0"
+ checksum: 10c0/fb0398869de6858df1ce0e1abe4f9fb59c52f98eae5a493d908c5b961a59d4ce94b2fe937e4395408ab1dbd3de921710d7441addc5ce14366c4747fc77286cf8
+ languageName: node
+ linkType: hard
+
+"@wallet-standard/errors@npm:^0.1.1":
+ version: 0.1.1
+ resolution: "@wallet-standard/errors@npm:0.1.1"
+ dependencies:
+ chalk: "npm:^5.4.1"
+ commander: "npm:^13.1.0"
+ bin:
+ errors: bin/cli.mjs
+ checksum: 10c0/03b7519b92c6058f1c1c20dee46ba5d35b890ed66b56708d18b9c94fc1b746384c270a49097d3b705e2272903234509133dd6aa1680a524d6c410c5e4ca95f83
+ languageName: node
+ linkType: hard
+
+"@wallet-standard/features@npm:^1.0.3, @wallet-standard/features@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "@wallet-standard/features@npm:1.1.0"
+ dependencies:
+ "@wallet-standard/base": "npm:^1.1.0"
+ checksum: 10c0/9df265b02c0ed7a19da6410e8379baba701f51486324b0eefb0f79f988cc7114dc3b8c97dc1250f76bb546706d242413d99e45c4fc55f67778850366e885d047
+ languageName: node
+ linkType: hard
+
+"@wallet-standard/wallet@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "@wallet-standard/wallet@npm:1.1.0"
+ dependencies:
+ "@wallet-standard/base": "npm:^1.1.0"
+ checksum: 10c0/aa53460568f209d4e38030ee5e98d4f6ea6fec159a1e7fb5a3ee81cf8d91c89f0be86b7188dbf0bb9803d10608bf88bd824f73cd6800823279738827304038e5
+ languageName: node
+ linkType: hard
+
"@walletconnect/core@npm:2.21.0":
version: 2.21.0
resolution: "@walletconnect/core@npm:2.21.0"
@@ -19475,7 +20036,7 @@ __metadata:
languageName: node
linkType: hard
-"axios@npm:^1.12.0":
+"axios@npm:^1.12.0, axios@npm:^1.3.3":
version: 1.13.2
resolution: "axios@npm:1.13.2"
dependencies:
@@ -20376,6 +20937,13 @@ __metadata:
languageName: node
linkType: hard
+"buffer-layout@npm:^1.2.0, buffer-layout@npm:^1.2.2":
+ version: 1.2.2
+ resolution: "buffer-layout@npm:1.2.2"
+ checksum: 10c0/d90d1f622f592553555dd290d0e6dd0bababb2566655d0728812b2667af5a23d795929c38c25f5065252024fa29d75ea54eeb6f469d69814f4ebf614c6672acf
+ languageName: node
+ linkType: hard
+
"buffer-xor@npm:^1.0.3":
version: 1.0.3
resolution: "buffer-xor@npm:1.0.3"
@@ -20759,6 +21327,13 @@ __metadata:
languageName: node
linkType: hard
+"chalk@npm:5.6.2, chalk@npm:^5.0.1, chalk@npm:^5.2.0, chalk@npm:^5.4.1, chalk@npm:^5.5.0, chalk@npm:^5.6.2":
+ version: 5.6.2
+ resolution: "chalk@npm:5.6.2"
+ checksum: 10c0/99a4b0f0e7991796b1e7e3f52dceb9137cae2a9dfc8fc0784a550dc4c558e15ab32ed70b14b21b52beb2679b4892b41a0aa44249bcb996f01e125d58477c6976
+ languageName: node
+ linkType: hard
+
"chalk@npm:^4.0.0, chalk@npm:^4.1.0, chalk@npm:^4.1.2":
version: 4.1.2
resolution: "chalk@npm:4.1.2"
@@ -20769,13 +21344,6 @@ __metadata:
languageName: node
linkType: hard
-"chalk@npm:^5.0.1, chalk@npm:^5.2.0, chalk@npm:^5.4.1, chalk@npm:^5.5.0, chalk@npm:^5.6.2":
- version: 5.6.2
- resolution: "chalk@npm:5.6.2"
- checksum: 10c0/99a4b0f0e7991796b1e7e3f52dceb9137cae2a9dfc8fc0784a550dc4c558e15ab32ed70b14b21b52beb2679b4892b41a0aa44249bcb996f01e125d58477c6976
- languageName: node
- linkType: hard
-
"char-regex@npm:^1.0.2":
version: 1.0.2
resolution: "char-regex@npm:1.0.2"
@@ -21400,6 +21968,13 @@ __metadata:
languageName: node
linkType: hard
+"commander@npm:14.0.1":
+ version: 14.0.1
+ resolution: "commander@npm:14.0.1"
+ checksum: 10c0/64439c0651ddd01c1d0f48c8f08e97c18a0a1fa693879451f1203ad01132af2c2aa85da24cf0d8e098ab9e6dc385a756be670d2999a3c628ec745c3ec124587b
+ languageName: node
+ linkType: hard
+
"commander@npm:^10.0.0":
version: 10.0.1
resolution: "commander@npm:10.0.1"
@@ -21414,6 +21989,13 @@ __metadata:
languageName: node
linkType: hard
+"commander@npm:^13.1.0":
+ version: 13.1.0
+ resolution: "commander@npm:13.1.0"
+ checksum: 10c0/7b8c5544bba704fbe84b7cab2e043df8586d5c114a4c5b607f83ae5060708940ed0b5bd5838cf8ce27539cde265c1cbd59ce3c8c6b017ed3eec8943e3a415164
+ languageName: node
+ linkType: hard
+
"commander@npm:^14.0.0":
version: 14.0.2
resolution: "commander@npm:14.0.2"
@@ -22067,6 +22649,13 @@ __metadata:
languageName: node
linkType: hard
+"crypto-hash@npm:^1.3.0":
+ version: 1.3.0
+ resolution: "crypto-hash@npm:1.3.0"
+ checksum: 10c0/651003421dce76fd686eb3ed4981117f9cd3f309a4af41697a646aecbbfc49179547bdd7146f7a74e2c8c7ab703227c25ce03e85fea2da3189ab65e32971ecce
+ languageName: node
+ linkType: hard
+
"crypto-js@npm:4.2.0, crypto-js@npm:^4.0.0, crypto-js@npm:^4.2.0":
version: 4.2.0
resolution: "crypto-js@npm:4.2.0"
@@ -22587,7 +23176,7 @@ __metadata:
languageName: node
linkType: hard
-"dayjs@npm:^1.11.11, dayjs@npm:^1.11.18":
+"dayjs@npm:^1.11.11, dayjs@npm:^1.11.18, dayjs@npm:^1.11.7":
version: 1.11.19
resolution: "dayjs@npm:1.11.19"
checksum: 10c0/7d8a6074a343f821f81ea284d700bd34ea6c7abbe8d93bce7aba818948957c1b7f56131702e5e890a5622cdfc05dcebe8aed0b8313bdc6838a594d7846b0b000
@@ -28867,6 +29456,13 @@ __metadata:
languageName: node
linkType: hard
+"js-base64@npm:^3.7.5":
+ version: 3.7.8
+ resolution: "js-base64@npm:3.7.8"
+ checksum: 10c0/a4452a7e7f32b0ef568a344157efec00c14593bbb1cf0c113f008dddff7ec515b35147af0cd70a7735adb69a2a2bdee921adffea2ea465e2c856ba50d649b11e
+ languageName: node
+ linkType: hard
+
"js-beautify@npm:^1.6.4":
version: 1.15.4
resolution: "js-beautify@npm:1.15.4"
@@ -30828,6 +31424,15 @@ __metadata:
languageName: node
linkType: hard
+"merge-options@npm:^3.0.4":
+ version: 3.0.4
+ resolution: "merge-options@npm:3.0.4"
+ dependencies:
+ is-plain-obj: "npm:^2.1.0"
+ checksum: 10c0/02b5891ceef09b0b497b5a0154c37a71784e68ed71b14748f6fd4258ffd3fe4ecd5cb0fd6f7cae3954fd11e7686c5cb64486daffa63c2793bbe8b614b61c7055
+ languageName: node
+ linkType: hard
+
"merge-stream@npm:^2.0.0":
version: 2.0.0
resolution: "merge-stream@npm:2.0.0"
@@ -33540,7 +34145,7 @@ __metadata:
languageName: node
linkType: hard
-"pako@npm:^2.0.4, pako@npm:^2.1.0":
+"pako@npm:^2.0.3, pako@npm:^2.0.4, pako@npm:^2.1.0":
version: 2.1.0
resolution: "pako@npm:2.1.0"
checksum: 10c0/8e8646581410654b50eb22a5dfd71159cae98145bd5086c9a7a816ec0370b5f72b4648d08674624b3870a521e6a3daffd6c2f7bc00fdefc7063c9d8232ff5116
@@ -35656,6 +36261,19 @@ __metadata:
languageName: node
linkType: hard
+"qrcode@npm:^1.5.4":
+ version: 1.5.4
+ resolution: "qrcode@npm:1.5.4"
+ dependencies:
+ dijkstrajs: "npm:^1.0.1"
+ pngjs: "npm:^5.0.0"
+ yargs: "npm:^15.3.1"
+ bin:
+ qrcode: bin/qrcode
+ checksum: 10c0/ae1d57c9cff6099639a590b432c71b15e3bd3905ce4353e6d00c95dee6bb769a8f773f6a7575ecc1b8ed476bf79c5138a4a65cb380c682de3b926d7205d34d10
+ languageName: node
+ linkType: hard
+
"qs@npm:^6.10.1, qs@npm:^6.11.2, qs@npm:^6.12.3, qs@npm:~6.14.0":
version: 6.14.0
resolution: "qs@npm:6.14.0"
@@ -39078,6 +39696,13 @@ __metadata:
languageName: node
linkType: hard
+"superstruct@npm:^0.15.4":
+ version: 0.15.5
+ resolution: "superstruct@npm:0.15.5"
+ checksum: 10c0/73ae2043443dcc7868da6e8b4e4895410c79a88e021b514c665161199675ee920d5eadd85bb9dee5a9f515817e62f4b65a67ccb82d29f73259d012afcbcd3ce4
+ languageName: node
+ linkType: hard
+
"superstruct@npm:^1.0.3":
version: 1.0.4
resolution: "superstruct@npm:1.0.4"
@@ -39747,6 +40372,13 @@ __metadata:
languageName: node
linkType: hard
+"toml@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "toml@npm:3.0.0"
+ checksum: 10c0/8d7ed3e700ca602e5419fca343e1c595eb7aa177745141f0761a5b20874b58ee5c878cd045c408da9d130cb2b611c639912210ba96ce2f78e443569aa8060c18
+ languageName: node
+ linkType: hard
+
"totalist@npm:^3.0.0":
version: 3.0.1
resolution: "totalist@npm:3.0.1"