UNPKG

@keyban/sdk-react

Version:

Keyban SDK React simplifies the integration of Keyban's MPC wallet in React apps with TypeScript support, flexible storage, and Ethereum blockchain integration.

716 lines (710 loc) 23.1 kB
import { useSuspenseQuery, useSubscription } from '@apollo/client/react'; import { KeybanClient, SdkError, SdkErrorTypes, formatBalance } from '@keyban/sdk-base'; export * from '@keyban/sdk-base'; import { walletBalanceDocument, walletSubscriptionDocument, walletTokenBalancesDocument, GqlTokenBalancesOrderBy, tokenBalancesSubscriptionDocument, walletTokenBalanceDocument, walletNftsDocument, GqlNftBalancesOrderBy, nftBalancesSubscriptionDocument, walletNftDocument, walletAssetTransfersDocument, GqlAssetTransfersOrderBy, assetTransfersSubscriptionDocument, walletOrdersDocument, GqlOrdersOrderBy, ordersSubscriptionDocument, nftDocument, nftSubscriptionDocument, GqlMutationType } from '@keyban/sdk-base/graphql'; import React from 'react'; import { jsxs, jsx } from 'react/jsx-runtime'; import styles from './KeybanInput.module-2KPLL4WZ.module.css'; // src/account.ts var caches = /* @__PURE__ */ new WeakMap(); var CacheContext = React.createContext(/* @__PURE__ */ new Map()); function PromiseCacheProvider({ children }) { const client = useKeybanClient(); let cache = caches.get(client); if (!cache) { cache = /* @__PURE__ */ new Map(); caches.set(client, cache); } return React.createElement(CacheContext.Provider, { value: cache }, children); } var updateWrappedPromise = (wrapped) => { wrapped.promise.then((result) => { wrapped.status = 1 /* Fulfilled */; wrapped.value = result; return result; }).catch((error) => { wrapped.status = 2 /* Rejected */; wrapped.value = error; throw error; }); }; function usePromise(key, promise, options) { const cache = React.useContext(CacheContext); let cached = cache.get(key); if (!cached) { cached = { status: 0 /* Pending */, value: null, promise: promise() }; updateWrappedPromise(cached); cache.set(key, cached); } const [loading, setLoading] = React.useState( cached.status === 0 /* Pending */ ); React.useEffect(() => { cached.promise.finally(() => setLoading(false)); }, [cached.promise]); const refresh = React.useCallback(() => { setLoading(true); cached.promise = promise(); updateWrappedPromise(cached); }, [cached, promise]); const reset = React.useCallback(() => { setLoading(true); cache.delete(key); }, [cache, key]); const extra = { refresh, reset, loading }; switch (cached.status) { case 0 /* Pending */: if (options?.suspense) throw cached.promise; return [null, null, extra]; case 1 /* Fulfilled */: return [cached.value, null, extra]; case 2 /* Rejected */: return [null, cached.value, extra]; } } var KeybanContext = React.createContext(null); function KeybanProvider(props) { const { children, ...config } = props; const client = React.useMemo( () => new KeybanClient(config), // eslint-disable-next-line react-hooks/exhaustive-deps Object.values(config) ); return /* @__PURE__ */ jsx(KeybanContext.Provider, { value: client, children: /* @__PURE__ */ jsx(KeybanAuthProvider, { children: /* @__PURE__ */ jsx(PromiseCacheProvider, { children }) }) }); } var useKeybanClient = () => { const ctx = React.useContext(KeybanContext); if (!ctx) throw new Error( "useKeybanClient hook must be used within a KeybanProvider" ); return ctx; }; var KeybanAuthContext = React.createContext(null); function KeybanAuthProvider({ children }) { const client = useKeybanClient(); const [user, setUser] = React.useState(); React.useEffect(() => { client.api.auth.getUser().then(setUser); }, [client]); const auth = React.useMemo( () => ({ user, isAuthenticated: user === void 0 ? void 0 : user !== null, isLoading: user === void 0, sendOtp: client.api.auth.sendOtp, signUp: (args) => client.api.auth.signUp(args).then((user2) => { setUser(user2); return user2; }), signIn: (args) => client.api.auth.signIn(args).then((user2) => { setUser(user2); return user2; }), signOut: () => client.api.auth.signOut().then(() => setUser(null)), updateUser: (args) => client.api.auth.updateUser(args).then((user2) => { setUser(user2); return user2; }) }), [user, client] ); return /* @__PURE__ */ jsx(KeybanAuthContext.Provider, { value: auth, children }); } function useKeybanAuth() { const ctx = React.useContext(KeybanAuthContext); if (!ctx) throw new Error("useKeybanAuth hook must be used within a KeybanProvider"); return ctx; } // src/account.ts function getPaginatedResults(data) { return { hasPrevPage: data.pageInfo.hasPreviousPage, hasNextPage: data.pageInfo.hasNextPage, totalCount: data.totalCount, nodes: data.edges.map(({ node }) => node).filter(Boolean) }; } function usePaginationExtra(data, fetchMore) { const [isPending, startTransition] = React.useTransition(); return { loading: isPending, fetchMore: () => { startTransition(() => { fetchMore({ variables: { after: data.pageInfo.endCursor } }); }); } }; } function updatePaginatedData(prev, mutationType, edge, isBefore, ignoreDeletions) { switch (mutationType) { // subql cannot differentiate between inserts and // updates when historical data are enabled case GqlMutationType.INSERT: case GqlMutationType.UPDATE: { if (!prev) return { pageInfo: { hasNextPage: false, hasPreviousPage: false, startCursor: edge.cursor, endCursor: edge.cursor }, totalCount: 1, edges: [edge] }; if (prev.edges.find(({ node }) => node?.id === edge.node?.id)) return prev; const insertIndex = prev.edges.findLastIndex((edge2) => isBefore(edge2)) + 1; const isFirst = insertIndex === 0; const isLast = insertIndex === prev.edges.length; const { hasPreviousPage, hasNextPage } = prev.pageInfo; if (hasPreviousPage && isFirst || hasNextPage && isLast) return { ...prev, totalCount: prev.totalCount + Number(mutationType === GqlMutationType.INSERT) }; const edges = [...prev.edges]; edges.splice(insertIndex, 0, edge); return { pageInfo: { ...prev.pageInfo, startCursor: edges[0].cursor, endCursor: edges[edges.length - 1].cursor }, totalCount: prev.totalCount + 1, edges }; } case GqlMutationType.DELETE: { if (!prev) return { pageInfo: { hasNextPage: false, hasPreviousPage: false, startCursor: null, endCursor: null }, totalCount: 0, edges: [] }; if (ignoreDeletions) return prev; const edges = prev.edges.filter(({ node }) => node?.id !== edge.node?.id); return { pageInfo: { ...prev.pageInfo, startCursor: edges[0]?.cursor ?? null, endCursor: edges[edges.length - 1]?.cursor ?? null }, totalCount: prev.totalCount - 1, edges }; } } } function useKeybanAccount() { const client = useKeybanClient(); const auth = useKeybanAuth(); const [data, error] = usePromise( `account:${auth.user?.id}`, () => client.initialize(), { suspense: true } ); return [data, error, void 0]; } function useKeybanAccountBalance(account) { const client = useKeybanClient(); const { data, error } = useSuspenseQuery(walletBalanceDocument, { client: client.apolloClient, variables: { walletId: account.address } }); useSubscription(walletSubscriptionDocument, { client: client.apolloClient, variables: { walletIds: [account.address] }, onData({ client: client2, data: { data: data2 } }) { client2.cache.updateQuery( { query: walletBalanceDocument, variables: { walletId: account.address } }, () => ({ res: data2.sub.entity }) ); } }); return error ? [null, error, void 0] : [data.res?.balance ?? "0", null, void 0]; } function useKeybanAccountTokenBalances(account, options) { const client = useKeybanClient(); const { data, error, fetchMore, subscribeToMore } = useSuspenseQuery( walletTokenBalancesDocument, { client: client.apolloClient, variables: { walletId: account.address, orderBy: [GqlTokenBalancesOrderBy.TOKEN_SYMBOL_ASC], ...options } } ); React.useEffect( () => subscribeToMore({ document: tokenBalancesSubscriptionDocument, updateQuery(_prev, { subscriptionData }) { const prev = _prev; if (!subscriptionData.data.sub?.entity) return prev; const { mutationType, entity } = subscriptionData.data.sub; if (entity.walletId !== account.address) return prev; return { res: updatePaginatedData( prev.res, mutationType, { cursor: btoa( JSON.stringify([ GqlTokenBalancesOrderBy.TOKEN_SYMBOL_ASC.toLowerCase(), [ Number(entity.token?.symbol), JSON.parse(atob(entity.nodeId))[0] ] ]) ), node: entity }, ({ node }) => node.token.symbol < entity.token.symbol, false // token balances can *really* be deleted when the balance reach 0 ) }; } }), [subscribeToMore, account.address] ); const extra = usePaginationExtra(data.res, fetchMore); return error ? [null, error, extra] : [getPaginatedResults(data.res), null, extra]; } function useKeybanAccountTokenBalance(account, tokenAddress) { const client = useKeybanClient(); const id = [account.address, tokenAddress].join(":"); const { data, error } = useSuspenseQuery(walletTokenBalanceDocument, { client: client.apolloClient, variables: { tokenBalanceId: id } }); useSubscription(tokenBalancesSubscriptionDocument, { client: client.apolloClient, variables: { tokenBalancesIds: [id] }, onData({ client: client2, data: { data: data2 } }) { client2.cache.updateQuery( { query: walletTokenBalanceDocument, variables: { tokenBalanceId: id } }, () => ({ res: data2.sub.entity }) ); } }); if (error) return [null, error, void 0]; if (!data.res) return [ null, new SdkError( SdkErrorTypes.TokenBalanceNotFound, "useKeybanAccountTokenBalance" ), void 0 ]; return [data.res, null, void 0]; } function useKeybanAccountNfts(account, options) { const client = useKeybanClient(); const { data, error, fetchMore, subscribeToMore } = useSuspenseQuery( walletNftsDocument, { client: client.apolloClient, variables: { ...options, walletId: account.address, orderBy: GqlNftBalancesOrderBy.NFT_TOKEN_ID_ASC } } ); React.useEffect( () => subscribeToMore({ document: nftBalancesSubscriptionDocument, updateQuery(_prev, { subscriptionData }) { const prev = _prev; if (!subscriptionData.data.sub?.entity) return prev; const { mutationType, entity } = subscriptionData.data.sub; if (entity.walletId !== account.address) return prev; return { res: updatePaginatedData( prev.res, mutationType, { cursor: btoa( JSON.stringify([ GqlNftBalancesOrderBy.NFT_TOKEN_ID_ASC.toLowerCase(), [ Number(entity.nft?.tokenId), JSON.parse(atob(entity.nodeId))[0] ] ]) ), node: entity }, ({ node }) => node.nft.tokenId < entity.nft.tokenId, false // ntf can *really* be deleted when transfering them ) }; } }), [subscribeToMore, account.address] ); const extra = usePaginationExtra(data.res, fetchMore); return error ? [null, error, extra] : [getPaginatedResults(data.res), null, extra]; } function useKeybanAccountNft(account, tokenAddress, tokenId) { const client = useKeybanClient(); const id = [account.address, tokenAddress, tokenId].join(":"); const { data, error } = useSuspenseQuery(walletNftDocument, { client: client.apolloClient, variables: { nftBalanceId: id } }); useSubscription(nftBalancesSubscriptionDocument, { client: client.apolloClient, variables: { nftBalanceIds: [id] }, onData({ client: client2, data: { data: data2 } }) { client2.cache.updateQuery( { query: walletNftDocument, variables: { nftBalanceId: id } }, () => ({ res: data2.sub.entity }) ); } }); if (error) return [null, error, void 0]; if (!data.res) return [ null, new SdkError(SdkErrorTypes.NftNotFound, "useKeybanAccountNft"), void 0 ]; return [data.res, null, void 0]; } function useKeybanAccountTransferHistory(account, options) { const client = useKeybanClient(); const { data, error, fetchMore, subscribeToMore } = useSuspenseQuery( walletAssetTransfersDocument, { client: client.apolloClient, variables: { walletId: account.address, orderBy: GqlAssetTransfersOrderBy.TRANSACTION_BLOCK_NUMBER_DESC, ...options } } ); React.useEffect( () => subscribeToMore({ document: assetTransfersSubscriptionDocument, updateQuery(_prev, { subscriptionData }) { const prev = _prev; if (!subscriptionData.data.sub?.entity) return prev; const { mutationType, entity } = subscriptionData.data.sub; const match = [entity.fromId, entity.toId].includes(account.address); if (!match) return prev; return { res: updatePaginatedData( prev.res, mutationType, { cursor: btoa( JSON.stringify([ GqlAssetTransfersOrderBy.TRANSACTION_BLOCK_NUMBER_DESC.toLowerCase(), [ Number(entity.transaction?.blockNumber), JSON.parse(atob(entity.nodeId))[0] ] ]) ), node: entity }, ({ node }) => BigInt(node.transaction.blockNumber) > BigInt(entity.transaction.blockNumber), true // transaction cannot *really* be deleted ) }; } }), [subscribeToMore, account.address] ); const extra = usePaginationExtra(data.res, fetchMore); return error ? [null, error, extra] : [getPaginatedResults(data.res), null, extra]; } function useKeybanAccountOrders(account, options) { const client = useKeybanClient(); const { data, error, fetchMore, subscribeToMore } = useSuspenseQuery( walletOrdersDocument, { client: client.apolloClient, variables: { walletId: account.address, orderBy: GqlOrdersOrderBy.ID_DESC, ...options } } ); React.useEffect( () => subscribeToMore({ document: ordersSubscriptionDocument, updateQuery(_prev, { subscriptionData }) { const prev = _prev; if (!subscriptionData.data.sub?.entity) return prev; const { mutationType, entity } = subscriptionData.data.sub; const match = entity.orderTransactions.nodes.some( (orderTransaction) => [ orderTransaction?.assetTransfer?.fromId, orderTransaction?.assetTransfer?.toId ].includes(account.address) ); if (!match) return prev; return { res: updatePaginatedData( prev.res, mutationType, { cursor: btoa( JSON.stringify([ GqlOrdersOrderBy.ID_DESC.toLowerCase(), [Number(entity.id), JSON.parse(atob(entity.nodeId))[0]] ]) ), node: entity }, ({ node }) => node.id > entity.id, true // orders cannot *really* be deleted ) }; } }), [subscribeToMore, account.address] ); const extra = usePaginationExtra(data.res, fetchMore); return error ? [null, error, extra] : [getPaginatedResults(data.res), null, extra]; } // src/api.tsx function useKeybanApiStatus() { const client = useKeybanClient(); return usePromise("api-status", () => client.apiStatus(), { suspense: true }); } function useFormattedBalance(balance, token) { return formatBalance(useKeybanClient(), balance, token); } function FormattedBalance(props) { const { balance, token } = props; return useFormattedBalance(balance, token); } var KeybanInput = React.forwardRef( (props, ref) => { const { // Container props className, style, // Input props type, inputMode, // Keyban props name, onFocus, onBlur, onInput, onChange, inputStyles = {} } = props; const [containerEl, setContainerEl] = React.useState( null ); const client = useKeybanClient(); const iframeUrl = new URL("/sdk-client/input", client.apiUrl); if (type) iframeUrl.searchParams.set("type", type); if (inputMode) iframeUrl.searchParams.set("inputMode", inputMode); inputStyles.colorScheme ??= getComputedStyle( containerEl ?? document.querySelector("html") ).colorScheme; for (const [key, value] of Object.entries(inputStyles)) iframeUrl.searchParams.set(key, value); const iframeRef = React.useRef(null); const focus = React.useCallback(() => { iframeRef.current?.contentWindow?.postMessage("focus", iframeUrl.origin); }, [iframeUrl.origin]); React.useImperativeHandle(ref, () => ({ focus }), [focus]); React.useEffect(() => { const listener = (evt) => { if (evt.source !== iframeRef.current?.contentWindow) return; switch (evt.data) { case "focus": setClassNames((arr) => [...arr, "focused"]); onFocus?.(); break; case "blur": setClassNames((arr) => arr.filter((s) => s !== "focused")); onBlur?.(); break; case "input": onInput?.(); break; case "change": onChange?.(); break; } }; window.addEventListener("message", listener); return () => window.removeEventListener("message", listener); }, [onFocus, onBlur, onInput, onChange]); const [classNames, setClassNames] = React.useState([]); return /* @__PURE__ */ jsxs( "div", { onClick: focus, style, className: [styles["KeybanInput-container"], ...classNames, className].filter(Boolean).join(" "), ref: setContainerEl, children: [ /* @__PURE__ */ jsx( "input", { tabIndex: -1, disabled: true, readOnly: true, className: styles["KeybanInput-input"] } ), containerEl && /* @__PURE__ */ jsx( "iframe", { ref: iframeRef, src: iframeUrl.toString(), name, className: styles["KeybanInput-iframe"] } ) ] } ); } ); // src/application.ts function useKeybanApplication() { const client = useKeybanClient(); return usePromise("application", () => client.api.application.getCurrent(), { suspense: true }); } function useKeybanNft(tokenAddress, tokenId) { const client = useKeybanClient(); const id = [tokenAddress, tokenId].join(":"); const { data, error } = useSuspenseQuery(nftDocument, { client: client.apolloClient, variables: { nftId: id } }); useSubscription(nftSubscriptionDocument, { client: client.apolloClient, variables: { nftIds: [id] }, onData({ client: client2, data: { data: data2 } }) { client2.cache.updateQuery( { query: nftDocument, variables: { nftId: id } }, () => ({ res: data2.sub.entity }) ); } }); if (error) return [null, error, void 0]; if (!data.res) return [ null, new SdkError(SdkErrorTypes.NftNotFound, "useKeybanNft"), void 0 ]; return [data.res, null, void 0]; } // src/dpp.ts function useKeybanProduct(productId) { const client = useKeybanClient(); return usePromise( `dpp:product:${productId}`, () => client.api.dpp.getProduct(productId), { suspense: true } ); } function useKeybanPassport(passportId) { const client = useKeybanClient(); return usePromise( `dpp:passport:${passportId}`, () => client.api.dpp.getPassport(passportId), { suspense: true } ); } function useKeybanPassportToken(passportId) { const { network } = useKeybanClient(); const [app, appError] = useKeybanApplication(); const tokenAddress = app?.dppSettings.tokens.find( (dppToken) => dppToken.network === network )?.address; const [tokenId, tokenIdError] = usePromise( ["dpp", passportId].join(":"), async () => { const buf = await crypto.subtle.digest( "SHA-256", new TextEncoder().encode(passportId) ); const h = "0123456789ABCDEF".toLowerCase(); const hex = [...new Uint8Array(buf)].flatMap((v) => h[v >> 4] + h[v & 15]).join(""); return BigInt(`0x${hex}`).toString(); }, { suspense: true } ); const [nft, nftError] = useKeybanNft(tokenAddress ?? "", tokenId ?? ""); return [nft, appError ?? tokenIdError ?? nftError]; } function useLoyaltyOptimisticBalance() { const client = useKeybanClient(); const auth = useKeybanAuth(); const [data, error, { refresh }] = usePromise( `loyalty:optimisticBalance:${auth.user?.id}`, () => client.api.loyalty.getOptimisticBalance().then(BigInt), { suspense: false } ); React.useEffect(() => { const interval = setInterval(() => { try { refresh(); } catch { console.warn( "Failed to refresh loyalty balance, keeping previous value" ); } }, 5e3); return () => clearInterval(interval); }, [refresh, error, data]); return [data, error, void 0]; } export { FormattedBalance, KeybanAuthProvider, KeybanInput, KeybanProvider, useFormattedBalance, useKeybanAccount, useKeybanAccountBalance, useKeybanAccountNft, useKeybanAccountNfts, useKeybanAccountOrders, useKeybanAccountTokenBalance, useKeybanAccountTokenBalances, useKeybanAccountTransferHistory, useKeybanApiStatus, useKeybanAuth, useKeybanClient, useKeybanNft, useKeybanPassport, useKeybanPassportToken, useKeybanProduct, useLoyaltyOptimisticBalance }; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map