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, - ) => ResultAsync + ) => ResultAsync disconnect: () => void destroy: () => void } 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/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 => { 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>( - (acc, response) => { - acc[response.interactionId] = response - return acc - }, - {}, + Result.combine(responses.map(validateWalletResponse)) + .asyncAndThen(() => + walletResponses.setItems( + responses.reduce>( + (acc, response) => { + acc[response.interactionId] = response + return acc + }, + {}, + ), ), - ), - ) + ) + .mapErr((error) => logger?.error({ method: 'addWalletResponses', error })) const toRequestItemMap = (items: RequestItem[]) => items.reduce>( 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, - ): ResultAsync => { + ): ResultAsync => { const cancelRequestSubject = new Subject>() 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, - ): ResultAsync => + ): ResultAsync => 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/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 + } +}): 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-sdk.ts b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request-sdk.ts index f13a9a8c..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 @@ -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' @@ -22,6 +24,7 @@ export type WalletRequestSdkInput = { ) => Promise providers: { transports: TransportProvider[] + interactionIdFactory?: () => string } } export type WalletRequestSdk = ReturnType @@ -34,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' }) @@ -50,7 +55,7 @@ export const WalletRequestSdk = (input: WalletRequestSdkInput) => { const createWalletInteraction = ( items: WalletInteractionItems, - interactionId = uuidV4(), + interactionId = interactionIdFactory(), ): WalletInteraction => ({ items, interactionId, @@ -86,15 +91,24 @@ export const WalletRequestSdk = (input: WalletRequestSdkInput) => { items, }: { interactionId?: string; items: WalletInteraction['items'] }, callbackFns: Partial = {}, - ): ResultAsync => + ): 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 { 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'],