diff --git a/packages/dapp-toolkit/README.md b/packages/dapp-toolkit/README.md index 13d6b11c..3a129c5d 100644 --- a/packages/dapp-toolkit/README.md +++ b/packages/dapp-toolkit/README.md @@ -449,7 +449,9 @@ Radix transactions are built using "transaction manifests", that use a simple sy It is important to note that what your dApp sends to the Radix Wallet is actually a "transaction manifest stub". It is completed before submission by the Radix Wallet. For example, the Radix Wallet will automatically add a command to lock the necessary amount of network fees from one of the user's accounts. It may also add "assert" commands to the manifest according to user desires for expected returns. -**NOTE:** Information will be provided soon on a ["comforming" transaction manifest stub format](https://docs.radixdlt.com/docs/conforming-transaction-manifest-types) that ensures clear presentation and handling in the Radix Wallet. +> [!NOTE] +> Some of the manifests will have a nice presentation in the Radix Wallet, others will be displayed as raw text. Read more on ["comforming" transaction manifest stub format](https://docs.radixdlt.com/docs/conforming-transaction-manifest-types). + ### Build transaction manifest @@ -457,7 +459,7 @@ We recommend using template strings for constructing simpler transaction manifes ### sendTransaction -This sends the transaction manifest stub to a user's Radix Wallet, where it will be completed, presented to the user for review, signed as required, and submitted to the Radix network to be processed. +This sends the transaction manifest stub to a user's Radix Wallet, where it will be completed, presented to the user for review, signed as required, and submitted to the Radix network to be processed. `sendTransaction` promise will only be resolved after transaction has been committed to the network (either successfuly or rejected/failure). If you want to do your own logic as soon as transaction id is available, please use `onTransactionId` callback. It will be called immediately after RDT receives response from the Radix Wallet. ```typescript type SendTransactionInput = { diff --git a/packages/dapp-toolkit/src/modules/wallet-request/request-items/request-item.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/request-items/request-item.module.ts index 8f6a2a98..8839f397 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/request-items/request-item.module.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/request-items/request-item.module.ts @@ -19,6 +19,7 @@ export const RequestItemModule = (input: RequestItemModuleInput) => { const logger = input?.logger?.getSubLogger({ name: 'RequestItemModule' }) const subscriptions = new Subscription() const storageModule = input.providers.storageModule + const signals = new Map void>() const createItem = ({ type, @@ -38,21 +39,36 @@ export const RequestItemModule = (input: RequestItemModuleInput) => { isOneTimeRequest, }) - const add = (value: { - type: RequestItem['type'] - walletInteraction: WalletInteraction - isOneTimeRequest: boolean - }) => { + const add = ( + value: { + type: RequestItem['type'] + walletInteraction: WalletInteraction + isOneTimeRequest: boolean + }, + onSignal?: (signalValue: string) => void, + ) => { const item = createItem(value) logger?.debug({ method: 'addRequestItem', item, }) + if (onSignal) { + signals.set(item.interactionId, onSignal) + } + return storageModule .setItems({ [item.interactionId]: item }) .map(() => item) } + const getAndRemoveSignal = (interactionId: string) => { + if (signals.has(interactionId)) { + const signal = signals.get(interactionId) + signals.delete(interactionId) + return signal + } + } + const patch = (id: string, partialValue: Partial) => { logger?.debug({ method: 'patchRequestItemStatus', @@ -138,6 +154,7 @@ export const RequestItemModule = (input: RequestItemModuleInput) => { cancel, updateStatus, patch, + getAndRemoveSignal, getById: (id: string) => storageModule.getItemById(id), getPending, requests$, 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 d7879455..9781d518 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 @@ -4,9 +4,7 @@ import type { WalletInteractionResponse } from '../../../schemas' import type { StorageModule } from '../../storage' import type { RequestItemModule } from '../request-items' import { SdkError } from '../../../error' -import { StateModule } from '../../state' import { filter, firstValueFrom, map } from 'rxjs' -import { GatewayModule } from '../../gateway' import { WalletResponseResolver } from './type' import { RequestItem } from 'radix-connect-common' @@ -14,11 +12,8 @@ export type RequestResolverModule = ReturnType export const RequestResolverModule = (input: { logger?: Logger providers: { - stateModule: StateModule storageModule: StorageModule requestItemModule: RequestItemModule - updateConnectButtonStatus: (status: 'fail' | 'success') => void - gatewayModule: GatewayModule resolvers: WalletResponseResolver[] } }) => { diff --git a/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/pre-authorization-response.ts b/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/pre-authorization-response.ts index ca425c7e..3209c659 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/pre-authorization-response.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/pre-authorization-response.ts @@ -1,17 +1,20 @@ import { err, okAsync } from 'neverthrow' -import { WalletInteractionResponse } from '../../../../schemas' +import { + SubintentResponseItem, + WalletInteractionResponse, +} from '../../../../schemas' import { RequestItemModule } from '../../request-items' import { SdkError } from '../../../../error' import { UpdateConnectButtonStatus, WalletResponseResolver } from '../type' const matchResponse = ( input: WalletInteractionResponse, -): string | undefined => { +): SubintentResponseItem | undefined => { if ( input.discriminator === 'success' && input.items.discriminator === 'preAuthorizationResponse' ) { - return input.items.response?.signedPartialTransaction + return input.items.response } } @@ -21,8 +24,10 @@ export const preAuthorizationResponseResolver = updateConnectButtonStatus: UpdateConnectButtonStatus }): WalletResponseResolver => ({ walletInteraction, walletInteractionResponse }) => { - const signedPartialTransaction = matchResponse(walletInteractionResponse) - if (!signedPartialTransaction) return okAsync(undefined) + const response = matchResponse(walletInteractionResponse) + if (!response) return okAsync(undefined) + const { signedPartialTransaction, expirationTimestamp, subintentHash } = + response const { interactionId } = walletInteraction @@ -34,6 +39,8 @@ export const preAuthorizationResponseResolver = status: 'success', metadata: { signedPartialTransaction, + expirationTimestamp, + subintentHash, }, }) .orElse((error) => { diff --git a/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/send-transaction-response.ts b/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/send-transaction-response.ts index 2250c06d..0818d172 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/send-transaction-response.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/request-resolver/resolvers/send-transaction-response.ts @@ -46,8 +46,15 @@ export const sendTransactionResponseResolver = send: { transactionIntentHash }, } = transactionResponse - return gatewayModule - .pollTransactionStatus(transactionIntentHash) + return requestItemModule + .getById(interactionId) + .mapErr(() => SdkError('FailedToGetItemWithInteractionId', interactionId)) + .andTee(() => + requestItemModule.getAndRemoveSignal(interactionId)?.( + transactionIntentHash, + ), + ) + .andThen(() => gatewayModule.pollTransactionStatus(transactionIntentHash)) .andThen(({ status }) => { const isFailedTransaction = determineFailedTransaction(status) const requestItemStatus = isFailedTransaction ? 'fail' : 'success' 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 new file mode 100644 index 00000000..41365075 --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.spec.ts @@ -0,0 +1,126 @@ +import { Logger } from './../../helpers/logger' +import { describe, expect, it, vi } from 'vitest' +import { WalletRequestModule } from './wallet-request' +import { GatewayModule, RadixNetwork, TransactionStatus } from '../gateway' +import { LocalStorageModule } from '../storage' +import { ok, okAsync, ResultAsync } from 'neverthrow' +import { WalletInteractionItems } from '../../schemas' +import { + RequestResolverModule, + sendTransactionResponseResolver, +} from './request-resolver' +import { RequestItemModule } from './request-items' +import { delayAsync } from '../../test-helpers/delay-async' + +const createMockEnvironment = () => { + const storageModule = LocalStorageModule(`rdt:${crypto.randomUUID()}:1`) + const requestItemModule = RequestItemModule({ + providers: { + storageModule, + }, + }) + const gatewayModule = { + pollTransactionStatus: (hash: string) => + ResultAsync.fromSafePromise(delayAsync(2000)).map(() => + ok({ status: 'success' as TransactionStatus }), + ), + } as any + const updateConnectButtonStatus = () => {} + return { + storageModule, + requestItemModule, + gatewayModule, + updateConnectButtonStatus, + } +} + +describe('WalletRequestModule', () => { + describe('given `onTransactionId` callback is provided', () => { + it('should call the callback before polling is finished', async () => { + // Arange + const { + storageModule, + requestItemModule, + gatewayModule, + updateConnectButtonStatus, + } = createMockEnvironment() + + const requestResolverModule = RequestResolverModule({ + providers: { + storageModule, + requestItemModule, + resolvers: [ + sendTransactionResponseResolver({ + gatewayModule, + requestItemModule, + updateConnectButtonStatus, + }), + ], + }, + }) + + const interactionId = 'abcdef' + const resultReturned = vi.fn() + const onTransactionIdSpy = vi.fn() + + const walletRequestModule = WalletRequestModule({ + useCache: false, + networkId: RadixNetwork.Stokenet, + dAppDefinitionAddress: '', + providers: { + stateModule: {} as any, + storageModule, + requestItemModule, + requestResolverModule, + gatewayModule, + walletRequestSdk: { + sendInteraction: () => okAsync({}), + createWalletInteraction: (items: WalletInteractionItems) => ({ + items, + interactionId, + metadata: {} as any, + }), + } as any, + }, + }) + + // Act + walletRequestModule + .sendTransaction({ + transactionManifest: ``, + onTransactionId: onTransactionIdSpy, + }) + .map(resultReturned) + + await delayAsync(50) + + requestResolverModule.addWalletResponses([ + { + interactionId, + discriminator: 'success', + items: { + discriminator: 'transaction', + send: { + transactionIntentHash: 'intent_hash', + }, + }, + }, + ]) + + // Assert + expect(resultReturned).not.toHaveBeenCalled() + await expect + .poll(() => onTransactionIdSpy, { + timeout: 1000, + }) + .toHaveBeenCalledWith('intent_hash') + await expect + .poll(() => resultReturned, { + timeout: 3000, + }) + .toHaveBeenCalledWith( + expect.objectContaining({ transactionIntentHash: 'intent_hash' }), + ) + }) + }) +}) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.ts b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.ts index 894c965a..744378df 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.ts @@ -100,7 +100,6 @@ export const WalletRequestModule = (input: { providers: { storageModule, requestItemModule, - stateModule, resolvers: [ sendTransactionResponseResolver({ gatewayModule, @@ -122,10 +121,6 @@ export const WalletRequestModule = (input: { updateConnectButtonStatus, }), ], - updateConnectButtonStatus: (status) => { - interactionStatusChangeSubject.next(status) - }, - gatewayModule, }, }) @@ -429,11 +424,14 @@ export const WalletRequestModule = (input: { }) return requestItemModule - .add({ - type: 'sendTransaction', - walletInteraction, - isOneTimeRequest: false, - }) + .add( + { + type: 'sendTransaction', + walletInteraction, + isOneTimeRequest: false, + }, + value.onTransactionId, + ) .mapErr(() => SdkError('FailedToAddRequestItem', walletInteraction.interactionId), ) @@ -450,9 +448,6 @@ export const WalletRequestModule = (input: { status: metadata!.transactionStatus as TransactionStatus, } - if (value.onTransactionId) - value.onTransactionId(output.transactionIntentHash) - return status === 'success' ? ok(output) : err(SdkError(output.status, interactionId)) diff --git a/packages/dapp-toolkit/src/schemas/index.ts b/packages/dapp-toolkit/src/schemas/index.ts index 736eda43..519e1e6e 100644 --- a/packages/dapp-toolkit/src/schemas/index.ts +++ b/packages/dapp-toolkit/src/schemas/index.ts @@ -274,6 +274,8 @@ export const SubintentRequestItem = object({ export type SubintentResponseItem = InferOutput export const SubintentResponseItem = object({ + expirationTimestamp: number(), + subintentHash: string(), signedPartialTransaction: string(), }) diff --git a/packages/dapp-toolkit/src/test-helpers/delay-async.ts b/packages/dapp-toolkit/src/test-helpers/delay-async.ts new file mode 100644 index 00000000..b204e3a2 --- /dev/null +++ b/packages/dapp-toolkit/src/test-helpers/delay-async.ts @@ -0,0 +1,6 @@ +export const delayAsync = (ms: number) => + new Promise((resolve) => { + setTimeout(() => { + resolve() + }, ms) + })