Compare commits

...

15 Commits

Author SHA1 Message Date
Tommy Verrall a6a661b4c5 change location 2022-03-03 16:59:02 +00:00
Tommy Verrall fad6381e27 continue rework 2022-03-03 16:54:35 +00:00
Tommy Verrall 67af1ea504 add base mocks for nymd client 2022-03-03 11:47:30 +00:00
Tommy Verrall 043c4c8d86 update a few things - will start to mock shortly. 2022-03-03 10:28:26 +00:00
Tommy Verrall c40375a6b0 merge develop 2022-03-02 10:55:28 +00:00
Tommy Verrall 459506c03e merge develop 2022-02-03 09:39:26 +00:00
Tommy Verrall e0fb0dba7f remove cast 2022-01-19 17:07:26 +00:00
Tommy Verrall 02cf16ea8c add example 2022-01-19 16:52:02 +00:00
Tommy Verrall f8171f3beb update integration test for when run in gh actions
- removing and cleaning
- next step is to start mocking out a few of the other basic interactions with the client
2022-01-19 16:49:46 +00:00
Tommy Verrall 7d39996f7e commit two new methods 2022-01-19 12:28:12 +00:00
Tommy Verrall 0c3e40ce5b merge develop 2022-01-19 12:25:57 +00:00
Tommy Verrall f3ea81c97d Add .env example.
added .env.example
2022-01-14 16:34:09 +00:00
Tommy Verrall aadeac332e adding some actions
- this is just for debugging purposes currently
2022-01-14 16:29:46 +00:00
Tommy Verrall 77140342d9 merge develop 2022-01-14 15:46:09 +00:00
Tommy Verrall f95e9a7d37 Implement base line for tests on the validator ts client
- This will need to be separated and configured accordingly 
- This was a quick spin up, using jest as a library to implement some coverage
- Further things to be refined - mocks, more coverage, better configuration, clean up methods, improve env vars
2022-01-06 10:01:08 +00:00
17 changed files with 4305 additions and 2988 deletions
+25
View File
@@ -0,0 +1,25 @@
# CLIENT INIT
NYMD_URL=https://sandbox-validator.nymtech.net
VALIDATOR_API=https://sandbox-validator.nymtech.net/api
MIXNET_CONTRACT=nymt1ghd753shjuwexxywmgs4xz7x2q732vcnstz02j
VESTING_CONTRACT=nymt1nc5tatafv6eyq7llkr2gv50ff9e22mnfp9pc5s
CURRENCY_PREFIX=nymt
CHAIN_ID=nym-sandbox
# USER DETAILS
USER_MNEMONIC=
USER_WALLET_ADDRESS=
# MIXNODE DETAILS
MIXNODE_IDENTITY=
MIXNODE_SPHINX_KEY=
MIXNODE_SIGNATURE=
MIXNODE_HOST="1.1.1.1"
MIXNODE_VERSION="0.12.1"
# GATEWAY DETAILS
GATEWAY_IDENTITY=
GATEWAY_SPHINX=
GATEAWAY_LOCATION=
GATEWAY_HOST="1.1.1.1"
GATEWAY_VERSION="0.12.1"
+6 -12
View File
@@ -3,26 +3,20 @@ Nym Validator Client
A TypeScript client for interacting with CosmWasm smart contracts in Nym validators.
Running examples
-----------------
With the code checked out, `cd examples`. This folder contains runnable example code that will set up a blockchain and allow you to interact with it through the client.
Running tests
-------------
The tests will be separated into three categories: unit, integration and mock.
Currently the command to run all tests:
```
npm test
```
You can also trigger test execution with a test watcher. I don't have the centuries of life left to me that are needed to fight through the arcana of wiring up a working TypeScript mocha triggered execution setup, so for now my Cargo-based hack is:
The tests require `.env.example` being renamed to `.env`. The variables and their values for these tests are currently pointing to the `nym-sandbox` environment.
```
cargo watch -s "cd clients/validator && npm test"
```
It's ugly but works fine if you have Cargo installed. TypeScript setup help happily accepted here.
`Tests are still in development` - the test libary is `jest` and the test script will execute currently with: `--coverage --verbosity false`
Generating Documentation
------------------------
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
setupFiles: ["dotenv/config"],
testTimeout: 20000
};
+8 -14
View File
@@ -6,9 +6,7 @@
"main": "./dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"test": "ts-mocha tests/**/*.test.ts",
"coverage": "nyc npm test",
"test": "jest --verbose false",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"docs": "typedoc --out docs src/index.ts"
@@ -21,27 +19,21 @@
],
"license": "Apache-2.0",
"devDependencies": {
"@types/chai": "^4.2.15",
"@types/expect": "^24.3.0",
"@types/mocha": "^8.2.1",
"@types/jest": "27.4.0",
"@typescript-eslint/eslint-plugin": "^5.7.0",
"@typescript-eslint/parser": "^5.7.0",
"chai": "^4.2.0",
"eslint": "^7.18.0",
"eslint-config-airbnb": "^19.0.2",
"eslint-config-airbnb-typescript": "^16.1.0",
"eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-root-import": "^1.0.4",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-mocha": "^10.0.3",
"eslint-plugin-prettier": "^4.0.0",
"mocha": "^8.2.1",
"moq.ts": "^7.2.0",
"nyc": "^15.1.0",
"jest": "^27.4.5",
"prettier": "^2.5.1",
"ts-mocha": "^8.0.0",
"ts-jest": "^27.1.2",
"typedoc": "^0.20.27",
"typescript": "^4.1.3"
"typescript": "^4.5.4"
},
"dependencies": {
"@cosmjs/cosmwasm-stargate": "^0.27.0-rc2",
@@ -51,6 +43,8 @@
"@cosmjs/stargate": "^0.27.0-rc2",
"@cosmjs/tendermint-rpc": "^0.27.0-rc2",
"axios": "^0.21.1",
"cosmjs-types": "^0.4.0"
"cosmjs-types": "^0.4.0",
"dotenv": "^10.0.0",
"moq.ts": "^7.3.4"
}
}
+12 -1
View File
@@ -1,8 +1,19 @@
import axios from 'axios';
import { GasPrice } from '@cosmjs/stargate';
const mainnetPrefix = 'n';
const mainnetDenom = 'nym';
export function nymGasPrice(prefix: string): GasPrice {
return GasPrice.fromString(`0.025u${prefix}`); // TODO: ideally this ugly conversion shouldn't be hardcoded here.
if (typeof prefix === 'string') {
if (prefix === mainnetPrefix) {
prefix = mainnetDenom;
}
return GasPrice.fromString(`0.025u${prefix}`); // TODO: ideally this ugly conversion shouldn't be hardcoded here.
}
else {
throw new Error(`${prefix} is not of type string`);
}
}
export const downloadWasm = async (url: string): Promise<Uint8Array> => {
@@ -0,0 +1,201 @@
import validator from "../../src/index";
import { ExecuteResult } from "@cosmjs/cosmwasm-stargate";
import { config } from "../test-utils/config";
import {buildCoin, buildWallet, profitPercentage} from "../test-utils/utils"
import {
Gateway,
GatewayOwnershipResponse,
MixNode,
MixOwnershipResponse,
} from "../../src/types";
let response: ExecuteResult;
let validatorClient: validator;
let ownsMixNode: MixOwnershipResponse;
let ownsGateway: GatewayOwnershipResponse;
beforeEach(async () => {
validatorClient = await validator.connect(
config.USER_MNEMONIC,
config.NYMD_URL,
config.VALIDATOR_API,
config.NETWORK_BECH,
config.MIXNET_CONTRACT,
config.VESTING_CONTRACT
);
});
describe("long running e2e tests", () => {
test.skip("token transfer", async () => {
try {
//make sure there's enough balance in the wallet
let coin = buildCoin("50000", "nymt");
let userAddress = await buildWallet();
let send = await validatorClient.send(
userAddress,
Array(coin),
"auto",
"send-tokens"
);
let jsonParse = JSON.parse(send.rawLog as string);
//check successful network broadcast - via events
//1 - get key attributes values for sender an assert them
//2 - get key attributes for receiver assert they match
//3 - transaction hash present in response
// { array of events -> attribute -> event information }
expect(jsonParse[0].events[1].attributes[1].value).toStrictEqual(
config.USER_WALLET_ADDRESS
);
expect(jsonParse[0].events[1].attributes[0].value).toStrictEqual(
userAddress
);
expect(jsonParse[0].events[1].type).toStrictEqual(
"transfer"
);
expect(send.transactionHash).toStrictEqual(expect.any(String));
} catch (error) {
throw error;
}
});
test.skip("update mixnode profit percentage", async () => {
const nodeIdentity = config.MIXNODE_IDENTITY;
const profitPercent = profitPercentage();
try {
//use auto fees - simulated gas
response = await validatorClient.updateMixnodeConfig(nodeIdentity, 'auto', profitPercent);
}
catch (error) {
throw error;
}
try {
ownsMixNode = await validatorClient.client.ownsMixNode(config.MIXNET_CONTRACT, config.USER_WALLET_ADDRESS);
}
catch (error) {
throw error;
}
expect(ownsMixNode.mixnode?.mix_node.profit_margin_percent).toStrictEqual(profitPercent);
});
test.skip("unbond and bond mixnode", async () => {
try {
await validatorClient.unbondMixNode("auto", "unbond-mixnode");
}
catch (error) {
throw error;
}
const profitPercent = profitPercentage();
const mixnodeDetails = <MixNode>{
host: config.MIXNODE_HOST,
mix_port: 1789,
verloc_port: 1790,
http_api_port: 8080,
identity_key: config.MIXNODE_IDENTITY,
sphinx_key: config.MIXNODE_SPHINX_KEY,
version: config.MIXNODE_VERSION,
profit_margin_percent: profitPercent
};
const bond = buildCoin("100000000", config.CURRENCY_DENOM)
try {
response = await validatorClient.bondMixNode(
mixnodeDetails,
config.MIXNODE_SIGNATURE,
bond,
"auto"
);
}
catch (error) {
throw error;
}
ownsMixNode = await validatorClient.client.ownsMixNode(config.MIXNET_CONTRACT, config.USER_WALLET_ADDRESS);
expect(ownsMixNode.mixnode?.mix_node.profit_margin_percent).toStrictEqual(profitPercent);
});
test.skip("unbond and bond gateway", async () => {
//gateway requires different user wallet
//init inside test
//todo
try {
await validatorClient.unbondGateway("auto", "unbonding gateway");
}
catch (error) {
throw error;
}
const gateway = <Gateway>{
host: config.GATEWAY_HOST,
mix_port: 1789,
clients_port: 9000,
version: config.GATEWAY_VERSION,
sphinx_key: config.GATEWAY_SPHINX,
identity_key: config.GATEWAY_IDENTITY,
location: "earth"
};
const bond = buildCoin("100000000", config.CURRENCY_DENOM)
try {
response = await validatorClient.bondGateway(
gateway,
config.GATEWAY_SIGNATURE,
bond,
"auto"
);
}
catch (error) {
throw error;
}
ownsGateway = await validatorClient.client.ownsGateway(config.MIXNET_CONTRACT, config.USER_WALLET_ADDRESS);
expect(ownsGateway.gateway?.bond_amount).toStrictEqual(bond.amount);
expect(ownsGateway.address).toStrictEqual(config.USER_WALLET_ADDRESS);
});
test.skip("delegate to mixnode, then undelegate", async () => {
const pledge = buildCoin("100000000", config.CURRENCY_DENOM)
try {
response = await validatorClient.delegateToMixNode(
config.MIXNODE_IDENTITY,
pledge,
"auto"
);
//todo - we can assert the events for responses
response.logs.forEach((log) => {
console.log(log.events);
console.log(log.log);
console.log(log.msg_index);
})
}
catch (error) {
throw error;
}
try {
const unbond = await validatorClient.undelegateFromMixNode(
config.MIXNODE_IDENTITY,
"auto"
);
//todo - we can assert the events for responses
unbond.logs.forEach((logs) => {
logs.events.forEach((events) => {
console.log(events.type);
console.log(events.attributes);
})
});
} catch (error) {
throw error;
}
});
});
@@ -0,0 +1,46 @@
import { Mock, Times } from "moq.ts";
import { Block, BlockHeader } from "@cosmjs/stargate";
import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate";
describe("implement cosmwasm client test", () => {
test.only("get height of a block then search for it", async () => {
let height = Promise.resolve(200);
let blockHeader = <BlockHeader>{
version: {
block: "200",
app: "testing",
},
height: 200,
chainId: "nym",
time: "today",
};
let block = Promise.resolve(<Block>{
header: blockHeader,
id: "test",
txs: [],
});
const getheight = new Mock<CosmWasmClient>()
.setup((nym) => nym.getHeight())
.returns(height);
const getblock = new Mock<CosmWasmClient>()
.setup((nym) => nym.getBlock(200))
.returns(block);
let heightC = getheight.object();
let blockC = getblock.object();
let executeHeight = await heightC.getHeight();
let executeBlock = await blockC.getBlock(200);
getheight.verify((nym) => nym.getHeight(), Times.Exactly(1));
getblock.verify((nym) => nym.getBlock(200), Times.Exactly(1));
expect(executeHeight).toStrictEqual(await height);
expect(executeBlock.header.height).toStrictEqual(await height);
expect(executeBlock.header.chainId).toStrictEqual("nym");
});
});
@@ -0,0 +1,25 @@
import { Mock, Times } from "moq.ts";
import { INymdQuery } from "../../src/query-client";
describe("nym-client mocks", () => {
test.only("gets interval rewarding percent", async () => {
let contract = "mixnet_contract";
let response = Promise.resolve(Number(2));
const client = new Mock<INymdQuery>()
.setup((nym) => nym.getIntervalRewardPercent(contract))
.returns(response);
const obj = client.object();
let execute = await obj.getIntervalRewardPercent(contract);
client.verify(
(nym) => nym.getIntervalRewardPercent(contract),
Times.Exactly(1)
);
expect(execute).toStrictEqual(await response);
});
});
@@ -0,0 +1,176 @@
import { Coin } from "@cosmjs/proto-signing";
import { Mock, Times } from "moq.ts";
import ValidatorClient from "../../src/index";
import { Gateway, MixNode } from "../../src/types";
import { config } from "../test-utils/config";
import { buildWallet, buildCoin, profitPercentage } from "../test-utils/utils";
import { promiseExecuteResult } from "../test-utils/expectedResults";
import { promiseTxResult } from "../test-utils/expectedResults"
describe("mock validator client tests", () => {
test.skip("token transfer", async () => {
//arrange
//todo -- add more here
let recipientAddress = "nymt14ev4p8qaa7ayr06cg3z7y2u2kxc9a8f4h9gkch";
let sender = "nymt1cv59jumgvz2chn7ffst8tzvnapqzp282m5vat2";
const coin = buildCoin("50000", "nymt");
let transaction = promiseTxResult();
let mockClient = new Mock<ValidatorClient>()
.setup((nym) => nym.send(recipientAddress, [coin], "auto", "test")).returns(transaction);
let token = mockClient.object();
//act
let response = await token.send(recipientAddress, [coin], "auto", "test");
//assert
mockClient.verify(cl => cl.send(recipientAddress, [coin], "auto"), Times.Exactly(1));
});
test.only("bond mixnode test", async () => {
//arrange
let ownerSignature = "ownersignature";
let coin = buildCoin("50000", "nymt");
let expectedResult = promiseExecuteResult();
const profitPercent = profitPercentage();
const mixnode = <MixNode>{
host: "1.1.1.1",
mix_port: 1789,
verloc_port: 1790,
http_api_port: 8080,
identity_key: "identity",
sphinx_key: "identity",
version: "0.12.1",
profit_margin_percent: profitPercent,
};
let client = new Mock<ValidatorClient>()
.setup((client) =>
client.bondMixNode(mixnode, ownerSignature, coin, "auto")
)
.returns(expectedResult);
let mixnodeBond = client.object();
//act
let response = await mixnodeBond.bondMixNode(
mixnode,
ownerSignature,
coin,
"auto"
);
client.verify((cl) =>
cl.bondMixNode(mixnode, ownerSignature, coin, "auto")
);
//assert
expect(response.logs[0].log).toStrictEqual("test");
expect(response.transactionHash).toStrictEqual(
"9C7BF465AB5CAB0D62446CBB251CF89CD173A640C5DE8DBC14A4BB950916114E"
);
});
test.only("un-bond mixnode", async () => {
//arrange
let expectedResult = promiseExecuteResult();
let client = new Mock<ValidatorClient>()
.setup((client) => client.unbondMixNode("auto"))
.returns(expectedResult);
let unbondNode = client.object();
//act
let response = await unbondNode.unbondMixNode("auto");
client.verify((cl) => cl.unbondMixNode("auto"));
//assert
expect(response.logs[0].log).toStrictEqual("test");
expect(response.transactionHash).toStrictEqual(
"9C7BF465AB5CAB0D62446CBB251CF89CD173A640C5DE8DBC14A4BB950916114E"
);
});
test.only("bond gateway", async () => {
//arrange
let expectedResult = promiseExecuteResult();
let ownerSignature = "ownersigntature";
let coin = buildCoin("50000", "nymt");
const gateway = <Gateway>{
host: '1.2.3.4',
mix_port: 1789,
clients_port: 9000,
version: "0.12.1",
sphinx_key: "sphinx_key",
identity_key: "identity_key",
location: "earth"
};
let client = new Mock<ValidatorClient>()
.setup((client) => client.bondGateway(gateway, ownerSignature, coin, "auto", "memo"))
.returns(expectedResult);
let mock = client.object();
//act
let response = await mock.bondGateway(gateway, ownerSignature, coin, "auto", "memo");
client.verify((cl) => cl.bondGateway(gateway, ownerSignature, coin, "auto", "memo"));
//assert
expect(response.logs[0].log).toStrictEqual("test");
expect(response.transactionHash).toStrictEqual(
"9C7BF465AB5CAB0D62446CBB251CF89CD173A640C5DE8DBC14A4BB950916114E"
);
});
test.only("unbond gateway", async () => {
//arrange
let expectedResult = promiseExecuteResult();
let client = new Mock<ValidatorClient>()
.setup((client) => client.unbondGateway())
.returns(expectedResult);
let mock = client.object();
//act
let response = await mock.unbondGateway();
client.verify((cl) => cl.unbondGateway());
//assert
expect(response.logs[0].log).toStrictEqual("test");
expect(response.transactionHash).toStrictEqual(
"9C7BF465AB5CAB0D62446CBB251CF89CD173A640C5DE8DBC14A4BB950916114E"
);
});
test.only("retrieve a newly created account and the balance should be empty", async () => {
let nymWallet = await buildWallet();
let coin = Promise.resolve(<Coin>{
denom: `${config.CURRENCY_DENOM}`,
amount: "0",
});
let client = new Mock<ValidatorClient>()
.setup((nym) => nym.getBalance(nymWallet))
.returns(coin);
let obj = client.object();
let execute = await obj.getBalance(nymWallet);
client.verify((nym) => nym.getBalance(nymWallet), Times.Exactly(1));
expect(execute).toStrictEqual(await coin);
});
});
@@ -0,0 +1,23 @@
export const config = {
NYMD_URL: process.env.NYMD_URL as string,
VALIDATOR_API: process.env.VALIDATOR_API as string,
MIXNET_CONTRACT: process.env.MIXNET_CONTRACT as string,
VESTING_CONTRACT: process.env.VESTING_CONTRACT as string,
USER_MNEMONIC: process.env.USER_MNEMONIC as string,
USER_WALLET_ADDRESS: process.env.USER_WALLET_ADDRESS as string,
CURRENCY_DENOM: process.env.CURRENCY_DENOM as string,
CHAIN_ID: process.env.CHAIN_ID as string,
MIXNODE_IDENTITY: process.env.MIXNODE_IDENTITY as string,
MIXNODE_SPHINX_KEY: process.env.MIXNODE_SPHINX_KEY as string,
MIXNODE_SIGNATURE: process.env.MIXNODE_SIGNATURE as string,
MIXNODE_HOST: process.env.MIXNODE_HOST as string,
MIXNODE_VERSION: process.env.MIXNODE_VERSION as string,
GATEWAY_IDENTITY: process.env.GATEWAY_IDENTITY as string,
GATEWAY_SIGNATURE: process.env.GATEWAY_SIGNATURE as string,
GATEWAY_SPHINX: process.env.GATEWAY_SPHINX as string,
GATEWAY_LOCATION: process.env.GATEWAY_LOCATION as string,
GATEWAY_HOST: process.env.GATEWAY_HOST as string,
GATEWAY_VERSION: process.env.GATEWAY_VERSION as string,
NETWORK_BECH: process.env.NETWORK_BECH as string,
};
@@ -0,0 +1,27 @@
import { ExecuteResult } from "@cosmjs/cosmwasm-stargate";
import { DeliverTxResponse, logs } from "@cosmjs/stargate";
export const promiseExecuteResult = (): Promise<ExecuteResult> => {
let log = <logs.Log>{
msg_index: 0,
log: "test",
events: [],
};
return Promise.resolve(<ExecuteResult>{
logs: [log],
transactionHash:
"9C7BF465AB5CAB0D62446CBB251CF89CD173A640C5DE8DBC14A4BB950916114E",
});
};
export const promiseTxResult = (): Promise<DeliverTxResponse> => {
return Promise.resolve(<DeliverTxResponse>{
code: 0,
height: 1208302,
rawLog: "[]",
transactionHash:
"9C7BF465AB5CAB0D62446CBB251CF89CD173A640C5DE8DBC14A4BB950916114E",
gasUsed: 65042,
gasWanted: 67977,
});
};
@@ -0,0 +1,46 @@
// timer actions
import ValidatorClient, { Coin } from "../../src";
import { config } from "./config";
// Store current time as `start`
export const now = (eventName = null) => {
if (eventName) {
console.log(`Started ${eventName}..`);
}
return new Date().getTime();
};
//takes arg of start time
export const elapsed = (beginning: number, log = false) => {
const duration = new Date().getTime() - beginning;
if (log) {
console.log(`${duration / 1000}s`);
}
return duration;
};
export const profitPercentage = (): number => {
return Math.floor(Math.random() * 100) + 1;
};
export const buildCoin = (amount: string, denomination: string): Coin => {
return {
denom: `u${denomination}`,
amount: amount,
};
};
export const buildWallet = async (): Promise<string> => {
let mnemonic = ValidatorClient.randomMnemonic();
const randomAddress = await ValidatorClient.buildWallet(
mnemonic,
config.NETWORK_BECH
);
let accountdetails = await randomAddress.getAccounts();
let nymWallet = accountdetails[0].address;
return nymWallet;
};
@@ -0,0 +1,10 @@
const currency = require("../../src/currency");
describe("currency module tests", () => {
test.skip("convert to native balance", () => {
const decimal = "12.0346";
const value = currency.printableBalanceToNative(decimal);
expect(value).toStrictEqual("12034600");
});
});
@@ -0,0 +1,19 @@
const stargate = require("../../src/stargate-helper");
import { config } from "../test-utils/config";
describe("test the stargate functions within the project", () => {
test.skip("gas price is returned correctly", () => {
const nymCurrency = config.CURRENCY_DENOM;
const getGasPrice = stargate.nymGasPrice(nymCurrency);
expect(getGasPrice.denom).toBe(`u${nymCurrency}`);
});
test.skip("provide invalid type returns an error message", () => {
//pass invalid type
expect(() => {
const nymCurrency = 13;
stargate.nymGasPrice(nymCurrency);
}).toThrow("13 is not of type string");
});
});
@@ -0,0 +1,10 @@
import validator from "../../src/index";
describe("validator build network mnemonic", () => {
test.skip("get mnemonic", async () => {
const mnemonic = validator.randomMnemonic();
const mnemonicCount = mnemonic.split(" ").length;
expect(mnemonicCount).toStrictEqual(24);
});
});
+10 -2
View File
@@ -5,7 +5,11 @@
"esModuleInterop": true,
"strict": true,
"declaration": true,
"outDir": "./dist"
"outDir": "./dist",
"types": [
"jest",
"node",
]
},
"typedocOptions": {
"entryPoints": [
@@ -16,7 +20,11 @@
"exclude": [
"dist",
"examples",
"tests",
"node_modules"
],
"include": [
"tests",
"./tests/*/*.tsx",
"./tests/*/*.ts"
]
}
+3654 -2959
View File
File diff suppressed because it is too large Load Diff