From d237a0abfdc8ae1ad8cf696b1669724e8fcd6113 Mon Sep 17 00:00:00 2001 From: open-junius Date: Fri, 7 Mar 2025 22:20:21 +0800 Subject: [PATCH 1/7] add types e2e test for evm --- evm-tests/.gitignore | 2 + evm-tests/.papi/descriptors/.gitignore | 3 + evm-tests/.papi/descriptors/package.json | 24 + evm-tests/.papi/metadata/devnet.scale | Bin 0 -> 222946 bytes evm-tests/.papi/polkadot-api.json | 11 + evm-tests/README.md | 23 + evm-tests/get-metadata.sh | 3 + evm-tests/local.test.ts | 53 ++ evm-tests/package.json | 31 + evm-tests/src/address-utils.ts | 82 ++ evm-tests/src/balance-math.ts | 26 + evm-tests/src/bridgeToken.ts | 633 +++++++++++++ evm-tests/src/config.ts | 35 + evm-tests/src/contracts/incremental.sol | 22 + evm-tests/src/contracts/incremental.ts | 39 + evm-tests/src/contracts/metagraph.ts | 391 ++++++++ evm-tests/src/contracts/neuron.ts | 235 +++++ evm-tests/src/contracts/staking.ts | 243 +++++ evm-tests/src/contracts/subnet.ts | 889 ++++++++++++++++++ evm-tests/src/contracts/withdraw.sol | 13 + evm-tests/src/contracts/withdraw.ts | 31 + evm-tests/src/eth.ts | 17 + evm-tests/src/main.ts | 6 + evm-tests/src/substrate.ts | 266 ++++++ evm-tests/src/subtensor.ts | 345 +++++++ evm-tests/src/utils.ts | 55 ++ .../test/ed25519.precompile.verify.test.ts | 122 +++ evm-tests/test/eth.bridgeToken.deploy.test.ts | 69 ++ evm-tests/test/eth.chain-id.test.ts | 76 ++ evm-tests/test/eth.incremental.deploy.test.ts | 61 ++ evm-tests/test/eth.substrate-transfer.test.ts | 412 ++++++++ evm-tests/test/metagraph.precompile.test.ts | 147 +++ .../neuron.precompile.emission-check.test.ts | 72 ++ .../neuron.precompile.reveal-weights.test.ts | 142 +++ ...n.precompile.serve.axon-prometheus.test.ts | 162 ++++ .../neuron.precompile.set-weights.test.ts | 65 ++ .../staking.precompile.add-remove.test.ts | 326 +++++++ .../test/staking.precompile.reward.test.ts | 105 +++ .../subnet.precompile.hyperparameter.test.ts | 442 +++++++++ evm-tests/tsconfig.json | 111 +++ 40 files changed, 5790 insertions(+) create mode 100644 evm-tests/.gitignore create mode 100644 evm-tests/.papi/descriptors/.gitignore create mode 100644 evm-tests/.papi/descriptors/package.json create mode 100644 evm-tests/.papi/metadata/devnet.scale create mode 100644 evm-tests/.papi/polkadot-api.json create mode 100644 evm-tests/README.md create mode 100644 evm-tests/get-metadata.sh create mode 100644 evm-tests/local.test.ts create mode 100644 evm-tests/package.json create mode 100644 evm-tests/src/address-utils.ts create mode 100644 evm-tests/src/balance-math.ts create mode 100644 evm-tests/src/bridgeToken.ts create mode 100644 evm-tests/src/config.ts create mode 100644 evm-tests/src/contracts/incremental.sol create mode 100644 evm-tests/src/contracts/incremental.ts create mode 100644 evm-tests/src/contracts/metagraph.ts create mode 100644 evm-tests/src/contracts/neuron.ts create mode 100644 evm-tests/src/contracts/staking.ts create mode 100644 evm-tests/src/contracts/subnet.ts create mode 100644 evm-tests/src/contracts/withdraw.sol create mode 100644 evm-tests/src/contracts/withdraw.ts create mode 100644 evm-tests/src/eth.ts create mode 100644 evm-tests/src/main.ts create mode 100644 evm-tests/src/substrate.ts create mode 100644 evm-tests/src/subtensor.ts create mode 100644 evm-tests/src/utils.ts create mode 100644 evm-tests/test/ed25519.precompile.verify.test.ts create mode 100644 evm-tests/test/eth.bridgeToken.deploy.test.ts create mode 100644 evm-tests/test/eth.chain-id.test.ts create mode 100644 evm-tests/test/eth.incremental.deploy.test.ts create mode 100644 evm-tests/test/eth.substrate-transfer.test.ts create mode 100644 evm-tests/test/metagraph.precompile.test.ts create mode 100644 evm-tests/test/neuron.precompile.emission-check.test.ts create mode 100644 evm-tests/test/neuron.precompile.reveal-weights.test.ts create mode 100644 evm-tests/test/neuron.precompile.serve.axon-prometheus.test.ts create mode 100644 evm-tests/test/neuron.precompile.set-weights.test.ts create mode 100644 evm-tests/test/staking.precompile.add-remove.test.ts create mode 100644 evm-tests/test/staking.precompile.reward.test.ts create mode 100644 evm-tests/test/subnet.precompile.hyperparameter.test.ts create mode 100644 evm-tests/tsconfig.json diff --git a/evm-tests/.gitignore b/evm-tests/.gitignore new file mode 100644 index 000000000..37d7e7348 --- /dev/null +++ b/evm-tests/.gitignore @@ -0,0 +1,2 @@ +node_modules +.env diff --git a/evm-tests/.papi/descriptors/.gitignore b/evm-tests/.papi/descriptors/.gitignore new file mode 100644 index 000000000..46d96ea47 --- /dev/null +++ b/evm-tests/.papi/descriptors/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!package.json \ No newline at end of file diff --git a/evm-tests/.papi/descriptors/package.json b/evm-tests/.papi/descriptors/package.json new file mode 100644 index 000000000..f6205b1c9 --- /dev/null +++ b/evm-tests/.papi/descriptors/package.json @@ -0,0 +1,24 @@ +{ + "version": "0.1.0-autogenerated.15932613768666598877", + "name": "@polkadot-api/descriptors", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "module": "./dist/index.mjs", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "browser": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "sideEffects": false, + "peerDependencies": { + "polkadot-api": "*" + } +} diff --git a/evm-tests/.papi/metadata/devnet.scale b/evm-tests/.papi/metadata/devnet.scale new file mode 100644 index 0000000000000000000000000000000000000000..5577c1ff81b4d9975ee6cdcbb07de2330335c7a5 GIT binary patch literal 222946 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 literal 0 HcmV?d00001 diff --git a/evm-tests/.papi/polkadot-api.json b/evm-tests/.papi/polkadot-api.json new file mode 100644 index 000000000..b415cf8fa --- /dev/null +++ b/evm-tests/.papi/polkadot-api.json @@ -0,0 +1,11 @@ +{ + "version": 0, + "descriptorPath": ".papi/descriptors", + "entries": { + "devnet": { + "wsUrl": "ws://localhost:9944", + "metadata": ".papi/metadata/devnet.scale", + "genesis": "0xd4ee169957410f461aada33a817b3800a1521267a1d34d4b8b14836ba4ebcb93" + } + } +} \ No newline at end of file diff --git a/evm-tests/README.md b/evm-tests/README.md new file mode 100644 index 000000000..bd366b03d --- /dev/null +++ b/evm-tests/README.md @@ -0,0 +1,23 @@ +# type-test + +test with ts + +## install papi + +npm install polkadot-api + +## polkadot api + +npx papi add devnet -w ws://10.0.0.11:9944 + +## get the new metadata + +sh get-metadta.sh + +## run all tests + +yarn test + +## update dependence for coding + +npm update @polkadot-api/descriptors diff --git a/evm-tests/get-metadata.sh b/evm-tests/get-metadata.sh new file mode 100644 index 000000000..6d7727009 --- /dev/null +++ b/evm-tests/get-metadata.sh @@ -0,0 +1,3 @@ +rm -rf .papi +npx papi add devnet -w ws://localhost:9944 + diff --git a/evm-tests/local.test.ts b/evm-tests/local.test.ts new file mode 100644 index 000000000..9eb24d432 --- /dev/null +++ b/evm-tests/local.test.ts @@ -0,0 +1,53 @@ +import * as assert from "assert"; +import { getAliceSigner, getClient, getDevnetApi, getRandomSubstrateKeypair } from "../src/substrate" +import { SUB_LOCAL_URL, } from "../src/config"; +import { devnet } from "@polkadot-api/descriptors" +import { PolkadotSigner, TypedApi } from "polkadot-api"; +import { convertPublicKeyToSs58, convertH160ToSS58 } from "../src/address-utils" +import { ethers } from "ethers" +import { INEURON_ADDRESS, INeuronABI } from "../src/contracts/neuron" +import { generateRandomEthersWallet } from "../src/utils" +import { forceSetBalanceToEthAddress, forceSetBalanceToSs58Address, addNewSubnetwork, burnedRegister } from "../src/subtensor" + +describe("Test neuron precompile Serve Axon Prometheus", () => { + // init eth part + // const wallet1 = generateRandomEthersWallet(); + // const wallet2 = generateRandomEthersWallet(); + // const wallet3 = generateRandomEthersWallet(); + + // init substrate part + + // const coldkey = getRandomSubstrateKeypair(); + + let api: TypedApi + + // sudo account alice as signer + let alice: PolkadotSigner; + before(async () => { + // init variables got from await and async + const subClient = await getClient(SUB_LOCAL_URL) + api = await getDevnetApi() + // alice = await getAliceSigner(); + + // await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey)) + // await forceSetBalanceToEthAddress(api, wallet1.address) + // await forceSetBalanceToEthAddress(api, wallet2.address) + // await forceSetBalanceToEthAddress(api, wallet3.address) + + + let index = 0; + while (index < 30) { + const hotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey)) + let netuid = await addNewSubnetwork(api, hotkey, coldkey) + } + + + }) + + it("Serve Axon", async () => { + + }); +}); \ No newline at end of file diff --git a/evm-tests/package.json b/evm-tests/package.json new file mode 100644 index 000000000..e789b5ce7 --- /dev/null +++ b/evm-tests/package.json @@ -0,0 +1,31 @@ +{ + "scripts": { + "test": "mocha --timeout 999999 --require ts-node/register test/eth.sub*test.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@polkadot-api/descriptors": "file:.papi/descriptors", + "@polkadot-labs/hdkd": "^0.0.10", + "@polkadot-labs/hdkd-helpers": "^0.0.11", + "@polkadot/api": "15.1.1", + "crypto": "^1.0.1", + "dotenv": "16.4.7", + "polkadot-api": "^1.9.1", + "ethers": "^6.13.5", + "viem": "2.23.4" + }, + "devDependencies": { + "@types/bun": "^1.1.13", + "@types/chai": "^5.0.1", + "@types/mocha": "^10.0.10", + "assert": "^2.1.0", + "chai": "^5.2.0", + "mocha": "^11.1.0", + "prettier": "^3.3.3", + "ts-node": "^10.9.2", + "typescript": "^5.7.2", + "vite": "^5.4.8" + } +} diff --git a/evm-tests/src/address-utils.ts b/evm-tests/src/address-utils.ts new file mode 100644 index 000000000..0fa364c77 --- /dev/null +++ b/evm-tests/src/address-utils.ts @@ -0,0 +1,82 @@ +import { Address } from "viem" +import { encodeAddress } from "@polkadot/util-crypto"; +import { MultiAddress } from '@polkadot-api/descriptors'; +import { ss58Address, KeyPair } from "@polkadot-labs/hdkd-helpers"; +import { hexToU8a } from "@polkadot/util"; +import { blake2AsU8a, decodeAddress } from "@polkadot/util-crypto"; +import { Binary } from "polkadot-api"; + +export function toViemAddress(address: string): Address { + let addressNoPrefix = address.replace("0x", "") + return `0x${addressNoPrefix}` +} + +export function convertSs58ToMultiAddress(ss58Address: string) { + const address = MultiAddress.Id(ss58Address) + return address +} + +export function convertH160ToSS58(ethAddress: string) { + // get the public key + const hash = convertH160ToPublicKey(ethAddress); + + // Convert the hash to SS58 format + const ss58Address = encodeAddress(hash, 42); // Assuming network ID 42 + return ss58Address; +} + +export function convertPublicKeyToSs58(publickey: Uint8Array) { + return ss58Address(publickey, 42); +} + +export function convertH160ToPublicKey(ethAddress: string) { + const prefix = "evm:"; + const prefixBytes = new TextEncoder().encode(prefix); + const addressBytes = hexToU8a( + ethAddress.startsWith("0x") ? ethAddress : `0x${ethAddress}` + ); + const combined = new Uint8Array(prefixBytes.length + addressBytes.length); + + // Concatenate prefix and Ethereum address + combined.set(prefixBytes); + combined.set(addressBytes, prefixBytes.length); + + // Hash the combined data (the public key) + const hash = blake2AsU8a(combined); + return hash; +} + +export function ss58ToEthAddress(ss58Address: string) { + // Decode the SS58 address to a Uint8Array public key + const publicKey = decodeAddress(ss58Address); + + // Take the first 20 bytes of the hashed public key for the Ethereum address + const ethereumAddressBytes = publicKey.slice(0, 20); + + // Convert the 20 bytes into an Ethereum H160 address format (Hex string) + const ethereumAddress = '0x' + Buffer.from(ethereumAddressBytes).toString('hex'); + + return ethereumAddress; +} + +export function ss58ToH160(ss58Address: string): Binary { + // Decode the SS58 address to a Uint8Array public key + const publicKey = decodeAddress(ss58Address); + + // Take the first 20 bytes of the hashed public key for the Ethereum address + const ethereumAddressBytes = publicKey.slice(0, 20); + + + return new Binary(ethereumAddressBytes); +} + +export function ethAddressToH160(ethAddress: string): Binary { + // Decode the SS58 address to a Uint8Array public key + const publicKey = hexToU8a(ethAddress); + + // Take the first 20 bytes of the hashed public key for the Ethereum address + // const ethereumAddressBytes = publicKey.slice(0, 20); + + + return new Binary(publicKey); +} \ No newline at end of file diff --git a/evm-tests/src/balance-math.ts b/evm-tests/src/balance-math.ts new file mode 100644 index 000000000..35e9f8868 --- /dev/null +++ b/evm-tests/src/balance-math.ts @@ -0,0 +1,26 @@ +import assert from "assert" + +export const TAO = BigInt(1000000000) // 10^9 +export const ETHPerRAO = BigInt(1000000000) // 10^9 +export const GWEI = BigInt(1000000000) // 10^9 +export const MAX_TX_FEE = BigInt(21000000) * GWEI // 100 times EVM to EVM transfer fee + +export function bigintToRao(value: bigint) { + return TAO * value +} + +export function tao(value: number) { + return TAO * BigInt(value) +} + +export function raoToEth(value: bigint) { + return ETHPerRAO * value +} + +export function compareEthBalanceWithTxFee(balance1: bigint, balance2: bigint) { + if (balance1 > balance2) { + assert((balance1 - balance2) < MAX_TX_FEE) + } else { + assert((balance2 - balance1) < MAX_TX_FEE) + } +} diff --git a/evm-tests/src/bridgeToken.ts b/evm-tests/src/bridgeToken.ts new file mode 100644 index 000000000..d726826b2 --- /dev/null +++ b/evm-tests/src/bridgeToken.ts @@ -0,0 +1,633 @@ +export const wagmiContract = { + abi: [ + { + "inputs": [ + { + "internalType": "string", + "name": "name_", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol_", + "type": "string" + }, + { + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "AccessControlBadConfirmation", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "neededRole", + "type": "bytes32" + } + ], + "name": "AccessControlUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "allowance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientAllowance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "approver", + "type": "address" + } + ], + "name": "ERC20InvalidApprover", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "ERC20InvalidReceiver", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "ERC20InvalidSender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "ERC20InvalidSpender", + "type": "error" + }, + { + "inputs": [], + "name": "UnauthorizedHandler", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "previousAdminRole", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "newAdminRole", + "type": "bytes32" + } + ], + "name": "RoleAdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "burnFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleAdmin", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "hasRole", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "isAdmin", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "callerConfirmation", + "type": "address" + } + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + bytecode: + '0x60806040523480156200001157600080fd5b5060405162000fac38038062000fac8339810160408190526200003491620001ea565b8282600362000044838262000308565b50600462000053828262000308565b5062000065915060009050826200006f565b50505050620003d4565b60008281526005602090815260408083206001600160a01b038516845290915281205460ff16620001185760008381526005602090815260408083206001600160a01b03861684529091529020805460ff19166001179055620000cf3390565b6001600160a01b0316826001600160a01b0316847f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a45060016200011c565b5060005b92915050565b634e487b7160e01b600052604160045260246000fd5b600082601f8301126200014a57600080fd5b81516001600160401b038082111562000167576200016762000122565b604051601f8301601f19908116603f0116810190828211818310171562000192576200019262000122565b8160405283815260209250866020858801011115620001b057600080fd5b600091505b83821015620001d45785820183015181830184015290820190620001b5565b6000602085830101528094505050505092915050565b6000806000606084860312156200020057600080fd5b83516001600160401b03808211156200021857600080fd5b620002268783880162000138565b945060208601519150808211156200023d57600080fd5b506200024c8682870162000138565b604086015190935090506001600160a01b03811681146200026c57600080fd5b809150509250925092565b600181811c908216806200028c57607f821691505b602082108103620002ad57634e487b7160e01b600052602260045260246000fd5b50919050565b601f82111562000303576000816000526020600020601f850160051c81016020861015620002de5750805b601f850160051c820191505b81811015620002ff57828155600101620002ea565b5050505b505050565b81516001600160401b0381111562000324576200032462000122565b6200033c8162000335845462000277565b84620002b3565b602080601f8311600181146200037457600084156200035b5750858301515b600019600386901b1c1916600185901b178555620002ff565b600085815260208120601f198616915b82811015620003a55788860151825594840194600190910190840162000384565b5085821015620003c45787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b610bc880620003e46000396000f3fe608060405234801561001057600080fd5b506004361061012c5760003560e01c806340c10f19116100ad57806395d89b411161007157806395d89b4114610288578063a217fddf14610290578063a9059cbb14610298578063d547741f146102ab578063dd62ed3e146102be57600080fd5b806340c10f191461021357806342966c681461022657806370a082311461023957806379cc67901461026257806391d148541461027557600080fd5b8063248a9ca3116100f4578063248a9ca3146101a657806324d7806c146101c95780632f2ff15d146101dc578063313ce567146101f157806336568abe1461020057600080fd5b806301ffc9a71461013157806306fdde0314610159578063095ea7b31461016e57806318160ddd1461018157806323b872dd14610193575b600080fd5b61014461013f3660046109ab565b6102f7565b60405190151581526020015b60405180910390f35b61016161032e565b60405161015091906109dc565b61014461017c366004610a47565b6103c0565b6002545b604051908152602001610150565b6101446101a1366004610a71565b6103d8565b6101856101b4366004610aad565b60009081526005602052604090206001015490565b6101446101d7366004610ac6565b6103fc565b6101ef6101ea366004610ae1565b610408565b005b60405160128152602001610150565b6101ef61020e366004610ae1565b610433565b6101ef610221366004610a47565b61046b565b6101ef610234366004610aad565b610480565b610185610247366004610ac6565b6001600160a01b031660009081526020819052604090205490565b6101ef610270366004610a47565b61048d565b610144610283366004610ae1565b6104a2565b6101616104cd565b610185600081565b6101446102a6366004610a47565b6104dc565b6101ef6102b9366004610ae1565b6104ea565b6101856102cc366004610b0d565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b60006001600160e01b03198216637965db0b60e01b148061032857506301ffc9a760e01b6001600160e01b03198316145b92915050565b60606003805461033d90610b37565b80601f016020809104026020016040519081016040528092919081815260200182805461036990610b37565b80156103b65780601f1061038b576101008083540402835291602001916103b6565b820191906000526020600020905b81548152906001019060200180831161039957829003601f168201915b5050505050905090565b6000336103ce81858561050f565b5060019392505050565b6000336103e685828561051c565b6103f1858585610599565b506001949350505050565b600061032881836104a2565b600082815260056020526040902060010154610423816105f8565b61042d8383610602565b50505050565b6001600160a01b038116331461045c5760405163334bd91960e11b815260040160405180910390fd5b6104668282610696565b505050565b6000610476816105f8565b6104668383610703565b61048a338261073d565b50565b6000610498816105f8565b610466838361073d565b60009182526005602090815260408084206001600160a01b0393909316845291905290205460ff1690565b60606004805461033d90610b37565b6000336103ce818585610599565b600082815260056020526040902060010154610505816105f8565b61042d8383610696565b6104668383836001610773565b6001600160a01b03838116600090815260016020908152604080832093861683529290522054600019811461042d578181101561058a57604051637dc7a0d960e11b81526001600160a01b038416600482015260248101829052604481018390526064015b60405180910390fd5b61042d84848484036000610773565b6001600160a01b0383166105c357604051634b637e8f60e11b815260006004820152602401610581565b6001600160a01b0382166105ed5760405163ec442f0560e01b815260006004820152602401610581565b610466838383610848565b61048a8133610972565b600061060e83836104a2565b61068e5760008381526005602090815260408083206001600160a01b03861684529091529020805460ff191660011790556106463390565b6001600160a01b0316826001600160a01b0316847f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a4506001610328565b506000610328565b60006106a283836104a2565b1561068e5760008381526005602090815260408083206001600160a01b0386168085529252808320805460ff1916905551339286917ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b9190a4506001610328565b6001600160a01b03821661072d5760405163ec442f0560e01b815260006004820152602401610581565b61073960008383610848565b5050565b6001600160a01b03821661076757604051634b637e8f60e11b815260006004820152602401610581565b61073982600083610848565b6001600160a01b03841661079d5760405163e602df0560e01b815260006004820152602401610581565b6001600160a01b0383166107c757604051634a1406b160e11b815260006004820152602401610581565b6001600160a01b038085166000908152600160209081526040808320938716835292905220829055801561042d57826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161083a91815260200190565b60405180910390a350505050565b6001600160a01b0383166108735780600260008282546108689190610b71565b909155506108e59050565b6001600160a01b038316600090815260208190526040902054818110156108c65760405163391434e360e21b81526001600160a01b03851660048201526024810182905260448101839052606401610581565b6001600160a01b03841660009081526020819052604090209082900390555b6001600160a01b03821661090157600280548290039055610920565b6001600160a01b03821660009081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161096591815260200190565b60405180910390a3505050565b61097c82826104a2565b6107395760405163e2517d3f60e01b81526001600160a01b038216600482015260248101839052604401610581565b6000602082840312156109bd57600080fd5b81356001600160e01b0319811681146109d557600080fd5b9392505050565b60006020808352835180602085015260005b81811015610a0a578581018301518582016040015282016109ee565b506000604082860101526040601f19601f8301168501019250505092915050565b80356001600160a01b0381168114610a4257600080fd5b919050565b60008060408385031215610a5a57600080fd5b610a6383610a2b565b946020939093013593505050565b600080600060608486031215610a8657600080fd5b610a8f84610a2b565b9250610a9d60208501610a2b565b9150604084013590509250925092565b600060208284031215610abf57600080fd5b5035919050565b600060208284031215610ad857600080fd5b6109d582610a2b565b60008060408385031215610af457600080fd5b82359150610b0460208401610a2b565b90509250929050565b60008060408385031215610b2057600080fd5b610b2983610a2b565b9150610b0460208401610a2b565b600181811c90821680610b4b57607f821691505b602082108103610b6b57634e487b7160e01b600052602260045260246000fd5b50919050565b8082018082111561032857634e487b7160e01b600052601160045260246000fdfea2646970667358221220e179fc58c926e64cb6e87416f8ca64c117044e3195b184afe45038857606c15364736f6c63430008160033' +} as const \ No newline at end of file diff --git a/evm-tests/src/config.ts b/evm-tests/src/config.ts new file mode 100644 index 000000000..5d15aef1a --- /dev/null +++ b/evm-tests/src/config.ts @@ -0,0 +1,35 @@ +export const ETH_LOCAL_URL = 'http://localhost:9944' +export const SUB_LOCAL_URL = 'ws://localhost:9944' + +export const IED25519VERIFY_ADDRESS = "0x0000000000000000000000000000000000000402"; +export const IEd25519VerifyABI = [ + { + inputs: [ + { internalType: "bytes32", name: "message", type: "bytes32" }, + { internalType: "bytes32", name: "publicKey", type: "bytes32" }, + { internalType: "bytes32", name: "r", type: "bytes32" }, + { internalType: "bytes32", name: "s", type: "bytes32" }, + ], + name: "verify", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "pure", + type: "function", + }, +]; + +export const IBALANCETRANSFER_ADDRESS = "0x0000000000000000000000000000000000000800"; +export const IBalanceTransferABI = [ + { + inputs: [ + { + internalType: "bytes32", + name: "data", + type: "bytes32", + }, + ], + name: "transfer", + outputs: [], + stateMutability: "payable", + type: "function", + }, +]; \ No newline at end of file diff --git a/evm-tests/src/contracts/incremental.sol b/evm-tests/src/contracts/incremental.sol new file mode 100644 index 000000000..2b3bc2fd4 --- /dev/null +++ b/evm-tests/src/contracts/incremental.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.2 <0.9.0; + +contract Storage { + uint256 number; + + /** + * @dev Store value in variable + * @param num value to store + */ + function store(uint256 num) public { + number = num; + } + + /** + * @dev Return value + * @return value of 'number' + */ + function retrieve() public view returns (uint256) { + return number; + } +} diff --git a/evm-tests/src/contracts/incremental.ts b/evm-tests/src/contracts/incremental.ts new file mode 100644 index 000000000..b19909e49 --- /dev/null +++ b/evm-tests/src/contracts/incremental.ts @@ -0,0 +1,39 @@ +export const INCREMENTAL_CONTRACT_ABI = [ + { + "inputs": [], + "name": "retrieve", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "num", + "type": "uint256" + } + ], + "name": "store", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +]; + +/* +"compiler": { + "version": "0.8.26+commit.8a97fa7a" + }, +*/ + +export const INCREMENTAL_CONTRACT_BYTECODE = "6080604052348015600e575f80fd5b506101438061001c5f395ff3fe608060405234801561000f575f80fd5b5060043610610034575f3560e01c80632e64cec1146100385780636057361d14610056575b5f80fd5b610040610072565b60405161004d919061009b565b60405180910390f35b610070600480360381019061006b91906100e2565b61007a565b005b5f8054905090565b805f8190555050565b5f819050919050565b61009581610083565b82525050565b5f6020820190506100ae5f83018461008c565b92915050565b5f80fd5b6100c181610083565b81146100cb575f80fd5b50565b5f813590506100dc816100b8565b92915050565b5f602082840312156100f7576100f66100b4565b5b5f610104848285016100ce565b9150509291505056fea26469706673582212209a0dd35336aff1eb3eeb11db76aa60a1427a12c1b92f945ea8c8d1dfa337cf2264736f6c634300081a0033" + + + diff --git a/evm-tests/src/contracts/metagraph.ts b/evm-tests/src/contracts/metagraph.ts new file mode 100644 index 000000000..d0c3bf515 --- /dev/null +++ b/evm-tests/src/contracts/metagraph.ts @@ -0,0 +1,391 @@ +export const IMETAGRAPH_ADDRESS = "0x0000000000000000000000000000000000000802"; + +export const IMetagraphABI = [ + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16", + name: "uid", + type: "uint16", + }, + ], + name: "getAxon", + outputs: [ + { + components: [ + { + internalType: "uint64", + name: "block", + type: "uint64", + }, + { + internalType: "uint32", + name: "version", + type: "uint32", + }, + { + internalType: "uint128", + name: "ip", + type: "uint128", + }, + { + internalType: "uint16", + name: "port", + type: "uint16", + }, + { + internalType: "uint8", + name: "ip_type", + type: "uint8", + }, + { + internalType: "uint8", + name: "protocol", + type: "uint8", + }, + ], + internalType: "struct AxonInfo", + name: "", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16", + name: "uid", + type: "uint16", + }, + ], + name: "getColdkey", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16", + name: "uid", + type: "uint16", + }, + ], + name: "getConsensus", + outputs: [ + { + internalType: "uint16", + name: "", + type: "uint16", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16", + name: "uid", + type: "uint16", + }, + ], + name: "getDividends", + outputs: [ + { + internalType: "uint16", + name: "", + type: "uint16", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16", + name: "uid", + type: "uint16", + }, + ], + name: "getEmission", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16", + name: "uid", + type: "uint16", + }, + ], + name: "getHotkey", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16", + name: "uid", + type: "uint16", + }, + ], + name: "getIncentive", + outputs: [ + { + internalType: "uint16", + name: "", + type: "uint16", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16", + name: "uid", + type: "uint16", + }, + ], + name: "getIsActive", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16", + name: "uid", + type: "uint16", + }, + ], + name: "getLastUpdate", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16", + name: "uid", + type: "uint16", + }, + ], + name: "getRank", + outputs: [ + { + internalType: "uint16", + name: "", + type: "uint16", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16", + name: "uid", + type: "uint16", + }, + ], + name: "getStake", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16", + name: "uid", + type: "uint16", + }, + ], + name: "getTrust", + outputs: [ + { + internalType: "uint16", + name: "", + type: "uint16", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + ], + name: "getUidCount", + outputs: [ + { + internalType: "uint16", + name: "", + type: "uint16", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16", + name: "uid", + type: "uint16", + }, + ], + name: "getValidatorStatus", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16", + name: "uid", + type: "uint16", + }, + ], + name: "getVtrust", + outputs: [ + { + internalType: "uint16", + name: "", + type: "uint16", + }, + ], + stateMutability: "view", + type: "function", + }, +]; \ No newline at end of file diff --git a/evm-tests/src/contracts/neuron.ts b/evm-tests/src/contracts/neuron.ts new file mode 100644 index 000000000..4a8fb47e4 --- /dev/null +++ b/evm-tests/src/contracts/neuron.ts @@ -0,0 +1,235 @@ +export const INEURON_ADDRESS = "0x0000000000000000000000000000000000000804"; + +export const INeuronABI = [ + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "bytes32", + name: "commitHash", + type: "bytes32", + }, + ], + name: "commitWeights", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16[]", + name: "uids", + type: "uint16[]", + }, + { + internalType: "uint16[]", + name: "values", + type: "uint16[]", + }, + { + internalType: "uint16[]", + name: "salt", + type: "uint16[]", + }, + { + internalType: "uint64", + name: "versionKey", + type: "uint64", + }, + ], + name: "revealWeights", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16[]", + name: "dests", + type: "uint16[]", + }, + { + internalType: "uint16[]", + name: "weights", + type: "uint16[]", + }, + { + internalType: "uint64", + name: "versionKey", + type: "uint64", + }, + ], + name: "setWeights", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint32", + name: "version", + type: "uint32", + }, + { + internalType: "uint128", + name: "ip", + type: "uint128", + }, + { + internalType: "uint16", + name: "port", + type: "uint16", + }, + { + internalType: "uint8", + name: "ipType", + type: "uint8", + }, + { + internalType: "uint8", + name: "protocol", + type: "uint8", + }, + { + internalType: "uint8", + name: "placeholder1", + type: "uint8", + }, + { + internalType: "uint8", + name: "placeholder2", + type: "uint8", + }, + ], + name: "serveAxon", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint32", + name: "version", + type: "uint32", + }, + { + internalType: "uint128", + name: "ip", + type: "uint128", + }, + { + internalType: "uint16", + name: "port", + type: "uint16", + }, + { + internalType: "uint8", + name: "ipType", + type: "uint8", + }, + { + internalType: "uint8", + name: "protocol", + type: "uint8", + }, + { + internalType: "uint8", + name: "placeholder1", + type: "uint8", + }, + { + internalType: "uint8", + name: "placeholder2", + type: "uint8", + }, + { + internalType: "bytes", + name: "certificate", + type: "bytes", + }, + ], + name: "serveAxonTls", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint32", + name: "version", + type: "uint32", + }, + { + internalType: "uint128", + name: "ip", + type: "uint128", + }, + { + internalType: "uint16", + name: "port", + type: "uint16", + }, + { + internalType: "uint8", + name: "ipType", + type: "uint8", + }, + ], + name: "servePrometheus", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "bytes32", + name: "hotkey", + type: "bytes32", + }, + ], + name: "burnedRegister", + outputs: [], + stateMutability: "payable", + type: "function", + }, +]; \ No newline at end of file diff --git a/evm-tests/src/contracts/staking.ts b/evm-tests/src/contracts/staking.ts new file mode 100644 index 000000000..9a30d307b --- /dev/null +++ b/evm-tests/src/contracts/staking.ts @@ -0,0 +1,243 @@ +export const ISTAKING_ADDRESS = "0x0000000000000000000000000000000000000801"; +export const ISTAKING_V2_ADDRESS = "0x0000000000000000000000000000000000000805"; + +export const IStakingABI = [ + { + inputs: [ + { + internalType: "bytes32", + name: "delegate", + type: "bytes32", + }, + ], + name: "addProxy", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "hotkey", + type: "bytes32", + }, + { + internalType: "uint256", + name: "netuid", + type: "uint256", + }, + ], + name: "addStake", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "delegate", + type: "bytes32", + }, + ], + name: "removeProxy", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "hotkey", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "coldkey", + type: "bytes32", + }, + { + internalType: "uint256", + name: "netuid", + type: "uint256", + }, + ], + name: "getStake", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "hotkey", + type: "bytes32", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "uint256", + name: "netuid", + type: "uint256", + }, + ], + name: "removeStake", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +]; + +export const IStakingV2ABI = [ + { + "inputs": [ + { + "internalType": "bytes32", + "name": "delegate", + "type": "bytes32" + } + ], + "name": "addProxy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "hotkey", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "netuid", + "type": "uint256" + } + ], + "name": "addStake", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "hotkey", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "coldkey", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "netuid", + "type": "uint256" + } + ], + "name": "getStake", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "coldkey", + "type": "bytes32" + } + ], + "name": "getTotalColdkeyStake", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "hotkey", + "type": "bytes32" + } + ], + "name": "getTotalHotkeyStake", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "delegate", + "type": "bytes32" + } + ], + "name": "removeProxy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "hotkey", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "netuid", + "type": "uint256" + } + ], + "name": "removeStake", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +]; \ No newline at end of file diff --git a/evm-tests/src/contracts/subnet.ts b/evm-tests/src/contracts/subnet.ts new file mode 100644 index 000000000..9b6fe0059 --- /dev/null +++ b/evm-tests/src/contracts/subnet.ts @@ -0,0 +1,889 @@ +export const ISUBNET_ADDRESS = "0x0000000000000000000000000000000000000803"; + +export const ISubnetABI = [ + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + ], + name: "getAdjustmentAlpha", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + ], + name: "getAlphaValues", + outputs: [ + { + internalType: "uint16", + name: "", + type: "uint16", + }, + { + internalType: "uint16", + name: "", + type: "uint16", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + ], + name: "getBondsMovingAverage", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + ], + name: "getCommitRevealWeightsEnabled", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + ], + name: "getDifficulty", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "", + type: "uint16", + }, + ], + name: "getImmunityPeriod", + outputs: [ + { + internalType: "uint16", + name: "", + type: "uint16", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "", + type: "uint16", + }, + ], + name: "getKappa", + outputs: [ + { + internalType: "uint16", + name: "", + type: "uint16", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + ], + name: "getMaxBurn", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + ], + name: "getMaxDifficulty", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + ], + name: "getMaxWeightLimit", + outputs: [ + { + internalType: "uint16", + name: "", + type: "uint16", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + ], + name: "getMinAllowedWeights", + outputs: [ + { + internalType: "uint16", + name: "", + type: "uint16", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + ], + name: "getMinBurn", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + ], + name: "getMinDifficulty", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + ], + name: "getNetworkRegistrationAllowed", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "", + type: "uint16", + }, + ], + name: "getRho", + outputs: [ + { + internalType: "uint16", + name: "", + type: "uint16", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + ], + name: "getServingRateLimit", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + ], + name: "getWeightsSetRateLimit", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + ], + name: "getWeightsVersionKey", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16", + name: "activityCutoff", + type: "uint16", + }, + ], + name: "setActivityCutoff", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + ], + name: "getActivityCutoff", + outputs: [ + { + internalType: "uint16", + name: "", + type: "uint16", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint64", + name: "adjustmentAlpha", + type: "uint64", + }, + ], + name: "setAdjustmentAlpha", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16", + name: "alphaLow", + type: "uint16", + }, + { + internalType: "uint16", + name: "alphaHigh", + type: "uint16", + }, + ], + name: "setAlphaValues", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint64", + name: "bondsMovingAverage", + type: "uint64", + }, + ], + name: "setBondsMovingAverage", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "bool", + name: "commitRevealWeightsEnabled", + type: "bool", + }, + ], + name: "setCommitRevealWeightsEnabled", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + ], + name: "getCommitRevealWeightsInterval", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint64", + name: "commitRevealWeightsInterval", + type: "uint64", + }, + ], + name: "setCommitRevealWeightsInterval", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint64", + name: "difficulty", + type: "uint64", + }, + ], + name: "setDifficulty", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16", + name: "immunityPeriod", + type: "uint16", + }, + ], + name: "setImmunityPeriod", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16", + name: "kappa", + type: "uint16", + }, + ], + name: "setKappa", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + ], + name: "getLiquidAlphaEnabled", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "bool", + name: "liquidAlphaEnabled", + type: "bool", + }, + ], + name: "setLiquidAlphaEnabled", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint64", + name: "maxBurn", + type: "uint64", + }, + ], + name: "setMaxBurn", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint64", + name: "maxDifficulty", + type: "uint64", + }, + ], + name: "setMaxDifficulty", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16", + name: "maxWeightLimit", + type: "uint16", + }, + ], + name: "setMaxWeightLimit", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16", + name: "minAllowedWeights", + type: "uint16", + }, + ], + name: "setMinAllowedWeights", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint64", + name: "minBurn", + type: "uint64", + }, + ], + name: "setMinBurn", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint64", + name: "minDifficulty", + type: "uint64", + }, + ], + name: "setMinDifficulty", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + ], + name: "getNetworkPowRegistrationAllowed", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "bool", + name: "networkPowRegistrationAllowed", + type: "bool", + }, + ], + name: "setNetworkPowRegistrationAllowed", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "bool", + name: "networkRegistrationAllowed", + type: "bool", + }, + ], + name: "setNetworkRegistrationAllowed", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint16", + name: "rho", + type: "uint16", + }, + ], + name: "setRho", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint64", + name: "servingRateLimit", + type: "uint64", + }, + ], + name: "setServingRateLimit", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint64", + name: "weightsSetRateLimit", + type: "uint64", + }, + ], + name: "setWeightsSetRateLimit", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + { + internalType: "uint64", + name: "weightsVersionKey", + type: "uint64", + }, + ], + name: "setWeightsVersionKey", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "hotkey", + type: "bytes32", + }, + ], + name: "registerNetwork", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "hotkey", + type: "bytes32" + }, + { + internalType: "string", + name: "subnetName", + type: "string" + }, + { + internalType: "string", + name: "githubRepo", + type: "string" + }, + { + internalType: "string", + name: "subnetContact", + type: "string" + }, + { + internalType: "string", + name: "subnetUrl", + type: "string" + }, + { + internalType: "string", + name: "discord", + type: "string" + }, + { + internalType: "string", + name: "description", + type: "string" + }, + { + internalType: "string", + name: "additional", + type: "string" + } + ], + name: "registerNetwork", + outputs: [], + stateMutability: "payable", + type: "function" + }, +]; \ No newline at end of file diff --git a/evm-tests/src/contracts/withdraw.sol b/evm-tests/src/contracts/withdraw.sol new file mode 100644 index 000000000..3945661e0 --- /dev/null +++ b/evm-tests/src/contracts/withdraw.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.7.0 <0.9.0; + +contract Withdraw { + constructor() {} + + function withdraw(uint256 value) public payable { + payable(msg.sender).transfer(value); + } + + receive() external payable {} +} diff --git a/evm-tests/src/contracts/withdraw.ts b/evm-tests/src/contracts/withdraw.ts new file mode 100644 index 000000000..46fe66bf2 --- /dev/null +++ b/evm-tests/src/contracts/withdraw.ts @@ -0,0 +1,31 @@ +export const WITHDRAW_CONTRACT_ABI = [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +]; + +// "compiler": { +// "version": "0.8.26+commit.8a97fa7a" +// }, + +export const WITHDRAW_CONTRACT_BYTECODE = "6080604052348015600e575f80fd5b506101148061001c5f395ff3fe608060405260043610601e575f3560e01c80632e1a7d4d146028576024565b36602457005b5f80fd5b603e6004803603810190603a919060b8565b6040565b005b3373ffffffffffffffffffffffffffffffffffffffff166108fc8290811502906040515f60405180830381858888f193505050501580156082573d5f803e3d5ffd5b5050565b5f80fd5b5f819050919050565b609a81608a565b811460a3575f80fd5b50565b5f8135905060b2816093565b92915050565b5f6020828403121560ca5760c96086565b5b5f60d58482850160a6565b9150509291505056fea2646970667358221220f43400858bfe4fcc0bf3c1e2e06d3a9e6ced86454a00bd7e4866b3d4d64e46bb64736f6c634300081a0033" + diff --git a/evm-tests/src/eth.ts b/evm-tests/src/eth.ts new file mode 100644 index 000000000..ea3ebb997 --- /dev/null +++ b/evm-tests/src/eth.ts @@ -0,0 +1,17 @@ + +import { ethers, Provider, TransactionRequest, Wallet } from "ethers"; +export async function estimateTransactionCost(provider: Provider, tx: TransactionRequest) { + const feeData = await provider.getFeeData(); + const estimatedGas = BigInt(await provider.estimateGas(tx)); + const gasPrice = feeData.gasPrice || feeData.maxFeePerGas; + if (gasPrice === null) + return estimatedGas + else + return estimatedGas * BigInt(gasPrice); +} + +export function getContract(contractAddress: string, abi: {}[], wallet: Wallet) { + const contract = new ethers.Contract(contractAddress, abi, wallet); + return contract + +} \ No newline at end of file diff --git a/evm-tests/src/main.ts b/evm-tests/src/main.ts new file mode 100644 index 000000000..ada3e7726 --- /dev/null +++ b/evm-tests/src/main.ts @@ -0,0 +1,6 @@ +async function main() { + +} + +main(); + diff --git a/evm-tests/src/substrate.ts b/evm-tests/src/substrate.ts new file mode 100644 index 000000000..5049382ff --- /dev/null +++ b/evm-tests/src/substrate.ts @@ -0,0 +1,266 @@ +import * as assert from "assert"; +import { devnet, MultiAddress } from '@polkadot-api/descriptors'; +import { createClient, TypedApi, Transaction, PolkadotSigner, Binary } from 'polkadot-api'; +import { getWsProvider } from 'polkadot-api/ws-provider/web'; +import { sr25519CreateDerive } from "@polkadot-labs/hdkd" +import { convertPublicKeyToSs58 } from "../src/address-utils" +import { DEV_PHRASE, entropyToMiniSecret, mnemonicToEntropy, KeyPair } from "@polkadot-labs/hdkd-helpers" +import { getPolkadotSigner } from "polkadot-api/signer" +import { randomBytes } from 'crypto'; +import { Keyring } from '@polkadot/keyring'; + +let api: TypedApi | undefined = undefined + +// define url string as type to extend in the future +// export type ClientUrlType = 'ws://localhost:9944' | 'wss://test.finney.opentensor.ai:443' | 'wss://dev.chain.opentensor.ai:443' | 'wss://archive.chain.opentensor.ai'; +export type ClientUrlType = 'ws://localhost:9944' + +export async function getClient(url: ClientUrlType) { + const provider = getWsProvider(url); + const client = createClient(provider); + return client +} + +export async function getDevnetApi() { + if (api === undefined) { + let client = await getClient('ws://localhost:9944') + api = client.getTypedApi(devnet) + } + return api +} + +export function getAlice() { + const entropy = mnemonicToEntropy(DEV_PHRASE) + const miniSecret = entropyToMiniSecret(entropy) + const derive = sr25519CreateDerive(miniSecret) + const hdkdKeyPair = derive("//Alice") + + return hdkdKeyPair +} + +export function getAliceSigner() { + const alice = getAlice() + const polkadotSigner = getPolkadotSigner( + alice.publicKey, + "Sr25519", + alice.sign, + ) + + return polkadotSigner +} + +export function getRandomSubstrateSigner() { + const keypair = getRandomSubstrateKeypair(); + return getSignerFromKeypair(keypair) +} + +export function getSignerFromKeypair(keypair: KeyPair) { + const polkadotSigner = getPolkadotSigner( + keypair.publicKey, + "Sr25519", + keypair.sign, + ) + return polkadotSigner +} + +export function getRandomSubstrateKeypair() { + const seed = randomBytes(32); + const miniSecret = entropyToMiniSecret(seed) + const derive = sr25519CreateDerive(miniSecret) + const hdkdKeyPair = derive("") + + return hdkdKeyPair +} + +export async function getBalance(api: TypedApi) { + const value = await api.query.Balances.Account.getValue("") + return value +} + +export async function getNonce(api: TypedApi, ss58Address: string): Promise { + const value = await api.query.System.Account.getValue(ss58Address); + return value.nonce +} + +export async function getNonceChangePromise(api: TypedApi, ss58Address: string) { + // api.query.System.Account.getValue() + const initValue = await api.query.System.Account.getValue(ss58Address); + return new Promise((resolve, reject) => { + const subscription = api.query.System.Account.watchValue(ss58Address).subscribe({ + next(value) { + if (value.nonce > initValue.nonce) { + subscription.unsubscribe(); + // Resolve the promise when the transaction is finalized + resolve(); + } + }, + + error(err: Error) { + console.error("Transaction failed:", err); + subscription.unsubscribe(); + // Reject the promise in case of an error + reject(err); + }, + complete() { + console.log("Subscription complete"); + } + }) + + setTimeout(() => { + subscription.unsubscribe(); + console.log('unsubscribed!'); + resolve() + }, 2000); + + }) +} + +export function convertPublicKeyToMultiAddress(publicKey: Uint8Array, ss58Format: number = 42): MultiAddress { + // Create a keyring instance + const keyring = new Keyring({ type: 'sr25519', ss58Format }); + + // Add the public key to the keyring + const address = keyring.encodeAddress(publicKey); + + return MultiAddress.Id(address); +} + + +export async function waitForTransactionCompletion(api: TypedApi, tx: Transaction<{}, string, string, void>, signer: PolkadotSigner,) { + const transactionPromise = await getTransactionWatchPromise(tx, signer) + const ss58Address = convertPublicKeyToSs58(signer.publicKey) + const noncePromise = await getNonceChangePromise(api, ss58Address) + + return new Promise((resolve, reject) => { + Promise.race([transactionPromise, noncePromise]) + .then(resolve) + .catch(reject); + }) +} + +export async function getTransactionWatchPromise(tx: Transaction<{}, string, string, void>, signer: PolkadotSigner,) { + return new Promise((resolve, reject) => { + const subscription = tx.signSubmitAndWatch(signer).subscribe({ + next(value) { + console.log("Event:", value); + + // TODO investigate why finalized not for each extrinsic + if (value.type === "finalized") { + console.log("Transaction is finalized in block:", value.txHash); + subscription.unsubscribe(); + // Resolve the promise when the transaction is finalized + resolve(); + + } + }, + error(err) { + console.error("Transaction failed:", err); + subscription.unsubscribe(); + // Reject the promise in case of an error + reject(err); + + }, + complete() { + console.log("Subscription complete"); + } + }); + + setTimeout(() => { + subscription.unsubscribe(); + console.log('unsubscribed!'); + resolve() + }, 2000); + }); +} + +export async function waitForFinalizedBlock(api: TypedApi) { + const currentBlockNumber = await api.query.System.Number.getValue() + return new Promise((resolve, reject) => { + + const subscription = api.query.System.Number.watchValue().subscribe({ + // TODO check why the block number event just get once + next(value: number) { + console.log("Event block number is :", value); + + if (value > currentBlockNumber + 6) { + console.log("Transaction is finalized in block:", value); + subscription.unsubscribe(); + + resolve(); + + } + + }, + error(err: Error) { + console.error("Transaction failed:", err); + subscription.unsubscribe(); + // Reject the promise in case of an error + reject(err); + + }, + complete() { + console.log("Subscription complete"); + } + }); + + setTimeout(() => { + subscription.unsubscribe(); + console.log('unsubscribed!'); + resolve() + }, 2000); + }); +} + +// second solution to wait for transaction finalization. pass the raw data to avoid the complex transaction type definition +export async function waitForTransactionCompletion2(api: TypedApi, raw: Binary, signer: PolkadotSigner,) { + const tx = await api.txFromCallData(raw); + return new Promise((resolve, reject) => { + const subscription = tx.signSubmitAndWatch(signer).subscribe({ + next(value) { + console.log("Event:", value); + + if (value.type === "txBestBlocksState") { + console.log("Transaction is finalized in block:", value.txHash); + subscription.unsubscribe(); + // Resolve the promise when the transaction is finalized + resolve(); + + } + }, + error(err: Error) { + console.error("Transaction failed:", err); + subscription.unsubscribe(); + // Reject the promise in case of an error + reject(err); + + }, + complete() { + console.log("Subscription complete"); + } + }); + }); +} + +export async function waitForNonceChange(api: TypedApi, ss58Address: string) { + const initNonce = await getNonce(api, ss58Address) + while (true) { + const currentNonce = await getNonce(api, ss58Address) + if (currentNonce > initNonce) { + break + } + + await new Promise(resolve => setTimeout(resolve, 200)); + } +} + + +// other approach to convert public key to ss58 +// export function convertPublicKeyToSs58(publicKey: Uint8Array, ss58Format: number = 42): string { +// // Create a keyring instance +// const keyring = new Keyring({ type: 'sr25519', ss58Format }); + +// // Add the public key to the keyring +// const address = keyring.encodeAddress(publicKey); + +// return address +// } \ No newline at end of file diff --git a/evm-tests/src/subtensor.ts b/evm-tests/src/subtensor.ts new file mode 100644 index 000000000..48dc5c83c --- /dev/null +++ b/evm-tests/src/subtensor.ts @@ -0,0 +1,345 @@ +import * as assert from "assert"; +import { devnet, MultiAddress } from '@polkadot-api/descriptors'; +import { TypedApi, TxCallData } from 'polkadot-api'; +import { KeyPair } from "@polkadot-labs/hdkd-helpers" +import { getAliceSigner, waitForTransactionCompletion, getSignerFromKeypair } from './substrate' +import { convertH160ToSS58, convertPublicKeyToSs58 } from './address-utils' +import { tao } from './balance-math' + +// create a new subnet and return netuid +export async function addNewSubnetwork(api: TypedApi, hotkey: KeyPair, coldkey: KeyPair) { + const alice = getAliceSigner() + const totalNetworks = await api.query.SubtensorModule.TotalNetworks.getValue() + + const rateLimit = await api.query.SubtensorModule.NetworkRateLimit.getValue() + if (rateLimit !== BigInt(0)) { + const internalCall = api.tx.AdminUtils.sudo_set_network_rate_limit({ rate_limit: BigInt(0) }) + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) + await waitForTransactionCompletion(api, tx, alice) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + } + + const signer = getSignerFromKeypair(coldkey) + const registerNetworkTx = api.tx.SubtensorModule.register_network({ hotkey: convertPublicKeyToSs58(hotkey.publicKey) }) + await waitForTransactionCompletion(api, registerNetworkTx, signer) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + + assert.equal(totalNetworks + 1, await api.query.SubtensorModule.TotalNetworks.getValue()) + return totalNetworks +} + +// force set balance for a ss58 address +export async function forceSetBalanceToSs58Address(api: TypedApi, ss58Address: string) { + const alice = getAliceSigner() + const balance = tao(1e8) + const internalCall = api.tx.Balances.force_set_balance({ who: MultiAddress.Id(ss58Address), new_free: balance }) + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) + + await waitForTransactionCompletion(api, tx, alice) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + + const balanceOnChain = (await api.query.System.Account.getValue(ss58Address)).data.free + // check the balance except for sudo account becasue of tx fee + if (ss58Address !== convertPublicKeyToSs58(alice.publicKey)) { + assert.equal(balance, balanceOnChain) + } +} + +// set balance for an eth address +export async function forceSetBalanceToEthAddress(api: TypedApi, ethAddress: string) { + const ss58Address = convertH160ToSS58(ethAddress) + await forceSetBalanceToSs58Address(api, ss58Address) +} + +export async function setCommitRevealWeightsEnabled(api: TypedApi, netuid: number, enabled: boolean) { + const value = await api.query.SubtensorModule.CommitRevealWeightsEnabled.getValue(netuid) + if (value === enabled) { + return; + } + + const alice = getAliceSigner() + const internalCall = api.tx.AdminUtils.sudo_set_commit_reveal_weights_enabled({ netuid: netuid, enabled: enabled }) + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) + + await waitForTransactionCompletion(api, tx, alice) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + assert.equal(enabled, await api.query.SubtensorModule.CommitRevealWeightsEnabled.getValue(netuid)) +} + +export async function setWeightsSetRateLimit(api: TypedApi, netuid: number, rateLimit: bigint) { + const value = await api.query.SubtensorModule.WeightsSetRateLimit.getValue(netuid) + if (value === rateLimit) { + return; + } + + const alice = getAliceSigner() + const internalCall = api.tx.AdminUtils.sudo_set_weights_set_rate_limit({ netuid: netuid, weights_set_rate_limit: rateLimit }) + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) + + await waitForTransactionCompletion(api, tx, alice) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + assert.equal(rateLimit, await api.query.SubtensorModule.WeightsSetRateLimit.getValue(netuid)) +} + +// tempo is u16 in rust, but we just number in js. so value should be less than u16::Max +export async function setTempo(api: TypedApi, netuid: number, tempo: number) { + const value = await api.query.SubtensorModule.Tempo.getValue(netuid) + console.log("init avlue is ", value) + if (value === tempo) { + return; + } + + const alice = getAliceSigner() + const internalCall = api.tx.AdminUtils.sudo_set_tempo({ netuid: netuid, tempo: tempo }) + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) + + await waitForTransactionCompletion(api, tx, alice) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + assert.equal(tempo, await api.query.SubtensorModule.Tempo.getValue(netuid)) +} + +export async function setCommitRevealWeightsInterval(api: TypedApi, netuid: number, interval: bigint) { + const value = await api.query.SubtensorModule.RevealPeriodEpochs.getValue(netuid) + if (value === interval) { + return; + } + + const alice = getAliceSigner() + const internalCall = api.tx.AdminUtils.sudo_set_commit_reveal_weights_interval({ netuid: netuid, interval: interval }) + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) + + await waitForTransactionCompletion(api, tx, alice) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + assert.equal(interval, await api.query.SubtensorModule.RevealPeriodEpochs.getValue(netuid)) +} + + +export async function forceSetChainID(api: TypedApi, chainId: bigint) { + const value = await api.query.EVMChainId.ChainId.getValue() + if (value === chainId) { + return; + } + + const alice = getAliceSigner() + const internalCall = api.tx.AdminUtils.sudo_set_evm_chain_id({ chain_id: chainId }) + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) + + await waitForTransactionCompletion(api, tx, alice) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + assert.equal(chainId, await api.query.EVMChainId.ChainId.getValue()) +} + +export async function disableWhiteListCheck(api: TypedApi, disabled: boolean) { + const value = await api.query.EVM.DisableWhitelistCheck.getValue() + if (value === disabled) { + return; + } + + const alice = getAliceSigner() + const internalCall = api.tx.EVM.disable_whitelist({ disabled: disabled }) + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) + + await waitForTransactionCompletion(api, tx, alice) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + assert.equal(disabled, await api.query.EVM.DisableWhitelistCheck.getValue()) +} + +export async function burnedRegister(api: TypedApi, netuid: number, ss58Address: string, keypair: KeyPair) { + const uids = await api.query.SubtensorModule.SubnetworkN.getValue(netuid) + const signer = getSignerFromKeypair(keypair) + const tx = api.tx.SubtensorModule.burned_register({ hotkey: ss58Address, netuid: netuid }) + await waitForTransactionCompletion(api, tx, signer) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + assert.equal(uids + 1, await api.query.SubtensorModule.SubnetworkN.getValue(netuid)) +} + + +export async function sendProxyCall(api: TypedApi, calldata: TxCallData, ss58Address: string, keypair: KeyPair) { + const signer = getSignerFromKeypair(keypair) + const tx = api.tx.Proxy.proxy({ + call: calldata, + real: MultiAddress.Id(ss58Address), + force_proxy_type: undefined + }); + await waitForTransactionCompletion(api, tx, signer) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); +} + + +export async function setTxRateLimit(api: TypedApi, txRateLimit: bigint) { + const value = await api.query.SubtensorModule.TxRateLimit.getValue() + if (value === txRateLimit) { + return; + } + const alice = getAliceSigner() + + const internalCall = api.tx.AdminUtils.sudo_set_tx_rate_limit({ tx_rate_limit: txRateLimit }) + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) + + + await waitForTransactionCompletion(api, tx, alice) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + assert.equal(txRateLimit, await api.query.SubtensorModule.TxRateLimit.getValue()) +} + +export async function setMaxAllowedValidators(api: TypedApi, netuid: number, maxAllowedValidators: number) { + const value = await api.query.SubtensorModule.MaxAllowedValidators.getValue(netuid) + if (value === maxAllowedValidators) { + return; + } + + const alice = getAliceSigner() + + const internalCall = api.tx.AdminUtils.sudo_set_max_allowed_validators({ + netuid: netuid, + max_allowed_validators: maxAllowedValidators + }) + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) + + await waitForTransactionCompletion(api, tx, alice) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + assert.equal(maxAllowedValidators, await api.query.SubtensorModule.MaxAllowedValidators.getValue(netuid)) +} + +export async function setSubnetOwnerCut(api: TypedApi, subnetOwnerCut: number) { + const value = await api.query.SubtensorModule.SubnetOwnerCut.getValue() + if (value === subnetOwnerCut) { + return; + } + + const alice = getAliceSigner() + + const internalCall = api.tx.AdminUtils.sudo_set_subnet_owner_cut({ + subnet_owner_cut: subnetOwnerCut + }) + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) + + await waitForTransactionCompletion(api, tx, alice) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + assert.equal(subnetOwnerCut, await api.query.SubtensorModule.SubnetOwnerCut.getValue()) +} + +export async function setActivityCutoff(api: TypedApi, netuid: number, activityCutoff: number) { + const value = await api.query.SubtensorModule.ActivityCutoff.getValue(netuid) + if (value === activityCutoff) { + return; + } + + const alice = getAliceSigner() + + const internalCall = api.tx.AdminUtils.sudo_set_activity_cutoff({ + netuid: netuid, + activity_cutoff: activityCutoff + }) + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) + + await waitForTransactionCompletion(api, tx, alice) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + assert.equal(activityCutoff, await api.query.SubtensorModule.ActivityCutoff.getValue(netuid)) +} + +export async function setMaxAllowedUids(api: TypedApi, netuid: number, maxAllowedUids: number) { + const value = await api.query.SubtensorModule.MaxAllowedUids.getValue(netuid) + if (value === maxAllowedUids) { + return; + } + + const alice = getAliceSigner() + + const internalCall = api.tx.AdminUtils.sudo_set_max_allowed_uids({ + netuid: netuid, + max_allowed_uids: maxAllowedUids + }) + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) + + await waitForTransactionCompletion(api, tx, alice) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + assert.equal(maxAllowedUids, await api.query.SubtensorModule.MaxAllowedUids.getValue(netuid)) +} + +export async function setMinDelegateTake(api: TypedApi, minDelegateTake: number) { + const value = await api.query.SubtensorModule.MinDelegateTake.getValue() + if (value === minDelegateTake) { + return; + } + + const alice = getAliceSigner() + + const internalCall = api.tx.AdminUtils.sudo_set_min_delegate_take({ + take: minDelegateTake + }) + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) + + await waitForTransactionCompletion(api, tx, alice) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + assert.equal(minDelegateTake, await api.query.SubtensorModule.MinDelegateTake.getValue()) +} + +export async function becomeDelegate(api: TypedApi, ss58Address: string, keypair: KeyPair) { + const singer = getSignerFromKeypair(keypair) + + const tx = api.tx.SubtensorModule.become_delegate({ + hotkey: ss58Address + }) + await waitForTransactionCompletion(api, tx, singer) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); +} + +export async function addStake(api: TypedApi, netuid: number, ss58Address: string, amount_staked: bigint, keypair: KeyPair) { + const singer = getSignerFromKeypair(keypair) + let tx = api.tx.SubtensorModule.add_stake({ + netuid: netuid, + hotkey: ss58Address, + amount_staked: amount_staked + }) + + await waitForTransactionCompletion(api, tx, singer) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + +} + +export async function setWeight(api: TypedApi, netuid: number, dests: number[], weights: number[], version_key: bigint, keypair: KeyPair) { + const singer = getSignerFromKeypair(keypair) + let tx = api.tx.SubtensorModule.set_weights({ + netuid: netuid, + dests: dests, + weights: weights, + version_key: version_key + }) + + await waitForTransactionCompletion(api, tx, singer) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + +} + +export async function rootRegister(api: TypedApi, ss58Address: string, keypair: KeyPair) { + const singer = getSignerFromKeypair(keypair) + let tx = api.tx.SubtensorModule.root_register({ + hotkey: ss58Address + }) + + await waitForTransactionCompletion(api, tx, singer) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + +} \ No newline at end of file diff --git a/evm-tests/src/utils.ts b/evm-tests/src/utils.ts new file mode 100644 index 000000000..36e922b49 --- /dev/null +++ b/evm-tests/src/utils.ts @@ -0,0 +1,55 @@ +import { defineChain, http, publicActions, createPublicClient } from "viem" +import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts' +import { ethers } from "ethers" +import { ETH_LOCAL_URL } from "./config" + +export type ClientUrlType = 'http://localhost:9944'; + +export const chain = (id: number, url: string) => defineChain({ + id: id, + name: 'bittensor', + network: 'bittensor', + nativeCurrency: { + name: 'tao', + symbol: 'TAO', + decimals: 9, + }, + rpcUrls: { + default: { + http: [url], + }, + }, + testnet: true, +}) + + +export async function getPublicClient(url: ClientUrlType) { + const wallet = createPublicClient({ + chain: chain(42, url), + transport: http(), + + }) + + return wallet.extend(publicActions) +} + +/** + * Generates a random Ethereum wallet + * @returns wallet keyring + */ +export function generateRandomEthWallet() { + let privateKey = generatePrivateKey().toString(); + privateKey = privateKey.replace('0x', ''); + + const account = privateKeyToAccount(`0x${privateKey}`) + return account +} + + +export function generateRandomEthersWallet() { + const account = ethers.Wallet.createRandom(); + const provider = new ethers.JsonRpcProvider(ETH_LOCAL_URL); + + const wallet = new ethers.Wallet(account.privateKey, provider); + return wallet; +} \ No newline at end of file diff --git a/evm-tests/test/ed25519.precompile.verify.test.ts b/evm-tests/test/ed25519.precompile.verify.test.ts new file mode 100644 index 000000000..fcd79ec9d --- /dev/null +++ b/evm-tests/test/ed25519.precompile.verify.test.ts @@ -0,0 +1,122 @@ +import { IED25519VERIFY_ADDRESS, IEd25519VerifyABI, ETH_LOCAL_URL } from '../src/config' +import { getPublicClient } from "../src/utils"; +import { toHex, toBytes, keccak256, PublicClient } from 'viem' +import { Keyring } from "@polkadot/keyring"; +import * as assert from "assert"; + +describe("Verfication of ed25519 signature", () => { + // init eth part + let ethClient: PublicClient; + + before(async () => { + ethClient = await getPublicClient(ETH_LOCAL_URL); + }); + + it("Verification of ed25519 works", async () => { + const keyring = new Keyring({ type: "ed25519" }); + const alice = keyring.addFromUri("//Alice"); + + // Use this example: https://github.com/gztensor/evm-demo/blob/main/docs/ed25519verify-precompile.md + // const keyring = new Keyring({ type: "ed25519" }); + // const myAccount = keyring.addFromUri("//Alice"); + + ////////////////////////////////////////////////////////////////////// + // Generate a signature + + // Your message to sign + const message = "Sign this message"; + const messageU8a = new TextEncoder().encode(message); + const messageHex = toHex(messageU8a); // Convert message to hex string + const messageHash = keccak256(messageHex); // Hash the message to fit into bytes32 + console.log(`messageHash = ${messageHash}`); + const hashedMessageBytes = toBytes(messageHash); + console.log(`hashedMessageBytes = ${hashedMessageBytes}`); + + // Sign the message + const signature = await alice.sign(hashedMessageBytes); + console.log(`Signature: ${toHex(signature)}`); + + // Verify the signature locally + const isValid = alice.verify( + hashedMessageBytes, + signature, + alice.publicKey + ); + console.log(`Is the signature valid? ${isValid}`); + + ////////////////////////////////////////////////////////////////////// + // Verify the signature using the precompile contract + + const publicKeyBytes = toHex(alice.publicKey); + console.log(`publicKeyBytes = ${publicKeyBytes}`); + + // Split signture into Commitment (R) and response (s) + let r = signature.slice(0, 32); // Commitment, a.k.a. "r" - first 32 bytes + let s = signature.slice(32, 64); // Response, a.k.a. "s" - second 32 bytes + let rBytes = toHex(r); + let sBytes = toHex(s); + + const isPrecompileValid = await ethClient.readContract({ + address: IED25519VERIFY_ADDRESS, + abi: IEd25519VerifyABI, + functionName: "verify", + args: [messageHash, + publicKeyBytes, + rBytes, + sBytes] + + }); + + console.log( + `Is the signature valid according to the smart contract? ${isPrecompileValid}` + ); + assert.equal(isPrecompileValid, true) + + ////////////////////////////////////////////////////////////////////// + // Verify the signature for bad data using the precompile contract + + let brokenHashedMessageBytes = hashedMessageBytes; + brokenHashedMessageBytes[0] = (brokenHashedMessageBytes[0] + 1) % 0xff; + const brokenMessageHash = toHex(brokenHashedMessageBytes); + console.log(`brokenMessageHash = ${brokenMessageHash}`); + + const isPrecompileValidBadData = await ethClient.readContract({ + address: IED25519VERIFY_ADDRESS, + abi: IEd25519VerifyABI, + functionName: "verify", + args: [brokenMessageHash, + publicKeyBytes, + rBytes, + sBytes] + + }); + + console.log( + `Is the signature valid according to the smart contract for broken data? ${isPrecompileValidBadData}` + ); + assert.equal(isPrecompileValidBadData, false) + + ////////////////////////////////////////////////////////////////////// + // Verify the bad signature for good data using the precompile contract + + let brokenR = r; + brokenR[0] = (brokenR[0] + 1) % 0xff; + rBytes = toHex(r); + const isPrecompileValidBadSignature = await ethClient.readContract({ + address: IED25519VERIFY_ADDRESS, + abi: IEd25519VerifyABI, + functionName: "verify", + args: [messageHash, + publicKeyBytes, + rBytes, + sBytes] + + }); + + console.log( + `Is the signature valid according to the smart contract for broken signature? ${isPrecompileValidBadSignature}` + ); + assert.equal(isPrecompileValidBadSignature, false) + + }); +}); \ No newline at end of file diff --git a/evm-tests/test/eth.bridgeToken.deploy.test.ts b/evm-tests/test/eth.bridgeToken.deploy.test.ts new file mode 100644 index 000000000..b28f4a354 --- /dev/null +++ b/evm-tests/test/eth.bridgeToken.deploy.test.ts @@ -0,0 +1,69 @@ +import * as assert from "assert"; +import * as chai from "chai"; + +import { getDevnetApi } from "../src/substrate" +import { generateRandomEthersWallet, getPublicClient } from "../src/utils"; +import { ETH_LOCAL_URL } from "../src/config"; +import { devnet } from "@polkadot-api/descriptors" +import { PublicClient } from "viem"; +import { TypedApi } from "polkadot-api"; +import { wagmiContract } from "../src/bridgeToken"; +import { toViemAddress } from "../src/address-utils"; +import { forceSetBalanceToEthAddress, disableWhiteListCheck } from "../src/subtensor"; +import { ethers } from "ethers" +describe("bridge token contract deployment", () => { + // init eth part + const wallet = generateRandomEthersWallet(); + let publicClient: PublicClient; + + // init substrate part + let api: TypedApi + + before(async () => { + // init variables got from await and async + publicClient = await getPublicClient(ETH_LOCAL_URL) + api = await getDevnetApi() + + await forceSetBalanceToEthAddress(api, wallet.address) + await disableWhiteListCheck(api, true) + }); + + it("Can deploy bridge token smart contract", async () => { + const contractFactory = new ethers.ContractFactory(wagmiContract.abi, wagmiContract.bytecode, wallet) + const contract = await contractFactory.deploy("name", + "symbol", wallet.address) + await contract.waitForDeployment() + assert.notEqual(contract.target, undefined) + + const contractAddress = contract.target.toString() + + const code = await publicClient.getCode({ address: toViemAddress(contractAddress) }) + if (code === undefined) { + throw new Error("code not available") + } + assert.ok(code.length > 100) + assert.ok(code.includes("0x60806040523480156")) + }); + + it("Can deploy bridge token contract with gas limit", async () => { + const contractFactory = new ethers.ContractFactory(wagmiContract.abi, wagmiContract.bytecode, wallet) + const successful_gas_limit = "12345678"; + const contract = await contractFactory.deploy("name", + "symbol", wallet.address, + { + gasLimit: successful_gas_limit, + } + ) + await contract.waitForDeployment() + assert.notEqual(contract.target, undefined) + + const contractAddress = contract.target.toString() + + const code = await publicClient.getCode({ address: toViemAddress(contractAddress) }) + if (code === undefined) { + throw new Error("code not available") + } + assert.ok(code.length > 100) + assert.ok(code.includes("0x60806040523480156")) + }); +}); \ No newline at end of file diff --git a/evm-tests/test/eth.chain-id.test.ts b/evm-tests/test/eth.chain-id.test.ts new file mode 100644 index 000000000..09174c121 --- /dev/null +++ b/evm-tests/test/eth.chain-id.test.ts @@ -0,0 +1,76 @@ + +import * as assert from "assert"; +import * as chai from "chai"; + +import { getDevnetApi, waitForTransactionCompletion, getRandomSubstrateKeypair } from "../src/substrate" +import { generateRandomEthWallet, getPublicClient } from "../src/utils"; +import { convertPublicKeyToSs58 } from "../src/address-utils" +import { ETH_LOCAL_URL } from "../src/config"; +import { devnet } from "@polkadot-api/descriptors" +import { getPolkadotSigner } from "polkadot-api/signer"; +import { PublicClient } from "viem"; +import { TypedApi } from "polkadot-api"; +import { forceSetBalanceToSs58Address, forceSetChainID } from "../src/subtensor"; + +describe("Test the EVM chain ID", () => { + // init eth part + const wallet = generateRandomEthWallet(); + let ethClient: PublicClient; + + // init substrate part + const keyPair = getRandomSubstrateKeypair(); + let api: TypedApi; + + // init other variable + const initChainId = 42; + + before(async () => { + // init variables got from await and async + ethClient = await getPublicClient(ETH_LOCAL_URL); + api = await getDevnetApi() + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(keyPair.publicKey)) + + }); + + it("EVM chain id update is ok", async () => { + let chainId = await ethClient.getChainId(); + // init chain id should be 42 + assert.equal(chainId, initChainId); + + const newChainId = BigInt(100) + await forceSetChainID(api, newChainId) + + chainId = await ethClient.getChainId(); + assert.equal(chainId, newChainId); + + await forceSetChainID(api, BigInt(initChainId)) + + chainId = await ethClient.getChainId(); + // back to original value for other tests. and we can run it repeatedly + assert.equal(chainId, initChainId); + + }); + + it("EVM chain id is the same, only sudo can change it.", async () => { + let chainId = await ethClient.getChainId(); + // init chain id should be 42 + assert.equal(chainId, initChainId); + + // invalide signer for set chain ID + let signer = getPolkadotSigner( + keyPair.publicKey, + "Sr25519", + keyPair.sign, + ) + + let tx = api.tx.AdminUtils.sudo_set_evm_chain_id({ chain_id: BigInt(100) }) + await waitForTransactionCompletion(api, tx, signer) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + + // extrinsic should be failed and chain ID not updated. + chainId = await ethClient.getChainId(); + assert.equal(chainId, 42); + + }); +}); \ No newline at end of file diff --git a/evm-tests/test/eth.incremental.deploy.test.ts b/evm-tests/test/eth.incremental.deploy.test.ts new file mode 100644 index 000000000..c22187538 --- /dev/null +++ b/evm-tests/test/eth.incremental.deploy.test.ts @@ -0,0 +1,61 @@ + + +import * as assert from "assert"; +import * as chai from "chai"; + +import { getDevnetApi } from "../src/substrate" +import { generateRandomEthersWallet, getPublicClient } from "../src/utils"; +import { ETH_LOCAL_URL } from "../src/config"; +import { devnet } from "@polkadot-api/descriptors" +import { PublicClient } from "viem"; +import { TypedApi } from "polkadot-api"; +import { INCREMENTAL_CONTRACT_ABI, INCREMENTAL_CONTRACT_BYTECODE } from "../src/contracts/incremental"; +import { toViemAddress } from "../src/address-utils"; +import { ethers } from "ethers" +import { disableWhiteListCheck, forceSetBalanceToEthAddress } from "../src/subtensor"; + +describe("bridge token contract deployment", () => { + // init eth part + const wallet = generateRandomEthersWallet(); + let publicClient: PublicClient; + + // init substrate part + let api: TypedApi + + before(async () => { + publicClient = await getPublicClient(ETH_LOCAL_URL) + api = await getDevnetApi() + + await forceSetBalanceToEthAddress(api, wallet.address) + await disableWhiteListCheck(api, true) + }); + + it("Can deploy incremental smart contract", async () => { + const contractFactory = new ethers.ContractFactory(INCREMENTAL_CONTRACT_ABI, INCREMENTAL_CONTRACT_BYTECODE, wallet) + const contract = await contractFactory.deploy() + await contract.waitForDeployment() + + const value = await publicClient.readContract({ + abi: INCREMENTAL_CONTRACT_ABI, + address: toViemAddress(contract.target.toString()), + functionName: "retrieve", + args: [] + }) + assert.equal(value, 0) + + const newValue = 1234 + + const deployContract = new ethers.Contract(contract.target.toString(), INCREMENTAL_CONTRACT_ABI, wallet) + const storeTx = await deployContract.store(newValue) + await storeTx.wait() + + const newValueAfterStore = await publicClient.readContract({ + abi: INCREMENTAL_CONTRACT_ABI, + address: toViemAddress(contract.target.toString()), + functionName: "retrieve", + args: [] + }) + + assert.equal(newValue, newValueAfterStore) + }); +}); diff --git a/evm-tests/test/eth.substrate-transfer.test.ts b/evm-tests/test/eth.substrate-transfer.test.ts new file mode 100644 index 000000000..84eb3c678 --- /dev/null +++ b/evm-tests/test/eth.substrate-transfer.test.ts @@ -0,0 +1,412 @@ +import * as assert from "assert"; + +import { getDevnetApi, waitForTransactionCompletion, getRandomSubstrateSigner, } from "../src/substrate" +import { getPublicClient } from "../src/utils"; +import { ETH_LOCAL_URL, IBALANCETRANSFER_ADDRESS, IBalanceTransferABI } from "../src/config"; +import { devnet, MultiAddress } from "@polkadot-api/descriptors" +import { PublicClient } from "viem"; +import { TypedApi, Binary, FixedSizeBinary } from "polkadot-api"; +import { generateRandomEthersWallet } from "../src/utils"; +import { tao, raoToEth, bigintToRao, compareEthBalanceWithTxFee } from "../src/balance-math"; +import { toViemAddress, convertSs58ToMultiAddress, convertPublicKeyToSs58, convertH160ToSS58, ss58ToH160, ss58ToEthAddress, ethAddressToH160 } from "../src/address-utils" +import { ethers } from "ethers" +import { estimateTransactionCost, getContract } from "../src/eth" + +import { WITHDRAW_CONTRACT_ABI, WITHDRAW_CONTRACT_BYTECODE } from "../src/contracts/withdraw" + +import { forceSetBalanceToEthAddress, forceSetBalanceToSs58Address, disableWhiteListCheck } from "../src/subtensor"; + +describe("Balance transfers between substrate and EVM", () => { + const gwei = BigInt("1000000000"); + // init eth part + const wallet = generateRandomEthersWallet(); + const wallet2 = generateRandomEthersWallet(); + let publicClient: PublicClient; + const provider = new ethers.JsonRpcProvider(ETH_LOCAL_URL); + // init substrate part + const signer = getRandomSubstrateSigner(); + let api: TypedApi + + before(async () => { + + publicClient = await getPublicClient(ETH_LOCAL_URL) + api = await getDevnetApi() + + await forceSetBalanceToEthAddress(api, wallet.address) + await forceSetBalanceToEthAddress(api, wallet2.address) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(signer.publicKey)) + await disableWhiteListCheck(api, true) + }); + + it("Can transfer token from EVM to EVM", async () => { + const senderBalance = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) + const receiverBalance = await publicClient.getBalance({ address: toViemAddress(wallet2.address) }) + const transferBalance = raoToEth(tao(1)) + const tx = { + to: wallet2.address, + value: transferBalance.toString() + } + const txFee = await estimateTransactionCost(provider, tx) + + const txResponse = await wallet.sendTransaction(tx) + await txResponse.wait(); + + + const senderBalanceAfterTransfer = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) + const receiverBalanceAfterTranser = await publicClient.getBalance({ address: toViemAddress(wallet2.address) }) + + assert.equal(senderBalanceAfterTransfer, senderBalance - transferBalance - txFee) + assert.equal(receiverBalance, receiverBalanceAfterTranser - transferBalance) + }); + + it("Can transfer token from Substrate to EVM", async () => { + const ss58Address = convertH160ToSS58(wallet.address) + const senderBalance = (await api.query.System.Account.getValue(ss58Address)).data.free + const receiverBalance = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) + const transferBalance = tao(1) + + const tx = api.tx.Balances.transfer_keep_alive({ value: transferBalance, dest: convertSs58ToMultiAddress(ss58Address) }) + await waitForTransactionCompletion(api, tx, signer) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + + + const senderBalanceAfterTransfer = (await api.query.System.Account.getValue(ss58Address)).data.free + const receiverBalanceAfterTranser = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) + + assert.equal(senderBalanceAfterTransfer, senderBalance + transferBalance) + assert.equal(receiverBalance, receiverBalanceAfterTranser - raoToEth(transferBalance)) + }); + + it("Can transfer token from EVM to Substrate", async () => { + const contract = getContract(IBALANCETRANSFER_ADDRESS, IBalanceTransferABI, wallet) + const senderBalance = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) + const receiverBalance = (await api.query.System.Account.getValue(convertPublicKeyToSs58(signer.publicKey))).data.free + const transferBalance = raoToEth(tao(1)) + + const tx = await contract.transfer(signer.publicKey, { value: transferBalance.toString() }) + await tx.wait() + + + const senderBalanceAfterTransfer = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) + const receiverBalanceAfterTranser = (await api.query.System.Account.getValue(convertPublicKeyToSs58(signer.publicKey))).data.free + + compareEthBalanceWithTxFee(senderBalanceAfterTransfer, senderBalance - transferBalance) + assert.equal(receiverBalance, receiverBalanceAfterTranser - tao(1)) + }); + + it("Transfer from EVM to substrate using evm::withdraw", async () => { + const ss58Address = convertPublicKeyToSs58(signer.publicKey) + const senderBalance = (await api.query.System.Account.getValue(ss58Address)).data.free + const ethAddresss = ss58ToH160(ss58Address); + + // transfer token to mirror eth address + const ethTransfer = { + to: ss58ToEthAddress(ss58Address), + value: raoToEth(tao(2)).toString() + } + + const txResponse = await wallet.sendTransaction(ethTransfer) + await txResponse.wait(); + + const tx = api.tx.EVM.withdraw({ address: ethAddresss, value: tao(1) }) + const txFee = (await tx.getPaymentInfo(ss58Address)).partial_fee + + await waitForTransactionCompletion(api, tx, signer) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + + const senderBalanceAfterWithdraw = (await api.query.System.Account.getValue(ss58Address)).data.free + + assert.equal(senderBalance, senderBalanceAfterWithdraw - tao(1) + txFee) + }); + + it("Transfer from EVM to substrate using evm::call", async () => { + const ss58Address = convertPublicKeyToSs58(signer.publicKey) + const ethAddresss = ss58ToH160(ss58Address); + + // transfer token to mirror eth address + const ethTransfer = { + to: ss58ToEthAddress(ss58Address), + value: raoToEth(tao(2)).toString() + } + + const txResponse = await wallet.sendTransaction(ethTransfer) + await txResponse.wait(); + + const source: FixedSizeBinary<20> = ethAddresss; + const target = ethAddressToH160(wallet.address) + const receiverBalance = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) + + // all these parameter value are tricky, any change could make the call failed + const tx = api.tx.EVM.call({ + source: source, + target: target, + // it is U256 in the extrinsic. + value: [raoToEth(tao(1)), tao(0), tao(0), tao(0)], + gas_limit: BigInt(1000000), + // it is U256 in the extrinsic. + max_fee_per_gas: [BigInt(10e9), BigInt(0), BigInt(0), BigInt(0)], + max_priority_fee_per_gas: undefined, + input: Binary.fromText(""), + nonce: undefined, + access_list: [] + }) + // txFee not accurate + const txFee = (await tx.getPaymentInfo(ss58Address)).partial_fee + + await waitForTransactionCompletion(api, tx, signer) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + + + const receiverBalanceAfterCall = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) + assert.equal(receiverBalanceAfterCall, receiverBalance + raoToEth(tao(1))) + }); + + it("Forward value in smart contract", async () => { + + + const contractFactory = new ethers.ContractFactory(WITHDRAW_CONTRACT_ABI, WITHDRAW_CONTRACT_BYTECODE, wallet) + const contract = await contractFactory.deploy() + await contract.waitForDeployment() + + const code = await publicClient.getCode({ address: toViemAddress(contract.target.toString()) }) + if (code === undefined) { + throw new Error("code length is wrong for deployed contract") + } + assert.ok(code.length > 100) + + // transfer 2 TAO to contract + const ethTransfer = { + to: contract.target.toString(), + value: raoToEth(tao(2)).toString() + } + + const txResponse = await wallet.sendTransaction(ethTransfer) + await txResponse.wait(); + + const contractBalance = await publicClient.getBalance({ address: toViemAddress(contract.target.toString()) }) + const callerBalance = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) + + const contractForCall = new ethers.Contract(contract.target.toString(), WITHDRAW_CONTRACT_ABI, wallet) + + const withdrawTx = await contractForCall.withdraw( + raoToEth(tao(1)).toString() + ); + + await withdrawTx.wait(); + + const contractBalanceAfterWithdraw = await publicClient.getBalance({ address: toViemAddress(contract.target.toString()) }) + const callerBalanceAfterWithdraw = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) + + compareEthBalanceWithTxFee(callerBalanceAfterWithdraw, callerBalance + raoToEth(tao(1))) + assert.equal(contractBalance, contractBalanceAfterWithdraw + raoToEth(tao(1))) + }); + + it("Transfer full balance", async () => { + const ethBalance = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) + const receiverBalance = await publicClient.getBalance({ address: toViemAddress(wallet2.address) }) + const tx = { + to: wallet2.address, + value: ethBalance.toString(), + }; + const txPrice = await estimateTransactionCost(provider, tx); + const finalTx = { + to: wallet2.address, + value: (ethBalance - txPrice).toString(), + }; + try { + // transfer should be failed since substrate requires existial balance to keep account + const txResponse = await wallet.sendTransaction(finalTx) + await txResponse.wait(); + } catch (error) { + if (error instanceof Error) { + assert.equal((error as any).code, "INSUFFICIENT_FUNDS") + assert.equal(error.toString().includes("insufficient funds"), true) + } + } + + const receiverBalanceAfterTransfer = await publicClient.getBalance({ address: toViemAddress(wallet2.address) }) + assert.equal(receiverBalance, receiverBalanceAfterTransfer) + }) + + it("Transfer more than owned balance should fail", async () => { + const ethBalance = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) + const receiverBalance = await publicClient.getBalance({ address: toViemAddress(wallet2.address) }) + const tx = { + to: wallet2.address, + value: (ethBalance + raoToEth(tao(1))).toString(), + }; + + try { + // transfer should be failed since substrate requires existial balance to keep account + const txResponse = await wallet.sendTransaction(tx) + await txResponse.wait(); + } catch (error) { + if (error instanceof Error) { + assert.equal((error as any).code, "INSUFFICIENT_FUNDS") + assert.equal(error.toString().includes("insufficient funds"), true) + } + } + + const receiverBalanceAfterTransfer = await publicClient.getBalance({ address: toViemAddress(wallet2.address) }) + assert.equal(receiverBalance, receiverBalanceAfterTransfer) + }); + + it("Transfer more than u64::max in substrate equivalent should receive error response", async () => { + const receiverBalance = await publicClient.getBalance({ address: toViemAddress(wallet2.address) }) + try { + const tx = { + to: wallet2.address, + value: raoToEth(BigInt(2) ** BigInt(64)).toString(), + }; + // transfer should be failed since substrate requires existial balance to keep account + const txResponse = await wallet.sendTransaction(tx) + await txResponse.wait(); + } catch (error) { + if (error instanceof Error) { + assert.equal((error as any).code, "INSUFFICIENT_FUNDS") + assert.equal(error.toString().includes("insufficient funds"), true) + } + } + + const contract = getContract(IBALANCETRANSFER_ADDRESS, IBalanceTransferABI, wallet) + try { + const tx = await contract.transfer(signer.publicKey, { value: raoToEth(BigInt(2) ** BigInt(64)).toString() }) + await tx.await() + } catch (error) { + if (error instanceof Error) { + console.log(error.toString()) + assert.equal(error.toString().includes("revert data"), true) + } + } + + try { + const dest = convertH160ToSS58(wallet2.address) + const tx = api.tx.Balances.transfer_keep_alive({ value: bigintToRao(BigInt(2) ** BigInt(64)), dest: MultiAddress.Id(dest) }) + await waitForTransactionCompletion(api, tx, signer) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + } catch (error) { + if (error instanceof Error) { + console.log(error.toString()) + assert.equal(error.toString().includes("Cannot convert"), true) + } + } + + try { + const dest = ethAddressToH160(wallet2.address) + const tx = api.tx.EVM.withdraw({ value: bigintToRao(BigInt(2) ** BigInt(64)), address: dest }) + await waitForTransactionCompletion(api, tx, signer) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + } catch (error) { + if (error instanceof Error) { + assert.equal(error.toString().includes("Cannot convert"), true) + } + } + + try { + const source = ethAddressToH160(wallet.address) + const target = ethAddressToH160(wallet2.address) + const tx = api.tx.EVM.call({ + source: source, + target: target, + // it is U256 in the extrinsic, the value is more than u64::MAX + value: [raoToEth(tao(1)), tao(0), tao(0), tao(1)], + gas_limit: BigInt(1000000), + // it is U256 in the extrinsic. + max_fee_per_gas: [BigInt(10e9), BigInt(0), BigInt(0), BigInt(0)], + max_priority_fee_per_gas: undefined, + input: Binary.fromText(""), + nonce: undefined, + access_list: [] + }) + await waitForTransactionCompletion(api, tx, signer) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + } catch (error) { + if (error instanceof Error) { + console.log(error.toString()) + assert.equal((error as any).code, "INSUFFICIENT_FUNDS") + assert.equal(error.toString().includes("insufficient funds"), true) + } + } + + const receiverBalanceAfterTransfer = await publicClient.getBalance({ address: toViemAddress(wallet2.address) }) + assert.equal(receiverBalance, receiverBalanceAfterTransfer) + }); + + it("Gas price should be 10 GWei", async () => { + const feeData = await provider.getFeeData(); + assert.equal(feeData.gasPrice, BigInt(10000000000)); + }); + + + it("max_fee_per_gas and max_priority_fee_per_gas affect transaction fee properly", async () => { + + const testCases = [ + [10, 0, 21000 * 10 * 1e9], + [10, 10, 21000 * 10 * 1e9], + [11, 0, 21000 * 10 * 1e9], + [11, 1, (21000 * 10 + 21000) * 1e9], + [11, 2, (21000 * 10 + 21000) * 1e9], + ]; + + for (let i in testCases) { + const tc = testCases[i]; + const actualFee = await transferAndGetFee( + wallet, wallet2, publicClient, + gwei * BigInt(tc[0]), + gwei * BigInt(tc[1]) + ); + assert.equal(actualFee, BigInt(tc[2])) + } + }); + + it("Low max_fee_per_gas gets transaction rejected", async () => { + try { + await transferAndGetFee(wallet, wallet2, publicClient, gwei * BigInt(9), BigInt(0)) + } catch (error) { + if (error instanceof Error) { + console.log(error.toString()) + assert.equal(error.toString().includes("gas price less than block base fee"), true) + } + } + }); + + it("max_fee_per_gas lower than max_priority_fee_per_gas gets transaction rejected", async () => { + try { + await transferAndGetFee(wallet, wallet2, publicClient, gwei * BigInt(10), gwei * BigInt(11)) + } catch (error) { + if (error instanceof Error) { + assert.equal(error.toString().includes("priorityFee cannot be more than maxFee"), true) + } + } + }); +}); + +async function transferAndGetFee(wallet: ethers.Wallet, wallet2: ethers.Wallet, client: PublicClient, max_fee_per_gas: BigInt, max_priority_fee_per_gas: BigInt) { + + const ethBalanceBefore = await client.getBalance({ address: toViemAddress(wallet.address) }) + // Send TAO + const tx = { + to: wallet2.address, + value: raoToEth(tao(1)).toString(), + // EIP-1559 transaction parameters + maxPriorityFeePerGas: max_priority_fee_per_gas.toString(), + maxFeePerGas: max_fee_per_gas.toString(), + gasLimit: 21000, + }; + + // Send the transaction + const txResponse = await wallet.sendTransaction(tx); + await txResponse.wait() + + // Check balances + const ethBalanceAfter = await client.getBalance({ address: toViemAddress(wallet.address) }) + const fee = ethBalanceBefore - ethBalanceAfter - raoToEth(tao(1)) + + return fee; +} \ No newline at end of file diff --git a/evm-tests/test/metagraph.precompile.test.ts b/evm-tests/test/metagraph.precompile.test.ts new file mode 100644 index 000000000..94c0df886 --- /dev/null +++ b/evm-tests/test/metagraph.precompile.test.ts @@ -0,0 +1,147 @@ +import * as assert from "assert"; + +import { getAliceSigner, getClient, getDevnetApi, waitForTransactionCompletion, convertPublicKeyToMultiAddress, getRandomSubstrateKeypair, getSignerFromKeypair } from "../src/substrate" +import { getPublicClient, } from "../src/utils"; +import { ETH_LOCAL_URL, SUB_LOCAL_URL, } from "../src/config"; +import { devnet } from "@polkadot-api/descriptors" +import { PublicClient } from "viem"; +import { PolkadotSigner, TypedApi } from "polkadot-api"; +import { toViemAddress, convertPublicKeyToSs58 } from "../src/address-utils" +import { IMetagraphABI, IMETAGRAPH_ADDRESS } from "../src/contracts/metagraph" + +describe("Test the EVM chain ID", () => { + // init substrate part + const hotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + let publicClient: PublicClient; + + let api: TypedApi + + // sudo account alice as signer + let alice: PolkadotSigner; + + // init other variable + let subnetId = 0; + + before(async () => { + // init variables got from await and async + publicClient = await getPublicClient(ETH_LOCAL_URL) + const subClient = await getClient(SUB_LOCAL_URL) + api = await getDevnetApi() + alice = await getAliceSigner(); + + { + const multiAddress = convertPublicKeyToMultiAddress(hotkey.publicKey) + const internalCall = api.tx.Balances.force_set_balance({ who: multiAddress, new_free: BigInt(1e12) }) + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) + + await waitForTransactionCompletion(api, tx, alice) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + } + + { + const multiAddress = convertPublicKeyToMultiAddress(coldkey.publicKey) + const internalCall = api.tx.Balances.force_set_balance({ who: multiAddress, new_free: BigInt(1e12) }) + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) + + await waitForTransactionCompletion(api, tx, alice) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + } + + const signer = getSignerFromKeypair(coldkey) + const registerNetworkTx = api.tx.SubtensorModule.register_network({ hotkey: convertPublicKeyToSs58(hotkey.publicKey) }) + await waitForTransactionCompletion(api, registerNetworkTx, signer) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + + let totalNetworks = await api.query.SubtensorModule.TotalNetworks.getValue() + assert.ok(totalNetworks > 1) + subnetId = totalNetworks - 1 + + let uid_count = + await api.query.SubtensorModule.SubnetworkN.getValue(subnetId) + if (uid_count === 0) { + const tx = api.tx.SubtensorModule.burned_register({ hotkey: convertPublicKeyToSs58(hotkey.publicKey), netuid: subnetId }) + await waitForTransactionCompletion(api, tx, signer) + .then(() => { }) + .catch((error) => { console.log(`transaction error ${error}`) }); + } + }) + + it("Metagraph data access via precompile contract is ok", async () => { + const uid = 0 + const uid_count = await publicClient.readContract({ + abi: IMetagraphABI, + address: toViemAddress(IMETAGRAPH_ADDRESS), + functionName: "getUidCount", + args: [subnetId] + }) + // back to original value for other tests. and we can run it repeatedly + assert.ok(uid_count != undefined); + + // const axon = api.query.SubtensorModule.Axons.getValue() + + const axon = await publicClient.readContract({ + abi: IMetagraphABI, + address: toViemAddress(IMETAGRAPH_ADDRESS), + functionName: "getAxon", + args: [subnetId, uid] + }) + + assert.ok(axon != undefined); + if (axon instanceof Object) { + assert.ok(axon != undefined); + if ("block" in axon) { + assert.ok(axon.block != undefined); + } else { + throw new Error("block not included in axon") + } + + if ("version" in axon) { + assert.ok(axon.version != undefined); + } else { + throw new Error("version not included in axon") + } + + if ("ip" in axon) { + assert.ok(axon.ip != undefined); + } else { + throw new Error("ip not included in axon") + } + + if ("port" in axon) { + assert.ok(axon.port != undefined); + } else { + throw new Error("port not included in axon") + } + + if ("ip_type" in axon) { + assert.ok(axon.ip_type != undefined); + } else { + throw new Error("ip_type not included in axon") + } + + if ("protocol" in axon) { + assert.ok(axon.protocol != undefined); + } else { + throw new Error("protocol not included in axon") + } + } + + const methodList = ["getEmission", "getVtrust", "getValidatorStatus", "getLastUpdate", "getIsActive", + "getHotkey", "getColdkey" + ] + for (const method of methodList) { + const value = await publicClient.readContract({ + abi: IMetagraphABI, + address: toViemAddress(IMETAGRAPH_ADDRESS), + functionName: method, + args: [subnetId, uid] + }) + + assert.ok(value != undefined); + } + }); +}); \ No newline at end of file diff --git a/evm-tests/test/neuron.precompile.emission-check.test.ts b/evm-tests/test/neuron.precompile.emission-check.test.ts new file mode 100644 index 000000000..ac609c1e2 --- /dev/null +++ b/evm-tests/test/neuron.precompile.emission-check.test.ts @@ -0,0 +1,72 @@ +import * as assert from "assert"; + +import { getAliceSigner, getClient, getDevnetApi, getRandomSubstrateKeypair } from "../src/substrate" +import { getPublicClient, } from "../src/utils"; +import { ETH_LOCAL_URL, SUB_LOCAL_URL, } from "../src/config"; +import { devnet } from "@polkadot-api/descriptors" +import { PublicClient } from "viem"; +import { PolkadotSigner, TypedApi } from "polkadot-api"; +import { convertPublicKeyToSs58, } from "../src/address-utils" +import { ethers } from "ethers" +import { INEURON_ADDRESS, INeuronABI } from "../src/contracts/neuron" +import { generateRandomEthersWallet } from "../src/utils" +import { forceSetBalanceToSs58Address, forceSetBalanceToEthAddress, addNewSubnetwork } from "../src/subtensor" + +describe("Test the EVM chain ID", () => { + // init eth part + const wallet = generateRandomEthersWallet(); + + // init substrate part + const hotkey = getRandomSubstrateKeypair(); + const hotkey2 = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + let publicClient: PublicClient; + + let api: TypedApi + + // sudo account alice as signer + let alice: PolkadotSigner; + + before(async () => { + // init variables got from await and async + publicClient = await getPublicClient(ETH_LOCAL_URL) + const subClient = await getClient(SUB_LOCAL_URL) + api = await getDevnetApi() + alice = await getAliceSigner(); + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey2.publicKey)) + + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey)) + await forceSetBalanceToEthAddress(api, wallet.address) + + const netuid = await addNewSubnetwork(api, hotkey2, coldkey) + console.log("test on subnet ", netuid) + }) + + it("Burned register and check emission", async () => { + let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 + const uid = await api.query.SubtensorModule.SubnetworkN.getValue(netuid) + const contract = new ethers.Contract(INEURON_ADDRESS, INeuronABI, wallet); + + const tx = await contract.burnedRegister( + netuid, + hotkey.publicKey + ); + await tx.wait(); + + const uidAfterNew = await api.query.SubtensorModule.SubnetworkN.getValue(netuid) + assert.equal(uid + 1, uidAfterNew) + + const key = await api.query.SubtensorModule.Keys.getValue(netuid, uid) + assert.equal(key, convertPublicKeyToSs58(hotkey.publicKey)) + + let i = 0; + while (i < 10) { + const emission = await api.query.SubtensorModule.PendingEmission.getValue(netuid) + + console.log("emission is ", emission); + await new Promise((resolve) => setTimeout(resolve, 2000)); + i += 1; + } + }) +}); \ No newline at end of file diff --git a/evm-tests/test/neuron.precompile.reveal-weights.test.ts b/evm-tests/test/neuron.precompile.reveal-weights.test.ts new file mode 100644 index 000000000..85125f095 --- /dev/null +++ b/evm-tests/test/neuron.precompile.reveal-weights.test.ts @@ -0,0 +1,142 @@ +import * as assert from "assert"; +import { getAliceSigner, getDevnetApi, getRandomSubstrateKeypair } from "../src/substrate" +import { devnet } from "@polkadot-api/descriptors" +import { PolkadotSigner, TypedApi } from "polkadot-api"; +import { convertPublicKeyToSs58, convertH160ToSS58 } from "../src/address-utils" +import { Vec, Tuple, VecFixed, u16, u8, u64 } from "@polkadot/types-codec"; +import { TypeRegistry } from "@polkadot/types"; +import { ethers } from "ethers" +import { INEURON_ADDRESS, INeuronABI } from "../src/contracts/neuron" +import { generateRandomEthersWallet } from "../src/utils" +import { convertH160ToPublicKey } from "../src/address-utils" +import { blake2AsU8a } from "@polkadot/util-crypto" +import { + forceSetBalanceToEthAddress, forceSetBalanceToSs58Address, addNewSubnetwork, setCommitRevealWeightsEnabled, setWeightsSetRateLimit, burnedRegister, + setTempo, setCommitRevealWeightsInterval +} from "../src/subtensor" + +// hardcode some values for reveal hash +const uids = [1]; +const values = [5]; +const salt = [9]; +const version_key = 0; + +function getCommitHash(netuid: number, address: string) { + const registry = new TypeRegistry(); + let publicKey = convertH160ToPublicKey(address); + + const tupleData = new Tuple( + registry, + [ + VecFixed.with(u8, 32), + u16, + Vec.with(u16), + Vec.with(u16), + Vec.with(u16), + u64, + ], + [publicKey, netuid, uids, values, salt, version_key] + ); + + const hash = blake2AsU8a(tupleData.toU8a()); + return hash; +} + +describe("Test neuron precompile reveal weights", () => { + // init eth part + const wallet = generateRandomEthersWallet(); + + // init substrate part + const hotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + + let api: TypedApi + + // sudo account alice as signer + let alice: PolkadotSigner; + before(async () => { + // init variables got from await and async + api = await getDevnetApi() + alice = await getAliceSigner(); + + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(alice.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey)) + await forceSetBalanceToEthAddress(api, wallet.address) + let netuid = await addNewSubnetwork(api, hotkey, coldkey) + + console.log("test the case on subnet ", netuid) + + // enable commit reveal feature + await setCommitRevealWeightsEnabled(api, netuid, true) + // set it as 0, we can set the weight anytime + await setWeightsSetRateLimit(api, netuid, BigInt(0)) + + const ss58Address = convertH160ToSS58(wallet.address) + await burnedRegister(api, netuid, ss58Address, coldkey) + + const uid = await api.query.SubtensorModule.Uids.getValue( + netuid, + ss58Address + ) + // eth wallet account should be the first neuron in the subnet + assert.equal(uid, uids[0]) + }) + + it("EVM neuron commit weights via call precompile", async () => { + let totalNetworks = await api.query.SubtensorModule.TotalNetworks.getValue() + const subnetId = totalNetworks - 1 + const commitHash = getCommitHash(subnetId, wallet.address) + const contract = new ethers.Contract(INEURON_ADDRESS, INeuronABI, wallet); + const tx = await contract.commitWeights(subnetId, commitHash) + await tx.wait() + + const ss58Address = convertH160ToSS58(wallet.address) + + const weightsCommit = await api.query.SubtensorModule.WeightCommits.getValue(subnetId, ss58Address) + if (weightsCommit === undefined) { + throw new Error("submit weights failed") + } + assert.ok(weightsCommit.length > 0) + }) + + it("EVM neuron reveal weights via call precompile", async () => { + let totalNetworks = await api.query.SubtensorModule.TotalNetworks.getValue() + const netuid = totalNetworks - 1 + const contract = new ethers.Contract(INEURON_ADDRESS, INeuronABI, wallet); + // set tempo or epoch large, then enough time to reveal weight + await setTempo(api, netuid, 60000) + // set interval epoch as 0, we can reveal at the same epoch + await setCommitRevealWeightsInterval(api, netuid, BigInt(0)) + + const tx = await contract.revealWeights( + netuid, + uids, + values, + salt, + version_key + ); + await tx.wait() + const ss58Address = convertH160ToSS58(wallet.address) + + // check the weight commit is removed after reveal successfully + const weightsCommit = await api.query.SubtensorModule.WeightCommits.getValue(netuid, ss58Address) + assert.equal(weightsCommit, undefined) + + // check the weight is set after reveal with correct uid + const neuron_uid = await api.query.SubtensorModule.Uids.getValue( + netuid, + ss58Address + ) + + const weights = await api.query.SubtensorModule.Weights.getValue(netuid, neuron_uid) + + if (weights === undefined) { + throw new Error("weights not available onchain") + } + for (const weight of weights) { + assert.equal(weight[0], neuron_uid) + assert.ok(weight[1] !== undefined) + } + }) +}); \ No newline at end of file diff --git a/evm-tests/test/neuron.precompile.serve.axon-prometheus.test.ts b/evm-tests/test/neuron.precompile.serve.axon-prometheus.test.ts new file mode 100644 index 000000000..aee84f130 --- /dev/null +++ b/evm-tests/test/neuron.precompile.serve.axon-prometheus.test.ts @@ -0,0 +1,162 @@ +import * as assert from "assert"; +import { getAliceSigner, getClient, getDevnetApi, getRandomSubstrateKeypair } from "../src/substrate" +import { SUB_LOCAL_URL, } from "../src/config"; +import { devnet } from "@polkadot-api/descriptors" +import { PolkadotSigner, TypedApi } from "polkadot-api"; +import { convertPublicKeyToSs58, convertH160ToSS58 } from "../src/address-utils" +import { ethers } from "ethers" +import { INEURON_ADDRESS, INeuronABI } from "../src/contracts/neuron" +import { generateRandomEthersWallet } from "../src/utils" +import { forceSetBalanceToEthAddress, forceSetBalanceToSs58Address, addNewSubnetwork, burnedRegister } from "../src/subtensor" + +describe("Test neuron precompile Serve Axon Prometheus", () => { + // init eth part + const wallet1 = generateRandomEthersWallet(); + const wallet2 = generateRandomEthersWallet(); + const wallet3 = generateRandomEthersWallet(); + + // init substrate part + const hotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + + let api: TypedApi + + // sudo account alice as signer + let alice: PolkadotSigner; + before(async () => { + // init variables got from await and async + const subClient = await getClient(SUB_LOCAL_URL) + api = await getDevnetApi() + alice = await getAliceSigner(); + + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(alice.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey)) + await forceSetBalanceToEthAddress(api, wallet1.address) + await forceSetBalanceToEthAddress(api, wallet2.address) + await forceSetBalanceToEthAddress(api, wallet3.address) + let netuid = await addNewSubnetwork(api, hotkey, coldkey) + + console.log("test the case on subnet ", netuid) + + await burnedRegister(api, netuid, convertH160ToSS58(wallet1.address), coldkey) + await burnedRegister(api, netuid, convertH160ToSS58(wallet2.address), coldkey) + await burnedRegister(api, netuid, convertH160ToSS58(wallet3.address), coldkey) + }) + + it("Serve Axon", async () => { + let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 + const version = 0; + const ip = 1; + const port = 2; + const ipType = 4; + const protocol = 0; + const placeholder1 = 8; + const placeholder2 = 9; + + const contract = new ethers.Contract(INEURON_ADDRESS, INeuronABI, wallet1); + + const tx = await contract.serveAxon( + netuid, + version, + ip, + port, + ipType, + protocol, + placeholder1, + placeholder2 + ); + await tx.wait(); + + const axon = await api.query.SubtensorModule.Axons.getValue( + netuid, + convertH160ToSS58(wallet1.address) + ) + assert.notEqual(axon?.block, undefined) + assert.equal(axon?.version, version) + assert.equal(axon?.ip, ip) + assert.equal(axon?.port, port) + assert.equal(axon?.ip_type, ipType) + assert.equal(axon?.protocol, protocol) + assert.equal(axon?.placeholder1, placeholder1) + assert.equal(axon?.placeholder2, placeholder2) + }); + + it("Serve Axon TLS", async () => { + let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 + const version = 0; + const ip = 1; + const port = 2; + const ipType = 4; + const protocol = 0; + const placeholder1 = 8; + const placeholder2 = 9; + // certificate length is 65 + const certificate = new Uint8Array([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, + 57, 58, 59, 60, 61, 62, 63, 64, 65, + ]); + + const contract = new ethers.Contract(INEURON_ADDRESS, INeuronABI, wallet2); + + const tx = await contract.serveAxonTls( + netuid, + version, + ip, + port, + ipType, + protocol, + placeholder1, + placeholder2, + certificate + ); + await tx.wait(); + + const axon = await api.query.SubtensorModule.Axons.getValue( + netuid, + convertH160ToSS58(wallet2.address)) + + assert.notEqual(axon?.block, undefined) + assert.equal(axon?.version, version) + assert.equal(axon?.ip, ip) + assert.equal(axon?.port, port) + assert.equal(axon?.ip_type, ipType) + assert.equal(axon?.protocol, protocol) + assert.equal(axon?.placeholder1, placeholder1) + assert.equal(axon?.placeholder2, placeholder2) + }); + + it("Serve Prometheus", async () => { + let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 + const version = 0; + const ip = 1; + const port = 2; + const ipType = 4; + + const contract = new ethers.Contract(INEURON_ADDRESS, INeuronABI, wallet3); + + const tx = await contract.servePrometheus( + netuid, + version, + ip, + port, + ipType + ); + await tx.wait(); + + const prometheus = ( + await api.query.SubtensorModule.Prometheus.getValue( + netuid, + convertH160ToSS58(wallet3.address) + ) + ) + + assert.notEqual(prometheus?.block, undefined) + assert.equal(prometheus?.version, version) + assert.equal(prometheus?.ip, ip) + assert.equal(prometheus?.port, port) + assert.equal(prometheus?.ip_type, ipType) + }); +}); \ No newline at end of file diff --git a/evm-tests/test/neuron.precompile.set-weights.test.ts b/evm-tests/test/neuron.precompile.set-weights.test.ts new file mode 100644 index 000000000..393c2b97b --- /dev/null +++ b/evm-tests/test/neuron.precompile.set-weights.test.ts @@ -0,0 +1,65 @@ +import * as assert from "assert"; + +import { getDevnetApi, getRandomSubstrateKeypair } from "../src/substrate" +import { devnet } from "@polkadot-api/descriptors" +import { TypedApi } from "polkadot-api"; +import { convertH160ToSS58, convertPublicKeyToSs58, } from "../src/address-utils" +import { ethers } from "ethers" +import { INEURON_ADDRESS, INeuronABI } from "../src/contracts/neuron" +import { generateRandomEthersWallet } from "../src/utils" +import { + forceSetBalanceToSs58Address, forceSetBalanceToEthAddress, addNewSubnetwork, burnedRegister, setCommitRevealWeightsEnabled, + setWeightsSetRateLimit +} from "../src/subtensor" + +describe("Test neuron precompile contract, set weights function", () => { + // init eth part + const wallet = generateRandomEthersWallet(); + + // init substrate part + const hotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + + let api: TypedApi + + before(async () => { + api = await getDevnetApi() + + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey.publicKey)) + + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey)) + await forceSetBalanceToEthAddress(api, wallet.address) + + const netuid = await addNewSubnetwork(api, hotkey, coldkey) + console.log("test on subnet ", netuid) + + await burnedRegister(api, netuid, convertH160ToSS58(wallet.address), coldkey) + const uid = await api.query.SubtensorModule.Uids.getValue(netuid, convertH160ToSS58(wallet.address)) + assert.notEqual(uid, undefined) + // disable reveal and enable direct set weights + await setCommitRevealWeightsEnabled(api, netuid, false) + await setWeightsSetRateLimit(api, netuid, BigInt(0)) + }) + + it("Set weights is ok", async () => { + let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 + const uid = await api.query.SubtensorModule.Uids.getValue(netuid, convertH160ToSS58(wallet.address)) + + const contract = new ethers.Contract(INEURON_ADDRESS, INeuronABI, wallet); + const dests = [1]; + const weights = [2]; + const version_key = 0; + + const tx = await contract.setWeights(netuid, dests, weights, version_key); + + await tx.wait(); + const weightsOnChain = await api.query.SubtensorModule.Weights.getValue(netuid, uid) + + weightsOnChain.forEach((weight, _) => { + const uidInWeight = weight[0]; + const value = weight[1]; + assert.equal(uidInWeight, uid) + assert.ok(value > 0) + }); + }) +}); \ No newline at end of file diff --git a/evm-tests/test/staking.precompile.add-remove.test.ts b/evm-tests/test/staking.precompile.add-remove.test.ts new file mode 100644 index 000000000..5387e6242 --- /dev/null +++ b/evm-tests/test/staking.precompile.add-remove.test.ts @@ -0,0 +1,326 @@ +import * as assert from "assert"; +import { getDevnetApi, getRandomSubstrateKeypair } from "../src/substrate" +import { devnet } from "@polkadot-api/descriptors" +import { PolkadotSigner, TypedApi } from "polkadot-api"; +import { convertPublicKeyToSs58, convertH160ToSS58 } from "../src/address-utils" +import { raoToEth, tao } from "../src/balance-math" +import { ethers } from "ethers" +import { generateRandomEthersWallet, getPublicClient } from "../src/utils" +import { convertH160ToPublicKey } from "../src/address-utils" +import { + forceSetBalanceToEthAddress, forceSetBalanceToSs58Address, addNewSubnetwork, burnedRegister, + sendProxyCall, +} from "../src/subtensor" +import { ETH_LOCAL_URL } from "../src/config"; +import { ISTAKING_ADDRESS, ISTAKING_V2_ADDRESS, IStakingABI, IStakingV2ABI } from "../src/contracts/staking" +import { PublicClient } from "viem"; + +describe("Test neuron precompile reveal weights", () => { + // init eth part + const wallet1 = generateRandomEthersWallet(); + const wallet2 = generateRandomEthersWallet(); + let publicClient: PublicClient; + // init substrate part + const hotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + const proxy = getRandomSubstrateKeypair(); + + let api: TypedApi + + // sudo account alice as signer + let alice: PolkadotSigner; + before(async () => { + publicClient = await getPublicClient(ETH_LOCAL_URL) + // init variables got from await and async + api = await getDevnetApi() + + // await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(alice.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(proxy.publicKey)) + await forceSetBalanceToEthAddress(api, wallet1.address) + await forceSetBalanceToEthAddress(api, wallet2.address) + let netuid = await addNewSubnetwork(api, hotkey, coldkey) + + console.log("test the case on subnet ", netuid) + + await burnedRegister(api, netuid, convertH160ToSS58(wallet1.address), coldkey) + await burnedRegister(api, netuid, convertH160ToSS58(wallet2.address), coldkey) + }) + + it("Can add stake", async () => { + let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 + // ETH unit + let stakeBalance = raoToEth(tao(20)) + const stakeBefore = await api.query.SubtensorModule.Alpha.getValue(convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet1.address), netuid) + const contract = new ethers.Contract(ISTAKING_ADDRESS, IStakingABI, wallet1); + const tx = await contract.addStake(hotkey.publicKey, netuid, { value: stakeBalance.toString() }) + await tx.wait() + + const stakeFromContract = BigInt( + await contract.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet1.address), netuid) + ); + + assert.ok(stakeFromContract > stakeBefore) + const stakeAfter = await api.query.SubtensorModule.Alpha.getValue(convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet1.address), netuid) + assert.ok(stakeAfter > stakeBefore) + }) + + it("Can add stake V2", async () => { + let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 + // the unit in V2 is RAO, not ETH + let stakeBalance = tao(20) + const stakeBefore = await api.query.SubtensorModule.Alpha.getValue(convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet2.address), netuid) + const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet2); + const tx = await contract.addStake(hotkey.publicKey, stakeBalance.toString(), netuid) + await tx.wait() + + const stakeFromContract = BigInt( + await contract.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet2.address), netuid) + ); + + assert.ok(stakeFromContract > stakeBefore) + const stakeAfter = await api.query.SubtensorModule.Alpha.getValue(convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet2.address), netuid) + assert.ok(stakeAfter > stakeBefore) + }) + + it("Can not add stake if subnet doesn't exist", async () => { + // wrong netuid + let netuid = 12345; + let stakeBalance = raoToEth(tao(20)) + const stakeBefore = await api.query.SubtensorModule.Alpha.getValue(convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet1.address), netuid) + const contract = new ethers.Contract(ISTAKING_ADDRESS, IStakingABI, wallet1); + try { + const tx = await contract.addStake(hotkey.publicKey, netuid, { value: stakeBalance.toString() }) + await tx.wait() + assert.fail("Transaction should have failed"); + } catch (error) { + // Transaction failed as expected + } + + const stakeFromContract = BigInt( + await contract.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet1.address), netuid) + ); + assert.equal(stakeFromContract, stakeBefore) + const stakeAfter = await api.query.SubtensorModule.Alpha.getValue(convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet1.address), netuid) + assert.equal(stakeAfter, stakeBefore) + }); + + it("Can not add stake V2 if subnet doesn't exist", async () => { + // wrong netuid + let netuid = 12345; + // the unit in V2 is RAO, not ETH + let stakeBalance = tao(20) + const stakeBefore = await api.query.SubtensorModule.Alpha.getValue(convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet2.address), netuid) + const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet2); + + try { + const tx = await contract.addStake(hotkey.publicKey, stakeBalance.toString(), netuid); + await tx.wait(); + assert.fail("Transaction should have failed"); + } catch (error) { + // Transaction failed as expected + } + + const stakeFromContract = BigInt( + await contract.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet2.address), netuid) + ); + assert.equal(stakeFromContract, stakeBefore) + const stakeAfter = await api.query.SubtensorModule.Alpha.getValue(convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet2.address), netuid) + assert.equal(stakeAfter, stakeBefore) + }) + + it("Can get stake via contract read method", async () => { + let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 + + // TODO need check how to pass bytes32 as parameter of readContract + // const value = await publicClient.readContract({ + // address: ISTAKING_ADDRESS, + // abi: IStakingABI, + // functionName: "getStake", + // args: [hotkey.publicKey, // Convert to bytes32 format + // convertH160ToPublicKey(wallet1.address), + // netuid] + // }) + // if (value === undefined || value === null) { + // throw new Error("value of getStake from contract is undefined") + // } + // const intValue = BigInt(value.toString()) + + const contractV1 = new ethers.Contract(ISTAKING_ADDRESS, IStakingABI, wallet1); + const stakeFromContractV1 = BigInt( + await contractV1.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet1.address), netuid) + ); + + const contractV2 = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet1); + // unit from contract V2 is RAO, not ETH + const stakeFromContractV2 = Number( + await contractV2.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet1.address), netuid) + ); + + assert.equal(stakeFromContractV1, tao(stakeFromContractV2)) + + }) + + it("Can remove stake", async () => { + let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 + const contract = new ethers.Contract( + ISTAKING_ADDRESS, + IStakingABI, + wallet1 + ); + + const stakeBeforeRemove = BigInt( + await contract.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet1.address), netuid) + ); + + let stakeBalance = raoToEth(tao(10)) + const tx = await contract.removeStake(hotkey.publicKey, stakeBalance, netuid) + await tx.wait() + + const stakeAfterRemove = BigInt( + await contract.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet1.address), netuid) + ); + assert.ok(stakeAfterRemove < stakeBeforeRemove) + + }) + + it("Can remove stake V2", async () => { + let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 + const contract = new ethers.Contract( + ISTAKING_V2_ADDRESS, + IStakingV2ABI, + wallet2 + ); + + const stakeBeforeRemove = BigInt( + await contract.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet2.address), netuid) + ); + + let stakeBalance = tao(10) + const tx = await contract.removeStake(hotkey.publicKey, stakeBalance, netuid) + await tx.wait() + + const stakeAfterRemove = BigInt( + await contract.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet2.address), netuid) + ); + + assert.ok(stakeAfterRemove < stakeBeforeRemove) + }) + + it("Can add/remove proxy", async () => { + let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 + // add/remove are done in a single test case, because we can't use the same private/public key + // between substrate and EVM, but to test the remove part, we must predefine the proxy first. + // it makes `remove` being dependent on `add`, because we should use `addProxy` from contract + // to prepare the proxy for `removeProxy` testing - the proxy is specified for the + // caller/origin. + + // first, check we don't have proxies + const ss58Address = convertH160ToSS58(wallet1.address); + // the result include two items array, first one is delegate info, second one is balance + const initProxies = await api.query.Proxy.Proxies.getValue(ss58Address); + assert.equal(initProxies[0].length, 0); + + // intialize the contract + const contract = new ethers.Contract( + ISTAKING_ADDRESS, + IStakingABI, + wallet1 + ); + + // test "add" + let tx = await contract.addProxy(proxy.publicKey); + await tx.wait(); + + const proxiesAfterAdd = await api.query.Proxy.Proxies.getValue(ss58Address); + + assert.equal(proxiesAfterAdd[0][0].delegate, convertPublicKeyToSs58(proxy.publicKey)) + + let stakeBefore = await api.query.SubtensorModule.Alpha.getValue( + convertPublicKeyToSs58(hotkey.publicKey), + ss58Address, + netuid + ) + + const call = api.tx.SubtensorModule.add_stake({ + hotkey: convertPublicKeyToSs58(hotkey.publicKey), + netuid: netuid, + amount_staked: tao(1) + }) + await sendProxyCall(api, call.decodedCall, ss58Address, proxy) + + let stakeAfter = await api.query.SubtensorModule.Alpha.getValue( + convertPublicKeyToSs58(hotkey.publicKey), + ss58Address, + netuid + ) + + assert.ok(stakeAfter > stakeBefore) + // test "remove" + tx = await contract.removeProxy(proxy.publicKey); + await tx.wait(); + + const proxiesAfterRemove = await api.query.Proxy.Proxies.getValue(ss58Address); + assert.equal(proxiesAfterRemove[0].length, 0) + }); + + it("Can add/remove proxy V2", async () => { + let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 + // add/remove are done in a single test case, because we can't use the same private/public key + // between substrate and EVM, but to test the remove part, we must predefine the proxy first. + // it makes `remove` being dependent on `add`, because we should use `addProxy` from contract + // to prepare the proxy for `removeProxy` testing - the proxy is specified for the + // caller/origin. + + // first, check we don't have proxies + const ss58Address = convertH160ToSS58(wallet1.address); + // the result include two items array, first one is delegate info, second one is balance + const initProxies = await api.query.Proxy.Proxies.getValue(ss58Address); + assert.equal(initProxies[0].length, 0); + + // intialize the contract + // const signer = new ethers.Wallet(fundedEthWallet.privateKey, provider); + const contract = new ethers.Contract( + ISTAKING_V2_ADDRESS, + IStakingV2ABI, + wallet1 + ); + + // test "add" + let tx = await contract.addProxy(proxy.publicKey); + await tx.wait(); + + const proxiesAfterAdd = await api.query.Proxy.Proxies.getValue(ss58Address); + + assert.equal(proxiesAfterAdd[0][0].delegate, convertPublicKeyToSs58(proxy.publicKey)) + + let stakeBefore = await api.query.SubtensorModule.Alpha.getValue( + convertPublicKeyToSs58(hotkey.publicKey), + ss58Address, + netuid + ) + + const call = api.tx.SubtensorModule.add_stake({ + hotkey: convertPublicKeyToSs58(hotkey.publicKey), + netuid: netuid, + amount_staked: tao(1) + }) + + await sendProxyCall(api, call.decodedCall, ss58Address, proxy) + + let stakeAfter = await api.query.SubtensorModule.Alpha.getValue( + convertPublicKeyToSs58(hotkey.publicKey), + ss58Address, + netuid + ) + + assert.ok(stakeAfter > stakeBefore) + // test "remove" + tx = await contract.removeProxy(proxy.publicKey); + await tx.wait(); + + const proxiesAfterRemove = await api.query.Proxy.Proxies.getValue(ss58Address); + assert.equal(proxiesAfterRemove[0].length, 0) + }); +}); diff --git a/evm-tests/test/staking.precompile.reward.test.ts b/evm-tests/test/staking.precompile.reward.test.ts new file mode 100644 index 000000000..3600a6d08 --- /dev/null +++ b/evm-tests/test/staking.precompile.reward.test.ts @@ -0,0 +1,105 @@ +import * as assert from "assert"; +import { getDevnetApi, getRandomSubstrateKeypair } from "../src/substrate" +import { devnet } from "@polkadot-api/descriptors" +import { TypedApi } from "polkadot-api"; +import { convertPublicKeyToSs58 } from "../src/address-utils" +import { tao } from "../src/balance-math" +import { + forceSetBalanceToSs58Address, addNewSubnetwork, burnedRegister, + setTxRateLimit, setTempo, setWeightsSetRateLimit, setSubnetOwnerCut, setMaxAllowedUids, + setMinDelegateTake, becomeDelegate, setActivityCutoff, addStake, setWeight, rootRegister +} from "../src/subtensor" + +describe("Test neuron precompile reveal weights", () => { + const hotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + + const validator = getRandomSubstrateKeypair(); + const miner = getRandomSubstrateKeypair(); + const nominator = getRandomSubstrateKeypair(); + + let api: TypedApi + + before(async () => { + const root_netuid = 0; + const root_tempo = 1; // neet root epoch to happen before subnet tempo + const subnet_tempo = 1; + api = await getDevnetApi() + + // await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(alice.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(validator.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(miner.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(nominator.publicKey)) + // await forceSetBalanceToEthAddress(api, wallet1.address) + // await forceSetBalanceToEthAddress(api, wallet2.address) + let netuid = await addNewSubnetwork(api, hotkey, coldkey) + + console.log("test the case on subnet ", netuid) + + await setTxRateLimit(api, BigInt(0)) + await setTempo(api, root_netuid, root_tempo) + await setTempo(api, netuid, subnet_tempo) + await setWeightsSetRateLimit(api, netuid, BigInt(0)) + + await burnedRegister(api, netuid, convertPublicKeyToSs58(validator.publicKey), coldkey) + await burnedRegister(api, netuid, convertPublicKeyToSs58(miner.publicKey), coldkey) + await burnedRegister(api, netuid, convertPublicKeyToSs58(nominator.publicKey), coldkey) + await setSubnetOwnerCut(api, 0) + await setActivityCutoff(api, netuid, 65535) + await setMaxAllowedUids(api, netuid, 65535) + await setMinDelegateTake(api, 0) + await becomeDelegate(api, convertPublicKeyToSs58(validator.publicKey), coldkey) + await becomeDelegate(api, convertPublicKeyToSs58(miner.publicKey), coldkey) + }) + + it("Staker receives rewards", async () => { + let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 + + await addStake(api, netuid, convertPublicKeyToSs58(miner.publicKey), tao(1), coldkey) + await addStake(api, netuid, convertPublicKeyToSs58(nominator.publicKey), tao(1), coldkey) + + await addStake(api, netuid, convertPublicKeyToSs58(validator.publicKey), tao(100), coldkey) + + const miner_alpha_before_emission = await api.query.SubtensorModule.Alpha.getValue( + convertPublicKeyToSs58(miner.publicKey), + convertPublicKeyToSs58(coldkey.publicKey), + netuid + ) + + await setWeight(api, netuid, [0, 1], [0xffff, 0xffff], BigInt(0), validator) + await rootRegister(api, convertPublicKeyToSs58(validator.publicKey), coldkey) + + let index = 0; + while (index < 60) { + const pending = await api.query.SubtensorModule.PendingEmission.getValue(netuid); + if (pending > 0) { + console.log("pending amount is ", pending); + break; + } + + await new Promise((resolve) => setTimeout(resolve, 1000)); + console.log("wait for the pendingEmission update"); + index += 1; + } + + index = 0; + while (index < 60) { + let miner_current_alpha = await api.query.SubtensorModule.Alpha.getValue( + convertPublicKeyToSs58(miner.publicKey), + convertPublicKeyToSs58(coldkey.publicKey), + netuid + ) + + if (miner_current_alpha > miner_alpha_before_emission) { + console.log("miner got reward"); + break; + } + + await new Promise((resolve) => setTimeout(resolve, 1000)); + console.log(" waiting for emission"); + index += 1; + } + }) +}) diff --git a/evm-tests/test/subnet.precompile.hyperparameter.test.ts b/evm-tests/test/subnet.precompile.hyperparameter.test.ts new file mode 100644 index 000000000..af3e12cb2 --- /dev/null +++ b/evm-tests/test/subnet.precompile.hyperparameter.test.ts @@ -0,0 +1,442 @@ +import * as assert from "assert"; + +import { getDevnetApi, getRandomSubstrateKeypair } from "../src/substrate" +import { devnet } from "@polkadot-api/descriptors" +import { TypedApi } from "polkadot-api"; +import { convertPublicKeyToSs58 } from "../src/address-utils" +import { generateRandomEthersWallet } from "../src/utils"; +import { ISubnetABI, ISUBNET_ADDRESS } from "../src/contracts/subnet" +import { ethers } from "ethers" +import { forceSetBalanceToEthAddress, forceSetBalanceToSs58Address } from "../src/subtensor" + +describe("Test the EVM chain ID", () => { + // init eth part + const wallet = generateRandomEthersWallet(); + // init substrate part + + const hotkey1 = getRandomSubstrateKeypair(); + const hotkey2 = getRandomSubstrateKeypair(); + let api: TypedApi + + before(async () => { + // init variables got from await and async + api = await getDevnetApi() + + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey1.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey2.publicKey)) + await forceSetBalanceToEthAddress(api, wallet.address) + }) + + it("Can register network without identity info", async () => { + const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + + const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + const tx = await contract.registerNetwork(hotkey1.publicKey); + await tx.wait(); + + const totalNetworkAfterAdd = await api.query.SubtensorModule.TotalNetworks.getValue() + assert.ok(totalNetwork + 1 === totalNetworkAfterAdd) + }); + + it("Can register network with identity info", async () => { + const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + + const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + const tx = await contract.registerNetwork(hotkey2.publicKey, + "name", + "repo", + "contact", + "subnetUrl", + "discord", + "description", + "additional" + ); + await tx.wait(); + + const totalNetworkAfterAdd = await api.query.SubtensorModule.TotalNetworks.getValue() + assert.ok(totalNetwork + 1 === totalNetworkAfterAdd) + }); + + it("Can set subnet parameter", async () => { + + const totalNetwork = await api.query.SubtensorModule.TotalNetworks.getValue() + const contract = new ethers.Contract(ISUBNET_ADDRESS, ISubnetABI, wallet); + const netuid = totalNetwork - 1; + + // servingRateLimit hyperparameter + { + const newValue = 100; + const tx = await contract.setServingRateLimit(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.ServingRateLimit.getValue(netuid) + + + let valueFromContract = Number( + await contract.getServingRateLimit(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + } + + // minDifficulty hyperparameter + // + // disabled: only by sudo + // + // newValue = 101; + // tx = await contract.setMinDifficulty(netuid, newValue); + // await tx.wait(); + + // await usingApi(async (api) => { + // onchainValue = Number( + // await api.query.subtensorModule.minDifficulty(netuid) + // ); + // }); + + // valueFromContract = Number(await contract.getMinDifficulty(netuid)); + + // expect(valueFromContract).to.eq(newValue); + // expect(valueFromContract).to.eq(onchainValue); + + // maxDifficulty hyperparameter + + { + const newValue = 102; + const tx = await contract.setMaxDifficulty(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.MaxDifficulty.getValue(netuid) + + + let valueFromContract = Number( + await contract.getMaxDifficulty(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + } + + // weightsVersionKey hyperparameter + { + const newValue = 103; + const tx = await contract.setWeightsVersionKey(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.WeightsVersionKey.getValue(netuid) + + + let valueFromContract = Number( + await contract.getWeightsVersionKey(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + } + // weightsSetRateLimit hyperparameter + { + const newValue = 104; + const tx = await contract.setWeightsSetRateLimit(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.WeightsSetRateLimit.getValue(netuid) + + + let valueFromContract = Number( + await contract.getWeightsSetRateLimit(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + } + + // adjustmentAlpha hyperparameter + { + const newValue = 105; + const tx = await contract.setAdjustmentAlpha(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.AdjustmentAlpha.getValue(netuid) + + + let valueFromContract = Number( + await contract.getAdjustmentAlpha(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + } + + // maxWeightLimit hyperparameter + { + const newValue = 106; + const tx = await contract.setMaxWeightLimit(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.MaxWeightsLimit.getValue(netuid) + + + let valueFromContract = Number( + await contract.getMaxWeightLimit(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + } + // immunityPeriod hyperparameter + { + const newValue = 107; + const tx = await contract.setImmunityPeriod(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.ImmunityPeriod.getValue(netuid) + + + let valueFromContract = Number( + await contract.getImmunityPeriod(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + } + + // minAllowedWeights hyperparameter + { + const newValue = 108; + const tx = await contract.setMinAllowedWeights(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.MinAllowedWeights.getValue(netuid) + + + let valueFromContract = Number( + await contract.getMinAllowedWeights(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + } + + // kappa hyperparameter + { + const newValue = 109; + const tx = await contract.setKappa(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.Kappa.getValue(netuid) + + + let valueFromContract = Number( + await contract.getKappa(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + } + + // rho hyperparameter + { + const newValue = 110; + const tx = await contract.setRho(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.Rho.getValue(netuid) + + + let valueFromContract = Number( + await contract.getRho(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + } + + // activityCutoff hyperparameter + { + const newValue = 111; + const tx = await contract.setActivityCutoff(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.ActivityCutoff.getValue(netuid) + + + let valueFromContract = Number( + await contract.getActivityCutoff(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + } + + // networkRegistrationAllowed hyperparameter + { + const newValue = true; + const tx = await contract.setNetworkRegistrationAllowed(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.NetworkRegistrationAllowed.getValue(netuid) + + + let valueFromContract = Boolean( + await contract.getNetworkRegistrationAllowed(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + } + + // networkPowRegistrationAllowed hyperparameter + { + const newValue = true; + const tx = await contract.setNetworkPowRegistrationAllowed(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.NetworkPowRegistrationAllowed.getValue(netuid) + + + let valueFromContract = Boolean( + await contract.getNetworkPowRegistrationAllowed(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + } + + // minBurn hyperparameter. only sudo can set it now + // newValue = 112; + + // tx = await contract.setMinBurn(netuid, newValue); + // await tx.wait(); + + // await usingApi(async (api) => { + // onchainValue = Number( + // await api.query.subtensorModule.minBurn(netuid) + // ); + // }); + + // valueFromContract = Number(await contract.getMinBurn(netuid)); + + // expect(valueFromContract).to.eq(newValue); + // expect(valueFromContract).to.eq(onchainValue); + + // maxBurn hyperparameter + { + const newValue = 113; + const tx = await contract.setMaxBurn(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.MaxBurn.getValue(netuid) + + + let valueFromContract = Number( + await contract.getMaxBurn(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + } + + + // difficulty hyperparameter (disabled: sudo only) + // newValue = 114; + + // tx = await contract.setDifficulty(netuid, newValue); + // await tx.wait(); + + // await usingApi(async (api) => { + // onchainValue = Number( + // await api.query.subtensorModule.difficulty(netuid) + // ); + // }); + + // valueFromContract = Number(await contract.getDifficulty(netuid)); + + // expect(valueFromContract).to.eq(newValue); + // expect(valueFromContract).to.eq(onchainValue); + + // bondsMovingAverage hyperparameter + { + const newValue = 115; + const tx = await contract.setBondsMovingAverage(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.BondsMovingAverage.getValue(netuid) + + + let valueFromContract = Number( + await contract.getBondsMovingAverage(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + } + + + // commitRevealWeightsEnabled hyperparameter + { + const newValue = true; + const tx = await contract.setCommitRevealWeightsEnabled(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.CommitRevealWeightsEnabled.getValue(netuid) + + + let valueFromContract = Boolean( + await contract.getCommitRevealWeightsEnabled(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + } + + // liquidAlphaEnabled hyperparameter + { + const newValue = true; + const tx = await contract.setLiquidAlphaEnabled(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.LiquidAlphaOn.getValue(netuid) + + + let valueFromContract = Boolean( + await contract.getLiquidAlphaEnabled(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + } + + // alphaValues hyperparameter + { + const newValue = [118, 52429]; + const tx = await contract.setAlphaValues(netuid, newValue[0], newValue[1]); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.AlphaValues.getValue(netuid) + + let value = await contract.getAlphaValues(netuid) + let valueFromContract = [Number(value[0]), Number(value[1])] + + assert.equal(valueFromContract[0], newValue[0]) + assert.equal(valueFromContract[1], newValue[1]) + assert.equal(valueFromContract[0], onchainValue[0]); + assert.equal(valueFromContract[1], onchainValue[1]); + } + + // commitRevealWeightsInterval hyperparameter + { + const newValue = 119; + const tx = await contract.setCommitRevealWeightsInterval(netuid, newValue); + await tx.wait(); + + let onchainValue = await api.query.SubtensorModule.RevealPeriodEpochs.getValue(netuid) + + let valueFromContract = Number( + await contract.getCommitRevealWeightsInterval(netuid) + ); + + assert.equal(valueFromContract, newValue) + assert.equal(valueFromContract, onchainValue); + } + }) +}); \ No newline at end of file diff --git a/evm-tests/tsconfig.json b/evm-tests/tsconfig.json new file mode 100644 index 000000000..c9c555d96 --- /dev/null +++ b/evm-tests/tsconfig.json @@ -0,0 +1,111 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} From 1c8e45af6a423e1dfd15511ac21aabceb05393d8 Mon Sep 17 00:00:00 2001 From: open-junius Date: Mon, 10 Mar 2025 20:57:28 +0800 Subject: [PATCH 2/7] fix comments --- evm-tests/.gitignore | 1 + evm-tests/.papi/descriptors/.gitignore | 3 - evm-tests/.papi/descriptors/package.json | 24 - evm-tests/.papi/metadata/devnet.scale | Bin 222946 -> 0 bytes evm-tests/.papi/polkadot-api.json | 11 - evm-tests/README.md | 24 +- evm-tests/package.json | 2 +- evm-tests/src/address-utils.ts | 13 +- evm-tests/src/balance-math.ts | 4 +- evm-tests/src/bridgeToken.ts | 633 ------------------ evm-tests/src/config.ts | 1 + evm-tests/src/contracts/bridgeToken.ts | 631 +++++++++++++++++ evm-tests/src/main.ts | 6 - evm-tests/src/substrate.ts | 3 +- evm-tests/test/eth.bridgeToken.deploy.test.ts | 6 +- evm-tests/test/eth.substrate-transfer.test.ts | 4 +- 16 files changed, 668 insertions(+), 698 deletions(-) delete mode 100644 evm-tests/.papi/descriptors/.gitignore delete mode 100644 evm-tests/.papi/descriptors/package.json delete mode 100644 evm-tests/.papi/metadata/devnet.scale delete mode 100644 evm-tests/.papi/polkadot-api.json delete mode 100644 evm-tests/src/bridgeToken.ts create mode 100644 evm-tests/src/contracts/bridgeToken.ts delete mode 100644 evm-tests/src/main.ts diff --git a/evm-tests/.gitignore b/evm-tests/.gitignore index 37d7e7348..661f94a6e 100644 --- a/evm-tests/.gitignore +++ b/evm-tests/.gitignore @@ -1,2 +1,3 @@ node_modules +.papi .env diff --git a/evm-tests/.papi/descriptors/.gitignore b/evm-tests/.papi/descriptors/.gitignore deleted file mode 100644 index 46d96ea47..000000000 --- a/evm-tests/.papi/descriptors/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -* -!.gitignore -!package.json \ No newline at end of file diff --git a/evm-tests/.papi/descriptors/package.json b/evm-tests/.papi/descriptors/package.json deleted file mode 100644 index f6205b1c9..000000000 --- a/evm-tests/.papi/descriptors/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "version": "0.1.0-autogenerated.15932613768666598877", - "name": "@polkadot-api/descriptors", - "files": [ - "dist" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "module": "./dist/index.mjs", - "import": "./dist/index.mjs", - "require": "./dist/index.js" - }, - "./package.json": "./package.json" - }, - "main": "./dist/index.js", - "module": "./dist/index.mjs", - "browser": "./dist/index.mjs", - "types": "./dist/index.d.ts", - "sideEffects": false, - "peerDependencies": { - "polkadot-api": "*" - } -} diff --git a/evm-tests/.papi/metadata/devnet.scale b/evm-tests/.papi/metadata/devnet.scale deleted file mode 100644 index 5577c1ff81b4d9975ee6cdcbb07de2330335c7a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 222946 zcmeFa4`^i9c{hBI_O7i>lUJZC3r7C-hcgWk|sTwmX;HKNhY z#(uq7%gt?W)>_r(V(Ijwp67eM2|nAJc``Wt=nMSl9SJUu+i5h&N(Js{CQt=mVY^->g;R)=t!jhfQ!L+B(nS z@pZjXyIwAJew~Y3TU+JLa#U@$KR;Z;03#+loaYT=nn%5nq1k$|Qi+=D8^uZygU2Jy z{dyE2GG#8#cpRjVKy5?mN!}jVO*9*P~MQ%h9by?ekGJ`_;%+rFbnKGZPDY z%|`J8KX~3KW^s=<)@K%NkNRF7li5m0v@AH*n9Q*#7$$3L&v|4NI6e~I7uX!d{t4EDuvs|mXZ^n0uRUoX^*q0}~ z+|UH*U^gmX+iu3=`tSPnM@OHLZ?NOUz}9-Ryc12Cq1NNmo|(V~)wb5-^5@-G-jwIv zGwMwb&ej{{opQ5$Jz5t;4_|!rvB$fp32Ph}^k#=Saszt~*kjkCYSbuij+V;TqPRIa zFaO|8GhC@%i_e~YschStB2lYiD zz<8m0tz3;3OP)7l#yCuC=8SibH=aCj6Oa4e@M=`7+{BZh%H`&E)bO*Td0xp*ZJ~O- z+^AJ|K;~EL*r=$)BX<$!4`&d6E@9Ox-b%;$TeyzruS7V`r9pqetbrD$K?@`G?P44~ zXUxb7{{RvqKDcK51<#B&YxVMG{4F!eHLn4qYlF{LYb6}&)X)T%_j_Z9P!g^*yN z>%Duu+*+gD+QBYibA0a|^9%dVW(}MMQ@S2?yp#)iJh51fo8(BR&R|oqo&@BjXlDbw z&-cEYK;F@>w#)T?kh4!@ARqIlSEDNCnE-wF0|I^YjQ0_5>}s=IDL40h?|aFfw)87I zLGPnD53N$o_uey=1gE81$NbNDLtb#HRcV&v@-^T4ezNyF_Peg<@ACp2UQd2czgR@PY!#KTGz^P zvw??ymdvGLzw3JbelK^vwgdJ^dc_9++`N(yM)GynTfgm1%$0V^RpJ~k{UUj($Zyw) z_qgk&-|>Rg;#LI02AYnVblLcFeFxumy)ohi3uH@CYsdHQGv--4wa9*HU+^CAh8CW_ z07SG<;~UBCd7U0n`tL8*UQjOo;OoYYA)g<1k9^*)gW?j z1)H^o3)Ee7n+QD7mXYY`4jla@1?v{WJ zK_I2b@%;gRrI6pm7lmNA7zZsC2jRr!XxHj$z8Nd3*5;Y1wR}EN;UF!x=Yndq8`$3k z!e%3)KnbTmT?VTv9SX$vpT#7z5CEVK0HFJwV&g^PVH7eHx?1P>W*7ozd)ka)Ru}P? zchs*hSA$yhh4WYIkP4+}uGQSGHK17lH1tCVqYgM`8Yg%2 z#r5mpk>LL0W_Y7kt9bWdy;Yz{fXHHt38c`k3yB06sT6J z9_5VFj?Bm=Rj)$2`J#$>2C(;I5_-uDLqI_lEc6>Cp&R?^(CB4R;2=A5%c=I}8E^cO zQ{B7)C=C4o>VRf&=(z~vdV`m1c-Pw=%xGK5n3h&&R3*XFQ|F7-YOT3ctG(E&bIfza z(sH8=1qox0Izd@6IsH@LKjMipMOB`JQLdJ&*C5q%muujps>(x`J-b$`VFmkCt3>OE zjy1AYdoil|UfoQ{UkdMDu)IdOxxEuL%bUK}G_&bPePYm?&Zq#zitp{2tWr>{=yS)s z@rAwZVhct}34&|le67)F)u}c>LglVji`Q}PiW`*(f@tPSt=7C;YtB_FwOu@ZpXXJw zNNZa%^x}~+2!C(T47O&dSn%>C)V@>z9`2uP!aDFI>5@d}V#%^7-ZYh57Z> z#XnepIaSZX&92-`oW0Phmg4Npuqn&c%DzJ-Hf8EugehJEo_AV1*cZEL=jC{NN4%-4 z)fcO^-RfK%M@?+R=mjxXum@OjtcC;@c#n5hcUxb)SgVx0d%bC(U||oZvLtvJ1S7B1 zHgVRyW1iO>oQBR`Og5)$W6aR_Rq}%^Oc3N0EaRNY-^QDnFJFh8uT{_O|3TEKk%srO zc<8l%W^ihW2E~=ANP!KAW0z}7#eFgy&-=pAj1@svqimB1o*9}Ut5iPFEJqXumMb*( z*MXXs%(VM;e+hCN(%?&YBS;>a2%>6hC#Y?Kd6i?!wqAlA@Ur(Zrabt{ zsQ1dyidgg3qFJ~27{ZiCS6UmD@+KaHF*O^N9)0YwQ%~gc?%OYQ8K-SX9E4c)7oqZx zEmTW97K437j)8EFB4=rj;zZ~3R)lWO&i8 z@k#`DNux}?N4z^9WPi-X5_}V77*yb5>#*ONt@u?lx+;Ho_Y7V#UGvT0g_?(*r=Dey ze7O;Wd8i7pW|_(!{eZwL@YX9oEAYAdFUzcu(n{sUb&g9A+gb&e5C zl$*gO#@rCPhszO*tblTXIC3EQ;#O97pZ`WD>TsSEZYS~rYfqH?o-FgX{K8yFBAztF|gR2>Y1t5yB$R^a`% zzug6|W#NQYPrVg*ztdxWIK6PrhofuTMNwFK%FLd-JJJJMQ=o)Pvg@P`Zu*5iHjkCU0l6(Gn(J4Bkdu9QMfzH%^2=nm{RdD z7}VypQyEk;nZe-;5Q@~tL2n|lsXo6p$ znr#&;G=N9Vl%34-7M;vys9COee(4Q9XXb*;EGSoSjuI$1OHhAzx67N`K>+|1d&LqC zjuV^UqBMuVMmH-hni+(Vdp^zJ;bPH0et!}aU044t*MMdAzYMoH03U5=W&wZ@Bc zoXVhg10j-A2za=E{pX9~g{kUOU>bo;I)r5+@4;TCZtA-?5VheO@*AJF5-FU3lJR0B zql|c+3SjpaqZ+F5+`o1{1O9UcYRijJYSakQp}dkgr(zF&uuAF0Xx|AVIBu+VvR!NP zdqNS$rn*ZwCG)eZ`2aIK;?H%?uuCZSoyVr@#GxN= zs^MLMMI-KZ4inB2vgI~CQ8ef;U08hf(n3CflOv!JAM8iX`Afw;>-4#bfqLQgnE&=p zaW4RJX#>0YSib;GE4mijFiN1hS>nLx1@t5S_o(0!u_);cgHw54AT8J zpq45>>c2strR1Uh;KH@J(jUUirdhEF6|`}^SZM=k34X}`rYPdc!&Ft}X>zE}Nrv0H zsTy`hl=>jh_xaz}{nMud_&W~&jxioOd(K-s@LGfNltzoHN%UmU`6Gt+1TXbEe?e}@2LeG<08Zcyj5 zZIIfIR`{$gnTPZWOo{HZW#{^U|6N_L5-4M}hn=o)esO093u^9zrpmQauNnWY|7zJj zL<+)FJ-2A>94z=45(fyFg9jAR$~I_rlK+?g#s(h@AU8yq>Z3hD&I-sCL~}b2zD4G! z|B3>GHO6OpLU|s?$I?!+s3|@#ZV`DDe2@rCQx9_#l~6*PKnW-2_G%y? zCe5T=y;FSr5C1)3DtHtp6wmVDgNyEnbA^*{)9$QF6rP0}9ASa&sD)`CJi`C&zt0g6 zyR|Re$~f~2(4(nE0!^)mDDuYq*CYF7AnYnu(yUaR!WLVchtu;>CAtQh6NPQ4TgT|c zhLf@%_rFjKtbU`%2?0s?K%g)vz;I?h+A4ziuR)8$tP;iX|M3f4ZApI(M9I^m9h*HD+VACuGcN<7-7n9q?PV8_sE^tJ|DXRQH!2sMHL^0R-Co;sG)v0r z3F)~1-7F+Wl1gj+;hx_vS4tqdzHlen;coJIlAh3TAGjAmfmy(-nWHf2Gc!Vr!FE`f z{YC6gzxPhyz2aW)_hvg@LQYnvAz}IO|Mcg(Mjb&eOc_Sr>jptE&EJC|j#KRA{1vK= zrJ88&JZ+^~H42xqI*w)7DIvO`_euYqtWqOPQ#I5_ke%STbXGMDpM@~KDcGU^-pcYb zR=WnF(4Zz$J_LTS7VXq~YCw;q#|HU(^;+AxXF)|JDaZD z()3jOLB`9NsJ<;cIg6EWUQE-qZTJeeVbuzySU9Jk*5fd#Nu1$y8Nmi)4P@gCr?9E{ zR9B+UwaPF#1T@rzO{maWz>oUJ3|>p+r=`b+ea{YQ%{%T8(kQ5-w9Wits-MqQkjjS< zx=?Z2e&BlK)~R!K-{-X6d@4d9q_hN)&Zqr z%`WdEXA@eptlX0SV}6~FT5}gcXb5)b%T|trOP8(R1B-RaTf(Xy_iylB%S)~I6l14+ z%7H0ZFkKp30_&6hm$I;WYi;40g#&Iv-{^S+9Az;UA2a^zSs;$9&}^Wz*u>**Ml*_% zbWIi?Px!AAgkvnJ5~Ub&Dg)xq;`4E{xPp{5NpH+pPs!Ty{vKg+8afA1qCRbBGfiGQ z?|j6!pnp861R$JO`9{L!$(kI*PzG3ZPVZY7VD`WP5;uA{eNp^OtGi4Pf}3&7I(0fI z*DnzfE=06APwmt`X(n*k*HxG5d`n2Q{VhCf6)!UZ5AMysh==WqPx)({jaA>sPIUH^ zIa%SFJuYYbA2`xTFfG`(+z8&F)oj1&)?-B#E1Ul@oTaj*|-TE|S_sA@ip> z6u*?SJI6#l`)tPURm%A$M8EZCA#sOvQJSlk*66KQm+e~Zao*w`8M_c*yQR`X)YZ~ zohrzqm8df^SM6Y(JfPW|!tB%Y{_CAfuzr|ibR_f25G&xbqO~JKE8V4T-Ny=QXD@kg znF0$UCQ$2=3$na!@{G6Oe-BVat)rx&r2^>U6LEY$byLR<4Y|xrddNyQfrAFV;D3ed zK5Rr7I8LJ}?ul}Wp}Ta(Pbsjf4Kq0bq-t=2$2e-9JPKf>=`>%Yo>Oo&CQ5261#S7#mG0zz&ORmFtm3 zS3)3AH7tkhSgHSm4#ITXE2=TQwny>W1_~FA{^1szNoWoeMxGb5hoWJt)Y3uXSf(xYA z9B}TYjh3lh9(i9MKiP<`qnInWe%jf6ScwZhW$Oa4S{7~qeCYk`WN_jHV#_B^_$grfAkRgN0rFZh&>DS7ekQLKI&rX#9gpavyI zXEM2Dl(V;imF1B)>|4JfNSL+DxgwiLZP{PgDNKwr2$GI;MGGO)zv zCY0>G^8%k^_2^Bk{aa?7Vvq%#XS~mheESdtY!EDHkO$|NgoZgoJh&m{KtwT*#9VM4 ziX@yWzy*&6awXdi2b0N@sagcVz-S^g%~jZyf){8h(4hBiGf!Ob8|0+SBE{njYa2U6 z;g#X{=AdB`4;Xk?OKO7&%tKFNJefH`u&y_wlN{80OxB@?U*=EtVLy^dAZghFLvW0~ z6)A1fv>`xvBt7VW8ot>rEEQEKaGLh`m++yKXJ1w81#G zIEwkwY38=nKE!R za2bSL!|$gHBf^5MD9tla4&>r+Wpy44d|En zl4w6&?~FH{#~+sYC8?m61`}!|BtLR6>q&Ct=mrbD!9_EN;JjGM6W37LU6sMe$L$H% zXF=u@U^n`NH6JFGK^CzT8+iy6#B3omo5Ex`@^lUCYNRiIpXVB#*GBMroiE%TRz~Dq zOZ5KUGusg`t*6ipAx{LTu?o%5euQ2dZP+VtHmMgI4+MN64$WY-*72;&Jbku`{1^M^ z(ZlenlYsBP6RkO9n;`qXdhG!N)FK?UA50{Z5YLSp#%>3YPqz{_507HoI&zW-3V)10jS6qTNIuQ(gHu>4|y9%2NY8epTWe2@c# zS}T?7;fx-eU&S(x`eXz#s1C`9mRlg88^n^FA#s}9SkUNs)Q>T^aXb6b<7mE;Jn7Y3}i_V)<ZX&1@>oIy@^b}O1mPtv%4dmXBNR`Tu?S64cNhx{~S}^h;@Vkh2 zDD;eNlljAX52uyNtZxZ5p6~aBWZg&y!?F|`Tm1^4aDftTqH^`!5BHRW_8YaQ-%Bd7Z6BDnLyBHQ zXT{Q#U^WZzDTPL?9yruO!!cVx-_=IL8@y`dRZ^xi^q_N)`%>^ZlN50z_UQ;jm*lfr zT1n%NJ{I?;vVH+^- z2ZN(gO~q0G#o;t<=)^Y1ssDH?gJ{jlz{p-H2ED)%P&!kw)ss0+WUTXki1+>bKj3hR zkEDqPLYFAO@25t9sXU8t67)IYWp2N2hM&AQluIggO0_n^le(p0n|)=_L9SF=S5>83 zj}afU{^+=%OlJpONt`+-iCT2yhMr%CLXhNbF^wJ;V(rDCUMX%ySvFY#dlk~u`k$*saN#w4uNLq!dSnv3T#Pyddh^@#uOT<}@WbG;b9FtGx9 zh?@Dl{bEuHq!ZbPTH4=FdK|dEQaz|7cRAX1t*C~~xT;Lx9UbWQOyGcY{W4C15fjxn z!Fl|Bk)3bQRANr{$GsOMP5G=Hd976q&$_Ya#DpkTh7X#dEx+!_AQ&rA47Ow%_Pp@OaX34oc22{fIq-s2!;E zS5{rRfSQ%fV*FwVE<&=Sx^%OUjKAbw$^b$uOm&je-W(?EADg{3`~!mtLND&gItI(7 z_sv!5)~KiZTDhX+GK zKjOFh(1w_Po~|4>nETe@lVg)abL66`7V#*z6BB{86MkQHF~G zlW}XZ7Z=hT87-!0rvkl&7CLlfh#t&Q)00@-K*L#_#TiZADQ{{(=L65~9{(wsotoz| zCTG*Cvf&hyt)yWDG#bnmJ{PoDXksm>p+ZY4dg7^e2MA_~FLZ-huwhR2?+(rH-oY1c zVtypFo1GuMAXq~qy5Z0sRcozl*acy-Tr&=#J^%2Oglj;#)4G;nTI+Kk81sYvPe~RT z#BZXmQH^&X-AO=;b-3byZTLp*@S#5?Arjb3>@f3Vh8Kp1z4x;b3BM~MftUn>7e5)A zvcd;$q`m!at~tVRz}~*o9}ch-JpXNRM(wG7noJtaZ&3sQH8rOu8Hq+#idb5!5uKOr zXlw#h$EPmct!I-6b~@hMmsZ9iE~%F=D`PZLT%o;qwF(BuMuHH^Y;)@UmgoH}Q6O#MOApR4m~#n#c(G?Y zAOCf7H5b1Uv7q3G%RLPbF{+$wD&xEx7AlS8_CcTQ z!6p0~U4!Ec>kDmTo8Du)hug+Jlwf{7bkXutIC-!lX(`;zRef~4bU#C<*8sK4MC82n zo5bOfJlu1oxy(vEQhGYJ=o2ya=diwozepyY>0jJq;`1wD=Eyez#oB(j61Q^St(Qy2 zn3?pU1;iQsw>d$wNG=Gbg$0nYVmBhX1D)H$jNynt=_t8?=#St(wB6OraRCEEJ5nFz$?Zt z;Ti~ZiL7svQ)o91rWYJFsfpuyMR)H`fNqe8>CnO&VMl#9E4q`-p&tB@`w;`qN%ou; z8i6B8PNlpC@g~iBBsQygNutgnR_W5j4}fnp%F_Ygz6o^_^XfOiDB3)VZ3$DR7}u)D z3@>hwKBM@AK+RkFfRI2s099!ue;}S*MhgzGcfQMNZ)p^{BC_&k8dkQqL^^^VUzEMJ z4Inf@sceUMWp4nE86hzX@fE=x>6c&0I{ATAnoKdAwUfC5tC0=KZhbmF;>+o5wu@** zNbQt!!oLa?gF*qPg5YM$!gRqx(u%K7z6i`mO+M;u0(BHAXYJ#F_C zhxYCn`WbRO;7z4v!(z`Y4oP^2AkTYejmdH1xh8W%pdsmrgw2Q=m3=}exB`|!XeVH? z?cFsbySXh9Y(M`%EIGC0hh7;@emP0wqau z!HD)iz26|G`qbl7NKk6l(hp}d5m%eaqp8;>7vc25H_rq0VEbV-6U;y?uJf9g%mp1D z{FLb2&VzvowZ(Etmu*Rs&PF)g!*jpsafb;P0_OODu~*_eI)xt24*)YqkTM57=|M9T zMAvtah}Vclgm~JpIy|<@j7@M$@8ibg$XVDMJSS zsRSGR4Awnz0f<1y>&NUH@)2Adz&oeWHkKKJB&abb55v+UBS{t*Dp+mdULAM_*pm67 zE+E-yot=R&*?jRT&w_37JT2eQNVR>QbBk4}pZ@*UP8|@Z?8hsNGh>?b^AQ}&EOsv0 zUx3;ijS=(*pO2zC6x3NY)=->Kk&e+&QkUFURnWp_Q|fG?t-8UeQ2O4(#vnV6ZR5R< zp?i9(xxBT!wFR{Vn-s{$PZeV*vg7hMJ8eUg&8?hI*8@FP2U^&vH}|2(y31N74R4Iu zojo3-r(Ssz4ZWI;Gf-^<6n8?s#z}!X`)~$%qcxd`n-3PLUMX+V_(Ooi{JoO~U*mr~ z`JbZ$*zC?Nw3H-!k8`F}AzKIn=zLhVO6kE2L+b~f$*n>1{KczPMD0t^c9JkGHXkj( zE7~l5F1Cc#PKqEndtXg&6oBxaFhdho8^tkb;|&sl8yQ-vU4!zBn|z*vK7$W9PG4AX zM;C;S`!JN#!bvCl8^ehUZXhyx40atvL7egtA34MB$<$x9sV~qqq#h3Y9+|tvl@q1z{5qs1bX`U)C*BVtNGv> z5PF|e2n5RgKc0;+(y2&i2>OggR?a+8yUSQ-7uNgCPZXdJqyt!23Y(R;TK9Wjc zdf5lMNTOri?m7H+0gWi5297o83sYWz;JGSyPI*v;<>w4*n#M~DP$qDId3_BeOY-58 zF=KSPp(G1T>(KQs^pF!euJ0|1oOpct$zU3CVv%wJnl&y1yJUvi+viWzqsBV)-YRYm zbAy8;45Lt(m#}@tBwjPQ6Q%oP4KAHSo)x?R;@ft)JYerw=t`>+BRo6WpocfUYRp+8 z{0cv5O(;D1{!1c>;y5WL2(;3*&X*_9+WAmST*dkZM!o0Isy|z^>u#JHnn6Ib0>2*o zr7S*6SLaPhXY2Ck#eW7?G_#22Tj;y)zJWA)fxv(46+3`GyM#TomrU74zj~X?pqZj} z?rsN(j9H>I*hI~-tKB$_D~wRv%BQLbXPh+c)d?2rDyIuJ6=4GR4148{2^1)A?8x>C z0bK_99T@$sTwUVrV4Jh3v)epO&Mf>h2P=iwrHE6Me`M95q3uib%tH*<6akBm4&Odw)v>wWFP3%_ty)0DxCwtBk8lM)i*wGlcPReG*DE2+{JNdxq~f0t_p3GeB?xvv#Y+!-_2^MGc5> z5it~widTgmLO(ch;_~v^LjJ^w;2AL@_g%>gorW-HrB^DVAaGkVMg^~&l10*T&dv;0 zJv7g!498ID2%(@iK7raMdaN(mL`wt6H&7&Y2dy&kx4^UE7TJ7k9Sm$q{tV~!dEa-QsYDf8aE zV8W5h25y#U>`w;D|75bAI8D^Zbi@!U&7!hT_oeodNQxbb35EpUIUvo^pKwv$x``v( zhVu6v%5f$&C0UM?@Z#h_hj`A)VI4?&&cFc#gp=diR#kS1kV1uJcJVt;4iC|iNdd6a za!dd%n)Z<=sv$(tyZ7%GKEp7b#gG+wLJ<;prYFu?3Jl^VAzaSbp8L=%%z-YI$AM@W z6x1eap?{x&;?@*O1I}=*60)kLGcAGwQqm|9ak@W5cXE^pWU4ky+b8@Sb)=T@@LvRE zXJ)*8elg?yQKwo_VRzwpO7eMJpaz$OPKb&v%gNkqhI?Z7*1Nu>zA9Iq{jo7GLt~w{ z^OU>GTwyi7m(M7X{bOjnR<*gA`)mKiq!-x!iRrq*)_#>&Ui>(C4= zH04E;U2L%#!aTlg408n9@eiW`IREmk;e0qn`EVD;^_vvo^4a?{BlmWxG>zSDuk#y< zDeAw&Ft~9sg5DQAvWplcE&`BG-kQLXQ3)sbMiwCm>6_IIm~oAEMOwo}|Y ze{@2(dd(RCR+DMB3X(4cmYs$L26dYay#$c<(lJYP*6 zJYS=O2mj;A{|tQHc=#_)={JyL_YM4Y1ikp+oBE5+jd#Yb_y%E8nDJTbEL(*}$uumy zcE4Ezn-O_M6%ju$Dxti%RD<c&)Z*yS>4sJ0}CT4`>m?j1dlNSsa<7>l$~!{-8>NVP0p4N;>rs+u$Bf*Y)MzV_ZF_H0h z)~ane>ZD1gE}-RnPlx#r|;sv%;TNUBR88`L&KFePjX zfQ;b?HhC`e)gPLu+HKc`{w4gE8}y+>(o0T?r?Kpu`|f(QIdM*Z2lrr6_q4x?BkyJI z+5W3E97Bs2JY>gJwWUVCKLm3T%pMmxPMLA^p}K^46mH-4meyz=W=}GKgx*z<2o^(f z4$$e8VKD4hxq2Pb=rTzv~Xm>*hKy+X)ybyZdCH*?mOb8iAIGyqiOEiFxbsVl}oyNYPD|t!O z8j|MvC2+(d%=GbXJjWj#T{=cI^NY#QwyK~Qn%O@HDO z0eO%PbP6p+3d<9xCJVs?&Wp8iDDNT?ksejf2HNqOBSDZ=0h#|mQ?Y`CP~b{IOkM@F zQ;7$FqFv5y`qfTLK?Z2i0H*1NcGaOAXTDt0JdS*xCK^qivjMzN)pbj3QDO4?97Z@8 z6kA3r8uCCwM7*=J5aPmc-ASF@9nzxz`j*f2-xxjE-$Brc{uf+sJv*6rWzw{qGq(_1;eLlypSjQ79I;Keq0ai!Lbhgb2x`rvpn@2qF|8z}jNvCB2slR%V(;Lx%9yIhr=5w|RQSaNEb9F~;6mE=UF!1m4lO~>A?tdbZkp>gRptiG#q7s*ILT^T|7gs+ z=W2Vi3z-LD?bo*0p7kO!ymNs_W_9%=mJj+w(;_nqpWV8&WmY&x2h3T#0so(56$%I# zO2^ClasS@jNb05`PIwbDST=N1~R;Mx&d)B=(QJu|=Xz-BZrlK~G? zghE|`nCU|8I(mL07|k@9Up9`-_-`PNkt-qu!TnN~g%i6JQ`kO-Zy^LtFpqHAG}{bd z_aHeLe|*5Gyi?uHy52=(;*AMMKD;qAD8OsZ+h`*6$CZsNOtQL{0oCmXjLqT8LZgI- z+FfRU2|#mH;=aPGB%&6t7y zF!hv4CAFvxJ@1wl4I&CYb!G1IJTfpgbn%?rh=OBQD;k_Eq(I8P6$B3jZ9xh9MA|)Y zGu1imeol4LZCsv=x1|CCIt_iZ&`gWCTX82^R0WC%&~1}s5w+0xrJcR{2zC3gD^yk* z(a9Q(n?NqAJvB8oWoHy>IfsG+8*;tU86pTK^in}O{{RExl~pIW>6(so?QH=RBv`H# zuK_dHCapUA8KQye47Fga#2H+Y{JFVZgH303K#K&$h}%3ixoNN$8}jr(_(FNGQLmbG zZ)ESVp^YXvF`@CfT3vLv187VG1vGe_dR4llrwU%`!htWQ`$T+muRzKij~^LPz^4T0 zcNMGnm1c)Mnl&scA)Jou)I;xC-examig-7n@Ix{*?~oezk7lsvP3ZGoa&pkW2-6Iz z2a6qX1dvfYYDS;~FXCqRsVoEdCu1&VU!T303#dZ;Q=hS>vJWT2Ah~V@h7Pve_x{qZGOzegn}2~GZT{6z^^;Tu%O#!nFv-V%#o!Gn zr_$Ns-=TC*Je>w<5)FxVDDee zP*0*7fehpY6IofBhDAeM1wkVD>e6^fI4v1d$}%a=(0Li|i3$v!HzsWIv^=l}<-;9l)NILvcAYMTW18C?f_-Hp%5(5YDL7+pb4B1uicUe+{13i|u~ zhfky*)L-_B=ztlHA(e8qhD7!w{@a=%?XcG_K4Im@c+84y=0b%9>t}Wb0TpsJ3Vi>R znUl5<9hy4zb*(Ig_Ee{+qBuZqZpLV^YB;qo_=~8sB_n0Io{n^sBXzuwF#z@UMtXL> zg?KoOsR-GiG!E3ZfsgV5tVk`8K9iNHO3q(`W9zMg3`+l{)6|(_+$2i8AXf- z#oXUOP>VlkVG}z_XOi*V!#IUF9!SHE9~dxWSM*P$JNFwgdBhA2AsG)mZukDDB%w?8va^UbQyo1HkocKToDJM@2bk6YRTIwJIL1YkM(EBWlM*dHBS{QEO8J1PSPwd%-F+`%CZ$`rcCm26vspZbAkWzFcQLP}|eUq<<{kSvR_MkW6IefFx))VbB_8 zk(>lKi=Ho?brV;LKyEkw^VC4!YNz%grx#c~vfNmZsvEvKM~XN(;4R!TMJ!s1_)LZ( z7H^p%E_G4FrT!FgsV_w=S&HEO@Sg`?kQC9V zb!>8NcfEw%?QO(?dC~QA17>B;UE8vRCP}Tjy$gN>?6nn5nF)BHEU!Yn+V|J&-7A{K zRw+Y{u#a#AlTeu*)dAN#+1gxeqh07V30xH~32`=NQs7GMSP1f|RfDeia4CsT0_t)E zN!v)a2^Wy-8OJD4y^KmQyS2}?i~xfi9l&Qq%CEaIhoB)#FroiO+{Qi) z3>M9T4Ju*5y>XvITw^n;s}s*I$9aTk^$E(UzUc%ViszjdGkvZ%pK8ONSD}^TPLKDh-|A{9a zcBIB&MzCLNp?D4f+rFbG31L6&_eh=ukpLSF#U9!_+=jj7G4Lp-4;JB-?_6?7EI+OkWQ?HHCDUh(ZbF zwh(+`*9h#NI+I>7ZZfPOmlK93-PAD=WE{98Hlc^iubc=@zDDi>5(Pk5T;vuE_a33} zZQi7aAFyr+J7~@74tJaZLn0y0UJH#yc0~H&Hev;mq!ORCYzN;&&CM_jS;g4RMVvw^ zepGh`m4}4h>j)Q;RTuQ`5-`DXy^Ot1sMc;x_#6X^83gjO1-3hn7|Ug@2gKwKoU584 z4V~EGkrW7iTgPSC(r?vznaEt}I=W;6Hx{srvR4wDLk6PFIh@B<;!Sh>+E&bBVEExI zrYE)#y^OG(Oz{j-qC#A;vEO9ofa6KnI99W|2tyaafFicbNS+b8K${QXpa~h&tlbD; z21Rw5W5-m4F$EJinbBl8t9-aswWNtwfav_$p9?3tTvi?)9s1@~2S5d)6M}NyYzG6- ziwqWe6{h^l*#b_i*B!rJSzcZX&)S^Cb&FXR&0t2w0W&X_MLs|0_AdZ@9; zSZ_GC$7A6V!R<;z@0-0&xK0(ENJAyyY!tDDu%eUHdl5B(aUC5hS~MF`s-iisz;!fW zmh|dJ<_OG7DlER%cyP1WKH*~{-_~b!$9Ux*n5hC~UpM1?61I}Y4#aX<8a(VH+6 zHK16*W5|nQx*1-VgeX!37*nNLM0v*eCs1N1Dci%m{u%+89_pj%A;ms2_6uICEi<)Q zYao+edSxQQIz{-yP8-x8GTI;*7<*aYyh?b?Se;Pr@MiX1mZ4H22?sk^Q~Zp%TCJ2> z_hX${mTQ7+gLML4XR~YY9dJ$?;XtMsd=_luquf>BK&Fq}Cc`@za}OCXNYD)dWSO;a zKuAJd3yhP|w$&mV!67tXM}|fw!5#AEKQ}NmkzO4$CD~+lSyPgHHm7SG9G`Q1%Xm}n zZ|L-U<_$9f1DZ{!(q~}4jYy+l-+N)ev^`I^#g*c&?`7+hym33u=|_F!-v*y;%{&>L ze)I)YGrM0No$9C$$A~Y$hPvksuSio0m1NzPl)7wspSi&xNx1a9VBX%mi|KjgZa~%~ zQ%|b1$);wWhD68lv(^s0vp4KF32#BmT8Z$G--#%e6BVv4iOmIGcLg9CWnslJ$_3xG zxV49e%n&q|JV8~435XDx&25w{?iMTI(jpY4EhMH=pKwzd@b~PW7`X-&4#rg53@)M; z-q}^IyTcLxCr{H}%)X*!xrwGwl3#mif8q23;><|wQXY;nt5?^oQu9c?xKEB3>`FqG zsPQ-y%%}{Xmw5n*7IUjMSq3?i-{Un?g5Wgo-#H0I{lr0bmYV~Yn}_Gw;t=k~FP)33 zh(&LfQHGJML3A@9@}vL>XW`35MjCg5>TQf{$8T#=2PFfuewt&Mh91IRdI6hAa z!Hik&qk1Kbhz!U<2a4IU;o)A|oq>)5-5ko~z|N%8?)U}%ef89%GdM}eQy*^0pPw5T z+_s*rjNXD`N~I~=83c!CaQx|0b5%J8U_gUUKgz@00%!1X8$b2?mMA5Pt#>KAjUksSxvP|uZ14Co< z3d0{g+(LkQ_$n&@2XUFhpAPuv%r^$$1s&RnH~w_M_*dZwti&7Nz@O(l?@#-^_~o`2 zzufiWm;1f=XKgS3S=Wny*6+nXZ+r32yI%bBelLEt?ZvNlz4+CBFMf?Le%&GGYw3$$ z$DcsX*E(Jtxw;0Mhd#ltr@stgCkD@*;>~TVUf1Ltv-{LQ}E&q$@D(n{jjSPI_hW8!(F>u5C-~8uq2R!_jR5v>6{U3fa z?)@GA!P9@A@s+kV;PQ%VuK8^+w^yrb0d`f9OBUdF^s_9?cH4PJ%TQW}@EO zKx_zZrTu?|k2=ltn{CbHkWq^aw5z8doqc?o$#dhy$~7o}&FvjK+&|v3_jgc(IWuW2 z=b!pd8U4MG^bPZ)6)Sp2?hJapf9^jfgYINLTwFj8;q@JZ&bVc%LC=rL5U zWyV1rHj5&&-`s)8|7-ur&#X!hwloYe#18r!Xv8OFP1%X`oAQi(9_5Va#?#Ea;{99y zfzP%NtZ2J)Lz@XZs}AD01K9j`-odeqhM3!2s!ADQST`J7&vys z!+*)a?mIGy|L+-fJs{%PeLU|!lE#QS_+DD9@olN?w*_&-?Tr@2evX^*cLuahpSOo1 z$Z8*M|2AjYCE1Zn?tcFvbMoZL0PX>`+pKrhzV93qu(>y(!_r{hiI^Wyj(0TMOuPY5 zg(O8dUWI|T$d0CA|`55Uhwv^hp~LlLdhtpC<|Q90vE3rvBY7TV^QjFxBPZ|h^PBPS60 zpIhoql(AU7mOs zoims~7oaR1)gq-?yA#BE#9KuklpIiX!k&6OfD$1yK;kB{U@d~J=M;QQR~VcMr(84x zm7zpJ_(gLuyI%>)wHCch4V_5m>I62Hjn~%sI!q0^YdU@H&Y?>r(IbVXQ3lNbWeZ3( z@K`au6b4Vs#;|mux%wXK8^Bx13n$!gNY=$X-1o1!fSB#&45TL}R0%~wg2&Js`$RC; zxEAN%Ig#-|PRyYIFVR^V~ne!FQ7-I3VZs0tMW-r&8 zV)7r){}6C(3KsdBB?^meLeoM_vC=UsI4*?>LhmAg0*odc!0T2zp67n_hh-i4s0f8O zh3OT1$@v39ZyWG*y`UG)?{lqUWfh0`c>etZ;Y#2VM5!gYIDgW(pdy9jI?W_8>MEW( zp8xLQ;7Tr)iqwv}v)6c}FhK&)67>Z!{rP#Zs<=I#|IvYfanXSF{0jrp?g$osU14M8 zILmC6VDranG`$ zik&CfvAxLqSH-hmN_e(ox5?^whx=syJ!}%ifjzuJCWrIz_W;(e+;Z_lu{|mDz@Z$< zEJGpY;LF-}QwW34PT*+4Z7--rAQK_o7rfUnWOldgxXE(3S(Ewu4`8&=jDFdi76RcM z>G&C?5>6IzINbhP0oqfZ?IOQI^C5V$yp%ls3=B6(t&E=}gTi|(zh?&-Dk2Du;~&V` z1hI%d@uNa(pTZ)v0!{FBV#9$-wt-)O`?Aj!L}vz)H?t{;^zJ}*^i%gt&i;;Y_4YLS z-?9y1Vd+5w?foP>|46W3pW!*;Ei9Nh@&O!EaT+T22`VFa4WgKIs24Gz^aT9`l%P>4 z1Xm{t2dF)X|5o*%!ess}r~XJXHE(Zl$kTa=&Y~wg^p=vt17a&cl>mS*H~`cv+@UE; z$9(3Kh8{R9n&q(Iz&5O!C=d~N^&R)BtUOU)MYdwQ>;y6jN=8l->Un{BMes3?yEoXk zy^w#<5Nx|_RZ!6AT*nJ}^Q59OHIJxYz)vB75YEOB*0arcyJB6(j0^eJO$>v~^@bE+ z?Z6&U`9L~^f-6Nu!q|y!<^w2ryJ(2f6`@u%$`j047J=T~2eSNW`#2FA#osdH}NoZ|Dp z=KN8~+VRJcPB#b~)TItD$f#Nx?M{&4xL9enf7Qb;Qhd#j!c)#_k9SrQkgAbMfM4t_ zn!2Mj=DDeNhNr&i_*xfF)fth&eIS+!{jxyRkqh&L(_#ca{iYk2zjc_3%uwaHIs-}% zkXk{J-2vuX4vhZl3Kg9a^D^H(Z zKfiqG(&F0s>f#?Pte<;sZDCdH9i7*@)EQd;>SlA%wDA!ggzl2YzBB;WFP!>t{j}Qy zdtmOKwzloisWbTC${E~61%;Iy-x;!!SKUBui>7~(TC2VhR%q$3;UL35QFjanDil&z z^WcF2{E%=dp;npBW1RWV7VME(V}|ZZalcY4qFIjtF$Te-AspK1cZe=Si2ajeI6bg{ zTv+6ltjF+MN76YM-J2K0cU53!V7b{EAcWIlfN!})e18j91=MyO44vTwJ7!IOcDvcE z$N5Jd(fCh@)jR@9FIG-giW`qT(yT;}AY!@k$kt5s_|&Nh` zFThBh<{zqF(9(W$R4=OHR}<=i^J#fjuQ}NevM>D7MF><=Q%+aC{gFYB{ZTC zhqH87H;Xl=z;*GyQMH5Mct&oaO@ox44iXS+H~DTlKtA}>+l~h^EJ2>AA@yHDFc6`g zg5m_{J&Ti!D_4JqGd->$QB=4xj0zFCX*mrw1u?SVZBiCqDZ#G#Fx)7C*^CW&8%}WV z&cz!Uuton#T$o|^5?5T6J{|2pd2k>v2{vYk0J(7`#!pf4gZR^33u2;z1h63zj>FZ8 zMjy#iiae}bxoZKp(XodGf|ipjijfkxQ-s6t|D6OwsZuxFFEaQN+De1Lg5 zFcaN?$g0ga(a)YheT$+PbvB3&zKhu+%7-3)=xdT*O(p)1BmgbJhe2=0ZJVUV!I*Z9(xF1Vh{C*n#5T;ItxIg~<#$MV zHp=6$A(37O4c-xa<|c_fl9J-PH%p57{C_cGoX5=7Ge~k|Cf3WK7!^~&;ygusvU*uL zOt3Zz<|~;{`7T^qM9^(e@yC_Hs$-1p_5{hSJYn$GwcSU49`v(Qr`A9D#N+a3+G9_P zrz-pOTFZYJEX*X&x!{G}J@)HfW8jh;I~{MrC2!d~X(n?|Ie2`K{|~nsGUFIk#@k>X zdM%QqO@8+A#~wTV82J0z3t@19`3PEOnF~%mY>T2cqOaKbeRe9GI)!KDofm?q0=^xd zevE(OuQW>w$9fG|b){>iCz%k_|5&>ngC5Jhpw%qJhf6W*L)L5b2!x)7l%HLc@QRrqA z7m@CIJpb^ma*Dnr0)C3J8}>4OF+!Y8nQWI|rX&6~t(n~Vmb>0(VDJ0FK1v8es~Cm& zby}L_3yXV&jH0dh;5DmUx;$67HLJX+!sc9Aw*S5~i>3VPFny_G10<9vNv^?|tZbJV zP@jdoeGk2+u{?g_&16iuwps0v^G!IjoDgj_pzGOg*946{m-QazsTH3)pZ+NR3`D0d|p2kk8ofkTBx0x{L*o18h5 zo~2#1EgD~|$tYI!#;=IhN;hc$VOI<89!GT~Mv5%4#j;&U=WyFPNVi``GpvbRnp>Q- z0}0xrb0b5Q){w>zil@-46Re#wlNhxkUQ!v4BYgwd(X>WjB0}^V&^4IGx^&zAL@SF! zZ%VoWN%QerIIy#J%k?DSLP))X>qz~lSJ+%gkBhx#M|R1c7q<_Ib5t|_K@U8_A2(^t zPYp!3acI+m(}U|I9K}>4Ib6^?i^$!DyO4M>*(9_knbvVeLw)${(9CzxHOcKt46l)h?$4`lc zE0mnhly?ZC)@tq`9?w6~wH~Bx+H-CB3286kCUz{Wj7Pn+i~bf$%DU5`H-VV^^=Q3_ zYUrRdcjcE{2BG?12@*&~o(wx}4w7IqfGI6Qr7anY(&ftdKH zWx2{v!36Eq8^T`YBT)7i#QdJK(R{S5UmCRq}|O$pz51ley+e zBc^b?TzBvM*uIlgexXc0yEo$OV!w%`ip!Snh2qzs?~R~ntuT_X2Gl2@_+u|%k9WhO zW5w3t6Wk0E#yg>5Vs1G)DOAZ%lg1v`iX!S(tlPKm&*S~8^DFsNd}QtXN`6YpWwO=$ z_Q}`r+h=k{gCyGjJI;+zc1^jVxBoj z5FL^qHp3EZI=CS_msZ=38r34{0^k%Q!~}O*)w14rm?>B80yMz zx=khj=*ru0p-BLlLh^9sJj`S~-I@w~!;1Z$Dlny}8aa1B6{arw8b%Xk^k7|h28U90 zVQ9GM(qQM9Ma6Ba!wfo6W61#fKniie!gHj4T{Nca(0<`%)Y|>gPAy|fT4HtFYcztC z?M-(px*2AkQbr@_>yjO_rf{dP_MW%DEi`n+PJ$W(Vt%5Z(1EwMADI%b39tNPjiDg`@ zHR-*^mPmbX((u9VQL;fz$|`lR^?*dnG|eMR{F2Ufm&RdH#;cvr9BClKLQ=UpZ5>Cy zWCLg`jT#`#h*Tv00Ny;ST`#gklsb$KGK5%V->%QQvFs3VKRC}2#ku51pZMp*D;pdSyVFfP+5D z=(mdoOj|9MYfE!`&~Az@J9!$#c7*vd+pVGnJrcl3Z=9~BPmnFW*6{eCeuE4F9X9Q= zp?5X~v5NYbA&SDL8mCXp8wymkr zuj!yPtN-Qy#1fHEO-VX-3s;l(;B4C2fTx8w$YKCYmffX*Kpk|JO^wmZ)6Kc7e7{c+ z>|XyJ)b2MD?k*<+86ycN4bYbehFU}J%WUGZ^=dBOE!Nj{*#1HI33dY4hdP=01^+E> z8tprD;KIG^R6?b^91D|Ivy8`uYfYDFPE*GFUw7DemK#c`Q z!uFfS^4y@|F#(h=+Cf9(vIZOT95A_LYWEXH1~(1{MW}lpxMxJHhhH>VAlYGrb6SmY z>4p6Z1cTep<2ujW6}cLME@%j(<|2Gaya>I-yUECPZIq0@GS`>F;v&iP7K20cFVQVb z|KQooWc0M(C)(nnAW9j4;ZLCCB6uS+l6FyPhx(9tB%7>nZkL5*Bz5GbL&Di)v`;tR zOi~~nv^1H=^{`i(4Lwp8=AE=A6AmqY4kcZlVa!V@!OpEOw5vj-HNCACA(LSGHtk|XN`HASd>*-Sli*FR7tawo>U#&%K#U2j*-!H=y8jH75V zLdK)HM{*+D_Q910Vwc1XT_9=zQ{2#hnpuh*qPACEf(j`B|AlWb!llIM?}YAWiC%@% zF8&Hlsq4t4!}Y#XxmPqN4ZDQM4R8FNO5cw8#lzyNU0~O+IKP60pSROk!&HI4LjGsE zX_cUd$x27`zZCsP!9qk*!RJXBXyiFb#P3z{9Ro9b9epEy=n01zw1vokH^ z7c%Ii&Wj3GoByKNAs+Q!#bBZ<=>u)1$~)-W=aeAs(`3ks?zO$_FkPCRT zNJu(Aw$opt#;6j;l(1LhR;ebvteSi6d}f_6QSFF=Dj!#`?qxn(5wwu|3HHgVa+$#R zO-&~B-fL$v*~3Babt+IqA5rrMh& zC;h#H?YhyeBJR6s4w>6<-^~uaccBPMeE?60(W4(v$e(-lu-_UfS zj1CoOrzlEjy|#N&9}2c`w*OHyRqUDxJqpw@{IYD$aTc>^|(dX`KO#1 zds|7p)xhOS(OH}-iOxDyq1~9(Pr5d#JrVS&K^QWUKc=^jp$I88hsAQ`IfB*UPA2J% z#V=xq@Ppl)DX5MrwV*`bw9S#I-ESGGVrNBCuFcUldB!{c&7VQwK~rO65g<`mqE+7X z#%HVHs*lB8H0fp*dVd6y0(+5gD6B{uU^*Sn+9$U?!e=l*Tg{0z^>kjuDdi!}AX=Rm zoflXqnRU7zOc1x-!BBGZ@6cUt+Y-sD?>BFWIK9|nJF9Koswwhrmno9Dj8DyQbqJ80 z1R0W2R**lt3>z2QbI*1*I6?lQco%?d!qY8~APAfSO}IjxS@Vl@TD?<(cv_|VpPd!$ z1~HVxzbT%;LTZW4kJC6VaKMM$quK5i|T0BYo@TG1tvzK>B8$NfmsIS zax9ZUEhWAD36xEuBQlK`v=Kw%v*z)Ejhv@m6X(Bf(30uIiB-_HN{pwGv;%jqJa^(m z9u1UY2olcCHr%&-n>L&~5m*AUOk6^8j@e)3t$mULS7b2~{)a(FrxPc*8HhG$J9L>v z&tYt#6SEG1C7Lrm_9RE^Ux`gI0bYlb#iv2%h)!hQ2`3HuL7h0^h8AohToWlOdQKwY znT`n5ryhq788l%!ablSQOT5X4qs_XkKn`si{hWD>kg(Te19Q6B*eq=IA(QLadlzGn zt&edF>*Iubo+t67g+33U@Okrq^jxlD(xST$VMgYYvT~enfeon~oaf!HtvX?<(9f>W zCai-Hc!Mp@tZS`9@0j>#IYoNN{?^P8^kw){6VM1>fi#)2MCkfSp(h?A@|pDHh(_a* zS^0H#R5c2yTfk+@VWH+jTQ0TGMl9m&F&50xqbaGE<_gON0n3t!%%k%3am)u50aQe5aV zPrJ*>^vXMkCb4%2()E~9hTk1(N;fb-Z4`b+* zidpN#?{v_MiU{Hcd&DNXFop97I6jC1U#ls}PCIWgS(pe~_z#&zDr<2gFf{xy;Zm1& z0LArUm&e6t{G1l3ATyo4!>8tdT0ei$+`6A1vjC=P!$LkN8F24+$##>XcTFu4=c4yo zJ1y~gblgdTI0H^Z5b+sgqmB$tP&lVL~fxEN3BqbZ>hFx)M6<5ORPBf>N?^Kk&gB?auG ziNmBdPS{*llVQ#*HkxlnL0jU@6&C(Q)WC(3bcmGqh$8B)6$w#E#dCqIQ55 ziB8lV!#;X6PzE*MEmPIkU&Gj%n7E(_xW(Xq`mc3O+5CrjXTJe?XG?UVsQ+c%5n5OI z(XYcjZP0vSk|gD3 zY98{ZUTl4=@wEB99S4)o#NM0p{U60nOq=eMLn}L{GO8{?OdE)6!d}L0aEp9ey zsF{(+WF7b$ZhR;Zi0RTY7KO(d+DY;2?a=%;(=F;r@=>FspH#m@0W^#bY|c)Ga@vj7 zOlo{*nJOES^ZjM@#H2be)Wh2OGQTGUF|}WvJbqC&JrspRew;Agxmsp_XXk0iF#>q z+I4U84ltC$pCGlAUQKdXI*#jmK!NF zMWOAKmjR?r6FO(t2i|Lk!{fwW!dkQR)3wSB9C^-?fh7xzzR;TynX?r&+(}r*5~1rCqexuC z)HNlVq>E~I&K&BH&B*Ldb4Fc+E>YB4Qi=N&@sJLBwX2}5MYTAysMLk3^bXC$?Hg{n z37JZ#lXvgB)6&cQp!yU346j(8=Lk_p#U~auJaQm0f39=tw*~{-G(o#XH1v~pB)6c+ zkesKTwsmTqEDd1XOBtu`O5!)pxNw0XqEuJx6ub8Qab0>X+QToL2o_*EfkWB}COW4E z3Q>12U> z#gW`v1B(X`!3J(jVY3u;1hL77N*`>@WiRqh&4u{wOYr7KY_gBk`Bl+^?J)xp6$166 zVrJHE2q)-7R=&_b&*+XE`Dd2CW>zuSO&)hgF^Le9lh^A=yE8<~Ptn_cyuV69R(!jo zV`;>CEClJ=H0t2v?M9NAZxXO{$1?nWzaxtKu~9pa9BaK7jEJw}U_Y2n&ys_iW%n@^ z0dE6CLSvv0G5P>UYC^J%{U*+92PoLh0K|0Cn>?Ca!(qpAM~{h;wr$O2t*!$PXE4Dl zyK}sCNsB9p@I;A_2+U+?0NkG7PF4rr)21mnbab5J1CKaMTMj+qho0>p>)9r zRTY<8`0S1Msn^c+>}SYmlJh=lx+>e60H#{N*h&f1r5+&}LgGKF9cb>QOvb(_4ow=% zP!&--_Jbr4iM>b70~_{Euy(OADg2WMke*2!m9!zr(7WF(;(l|M_niFW@iwADmKyFGT#Y}Ah%tx{RqaM^I`7IFwtSt?OR?Rg)+ zOzl2Em=%HT;Mobb6DzaS@c zSwAZJ+s*U4&JKUriydyG8Sbw!KYR!0SCzM4>Kza-+iLCu9QFu2-OaX@{n+8p$!gSe zy3-u-q;(_y(wSms$m7G#oK|sBClh;H^b2&{h0dI3skdg?D0;N?Q77++op=je_7B!R zG$~Q$AWi~NY}F+A^*1ReX%lCZKW7o&4vYJH!fMm`2gV@(lD1_2@{W6zw`(ZYg0Pb!BL?`s@Q&b!q$YLQXHlxeK{zf({>`vDPP-; zU_PcGk+|Wm6J{#@?-5^#XC&cgC(vrC5dWh@sGq z0&Yy0*ER=}t}uDVWx57#>DomjeBg-Z4Lo?n!+*Vjk00^yU(X+W`)%(belg`e%zwT! z;7uMGe71^K{Y;Y5j=byW=g(`LbD0uW&pbQ2Dv7VYw`0s-7&d^&=i~Z%v0h%^F2k)< zUAIkTVmQZ{8{67M4;%%R&$mC4&m(nFHtHj2MhV<=zxeNkT4Pqn$pv=!GoI&-uVFse zk)68l{gp9)eK&_shRV!fz#ID%wyjhz`rbbn^OtvXd^=EPbl(eXpZwVOt`3;Lx|_qh zkuu{S^_WAX#Bm9Cc2^?byZ?y!ySq7!jsY{Xx!23BHOj4>>t*%|@V%2q%y;hQOfrLI zW-;hp%=~akn=AR=sUzkee@T3j<9y?%wk?w%JrP-gUxdc&;1_r1>?F<-x{%f~30afiI%5*mw@;q=BiJ#u%?sT(OX{(WAs z(um4C@NN3u3wL>ZH&SN&-}1&?Qhmesibw7)pH7C#%;00*2#Q4a_I#C+p!ZoLeX z8F|=)D@fY#Verr0W!$@wGUMOx<<6t^Zn?>N4Ty<9^6xHJ%nX*9#cz8Pb0q{Zc_uN5 zFCR63b2m339V;`3-|>Rg;#LGfg;VfnNAK<_bR%WPAMt_(sFaPUwc~q#ewW90BW1>a zz#Ce4`jYQ`=cxJG-9$bH$&C8D-q^Wf99@Wj^uIrPcR9WtC^Pzl-pD-lGiYZ=@9w#m zVKO8C4-X2+(7fJ6#H^4Cx}l<#>ZpD*Or>_vU-SG$&y40+j9?ovq7@;_`0S_RfSG3+ zdp6_o3_5)v;ItFHX;y41cgN4;h?` zy#*9xq1Y&yLLo2t2`n4b#}M?wmBc8t3aX6ZF;9c=wo&sYIoK!)W2Y^qVaK>d199aa z0iw*_s)Cwo_p^(=bm%! zIp-p4$9f&=6~1(XF<&=u_=!_K45;Y*;~6Jbt~B7%>oWJ5G4WuiRXB1ECrl2#Y@tpb?R{UOA6c znw3VK5qdf)j35@Zm^aNd&Pz_z&$hTTdIt+Y__qA9wZi84k{$oqcqyFR+}5cD__)2n z;eX!E^(r*ua}AuJt>HL>6Q$E5;fy@W-+Wy?$AmC|jd0brH&>q$Wmr8Q6>u2|Eo99N zz%{NZ$IIW`nCqC7wn`rm=Q)M zF|VuWlTAIXl;GCQePeLIBUx(JACg%=ibs;RzM=5skz3&%U@8bL zl|<4*I+XqAN`Bhkt*g~;rR>Z-tScmgSBGadhtoCE))TcVh4f*eL6S;%=eT#r)3*^_> z5>5;dF@>%9!#>`;$Oo{>_zcwM*8QMccqN=&EIrbs=;8zLcI)4XQoqRWRv;rF`2kiR zUnBf#y}?8i!v6e-EmQbO4*w$-9S{U{i>1>=mw^OwrDp8s_UnU9U$-aorU}3IFlSuy zf+aArZT_#(1w%BmObC|F1eO_mL9h)Bf17TTxlOpK$PP@u8gfdHKP+kH96QoDKzeHN zKx~6+miQ%=gSTiR{T4PyoBQ-q0g@10HZ(<}E*Ga>@rT=50lS0vVlbc_IA+Fke`ok zDZ9WmqShSPib=r+7b@jq$k%Bfmk}4VmRP>7p))j7n=cZacsRGZ4lhDvc7ed7z75>= z#iu~lO6;%0`00G1T3qKr))2q-YDIAt%R@ zk3d2#SKyS2697i=V+xi+`tgAq`^hJg8%IbRqk!#A9ml}A&%x;z^K6KMi!XB_vt@@y zGDD_Q+hz&mp@FvO)vl0fz<>nI5i%5SG~b{M6$FW&%e7|}ZjtTy16D}$scAw%v|LS& zz8Q!DPBxT@bcCU>!c;yCaK#Y}02Yx72#MLMmflW93BTMCHZ}#BfgS2;P2q$DZx{xI zMo0TiKfs}nzvOgv3UI0`aFU2+I$)Q5!mw(N&~pt4)*?o_^(`Ph13yl93zMJ8Jf01) zq{`25Pb)#aR0i(&&AuJLiEPKWr$ z?BAsJJO#Ps3XX{;kf-wAp?$f-hjXJ)ipY(LIaoSQH!?25!BPa)Rt{2c5h*A<{t|@< zgnfE}K+vp7ymUO2V!hg7}FJffuR0S)15PN z0;w-yrj42gYK{+hV}ucppzOCZM(iD;53#-zg}vZvXZC~(lNv)cTb!#@E;P1IoTJbh z^j=n>)evMJFXzcBI0c~2IQTGmp3SRnn3U{Wd0H%rHeGW(YFzeq)NR-c`9mCWt+2ri zMd;b@i0u36EA_1nSQTEcp3u$_L$APe9Cp}?8S41!p8jX)pRHOc{7>!NXrllUywe8f z<!VzFyt-C$zWyzLE(A)O3U*aC_GbvW4x zeLD*GDu}iAwx&$qQmERyEUS6fToDRn3>{aD3p?5^S#q%mjEY~$P(_;H^2uczV5WLR%%6t_Ob&sVrzv+4X5ZubhegNoru<=2AS-IM7ZQ&Ddjnd+E6 zJt^2lIAsb4`d4_9QWW-63acH4v6#n7NnR}8N%@q!n-w@k-$XKj zPjYAUDy{GvDz#=e<6S0O^&C^cl(3JUrA(ZTmLFlzM@;J?*2e4t3EC z0`isQJO)6jmMGLzTN5x-VdOc4WG+amQRXp>@jm%J515)wC|~ED49BbyZZf1ewA{eUw8~?!ki3>19 z^5kbdw_2{QHa1`^g7LG48LxO7u}rK{k*x|S&C)7$Ak^SuG>wG%OpXPE2QOfOpuu(- zu}SS7$i!t;bYj|0@wcotw(!1a*fPOOAue(OuFh^^=JZD!>6q*Q9j{j~fugFjS#Ln{ zs=Oj9jgS*4U>^f=PE-M(V7lmHfid?;7%xIkZP@(2ez$hhlE4R)U|==JtkN5H;Fz znH6nR05F(Hgf&v9Oq^1bv~3L$E%=NDvuG^Q!p|ZHui^peQZbt*8&1CIL?#uhB-4qL zWIY%KCTv$4INAUb03%D1KO3JxO(nN5wy*swzxaev5h z$UD%FaCMnlq1Y&FH?Rrufd;$A%6e>aM*`9!h@v&4)D~kE&^z~Zgv&aMsAkD z9m_4Nwz3=#3IS>Yx1DbXdq#1k)RndanbbnPvVm0T>zFTde8j2WKsF2l3N0OB{7EyK zpk5W;5i53CNRZSXWFPTY8X_yMaFJIw9b@zXI{aE}I>Qz_mkYIQI0y_@UNM;o7tOUu z3&ifaaj3v4wY^a2sys3?@Z~8x9W;(vFMK6q*%{ru z!yvTP(z1}#wMbUc=+z{vZ!T@Fml15B0u0RnV3iu7EV~y53sSsPp$mlDME~}Z#{9L( zW4|VZLHtZzqIFfAPhY0hHSk4C1)XTXCt1=Xg7ihAE;oiqu(Ds8(*z?Zk*VtuWcsSC z&nAu#xrrsSHl$nG4Fz5wv(VuTVq5lQg2|4tEh8sH-8e>$R0Ss*P{BM3rZZu^Syb0I zClb*p&G84bnJ^5ETIO9~#>bn#8#WR_!bNa=d=_Cc6v7YO8)TDSqw}~*&a^10@Zm~o z+Y4?E`W~p1;py15C!7plZWzw1mTBUH2wNH-XR`rjZaULDYNh}#c<}6Vg2&*=!X+J6 zH0VwW!XWJJBVr& zffwbaRPg?mO98J+=|Oa;<#IrXQjk9(w9-1w9l-9bIKOPW`!!z+c(n`st_o3__k{h| z8OHq(6d}2~o&06-rB5>4!(9P>HLkQ9S$Z#x@>f55*pofmW-0AaLDQ0=K~<^L>Bf;0 ze1Ui(u+M5-bQI%eq90KF>iwVaO7I&{R|XKgg7-6=3b zdb-7V52D_8E#O+K!Y2F)pmgXjI)N72I9R{)PQfa;xKg8p#j3HnpLeUpciwq@c9x|~ zuVo*i1lG#uQw*SQp`D7D4X5P9X&Ij&1!<*PE_FDG+xhARBlm~1iQ8-JQh`Ybbo;e0 zp09IT2Zd1Ic6ahwzX^Y!%D3o~up1JQD?{?BF&Z17Ri$%7dhX^<|J9g53m5cmv0Y%p zQCTDmTSg%c(^i>Rx3Mx##>HEdJkt#bUJEd7)nYn|a!d(BB;^($hS6H#u=81*GeV!I z8G_z!xIK$rWO$1$+8Kow25SLb)msd7w}dB#-U_z_XQO+owb3>EQONw0mBWSy!uNh7 zMo&&_2H#ZqmtmS;Fdwoz}+00nWvFXF|Oe3j=Y^~Rh)Fhs%T9`un+u98^ip=N1a)~ z>bO=1=#h7m1eXkWXtFAedr*~Nl)^#+v4~un1l(A2?AppgbA2c+FBG;JK?R%gl$tnc zp@mB*Q#=7}fCQzg^JxUi;E#BsZi;r`5cbv^wZlzrxLgw)gPT@y|0!5NUT*$D6KbTz zAS;jt(IBcEbZcl8rZgCRMo|scwpyZ*XqQB~MB}`hJ3|*btKP6`$ASHs)~Y6OR@C=Q zU3?rBO-cX4V|trHw6ITrA-tEvSyZWT`Pn_-nT& zsH1@pzuMq>b7H{{WjyzX?55+lc*lKt3N8fD*PT_`ksPv((d*lqsX_AJOd7((sJqWv za+}d6X))Uo>$=q#USUO`H>ixw4SthXO3Q@OuZ>nvLE`fzT-*m_hw~eRV$vk6nzw{` zeYFJPLirF+3*@TgVfZc*D=~WU@G)6QDTyZiLzRHefD2p$H>CkE@3B#M@MVw&pvxZs zb(|nZ(a~-xgss$=>gfS%`;cP|vQ;jcsdnx`Vlkn;m9UA61;#)#t3})4?*fTP*5@S1 z$k=e**#?|}Y3Ri!I&H49GY1r{n}7zDoWeFBTLF4QzSpnr&n<0newm$arGOhK5jbM_ z2Z$M>i7QxF!I-yhoxtlf;A2|EG1ECQBBd;T!m5<2@WWXFx1;OwDet;`N?gr36ONR5 zKW9SVh;w;;X_ZVx9p4eJbjph)7N?CmuZc{>2@rW8E5Iq3cojz3(cnLi_?X;l1r-cX zV8sc~c!2m;58_)D#LN4!JI2pyLpy2%!;HV#G^0r5j;mQ}@KRY_6D3J!2J-U>q=Y=r z)xbyl;-BFnvp3+vumivkT>Pz5Hq1~4tjz&Xv?Th_YDA8?B~uYI9x*`jda`(3^KdoW zI#j}l=P@s*GyrYftP+?uSFB#zz|EiqL>pi&5Zhv6RJfxtTCC0Nb7zzTwp*}%OarSo z#87frFP|%kJ3M#1#2$s=Izr0bnQPf+jQ-Lx+{~oFd10Y$yBB2^ z;Rh{B76e?l@#~H2u$xf)+J7EeH9Vb-SlA@6=O*DbV76_FqN*p_fc5q5FFIYyNpw!F zbh4lXei}LVA7%|te8}8VS~b-!XIjTft^|ZHL_>Dau`VZ-F}EU4*W499ZFbpuh5xyb zYqJkDiO}zAFOE}CHWK$%LXNg}UMok(*T;-|CKC;)T?}56;kU!A9#92H90k2T3cig4 ztN1&&03YdG-TV+90A8Ry~!yp=J)t_#0X7*HWe(BU=4&~g@>D;l&xw+}7+==66fpYUFPEO|* zXXmF+R9&7p(6 zkR8cMEOmW=$mdcOwT>+lUAA*%KC+XMnjDE3?+g3QzirZ^{OX`m6g_wmhYK zxv9y-;v7lfL+?y|rbBX{(L045%Q*~oKf6vku;$)5KnRWGj1*uu_uHQjhsaZ~NoiJP zOzZ);F%QR9IuPuekRx=Gn%CdTff9ZJ@yQC5^I=mHa$R=*NI z2yhG02lDbVZyDRjB=dnTu#rVnUyBn@FgPDh*#wxJJ#|9VlT72B1BusOsH}-buxnud z0l&o7P-q|yvWkOwFkkRZb5=dV925%aE@PIO@>1PMWuB8`l<^ciGAC*k*r0N1;t=5~ zuu{S`eu(WnhZ`2qNb3}$!BcX_w%#~cqR#=e5X_|BF zFdqzbd^Dy7QC)Gm%bC>y*tv`c-3%eQ8>C0%@~W3QFaD5U_7}nt#IU>NMsipYKHG{< z;ox-W-LcHb?DXk^xz&{TJT57&U?BW5lN=0BuOZp+ep*iEF3r)&L}NE`Eo4NUR#Yam zRE5J;rLkVbVK+?<1||vdxx5|!+%_WAU|NDqu~9~-t)!Y_1Wj_ebY;!><)A(t_7ih_h9N&#<>S6uX$IRl~VsZO4Uj{qDCbpfxpfL-m~}V zC0JINa2y0_X2v3!AxXQ!*J0uA*zLmUe5kZ2s}PH)m^^k`KI1M8krbC104Xhtu?Av> zdy-P^dX6O~9oj{Z8TfQ^Zm>{I@7v?*mpMI?q0P06kaduc@gySQ5NjOw1hW7OgIP)3 zJ(%kXE;ehxPeg#y{fX|$dUAY547ScJhhZmkDMBz_rB7=aGZN?SymJ!c(i#$ooa34a zab{9vx2wbRc~}8hX5YQxZaIn?MvO;lG;>dQYO8Pwqr`53Orub9F4+a4gY_;W*GlKa(M%wy3SVu5lE#TY^Fyh`OrHzs=$e7{dB#ZoWH z)1`(N=juz;U)u2uy|UBVw}tt)>~vmkR!YN2p!hHkFnRlYm>4zVtbqpdMPYpdQx7BT zCEEL-ZgC@!D$Ax2y_{dLJ2!G7Rdiu7PnUr}xT}av+9rNG#|*YBlJFsxk%)~7%xjWf zqJ@nWXvUgqPJZyZusz*3NGZpbh-jB|nvBX9xTr^-4U@-WL#6n9EN19j-XxkBAD8Gy zxKrhfvmqP`Uq{6b&Vk5opudWjA&6}SWV@YvJJJvxY4RNy$^UgSU##+dRP_zyQ?RZh zM7a*{U67efManX6CProgiC5^X z!zIn%8IM?(BKoM%3M}huZ0_+OFOU`T><{-syT1ev&0UHFf&kWSd;kjc`L%i}Y?p~R z97LKhT-k)9QoJDM&aNXLu@br*eOpVsf&{k9Kmy^V%mnG!-?i`}IV9JKSSw+J$ISb4 zozWy$h>^m)GfA5h36PbMNiFXa2VI{XAi;Z$a9FRrKQCNZ(PXt!1^?Q@J&}q8~VeP7Ooxf`_0|6#X z-=J$RAdnq&Tz{C{R4T-A!Ic#>??*Bkjvm9e)>`8=AFgt}&D z+pjahO~>vuqB;maf>&~*jsY;Cj%+Q~_gn^EgaruMIh>=fv0Y-b!Oo!@PV>-o2bF`> z0`5(RAAPBKN2uIN&Dhx5YL(Tp$STgST@ebm5Xc5&GZCH_k%pTf&Ui)gMTq2a$m!ZQ zD(Tp|fUQ|fX?7~*ZW=2o$w*CV5FYQ-+anHc(6P#N@K75IYkg`c223VI^(l3cHK7#T zm@gqR0{_CBz0G?X$kD`}aS|sY350&7|0Y1=;0^%eV}J~88tEF_g{+=k6`OWLj!Kh~ zizu+o374;nVtX^`jv(y7_wNb{>70?ClS6+wDBm-*rJD#=POS>bmdVxU)#u)zv4r&~ zmRU*#r%|BLgv>FR2H2rL8n!J$8iRHY%TJ@aIYQ22chubluq9R1U$G_Pxxlg=HpvJ# zLR{T?a0Skk9r~nMCeB-Wp|}%}^wdDXEX8e>tDgx{}Br3V=KLJfSVAKV~VI|pg zTO{NR5>FY>a4i%WPYB+-buISoWKplO8y#u2b!}u`wz=?z;p<~AdM4(!6&i5r3$LBx$>fPff1#bs_IxG1KDpagEz(vTnbSvTna1*6oMAYn)o4s%Zmv zc7*3+xJ?L0r1b3=(#Lb)6u6-#nU50=61a_{(#V8QF;&p4`D7M#yb%kc1df$qe`Ea7 zeP!qkHH54N(P1bE)&d4S!h9td1_{K<-3h&%iu)xvnn0=mj=99e_RDxo(?HsCbVZP( zcd647so#DfT+?@beO9EAp6h;^E@~sQiHVAaWM<~dxC0Ou=7vn~ZKiA+QNTwT;Z`cr zpJ`|c*$;cao zdWUV@#nS4KA0Vz<3m?ryr+^_r1I}FH!v%bKG=rM#W0}V?!DF4K)QtL;&^d%6<;h4C zJdSUX#sq}}QSbx`d!%qM3f_Z4MhbV~!b5f;h0!RO34$(pK7@M+@k6&1?#88nDD+6- zjZyF<3P^5(mdB#t8F-!EAccFP;G_8ERY5=d1Oy*L>D5{S2+yF@t0lnj36yTs5`g#= zO245cK=B!ra4jZV0UV!0sZUD)X*_%z;ZLd zyje?t3#9=q0haHegsb`h%R#{MJ(PY=OMvA$lzv}JfaM1$ z?bQ-s`5{WTY6-CX7^Odu(p`Y%MU?(fOMvC4DCM*SSda?lHZ1^@m+;eTv;W-Kiyjaw|%& z*Aig44W(f%0hT*Z+OH+RvJa(LOMqoRN^j5-U_rCNh?W2gUJedu39#TVI4Gq2AORhw9+4mHH9y;sUIat(p#mfU)GHHIi=zP2*9B7g>Su zNYc&ljjRxBrb`}4s+n$iB%x+{*bNO zkr|dp5=LgfJd!Fhu{;8!g3KG_k@S#Zj;5>-L*{@yk`gip<&k8Nxl0~N1DR2IBmra& z$s@5pbGJO+&&M~)Bk?{nCXYn=%sui*jL(eABhfwcCV3>TXC~y4h@LqtkHqo}wje-8 z6wmyTJT~yS=g~~C9VMd?ZVpp27x^vz9*ufuj7tFmtnFRDDX-n^809Yi26De|LPahY ztzB4zt;eG9T@<#EQ6(&+G-}h*CGTU`yrofyds1y-S>5WK zPwl%;A&Jp`z}VU>KhY~QuaDsLSljth*=ekK+|AMxgobk>Wt3F6U*P@2{f#G@xsx=%`Pgn0B(l-{f*Y-S%rX+}#BkDftkR!b0%K7rC7 zYYF1fr%-x}mH^9VP@2;cVEG(M^I8HdpGWDqmH^8aP&%O{!15)O7Nj%=SiXYN{aOMn zUqk7nmH^8)P&%b0!167W7PSOezJt<|mH^B5P1|p9EI&o*AuR!xmry#ZCBX6vl>S6ZfF*?JwyY(<(uGo9OMvAD zlnPn`EH|RGq9wp`6H2RE0xUP9R15}0`vNexpja|RKyxdK=S&gc+=k+LQv^JBptxp= z0B9eIWm5!1`%!$mDFUPeC|)o{z;p=3byEaT_n^38ih$}cikn)bQaFiX#S{V6G>TiM z2&iUJeApBL)jWz-Qv_7^qgXRVK(&No-4p@U85A3)2&m4Yc+nIARRP6IrU2lIlk)g2KEe=!j=#glMR_E7W|riUw3&H89!Z#)2j!7e znK>4SsqDPnYYU$i7InJ z9!XG{b$KK)Wj5q-l8>A6IL*h3JkIiQOCCu)nTO?(gp;YtBZ($clSdLvhBT3w$YoK+t+x(0x+t80chhpB=5t~A zd_ov*xl?9RJ%a1ovxxFAMq5*b)}Z_J{?#HZrQR_B3V?coixZW9RkVxAMipm&u<oPeh_5P64yl*Otu5Br%j4r@CvuK#&k%Fcs%lX5KY*75Oq0?`Vwka} z&!rAkES)PL)x$CmqN8vmS+u%T@7J33n&C7MA;kAb(UUvg!{tZ~36QfP(!87T1%nW3DzuRF3%qOj*M^sj01*;o> ze}|X~X9Dv{u0nsWc>k_fIt^0%keddQe6f7)Tp1T^^AM)R6foBOyBU7eark(>ywTWz z?~-lO7z2KJ!%u^h96oZDdRW^Gzv*`~{I1T!FI@hW8-5C;c+0K6h7s15&22Nxq@0?A zxi&AEiRkYAeg`rz-?(dR(C~Fagaf;#gElb5kEEu8Owe<{)NGu#v(fyXnUJ2|A6;ca z$bMTm+Yv+jH}KlihMO_u zK)6Fz$)Hi*xW>&ox$!H@G3;~qXwwGJxaiQv|B8#0w$=)%$vM*chC!7ZdcWBLRs09= z5J!{E)3WOgM4i=kvzN94lo&o-&`qvqHSDH`-};6@jNXodQKGjMT*Y^tMz0r7z0A=Q zo;dH;zPz!~K-karvIZ=t#y-*Vf*Jaa9fxk;w?Re}3tv8W*!|F3;7PCriUklU8AfH+ zA5r@)vcp`Pm&_FWM(+z9kl*gsrg0PKt}!_;e<~(-y?JzitA!Nol;AKWJs(F%3duLj zWc+6D^PMKc?A>-UmS(3~W@5*RPkHwvzbX)8jv_7~=VzJ)_Di)HZC~%x9Y!l}T_J#4x_<4*J&&*q_Xp|0E7g$LI`X5BDvLSqh2|8 zE;Xu&mKV$jf2$+6vhU-nb2fOLM!S1%)dElaez%B5twt^ex`RRdf~T0J#MC@2w0mnX z>|3c+)`Q>f?P?(;NKV#FA-X%fO?@YNS)1`{eEIBasip6oqk@@%w!pL-TyZtlIMWdv>p zjc;`l+Yoibn9;V<^xwcnaaD-oDK}>n4OZZlpQ65;{l$pMztbz%GlfQ z=eua0jvb@({H}-J8I-v0R(%CQkhuH<=|f>Q#r?`v(;SzYG{Hc zey_s-B6`z?Bio#~Z>11n{R%Ie>G=KLn_3pfGj~H9O})wXrnl!bX~sJykU~p$JD(9t zARXXIsDWyAS{9={MQymWw^o_q@9k&-N?NWcT{k1dj)%K@i%%;6iE|StOFREPgh6~U5W32CDmFvh?JVRf}AbZVPfz1MaW8)fBO6Yjg+4|4ZL(GH*_JS3`x3M7WrZ3NC0 zQzF7h@>etN*L75kY%{r&lP6@b&YrU44RQC&ne04aQ^WP#7jT^>Vo7Rim38EF9wb#t ztr^dqy-#)EvG(YV#2{bneBax*YsyP^ny`{p018!{We&n7IMdOz8QJT5f6`%O_*@hl z85PVsCQ}+^woTi|HTdjo?|QU51H{X>aCWi5-Qv@eI0)|G3bVhnJ+S4J*EXyUKMD7g zYnX{Ac0Ch22WHJ}G{gc6wQ+GmCg%$!!+Yc2PhVyI?Z-0Yon!YsyB?)o@8*s_=tiV5 zFF@^!NE&I+t(Hv|xfFZaYksh{-vg<&HZQz^nR<|RjoD%tEb|7W^k5H$OKI{1-4J%~ z|Fdh^5N3r&DWS%%mqCw3J+0pH*bfeVeOi+3Nm6S!-Z^P2H<>u5ZW?Vca(iyU<-nR} zMiP^kO>-w`!DSVKB?K9?BrUd0n?f-x>A{!si~1tkgd~U|nbbl!lEsA*Cgd7nI#stb znkB%6k9o6l_+%2-Emm;L0&Ixuh4Xa#)8E*U8D}mz%s9y1o_kOdXMGKbiu4lhj(SN= zfvbKm!{D!smLnUHBqU4+vyKEhWk4iJ;?SYHmjtMiUUZEdft9pH03!=X z(aNCv$?SWFir<#Dxqeo8*8RYmyNn7iX;yK$v*{xaXvIp-s=Tj9EKP`jm4dQ{$!hID z8&*8Dt#;9wL~qDwI;}wp@Mh>_E@Z`UN2tE4o+Zf8b9-*GdcGk^ijdCePKV35VVa!q z+L1Z_G8!?+YZ^&ouVzGX^Yc}F;wC$9zM_k4yK(3o#lhh1xs#Fwr-qzG<_Wj(T^JhL zhr3rzk|&3bOyFzo7(3ShMbZmlGgqos@ql;B&>5IVPnw;+T;O$1h{nD>cO3Wr)PnDF zHj)d7RLw~3f;3|*=AIKtb=gYT@%MEW-F!fzVv1Lv>4UUMI4<)xaM%g$+6Du+=N3;) zod`ahVD#%nxSTDE)3QxUzg&dVFvV_2+xD?-cHFzio^`luUvxYQVji8@*s{x#9|t^W z!kGOr#SXC6HBE%&bgZ^*`!O`#;Ws47f#(!x0VOv!58#q+nnO`WC3$CZbl&Ep;(T{m%9$DA_K7J% zhEQa?{Tsu)vpdGTj${0l#l9qiqoCO;$y#$lYI}B80<$~cbL1xu^;BlX&Eq1nD$dJE zGA?VfOr&OaX(K`fx9;i8?~Lg~Ea2Qbk|anTZg*|R3WlHoBUj(*T5TU0GkLoTfeIRP zBU`YnSg6nw=9-1wiUX~amD(1U*zyi#1k)Jz6Q4~}!A08RDsC1p zadNQ$xTPJm(yl zxmETlSt;j0xF1+3(~0BmKODCiZk%k6I|7ww-bOha*0wiS*Qym{*X1RBPQ)#8?|=#c zSHpEOB&PNhzHTn*O3Ve@R{c+BcRn=A1U@%|(t(Xj##YA@Ey-$Y2$Pf-K`l`0VVT0MpZ0^ilVYL*a1tj z;ixZQ&kDnE49>0Q*J)rc68afKm|fU=M?SeIsGz0~{goCEav=}`cQ)T>^X7JaG{+l7 zFmYf4Zo8jp5=F7-8JkEmG~^FD5%Kfb4XueUVIoNk2V_{c*=CzYw!1EJiExKHQ!)RXrJsiSrefw0W_D&|)2^@_<_Wfi8coYvEi8-dx2} zD%d;Mj|DFwf_>nYaGFoJOYmH2Ki}mExw%IN_HOcL3Y%}sACvnR$H(RMDPw2JpEX&> znYR0+-gsY2y{uWfCvhk1W*uMHYZWA8Zm9|bosHf8^fY9A6gSbt^hJPZy2C{tXLHbA zh@{}J4Y>v-zA;pJN9P7#kRc|&DC~6yuxQHv&Oa2Mpzcotvg?%3*UQ zB-+a#M^58>JS^KIjP0D&u+6N(_8_mwY$7>$sk+}-6Ya%@VEtyh0q)EG1N+-AVMcgpCNRoR z3y|qLBV|D695icWLNT3*6^z0KmK|;y)?^jpX&7{{)5N@e-$jF|nNWH)m-wZv+ zZA8+KWLcms7(y6KFJx_+?HOFnbOZ^|U*!r-_35kiiB1fOjTjP>DMyOK{q3=9mxOkW zi~FN%UtC^og7_R5V}HJL0+YeEQrM843FP^W2J_IEA)7k#vJ(&ow}$O7*5IF6aejOf zH;`90$H#@H+i}{LHQFVJyYm}aYY-j*b{T82*?b9v;|AwmbZ$S z_|eGiYY@%>;98|};f~PhLq_;E7nlIyn=YpyYry89{CU9g{E#&fz@H7FQsIBdr~Pi3U;-)pwKR2uAl;rwk6ExHB| zgs0dHY*lt6LgU;nO>1C-u!0#bLc^T0Voax-d)_#2u4R}4Z@@Hg@^uC!t2(By&2f$@ zCBBsevkJayu*`4ThH(h6?V00u+tZIO5+)Y=ZTrr`aL2@BplGF@30!3YgfG3k#poV6 z6fU$%N~YxlFq9PB!hT@m*uwW+ro~0sNe!!{~|?IxxNVZgTd0`M|%l& z0K3x#U+;uG2KKg+Uk84a#GYo6BZ027?FBwy6^3UzJyRla0|&1>OdTaYD}*~3sYXqq&=N4KT54>h6voK8ufR(;;mPu3S2(;#lM;9C3eu-V@3KYiOVg%3_ch+R zPIoXr+0~n~;3gp&wf&>32Mz{d@4y`HFIe41md&nkN}m(2AXVDl>Dh&Y_b`+Er@F!! zQ%JrN$}0yB9l8gJH1!o0lCSIu(0qG<+V#0N?1#}13RD>OEg?rajNIo72r0#8+*&98 z&HlXso#>BYEQ$Unb@s!4F#`Ly`^%eKjrv&Fe{`FU8{wc>8F{DJGq{#4(EzW@EU-QZ zKGta*iPvp^*}h}gIgmILhgO0pFsC_K?Dp%`4g-SWP!C#QtTNyKl2&jSq8`jL z$i-5tf2J!Onv|j*l#Enx_cn5s6mY@GwzcH z8dqY|KsO%WC+IiT_7|bA>q<-4YbCzW)jMR_@5MCd9T0(`SS?&?S^*_DL+}IP$kD<& zkGS>H!65qngE)odN4XXKL9K2yr^2L&;|C*6yuthocL0ij^|+6s;9xjVt2AKXadaIH zXzaJIu&iY-SG+OCTU=V0kDqc&GM$&FKDi!W=?YI1t|2m| z;haOtefT~)8_ptZibpJB>=wz|Ij-o6xe$&wyY_m z<|W^As-I=_UBUGc2fx&nyo&<7HS4Y&IE4#Gkz*QLJN~}H9pr0W0se5Q(oNx~gKx0r zrZD)H7bsV#XoDfA1j_6J$8CSMoE*c)%Z{Jp>KVQT^%aW1D!4uZQGH<^`;e&M>+68$ z`V9U%UA-fQ8H0On7SnJK%k^Qnt9iYK^S3ei7m{0Hd%aR9ey=P1_yT{aEocQVFO%zE zSBG;)(_RS)f$XOc$PA;yH%=bIJ{mp$K&e_j2h9&UxVgLxo($;4AEm6obOtgL-cIeX zN!w=Was+99}avSTMAFL1h5K?2(TKH zuL3&NWfgh_%mDE7^Ccuj#h1+S3wC@-e1*NXD5pqZ;cynf(HGb-)9RvGG*XAzE50cd z-Gg2Vxa1W=BwV}ToIqxT^MaR68xc6MQ5*ZP{cVS1dj9NO+*qj4dA`> z$B`*BznC>>YWn(M<@Z{!QZFzWZNjwdfZ|#BT-R^{)55->Hyja-b6Jm<`oiSzIIL0> z{sDwZ?vv6ewn~Rx zTheEX`&6<{RsGL3eRe;GLI3n>u~y&}UEyenkM1{URzDzL`C(V^!?x4VFAyWUbl8tO zB|);`@JarAT>O6mhOTcGD;w~juKl>H=`xqb*l_L`GECQ(?m9Se*Fks*CFt?RuCSiS zb=~ytfzeT3KltOWYrXj;8zB{Y6pZQCgYFfWBw|=Vl zC71BUFr0B~=y*}nGV&F-j{X(DlXRPKg+OFbSXCAbB%vMqiyTiFQt+z$- zu)CT6E2XvlJlGo!DUDF~>FN$APHYtrm!#D4meMvm0NbhHQrF(bT`)zAkfqhiwPmtq zTiM4O!GV~=2H9p%1k#=dE^o=b&Z*x9>6f-qf-0K&336(^fFw4B8@j{Xym=0SuI`tQ zs5i8onM5ZDAXN_72C(49?xaxh;+_4hfZSiY4=qay^~*wXR32;hdZiRyw%NgbaoT!O}RL`wTO#1 zgFTVPFv?DbEv8=Grr)jXX*dw2Zc(_5tpYDCB;$XvDJ9DGCq1lCtqx*P5bGJO- zy)QoEILD?oUu2fKki9#2Y0?xrR8S$(**~qVz>Yw_4>X3O z##+4=bn6nC2AaKjYg=u*Hft92L?6Fe-*U9AiP#t5nlb4vv_FQ>8xmzUdlt?Bpi}t! zak&^tGmK*mdEklw`r0ajL2x+8qjPw~;B19g5olPD9I=T|fkPYDx`+&OTnjTUm}xld zc#H#wqvQFtQ|u|YA^Yj6>4lTi$0ir2r^a(qnKSMr0EeftqPC-lca*A?Sb{>Sbwadp zb0w!MC218WzigL70kgpwz=n!TY9@?vcAo5hv1084!K<>G-NCet{usuMSs@s#CQ7U`tZX=!u*=P**?b*+TtWQ0GE7Ttr%vHtwT;9Nxh{;|X0alj z2RB0FX6tQN1@ynuHL=)g*JQ!037YT=a@urmaiXv;EH?O!Y@r1R&)(30r5P>*&!211 z3V|{7A;1#wQSb=7z~D6~0bzcda@6=27Q+#ZTdfdTF<3)d7TDrjf%yiuNjg1S05^l* z%ua_(2>B4JCq#8<2e!dsXX?`aK)|G++z?Mq+4%5cRFYXGKQjYPPv} zQ4tWbH<9AuMq&LFyc~j?Fw$mBNcYFdQ=-TbH$_22!=U1vp%Lha7fY*5YA2{U!7O;7 zU?6S{oy4P44LGCIXL||xjDz2buHL65LJkcMR_61ZBrY}#!GHwEj%xXG{1BSP;2>z% zp{ALw>BMF!`0eb~XVf%>`Ngp(&mrjnSYmI$98@`$C@jfvq#;T{aJt@Fix(c0IBxVm zSK2&ZUz@GXRW1enks>(EDqDrRZqVu3wIc;u8x4~~K3BQ~{)B^di4rCh*y_O(MaP9h zVSTYuna3`qTc?Jj)_B?}N?CAoc0N?iglcGWvN4%Ha6Zx2fT63U65_MC$fj}YAUITx zOYv~>jZYehXASa2)w}4whW2oDqS;EN-Pi1kA2B<#`id){+`HW#|2rvMZ`vDFCQyvt z6ml_gj`coo-XFLXfh&T8$ES~%E@|hpwNr(S(u0`M6YIs`ccb?c*Xb;8_D@U$2VTQV z*k*I!5L|dGBs~aSU*~fmdY)g_8YZfO*<2}6n@L171}V=qc)Z08eiP?`;)pe6ka5ag zWr8c4mAiLp&*9rf$=EKOyyzNn^4BS3hzNc!lI+gu25Zb8;oB2Dlp))p$UQ+*^M%Wb z!_$HfrDE{=W~xh28?>d@eGN#-RQAc0b#Q#>PZbq?v@nRD;!PkV- zTMG6@x|8|!Y^Jakx~XGr;mV|60EZAf!5K(=V!|2xTQOs>$a8g4YM{3Jz!h-2_Vbg@ z%8W%;>H(u=gDtsHfQiDjb#%2Dh%Yhdt411uxvd5}5v9dH0H>Jr%$jmZ^DvTfiJGJm zZwrrv_e%xQI`rWdUSw}fi6JM+1w|(@T{7oxh5r!R#iY?7FqSQBlsxKg;7~$8x#I8& zhOLM=uJjpik-sYo)LvM2^qz|}O{14OIuBCDBx0ekeC2<7PXGiwRmm1Fr^j>~C~LYfHvbt7R}LFr8=n!(kaOSakQjNBzRr)5%%dh zKvIB*L`P~?b8I{Tap@G|J?f?T5~Es!q38xIkpHw9hl`NGg^aiqlB@zZ%i&{~O!&2W zJr*nR*nNe@YN_rfabGm2HBHk;wGJ~696iCiyMjBTIn?BA!ZLFu&3lv6kKa|4g!p%2 zlcor5Ab5TB*%R2cby~4Tn`j7SFkuRcR;Z@kTsQf6Msx)%AY>W1X|btbIJ(&nUTCyl zso*N@dL6-JHBk{$yzA{1vp!<@FpV!xuCE(127{P27oY_DqYvW;0|A&Iwo<#e<`u#W zV^{nL2C5pb(`|5=a0*ml);Nxy=3E&qX~mpRL&U=&N{U)W@oHkO-Vl8kz%|On)}2!n z5yRJIIF@}_Cch33KePqnW^-nQW)lfUqNnh)%zP6=2l*1?k`2aKXw?yBi<1|TC53M8 z!GQ?MEm#(H2L$8FK4E{1XB^jp2BOOr9E^&#R`Z2nb!5dVm*P45%c8zNgM>c8UD2AY zZu)@Peqxw9%utR=0QY^cJrsjc=y41+lq#rds1t>aL&BbN5#d%vLoct#ba7Vk?g~@H zk-}v^ZYClhJa1K;_z@#Soh&iCfObzCCgyscGEHzO`li+rn?*EpLJt&0CPpH`;|u(3 z+NT9Gp3|?i-DycpRSfQqUYKlBySQahw6wXt6lx3un8 zx~JIy^x|3YvB~O6+3Z}wy-__07}^0REBF^i3`UF1C-|f2CvDcp+hxV5BV>j7SEDF* zM=QR>V@}1^_Krj!bT!Z`VOCPa!m^7{DpkVu zR(EJ%0_Z_aRQg4W!OD%e2~{$snz%}<`r9g64LNj%4*k4)+Pr%tjaU%*aP!p=e^ z!S4O>0{Oea0UH}%45p%oSnt-|2GkehBlm%GmWnWYnLzqrIy%jqQ(yvEN-m$poEhkh z1{G#&*g~+P!F|ypYV8}uTAlj2Usk2i>~#qu_Y-X+v*{3WkSUCM=_;^ik4p<1yVJ0K;^ly1pHph(375??C`1z8C{n z;2WztQEh2c_M%`Rx;1S2d`0x~79dmy!ea18fT<=nun{x}E7j)twR$ZEXXGZ-MpwQ6 zW$3CS8jXkH&Ve7Cj6|D&8ZE)K3Em$s&caN}D_YA~D&2U9nWsQGo@;6{K-2>f*2}or z>pbNXZY{xL^pvf#&C2#-Ic!pKnh~)chDGjbstX275mgh26|!#`2M|jCcrDiRHjUWL zV{^uRvJ%7zLN!0>#>$h}gr;IZXHjj0v;IKpWKfoMz*+HX!%oao5 z7w__gz+a+2>_>`-(&fHzRG2M>G|Mu)16&?pI8mt90V!Y;IZFBvvr#MT3CCb*oN!IU zp`dTGu-*2DUc|_@xMBpicK4cRh>P6V2JmoMdYfh5#c9=4jp!g?)jNV33L4 zEkELH#*qv+Y1^Slf8G?$1;VlAX%86_XXM~$g2SldLzdDPE63I=wUX@#2a|e9sz`(s z;`plw1Fj!N9?6Mb-EsjO8kB(1`kqO}LeS77^u*KXi=1szFvs@9BTU}-g0EJgG}y$4 zm~_MIfXaFBJ@-CRx2Or!0)m9V_DAJ?b2z?Q1b+wph(Y=os7QaonI~dpY_b#Og8Nrm zMR{4b9EZn|K_rNuvK=Uc6XyckOY=`bHrWHYSAA_qJ=hR!FrreG(8I(J4>MRJSL7QPj1=jyGW-v;>uECFB3N zktYd;jqRrO5xNTFAmP>~MG1#$qAKLA_inpU;V( z79RSV_o=yi z5c`~|tH80(H;!jmGPUK&?(i=X-f?cn8_p&B2W3rO4^*A*b`ffJR?-VHb_7TGlrMLiL;%;n3R8C>5@9f!KhiPVW)grHx^2` zb>J>e8zoZ%yTc3C=0;cXc*Y=mvVllEWbmp>s+RIAt*N#SwPfwv}4AWx-32C&QQ~V^OT2 zLDYIMNH_tmsz$D!=)0CcyjQ(QL2B@7W5vR*y;@qAHq?5XjrNBpn7jNe0+Tm8DmujSV9!sYHX7r=qELN3s|(O(x|T zyX~F=A%IM8)N=wF3OPhz9pli5LY{85W(T}})-zpXzAA4(3p#>RY4UgYL&4ZyZ!{|m zmQ#iUVRo2muW@)mgqGo)^WEVBWAmsT%ogGMuU77TaFm9GWqdz_V9@kYD)zch@9z$` zOdvqJ7QnLmrCIAh$_A19aM*kUPgA@S1oOJ33K?cH3NeogxDIePmg2zqsbt;FNjVC z(NvJVZwosL;zx(>8y`0d8U$x`p3ig#g{Fzd_**BMuVSK0^2KOhP`>89G3`IT;kQPM zyXmblHvr;0T0Rf(p?D~UlTx_DAjA!EZJ;3`7ics4#n~kuF*_p|t!#@>D9nc`OYSCL z@n2HnH7PuiN)a34mlwe@t*0zrX4;oEa8@Q-M90?3BzvoxnuIrN&l%CRpevf*!DiV6 zC|>;w;f^NxC18ytVEm12RZPq%U?1{C2Cb4ugsv?nMrvuyIw6=X6dN5(YRGGeLqkL6 zwRq$>J!1{cW4GKNa)7Q?^By;Zmc|uQFwJ(&!lK1peXrTou~ROE^QxLc%gn=0yF4>_ zYG!$9;ms!}r>2*WF3r7V`Phl2&Bv79kjOPVt?OpP=6Rnw0ihxce5Zpx`6#1w`81u;v<#oJA~z=@x-2rV_@VRkZwQcPRY^;g~k0=cI`A&xG$cEH3e}9HKVK z=;&xb<&a54$20*O1dn$2wdHMt79DGQjhqhCQMfM?&4iD2`=kh=gddECgEQmfkN{!7 zM3Be8>?ZTCM#1CV;i&mqtH8bn=8)B>v6XG}WpZzWJO5+dy@_xk!*en9Jk-2YVIV*L z_xLrVNty>f0UX%V=nme~1__|0!Fg3J6Vbqe%&x>ljp~JoRs0OOG#h3cV^DBh?l@E7 zA~?*aLGZprgo4GiG{@~vA8-UwQ4c+Y*_uyb86b{$aTXT2W6C#>j8=tBiySze#jEi{ zi{#k#gcdiuxj+}FJ<(HdhJh14RPuuHfHUqJ;PytkM(6-^hm$4S?D5Jm*lutTn2BES zwQ>OnmMePa?2q*aiS4gxk2EL{S8b{kk!rbDaDXXJOb?Hu|`hI&Hcyu3ZJ!2jaFi?Q0UUiBu~ zz@Yrd(2wwiX6}KwGv~qv1adMTY_!tsGDALI2z>*P--=Ag)N1_wD6#m-?%-oG&Sx;r zXRdjipD^S6RAQW;Xc_0oB(|jnHUc`ZHVv|m*4l71HNHM*J_xZ*j%|>rDB(#A$fl7u z9MJr0t;6~hhxLi>;4?mcNaiI?GdZ{Q1<`HXQd`wnen%D79+Cl{c-(7EJR}=CE?#dS zyLLiT45PZNqWNAWJlX$6Q_e=&z0BVekD3$LH!&kz&J}dsB1l8>vt9k z1&@LWIGoAe3c=*&@EDm(Ld>!8EIOP7aq}Ip1gI%AwIc~Dwzi8njy+$habR|Cix5k` zo(5G4cg0-fP@ucVP3eviO1oVGzHJ_XPC1x_O*kD_665>7ymd3+==?SK({ z&WpUpBW+v5^hT2IR#~s4&Cw=>*a=c~Vg0^xX&t`qOlZ=^^o6Vgz=lltE_;?ci!zni z)&^kP`g|IYG*T-NfdK$rUYomIUx`*7L*ZwW10}}D;EO>x_H_)X3bj? z@Y3XQYQMd@>HZ7dy)#B=)8kFx(lM!La@qlw_M5h0%cl4{ zS5ZP;d=qwMHICu32k;cU;Xv@s-p703N2`_#(Gi_*g0`14HWw%k5G01%d4pVi<-g+R zwou$aUVobOYi;IsM$eF^k{%O*`U9!IqL)fUeHz9POStzKXiN_ry0p1yh|55o2W;3) zZH_{c99yA!X%-wIj6NZX+=ektdB<1N6-??Hvx8H_1|m_QT=_6E_L%+>91*8u#auEx zMq7*Dg?`O~vU__tcTBSw3q*T%0gehEfB|%u7VZ?f`#5 z)ca)DbKO7f4xa1I4xB@BiLKRSVmmdyB7H=7B;j!41W(BZHc88KsYOZn~9}cmK*V9xs ze%y^bdwf8~iYD>^1g-ReEIBj<{*W_qI1GN=on>HClb!X7`(rajwOFcIRIm;sf|I616MwN$Pk{{ua;3e|1kgh}Bx#2G0q)5qK; zw@RXPt@)?XsxDwlb^93`;^9zv6(`BtE~ygkk(JCSzQQMJ^L07qqvXQ)szIvx0Cr=` z=5reYuc;Q6DG~S_lkoDn+&pg0*w)n(_Ricrifj};y<-e#Vxm#ej%#9s(jTMX8PKa> z{TPt_h8`-#UBQj~H|z;+;=kY)H}l`;x`SJK+{v(QGA%`9ZP=h5M8ZJ@e8-%9qCiG% z{Xs_-s&Jn?kBx28v(bFb#U}9q(8#1R9Ja-GC_hRG2Ho;s3ZdZb= zXwC0xc^|&7RC4adVf0QYkU$@7axj34#SHdh9Pt{@<5F<~u^kk3-8Xxp_olx=WCveL zd^h$WLO+@5Ky6~!HRo)N^q{`LV{b0hU`}1NQ4=6*)KE8Fm1U#u4*@H-F53`7R!i98 zj>tn|VcC#KxzLq38{OKFdNE^dZ9@}@_rwjdCpyg@n_i?JWIX0vjj_)nKWR3~W7}5= z8~RhCZ2dd~I~dV&KzeIw=|b-RY-2ju@aK*hAw7Dew` z-o#Z6%(}1)PKj-I<`}gxEFv>Hs7VbZOuPS~&P~l|Q_PpWjEQ9PN2HxosKKaNVOqJT zb1O%*mHK7d#+bCRc)7WS!<}0g)fPf8y+@rHhH2yzCD5h=gQl@+d6yRrh->&gn* zjjpVq{e~+mY`^Ks3S6HnD|9!xvV!+puB`C=wk;o2`1)O0;k((D6~5nbWrgo|U0LB9 zaAk$>7FSmIe$SN^zTbCcg>SDbD}1-wGBb~3KL5a#6}~@oWrZ*2$_n3YuB`CA#+4Pm z+g(}V8+2ub?+#a1_+IPE3g7E&c~s#Wa%F{YpDQbTce=8|_j*@W_=a6s;oI-Z3Loy- zQ+yg#_}<{k3g3t;D|`oR`H;eQ(3KUwyIfh}8+B!c?~p4ie0RID!uLj3R`|wTS>e0K zl@-2mS62AmWXpFed=sv$@Evw#h3{ThR`~wNl@-1tuB`A)y0XG|)Rh&!W3H_5;f7e9 z-@6sQXLLn^q3I*|; zuTU7z`w9he%~vRtWnZCSzTHaX=y6^c4ze#aAe#TfRaeeb`qh zq*Y&`kk)*KLR$9~3TeYvD5MvCg+hACRhUiXvae7`w|#{|`VL>Akp8K!P)Hx~6$AQV}Li&WSP)PqVU!joxJy&5in!oTB3h8@%g+luGeT72$kNXOR^u4}9A$^~(P)Pp? zU!joxlfFVBeZQ|zNI&2z%$D;{`3i;fpY|0B=?8s8E^! zLi%s`3WfCFbQNYp`n0c5NI&B%6w-goS16?awy#h~KkF+L($Dz{h4kO?6$njw} zXMKf2`gvDjwx)m2S16?azOPV7|I$||q+jqA3h95~D-_cI&{rs=U-T6U>6d(kLi!*1 z3WfAPwiO3Ql=FYtS16=k@f8Z`f8r|?(*M*~D5PKY6$3{7j6w+_|3WfALzCt1WZ+wM9 z`ro<=vqyc`S16?4^A!r|f9ER{(*NF9D5T%_6$Hq926w<%)6$Q`t4s#INKcr|^%6tr3z z5~~ReK&vHV@+gI24i_9q6`~Y6TTn){84VB_mUc9d#28=h@5!zi?-6T;hhd1<8qn07 zhf^aC_!H(*W2iP@jI>^vv()1~i0z2-r|c6=w>S&2<}Nf6o`Iz-IMb8uFbjK6BJ=|X zCNy@P?a8)rbXoI$7`0CA7kaXFwYV{-B@bHUXl2u16 zY$I_BCuc|UN^QGGVgPg?dHR{5zdEmY_}<53)f`9Yj^Q!%@_y569fS8zAXBKT|F z{Rr2+V3g5%vzs*WZ}OZD&gMPYy=V%l7E5EAhu_i!?Af?*!Ck!+Jl>NGDR`_WbvFMP zphJsK5XipZJ$U_OsfHs|I?#Uv8FJr;PqHU2M8QV@NdWt7H45IR-@Feea(+MyNxme` zL+~ugCs-2BKg#gIGd>n!cCR@Rh=LFFg!XtK@jklSyR>;>6Pcx>;6r%D6q2t1@()}c zigt~M!y2c6P7JTaGz{D)c~gTuLH{Y3uHhvfAegNPKhDZIOzhO;rX(0STCUdDEpn~_ zBl93$6Yf|IUKL=_SDoOtlhQk)jxZX$fO$j~H)P8Zu5P+xuyZSvaBX<9Cp&^AEn8&6 zQKO>Wlz31?VIR5jM|;BeA6>*b-F#sS*?{y7b;N{kWmT^r!!2nbwU72hIs0lhdQ13B z7(EbpiGkL7$$^MX@uz@FF`n$_zB0?QX7Og+Z35-Udx4X8Q%6*y)yWg(l)~; za6*nAW{hjcC|o<9YhwNgU*+W~m{>UxZsOQKXoBad6dnOX0nHDa-g+(#^>YgKk9&Hr zAUB!|UySf(^Rdu5IN?w}pn_;POa~()poXTB;kmf4tdAHMI&ClQ07@_w0}MJ%!ig9ed(~T_?{F{Tbz5d5oGTRHj-3L+l1^;P@4+v6 zdV5P3H}}=`;QhNWr{by@wD^~OD%Wo24Ob2^W%=lR=&k6?SM&Lc+YS7H)O#u z)(kVe0S9%KU#!W?H)O?c5g<)WUoNe}k-35-_vb1P9Hf#kMwt|z$#rC5=LHG3gy9gq zR!>R^xN?yQKNj*<8|MdiBSI6F;Ccne6JU**hBDf`32}yIc@+cM5!SSgW894nEY}pU z1#S-9WUEC#NrNS%50Lakt`2t(Bq!=-?AR>~{$@-AChBI_$&iPKNk_)um@2m%Y3lHA zbo=o1yGH@=5%8$Wxn(*zb$G+K(BQ?JS8So}8^JA2t9Wa26>m+g;;n8Z?N;$7+-kLh zWrR=PbzjHZnwH`h=>XVYz#SAzaYxg8H>Js#o^yC_U(XX(G@FsvW2{TASgp0KVm~ZU~bCW0#UxIJ~JOxG)rpcFIpF=jj1y`l-^n?Fh zDD4Ct`XHNKNWqdTnc%wCMF|J74?`CPps9}rVF^&VEA>-7@nXO;5(_Mf2D%Xclh$? z7Osqh1L<;&>Uk@~!PzGCJ(EUXASltdKx8dtgVIisb<88{9PZa`W+^kTAndO-!d?Zb z%1ubcP=gi`28cwY+Gu+3;q-eC>w6Dh5$QC5N;xEK#^7Y>Jq!rffy4Dy*D9@45%ym| zrm#Y)I^ofuqV9>ZNyX3o1*!N`AD>2m+Oi2rTXw7<%|qrGl4svDj3L$76dN-?st}ic zg#pDR{Pn`6O(PBJX~fK|UVuhR{)>LV`=C5*rRJue zcXg#_`3ROo`i8Y!{4`S_OxQP!lWzk$7a8Q+AZ5HMcknEj;e_24IHyP%mFm22rOiGb z_KU>Pv@-?`^0@76fWIIE+ePr<;89j>WP`_=F!k{?rarEi`uI*Ub;M)p#e>bvaqbmd z{acEwW6ij_dsGcwkob+=Vcuw^2BS>$_yfp97Cb@peJmTirwM)Emqy?BDf+%|r|3JS zoz1|wq&J@7U|>DG7G8Q)etEt4Ufl2F? zN2lH~_7+f6`1)WN=s=mto+*jq9lw5GLi7B)fGu;WDooY5Yxe9XsOnoy%>x zC4;Nm|8hiRQ3MxX^*8T*-o5Vc$HWwkw&-0&nC< zL2{+-dtp@)#sg5^T4MI_R15Xyx9kd=3r^gWBWxP|%heyLp$_VA2%Jj&w4fQ`v4Z@% zrP1I6q~#A~gAX;)@{?&=eo|@q$t%(FNO2qaf68hJ7@pEXT5MYExD}14#@^*ni zha264kCGrhmJRUt|F`!&(2U(qIc`h~-$$S=SFL>JgS6yC7^K3%uHumffJ; z71As$0iv1Jj8+4ZMrcO6D;Jve2~Kd08#}=XB_g~X)w zsgvSU>_TcM-`4*6L=rz zuMcOvhf~+&k=ASS2pU2Md1R|=($ByTWbj*E?59Ut{+1TjE=D{<8_fdM2ihTd-&W|o zJ>I@uV86nPs<9<4So#0%>N%undm+Am13mw|H?ZAh`Hn=hS4;AGRYat*^GQGFlMDEg zb8b`hRiv6tzih5EZYe)(ErUV-rVNCK?V966mWX zD{fOx+n&(3rC8>_LRs(pa`R4=tv0P+`Y~^O71{beWxf~F5D}Ki=khd_H#Uwe zKt-fAnI`@E6J}X)CVhP9Q``UNI{bc$X~%1|CH&fA#cukZHVtm>u}PZAFW%AQ)PXAo z(?pV3oA0p^e)B2vkxQVBHz;-tY(9W`2R8pE{)x>$6~rV4sV8;a+iYS`WjyhVv}w{$ z8E>lwgX*-}stN924XUg;EyVw&xV0T$%%ZOG6?XSNMRDx0toK+-9DA{&#U%V93~ms| zo?!cDJG`e-?LX5hzI{f;x6f=Pz73)-!YRf`LqZ#zmH*5#-%DDAYi}EV#aHnASEt_C z{C@m@c6IhRaTLltvs7$+F6$p7HB2e4Bw4VweU7+$@ec19!Qrg;>{htR;O9G#*c$HB zejw!?l~(P$|2y%LwbMe>>)Im7_tA1+DYyI{(cP9WyGTVGI9k5;BAv`8%JFa&;oDIT zOJ^YQzCZ~5Mb^Xr(Tfg%eJOR{pKZPG&!Q>l!hjN6KhP)r*3z-t8!W6<*OyOLX&1|&M4kV#b&6~1HT4=q z&P8TaM>ID6Bi{SbQIsP@?9qH>_qB-iMZv&mOe=+zK@qNKXe)(9mU3Pmg;2$yRu4Ve-H z!Mm*Tgj^t9(J{K3DlHu@^a5B~r9w%@TaG&*A&&gm(&2Splm(wr>WLG_lLVr{T1%H7 z@Olw8ae5lj<>&N2dn3~br;UaGk_8P>;&_oK4gL!v&cHhHv}2)*~HdeD$14~ND(rL01_r9 z%OJH_bRI6XSWAaDAawY~LqOeb-^G+75U{1$4|%cG!%X>we^@@s zgA?IM^!!2#UV6EVm-%E+s3FFj87J+X43q-AdWuS%x$L7NkTMOHJ=hpuU0teHPk{uR>Sx5$T7-4nR{hhZZ0*5M@z7& z$CD38rceu6HZYE-XtD#up`z+x0Er#xJy9NXK>Bk%iM_G7Yom5T);btX;O5nd`uCEl z@!1g$t1~xzMwYn6|70FO#k5r>7MB4m=aVGt)gO=q6s0-B;phPP?mz~|3qX{-ls7#3 zj!IMW@)(&0Dzz|FA1kd&Dy(Qj$Tlv?G9@V%5`Oa;!$9bsRC&1^SB>PTDd;8_iNj%5 zIFJUgA%v@j0n!OGJ!-Kcfv^>(#hkyR80M!XL!v;%+XhHh12%QZ)?KnM1yE>jt$frM z_cAZxwHlvbxwff24+5d;NMZ(rcHa%8k_Zy%V^J)jQTM$mWJP)?(0{qSs7OGzDIi1d z&3S)UFL zHh5u>rb{ZGovPN6)NW-#Qn2#`r)xJNMcliQlnnXNZTsF3PF&Wl7o=nbe5xx$V2O|p zrAHX-t(_Juz|`0y&zLF1PDtsQq1u8qemP*QH6}$GSWI3=038ypX`WqKEqX3uhm!J@ zRgW!UFygtS=0jeVA|%0g7z3(+wZ{pEZ3Wh|sEbF{%_5TOp%TTQ{7|T^R_E&^aKpFx z883gS&ug(t{5p;Z4c3cip)WKF@@o~K1On<T`gvSOt8CBzn|c1 zTEtki3bzdJ2E-ELzUv$(A+;cGR6$&>z$*&wgVkbsmnaUXEl-prlP)6hxKtc1&rQY% z0fuUQ(kE4`oni(-P_3bP5HdTDC9FwO&an$H0`EEiZ4@JkrhuVt{tk{C7yrf{pO20U z`*QsjDky9zlM|?k9Sa|6e0S@37ld0y4f=Bujy(dKi(^ZASWdn2#`(0%vY-knUD-ghf z&4Rl2N3XnMrm|o+gKQ2kDy*&Jf(!hFxkZ(kK&7M_Uy)THAW~_+A_)sq5f;N>KS)_6 z$Rnp?2?FB)M*(EQVmu)v>eZ!+>+6%XW^dcZoGv69f`A+bC=;QAr)*M_0tbU?#%@@* zk^`+vfrMbMAEtp>6+De5pp`c(KfuF?01iy-149z-a?IgGTq`U(q$>M8j1 z{%d#r!NW&y>v;Ww{`ockwcf73x%>AzT(dufvgca<#~Ux(zr(fs3RuYUf8 z*BhtbBi+b9pAKX}!9Pn%66UlYl}9m}gOa?ttZ7D3a6|e)Oo34H0e_e0oyJfP$^U0Q zD&!Rb4J>sk#pWTn0HDSo5;9CUBEyh43LfTi#S5?^;QpbO4EM)*;~dcjv(SIzuomH= z5IPK`s0DXaDyu;rRr1Jl8P!WRSzW6)2Et(CxI+EI{+>Oj?*{1b=S$z{#h)m4!#VE(~Ihrbl;Jk97NXDz~BG={cnB$m)|ux#rA)3=Lc?m>Wbr^XPe&|>s%Xn%l~{o|NgZHpZeIp z{jslogY6&v$ooEc>gM;qhJTm;&)gGSXZ1uyz7Y9tNUn{=2Zj0B*{cu}kHoO2 z&t@<4dKH&&m{MvLXUXq4S2oZ~3JN5Hh2GWV9r!1>?@|c@pWNU}_*??)0+l;2p&*D* zEac8FSMUOk)Y*8xj1Z8>h9GWy1_jEtn>+(GV@L?xX`AH42hKqW0+oKX^tjx9zPEK0M%V6t8~`fL;DXhbS0JkHi_8-Hm#V9N+b@|B?Q;nB zgf%?e^Sy^bmwHNI9{4vvfZ6S$h(w7R0(fsAI}$!IvRgkBR;-RQ4>kv;OT7=x>MRTl zz{$`mEns9oxU_~AC-9NEYXfLjkb%}&<5g=06=ljf6R<7~Ms|}qvlT(9B0b`(wC-yW zMnZD13HfLjm)AN3+5m6*3ofU(AOAJmbGUX2Z=nOW;h)SyLUf#TP0_E7blg8tEgzM) z2f~4J|0PoHxHxu%6=-=?WB?)H`cT181)G=6g&v5MV4I36_}#U`zKVDg0j{vV3OuT> z&r|t;PK&=Dx8M?<3#7$pG6;#RK~}DWlRjl`42RWF44147tX#y^L5O3|N(qvb9&60E zob-y9C&;RdNa1&51&gSJ7y6I*US{}^mY$xLxa+>)ZiSoIcb`&g{mOjG2+PK4sYEN2 z)I$%V!Cur)I)yufg~)2KMEqZ(w67lLWv)Q)Wp1qK4(W<<2g~cQxZzO@Bu`jxV6}sU zgrDhpsfWSMcs7lV*C0QW-RrxQpe}Ywm9Hd|1ccU&fPvAjP8DHR1AaPTm}TAAS!!A7 z+ypoz6)8c1GhV8oHk1m)*~a~a&28|Cxfi4y8YlxO~}FU4W=CjfI7S! zhw$Zpvx6ouG;jrd?^6^NmQwg>m>qg>pH@`iYiHkk$`9;pyq+_uUVIbH^M{fX zq2c|Cg>JzZj$10q`y>_XYo8bL1{09z&w%*kx>iCld%bjMjnl35W+F zA5g7<9?dD1A?ZRL#0iz_A(VU$5u(uRbs9oTtLiNVtZ7elHsSIY_5J@4N z`Pz4X+1U%1LZjk)pWy+05b`$urSOey6X}y0o>o0-pKJim4EB67uwQLoqd{KS6*AP| z;tnRN5bC}Mklh?Q1kiAvIc#zm;!$YePOES-4kUWqHfS=GFc#RDyOD{*omeYy6}_LA zr5nLf$(@(f2qCTrs2N*^yBw0?11FE<3*rSOtFDjO;W z!je!~)50o50lZ_J2j8uO;NiaUh=BP6PrBQ63DEKqLe@M8L=RV|NEi}tbiInYwXB7tdYZ?UD5n=x6!NmH^(z-kL1U*jl3)AGAi zV*(KcsF-nwToa%ik{HlBd&13ah3b?EYzBg&hl= z>68dM3saUoqt{em)#M z&CTU{+4%D^)pCno62IY<;}hN-figUXp^`W~iLGFw`1bW(t)#PcVBoEYa5ySaYK&76 z<&kb5zdD>&v^OOv^dPp*GOj}it3q{FvA@VmM6SIpjHU=utq=G{%N;jBVNg&>A|LRB zaqT?Uh;aZ_hz2wqk@ARb$XIu^4w|lFpj@UrmFATV=w$~X(j>18Xk3|(vba>@f(3bV zMsjQX3IMNHhJ`X7uSmh?(7&J~;d6w%V&2D{!(u=`Sg)_sxtFATuxuBw35g8>!m*}g z70r~taeKOQx&;)aHyy<6&&g2$1*ittzd;8`fp@n63nVLDmZOX|L&|^B`w;MLJAcj; zZEm+$C&5%si*&<_OJktkJe-!z&HIcytiF@b%Lo4yCVJ`N4F|@SvSY>l`)Y?Q2 zX80@CV9Y{HJL@*EMzsipYl#NV5L6z7dXpG~{)RY#NKX*lo#QsVH~17L1xv)vPFU!* z*c1}(Op3FzgraTxRRd6?9e8BAc-c1qc}1}VqT?VymT1?8NfBkD@UjJ@+#RYNp*vWi zUoqY~Y>;i&m;Ioagm6)PF(sz!sYCLX`|Jj!(L?Dj3PB02F*I@w94Z*hKqZQM?ZP>@ zq0Z`J6^aTjJNaFa*q~Sd(<`W#97Z&b%t*Qvg8e8*Xyv~~0>u*$t07KohYCg}-7E62#L?ZV|FzF3gvOsf~+YF{yd?s6?cfs9@2&mzuZb7r^HWBIxvIAJCG%U7P z&}HfZEbj>6mRF8EaRF9J`MXRKBFrKyv z>9VvezG6#?C730gI3}Jn3a*5~p`){fVDDsb-OS{8VR9xY92h?w{OIJ&k&Efj0DId2 z8M+L$QrOt*MlYq`M^=r*IyM z{2`!qicD8WD&u<9Xf(jQz@<|wu+h}-7}Xx4DstVhPvBlImu`oiG*O#1zf{N-n^T0# z7C(T5dq7{tu#zp0_-PvZ8gL1gp9&*=baobM7J3UuG}x*$2;7I=VlY%iLiJt+31u1C z1wg@?JfZgz+=2}x;lu4*sf=P-vIS~&w2KkRwN&1`omYon)9a$3@1;rqbHQ!tA*yDNS zU;o{I_uPd_IPkwC#@@^Y;Cpw1F2R0ze+Dm);YB5Cpt5&zplqhIfXg;6)#U7k(K1Z5 zN_HwH9W%Ho6wcNmgI|J(w+6w0g2QjGL|lDq8d|Oz)i=v&|RQ-kyl}5ShEIcaoMTsHIiHaY`I-6k*I!iXzJ09CAetH zu!hW@f>vXZiY`+n15F;f54Zm=X@JH^~f*OktPz(2zfXN{rYxwM1j*oYt;mVt!LpOm>1* zV)LPOOh^DYjA%qvPg4^aK;+PX{~cQoN8tm*LG~h@hu{t%@Ct1@q zkriozF&P+VQiV0IUJJa48rX8VU{;CJ*5+*9O-u)gZ5%cr*s??o#I!08QUJ zo9zuV_r+cE&ZjaLCEO#}gMl#^_nW)I8h2N+E9CXoZl%-v;9lSrJ>QJ;)^0AIvAOGJ zGd6YWJ;LmKqLO%L++}U|-waF>hsLLaJUn!bU_c;-Q1o(e7j!nJ)YF?}mQS&T@iVQj z#F`Sj;U8hY1C44<0@X)ahn9_{ht>fAbxChXpAQ?+e2wrxI@_0wFN=K6iNW_pYv2On zZtyE4!R%){y4E|+^2Gu;!9=u2kbUe6c1zuB`vp0u%5rS8^vg5E%5KY^$O(hkrd4q2Xoquf#$p+KYf_%Wj-rKR~DD5@LKdE-pET#_5+I1F9lda_MmL%>*$+se{g4Ra;N*@d3 zN2EneP`rYEk+mFLaTK7r)v-jYBkPTJ&8BRcQmy?^w4BYXC0kpZz zaAK^3E6ra=5?`_C*3ZClh;B-HRe~tdYO#x=Ou6M}hD$(pBT0TZNfJRV@)MKOGn4yZ z{G1qYtPv!S+!R8M@_dn_n`oc@roLV}98|>7a5~&S=MlUdYVLR^lvd8DHSSu!7wWCz&;NOnSUp>kuXg6c9*)0W~eiV}G= z-db?Bl-MRlZ4+j~UR$wpnoX92=Z#dKW+m5?oZ(1s?xl~M{b?_+=Sz!z>UY}h%Qu0= z@z;?U*K=EIdivE#?$Zd&LSi4sF0R?S@4dp6%?5UmPfkn$;8FsvB`k`iGS2Q%ht#43 zf;anY7C~x{MpAt~Fkt~!%v6$qM1r(nz@Dp5!6t;jsk7N0FLO3A1p3mBZ-Iz&Nat>> zza9)IM?I{f0D`ncWc_5Kb3+bVPm^86M2y>#7^7g_QTLqQ;|**G!E>Z{&pj!m$;&{o zg+`QjGRIs~{?lNDz$6sB=L=%|#BBJP7b zJL4fxB0UCGCTNFjC23!RfdjS~bf?0(52i#}MGisZ*i7cMjliA;#yN_N5Nlv`9GJ51 z+{dQ<@3Q`~`b`E{bpv3`89e;d5J5}#$CW((KDB`$)HA&oSp@*0U7{3$!GO^}QOx#% z{;Aekw(fP2nCBqrBh@*Q1_lFrYzZ?^jt!^c%L9*8dRnT`nz1k#C`aKU0AI?AlRQap zq(EaByd{O&i4#kDY5OxMc(H{Hcg;0L`5NeD4+vb9;LdX;jW4o29y0Qd#hdYlr&xPV zkE-=+Z-J?3rO(;yYrLKVt?SdD0%>T=*`yWiW;p9Z@KKiVItlL|ftlgA39!LGsJ;GyyB@IG(%AbvVLdt_!bxMJ8wu8-5IF5~i1 z&O_~nwI$6VcYxU8#}3xr+9asW_#4N#U&e@)aWK>-eT8Xj7OlHdAsUxzj*vvFdHQ^- zQL?0+9f4`sz|*x;TaEp(^Nr2)P)4&HwbAiN1;?Ak8{73J2QIO|bMs>MFON78>_$n9 zEIWK^G97t7>UU>W|0)TdAG`t zHvzw#VM|W!V;o0`4g5qLFcvTphx%l@H41Nvo-#_BZso2O=`1b#cfZ{C) zjxrw8b7Wu@V7EJb+v}qA%=ns$&wKD1gsEp^#-^v{v@Sf^YPVMHm`FL>GqV_T1~A1o z9nq@ydWQGQe$HmU{o|QNySCDUSz7icQ`7`|u>LaVG9H&coVaL{+ z&{z(Y=_ahd`SW44m1QKTNZSjj!_?;xj>9naIk4Lah=nGU+qe#?GnuyIP||jAwK1o$ zF&hW`%0lREC`kEj$z}40;PtkS2gaq488e+U;eOd`zpO;MLzg7`z+S!rF{2K)O`jz} z-WNRd`x+vV`JJ}-$gXi}Z0Wp9#slvvEhR-Pm18!+A5E_f4#@UX24RF6J?MisZHPd6Q7hlh+RZH9~!o?lReAYzp6FpVinJ)wR;Ec{h{z6B zMtTMTYNaJRoyip`;Vf{ZjQUc$b;2bK?H&}Jor9BCm`f4ve8k4##90VBN1|v);+fkb zv)XP%HqK8}7vNDsV1v8YXMXMh!4AX{wt-mOWhbt4V#?r`QIQOP%qzJGf?E5XxW0*g z-U}xWym9M(+MbX1;@NGZ(SqAvi&QT@YG@h59-F_by|A3>HAVR<#)?gat53HBpFJ}N z7p9nn;30=x;jtEe7LP1^|LJyPraG#BA8tzN9WKs{ADOAlZ_EQh_Se>uO&zMpso~t( zBA*$dFcMg%4mAh=XNrFg7EZ@>26l=UcIdDs8_{S0PgMhEtc1bf)vaSp z2k`*rb*+e_x_LNpQ-akLV53b9*Xeet)FmEH9FFMGI^6J-#74`lKf;UEah7-Vo9&Ll zrALBin85% z5NY6eSflo6p-L`qLgv$(jRpRvaU2*)j9w`d?4S&!gh>|m1gzja549RSS{!BagEln# z49x^^i@Im4W)Tf!tZk2&qva08KD2GI)wH_9EP+zdXcUKR!T04C)QrUkP>zM(W%5e?rFCeL|*8_Jrll6iWep{S;(v`-z097K`ze)MaBN^n%aI{@afv=IW-@$Gyxb z?QZ^>y6&^$yHH_#V2P^)^;G|+t9F^@-AR-n?T z&zCsFqb(=eGz8*ktedPTZ`rdEC<=zrozfmB8v!Tlz%P5v}+ zEnKx^$c$eOFcxxM!OpKEnTIxd7O4V)KE>9>gcJK_ZoD$JdR+d_PJc(|4y^t?o2=dz zDF&zJZD40)lFZt|Kz6v%5c(?b$@o|7ZP0X{xi#kPYqR*1S8jj(2UG!5FQr)4NVPkm zrAKP)#b#h*aFy;_mmPaWWG{^WRxygffVy0#p@B5R30hwwKAY42fcWqQUF2@ap(BI< zP{T6-Ide{E?te#HbmsN94@)dJPt$UqK!eaIRmCX0FV~j{m#-nc;7O!97(+s-LlWZ( z;xMx=n&Aip*q+TI)bzLDI?X_!cYU*V{zW_d2R=V`5RP6PnTsioZ+LDS1j5LSjS+wY z0tG3S;H9w9nQe>$N2D&ovOLT)HdBMG!=inH>SuN$Mc6v@s0^5+>xl~)ouOS)Afvos z45zJ;lSX8jqK6fu{_a}n*|&lp`Y$D%c(dEgY)aL~q}nEBefb z=nEoyMWQiwB8|&7a;)V$o6^~AnIg7*Pw4LmwnrcWgtO3f7U?Qw4op}A5lCS8AVO7u zI}52WKrUU{4Z|t>Dr1-lP>}D|rhaDb3-7lMTU*4=W?>BbSTuxMzd*AR@hD926YnK& z!@`jvm^l*QZ=iL0StHaH--Wn0_5||xxJkiAz(Lquq*{l)|YZNnMOhC-82#d_A0FH;sTId#?w&Ba7 zagi<11qp9RrP1hk$3eKpjV_7Hz}{ zO@)~hx+Fj!u{d@CTplTttH7-^3O^#Jpgt=7$*PdTMG}o2mxko&&17x~y(UxT#EB3r3sAf!m9M>{03-jrOx7rBJ z?#{j-@^r37o96>~UKpC?j2|7a{t1g-!k%{~>I!@LN?mo2WcSli_vEQX#yEs9Im_NB zqoM5Z@E8XNoP3D3`M}8oNemp>_~L<6ux&T+2P1%O@%$`7fW8{78whW&)5Pp5Prg(h zOFoys9SRiW3JxAoq?ogaJuq>&F?oxP!)p4YG=3b6PjV`NP=%|u@WUz|IKrfKDII@j zxNf%-j{-GV&(gUy4Rk1yo0L}L{wL^A)$#(E@au+Tq>y8Mwjhkdz$-@4VS_G)?%oJ| zuB*F6vN=Z5Ir^)*uZyw}x)GzU>s^=VDu{uN@V+!aIC)DHT_T1qrXo~6L0v6+F6Mvk zF6ctUjVb|_@hWhOBNCebJ&Hn<4ke!->#c#VHH3!Ad9&lcAa@q)zWlKGsv3f~`!F`3VSG`=;I za9QCCNy9asLec0vRXcSyd*$!C@u>B@!tv5Zb=uP(<=gM264GMU0QWL;NK5wR;bOF{Ml-HxQ9YFgB0tq58%%B4Kz?z~Afc^Ez0?bfd!vj?8KT83I&+J`0 zi{)0^fLbGv&!XBuI$O&`7WUnUq~I$jWRN#;`cDM-g){RFyY_U$Av2q#h~@jbl@lPC zCqkH3bxG;G^#^E~(p@pX$3kf>+-X&0e6K_k%GHYXE+LvrZ)f|3f^EfaG?^=Lt4@0e zp8Vk%9R8E1PmW>t5N*~g10m|H;3X(-F2=6t*m$3dx1l;Rpa^Arv9g5M)p6QQg4Rhk zC;7tM@_tab#(REEe^WZnXW|lMh$L-8JN(Bz|IH{+yGh+0=4EKE#gmOUelg+uX*u4e zeIv1+=1RiOXe6-oy!aQaNvXY^OLb3fQBH>y2PFO*OWnQ0f70_W?P{*$;}7c_Zaa{~ z0tddg3%qKF|7p)3>WWJOdH&Tq{Lf~lj+c;vpQa`VuyqY{$>o0YGMuiaSzclsJyDb= z7{6n%Zp`1XL;!SI^&nqs2=#zgD~4z`zT&v%>p6%-HHLU~)EW{2&A(xX|Fq{{)s?6# zgy*WvFTe^z{qApK7wovY>l%WNDNd1j1`p>joIF95gaqi2$jOcTK0Ex+_Y7K6pS?gc zFy4W`6wiF$&5FD7R55-#uIK{u39~8!DJo-iU@H2fbfisq06CL(4Rh9u?$ks+Y@f`Z`L1k8fCAaebrP6qnz zMslrT_R;pV;-4JGu#tAVwL}kdm#)yC?5DOW+HY=&Zk@c=p&*jR*%&!UpIi(|BKMoX< zskDp)8IUMqzguVMPj)3rTKQ3NtDgQ@P_OkmA`67dA0(zKlc4~N(0VLQG8+SF9myxN z2>0-P0KTXDAKiBU!D%a{Wm8wWs_5(ax9#x1==sZC$%0ybTw5y#(lWUN~EES}C1+0;tO3$f+=t@a5iCY5hKnRKY?WlzC0sIOq(;R7u zo**dUPP}U8fzY}0{0BGIq9ctX6nlgstZ<)$ia9qP;xqn?jQ7^;Hbr znRy=B-M)z@68r(#fojO%6)@`&H`B6c=8@9enMS4>7*quj8QmxNGIVP^E zBM+3-(E%WkG!M>R$s5X8q`gbf;I0_D;;OeZcR1)QCxy(Z>DEsU?@DwC{n8Hqxh!LH zsKTqpJGkeyNaFvx7du}6S^L9)*8;K83=d{J6=*Zd7NG1Tsj`?L^iVJc89Gq}e=ZVY%y0U_|EG{j2pC<1^a+$XU zASA#o0Le0K)ZdB~ugotlm)7uvMJu70wup0Dgy;?eqD+WGb0TMx4rZAb=E-y;eK&~g zJUE8s%1}HuGNftpw9x-hm#Y@%YtgvuP6$z_fr~_W@55dFs+Zt@vy%!1<1{XV9ON`M zN0v9%r{bdA7xhBmuw6a`J@Z79I?!BO5koZjsnht0A4vLP<+|d8IYys^S%4Rp;c=-~ z8{zQy$_fbld_@Y=$>M)Cv3S@v3tU>V5%30i9qqny8yi6$oL~SMuy*)g?b|~@Y@`|H zPyOr;!XtN)g5ni3OquP~V18f@MPq!%>b7QYQR`O5b|AT5$KCe)%fA=y*88Z!PgDMZ zAT$dHWNr#Wd2S;a%FoNTR*}hxawk&wNoh$+FhyBL0d;hck2hv(C4Vc%hQ+NTy^XR{ zqnKonI#`NhL+WCIEGzK^n)G!{PsV9!6qWtUR_2(kHN=%f<$?S_2-9&sf-Y9doSY#u zvvI}3P9+oaODS3rvr`Ur8W!r_~j)_#uAf2=EA&D2kpHtp%3fw^f)pH7>ge3#sbwa6eYgR6UAxX!M9 z5}Cli^JKDJ2;F#M>$4frH~?2U)jCn~^zY92-^}&n43cNnEb_#nNb}Cgi(!3W0!zq2 z_^RBOX$0JCt;fczfGFY~G%+aB{9MaQ$v!||W$N>!otlND*f>+vK?l6p@{ z%sd2$4&TS)W4L9QODFjqsc)IW3ub!;*ZUvp^k4S;&vd=si>lQk#k|$6&F-3{_ZN@N zA%rc7PSFdg757aG9Z(ob#BSJeI(;FiW0)blkhN4}h~Owe!~}lnbwM`ObAiZyAn?ez zxB{Ljq&4mb+6840#;cHdF1ain55J=hK#fURm7r=X?aoy>V^KA zn$k`%rQ3=m&%?GQ0WXl8Kc1|Dp@1`|v&-x9^g6`)*Bau4*I53nw$_jGE~1iz{)apL zPQQ0p4lOqO%AJ6$x*uZqB1&5=Q+<#ns>BvLrxAUh363dr)WZV`A3gYSkqMsxo$KjL(PuqytN3VGz|4M-+POCJ`NXn`(}0aP z4vAJV8TuKTvGgW{ON-})rbaDO?j8q7FI!YrAU1Xa8PANY;nbvn#PBq++zY;o&b%gxXHXCe8zb&U^(SgR&5$50py;tilj+$u67(PZD@ouYvfiiM%w4 zi^vjk(u3_`0uW_CalPprJ}K61Rw0sQ@GT(pJ(dF%jCKPkD!u?RB1=)5gelILU$;m! z5K&lIGE8jLI#)|l)R?tu&VNEWeCr9>GN`6tYKL3>5pwy@cJ;#+MQ#@@GUTv@_C@I9I#Ixg~2DchS){(Pv?C{h2S#q1*g%~+y0 zX5Z~i33_HbIxgUj9We+@I?=+^@WP{)gMIfBctD{?!W){*!Nf_4yy&fknUd z;;)p?%>371{PoxMJpOlo_4jA4dAf1kFMs2@Q&;`tyB6N~GrxC#vFDx_?>zJMQ_p_- zUtj#ocm3q(kN@mlw=cipz4!P2=^tG1xrq<#-T3V*ciwhw@%U1G2$mp%zEU1Kv3qE3 zVgBkXM|Qz1SDn8D!2!4rLNE}f0uCyhP1{-frq@xeSIn_}FXGG*q`3?sP$G>Zn=anX zv(*?ScnO;>&(M#a&S=9|`bSap>jaj992{0+yZ1t*&_az)RP~aTsI()Jn>@i^w;oB- zpcI~>JnCw!ianlBa3~AI6=QCoidnMh`sK`c5o^=8HuL2ZE5dKF5mw`r+!(qvRELGf~FfvlERK^C=rJBa_2m=1BJpgn4$Vm8nrz2Iq_PA z{y_H+dzqU%26lX_bEtdpfY2p~Qa_5)5pceuBHixpb9?01S;*Np>ihevx^pA)Tj8|* za&>nOS!+SlEa>~1?qO|l2wrdmRyH=qyNB~`^!duc?sJ3kFSXMj>ONNliNpKr1Us=B z0yp3rB4#wdH9b*yJV()p%>QWjIeQ5$%5>A$+q%z9$Rv*P)7nz%e&OcssoXx8343-z z?2xi}5BhL4CDR1k2i^?=&<-~oHs6gth{*H`f>*oG!ICm5m9Qc0qPa#RDE9Ax{AEQW ztykZo6pmf)zPmdE$<@L%;E99~YPVX76Ga1Qdyyto14*t4cq%2h;bEoH7t#$>K-VI7 zSad5#ruHg&0gZ9-ar*#Jt!@)Fpu-lF1z|Hp4T2NqdE=Np2u?8G`JQg%m`*~CD|+sTc%}*qj96%yl6vKYGHsp26!7#I_=K!M zL=4;!Y0>n(lq@T2xiO~l-#|(}I2CcV<}>}#U+?VxKzD8_7-s{)JUyT2yOfD`TNTT^ zRa+n|L1{pjq-vEZ>fm39&XJRfh9JSmOHY)Nz|Ilpr=RsQfQCT_4M(J$25yPoB5%1H z9Ckl07ivxtH=LxZw{EJsLYaC6DTCdP>KTlzKYpZpqdRkrACOpyY0iD1TA}-gbBebp z`XI%$1hY=EewSDxL|h=^$X~*-c@jW8*V9SrSw|jn?EI1Lk9V^SyAUL-7?{pwN3Df` z!#q+gf9Er2?zB6vDw*8vDD6G+aCd)&HP(PkL2*Wjfb~+D5dK5;9skJek;5c&$K_qSCd(Jks)lx} zHX|Wa~<9(c=|EIJy}=CDb9*wc+U5%q@;(srYXLbUBE7Dt_12pgtkyhGAQd1@m9% z^ohh0O>8Mum*Ahmg{rKru?x$vtNkhTZbR6F=(dC+nGVotlWCV3dj@GpOV4bvW3k>N zgI(=br==Plh4e*o6y}i{1^HG0#zrPvEkDxzsqT@I1RTI3Min>SYgD_!lywCPai%eZ z^K1ijb^XJQKm%1DN_3Trp@%yPnDX!3zG>tHadfbb%$;v~GweW|YBN9BA+lXIQ+B-1 zFm9-u5r7(?9i)nFh73Kh;v-n?+9}lEXkD_wY-@1gOb~2Qh8?!ey7f%V4PxLEhz%06 z3}jV=_pT$7${IVv9u82VenR6^fIVx+kFAefTXblHve**&gqs$_I4^-&14h`1$}BNL zG_ZoPFihrGftj#igNZR5j9T%4l<3t+ET|Yrn=Xi3g6PTzPhy><%sLOWMX}g-JJX6C z9w>}mZ`8Dr>c%GNE5+1cF-Um8bD-^V1>z*)FQLi3tg(ImBLN-xY*o)fw}eUXtz%4E-LiML0TfKS4R+s;;0+F3L#jsm$@9>)vDCIUIJx zP8~qw0|wLxqtZ#x2ig?GID}ZRb}QD=A!J~W1))y7#2AG1n0HBz%tQ~U2nZ@d;bFq+ zmSURlkB-Jf01}feA2-zZK{*sr4aI;8IMj$DS-^xly=YckYNSPNY=lUvG(LS$#7`O< zVe8W+Y(}7)C7EM|(g~b&LUXu*50Nv|!cD=W<`OzyPoLJ?d!jqrIDKb#Uc`ktnEn=r zpX%;kd8csx6&H(J4ju=I*Ju?JP4q!6cchpkelEhMA6rYF>wJ)Ct%GW{Wl|`@N2y) zx&&A~ATG$}dEGG_t97duzzq>9?pjDw+Cac)aB70ku$QLCrrGpx#E(S86B*V5t6&lU z=CWX(3!ovC(tZNbauWNfpR=eY+#=aLu@lHnkpcQSN~7ZjATc2f$&9lrVA;jZ@gXS> z@dC*cOC$~^k%>q{VrGrV3MY;%EQow}6W+{L*2Y(>nK5M0lb`1Jj<&Lvi8Q)%h?<5K z6y=rVW})4X35g7^ijwq|AAyL;@I0)ByLM@bMZ@ ziInU#!UGK{PBaaX8ID(b1?J^?U1VphP643Jq*}Y2K#6M}w9?v$fv#r@Tna_>Ij|R( z`mdhJ)uF9I0@(?B4QZ4J@$ePs?ADab#tQw`R#n6WCsK)%K+vP$NZ|#FInp6^_lXp$ zv=A7_E^GivlrfxY96DKzblx#44|FoZ+QHy9?|0-_IX*UT$~Caq%EAZ5^~Suq=8c*?C2c{fI5Pb9-yoI;3kQ*ru`1_D=t;SUWDzEIYG!! z*;7Vy0gj193#p8d2|E><2^#JcSxj2TgTJyQDiG-d8`F!C-}^Qsvflf)D+Yrw3{lU$ z0G!ZIdoewWw?0lX?1qXcY7%O~;H}fp?(h?MP{#Zo5PV=QnS(X3da?#2gBfkk*(GCb zIeQ@00GzzG@E86V4&Kt>$}eB){&IJ45n=>nXn-0OaGi-WQl-habW(m#4y*eTci`GL z!3e;toNUb_N;c&ODtkD+lAxa?Jr>d-UtSLK5v&8PwTV;H%U+C#NVI%*ggS#EiwfP& zWSz^1?zz2iS{puDs+uNs!8h>H(h_J6Spw~-Qo;9i9{zQ1@5-yt0C%? z3BQh{8w^2jztS8zLF%5iG_3px3X2n^p-3w~#sZZql6dj`e7L$5+_QbSGjr8)-!6x< z8~E3~FLnTH!7m z45sV&5dgwN4}3Pc7yYq&M<79NK=K*!LIW$?0~$nA5>=1|_Yxsq0vB4X!(S77v5nUt zj73f)in?1D@gw2owpl|&4*@yrxLYkdr(~Vxrh2pGJxCG@3fb976`e4BgPiJqT0JeXZM@&@U)ijp!XwS(6R^w%eQ11}clqHEOJGo$1qm4)(dvnlB~#tpCjQ zp}=?Rg7!hY;-bcMWYPp(S&n}l*kMb{z;JhiY_ZCs@md$2a7jKBM7#n6`i*oD8L&KU zq~LQRbdj?b!()j#lY351{kPqCXsk1HFsI4O%$p9nHHhf9(b0&A@Q=Wl%oXxqP6968 zBs(b7_Oo=jaSpmssw-$6+5~NYysZhQDk~=%i|r=x3NAuuM#;&{^gbLB66-KLLMhoN zB7MPKCfLmwJ7NCrxys4q(b0Vx$`VFc4;wvPOFI!+3|j-3+dp=tgM?Dnm&gZBdT;yjPwTpc5 zPmT17QDD*+Fw2v%Nq-DaNO;HRV7ehtpXw~~7aW3wU8Fzt3bA^sGk~oZ45U%%9|ZJq z&UhD%oU+-@tk+7iXck>WQVUo_r1WxW+En&AT*<7`jc` zTZ3@b^+?!{#6L#f=M7{xH+Ole{kaABPhe~6Io+slbYIY& zfr2{2kiC;Cz0q!eWVHm6HtBqrFyadll#XD*P+5tcusTb)VbWk(5A$HB7VhQk&LVg( zvEBHSv({B%w_4>b6GWLa8yH{N`_#&@*IBYMKEXYE^bXC(iTN@QJ!(t*dBi%bT==Daq z)fk{b@TJGVDHQNyl#d~nSJ<`=lRo8X{@oGE68!Xx%>bKA2Zq3q-?&u(Q3@`xSuU}q z!ghafA=s#`%Zs_&=|wEp82muX5+zR!X@{b>0Fx%%F7b-+F`lV;;QHWm991Hl=4-1P zI>qM8i$q9~{zFJCt#&=fIq>vn8UC2+@hW3~@LXYo2(wS=VnPZq4g>a*nSz)_s+qLb zX9g4a@R80tk$T|Rgr)B~2tgq3-ZM2~eK8cXig=mHZ{M$9@Y`*;&WS#_ zI&h!67Nx8=7Jrm)>E2Z?1a=lolwL?)`auQ67ZIJJF_&#Z{l*0YQ-0W{%V8l>@U%wKa=&A+PAM~_B3^7XuI@Pp2DVFq%*v}5hS{C58^CfI& kRS(@>^2yzc(LU4Qq!^Y8YO02U6p{2GbPwz(d^zj=Urj@GtpET3 diff --git a/evm-tests/.papi/polkadot-api.json b/evm-tests/.papi/polkadot-api.json deleted file mode 100644 index b415cf8fa..000000000 --- a/evm-tests/.papi/polkadot-api.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": 0, - "descriptorPath": ".papi/descriptors", - "entries": { - "devnet": { - "wsUrl": "ws://localhost:9944", - "metadata": ".papi/metadata/devnet.scale", - "genesis": "0xd4ee169957410f461aada33a817b3800a1521267a1d34d4b8b14836ba4ebcb93" - } - } -} \ No newline at end of file diff --git a/evm-tests/README.md b/evm-tests/README.md index bd366b03d..45d278fb9 100644 --- a/evm-tests/README.md +++ b/evm-tests/README.md @@ -4,20 +4,38 @@ test with ts ## install papi -npm install polkadot-api +```bash +yarn add polkadot-api +``` ## polkadot api +```bash npx papi add devnet -w ws://10.0.0.11:9944 +``` ## get the new metadata -sh get-metadta.sh +```bash +sh get-metadata.sh +``` ## run all tests +```bash yarn test +``` + +## To run a particular test case, you can pass an argument with the name or part of + +the name. For example: + +```bash +yarn run test -- -g "Can set subnet parameter" +``` ## update dependence for coding -npm update @polkadot-api/descriptors +```bash +yarn upgrade @polkadot-api/descriptors +``` diff --git a/evm-tests/package.json b/evm-tests/package.json index e789b5ce7..7208fcb84 100644 --- a/evm-tests/package.json +++ b/evm-tests/package.json @@ -12,8 +12,8 @@ "@polkadot/api": "15.1.1", "crypto": "^1.0.1", "dotenv": "16.4.7", - "polkadot-api": "^1.9.1", "ethers": "^6.13.5", + "polkadot-api": "^1.9.5", "viem": "2.23.4" }, "devDependencies": { diff --git a/evm-tests/src/address-utils.ts b/evm-tests/src/address-utils.ts index 0fa364c77..ed3abc500 100644 --- a/evm-tests/src/address-utils.ts +++ b/evm-tests/src/address-utils.ts @@ -1,32 +1,27 @@ import { Address } from "viem" import { encodeAddress } from "@polkadot/util-crypto"; -import { MultiAddress } from '@polkadot-api/descriptors'; -import { ss58Address, KeyPair } from "@polkadot-labs/hdkd-helpers"; +import { ss58Address } from "@polkadot-labs/hdkd-helpers"; import { hexToU8a } from "@polkadot/util"; import { blake2AsU8a, decodeAddress } from "@polkadot/util-crypto"; import { Binary } from "polkadot-api"; +import { SS58_PREFIX } from "./config" export function toViemAddress(address: string): Address { let addressNoPrefix = address.replace("0x", "") return `0x${addressNoPrefix}` } -export function convertSs58ToMultiAddress(ss58Address: string) { - const address = MultiAddress.Id(ss58Address) - return address -} - export function convertH160ToSS58(ethAddress: string) { // get the public key const hash = convertH160ToPublicKey(ethAddress); // Convert the hash to SS58 format - const ss58Address = encodeAddress(hash, 42); // Assuming network ID 42 + const ss58Address = encodeAddress(hash, SS58_PREFIX); return ss58Address; } export function convertPublicKeyToSs58(publickey: Uint8Array) { - return ss58Address(publickey, 42); + return ss58Address(publickey, SS58_PREFIX); } export function convertH160ToPublicKey(ethAddress: string) { diff --git a/evm-tests/src/balance-math.ts b/evm-tests/src/balance-math.ts index 35e9f8868..8d6e86bd5 100644 --- a/evm-tests/src/balance-math.ts +++ b/evm-tests/src/balance-math.ts @@ -1,7 +1,7 @@ import assert from "assert" export const TAO = BigInt(1000000000) // 10^9 -export const ETHPerRAO = BigInt(1000000000) // 10^9 +export const ETH_PER_RAO = BigInt(1000000000) // 10^9 export const GWEI = BigInt(1000000000) // 10^9 export const MAX_TX_FEE = BigInt(21000000) * GWEI // 100 times EVM to EVM transfer fee @@ -14,7 +14,7 @@ export function tao(value: number) { } export function raoToEth(value: bigint) { - return ETHPerRAO * value + return ETH_PER_RAO * value } export function compareEthBalanceWithTxFee(balance1: bigint, balance2: bigint) { diff --git a/evm-tests/src/bridgeToken.ts b/evm-tests/src/bridgeToken.ts deleted file mode 100644 index d726826b2..000000000 --- a/evm-tests/src/bridgeToken.ts +++ /dev/null @@ -1,633 +0,0 @@ -export const wagmiContract = { - abi: [ - { - "inputs": [ - { - "internalType": "string", - "name": "name_", - "type": "string" - }, - { - "internalType": "string", - "name": "symbol_", - "type": "string" - }, - { - "internalType": "address", - "name": "admin", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "inputs": [], - "name": "AccessControlBadConfirmation", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - }, - { - "internalType": "bytes32", - "name": "neededRole", - "type": "bytes32" - } - ], - "name": "AccessControlUnauthorizedAccount", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "allowance", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "needed", - "type": "uint256" - } - ], - "name": "ERC20InsufficientAllowance", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "sender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "balance", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "needed", - "type": "uint256" - } - ], - "name": "ERC20InsufficientBalance", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "approver", - "type": "address" - } - ], - "name": "ERC20InvalidApprover", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "receiver", - "type": "address" - } - ], - "name": "ERC20InvalidReceiver", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "sender", - "type": "address" - } - ], - "name": "ERC20InvalidSender", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - } - ], - "name": "ERC20InvalidSpender", - "type": "error" - }, - { - "inputs": [], - "name": "UnauthorizedHandler", - "type": "error" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "Approval", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - }, - { - "indexed": true, - "internalType": "bytes32", - "name": "previousAdminRole", - "type": "bytes32" - }, - { - "indexed": true, - "internalType": "bytes32", - "name": "newAdminRole", - "type": "bytes32" - } - ], - "name": "RoleAdminChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - }, - { - "indexed": true, - "internalType": "address", - "name": "account", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "sender", - "type": "address" - } - ], - "name": "RoleGranted", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - }, - { - "indexed": true, - "internalType": "address", - "name": "account", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "sender", - "type": "address" - } - ], - "name": "RoleRevoked", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "Transfer", - "type": "event" - }, - { - "inputs": [], - "name": "DEFAULT_ADMIN_ROLE", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "internalType": "address", - "name": "spender", - "type": "address" - } - ], - "name": "allowance", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "approve", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "balanceOf", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "burn", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "burnFrom", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "decimals", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - } - ], - "name": "getRoleAdmin", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - }, - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "grantRole", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - }, - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "hasRole", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "isAdmin", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "mint", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "name", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - }, - { - "internalType": "address", - "name": "callerConfirmation", - "type": "address" - } - ], - "name": "renounceRole", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - }, - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "revokeRole", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes4", - "name": "interfaceId", - "type": "bytes4" - } - ], - "name": "supportsInterface", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "symbol", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "totalSupply", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "transfer", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "transferFrom", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - } - ], - bytecode: - '0x60806040523480156200001157600080fd5b5060405162000fac38038062000fac8339810160408190526200003491620001ea565b8282600362000044838262000308565b50600462000053828262000308565b5062000065915060009050826200006f565b50505050620003d4565b60008281526005602090815260408083206001600160a01b038516845290915281205460ff16620001185760008381526005602090815260408083206001600160a01b03861684529091529020805460ff19166001179055620000cf3390565b6001600160a01b0316826001600160a01b0316847f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a45060016200011c565b5060005b92915050565b634e487b7160e01b600052604160045260246000fd5b600082601f8301126200014a57600080fd5b81516001600160401b038082111562000167576200016762000122565b604051601f8301601f19908116603f0116810190828211818310171562000192576200019262000122565b8160405283815260209250866020858801011115620001b057600080fd5b600091505b83821015620001d45785820183015181830184015290820190620001b5565b6000602085830101528094505050505092915050565b6000806000606084860312156200020057600080fd5b83516001600160401b03808211156200021857600080fd5b620002268783880162000138565b945060208601519150808211156200023d57600080fd5b506200024c8682870162000138565b604086015190935090506001600160a01b03811681146200026c57600080fd5b809150509250925092565b600181811c908216806200028c57607f821691505b602082108103620002ad57634e487b7160e01b600052602260045260246000fd5b50919050565b601f82111562000303576000816000526020600020601f850160051c81016020861015620002de5750805b601f850160051c820191505b81811015620002ff57828155600101620002ea565b5050505b505050565b81516001600160401b0381111562000324576200032462000122565b6200033c8162000335845462000277565b84620002b3565b602080601f8311600181146200037457600084156200035b5750858301515b600019600386901b1c1916600185901b178555620002ff565b600085815260208120601f198616915b82811015620003a55788860151825594840194600190910190840162000384565b5085821015620003c45787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b610bc880620003e46000396000f3fe608060405234801561001057600080fd5b506004361061012c5760003560e01c806340c10f19116100ad57806395d89b411161007157806395d89b4114610288578063a217fddf14610290578063a9059cbb14610298578063d547741f146102ab578063dd62ed3e146102be57600080fd5b806340c10f191461021357806342966c681461022657806370a082311461023957806379cc67901461026257806391d148541461027557600080fd5b8063248a9ca3116100f4578063248a9ca3146101a657806324d7806c146101c95780632f2ff15d146101dc578063313ce567146101f157806336568abe1461020057600080fd5b806301ffc9a71461013157806306fdde0314610159578063095ea7b31461016e57806318160ddd1461018157806323b872dd14610193575b600080fd5b61014461013f3660046109ab565b6102f7565b60405190151581526020015b60405180910390f35b61016161032e565b60405161015091906109dc565b61014461017c366004610a47565b6103c0565b6002545b604051908152602001610150565b6101446101a1366004610a71565b6103d8565b6101856101b4366004610aad565b60009081526005602052604090206001015490565b6101446101d7366004610ac6565b6103fc565b6101ef6101ea366004610ae1565b610408565b005b60405160128152602001610150565b6101ef61020e366004610ae1565b610433565b6101ef610221366004610a47565b61046b565b6101ef610234366004610aad565b610480565b610185610247366004610ac6565b6001600160a01b031660009081526020819052604090205490565b6101ef610270366004610a47565b61048d565b610144610283366004610ae1565b6104a2565b6101616104cd565b610185600081565b6101446102a6366004610a47565b6104dc565b6101ef6102b9366004610ae1565b6104ea565b6101856102cc366004610b0d565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b60006001600160e01b03198216637965db0b60e01b148061032857506301ffc9a760e01b6001600160e01b03198316145b92915050565b60606003805461033d90610b37565b80601f016020809104026020016040519081016040528092919081815260200182805461036990610b37565b80156103b65780601f1061038b576101008083540402835291602001916103b6565b820191906000526020600020905b81548152906001019060200180831161039957829003601f168201915b5050505050905090565b6000336103ce81858561050f565b5060019392505050565b6000336103e685828561051c565b6103f1858585610599565b506001949350505050565b600061032881836104a2565b600082815260056020526040902060010154610423816105f8565b61042d8383610602565b50505050565b6001600160a01b038116331461045c5760405163334bd91960e11b815260040160405180910390fd5b6104668282610696565b505050565b6000610476816105f8565b6104668383610703565b61048a338261073d565b50565b6000610498816105f8565b610466838361073d565b60009182526005602090815260408084206001600160a01b0393909316845291905290205460ff1690565b60606004805461033d90610b37565b6000336103ce818585610599565b600082815260056020526040902060010154610505816105f8565b61042d8383610696565b6104668383836001610773565b6001600160a01b03838116600090815260016020908152604080832093861683529290522054600019811461042d578181101561058a57604051637dc7a0d960e11b81526001600160a01b038416600482015260248101829052604481018390526064015b60405180910390fd5b61042d84848484036000610773565b6001600160a01b0383166105c357604051634b637e8f60e11b815260006004820152602401610581565b6001600160a01b0382166105ed5760405163ec442f0560e01b815260006004820152602401610581565b610466838383610848565b61048a8133610972565b600061060e83836104a2565b61068e5760008381526005602090815260408083206001600160a01b03861684529091529020805460ff191660011790556106463390565b6001600160a01b0316826001600160a01b0316847f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a4506001610328565b506000610328565b60006106a283836104a2565b1561068e5760008381526005602090815260408083206001600160a01b0386168085529252808320805460ff1916905551339286917ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b9190a4506001610328565b6001600160a01b03821661072d5760405163ec442f0560e01b815260006004820152602401610581565b61073960008383610848565b5050565b6001600160a01b03821661076757604051634b637e8f60e11b815260006004820152602401610581565b61073982600083610848565b6001600160a01b03841661079d5760405163e602df0560e01b815260006004820152602401610581565b6001600160a01b0383166107c757604051634a1406b160e11b815260006004820152602401610581565b6001600160a01b038085166000908152600160209081526040808320938716835292905220829055801561042d57826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161083a91815260200190565b60405180910390a350505050565b6001600160a01b0383166108735780600260008282546108689190610b71565b909155506108e59050565b6001600160a01b038316600090815260208190526040902054818110156108c65760405163391434e360e21b81526001600160a01b03851660048201526024810182905260448101839052606401610581565b6001600160a01b03841660009081526020819052604090209082900390555b6001600160a01b03821661090157600280548290039055610920565b6001600160a01b03821660009081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161096591815260200190565b60405180910390a3505050565b61097c82826104a2565b6107395760405163e2517d3f60e01b81526001600160a01b038216600482015260248101839052604401610581565b6000602082840312156109bd57600080fd5b81356001600160e01b0319811681146109d557600080fd5b9392505050565b60006020808352835180602085015260005b81811015610a0a578581018301518582016040015282016109ee565b506000604082860101526040601f19601f8301168501019250505092915050565b80356001600160a01b0381168114610a4257600080fd5b919050565b60008060408385031215610a5a57600080fd5b610a6383610a2b565b946020939093013593505050565b600080600060608486031215610a8657600080fd5b610a8f84610a2b565b9250610a9d60208501610a2b565b9150604084013590509250925092565b600060208284031215610abf57600080fd5b5035919050565b600060208284031215610ad857600080fd5b6109d582610a2b565b60008060408385031215610af457600080fd5b82359150610b0460208401610a2b565b90509250929050565b60008060408385031215610b2057600080fd5b610b2983610a2b565b9150610b0460208401610a2b565b600181811c90821680610b4b57607f821691505b602082108103610b6b57634e487b7160e01b600052602260045260246000fd5b50919050565b8082018082111561032857634e487b7160e01b600052601160045260246000fdfea2646970667358221220e179fc58c926e64cb6e87416f8ca64c117044e3195b184afe45038857606c15364736f6c63430008160033' -} as const \ No newline at end of file diff --git a/evm-tests/src/config.ts b/evm-tests/src/config.ts index 5d15aef1a..0aa258a3a 100644 --- a/evm-tests/src/config.ts +++ b/evm-tests/src/config.ts @@ -1,5 +1,6 @@ export const ETH_LOCAL_URL = 'http://localhost:9944' export const SUB_LOCAL_URL = 'ws://localhost:9944' +export const SS58_PREFIX = 42; export const IED25519VERIFY_ADDRESS = "0x0000000000000000000000000000000000000402"; export const IEd25519VerifyABI = [ diff --git a/evm-tests/src/contracts/bridgeToken.ts b/evm-tests/src/contracts/bridgeToken.ts new file mode 100644 index 000000000..f8b3ea4d0 --- /dev/null +++ b/evm-tests/src/contracts/bridgeToken.ts @@ -0,0 +1,631 @@ +export const BRIDGE_TOKEN_CONTRACT_ABI = [ + { + "inputs": [ + { + "internalType": "string", + "name": "name_", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol_", + "type": "string" + }, + { + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "AccessControlBadConfirmation", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "neededRole", + "type": "bytes32" + } + ], + "name": "AccessControlUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "allowance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientAllowance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "approver", + "type": "address" + } + ], + "name": "ERC20InvalidApprover", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "ERC20InvalidReceiver", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "ERC20InvalidSender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "ERC20InvalidSpender", + "type": "error" + }, + { + "inputs": [], + "name": "UnauthorizedHandler", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "previousAdminRole", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "newAdminRole", + "type": "bytes32" + } + ], + "name": "RoleAdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "burnFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleAdmin", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "hasRole", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "isAdmin", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "callerConfirmation", + "type": "address" + } + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +]; + +export const BRIDGE_TOKEN_CONTRACT_BYTECODE = "0x60806040523480156200001157600080fd5b5060405162000fac38038062000fac8339810160408190526200003491620001ea565b8282600362000044838262000308565b50600462000053828262000308565b5062000065915060009050826200006f565b50505050620003d4565b60008281526005602090815260408083206001600160a01b038516845290915281205460ff16620001185760008381526005602090815260408083206001600160a01b03861684529091529020805460ff19166001179055620000cf3390565b6001600160a01b0316826001600160a01b0316847f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a45060016200011c565b5060005b92915050565b634e487b7160e01b600052604160045260246000fd5b600082601f8301126200014a57600080fd5b81516001600160401b038082111562000167576200016762000122565b604051601f8301601f19908116603f0116810190828211818310171562000192576200019262000122565b8160405283815260209250866020858801011115620001b057600080fd5b600091505b83821015620001d45785820183015181830184015290820190620001b5565b6000602085830101528094505050505092915050565b6000806000606084860312156200020057600080fd5b83516001600160401b03808211156200021857600080fd5b620002268783880162000138565b945060208601519150808211156200023d57600080fd5b506200024c8682870162000138565b604086015190935090506001600160a01b03811681146200026c57600080fd5b809150509250925092565b600181811c908216806200028c57607f821691505b602082108103620002ad57634e487b7160e01b600052602260045260246000fd5b50919050565b601f82111562000303576000816000526020600020601f850160051c81016020861015620002de5750805b601f850160051c820191505b81811015620002ff57828155600101620002ea565b5050505b505050565b81516001600160401b0381111562000324576200032462000122565b6200033c8162000335845462000277565b84620002b3565b602080601f8311600181146200037457600084156200035b5750858301515b600019600386901b1c1916600185901b178555620002ff565b600085815260208120601f198616915b82811015620003a55788860151825594840194600190910190840162000384565b5085821015620003c45787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b610bc880620003e46000396000f3fe608060405234801561001057600080fd5b506004361061012c5760003560e01c806340c10f19116100ad57806395d89b411161007157806395d89b4114610288578063a217fddf14610290578063a9059cbb14610298578063d547741f146102ab578063dd62ed3e146102be57600080fd5b806340c10f191461021357806342966c681461022657806370a082311461023957806379cc67901461026257806391d148541461027557600080fd5b8063248a9ca3116100f4578063248a9ca3146101a657806324d7806c146101c95780632f2ff15d146101dc578063313ce567146101f157806336568abe1461020057600080fd5b806301ffc9a71461013157806306fdde0314610159578063095ea7b31461016e57806318160ddd1461018157806323b872dd14610193575b600080fd5b61014461013f3660046109ab565b6102f7565b60405190151581526020015b60405180910390f35b61016161032e565b60405161015091906109dc565b61014461017c366004610a47565b6103c0565b6002545b604051908152602001610150565b6101446101a1366004610a71565b6103d8565b6101856101b4366004610aad565b60009081526005602052604090206001015490565b6101446101d7366004610ac6565b6103fc565b6101ef6101ea366004610ae1565b610408565b005b60405160128152602001610150565b6101ef61020e366004610ae1565b610433565b6101ef610221366004610a47565b61046b565b6101ef610234366004610aad565b610480565b610185610247366004610ac6565b6001600160a01b031660009081526020819052604090205490565b6101ef610270366004610a47565b61048d565b610144610283366004610ae1565b6104a2565b6101616104cd565b610185600081565b6101446102a6366004610a47565b6104dc565b6101ef6102b9366004610ae1565b6104ea565b6101856102cc366004610b0d565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b60006001600160e01b03198216637965db0b60e01b148061032857506301ffc9a760e01b6001600160e01b03198316145b92915050565b60606003805461033d90610b37565b80601f016020809104026020016040519081016040528092919081815260200182805461036990610b37565b80156103b65780601f1061038b576101008083540402835291602001916103b6565b820191906000526020600020905b81548152906001019060200180831161039957829003601f168201915b5050505050905090565b6000336103ce81858561050f565b5060019392505050565b6000336103e685828561051c565b6103f1858585610599565b506001949350505050565b600061032881836104a2565b600082815260056020526040902060010154610423816105f8565b61042d8383610602565b50505050565b6001600160a01b038116331461045c5760405163334bd91960e11b815260040160405180910390fd5b6104668282610696565b505050565b6000610476816105f8565b6104668383610703565b61048a338261073d565b50565b6000610498816105f8565b610466838361073d565b60009182526005602090815260408084206001600160a01b0393909316845291905290205460ff1690565b60606004805461033d90610b37565b6000336103ce818585610599565b600082815260056020526040902060010154610505816105f8565b61042d8383610696565b6104668383836001610773565b6001600160a01b03838116600090815260016020908152604080832093861683529290522054600019811461042d578181101561058a57604051637dc7a0d960e11b81526001600160a01b038416600482015260248101829052604481018390526064015b60405180910390fd5b61042d84848484036000610773565b6001600160a01b0383166105c357604051634b637e8f60e11b815260006004820152602401610581565b6001600160a01b0382166105ed5760405163ec442f0560e01b815260006004820152602401610581565b610466838383610848565b61048a8133610972565b600061060e83836104a2565b61068e5760008381526005602090815260408083206001600160a01b03861684529091529020805460ff191660011790556106463390565b6001600160a01b0316826001600160a01b0316847f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a4506001610328565b506000610328565b60006106a283836104a2565b1561068e5760008381526005602090815260408083206001600160a01b0386168085529252808320805460ff1916905551339286917ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b9190a4506001610328565b6001600160a01b03821661072d5760405163ec442f0560e01b815260006004820152602401610581565b61073960008383610848565b5050565b6001600160a01b03821661076757604051634b637e8f60e11b815260006004820152602401610581565b61073982600083610848565b6001600160a01b03841661079d5760405163e602df0560e01b815260006004820152602401610581565b6001600160a01b0383166107c757604051634a1406b160e11b815260006004820152602401610581565b6001600160a01b038085166000908152600160209081526040808320938716835292905220829055801561042d57826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161083a91815260200190565b60405180910390a350505050565b6001600160a01b0383166108735780600260008282546108689190610b71565b909155506108e59050565b6001600160a01b038316600090815260208190526040902054818110156108c65760405163391434e360e21b81526001600160a01b03851660048201526024810182905260448101839052606401610581565b6001600160a01b03841660009081526020819052604090209082900390555b6001600160a01b03821661090157600280548290039055610920565b6001600160a01b03821660009081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161096591815260200190565b60405180910390a3505050565b61097c82826104a2565b6107395760405163e2517d3f60e01b81526001600160a01b038216600482015260248101839052604401610581565b6000602082840312156109bd57600080fd5b81356001600160e01b0319811681146109d557600080fd5b9392505050565b60006020808352835180602085015260005b81811015610a0a578581018301518582016040015282016109ee565b506000604082860101526040601f19601f8301168501019250505092915050565b80356001600160a01b0381168114610a4257600080fd5b919050565b60008060408385031215610a5a57600080fd5b610a6383610a2b565b946020939093013593505050565b600080600060608486031215610a8657600080fd5b610a8f84610a2b565b9250610a9d60208501610a2b565b9150604084013590509250925092565b600060208284031215610abf57600080fd5b5035919050565b600060208284031215610ad857600080fd5b6109d582610a2b565b60008060408385031215610af457600080fd5b82359150610b0460208401610a2b565b90509250929050565b60008060408385031215610b2057600080fd5b610b2983610a2b565b9150610b0460208401610a2b565b600181811c90821680610b4b57607f821691505b602082108103610b6b57634e487b7160e01b600052602260045260246000fd5b50919050565b8082018082111561032857634e487b7160e01b600052601160045260246000fdfea2646970667358221220e179fc58c926e64cb6e87416f8ca64c117044e3195b184afe45038857606c15364736f6c63430008160033" diff --git a/evm-tests/src/main.ts b/evm-tests/src/main.ts deleted file mode 100644 index ada3e7726..000000000 --- a/evm-tests/src/main.ts +++ /dev/null @@ -1,6 +0,0 @@ -async function main() { - -} - -main(); - diff --git a/evm-tests/src/substrate.ts b/evm-tests/src/substrate.ts index 5049382ff..45520db3e 100644 --- a/evm-tests/src/substrate.ts +++ b/evm-tests/src/substrate.ts @@ -8,6 +8,7 @@ import { DEV_PHRASE, entropyToMiniSecret, mnemonicToEntropy, KeyPair } from "@po import { getPolkadotSigner } from "polkadot-api/signer" import { randomBytes } from 'crypto'; import { Keyring } from '@polkadot/keyring'; +import { SS58_PREFIX } from "./config"; let api: TypedApi | undefined = undefined @@ -115,7 +116,7 @@ export async function getNonceChangePromise(api: TypedApi, ss58Ad }) } -export function convertPublicKeyToMultiAddress(publicKey: Uint8Array, ss58Format: number = 42): MultiAddress { +export function convertPublicKeyToMultiAddress(publicKey: Uint8Array, ss58Format: number = SS58_PREFIX): MultiAddress { // Create a keyring instance const keyring = new Keyring({ type: 'sr25519', ss58Format }); diff --git a/evm-tests/test/eth.bridgeToken.deploy.test.ts b/evm-tests/test/eth.bridgeToken.deploy.test.ts index b28f4a354..94ebcd126 100644 --- a/evm-tests/test/eth.bridgeToken.deploy.test.ts +++ b/evm-tests/test/eth.bridgeToken.deploy.test.ts @@ -7,7 +7,7 @@ import { ETH_LOCAL_URL } from "../src/config"; import { devnet } from "@polkadot-api/descriptors" import { PublicClient } from "viem"; import { TypedApi } from "polkadot-api"; -import { wagmiContract } from "../src/bridgeToken"; +import { BRIDGE_TOKEN_CONTRACT_ABI, BRIDGE_TOKEN_CONTRACT_BYTECODE } from "../src/contracts/bridgeToken"; import { toViemAddress } from "../src/address-utils"; import { forceSetBalanceToEthAddress, disableWhiteListCheck } from "../src/subtensor"; import { ethers } from "ethers" @@ -29,7 +29,7 @@ describe("bridge token contract deployment", () => { }); it("Can deploy bridge token smart contract", async () => { - const contractFactory = new ethers.ContractFactory(wagmiContract.abi, wagmiContract.bytecode, wallet) + const contractFactory = new ethers.ContractFactory(BRIDGE_TOKEN_CONTRACT_ABI, BRIDGE_TOKEN_CONTRACT_BYTECODE, wallet) const contract = await contractFactory.deploy("name", "symbol", wallet.address) await contract.waitForDeployment() @@ -46,7 +46,7 @@ describe("bridge token contract deployment", () => { }); it("Can deploy bridge token contract with gas limit", async () => { - const contractFactory = new ethers.ContractFactory(wagmiContract.abi, wagmiContract.bytecode, wallet) + const contractFactory = new ethers.ContractFactory(BRIDGE_TOKEN_CONTRACT_ABI, BRIDGE_TOKEN_CONTRACT_BYTECODE, wallet) const successful_gas_limit = "12345678"; const contract = await contractFactory.deploy("name", "symbol", wallet.address, diff --git a/evm-tests/test/eth.substrate-transfer.test.ts b/evm-tests/test/eth.substrate-transfer.test.ts index 84eb3c678..9e3a2b205 100644 --- a/evm-tests/test/eth.substrate-transfer.test.ts +++ b/evm-tests/test/eth.substrate-transfer.test.ts @@ -8,7 +8,7 @@ import { PublicClient } from "viem"; import { TypedApi, Binary, FixedSizeBinary } from "polkadot-api"; import { generateRandomEthersWallet } from "../src/utils"; import { tao, raoToEth, bigintToRao, compareEthBalanceWithTxFee } from "../src/balance-math"; -import { toViemAddress, convertSs58ToMultiAddress, convertPublicKeyToSs58, convertH160ToSS58, ss58ToH160, ss58ToEthAddress, ethAddressToH160 } from "../src/address-utils" +import { toViemAddress, convertPublicKeyToSs58, convertH160ToSS58, ss58ToH160, ss58ToEthAddress, ethAddressToH160 } from "../src/address-utils" import { ethers } from "ethers" import { estimateTransactionCost, getContract } from "../src/eth" @@ -65,7 +65,7 @@ describe("Balance transfers between substrate and EVM", () => { const receiverBalance = await publicClient.getBalance({ address: toViemAddress(wallet.address) }) const transferBalance = tao(1) - const tx = api.tx.Balances.transfer_keep_alive({ value: transferBalance, dest: convertSs58ToMultiAddress(ss58Address) }) + const tx = api.tx.Balances.transfer_keep_alive({ value: transferBalance, dest: MultiAddress.Id(ss58Address) }) await waitForTransactionCompletion(api, tx, signer) .then(() => { }) .catch((error) => { console.log(`transaction error ${error}`) }); From 8ce14e1c042b5e3f50a3008098886df3a1524dfd Mon Sep 17 00:00:00 2001 From: open-junius Date: Mon, 10 Mar 2025 21:09:44 +0800 Subject: [PATCH 3/7] update readme --- evm-tests/README.md | 2 +- evm-tests/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/evm-tests/README.md b/evm-tests/README.md index 45d278fb9..fc42f62d9 100644 --- a/evm-tests/README.md +++ b/evm-tests/README.md @@ -23,7 +23,7 @@ sh get-metadata.sh ## run all tests ```bash -yarn test +yarn run test ``` ## To run a particular test case, you can pass an argument with the name or part of diff --git a/evm-tests/package.json b/evm-tests/package.json index 7208fcb84..a96a2c4a0 100644 --- a/evm-tests/package.json +++ b/evm-tests/package.json @@ -1,6 +1,6 @@ { "scripts": { - "test": "mocha --timeout 999999 --require ts-node/register test/eth.sub*test.ts" + "test": "mocha --timeout 999999 --require ts-node/register test/*test.ts" }, "keywords": [], "author": "", From 19be9c68ae87eef4d32cc12b6eab7ca86196cdad Mon Sep 17 00:00:00 2001 From: open-junius Date: Mon, 10 Mar 2025 23:32:32 +0800 Subject: [PATCH 4/7] update test case name --- evm-tests/test/subnet.precompile.hyperparameter.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evm-tests/test/subnet.precompile.hyperparameter.test.ts b/evm-tests/test/subnet.precompile.hyperparameter.test.ts index af3e12cb2..1805b85ce 100644 --- a/evm-tests/test/subnet.precompile.hyperparameter.test.ts +++ b/evm-tests/test/subnet.precompile.hyperparameter.test.ts @@ -9,7 +9,7 @@ import { ISubnetABI, ISUBNET_ADDRESS } from "../src/contracts/subnet" import { ethers } from "ethers" import { forceSetBalanceToEthAddress, forceSetBalanceToSs58Address } from "../src/subtensor" -describe("Test the EVM chain ID", () => { +describe("Test the Subnet precompile contract", () => { // init eth part const wallet = generateRandomEthersWallet(); // init substrate part From def4b931c2cebcf5c6325d4656d972b853179c27 Mon Sep 17 00:00:00 2001 From: open-junius Date: Tue, 11 Mar 2025 20:32:29 +0800 Subject: [PATCH 5/7] update readme --- evm-tests/README.md | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/evm-tests/README.md b/evm-tests/README.md index fc42f62d9..7d01034bd 100644 --- a/evm-tests/README.md +++ b/evm-tests/README.md @@ -2,12 +2,6 @@ test with ts -## install papi - -```bash -yarn add polkadot-api -``` - ## polkadot api ```bash @@ -26,16 +20,8 @@ sh get-metadata.sh yarn run test ``` -## To run a particular test case, you can pass an argument with the name or part of - -the name. For example: +## To run a particular test case, you can pass an argument with the name or part of the name. For example: ```bash yarn run test -- -g "Can set subnet parameter" ``` - -## update dependence for coding - -```bash -yarn upgrade @polkadot-api/descriptors -``` From 4892604893c8a8376b39d189d544ce1417a6ddf1 Mon Sep 17 00:00:00 2001 From: open-junius Date: Tue, 11 Mar 2025 21:40:24 +0800 Subject: [PATCH 6/7] upgrade ring --- Cargo.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 06933b923..fccc179ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8011,9 +8011,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.12" +version = "0.17.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9b823fa29b721a59671b41d6b06e66b29e0628e207e8b1c3ceeda701ec928d" +checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee" dependencies = [ "cc", "cfg-if", @@ -8205,7 +8205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", - "ring 0.17.12", + "ring 0.17.13", "rustls-webpki", "sct", ] @@ -8237,7 +8237,7 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.17.12", + "ring 0.17.13", "untrusted 0.9.0", ] @@ -9488,7 +9488,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.17.12", + "ring 0.17.13", "untrusted 0.9.0", ] @@ -9911,7 +9911,7 @@ dependencies = [ "chacha20poly1305", "curve25519-dalek", "rand_core", - "ring 0.17.12", + "ring 0.17.13", "rustc_version 0.4.1", "sha2 0.10.8", "subtle 2.6.1", @@ -12534,7 +12534,7 @@ version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" dependencies = [ - "ring 0.17.12", + "ring 0.17.13", "untrusted 0.9.0", ] From cff4153fb80862f2919005081d5b239a703f02d0 Mon Sep 17 00:00:00 2001 From: open-junius Date: Tue, 11 Mar 2025 23:36:15 +0800 Subject: [PATCH 7/7] remove unnecessary subscription --- evm-tests/src/config.ts | 2 ++ evm-tests/src/substrate.ts | 33 ++++++++++++++++++++------------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/evm-tests/src/config.ts b/evm-tests/src/config.ts index 0aa258a3a..601c89c8c 100644 --- a/evm-tests/src/config.ts +++ b/evm-tests/src/config.ts @@ -1,6 +1,8 @@ export const ETH_LOCAL_URL = 'http://localhost:9944' export const SUB_LOCAL_URL = 'ws://localhost:9944' export const SS58_PREFIX = 42; +// set the tx timeout as 2 second when eable the fast-blocks feature. +export const TX_TIMEOUT = 2000; export const IED25519VERIFY_ADDRESS = "0x0000000000000000000000000000000000000402"; export const IEd25519VerifyABI = [ diff --git a/evm-tests/src/substrate.ts b/evm-tests/src/substrate.ts index 45520db3e..ddfdfb626 100644 --- a/evm-tests/src/substrate.ts +++ b/evm-tests/src/substrate.ts @@ -8,7 +8,7 @@ import { DEV_PHRASE, entropyToMiniSecret, mnemonicToEntropy, KeyPair } from "@po import { getPolkadotSigner } from "polkadot-api/signer" import { randomBytes } from 'crypto'; import { Keyring } from '@polkadot/keyring'; -import { SS58_PREFIX } from "./config"; +import { SS58_PREFIX, TX_TIMEOUT } from "./config"; let api: TypedApi | undefined = undefined @@ -111,7 +111,7 @@ export async function getNonceChangePromise(api: TypedApi, ss58Ad subscription.unsubscribe(); console.log('unsubscribed!'); resolve() - }, 2000); + }, TX_TIMEOUT); }) } @@ -129,21 +129,28 @@ export function convertPublicKeyToMultiAddress(publicKey: Uint8Array, ss58Format export async function waitForTransactionCompletion(api: TypedApi, tx: Transaction<{}, string, string, void>, signer: PolkadotSigner,) { const transactionPromise = await getTransactionWatchPromise(tx, signer) - const ss58Address = convertPublicKeyToSs58(signer.publicKey) - const noncePromise = await getNonceChangePromise(api, ss58Address) - - return new Promise((resolve, reject) => { - Promise.race([transactionPromise, noncePromise]) - .then(resolve) - .catch(reject); - }) + return transactionPromise + + // If we can't always get the finalized event, then add nonce subscribe as other evidence for tx is finalized. + // Don't need it based on current testing. + // const ss58Address = convertPublicKeyToSs58(signer.publicKey) + // const noncePromise = await getNonceChangePromise(api, ss58Address) + + // return new Promise((resolve, reject) => { + // Promise.race([transactionPromise, noncePromise]) + // .then(resolve) + // .catch(reject); + // }) } export async function getTransactionWatchPromise(tx: Transaction<{}, string, string, void>, signer: PolkadotSigner,) { return new Promise((resolve, reject) => { + // store the txHash, then use it in timeout. easier to know which tx is not finalized in time + let txHash = "" const subscription = tx.signSubmitAndWatch(signer).subscribe({ next(value) { console.log("Event:", value); + txHash = value.txHash // TODO investigate why finalized not for each extrinsic if (value.type === "finalized") { @@ -168,9 +175,9 @@ export async function getTransactionWatchPromise(tx: Transaction<{}, string, str setTimeout(() => { subscription.unsubscribe(); - console.log('unsubscribed!'); - resolve() - }, 2000); + console.log('unsubscribed because of timeout for tx {}', txHash); + reject() + }, TX_TIMEOUT); }); }