feat: release utxos after user rejection or error#162
feat: release utxos after user rejection or error#162raul-oliveira wants to merge 1 commit intomasterfrom
Conversation
📝 WalkthroughWalkthroughThis PR adds UTXO cleanup functionality ( Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
packages/hathor-rpc-handler/src/rpcMethods/sendNanoContractTx.ts (2)
176-196:⚠️ Potential issue | 🟠 MajorCleanup still misses pre-signing failures after the lock is acquired.
Once the pre-build succeeds,
releaseUtxos()is only reached for explicit rejections and the later sign/sendtry/catch. IftriggerHandler(...)rejects, or the confirmation payload failssafeParse(...)on Lines 179-181, or the PIN prompt call on Line 193 throws, the reserved UTXOs still leak.Based on learnings, the pre-build
createNanoContractTransaction(..., signTx: false)intentionally locks UTXOs from that point onward.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/hathor-rpc-handler/src/rpcMethods/sendNanoContractTx.ts` around lines 176 - 196, After pre-build (the call that locks UTXOs via createNanoContractTransaction(..., signTx: false)), ensure preBuildSendTx.releaseUtxos() is always executed by wrapping the sequence that calls triggerHandler(sendNanoContractTxPrompt...), the safeParse using sendNanoContractTxConfirmationResponseSchema, the triggerHandler(pinPrompt...) and subsequent sign/send logic in a try/finally (or ensure a finally-like cleanup) so any thrown errors (handler rejection, parse failure, PIN prompt rejection) will call preBuildSendTx.releaseUtxos(); locate and modify the block that begins after createNanoContractTransaction to add the try { ... } finally { await preBuildSendTx.releaseUtxos(); } pattern and remove existing ad-hoc release calls where duplicated.
187-237:⚠️ Potential issue | 🔴 CriticalAdd type bridge for
preBuildSendTx— compile blocker.The code calls
preBuildSendTx.releaseUtxos()at lines 187, 196, and 237 without a type declaration. TypeScript will reportTS2339: Property 'releaseUtxos' does not exist on type 'SendTransactionWalletService'. Add theISendTransactionObjectinterface fromsendTransaction.ts(lines 33–39) to this file before merging.Type bridge pattern from sendTransaction.ts
interface ISendTransactionObject { prepareTx(): Promise<Transaction>; signTx(pin: string): Promise<Transaction>; runFromMining(): Promise<Transaction>; releaseUtxos(): Promise<void>; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/hathor-rpc-handler/src/rpcMethods/sendNanoContractTx.ts` around lines 187 - 237, Declare and use the ISendTransactionObject type bridge in this file so preBuildSendTx has the releaseUtxos signature: add the ISendTransactionObject interface (prepareTx, signTx, runFromMining, releaseUtxos returning Promise<void>) from sendTransaction.ts to this file and annotate the preBuildSendTx variable/parameter with that interface (reference symbol: preBuildSendTx) or import it if you move the interface to a shared module; this ensures calls like preBuildSendTx.releaseUtxos() compile.packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts (1)
186-231:⚠️ Potential issue | 🟠 MajorThe cleanup boundary still starts too late.
After
prepareTx()succeeds, any exception before the innertryon Line 208 still bypassesreleaseUtxos()—for examplefetchTokenDetails(), fee validation, eitherpromptHandler(...)call rejecting, or a malformed prompt payload. Those paths still leave the prepared inputs locked, which is the exact failure mode this PR is trying to eliminate.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts` around lines 186 - 231, The cleanup (sendTransactionObject.releaseUtxos()) must run for any error that occurs after prepareTx() succeeds; wrap the whole logic that follows prepareTx() (everything from prompt creation/validation, both promptHandler calls, fee/token checks, signTx, runFromMining, and the inner try/catch) in a try/finally (or ensure a finally block) that always calls sendTransactionObject.releaseUtxos() (best-effort) so prepared UTXOs are released on any exception, and keep the existing inner try/catch for handling send errors; locate prepareTx(), sendTransactionObject.releaseUtxos(), promptHandler(...) and sendTransactionObject.signTx()/runFromMining() to apply the finally around the correct region.packages/hathor-rpc-handler/src/rpcMethods/createNanoContractCreateTokenTx.ts (1)
155-179:⚠️ Potential issue | 🟠 MajorErrors between pre-build and the post-PIN
trystill leak the lock.This only unlocks on explicit rejection and inside the later
try/catch. If the confirmation prompt throws, the response failssafeParse(...), or the PIN prompt call itself throws, the pre-built UTXOs still remain locked.Based on learnings, the
signTx: falsepre-build step is intentionally allowed to lock UTXOs from the pre-build phase onward.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/hathor-rpc-handler/src/rpcMethods/createNanoContractCreateTokenTx.ts` around lines 155 - 179, The code currently can leak the pre-build UTXO lock if any error occurs between pre-build and the post-PIN try/catch (e.g., promptHandler throws, safeParse fails, or PIN prompt throws); ensure preBuildResult.releaseUtxos() is always called on any early exit. Fix by surrounding the confirmation + PIN flow (the promptHandler call that produces rawResponse, the safeParse using createNanoContractCreateTokenTxConfirmationResponseSchema, and the PIN prompt creation/await) with a try/finally (or add catch blocks) that calls preBuildResult.releaseUtxos() before rethrowing; alternatively, call preBuildResult.releaseUtxos() right before every throw (including after safeParse failure, on promptHandler errors, and before throwing when pinResponse.data.accepted is false) so the lock is released in all error paths. Reference symbols: preBuildResult.releaseUtxos(), promptHandler(...), createNanoContractCreateTokenTxConfirmationResponseSchema.safeParse(...), PinConfirmationPrompt and pinResponse.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/hathor-rpc-handler/__tests__/rpcMethods/sendNanoContractTx.test.ts`:
- Around line 423-509: The tests in sendNanoContractTx.test.ts currently use
generic rejects.toThrow() for the two rejection cases; update both assertions to
specifically expect a PromptRejectedError to lock the contract (replace await
expect(sendNanoContractTx(...)).rejects.toThrow() with await
expect(...).rejects.toThrow(PromptRejectedError) or equivalent instance check).
Ensure the test file imports PromptRejectedError (or references the exact
exported symbol) and keep the existing mockReleaseUtxos assertions unchanged.
---
Outside diff comments:
In
`@packages/hathor-rpc-handler/src/rpcMethods/createNanoContractCreateTokenTx.ts`:
- Around line 155-179: The code currently can leak the pre-build UTXO lock if
any error occurs between pre-build and the post-PIN try/catch (e.g.,
promptHandler throws, safeParse fails, or PIN prompt throws); ensure
preBuildResult.releaseUtxos() is always called on any early exit. Fix by
surrounding the confirmation + PIN flow (the promptHandler call that produces
rawResponse, the safeParse using
createNanoContractCreateTokenTxConfirmationResponseSchema, and the PIN prompt
creation/await) with a try/finally (or add catch blocks) that calls
preBuildResult.releaseUtxos() before rethrowing; alternatively, call
preBuildResult.releaseUtxos() right before every throw (including after
safeParse failure, on promptHandler errors, and before throwing when
pinResponse.data.accepted is false) so the lock is released in all error paths.
Reference symbols: preBuildResult.releaseUtxos(), promptHandler(...),
createNanoContractCreateTokenTxConfirmationResponseSchema.safeParse(...),
PinConfirmationPrompt and pinResponse.
In `@packages/hathor-rpc-handler/src/rpcMethods/sendNanoContractTx.ts`:
- Around line 176-196: After pre-build (the call that locks UTXOs via
createNanoContractTransaction(..., signTx: false)), ensure
preBuildSendTx.releaseUtxos() is always executed by wrapping the sequence that
calls triggerHandler(sendNanoContractTxPrompt...), the safeParse using
sendNanoContractTxConfirmationResponseSchema, the triggerHandler(pinPrompt...)
and subsequent sign/send logic in a try/finally (or ensure a finally-like
cleanup) so any thrown errors (handler rejection, parse failure, PIN prompt
rejection) will call preBuildSendTx.releaseUtxos(); locate and modify the block
that begins after createNanoContractTransaction to add the try { ... } finally {
await preBuildSendTx.releaseUtxos(); } pattern and remove existing ad-hoc
release calls where duplicated.
- Around line 187-237: Declare and use the ISendTransactionObject type bridge in
this file so preBuildSendTx has the releaseUtxos signature: add the
ISendTransactionObject interface (prepareTx, signTx, runFromMining, releaseUtxos
returning Promise<void>) from sendTransaction.ts to this file and annotate the
preBuildSendTx variable/parameter with that interface (reference symbol:
preBuildSendTx) or import it if you move the interface to a shared module; this
ensures calls like preBuildSendTx.releaseUtxos() compile.
In `@packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts`:
- Around line 186-231: The cleanup (sendTransactionObject.releaseUtxos()) must
run for any error that occurs after prepareTx() succeeds; wrap the whole logic
that follows prepareTx() (everything from prompt creation/validation, both
promptHandler calls, fee/token checks, signTx, runFromMining, and the inner
try/catch) in a try/finally (or ensure a finally block) that always calls
sendTransactionObject.releaseUtxos() (best-effort) so prepared UTXOs are
released on any exception, and keep the existing inner try/catch for handling
send errors; locate prepareTx(), sendTransactionObject.releaseUtxos(),
promptHandler(...) and sendTransactionObject.signTx()/runFromMining() to apply
the finally around the correct region.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d5b84f95-f872-4d9b-91b1-6f9222ab09b4
📒 Files selected for processing (6)
packages/hathor-rpc-handler/__tests__/rpcMethods/createNanoContractCreateTokenTx.test.tspackages/hathor-rpc-handler/__tests__/rpcMethods/sendNanoContractTx.test.tspackages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.tspackages/hathor-rpc-handler/src/rpcMethods/createNanoContractCreateTokenTx.tspackages/hathor-rpc-handler/src/rpcMethods/sendNanoContractTx.tspackages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts
| it('should call releaseUtxos when user rejects the confirmation prompt', async () => { | ||
| const address = 'address123'; | ||
|
|
||
| const mockTransaction = { | ||
| getFeeHeader: jest.fn().mockReturnValue({ entries: [] }), | ||
| getNanoHeaders: jest.fn().mockReturnValue([{ address: null, seqnum: 0 }]), | ||
| prepareToSend: jest.fn(), | ||
| toHex: jest.fn().mockReturnValue('tx-hex'), | ||
| }; | ||
| const mockReleaseUtxos = jest.fn().mockResolvedValue(undefined); | ||
| const mockSendTx = { | ||
| transaction: mockTransaction, | ||
| runFromMining: jest.fn().mockResolvedValue({ tx_id: 'mock-tx-id' }), | ||
| releaseUtxos: mockReleaseUtxos, | ||
| }; | ||
| (wallet.createNanoContractTransaction as jest.Mock).mockResolvedValue(mockSendTx); | ||
|
|
||
| promptHandler.mockResolvedValueOnce({ | ||
| type: TriggerResponseTypes.SendNanoContractTxConfirmationResponse, | ||
| data: { | ||
| accepted: false, | ||
| nc: { | ||
| caller: address, | ||
| method: rpcRequest.params.method, | ||
| blueprintId: rpcRequest.params.blueprint_id, | ||
| ncId: rpcRequest.params.nc_id, | ||
| args: rpcRequest.params.args, | ||
| parsedArgs: [], | ||
| actions: [], | ||
| pushTx: true, | ||
| fee: 0n, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| await expect(sendNanoContractTx(rpcRequest, wallet, {}, promptHandler)).rejects.toThrow(); | ||
|
|
||
| expect(mockReleaseUtxos).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('should call releaseUtxos when user rejects the PIN prompt', async () => { | ||
| const address = 'address123'; | ||
|
|
||
| const mockTransaction = { | ||
| getFeeHeader: jest.fn().mockReturnValue({ entries: [] }), | ||
| getNanoHeaders: jest.fn().mockReturnValue([{ address: null, seqnum: 0 }]), | ||
| prepareToSend: jest.fn(), | ||
| toHex: jest.fn().mockReturnValue('tx-hex'), | ||
| }; | ||
| const mockReleaseUtxos = jest.fn().mockResolvedValue(undefined); | ||
| const mockSendTx = { | ||
| transaction: mockTransaction, | ||
| runFromMining: jest.fn().mockResolvedValue({ tx_id: 'mock-tx-id' }), | ||
| releaseUtxos: mockReleaseUtxos, | ||
| }; | ||
| (wallet.createNanoContractTransaction as jest.Mock).mockResolvedValue(mockSendTx); | ||
|
|
||
| promptHandler | ||
| .mockResolvedValueOnce({ | ||
| type: TriggerResponseTypes.SendNanoContractTxConfirmationResponse, | ||
| data: { | ||
| accepted: true, | ||
| nc: { | ||
| caller: address, | ||
| method: rpcRequest.params.method, | ||
| blueprintId: rpcRequest.params.blueprint_id, | ||
| ncId: rpcRequest.params.nc_id, | ||
| args: rpcRequest.params.args, | ||
| parsedArgs: [], | ||
| actions: [], | ||
| pushTx: true, | ||
| fee: 0n, | ||
| }, | ||
| }, | ||
| }) | ||
| .mockResolvedValueOnce({ | ||
| type: TriggerResponseTypes.PinRequestResponse, | ||
| data: { | ||
| accepted: false, | ||
| pinCode: '', | ||
| }, | ||
| }); | ||
|
|
||
| await expect(sendNanoContractTx(rpcRequest, wallet, {}, promptHandler)).rejects.toThrow(); | ||
|
|
||
| expect(mockReleaseUtxos).toHaveBeenCalledTimes(1); | ||
| }); |
There was a problem hiding this comment.
Assert PromptRejectedError in the new rejection cleanup tests.
Both new rejection cases use rejects.toThrow(), so they would still pass if releaseUtxos() or prompt parsing started throwing some other error. These should lock in the intended contract: user rejection still surfaces PromptRejectedError.
🧪 Tighten the assertions
-import { SendNanoContractTxError, InvalidParamsError } from '../../src/errors';
+import { SendNanoContractTxError, InvalidParamsError, PromptRejectedError } from '../../src/errors';
...
- await expect(sendNanoContractTx(rpcRequest, wallet, {}, promptHandler)).rejects.toThrow();
+ await expect(sendNanoContractTx(rpcRequest, wallet, {}, promptHandler)).rejects.toThrow(PromptRejectedError);
...
- await expect(sendNanoContractTx(rpcRequest, wallet, {}, promptHandler)).rejects.toThrow();
+ await expect(sendNanoContractTx(rpcRequest, wallet, {}, promptHandler)).rejects.toThrow(PromptRejectedError);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('should call releaseUtxos when user rejects the confirmation prompt', async () => { | |
| const address = 'address123'; | |
| const mockTransaction = { | |
| getFeeHeader: jest.fn().mockReturnValue({ entries: [] }), | |
| getNanoHeaders: jest.fn().mockReturnValue([{ address: null, seqnum: 0 }]), | |
| prepareToSend: jest.fn(), | |
| toHex: jest.fn().mockReturnValue('tx-hex'), | |
| }; | |
| const mockReleaseUtxos = jest.fn().mockResolvedValue(undefined); | |
| const mockSendTx = { | |
| transaction: mockTransaction, | |
| runFromMining: jest.fn().mockResolvedValue({ tx_id: 'mock-tx-id' }), | |
| releaseUtxos: mockReleaseUtxos, | |
| }; | |
| (wallet.createNanoContractTransaction as jest.Mock).mockResolvedValue(mockSendTx); | |
| promptHandler.mockResolvedValueOnce({ | |
| type: TriggerResponseTypes.SendNanoContractTxConfirmationResponse, | |
| data: { | |
| accepted: false, | |
| nc: { | |
| caller: address, | |
| method: rpcRequest.params.method, | |
| blueprintId: rpcRequest.params.blueprint_id, | |
| ncId: rpcRequest.params.nc_id, | |
| args: rpcRequest.params.args, | |
| parsedArgs: [], | |
| actions: [], | |
| pushTx: true, | |
| fee: 0n, | |
| }, | |
| }, | |
| }); | |
| await expect(sendNanoContractTx(rpcRequest, wallet, {}, promptHandler)).rejects.toThrow(); | |
| expect(mockReleaseUtxos).toHaveBeenCalledTimes(1); | |
| }); | |
| it('should call releaseUtxos when user rejects the PIN prompt', async () => { | |
| const address = 'address123'; | |
| const mockTransaction = { | |
| getFeeHeader: jest.fn().mockReturnValue({ entries: [] }), | |
| getNanoHeaders: jest.fn().mockReturnValue([{ address: null, seqnum: 0 }]), | |
| prepareToSend: jest.fn(), | |
| toHex: jest.fn().mockReturnValue('tx-hex'), | |
| }; | |
| const mockReleaseUtxos = jest.fn().mockResolvedValue(undefined); | |
| const mockSendTx = { | |
| transaction: mockTransaction, | |
| runFromMining: jest.fn().mockResolvedValue({ tx_id: 'mock-tx-id' }), | |
| releaseUtxos: mockReleaseUtxos, | |
| }; | |
| (wallet.createNanoContractTransaction as jest.Mock).mockResolvedValue(mockSendTx); | |
| promptHandler | |
| .mockResolvedValueOnce({ | |
| type: TriggerResponseTypes.SendNanoContractTxConfirmationResponse, | |
| data: { | |
| accepted: true, | |
| nc: { | |
| caller: address, | |
| method: rpcRequest.params.method, | |
| blueprintId: rpcRequest.params.blueprint_id, | |
| ncId: rpcRequest.params.nc_id, | |
| args: rpcRequest.params.args, | |
| parsedArgs: [], | |
| actions: [], | |
| pushTx: true, | |
| fee: 0n, | |
| }, | |
| }, | |
| }) | |
| .mockResolvedValueOnce({ | |
| type: TriggerResponseTypes.PinRequestResponse, | |
| data: { | |
| accepted: false, | |
| pinCode: '', | |
| }, | |
| }); | |
| await expect(sendNanoContractTx(rpcRequest, wallet, {}, promptHandler)).rejects.toThrow(); | |
| expect(mockReleaseUtxos).toHaveBeenCalledTimes(1); | |
| }); | |
| it('should call releaseUtxos when user rejects the confirmation prompt', async () => { | |
| const address = 'address123'; | |
| const mockTransaction = { | |
| getFeeHeader: jest.fn().mockReturnValue({ entries: [] }), | |
| getNanoHeaders: jest.fn().mockReturnValue([{ address: null, seqnum: 0 }]), | |
| prepareToSend: jest.fn(), | |
| toHex: jest.fn().mockReturnValue('tx-hex'), | |
| }; | |
| const mockReleaseUtxos = jest.fn().mockResolvedValue(undefined); | |
| const mockSendTx = { | |
| transaction: mockTransaction, | |
| runFromMining: jest.fn().mockResolvedValue({ tx_id: 'mock-tx-id' }), | |
| releaseUtxos: mockReleaseUtxos, | |
| }; | |
| (wallet.createNanoContractTransaction as jest.Mock).mockResolvedValue(mockSendTx); | |
| promptHandler.mockResolvedValueOnce({ | |
| type: TriggerResponseTypes.SendNanoContractTxConfirmationResponse, | |
| data: { | |
| accepted: false, | |
| nc: { | |
| caller: address, | |
| method: rpcRequest.params.method, | |
| blueprintId: rpcRequest.params.blueprint_id, | |
| ncId: rpcRequest.params.nc_id, | |
| args: rpcRequest.params.args, | |
| parsedArgs: [], | |
| actions: [], | |
| pushTx: true, | |
| fee: 0n, | |
| }, | |
| }, | |
| }); | |
| await expect(sendNanoContractTx(rpcRequest, wallet, {}, promptHandler)).rejects.toThrow(PromptRejectedError); | |
| expect(mockReleaseUtxos).toHaveBeenCalledTimes(1); | |
| }); | |
| it('should call releaseUtxos when user rejects the PIN prompt', async () => { | |
| const address = 'address123'; | |
| const mockTransaction = { | |
| getFeeHeader: jest.fn().mockReturnValue({ entries: [] }), | |
| getNanoHeaders: jest.fn().mockReturnValue([{ address: null, seqnum: 0 }]), | |
| prepareToSend: jest.fn(), | |
| toHex: jest.fn().mockReturnValue('tx-hex'), | |
| }; | |
| const mockReleaseUtxos = jest.fn().mockResolvedValue(undefined); | |
| const mockSendTx = { | |
| transaction: mockTransaction, | |
| runFromMining: jest.fn().mockResolvedValue({ tx_id: 'mock-tx-id' }), | |
| releaseUtxos: mockReleaseUtxos, | |
| }; | |
| (wallet.createNanoContractTransaction as jest.Mock).mockResolvedValue(mockSendTx); | |
| promptHandler | |
| .mockResolvedValueOnce({ | |
| type: TriggerResponseTypes.SendNanoContractTxConfirmationResponse, | |
| data: { | |
| accepted: true, | |
| nc: { | |
| caller: address, | |
| method: rpcRequest.params.method, | |
| blueprintId: rpcRequest.params.blueprint_id, | |
| ncId: rpcRequest.params.nc_id, | |
| args: rpcRequest.params.args, | |
| parsedArgs: [], | |
| actions: [], | |
| pushTx: true, | |
| fee: 0n, | |
| }, | |
| }, | |
| }) | |
| .mockResolvedValueOnce({ | |
| type: TriggerResponseTypes.PinRequestResponse, | |
| data: { | |
| accepted: false, | |
| pinCode: '', | |
| }, | |
| }); | |
| await expect(sendNanoContractTx(rpcRequest, wallet, {}, promptHandler)).rejects.toThrow(PromptRejectedError); | |
| expect(mockReleaseUtxos).toHaveBeenCalledTimes(1); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/hathor-rpc-handler/__tests__/rpcMethods/sendNanoContractTx.test.ts`
around lines 423 - 509, The tests in sendNanoContractTx.test.ts currently use
generic rejects.toThrow() for the two rejection cases; update both assertions to
specifically expect a PromptRejectedError to lock the contract (replace await
expect(sendNanoContractTx(...)).rejects.toThrow() with await
expect(...).rejects.toThrow(PromptRejectedError) or equivalent instance check).
Ensure the test file imports PromptRejectedError (or references the exact
exported symbol) and keep the existing mockReleaseUtxos assertions unchanged.
Requires HathorNetwork/hathor-wallet-lib#1050
Motivation
The nano contract methods in rpc-lib causes utxos to be indefinitely locked when user rejects a prompt or an error is thrown.
Acceptance Criteria
Checklist
master, confirm this code is production-ready and can be included in future releases as soon as it gets mergedSummary by CodeRabbit
Release Notes
Bug Fixes
Tests