Skip to content
This repository has been archived by the owner on Aug 16, 2024. It is now read-only.

Commit

Permalink
feat: [ABW-553] add login request
Browse files Browse the repository at this point in the history
  • Loading branch information
xstelea committed Nov 1, 2022
1 parent 4b743f2 commit a4c58db
Show file tree
Hide file tree
Showing 15 changed files with 254 additions and 146 deletions.
14 changes: 14 additions & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Wallet SDK</title>
</head>
<body>
<script type="module" src="/index.ts"></script>
<button id="account-address-btn">Get account addresses</button>
<button id="login-btn">Login</button>
<div id="results"></div>
</body>
</html>
46 changes: 46 additions & 0 deletions examples/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import loglevel from 'loglevel'
import WalletSdk from '../lib/wallet-sdk'
import { Buffer } from 'buffer'
import { Result } from 'neverthrow'

const sdk = WalletSdk({ dAppId: 'radixDashboard' })
loglevel.setLevel('debug')

const displayResults = (result: Result<any, any>) => {
document.getElementById('results')!.innerHTML = `<pre>${JSON.stringify(
result.isErr() ? result.error : result.value,
null,
2
)}</pre>`
}

const clearResults = () => {
document.getElementById('results')!.innerHTML = ``
}

document.getElementById('login-btn')!.onclick = async () => {
clearResults()
// this is for example purposes. The challenge should be generated and stored on the backend to prevent replay attacks
const challenge = Buffer.from(
crypto.getRandomValues(new Uint8Array(32))
).toString('hex')

displayResults(
await sdk.request({
login: {
challenge,
},
})
)
}

document.getElementById('account-address-btn')!.onclick = async () => {
clearResults()
displayResults(
await sdk.request({
accountAddresses: {
numberOfAddresses: 1,
},
})
)
}
8 changes: 8 additions & 0 deletions examples/public/.well-known/radix.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"dApps": [
{
"id": "radixDashboard",
"definitionAddress": "account_tdx_a_1qd5svul20u30qnq408zhj2tw5evqrunq48eg0jsjf9qsx5t8qu"
}
]
}
189 changes: 131 additions & 58 deletions lib/__tests__/wallet-sdk.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,55 @@
/* eslint-disable max-params */
/* eslint-disable array-callback-return */
/* eslint-disable max-nested-callbacks */
import WalletSdk, { Network, WalletSdk as WalletSdkType } from '../wallet-sdk'
import { subscribeSpyTo } from '@hirez_io/observer-spy'
import log from 'loglevel'
import { testHelper } from '../test-utils/helper'
import { messageLifeCycleEvent } from '../messages/events/_types'
import { requestType } from '../methods'

let sdk: WalletSdkType
import {
RequestMethodInput,
RequestMethodResponse,
RequestWalletResponse,
} from '../methods'
import { IncomingMessage } from '../messages'

const mockAccountAddressesWalletResponse: RequestWalletResponse['accountAddresses'] =
{
requestType: 'accountAddresses',
addresses: [
{
address: 'rdx61333732663539372d383861352d3461',
label: 'address-0',
},
{
address: 'rdx34316364646264632d616434662d3463',
label: 'address-1',
},
{
address: 'rdx34313261646463652d363539392d3462',
label: 'address-2',
},
],
}

const mockPersonaDataWalletResponse: RequestWalletResponse['personaData'] = {
requestType: 'personaData',
personaData: [{ field: 'email', value: '[email protected]' }],
}

const mockLoginWalletResponse: RequestWalletResponse['login'] = {
requestType: 'login',
challenge: '60ffcd3fae6e57b5fbbc2ac241250575f342dfb8287499171a322a5aaf56ee20',
publicKey: '<<TEST_PUBLIC_KEY>>',
identityComponentAddress: '<<TEST_IDENTITY_ADDRESS>>',
signature: '<<TEST_SIGNATURE>>',
}

