Skip to content

Commit

Permalink
fix: call onTransactionId immediately when tx id is available
Browse files Browse the repository at this point in the history
Previously it was called only after gateway polling has finished which is not what it was intended to do.
  • Loading branch information
dawidsowardx committed Nov 29, 2024
1 parent 1936867 commit be34bce
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 17 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 maybeGetSignal = (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,
maybeGetSignal,
getById: (id: string) => storageModule.getItemById(id),
getPending,
requests$,
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.maybeGetSignal(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,132 @@
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 = () => {}
const stateModule = {} as any
return {
storageModule,
requestItemModule,
gatewayModule,
updateConnectButtonStatus,
stateModule,
}
}

describe('WalletRequestModule', () => {
describe('given `onTransactionId` callback is provided', () => {
it('should call the callback before polling is finished', async () => {
// Arange
const {
storageModule,
requestItemModule,
gatewayModule,
updateConnectButtonStatus,
stateModule,
} = createMockEnvironment()

const requestResolverModule = RequestResolverModule({
providers: {
stateModule,
gatewayModule,
storageModule,
requestItemModule,
resolvers: [
sendTransactionResponseResolver({
gatewayModule,
requestItemModule,
updateConnectButtonStatus,
}),
],
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' }),
)
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -429,11 +429,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 +453,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
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 be34bce

Please sign in to comment.