From 0e29693c665389c96cedaef697459d19d87f0743 Mon Sep 17 00:00:00 2001 From: Alex Stelea Date: Fri, 7 Jun 2024 13:57:43 +0100 Subject: [PATCH 01/14] refactor: encode wallet interaction in deep link url --- package-lock.json | 9 ++++ packages/dapp-toolkit/package.json | 1 + .../radix-connect-relay/helpers/base64url.ts | 5 +++ .../radix-connect-relay.module.ts | 42 ++++++++----------- packages/dapp-toolkit/src/polyfills.ts | 5 +++ .../dapp-toolkit/src/radix-dapp-toolkit.ts | 4 ++ 6 files changed, 42 insertions(+), 24 deletions(-) create mode 100644 packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/helpers/base64url.ts create mode 100644 packages/dapp-toolkit/src/polyfills.ts diff --git a/package-lock.json b/package-lock.json index fce98d39..ed207858 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13756,6 +13756,14 @@ } ] }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/before-after-hook": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", @@ -33505,6 +33513,7 @@ "license": "SEE LICENSE IN RADIX-SOFTWARE-EULA", "dependencies": { "@noble/curves": "^1.4.0", + "base64url": "^3.0.1", "bowser": "^2.11.0", "buffer": "^6.0.3", "immer": "^10.0.4", diff --git a/packages/dapp-toolkit/package.json b/packages/dapp-toolkit/package.json index e13f3922..19a8f8cc 100644 --- a/packages/dapp-toolkit/package.json +++ b/packages/dapp-toolkit/package.json @@ -60,6 +60,7 @@ }, "dependencies": { "@noble/curves": "^1.4.0", + "base64url": "^3.0.1", "bowser": "^2.11.0", "buffer": "^6.0.3", "immer": "^10.0.4", diff --git a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/helpers/base64url.ts b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/helpers/base64url.ts new file mode 100644 index 00000000..8644dbdb --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/helpers/base64url.ts @@ -0,0 +1,5 @@ +import base64url from 'base64url' + +export const base64urlEncode = >( + value: T, +): string => base64url.encode(Buffer.from(JSON.stringify(value))) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts index e2c7c722..5bedde45 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts @@ -23,6 +23,7 @@ import { RadixConnectRelayApiService } from './radix-connect-relay-api.service' import { RequestItem } from 'radix-connect-common' import type { TransportProvider } from '../../../../_types' import { RcfmPageModule, RcfmPageState } from './rcfm-page.module' +import { base64urlEncode } from './helpers/base64url' export type RadixConnectRelayModule = ReturnType export const RadixConnectRelayModule = (input: { @@ -119,16 +120,11 @@ export const RadixConnectRelayModule = (input: { .andThen((pendingItem) => requestItemModule .patch(walletInteraction.interactionId, { sentToWallet: true }) - .andThen(() => - radixConnectRelayApiService.sendRequest( - session, - pendingItem.walletInteraction, - ), - ) .andThen(() => deepLinkModule.deepLinkToWallet({ sessionId: session.sessionId, interactionId: pendingItem.interactionId, + walletInteraction: base64urlEncode(walletInteraction), }), ), ) @@ -204,27 +200,25 @@ export const RadixConnectRelayModule = (input: { pendingItem?: RequestItem }) => pendingItem - ? radixConnectRelayApiService - .sendRequest(activeSession, pendingItem.walletInteraction) - .andThen(() => - requestItemModule - .patch(pendingItem.interactionId, { - sentToWallet: true, - }) - .mapErr(() => - SdkError( - 'FailedToUpdateRequestItem', - pendingItem.interactionId, - ), - ) - .map(() => pendingItem.interactionId), + ? requestItemModule + .patch(pendingItem.interactionId, { + sentToWallet: true, + }) + .mapErr(() => + SdkError( + 'FailedToUpdateRequestItem', + pendingItem.interactionId, + ), ) - .map((interactionId) => { + .andThen(() => deepLinkModule.deepLinkToWallet({ sessionId, - interactionId, - }) - }) + interactionId: pendingItem.walletInteraction.interactionId, + walletInteraction: base64urlEncode( + pendingItem.walletInteraction, + ), + }), + ) : ok(pendingItem), ) diff --git a/packages/dapp-toolkit/src/polyfills.ts b/packages/dapp-toolkit/src/polyfills.ts new file mode 100644 index 00000000..2e956685 --- /dev/null +++ b/packages/dapp-toolkit/src/polyfills.ts @@ -0,0 +1,5 @@ +import { Buffer } from 'buffer' + +export default () => { + if (!globalThis.Buffer) globalThis.Buffer = Buffer +} diff --git a/packages/dapp-toolkit/src/radix-dapp-toolkit.ts b/packages/dapp-toolkit/src/radix-dapp-toolkit.ts index 10e4cd1c..ced31d94 100644 --- a/packages/dapp-toolkit/src/radix-dapp-toolkit.ts +++ b/packages/dapp-toolkit/src/radix-dapp-toolkit.ts @@ -1,3 +1,7 @@ +import polyfills from './polyfills' + +polyfills() + import type { ButtonApi, GatewayApiClientConfig, From bcda653b3e72fa7227c94e9751dfd7fa06717785 Mon Sep 17 00:00:00 2001 From: Alex Stelea Date: Thu, 13 Jun 2024 10:31:36 +0100 Subject: [PATCH 02/14] feat: add create signature method in identity --- package-lock.json | 6 +++ packages/dapp-toolkit/package.json | 3 +- .../modules/wallet-request/crypto/blake2b.ts | 26 +++++++++++ .../crypto/create-signature-message.ts | 35 +++++++++++++++ .../wallet-request/crypto/curve25519.ts | 13 +++++- .../identity/identity.module.ts | 43 +++++++++++++++++-- .../radix-connect-relay.module.ts | 19 +++++--- 7 files changed, 132 insertions(+), 13 deletions(-) create mode 100644 packages/dapp-toolkit/src/modules/wallet-request/crypto/blake2b.ts create mode 100644 packages/dapp-toolkit/src/modules/wallet-request/crypto/create-signature-message.ts diff --git a/package-lock.json b/package-lock.json index ed207858..172b7b03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13814,6 +13814,11 @@ "readable-stream": "^3.4.0" } }, + "node_modules/blakejs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", + "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==" + }, "node_modules/bn.js": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", @@ -33514,6 +33519,7 @@ "dependencies": { "@noble/curves": "^1.4.0", "base64url": "^3.0.1", + "blakejs": "^1.2.1", "bowser": "^2.11.0", "buffer": "^6.0.3", "immer": "^10.0.4", diff --git a/packages/dapp-toolkit/package.json b/packages/dapp-toolkit/package.json index 19a8f8cc..311b8e28 100644 --- a/packages/dapp-toolkit/package.json +++ b/packages/dapp-toolkit/package.json @@ -61,6 +61,7 @@ "dependencies": { "@noble/curves": "^1.4.0", "base64url": "^3.0.1", + "blakejs": "^1.2.1", "bowser": "^2.11.0", "buffer": "^6.0.3", "immer": "^10.0.4", @@ -92,4 +93,4 @@ "publishConfig": { "registry": "https://registry.npmjs.org" } -} \ No newline at end of file +} diff --git a/packages/dapp-toolkit/src/modules/wallet-request/crypto/blake2b.ts b/packages/dapp-toolkit/src/modules/wallet-request/crypto/blake2b.ts new file mode 100644 index 00000000..d434820f --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/crypto/blake2b.ts @@ -0,0 +1,26 @@ +import type { Result } from 'neverthrow' +import { err, ok } from 'neverthrow' +import blake from 'blakejs' +import { Buffer } from 'buffer' + +const bufferToArrayBuffer = (buffer: Buffer): ArrayBuffer => { + const arrayBuffer = new ArrayBuffer(buffer.length) + const view = new Uint8Array(arrayBuffer) + for (let i = 0; i < buffer.length; ++i) { + view[i] = buffer[i] + } + return arrayBuffer +} + +const bufferToUnit8Array = (buffer: Buffer): Uint8Array => + new Uint8Array(bufferToArrayBuffer(buffer)) + +export const blake2b = (input: Buffer): Result => { + try { + return ok(blake.blake2bHex(bufferToUnit8Array(input), undefined, 32)).map( + (hex) => Buffer.from(hex, 'hex'), + ) + } catch (error) { + return err(error as Error) + } +} diff --git a/packages/dapp-toolkit/src/modules/wallet-request/crypto/create-signature-message.ts b/packages/dapp-toolkit/src/modules/wallet-request/crypto/create-signature-message.ts new file mode 100644 index 00000000..71afb6ee --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/crypto/create-signature-message.ts @@ -0,0 +1,35 @@ +import { Buffer } from 'buffer' +import type { Result } from 'neverthrow' +import { blake2b } from './blake2b' + +export const createSignatureMessage = ({ + interactionId, + dAppDefinitionAddress, + origin, +}: { + interactionId: string + dAppDefinitionAddress: string + origin: string +}): Result => { + const prefix = Buffer.from('C', 'ascii') + const lengthOfDappDefAddress = dAppDefinitionAddress.length + const lengthOfDappDefAddressBuffer = Buffer.from( + lengthOfDappDefAddress.toString(16), + 'hex', + ) + const dappDefAddressBuffer = Buffer.from(dAppDefinitionAddress, 'utf-8') + const originBuffer = Buffer.from(origin, 'utf-8') + const interactionIdBuffer = Buffer.from(interactionId, 'hex') + + const messageBuffer = Buffer.concat([ + prefix, + interactionIdBuffer, + lengthOfDappDefAddressBuffer, + dappDefAddressBuffer, + originBuffer, + ]) + + return blake2b(messageBuffer) + .map((hash) => Buffer.from(hash).toString('hex')) + .mapErr((jsError) => ({ reason: 'couldNotHashMessage', jsError })) +} diff --git a/packages/dapp-toolkit/src/modules/wallet-request/crypto/curve25519.ts b/packages/dapp-toolkit/src/modules/wallet-request/crypto/curve25519.ts index 5fdc20bd..8ea3e638 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/crypto/curve25519.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/crypto/curve25519.ts @@ -1,4 +1,4 @@ -import { x25519 } from '@noble/curves/ed25519' +import { x25519, ed25519 } from '@noble/curves/ed25519' import { Buffer } from 'buffer' import { Result, err, ok } from 'neverthrow' @@ -8,6 +8,7 @@ export type KeyPairProvider = (privateKeyHex?: string) => { getPublicKey: () => string getPrivateKey: () => string calculateSharedSecret: (publicKeyHex: string) => Result + sign: (messageHex: string) => Result } export type Curve25519 = ReturnType @@ -29,5 +30,13 @@ export const Curve25519: KeyPairProvider = ( } } - return { getPublicKey, getPrivateKey, calculateSharedSecret } + const sign = (messageHex: string): Result => { + try { + return ok(toHex(ed25519.sign(privateKeyHex, messageHex))) + } catch (error) { + return err(error as Error) + } + } + + return { getPublicKey, getPrivateKey, calculateSharedSecret, sign } } diff --git a/packages/dapp-toolkit/src/modules/wallet-request/identity/identity.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/identity/identity.module.ts index 29384ebc..a713ee5b 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/identity/identity.module.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/identity/identity.module.ts @@ -1,6 +1,7 @@ -import { err, ok, okAsync } from 'neverthrow' +import { ResultAsync, err, ok, okAsync } from 'neverthrow' import { StorageModule } from '../../storage/local-storage.module' import type { KeyPairProvider } from '../crypto' +import { createSignatureMessage } from '../crypto/create-signature-message' export const IdentityKind = { dApp: 'dApp', @@ -42,9 +43,12 @@ export const IdentityModule = (input: { ) const getOrCreateIdentity = (kind: IdentityKind) => - getIdentity(kind).andThen((keyPair) => - keyPair ? okAsync(keyPair) : createIdentity(kind), - ) + getIdentity(kind) + .andThen((keyPair) => (keyPair ? okAsync(keyPair) : createIdentity(kind))) + .mapErr((error) => ({ + reason: 'couldNotGetOrCreateIdentity', + jsError: error, + })) const deriveSharedSecret = (kind: IdentityKind, publicKey: string) => getIdentity(kind) @@ -57,8 +61,39 @@ export const IdentityModule = (input: { : err({ reason: 'DappIdentityNotFound' }), ) + const createSignature = ({ + kind, + interactionId, + dAppDefinitionAddress, + origin, + }: { + kind: IdentityKind + interactionId: string + dAppDefinitionAddress: string + origin: string + }): ResultAsync< + string, + { + reason: string + jsError: Error + } + > => + getOrCreateIdentity(kind).andThen((identity) => + createSignatureMessage({ + interactionId, + dAppDefinitionAddress, + origin, + }).andThen((message) => + identity.sign(message).mapErr((error) => ({ + reason: 'couldNotSignMessage', + jsError: error, + })), + ), + ) + return { get: (kind: IdentityKind) => getOrCreateIdentity(kind), deriveSharedSecret, + createSignature, } } diff --git a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts index 5bedde45..3f5d7ae8 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts @@ -136,12 +136,19 @@ export const RadixConnectRelayModule = (input: { walletInteraction: WalletInteraction, callbackFns: Partial, ): ResultAsync => - sessionModule - .getCurrentSession() - .mapErr(() => - SdkError('FailedToReadSession', walletInteraction.interactionId), - ) - .andThen((session) => + ResultAsync.combine([ + identityModule + .get('dApp') + .mapErr(() => + SdkError('FailedToGetDappIdentity', walletInteraction.interactionId), + ), + sessionModule + .getCurrentSession() + .mapErr(() => + SdkError('FailedToReadSession', walletInteraction.interactionId), + ), + ]) + .andThen(([dAppIdentity, session]) => (session.status === 'Pending' ? sendWalletLinkingRequest(session) : sendWalletInteractionRequest(session, walletInteraction) From 459d2a340836d3778a69fee77f080e7b13b346ff Mon Sep 17 00:00:00 2001 From: Alex Stelea Date: Thu, 13 Jun 2024 18:27:05 +0100 Subject: [PATCH 03/14] refactor: update rcfm flow --- .../wallet-request/crypto/curve25519.ts | 58 +-- .../identity/identity.module.ts | 19 +- .../wallet-request/session/session.module.ts | 103 ++---- .../radix-connect-relay/deep-link.module.ts | 81 +---- .../radix-connect-relay-api.service.ts | 159 +------- .../radix-connect-relay.module.ts | 339 +++++++----------- .../modules/wallet-request/wallet-request.ts | 4 +- 7 files changed, 235 insertions(+), 528 deletions(-) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/crypto/curve25519.ts b/packages/dapp-toolkit/src/modules/wallet-request/crypto/curve25519.ts index 8ea3e638..0d19da3e 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/crypto/curve25519.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/crypto/curve25519.ts @@ -5,10 +5,15 @@ import { Result, err, ok } from 'neverthrow' const toHex = (input: Uint8Array) => Buffer.from(input).toString('hex') export type KeyPairProvider = (privateKeyHex?: string) => { - getPublicKey: () => string getPrivateKey: () => string - calculateSharedSecret: (publicKeyHex: string) => Result - sign: (messageHex: string) => Result + x25519: { + getPublicKey: () => string + calculateSharedSecret: (publicKeyHex: string) => Result + } + ed25519: { + getPublicKey: () => string + sign: (messageHex: string) => Result + } } export type Curve25519 = ReturnType @@ -17,26 +22,31 @@ export const Curve25519: KeyPairProvider = ( privateKeyHex = toHex(x25519.utils.randomPrivateKey()), ) => { const getPrivateKey = () => privateKeyHex - - const getPublicKey = () => toHex(x25519.getPublicKey(privateKeyHex)) - - const calculateSharedSecret = ( - publicKeyHex: string, - ): Result => { - try { - return ok(toHex(x25519.getSharedSecret(privateKeyHex, publicKeyHex))) - } catch (error) { - return err(error as Error) - } + const x25519Api = { + getPublicKey: () => toHex(x25519.getPublicKey(privateKeyHex)), + calculateSharedSecret: (publicKeyHex: string): Result => { + try { + return ok(toHex(x25519.getSharedSecret(privateKeyHex, publicKeyHex))) + } catch (error) { + return err(error as Error) + } + }, + } as const + + const ed25519Api = { + getPublicKey: () => toHex(ed25519.getPublicKey(privateKeyHex)), + sign: (messageHex: string): Result => { + try { + return ok(toHex(ed25519.sign(privateKeyHex, messageHex))) + } catch (error) { + return err(error as Error) + } + }, + } as const + + return { + getPrivateKey, + x25519: x25519Api, + ed25519: ed25519Api, } - - const sign = (messageHex: string): Result => { - try { - return ok(toHex(ed25519.sign(privateKeyHex, messageHex))) - } catch (error) { - return err(error as Error) - } - } - - return { getPublicKey, getPrivateKey, calculateSharedSecret, sign } } diff --git a/packages/dapp-toolkit/src/modules/wallet-request/identity/identity.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/identity/identity.module.ts index a713ee5b..e52a3fdb 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/identity/identity.module.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/identity/identity.module.ts @@ -55,7 +55,7 @@ export const IdentityModule = (input: { .mapErr(() => ({ reason: 'couldNotDeriveSharedSecret' })) .andThen((identity) => identity - ? identity.calculateSharedSecret(publicKey).mapErr(() => ({ + ? identity.x25519.calculateSharedSecret(publicKey).mapErr(() => ({ reason: 'FailedToDeriveSharedSecret', })) : err({ reason: 'DappIdentityNotFound' }), @@ -72,7 +72,7 @@ export const IdentityModule = (input: { dAppDefinitionAddress: string origin: string }): ResultAsync< - string, + { signature: string; publicKey: string }, { reason: string jsError: Error @@ -84,10 +84,17 @@ export const IdentityModule = (input: { dAppDefinitionAddress, origin, }).andThen((message) => - identity.sign(message).mapErr((error) => ({ - reason: 'couldNotSignMessage', - jsError: error, - })), + identity.ed25519 + .sign(message) + .map((signature) => ({ + signature, + publicKey: identity.x25519.getPublicKey(), + identity: identity.ed25519.getPublicKey(), + })) + .mapErr((error) => ({ + reason: 'couldNotSignMessage', + jsError: error, + })), ), ) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/session/session.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/session/session.module.ts index bf7686c9..85f11755 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/session/session.module.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/session/session.module.ts @@ -1,117 +1,70 @@ import type { ResultAsync } from 'neverthrow' -import { errAsync, okAsync } from 'neverthrow' -import type { IdentityModule } from '../identity/identity.module' +import { okAsync } from 'neverthrow' import { StorageModule } from '../../storage/local-storage.module' import { v4 as uuidV4 } from 'uuid' -type Status = (typeof Status)[keyof typeof Status] -const Status = { Pending: 'Pending', Active: 'Active' } as const - -export type PendingSession = { - status: typeof Status.Pending - createdAt: number +export type Session = { sessionId: string - sentToWallet: boolean -} - -export type ActiveSession = { - status: typeof Status.Active - walletIdentity: string createdAt: number - sharedSecret: string - sessionId: string } -export type Session = PendingSession | ActiveSession - export type SessionModule = ReturnType export const SessionModule = (input: { providers: { storageModule: StorageModule - identityModule: IdentityModule } }) => { const storageModule = input.providers.storageModule - const identityModule = input.providers.identityModule - const findActiveSession = (): ResultAsync< - ActiveSession | undefined, - { reason: string } + const getSession = (): ResultAsync< + Session | undefined, + { reason: string; jsError: Error } > => storageModule .getItems() - .mapErr(() => ({ reason: 'couldNotReadFromStore' })) - .map((sessions) => { - const activeSession = Object.values(sessions).find( - (session): session is ActiveSession => - session.status === Status.Active, - ) - return activeSession - }) + .mapErr((error) => ({ + reason: 'couldNotReadSessionFromStore', + jsError: error, + })) + .map((sessions) => sessions[0]) const getSessionById = (sessionId: string) => - storageModule.getItemById(sessionId) + storageModule + .getItemById(sessionId) + .mapErr((error) => ({ reason: 'couldNotGetSessionById', jsError: error })) - const createSession = (): ResultAsync => { + const createSession = (): ResultAsync< + Session, + { reason: string; jsError: Error } + > => { const sessionId = uuidV4() - const newSession: PendingSession = { + const newSession: Session = { sessionId, - status: Status.Pending, createdAt: Date.now(), - sentToWallet: false, } return storageModule .setItems({ [sessionId]: newSession }) .map(() => newSession) + .mapErr((error) => ({ reason: 'couldNotCreateSession', jsError: error })) } const patchSession = (sessionId: string, value: Partial) => - storageModule.patchItem(sessionId, value) - - const convertToActiveSession = ( - sessionId: string, - walletIdentity: string, - ): ResultAsync => storageModule - .getItemById(sessionId) - .mapErr(() => ({ reason: 'readFromStorageError' })) - .andThen((session) => - session && session.status === Status.Pending - ? identityModule - .deriveSharedSecret('dApp', walletIdentity) - .andThen((sharedSecret) => - storageModule - .setItems({ - [sessionId]: { - ...session, - status: Status.Active, - walletIdentity, - sharedSecret, - }, - }) - .map(() => ({ - ...session, - status: Status.Active, - walletIdentity, - sharedSecret, - })) - .mapErr(() => ({ reason: 'writeToStorageError' })), - ) - : errAsync({ reason: 'sessionNotPending' }), - ) + .patchItem(sessionId, value) + .mapErr((error) => ({ reason: 'couldNotPatchSession', jsError: error })) - const getCurrentSession = (): ResultAsync => - findActiveSession().andThen((activeSession) => - activeSession - ? okAsync(activeSession) - : createSession().mapErr(() => ({ reason: 'couldNotCreateSession' })), + const getCurrentSession = (): ResultAsync< + Session, + { reason: string; jsError: Error } + > => + getSession().andThen((session) => + session ? okAsync(session) : createSession(), ) return { getCurrentSession, - convertToActiveSession, - findActiveSession, + getSession, store: storageModule, getSessionById, patchSession, diff --git a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/deep-link.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/deep-link.module.ts index e2f5b5be..8f3e9478 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/deep-link.module.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/deep-link.module.ts @@ -1,49 +1,19 @@ -import { Result, ResultAsync } from 'neverthrow' -import { errAsync, ok, okAsync } from 'neverthrow' +import { ResultAsync } from 'neverthrow' +import { errAsync, okAsync } from 'neverthrow' import { Logger } from '../../../../helpers' -import { ReplaySubject } from 'rxjs' import Bowser from 'bowser' import { SdkError } from '../../../../error' export type DeepLinkModule = ReturnType export const DeepLinkModule = (input: { logger?: Logger - callBackPath: string walletUrl: string }) => { - const { callBackPath, walletUrl } = input + const { walletUrl } = input const userAgent = Bowser.parse(window.navigator.userAgent) - const { platform, browser } = userAgent + const { platform } = userAgent const logger = input?.logger?.getSubLogger({ name: 'DeepLinkModule' }) - const getNavigator = (): Navigator | undefined => globalThis?.navigator - - // Only exists in Brave browser - const getBrave = (): { isBrave: () => Promise } | undefined => - (getNavigator() as any)?.brave - - const isBrave = () => { - const maybeBrave = getBrave() - return maybeBrave - ? ResultAsync.fromPromise(maybeBrave.isBrave(), (error) => error as Error) - : okAsync(false) - } - - isBrave().map((isBrave) => { - if (isBrave) { - browser.name = 'Brave' - } - - logger?.debug({ platform, browser }) - }) - - const walletResponseSubject = new ReplaySubject>(1) - - const isCallbackUrl = () => window.location.href.includes(callBackPath) - - const shouldHandleWalletCallback = () => - platform.type === 'mobile' && isCallbackUrl() - const deepLinkToWallet = ( values: Record, ): ResultAsync => { @@ -53,56 +23,21 @@ export const DeepLinkModule = (input: { outboundUrl.searchParams.append(key, value) }) - outboundUrl.searchParams.append('browser', browser.name ?? 'unknown') - logger?.debug({ method: 'deepLinkToWallet', - queryParams: outboundUrl.searchParams.toString(), - browser: browser.name ?? 'unknown', + data: { ...values }, }) - if (platform.type === 'mobile') { - window.location.href = outboundUrl.toString() + if (platform.type === 'mobile' && globalThis.location?.href) { + globalThis.location.href = outboundUrl.toString() return okAsync(undefined) } - return errAsync(SdkError('UnhandledOs', '')) - } - - const getWalletResponseFromUrl = (): Result< - Record, - { reason: string } - > => { - const url = new URL(window.location.href) - const values = Object.fromEntries([...url.searchParams.entries()]) - logger?.debug({ - method: 'getWalletResponseFromUrl', - values, - }) - return ok(values) - } - - const handleWalletCallback = () => { - if (shouldHandleWalletCallback()) - return getWalletResponseFromUrl() - .map((values) => { - walletResponseSubject.next(values) - - return errAsync({ reason: 'InvalidCallbackValues' }) - }) - .mapErr((error) => { - logger?.debug({ - method: 'handleWalletCallback.error', - reason: error.reason, - }) - return error - }) + return errAsync(SdkError('UnhandledEnvironment', '')) } return { deepLinkToWallet, - handleWalletCallback, - walletResponse$: walletResponseSubject.asObservable(), } } diff --git a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay-api.service.ts b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay-api.service.ts index 85ee9cf4..18d7c58d 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay-api.service.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay-api.service.ts @@ -1,34 +1,29 @@ -import { Ok, Result, ResultAsync, err, ok } from 'neverthrow' import { SdkError } from '../../../../error' import { Logger, fetchWrapper, parseJSON } from '../../../../helpers' -import { - WalletInteraction, - WalletInteractionResponse, -} from '../../../../schemas' -import { EncryptionModule, transformBufferToSealbox } from '../../encryption' -import { - Subject, - filter, - first, - firstValueFrom, - merge, - of, - switchMap, -} from 'rxjs' -import { Buffer } from 'buffer' -import { ActiveSession } from '../../session/session.module' export type RadixConnectRelayApiService = ReturnType< typeof RadixConnectRelayApiService > + +export type WalletSuccessResponse = { + sessionId: string + publicKey: string + data: string +} + +export type WalletErrorResponse = { + sessionId: string + error: string +} + +export type WalletResponse = WalletSuccessResponse | WalletErrorResponse + export const RadixConnectRelayApiService = (input: { baseUrl: string logger?: Logger - providers: { encryptionModule: EncryptionModule } }) => { const baseUrl = input.baseUrl const logger = input.logger?.getSubLogger({ name: 'RadixConnectRelayApi' }) - const encryptionModule = input.providers.encryptionModule const callApi = ( body: Record & { method: string }, @@ -43,6 +38,7 @@ export const RadixConnectRelayApiService = (input: { .map((response) => { logger?.debug({ method: `callApi.${body.method}.success`, + response, }) return response }) @@ -58,132 +54,13 @@ export const RadixConnectRelayApiService = (input: { }) } - const decryptResponse = ( - secretHex: string, - value: string, - ): ResultAsync => - transformBufferToSealbox(Buffer.from(value, 'hex')) - .asyncAndThen(({ ciphertextAndAuthTag, iv }) => - encryptionModule.decrypt( - ciphertextAndAuthTag, - Buffer.from(secretHex, 'hex'), - iv, - ), - ) - .andThen((decrypted) => - parseJSON(decrypted.toString('utf-8')), - ) - .mapErr(() => SdkError('FailedToDecrypt', '')) - - const encryptWalletInteraction = ( - walletInteraction: WalletInteraction, - secret: Buffer, - ): ResultAsync => - encryptionModule - .encrypt(Buffer.from(JSON.stringify(walletInteraction), 'utf-8'), secret) - .mapErr(() => - SdkError( - 'FailEncryptWalletInteraction', - walletInteraction.interactionId, - ), - ) - .map((sealedBoxProps) => sealedBoxProps.combined.toString('hex')) - - const sendRequest = ( - { sessionId, sharedSecret }: ActiveSession, - walletInteraction: WalletInteraction, - ): ResultAsync => - encryptWalletInteraction( - walletInteraction, - Buffer.from(sharedSecret, 'hex'), - ).andThen((encryptedWalletInteraction) => - callApi({ - method: 'sendRequest', - sessionId, - data: encryptedWalletInteraction, - }).map(() => undefined), - ) - - const getResponses = (session: ActiveSession) => - callApi({ + const getResponses = (sessionId: string) => + callApi({ method: 'getResponses', - sessionId: session.sessionId, - }).andThen((value) => - ResultAsync.combine( - value.data.map((encryptedWalletInteraction) => - decryptResponse(session.sharedSecret, encryptedWalletInteraction), - ), - ), - ) - - const sendHandshakeRequest = ( - sessionId: string, - publicKeyHex: string, - ): ResultAsync => - callApi({ - method: 'sendHandshakeRequest', sessionId, - data: publicKeyHex, - }).map(() => undefined) - - const getHandshakeResponse = ( - sessionId: string, - ): ResultAsync => { - const getPublicKeyFromData = (data: { - publicKey: string - }): Result => { - 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 => { - logger?.debug({ parsed }) - return parsed?.publicKey - ? ok(parsed.publicKey) - : err({ reason: 'NotFound' }) - }) - } - - const sendApiRequest = () => - callApi<{ publicKey: string }>({ - method: 'getHandshakeResponse', - sessionId, - }).andThen(({ data }) => getPublicKeyFromData(data)) - - return ResultAsync.fromPromise( - firstValueFrom( - of(null).pipe( - switchMap(() => { - const trigger = new Subject() - return merge(trigger, of(0)).pipe( - switchMap((retry) => - sendApiRequest().mapErr((err) => { - trigger.next(retry + 1) - return err - }), - ), - filter((result): result is Ok => { - return result.isOk() - }), - first(), - ) - }), - ), - ), - () => { - return SdkError('FailedToGetHandshakeResponseToRadixConnectRelay', '') - }, - ).andThen((result) => result) - } + }).map((value) => value.data) return { - sendRequest, getResponses, - sendHandshakeRequest, - getHandshakeResponse, } } diff --git a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts index 3f5d7ae8..b034b485 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts @@ -1,28 +1,25 @@ -import { ResultAsync, err, ok } from 'neverthrow' -import { Subject, Subscription, filter, switchMap } from 'rxjs' -import { EncryptionModule } from '../../encryption' -import { - ActiveSession, - PendingSession, - Session, - SessionModule, -} from '../../session/session.module' +import { ResultAsync, err, errAsync, ok } from 'neverthrow' +import { Subject, Subscription, share } from 'rxjs' +import { EncryptionModule, transformBufferToSealbox } from '../../encryption' +import { Session, SessionModule } from '../../session/session.module' import type { CallbackFns, WalletInteraction, WalletInteractionResponse, } from '../../../../schemas' -import { Logger, isMobile } from '../../../../helpers' +import { Logger, isMobile, parseJSON } from '../../../../helpers' import { SdkError } from '../../../../error' import { DeepLinkModule } from './deep-link.module' import { IdentityModule } from '../../identity/identity.module' import { RequestItemModule } from '../../request-items/request-item.module' import { StorageModule } from '../../../storage' -import { Curve25519 } from '../../crypto' -import { RadixConnectRelayApiService } from './radix-connect-relay-api.service' +import { Curve25519, KeyPairProvider } from '../../crypto' +import { + RadixConnectRelayApiService, + WalletResponse, +} from './radix-connect-relay-api.service' import { RequestItem } from 'radix-connect-common' import type { TransportProvider } from '../../../../_types' -import { RcfmPageModule, RcfmPageState } from './rcfm-page.module' import { base64urlEncode } from './helpers/base64url' export type RadixConnectRelayModule = ReturnType @@ -36,7 +33,6 @@ export const RadixConnectRelayModule = (input: { encryptionModule?: EncryptionModule identityModule?: IdentityModule sessionModule?: SessionModule - rcfmPageModule?: RcfmPageModule deepLinkModule?: DeepLinkModule } }): TransportProvider => { @@ -46,18 +42,13 @@ export const RadixConnectRelayModule = (input: { const encryptionModule = providers?.encryptionModule ?? EncryptionModule() - const sessionChangeSubject = new Subject() - const deepLinkModule = providers?.deepLinkModule ?? DeepLinkModule({ logger, walletUrl, - callBackPath: '/connect', }) - const rcfmPageModule = providers?.rcfmPageModule ?? RcfmPageModule({ logger }) - const identityModule = providers?.identityModule ?? IdentityModule({ @@ -72,39 +63,29 @@ export const RadixConnectRelayModule = (input: { SessionModule({ providers: { storageModule: storageModule.getPartition('sessions'), - identityModule, }, }) const radixConnectRelayApiService = RadixConnectRelayApiService({ baseUrl: `${baseUrl}/api/v1`, logger, - providers: { encryptionModule }, }) const subscriptions = new Subscription() - const sendWalletLinkingRequest = (session: PendingSession) => - identityModule - .get('dApp') - .mapErr(() => SdkError('FailedToReadIdentity', '')) - .andThen((dAppIdentity) => - sessionModule - .patchSession(session.sessionId, { sentToWallet: true }) - .mapErr(() => SdkError('FailedToUpdateSession', '')) - .andThen(() => - deepLinkModule.deepLinkToWallet({ - sessionId: session.sessionId, - origin, - publicKey: dAppIdentity.getPublicKey(), - }), - ), - ) - - const sendWalletInteractionRequest = ( - session: ActiveSession, - walletInteraction: WalletInteraction, - ) => + const sendWalletInteractionRequest = ({ + session, + walletInteraction, + signature, + publicKey, + identity, + }: { + session: Session + walletInteraction: WalletInteraction + signature: string + publicKey: string + identity: string + }) => requestItemModule .getById(walletInteraction.interactionId) .mapErr(() => @@ -117,14 +98,19 @@ export const RadixConnectRelayModule = (input: { SdkError('PendingItemNotFound', walletInteraction.interactionId), ), ) - .andThen((pendingItem) => + .andThen(() => requestItemModule .patch(walletInteraction.interactionId, { sentToWallet: true }) .andThen(() => deepLinkModule.deepLinkToWallet({ sessionId: session.sessionId, - interactionId: pendingItem.interactionId, - walletInteraction: base64urlEncode(walletInteraction), + request: base64urlEncode(walletInteraction), + signature, + publicKey, + identity: identity, + origin: walletInteraction.metadata.origin, + dAppDefinitionAddress: + walletInteraction.metadata.dAppDefinitionAddress, }), ), ) @@ -137,104 +123,77 @@ export const RadixConnectRelayModule = (input: { callbackFns: Partial, ): ResultAsync => ResultAsync.combine([ - identityModule - .get('dApp') - .mapErr(() => - SdkError('FailedToGetDappIdentity', walletInteraction.interactionId), - ), sessionModule .getCurrentSession() - .mapErr(() => - SdkError('FailedToReadSession', walletInteraction.interactionId), + .mapErr((error) => + SdkError(error.reason, walletInteraction.interactionId), ), - ]) - .andThen(([dAppIdentity, session]) => - (session.status === 'Pending' - ? sendWalletLinkingRequest(session) - : sendWalletInteractionRequest(session, walletInteraction) - ).map(() => session), - ) - .andThen((session) => - waitForWalletResponse({ - sessionId: session.sessionId, + identityModule + .get('dApp') + .mapErr((error) => + SdkError(error.reason, walletInteraction.interactionId), + ), + ]).andThen(([session, dAppIdentity]) => + identityModule + .createSignature({ + dAppDefinitionAddress: + walletInteraction.metadata.dAppDefinitionAddress, interactionId: walletInteraction.interactionId, - }), - ) + origin: walletInteraction.metadata.origin, + kind: 'dApp', + }) + .mapErr((error) => + SdkError(error.reason, walletInteraction.interactionId), + ) + .andThen(({ signature }) => + sendWalletInteractionRequest({ + session, + walletInteraction, + signature, + identity: dAppIdentity.ed25519.getPublicKey(), + publicKey: dAppIdentity.x25519.getPublicKey(), + }), + ) + .andThen(() => + waitForWalletResponse({ + session, + interactionId: walletInteraction.interactionId, + dAppIdentity, + }), + ), + ) - // check if session exists - // -- if not, send error message to wallet - // generate shared secret - // update session - // send encrypted wallet interaction to RCR - // deep link to wallet with sessionId, interactionId, browser - const handleWalletLinkingResponse = ( - sessionId: string, - walletPublicKey: string, - ) => - sessionModule - .getSessionById(sessionId) - .mapErr(() => SdkError('FailedToReadSession', '')) - .andThen((session) => - session ? ok(session) : err(SdkError('SessionNotFound', '')), - ) - .andThen((session) => - session.status === 'Active' - ? ok(session) - : sessionModule - .convertToActiveSession(sessionId, walletPublicKey) - .mapErr(() => SdkError('FailedToUpdateSession', '')), + const decryptWalletResponseData = ( + sharedSecretHex: string, + value: string, + ): ResultAsync< + WalletInteractionResponse, + { reason: string; jsError: Error } + > => + transformBufferToSealbox(Buffer.from(value, 'hex')) + .asyncAndThen(({ ciphertextAndAuthTag, iv }) => + encryptionModule.decrypt( + ciphertextAndAuthTag, + Buffer.from(sharedSecretHex, 'hex'), + iv, + ), ) - .andThen((activeSession) => { - sessionChangeSubject.next(activeSession) - return requestItemModule - .getPending() - .mapErr(() => SdkError('FailedToReadPendingItems', '')) - .map((items) => { - const [item] = items.filter( - (item) => !item.walletResponse && !item.sentToWallet, - ) - return item - }) - - .map((item) => ({ activeSession, pendingItem: item })) - }) - .andThen( - ({ - activeSession, - pendingItem, - }: { - activeSession: ActiveSession - pendingItem?: RequestItem - }) => - pendingItem - ? requestItemModule - .patch(pendingItem.interactionId, { - sentToWallet: true, - }) - .mapErr(() => - SdkError( - 'FailedToUpdateRequestItem', - pendingItem.interactionId, - ), - ) - .andThen(() => - deepLinkModule.deepLinkToWallet({ - sessionId, - interactionId: pendingItem.walletInteraction.interactionId, - walletInteraction: base64urlEncode( - pendingItem.walletInteraction, - ), - }), - ) - : ok(pendingItem), + .andThen((decrypted) => + parseJSON(decrypted.toString('utf-8')), ) + .mapErr((error) => ({ + reason: 'FailedToDecryptWalletResponseData', + jsError: error, + })) const waitForWalletResponse = ({ - sessionId, + session, interactionId, + dAppIdentity, }: { - sessionId: string + session: Session interactionId: string + dAppIdentity: Curve25519 }): ResultAsync => ResultAsync.fromPromise( new Promise(async (resolve, reject) => { @@ -246,56 +205,57 @@ export const RadixConnectRelayModule = (input: { logger?.debug({ method: 'waitForWalletResponse', - sessionId, + sessionId: session.sessionId, interactionId, }) - while (!response) { - const sessionResult = await sessionModule.getSessionById(sessionId) - - if (sessionResult.isErr()) - return reject(SdkError('FailedToReadSession', interactionId)) + const getEncryptedWalletResponses = () => + radixConnectRelayApiService.getResponses(session.sessionId) - if (!sessionResult.value) { - return reject(SdkError('SessionNotFound', interactionId)) + const decryptWalletResponse = ( + walletResponse: WalletResponse, + ): ResultAsync => { + if ('error' in walletResponse) { + return errAsync({ reason: walletResponse.error }) } + return dAppIdentity.x25519 + .calculateSharedSecret(walletResponse.publicKey) + .mapErr(() => ({ reason: 'FailedToDeriveSharedSecret' })) + .asyncAndThen((sharedSecret) => + decryptWalletResponseData(sharedSecret, walletResponse.data), + ) + } + + while (!response) { + const encryptedWalletResponsesResult = + await getEncryptedWalletResponses() - const session = sessionResult.value + if (encryptedWalletResponsesResult.isOk()) { + const encryptedWalletResponses = + encryptedWalletResponsesResult.value - if (session.status === 'Active') { - await radixConnectRelayApiService - .getResponses(session) - .andThen((walletResponses) => - ResultAsync.combine( - walletResponses.map((walletResponse) => - requestItemModule.patch(walletResponse.interactionId, { - walletResponse, - }), - ), - ).map(() => walletResponses), + for (const encryptedWalletResponse of encryptedWalletResponses) { + const walletResponseResult = await decryptWalletResponse( + encryptedWalletResponse, ) - .andThen(() => requestItemModule.getById(interactionId)) - .map((walletInteraction) => { - if (walletInteraction) { - logger?.debug({ - method: 'waitForWalletResponse.success', - retry, - sessionId, - interactionId, - walletResponse: walletInteraction.walletResponse, - }) - response = walletInteraction.walletResponse - } - }) - .mapErr((error) => { - logger?.debug({ - method: 'waitForWalletResponse.error', - retry, - sessionId, + + if (walletResponseResult.isErr()) + logger?.error({ + method: 'waitForWalletResponse.decryptWalletResponse.error', + error: walletResponseResult.error, + sessionId: session.sessionId, interactionId, - error, }) - }) + + if (walletResponseResult.isOk()) { + const walletResponse = walletResponseResult.value + + if (walletResponse.interactionId === interactionId) { + response = walletResponse + break + } + } + } } if (!response) { @@ -309,47 +269,10 @@ export const RadixConnectRelayModule = (input: { (err) => err as SdkError, ) - const handleWalletCallback = async (values: Record) => { - const { sessionId, publicKey } = values - - const isLinkingResponse = sessionId && publicKey - - if (isLinkingResponse) { - return handleWalletLinkingResponse(sessionId, publicKey).mapErr( - (error) => { - logger?.debug({ method: 'handleWalletLinkingResponse.error', error }) - - deepLinkModule.deepLinkToWallet({ error: error.error }) - }, - ) - } - - logger?.debug({ method: 'handleWalletCallback.unhandled', values }) - } - - subscriptions.add( - deepLinkModule.walletResponse$ - .pipe( - filter((values) => Object.values(values).length > 0), - switchMap((item) => handleWalletCallback(item)), - ) - .subscribe(), - ) - - subscriptions.add( - sessionChangeSubject - .asObservable() - .pipe(filter((session) => session.status === 'Active')) - .subscribe(() => rcfmPageModule.show(RcfmPageState.dAppVerified)), - ) - - deepLinkModule.handleWalletCallback() - return { id: 'radix-connect-relay' as const, isSupported: () => isMobile(), send: sendToWallet, - sessionChange$: sessionChangeSubject.asObservable(), disconnect: () => {}, destroy: () => { subscriptions.unsubscribe() diff --git a/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.ts b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.ts index 7da6ee7b..b4c4b6cc 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.ts @@ -102,7 +102,9 @@ export const WalletRequestModule = (input: { RadixConnectRelayModule({ logger, walletUrl: 'radixWallet://', - baseUrl: 'https://radix-connect-relay.radixdlt.com', + // baseUrl: 'https://radix-connect-relay.radixdlt.com', + baseUrl: + 'https://radix-connect-relay-dev.rdx-works-main.extratools.works', providers: { requestItemModule, storageModule, From 091520e18623a740af1d6bfb16ffb51399a40274 Mon Sep 17 00:00:00 2001 From: Alex Stelea Date: Thu, 13 Jun 2024 18:32:18 +0100 Subject: [PATCH 04/14] code: set response instead of breaking --- .../transport/radix-connect-relay/radix-connect-relay.module.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts index b034b485..224f2105 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts @@ -252,7 +252,6 @@ export const RadixConnectRelayModule = (input: { if (walletResponse.interactionId === interactionId) { response = walletResponse - break } } } From 53f051909b98318e8a1a0aeee317108028b1b3eb Mon Sep 17 00:00:00 2001 From: Alex Stelea Date: Thu, 13 Jun 2024 18:38:39 +0100 Subject: [PATCH 05/14] code: add response to request item --- .../radix-connect-relay/radix-connect-relay.module.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts index 224f2105..d28e7de4 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts @@ -252,6 +252,9 @@ export const RadixConnectRelayModule = (input: { if (walletResponse.interactionId === interactionId) { response = walletResponse + await requestItemModule.patch(walletResponse.interactionId, { + walletResponse, + }) } } } From 78e7db14f4865bc515d829ece6d2c09d8e1e6433 Mon Sep 17 00:00:00 2001 From: Alex Stelea Date: Fri, 14 Jun 2024 10:15:51 +0100 Subject: [PATCH 06/14] fix: change interaction id to utf8 encoding --- .../crypto/create-signature-message.ts | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/crypto/create-signature-message.ts b/packages/dapp-toolkit/src/modules/wallet-request/crypto/create-signature-message.ts index 71afb6ee..c4b140d9 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/crypto/create-signature-message.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/crypto/create-signature-message.ts @@ -1,17 +1,21 @@ import { Buffer } from 'buffer' import type { Result } from 'neverthrow' import { blake2b } from './blake2b' +import { Logger } from '../../../helpers' export const createSignatureMessage = ({ interactionId, dAppDefinitionAddress, origin, + logger, }: { + logger?: Logger interactionId: string dAppDefinitionAddress: string origin: string }): Result => { - const prefix = Buffer.from('C', 'ascii') + const prefix = 'C' + const prefixBuffer = Buffer.from('C', 'ascii') const lengthOfDappDefAddress = dAppDefinitionAddress.length const lengthOfDappDefAddressBuffer = Buffer.from( lengthOfDappDefAddress.toString(16), @@ -19,17 +23,40 @@ export const createSignatureMessage = ({ ) const dappDefAddressBuffer = Buffer.from(dAppDefinitionAddress, 'utf-8') const originBuffer = Buffer.from(origin, 'utf-8') - const interactionIdBuffer = Buffer.from(interactionId, 'hex') + const interactionIdBuffer = Buffer.from(interactionId, 'utf-8') const messageBuffer = Buffer.concat([ - prefix, + prefixBuffer, interactionIdBuffer, lengthOfDappDefAddressBuffer, dappDefAddressBuffer, originBuffer, ]) - return blake2b(messageBuffer) - .map((hash) => Buffer.from(hash).toString('hex')) + const blake2bHash = blake2b(messageBuffer) + .map((hash) => { + logger?.debug({ + method: 'createSignatureMessage', + messagePartsRaw: [ + prefix, + lengthOfDappDefAddress, + dAppDefinitionAddress, + origin, + interactionId, + ], + messageParts: [ + prefixBuffer.toString('hex'), + lengthOfDappDefAddressBuffer.toString('hex'), + dappDefAddressBuffer.toString('hex'), + originBuffer.toString('hex'), + interactionIdBuffer.toString('hex'), + ], + message: messageBuffer.toString('hex'), + blake2bHash: hash.toString('hex'), + }) + return Buffer.from(hash).toString('hex') + }) .mapErr((jsError) => ({ reason: 'couldNotHashMessage', jsError })) + + return blake2bHash } From 5edb872b52a9180ec3bcf5548e09df063e628b78 Mon Sep 17 00:00:00 2001 From: Alex Stelea Date: Fri, 14 Jun 2024 10:16:16 +0100 Subject: [PATCH 07/14] fix: stop polling if interactionId is not in pending state --- .../identity/identity.module.ts | 3 +++ .../radix-connect-relay.module.ts | 22 ++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/identity/identity.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/identity/identity.module.ts index e52a3fdb..7d5016cc 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/identity/identity.module.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/identity/identity.module.ts @@ -2,6 +2,7 @@ import { ResultAsync, err, ok, okAsync } from 'neverthrow' import { StorageModule } from '../../storage/local-storage.module' import type { KeyPairProvider } from '../crypto' import { createSignatureMessage } from '../crypto/create-signature-message' +import { Logger } from '../../../helpers' export const IdentityKind = { dApp: 'dApp', @@ -14,6 +15,7 @@ export type IdentityStore = { export type IdentityModule = ReturnType export const IdentityModule = (input: { + logger?: Logger providers: { storageModule: StorageModule KeyPairModule: KeyPairProvider @@ -83,6 +85,7 @@ export const IdentityModule = (input: { interactionId, dAppDefinitionAddress, origin, + logger: input.logger, }).andThen((message) => identity.ed25519 .sign(message) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts index d28e7de4..ed317f18 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/transport/radix-connect-relay/radix-connect-relay.module.ts @@ -52,6 +52,7 @@ export const RadixConnectRelayModule = (input: { const identityModule = providers?.identityModule ?? IdentityModule({ + logger, providers: { storageModule: storageModule.getPartition('identities'), KeyPairModule: Curve25519, @@ -198,6 +199,7 @@ export const RadixConnectRelayModule = (input: { ResultAsync.fromPromise( new Promise(async (resolve, reject) => { let response: WalletInteractionResponse | undefined + let error: SdkError | undefined let retry = 0 const wait = (timer = 1500) => @@ -227,6 +229,24 @@ export const RadixConnectRelayModule = (input: { } while (!response) { + const requestItemResult = + await requestItemModule.getById(interactionId) + + if (requestItemResult.isOk()) { + logger?.debug({ + method: 'waitForWalletResponse.requestItemResult', + requestItemResult: requestItemResult.value, + }) + if (requestItemResult.value?.status !== 'pending') { + error = SdkError( + 'RequestItemNotPending', + interactionId, + 'request not in pending state', + ) + break + } + } + const encryptedWalletResponsesResult = await getEncryptedWalletResponses() @@ -266,7 +286,7 @@ export const RadixConnectRelayModule = (input: { } } - return resolve(response) + return response ? resolve(response) : reject(error) }), (err) => err as SdkError, ) From ce4773b1c7b4f570768eedb7044fa551aa582291 Mon Sep 17 00:00:00 2001 From: Alex Stelea Date: Fri, 14 Jun 2024 10:21:47 +0100 Subject: [PATCH 08/14] fix: use one session object --- .../src/modules/wallet-request/session/session.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/session/session.module.ts b/packages/dapp-toolkit/src/modules/wallet-request/session/session.module.ts index 85f11755..d19118d7 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/session/session.module.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/session/session.module.ts @@ -21,7 +21,7 @@ export const SessionModule = (input: { { reason: string; jsError: Error } > => storageModule - .getItems() + .getItemList() .mapErr((error) => ({ reason: 'couldNotReadSessionFromStore', jsError: error, From aa11af466d8f44d89d52b025cf755e0bee6d5139 Mon Sep 17 00:00:00 2001 From: Alex Stelea Date: Fri, 14 Jun 2024 10:26:10 +0100 Subject: [PATCH 09/14] code: update order of items in logs --- .../modules/wallet-request/crypto/create-signature-message.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/crypto/create-signature-message.ts b/packages/dapp-toolkit/src/modules/wallet-request/crypto/create-signature-message.ts index c4b140d9..687f0dc5 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/crypto/create-signature-message.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/crypto/create-signature-message.ts @@ -39,17 +39,17 @@ export const createSignatureMessage = ({ method: 'createSignatureMessage', messagePartsRaw: [ prefix, + interactionId, lengthOfDappDefAddress, dAppDefinitionAddress, origin, - interactionId, ], messageParts: [ prefixBuffer.toString('hex'), + interactionIdBuffer.toString('hex'), lengthOfDappDefAddressBuffer.toString('hex'), dappDefAddressBuffer.toString('hex'), originBuffer.toString('hex'), - interactionIdBuffer.toString('hex'), ], message: messageBuffer.toString('hex'), blake2bHash: hash.toString('hex'), From 565cc7d51d497df6a79a0a07269e4f00ea341872 Mon Sep 17 00:00:00 2001 From: Alex Stelea Date: Fri, 14 Jun 2024 15:31:56 +0100 Subject: [PATCH 10/14] test: update unit tests --- .../modules/wallet-request/crypto/blake2b.spec.ts | 14 ++++++++++++++ .../identity/tests/ecdh-key-exchange.test.ts | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 packages/dapp-toolkit/src/modules/wallet-request/crypto/blake2b.spec.ts diff --git a/packages/dapp-toolkit/src/modules/wallet-request/crypto/blake2b.spec.ts b/packages/dapp-toolkit/src/modules/wallet-request/crypto/blake2b.spec.ts new file mode 100644 index 00000000..88436ebd --- /dev/null +++ b/packages/dapp-toolkit/src/modules/wallet-request/crypto/blake2b.spec.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest' +import { blake2b } from './blake2b' + +describe('blake2b', () => { + it('should hash a string', async () => { + const result = blake2b(Buffer.from('test')) + + if (result.isErr()) throw result.error + + expect(result.value.toString('hex')).toBe( + '928b20366943e2afd11ebc0eae2e53a93bf177a4fcf35bcc64d503704e65e202', + ) + }) +}) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/identity/tests/ecdh-key-exchange.test.ts b/packages/dapp-toolkit/src/modules/wallet-request/identity/tests/ecdh-key-exchange.test.ts index 998232b4..b463b596 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/identity/tests/ecdh-key-exchange.test.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/identity/tests/ecdh-key-exchange.test.ts @@ -7,7 +7,7 @@ describe('ECDH key exchange', () => { for (const { privateKey1, publicKey2, sharedSecret } of testVectors) { expect( Curve25519(privateKey1) - .calculateSharedSecret(publicKey2) + .x25519.calculateSharedSecret(publicKey2) ._unsafeUnwrap(), ).toBe(sharedSecret) } From f14f36d4ccba0f967117aeed948b85b7bed0ff69 Mon Sep 17 00:00:00 2001 From: Alex Stelea Date: Fri, 14 Jun 2024 15:32:37 +0100 Subject: [PATCH 11/14] code: export blake2b --- packages/dapp-toolkit/src/modules/wallet-request/crypto/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/crypto/index.ts b/packages/dapp-toolkit/src/modules/wallet-request/crypto/index.ts index 3f26b3df..699a9cdd 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/crypto/index.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/crypto/index.ts @@ -1 +1,2 @@ export * from './curve25519' +export * from './blake2b' From c9fd3d09fe372a853a8f2e4eb2a3eee31c54c874 Mon Sep 17 00:00:00 2001 From: Alex Stelea Date: Fri, 14 Jun 2024 15:59:46 +0100 Subject: [PATCH 12/14] test: add unit test --- .../src/modules/wallet-request/crypto/blake2b.spec.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/crypto/blake2b.spec.ts b/packages/dapp-toolkit/src/modules/wallet-request/crypto/blake2b.spec.ts index 88436ebd..da760751 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/crypto/blake2b.spec.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/crypto/blake2b.spec.ts @@ -11,4 +11,14 @@ describe('blake2b', () => { '928b20366943e2afd11ebc0eae2e53a93bf177a4fcf35bcc64d503704e65e202', ) }) + + it('should generate an array of blake2b hashes', async () => { + const vectors = new Array(100).fill(null).map(() => { + const buffer = Buffer.from(crypto.getRandomValues(new Uint8Array(64))) + const blake2bHash = blake2b(buffer)._unsafeUnwrap().toString('hex') + const message = buffer.toString('hex') + return { message, blake2bHash } + }) + console.log(vectors) + }) }) From 6dab0b36297b176638e45eb9cb393b354234fbac Mon Sep 17 00:00:00 2001 From: Alex Stelea Date: Fri, 14 Jun 2024 16:00:05 +0100 Subject: [PATCH 13/14] code: remove console log --- .../src/modules/wallet-request/crypto/blake2b.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/crypto/blake2b.spec.ts b/packages/dapp-toolkit/src/modules/wallet-request/crypto/blake2b.spec.ts index da760751..24399136 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/crypto/blake2b.spec.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/crypto/blake2b.spec.ts @@ -19,6 +19,5 @@ describe('blake2b', () => { const message = buffer.toString('hex') return { message, blake2bHash } }) - console.log(vectors) }) }) From e2e202ac087e4ff710c18bf5396847d80aed3c76 Mon Sep 17 00:00:00 2001 From: Alex Stelea Date: Mon, 17 Jun 2024 10:57:02 +0100 Subject: [PATCH 14/14] refactor: use prod rcr --- .../dapp-toolkit/src/modules/wallet-request/wallet-request.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.ts b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.ts index b4c4b6cc..7da6ee7b 100644 --- a/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.ts +++ b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.ts @@ -102,9 +102,7 @@ export const WalletRequestModule = (input: { RadixConnectRelayModule({ logger, walletUrl: 'radixWallet://', - // baseUrl: 'https://radix-connect-relay.radixdlt.com', - baseUrl: - 'https://radix-connect-relay-dev.rdx-works-main.extratools.works', + baseUrl: 'https://radix-connect-relay.radixdlt.com', providers: { requestItemModule, storageModule,