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: add notification (un-)registration endpoints #128

Merged
merged 4 commits into from
Aug 22, 2023
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
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.9.0",
"version": "3.10.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
Expand Down
13 changes: 11 additions & 2 deletions src/endpoint.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { fetchData, insertParams, stringifyQuery } from './utils'
import type { GetEndpoint, paths, PostEndpoint, Primitive } from './types/api'
import { deleteData, fetchData, insertParams, stringifyQuery } from './utils'
import type { DeleteEndpoint, GetEndpoint, paths, PostEndpoint, Primitive } from './types/api'

function makeUrl(
baseUrl: string,
Expand Down Expand Up @@ -33,3 +33,12 @@ export function getEndpoint<T extends keyof paths>(
const url = makeUrl(baseUrl, path as string, params?.path, params?.query)
return fetchData(url)
}

export function deleteEndpoint<T extends keyof paths>(
baseUrl: string,
path: T,
params?: paths[T] extends DeleteEndpoint ? paths[T]['delete']['parameters'] : never,
): Promise<paths[T] extends DeleteEndpoint ? paths[T]['delete']['responses'][200]['schema'] : never> {
const url = makeUrl(baseUrl, path as string, params?.path)
return deleteData(url)
}
29 changes: 28 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getEndpoint, postEndpoint } from './endpoint'
import { deleteEndpoint, getEndpoint, postEndpoint } from './endpoint'
import type { operations } from './types/api'
import type {
SafeTransactionEstimation,
Expand Down Expand Up @@ -355,4 +355,31 @@ export function getDelegates(chainId: string, query: DelegatesRequest = {}): Pro
})
}

/**
* Registers a device/Safe for notifications
*/
export function registerDevice(body: operations['register_device']['parameters']['body']): Promise<void> {
return postEndpoint(baseUrl, '/v1/register/notifications', {
body,
})
}

/**
* Unregisters a Safe from notifications
*/
export function unregisterSafe(chainId: string, address: string, uuid: string): Promise<void> {
return deleteEndpoint(baseUrl, '/v1/chains/{chainId}/notifications/devices/{uuid}/safes/{safe_address}', {
path: { chainId, safe_address: address, uuid },
})
}

/**
* Unregisters a device from notifications
*/
export function unregisterDevice(chainId: string, uuid: string): Promise<void> {
return deleteEndpoint(baseUrl, '/v1/chains/{chainId}/notifications/devices/{uuid}', {
path: { chainId, uuid },
})
}

/* eslint-enable @typescript-eslint/explicit-module-boundary-types */
75 changes: 73 additions & 2 deletions src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,15 @@ import type {
SafeMessageListPage,
} from './safe-messages'
import type { DelegateResponse, DelegatesRequest } from './delegates'
import type { RegisterNotificationsRequest } from './notifications'

export type Primitive = string | number | boolean | null

interface GetParams {
interface Params {
path?: { [key: string]: Primitive }
}

interface GetParams extends Params {
query?: { [key: string]: Primitive }
}

Expand Down Expand Up @@ -64,8 +68,15 @@ export interface PostEndpoint extends Endpoint {
}
}

export interface DeleteEndpoint extends Endpoint {
delete: {
parameters: Params | null
responses: Responses
}
}

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

export interface paths extends PathRegistry {
Expand Down Expand Up @@ -270,6 +281,29 @@ export interface paths extends PathRegistry {
query: DelegatesRequest
}
}
'/v1/register/notifications': {
post: operations['register_device']
parameters: null
}
'/v1/chains/{chainId}/notifications/devices/{uuid}/safes/{safe_address}': {
delete: operations['unregister_safe']
parameters: {
path: {
uuid: string
chainId: string
safe_address: string
}
}
}
'/v1/chains/{chainId}/notifications/devices/{uuid}': {
delete: operations['unregister_device']
parameters: {
path: {
uuid: string
chainId: string
}
}
}
}

export interface operations {
Expand Down Expand Up @@ -689,4 +723,41 @@ export interface operations {
}
}
}
register_device: {
parameters: {
body: RegisterNotificationsRequest
}
responses: {
200: {
schema: void
}
}
}
unregister_safe: {
parameters: {
path: {
uuid: string
chainId: string
safe_address: string
}
}
responses: {
200: {
schema: void
}
}
}
unregister_device: {
parameters: {
path: {
uuid: string
chainId: string
}
}
responses: {
200: {
schema: void
}
}
}
}
22 changes: 22 additions & 0 deletions src/types/notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export enum DeviceType {
ANDROID = 'ANDROID',
IOS = 'IOS',
WEB = 'WEB',
}

type SafeRegistration = {
chainId: string
safes: Array<string>
signatures: Array<string>
}

export type RegisterNotificationsRequest = {
uuid?: string
cloudMessagingToken: string
buildNumber: string
bundle: string
deviceType: DeviceType
version: string
timestamp?: string
safeRegistrations: Array<SafeRegistration>
}
39 changes: 27 additions & 12 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,25 @@ export function stringifyQuery(query?: Params): string {
return searchString ? `?${searchString}` : ''
}

async function parseResponse<T>(resp: Response): Promise<T> {
let json

try {
json = await resp.json()
} catch {
if (resp.headers && resp.headers.get('content-length') !== '0') {
throw new Error(`Invalid response content: ${resp.statusText}`)
}
}

if (!resp.ok) {
const errTxt = isErrorResponse(json) ? `${json.code}: ${json.message}` : resp.statusText
throw new Error(errTxt)
}

return json
}

export async function fetchData<T>(url: string, body?: unknown): Promise<T> {
let options:
| {
Expand All @@ -56,20 +75,16 @@ export async function fetchData<T>(url: string, body?: unknown): Promise<T> {
}

const resp = await fetch(url, options)
let json

try {
json = await resp.json()
} catch {
if (resp.headers && resp.headers.get('content-length') !== '0') {
throw new Error(`Invalid response content: ${resp.statusText}`)
}
}
return parseResponse<T>(resp)
}

if (!resp.ok) {
const errTxt = isErrorResponse(json) ? `${json.code}: ${json.message}` : resp.statusText
throw new Error(errTxt)
export async function deleteData<T>(url: string): Promise<T> {
const options = {
method: 'DELETE',
}

return json
const resp = await fetch(url, options)

return parseResponse<T>(resp)
}
Loading