Skip to content

Commit ff9df26

Browse files
authored
feat: sign in with ethereum support (#119)
* feat: sign in with ethereum support * fix: build * feat: add siweMessage helper * fix: typing * fix: remove test for removed code * fix: github action
1 parent 69be3c9 commit ff9df26

File tree

16 files changed

+396
-336
lines changed

16 files changed

+396
-336
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,9 @@ jobs:
9999
name: github-pages
100100
runs-on: ubuntu-latest
101101
needs: [build, release-please]
102+
if: github.ref == 'refs/heads/main'
102103
steps:
103104
- name: Deploy to GitHub Pages
104105
id: docs-deployment
105-
if: github.ref == 'refs/heads/main' && needs.release-please.outputs.releases_created == 'true'
106+
if: needs.release-please.outputs.releases_created == 'true'
106107
uses: actions/deploy-pages@v4

.vscode/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
"cSpell.words": [
88
"Commitlint",
99
"Monerium",
10+
"PKCE",
1011
"sepolia",
12+
"SIWE",
1113
"stylelint"
1214
],
1315
"editor.codeActionsOnSave": {

apps/customer/app/test/page.tsx

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use client';
2-
import { ChangeEvent, FormEvent, useContext, useEffect, useState } from 'react';
2+
import { ChangeEvent, FormEvent, useContext, useState } from 'react';
33
import Link from 'next/link';
44
import { useAccount, useChainId, useSignMessage } from 'wagmi';
55
import { ConnectButton } from '@rainbow-me/rainbowkit';
@@ -14,6 +14,8 @@ import {
1414
OrderState,
1515
PaymentStandard,
1616
placeOrderMessage,
17+
rfc3339,
18+
siweMessage,
1719
} from '@monerium/sdk';
1820
import {
1921
MoneriumContext,
@@ -48,11 +50,16 @@ export default function Test() {
4850
* Monerium queries
4951
*/
5052
const context = useContext(MoneriumContext);
51-
const { isAuthorized, authorize, revokeAccess, error: authError } = useAuth();
5253

53-
const { data: profile } = useProfile();
54+
const {
55+
isAuthorized,
56+
authorize,
57+
siwe,
58+
revokeAccess,
59+
error: authError,
60+
} = useAuth();
5461

55-
// const { authContext } = useAuthContext();
62+
const { data: profile } = useProfile();
5663

5764
const { data: orders } = useOrders();
5865

@@ -634,7 +641,22 @@ export default function Test() {
634641

635642
const autoLink = () => {
636643
signMessageAsync({ message: constants.LINK_MESSAGE }).then((signature) => {
637-
authorize({ address, signature, chain: chainId });
644+
authorize({ address: `${address}`, signature, chain: chainId });
645+
});
646+
};
647+
const authorizeSiwe = () => {
648+
const siwe_message = siweMessage({
649+
domain: 'localhost:3000',
650+
address: `${walletAddress}`,
651+
appName: 'SDK TEST APP',
652+
redirectUri: 'http://localhost:3000/dashboard',
653+
chainId: chainId,
654+
privacyPolicyUrl: 'https://example.com/privacy-policy',
655+
termsOfServiceUrl: 'https://example.com/terms-of-service',
656+
});
657+
658+
signMessageAsync({ message: siwe_message }).then((signature) => {
659+
siwe({ message: siwe_message, signature });
638660
});
639661
};
640662

@@ -663,7 +685,7 @@ export default function Test() {
663685
<p>
664686
{!isAuthorized ? (
665687
<>
666-
<button type="submit" onClick={authorize}>
688+
<button type="submit" onClick={() => authorize()}>
667689
Authorize
668690
</button>
669691
<button
@@ -681,6 +703,9 @@ export default function Test() {
681703
<button type="submit" onClick={autoLink}>
682704
Authorize with auto linking.
683705
</button>
706+
<button type="submit" onClick={authorizeSiwe}>
707+
Siwe.
708+
</button>
684709
</>
685710
) : (
686711
<button

apps/customer/components/MoneriumConnect/MoneriumConnect.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ export const MoneriumConnect = () => {
1515
href="https://monerium.com"
1616
size="large"
1717
variant="contained"
18-
onClick={authorize}
18+
onClick={() => {}}
1919
>
2020
Read more
2121
</Button>
2222
<Button
2323
size="large"
2424
variant="outlined"
25-
onClick={authorize}
25+
onClick={() => authorize()}
2626
startIcon={
2727
<Image
2828
src="/monerium-icon.png"

packages/sdk-react-provider/src/lib/hooks.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ function useSdk(): MoneriumClient | undefined {
111111
*
112112
* @returns {UseAuthReturn}
113113
* - `authorize` - Redirects to the Monerium auth flow.
114+
* - `siwe` - Sign in with Ethereum. https://monerium.com/siwe
114115
* - `isAuthorized` - Whether the user is authorized.
115116
* - `isLoading` - Whether the auth flow is loading.
116117
* - `error` - Error message if the auth flow fails.
@@ -123,9 +124,11 @@ export function useAuth(): UseAuthReturn {
123124
if (context === null) {
124125
throw new Error('useAuth must be used within a MoneriumProvider');
125126
}
127+
126128
return {
127129
isAuthorized: context.isAuthorized,
128130
authorize: context.authorize,
131+
siwe: context.siwe,
129132
isLoading: context.isLoading,
130133
error: context.error,
131134
disconnect: context.disconnect,

packages/sdk-react-provider/src/lib/provider.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const TestConsumerComponent = () => {
4646

4747
return (
4848
<div>
49-
<button onClick={authorize}>Authorize</button>
49+
<button onClick={() => authorize()}>Authorize</button>
5050
{isAuthorized && <p>Authorized!</p>}
5151
{profile && <p data-testid="profile">{profile.name}</p>}
5252
{/* You can add more elements for other context values */}

packages/sdk-react-provider/src/lib/provider.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { ReactNode, useCallback, useEffect, useState } from 'react';
22
import { useQueryClient } from '@tanstack/react-query';
33

4-
import { MoneriumClient } from '@monerium/sdk';
4+
import {
5+
AuthFlowOptions,
6+
AuthFlowSIWEOptions,
7+
MoneriumClient,
8+
} from '@monerium/sdk';
59

610
import { MoneriumContext } from './context';
7-
import { AuthorizeParams } from './types';
811

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

101104
const authorize = useCallback(
102-
async (params?: AuthorizeParams) => {
105+
async (params?: AuthFlowOptions) => {
103106
try {
104107
if (sdk) {
105108
await sdk.authorize(params);
@@ -112,6 +115,20 @@ export const MoneriumProvider = ({
112115
[sdk]
113116
);
114117

118+
const siwe = useCallback(
119+
async (params: AuthFlowSIWEOptions) => {
120+
try {
121+
if (sdk) {
122+
await sdk.siwe(params);
123+
}
124+
} catch (err) {
125+
console.error('Error during sign in with ethereum:', err);
126+
setError(err);
127+
}
128+
},
129+
[sdk]
130+
);
131+
115132
const revokeAccess = async () => {
116133
try {
117134
if (sdk) {
@@ -131,6 +148,7 @@ export const MoneriumProvider = ({
131148
value={{
132149
sdk,
133150
authorize,
151+
siwe,
134152
isAuthorized,
135153
isLoading: loadingAuth,
136154
error,

packages/sdk-react-provider/src/lib/types.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,23 @@ import {
66
} from '@tanstack/react-query';
77

88
import type MoneriumClient from '@monerium/sdk';
9-
import { ChainId } from '@monerium/sdk';
9+
import type { AuthFlowOptions, AuthFlowSIWEOptions } from '@monerium/sdk';
1010

1111
export type SdkInstance = {
1212
/** Monerium SDK instance. */
1313
sdk?: MoneriumClient;
1414
};
1515

16-
export type AuthorizeParams =
17-
| { address: string; signature: string; chainId?: ChainId }
18-
| { state?: string; scope?: string }
19-
| {};
20-
2116
export type UseAuthReturn = {
2217
/**
2318
* Constructs the url and redirects to the Monerium auth flow.
2419
*/
25-
authorize: (params?: AuthorizeParams) => Promise<void>;
20+
authorize: (params?: AuthFlowOptions) => Promise<void>;
21+
/**
22+
* Sign in with Ethereum.
23+
* https://monerium.com/siwe
24+
*/
25+
siwe: (params: AuthFlowSIWEOptions) => Promise<void>;
2626
/**
2727
* Indicates whether the SDK is authorized.
2828
*/

packages/sdk/src/client.ts

Lines changed: 70 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { MONERIUM_CONFIG } from './config';
22
import constants from './constants';
33
import {
44
cleanQueryString,
5-
getAuthFlowUrlAndStoreCodeVerifier,
65
isAuthCode,
76
isClientCredentials,
87
isRefreshToken,
8+
preparePKCEChallenge,
99
queryParams,
1010
rest,
1111
} from './helpers';
@@ -18,6 +18,7 @@ import type {
1818
AuthArgs,
1919
AuthCodePayload,
2020
AuthFlowOptions,
21+
AuthFlowSIWEOptions,
2122
AuthorizationCodeCredentials,
2223
Balances,
2324
BearerProfile,
@@ -41,7 +42,8 @@ import type {
4142
OrderFilter,
4243
OrderNotificationQueryParams,
4344
OrdersResponse,
44-
PKCERequestArgs,
45+
PKCERequest,
46+
PKCESIWERequest,
4547
Profile,
4648
ProfilesQueryParams,
4749
ProfilesResponse,
@@ -152,41 +154,86 @@ export class MoneriumClient {
152154
}
153155

154156
/**
155-
* Construct the url to the authorization code flow and redirects,
157+
* Constructs the url to the authorization code flow and redirects,
156158
* Code Verifier needed for the code challenge is stored in local storage
157159
* For automatic wallet link, add the following properties: `address`, `signature` & `chain`
158160
*
161+
* This authorization code is then used to request an access token via the token endpoint. (https://monerium.dev/api-docs#operation/auth-token)
162+
*
159163
* @group Authentication
160164
* @see {@link https://monerium.dev/api-docs-v2#tag/auth/operation/auth | API Documentation}
161165
* @param {AuthFlowOptions} [params] - the auth flow params
162-
* @returns string
166+
* @returns void
163167
*
164168
*/
165169
async authorize(params?: AuthFlowOptions) {
166-
const clientId =
167-
params?.clientId ||
168-
(this.#client as AuthorizationCodeCredentials)?.clientId;
169-
const redirectUri =
170-
params?.redirectUri ||
171-
(this.#client as AuthorizationCodeCredentials)?.redirectUri;
172-
173-
if (!clientId) {
174-
throw new Error('Missing ClientId');
175-
}
176-
177-
const authFlowUrl = getAuthFlowUrlAndStoreCodeVerifier(this.#env, {
178-
client_id: clientId,
179-
redirect_uri: redirectUri,
180-
address: params?.address,
181-
signature: params?.signature,
182-
chain: params?.chain,
170+
const codeChallenge = preparePKCEChallenge();
171+
172+
const autoLink = params?.address
173+
? {
174+
address: params?.address,
175+
signature: params?.signature,
176+
chain: params?.chain
177+
? parseChainBackwardsCompatible(this.#env.name, params?.chain)
178+
: undefined,
179+
}
180+
: {};
181+
182+
const queryParams = urlEncoded({
183+
client_id: (this.#client as AuthorizationCodeCredentials)?.clientId,
184+
redirect_uri: (this.#client as AuthorizationCodeCredentials)?.redirectUri,
185+
code_challenge: codeChallenge,
186+
code_challenge_method: 'S256' as PKCERequest['code_challenge_method'],
187+
response_type: 'code' as PKCERequest['response_type'],
183188
state: params?.state,
184-
email: params?.email,
185189
skip_create_account: params?.skipCreateAccount,
186190
skip_kyc: params?.skipKyc,
191+
email: params?.email,
192+
...autoLink,
187193
});
188194

189-
this.#debug(`Authorization URL: ${authFlowUrl}`);
195+
const authFlowUrl = `${this.#env.api}/auth?${queryParams}`;
196+
197+
this.#debug(`Auth flow URL: ${authFlowUrl}`);
198+
// Redirect to the authFlow
199+
window.location.assign(authFlowUrl);
200+
}
201+
/**
202+
* Constructs the url to the authorization code flow and redirects,
203+
* Code Verifier needed for the code challenge is stored in local storage
204+
*
205+
* "Sign in with Ethereum" (SIWE) flow can be used for existing Monerium customers.
206+
* In this case the payload must include a valid EIP-4361 (https://eips.ethereum.org/EIPS/eip-4361) message and signature.
207+
* On successful authorization the authorization code is returned at once.
208+
*
209+
* This authorization code is then used to request an access token via the token endpoint.
210+
*
211+
* https://monerium.com/siwe
212+
*
213+
* @group Authentication
214+
* @see {@link https://monerium.dev/api-docs-v2#tag/auth/operation/auth | API Documentation}
215+
* @param {AuthFlowSIWEOptions} [params] - the auth flow SIWE params
216+
* @returns void
217+
*
218+
*/
219+
async siwe(params: AuthFlowSIWEOptions) {
220+
const codeChallenge = preparePKCEChallenge();
221+
222+
const queryParams = urlEncoded({
223+
client_id: (this.#client as AuthorizationCodeCredentials)?.clientId,
224+
redirect_uri: (this.#client as AuthorizationCodeCredentials)?.redirectUri,
225+
message: params.message,
226+
signature: params.signature,
227+
code_challenge: codeChallenge,
228+
code_challenge_method: 'S256' as PKCESIWERequest['code_challenge_method'],
229+
authentication_method: 'siwe' as PKCESIWERequest['authentication_method'],
230+
state: params?.state,
231+
});
232+
233+
const authFlowUrl = `${this.#env.api}/auth?${queryParams}`;
234+
235+
this.#debug(`Auth flow SIWE URL: ${authFlowUrl}`);
236+
190237
// Redirect to the authFlow
191238
window.location.assign(authFlowUrl);
192239
}
@@ -791,15 +838,4 @@ export class MoneriumClient {
791838
* @hidden
792839
*/
793840
getEnvironment = (): Environment => this.#env;
794-
/**
795-
*
796-
* @hidden
797-
*/
798-
getAuthFlowURI = (args: PKCERequestArgs): string => {
799-
const url = getAuthFlowUrlAndStoreCodeVerifier(
800-
this.#env,
801-
mapChainIdToChain(this.#env.name, args)
802-
);
803-
return url;
804-
};
805841
}

0 commit comments

Comments
 (0)