From 42548788cd0fc0fcf0c8fadc2828578e9cac5da0 Mon Sep 17 00:00:00 2001
From: Dawid Sowa <dawid.sowa@rdx.works>
Date: Thu, 5 Dec 2024 21:20:38 +0100
Subject: [PATCH 1/3] fix: error interactions regression

failed interactions were never resolved in connect button
---
 examples/simple-dapp/src/main.ts              |  5 ++--
 packages/dapp-toolkit/src/_types.ts           |  3 +-
 .../src/helpers/validate-wallet-response.ts   | 30 +++++++------------
 .../request-resolver.module.ts                | 24 ++++++++-------
 .../connector-extension.module.ts             |  3 +-
 .../radix-connect-relay.module.ts             |  3 +-
 .../wallet-request/wallet-request-sdk.ts      | 23 ++++++++++----
 7 files changed, 50 insertions(+), 41 deletions(-)

diff --git a/examples/simple-dapp/src/main.ts b/examples/simple-dapp/src/main.ts
index ca6e273a..7a2be7c0 100644
--- a/examples/simple-dapp/src/main.ts
+++ b/examples/simple-dapp/src/main.ts
@@ -217,8 +217,8 @@ resetButton.onclick = () => {
   window.location.replace(window.location.origin)
 }
 
