@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
JavaScript
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