Merge pull request #714 from 0xProject/feature/contracts/cancelOrdersUpToSender
Make cancelOrdersUpTo compatible with sender abstraction
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
"ERC20Proxy",
|
||||
"ERC721Proxy",
|
||||
"Exchange",
|
||||
"ExchangeWrapper",
|
||||
"MixinAuthorizable",
|
||||
"MultiSigWallet",
|
||||
"MultiSigWalletWithTimeLock",
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
"test:circleci": "yarn test"
|
||||
},
|
||||
"config": {
|
||||
"abis": "../migrations/artifacts/2.0.0/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|ERC20Proxy|ERC721Proxy|Exchange|MixinAuthorizable|MultiSigWallet|MultiSigWalletWithTimeLock|TestAssetDataDecoders|TestAssetProxyDispatcher|TestLibBytes|TestLibMem|TestLibs|TestSignatureValidator|TokenRegistry|Whitelist|WETH9|ZRXToken).json"
|
||||
"abis":
|
||||
"../migrations/artifacts/2.0.0/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|ERC20Proxy|ERC721Proxy|Exchange|ExchangeWrapper|MixinAuthorizable|MultiSigWallet|MultiSigWalletWithTimeLock|TestAssetDataDecoders|TestAssetProxyDispatcher|TestLibBytes|TestLibMem|TestLibs|TestSignatureValidator|TokenRegistry|Whitelist|WETH9|ZRXToken).json"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -44,32 +44,36 @@ contract MixinExchangeCore is
|
||||
// Mapping of orderHash => cancelled
|
||||
mapping (bytes32 => bool) public cancelled;
|
||||
|
||||
// Mapping of makerAddress => lowest salt an order can have in order to be fillable
|
||||
// Orders with a salt less than their maker's epoch are considered cancelled
|
||||
mapping (address => uint256) public makerEpoch;
|
||||
// Mapping of makerAddress => senderAddress => lowest salt an order can have in order to be fillable
|
||||
// Orders with specified senderAddress and with a salt less than their epoch to are considered cancelled
|
||||
mapping (address => mapping (address => uint256)) public orderEpoch;
|
||||
|
||||
////// Core exchange functions //////
|
||||
|
||||
/// @dev Cancels all orders created by sender with a salt less than or equal to the specified salt value.
|
||||
/// @param salt Orders created with a salt less or equal to this value will be cancelled.
|
||||
function cancelOrdersUpTo(uint256 salt)
|
||||
/// @dev Cancels all orders created by makerAddress with a salt less than or equal to the targetOrderEpoch
|
||||
/// and senderAddress equal to msg.sender (or null address if msg.sender == makerAddress).
|
||||
/// @param targetOrderEpoch Orders created with a salt less or equal to this value will be cancelled.
|
||||
function cancelOrdersUpTo(uint256 targetOrderEpoch)
|
||||
external
|
||||
{
|
||||
address makerAddress = getCurrentContextAddress();
|
||||
// If this function is called via `executeTransaction`, we only update the orderEpoch for the makerAddress/msg.sender combination.
|
||||
// This allows external filter contracts to add rules to how orders are cancelled via this function.
|
||||
address senderAddress = makerAddress == msg.sender ? address(0) : msg.sender;
|
||||
|
||||
// makerEpoch is initialized to 0, so to cancelUpTo we need salt + 1
|
||||
uint256 newMakerEpoch = salt + 1;
|
||||
uint256 oldMakerEpoch = makerEpoch[makerAddress];
|
||||
// orderEpoch is initialized to 0, so to cancelUpTo we need salt + 1
|
||||
uint256 newOrderEpoch = targetOrderEpoch + 1;
|
||||
uint256 oldOrderEpoch = orderEpoch[makerAddress][senderAddress];
|
||||
|
||||
// Ensure makerEpoch is monotonically increasing
|
||||
// Ensure orderEpoch is monotonically increasing
|
||||
require(
|
||||
newMakerEpoch > oldMakerEpoch,
|
||||
INVALID_NEW_MAKER_EPOCH
|
||||
newOrderEpoch > oldOrderEpoch,
|
||||
INVALID_NEW_ORDER_EPOCH
|
||||
);
|
||||
|
||||
// Update makerEpoch
|
||||
makerEpoch[makerAddress] = newMakerEpoch;
|
||||
emit CancelUpTo(makerAddress, newMakerEpoch);
|
||||
// Update orderEpoch
|
||||
orderEpoch[makerAddress][senderAddress] = newOrderEpoch;
|
||||
emit CancelUpTo(makerAddress, senderAddress, newOrderEpoch);
|
||||
}
|
||||
|
||||
/// @dev Fills the input order.
|
||||
@@ -180,7 +184,7 @@ contract MixinExchangeCore is
|
||||
orderInfo.orderStatus = uint8(OrderStatus.CANCELLED);
|
||||
return orderInfo;
|
||||
}
|
||||
if (makerEpoch[order.makerAddress] > order.salt) {
|
||||
if (orderEpoch[order.makerAddress][order.senderAddress] > order.salt) {
|
||||
orderInfo.orderStatus = uint8(OrderStatus.CANCELLED);
|
||||
return orderInfo;
|
||||
}
|
||||
|
||||
@@ -24,9 +24,10 @@ import "../libs/LibFillResults.sol";
|
||||
|
||||
contract IExchangeCore {
|
||||
|
||||
/// @dev Cancels all orders reated by sender with a salt less than or equal to the specified salt value.
|
||||
/// @param salt Orders created with a salt less or equal to this value will be cancelled.
|
||||
function cancelOrdersUpTo(uint256 salt)
|
||||
/// @dev Cancels all orders created by makerAddress with a salt less than or equal to the targetOrderEpoch
|
||||
/// and senderAddress equal to msg.sender (or null address if msg.sender == makerAddress).
|
||||
/// @param targetOrderEpoch Orders created with a salt less or equal to this value will be cancelled.
|
||||
function cancelOrdersUpTo(uint256 targetOrderEpoch)
|
||||
external;
|
||||
|
||||
/// @dev Fills the input order.
|
||||
|
||||
@@ -36,7 +36,7 @@ contract LibExchangeErrors {
|
||||
string constant SIGNATURE_UNSUPPORTED = "SIGNATURE_UNSUPPORTED"; // Signature type unsupported.
|
||||
|
||||
/// cancelOrdersUptTo errors ///
|
||||
string constant INVALID_NEW_MAKER_EPOCH = "INVALID_NEW_MAKER_EPOCH"; // Specified salt must be greater than or equal to existing makerEpoch.
|
||||
string constant INVALID_NEW_ORDER_EPOCH = "INVALID_NEW_ORDER_EPOCH"; // Specified salt must be greater than or equal to existing orderEpoch.
|
||||
|
||||
/// fillOrKillOrder errors ///
|
||||
string constant COMPLETE_FILL_FAILED = "COMPLETE_FILL_FAILED"; // Desired takerAssetFillAmount could not be completely filled.
|
||||
@@ -55,7 +55,7 @@ contract LibExchangeErrors {
|
||||
string constant ASSET_PROXY_ID_MISMATCH = "ASSET_PROXY_ID_MISMATCH"; // newAssetProxyId does not match given assetProxyId.
|
||||
|
||||
/// Length validation errors ///
|
||||
string constant LENGTH_GREATER_THAN_0_REQUIRED = "LENGTH_GREATER_THAN_0_REQUIRED"; // Byte array must have a length greater than 0.
|
||||
string constant LENGTH_GREATER_THAN_0_REQUIRED = "LENGTH_GREATER_THAN_0_REQUIRED"; // Byte array must have a length greater than 0.
|
||||
string constant LENGTH_0_REQUIRED = "LENGTH_1_REQUIRED"; // Byte array must have a length of 1.
|
||||
string constant LENGTH_65_REQUIRED = "LENGTH_66_REQUIRED"; // Byte array must have a length of 66.
|
||||
}
|
||||
|
||||
@@ -52,7 +52,8 @@ contract MExchangeCore is
|
||||
// CancelUpTo event is emitted whenever `cancelOrdersUpTo` is executed succesfully.
|
||||
event CancelUpTo(
|
||||
address indexed makerAddress,
|
||||
uint256 makerEpoch
|
||||
address indexed senderAddress,
|
||||
uint256 orderEpoch
|
||||
);
|
||||
|
||||
/// @dev Updates state with results of a fill order.
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
|
||||
Copyright 2018 ZeroEx Intl.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
*/
|
||||
|
||||
pragma solidity ^0.4.24;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
import "../../protocol/Exchange/interfaces/IExchange.sol";
|
||||
import "../../protocol/Exchange/libs/LibOrder.sol";
|
||||
|
||||
contract ExchangeWrapper {
|
||||
|
||||
// Exchange contract.
|
||||
IExchange EXCHANGE;
|
||||
|
||||
constructor (address _exchange)
|
||||
public
|
||||
{
|
||||
EXCHANGE = IExchange(_exchange);
|
||||
}
|
||||
|
||||
/// @dev Fills an order using `msg.sender` as the taker.
|
||||
/// @param order Order struct containing order specifications.
|
||||
/// @param takerAssetFillAmount Desired amount of takerAsset to sell.
|
||||
/// @param salt Arbitrary value to gaurantee uniqueness of 0x transaction hash.
|
||||
/// @param orderSignature Proof that order has been created by maker.
|
||||
/// @param takerSignature Proof that taker wishes to call this function with given params.
|
||||
function fillOrder(
|
||||
LibOrder.Order memory order,
|
||||
uint256 takerAssetFillAmount,
|
||||
uint256 salt,
|
||||
bytes memory orderSignature,
|
||||
bytes memory takerSignature
|
||||
)
|
||||
public
|
||||
{
|
||||
address takerAddress = msg.sender;
|
||||
|
||||
// Encode arguments into byte array.
|
||||
bytes memory data = abi.encodeWithSelector(
|
||||
EXCHANGE.fillOrder.selector,
|
||||
order,
|
||||
takerAssetFillAmount,
|
||||
orderSignature
|
||||
);
|
||||
|
||||
// Call `fillOrder` via `executeTransaction`.
|
||||
EXCHANGE.executeTransaction(
|
||||
salt,
|
||||
takerAddress,
|
||||
data,
|
||||
takerSignature
|
||||
);
|
||||
}
|
||||
|
||||
/// @dev Cancels all orders created by sender with a salt less than or equal to the targetOrderEpoch
|
||||
/// and senderAddress equal to this contract.
|
||||
/// @param targetOrderEpoch Orders created with a salt less or equal to this value will be cancelled.
|
||||
/// @param salt Arbitrary value to gaurantee uniqueness of 0x transaction hash.
|
||||
/// @param makerSignature Proof that maker wishes to call this function with given params.
|
||||
function cancelOrdersUpTo(
|
||||
uint256 targetOrderEpoch,
|
||||
uint256 salt,
|
||||
bytes makerSignature
|
||||
)
|
||||
external
|
||||
{
|
||||
address makerAddress = msg.sender;
|
||||
|
||||
// Encode arguments into byte array.
|
||||
bytes memory data = abi.encodeWithSelector(
|
||||
EXCHANGE.cancelOrdersUpTo.selector,
|
||||
targetOrderEpoch
|
||||
);
|
||||
|
||||
// Call `cancelOrdersUpTo` via `executeTransaction`.
|
||||
EXCHANGE.executeTransaction(
|
||||
salt,
|
||||
makerAddress,
|
||||
data,
|
||||
makerSignature
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
*/
|
||||
|
||||
pragma solidity ^0.4.23;
|
||||
pragma solidity ^0.4.24;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
import "../../protocol/Exchange/interfaces/IExchange.sol";
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as DummyERC721Token from '../artifacts/DummyERC721Token.json';
|
||||
import * as ERC20Proxy from '../artifacts/ERC20Proxy.json';
|
||||
import * as ERC721Proxy from '../artifacts/ERC721Proxy.json';
|
||||
import * as Exchange from '../artifacts/Exchange.json';
|
||||
import * as ExchangeWrapper from '../artifacts/ExchangeWrapper.json';
|
||||
import * as MixinAuthorizable from '../artifacts/MixinAuthorizable.json';
|
||||
import * as MultiSigWallet from '../artifacts/MultiSigWallet.json';
|
||||
import * as MultiSigWalletWithTimeLock from '../artifacts/MultiSigWalletWithTimeLock.json';
|
||||
@@ -29,6 +30,7 @@ export const artifacts = {
|
||||
ERC20Proxy: (ERC20Proxy as any) as ContractArtifact,
|
||||
ERC721Proxy: (ERC721Proxy as any) as ContractArtifact,
|
||||
Exchange: (Exchange as any) as ContractArtifact,
|
||||
ExchangeWrapper: (ExchangeWrapper as any) as ContractArtifact,
|
||||
EtherToken: (EtherToken as any) as ContractArtifact,
|
||||
MixinAuthorizable: (MixinAuthorizable as any) as ContractArtifact,
|
||||
MultiSigWallet: (MultiSigWallet as any) as ContractArtifact,
|
||||
|
||||
@@ -620,30 +620,30 @@ describe('Exchange core', () => {
|
||||
});
|
||||
|
||||
describe('cancelOrdersUpTo', () => {
|
||||
it('should fail to set makerEpoch less than current makerEpoch', async () => {
|
||||
const makerEpoch = new BigNumber(1);
|
||||
await exchangeWrapper.cancelOrdersUpToAsync(makerEpoch, makerAddress);
|
||||
const lesserMakerEpoch = new BigNumber(0);
|
||||
it('should fail to set orderEpoch less than current orderEpoch', async () => {
|
||||
const orderEpoch = new BigNumber(1);
|
||||
await exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress);
|
||||
const lesserOrderEpoch = new BigNumber(0);
|
||||
return expectRevertOrAlwaysFailingTransactionAsync(
|
||||
exchangeWrapper.cancelOrdersUpToAsync(lesserMakerEpoch, makerAddress),
|
||||
exchangeWrapper.cancelOrdersUpToAsync(lesserOrderEpoch, makerAddress),
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail to set makerEpoch equal to existing makerEpoch', async () => {
|
||||
const makerEpoch = new BigNumber(1);
|
||||
await exchangeWrapper.cancelOrdersUpToAsync(makerEpoch, makerAddress);
|
||||
it('should fail to set orderEpoch equal to existing orderEpoch', async () => {
|
||||
const orderEpoch = new BigNumber(1);
|
||||
await exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress);
|
||||
return expectRevertOrAlwaysFailingTransactionAsync(
|
||||
exchangeWrapper.cancelOrdersUpToAsync(makerEpoch, makerAddress),
|
||||
exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress),
|
||||
);
|
||||
});
|
||||
|
||||
it('should cancel only orders with a makerEpoch less than existing makerEpoch', async () => {
|
||||
// Cancel all transactions with a makerEpoch less than 1
|
||||
const makerEpoch = new BigNumber(1);
|
||||
await exchangeWrapper.cancelOrdersUpToAsync(makerEpoch, makerAddress);
|
||||
it('should cancel only orders with a orderEpoch less than existing orderEpoch', async () => {
|
||||
// Cancel all transactions with a orderEpoch less than 1
|
||||
const orderEpoch = new BigNumber(1);
|
||||
await exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress);
|
||||
|
||||
// Create 3 orders with makerEpoch values: 0,1,2,3
|
||||
// Since we cancelled with makerEpoch=1, orders with makerEpoch<=1 will not be processed
|
||||
// Create 3 orders with orderEpoch values: 0,1,2,3
|
||||
// Since we cancelled with orderEpoch=1, orders with orderEpoch<=1 will not be processed
|
||||
erc20Balances = await erc20Wrapper.getBalancesAsync();
|
||||
const signedOrders = [
|
||||
orderFactory.newSignedOrder({
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as chai from 'chai';
|
||||
import { DummyERC20TokenContract } from '../../src/generated_contract_wrappers/dummy_e_r_c20_token';
|
||||
import { ERC20ProxyContract } from '../../src/generated_contract_wrappers/e_r_c20_proxy';
|
||||
import { ExchangeContract } from '../../src/generated_contract_wrappers/exchange';
|
||||
import { ExchangeWrapperContract } from '../../src/generated_contract_wrappers/exchange_wrapper';
|
||||
import { WhitelistContract } from '../../src/generated_contract_wrappers/whitelist';
|
||||
import { artifacts } from '../../src/utils/artifacts';
|
||||
import { expectRevertOrAlwaysFailingTransactionAsync } from '../../src/utils/assertions';
|
||||
@@ -198,6 +199,117 @@ describe('Exchange transactions', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelOrdersUpTo', () => {
|
||||
let exchangeWrapperContract: ExchangeWrapperContract;
|
||||
|
||||
before(async () => {
|
||||
exchangeWrapperContract = await ExchangeWrapperContract.deployFrom0xArtifactAsync(
|
||||
artifacts.ExchangeWrapper,
|
||||
provider,
|
||||
txDefaults,
|
||||
exchange.address,
|
||||
);
|
||||
});
|
||||
|
||||
it("should cancel an order if called from the order's sender", async () => {
|
||||
const orderSalt = new BigNumber(0);
|
||||
signedOrder = orderFactory.newSignedOrder({
|
||||
senderAddress: exchangeWrapperContract.address,
|
||||
salt: orderSalt,
|
||||
});
|
||||
const targetOrderEpoch = orderSalt.add(1);
|
||||
const cancelData = exchange.cancelOrdersUpTo.getABIEncodedTransactionData(targetOrderEpoch);
|
||||
const signedCancelTx = makerTransactionFactory.newSignedTransaction(cancelData);
|
||||
await exchangeWrapperContract.cancelOrdersUpTo.sendTransactionAsync(
|
||||
targetOrderEpoch,
|
||||
signedCancelTx.salt,
|
||||
signedCancelTx.signature,
|
||||
{
|
||||
from: makerAddress,
|
||||
},
|
||||
);
|
||||
|
||||
const takerAssetFillAmount = signedOrder.takerAssetAmount;
|
||||
orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder);
|
||||
const fillData = exchange.fillOrder.getABIEncodedTransactionData(
|
||||
orderWithoutExchangeAddress,
|
||||
takerAssetFillAmount,
|
||||
signedOrder.signature,
|
||||
);
|
||||
const signedFillTx = takerTransactionFactory.newSignedTransaction(fillData);
|
||||
return expectRevertOrAlwaysFailingTransactionAsync(
|
||||
exchangeWrapperContract.fillOrder.sendTransactionAsync(
|
||||
orderWithoutExchangeAddress,
|
||||
takerAssetFillAmount,
|
||||
signedFillTx.salt,
|
||||
signedOrder.signature,
|
||||
signedFillTx.signature,
|
||||
{ from: takerAddress },
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not cancel an order if not called from the order's sender", async () => {
|
||||
const orderSalt = new BigNumber(0);
|
||||
signedOrder = orderFactory.newSignedOrder({
|
||||
senderAddress: exchangeWrapperContract.address,
|
||||
salt: orderSalt,
|
||||
});
|
||||
const targetOrderEpoch = orderSalt.add(1);
|
||||
await exchangeWrapper.cancelOrdersUpToAsync(targetOrderEpoch, makerAddress);
|
||||
|
||||
erc20Balances = await erc20Wrapper.getBalancesAsync();
|
||||
const takerAssetFillAmount = signedOrder.takerAssetAmount;
|
||||
orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder);
|
||||
const data = exchange.fillOrder.getABIEncodedTransactionData(
|
||||
orderWithoutExchangeAddress,
|
||||
takerAssetFillAmount,
|
||||
signedOrder.signature,
|
||||
);
|
||||
signedTx = takerTransactionFactory.newSignedTransaction(data);
|
||||
await exchangeWrapperContract.fillOrder.sendTransactionAsync(
|
||||
orderWithoutExchangeAddress,
|
||||
takerAssetFillAmount,
|
||||
signedTx.salt,
|
||||
signedOrder.signature,
|
||||
signedTx.signature,
|
||||
{ from: takerAddress },
|
||||
);
|
||||
|
||||
const newBalances = await erc20Wrapper.getBalancesAsync();
|
||||
const makerAssetFillAmount = takerAssetFillAmount
|
||||
.times(signedOrder.makerAssetAmount)
|
||||
.dividedToIntegerBy(signedOrder.takerAssetAmount);
|
||||
const makerFeePaid = signedOrder.makerFee
|
||||
.times(makerAssetFillAmount)
|
||||
.dividedToIntegerBy(signedOrder.makerAssetAmount);
|
||||
const takerFeePaid = signedOrder.takerFee
|
||||
.times(makerAssetFillAmount)
|
||||
.dividedToIntegerBy(signedOrder.makerAssetAmount);
|
||||
expect(newBalances[makerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal(
|
||||
erc20Balances[makerAddress][defaultMakerTokenAddress].minus(makerAssetFillAmount),
|
||||
);
|
||||
expect(newBalances[makerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal(
|
||||
erc20Balances[makerAddress][defaultTakerTokenAddress].add(takerAssetFillAmount),
|
||||
);
|
||||
expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
|
||||
erc20Balances[makerAddress][zrxToken.address].minus(makerFeePaid),
|
||||
);
|
||||
expect(newBalances[takerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal(
|
||||
erc20Balances[takerAddress][defaultTakerTokenAddress].minus(takerAssetFillAmount),
|
||||
);
|
||||
expect(newBalances[takerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal(
|
||||
erc20Balances[takerAddress][defaultMakerTokenAddress].add(makerAssetFillAmount),
|
||||
);
|
||||
expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
|
||||
erc20Balances[takerAddress][zrxToken.address].minus(takerFeePaid),
|
||||
);
|
||||
expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
|
||||
erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Whitelist', () => {
|
||||
|
||||
150
packages/migrations/artifacts/2.0.0/Exchange.json
vendored
150
packages/migrations/artifacts/2.0.0/Exchange.json
vendored
File diff suppressed because one or more lines are too long
206
packages/migrations/artifacts/2.0.0/ExchangeWrapper.json
vendored
Normal file
206
packages/migrations/artifacts/2.0.0/ExchangeWrapper.json
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user