Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion packages/enoki/src/wallet/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
// SPDX-License-Identifier: Apache-2.0

import type { AuthProvider, EnokiNetwork } from '../EnokiClient/type.js';
import type { ZkLoginSession } from './types.js';
import type { EnokiSessionContext, PKCEContext, ZkLoginSession } from './types.js';
import type { StandardConnect, StandardConnectMethod } from '@mysten/wallet-standard';

/** Name of the feature for retrieving basic wallet metadata. */
export const EnokiGetMetadata = 'enoki:getMetadata';
Expand Down Expand Up @@ -65,3 +66,57 @@ export type EnokiGetSessionInput = {

/** Output of retrieving the Enoki session. */
export type EnokiGetSessionOutput = ZkLoginSession | null;

/** Extended connect method for manual auth flow. */
export type EnokiConnectMethod = (
input?: {
/** Whether to disable popup and return URL for manual handling. */
disablePopup?: boolean;
/** Standard connect input fields. */
} & Parameters<StandardConnectMethod>[0],
) => ReturnType<StandardConnectMethod> | (ReturnType<StandardConnectMethod> & Promise<{
authorizationUrl: string;
pkceContext: PKCEContext | undefined;
sessionContext: EnokiSessionContext;
}>);



/** Name of the feature for auth flow. */
export const EnokiHandleAuthCallback = 'enoki:handleAuthCallback';

/** The latest API version of the auth API. */
export type EnokiHandleAuthCallbackVersion = '1.0.0';

export type EnokiHandleAuthCallbackInput = {
hash: string;
sessionContext: EnokiSessionContext;
pkceContext?: PKCEContext;
search: string;
};

export type EnokiHandleAuthCallbackMethod = (input: EnokiHandleAuthCallbackInput) => Promise<string | null>;


/**
* A Wallet Standard feature for authentication flow.
*/
export type EnokiHandleAuthCallbackFeature = {
/** Namespace for the feature. */
[EnokiHandleAuthCallback]: {
/** Version of the feature API. */
version: EnokiHandleAuthCallbackVersion;
handleAuthCallback: EnokiHandleAuthCallbackMethod;
};
};

// TODO: maybe move to standart package and remove here
/**
* Extended connect method for manual auth flow.
*/
export type EnokiConnectFeature = {
[StandardConnect]: {
version: '1.0.0';
connect: EnokiConnectMethod;
};
};
25 changes: 14 additions & 11 deletions packages/enoki/src/wallet/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,18 @@ export type RegisterEnokiWalletsOptions = {
(
| ClientConfig
| {
/**
* The SuiClient instance to use when building and executing transactions.
*/
client: SuiClient;

/**
* The network to use when building and executing transactions.
* @default 'mainnet'
*/
network?: EnokiNetwork;
}
/**
* The SuiClient instance to use when building and executing transactions.
*/
client: SuiClient;

/**
* The network to use when building and executing transactions.
* @default 'mainnet'
*/
network?: EnokiNetwork;
}
);


export type PKCEContext = { codeChallenge: string; codeVerifier: string };
70 changes: 43 additions & 27 deletions packages/enoki/src/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import { Transaction } from '@mysten/sui/transactions';
import { fromBase64, toBase64 } from '@mysten/sui/utils';
import type {
IdentifierString,
StandardConnectFeature,
StandardConnectMethod,
StandardDisconnectFeature,
StandardDisconnectMethod,
StandardEventsFeature,
Expand All @@ -32,14 +30,22 @@ import type { Emitter } from 'mitt';
import mitt from 'mitt';

import type { AuthProvider } from '../EnokiClient/type.js';
import type { EnokiWalletOptions, WalletEventsMap, EnokiSessionContext } from './types.js';
import type { EnokiWalletOptions, WalletEventsMap, EnokiSessionContext, PKCEContext } from './types.js';
import type {
EnokiGetMetadataFeature,
EnokiGetMetadataMethod,
EnokiGetSessionFeature,
EnokiGetSessionMethod,
EnokiHandleAuthCallbackFeature,
EnokiConnectMethod,
EnokiHandleAuthCallbackInput,
EnokiConnectFeature,
} from './features.js';
import {
EnokiGetMetadata,
EnokiGetSession,
EnokiHandleAuthCallback,
} from './features.js';
import { EnokiGetMetadata, EnokiGetSession } from './features.js';
import type { Experimental_SuiClientTypes } from '@mysten/sui/experimental';
import { decodeJwt } from '@mysten/sui/zklogin';
import type { ExportedWebCryptoKeypair } from '@mysten/signers/webcrypto';
Expand All @@ -59,8 +65,6 @@ const pkceFlowProviders: Partial<Record<AuthProvider, { tokenEndpoint: string }>
},
};

