Skip to content

Commit

Permalink
fix: RCfM interactions getting stuck (#253)
Browse files Browse the repository at this point in the history
* chore: remove obsolete `callbackPath`
* chore: fix watch in dev mode
* chore: change example dapp configuration
* fix: single loop to query wallet relay responses
  • Loading branch information
dawidsowardx authored Sep 19, 2024
1 parent b7e44eb commit b0afda3
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 85 deletions.
10 changes: 5 additions & 5 deletions examples/simple-dapp/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,12 @@ const gatewayApi = GatewayApiClient.initialize(
dAppToolkit.gatewayApi.clientConfig,
)

dAppToolkit.walletApi.provideChallengeGenerator(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return generateRolaChallenge()
})
dAppToolkit.walletApi.provideChallengeGenerator(async () => generateRolaChallenge())

dAppToolkit.walletApi.setRequestData(DataRequestBuilder.persona().withProof())
dAppToolkit.walletApi.setRequestData(
DataRequestBuilder.persona().withProof(),
DataRequestBuilder.accounts().atLeast(1),
)

gatewayConfig.innerHTML = `
[Gateway]
Expand Down
1 change: 0 additions & 1 deletion examples/simple-dapp/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ fs.writeFileSync(
path.resolve(__dirname, 'public', '.well-known', 'radix.json'),
JSON.stringify(
{
callbackPath: process.env.VITE_RETURN_URL,
dApps: [
{
dAppDefinitionAddress: process.env.DAPP_DEFINITION_ADDRESS,
Expand Down
2 changes: 1 addition & 1 deletion packages/dapp-toolkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"types"
],
"scripts": {
"dev": "npm run build -- --watch",
"dev": "tsup --watch",
"build": "tsup && npm run build:single",
"build:single": "vite build --config vite-single-file.config.ts && cp dist/radix-dapp-toolkit.bundle.umd.cjs ../../examples/cdn",
"test": "vitest",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* @vitest-environment jsdom
*/

import { beforeEach, describe, expect, it } from 'vitest'
import { LocalStorageModule, StorageModule } from './local-storage.module'
import { ResultAsync } from 'neverthrow'

describe('LocalStorageModule', () => {
let storageModule: StorageModule

beforeEach(() => {
storageModule = LocalStorageModule(`rdt:${crypto.randomUUID()}:1`)
})

it('should store and read data', async () => {
await storageModule.setState({ key: 'value' })
const data = await storageModule.getState()
expect(data.isOk() && data.value).toEqual({ key: 'value' })
})

describe('getItemById', () => {
it('should get specific item', async () => {
await storageModule.setState({ specific: 'value', key: 'value' })
const data = await storageModule.getItemById('specific')
expect(data.isOk() && data.value).toEqual('value')
})
})

describe('setItems', () => {
it('should set multiple items separately', async () => {
await storageModule.setItems({
specific: 'value',
})

await storageModule.setItems({ key: 'value' })
const data = await storageModule.getItems()
expect(data.isOk() && data.value).toEqual({
specific: 'value',
key: 'value',
})
})

// TODO: This currently fails. Uncomment this test when working on RDT-225 and ensure it's passing
it.skip('should set multiple items at once', async () => {
await ResultAsync.combine([
storageModule.setItems({
specific: 'value',
}),
storageModule.setItems({ key: 'value' }),
])

const data = await storageModule.getItems()
expect(data.isOk() && data.value).toEqual({
specific: 'value',
key: 'value',
})
})
})

describe('removeItemById', () => {
it('should remove specific item', async () => {
await storageModule.setState({ specific: 'value', key: 'value' })
await storageModule.removeItemById('specific')
const data = await storageModule.getState()
expect(data.isOk() && data.value).toEqual({ key: 'value' })
})
})
})
27 changes: 21 additions & 6 deletions packages/dapp-toolkit/src/modules/storage/local-storage.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type PartitionKey =
| 'requests'
| 'state'
| 'connectButton'
| 'walletResponses'
| 'connectorExtension'
type dAppDefinitionAddress = string

Expand Down Expand Up @@ -53,6 +54,11 @@ export const LocalStorageModule = <T extends object = any>(
data ? parseJSON(data) : ok({}),
)

const getState = (): ResultAsync<T | undefined, Error> =>
ResultAsync.fromPromise(getDataAsync(), typedError).andThen((data) =>
data ? parseJSON<T>(data) : ok(undefined),
)

const getItemById = (id: string): ResultAsync<T | undefined, Error> =>
ResultAsync.fromPromise(getDataAsync(), typedError)
.andThen((data) => (data ? parseJSON(data) : ok(undefined)))
Expand All @@ -61,7 +67,21 @@ export const LocalStorageModule = <T extends object = any>(
const removeItemById = (id: string): ResultAsync<void, Error> =>
getItems().andThen((items) => {
const { [id]: _, ...newItems } = items
return setItems(newItems)
return stringify(newItems).asyncAndThen((serialized) => {
const result = ResultAsync.fromPromise(
setDataAsync(serialized),
typedError,
).map(() => {
window.dispatchEvent(
new StorageEvent('storage', {
key: storageKey,
oldValue: JSON.stringify(items),
newValue: serialized,
}),
)
})
return result
})
})

const patchItem = (id: string, patch: Partial<T>): ResultAsync<void, Error> =>
Expand Down Expand Up @@ -93,11 +113,6 @@ export const LocalStorageModule = <T extends object = any>(
const getItemList = (): ResultAsync<T[], Error> =>
getItems().map(Object.values)

const getState = (): ResultAsync<T | undefined, Error> =>
ResultAsync.fromPromise(getDataAsync(), typedError).andThen((data) =>
data ? parseJSON<T>(data) : ok(undefined),
)

const setState = (newValue: T): ResultAsync<void, Error> =>
getState().andThen((oldValue) =>
stringify({ ...(oldValue ?? {}), ...newValue }).asyncAndThen(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ResultAsync, err, errAsync, ok } from 'neverthrow'
import { ResultAsync, err, errAsync, ok, okAsync } from 'neverthrow'
import { Subscription } from 'rxjs'
import { EncryptionModule, transformBufferToSealbox } from '../../encryption'
import { Session, SessionModule } from '../../session/session.module'
Expand Down Expand Up @@ -40,6 +40,9 @@ export const RadixConnectRelayModule = (input: {
const { baseUrl, providers, walletUrl } = input
const { requestItemModule, storageModule } = providers

const walletResponses: StorageModule<WalletInteractionResponse> =
storageModule.getPartition('walletResponses')

const encryptionModule = providers?.encryptionModule ?? EncryptionModule()

const deepLinkModule =
Expand Down Expand Up @@ -75,6 +78,62 @@ export const RadixConnectRelayModule = (input: {

const subscriptions = new Subscription()

const wait = (timer = 1500) =>
new Promise((resolve) => setTimeout(resolve, timer))

const decryptWalletResponse = (
walletResponse: WalletResponse,
): ResultAsync<WalletInteractionResponse, { reason: string }> => {
if ('error' in walletResponse) {
return errAsync({ reason: walletResponse.error })
}

return identityModule.get('dApp').andThen((dAppIdentity) =>
dAppIdentity.x25519
.calculateSharedSecret(
walletResponse.publicKey,
input.dAppDefinitionAddress,
)
.mapErr(() => ({ reason: 'FailedToDeriveSharedSecret' }))
.asyncAndThen((sharedSecret) =>
decryptWalletResponseData(sharedSecret, walletResponse.data),
),
)
}

const checkRelayLoop = async () => {
await requestItemModule.getPending().andThen((pendingItems) => {
if (pendingItems.length === 0) {
return okAsync(undefined)
}

return sessionModule
.getCurrentSession()
.andThen((session) =>
radixConnectRelayApiService.getResponses(session.sessionId),
)
.andThen((responses) =>
ResultAsync.combine(
responses.map((response) => decryptWalletResponse(response)),
).andThen((decryptedResponses) => {
return walletResponses.setItems(
decryptedResponses.reduce(
(acc, response) => {
acc[response.interactionId] = response
return acc
},
{} as Record<string, WalletInteractionResponse>,
),
)
}),
)
})
await wait()
checkRelayLoop()
}

checkRelayLoop()

const sendWalletInteractionRequest = ({
session,
walletInteraction,
Expand Down Expand Up @@ -156,13 +215,7 @@ export const RadixConnectRelayModule = (input: {
publicKey: dAppIdentity.x25519.getPublicKey(),
}),
)
.andThen(() =>
waitForWalletResponse({
session,
interactionId: walletInteraction.interactionId,
dAppIdentity,
}),
),
.andThen(() => waitForWalletResponse(walletInteraction.interactionId)),
)

const decryptWalletResponseData = (
Expand All @@ -188,50 +241,19 @@ export const RadixConnectRelayModule = (input: {
jsError: error,
}))

const waitForWalletResponse = ({
session,
interactionId,
dAppIdentity,
}: {
session: Session
interactionId: string
dAppIdentity: Curve25519
}): ResultAsync<WalletInteractionResponse, SdkError> =>
const waitForWalletResponse = (
interactionId: string,
): ResultAsync<WalletInteractionResponse, SdkError> =>
ResultAsync.fromPromise(
new Promise(async (resolve, reject) => {
let response: WalletInteractionResponse | undefined
let error: SdkError | undefined
let retry = 0

const wait = (timer = 1500) =>
new Promise((resolve) => setTimeout(resolve, timer))

logger?.debug({
method: 'waitForWalletResponse',
sessionId: session.sessionId,
interactionId,
})

const getEncryptedWalletResponses = () =>
radixConnectRelayApiService.getResponses(session.sessionId)

const decryptWalletResponse = (
walletResponse: WalletResponse,
): ResultAsync<WalletInteractionResponse, { reason: string }> => {
if ('error' in walletResponse) {
return errAsync({ reason: walletResponse.error })
}
return dAppIdentity.x25519
.calculateSharedSecret(
walletResponse.publicKey,
input.dAppDefinitionAddress,
)
.mapErr(() => ({ reason: 'FailedToDeriveSharedSecret' }))
.asyncAndThen((sharedSecret) =>
decryptWalletResponseData(sharedSecret, walletResponse.data),
)
}

while (!response) {
const requestItemResult =
await requestItemModule.getById(interactionId)
Expand All @@ -251,41 +273,20 @@ export const RadixConnectRelayModule = (input: {
}
}

const encryptedWalletResponsesResult =
await getEncryptedWalletResponses()

if (encryptedWalletResponsesResult.isOk()) {
const encryptedWalletResponses =
encryptedWalletResponsesResult.value

for (const encryptedWalletResponse of encryptedWalletResponses) {
const walletResponseResult = await decryptWalletResponse(
encryptedWalletResponse,
)

if (walletResponseResult.isErr())
logger?.error({
method: 'waitForWalletResponse.decryptWalletResponse.error',
error: walletResponseResult.error,
sessionId: session.sessionId,
interactionId,
})

if (walletResponseResult.isOk()) {
const walletResponse = walletResponseResult.value
const walletResponse =
await walletResponses.getItemById(interactionId)

if (walletResponse.interactionId === interactionId) {
response = walletResponse
await requestItemModule.patch(walletResponse.interactionId, {
walletResponse,
})
}
}
if (walletResponse.isOk()) {
if (walletResponse.value) {
response = walletResponse.value
await walletResponses.removeItemById(interactionId)
await requestItemModule.patch(interactionId, {
walletResponse: walletResponse.value,
})
}
}

if (!response) {
retry += 1
await wait()
}
}
Expand Down

0 comments on commit b0afda3

Please sign in to comment.