describe('sdk flow', () => {
let sdk: WalletSdkType

beforeEach(() => {
log.setLevel('silent')
sdk = WalletSdk()
sdk = WalletSdk({ dAppId: 'radixDashboard' })
log.setLevel('debug')
})

Expand All @@ -21,70 +58,85 @@ describe('sdk flow', () => {
sdk.destroy()
})

describe('request method', () => {
it('should send request and receive response', (done) => {
const eventDispatchSpy = jest.spyOn(globalThis, 'dispatchEvent')

const outgoingMessageSpy = subscribeSpyTo(
sdk.__subjects.outgoingMessageSubject
)

const messageEventSpy = subscribeSpyTo(
sdk.__subjects.messageLifeCycleEventSubject
)
const createRequestHelper = ({
input,
walletResponse,
eventCallback,
callback,
}: {
input: RequestMethodInput
walletResponse: Omit<IncomingMessage['request'], 'requestId'>
eventCallback?: () => void
callback: (message: RequestMethodResponse) => void
}) => {
const eventDispatchSpy = jest.spyOn(globalThis, 'dispatchEvent')
const outgoingMessageSpy = subscribeSpyTo(
sdk.__subjects.outgoingMessageSubject
)
const messageEventSpy = subscribeSpyTo(
sdk.__subjects.messageLifeCycleEventSubject
)

sdk.request(input, eventCallback).map((response) => {
callback(response)
})

const callbackSpy = jest.fn()
expect(eventDispatchSpy).toBeCalled()

const addresses = testHelper.createAccountAddressResponse(3)
const outgoingMessage = outgoingMessageSpy.getFirstValue()
sendReceivedEvent(outgoingMessage.requestId)

sdk
.request(
{
accountAddresses: { reset: true },
personaData: {
revokeOngoingAccess: ['firstName'],
fields: ['email'],
},
},
callbackSpy
)
.map((message) => {
expect(message.accountAddresses).toEqual(addresses.addresses)
expect(message.personaData).toEqual([
{ field: 'email', value: '[email protected]' },
])
done()
})
sdk.__subjects.incomingMessageSubject.next({
...walletResponse,
requestId: outgoingMessage.requestId,
})

expect(eventDispatchSpy).toBeCalled()
return {
outgoingMessage,
getMessageEvents: () => messageEventSpy.getValues(),
}
}

const outgoingMessage = outgoingMessageSpy.getFirstValue()
const sendReceivedEvent = (requestId: string) => {
sdk.__subjects.incomingMessageSubject.next({
requestId,
eventType: messageLifeCycleEvent.receivedByExtension,
})
}

expect(outgoingMessage.metadata.networkId).toBe(Network.Mainnet)
expect((outgoingMessage.payload as any)[0].reset).toBe(true)
expect((outgoingMessage.payload as any)[1].revokeOngoingAccess).toEqual([
'firstName',
])
describe('request method', () => {
it('should request account addresses and persona data', (done) => {
const walletResponse: Omit<IncomingMessage['request'], 'requestId'> = {
method: 'request',
payload: [
mockAccountAddressesWalletResponse,
mockPersonaDataWalletResponse,
],
}

sdk.__subjects.incomingMessageSubject.next({
requestId: outgoingMessage.requestId,
eventType: messageLifeCycleEvent.receivedByExtension,
})
const callbackSpy = jest.fn()

const incomingMessage = testHelper.createRequestReponse(
outgoingMessage.requestId,
[
addresses,
{
requestType: requestType.personaData,
personaData: [{ field: 'email', value: '[email protected]' }],
const { outgoingMessage, getMessageEvents } = createRequestHelper({
input: {
accountAddresses: {},
personaData: {
fields: ['email'],
},
]
)
},
walletResponse,
eventCallback: callbackSpy,
callback: (response) => {
expect({
accountAddresses: mockAccountAddressesWalletResponse.addresses,
personaData: mockPersonaDataWalletResponse.personaData,
}).toEqual(response)
done()
},
})

sdk.__subjects.incomingMessageSubject.next(incomingMessage)
expect(outgoingMessage.metadata.networkId).toBe(Network.Mainnet)

expect(messageEventSpy.getValues()).toEqual([
expect(getMessageEvents()).toEqual([
{
requestId: outgoingMessage.requestId,
eventType: messageLifeCycleEvent.receivedByExtension,
Expand All @@ -95,6 +147,27 @@ describe('sdk flow', () => {
messageLifeCycleEvent.receivedByExtension
)
})
it('should handle login request', (done) => {
const walletResponse: Omit<IncomingMessage['request'], 'requestId'> = {
method: 'request',
payload: [mockLoginWalletResponse],
}

createRequestHelper({
input: {
login: {
challenge:
'60ffcd3fae6e57b5fbbc2ac241250575f342dfb8287499171a322a5aaf56ee20',
},
},
walletResponse,
callback: (response) => {
const { requestType, ...expected } = mockLoginWalletResponse
expect({ login: expected }).toEqual(response)
done()
},
})
})
})

describe('send transaction method', () => {
Expand Down
7 changes: 5 additions & 2 deletions lib/messages/observables/send-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ import { messageEvents } from './message-events'
export type SendMessage = ReturnType<typeof sendMessage>

export const sendMessage =
(networkId: number, subjects: SubjectsType) =>
(
{ networkId, dAppId }: { networkId: number; dAppId: string },
subjects: SubjectsType
) =>
<M extends MethodType>(
message: OutgoingMessageType,
eventCallback?: (eventType: MessageLifeCycleEvent) => void
): Observable<Result<IncomingMessage[M]['payload'], SdkError>> => {
const metadata = { networkId }
const metadata = { networkId, dAppId }
subjects.outgoingMessageSubject.next({ ...message, metadata })

const response$ = subjects.responseSubject.pipe(
Expand Down
30 changes: 21 additions & 9 deletions lib/methods/request/_types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const requestType = {
accountAddresses: 'accountAddresses',
personaData: 'personaData',
login: 'login',
} as const

export const requestTypeSet = new Set(Object.keys(requestType))
Expand All @@ -9,35 +10,31 @@ export type RequestTypes = keyof typeof requestType

type AccountAddressesRequestMethodInput = {
numberOfAddresses?: number
ongoing?: boolean
reset?: boolean
}

type PersonaDataRequestMethodInput = {
fields: string[]
ongoing?: boolean
reset?: boolean
revokeOngoingAccess?: string[]
}

type LoginRequestMethodInput = {
challenge: string
}

export type RequestMethodInput = Partial<{
[requestType.accountAddresses]: AccountAddressesRequestMethodInput
[requestType.personaData]: PersonaDataRequestMethodInput
[requestType.login]: LoginRequestMethodInput
}>

type AccountAddressesWalletRequestItem = {
requestType: typeof requestType['accountAddresses']
ongoing: boolean
numberOfAddresses?: number
reset: boolean
}

type PersonaDataWalletRequestItem = {
requestType: typeof requestType['personaData']
ongoing: boolean
fields: string[]
reset: boolean
revokeOngoingAccess?: string[]
}

export type WalletRequestItem =
Expand All @@ -54,14 +51,29 @@ type PersonaDataRequestWalletResponse = {
personaData: { field: string; value: string }[]
}

type LoginRequestWalletResponse = {
requestType: typeof requestType['login']
challenge: string
signature: string
publicKey: string
identityComponentAddress: string
}

export type RequestWalletResponse = {
[requestType.accountAddresses]: AccountAddressesRequestWalletResponse
[requestType.personaData]: PersonaDataRequestWalletResponse
[requestType.login]: LoginRequestWalletResponse
}

export type RequestWalletResponseType = RequestWalletResponse[RequestTypes]

export type RequestMethodResponse = Partial<{
[requestType.accountAddresses]: RequestWalletResponse['accountAddresses']['addresses']
[requestType.personaData]: RequestWalletResponse['personaData']['personaData']
[requestType.login]: {
challenge: string
signature: string
publicKey: string
identityComponentAddress: string
}
}>
Loading

0 comments on commit a4c58db

Please sign in to comment.