Skip to content

Commit

Permalink
fix: call onTransactionId immediately when tx id is available (#287)
Browse files Browse the repository at this point in the history
* fix: call `onTransactionId` immediately when tx id is available

Previously it was called only after gateway polling has finished which is not what it was intended to do.

* fix: adjust preauthorization response model
  • Loading branch information
dawidsowardx authored Dec 3, 2024
1 parent e0d26fc commit 0458176
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 32 deletions.
6 changes: 4 additions & 2 deletions packages/dapp-toolkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,15 +449,17 @@ 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

We recommend using template strings for constructing simpler transaction manifests. If your dApp is sending complex manifests a manifest builder can be found in [TypeScript Radix Engine Toolkit](https://github.com/radixdlt/typescript-radix-engine-toolkit#building-manifests)

### 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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, (val: string) => void>()

const createItem = ({
type,
Expand All @@ -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<RequestItem>) => {
logger?.debug({
method: 'patchRequestItemStatus',
Expand Down Expand Up @@ -138,6 +154,7 @@ export const RequestItemModule = (input: RequestItemModuleInput) => {
cancel,
updateStatus,
patch,
getAndRemoveSignal,
getById: (id: string) => storageModule.getItemById(id),
getPending,
requests$,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,16 @@ 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'

export type RequestResolverModule = ReturnType<typeof RequestResolverModule>
export const RequestResolverModule = (input: {
logger?: Logger
providers: {
stateModule: StateModule
storageModule: StorageModule
requestItemModule: RequestItemModule
updateConnectButtonStatus: (status: 'fail' | 'success') => void
gatewayModule: GatewayModule
resolvers: WalletResponseResolver[]
}
}) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}

Expand All @@ -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

Expand All @@ -34,6 +39,8 @@ export const preAuthorizationResponseResolver =
status: 'success',
metadata: {
signedPartialTransaction,
expirationTimestamp,
subintentHash,
},
})
.orElse((error) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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' }),
)
})
})
})
21 changes: 8 additions & 13 deletions packages/dapp-toolkit/src/modules/wallet-request/wallet-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ export const WalletRequestModule = (input: {
providers: {
storageModule,
requestItemModule,
stateModule,
resolvers: [
sendTransactionResponseResolver({
gatewayModule,
Expand All @@ -122,10 +121,6 @@ export const WalletRequestModule = (input: {
updateConnectButtonStatus,
}),
],
updateConnectButtonStatus: (status) => {
interactionStatusChangeSubject.next(status)
},
gatewayModule,
},
})

Expand Down Expand Up @@ -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),
)
Expand All @@ -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))
Expand Down
2 changes: 2 additions & 0 deletions packages/dapp-toolkit/src/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,8 @@ export const SubintentRequestItem = object({

export type SubintentResponseItem = InferOutput<typeof SubintentResponseItem>
export const SubintentResponseItem = object({
expirationTimestamp: number(),
subintentHash: string(),
signedPartialTransaction: string(),
})

Expand Down
6 changes: 6 additions & 0 deletions packages/dapp-toolkit/src/test-helpers/delay-async.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const delayAsync = (ms: number) =>
new Promise<void>((resolve) => {
setTimeout(() => {
resolve()
}, ms)
})

0 comments on commit 0458176

Please sign in to comment.