From 3742e56aab2b3236a95f4dbf8c0dcb7cd3dc7ead Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Tue, 8 Sep 2026 15:34:29 +0200 Subject: [PATCH 1/4] feat(stellar-wallet-snap): add recoverable confirmation refresh outcome --- packages/stellar-wallet-snap/CHANGELOG.md | 1 + .../cron-job/refreshConfirmationContext.md | 7 +- .../cronjob/refreshConfirmationContext/api.ts | 10 +++ .../handler.test.ts | 73 +++++++++++++++++++ .../refreshConfirmationContext/handler.ts | 17 ++++- .../transactionRefresher.test.ts | 24 +++++- .../transactionRefresher.ts | 7 +- 7 files changed, 127 insertions(+), 12 deletions(-) diff --git a/packages/stellar-wallet-snap/CHANGELOG.md b/packages/stellar-wallet-snap/CHANGELOG.md index 47bd0888..c2e8305f 100644 --- a/packages/stellar-wallet-snap/CHANGELOG.md +++ b/packages/stellar-wallet-snap/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add `recoverable` confirmation-refresh outcome for send-flow SEP-29 RequiresMemo (`confirmSend`): omit scan this cycle without nulling `securityScanRequest`, pause auto-cron until UI reschedules ([WPN-2041](https://consensyssoftware.atlassian.net/browse/WPN-2041)) - Add `signProofOfOwnership` client request for silent proof-of-ownership signing (SEP-0053) ([#186](https://github.com/MetaMask/internal-snaps/pull/186)) - Add `exportAccount` keyring method for base32 Stellar secret-seed export ([#187](https://github.com/MetaMask/internal-snaps/pull/187)) - Add `TrustlineExceedLimitException` for send simulation when a payment would exceed the destination trustline limit (previously a generic `TransactionValidationException`) ([#185](https://github.com/MetaMask/internal-snaps/pull/185)) diff --git a/packages/stellar-wallet-snap/docs/use-cases/cron-job/refreshConfirmationContext.md b/packages/stellar-wallet-snap/docs/use-cases/cron-job/refreshConfirmationContext.md index e877296e..20bdb992 100644 --- a/packages/stellar-wallet-snap/docs/use-cases/cron-job/refreshConfirmationContext.md +++ b/packages/stellar-wallet-snap/docs/use-cases/cron-job/refreshConfirmationContext.md @@ -43,16 +43,17 @@ Runs (or refreshes) the remote security scan on `securityScanRequest` in context Runs **first** when enabled. Rebuilds from the original request against a live on-chain account (fresh fee, sequence, time bounds, destination activation). Confirm-time send / change-trust rebuilds again before signing; this cycle does not patch the stored confirmation `transaction` XDR. - **Success** — write the rebuilt XDR into `securityScanRequest` so scan does not use a stale snapshot. -- **Failure** — set `transactionsFetchStatus` to error, set mapped `errorMessage` for the confirmation banner, and set `scanFetchStatus` to error, so scan is skipped. +- **Hard failure (`halt`)** — set `transactionsFetchStatus` / `scanFetchStatus` to error with a mapped banner message; omit the security scan this cycle; do **not** reschedule further auto-cron cycles. Used for validation errors the user cannot fix in-dialog (balance, trustline, etc.). +- **Recoverable failure (`recoverable`)** — same banner + omit scan this cycle + pause auto-cron, but keep `securityScanRequest` intact (do not null it). Used for SEP-29 `RequiresMemo` so a later UI-triggered refresh (after the user adds a memo) can rebuild and re-scan without reconstructing the scan request. Distinct from `halt`: soft-fail intended to be resumed by the UI, not a permanent hard-stop signal. ## Step-by-step (one cycle) 1. Resolve enabled refreshers from `refresherKeys`. 2. Load interface context; if the dialog was dismissed → stop (no reschedule). 3. Run **transaction** refresher alone (if selected), merge its patch. -4. Run **prices** and **scan** in parallel on the updated context. +4. Run **prices** and **scan** in parallel on the updated context (scan is omitted when the transaction refresher returned `halt` or `recoverable`). 5. Merge patches → `ConfirmationUXController.updateConfirmation`. -6. If any refresher asks to **reschedule** → schedule the next `refreshConfirmationContext` event. +6. If any refresher **halt**ed or returned **recoverable** → stop (no auto-reschedule). Otherwise if any refresher asks to **reschedule** → schedule the next `refreshConfirmationContext` event. ## Sequence diff --git a/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/api.ts b/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/api.ts index 6c1a487d..c51fdb72 100644 --- a/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/api.ts +++ b/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/api.ts @@ -30,8 +30,18 @@ export type ConfirmationContextRefreshResult = { /** * When true, the handler does not reschedule after this cycle. * But other refreshers may still run (e.g. prices). + * Use for hard validation failures the user cannot fix in-dialog. */ halt?: boolean; + /** + * Soft validation failure (e.g. SEP-29 RequiresMemo): omit the security scan + * this cycle and do not auto-reschedule, but keep `securityScanRequest` intact + * so a later UI-triggered `scheduleBackgroundEvent` (after the user fixes the + * issue) can rebuild and re-scan without reconstructing the scan request. + * Distinct from {@link ConfirmationContextRefreshResult.halt} — not a permanent + * hard-stop signal for unrecoverable errors. + */ + recoverable?: boolean; } | null; /** diff --git a/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/handler.test.ts b/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/handler.test.ts index 2661512d..4a0b3d1e 100644 --- a/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/handler.test.ts +++ b/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/handler.test.ts @@ -502,4 +502,77 @@ describe('RefreshConfirmationContextHandler', () => { ); expect(scheduleBackgroundEvent).not.toHaveBeenCalled(); }); + + it('skips the security scan and does not reschedule when a refresher is recoverable', async () => { + jest + .mocked(getInterfaceContextIfExists) + .mockResolvedValueOnce(baseContext) + .mockResolvedValueOnce(baseContext); + + const transactionRefresher = createMockRefresher( + ConfirmationContextRefresherKey.Transaction, + { + refresh: jest.fn().mockResolvedValue({ + result: { + transactionsFetchStatus: FetchStatus.Error, + scanFetchStatus: FetchStatus.Error, + }, + reschedule: false, + recoverable: true, + }), + }, + ); + const pricesRefresher = createMockRefresher( + ConfirmationContextRefresherKey.Prices, + { + refresh: jest.fn().mockResolvedValue({ + result: { tokenPricesFetchStatus: FetchStatus.Fetched }, + reschedule: true, + }), + }, + ); + const scanRefresher = createMockRefresher( + ConfirmationContextRefresherKey.Scan, + { + refresh: jest.fn().mockResolvedValue({ + result: { scanFetchStatus: FetchStatus.Fetched }, + reschedule: true, + }), + }, + ); + + const { handler, updateConfirmation } = setup([ + transactionRefresher, + pricesRefresher, + scanRefresher, + ]); + + await handler.handle({ + jsonrpc: '2.0', + id: '1', + method: BackgroundEventMethod.RefreshConfirmationContext, + params: { + ...confirmationContextRequestParams, + refresherKeys: [ + ConfirmationContextRefresherKey.Transaction, + ConfirmationContextRefresherKey.Prices, + ConfirmationContextRefresherKey.Scan, + ], + }, + }); + + expect(transactionRefresher.refresh).toHaveBeenCalledTimes(1); + expect(pricesRefresher.refresh).toHaveBeenCalledTimes(1); + expect(scanRefresher.refresh).not.toHaveBeenCalled(); + expect(updateConfirmation).toHaveBeenCalledWith( + expect.objectContaining({ + updatedContext: expect.objectContaining({ + transactionsFetchStatus: FetchStatus.Error, + scanFetchStatus: FetchStatus.Error, + tokenPricesFetchStatus: FetchStatus.Fetched, + }), + }), + ); + expect(scheduleBackgroundEvent).not.toHaveBeenCalled(); + }); }); diff --git a/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/handler.ts b/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/handler.ts index de88ce9c..d8308d16 100644 --- a/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/handler.ts +++ b/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/handler.ts @@ -147,6 +147,16 @@ export class RefreshConfirmationContextHandler extends CronjobBaseHandler result?.recoverable)) { + this.logger.info( + 'Confirmation refresh recoverable; cron paused until UI reschedules', + ); + return; + } + if (results.some((result) => result?.reschedule)) { await RefreshConfirmationContextHandler.scheduleBackgroundEvent({ scope, @@ -184,7 +194,8 @@ export class RefreshConfirmationContextHandler extends CronjobBaseHandler { errorMessage: 'confirmation.txnError.insufficientBalanceToCoverBaseReserve', }, - { - error: new RequiresMemoException(toAddress), - errorMessage: 'confirmation.txnError.requiresMemo', - }, { error: new InvalidAmountForCreateAccountException('0.5'), errorMessage: 'confirmation.txnError.invalidCreateAccountAmount', @@ -263,6 +259,26 @@ describe('ConfirmationTransactionRefresher', () => { }, ); + it('marks RequiresMemo as recoverable without nulling securityScanRequest', async () => { + const { refresher, transactionService } = setup(); + transactionService.createValidatedSendTransaction.mockRejectedValueOnce( + new RequiresMemoException(toAddress), + ); + + const result = await refresher.refresh(createTransactionContext()); + + expect(result).toStrictEqual({ + result: { + transactionsFetchStatus: FetchStatus.Error, + errorMessage: 'confirmation.txnError.requiresMemo', + scanFetchStatus: FetchStatus.Error, + }, + reschedule: false, + recoverable: true, + }); + expect(result?.result.securityScanRequest).toBeUndefined(); + }); + it('re-validates a change-trust opt-in transaction', async () => { const { refresher, transactionService } = setup(); diff --git a/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/transactionRefresher.ts b/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/transactionRefresher.ts index dc601372..76b64a3b 100644 --- a/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/transactionRefresher.ts +++ b/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/transactionRefresher.ts @@ -3,6 +3,7 @@ import type { Json } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; import type { AssetMetadataService } from '../../../services/asset-metadata'; +import { RequiresMemoException } from '../../../services/transaction'; import type { Transaction, TransactionService, @@ -180,15 +181,17 @@ export class ConfirmationTransactionRefresher implements IConfirmationContextRef 'Error re-validating confirmation transaction:', error, ); + const recoverable = error instanceof RequiresMemoException; return { result: { transactionsFetchStatus: FetchStatus.Error, errorMessage: getTxnErrorMessageKey(error, accountAddress), - // Clear the scan loading state in the confirmation UI + skip the security scan request. + // Clear the scan loading state in the confirmation UI. Scan is omitted + // via `halt` / `recoverable` — do not null `securityScanRequest`. scanFetchStatus: FetchStatus.Error, }, reschedule: false, - halt: true, + ...(recoverable ? { recoverable: true } : { halt: true }), }; } } From b7ad3327bf2cad47ac18940ec37e89301327da8e Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Tue, 8 Sep 2026 16:55:03 +0200 Subject: [PATCH 2/4] refactor: combine halt and recoverable conditions in refresh confirmation handler --- .../refreshConfirmationContext/handler.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/handler.ts b/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/handler.ts index d8308d16..b18c2537 100644 --- a/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/handler.ts +++ b/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/handler.ts @@ -140,19 +140,12 @@ export class RefreshConfirmationContextHandler extends CronjobBaseHandler result?.halt)) { + // `halt` (hard fail) and `recoverable` (soft fail, e.g. RequiresMemo) both + // pause auto-cron. UI may call scheduleBackgroundEvent again after a + // recoverable fix (e.g. user adds a memo). + if (results.some((result) => result?.halt || result?.recoverable)) { this.logger.info( - 'Confirmation refresh halted; cron will not be rescheduled', - ); - return; - } - - // Recoverable soft-fail (e.g. RequiresMemo): pause auto-cron this cycle so we - // do not hammer re-validation, but do not treat as halt — UI may call - // scheduleBackgroundEvent after the user fixes the issue (e.g. adds a memo). - if (results.some((result) => result?.recoverable)) { - this.logger.info( - 'Confirmation refresh recoverable; cron paused until UI reschedules', + 'Confirmation refresh halted or recoverable; cron will not be rescheduled', ); return; } From 63c15ada5200840899d97ace69dfdfa9340c67c6 Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Tue, 8 Sep 2026 17:00:00 +0200 Subject: [PATCH 3/4] chore: lint --- .../cronjob/refreshConfirmationContext/handler.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/handler.ts b/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/handler.ts index b18c2537..eafa1a21 100644 --- a/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/handler.ts +++ b/packages/stellar-wallet-snap/src/handlers/cronjob/refreshConfirmationContext/handler.ts @@ -143,7 +143,11 @@ export class RefreshConfirmationContextHandler extends CronjobBaseHandler result?.halt || result?.recoverable)) { + if ( + results.some( + (result) => result?.halt === true || result?.recoverable === true, + ) + ) { this.logger.info( 'Confirmation refresh halted or recoverable; cron will not be rescheduled', ); @@ -227,7 +231,10 @@ export class RefreshConfirmationContextHandler extends CronjobBaseHandler Date: Tue, 8 Sep 2026 17:08:04 +0200 Subject: [PATCH 4/4] chore: update CHANGELOG.md --- packages/stellar-wallet-snap/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stellar-wallet-snap/CHANGELOG.md b/packages/stellar-wallet-snap/CHANGELOG.md index c2e8305f..def76ac1 100644 --- a/packages/stellar-wallet-snap/CHANGELOG.md +++ b/packages/stellar-wallet-snap/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `recoverable` confirmation-refresh outcome for send-flow SEP-29 RequiresMemo (`confirmSend`): omit scan this cycle without nulling `securityScanRequest`, pause auto-cron until UI reschedules ([WPN-2041](https://consensyssoftware.atlassian.net/browse/WPN-2041)) +- Add `recoverable` confirmation-refresh outcome for send-flow SEP-29 RequiresMemo (`confirmSend`): omit scan this cycle without nulling `securityScanRequest`, pause auto-cron until UI reschedules ([#291](https://github.com/MetaMask/internal-snaps/pull/291)) - Add `signProofOfOwnership` client request for silent proof-of-ownership signing (SEP-0053) ([#186](https://github.com/MetaMask/internal-snaps/pull/186)) - Add `exportAccount` keyring method for base32 Stellar secret-seed export ([#187](https://github.com/MetaMask/internal-snaps/pull/187)) - Add `TrustlineExceedLimitException` for send simulation when a payment would exceed the destination trustline limit (previously a generic `TransactionValidationException`) ([#185](https://github.com/MetaMask/internal-snaps/pull/185))