-sendTxButton.onclick = () => {
-  dAppToolkit.walletApi.sendTransaction({
+sendTxButton.onclick = async () => {
+  const res = await dAppToolkit.walletApi.sendTransaction({
     transactionManifest: `
     CALL_METHOD
       Address("component_tdx_2_1cptxxxxxxxxxfaucetxxxxxxxxx000527798379xxxxxxxxxyulkzl")
@@ -232,6 +232,7 @@ sendTxButton.onclick = () => {
       Enum<0u8>()
     ;`,
   })
+  console.log('send tx result', res)
 }
 
 oneTimeRequest.onclick = () => {
diff --git a/packages/dapp-toolkit/src/_types.ts b/packages/dapp-toolkit/src/_types.ts
index 68038185..844969b8 100644
--- a/packages/dapp-toolkit/src/_types.ts
+++ b/packages/dapp-toolkit/src/_types.ts
@@ -7,6 +7,7 @@ import {
   Persona,
   PersonaDataName,
   WalletInteraction,
+  WalletInteractionResponse,
 } from './schemas'
 import type { Logger } from './helpers'
 import type { SdkError } from './error'
@@ -175,7 +176,7 @@ export type TransportProvider = {
   send: (
     walletInteraction: WalletInteraction,
     callbackFns: Partial<CallbackFns>,
-  ) => ResultAsync<unknown, SdkError>
+  ) => ResultAsync<WalletInteractionResponse, SdkError>
   disconnect: () => void
   destroy: () => void
 }
diff --git a/packages/dapp-toolkit/src/helpers/validate-wallet-response.ts b/packages/dapp-toolkit/src/helpers/validate-wallet-response.ts
index b8856335..a99b12a2 100644
--- a/packages/dapp-toolkit/src/helpers/validate-wallet-response.ts
+++ b/packages/dapp-toolkit/src/helpers/validate-wallet-response.ts
@@ -1,30 +1,22 @@
-import { Result, ResultAsync, errAsync, okAsync } from 'neverthrow'
-import {
-  WalletInteractionResponse,
-  WalletInteractionSuccessResponse,
-} from '../schemas'
+import { Result } from 'neverthrow'
+import { WalletInteractionResponse } from '../schemas'
 import { SdkError } from '../error'
 import { parse } from 'valibot'
 
 export const validateWalletResponse = (
   walletResponse: unknown,
-): ResultAsync<
-  WalletInteractionSuccessResponse,
-  SdkError | { discriminator: 'failure'; interactionId: string; error: string }
-> => {
+): Result<WalletInteractionResponse, SdkError> => {
   const fn = Result.fromThrowable(
     (_) => parse(WalletInteractionResponse, _),
     (error) => error,
   )
 
-  const result = fn(walletResponse)
-  if (result.isErr()) {
-    return errAsync(SdkError('walletResponseValidation', '', 'Invalid input'))
-  } else if (result.isOk()) {
-    return result.value.discriminator === 'success'
-      ? okAsync(result.value)
-      : errAsync(result.value)
-  }
-
-  return errAsync(SdkError('walletResponseValidation', ''))
+  return fn(walletResponse).mapErr((response) =>
+    SdkError(
+      'walletResponseValidation',
+      (walletResponse as any)?.interactionId,
+      'Invalid input',
+      response,
+    ),
+  )
 }
diff --git a/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/request-resolver.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/request-resolver.module.ts
index 9781d518..d2fe73d7 100644
--- a/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/request-resolver.module.ts
+++ b/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/request-resolver.module.ts
@@ -1,4 +1,4 @@
-import { err, ok, okAsync, ResultAsync } from 'neverthrow'
+import { err, ok, okAsync, Result, ResultAsync } from 'neverthrow'
 import { validateWalletResponse, type Logger } from '../../../helpers'
 import type { WalletInteractionResponse } from '../../../schemas'
 import type { StorageModule } from '../../storage'
@@ -62,17 +62,19 @@ export const RequestResolverModule = (input: {
     requestItemModule.patch(interactionId, { sentToWallet: true })
 
   const addWalletResponses = (responses: WalletInteractionResponse[]) =>
-    ResultAsync.combine(responses.map(validateWalletResponse)).andThen(() =>
-      walletResponses.setItems(
-        responses.reduce<Record<string, WalletInteractionResponse>>(
-          (acc, response) => {
-            acc[response.interactionId] = response
-            return acc
-          },
-          {},
+    Result.combine(responses.map(validateWalletResponse))
+      .asyncAndThen(() =>
+        walletResponses.setItems(
+          responses.reduce<Record<string, WalletInteractionResponse>>(
+            (acc, response) => {
+              acc[response.interactionId] = response
+              return acc
+            },
+            {},
+          ),
         ),
-      ),
-    )
+      )
+      .mapErr((error) => logger?.error({ method: 'addWalletResponses', error }))
 
   const toRequestItemMap = (items: RequestItem[]) =>
     items.reduce<Record<string, RequestItem>>(
diff --git a/packages/dapp-toolkit/src/modules/wallet-request/transport/connector-extension/connector-extension.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/transport/connector-extension/connector-extension.module.ts
index 642196a5..0bbf9cdd 100644
--- a/packages/dapp-toolkit/src/modules/wallet-request/transport/connector-extension/connector-extension.module.ts
+++ b/packages/dapp-toolkit/src/modules/wallet-request/transport/connector-extension/connector-extension.module.ts
@@ -27,6 +27,7 @@ import {
   MessageLifeCycleExtensionStatusEvent,
   WalletInteraction,
   WalletInteractionExtensionInteraction,
+  WalletInteractionResponse,
   eventType,
 } from '../../../../schemas'
 import { StorageModule } from '../../../storage'
@@ -143,7 +144,7 @@ export const ConnectorExtensionModule = (input: {
   const sendWalletInteraction = (
     walletInteraction: WalletInteraction,
     callbackFns: Partial<CallbackFns>,
-  ): ResultAsync<unknown, SdkError> => {
+  ): ResultAsync<WalletInteractionResponse, SdkError> => {
     const cancelRequestSubject = new Subject<Err<never, SdkError>>()
 
     const maybeResolved$ = from(
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 58fea336..3b56bc65 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
@@ -156,7 +156,7 @@ export const RadixConnectRelayModule = (input: {
   const sendToWallet = (
     walletInteraction: WalletInteraction,
     callbackFns: Partial<CallbackFns>,
-  ): ResultAsync<unknown, SdkError> =>
+  ): ResultAsync<WalletInteractionResponse, SdkError> =>
     ResultAsync.combine([
       sessionModule
         .getCurrentSession()
@@ -194,6 +194,7 @@ export const RadixConnectRelayModule = (input: {
             walletInteraction.interactionId,
           ),
         )
+        .map((requestItem) => requestItem.walletResponse),
     )
 
   const decryptWalletResponseData = (
diff --git a/packages/dapp-toolkit/src/modules/wallet-request/wallet-request-sdk.ts b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request-sdk.ts
index f13a9a8c..0c2ecb7b 100644
--- a/packages/dapp-toolkit/src/modules/wallet-request/wallet-request-sdk.ts
+++ b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request-sdk.ts
@@ -1,12 +1,14 @@
-import { Result, ResultAsync, err, ok } from 'neverthrow'
+import { Result, ResultAsync, err, errAsync, ok, okAsync } from 'neverthrow'
 import { TransportProvider } from '../../_types'
-import { Logger, validateWalletResponse } from '../../helpers'
+import { Logger } from '../../helpers'
 import {
   Metadata,
   CallbackFns,
   WalletInteractionItems,
   WalletInteraction,
+  WalletInteractionFailureResponse,
   WalletInteractionResponse,
+  WalletInteractionSuccessResponse,
 } from '../../schemas'
 import { parse } from 'valibot'
 import { SdkError } from '../../error'
@@ -86,15 +88,24 @@ export const WalletRequestSdk = (input: WalletRequestSdkInput) => {
       items,
     }: { interactionId?: string; items: WalletInteraction['items'] },
     callbackFns: Partial<CallbackFns> = {},
-  ): ResultAsync<unknown, SdkError> =>
+  ): ResultAsync<
+    WalletInteractionSuccessResponse,
+    SdkError | WalletInteractionFailureResponse
+  > =>
     withInterceptor({
       items,
       interactionId,
       metadata,
     }).andThen((walletInteraction) =>
-      getTransport(walletInteraction.interactionId).asyncAndThen((transport) =>
-        transport.send(walletInteraction, callbackFns),
-      ),
+      getTransport(walletInteraction.interactionId)
+        .asyncAndThen((transport) =>
+          transport.send(walletInteraction, callbackFns),
+        )
+        .andThen((response) =>
+          response.discriminator === 'failure'
+            ? errAsync(response)
+            : okAsync(response),
+        ),
     )
 
   return {

From 51201aa83256fa4eaf131f87f57974e2060b02e5 Mon Sep 17 00:00:00 2001
From: Dawid Sowa <dawid.sowa@rdx.works>
Date: Fri, 6 Dec 2024 09:58:44 +0100
Subject: [PATCH 2/3] refactor: optionally provide interactionIdFactory for
 WalletRequestSdk module

---
 .../src/modules/wallet-request/wallet-request-sdk.ts         | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/packages/dapp-toolkit/src/modules/wallet-request/wallet-request-sdk.ts b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request-sdk.ts
index 0c2ecb7b..a79732af 100644
--- a/packages/dapp-toolkit/src/modules/wallet-request/wallet-request-sdk.ts
+++ b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request-sdk.ts
@@ -24,6 +24,7 @@ export type WalletRequestSdkInput = {
   ) => Promise<WalletInteraction>
   providers: {
     transports: TransportProvider[]
+    interactionIdFactory?: () => string
   }
 }
 export type WalletRequestSdk = ReturnType<typeof WalletRequestSdk>
@@ -36,6 +37,8 @@ export const WalletRequestSdk = (input: WalletRequestSdkInput) => {
     origin: input.origin || window.location.origin,
   } as Metadata
 
+  const interactionIdFactory = input.providers.interactionIdFactory ?? uuidV4
+
   parse(Metadata, metadata)
 
   const logger = input?.logger?.getSubLogger({ name: 'WalletSdk' })
@@ -52,7 +55,7 @@ export const WalletRequestSdk = (input: WalletRequestSdkInput) => {
 
   const createWalletInteraction = (
     items: WalletInteractionItems,
-    interactionId = uuidV4(),
+    interactionId = interactionIdFactory(),
   ): WalletInteraction => ({
     items,
     interactionId,

From 6b337aa13b50b75e47dcd913a98a972db01f465e Mon Sep 17 00:00:00 2001
From: Dawid Sowa <dawid.sowa@rdx.works>
Date: Fri, 6 Dec 2024 09:58:56 +0100
Subject: [PATCH 3/3] test: fix failing unit tests

---
 .../helpers/validate-wallet-response.spec.ts  | 31 ++------
 .../transport.testing-module.ts               | 35 +++++++++
 .../wallet-request/wallet-request.spec.ts     | 77 ++++++++++++++++++-
 packages/dapp-toolkit/vitest.config.ts        |  1 +
 4 files changed, 120 insertions(+), 24 deletions(-)
 create mode 100644 packages/dapp-toolkit/src/modules/wallet-request/transport/testing-transport/transport.testing-module.ts

diff --git a/packages/dapp-toolkit/src/helpers/validate-wallet-response.spec.ts b/packages/dapp-toolkit/src/helpers/validate-wallet-response.spec.ts
index 2aee72f5..a858b6bf 100644
--- a/packages/dapp-toolkit/src/helpers/validate-wallet-response.spec.ts
+++ b/packages/dapp-toolkit/src/helpers/validate-wallet-response.spec.ts
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest'
 import { validateWalletResponse } from './validate-wallet-response'
 
 describe('validateWalletResponse', () => {
-  it('should parse valid response', async () => {
+  it('should parse valid response', () => {
     const walletResponse = {
       discriminator: 'success',
       interactionId: 'ab0f0190-1ae1-424b-a2c5-c36838f5b136',
@@ -38,7 +38,7 @@ describe('validateWalletResponse', () => {
       },
     }
 
-    const result = await validateWalletResponse(walletResponse)
+    const result = validateWalletResponse(walletResponse)
 
     expect(result.isOk() && result.value).toEqual(walletResponse)
   })
@@ -48,26 +48,11 @@ describe('validateWalletResponse', () => {
 
     const result = await validateWalletResponse(walletResponse)
 
-    expect(result.isErr() && result.error).toEqual({
-      error: 'walletResponseValidation',
-      interactionId: '',
-      message: 'Invalid input',
-    })
-  })
-
-  it('should return error valid failure response', async () => {
-    const walletResponse = {
-      discriminator: 'failure',
-      interactionId: '8cefec84-542d-40af-8782-b89df05db8ac',
-      error: 'rejectedByUser',
-    }
-
-    const result = await validateWalletResponse(walletResponse)
-
-    expect(result.isErr() && result.error).toEqual({
-      discriminator: 'failure',
-      interactionId: '8cefec84-542d-40af-8782-b89df05db8ac',
-      error: 'rejectedByUser',
-    })
+    expect(result.isErr() && result.error).toEqual(
+      expect.objectContaining({
+        error: 'walletResponseValidation',
+        message: 'Invalid input',
+      }),
+    )
   })
 })
diff --git a/packages/dapp-toolkit/src/modules/wallet-request/transport/testing-transport/transport.testing-module.ts b/packages/dapp-toolkit/src/modules/wallet-request/transport/testing-transport/transport.testing-module.ts
new file mode 100644
index 00000000..6686fa8c
--- /dev/null
+++ b/packages/dapp-toolkit/src/modules/wallet-request/transport/testing-transport/transport.testing-module.ts
@@ -0,0 +1,35 @@
+import { okAsync, ResultAsync } from 'neverthrow'
+import { TransportProvider } from '../../../../_types'
+import { WalletInteractionResponse } from '../../../../schemas'
+
+export const TestingTransportModule = ({
+  requestResolverModule,
+}: {
+  requestResolverModule: {
+    addWalletResponses: (
+      responses: WalletInteractionResponse[],
+    ) => ResultAsync<unknown, unknown>
+  }
+}): TransportProvider & {
+  setNextWalletResponse: (response: WalletInteractionResponse) => void
+} => {
+  let _isSupported = true
+  let id = 'TestingTransportModule'
+  let _sendResponse: WalletInteractionResponse
+
+  return {
+    id,
+    isSupported: () => _isSupported,
+    destroy: () => {},
+    disconnect: () => {},
+    send: () => {
+      requestResolverModule.addWalletResponses([_sendResponse])
+      return okAsync(_sendResponse)
+    },
+
+    // Test helpers
+    setNextWalletResponse: (response: WalletInteractionResponse) => {
+      _sendResponse = response
+    },
+  }
+}
diff --git a/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.spec.ts b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.spec.ts
index 884295fe..c9c4c315 100644
--- a/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.spec.ts
+++ b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.spec.ts
@@ -3,13 +3,20 @@ import { WalletRequestModule } from './wallet-request'
 import { RadixNetwork, TransactionStatus } from '../gateway'
 import { LocalStorageModule } from '../storage'
 import { ok, okAsync, ResultAsync } from 'neverthrow'
-import { WalletInteractionItems } from '../../schemas'
 import {
+  WalletInteractionFailureResponse,
+  WalletInteractionItems,
+} from '../../schemas'
+import {
+  failedResponseResolver,
   RequestResolverModule,
   sendTransactionResponseResolver,
 } from './request-resolver'
 import { RequestItemModule } from './request-items'
 import { delayAsync } from '../../test-helpers/delay-async'
+import { WalletRequestSdk } from './wallet-request-sdk'
+import { TransportProvider } from '../../_types'
+import { TestingTransportModule } from './transport/testing-transport/transport.testing-module'
 
 const createMockEnvironment = () => {
   const storageModule = LocalStorageModule(`rdt:${crypto.randomUUID()}:1`)
@@ -124,4 +131,72 @@ describe('WalletRequestModule', () => {
         )
     })
   })
+
+  describe('GIVEN wallet responds with discriminator "failure"', () => {
+    it('should return error result', async () => {
+      // Arange
+      const {
+        storageModule,
+        requestItemModule,
+        gatewayModule,
+        updateConnectButtonStatus,
+      } = createMockEnvironment()
+
+      const requestResolverModule = RequestResolverModule({
+        providers: {
+          storageModule,
+          requestItemModule,
+          resolvers: [
+            failedResponseResolver({
+              requestItemModule,
+              updateConnectButtonStatus,
+            }),
+          ],
+        },
+      })
+
+      const interactionId = '8cefec84-542d-40af-8782-b89df05db8ac'
+
+      const testingTransport = TestingTransportModule({ requestResolverModule })
+      testingTransport.setNextWalletResponse({
+        discriminator: 'failure',
+        interactionId: '8cefec84-542d-40af-8782-b89df05db8ac',
+        error: 'rejectedByUser',
+      })
+
+      const walletRequestModule = WalletRequestModule({
+        useCache: false,
+        networkId: RadixNetwork.Stokenet,
+        dAppDefinitionAddress: '',
+        providers: {
+          stateModule: {} as any,
+          storageModule,
+          requestItemModule,
+          requestResolverModule,
+          gatewayModule,
+          walletRequestSdk: WalletRequestSdk({
+            networkId: 2,
+            dAppDefinitionAddress: '',
+            providers: {
+              interactionIdFactory: () => interactionId,
+              transports: [testingTransport],
+            },
+          }),
+        },
+      })
+
+      // Act
+      const result = await walletRequestModule.sendTransaction({
+        transactionManifest: ``,
+      })
+
+      // Assert
+      expect(result.isErr() && result.error).toEqual(
+        expect.objectContaining({
+          discriminator: 'failure',
+          error: 'rejectedByUser',
+        }),
+      )
+    })
+  })
 })
diff --git a/packages/dapp-toolkit/vitest.config.ts b/packages/dapp-toolkit/vitest.config.ts
index 466ada91..e3cadbda 100644
--- a/packages/dapp-toolkit/vitest.config.ts
+++ b/packages/dapp-toolkit/vitest.config.ts
@@ -7,6 +7,7 @@ export default defineConfig({
     coverage: {
       provider: 'v8',
       include: ['src/**/*.ts'],
+      exclude: ['src/**/*.testing-module.ts'],
       reporter: ['lcov', 'text', 'html'],
     },
     include: ['src/**/*.spec.ts'],