From b03d183a0603b6cf2d3c28fd632f4d5ce9b30111 Mon Sep 17 00:00:00 2001 From: Patryk Kalinowski Date: Mon, 9 Dec 2024 15:06:44 +0100 Subject: [PATCH 1/2] waas: add support for intent confirmations --- packages/waas/src/auth.ts | 17 +++++++++++++++++ packages/waas/src/base.ts | 15 ++++++++++++++- packages/waas/src/intents/responses.ts | 13 ++++++++++++- packages/waas/src/intents/session.ts | 9 ++++++++- 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/packages/waas/src/auth.ts b/packages/waas/src/auth.ts index a27dc2826..ccb49c607 100644 --- a/packages/waas/src/auth.ts +++ b/packages/waas/src/auth.ts @@ -22,9 +22,11 @@ import { AdoptChildWalletArgs } from './intents' import { + ConfirmationRequiredResponse, FeeOptionsResponse, isChildWalletAdoptedResponse, isCloseSessionResponse, + isConfirmationRequiredResponse, isFeeOptionsResponse, isFinishValidateSessionResponse, isGetAdopterResponse, @@ -333,6 +335,11 @@ export class SequenceWaaS { return this.waitForSessionValid() } + private async handleConfirmationRequired(response: ConfirmationRequiredResponse) { + const intent2 = await this.waas.confirmIntent(response.data.salt, "111111") + return intent2 + } + private headers() { return { 'X-Access-Key': this.config.projectAccessKey @@ -775,6 +782,16 @@ export class SequenceWaaS { return response } + if (isConfirmationRequiredResponse(response)) { + const intent2 = await this.handleConfirmationRequired(response) + if (intent2) { + const response2 = await this.sendIntent(intent2) + if (isExpectedResponse(response2)) { + return response2 + } + } + } + if (isValidationRequiredResponse(response)) { const proceed = await this.handleValidationRequired(args.validation) diff --git a/packages/waas/src/base.ts b/packages/waas/src/base.ts index cdde0f509..7b6ec4296 100644 --- a/packages/waas/src/base.ts +++ b/packages/waas/src/base.ts @@ -4,6 +4,7 @@ import { changeIntentTime, closeSession, combineTransactionIntents, + confirmIntent, feeOptions, finishValidateSession, getAdopter, @@ -50,7 +51,8 @@ import { IntentDataOpenSession, IntentDataSendTransaction, IntentDataSignMessage, - IntentDataValidateSession + IntentDataValidateSession, + IntentDataConfirmIntent, } from './clients/intent.gen' import { getDefaultSubtleCryptoBackend, SubtleCryptoBackend } from './subtle-crypto' import { getDefaultSecureStoreBackend, SecureStoreBackend } from './secure-store' @@ -513,6 +515,17 @@ export class SequenceWaaSBase { return this.signIntent(intent) } + async confirmIntent(salt: string, secretCode: string): Promise> { + const intent = confirmIntent({ + lifespan: DEFAULT_LIFESPAN, + wallet: await this.getWalletAddress(), + confirmationID: salt, + challengeAnswer: ethers.id(salt + secretCode), + }) + + return this.signIntent(intent) + } + async getSession(): Promise> { const sessionId = await this.sessionId.get() if (!sessionId) { diff --git a/packages/waas/src/intents/responses.ts b/packages/waas/src/intents/responses.ts index 6af89ba43..7a1829e00 100644 --- a/packages/waas/src/intents/responses.ts +++ b/packages/waas/src/intents/responses.ts @@ -10,7 +10,8 @@ import { IntentResponseGetSession, IntentResponseIdToken, IntentResponseValidationFinished, - IntentResponseValidationStarted + IntentResponseValidationStarted, + IntentResponseConfirmationRequired, } from '../clients/intent.gen' import { WebrpcEndpointError, WebrpcError } from '../clients/authenticator.gen' @@ -127,6 +128,7 @@ export interface Response { export type InitiateAuthResponse = Response export type ValidateSessionResponse = Response +export type ConfirmationRequiredResponse = Response export type FinishValidateSessionResponse = Response export type GetSessionResponse = Response export type LinkAccountResponse = Response @@ -249,6 +251,15 @@ export function isFinishValidateSessionResponse(receipt: any): receipt is Finish return typeof receipt === 'object' && receipt.code === IntentResponseCode.validationFinished && typeof receipt.data === 'object' } +export function isConfirmationRequiredResponse(receipt: any): receipt is ConfirmationRequiredResponse { + return ( + typeof receipt === 'object' && + receipt.code === IntentResponseCode.confirmationRequired && + typeof receipt.data === 'object' && + typeof receipt.data.salt === 'string' + ) +} + export function isCloseSessionResponse(receipt: any): receipt is CloseSessionResponse { return typeof receipt === 'object' && typeof receipt.code === 'string' && receipt.code === 'sessionClosed' } diff --git a/packages/waas/src/intents/session.ts b/packages/waas/src/intents/session.ts index e8028fe77..4a534bbc4 100644 --- a/packages/waas/src/intents/session.ts +++ b/packages/waas/src/intents/session.ts @@ -11,7 +11,8 @@ import { IntentDataGetIdToken, IntentName, IntentDataAdoptChildWallet, - IntentDataGetAdopter + IntentDataGetAdopter, + IntentDataConfirmIntent, } from '../clients/intent.gen' interface BaseArgs { @@ -83,3 +84,9 @@ export type GetAdopterArgs = BaseArgs & IntentDataGetAdopter export function getAdopter({ lifespan, ...data }: GetAdopterArgs): Intent { return makeIntent(IntentName.getAdopter, lifespan, data) } + +export type ConfirmIntentArgs = BaseArgs & IntentDataConfirmIntent + +export function confirmIntent({ lifespan, ...data }: ConfirmIntentArgs): Intent { + return makeIntent(IntentName.confirmIntent, lifespan, data) +} From 9ab8f14d2f7b1601f96ce422afb2eb7d2bdde3df Mon Sep 17 00:00:00 2001 From: Patryk Kalinowski Date: Thu, 12 Dec 2024 20:28:11 +0100 Subject: [PATCH 2/2] add support for confirmation callback --- packages/waas/src/auth.ts | 58 +++++++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/packages/waas/src/auth.ts b/packages/waas/src/auth.ts index ccb49c607..b3a8dc7a8 100644 --- a/packages/waas/src/auth.ts +++ b/packages/waas/src/auth.ts @@ -170,7 +170,9 @@ export class SequenceWaaS { private validationRequiredCallback: (() => void)[] = [] private emailConflictCallback: ((info: EmailConflictInfo, forceCreate: () => Promise) => Promise)[] = [] private emailAuthCodeRequiredCallback: ((respondWithCode: (code: string) => Promise) => Promise)[] = [] + private confirmationRequiredCallback: ((respondWithCode: (code: string) => Promise) => Promise)[] = [] private validationRequiredSalt: string + private lastConfirmationAttemptAt: Date | undefined public readonly config: Required & Required & ExtendedSequenceConfig @@ -315,6 +317,13 @@ export class SequenceWaaS { } } + onConfirmationRequired(callback: (respondWithCode: (code: string) => Promise) => Promise) { + this.confirmationRequiredCallback.push(callback) + return () => { + this.confirmationRequiredCallback = this.confirmationRequiredCallback.filter(c => c !== callback) + } + } + private async handleValidationRequired({ onValidationRequired }: ValidationArgs = {}): Promise { const proceed = onValidationRequired ? onValidationRequired() : true if (!proceed) { @@ -336,8 +345,42 @@ export class SequenceWaaS { } private async handleConfirmationRequired(response: ConfirmationRequiredResponse) { - const intent2 = await this.waas.confirmIntent(response.data.salt, "111111") - return intent2 + if (this.confirmationRequiredCallback.length === 0) { + throw new Error('Missing confirmationRequired callback') + } + + return new Promise((resolve, reject) => { + const respondToChallenge = async (answer: string) => { + if (this.lastConfirmationAttemptAt) { + const timeSinceLastAttempt = new Date().getTime() - this.lastConfirmationAttemptAt.getTime() + if (timeSinceLastAttempt < 32000) { + console.info(`Waiting ${Math.ceil((32000 - timeSinceLastAttempt) / 1000)}s before retrying confirmation attempt`) + await new Promise(resolve => setTimeout(resolve, 32000 - timeSinceLastAttempt)) + } + } + + this.lastConfirmationAttemptAt = new Date() + + const intent = await this.waas.confirmIntent(response.data.salt, answer) + + try { + const response2 = await this.sendIntent(intent) + resolve(response2) + } catch (e) { + if (e instanceof AnswerIncorrectError) { + // This will NOT resolve NOR reject the top-level promise returned from signIn, it'll keep being pending + // It allows the caller to retry calling the respondToChallenge callback + throw e + } else { + reject(e) + } + } + } + + for (const callback of this.confirmationRequiredCallback) { + callback(respondToChallenge) + } + }) } private headers() { @@ -783,12 +826,11 @@ export class SequenceWaaS { } if (isConfirmationRequiredResponse(response)) { - const intent2 = await this.handleConfirmationRequired(response) - if (intent2) { - const response2 = await this.sendIntent(intent2) - if (isExpectedResponse(response2)) { - return response2 - } + const response2 = await this.handleConfirmationRequired(response) + if (isExpectedResponse(response2)) { + return response2 + } else { + throw new Error(JSON.stringify(response2)) } }