diff --git a/packages/dapp-toolkit/src/modules/storage/local-storage.module.spec.ts b/packages/dapp-toolkit/src/modules/storage/local-storage.module.spec.ts new file mode 100644 index 00000000..23d5c36d --- /dev/null +++ b/packages/dapp-toolkit/src/modules/storage/local-storage.module.spec.ts @@ -0,0 +1,69 @@ +/** + * @vitest-environment jsdom + */ + +import { beforeEach, describe, expect, it } from 'vitest' +import { LocalStorageModule, StorageModule } from './local-storage.module' +import { ResultAsync } from 'neverthrow' + +describe('LocalStorageModule', () => { + let storageModule: StorageModule + + beforeEach(() => { + storageModule = LocalStorageModule(`rdt:${crypto.randomUUID()}:1`) + }) + + it('should store and read data', async () => { + await storageModule.setState({ key: 'value' }) + const data = await storageModule.getState() + expect(data.isOk() && data.value).toEqual({ key: 'value' }) + }) + + describe('getItemById', () => { + it('should get specific item', async () => { + await storageModule.setState({ specific: 'value', key: 'value' }) + const data = await storageModule.getItemById('specific') + expect(data.isOk() && data.value).toEqual('value') + }) + }) + + describe('setItems', () => { + it('should set multiple items separately', async () => { + await storageModule.setItems({ + specific: 'value', + }) + + await storageModule.setItems({ key: 'value' }) + const data = await storageModule.getItems() + expect(data.isOk() && data.value).toEqual({ + specific: 'value', + key: 'value', + }) + }) + + // TODO: This currently fails. Uncomment this test when working on RDT-225 and ensure it's passing + it.skip('should set multiple items at once', async () => { + await ResultAsync.combine([ + storageModule.setItems({ + specific: 'value', + }), + storageModule.setItems({ key: 'value' }), + ]) + + const data = await storageModule.getItems() + expect(data.isOk() && data.value).toEqual({ + specific: 'value', + key: 'value', + }) + }) + }) + + describe('removeItemById', () => { + it('should remove specific item', async () => { + await storageModule.setState({ specific: 'value', key: 'value' }) + await storageModule.removeItemById('specific') + const data = await storageModule.getState() + expect(data.isOk() && data.value).toEqual({ key: 'value' }) + }) + }) +}) diff --git a/packages/dapp-toolkit/src/modules/storage/local-storage.module.ts b/packages/dapp-toolkit/src/modules/storage/local-storage.module.ts index 73063d08..4589fe0a 100644 --- a/packages/dapp-toolkit/src/modules/storage/local-storage.module.ts +++ b/packages/dapp-toolkit/src/modules/storage/local-storage.module.ts @@ -9,6 +9,7 @@ type PartitionKey = | 'requests' | 'state' | 'connectButton' + | 'walletResponses' | 'connectorExtension' type dAppDefinitionAddress = string @@ -53,6 +54,11 @@ export const LocalStorageModule = ( data ? parseJSON(data) : ok({}), ) + const getState = (): ResultAsync => + ResultAsync.fromPromise(getDataAsync(), typedError).andThen((data) => + data ? parseJSON(data) : ok(undefined), + ) + const getItemById = (id: string): ResultAsync => ResultAsync.fromPromise(getDataAsync(), typedError) .andThen((data) => (data ? parseJSON(data) : ok(undefined))) @@ -61,7 +67,21 @@ export const LocalStorageModule = ( const removeItemById = (id: string): ResultAsync => getItems().andThen((items) => { const { [id]: _, ...newItems } = items - return setItems(newItems) + return stringify({ ...newItems }).asyncAndThen((serialized) => { + const result = ResultAsync.fromPromise( + setDataAsync(serialized), + typedError, + ).map(() => { + window.dispatchEvent( + new StorageEvent('storage', { + key: storageKey, + oldValue: JSON.stringify(items), + newValue: serialized, + }), + ) + }) + return result + }) }) const patchItem = (id: string, patch: Partial): ResultAsync => @@ -93,11 +113,6 @@ export const LocalStorageModule = ( const getItemList = (): ResultAsync => getItems().map(Object.values) - const getState = (): ResultAsync => - ResultAsync.fromPromise(getDataAsync(), typedError).andThen((data) => - data ? parseJSON(data) : ok(undefined), - ) - const setState = (newValue: T): ResultAsync => getState().andThen((oldValue) => stringify({ ...(oldValue ?? {}), ...newValue }).asyncAndThen( diff --git a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts index 7988bd8f..44c1d15a 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts @@ -1,4 +1,4 @@ -import { ResultAsync, err, errAsync, ok } from 'neverthrow' +import { ResultAsync, err, errAsync, ok, okAsync } from 'neverthrow' import { Subscription } from 'rxjs' import { EncryptionModule, transformBufferToSealbox } from '../../encryption' import { Session, SessionModule } from '../../session/session.module' @@ -40,6 +40,8 @@ export const RadixConnectRelayModule = (input: { const { baseUrl, providers, walletUrl } = input const { requestItemModule, storageModule } = providers + const walletResponses = storageModule.getPartition('walletResponses') + const encryptionModule = providers?.encryptionModule ?? EncryptionModule() const deepLinkModule = @@ -75,6 +77,62 @@ export const RadixConnectRelayModule = (input: { const subscriptions = new Subscription() + const wait = (timer = 1800) => + new Promise((resolve) => setTimeout(resolve, timer)) + + const decryptWalletResponse = ( + walletResponse: WalletResponse, + ): ResultAsync => { + if ('error' in walletResponse) { + return errAsync({ reason: walletResponse.error }) + } + + return identityModule.get('dApp').andThen((dAppIdentity) => + dAppIdentity.x25519 + .calculateSharedSecret( + walletResponse.publicKey, + input.dAppDefinitionAddress, + ) + .mapErr(() => ({ reason: 'FailedToDeriveSharedSecret' })) + .asyncAndThen((sharedSecret) => + decryptWalletResponseData(sharedSecret, walletResponse.data), + ), + ) + } + + const checkRelayLoop = async () => { + await requestItemModule.getPending().andThen((pendingItems) => { + if (pendingItems.length === 0) { + return okAsync(undefined) + } + + return sessionModule + .getCurrentSession() + .andThen((session) => + radixConnectRelayApiService.getResponses(session.sessionId), + ) + .andThen((responses) => + ResultAsync.combine( + responses.map((response) => decryptWalletResponse(response)), + ).andThen((decryptedResponses) => { + return walletResponses.setItems( + decryptedResponses.reduce( + (acc, response) => { + acc[response.interactionId] = response + return acc + }, + {} as Record, + ), + ) + }), + ) + }) + await wait(15000) + checkRelayLoop() + } + + checkRelayLoop() + const sendWalletInteractionRequest = ({ session, walletInteraction, @@ -156,13 +214,7 @@ export const RadixConnectRelayModule = (input: { publicKey: dAppIdentity.x25519.getPublicKey(), }), ) - .andThen(() => - waitForWalletResponse({ - session, - interactionId: walletInteraction.interactionId, - dAppIdentity, - }), - ), + .andThen(() => waitForWalletResponse(walletInteraction.interactionId)), ) const decryptWalletResponseData = ( @@ -188,50 +240,19 @@ export const RadixConnectRelayModule = (input: { jsError: error, })) - const waitForWalletResponse = ({ - session, - interactionId, - dAppIdentity, - }: { - session: Session - interactionId: string - dAppIdentity: Curve25519 - }): ResultAsync => + const waitForWalletResponse = ( + interactionId: string, + ): ResultAsync => ResultAsync.fromPromise( new Promise(async (resolve, reject) => { let response: WalletInteractionResponse | undefined let error: SdkError | undefined - let retry = 0 - - const wait = (timer = 1500) => - new Promise((resolve) => setTimeout(resolve, timer)) logger?.debug({ method: 'waitForWalletResponse', - sessionId: session.sessionId, interactionId, }) - const getEncryptedWalletResponses = () => - radixConnectRelayApiService.getResponses(session.sessionId) - - const decryptWalletResponse = ( - walletResponse: WalletResponse, - ): ResultAsync => { - if ('error' in walletResponse) { - return errAsync({ reason: walletResponse.error }) - } - return dAppIdentity.x25519 - .calculateSharedSecret( - walletResponse.publicKey, - input.dAppDefinitionAddress, - ) - .mapErr(() => ({ reason: 'FailedToDeriveSharedSecret' })) - .asyncAndThen((sharedSecret) => - decryptWalletResponseData(sharedSecret, walletResponse.data), - ) - } - while (!response) { const requestItemResult = await requestItemModule.getById(interactionId) @@ -251,41 +272,20 @@ export const RadixConnectRelayModule = (input: { } } - const encryptedWalletResponsesResult = - await getEncryptedWalletResponses() - - if (encryptedWalletResponsesResult.isOk()) { - const encryptedWalletResponses = - encryptedWalletResponsesResult.value - - for (const encryptedWalletResponse of encryptedWalletResponses) { - const walletResponseResult = await decryptWalletResponse( - encryptedWalletResponse, - ) - - if (walletResponseResult.isErr()) - logger?.error({ - method: 'waitForWalletResponse.decryptWalletResponse.error', - error: walletResponseResult.error, - sessionId: session.sessionId, - interactionId, - }) - - if (walletResponseResult.isOk()) { - const walletResponse = walletResponseResult.value + const walletResponse = + await walletResponses.getItemById(interactionId) - if (walletResponse.interactionId === interactionId) { - response = walletResponse - await requestItemModule.patch(walletResponse.interactionId, { - walletResponse, - }) - } - } + if (walletResponse.isOk()) { + if (walletResponse.value) { + response = walletResponse.value + await walletResponses.removeItemById(interactionId) + await requestItemModule.patch(interactionId, { + walletResponse: walletResponse.value, + }) } } if (!response) { - retry += 1 await wait() } }