-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding tests for Crosschain_Base and PP_Connext_Crosschain - DRAFT #694
Draft
leeftk
wants to merge
108
commits into
InverterNetwork:feature/crosschain-paymentProcessor
Choose a base branch
from
leeftk:zuhaibmohd-branch-tests
base: feature/crosschain-paymentProcessor
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,151
−600
Draft
Changes from 1 commit
Commits
Show all changes
108 commits
Select commit
Hold shift + click to select a range
dc03239
add cross-chain module, contracts compile
leeftk b4b0c1f
refactor tempalte module
leeftk 7041fa4
fix compile issues
zzzuhaibmohd 6cd983c
add todo's for implementation contract
leeftk 07e9b81
added notes handoff
leeftk a12749e
fix compile issues before everclear impl
zzzuhaibmohd 793d8a7
add everclear poc impl
zzzuhaibmohd af340d1
add everclear logic impl
zzzuhaibmohd b565de2
remove old templates
leeftk 7da377d
code compile issue solved
zzzuhaibmohd 865e268
fix minor dependency
zzzuhaibmohd 960fcd2
remove file
zzzuhaibmohd fc33b2e
added contract for bridge testing
leeftk 7584abb
add tests for base contracts
leeftk 7f20d42
fix file structure
leeftk 5ff8e1c
uncomment bridge return value and run fmt
leeftk dfdb828
change contract to abstract
leeftk 0fa97c5
add logic to PP_Connext_Crosschain
leeftk c06d6ec
fix inheritance chain
leeftk 802f3b5
remove function impl
leeftk e0608ea
seperate interfaces
leeftk 6909a38
remove unecessary state variables
leeftk 54f1eea
remove unecessary state variables
leeftk 09047da
add unit tests for Connext
zzzuhaibmohd d8f88b9
unit test processPayments & xcall
zzzuhaibmohd 46df46a
fix directory structure
leeftk a5a10bf
unit test chainid test
zzzuhaibmohd 5a78d5f
unit test processpayments test
zzzuhaibmohd ac662d5
unit test paymentId check and test
zzzuhaibmohd 4b74169
unit test chainId and ProcessPayment
zzzuhaibmohd 32b2c05
unit test paymentId check and test
zzzuhaibmohd 567815e
resolve conflicts
zzzuhaibmohd af9df30
add unit test verify bridgeData
zzzuhaibmohd 2c96138
add unit test process payments insufficient balance
zzzuhaibmohd 330cbc2
add feedback comment
leeftk be2fc27
fixed unit tests after rebase
zzzuhaibmohd 6928ec0
add comments and fuzz tests
leeftk ca1b6a8
refrator test file and add unit tests
zzzuhaibmohd 3468df9
verify executionData & add unit tests
zzzuhaibmohd 293db4f
add ttl validation
leeftk e96b3fb
refractor unit tests
zzzuhaibmohd 45816bf
add unit tests CrosschainBase_v1
zzzuhaibmohd f460be3
singlePayment fuzz tests added
zzzuhaibmohd c251997
move balance setup internal function
zzzuhaibmohd 01f5ff8
multiplePayment fuzz tests added
zzzuhaibmohd 8e0b414
camelcase changes for CrossChainBase contract
zzzuhaibmohd 6300df5
add gherking comments
leeftk 084e594
remove unecessary variables and add comments
leeftk 3fff1b7
add more gherkin to each test
leeftk 8fc2cd8
add validation of ttl
leeftk d3d0af3
add getbridgedata and natspec
leeftk 24ba1ce
chore: cleaned code changed event name
leeftk eb14eeb
add templates folder
leeftk 167f624
emit PaymentProcessed via Interface
zzzuhaibmohd 66f5208
chore: use exposed payment processor
zzzuhaibmohd bdb3c65
update gherkin for tests
leeftk ee1e448
add assertions to payment processor test
leeftk 918b836
update gherkin and remove vs code settings
leeftk 3c98fa7
feat: add cancel payments
leeftk 855f6f2
feat:add cancel payment & test
leeftk d64d977
feat:add retry payments & test
leeftk cedbb59
fix:retry payments test
leeftk f1e0e69
fix:format
leeftk a15150f
fix:fmt and standard updates
leeftk c3fbdf8
chore:delete bridging folder
leeftk 197a65a
fix:rebase onto feature
leeftk 90615c2
chore:remove redundant files
leeftk 8f34e00
fix:add standard to test file
leeftk 18aa1f4
chore:remove redundant files
leeftk 102cbd4
fix:update crosschain base format
leeftk b93500c
fix:delete console import
leeftk 9bdbf8a
fix:move files to proper directories
leeftk d6bdba6
fix:remove impl for functions
leeftk bd55516
fix: fix retry failed payments
leeftk ed26634
chore:add comments for clarity
leeftk 81e4275
fix:remove files
leeftk 54e595f
add tests for unhappy paths
leeftk a22d76b
chore:change mapping name
leeftk d986ff1
remove redundant file
leeftk c9a9d9a
chore:rename file
leeftk e17ae52
add base test file
leeftk a3d5311
fix: remove chainid impl
zzzuhaibmohd 6d0378c
chore: add unit tests
zzzuhaibmohd f4164af
chore: add unit tests
zzzuhaibmohd 89184cd
fix: make unit tests generic input
zzzuhaibmohd 3e76524
fix: process payment if else
zzzuhaibmohd 3a753d4
chore: add unit tests for PP_Crosschain_v1
zzzuhaibmohd 7adcca7
fix: add auth checks and retrypayment issue and format unit tests
zzzuhaibmohd 238254c
fix: change test to fuzz tests, fix minor bugs
zzzuhaibmohd e7d0660
fix: code format invertor standard
zzzuhaibmohd 002b3f6
fix: change the _validateTransferRequest msg.sender to client
zzzuhaibmohd 4e5b488
fix: refractor the unit tests
zzzuhaibmohd d400668
chore: add unit tests for client.amountPaid functionality
zzzuhaibmohd a3564ea
fix: track processedIntentId fix
zzzuhaibmohd b4d9f77
feat: add unclaimed amount impl
zzzuhaibmohd 85ca775
fix: format code
zzzuhaibmohd 8a406cd
fix: follow invertor standard
zzzuhaibmohd baf13d7
Merge remote-tracking branch 'upstream/feature/crosschain-paymentProc…
Zitzak 289221e
Merge remote-tracking branch 'upstream/feature/crosschain-paymentProc…
Zitzak 377f888
refactor: Rename ERC20PaymentClientBase_v1 to v2
Zitzak 6c351c8
refactor: adapt implementation and tests to new Payment order struct
Zitzak 27dd77c
feat(IERC20PaymentClientBase_v2): add maxFee and TTL to payment order…
Zitzak e53ebb5
refactor: CrossChainBase_v1 & ICrossChainBase_v1
Zitzak 7dfb039
refactor: PP_Crosschain_v1 & IPP_Crosschain_v1
Zitzak b9e7168
refactor: PP_Connext_Crosschain_v1 & IPP_Connext_Crosschain_v1
Zitzak a3534bb
test: WIP
Zitzak db1ec3e
fix: code refactor & fix unit tests
zzzuhaibmohd 34961e1
fix: code refactor & fix unit tests
zzzuhaibmohd File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
fix:retry payments test
commit cedbb592fe14075965c18b276490ec940d8281a9
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,6 +1,11 @@ | ||||||
// SPDX-License-Identifier: MIT | ||||||
pragma solidity ^0.8.20; | ||||||
|
||||||
import {console} from "forge-std/console.sol"; | ||||||
// External Imports | ||||||
import {IERC20} from "@oz/token/ERC20/IERC20.sol"; | ||||||
import {Module_v1} from "src/modules/base/Module_v1.sol"; | ||||||
|
||||||
// Internal Imports | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
import {CrossChainBase_v1} from | ||||||
"src/modules/paymentProcessor/abstracts/CrossChainBase_v1.sol"; | ||||||
import {ICrossChainBase_v1} from | ||||||
|
@@ -9,15 +14,13 @@ import {IPP_Connext_Crosschain_v1} from | |||||
"src/modules/paymentProcessor/interfaces/IPP_Connext_Crosschain_v1.sol"; | ||||||
import {IERC20PaymentClientBase_v1} from | ||||||
"@lm/interfaces/IERC20PaymentClientBase_v1.sol"; | ||||||
import {IERC20} from "@oz/token/ERC20/IERC20.sol"; | ||||||
import {PP_Crosschain_v1} from | ||||||
"src/modules/paymentProcessor/abstracts/PP_Crosschain_v1.sol"; | ||||||
import {IWETH} from "src/modules/paymentProcessor/interfaces/IWETH.sol"; | ||||||
import {IEverclearSpoke} from | ||||||
"src/modules/paymentProcessor/interfaces/IEverclear.sol"; | ||||||
import {IOrchestrator_v1} from | ||||||
"src/orchestrator/interfaces/IOrchestrator_v1.sol"; | ||||||
import {Module_v1} from "src/modules/base/Module_v1.sol"; | ||||||
|
||||||
/** | ||||||
* @title Connext Cross-chain Payment Processor | ||||||
|
@@ -35,28 +38,35 @@ import {Module_v1} from "src/modules/base/Module_v1.sol"; | |||||
* @custom:security-contact [email protected] | ||||||
* In case of any concerns or findings, please refer to our Security Policy | ||||||
* at security.inverter.network | ||||||
* | ||||||
* @custom:version 1.0.0 | ||||||
* @custom:standard-version 1.0.0 | ||||||
* @author Inverter Network | ||||||
*/ | ||||||
contract PP_Connext_Crosschain_v1 is PP_Crosschain_v1 { | ||||||
// Storage Variables | ||||||
IEverclearSpoke public everClearSpoke; | ||||||
IWETH public weth; | ||||||
|
||||||
error FailedTransfer(); | ||||||
|
||||||
/// @dev Tracks all details for all payment orders of a paymentReceiver for a specific paymentClient. | ||||||
/// paymentClient => paymentReceiver => intentId. | ||||||
/// @dev Tracks all details for all payment orders of a paymentReceiver for a specific paymentClient. | ||||||
/// paymentClient => paymentReceiver => intentId. | ||||||
mapping( | ||||||
address paymentClient => mapping(address recipient => bytes32 intentId) | ||||||
) public intentId; | ||||||
|
||||||
/// @dev Tracks failed transfers that can be retried | ||||||
/// paymentClient => recipient => intentId => amount | ||||||
/// paymentClient => recipient => intentId => amount | ||||||
mapping( | ||||||
address paymentClient | ||||||
=> mapping( | ||||||
address recipient => mapping(bytes32 intentId => uint amount) | ||||||
) | ||||||
) public failedTransfers; | ||||||
|
||||||
// Errors | ||||||
error FailedTransfer(); | ||||||
|
||||||
// External Functions | ||||||
/// @notice Initializes the payment processor module | ||||||
/// @param orchestrator_ The address of the orchestrator contract | ||||||
/// @param metadata Module metadata | ||||||
|
@@ -74,42 +84,6 @@ contract PP_Connext_Crosschain_v1 is PP_Crosschain_v1 { | |||||
weth = IWETH(weth_); | ||||||
} | ||||||
|
||||||
/// @notice Retrieves the bridge data for a specific payment ID | ||||||
/// @param paymentId The unique identifier of the payment | ||||||
/// @return The bridge data associated with the payment (encoded intentId) | ||||||
function getBridgeData(uint paymentId) | ||||||
public | ||||||
view | ||||||
override | ||||||
returns (bytes memory) | ||||||
{ | ||||||
return _bridgeData[paymentId]; | ||||||
} | ||||||
|
||||||
/// @notice Execute the cross-chain bridge transfer | ||||||
/// @dev Override this function to implement specific bridge logic | ||||||
/// @param order The payment order containing all necessary transfer details | ||||||
/// @return bridgeData Arbitrary data returned by the bridge implementation | ||||||
function _executeBridgeTransfer( | ||||||
IERC20PaymentClientBase_v1.PaymentOrder memory order, | ||||||
bytes memory executionData, | ||||||
address client | ||||||
) internal returns (bytes memory) { | ||||||
bytes32 _intentId = createCrossChainIntent(order, executionData); | ||||||
if (_intentId == bytes32(0)) { | ||||||
// Track failed transfer | ||||||
failedTransfers[client][order.recipient][_intentId] = order.amount; | ||||||
emit TransferFailed( | ||||||
client, order.recipient, _intentId, order.amount | ||||||
); | ||||||
|
||||||
// revert Module__PP_Crosschain__MessageDeliveryFailed( | ||||||
// 8453, 8453, executionData | ||||||
// ); | ||||||
} | ||||||
return abi.encode(_intentId); | ||||||
} | ||||||
|
||||||
/// @notice Processes multiple payment orders through the bridge | ||||||
/// @param client The payment client contract interface | ||||||
/// @param executionData Additional data needed for execution (encoded maxFee and TTL) | ||||||
|
@@ -143,51 +117,113 @@ contract PP_Connext_Crosschain_v1 is PP_Crosschain_v1 { | |||||
} | ||||||
} | ||||||
|
||||||
/// @notice Executes a cross-chain transfer through the Connext bridge | ||||||
/// @param order The payment order to be processed | ||||||
/// @param executionData Encoded data containing maxFee and TTL for the transfer | ||||||
/// @return intentId The unique identifier for the cross-chain transfer | ||||||
/// @notice Cancels a pending transfer and returns funds to the client | ||||||
/// @param client The payment client address | ||||||
/// @param recipient The recipient address | ||||||
/// @param pendingIntentId The intentId to cancel | ||||||
function cancelTransfer( | ||||||
address client, | ||||||
address recipient, | ||||||
bytes32 pendingIntentId, | ||||||
IERC20PaymentClientBase_v1.PaymentOrder memory order | ||||||
) external { | ||||||
_validateTransferRequest(client, recipient, pendingIntentId); | ||||||
_cleanupFailedTransfer(client, recipient, pendingIntentId); | ||||||
//Set approval to 0 | ||||||
IERC20(order.paymentToken).approve(address(everClearSpoke), 0); | ||||||
// Just do the transfer inline | ||||||
if (!IERC20(order.paymentToken).transfer(recipient, order.amount)) { | ||||||
revert FailedTransfer(); | ||||||
} | ||||||
|
||||||
emit TransferCancelled(client, recipient, pendingIntentId, order.amount); | ||||||
} | ||||||
|
||||||
function retryFailedTransfer( | ||||||
address client, | ||||||
address recipient, | ||||||
bytes32 pendingIntentId, | ||||||
IERC20PaymentClientBase_v1.PaymentOrder memory order, | ||||||
bytes memory executionData | ||||||
) external { | ||||||
_validateTransferRequest(client, recipient, pendingIntentId); | ||||||
|
||||||
bytes32 newIntentId = createCrossChainIntent(order, executionData); | ||||||
if (newIntentId == bytes32(0)) { | ||||||
revert Module__PP_Crosschain__MessageDeliveryFailed( | ||||||
8453, 8453, executionData | ||||||
); | ||||||
} | ||||||
|
||||||
_cleanupFailedTransfer(client, recipient, pendingIntentId); | ||||||
intentId[client][recipient] = newIntentId; | ||||||
} | ||||||
|
||||||
// Public Functions | ||||||
/// @notice Retrieves the bridge data for a specific payment ID | ||||||
/// @param paymentId The unique identifier of the payment | ||||||
/// @return The bridge data associated with the payment (encoded intentId) | ||||||
function getBridgeData(uint paymentId) | ||||||
public | ||||||
view | ||||||
override | ||||||
returns (bytes memory) | ||||||
{ | ||||||
return _bridgeData[paymentId]; | ||||||
} | ||||||
|
||||||
// Internal Functions | ||||||
/// @notice Execute the cross-chain bridge transfer | ||||||
/// @dev Override this function to implement specific bridge logic | ||||||
/// @param order The payment order containing all necessary transfer details | ||||||
/// @return bridgeData Arbitrary data returned by the bridge implementation | ||||||
function _executeBridgeTransfer( | ||||||
IERC20PaymentClientBase_v1.PaymentOrder memory order, | ||||||
bytes memory executionData, | ||||||
address client | ||||||
) internal returns (bytes memory) { | ||||||
bytes32 _intentId = createCrossChainIntent(order, executionData); | ||||||
|
||||||
if (_intentId == bytes32(0)) { | ||||||
failedTransfers[client][order.recipient][_intentId] = order.amount; | ||||||
intentId[client][order.recipient] = _intentId; | ||||||
emit TransferFailed( | ||||||
client, order.recipient, _intentId, order.amount | ||||||
); | ||||||
} | ||||||
|
||||||
return abi.encode(_intentId); | ||||||
} | ||||||
|
||||||
function createCrossChainIntent( | ||||||
IERC20PaymentClientBase_v1.PaymentOrder memory order, | ||||||
bytes memory executionData | ||||||
) internal returns (bytes32) { | ||||||
_validateOrder(order); | ||||||
|
||||||
if (executionData.length == 0) { | ||||||
revert | ||||||
ICrossChainBase_v1 | ||||||
.Module__CrossChainBase_InvalidExecutionData(); | ||||||
} | ||||||
if (order.amount == 0) { | ||||||
revert ICrossChainBase_v1.Module__CrossChainBase__InvalidAmount(); | ||||||
} | ||||||
if (order.recipient == address(0)) { | ||||||
revert ICrossChainBase_v1.Module__CrossChainBase__InvalidRecipient(); | ||||||
} | ||||||
// Decode the execution data | ||||||
(uint maxFee, uint ttl) = abi.decode(executionData, (uint, uint)); | ||||||
if (ttl == 0) { | ||||||
revert | ||||||
IPP_Connext_Crosschain_v1 | ||||||
.Module__PP_Connext_Crosschain__InvalidTTL(); | ||||||
} | ||||||
|
||||||
// Wrap ETH into WETH to send with the xcall | ||||||
IERC20(order.paymentToken).transferFrom( | ||||||
msg.sender, address(this), order.amount | ||||||
); | ||||||
|
||||||
// This contract approves transfer to EverClearSpoke | ||||||
IERC20(order.paymentToken).approve( | ||||||
address(everClearSpoke), order.amount | ||||||
); | ||||||
|
||||||
// Create destinations array with the target chain | ||||||
uint32[] memory destinations = new uint32[](1); | ||||||
destinations[0] = 8453; | ||||||
// @note -> hardcode for now -> order.destinationChainId when the | ||||||
// new struct is created for us | ||||||
|
||||||
// Call newIntent on the EverClearSpoke contract | ||||||
(bytes32 intentId) = everClearSpoke.newIntent( | ||||||
return everClearSpoke.newIntent( | ||||||
destinations, | ||||||
order.recipient, | ||||||
order.paymentToken, | ||||||
|
@@ -197,87 +233,48 @@ contract PP_Connext_Crosschain_v1 is PP_Crosschain_v1 { | |||||
uint48(ttl), | ||||||
"" | ||||||
); | ||||||
return intentId; | ||||||
} | ||||||
/// @notice Cancels a pending transfer and returns funds to the client | ||||||
/// @param client The payment client address | ||||||
/// @param recipient The recipient address | ||||||
/// @param pendingIntentId The intentId to cancel | ||||||
|
||||||
function cancelTransfer( | ||||||
/// @dev Common validation for transfer-related operations | ||||||
function _validateTransferRequest( | ||||||
address client, | ||||||
address recipient, | ||||||
bytes32 pendingIntentId, | ||||||
IERC20PaymentClientBase_v1.PaymentOrder memory order | ||||||
) external { | ||||||
// Verify the caller is the intended recipient of the transfer | ||||||
bytes32 pendingIntentId | ||||||
) internal view returns (uint) { | ||||||
if (msg.sender != client) { | ||||||
revert Module__InvalidAddress(); | ||||||
} | ||||||
|
||||||
// Verify the transfer exists and matches the provided intentId | ||||||
if (intentId[client][recipient] != pendingIntentId) { | ||||||
revert Module__PP_Crosschain__InvalidIntentId(); | ||||||
} | ||||||
|
||||||
// Verify the order details by checking if there's a failed transfer record | ||||||
uint failedAmount = failedTransfers[client][recipient][pendingIntentId]; | ||||||
|
||||||
if (failedAmount == 0) { | ||||||
//@audit - change this to something | ||||||
revert Module__CrossChainBase__InvalidAmount(); | ||||||
} | ||||||
|
||||||
// Clean up storage | ||||||
delete intentId[client][recipient]; | ||||||
delete failedTransfers[client][recipient][pendingIntentId]; | ||||||
|
||||||
// // Update client's payment records | ||||||
// IERC20PaymentClientBase_v1(client).amountPaid( | ||||||
// order.paymentToken, order.amount | ||||||
// ); | ||||||
|
||||||
// Transfer tokens to the recipient since they are cancelling their own failed transfer | ||||||
if (!IERC20(order.paymentToken).transfer(recipient, order.amount)) { | ||||||
revert FailedTransfer(); | ||||||
} | ||||||
|
||||||
emit TransferCancelled(client, recipient, pendingIntentId, order.amount); | ||||||
return failedAmount; | ||||||
} | ||||||
|
||||||
function retryFailedTransfer( | ||||||
/// @dev Helper function to clean up storage after handling a failed transfer | ||||||
function _cleanupFailedTransfer( | ||||||
address client, | ||||||
address recipient, | ||||||
bytes32 pendingIntentId, | ||||||
IERC20PaymentClientBase_v1.PaymentOrder memory order, | ||||||
bytes memory executionData | ||||||
) external { | ||||||
//verify the caller is the intended recipient of the transfer | ||||||
if (msg.sender != client) { | ||||||
revert Module__InvalidAddress(); | ||||||
} | ||||||
|
||||||
//verify the transfer exists and matches the provided intentId | ||||||
if (intentId[client][recipient] != pendingIntentId) { | ||||||
revert Module__PP_Crosschain__InvalidIntentId(); | ||||||
} | ||||||
bytes32 pendingIntentId | ||||||
) internal { | ||||||
delete intentId[client][recipient]; | ||||||
delete failedTransfers[client][recipient][pendingIntentId]; | ||||||
} | ||||||
|
||||||
//verify the order details by checking if there's a failed transfer record | ||||||
uint failedAmount = failedTransfers[client][recipient][pendingIntentId]; | ||||||
if (failedAmount == 0) { | ||||||
revert Module__CrossChainBase__InvalidAmount(); | ||||||
function _validateOrder( | ||||||
IERC20PaymentClientBase_v1.PaymentOrder memory order | ||||||
) internal pure { | ||||||
if (order.amount == 0) { | ||||||
revert ICrossChainBase_v1.Module__CrossChainBase__InvalidAmount(); | ||||||
} | ||||||
//create a new intent with the updated execution data | ||||||
bytes32 newIntentId = createCrossChainIntent(order, executionData); | ||||||
if (newIntentId == bytes32(0)) { | ||||||
revert Module__PP_Crosschain__MessageDeliveryFailed( | ||||||
8453, 8453, executionData | ||||||
); | ||||||
if (order.recipient == address(0)) { | ||||||
revert ICrossChainBase_v1.Module__CrossChainBase__InvalidRecipient(); | ||||||
} | ||||||
// delete the failed transfer record | ||||||
delete failedTransfers[client][recipient][pendingIntentId]; | ||||||
|
||||||
// update the intentId with the new intentId | ||||||
intentId[client][recipient] = newIntentId; | ||||||
} | ||||||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.