From 94e543a4912cd42b308bd5d3f928c6e1fa11b79b Mon Sep 17 00:00:00 2001 From: Dawid Sowa Date: Mon, 14 Oct 2024 21:53:21 +0200 Subject: [PATCH] feat: add subintents support --- examples/simple-dapp/src/main.ts | 22 +++++++ packages/common/src/index.ts | 1 + .../src/components/card/request-card.ts | 12 +++- packages/dapp-toolkit/src/_types.ts | 18 +++++- .../wallet-request/wallet-request-sdk.ts | 26 +------- .../modules/wallet-request/wallet-request.ts | 59 +++++++++++++------ .../dapp-toolkit/src/radix-dapp-toolkit.ts | 1 + packages/dapp-toolkit/src/schemas/index.ts | 38 ++++++++++++ 8 files changed, 135 insertions(+), 42 deletions(-) diff --git a/examples/simple-dapp/src/main.ts b/examples/simple-dapp/src/main.ts index c5081059..47ed5f18 100644 --- a/examples/simple-dapp/src/main.ts +++ b/examples/simple-dapp/src/main.ts @@ -34,6 +34,10 @@ content.innerHTML = `
+
+ + +

   

@@ -48,6 +52,10 @@ const sendTxButton = document.getElementById('sendTx')!
 const sessions = document.getElementById('sessions')!
 const removeCb = document.getElementById('removeCb')!
 const addCb = document.getElementById('addCb')!
+const subintentButton = document.getElementById('subintent')!
+const subintentManifest = document.getElementById(
+  'subintentManifest',
+)! as HTMLTextAreaElement
 const requests = document.getElementById('requests')!
 const logs = document.getElementById('logs')!
 const state = document.getElementById('state')!
@@ -75,6 +83,20 @@ removeCb.onclick = () => {
   document.querySelector('radix-connect-button')?.remove()
 }
 
+subintentButton.onclick = async () => {
+  console.log(subintentManifest.value)
+  const result = await dAppToolkit.walletApi.sendPreAuthorizationRequest({
+    transactionManifest: subintentManifest.value,
+    childSubintentHashes: [],
+    expiration: {
+      discriminator: 'expireAfterSignature',
+      value: '3600',
+    },
+  })
+
+  console.log(result);
+}
+
 addCb.onclick = () => {
   const connectButton = document.createElement('radix-connect-button')
   const header = document.querySelector('header')!
diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts
index dd4f450c..42c5bfa3 100644
--- a/packages/common/src/index.ts
+++ b/packages/common/src/index.ts
@@ -40,6 +40,7 @@ export const RequestItemType = {
   dataRequest: 'dataRequest',
   sendTransaction: 'sendTransaction',
   proofRequest: 'proofRequest',
+  preAuthorizationRequest: 'preAuthorizationRequest',
 } as const
 
 export type RequestItemType = typeof RequestItemType
diff --git a/packages/connect-button/src/components/card/request-card.ts b/packages/connect-button/src/components/card/request-card.ts
index 5f7fa60c..c4fe3716 100644
--- a/packages/connect-button/src/components/card/request-card.ts
+++ b/packages/connect-button/src/components/card/request-card.ts
@@ -102,7 +102,17 @@ export class RadixRequestCard extends LitElement {
         content: this.getRequestContentTemplate(
           'Open Your Radix Wallet App to complete the request',
         ),
-      }
+      },
+      preAuthorizationRequest: {
+        pending: 'Preauthorization Request Pending',
+        fail: 'Preauthorization Request Rejected',
+        cancelled: 'Preauthorization Request Rejected',
+        success: 'Preauthorization Request',
+        ignored: '',
+        content: this.getRequestContentTemplate(
+          'Open Your Radix Wallet App to complete the request',
+        ),
+      },
     }
 
     return html`[0]
+export type WalletDataRequest = Parameters<
+  WalletRequestSdk['sendInteraction']
+>[0]
 
 export type WalletRequest =
   | { type: 'sendTransaction'; payload: WalletInteraction }
@@ -96,6 +100,15 @@ export type SendTransactionInput = {
   onTransactionId?: (transactionId: string) => void
 }
 
