UNPKG

@magicblock-labs/gum-react-sdk

Version:
543 lines (532 loc) 20.9 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { GPLSESSION_PROGRAMS: () => import_gum_sdk3.GPLSESSION_PROGRAMS, GumProvider: () => GumProvider, SDK: () => import_gum_sdk3.SDK, SessionWalletProvider: () => SessionWalletProvider, useGum: () => useGum, useGumContext: () => useGumContext, useSessionKeyManager: () => useSessionKeyManager, useSessionWallet: () => useSessionWallet }); module.exports = __toCommonJS(src_exports); // src/hooks/session/useSessionKeyManager.ts var import_web3 = require("@solana/web3.js"); var import_react = require("react"); // src/utils/crypto.ts var CryptoJS = __toESM(require("crypto-js")); var ENCRYPTION_KEY_LENGTH = 32; function generateEncryptionKey() { const key = CryptoJS.lib.WordArray.random(ENCRYPTION_KEY_LENGTH); return CryptoJS.enc.Base64.stringify(key); } function encrypt(data, password) { const iv = CryptoJS.lib.WordArray.random(16); const passwordHash = CryptoJS.SHA256(password); const cipher = CryptoJS.AES.encrypt(data, passwordHash, { iv }); const encryptedData = cipher.ciphertext.toString(CryptoJS.enc.Base64); return `${encryptedData}.${iv.toString(CryptoJS.enc.Base64)}`; } function decrypt(data, password) { const [encryptedDataString, ivString] = data.split("."); const encryptedData = CryptoJS.enc.Base64.parse(encryptedDataString); const iv = CryptoJS.enc.Base64.parse(ivString); const passwordHash = CryptoJS.SHA256(password); const cipherParams = CryptoJS.lib.CipherParams.create({ ciphertext: encryptedData }); const decryptedData = CryptoJS.AES.decrypt(cipherParams, passwordHash, { iv }); return decryptedData.toString(CryptoJS.enc.Utf8); } // src/hooks/session/useSessionKeyManager.ts var import_gum_sdk = require("@magicblock-labs/gum-sdk"); var nacl = __toESM(require("tweetnacl")); var import_anchor = require("@coral-xyz/anchor"); // src/utils/indexedDB.ts var DB_NAME = "session_data"; var SESSION_OBJECT_STORE = "sessions"; var WALLET_PUBKEY_TO_SESSION_STORE = "walletPublicKeyToSessionData"; var ENCRYPTION_KEY_OBJECT_STORE = "user_preferences"; var openIndexedDB = async () => { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, 1); request.onupgradeneeded = (event) => { const db = request.result; db.createObjectStore(SESSION_OBJECT_STORE); db.createObjectStore(ENCRYPTION_KEY_OBJECT_STORE); db.createObjectStore(WALLET_PUBKEY_TO_SESSION_STORE); }; request.onsuccess = (event) => { resolve(request.result); }; request.onerror = (event) => { reject(request.error); }; }); }; var getItemFromIndexedDB = async (storeName, key) => { const db = await openIndexedDB(); return new Promise((resolve, reject) => { const transaction = db.transaction(storeName, "readonly"); const store = transaction.objectStore(storeName); const request = store.get(key); request.onsuccess = (event) => { resolve(request.result); }; request.onerror = (event) => { reject(request.error); }; }); }; var setItemToIndexedDB = async (storeName, data, key) => { const db = await openIndexedDB(); return new Promise((resolve, reject) => { const transaction = db.transaction(storeName, "readwrite"); const store = transaction.objectStore(storeName); const request = store.put(data, key); request.onsuccess = (event) => { resolve(); }; request.onerror = (event) => { reject(request.error); }; }); }; var deleteItemFromIndexedDB = async (storeName, key) => { const db = await openIndexedDB(); return new Promise((resolve, reject) => { const transaction = db.transaction(storeName, "readwrite"); const store = transaction.objectStore(storeName); const request = store.delete(key); request.onsuccess = (event) => { resolve(); }; request.onerror = (event) => { reject(request.error); }; }); }; // src/hooks/session/useSessionKeyManager.ts var SESSION_OBJECT_STORE2 = "sessions"; var WALLET_PUBKEY_TO_SESSION_STORE2 = "walletPublicKeyToSessionData"; var ENCRYPTION_KEY_OBJECT_STORE2 = "user_preferences"; function useSessionKeyManager(wallet, connection, cluster) { const keypairRef = (0, import_react.useRef)(null); const sessionTokenRef = (0, import_react.useRef)(null); const [, forceUpdate] = (0, import_react.useState)({}); const [isLoading, setIsLoading] = (0, import_react.useState)(false); const [error, setError] = (0, import_react.useState)(null); const sessionConnection = connection; const sdk = new import_gum_sdk.SessionTokenManager(wallet, connection); const generateKeypair = () => { keypairRef.current = import_web3.Keypair.generate(); }; const loadKeypairFromDecryptedSecret = (decryptedKeypair) => { keypairRef.current = import_web3.Keypair.fromSecretKey(new Uint8Array(Buffer.from(decryptedKeypair, "base64"))); }; const triggerRerender = () => { forceUpdate({}); }; (0, import_react.useEffect)(() => { if (!wallet) { return; } getSessionToken().then((token) => { if (!token) { resetSessionData(); } }).catch(() => { resetSessionData(); }); }, [wallet?.publicKey, cluster]); const deleteSessionData = async () => { try { const walletPublicKey = wallet.publicKey.toBase58(); const sessionKey = await getItemFromIndexedDB(WALLET_PUBKEY_TO_SESSION_STORE2, walletPublicKey); if (sessionKey) { await deleteItemFromIndexedDB(SESSION_OBJECT_STORE2, sessionKey); await deleteItemFromIndexedDB(WALLET_PUBKEY_TO_SESSION_STORE2, walletPublicKey); } await deleteItemFromIndexedDB(ENCRYPTION_KEY_OBJECT_STORE2, walletPublicKey); } catch (error2) { console.error("Error deleting session data:", error2); setError(error2); } }; const withLoading = async (asyncFunction) => { setError(null); setIsLoading(true); try { const result = await asyncFunction(); return result; } finally { setIsLoading(false); } }; const signTransaction = async (transaction, connection2, sendOptions) => { return withLoading(async () => { if (!keypairRef.current || !sessionTokenRef.current) { throw new Error("Cannot sign transaction - keypair or session token not loaded. Please create a session first."); } if (!connection2) { connection2 = sessionConnection; } const feePayer = keypairRef.current.publicKey; transaction.recentBlockhash = transaction.recentBlockhash || (await connection2.getLatestBlockhash({ commitment: sendOptions?.preflightCommitment, minContextSlot: sendOptions?.minContextSlot })).blockhash; transaction.feePayer = transaction.feePayer || feePayer; transaction.sign(keypairRef.current); return transaction; }); }; const signAllTransactions = async (transactions, connection2, sendOptions) => { return withLoading(async () => { return Promise.all(transactions.map((transaction) => signTransaction(transaction, connection2, sendOptions))); }); }; const signMessage = async (message) => { return withLoading(async () => { if (!keypairRef.current) { throw new Error("Cannot sign message - keypair not loaded. Please create a session first."); } return nacl.sign.detached(message, keypairRef.current.secretKey); }); }; const sendTransaction = async (transaction, connection2, options = {}) => { return withLoading(async () => { const keypair = keypairRef.current; const sessionToken = sessionTokenRef.current; if (!connection2) { connection2 = sessionConnection; } if (!keypair || !sessionToken) { throw new Error( "Cannot sign transaction - keypair or session token not loaded. Please create a session first." ); } const { signers, ...sendOptions } = options; const publicKey = keypair.publicKey; if (!publicKey) { throw new Error( "Cannot send transaction - keypair not loaded. Please create a session first." ); } transaction.feePayer = transaction.feePayer || publicKey; transaction.recentBlockhash = transaction.recentBlockhash || (await connection2.getLatestBlockhash({ commitment: sendOptions.preflightCommitment, minContextSlot: sendOptions.minContextSlot })).blockhash; if (signers?.length) { transaction.partialSign(...signers); } transaction = await signTransaction(transaction, connection2); const txid = await connection2.sendRawTransaction( transaction.serialize(), sendOptions ); return txid; }); }; const signAndSendTransaction = async (transaction, connection2, options = {}) => { return withLoading(async () => { if (!connection2) { connection2 = sessionConnection; } const transactionsArray = Array.isArray(transaction) ? transaction : [transaction]; const txids = await Promise.all(transactionsArray.map((signedTransaction) => sendTransaction(signedTransaction, connection2, options))); return txids; }); }; const getSessionToken = async () => { try { const sessionKey = await getItemFromIndexedDB(WALLET_PUBKEY_TO_SESSION_STORE2, wallet.publicKey.toString()); if (!sessionKey) { resetSessionData(); return null; } const encryptedSessionData = await getItemFromIndexedDB(SESSION_OBJECT_STORE2, sessionKey); const encryptionKey = await getItemFromIndexedDB(ENCRYPTION_KEY_OBJECT_STORE2, wallet.publicKey.toString()); if (!encryptedSessionData || !encryptionKey) { resetSessionData(); return null; } if (encryptedSessionData && encryptionKey) { const { encryptedToken, encryptedKeypair, validUntilTimestamp } = encryptedSessionData; const { userPreferences: storedEncryptionKey, validUntilTimestamp: encryptionKeyExpiry } = encryptionKey; const currentTimestamp = Math.ceil(Date.now() / 1e3); if (currentTimestamp > encryptionKeyExpiry || currentTimestamp > validUntilTimestamp) { await deleteSessionData(); return null; } const decryptedToken = decrypt(encryptedToken, storedEncryptionKey); const decryptedKeypair = decrypt(encryptedKeypair, storedEncryptionKey); loadKeypairFromDecryptedSecret(decryptedKeypair); sessionTokenRef.current = decryptedToken; triggerRerender(); return decryptedToken; } } catch (error2) { console.error("Error getting session data from IndexedDB:", error2); setError(error2); } return null; }; const createSession = async (targetProgramPublicKey, topUpLamports = 0, expiryInMinutes = 60, sessionCreatedCallback) => { const topUp = topUpLamports > 0; return withLoading(async () => { try { if (expiryInMinutes > 24 * 60) { throw new Error("Expiry cannot be more than 24 hours."); } if (!keypairRef.current) { generateKeypair(); } const expiryTimestamp = Math.ceil((Date.now() + expiryInMinutes * 60 * 1e3) / 1e3); const validUntilBN = new import_anchor.BN(expiryTimestamp); const topUpLamportsBN = topUp ? new import_anchor.BN(topUpLamports) : null; const sessionKeypair = keypairRef.current; if (!sessionKeypair) { throw new Error("Session keypair not generated."); } const sessionSignerPublicKey = sessionKeypair.publicKey; const instructionMethodBuilder = sdk.program.methods.createSession(topUp, validUntilBN, topUpLamportsBN).accounts({ targetProgram: targetProgramPublicKey, sessionSigner: sessionSignerPublicKey, authority: wallet.publicKey }); const pubKeys = await instructionMethodBuilder.pubkeys(); const sessionToken = pubKeys.sessionToken; await instructionMethodBuilder.signers([sessionKeypair]).rpc(); await deleteSessionData(); const encryptionKey = generateEncryptionKey(); const sessionTokenString = sessionToken.toBase58(); const keypairSecretBase64String = Buffer.from(sessionKeypair.secretKey).toString("base64"); const encryptedToken = encrypt(sessionTokenString, encryptionKey); const encryptedKeypair = encrypt(keypairSecretBase64String, encryptionKey); const encryptedSessionData = { encryptedToken, encryptedKeypair, validUntilTimestamp: expiryTimestamp }; await setItemToIndexedDB(SESSION_OBJECT_STORE2, encryptedSessionData, sessionTokenString); await setItemToIndexedDB(WALLET_PUBKEY_TO_SESSION_STORE2, sessionTokenString, wallet.publicKey.toBase58()); await setItemToIndexedDB(ENCRYPTION_KEY_OBJECT_STORE2, { "userPreferences": encryptionKey, validUntilTimestamp: expiryTimestamp }, wallet.publicKey.toBase58()); sessionTokenRef.current = sessionTokenString; triggerRerender(); if (!sessionTokenRef.current) { console.error("Session token not generated."); } if (sessionCreatedCallback) { sessionCreatedCallback({ sessionToken: sessionTokenRef.current, publicKey: sessionSignerPublicKey.toBase58() }); } return { ownerPublicKey: wallet.publicKey, isLoading: false, error: null, sessionToken: sessionTokenRef.current, publicKey: sessionSignerPublicKey, signMessage, signTransaction, signAllTransactions, signAndSendTransaction, sendTransaction }; } catch (error2) { console.error("Error creating session:", error2); setError(error2); return { publicKey: wallet.publicKey, ownerPublicKey: null, isLoading: false, error: error2.message, sessionToken: null, signTransaction: null, signAllTransactions: null, signMessage: null, sendTransaction: null, signAndSendTransaction: null, getSessionToken: null, createSession: null, revokeSession: null }; } }); }; const resetSessionData = () => { keypairRef.current = null; sessionTokenRef.current = null; triggerRerender(); }; const revokeSession = async () => { return withLoading(async () => { try { if (!sessionTokenRef.current || !keypairRef.current) { return; } const sessionTokenPublicKey = keypairRef.current.publicKey; const instructionMethodBuilder = sdk.program.methods.revokeSession().accounts({ // @ts-ignore sessionToken: new import_anchor.web3.PublicKey(sessionTokenRef.current), authority: wallet.publicKey, systemProgram: import_web3.SystemProgram.programId }); const txId = await instructionMethodBuilder.rpc(); const sessionSignerSolanaBalance = await connection.getBalance(sessionTokenPublicKey); if (sessionSignerSolanaBalance > 0) { const tx = new import_web3.Transaction().add( import_web3.SystemProgram.transfer({ fromPubkey: sessionTokenPublicKey, toPubkey: wallet.publicKey, lamports: sessionSignerSolanaBalance }) ); tx.feePayer = sessionTokenPublicKey; tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; const estimatedFee = await tx.getEstimatedFee(connection); if (estimatedFee && sessionSignerSolanaBalance > estimatedFee) { const transaction = new import_web3.Transaction().add( import_web3.SystemProgram.transfer({ fromPubkey: sessionTokenPublicKey, toPubkey: wallet.publicKey, lamports: sessionSignerSolanaBalance - estimatedFee }) ); await sendTransaction(transaction); } } const walletPublicKey = wallet.publicKey.toBase58(); await deleteItemFromIndexedDB(SESSION_OBJECT_STORE2, sessionTokenRef.current); await deleteItemFromIndexedDB(ENCRYPTION_KEY_OBJECT_STORE2, walletPublicKey); await deleteItemFromIndexedDB(WALLET_PUBKEY_TO_SESSION_STORE2, walletPublicKey); resetSessionData(); return txId; } catch (error2) { console.error("Error revoking session:", error2); setError(error2); return null; } }); }; if (!wallet) { return { publicKey: null, ownerPublicKey: null, isLoading: false, sessionToken: null, signTransaction: void 0, signAllTransactions: void 0, signMessage: void 0, sendTransaction: void 0, signAndSendTransaction: void 0, getSessionToken: async () => null, createSession: async () => void 0, revokeSession: async () => null, error }; } return { publicKey: sessionTokenRef.current && keypairRef.current ? keypairRef.current.publicKey : null, ownerPublicKey: wallet.publicKey, isLoading, error, sessionToken: sessionTokenRef.current, signTransaction, signAllTransactions, signMessage, sendTransaction, signAndSendTransaction, getSessionToken, createSession, revokeSession }; } // src/hooks/useGum.ts var import_react2 = require("react"); var import_gum_sdk2 = require("@magicblock-labs/gum-sdk"); var useGum = (wallet, connection, opts) => { const sdk = (0, import_react2.useMemo)(() => { return new import_gum_sdk2.SDK(wallet, connection, opts); }, [wallet]); return sdk; }; // src/providers/GumContext.tsx var React = __toESM(require("react")); var import_react3 = require("react"); var GumContext = (0, import_react3.createContext)(null); var GumProvider = ({ children, sdk }) => { return /* @__PURE__ */ React.createElement(GumContext.Provider, { value: { sdk } }, children); }; var useGumContext = () => { const context = (0, import_react3.useContext)(GumContext); if (!context) { throw new Error("useGumContext must be used within a GumProvider"); } return context; }; // src/providers/SessionWalletContext.ts var React2 = __toESM(require("react")); var import_react4 = require("react"); var SessionWalletContext = (0, import_react4.createContext)(null); var useSessionWallet = () => { const context = (0, import_react4.useContext)(SessionWalletContext); if (!context) { throw new Error("useSessionWallet must be used within a SessionWalletProvider"); } return context; }; var SessionWalletProvider = ({ sessionWallet, children }) => { const [sessionCreated, setSessionCreated] = (0, import_react4.useState)(false); React2.useEffect(() => { if (sessionWallet.sessionToken) { setSessionCreated(true); } }, [sessionWallet.sessionToken]); const value = { ...sessionWallet, sessionCreated, setSessionCreated }; return React2.createElement(SessionWalletContext.Provider, { value }, children); }; // src/index.ts var import_gum_sdk3 = require("@magicblock-labs/gum-sdk"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { GPLSESSION_PROGRAMS, GumProvider, SDK, SessionWalletProvider, useGum, useGumContext, useSessionKeyManager, useSessionWallet });