Skip to content

Commit

Permalink
code: update rcr flow
Browse files Browse the repository at this point in the history
  • Loading branch information
xstelea committed Apr 26, 2024
1 parent 396cd4d commit e060434
Show file tree
Hide file tree
Showing 10 changed files with 423 additions and 294 deletions.
2 changes: 1 addition & 1 deletion examples/simple-dapp/public/.well-known/radix.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"callbackPath": "#connect",
"callbackPath": "/connect",
"dApps": [
{
"dAppDefinitionAddress": "account_tdx_2_12yf9gd53yfep7a669fv2t3wm7nz9zeezwd04n02a433ker8vza6rhe"
Expand Down
14 changes: 6 additions & 8 deletions examples/simple-dapp/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ const stateStore = storageClient.getPartition('state')

const content = document.getElementById('app')!

content.innerHTML = `
<button id="continue">Continue login request</button>
content.innerHTML = `
<button id="reset">Reset</button>
<div class="mt-25"><button id="one-time-request">Send one time request</button></div>
Expand Down Expand Up @@ -115,11 +113,11 @@ resetButton.onclick = () => {
window.location.replace(window.location.origin)
}

continueButton.onclick = () => {
requestItemClient.getPendingItems().map((items) => {
if (items[0]) rcr.resume(items[0].interactionId)
})
}
// continueButton.onclick = () => {
// requestItemClient.getPendingItems().map((items) => {
// if (items[0]) rcr.resume(items[0].interactionId)
// })
// }

oneTimeRequest.onclick = () => {
dAppToolkit.walletApi.sendOneTimeRequest(
Expand Down
6 changes: 4 additions & 2 deletions examples/simple-dapp/src/sst-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {}
interface ImportMetaEnv {

}
interface ImportMeta {
readonly env: ImportMetaEnv
}
}
18 changes: 17 additions & 1 deletion examples/simple-dapp/sst.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
import type { SSTConfig } from 'sst'
import { StaticSite } from 'sst/constructs'

import { exec } from 'child_process'

const getBranchName = () =>
new Promise<string>((resolve, reject) => {
exec('git branch --show-current', (err, stdout) => {
if (err) {
reject(err)
} else {
const branchName = stdout.trim().split('/').slice(-1)[0]
resolve(branchName)
}
})
})

const branchName = await getBranchName()

export default {
config() {
return {
name: 'simple-dapp',
name: `simple-dapp-${branchName}`,
region: process.env.AWS_REGION,
profile: process.env.AWS_PROFILE,
stage: process.env.AWS_STAGE,
Expand Down
1 change: 1 addition & 0 deletions packages/dapp-toolkit/src/radix-dapp-toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export const RadixDappToolkit = (
storageClient,
gatewayClient,
transports: options.providers?.transports,
requestItemClient: options.providers?.requestItemClient,
},
})

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { RequestItem, RequestStatusTypes } from 'radix-connect-common'
import { Subscription } from 'rxjs'
import { Subscription, filter, firstValueFrom, merge, mergeMap, of } from 'rxjs'
import { RequestItemSubjects } from './subjects'
import { Logger } from '../../helpers'
import { ErrorType } from '../../error'
import { ErrorType, SdkError } from '../../error'
import { WalletInteraction } from '../../schemas'
import { StorageProvider } from '../../storage'
import { ResultAsync, errAsync } from 'neverthrow'
import { Result, ResultAsync, errAsync } from 'neverthrow'
export type RequestItemClientInput = {
logger?: Logger
subjects?: RequestItemSubjects
Expand All @@ -15,13 +15,11 @@ export type RequestItemClient = ReturnType<typeof RequestItemClient>
export const RequestItemClient = (input: RequestItemClientInput) => {
const logger = input?.logger?.getSubLogger({ name: 'RequestItemClient' })
const subscriptions = new Subscription()
const subjects = input.subjects || RequestItemSubjects()
const storageClient = input.providers.storageClient

storageClient.getItemList().map((items) => {
logger?.debug({ method: 'initRequestItems', items })
subjects.items.next(items)
})
storageClient.getItemList().map((items) => {
logger?.debug({ method: 'initRequestItems', items })
})

const createItem = ({
type,
Expand Down Expand Up @@ -114,6 +112,31 @@ export const RequestItemClient = (input: RequestItemClientInput) => {
Object.values(items).filter((item) => !item.walletResponse),
)

const storeChange$ = merge(storageClient.storage$, of(null))

const waitForWalletResponse = (
interactionId: string,
): ResultAsync<RequestItem, SdkError> =>
ResultAsync.fromPromise(
firstValueFrom(
storeChange$.pipe(
mergeMap(() =>
storageClient
.getItemById(interactionId)
.mapErr(() => SdkError('FailedToGetRequestItem', interactionId)),
),
filter((result): result is Result<RequestItem, SdkError> => {
if (result.isErr()) return false
return (
result.value?.interactionId === interactionId &&
['success', 'fail'].includes(result.value.status)
)
}),
),
),
() => SdkError('FailedToListenForWalletResponse', interactionId),
).andThen((result) => result)

return {
add,
cancel,
Expand All @@ -122,6 +145,8 @@ export const RequestItemClient = (input: RequestItemClientInput) => {
getById: (id: string) => storageClient.getItemById(id),
getPendingItems,
store: storageClient,
waitForWalletResponse,
storeChange$,
destroy: () => {
subscriptions.unsubscribe()
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,4 @@ export type RequestItemSubjects = ReturnType<typeof RequestItemSubjects>
export const RequestItemSubjects = () => ({
initialized: new BehaviorSubject<boolean>(false),
onChange: new Subject<RequestItemChange>(),
items: new BehaviorSubject<RequestItem[]>([]),
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import { Ok, Result, ResultAsync, err, ok } from 'neverthrow'
import { SdkError } from '../../../error'
import { Logger, fetchWrapper, parseJSON } from '../../../helpers'
import { WalletInteraction, WalletInteractionResponse } from '../../../schemas'
import { EncryptionClient, transformBufferToSealbox } from '../../encryption'
import { ActiveSession } from '../../../../dist'
import {
Subject,
filter,
first,
firstValueFrom,
merge,
of,
switchMap,
} from 'rxjs'
import { Buffer } from 'buffer'

export type RadixConnectRelayApi = ReturnType<typeof RadixConnectRelayApi>
export const RadixConnectRelayApi = (input: {
baseUrl: string
logger?: Logger
providers: { encryptionClient: EncryptionClient }
}) => {
const baseUrl = input.baseUrl
const logger = input.logger?.getSubLogger({ name: 'RadixConnectRelayApi' })
const encryptionClient = input.providers.encryptionClient

const decryptResponse = (
secretHex: string,
value: string,
): ResultAsync<WalletInteractionResponse, SdkError> =>
transformBufferToSealbox(Buffer.from(value, 'hex'))
.asyncAndThen(({ ciphertextAndAuthTag, iv }) =>
encryptionClient.decrypt(
ciphertextAndAuthTag,
Buffer.from(secretHex, 'hex'),
iv,
),
)
.andThen((decrypted) =>
parseJSON<WalletInteractionResponse>(decrypted.toString('utf-8')),
)
.mapErr(() => SdkError('FailedToDecrypt', ''))

const encryptWalletInteraction = (
walletInteraction: WalletInteraction,
secret: Buffer,
): ResultAsync<string, SdkError> =>
encryptionClient
.encrypt(Buffer.from(JSON.stringify(walletInteraction), 'utf-8'), secret)
.mapErr(() =>
SdkError(
'FailEncryptWalletInteraction',
walletInteraction.interactionId,
),
)
.map((sealedBoxProps) => sealedBoxProps.combined.toString('hex'))

const sendRequest = (
sessionId: string,
secretHex: string,
walletInteraction: WalletInteraction,
): ResultAsync<void, SdkError> => {
logger?.debug({ method: 'sendRequest', sessionId, walletInteraction })
return encryptWalletInteraction(
walletInteraction,
Buffer.from(secretHex, 'hex'),
).andThen((encryptedWalletInteraction) =>
fetchWrapper(
fetch(baseUrl, {
method: 'POST',
body: JSON.stringify({
method: 'sendRequest',
sessionId,
data: encryptedWalletInteraction,
}),
}),
)
.map(() => undefined)
.mapErr(() => SdkError('FailedToSendRequestToRadixConnectRelay', '')),
)
}

const getResponses = (session: ActiveSession) =>
fetchWrapper<string[]>(
fetch(baseUrl, {
method: 'POST',
body: JSON.stringify({
method: 'getResponses',
sessionId: session.sessionId,
}),
}),
)
.mapErr(() => SdkError('FailedToGetRequestsFromRadixConnectRelay', ''))
.andThen((value) =>
ResultAsync.combine(
value.data.map((encryptedWalletInteraction) =>
decryptResponse(session.sharedSecret, encryptedWalletInteraction),
),
),
)

const sendHandshakeRequest = (
sessionId: string,
publicKeyHex: string,
): ResultAsync<void, SdkError> => {
logger?.debug({ method: 'sendHandshakeRequest', sessionId, publicKeyHex })
return fetchWrapper(
fetch(baseUrl, {
method: 'POST',
body: JSON.stringify({
method: 'sendHandshakeRequest',
sessionId,
data: publicKeyHex,
}),
}),
)
.map(() => {
logger?.debug({
method: 'sendHandshakeRequestToRadixConnectRelay.success',
})
})
.mapErr(() => {
logger?.debug({
method: 'sendHandshakeRequestToRadixConnectRelay.error',
})
return SdkError('FailedToSendHandshakeRequestToRadixConnectRelay', '')
})
}

const getHandshakeResponse = (
sessionId: string,
): ResultAsync<string, SdkError> => {
const getPublicKeyFromData = (data: {
publicKey: string
}): Result<string, { reason: string }> => {
const publicKeyRaw = data?.publicKey

if (!publicKeyRaw) err({ reason: 'NotFound' })

const publicKey = Buffer.from(publicKeyRaw, 'hex').toString('utf-8')

return parseJSON(publicKey)
.mapErr(() => ({ reason: 'FailedToParsePublicKey' }))
.andThen((parsed): Result<string, { reason: string }> => {
logger?.debug({ parsed })
return parsed?.publicKey
? ok(parsed.publicKey)
: err({ reason: 'NotFound' })
})
}

const sendApiRequest = (retry: number) => {
logger?.debug({ method: 'getHandshakeResponse', sessionId, retry })
return fetchWrapper<{ publicKey: string }>(
fetch(baseUrl, {
method: 'POST',
body: JSON.stringify({
method: 'getHandshakeResponse',
sessionId,
}),
}),
).andThen(({ data }) => getPublicKeyFromData(data))
}

return ResultAsync.fromPromise(
firstValueFrom(
of(null).pipe(
switchMap(() => {
const trigger = new Subject<number>()
return merge(trigger, of(0)).pipe(
switchMap((retry) =>
sendApiRequest(retry).mapErr((err) => {
trigger.next(retry + 1)
return err
}),
),
filter((result): result is Ok<string, never> => {
return result.isOk()
}),
first(),
)
}),
),
),
(error) => {
return SdkError('FailedToGetHandshakeResponseToRadixConnectRelay', '')
},
).andThen((result) => result)

// return fetchWrapper<{ publicKey: string }>(
// fetch(baseUrl, {
// method: 'POST',
// body: JSON.stringify({
// method: 'getHandshakeResponse',
// sessionId,
// }),
// }),
// )
// .andThen(({ data }) => getPublicKeyFromData(data))
// .mapErr((err) =>
// SdkError('FailedToGetHandshakeResponseToRadixConnectRelay', ''),
// )
}

return {
sendRequest,
getResponses,
sendHandshakeRequest,
getHandshakeResponse,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const DeepLinkClient = (input: {

const walletResponseSubject = new BehaviorSubject<Record<string, string>>({})

const isCallbackUrl = () => window.location.hash.includes(callBackPath)
const isCallbackUrl = () => window.location.href.includes(callBackPath)

const shouldHandleWalletCallback = () =>
platform.type === 'mobile' && isCallbackUrl()
Expand Down
Loading

0 comments on commit e060434

Please sign in to comment.