Skip to content
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

feat: register, change and verify email addresses #148

Merged
merged 8 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@safe-global/safe-gateway-typescript-sdk",
"version": "3.15.0",
"version": "3.16.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
Expand Down
15 changes: 12 additions & 3 deletions src/endpoint.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { deleteData, fetchData, insertParams, stringifyQuery } from './utils'
import type { DeleteEndpoint, GetEndpoint, paths, PostEndpoint, Primitive } from './types/api'
import type { DeleteEndpoint, GetEndpoint, paths, PostEndpoint, Primitive, PutEndpoint } from './types/api'

function makeUrl(
baseUrl: string,
Expand All @@ -18,7 +18,16 @@ export function postEndpoint<T extends keyof paths>(
params?: paths[T] extends PostEndpoint ? paths[T]['post']['parameters'] : never,
): Promise<paths[T] extends PostEndpoint ? paths[T]['post']['responses'][200]['schema'] : never> {
const url = makeUrl(baseUrl, path as string, params?.path, params?.query)
return fetchData(url, params?.body)
return fetchData(url, 'POST', params?.body, params?.headers)
}

export function putEndpoint<T extends keyof paths>(
baseUrl: string,
path: T,
params?: paths[T] extends PutEndpoint ? paths[T]['put']['parameters'] : never,
): Promise<paths[T] extends PutEndpoint ? paths[T]['put']['responses'][200]['schema'] : never> {
const url = makeUrl(baseUrl, path as string, params?.path, params?.query)
return fetchData(url, 'PUT', params?.body, params?.headers)
}

export function getEndpoint<T extends keyof paths>(
Expand All @@ -31,7 +40,7 @@ export function getEndpoint<T extends keyof paths>(
return fetchData(rawUrl)
}
const url = makeUrl(baseUrl, path as string, params?.path, params?.query)
return fetchData(url)
return fetchData(url, undefined, undefined, params?.headers)
}

export function deleteEndpoint<T extends keyof paths>(
Expand Down
129 changes: 128 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { deleteEndpoint, getEndpoint, postEndpoint } from './endpoint'
import { deleteEndpoint, getEndpoint, postEndpoint, putEndpoint } from './endpoint'
import type { operations } from './types/api'
import type {
SafeTransactionEstimation,
Expand All @@ -25,6 +25,7 @@ import type { DecodedDataResponse } from './types/decoded-data'
import type { SafeMessage, SafeMessageListPage } from './types/safe-messages'
import { DEFAULT_BASE_URL } from './config'
import type { DelegateResponse, DelegatesRequest } from './types/delegates'
import type { GetEmailResponse } from './types/emails'

export * from './types/safe-info'
export * from './types/safe-apps'
Expand Down Expand Up @@ -413,4 +414,130 @@ export function unregisterDevice(chainId: string, uuid: string): Promise<void> {
})
}

/**
* Registers a email address for a safe signer.
*
* The signer wallet has to sign a message of format: `email-register-{chainId}-{safeAddress}-{emailAddress}-{signer}-{timestamp}`
* The signature is valid for 5 minutes.
*
* @param chainId
* @param safeAddress
* @param body Signer address and email address
* @param headers Signature and Signature timestamp
* @returns 200 if signature matches the data
usame-algan marked this conversation as resolved.
Show resolved Hide resolved
*/
export function registerEmail(
chainId: string,
safeAddress: string,
body: operations['register_email']['parameters']['body'],
headers: operations['register_email']['parameters']['headers'],
): Promise<void> {
return postEndpoint(baseUrl, '/v1/chains/{chainId}/safes/{safe_address}/emails', {
path: { chainId, safe_address: safeAddress },
body,
headers,
})
}

/**
* Changes an already registered email address for a safe signer. The new email address still needs to be verified.
*
* The signer wallet has to sign a message of format: `email-edit-{chainId}-{safeAddress}-{emailAddress}-{signer}-{timestamp}`
* The signature is valid for 5 minutes.
*
* @param chainId
* @param safeAddress
* @param body New email address
* @param headers Signature and Signature timestamp
* @returns 202 if signature matches the data
*/
export function changeEmail(
chainId: string,
safeAddress: string,
signerAddress: string,
body: operations['change_email']['parameters']['body'],
headers: operations['change_email']['parameters']['headers'],
): Promise<void> {
return putEndpoint(baseUrl, '/v1/chains/{chainId}/safes/{safe_address}/emails/{signer}', {
path: { chainId, safe_address: safeAddress, signer: signerAddress },
body,
headers,
})
}

/**
* Resends an email verification code.
*/
export function resendEmailVerificationCode(
chainId: string,
safeAddress: string,
signerAddress: string,
): Promise<void> {
return postEndpoint(baseUrl, '/v1/chains/{chainId}/safes/{safe_address}/emails/{signer}/verify-resend', {
path: { chainId, safe_address: safeAddress, signer: signerAddress },
body: '',
})
}

/**
* Verifies a pending email address registration.
*
* @param chainId
* @param safeAddress
* @param signerAddress address who signed the email registration
* @param body Verification code
*/
export function verifyEmail(
chainId: string,
safeAddress: string,
signerAddress: string,
body: operations['verify_email']['parameters']['body'],
): Promise<void> {
return putEndpoint(baseUrl, '/v1/chains/{chainId}/safes/{safe_address}/emails/{signer}/verify', {
path: { chainId, safe_address: safeAddress, signer: signerAddress },
body,
})
}

/**
* Gets the registered email address of the signer
*
* @param chainId
* @param safeAddress
* @param signerAddress address of the owner of the Safe
*
* @returns email address and verified flag
*/
export function getRegisteredEmail(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it's mentioned in the other authenticated endpoints. This one also requires a signature: email-retrieval-{chainId}-{safe}-{signer}-{timestamp}

chainId: string,
safeAddress: string,
signerAddress: string,
headers: operations['get_email']['parameters']['headers'],
): Promise<GetEmailResponse> {
return getEndpoint(baseUrl, '/v1/chains/{chainId}/safes/{safe_address}/emails/{signer}', {
path: { chainId, safe_address: safeAddress, signer: signerAddress },
headers,
})
}

/**
* Delete a registered email address for the signer
*
* @param chainId
* @param safeAddress
* @param signerAddress
* @param headers
*/
export function deleteRegisteredEmail(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as my comment above. Signature would be email-delete-{chainId}-{safe}-{signer}-{timestamp}

chainId: string,
safeAddress: string,
signerAddress: string,
headers: operations['delete_email']['parameters']['headers'],
): Promise<void> {
return deleteEndpoint(baseUrl, '/v1/chains/{chainId}/safes/{safe_address}/emails/{signer}', {
path: { chainId, safe_address: safeAddress, signer: signerAddress },
headers,
})
}

/* eslint-enable @typescript-eslint/explicit-module-boundary-types */
167 changes: 166 additions & 1 deletion src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,19 @@ import type {
} from './safe-messages'
import type { DelegateResponse, DelegatesRequest } from './delegates'
import type { RegisterNotificationsRequest } from './notifications'
import type {
ChangeEmailRequestBody,
GetEmailResponse,
RegisterEmailRequestBody,
AuthorizationEmailRequestHeader,
VerifyEmailRequestBody,
} from './emails'

export type Primitive = string | number | boolean | null

interface Params {
path?: { [key: string]: Primitive }
headers?: Record<string, string>
}

interface GetParams extends Params {
Expand Down Expand Up @@ -70,6 +78,13 @@ export interface PostEndpoint extends Endpoint {
}
}

export interface PutEndpoint extends Endpoint {
put: {
parameters: PostParams | null

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does PostParams represent under the PutEndpoint?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not duplicate the types here as they are identical.
The Params represent what kind of parameters the endpoints accept:
In both cases

  • body params
  • header params
  • query params

responses: Responses
}
}

export interface DeleteEndpoint extends Endpoint {
delete: {
parameters: Params | null
Expand All @@ -78,7 +93,7 @@ export interface DeleteEndpoint extends Endpoint {
}

interface PathRegistry {
[key: string]: GetEndpoint | PostEndpoint | (GetEndpoint & PostEndpoint) | DeleteEndpoint
[key: string]: GetEndpoint | PostEndpoint | PutEndpoint | DeleteEndpoint
}

export interface paths extends PathRegistry {
Expand Down Expand Up @@ -332,6 +347,47 @@ export interface paths extends PathRegistry {
}
}
}
'/v1/chains/{chainId}/safes/{safe_address}/emails': {
post: operations['register_email']
parameters: {
path: {
chainId: string
safe_address: string
}
}
}
'/v1/chains/{chainId}/safes/{safe_address}/emails/{signer}': {
put: operations['change_email']
get: operations['get_email']
delete: operations['delete_email']
parameters: {
path: {
chainId: string
safe_address: string
signer: string
}
}
}
'/v1/chains/{chainId}/safes/{safe_address}/emails/{signer}/verify-resend': {
post: operations['verify_resend']
parameters: {
path: {
chainId: string
safe_address: string
signer: string
}
}
}
'/v1/chains/{chainId}/safes/{safe_address}/emails/{signer}/verify': {
put: operations['verify_email']
parameters: {
path: {
chainId: string
safe_address: string
signer: string
}
}
}
}

export interface operations {
Expand Down Expand Up @@ -833,4 +889,113 @@ export interface operations {
}
}
}
register_email: {
parameters: {
path: {
chainId: string
safe_address: string
}
body: RegisterEmailRequestBody
headers: AuthorizationEmailRequestHeader
}
responses: {
200: {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it should be 201.

schema: void
}
}
}
change_email: {
parameters: {
path: {
chainId: string
safe_address: string
signer: string
}
body: ChangeEmailRequestBody
headers: AuthorizationEmailRequestHeader
}
responses: {
200: {
schema: void
}
202: {
schema: void
}
}
Comment on lines +920 to +927

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this only include successful responses? If so then only 202 should be expected. There are client related error codes returned under other conditions though.

}
get_email: {
parameters: {
path: {
chainId: string
safe_address: string
signer: string
}
headers: AuthorizationEmailRequestHeader
}
responses: {
200: {
schema: GetEmailResponse
}
}
}
verify_resend: {
parameters: {
path: {
chainId: string
safe_address: string
signer: string
}
body: ''
}
responses: {
202: {
schema: void
}
200: {
schema: void
}
429: unknown
409: unknown
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: After safe-global/safe-client-gateway#1181 is merged, only 202 will be returned.

}
verify_email: {
parameters: {
path: {
chainId: string
safe_address: string
signer: string
}
body: VerifyEmailRequestBody
}

responses: {
204: {
schema: void
}
200: {
schema: void
}
Comment on lines +976 to +978

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This status code is not expected to be returned on this endpoint.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently our types enforce a 200 response code this. We should look into how to make them more flexible.
Probably the success-return type could be expressed through generics instead of using the 200 code return type.

400: unknown
}
}
delete_email: {
parameters: {
path: {
chainId: string
safe_address: string
signer: string
}
headers: AuthorizationEmailRequestHeader
}

responses: {
204: {
schema: void
}
200: {
schema: void
}
403: unknown
}
}
}
Loading
Loading