Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: RCfM interactions getting stuck #253

Merged
merged 6 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
dawidsowardx marked this conversation as resolved.
Show resolved Hide resolved

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
Loading