type PKCEContext = { codeChallenge: string; codeVerifier: string };

export class EnokiWallet implements Wallet {
#events: Emitter<WalletEventsMap>;
#accounts: ReadonlyWalletAccount[];
Expand Down Expand Up @@ -101,14 +105,15 @@ export class EnokiWallet implements Wallet {
return this.#accounts;
}

get features(): StandardConnectFeature &
get features(): EnokiConnectFeature &
StandardDisconnectFeature &
StandardEventsFeature &
SuiSignTransactionFeature &
SuiSignAndExecuteTransactionFeature &
SuiSignPersonalMessageFeature &
EnokiGetMetadataFeature &
EnokiGetSessionFeature {
EnokiGetSessionFeature &
EnokiHandleAuthCallbackFeature {
return {
[StandardConnect]: {
version: '1.0.0',
Expand Down Expand Up @@ -142,6 +147,12 @@ export class EnokiWallet implements Wallet {
version: '1.0.0',
getSession: this.#getSession,
},
[EnokiHandleAuthCallback]: {
version: '1.0.0',
handleAuthCallback: async (input) => {
return this.#handleAuthCallback(input);
}
},
};
}

Expand Down Expand Up @@ -260,7 +271,7 @@ export class EnokiWallet implements Wallet {
return () => this.#events.off(event, listener);
};

#connect: StandardConnectMethod = async (input) => {
#connect: EnokiConnectMethod = async (input) => {
// NOTE: This is a hackfix for the old version of dApp Kit where auto-connection logic
// only fires on initial mount of the WalletProvider component. Since hydrating the
// zkLogin state from IndexedDB is an asynchronous process, we need to make sure it
Expand All @@ -272,9 +283,9 @@ export class EnokiWallet implements Wallet {
}

const currentNetwork = this.#getCurrentNetwork();
await this.#createSession({ network: currentNetwork });
const session = await this.#createSession({ network: currentNetwork, disablePopup: input?.disablePopup });

return { accounts: this.#accounts };
return { accounts: this.#accounts, ...session };
};

#disconnect: StandardDisconnectMethod = async () => {
Expand All @@ -301,8 +312,9 @@ export class EnokiWallet implements Wallet {

async #getKeypair(sessionContext: EnokiSessionContext) {
const session = await this.#state.getSession(sessionContext);

if (!session?.jwt || Date.now() > session.expiresAt) {
await this.#createSession({ network: sessionContext.client.network });
await this.#createSession({ network: sessionContext.client.network, disablePopup: false });
}

const storedNativeSigner = await get<ExportedWebCryptoKeypair>(
Expand Down Expand Up @@ -352,7 +364,19 @@ export class EnokiWallet implements Wallet {
return { client: sessionContext.client, keypair };
}

async #createSession({ network }: { network: Experimental_SuiClientTypes.Network }) {
async #createSession({ network, disablePopup = false }: { network: Experimental_SuiClientTypes.Network; disablePopup?: boolean }) {
const sessionContext = this.#state.getSessionContext(network);
const pkceContext = await this.#getPKCEFlowContext();
const authorizationUrl = await this.#createAuthorizationURL(sessionContext, pkceContext);

if (disablePopup) {
return {
authorizationUrl,
pkceContext,
sessionContext,
};
}

const popup = window.open(
undefined,
'_blank',
Expand All @@ -363,10 +387,7 @@ export class EnokiWallet implements Wallet {
throw new Error('Failed to open popup');
}

const sessionContext = this.#state.getSessionContext(network);
const pkceContext = await this.#getPKCEFlowContext();

popup.location = await this.#createAuthorizationURL(sessionContext, pkceContext);
popup.location = authorizationUrl;

return await new Promise<void>((resolve, reject) => {
const interval = setInterval(() => {
Expand Down Expand Up @@ -440,10 +461,10 @@ export class EnokiWallet implements Wallet {
.join(' '),
...(pkceContext
? {
response_type: 'code',
code_challenge_method: 'S256',
code_challenge: pkceContext.codeChallenge,
}
response_type: 'code',
code_challenge_method: 'S256',
code_challenge: pkceContext.codeChallenge,
}
: undefined),
});

Expand Down Expand Up @@ -484,12 +505,7 @@ export class EnokiWallet implements Wallet {
sessionContext,
pkceContext,
search,
}: {
hash: string;
sessionContext: EnokiSessionContext;
pkceContext?: PKCEContext;
search: string;
}) {
}: EnokiHandleAuthCallbackInput) {
const params = new URLSearchParams(hash.startsWith('#') ? hash.slice(1) : hash);
const zkp = await this.#state.getSession(sessionContext);

Expand Down