+export type SendPreAuthorizationRequestInput = {
+  transactionManifest: string
+  version?: number
+  blobs?: string[]
+  message?: string
+  childSubintentHashes: string[]
+  expiration: ExpireAtTime | ExpireAfterSignature
+}
+
 export type ButtonApi = {
   setMode: (value: 'light' | 'dark') => void
   setTheme: (value: RadixButtonTheme) => void
@@ -128,6 +141,9 @@ export type WalletApi = {
   dataRequestControl: (fn: (walletResponse: WalletData) => Promise) => void
   updateSharedAccounts: () => WalletDataRequestResult
   sendTransaction: (input: SendTransactionInput) => SendTransactionResult
+  sendPreAuthorizationRequest: (
+    input: SendPreAuthorizationRequestInput,
+  ) => ResultAsync<{ signedPartialTransaction: string }, SdkError>
   setRequestData: (...dataRequestBuilderItem: DataRequestBuilderItem[]) => void
   sendRequest: () => WalletDataRequestResult
   sendOneTimeRequest: (
diff --git a/packages/dapp-toolkit/src/modules/wallet-request/wallet-request-sdk.ts b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request-sdk.ts
index ec9ec97b..92921bf8 100644
--- a/packages/dapp-toolkit/src/modules/wallet-request/wallet-request-sdk.ts
+++ b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request-sdk.ts
@@ -80,11 +80,11 @@ export const WalletRequestSdk = (input: WalletRequestSdkInput) => {
         })
   }
 
-  const request = (
+  const sendInteraction = (
     {
       interactionId = uuidV4(),
       items,
-    }: Pick & { interactionId?: string },
+    }: { interactionId?: string; items: WalletInteraction['items'] },
     callbackFns: Partial = {},
   ): ResultAsync =>
     withInterceptor({
@@ -99,28 +99,8 @@ export const WalletRequestSdk = (input: WalletRequestSdkInput) => {
       ),
     )
 
-  const sendTransaction = (
-    {
-      interactionId = uuidV4(),
-      items,
-    }: { interactionId?: string; items: WalletInteraction['items'] },
-    callbackFns: Partial = {},
-  ): ResultAsync =>
-    withInterceptor({
-      interactionId,
-      items,
-      metadata,
-    }).andThen((walletInteraction) =>
-      getTransport(interactionId).asyncAndThen((transport) =>
-        transport
-          .send(walletInteraction, callbackFns)
-          .andThen(validateWalletResponse),
-      ),
-    )
-
   return {
-    request,
-    sendTransaction,
+    sendInteraction,
     createWalletInteraction,
     getTransport,
   }
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 b38bcf12..a97f2f9f 100644
--- a/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.ts
+++ b/packages/dapp-toolkit/src/modules/wallet-request/wallet-request.ts
@@ -11,7 +11,12 @@ import {
 import { validateRolaChallenge, type Logger } from '../../helpers'
 import { TransactionStatus } from '../gateway'
 import { ResultAsync, err, ok, okAsync } from 'neverthrow'
-import type { MessageLifeCycleEvent, WalletInteraction } from '../../schemas'
+import type {
+  ExpireAfterSignature,
+  ExpireAtTime,
+  MessageLifeCycleEvent,
+  WalletInteraction,
+} from '../../schemas'
 import { SdkError } from '../../error'
 import {
   DataRequestBuilderItem,
@@ -25,6 +30,8 @@ import { StorageModule } from '../storage'
 import type { StateModule, WalletData } from '../state'
 import {
   AwaitedWalletDataRequestResult,
+  SendPreAuthorizationRequestInput,
+  SendTransactionInput,
   TransportProvider,
   WalletDataRequestResult,
 } from '../../_types'
@@ -37,14 +44,7 @@ import {
   failedResponseResolver,
   sendTransactionResponseResolver,
 } from './request-resolver'
-
-type SendTransactionInput = {
-  transactionManifest: string
-  version?: number
-  blobs?: string[]
-  message?: string
-  onTransactionId?: (transactionId: string) => void
-}
+import { RequestItemTypes } from 'radix-connect-common'
 
 export type WalletRequestModule = ReturnType
 export const WalletRequestModule = (input: {
@@ -199,7 +199,7 @@ export const WalletRequestModule = (input: {
           ),
         )
       },
-    } satisfies Parameters[1]
+    } satisfies Parameters[1]
   }
 
   let challengeGeneratorFn: () => Promise = () => Promise.resolve('')
@@ -249,13 +249,10 @@ export const WalletRequestModule = (input: {
 
   const sendRequestAndAwaitResponse = (
     walletInteraction: WalletInteraction,
-    type: 'transaction' | 'data',
   ) => {
     updateConnectButtonStatus('pending')
     return ResultAsync.combine([
-      (type === 'data'
-        ? walletRequestSdk.request
-        : walletRequestSdk.sendTransaction)(
+      walletRequestSdk.sendInteraction(
         walletInteraction,
         cancelRequestControl(walletInteraction.interactionId),
       ),
@@ -273,7 +270,7 @@ export const WalletRequestModule = (input: {
     })
 
   const sendDataRequest = (walletInteraction: WalletInteraction) =>
-    sendRequestAndAwaitResponse(walletInteraction, 'data')
+    sendRequestAndAwaitResponse(walletInteraction)
       .andThen((response) => {
         logger?.debug({ method: 'sendDataRequest.successResponse', response })
         return ok(response.walletData! as WalletData)
@@ -287,7 +284,7 @@ export const WalletRequestModule = (input: {
     stateModule.getState().mapErr(() => SdkError('FailedToReadRdtState', ''))
 
   const addNewRequest = (
-    type: 'loginRequest' | 'dataRequest' | 'proofRequest',
+    type: RequestItemTypes,
     walletInteraction: WalletInteraction,
     isOneTimeRequest: boolean,
   ) =>
@@ -305,6 +302,33 @@ export const WalletRequestModule = (input: {
         ),
       )
 
+  const sendPreAuthorizationRequest = (
+    value: SendPreAuthorizationRequestInput,
+  ): ResultAsync<
+    {
+      signedPartialTransaction: string
+    },
+    SdkError
+  > => {
+    const walletInteraction = walletRequestSdk.createWalletInteraction({
+      discriminator: 'preAuthorizedRequest',
+      subintent: {
+        discriminator: 'subintent',
+        blobs: value.blobs,
+        transactionManifest: value.transactionManifest,
+        message: value.message,
+        version: value.version ?? 1,
+      },
+    })
+
+    return addNewRequest('preAuthorizationRequest', walletInteraction, false)
+      .andThen(() => sendRequestAndAwaitResponse(walletInteraction))
+      .map((requestItem) => ({
+        signedPartialTransaction:
+          requestItem.walletResponse.signedPartialTransaction,
+      }))
+  }
+
   const sendRequest = ({
     isConnect,
     oneTime,
@@ -423,7 +447,7 @@ export const WalletRequestModule = (input: {
 
     return createTransactionRequest()
       .andThen((walletInteraction) =>
-        sendRequestAndAwaitResponse(walletInteraction, 'transaction'),
+        sendRequestAndAwaitResponse(walletInteraction),
       )
       .andThen(({ status, transactionIntentHash, metadata, interactionId }) => {
         const output = {
@@ -502,6 +526,7 @@ export const WalletRequestModule = (input: {
           return err(error)
         }),
     sendTransaction,
+    sendPreAuthorizationRequest,
     cancelRequest,
     ignoreTransaction,
     requestItemModule,
diff --git a/packages/dapp-toolkit/src/radix-dapp-toolkit.ts b/packages/dapp-toolkit/src/radix-dapp-toolkit.ts
index 50ce4f84..5973ef90 100644
--- a/packages/dapp-toolkit/src/radix-dapp-toolkit.ts
+++ b/packages/dapp-toolkit/src/radix-dapp-toolkit.ts
@@ -119,6 +119,7 @@ export const RadixDappToolkit = (
         walletRequestModule.provideConnectResponseCallback,
       updateSharedAccounts: () => walletRequestModule.updateSharedAccounts(),
       sendOneTimeRequest: walletRequestModule.sendOneTimeRequest,
+      sendPreAuthorizationRequest: walletRequestModule.sendPreAuthorizationRequest,
       sendTransaction: (input: SendTransactionInput) =>
         walletRequestModule.sendTransaction(input),
       walletData$: stateModule.walletData$,
diff --git a/packages/dapp-toolkit/src/schemas/index.ts b/packages/dapp-toolkit/src/schemas/index.ts
index e82d17f1..1d72b34a 100644
--- a/packages/dapp-toolkit/src/schemas/index.ts
+++ b/packages/dapp-toolkit/src/schemas/index.ts
@@ -244,11 +244,49 @@ export const CancelRequest = object({
   discriminator: literal('cancelRequest'),
 })
 
+export type ExpireAtTime = InferOutput
+export const ExpireAtTime = object({
+  discriminator: literal('expireAtTime'),
+  value: string(),
+})
+
+export type ExpireAfterSignature = InferOutput
+export const ExpireAfterSignature = object({
+  discriminator: literal('expireAfterSignature'),
+  value: string(),
+})
+
+export type SubintentRequestItem = InferOutput
+export const SubintentRequestItem = object({
+  discriminator: literal('subintent'),
+  version: number(),
+  transactionManifest: string(),
+  blobs: optional(array(string())),
+  message: optional(string()),
+  childSubintentHashes: optional(array(string())),
+  expiration: optional(union([ExpireAtTime, ExpireAfterSignature])),
+})
+
+export type SubintentResponseItem = InferOutput
+export const SubintentResponseItem = object({
+  discriminator: literal('subintent'),
+  signedPartialTransaction: string(),
+})
+
+export type WalletPreAuthorizationItems = InferOutput<
+  typeof WalletPreAuthorizationItems
+>
+export const WalletPreAuthorizationItems = object({
+  discriminator: literal('preAuthorizedRequest'),
+  subintent: optional(SubintentRequestItem),
+})
+
 export type WalletInteractionItems = InferOutput
 export const WalletInteractionItems = union([
   WalletRequestItems,
   WalletTransactionItems,
   CancelRequest,
+  WalletPreAuthorizationItems,
 ])
 
 export type Metadata = InferOutput