Skip to content

feat: sign in with ethereum support #119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 10, 2025
Merged
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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@ jobs:
name: github-pages
runs-on: ubuntu-latest
needs: [build, release-please]
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to GitHub Pages
id: docs-deployment
if: github.ref == 'refs/heads/main' && needs.release-please.outputs.releases_created == 'true'
if: needs.release-please.outputs.releases_created == 'true'
uses: actions/deploy-pages@v4
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"cSpell.words": [
"Commitlint",
"Monerium",
"PKCE",
"sepolia",
"SIWE",
"stylelint"
],
"editor.codeActionsOnSave": {
Expand Down
37 changes: 31 additions & 6 deletions apps/customer/app/test/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use client';
import { ChangeEvent, FormEvent, useContext, useEffect, useState } from 'react';
import { ChangeEvent, FormEvent, useContext, useState } from 'react';
import Link from 'next/link';
import { useAccount, useChainId, useSignMessage } from 'wagmi';
import { ConnectButton } from '@rainbow-me/rainbowkit';
Expand All @@ -14,6 +14,8 @@ import {
OrderState,
PaymentStandard,
placeOrderMessage,
rfc3339,
siweMessage,
} from '@monerium/sdk';
import {
MoneriumContext,
Expand Down Expand Up @@ -48,11 +50,16 @@ export default function Test() {
* Monerium queries
*/
const context = useContext(MoneriumContext);
const { isAuthorized, authorize, revokeAccess, error: authError } = useAuth();

const { data: profile } = useProfile();
const {
isAuthorized,
authorize,
siwe,
revokeAccess,
error: authError,
} = useAuth();

// const { authContext } = useAuthContext();
const { data: profile } = useProfile();

const { data: orders } = useOrders();

Expand Down Expand Up @@ -634,7 +641,22 @@ export default function Test() {

const autoLink = () => {
signMessageAsync({ message: constants.LINK_MESSAGE }).then((signature) => {
authorize({ address, signature, chain: chainId });
authorize({ address: `${address}`, signature, chain: chainId });
});
};
const authorizeSiwe = () => {
const siwe_message = siweMessage({
domain: 'localhost:3000',
address: `${walletAddress}`,
appName: 'SDK TEST APP',
redirectUri: 'http://localhost:3000/dashboard',
chainId: chainId,
privacyPolicyUrl: 'https://example.com/privacy-policy',
termsOfServiceUrl: 'https://example.com/terms-of-service',
});

signMessageAsync({ message: siwe_message }).then((signature) => {
siwe({ message: siwe_message, signature });
});
};

Expand Down Expand Up @@ -663,7 +685,7 @@ export default function Test() {
<p>
{!isAuthorized ? (
<>
<button type="submit" onClick={authorize}>
<button type="submit" onClick={() => authorize()}>
Authorize
</button>
<button
Expand All @@ -681,6 +703,9 @@ export default function Test() {
<button type="submit" onClick={autoLink}>
Authorize with auto linking.
</button>
<button type="submit" onClick={authorizeSiwe}>
Siwe.
</button>
</>
) : (
<button
Expand Down
4 changes: 2 additions & 2 deletions apps/customer/components/MoneriumConnect/MoneriumConnect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ export const MoneriumConnect = () => {
href="https://monerium.com"
size="large"
variant="contained"
onClick={authorize}
onClick={() => {}}
>
Read more
</Button>
<Button
size="large"
variant="outlined"
onClick={authorize}
onClick={() => authorize()}
startIcon={
<Image
src="/monerium-icon.png"
Expand Down
3 changes: 3 additions & 0 deletions packages/sdk-react-provider/src/lib/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ function useSdk(): MoneriumClient | undefined {
*
* @returns {UseAuthReturn}
* - `authorize` - Redirects to the Monerium auth flow.
* - `siwe` - Sign in with Ethereum. https://monerium.com/siwe
* - `isAuthorized` - Whether the user is authorized.
* - `isLoading` - Whether the auth flow is loading.
* - `error` - Error message if the auth flow fails.
Expand All @@ -123,9 +124,11 @@ export function useAuth(): UseAuthReturn {
if (context === null) {
throw new Error('useAuth must be used within a MoneriumProvider');
}

return {
isAuthorized: context.isAuthorized,
authorize: context.authorize,
siwe: context.siwe,
isLoading: context.isLoading,
error: context.error,
disconnect: context.disconnect,
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk-react-provider/src/lib/provider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const TestConsumerComponent = () => {

return (
<div>
<button onClick={authorize}>Authorize</button>
<button onClick={() => authorize()}>Authorize</button>
{isAuthorized && <p>Authorized!</p>}
{profile && <p data-testid="profile">{profile.name}</p>}
{/* You can add more elements for other context values */}
Expand Down
24 changes: 21 additions & 3 deletions packages/sdk-react-provider/src/lib/provider.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { ReactNode, useCallback, useEffect, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';

import { MoneriumClient } from '@monerium/sdk';
import {
AuthFlowOptions,
AuthFlowSIWEOptions,
MoneriumClient,
} from '@monerium/sdk';

import { MoneriumContext } from './context';
import { AuthorizeParams } from './types';

/**
* Wrap your application with the Monerium provider.
Expand Down Expand Up @@ -99,7 +102,7 @@ export const MoneriumProvider = ({
}, [refreshToken]);

const authorize = useCallback(
async (params?: AuthorizeParams) => {
async (params?: AuthFlowOptions) => {
try {
if (sdk) {
await sdk.authorize(params);
Expand All @@ -112,6 +115,20 @@ export const MoneriumProvider = ({
[sdk]
);

const siwe = useCallback(
async (params: AuthFlowSIWEOptions) => {
try {
if (sdk) {
await sdk.siwe(params);
}
} catch (err) {
console.error('Error during sign in with ethereum:', err);
setError(err);
}
},
[sdk]
);

const revokeAccess = async () => {
try {
if (sdk) {
Expand All @@ -131,6 +148,7 @@ export const MoneriumProvider = ({
value={{
sdk,
authorize,
siwe,
isAuthorized,
isLoading: loadingAuth,
error,
Expand Down
14 changes: 7 additions & 7 deletions packages/sdk-react-provider/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@ import {
} from '@tanstack/react-query';

import type MoneriumClient from '@monerium/sdk';
import { ChainId } from '@monerium/sdk';
import type { AuthFlowOptions, AuthFlowSIWEOptions } from '@monerium/sdk';

export type SdkInstance = {
/** Monerium SDK instance. */
sdk?: MoneriumClient;
};

export type AuthorizeParams =
| { address: string; signature: string; chainId?: ChainId }
| { state?: string; scope?: string }
| {};

export type UseAuthReturn = {
/**
* Constructs the url and redirects to the Monerium auth flow.
*/
authorize: (params?: AuthorizeParams) => Promise<void>;
authorize: (params?: AuthFlowOptions) => Promise<void>;
/**
* Sign in with Ethereum.
* https://monerium.com/siwe
*/
siwe: (params: AuthFlowSIWEOptions) => Promise<void>;
/**
* Indicates whether the SDK is authorized.
*/
Expand Down
104 changes: 70 additions & 34 deletions packages/sdk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { MONERIUM_CONFIG } from './config';
import constants from './constants';
import {
cleanQueryString,
getAuthFlowUrlAndStoreCodeVerifier,
isAuthCode,
isClientCredentials,
isRefreshToken,
preparePKCEChallenge,
queryParams,
rest,
} from './helpers';
Expand All @@ -18,6 +18,7 @@ import type {
AuthArgs,
AuthCodePayload,
AuthFlowOptions,
AuthFlowSIWEOptions,
AuthorizationCodeCredentials,
Balances,
BearerProfile,
Expand All @@ -41,7 +42,8 @@ import type {
OrderFilter,
OrderNotificationQueryParams,
OrdersResponse,
PKCERequestArgs,
PKCERequest,
PKCESIWERequest,
Profile,
ProfilesQueryParams,
ProfilesResponse,
Expand Down Expand Up @@ -152,41 +154,86 @@ export class MoneriumClient {
}

/**
* Construct the url to the authorization code flow and redirects,
* Constructs the url to the authorization code flow and redirects,
* Code Verifier needed for the code challenge is stored in local storage
* For automatic wallet link, add the following properties: `address`, `signature` & `chain`
*
* This authorization code is then used to request an access token via the token endpoint. (https://monerium.dev/api-docs#operation/auth-token)
*
* @group Authentication
* @see {@link https://monerium.dev/api-docs-v2#tag/auth/operation/auth | API Documentation}
* @param {AuthFlowOptions} [params] - the auth flow params
* @returns string
* @returns void
*
*/
async authorize(params?: AuthFlowOptions) {
const clientId =
params?.clientId ||
(this.#client as AuthorizationCodeCredentials)?.clientId;
const redirectUri =
params?.redirectUri ||
(this.#client as AuthorizationCodeCredentials)?.redirectUri;

if (!clientId) {
throw new Error('Missing ClientId');
}

const authFlowUrl = getAuthFlowUrlAndStoreCodeVerifier(this.#env, {
client_id: clientId,
redirect_uri: redirectUri,
address: params?.address,
signature: params?.signature,
chain: params?.chain,
const codeChallenge = preparePKCEChallenge();

const autoLink = params?.address
? {
address: params?.address,
signature: params?.signature,
chain: params?.chain
? parseChainBackwardsCompatible(this.#env.name, params?.chain)
: undefined,
}
: {};

const queryParams = urlEncoded({
client_id: (this.#client as AuthorizationCodeCredentials)?.clientId,
redirect_uri: (this.#client as AuthorizationCodeCredentials)?.redirectUri,
code_challenge: codeChallenge,
code_challenge_method: 'S256' as PKCERequest['code_challenge_method'],
response_type: 'code' as PKCERequest['response_type'],
state: params?.state,
email: params?.email,
skip_create_account: params?.skipCreateAccount,
skip_kyc: params?.skipKyc,
email: params?.email,
...autoLink,
});

this.#debug(`Authorization URL: ${authFlowUrl}`);
const authFlowUrl = `${this.#env.api}/auth?${queryParams}`;

this.#debug(`Auth flow URL: ${authFlowUrl}`);
// Redirect to the authFlow
window.location.assign(authFlowUrl);
}
/**
* Constructs the url to the authorization code flow and redirects,
* Code Verifier needed for the code challenge is stored in local storage
*
* "Sign in with Ethereum" (SIWE) flow can be used for existing Monerium customers.
* In this case the payload must include a valid EIP-4361 (https://eips.ethereum.org/EIPS/eip-4361) message and signature.
* On successful authorization the authorization code is returned at once.
*
* This authorization code is then used to request an access token via the token endpoint.
*
* https://monerium.com/siwe
*
* @group Authentication
* @see {@link https://monerium.dev/api-docs-v2#tag/auth/operation/auth | API Documentation}
* @param {AuthFlowSIWEOptions} [params] - the auth flow SIWE params
* @returns void
*
*/
async siwe(params: AuthFlowSIWEOptions) {
const codeChallenge = preparePKCEChallenge();

const queryParams = urlEncoded({
client_id: (this.#client as AuthorizationCodeCredentials)?.clientId,
redirect_uri: (this.#client as AuthorizationCodeCredentials)?.redirectUri,
message: params.message,
signature: params.signature,
code_challenge: codeChallenge,
code_challenge_method: 'S256' as PKCESIWERequest['code_challenge_method'],
authentication_method: 'siwe' as PKCESIWERequest['authentication_method'],
state: params?.state,
});

const authFlowUrl = `${this.#env.api}/auth?${queryParams}`;

this.#debug(`Auth flow SIWE URL: ${authFlowUrl}`);

// Redirect to the authFlow
window.location.assign(authFlowUrl);
}
Expand Down Expand Up @@ -791,15 +838,4 @@ export class MoneriumClient {
* @hidden
*/
getEnvironment = (): Environment => this.#env;
/**
*
* @hidden
*/
getAuthFlowURI = (args: PKCERequestArgs): string => {
const url = getAuthFlowUrlAndStoreCodeVerifier(
this.#env,
mapChainIdToChain(this.#env.name, args)
);
return url;
};
}
Loading
Loading