@magicblock-labs/gum-react-sdk
Version:
React SDK for Gum
543 lines (532 loc) • 20.9 kB
JavaScript
;
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
});