diff --git a/.gitignore b/.gitignore index c22337c0c9a..72f88324900 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,5 @@ packages/web3/.in3/ benchmark-data.txt .eslintcache + +.history \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 94a802644cc..783691404b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2444,7 +2444,7 @@ If there are any bugs, improvements, optimizations or any new feature proposal f - Added `signature` to type `AbiFunctionFragment` (#6922) - update type `Withdrawals`, `block` and `BlockHeaderOutput` to include properties of eip 4844, 4895, 4788 (#6933) -## [Unreleased] +## [4.9.0] ### Added @@ -2456,6 +2456,10 @@ If there are any bugs, improvements, optimizations or any new feature proposal f - `defaultReturnFormat` was added to the configuration options. (#6947) +#### web3-errors + +- Added `InvalidIntegerError` error for fromWei and toWei (#7052) + #### web3-eth - `defaultReturnFormat` was added to all methods that have `ReturnType` param. (#6947) @@ -2480,6 +2484,8 @@ If there are any bugs, improvements, optimizations or any new feature proposal f #### web3-utils +- `toWei` add warning when using large numbers or large decimals that may cause precision loss (#6908) +- `toWei` and `fromWei` now supports integers as a unit. (#7053) ### Fixed @@ -2489,9 +2495,17 @@ If there are any bugs, improvements, optimizations or any new feature proposal f #### web3-utils +- `toWei` support numbers in scientific notation (#6908) +- `toWei` and `fromWei` trims according to ether unit successfuly (#7044) #### web3-validator +- The JSON schema conversion process now correctly assigns an id when the `abi.name` is not available, for example, in the case of public mappings. (#6981) +- `browser` entry point that was pointing to an non-existing bundle file was removed from `package.json` (#7015) + +#### web3-core + +- Set a try catch block if processesingError fails (#7022) ### Changed @@ -2504,6 +2518,49 @@ If there are any bugs, improvements, optimizations or any new feature proposal f - Added parameter `customTransactionReceiptSchema` into methods `emitConfirmation`, `waitForTransactionReceipt`, `watchTransactionByPolling`, `watchTransactionBySubscription`, `watchTransactionForConfirmations` (#7000) - Changed functionality: For networks that returns `baseFeePerGas===0x0` fill `maxPriorityFeePerGas` and `maxFeePerGas` by `getGasPrice` method (#7050) +#### web3-eth-abi + +- Dependencies updated + #### web3-rpc-methods - Change `estimateGas` method to add possibility pass Transaction type (#7000) + +## [4.10.0] + +### Added + +#### web3 + +- Now when existing packages are added in web3, will be avalible for plugins via context. (#7088) + +#### web3-core + +- Now when existing packages are added in web3, will be avalible for plugins via context. (#7088) + +#### web3-eth + +- `sendTransaction` in `rpc_method_wrappers` accepts optional param of `TransactionMiddleware` (#7088) +- WebEth has `setTransactionMiddleware` and `getTransactionMiddleware` for automatically passing to `sentTransaction` (#7088) + +#### web3-eth-ens + +- `getText` now supports first param Address +- `getName` has optional second param checkInterfaceSupport + +### web3-types + +- Added `result` as optional `never` and `error` as optional `never in type `JsonRpcNotification` (#7091) +- Added `JsonRpcNotfication` as a union type in `JsonRpcResponse` (#7091) + +### web3-rpc-providers + +- RC release + +### Fixed + +#### web3-eth-ens + +- `getName` reverse resolution + +## [Unreleased] diff --git a/docs/docs/guides/advanced/_category_.yml b/docs/docs/guides/advanced/_category_.yml index 3131878f18e..c6ab4f48016 100644 --- a/docs/docs/guides/advanced/_category_.yml +++ b/docs/docs/guides/advanced/_category_.yml @@ -2,4 +2,4 @@ label: '🧠 Advanced' collapsible: true collapsed: true link: null -position: 15 \ No newline at end of file +position: 11 diff --git a/docs/docs/guides/events_subscriptions/_category_.yml b/docs/docs/guides/events_subscriptions/_category_.yml index 2f13d5222b4..9207b5b28c2 100644 --- a/docs/docs/guides/events_subscriptions/_category_.yml +++ b/docs/docs/guides/events_subscriptions/_category_.yml @@ -2,4 +2,4 @@ label: '🔔 Events subscription' collapsible: true collapsed: true link: null -position: 7 \ No newline at end of file +position: 5 diff --git a/docs/docs/guides/feedback/index.md b/docs/docs/guides/feedback/index.md index 0118436ec91..228003d5a6e 100644 --- a/docs/docs/guides/feedback/index.md +++ b/docs/docs/guides/feedback/index.md @@ -1,5 +1,5 @@ --- -sidebar_position: 18 +sidebar_position: 17 sidebar_label: '🗣️ Feedback' --- diff --git a/docs/docs/guides/getting_started/getting_started.md b/docs/docs/guides/getting_started/getting_started.md deleted file mode 100644 index 18b95035957..00000000000 --- a/docs/docs/guides/getting_started/getting_started.md +++ /dev/null @@ -1,254 +0,0 @@ ---- -slug: / -sidebar_position: 2 -sidebar_label: Quickstart ---- - -# Quickstart - - -## Live code editor - - - -## Installation - -If NPM is being used as package manager, use the following for installing the web3.js library. - -``` -npm i web3 -``` - -For installing using yarn package manager: - -``` -yarn add web3 -``` - -Note: Installing web3.js in this way will bring in all web3.js sub-packages, if you only need specific packages, it is recommended to install the specific required packages (e.g, if you want the contract package `npm i web3-eth-contract` instead) - -## Importing Web3.js - -Web3.js v4 supports both CJS ( CommonJS ) and native ESM module imports. For importing the main Web3 class in CJS you can use: - -``` js -const { Web3 } = require('web3'); -``` - -and for ESM style imports, you can use: - -``` ts -import { Web3 } from 'web3'; -``` - -## Initialize `Web3` with a provider - -Web3.js is in compliance with [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) so any EIP-1193 provider can be injected in web3.js . There are HTTP, WebSocket and IPC providers also available as web3.js packages for using. - -:::warning -You must initialize the `Web3` object with a provider, otherwise, you won't be able to fully use web3.js functionalities. Here is an example of creating a `web3` instance with an HTTP provider: -::: - -``` ts -import { Web3 } from 'web3'; - -//private RPC endpoint -const web3 = new Web3('https://mainnet.infura.io/v3/YOUR_INFURA_ID'); - -//or public RPC endpoint -//const web3 = new Web3('https://eth.llamarpc.com'); - -web3.eth.getBlockNumber().then(console.log); -// ↳ 18849658n -``` -## Querying the blockchain - -After instantiating the `web3` instance with a `new Web3 provider`, we can access the `web3.eth` package to fetch data from the blockchain: - -```ts -// get the balance of an address -await web3.eth.getBalance('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'); -// ↳ 114438180989009447638n - -// get last block number -await web3.eth.getBlockNumber(); -// ↳ 18849658n - -// get the chain id of the current provider -await web3.eth.getChainId(); -// ↳ 1n - -// get the nonce of an address -await web3.eth.getTransactionCount('0x37826D8B5F4B175517A0f42c886f8Fca38C55Fe7'); -// ↳ 7n - -// get the current gas price -await web3.eth.getGasPrice(); -// ↳ 23879160756n -``` - -## Setting up a wallet - -If you want to write data/interact with contracts or send transactions on the blockchain, you must have an account with funds to cover the gas fees. - -The object `Wallet` is an array of accounts, it will allow you to hold several accounts from which you can send transactions `web3.eth.sendTransaction` or interact with contract objects `web3.eth.contract.methods.contractfunction().send()`, when you perform these actions, the `Wallet` object uses the account/s it holds to send the transactions. - -### Create random wallet - -```ts -//create random wallet with 1 account -web3.eth.accounts.wallet.create(1) -/* ↳ -Wallet(1) -[ - { - address: '0xcE6A5235d6033341972782a15289277E85E5b305', - privateKey: '0x50d349f5cf627d44858d6fcb6fbf15d27457d35c58ba2d5cfeaf455f25db5bec', - signTransaction: [Function: signTransaction], - sign: [Function: sign], - encrypt: [Function: encrypt] - }, - _accountProvider: { - create: [Function: createWithContext], - privateKeyToAccount: [Function: privateKeyToAccountWithContext], - decrypt: [Function: decryptWithContext] - }, - _addressMap: Map(1) { '0xce6a5235d6033341972782a15289277e85e5b305' => 0 }, - _defaultKeyName: 'web3js_wallet' -] -*/ -``` - -### Add a private key to create a wallet - -```ts -//the private key must start with the '0x' prefix -const account = web3.eth.accounts.wallet.add('0x50d349f5cf627d44858d6fcb6fbf15d27457d35c58ba2d5cfeaf455f25db5bec'); - -console.log(account[0].address); -//↳ 0xcE6A5235d6033341972782a15289277E85E5b305 - -console.log(account[0].privateKey); -//↳ 0x50d349f5cf627d44858d6fcb6fbf15d27457d35c58ba2d5cfeaf455f25db5bec -``` - -### Send transactions - -```ts title='Sending value' -//add an account to a wallet -const account = web3.eth.accounts.wallet.add('0x50d349f5cf627d44858d6fcb6fbf15d27457d35c58ba2d5cfeaf455f25db5bec'); - -//create transaction object to send 1 eth to '0xa32...c94' address from the account[0] -const tx = -{ - from: account[0].address, - to: '0xa3286628134bad128faeef82f44e99aa64085c94', - value: web3.utils.toWei('1', 'ether') -}; -//the `from` address must match the one previously added with wallet.add - -//send the transaction -const txReceipt = await web3.eth.sendTransaction(tx); - -console.log('Tx hash:', txReceipt.transactionHash) -// ↳ Tx hash: 0x03c844b069646e08af1b6f31519a36e3e08452b198ef9f6ce0f0ccafd5e3ae0e -``` - -## Interact with smart contracts - -### Instantiate a contract - -The first step to interact with a contract is to instantiate the contract, for which we will need the ABI and the address of the contract - -```ts -//Uniswap token address in mainnet -const address = '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984' - -//you can find the complete ABI in etherscan.io -const ABI = -[ - { - name: 'symbol', - outputs: [{ type: 'string' }], - type: 'function', - }, - { - name: 'totalSupply', - outputs: [{ type: 'uint256' }], - type: 'function', - }, -]; - -//instantiate the contract -const uniswapToken = new web3.eth.Contract(abi, address); -``` - -### Read-methods - -```ts -//make the call to the contract -const symbol = await uniswapToken.methods.symbol().call(); - -console.log('Uniswap symbol:',symbol); -// ↳ Uniswap symbol: UNI - -//make the call to the contract -const totalSupply = await uniswapToken.methods.totalSupply().call(); - -console.log('Uniswap Total supply:', totalSupply); -// ↳ Uniswap Total Supply: 1000000000000000000000000000n - -//use web3 utils to format the units -console.log(web3.utils.fromWei(totalSupply, 'ether')) -// ↳ 1000000000 -``` - -### Writing-methods - -```ts -//address to send the token -const to = '0xcf185f2F3Fe19D82bFdcee59E3330FD7ba5f27ce'; - -//value to transfer (1 with 18 decimals) -const value = web3.utils.toWei('1','ether'); - -//send the transaction => return the Tx receipt -const txReceipt = await uniswapToken.methods.transfer(to,value).send({from: account[0].address}); - -console.log('Tx hash:',txReceipt.transactionHash); -// ↳ Tx hash: 0x14273c2b5781cc8f1687906c68bfc93482c603026d01b4fd37a04adb6217ad43 -``` - -### Query past events - -```ts -//get past `Transfer` events from block 18850576 -const eventTransfer = await uniswapToken.getPastEvents('Transfer', { fromBlock: 18850576 }); - -console.log(eventTransfer); -// ↳ [{...},{...}, ...] array with all the events emitted -//you can only query logs from the previous 100_000 blocks -``` - -### Listening to live events - -:::warning -You MUST initialize the `Web3 provider` with a WebSocket endpoint to subscribe to live events -::: - -```ts -import { Web3 } from 'web3'; - -//WebSocket provider -const web3 = new Web3('wss://ethereum.publicnode.com'); - -//instantiate contract -const uniswapToken = new web3.eth.Contract(abi, address) - -//create the subcription to all the 'Transfer' events -const subscription = uniswapToken.events.Transfer(); - -//listen to the events -subscription.on('data',console.log); -// ↳ [{...},{...}, ...] live events will be printed in the console -``` diff --git a/docs/docs/guides/getting_started/introduction.md b/docs/docs/guides/getting_started/introduction.md index ab9a303fc84..0ccdbced97e 100644 --- a/docs/docs/guides/getting_started/introduction.md +++ b/docs/docs/guides/getting_started/introduction.md @@ -1,66 +1,92 @@ --- +slug: / sidebar_position: 1 sidebar_label: Introduction --- # Introduction -Welcome to Web3.js Documentation!👋 +Web3.js is a robust and flexible collection of **TypeScript and JavaScript** libraries that allows developers to interact with local or remote [Ethereum](https://ethereum.org/en/) nodes (or **any EVM-compatible blockchain**) over **HTTP, IPC or WebSocket** connections. It is a powerful and efficient toolkit for crafting applications within the Ethereum ecosystem and beyond. -Web3.js is a robust and flexible collection of libraries for **TypeScript and JavaScript** developers. It allows you to interact with a local or remote Ethereum node (or **any EVM-compatible blockchain**) using **HTTP, IPC or WebSocket.** It serves as an essential tool for connecting and crafting applications within the Ethereum ecosystem. - -The following documentation will guide you through different use cases of Web3.js, upgrading from older versions, as well as providing an API reference documentation with examples. +This documentation is the entrypoint to Web3.js for developers. It covers [basic](/guides/getting_started/quickstart) and [advanced](/guides/smart_contracts/mastering_smart_contracts) usage with examples, and includes comprehensive [API documentation](/api) as well as guides for common tasks, like [upgrading](/guides/web3_upgrade_guide/x/) from older versions. ## Features of Web3.js v4 -- Web3.js [Plugins Feature](/guides/web3_plugin_guide/) for extending functionality ( [List of Existing Plugins](https://web3js.org/plugins) ) -- ECMAScript (ESM) and CommonJS (CJS) builds -- [Tree shakable with ESM](/guides/advanced/tree_shaking) -- [Contracts dynamic types](/guides/smart_contracts/infer_contract_types/) & full API in TypeScript -- Using native BigInt instead of large BigNumber libraries -- More efficient ABI Encoder & Decoder -- Custom Output formatters -- In compliance with Eth EL API +- Flexible + - ECMAScript (ESM) and CommonJS (CJS) builds + - [Plugins](/guides/web3_plugin_guide/) for extending functionality +- Efficient + - Modular, [package](/#packages)-based design reduces unneeded dependencies + - [Tree shakable with ESM](/guides/advanced/tree_shaking) + - Use of native [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) (instead of large [BigNumber](https://mikemcl.github.io/bignumber.js/) libraries) + - Efficient ABI [encoding](/api/web3-eth-abi/function/encodeFunctionCall) & [decoding](/api/web3-eth-abi/function/decodeParameter) +- Developer-Friendly + - [Dynamic contract types](/guides/smart_contracts/infer_contract_types/) & full API in TypeScript + - Custom output [formatters](https://docs.web3js.org/api/web3-utils/function/format) + - In compliance with the [Ethereum JSON-RPC Specification](https://ethereum.github.io/execution-apis/api-documentation/) + +## Using These Docs + +There is a lot to learn about Web3.js! Here are some tips for developers of different skill levels. Remember, you can always [reach out directly](/guides/feedback/#urgent-questions-or-concerns) with Discord or Twitter if you're feeling stuck. + +### For Beginner Web3.js Developers + +New Web3.js developers should proceed to the [Quickstart](/guides/getting_started/quickstart) section to learn how to get started with Web3.js. Once you understand the basics, you may want to consider learning more about [providers](/guides/web3_providers_guide/), [wallets and accounts](/guides/wallet/), [smart contracts](/guides/smart_contracts/smart_contracts_guide), and how to [use Web3.js with the Hardhat development environment](/guides/hardhat_tutorial/). + +### For Intermediate & Advanced Web3.js Developers + +If you're already familiar with Ethereum and Web3.js development, you may want to review the Web3.js [package structure](#packages--plugins) and proceed directly to the [package-level documentation](/libdocs/ABI) and [API documentation](/api). Application developers may wish to review the [Web3.js configuration guide](/guides/web3_config/) or learn how to use Web3.js with tools like the [MetaMask](/guides/wallet/metamask) wallet or the [WalletConnect](/guides/wallet/web3_modal_guide/) wallet selection modal. Don't forget to review the [list of available plugins](https://web3js.org/plugins) or even [learn how to build your own Web3.js plugin](/guides/web3_plugin_guide/plugin_authors)! + +## Packages & Plugins + +Web3.js is a modular collection of packages, each of which serves a specific needs. This means developers don't need to install the entire Web3 library for most use cases. Instead, necessary packages are selectively installed for a more efficient development experience. Here is an overview of a selection of available packages: + +- [**Web3Eth:**](/libdocs/Web3Eth) The `web3-eth` package is the entrypoint to Web3.js - it's the control center for managing interactions with Ethereum and other EVM-compatible networks. + +- [**Net:**](/libdocs/Net) The `web3-net` package provides discovery and interactions for an **Ethereum node's network properties.** + +- [**Accounts:**](/libdocs/Accounts) The `web3-eth-accounts` package has tools for creating Ethereum accounts and the **secure signing** of transactions and data. + +- [**Personal:**](/libdocs/Personal) Use `web3-eth-personal` for **direct communication about your accounts with the Ethereum node**, which streamlines account management during development. -## Packages + **NOTE:** *For enhanced security in production and when interacting with public nodes, consider using `web3-eth-accounts` for local signing operations, which keeps your private keys and sensitive information secure on your local machine* -Web3.js is modular, consisting of several packages, each serving specific functionalities. If you have specific tasks in mind, you don't need to install the entire Web3 library. Instead, selectively install the package that suits your requirements for a more efficient development experience. Here is an overview of the available packages: +- [**Utils:**](/libdocs/Utils) The `web3-utils` package provides helpers to perform a range of essential Ethereum development tasks, including **converting data formats, checking addresses, encoding and decoding data, hashing, handling numbers, and much more.**. -- [**ABI:**](/libdocs/ABI) The `web3-eth-abi` package simplifies decoding logs and parameters, encoding function calls and signatures, and inferring types for efficient Ethereum **contract interactions.** +- [**Contract:**](/libdocs/Contract) The `web3-eth-contract` package makes it easy to **interact with smart contracts through JavaScript or TypeScript,** which streamlines the development process and makes it less error-prone. -- [**Accounts:**](/libdocs/Accounts) The `web3-eth-accounts` package has tools for creating Ethereum accounts/wallets and making sure transactions and data are **securely signed.** +- [**ABI:**](/libdocs/ABI) The `web3-eth-abi` package simplifies decoding logs and parameters, encoding function calls and signatures, and inferring types for efficient Ethereum **smart contract interactions.** -- [**Contract:**](/libdocs/Contract) With the `web3-eth-Contract`, you can interact with Smart contracts. This functionality allows **communication with contracts through JavaScript or TypeScript objects**, simplifying your development and interaction processes. +- [**ENS:**](/libdocs/ENS) The `web3-eth-ens` package makes it easy for developers to communicate with the **Ethereum Name Service (ENS).** -- [**ENS:**](/libdocs/ENS) The `web3-eth-ens` package helps you communicate with the **Ethereum Name Service (ENS)** on the blockchain. +- [**Iban:**](/libdocs/Iban) The `web3-eth-iban` package allows you to switch between **Ethereum addresses and special banking-like addresses** (IBAN or BBAN) and simplifies conversion between the types. -- [**Iban:**](/libdocs/Iban) The `web3-eth-iban` package allows you to switch between **Ethereum addresses and special banking-like addresses** (IBAN or BBAN). It makes the conversion back and forth easier. +### Additional Supporting Packages -- [**Net:**](/libdocs/Net) The `web3-net` class allows you to talk about and deal with an **Ethereum node's network details.** +- [**Web3 Core:**](/api/web3-core) subscriptions, request management, and configuration used by other Web3 packages -- [**Personal:**](/libdocs/Personal) Use `web3-eth-personal` for **direct communication with the Ethereum node about your accounts**, streamlining account management in your development workflow. - **NOTE:** *For enhanced security when interacting with public nodes, consider using `web3-eth-accounts` for local signing operations, keeping your private keys and sensitive information secure on your local machine* +- [**Web3 Types:**](/api/web3-types) data structures, objects, interfaces and types used by Web3 -- [**Utils:**](/libdocs/Utils) With the `web3-utils` package you can perform a range of essential tasks in Ethereum development, including **converting data formats, checking addresses, encoding and decoding, hashing, handling numbers, and much more**, providing versatile utility functions for your applications. +- [**Web3 Validator:**](/api/web3-validator) runtime type validation against predefined types or custom schemas -- [**Web3Eth:**](/libdocs/Web3Eth) The `web3-eth` is your main tool for interacting with the Ethereum blockchain. It's like the control center for managing your interactions with Ethereum. +- [**Web3 Errors:**](/api/web3-errors) error codes and common error classes that are used by other Web3 packages -### Additional supporting packages +- [**Web3 RPC Methods:**](/api/web3/namespace/rpcMethods) functions for making RPC requests to Ethereum using a given provider -- **Web3 Types:** This package has common typescript types. +### Plugins -- **Web3 Validator:** This package offers functionality for validation using provided Schema. +Web3.js supports [plugins](/guides/web3_plugin_guide/), which are another way to encapsulate capabilities that support a specific need. There are plugins that exist to support native features, like those described by [EIPs](https://eips.ethereum.org/) as well as plugins that are designed to support specific smart contracts, middleware, or even other Ethereum-compatible networks. Visit the [Web3.js plugins homepage](https://web3js.org/plugins) to view a list of the most important Web3.js plugins, which includes: -- **Web3 Core:** Web3 Core has configuration, Subscriptions and Requests management functionality used by other Web3 packages. +- [EIP-4337 (Account Abstraction) Plugin](https://www.npmjs.com/package/@chainsafe/web3-plugin-eip4337) -- **Web3 Errors:** Web3 Errors has error codes and common error classes that are used by other Web3 packages. +- [EIP-4844 (Blob Transactions) Plugin](https://www.npmjs.com/package/web3-plugin-blob-tx) -- **Web3 RPC Methods:** This is for advanced uses for building more lightweight applications. It has functions for making RPC requests to Ethereum using a given provider. +- [zkSync Plugin](https://www.npmjs.com/package/web3-plugin-zksync) -## Advantages over other libraries +## Advantages Over Other Libraries -- **Extensive Documentation and Community**: Being one of the earliest Ethereum libraries, Web3.js benefits from extensive documentation and a large, active community. Web3.js is widely adopted and has been thoroughly tested in various production environments and is compatible with a broad range of other tools and services in the Ethereum ecosystem. +- **Extensive Documentation and Community**: Web3.js is one of the most established Ethereum libraries, which means it benefits from extensive documentation and a large, active community. Web3.js is widely adopted and has been thoroughly tested in various production environments, and is compatible with a broad range of other tools and services in the Ethereum ecosystem. -- **Modular Design**: Web3.js is designed to be modular, meaning it allows developers to use specific packages according to their needs. This may lead to smaller bundle sizes and faster load times for web applications. +- **Modular Design**: Web3.js is designed to be modular, which allows developers to use specific packages according to their needs. This leads to smaller bundle sizes and faster load times for web applications. - **Active Development and Support**: Web3.js sees regular updates and active development. This support is crucial for developers needing assurance that the library they're using will keep pace with the evolving Ethereum landscape. diff --git a/docs/docs/guides/getting_started/quickstart.md b/docs/docs/guides/getting_started/quickstart.md new file mode 100644 index 00000000000..80551ad735b --- /dev/null +++ b/docs/docs/guides/getting_started/quickstart.md @@ -0,0 +1,272 @@ +--- +sidebar_position: 2 +sidebar_label: Quickstart +--- + +# Quickstart + +Use the live code editor to try Web3.js in your browser now! Keep reading to learn how to use Web3.js in a local development environment. + +## Live Code Editor + + + +## Installation + +If NPM is being used as package manager, install Web3.js with the following command: + +``` +npm i web3 +``` + +For projects using Yarn as a package manager, use: + +``` +yarn add web3 +``` + +Note: Installing Web3.js in this way will bring in all Web3.js sub-[packages](/#packages). If you only need specific packages, it is recommended to install them individually (e.g, if you want the [Contract](/libdocs/Contract) package, use `npm i web3-eth-contract` instead) + +## Importing Web3.js + +Web3.js v4 supports both CommonJS (CJS) and native ECMAScript module (ESM) imports. For importing the main Web3 class in CJS, use: + +``` js +const { Web3 } = require('web3'); +``` + +For ESM-style imports, use: + +``` ts +import { Web3 } from 'web3'; +``` + +## Initialize `Web3` with a Provider + +[Providers](/guides/web3_providers_guide/) are services that are responsible for enabling connectivity with the Ethereum network. The `Web3` object must be initialized with a valid provider to function as intended. Web3.js supports [HTTP](/guides/web3_providers_guide/#http-provider), [WebSocket](/guides/web3_providers_guide/#websocket-provider), and [IPC](/guides/web3_providers_guide/#ipc-provider) providers, and exposes packages for working with each type of provider. + +Web3.js is in compliance with [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193), the Ethereum Provider JavaScript API, so any EIP-1193 provider can be used to initialize the `Web3` object. + +``` ts +import { Web3 } from 'web3'; + +// private RPC endpoint +const web3 = new Web3('https://mainnet.infura.io/v3/YOUR_INFURA_ID'); + +// or public RPC endpoint +// const web3 = new Web3('https://eth.llamarpc.com'); + +web3.eth.getBlockNumber().then(console.log); +// ↳ 18849658n +``` +## Querying the Blockchain + +After instantiating the `Web3` instance with a provider, the [`web3-eth`](/libdocs/Web3Eth) package can be used to fetch data from the Ethereum network: + +```ts +// get the balance of an address +await web3.eth.getBalance('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'); +// ↳ 114438180989009447638n + +// get last block number +await web3.eth.getBlockNumber(); +// ↳ 18849658n + +// get the chain id of the current provider +await web3.eth.getChainId(); +// ↳ 1n + +// get the nonce of an address +await web3.eth.getTransactionCount('0x37826D8B5F4B175517A0f42c886f8Fca38C55Fe7'); +// ↳ 7n + +// get the current gas price +await web3.eth.getGasPrice(); +// ↳ 23879160756n +``` + +## Setting Up a Wallet + +To send transactions to the Ethereum network (e.g. [transferring ETH](/guides/getting_started/quickstart#transfer-eth) or [interacting with smart contracts](/guides/getting_started/quickstart#interact-with-smart-contracts)), it's necessary to use an [account](https://ethereum.org/en/developers/docs/accounts/) with funds to cover [gas fees](https://ethereum.org/en/developers/docs/gas/). + +The [`Wallet`](/api/web3-eth-accounts/class/Wallet) object is designed to manage a set of accounts that can be used to send transactions with [`web3.eth.sendTransaction`](/api/web3/class/Web3Eth#sendTransaction) or [`web3.eth.contract.methods.contractfunction().send()`](/api/web3-eth-contract/class/Contract). + +### Create a Random Account + +Using the `Wallet` to create a random account is a good way to accelerate the development process, but it's not suitable for mainnet or production uses, since random accounts will not have funds to cover gas fees. Use the [`Wallet.create`](/api/web3-eth-accounts/class/Wallet#create) method to create a random account. + +```ts +// create random wallet with 1 account +web3.eth.accounts.wallet.create(1) +/* ↳ +Wallet(1) +[ + { + address: '0xcE6A5235d6033341972782a15289277E85E5b305', + privateKey: '0x50d349f5cf627d44858d6fcb6fbf15d27457d35c58ba2d5cfeaf455f25db5bec', + signTransaction: [Function: signTransaction], + sign: [Function: sign], + encrypt: [Function: encrypt] + }, + _accountProvider: { + create: [Function: createWithContext], + privateKeyToAccount: [Function: privateKeyToAccountWithContext], + decrypt: [Function: decryptWithContext] + }, + _addressMap: Map(1) { '0xce6a5235d6033341972782a15289277e85e5b305' => 0 }, + _defaultKeyName: 'web3js_wallet' +] +*/ +``` + +### Add an Account from a Private Key + +Use the [`Wallet.add`](/api/web3-eth-accounts/class/Wallet#add) method to use a private key to add an existing account to a wallet. + +:::warning +Private keys are sensitive data and should be treated as such. Make sure that private keys are kept private, which includes making sure they are not committed to code repositories. +::: + +```ts +// the private key must start with the "0x" prefix +const account = web3.eth.accounts.wallet.add('0x50d349f5cf627d44858d6fcb6fbf15d27457d35c58ba2d5cfeaf455f25db5bec'); + +console.log(account[0].address); +//↳ 0xcE6A5235d6033341972782a15289277E85E5b305 + +console.log(account[0].privateKey); +//↳ 0x50d349f5cf627d44858d6fcb6fbf15d27457d35c58ba2d5cfeaf455f25db5bec +``` + +### Transfer ETH + +This is an example of using a private key to add an account to a wallet, and then using that account to transfer ETH: + +```ts +// add an account to a wallet +const account = web3.eth.accounts.wallet.add('0x50d349f5cf627d44858d6fcb6fbf15d27457d35c58ba2d5cfeaf455f25db5bec'); + +// create transaction object to send 1 eth to '0xa32...c94' address from the account[0] +const tx = +{ + from: account[0].address, + to: '0xa3286628134bad128faeef82f44e99aa64085c94', + value: web3.utils.toWei('1', 'ether') +}; +// the "from" address must match the one previously added with wallet.add + +// send the transaction +const txReceipt = await web3.eth.sendTransaction(tx); + +console.log('Tx hash:', txReceipt.transactionHash) +// ↳ Tx hash: 0x03c844b069646e08af1b6f31519a36e3e08452b198ef9f6ce0f0ccafd5e3ae0e +``` + +## Interact with Smart Contracts + +[Smart contracts](https://ethereum.org/en/developers/docs/smart-contracts/) are programs that run on the Ethereum network. Keep reading to learn how to use Web3.js to interact with smart contracts. + +### Instantiate a Smart Contract + +The first step to interacting with a smart contract is to instantiate it, which requires the [ABI](https://docs.soliditylang.org/en/develop/abi-spec.html) and address of the smart contract. The following examples demonstrates instantiating the [Uniswap](https://uniswap.org/) token smart contract: + +```ts +// Uniswap token smart contract address (Mainnet) +const address = '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984' + +// you can find the complete ABI on etherscan.io +// https://etherscan.io/address/0x1f9840a85d5af5bf1d1762f925bdaddc4201f984#code +const ABI = +[ + { + name: 'symbol', + outputs: [{ type: 'string' }], + type: 'function', + }, + { + name: 'totalSupply', + outputs: [{ type: 'uint256' }], + type: 'function', + }, +]; + +// instantiate the smart contract +const uniswapToken = new web3.eth.Contract(abi, address); +``` + +### Read Methods + +Since reading data from a smart contract does not consume any gas, it's not necessary to use an account to do so. Here are some examples of reading data from the Uniswap token smart contract: + +```ts +// make the call to the contract +const symbol = await uniswapToken.methods.symbol().call(); + +console.log('Uniswap symbol:',symbol); +// ↳ Uniswap symbol: UNI + +// make the call to the contract +const totalSupply = await uniswapToken.methods.totalSupply().call(); + +console.log('Uniswap Total supply:', totalSupply); +// ↳ Uniswap Total Supply: 1000000000000000000000000000n +``` + +### Write Methods + +Writing data to a smart contract consumes gas and requires the use of an account with funds. The following example demonstrates such an interaction: + +```ts +// address to send the token +const to = '0xcf185f2F3Fe19D82bFdcee59E3330FD7ba5f27ce'; + +// value to transfer (1 with 18 decimals) +const value = web3.utils.toWei('1','ether'); + +// send the transaction => return the Tx receipt +const txReceipt = await uniswapToken.methods.transfer(to,value).send({from: account[0].address}); + +console.log('Tx hash:',txReceipt.transactionHash); +// ↳ Tx hash: 0x14273c2b5781cc8f1687906c68bfc93482c603026d01b4fd37a04adb6217ad43 +``` + +### Query Past Events + +Smart contracts emit [events](https://ethereum.org/en/developers/docs/smart-contracts/anatomy/#events-and-logs) to communicate important interactions. This example demonstrates how to query the Uniswap token smart contract for all `Transfer` events that occurred after a certain block number: + +```ts +// get past `Transfer` events from block 18850576 +const eventTransfer = await uniswapToken.getPastEvents('Transfer', { fromBlock: 18850576 }); + +console.log(eventTransfer); +// ↳ [{...},{...}, ...] array with all the events emitted +``` + +:::note +You can only query logs from the most recent 100,000 blocks. +::: + +### Subscribing to Events + +Web3.js allows user to subscribe to events for real-time notification of important contract interactions. Here is an example of creating a subscription to the Uniswap token's `Transfer` event: + +:::note +HTTP providers do not support real-time event subscriptions. Use one of the other [provider types](/guides/web3_providers_guide/#providers-types) to subscribe to real-time events. +::: + +```ts +import { Web3 } from 'web3'; + +// WebSocket provider +const web3 = new Web3('wss://ethereum.publicnode.com'); + +// instantiate contract +const uniswapToken = new web3.eth.Contract(abi, address) + +// create the subscription to all the 'Transfer' events +const subscription = uniswapToken.events.Transfer(); + +// listen to the events +subscription.on('data',console.log); +// ↳ [{...},{...}, ...] live events will be printed in the console +``` diff --git a/docs/docs/guides/hardhat_tutorial/_category_.yml b/docs/docs/guides/hardhat_tutorial/_category_.yml index 191ad0faa29..8493ca97d6e 100644 --- a/docs/docs/guides/hardhat_tutorial/_category_.yml +++ b/docs/docs/guides/hardhat_tutorial/_category_.yml @@ -2,4 +2,4 @@ label: '⛑️ Hardhat Tutorial' collapsible: true collapsed: true link: null -position: 3 +position: 6 diff --git a/docs/docs/guides/migration_from_other_libs/_category_.yml b/docs/docs/guides/migration_from_other_libs/_category_.yml index 665581166bc..59fb5de8cdc 100644 --- a/docs/docs/guides/migration_from_other_libs/_category_.yml +++ b/docs/docs/guides/migration_from_other_libs/_category_.yml @@ -2,4 +2,4 @@ label: '🔄 Migration Guides' collapsible: true collapsed: true link: null -position: 12 \ No newline at end of file +position: 15 diff --git a/docs/docs/guides/resources_and_troubleshooting/index.md b/docs/docs/guides/resources_and_troubleshooting/index.md index 6426b99d8df..2dd82f1e5b6 100644 --- a/docs/docs/guides/resources_and_troubleshooting/index.md +++ b/docs/docs/guides/resources_and_troubleshooting/index.md @@ -1,5 +1,5 @@ --- -sidebar_position: 17 +sidebar_position: 16 sidebar_label: '📚 Resources & Troubleshooting' --- # Resources & Troubleshooting diff --git a/docs/docs/guides/smart_contracts/_category_.yml b/docs/docs/guides/smart_contracts/_category_.yml index fcbba9574a5..8a1d21c75b4 100644 --- a/docs/docs/guides/smart_contracts/_category_.yml +++ b/docs/docs/guides/smart_contracts/_category_.yml @@ -2,4 +2,4 @@ label: '📜 Smart Contracts' collapsible: true collapsed: true link: null -position: 6 \ No newline at end of file +position: 4 diff --git a/docs/docs/guides/smart_contracts/index.md b/docs/docs/guides/smart_contracts/mastering_smart_contracts.md similarity index 97% rename from docs/docs/guides/smart_contracts/index.md rename to docs/docs/guides/smart_contracts/mastering_smart_contracts.md index 4cb6c9eed34..dfc1759ba23 100644 --- a/docs/docs/guides/smart_contracts/index.md +++ b/docs/docs/guides/smart_contracts/mastering_smart_contracts.md @@ -1,5 +1,5 @@ --- -sidebar_position: 1 +sidebar_position: 2 sidebar_label: 'Mastering Smart Contracts' --- # Mastering Smart Contracts @@ -103,7 +103,7 @@ When you instantiate a `Contract`, you primarily provide one or two parameters, 1. **ABI (Application Binary Interface):** The ABI tells the `Contract` how to format calls and transactions so that the contract can understand them. :::tip -If you do not know how to get the contract ABI, we recommend you to check the Step 4 at the [# Step 4: Deploying and Interacting with Smart Contracts](./smart_contracts_guide/#step-4-compile-the-solidity-code-using-the-solidity-compiler-and-get-its-abi-and-bytecode) tutorial. And to look into the guide: [Infer Contract Types from JSON Artifact](./infer_contract_types). +If you do not know how to get the contract ABI, we recommend you to check the Step 4 at the [# Step 4: Deploying and Interacting with Smart Contracts](./smart_contracts_guide#step-4-compile-the-solidity-code-with-the-solidity-compiler-and-get-its-abi-and-bytecode) tutorial. And to look into the guide: [Infer Contract Types from JSON Artifact](./infer_contract_types). ::: 2. (optional) **Contract Address:** The Ethereum address at which your contract is deployed. If the contract is not deployed yet, do not pass a second parameter or pass `undefined` to it. @@ -274,7 +274,7 @@ console.log('Contract deployed at address: ' + tx.options.address); ``` :::tip -If you do not know how to get the contract bytecode, we recommend you to check the Step 4 at the [Deploying and Interacting with Smart Contracts](./smart_contracts_guide#step-4-compile-the-solidity-code-using-the-solidity-compiler-and-get-its-abi-and-bytecode) tutorial. +If you do not know how to get the contract bytecode, we recommend you to check the Step 4 at the [Deploying and Interacting with Smart Contracts](./smart_contracts_guide#step-4-compile-the-solidity-code-with-the-solidity-compiler-and-get-its-abi-and-bytecode) tutorial. ::: - **getPastEvents**: Gets past events for this contract. It differs from `events` properties that it returns the past events as an array, rather than allowing to subscribe to them like when using `events` properties. More on the [API documentation](/api/web3-eth-contract/class/Contract#getPastEvents) @@ -351,7 +351,7 @@ const bytecode = '0x60806040523480156100115760006000fd5b506040516102243803806102 :::info And as mentioned in the tips inside previous sections: -If you do not know how to get the contract ABI and bytecode, we recommend you to check the Step 4 at the [Deploying and Interacting with Smart Contracts](./smart_contracts_guide#step-4-compile-the-solidity-code-using-the-solidity-compiler-and-get-its-abi-and-bytecode) tutorial. +If you do not know how to get the contract ABI and bytecode, we recommend you to check the Step 4 at the [Deploying and Interacting with Smart Contracts](./smart_contracts_guide#step-4-compile-the-solidity-code-with-the-solidity-compiler-and-get-its-abi-and-bytecode) tutorial. ::: ### Do I always need the contract ByteCode? diff --git a/docs/docs/guides/smart_contracts/smart_contracts_guide.md b/docs/docs/guides/smart_contracts/smart_contracts_guide.md index 2df24c562eb..9ca62a4b2cd 100644 --- a/docs/docs/guides/smart_contracts/smart_contracts_guide.md +++ b/docs/docs/guides/smart_contracts/smart_contracts_guide.md @@ -1,41 +1,41 @@ --- -sidebar_position: 2 +sidebar_position: 1 sidebar_label: 'Tutorial: Deploying and Interacting with Smart Contracts' --- # Deploying and Interacting with Smart Contracts -## Introduction - -In this tutorial, we will walk through the process of deploying a smart contract to the Ethereum network, generating the ABI, and interacting with the smart contract using web3.js version 4.x. We will cover the basic concepts of Ethereum, Solidity, and web3.js and provide step-by-step instructions for deploying a simple smart contract to a test network using Ganache. +In this tutorial, we will walk through the process of deploying a [smart contract](https://ethereum.org/en/developers/docs/smart-contracts/) to a [development network](https://ethereum.org/en/developers/docs/development-networks/), generating the smart contract's [ABI](/glossary#json-interface-abi) and [bytecode](https://en.wikipedia.org/wiki/Bytecode), and interacting with the smart contract using Web3.js. We will cover the basic concepts of [Ethereum](https://ethereum.org/), the [Solidity](https://soliditylang.org/) smart contract programming language, the [Hardhat](https://hardhat.org/) development environment, and Web3.js. ## Overview Here is a high-level overview of the steps we will be taking in this tutorial: -1. Setting up the Environment -2. Create a new project directory and initialize a new Node.js project. -3. Write the Solidity code for the smart contract and save it to a file. -4. Compile the Solidity code using the Solidity Compiler and get its ABI and Bytecode. -5. Set up the web3.js library and connect to the Ganache network. -6. Deploy the smart contract to the Ganache network using web3.js. -7. Interact with the smart contract using web3.js. +1. Review prerequisites +2. Create a new directory and initialize a new Node.js project +3. Write the Solidity code for the smart contract and save it to a file +4. Compile the Solidity code with the Solidity compiler and get its ABI and bytecode +5. Set up Web3.js and Hardhat +6. Deploy the smart contract with Web3.js +7. Use Web3.js to interact with the smart contract :::tip -📝 **Community support:** -If you encounter any issues while following this guide or have questions, don't hesitate to seek assistance. Our friendly community is ready to help you out! -Join our [Discord](https://discord.gg/F4NUfaCC) server and head to the **#web3js-general** channel to connect with other developers and get the support you need. +If you encounter any issues while following this guide or have any questions, don't hesitate to seek assistance. Our friendly community is ready to help you out! Join our [Discord](https://discord.gg/F4NUfaCC) server and head to the **#web3js-general** channel to connect with other developers and get the support you need. ::: -## Step 1: Setting up the Environment +## Step 1: Prerequisites -Before we start writing and deploying our contract, we need to set up our environment. For that, we need to install the following: +This tutorial assumes basic familiarity with the command line as well as familiarity with JavaScript and [Node.js](https://nodejs.org/). Before starting this tutorial, ensure that Node.js and its package manager, npm, are installed. -1. Ganache - Ganache is a personal blockchain for Ethereum development that allows you to see how your smart contracts function in real-world scenarios. You can download it from http://truffleframework.com/ganache -2. Node.js - Node.js is a JavaScript runtime environment that allows you to run JavaScript on the server-side. You can download it from https://nodejs.org/en/download/ -3. npm - Node Package Manager is used to publish and install packages to and from the public npm registry or a private npm registry. Here is how to install it https://docs.npmjs.com/downloading-and-installing-node-js-and-npm. (Alternatively, you can use yarn instead of npm https://classic.yarnpkg.com/lang/en/docs/getting-started/) +```bash +$: node -v +# your version may be different, but it's best to use the current stable version +v18.16.1 +$: npm -v +9.5.1 +``` -## Step 2: Create a new project directory and initialize a new Node.js project +## Step 2: Create a New Directory and Initialize a New Node.js Project First, create a new project directory for your project and navigate into it: @@ -52,113 +52,98 @@ npm init -y This will create a new `package.json` file in your project directory. -## Step 3: Write the Solidity code for the smart contract and save it to a file +## Step 3: Write the Solidity Code for the Smart Contract and Save It to a File In this step, we will write the Solidity code for the smart contract and save it as a file in our project directory. Create a new file called `MyContract.sol` in your project directory and add the following Solidity code to it: -```ts +```js // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract MyContract { - uint256 public myNumber; + uint256 public myNumber; - constructor(uint256 _myNumber) { - myNumber = _myNumber; - } + constructor(uint256 _myNumber) { + myNumber = _myNumber; + } - function setMyNumber(uint256 _myNumber) public { - myNumber = _myNumber; - } + function setMyNumber(uint256 _myNumber) public { + myNumber = _myNumber; + } } ``` This simple smart contract defines a `myNumber` variable that can be set by calling the `setMyNumber` function. -## Step 4: Compile the Solidity code using the Solidity Compiler and get its ABI and Bytecode. - -:::tip -📝 Alternatively, you can use something like `npm i solc && npx solcjs MyContract.sol --bin --abi`. And then rename the files to `MyContractBytecode.bin` and `MyContractAbi.json`, in order to keep them the same as they will be used later in this tutorial. -More on solc-js is at https://github.com/ethereum/solc-js. -::: - -:::tip -📝 You can totally skip the step of manually compiling the Solidity code if you use a web3.js plugin: https://www.npmjs.com/package/web3-plugin-craftsman that would compile the solidity code internally and enables you to interact with smart contracts directly from its Solidity code. -::: - -In this step, we will use the Solidity Compiler (solc) to compile the Solidity code and generate the compiled code. +## Step 4: Compile the Solidity Code with the Solidity Compiler and Get Its ABI and Bytecode -First, install the `solc` package using npm. +In this step, we will use the [Solidity compiler](https://docs.soliditylang.org/en/latest/using-the-compiler.html) (`solc`) to compile the contract's Solidity code and output the bytecode and ABI that was generated by the Solidity compiler. -:::note -📝 Specify a version for the compiler that is compatible with the version you specified in the .sol file above (with `pragma solidity ^0.8.0;`): -::: +First, install the `solc` package using npm. Make sure that the version of `solc` is compatible with the version of Solidity that is specified in the smart contract with the `pragma solidity` directive. -``` -npm i solc@0.8.0 +```bash +npm i solc@^0.8.0 ``` Next, create a new file called `compile.js` in your project directory and add the following code to it: -```typescript -// This code will compile smart contract and generate its ABI and bytecode -// Alternatively, you can use something like `npm i solc && npx solcjs MyContract.sol --bin --abi` +```js +const solc = require("solc"); +const path = require("path"); +const fs = require("fs"); -import solc from 'solc'; -import path from 'path'; -import fs from 'fs'; - -const fileName: string = 'MyContract.sol'; -const contractName: string = 'MyContract'; +const contractName = "MyContract"; +const fileName = `${contractName}.sol`; // Read the Solidity source code from the file system -const contractPath: string = path.join(__dirname, fileName); -const sourceCode: string = fs.readFileSync(contractPath, 'utf8'); +const contractPath = path.join(__dirname, fileName); +const sourceCode = fs.readFileSync(contractPath, "utf8"); // solc compiler config const input = { - language: 'Solidity', - sources: { - [fileName]: { - content: sourceCode, - }, - }, - settings: { - outputSelection: { - '*': { - '*': ['*'], - }, - }, - }, + language: "Solidity", + sources: { + [fileName]: { + content: sourceCode, + }, + }, + settings: { + outputSelection: { + "*": { + "*": ["*"], + }, + }, + }, }; // Compile the Solidity code using solc const compiledCode = JSON.parse(solc.compile(JSON.stringify(input))); // Get the bytecode from the compiled contract -const bytecode: string = compiledCode.contracts[fileName][contractName].evm.bytecode.object; +const bytecode = + compiledCode.contracts[fileName][contractName].evm.bytecode.object; // Write the bytecode to a new file -const bytecodePath: string = path.join(__dirname, 'MyContractBytecode.bin'); +const bytecodePath = path.join(__dirname, "MyContractBytecode.bin"); fs.writeFileSync(bytecodePath, bytecode); // Log the compiled contract code to the console -console.log('Contract Bytecode:\n', bytecode); +console.log("Contract Bytecode:\n", bytecode); // Get the ABI from the compiled contract -const abi: any[] = compiledCode.contracts[fileName][contractName].abi; +const abi = compiledCode.contracts[fileName][contractName].abi; // Write the Contract ABI to a new file -const abiPath: string = path.join(__dirname, 'MyContractAbi.json'); -fs.writeFileSync(abiPath, JSON.stringify(abi, null, '\t')); +const abiPath = path.join(__dirname, "MyContractAbi.json"); +fs.writeFileSync(abiPath, JSON.stringify(abi, null, "\t")); // Log the Contract ABI to the console -console.log('Contract ABI:\n', abi); +console.log("Contract ABI:\n", abi); ``` -This code reads the Solidity code from the `MyContract.sol` file, compiles it using `solc`, and generates the ABI and bytecode for the smart contract. It then writes the bytecode to a new file called `MyContractBytecode.bin` and the contract ABI to `MyContractAbi.json`. And it logs them to the console. +This code reads the Solidity code from the `MyContract.sol` file, compiles it using `solc`, and generates the ABI and bytecode for the smart contract. It then writes the bytecode to a new file called `MyContractBytecode.bin` and the contract ABI to `MyContractAbi.json` (these values are also logged to the console). Run the following command to compile the Solidity code: @@ -166,42 +151,84 @@ Run the following command to compile the Solidity code: node compile.js ``` -If everything is working correctly, you should see both the Contract Bytecode and the Contract ABI logged to the console. +If everything is working correctly, you should see the contract's bytecode and ABI logged to the console and see two new files (`MyContractAbi.json` and `MyContractBytecode.bin`) in the project directory. :::tip -📝 There are couple of other ways to get the bytecode and the ABI like using Remix and check the _Compilation Details_ after compiling the smart contract (https://remix-ide.readthedocs.io/en/latest/run.html#using-the-abi-with-ataddress). +Alternatively, you can generate the bytecode and ABI with `npm i solc && npx solcjs MyContract.sol --bin --abi`. Keep in mind that this will generate files with different names than those used in this tutorial, so the files would need to be renamed to `MyContractBytecode.bin` and `MyContractAbi.json` to follow the rest of this guide exactly as it's written. Learn more about `solc-js` at https://github.com/ethereum/solc-js. ::: -## Step 5: Set up web3.js and connect to the Ganache network +## Step 5: Set Up Web3.js and Hardhat + +In this step, we will set up Web3.js and Hardhat. -In this step, we will set up the web3.js library and connect to the Ganache network. So, be sure to run Ganache if you did not already did. +First, install the required packages with npm: -First, install the `web3` package using npm: +```bash +npm i web3 hardhat +``` + +Next, initialize the Hardhat project: + +```bash +npx hardhat init +``` + +Initializing the Hardhat project will require responding to several prompts - select the default option for each prompt. After you initialize the Hardhat project, a number of new files and directories will be created. + +To start the Hardhat development network, execute the following command: ```bash -npm i web3 +npx hardhat node ``` +Executing this command will produce the following output, which provides the URL that can be used to connect to the development network as well as the development network's test accounts: + +```bash +Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/ + +Accounts +======== + +WARNING: These accounts, and their private keys, are publicly known. +Any funds sent to them on Mainnet or any other live network WILL BE LOST. + +Account #0: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000 ETH) +Private Key: + +... + +Account #19: 0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199 (10000 ETH) +Private Key: + +WARNING: These accounts, and their private keys, are publicly known. +Any funds sent to them on Mainnet or any other live network WILL BE LOST. +``` + +The Hardhat development network needs to remain running in the terminal that was used to start it. Open a new terminal instance in the project directory to execute the remaining commands in this tutorial. + Next, create a new file called `index.js` in your project directory and add the following code to it: -```typescript -import { Web3 } from 'web3'; +```js +const { Web3 } = require("web3"); -// Set up a connection to the Ganache network -const web3: Web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:7545')); +const web3 = new Web3("http://127.0.0.1:8545/"); -// Log the current block number to the console +// Log the chain ID to the console web3.eth - .getBlockNumber() - .then((result: number) => { - console.log('Current block number: ' + result); - }) - .catch((error: Error) => { - console.error(error); - }); + .getChainId() + .then((result) => { + console.log("Chain ID: " + result); + }) + .catch((error) => { + console.error(error); + }); ``` -This code sets up a connection to the Ganache network and logs the current block number to the console. +This code sets up a Web3.js connection to the Hardhat development network and logs the chain ID to the console. + +:::note +This tutorial uses the default URL for the Hardhat development network (http://127.0.0.1:8545/). Make sure to use the actual URL that was provided when the Hardhat development network was started. +::: Run the following command to test the connection: @@ -209,64 +236,66 @@ Run the following command to test the connection: node index.js ``` -If everything is working correctly, you should see the current block number logged to the console. However, if you got an error with the reason `connect ECONNREFUSED 127.0.0.1:7545` then double check that you are running Ganache locally on port `7545`. +If everything is working correctly, you should see the chain ID logged to the console: + +```bash +Chain ID: 31337 +``` -## Step 6: Deploy the smart contract to the Ganache network using web3.js +## Step 6: Deploy the Smart Contract with Web3.js -In this step, we will use web3.js to deploy the smart contract to the Ganache network. +In this step, we will use Web3.js to deploy the smart contract to the development network. Create a file named `deploy.js` and fill it with the following code: -```ts -// For simplicity we use `web3` package here. However, if you are concerned with the size, -// you may import individual packages like 'web3-eth', 'web3-eth-contract' and 'web3-providers-http'. -import { Web3 } from 'web3'; -import fs from 'fs'; -import path from 'path'; +```js +const { Web3 } = require("web3"); +const path = require("path"); +const fs = require("fs"); -const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:7545')); +const web3 = new Web3("http://127.0.0.1:8545/"); -const bytecodePath: string = path.join(__dirname, 'MyContractBytecode.bin'); -const bytecode: string = fs.readFileSync(bytecodePath, 'utf8'); +const bytecodePath = path.join(__dirname, "MyContractBytecode.bin"); +const bytecode = fs.readFileSync(bytecodePath, "utf8"); -const abi: any = require('./MyContractAbi.json'); -const myContract: any = new web3.eth.Contract(abi); +const abi = require("./MyContractAbi.json"); +const myContract = new web3.eth.Contract(abi); myContract.handleRevert = true; -async function deploy(): Promise { - const providersAccounts: string[] = await web3.eth.getAccounts(); - const defaultAccount: string = providersAccounts[0]; - console.log('deployer account:', defaultAccount); - - const contractDeployer: any = myContract.deploy({ - data: '0x' + bytecode, - arguments: [1], - }); - - const gas: number = await contractDeployer.estimateGas({ - from: defaultAccount, - }); - console.log('estimated gas:', gas); - - try { - const tx: any = await contractDeployer.send({ - from: defaultAccount, - gas, - gasPrice: 10000000000, - }); - console.log('Contract deployed at address: ' + tx.options.address); - - const deployedAddressPath: string = path.join(__dirname, 'MyContractAddress.bin'); - fs.writeFileSync(deployedAddressPath, tx.options.address); - } catch (error) { - console.error(error); - } +async function deploy() { + const providersAccounts = await web3.eth.getAccounts(); + const defaultAccount = providersAccounts[0]; + console.log("Deployer account:", defaultAccount); + + const contractDeployer = myContract.deploy({ + data: "0x" + bytecode, + arguments: [1], + }); + + const gas = await contractDeployer.estimateGas({ + from: defaultAccount, + }); + console.log("Estimated gas:", gas); + + try { + const tx = await contractDeployer.send({ + from: defaultAccount, + gas, + gasPrice: 10000000000, + }); + console.log("Contract deployed at address: " + tx.options.address); + + const deployedAddressPath = path.join(__dirname, "MyContractAddress.txt"); + fs.writeFileSync(deployedAddressPath, tx.options.address); + } catch (error) { + console.error(error); + } } deploy(); ``` -This code reads the bytecode from the `MyContractBytecode.bin` file and creates a new contract object using the ABI and bytecode. And, as an optional step, it estimates the gas that will be used to deploy the smart contract. It then deploys the contract to the Ganache network. It also saves the address inside the file `MyContractAddress.bin` which we be used when interacting with the contract. +This code reads the bytecode from the `MyContractBytecode.bin` file and the ABI from the `MyContractAbi.json` file and uses these values to instantiate a new contract object. This example also includes the optional step of estimating the [gas](https://ethereum.org/en/developers/docs/gas/) that will be used to deploy the smart contract. It then deploys the contract to the development network. Finally, it saves the [address](https://ethereum.org/en/developers/docs/accounts/#contract-accounts) of the deployed contract in the `MyContractAddress.txt` file, which will be used when interacting with the contract. Run the following command to deploy the smart contract: @@ -274,70 +303,67 @@ Run the following command to deploy the smart contract: node deploy.js ``` -If everything is working correctly, you should see something like the following: +If everything is working correctly, you should see something like the following (note that the values may not be exactly the same): ```bash -Deployer account: 0xdd5F9948B88608a1458e3a6703b0B2055AC3fF1b -Estimated gas: 142748n -Contract deployed at address: 0x16447837D4A572d0a8b419201bdcD91E6e428Df1 +Deployer account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 +Estimated gas: 141681n +Contract deployed at address: 0x5FbDB2315678afecb367f032d93F642f64180aa3 ``` -## Step 7: Interact with the smart contract using web3.js +## Step 7: Use Web3.js to Interact with the Smart Contract -In this step, we will use web3.js to interact with the smart contract on the Ganache network. +In this step, we will use Web3.js to interact with the smart contract on the development network. Create a file named `interact.js` and fill it with the following code: -```ts -import { Web3 } from 'web3'; -import fs from 'fs'; -import path from 'path'; +```js +const { Web3 } = require("web3"); +const path = require("path"); +const fs = require("fs"); -// Set up a connection to the Ethereum network -const web3: Web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:7545')); +const web3 = new Web3("http://127.0.0.1:8545/"); // Read the contract address from the file system -const deployedAddressPath: string = path.join(__dirname, 'MyContractAddress.bin'); -const deployedAddress: string = fs.readFileSync(deployedAddressPath, 'utf8'); - -// Read the bytecode from the file system -const bytecodePath: string = path.join(__dirname, 'MyContractBytecode.bin'); -const bytecode: string = fs.readFileSync(bytecodePath, 'utf8'); +const deployedAddressPath = path.join(__dirname, "MyContractAddress.txt"); +const deployedAddress = fs.readFileSync(deployedAddressPath, "utf8"); -// Create a new contract object using the ABI and bytecode -const abi: any = require('./MyContractAbi.json'); -const myContract: any = new web3.eth.Contract(abi, deployedAddress); +// Create a new contract object using the ABI and address +const abi = require("./MyContractAbi.json"); +const myContract = new web3.eth.Contract(abi, deployedAddress); myContract.handleRevert = true; -async function interact(): Promise { - const providersAccounts: string[] = await web3.eth.getAccounts(); - const defaultAccount: string = providersAccounts[0]; - - try { - // Get the current value of my number - const myNumber: string = await myContract.methods.myNumber().call(); - console.log('my number value: ' + myNumber); - - // Increment my number - const receipt: any = await myContract.methods.setMyNumber(BigInt(myNumber) + 1n).send({ - from: defaultAccount, - gas: 1000000, - gasPrice: '10000000000', - }); - console.log('Transaction Hash: ' + receipt.transactionHash); - - // Get the updated value of my number - const myNumberUpdated: string = await myContract.methods.myNumber().call(); - console.log('my number updated value: ' + myNumberUpdated); - } catch (error) { - console.error(error); - } +async function interact() { + const accounts = await web3.eth.getAccounts(); + const defaultAccount = accounts[0]; + + try { + // Get the current value of my number + const myNumber = await myContract.methods.myNumber().call(); + console.log("myNumber value: " + myNumber); + + // Increment my number + const receipt = await myContract.methods + .setMyNumber(BigInt(myNumber) + 1n) + .send({ + from: defaultAccount, + gas: 1000000, + gasPrice: "10000000000", + }); + console.log("Transaction Hash: " + receipt.transactionHash); + + // Get the updated value of my number + const myNumberUpdated = await myContract.methods.myNumber().call(); + console.log("myNumber updated value: " + myNumberUpdated); + } catch (error) { + console.error(error); + } } interact(); ``` -This code uses the `MyContract` object to interact with the smart contract. It gets the current value of myNumber, increments it and update it, and gets its updated value. It logs myNumber values and transaction receipts to the console. +This code uses the previously generated ABI and contract address to instantiate a [`Contract`](/api/web3-eth-contract/class/Contract) object for interacting with the `MyContract` smart contract. It gets the current value of `myNumber` from `MyContract`, logs it, updates it, and gets its updated value. It logs the updated `myNumber` value and the [transaction hash](https://help.coinbase.com/en-au/coinbase/getting-started/crypto-education/what-is-a-transaction-hash-hash-id) to the console. Run the following command to interact with the smart contract: @@ -345,91 +371,22 @@ Run the following command to interact with the smart contract: node interact.js ``` -If everything is working correctly, you should see the current counter value logged to the console, followed by the transaction receipt, and then the updated counter value. The output would like: +If everything is working correctly, you should see the current counter value logged to the console, followed by the transaction receipt, and then the updated counter value. The output should look like: ```bash -my number value: 1 -Transaction Hash: 0x9825e2a2115896728d0c9c04c2deaf08dfe1f1ff634c4b0e6eeb2f504372f927 -my number updated value: 2 -``` - -## Troubleshooting and errors - -If you are running into errors when executing contract methods such as `myContract.methods.call` or `myContract.deploy.estimateGas()` you might be seeing a contract execution revert error such as: `value transfer did not complete from a contract execution reverted` - -or response error: ResponseError: Returned error: unknown field `input`, expected one of `from`, `to`, `gasPrice`, `maxFeePerGas`, `maxPriorityFeePerGas`, `gas`, `value`, `data`, `nonce`, `chainId`, `accessList`, `type`. - -This could be due to the node you are connected to and is expecting the `data` property to be populated in your contract instead of `input`, for example this issue will happen with an Anvil node from Foundry. Web3 version >4.0.3 will always populate `input` when sending transactions. -To fix this, configure the `contractDataInputFill` in `Web3Config` or when initializing your contract to specify `data` in `dataInputFill` to be filled. -Another way to fix this is to provide `data` when using the send or call method. -If you want both `data` and `input` filled, set the property to `both`. - -Here are examples: - -```ts -// Configuring Web3Context with `contractDataInputFill` -import { Web3Context } from 'web3-core'; -import { Contract } from 'web3-eth-contract'; - -const expectedProvider = 'http://127.0.0.1:8545'; -const web3Context = new Web3Context({ - provider: expectedProvider, - config: { contractDataInputFill: 'data' }, // all new contracts created to populate `data` field -}); - -const contract = new Contract(GreeterAbi, web3Context); - -// data will now be populated when using the call method -const res = await contract.methods.greet().call(); - -// Another way to do this is to set it within the contract using `dataInputFill` - -const contract = new Contract( - erc721Abi, - '0x1230B93ffd14F2F022039675fA3fc3A46eE4C701', - { gas: '123', dataInputFill: 'data' }, // methods will now be populating `data` field -); - -// `data` will now be populated instead of `input` -contract.methods.approve('0x00000000219ab540356cBB839Cbe05303d7705Fa', 1).call(); - -// Another way to do this is to set `data` when calling methods - -const contract = new Contract(erc721Abi, '0x1230B93ffd14F2F022039675fA3fc3A46eE4C701'); - -contract.methods - .approve('0x00000000219ab540356cBB839Cbe05303d7705Fa', 1) - .call({ - data: contract.methods.approve('0x00000000219ab540356cBB839Cbe05303d7705Fa', 1).encodeABI(), - }); +myNumber value: 1 +Transaction Hash: 0x956bf08a0ba71a768fdbf9879531e52e9b6a0e2802ad92f66387fc30fa939eb8 +myNumber updated value: 2 ``` ## Conclusion -In this tutorial, we learned how to generate the ABI and the Bytecode of a smart contract, deploy it to the Ethereum network, and interact with it using web3.js version 4.x. - -With this knowledge, you can start experimenting with writing smart contract in order for building your decentralized applications (dApps) on the Ethereum network using web3.js. Keep in mind that this is just the beginning, and there is a lot more to learn about Ethereum and web3.js. So keep exploring and building, and have fun! - -## Additional Resources +In this tutorial, you learned how to generate the ABI and bytecode of a smart contract, deploy it to a Hardhat development network, and interact with it using Web3.js. -- [Official web3.js Documentation](https://docs.web3js.org/) -- [Solidity Documentation](https://solidity.readthedocs.io/) -- [Ganache](https://www.trufflesuite.com/ganache) -- [Truffle](https://trufflesuite.com/) -- [Remix IDE](https://remix.ethereum.org/) +With this knowledge, you can start experimenting with writing smart contract in order to build decentralized applications ([dApps](https://ethereum.org/en/dapps/#what-are-dapps)) on the Ethereum network using Web3.js. Keep in mind that this is just the beginning, and there is a lot more to learn about Ethereum and Web3.js - so keep exploring and building, and have fun! ## Tips and Best Practices -- Always test your smart contracts on a local network like Ganache before deploying them to the mainnet. -- Use the latest version of web3.js and Solidity to take advantage of the latest features and security patches. -- Keep your private keys secure and never share them with anyone. -- Use the gas limit and gas price parameters carefully to avoid spending too much on transaction fees. -- Use the `estimateGas` function in web3.js to estimate the gas required for a transaction before sending it to the network. -- Use events to notify the client application about state changes in the smart contract. -- Use a linter like Solhint to check for common Solidity coding errors. - -## Final Thoughts - -Web3.js version 4.x provides a powerful and easy-to-use interface for interacting with the Ethereum network and building decentralized applications. And it has been rewritten in TypeScript but for simplicity of this tutorial we interacted with it in JavaScript. - -The Ethereum ecosystem is constantly evolving, and there is always more to learn and discover. As you continue to develop your skills and knowledge, keep exploring and experimenting with new technologies and tools to build innovative and decentralized solutions. +- Always test your smart contracts on a development network like Hardhat before deploying them to production. +- Use the latest version of Web3.js and Solidity to take advantage of the latest features and security patches. +- Use the Web3.js [`estimateGas`](https://docs.web3js.org/libdocs/Contract#estimategas) function to estimate the gas required for a transaction before sending it to the network. diff --git a/docs/docs/guides/wagmi_usage/_category_.yml b/docs/docs/guides/wagmi_usage/_category_.yml index 90b55da4312..ee90c923e38 100644 --- a/docs/docs/guides/wagmi_usage/_category_.yml +++ b/docs/docs/guides/wagmi_usage/_category_.yml @@ -2,4 +2,4 @@ label: '🔄 Wagmi usage' collapsible: true collapsed: true link: null -position: 13 +position: 14 diff --git a/docs/docs/guides/wallet/_category_.yml b/docs/docs/guides/wallet/_category_.yml index 6b079975e01..dde0bddd9ed 100644 --- a/docs/docs/guides/wallet/_category_.yml +++ b/docs/docs/guides/wallet/_category_.yml @@ -2,4 +2,4 @@ label: '🔑 Wallet and Accounts ' collapsible: true collapsed: true link: null -position: 5 \ No newline at end of file +position: 3 diff --git a/docs/docs/guides/wallet/image.jpeg b/docs/docs/guides/wallet/image.jpeg deleted file mode 100644 index 455abbf9bcf..00000000000 Binary files a/docs/docs/guides/wallet/image.jpeg and /dev/null differ diff --git a/docs/docs/guides/wallet/index.md b/docs/docs/guides/wallet/index.md index 9dd38e750b5..0e8858ad530 100644 --- a/docs/docs/guides/wallet/index.md +++ b/docs/docs/guides/wallet/index.md @@ -1,65 +1,118 @@ --- sidebar_position: 1 -sidebar_label: 'Mastering Wallets & Accounts' +sidebar_label: 'Introduction to Accounts & Wallets' --- -# Wallets and Accounts Overview +# Introduction to Accounts & Wallets +The concept of an [account](https://ethereum.org/en/developers/docs/accounts/) is central to Ethereum and it can be used to refer to two types of entities that are native to Ethereum: externally-owned accounts and contract accounts. This document relates _exclusively_ to **externally-owned accounts**. An externally-owned account is associated with a "[key pair](https://ethereum.org/en/developers/docs/accounts/#externally-owned-accounts-and-key-pairs)", which is a general concept that is related to [public-key cryptography](https://en.wikipedia.org/wiki/Public-key_cryptography). The key pair consists of a private key, which must always be kept secret, and a public key, which is used to derive a public identifier (address) for an account. Ethereum accounts have an [ETH](https://ethereum.org/en/developers/docs/intro-to-ethereum/#eth) balance, which can be [transferred](/guides/wallet/transactions) to other accounts or used to pay for interactions with [smart contracts](/guides/smart_contracts/smart_contracts_guide). Anyone with access to an account's private key has the ability to control that account's ETH balance, so it's important that an account's private key is always kept secret. In addition to the general guidelines for [protecting private keys](https://ethereum.org/en/security/#protect-private-keys/), private keys should never be included in client-side code that can be seen by end users and should never be committed to code repositories. -## Live code editor +In the context of this document, the term "wallet" refers to a collection of accounts and should not be confused with [wallet "applications"](https://ethereum.org/en/wallets/). - +## Accounts -## Introduction +The [`web3-eth-accounts`](/api/web3-eth-accounts) package contains functions to generate Ethereum accounts, sign transactions and data, and more. In Web3.js, the [`Web3Account`](/api/web3-eth-accounts/interface/Web3Account) interface is used to represent an externally-owned account. The following snippet demonstrates using Web3.js to generate a new random account and then using that account to sign a message: -A Web3.js `Wallet` is your main entry point if you want to use a private key directly to do any blockchain operations (transactions), also called `Signer` in other libraries. - -Unlike other libraries where a wallet holds just one account, a Web3.js `Wallet` can handle **multiple accounts**. They each have their private key and address. So, whether those keys are in your computer's memory or protected by MetaMask, the Wallet makes Ethereum tasks secure and simple. - -The `web3-eth-accounts` package contains functions to generate Ethereum accounts and sign transactions and data. - -In Ethereum, a private key is a critical part of the cryptographic key pair used to secure and control ownership of Ethereum addresses. Each Ethereum address has a matching set of public and private keys in a public-key cryptography system. This key pair enables you to own an Ethereum address, manage funds, and initiate transactions. - -Learn more about wallets [here](https://ethereum.org/en/wallets/) - -You can sign and send transactions in different ways. +```js +// generate a new random account +const account = web3.eth.accounts.create(); -- [Local wallet](./local_wallet) **(Highly recommended)** -- [Node Wallet](./node_wallet) **(Deprecated)** +console.log(account); +/* ↳ +{ + address: '0x9E82491d1978217d631a3b467BF912933F54788f', + privateKey: '', + signTransaction: [Function: signTransaction], + sign: [Function: sign], + encrypt: [Function: encrypt] +} +*/ -For each of them you can use [Web3PromiEvent](./promi_event) to catch extra transaction's events. +// use the account to sign a message +const signature = account.sign("Hello, Web3.js!"); +/* ↳ +{ + message: 'Hello, Web3.js!', + messageHash: '0xc0f5f7ee704f1473acbb7959f5f925d787a9aa76dccc1b4914cbe77c09fd68d5', + v: '0x1b', + r: '0x129822b685d4404924a595af66c9cdd6367a57c66ac66e2e10fd9915d4772fbd', + s: '0x62db48d6f5e47fe87c64a0991d6d94d23b6024d5d8335348f6686b8c46edb1e9', + signature: '0x129822b685d4404924a595af66c9cdd6367a57c66ac66e2e10fd9915d4772fbd62db48d6f5e47fe87c64a0991d6d94d23b6024d5d8335348f6686b8c46edb1e91b' +} +*/ +``` -## Wallets vs Accounts +Note that many of these values will change each time the code is executed, since a new account is created each time. -An **account** in web3.js is an `object`, it refers to an individual Ethereum address with its associated public and private keys. While a wallet is a higher-level construct for managing multiple accounts, an individual Ethereum address is considered an account. +In addition to generating new random accounts, the Account package can also be used to load an existing account from its private key, as in the following snippet: -```ts title='Create a new account' -const account = web3.eth.accounts.create(); +```js +// load an existing account from its private key +const account = web3.eth.accounts.privateKeyToAccount(""); -console.log(account) -/* ↳ +console.log(account); +/* ↳ { address: '0x9E82491d1978217d631a3b467BF912933F54788f', - privateKey: '0x4651f9c219fc6401fe0b3f82129467c717012287ccb61950d2a8ede0687857ba', + privateKey: '', signTransaction: [Function: signTransaction], sign: [Function: sign], encrypt: [Function: encrypt] } */ + +// use the account to sign a message +const signature = account.sign("Hello, Web3.js!"); +/* ↳ +{ + message: 'Hello, Web3.js!', + messageHash: '0xc0f5f7ee704f1473acbb7959f5f925d787a9aa76dccc1b4914cbe77c09fd68d5', + v: '0x1b', + r: '0x129822b685d4404924a595af66c9cdd6367a57c66ac66e2e10fd9915d4772fbd', + s: '0x62db48d6f5e47fe87c64a0991d6d94d23b6024d5d8335348f6686b8c46edb1e9', + signature: '0x129822b685d4404924a595af66c9cdd6367a57c66ac66e2e10fd9915d4772fbd62db48d6f5e47fe87c64a0991d6d94d23b6024d5d8335348f6686b8c46edb1e91b' +} +*/ ``` -A **wallet** in web3.js is an `array` that holds multiple Ethereum accounts. It provides a convenient way to manage and interact with a collection of accounts. Think of it as a digital wallet that you use to store and organize your various Ethereum addresses. +### Account Methods -```ts title='Create a new wallet' -//create a wallet with `1` random account -const wallet = web3.eth.accounts.wallet.create(1); +The following is a list of [`Accounts`](/libdocs/Accounts) methods in the `web3.eth.accounts` package with descriptions and example usage: + +- [create](/libdocs/Accounts#create) +- [decrypt](/libdocs/Accounts#decrypt) +- [encrypt](/libdocs/Accounts#encrypt) +- [hashMessage](/libdocs/Accounts#hashMessage) +- [parseAndValidatePrivateKey](/libdocs/Accounts#libdocs/Accounts#parseandvalidateprivatekey) +- [privateKeyToAccount](/libdocs/Accounts#privatekeytoaccount) +- [privateKeyToAddress](/libdocs/Accounts#privatekeytoaddress) +- [privateKeyToPublicKey](/libdocs/Accounts#privatekeytopublickey) +- [recover](/libdocs/Accounts#recover) +- [recoverTransaction](/libdocs/Accounts#recovertransaction) +- [sign](/libdocs/Accounts#sign) +- [signTransaction](/libdocs/Accounts#signtransaction) + +## Wallets + +A Web3.js wallet is a collection of accounts and is represented with the [`Wallet`](/api/web3-eth-accounts/class/Wallet) class. When a wallet is used to track an account, that account is added to an internal context (i.e. [`Web3Context`](/api/web3-core/class/Web3Context/)), which makes it easier to use that account in the future - this is described in more detail in the [transactions tutorial](/guides/wallet/transactions). The following snippet demonstrates creating a wallet with 2 new random accounts and using the second account to sign a message: + +```js +// create a wallet with 2 new random accounts +const wallet = web3.eth.accounts.wallet.create(2); console.log(wallet) -/* ↳ -Wallet(1) [ +/* ↳ +Wallet(2) [ + { + address: '0xaaD0d33dc9800258c1265bdDA47b9266472144F7', + privateKey: '', + signTransaction: [Function: signTransaction], + sign: [Function: sign], + encrypt: [Function: encrypt] + }, { - address: '0xB2D5647C03F36cA54f7d783b6Fa5afED297330d4', - privateKey: '0x7b907534ec13b19c67c2a738fdaa69014298c71f2221d7e5dec280232e996610', + address: '0x359caa845324802C64B97544460F31fba4f9B9ba', + privateKey: '', signTransaction: [Function: signTransaction], sign: [Function: sign], encrypt: [Function: encrypt] @@ -69,133 +122,166 @@ Wallet(1) [ privateKeyToAccount: [Function: privateKeyToAccountWithContext], decrypt: [Function: decryptWithContext] }, - _addressMap: Map(1) { '0xb2d5647c03f36ca54f7d783b6fa5afed297330d4' => 0 }, + _addressMap: Map(2) { + '0xaad0d33dc9800258c1265bdda47b9266472144f7' => 0, + '0x359caa845324802c64b97544460f31fba4f9b9ba' => 1 + }, _defaultKeyName: 'web3js_wallet' ] */ -``` - -## Diagram - -![Diagram wallet and accounts](image.jpeg) - -To learn more about the `accounts` methods, please visit [web3.js accounts API](/libdocs/Accounts) - -To learn more about the `wallet` methods, please visit [web3.js wallet API](/libdocs/Wallet) - -## Sending transactions - -The shortest way to do this, is by creating a `Wallet` directly by adding a private key (the private key must start with '0x' and it must have funds to execute the transaction) - -```ts title='Sending a transaction adding a privateKey' -import { Web3 } from 'web3'; - -const web3 = new Web3('https://ethereum-sepolia.publicnode.com'); -//this will create an array `Wallet` with 1 account with this privateKey -//it will generate automatically a public key for it -//make sure you have funds in this accounts -//highlight-next-line -const wallet = web3.eth.accounts.wallet.add('0x152c39c430806985e4dc16fa1d7d87f90a7a1d0a6b3f17efe5158086815652e5'); - -const _to = '0xc7203efeb54846c149f2c79b715a8927f7334e74'; -const _value = '1'; //1 wei - -//the `from` address in the transaction must match the address stored in our `Wallet` array -//that's why we explicitly access it using `wallet[0].address` to ensure accuracy -const receipt = await web3.eth.sendTransaction({ - from: wallet[0].address, - to: _to, - value: _value, -}); -//if you have more than 1 account, you can change the address by accessing to another account -//e.g, `from: wallet[1].address` - -console.log('Tx receipt:', receipt); +// use the second account in the wallet to sign a message +const signature = wallet[1].sign("Hello, Web3.js!"); +// wallet accounts can also be accessed with the "at" and "get" methods +// wallet.at(1).sign("Hello, Web3.js!") +// wallet.get(1).sign("Hello, Web3.js!") +console.log(signature); /* ↳ -Tx receipt: { - blockHash: '0xa43b43b6e13ba47f2283b4afc15271ba07d1bba0430bd0c430f770ba7c98d054', - blockNumber: 4960689n, - cumulativeGasUsed: 7055436n, - effectiveGasPrice: 51964659212n, - from: '0xa3286628134bad128faeef82f44e99aa64085c94', - gasUsed: 21000n, - logs: [], - logsBloom: '0x00000...00000000', - status: 1n, - to: '0xc7203efeb54846c149f2c79b715a8927f7334e74', - transactionHash: '0xb88f3f300f1a168beb3a687abc2d14c389ac9709f18b768c90792c7faef0de7c', - transactionIndex: 41n, - type: 2n +{ + message: 'Hello, Web3.js!', + messageHash: '0xc0f5f7ee704f1473acbb7959f5f925d787a9aa76dccc1b4914cbe77c09fd68d5', + v: '0x1c', + r: '0xd90fc42ff83fdf0ec6778c1c27f3051439de7844eacf06195c761fece19ed77d', + s: '0x729693156c48d07df9f4970772049dbe24ebce979765f788974a13c318b2834a', + signature: '0xd90fc42ff83fdf0ec6778c1c27f3051439de7844eacf06195c761fece19ed77d729693156c48d07df9f4970772049dbe24ebce979765f788974a13c318b2834a1c' } */ ``` -## Interacting with contracts - -### Writing functions +Note that many of these values will change each time the code is executed, since new accounts are created each time. -To interact with functions that modify or update data in smart contracts(writing-functions), we need to create a `Wallet`. This `Wallet` must holds at least 1 account with the necessary funds to execute these operations on the blockchain. +In addition to generating new random accounts, a wallet can also be used to load an existing account from its private key, as in the following snippet: -```ts title='Interacting with writing-functions of a smart contract' -import { Web3 } from 'web3'; +```js +// create a wallet with a single existing account +const wallet = web3.eth.accounts.wallet.add(""); -const web3 = new Web3('https://ethereum-sepolia.publicnode.com'); - -//create a wallet -//highlight-next-line -const wallet = web3.eth.accounts.wallet.add('0x152c39c430806985e4dc16fa1d7d87f90a7a1d0a6b3f17efe5158086815652e5'); - -//this is how we can access to the first account of the wallet -console.log('Account 1:', wallet[0]); +console.log(wallet); /* ↳ -Account 1: { - address: '0x57CaabD59a5436F0F1b2B191b1d070e58E6449AE', - privateKey: '0x152c39c430806985e4dc16fa1d7d87f90a7a1d0a6b3f17efe5158086815652e5', - ... -} +Wallet(1) [ + { + address: '0xC978F87516152f542dc4D6f64C810B0c206b11A8', + privateKey: '', + signTransaction: [Function: signTransaction], + sign: [Function: sign], + encrypt: [Function: encrypt] + }, + _accountProvider: { + create: [Function: createWithContext], + privateKeyToAccount: [Function: privateKeyToAccountWithContext], + decrypt: [Function: decryptWithContext] + }, + _addressMap: Map(1) { '0xc978f87516152f542dc4d6f64c810b0c206b11a8' => 0 }, + _defaultKeyName: 'web3js_wallet' +] */ +``` -//instantiate the contract -const myContract = new web3.eth.Contract(ABI, CONTRACT_ADDRESS); +New accounts can be added to an existing wallet, as is demonstrated by the following code snippet: -//interact with the contract -//wallet[0].address == '0x57CaabD59a5436F0F1b2B191b1d070e58E6449AE' -//highlight-next-line -const txReceipt = await myContract.methods.doSomething().send({ from: wallet[0].address }); +```js +// create a wallet with a single random accounts +const wallet = web3.eth.accounts.wallet.create(1); -console.log('Transaction receipt:', txReceipt); +console.log(wallet); /* ↳ - Transaction receipt: {...} +Wallet(1) [ + { + address: '0x6680D50C2165e8F1841D9CdaA42C2F1b949a39f2', + privateKey: '', + signTransaction: [Function: signTransaction], + sign: [Function: sign], + encrypt: [Function: encrypt] + }, + _accountProvider: { + create: [Function: createWithContext], + privateKeyToAccount: [Function: privateKeyToAccountWithContext], + decrypt: [Function: decryptWithContext] + }, + _addressMap: Map(1) { '0x6680d50c2165e8f1841d9cdaa42c2f1b949a39f2' => 0 }, + _defaultKeyName: 'web3js_wallet' +] */ -``` - -### Reading functions (view) - -To interact with smart contracts `view public/external returns`, we don't need to instantiate a `Wallet`, we can do it just by instantiating the smart contract and the provider. -```ts title='Interacting with reading-functions of a smart contract' -import { Web3 } from 'web3'; +// add a new account to the wallet with the wallet's "create" method +wallet.create(1); -//instantiate the provider -const web3 = new Web3('https://ethereum-sepolia.publicnode.com'); - -//instantiate the contract -const myContract = new web3.eth.Contract(ABI, CONTRACT_ADDRESS); +console.log(wallet); +/* ↳ +Wallet(2) [ + { + address: '0x6680D50C2165e8F1841D9CdaA42C2F1b949a39f2', + privateKey: '', + signTransaction: [Function: signTransaction], + sign: [Function: sign], + encrypt: [Function: encrypt] + }, + { + address: '0x5eD8a3ED6Bb1f32e4B479380cFAcf43C49a5440A', + privateKey: '', + signTransaction: [Function: signTransaction], + sign: [Function: sign], + encrypt: [Function: encrypt] + }, + _accountProvider: { + create: [Function: createWithContext], + privateKeyToAccount: [Function: privateKeyToAccountWithContext], + decrypt: [Function: decryptWithContext] + }, + _addressMap: Map(2) { + '0x6680d50c2165e8f1841d9cdaa42c2f1b949a39f2' => 0, + '0x5ed8a3ed6bb1f32e4b479380cfacf43c49a5440a' => 1 + }, + _defaultKeyName: 'web3js_wallet' +] +*/ -//call the `view function` in the contract -//highlight-next-line -const result = await myContract.methods.doSomething().call(); +// create a new account and add it to the wallet +const newAccount = web3.eth.accounts.create(); +wallet.add(newAccount); -console.log('Result:', result) +console.log(wallet); /* ↳ - Result: ... +Wallet(3) [ + { + address: '0x6680D50C2165e8F1841D9CdaA42C2F1b949a39f2', + privateKey: '', + signTransaction: [Function: signTransaction], + sign: [Function: sign], + encrypt: [Function: encrypt] + }, + { + address: '0x5eD8a3ED6Bb1f32e4B479380cFAcf43C49a5440A', + privateKey: '', + signTransaction: [Function: signTransaction], + sign: [Function: sign], + encrypt: [Function: encrypt] + }, + { + address: '0x3065Cf410Bd6A10c5FF3Df8f60b82fF5Ee5db18a', + privateKey: '', + signTransaction: [Function: signTransaction], + sign: [Function: sign], + encrypt: [Function: encrypt] + }, + _accountProvider: { + create: [Function: createWithContext], + privateKeyToAccount: [Function: privateKeyToAccountWithContext], + decrypt: [Function: decryptWithContext] + }, + _addressMap: Map(3) { + '0x6680d50c2165e8f1841d9cdaa42c2f1b949a39f2' => 0, + '0x5ed8a3ed6bb1f32e4b479380cfacf43c49a5440a' => 1, + '0x3065cf410bd6a10c5ff3df8f60b82ff5ee5db18a' => 2 + }, + _defaultKeyName: 'web3js_wallet' +] */ ``` -## Wallet methods +### Wallet Methods -The following is a list of `Wallet` [methods](/libdocs/Wallet) in the `web3.eth.accounts.wallet` package with description and example usage: +The following is a list of [`Wallet`](/libdocs/Wallet) methods in the `web3.eth.accounts.wallet` package with description and example usage: - [add](/libdocs/Wallet#add) - [clear](/libdocs/Wallet#clear) @@ -208,20 +294,10 @@ The following is a list of `Wallet` [methods](/libdocs/Wallet) in the `web3.eth. - [save](/libdocs/Wallet#save) - [getStorage](/libdocs/Wallet#getStorage) -## Account methods - -The following is a list of `Accounts` [methods](/libdocs/Wallet) in the `web3.eth.accounts` package with description and example usage: +## Next Steps -- [create](/libdocs/Accounts#create) -- [decrypt](/libdocs/Accounts#decrypt) -- [encrypt](/libdocs/Accounts#encrypt) -- [hashMessage](/libdocs/Accounts#hashMessage) -- [parseAndValidatePrivateKey](/libdocs/Accounts#libdocs/Accounts#parseandvalidateprivatekey) -- [privateKeyToAccount](/libdocs/Accounts#privatekeytoaccount) -- [privateKeyToAddress](/libdocs/Accounts#privatekeytoaddress) -- [privateKeyToPublicKey](/libdocs/Accounts#privatekeytopublickey) -- [recover](/libdocs/Accounts#recover) -- [recoverTransaction](/libdocs/Accounts#recovertransaction) -- [sign](/libdocs/Accounts#sign) -- [signTransaction](/libdocs/Accounts#signtransaction) +This document is just an introduction to Web3.js accounts and wallets. Here are some suggestions for what to review next: +- Learn how to [transfer ETH](/guides/wallet/transactions) from one account to another. +- Build a front-end application that uses [injected accounts](/guides/wallet/metamask) from the MetaMask wallet. +- Use an account to [deploy and interact with a smart contract](/guides/smart_contracts/smart_contracts_guide). diff --git a/docs/docs/guides/getting_started/metamask.md b/docs/docs/guides/wallet/metamask.md similarity index 99% rename from docs/docs/guides/getting_started/metamask.md rename to docs/docs/guides/wallet/metamask.md index e40db544ac6..888f5890b1a 100644 --- a/docs/docs/guides/getting_started/metamask.md +++ b/docs/docs/guides/wallet/metamask.md @@ -1,6 +1,6 @@ --- sidebar_position: 4 -sidebar_label: Connecting to Metamask +sidebar_label: 'Tutorial: Connecting to Metamask' --- # Connecting to Metamask diff --git a/docs/docs/guides/wallet/node_wallet.md b/docs/docs/guides/wallet/node_wallet.md index a4adbbab987..7b70ed655a3 100644 --- a/docs/docs/guides/wallet/node_wallet.md +++ b/docs/docs/guides/wallet/node_wallet.md @@ -1,5 +1,5 @@ --- -sidebar_position: 6 +sidebar_position: 8 sidebar_label: 'Tutorial: Node Wallet' --- diff --git a/docs/docs/guides/wallet/signing.md b/docs/docs/guides/wallet/signing.md index 88184c2a7fa..54b3779cd17 100644 --- a/docs/docs/guides/wallet/signing.md +++ b/docs/docs/guides/wallet/signing.md @@ -1,5 +1,5 @@ --- -sidebar_position: 4 +sidebar_position: 6 sidebar_label: 'Tutorial: Signing operations' --- diff --git a/docs/docs/guides/wallet/transactions.md b/docs/docs/guides/wallet/transactions.md index 5129c5057a4..c86da4c9113 100644 --- a/docs/docs/guides/wallet/transactions.md +++ b/docs/docs/guides/wallet/transactions.md @@ -1,5 +1,5 @@ --- -sidebar_position: 5 +sidebar_position: 7 sidebar_label: 'Tutorial: Sending Transactions' --- diff --git a/docs/docs/guides/web3_modal_guide/_category_.yml b/docs/docs/guides/wallet/web3_modal_guide/_category_.yml similarity index 87% rename from docs/docs/guides/web3_modal_guide/_category_.yml rename to docs/docs/guides/wallet/web3_modal_guide/_category_.yml index 6744910f3b6..19d6fe3a639 100644 --- a/docs/docs/guides/web3_modal_guide/_category_.yml +++ b/docs/docs/guides/wallet/web3_modal_guide/_category_.yml @@ -2,4 +2,4 @@ label: '📱 WalletConnect Tutorial' collapsible: true collapsed: true link: null -position: 14 \ No newline at end of file +position: 5 diff --git a/docs/docs/guides/web3_modal_guide/index.mdx b/docs/docs/guides/wallet/web3_modal_guide/index.mdx similarity index 97% rename from docs/docs/guides/web3_modal_guide/index.mdx rename to docs/docs/guides/wallet/web3_modal_guide/index.mdx index a1bef97eb66..2d8b878196f 100644 --- a/docs/docs/guides/web3_modal_guide/index.mdx +++ b/docs/docs/guides/wallet/web3_modal_guide/index.mdx @@ -104,8 +104,8 @@ const USDTAddress = '0xdac17f958d2ee523a2206206994597c13d831ec7'; function Components() { const { isConnected } = useWeb3ModalAccount() const { walletProvider } = useWeb3ModalProvider() - const [USDTBalance, setUSDTBalance] = useState(0); - const [smartContractName, setSmartContractName] = useState(''); + const [USDTBalance, setUSDTBalance] = useState(0); + const [smartContractName, setSmartContractName] = useState(''); async function getContractInfo() { if (!isConnected) throw Error('not connected'); @@ -126,5 +126,5 @@ function Components() { ``` :::info -- To learn how to set up Web3modal with vue, click [here](/guides/web3_modal_guide/vue). -::: \ No newline at end of file +- To learn how to set up Web3modal with vue, click [here](/guides/wallet/web3_modal_guide/vue). +::: diff --git a/docs/docs/guides/web3_modal_guide/vue.md b/docs/docs/guides/wallet/web3_modal_guide/vue.md similarity index 100% rename from docs/docs/guides/web3_modal_guide/vue.md rename to docs/docs/guides/wallet/web3_modal_guide/vue.md diff --git a/docs/docs/guides/web3_config/_category_.yml b/docs/docs/guides/web3_config/_category_.yml index d1fd0583848..6070480ec67 100644 --- a/docs/docs/guides/web3_config/_category_.yml +++ b/docs/docs/guides/web3_config/_category_.yml @@ -2,4 +2,4 @@ label: '⚙️ Web3 config' collapsible: true collapsed: true link: null -position: 16 \ No newline at end of file +position: 7 diff --git a/docs/docs/guides/web3_plugin_guide/_category_.yml b/docs/docs/guides/web3_plugin_guide/_category_.yml index 42237f7ea38..8ab333571fd 100644 --- a/docs/docs/guides/web3_plugin_guide/_category_.yml +++ b/docs/docs/guides/web3_plugin_guide/_category_.yml @@ -2,4 +2,4 @@ label: '🛠️ Web3 Plugin 🧩' collapsible: true collapsed: true link: null -position: 2 \ No newline at end of file +position: 12 diff --git a/docs/docs/guides/web3_providers_guide/_category_.yml b/docs/docs/guides/web3_providers_guide/_category_.yml index f426dbedf9a..2af69af2ea3 100644 --- a/docs/docs/guides/web3_providers_guide/_category_.yml +++ b/docs/docs/guides/web3_providers_guide/_category_.yml @@ -2,4 +2,4 @@ label: '🔌 Providers' collapsible: true collapsed: true link: null -position: 4 \ No newline at end of file +position: 2 diff --git a/docs/docs/guides/web3_providers_guide/http.md b/docs/docs/guides/web3_providers_guide/http.md deleted file mode 100644 index 06dbc0d8125..00000000000 --- a/docs/docs/guides/web3_providers_guide/http.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -sidebar_position: 4 -sidebar_label: 'Tutorial: HTTP Provider' ---- - -# Tutorial: HTTP Provider - -The HTTP Provider is the simplest and most widely used provider, while the Websocket Provider and IPC Provider offer real-time communication and faster performance, respectively. With these providers, you can connect your web application to the Ethereum network and start building decentralized applications. - -## Prerequisites - -Before we get started, make sure you have a basic understanding of JavaScript and Ethereum. Additionally, we need to set up our environment by installing the following: - -1. **Ganache** - - Ganache is a personal blockchain for Ethereum development that allows you to test how your smart contracts function in real-world scenarios. You can download it from [http://truffleframework.com/ganache](http://truffleframework.com/ganache). - -2. **Node.js** - - Node.js is a JavaScript runtime environment that enables you to run JavaScript on the server-side. You can download it from [https://nodejs.org/en/download/](https://nodejs.org/en/download/). - -3. **npm** - - npm (Node Package Manager) is used to publish and install packages to and from the public npm registry or a private npm registry. You can install it by following the instructions here: [https://docs.npmjs.com/downloading-and-installing-node-js-and-npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). - - Alternatively, you can use **yarn** instead of **npm** by following the instructions here: [https://classic.yarnpkg.com/lang/en/docs/getting-started/](https://classic.yarnpkg.com/lang/en/docs/getting-started/). - -## HTTP Provider - -The HTTP Provider allows you to connect to a publicly available Ethereum node, making it easy and straightforward to communicate with the Ethereum network from your web application. - -To connect to the Ethereum network using the HTTP provider, follow these steps: - -1. Open a command prompt or terminal window and navigate to the directory where you want to create the folder for this example. -2. Create a new folder and navigate to it: - -```bash -mkdir web3-providers-tutorial -cd web3-providers-tutorial -``` - -3. Install web3.js using npm: - -```bash -npm i web3 -``` - -4. Create a new JavaScript file called `web3-http-provider.js` in your code editor. - -5. Copy and paste the following code into your `web3-http-provider.js` file and save it: - -```typescript title='HTTP Provider' -import { Web3 } from 'web3'; - -// Connect to the Ethereum network using the HTTP provider -const ganacheUrl = 'http://localhost:7545'; -const httpProvider = new Web3.providers.HttpProvider(ganacheUrl); -const web3 = new Web3(httpProvider); - -async function main() { - try { - // Get the current block number from the network - const currentBlockNumber = await web3.eth.getBlockNumber(); - console.log('Current block number:', currentBlockNumber); - - // Get the list of accounts in the connected node (e.g., Ganache) - const accounts = await web3.eth.getAccounts(); - - // Send a transaction to the network and wait for the transaction to be mined. - // Note that sending a transaction with Ganache will cause it, in its default configuration, to min a new block. - const transactionReceipt = await web3.eth.sendTransaction({ - from: accounts[0], - to: accounts[1], - value: web3.utils.toWei('0.001', 'ether'), - }); - console.log('Transaction Receipt:', transactionReceipt); - - // Get the updated block number - const updatedBlockNumber = await web3.eth.getBlockNumber(); - console.log('Updated block number:', updatedBlockNumber); - } catch (error) { - console.error('An error occurred:', error); - } -} - -main(); -``` - -6. Ensure that Ganache is running as mentioned in the [Prerequisites](#prerequisites) section. - -7. In the command prompt or terminal window, type `node web3-http-provider.js` and press Enter. This will run your JavaScript file and connect to the Ethereum network using the HTTP provider and Ganache. - -If everything is set up properly, you should see the current block number, the transaction receipt, and the updated block number printed in the console: - -```bash -Current block number: 0n -Transaction Receipt: { - transactionHash: '0x0578672e97d072b4b91773c8bfc710e4f777616398b82b276323408e59d11362', - transactionIndex: 0n, - blockNumber: 1n, - blockHash: '0x348a6706e7cce6547fae2c06b3e8eff1f58e4669aff88f0af7ca250ffdcdeef5', - from: '0x6e599da0bff7a6598ac1224e4985430bf16458a4', - to: '0x6f1df96865d09d21e8f3f9a7fba3b17a11c7c53c', - cumulativeGasUsed: 21000n, - gasUsed: 21000n, - logs: [], - logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', - status: 1n, - effectiveGasPrice: 2000000000n, - type: 0n -} -Updated block number: 1n -``` diff --git a/docs/docs/guides/web3_providers_guide/index.md b/docs/docs/guides/web3_providers_guide/index.md index e3ecc880cb0..726ccd8c229 100644 --- a/docs/docs/guides/web3_providers_guide/index.md +++ b/docs/docs/guides/web3_providers_guide/index.md @@ -3,202 +3,67 @@ sidebar_position: 1 sidebar_label: 'Mastering Providers' --- -# Web3js providers overview +# Web3.js Providers Overview +Providers are services that are responsible for enabling Web3.js connectivity with the Ethereum network. Using a provider to connect your application to an Ethereum node is necessary for querying data, sending transactions, and interacting with smart contracts. This guide will explore the different types of Web3.js providers, how to set them up, and how to use them in an application. -## Live code editor - - - -## Introduction - -web3.js providers are objects responsible for enabling connectivity with the Ethereum network in various ways. Connecting your web application to an Ethereum node is necessary for sending transactions, querying data, and interacting with smart contracts on the network. In this guide, we will explore the different types of providers available in web3.js, how to set them up, and how to use them in your code. - -Connecting to a chain happens through a provider. You can pass the provider to the constructor as in the following example: - -:::tip -If you want to subscribe to live events in the blockchain, you should use [`WebSocket provider`](#websocket-provider) or [`IPC provider`](#ipc-provider) -::: +A provider is typically supplied when constructing a new `Web3` object: ```typescript title='Initialize a provider' import { Web3 } from 'web3'; const web3 = new Web3(/* PROVIDER*/); -//calling any method that interact with the network would use the previous passed provider. +// calling any method that interacts with the network will use the supplied provider await web3.eth.getBlockNumber(); ``` -The created `Web3` instance will use the passed provider to interact with the blockchain network. This interaction happens when sending a request and receiving the response, and possibly when listening to provider events (if the provider support this). +The new `Web3` instance will use the supplied provider to interact with the blockchain network. This interaction happens when sending requests and receiving responses, and possibly when listening to provider events (if the provider supports this). ## Providers Types -web3.js supports several types of providers, each with its own unique features or specific use cases. Here are the main types: +Web3.js supports several types of providers for different use cases. Here are the available types: -1. [HTTP Provider](/api/web3-providers-http/class/HttpProvider) -2. [WebSocket Provider](/api/web3-providers-ws/class/WebSocketProvider) -3. [IPC Provider (for Node.js)](/api/web3-providers-ipc/class/IpcProvider) -4. [Third-party Providers (Compliant with EIP 1193)](https://eips.ethereum.org/EIPS/eip-1193) +1. [HTTP Provider](#http-provider) +2. [WebSocket Provider](#websocket-provider) +3. [IPC Provider (for Node.js)](#ipc-provider) +4. [Injected Providers (Compliant with EIP 1193)](#injected-provider) -A string containing string url for `http`/`https` or `ws`/`wss` protocol. And when a string is passed, an instance of the compatible class above will be created accordingly. ex. WebSocketProvider instance will be created for string containing `ws` or `wss`. And you access this instance by calling `web3.provider` to read the provider and possibly register an event listener. +HTTP and WebSocket providers can be supplied as URL strings. All provider types can be supplied by constructing one of the [`SupportedProviders`](/api/web3/namespace/types#SupportedProviders) types. -:::tip -The passed provider can be either type `string` or one of the [`SupportedProviders`](/api/web3-core#SupportedProviders). And if it is passed as a string, then internally the compatible provider object will be created and used. -::: +Keep reading to learn more about the different types of providers and how to use them. ### HTTP Provider -``` ts title='Initialize HTTP Provider' +HTTP is a request-response protocol and does not support persistent connection, which means that HTTP providers are not suitable for use cases that require real-time [event subscriptions](/guides/events_subscriptions/). + +``` ts title='Initialize an HTTP Provider' import { Web3, HttpProvider } from 'web3'; -//highlight-next-line +// supply an HTTP provider as a URL string +// highlight-next-line const web3 = new Web3('https://mainnet.infura.io/v3/YOUR_INFURA_ID'); await web3.eth.getBlockNumber() // ↳ 18849658n -// or +// OR -//highlight-next-line +// supply an HTTP provider by constructing a new HttpProvider +// highlight-next-line const web3_2 = new Web3(new HttpProvider('https://mainnet.infura.io/v3/YOUR_INFURA_ID')); await web3.eth.getBlockNumber() // ↳ 18849658n ``` -### WebSocket provider - -``` ts title='Initialize WS Provider' -import { Web3, WebSocketProvider } from 'web3'; - -//highlight-next-line -const web3 = new Web3('wss://mainnet.infura.io/ws/v3/YOUR_INFURA_ID'); - -await web3.eth.getBlockNumber(); -// ↳ 18849658n - -// or -//highlight-next-line -const web3_2 = new Web3(new WebSocketProvider('wss://mainnet.infura.io/ws/v3/YOUR_INFURA_ID')); - -await web3.eth.getBlockNumber(); -// ↳ 18849658n -``` - -### IPC Provider - -``` ts title='Initialize IPC Provider' -import { Web3 } from 'web3'; -//highlight-next-line -import { IpcProvider } from 'web3-providers-ipc'; - -//highlight-next-line -const web3 = new Web3(new IpcProvider('/users/myuser/.ethereum/geth.ipc')); - -await web3.eth.getBlockNumber(); -// ↳ 18849658n -``` - -## Providers Priorities - -There are multiple ways to set the provider. - -```ts title='Setting a provider' -web3.setProvider(myProvider); -web3.eth.setProvider(myProvider); -web3.Contract.setProvider(myProvider); -contractInstance.setProvider(myProvider); -``` - -The key rule for setting provider is as follows: - -1. Any provider set on the higher level will be applied to all lower levels. e.g. Any provider set using `web3.setProvider` will also be applied to `web3.eth` object. -2. For contracts `web3.Contract.setProvider` can be used to set provider for **all instances** of contracts created by `web3.eth.Contract`. - ---- - -## Usage Scenarios - -### Local Geth Node - -```typescript title='IPC, HTTP and WS provider' -import { Web3 } from 'web3'; -import { IpcProvider } from 'web3-providers-ipc'; - -//highlight-next-line -//IPC provider -const web3 = new Web3(new IpcProvider('/Users/myuser/Library/Ethereum/geth.ipc'));//mac os path -// on windows the path is: '\\\\.\\pipe\\geth.ipc' -// on linux the path is: '/users/myuser/.ethereum/geth.ipc' - -//highlight-next-line -//HTTP provider -web3.setProvider('http://localhost:8545'); -// or -web3.setProvider(new Web3.providers.HttpProvider('http://localhost:8545')); - -//highlight-next-line -//WebSocket provider -web3.setProvider('ws://localhost:8546'); -// or -web3.setProvider(new Web3.providers.WebsocketProvider('ws://localhost:8546')); -``` - -### Remote Node Provider - -```ts title='Alchemy, Infura, etc' -// like Alchemy (https://www.alchemyapi.io/supernode) -// or infura (https://mainnet.infura.io/v3/your_infura_key) -import { Web3 } from 'web3'; -const web3 = new Web3('https://eth-mainnet.alchemyapi.io/v2/your-api-key'); -``` - -### Injected Provider - -As stated above, the injected provider should be in compliance with [EIP-1193](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1193.md). And it is tested with Ganache provider, Hardhat provider, and Incubed (IN3) as a provider. - -The web3.js 4.x Provider specifications are defined in [web3 base provider](https://github.com/ChainSafe/web3.js/blob/4.x/packages/web3-types/src/web3_base_provider.ts) for Injected Providers. - -```html title='E.g, Metamask' - - -``` - -Note that the above code should be hosted in a web server (that could be a simple local web server), because many browser does not support this feature for static files located on your machine. +#### Configuring HTTP Providers -## Provider Options +HTTP providers can be configured by including an [`HttpProviderOptions`](/api/web3-providers-http/interface/HttpProviderOptions/) object in the [`HttpProvider` constructor](/api/web3-providers-http/class/HttpProvider#constructor). The `HttpProviderOptions` type has a single property, `providerOptions`, which is a standard TypeScript [`RequestInit`](https://microsoft.github.io/PowerBI-JavaScript/interfaces/_node_modules_typedoc_node_modules_typescript_lib_lib_dom_d_.requestinit.html) object. -There are differences in the objects that could be passed in the Provider constructors. - -### HttpProvider - -The options is of type `HttpProviderOptions`, which is an object with a single key named `providerOptions` and its value is an object of type `RequestInit`. -Regarding `RequestInit` see [microsoft's github](https://microsoft.github.io/PowerBI-JavaScript/interfaces/_node_modules_typedoc_node_modules_typescript_lib_lib_dom_d_.requestinit.html). +```ts title='Configuring an HTTP Provider' +import { Web3, HttpProvider } from 'web3'; -```ts title='HTTP Provider example' const httpOptions = { providerOptions: { body: undefined, @@ -207,44 +72,57 @@ const httpOptions = { headers: { 'Content-Type': 'application/json', }, - integrity: 'foo', + integrity: undefined, keepalive: true, method: 'GET', mode: 'same-origin', redirect: 'error', - referrer: 'foo', + referrer: undefined, referrerPolicy: 'same-origin', signal: undefined, window: undefined, } as RequestInit, }; + +const web3 = new Web3(new HttpProvider('https://eth.llamarpc.com', httpOptions)); ``` -### WebSocketProvider +### WebSocket Provider -Use WebSocketProvider to connect to a Node using a WebSocket connection, i.e. over the `ws` or `wss` protocol. +WebSockets support a persistent connection between a client and a server, which means they are suitable for use cases that require real-time [event subscriptions](/guides/events_subscriptions/). -The options object is of type `ClientRequestArgs` or of `ClientOptions`. See [here](https://microsoft.github.io/PowerBI-JavaScript/interfaces/_node_modules__types_node_http_d_._http_.clientrequestargs.html) for `ClientRequestArgs` and [here](https://github.com/websockets/ws) for `ClientOptions`. +``` ts title='Initialize WS Provider' +import { Web3, WebSocketProvider } from 'web3'; -The second option parameter can be given regarding reconnecting. And here is its type: +// supply a WebSocket provider as a URL string +// highlight-next-line +const web3 = new Web3('wss://mainnet.infura.io/ws/v3/YOUR_INFURA_ID'); -```ts title='WebSocket Provider example' -// this is the same options interface used for both WebSocketProvider and IpcProvider -type ReconnectOptions = { - autoReconnect: boolean, // default: `true` - delay: number, // default: `5000` - maxAttempts: number, // default: `5` -}; +await web3.eth.getBlockNumber(); +// ↳ 18849658n + +// OR +// supply a WebSocket provider by constructing a new WebSocketProvider +// highlight-next-line +const web3_2 = new Web3(new WebSocketProvider('wss://mainnet.infura.io/ws/v3/YOUR_INFURA_ID')); + +await web3.eth.getBlockNumber(); +// ↳ 18849658n ``` -```ts title='Instantiation of WebSocket Provider' +#### Configuring WebSocket Providers + +The [`WebSocketProvider` constructor](/api/web3-providers-ws/class/WebSocketProvider#constructor) accepts two optional parameters that can be used to configure the behavior of the `WebSocketProvider`: the first parameter must be of type [`ClientRequestArgs`](https://microsoft.github.io/PowerBI-JavaScript/interfaces/_node_modules__types_node_http_d_._http_.clientrequestargs.html) or of [`ClientOptions`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/e5ee5eae6a592198e469ad9f412bab8d223fcbb6/types/ws/index.d.ts#L243) and the second parameter must be of type [`ReconnectOptions`](/api/web3/namespace/utils#ReconnectOptions). + +```ts title='Configuring a WebSocket Provider' +// include both optional parameters const provider = new WebSocketProvider( `ws://localhost:8545`, { headers: { - // to provide the API key if the Node requires the key to be inside the `headers` for example: - 'x-api-key': '', + // for node services that require an API key in a header + 'x-api-key': '', }, }, { @@ -253,11 +131,8 @@ const provider = new WebSocketProvider( maxAttempts: 10, } ); -``` - -The second and the third parameters are both optional. And, for example, the second parameter could be an empty object or undefined, like in the following example: -```ts title='Instantiation of WebSocket Provider' +// OR include only ReconnectOptions const provider = new WebSocketProvider( `ws://localhost:8545`, {}, @@ -269,72 +144,32 @@ const provider = new WebSocketProvider( ); ``` -Below is an example for the passed options: - -```ts title='WS Provider options example' -let clientOptions: ClientOptions = { - // Useful for credentialed urls, e.g: ws://username:password@localhost:8546 - headers: { - authorization: 'Basic username:password', - }, - maxPayload: 100000000, -}; - -const reconnectOptions: ReconnectOptions = { - autoReconnect: true, - delay: 5000, - maxAttempts: 5, -}; -``` - -### IpcProvider - -The IPC Provider could be used in node.js dapps when running a local node. And it provide the most secure connection. +### IPC Provider -It accepts a second parameter called `socketOptions`. And, its type is `SocketConstructorOpts`. See [here](https://microsoft.github.io/PowerBI-JavaScript/interfaces/_node_modules__types_node_net_d_._net_.socketconstructoropts.html) for full details. And here is its interface: +IPC (inter-process communication) providers offer high-performance local communication and provide a faster alternative to HTTP providers. IPC providers are tailored for efficiency and excel in local environments, and also support real-time [event subscriptions](/guides/events_subscriptions/). -```ts title='IPC Provider options' -// for more check https://microsoft.github.io/PowerBI-JavaScript/interfaces/_node_modules__types_node_net_d_._net_.socketconstructoropts.html -interface SocketConstructorOpts { - fd?: number | undefined; - allowHalfOpen?: boolean | undefined; - readable?: boolean | undefined; - writable?: boolean | undefined; -} -``` +``` ts title='Initialize IPC Provider' +import { Web3 } from 'web3'; +// highlight-next-line +import { IpcProvider } from 'web3-providers-ipc'; -And, the third parameter is called `reconnectOptions` that is of the type `ReconnectOptions`. It can be given to control: auto-reconnecting, delay and max tries attempts. And here its type: +// highlight-next-line +const web3 = new Web3(new IpcProvider('/users/myuser/.ethereum/geth.ipc')); -```ts -// this is the same options interface used for both WebSocketProvider and IpcProvider -type ReconnectOptions = { - autoReconnect: boolean, // default: `true` - delay: number, // default: `5000` - maxAttempts: number, // default: `5` -}; +await web3.eth.getBlockNumber(); +// ↳ 18849658n ``` -Below is an example for the passed options for each version: +#### Configuring IPC Providers -```ts title='Options Example' -let clientOptions: SocketConstructorOpts = { - allowHalfOpen: false; - readable: true; - writable: true; -}; +The [`IpcProvider` constructor](/api/web3-providers-ipc/class/IpcProvider#constructor) accepts two optional parameters that can be used to configure the behavior of the `IpcProvider`: the first parameter must be of type [`SocketConstructorOpts`](https://microsoft.github.io/PowerBI-JavaScript/interfaces/_node_modules__types_node_net_d_._net_.socketconstructoropts.html) and the second parameter must be of type [`ReconnectOptions`](/api/web3/namespace/utils#ReconnectOptions). -const reconnectOptions: ReconnectOptions = { - autoReconnect: true, - delay: 5000, - maxAttempts: 5, -}; -``` -And here is a sample instantiation for the `IpcProvider`: -```ts title='IPC Provider example' +```ts title='Configuring an IPC Provider' +// include both optional parameters const provider = new IpcProvider( - `path.ipc`, + '/Users/myuser/Library/Ethereum/geth.ipc', { writable: false, }, @@ -344,13 +179,10 @@ const provider = new IpcProvider( maxAttempts: 10, } ); -``` - -The second and the third parameters are both optional. And, for example, the second parameter could be an empty object or undefined. -```ts title='IPC Provider example' +// OR include only ReconnectOptions const provider = new IpcProvider( - `path.ipc`, + '/Users/myuser/Library/Ethereum/geth.ipc', {}, { delay: 500, @@ -360,23 +192,83 @@ const provider = new IpcProvider( ); ``` -:::info -This section applies for both `IpcProvider` and `WebSocketProvider`. -::: +### Injected Provider -The error message, for the max reconnect attempts, will contain the value of the variable `maxAttempts` as follows: +Injected providers are supplied by an external third-party, most often a wallet or a web browser that is designed to be used with the Ethereum network. In addition to providing network connectivity, injected providers often supply one or more [accounts](/guides/wallet/). Web3.js supports any injected provider that is compliant with [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193). Injected providers support real-time [event subscriptions](/guides/events_subscriptions/). Continue reading for an [example](#injected-provider-1) of using an injected provider. -`` `Maximum number of reconnect attempts reached! (${maxAttempts})` `` +## Provider Origins -And here is how to catch the error, if max attempts reached when there is auto reconnecting: +A provider may be local to an application (i.e. running on the same machine) or remote (i.e. running on a third-party server). Injected providers are a third alternative that are supplied by an external third-party, most often a wallet or a web browser that is designed to be used with the Ethereum network. Keep reading for more examples that illustrate how to work with local, remote, and injected providers. -```ts title='Error message for reconnect attempts' -provider.on('error', (error) => { - if (error.message.startsWith('Maximum number of reconnect attempts reached!')) { - // the `error.message` will be `Maximum number of reconnect attempts reached! (${maxAttempts})` - // the `maxAttempts` is equal to the provided value by the user, or the default value `5`. - } -}); +### Local Provider + +Local providers can usually be accessed via IPC, HTTP, or WebSocket. The following examples demonstrates using a local Geth node to supply the Web3.js provider. + +```typescript title='IPC, HTTP and WS provider' +import { Web3 } from 'web3'; +import { IpcProvider } from 'web3-providers-ipc'; + +// highlight-next-line +// IPC provider +const web3 = new Web3(new IpcProvider('/Users/myuser/Library/Ethereum/geth.ipc')); +// the path above is for macOS +// on Windows the path is: '\\\\.\\pipe\\geth.ipc' +// on Linux the path is: '/users/myuser/.ethereum/geth.ipc' + +// highlight-next-line +// HTTP provider +web3.setProvider('http://localhost:8545'); +// OR +web3.setProvider(new Web3.providers.HttpProvider('http://localhost:8545')); + +// highlight-next-line +// WebSocket provider +web3.setProvider('ws://localhost:8546'); +// OR +web3.setProvider(new Web3.providers.WebsocketProvider('ws://localhost:8546')); ``` +### Remote Provider + +Services like [Alchemy](https://www.alchemy.com/), [Infura](https://www.infura.io/), and [QuickNode](https://www.quicknode.com/) offer Ethereum node services that can be accessed via HTTP or Websocket. + +```ts title='Alchemy, Infura, etc' +import { Web3 } from 'web3'; +const web3 = new Web3('https://eth-mainnet.alchemyapi.io/v2/your-api-key'); +``` + +### Injected Provider + +Injected providers are supplied by an external third-party, most often a wallet or a web browser that is designed to be used with the Ethereum network. In addition to providing network connectivity, injected providers often supply one or more [accounts](/guides/wallet/). Web3.js supports any injected provider that is compliant with [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) and has been tested with multiple EIP-1193 providers, including [MetaMask](https://docs.metamask.io/wallet/reference/provider-api/), [Hardhat](https://hardhat.org/hardhat-runner/docs/advanced/hardhat-runtime-environment), and [Incubed (IN3)](https://in3.readthedocs.io/en/develop/index.html). +:::note +The following example should be run in a browser with the MetaMask extension installed. +::: + +```html title='E.g, Metamask' + + +``` diff --git a/docs/docs/guides/web3_providers_guide/injected_provider.md b/docs/docs/guides/web3_providers_guide/injected_provider.md deleted file mode 100644 index e6971629c58..00000000000 --- a/docs/docs/guides/web3_providers_guide/injected_provider.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -sidebar_position: 7 -sidebar_label: 'Tutorial: Injected provider' ---- - -# Browser Injected Ethereum Provider - -It is easy to connect to the Ethereum network using an Ethereum browser extension such as MetaMask, or an Ethereum-enabled browser like the browser inside TrustWallet. Because they inject their provider object into the browser's JavaScript context, enabling direct interaction with the Ethereum network from your web application. Moreover, the wallet management is conveniently handled by these extensions or browsers, making it the standard approach for DApp developers to facilitate user interactions with the Ethereum network. - -Technically, you use `window.ethereum` when it is injected by the Ethereum browser extension or the Ethereum-enabled browser. However, before using this provider, you need to check if it is available and then call `enable()` to request access to the user's MetaMask account. - -Before start coding you will need to setup and configure Ganache and MetaMask, if you have not already: - -- Ensure that Ganache is running as mentioned in the [Prerequisites](#prerequisites) section. -- Install the MetaMask extension for your browser. You can download MetaMask from their website: https://metamask.io/. - -Follow these steps to connect to the Ethereum network with MetaMask and web3.js, including the steps to create a local web server using Node.js: - -1. Open a command prompt or terminal window and navigate to where you would like to create the folder for this example. -2. Create a new folder and navigate to it: - -```bash -mkdir web3-browser-injected-providers -cd web3-browser-injected-providers -``` - -3. Use npm to initialize the folder. This will simply create a `package.json` file: - -```bash -npm init -y -``` - -4. Install the Express module and add it to your project's dependencies: - -```bash -npm i express -``` - -5. Create a new HTML file named `index.html` in your code editor (inside `web3-browser-injected-providers`). - -6. Copy and paste the following code into `index.html`, and save it after: - -```html - - - - - Connecting to the Ethereum network with Web3.js and MetaMask - - -

Connecting to the Ethereum network with Web3.js and MetaMask

-
-  You need to approve connecting this website to MetaMask.
-  Click on the MetaMask icon in the browser extension, if it did not show a popup already.
-  
- - - - - -``` - -7. Create a new file called `server.js` (inside `web3-browser-injected-providers`). -8. Copy and paste the following code into `server.js`, and save it after: - -```js -const express = require('express'); -const app = express(); -const path = require('path'); - -app.use(express.static(path.join(__dirname, '.'))); - -app.listen(8097, () => { - console.log('Server started on port 8097'); -}); -``` - -9. Start the Node.js server by executing the following command. This will execute the content of `server.js` which will run the server on port 8097: - -```bash -node server.js -``` - -10. Open your web browser and navigate to `http://localhost:8097/`. MetaMask should ask for your approval to connect to your website. Follow the steps and give your consent. -11. If everything is set up properly, you should be able to connect to the Ethereum network with MetaMask and see the logged account address. - -Note that in the above steps you had created a local web server using Node.js and Express, serving your HTML file from the root directory of your project. You needs this local server because many browser does not allow extensions to inject objects for static files located on your machine. However, you can customize the port number and the root directory if needed. - -Now you can start building your Ethereum application with web3.js and MetaMask! diff --git a/docs/docs/guides/web3_providers_guide/ipc.md b/docs/docs/guides/web3_providers_guide/ipc.md deleted file mode 100644 index 1b31eb44f2b..00000000000 --- a/docs/docs/guides/web3_providers_guide/ipc.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -sidebar_position: 6 -sidebar_label: 'Tutorial: IPC Provider' ---- - -# Tutorial: IPC Provider - -The IPC Provider offers high-performance local communication, providing a swift alternative to the straightforward HTTP Provider. Tailored for efficiency, it excels in local environments, enhancing the speed of your web application's connection to the Ethereum network for decentralized applications. - -## Prerequisites - -Before we get started, make sure you have a basic understanding of JavaScript and Ethereum. Additionally, we need to set up our environment by installing the following: - -1. **Node.js** - - Node.js is a JavaScript runtime environment that enables you to run JavaScript on the server-side. You can download it from [https://nodejs.org/en/download/](https://nodejs.org/en/download/). - -2. **npm** - - npm (Node Package Manager) is used to publish and install packages to and from the public npm registry or a private npm registry. You can install it by following the instructions here: [https://docs.npmjs.com/downloading-and-installing-node-js-and-npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). - - Alternatively, you can use **yarn** instead of **npm** by following the instructions here: [https://classic.yarnpkg.com/lang/en/docs/getting-started/](https://classic.yarnpkg.com/lang/en/docs/getting-started/). - -3. **Geth** (Optional, used only at the IPC provider example) - - Geth (go-ethereum) is an Ethereum execution client meaning it handles transactions, deployment and execution of smart contracts and contains an embedded computer known as the Ethereum Virtual Machine. You can install it by following the instructions here: [https://geth.ethereum.org/docs/getting-started/installing-geth](https://geth.ethereum.org/docs/getting-started/installing-geth) - -## IPC Provider (for Node.js) - -The IPC Provider allows you to connect to an Ethereum node using Inter-Process Communication (IPC) in a Node.js environment. This provider is useful when you have a local Ethereum node running on your machine and want to interact with it using Node.js. - -In the following steps you will run `geth` in development mode and you will run a piece of code that reads the Ethereum accounts and sends a transaction: - -To connect to the Ethereum network using the IPC provider, follow these steps: - -1. Start a `geth` node in development mode by opening a terminal window and navigating to the `geth` executable file. Then, run the following command to create a development chain: - -```bash -geth --dev --ipcpath -``` - -Make sure to replace `` with the desired IPC path. For example: - -```bash -geth --dev --ipcpath /Users/username/Library/Ethereum/geth.ipc -``` - -This will start a `geth` node in development mode with IPC enabled and an IPC path specified. If the command is successful, the `geth` node will be running, and you should see output similar to the following: - -```bash -INFO [12-10|15:10:37.121] IPC endpoint opened url= -INFO [12-10|15:10:37.122] HTTP endpoint opened url=http://localhost:8545 -INFO [12-10|15:10:37.122] WebSocket endpoint opened url=ws://localhost:8546 -INFO [12-10|15:10:37.127] Mapped network port proto=udp extport=0 intport=30303 interface=UPnP(UDP) -``` - -2. Open a command prompt or terminal window and navigate to where you would like to create the folder for this example. -3. Create a new folder and navigate to it: - -```bash -mkdir web3-providers-tutorial -cd web3-providers-tutorial -``` - -4. Install web3.js using npm: - -```bash -npm i web3 -``` - -5. Create a new JavaScript file called `web3-ipc-provider.js` in your code editor. - -6. Copy and paste the following code into your `web3-ipc-provider.js` file and save it: - -```typescript title='IPC Provider' -import { Web3 } from 'web3'; -import { IpcProvider } from 'web3-providers-ipc'; - -// Connect to the Ethereum network using IPC provider -const ipcPath = ''; // Replace with your actual IPC path -const ipcProvider = new IpcProvider(ipcPath); - -const web3 = new Web3(ipcProvider); - -async function main() { - try { - console.log('Does the provider support subscriptions?:', ipcProvider.supportsSubscriptions()); - - // Get the list of accounts in the connected node which is in this case: geth in dev mode. - const accounts = await web3.eth.getAccounts(); - console.log('Accounts:', accounts); - - // Send a transaction to the network - const transactionReceipt = await web3.eth.sendTransaction({ - from: accounts[0], - to: accounts[0], // sending a self-transaction - value: web3.utils.toWei('0.001', 'ether'), - }); - console.log('Transaction Receipt:', transactionReceipt); - } catch (error) { - console.error('An error occurred:', error); - } -} - -main(); -``` - -7. replace `` with the `ipcPath` that you had specified, when starting the `geth` node, in the first step. - -8. Type `node web3-ipc-provider.js` in the command prompt or terminal window and press Enter. This will run your JavaScript file. - -If everything is set up properly, you should see the list of accounts and transaction receipt printed in the console, similar to the following: - -```bash -Do the provider supports subscription?: true -Accounts: [ '0x82333ED0FAA7a883297C4d8e0FDE1E1CFABAeB7D' ] -Transaction Receipt: { - blockHash: '0xd1220a9b6f86083e420da025179593f5aad3732165a687019a89528a4ab2bcd8', - blockNumber: 1n, - cumulativeGasUsed: 21000n, - effectiveGasPrice: 1000000001n, - from: '0x82333ed0faa7a883297c4d8e0fde1e1cfabaeb7d', - gasUsed: 21000n, - logs: [], - logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', - status: 1n, - to: '0x82333ed0faa7a883297c4d8e0fde1e1cfabaeb7d', - transactionHash: '0x76c05df78dc5dbfade0d11322b3cadc894c17efe36851856aca29488b47c3fbd', - transactionIndex: 0n, - type: 0n -} -``` - -Keep in mind that using IPC Provider with `geth` in development mode in a production environment is not recommended as it can pose a security risk. diff --git a/docs/docs/guides/web3_providers_guide/truffle.md b/docs/docs/guides/web3_providers_guide/truffle.md deleted file mode 100644 index ba98c3a3335..00000000000 --- a/docs/docs/guides/web3_providers_guide/truffle.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -sidebar_position: 8 -sidebar_label: 'Tutorial: Third Party Provider' ---- - -# Truffle - -The Truffle HDWallet Provider will be used as an example of a third party provider that is EIP 1193 compatible. - -## Prerequisites - -Before we get started, make sure you have a basic understanding of JavaScript and Ethereum. Additionally, we need to set up our environment by installing the following: - -1. **Ganache** - - Ganache is a personal blockchain for Ethereum development that allows you to test how your smart contracts function in real-world scenarios. You can download it from [http://truffleframework.com/ganache](http://truffleframework.com/ganache). - -2. **Node.js** - - Node.js is a JavaScript runtime environment that enables you to run JavaScript on the server-side. You can download it from [https://nodejs.org/en/download/](https://nodejs.org/en/download/). - -3. **npm** - - npm (Node Package Manager) is used to publish and install packages to and from the public npm registry or a private npm registry. You can install it by following the instructions here: [https://docs.npmjs.com/downloading-and-installing-node-js-and-npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). - - Alternatively, you can use **yarn** instead of **npm** by following the instructions here: [https://classic.yarnpkg.com/lang/en/docs/getting-started/](https://classic.yarnpkg.com/lang/en/docs/getting-started/). - -## Third-party Providers (Compliant with EIP 1193) - -web3.js accepts any provider that is in compliance with [EIP-1193](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1193.md). It has tests written to ensure compatibility with @truffle/hdwallet-provider, Ganache provider, Hardhat provider, and Incubed (IN3) as a provider. The following section, [Browser Injected Ethereum Provider](#browser-injected-ethereum-provider), in this tutorial explains how to use a special case of these third-party providers. - -Here is a step-by-step example and a code snippet to connect your web application to the Ethereum network using `@truffle/hdwallet-provider` as an example of an external provider compliant with EIP 1193. - -1. Open a command prompt or terminal window in a new folder. -2. Type `npm init -y` and press Enter. This will create a `package.json` file in the current directory. -3. Install web3.js and HTTP provider using npm: - -```bash -npm i web3 @truffle/hdwallet-provider bip39 -``` - -4. Create a new JavaScript file, called `index.js`, in your code editor. -5. Copy and paste the following code into your JavaScript file, and then save the file: - -```typescript title='EIP1193 Provider (Truffle)' -import { Web3 } from 'web3'; -import HDWalletProvider from '@truffle/hdwallet-provider'; -import bip39 from 'bip39'; - -const mnemonic: string = bip39.generateMnemonic(); // generates seed phrase -console.log('seed phrase:', mnemonic); - -// Connect to the Ethereum network using an HTTP provider and WalletProvider -const provider: HDWalletProvider = new HDWalletProvider(mnemonic, 'https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID'); -const web3: Web3 = new Web3(provider); - -// Get the current block number from the network -web3.eth - .getBlockNumber() - .then(function (blockNumber: number) { - console.log('Current block number:', blockNumber); - }) - .catch(function (error: any) { - console.log('Error:', error); - }); -``` - -6. Replace `'YOUR_INFURA_PROJECT_ID'` with your own Infura project ID. You can obtain an Infura project ID by signing up for a free account at https://infura.io/register. Alternatively, you can use any other URL that is compatible with HDWalletProvider, such as a local Ganache accessible at `'http://localhost:7545'`. - -7. In the command prompt, run `node index.js` and press Enter. This will execute your JavaScript file and connect to the Ethereum network using HDWalletProvider with Infura. - -If everything is set up properly, you should see the current block number printed in the console similar to the following. - -```bash -seed phrase: remain climb clock valid budget cable tunnel force split level measure repair -Current block number: 17317844n -``` - -:::danger -Your seed phrase gives complete access to your Ethereum account and it should **never** be shared with anyone you don't want to give full access to your account. The seed phrase is `console.log`ed in the code example to show you what it looks like, but you should **never** do this with a seed phrase to an account you plan on using to send real money. -::: - -The sample above connected you to the Ethereum network using truffle HD Wallet-enabled Web3 provider. You can modify it to interact with the network, perform transactions, and read/write data from the Ethereum network. - diff --git a/docs/docs/guides/web3_providers_guide/websocket.md b/docs/docs/guides/web3_providers_guide/websocket.md deleted file mode 100644 index 96288f4e031..00000000000 --- a/docs/docs/guides/web3_providers_guide/websocket.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -sidebar_position: 5 -sidebar_label: 'Tutorial: WebSocket Provider' ---- - -# Tutorial: WebSocket Provider - -The WebSocket Provider provides real-time communication and enhanced performance, offering a dynamic alternative to the simplicity of the HTTP Provider. In comparison to the widely used HTTP Provider, the WebSocket Provider enables your web application to establish a continuous, bidirectional connection, allowing for live updates and faster interactions with the Ethereum network. Incorporate the WebSocket Provider to empower your decentralized applications with real-time capabilities. - -## Prerequisites - -Before we get started, make sure you have a basic understanding of JavaScript and Ethereum. Additionally, we need to set up our environment by installing the following: - -1. **Ganache** - - Ganache is a personal blockchain for Ethereum development that allows you to test how your smart contracts function in real-world scenarios. You can download it from [http://truffleframework.com/ganache](http://truffleframework.com/ganache). - -2. **Node.js** - - Node.js is a JavaScript runtime environment that enables you to run JavaScript on the server-side. You can download it from [https://nodejs.org/en/download/](https://nodejs.org/en/download/). - -3. **npm** - - npm (Node Package Manager) is used to publish and install packages to and from the public npm registry or a private npm registry. You can install it by following the instructions here: [https://docs.npmjs.com/downloading-and-installing-node-js-and-npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). - - Alternatively, you can use **yarn** instead of **npm** by following the instructions here: [https://classic.yarnpkg.com/lang/en/docs/getting-started/](https://classic.yarnpkg.com/lang/en/docs/getting-started/). - -## WebSocket Provider - -WebSocket Provider allows us to communicate with the Ethereum node via WebSocket protocol, which is useful when we want continuous updates on our subscribed items. This provider is ideal for real-time applications that require constant updates from the Ethereum network. - -Follow these steps to connect to the Ethereum network using WebSocket provider: - -:::tip -The first 3 steps are the same as in the previous section. So, you may skip them if you already executed the previous section. -::: - -1. Open a command prompt or terminal window and navigate to where you would like to create the folder for this example. -2. Create a new folder and navigate to it: - -```bash -mkdir web3-providers-tutorial -cd web3-providers-tutorial -``` - -3. Install web3.js using npm: - -```bash -npm i web3 -``` - -4. Create a new JavaScript file called `web3-websocket-provider.js` in your code editor. - -5. Copy and paste the following code into your `web3-websocket-provider.js` file and save it: - -```typescript title='WebSocket Provider' -import { Web3 } from 'web3'; - -// Connect to the Ethereum network using WebSocket provider -const ganacheUrl = 'ws://localhost:8545'; -const wsProvider = new Web3.providers.WebsocketProvider(ganacheUrl); -const web3 = new Web3(wsProvider); - -async function main() { - try { - console.log('Does the provider support subscriptions?:', wsProvider.supportsSubscriptions()); - - // Subscribe to new block headers - const subscription = await web3.eth.subscribe('newBlockHeaders'); - - subscription.on('data', async (blockhead) => { - console.log('New block header: ', blockhead); - - // You do not need the next line if you like to keep being notified for every new block - await subscription.unsubscribe(); - console.log('Unsubscribed from new block headers.'); - }); - subscription.on('error', (error) => console.log('Error when subscribing to New block header: ', error)); - - // Get the list of accounts in the connected node which is in this case: Ganache. - const accounts = await web3.eth.getAccounts(); - // Send a transaction to the network - const transactionReceipt = await web3.eth.sendTransaction({ - from: accounts[0], - to: accounts[1], - value: web3.utils.toWei('0.001', 'ether'), - }); - console.log('Transaction Receipt:', transactionReceipt); - } catch (error) { - console.error(error); - } -} - -main(); -``` - -6. Ensure that Ganache is running as mentioned in the [Prerequisites](#prerequisites) section. - -7. Type `node web3-websocket-provider.js` in the command prompt or terminal window and press Enter. This will run your JavaScript file. - -If everything is set up properly, you should see the new block headers, transaction hash, and pending transaction printed in the console. The unique feature of WebSocket provider highlighted in this example is that it can subscribe to new block headers and pending transactions to get them in real-time. And by running the sample, you will get something similar to this in your console: - -```bash -Do the provider supports subscription?: true -New block header: { - logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', - miner: '0x0000000000000000000000000000000000000000', - difficulty: '0', - totalDifficulty: '0', - extraData: '0x', - gasLimit: 6721975, - gasUsed: 21000, - hash: '0xd315cecf3336640bcd1301930805370b7fe7528c894b931dcf8a3b1c833b68c8', - mixHash: '0x1304070fde1c7bee383f3a59da8bb94d515cbd033b2638046520fb6fb596d827', - nonce: '0x0000000000000000', - number: 40, - parentHash: '0xeb7ce3260911db2596ac843df11dbcbef302e813e1922db413f6f0b2a54d584d', - receiptsRoot: '0xf78dfb743fbd92ade140711c8bbc542b5e307f0ab7984eff35d751969fe57efa', - stateRoot: '0x95e416eec0932e725ec253779a4e28b3d014d05e41e63c3369f5da42d26d1240', - timestamp: 1684165088, - transactionsRoot: '0x8f87380cc7acfb6d10633e10f72567136492cb8301f52a41742eaca9449bb378', - sha3Uncles: '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347', - baseFeePerGas: 4959456, - size: undefined -} -Transaction Receipt: { - transactionHash: '0x0578672e97d072b4b91773c8bfc710e4f777616398b82b276323408e59d11362', - transactionIndex: 0n, - blockNumber: 1n, - blockHash: '0x5c05248fe0fb8f45a8c9b9600904a36c0e5c74dce01495cfc72278c185fe7838', - from: '0x6e599da0bff7a6598ac1224e4985430bf16458a4', - to: '0x6f1df96865d09d21e8f3f9a7fba3b17a11c7c53c', - cumulativeGasUsed: 21000n, - gasUsed: 21000n, - logs: [], - logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', - status: 1n, - effectiveGasPrice: 2000000000n, - type: 0n -} -Unsubscribed from new block headers. -``` diff --git a/docs/docs/guides/web3_upgrade_guide/_category_.yml b/docs/docs/guides/web3_upgrade_guide/_category_.yml index aeab3125b26..8335efbea79 100644 --- a/docs/docs/guides/web3_upgrade_guide/_category_.yml +++ b/docs/docs/guides/web3_upgrade_guide/_category_.yml @@ -2,4 +2,4 @@ label: '⬆️ Upgrading' collapsible: true collapsed: true link: null -position: 11 \ No newline at end of file +position: 13 diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 3cf7c9a8443..2fff620f657 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -41,7 +41,7 @@ const packages = [ /** @type {import('@docusaurus/types').Config} */ const config = { - title: 'web3.js', + title: 'Web3.js', tagline: 'Powerful TypeScript libraries for Ethereum interaction and utility functions', url: 'https://docs.web3js.org', baseUrl: '/', @@ -52,7 +52,7 @@ const config = { // GitHub pages deployment config. // If you aren't using GitHub pages, you don't need these. organizationName: 'ChainSafe', // Usually your GitHub org/user name. - projectName: 'web3.js', // Usually your repo name. + projectName: 'Web3.js', // Usually your repo name. // Even if you don't use internalization, you can use this field to set useful // metadata like html lang. For example, if your site is Chinese, you may want @@ -109,37 +109,36 @@ const config = { /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ ({ navbar: { - title: 'Web3.js Docs', + title: 'Web3.js Docs', logo: { src: 'img/web3js.svg', }, items: [ { - to: '/', - activeBasePath: '/', + to: '/guides/getting_started/quickstart', + activeBasePath: '/guides', label: 'Guides & Tutorials', position: 'left', }, { to: '/libdocs/ABI', - activeBasePath: '/libdocs/', - label: 'Documentation', + activeBasePath: '/libdocs', + label: 'Packages', position: 'left', }, { - to: 'api', // 'api' is the 'out' directory + to: '/api', // 'api' is the 'out' directory label: 'API', position: 'left', }, { - to: 'glossary', - activeBasePath: '/glossary/', + to: '/glossary', + activeBasePath: '/glossary', label: 'Glossary', position: 'left', }, { to: '/web3_playground', - activeBasePath: '/', label: 'Playground', position: 'right', }, diff --git a/packages/web3-core/CHANGELOG.md b/packages/web3-core/CHANGELOG.md index d69e71c8cf9..67ef64ece5b 100644 --- a/packages/web3-core/CHANGELOG.md +++ b/packages/web3-core/CHANGELOG.md @@ -207,7 +207,7 @@ Documentation: - Web3config `contractDataInputFill` has been defaulted to `data`, istead of `input`. (#6622) -## [Unreleased] +## [4.4.0] ### Added @@ -217,3 +217,14 @@ Documentation: - Interface `RequestManagerMiddleware` was changed (#7003) +### Fixed + +- Set a try catch block if processesingError fails (#7022) + +## [4.5.0] + +### Added + +- Now when existing packages are added in web3, will be avalible for plugins via context. (#7088) + +## [Unreleased] \ No newline at end of file diff --git a/packages/web3-core/package.json b/packages/web3-core/package.json index 04fcb06f509..a619cfbbc1c 100644 --- a/packages/web3-core/package.json +++ b/packages/web3-core/package.json @@ -1,6 +1,6 @@ { "name": "web3-core", - "version": "4.3.2", + "version": "4.5.0", "description": "Web3 core tools for sub-packages. This is an internal package.", "main": "./lib/commonjs/index.js", "module": "./lib/esm/index.js", @@ -42,14 +42,14 @@ "test:integration": "jest --config=./test/integration/jest.config.js --passWithNoTests" }, "dependencies": { - "web3-errors": "^1.1.4", + "web3-errors": "^1.2.0", + "web3-eth-accounts": "^4.1.2", "web3-eth-iban": "^4.0.7", - "web3-eth-accounts": "^4.1.0", "web3-providers-http": "^4.1.0", "web3-providers-ws": "^4.0.7", - "web3-types": "^1.3.1", - "web3-utils": "^4.1.0", - "web3-validator": "^2.0.3" + "web3-types": "^1.7.0", + "web3-utils": "^4.3.0", + "web3-validator": "^2.0.6" }, "optionalDependencies": { "web3-providers-ipc": "^4.0.7" diff --git a/packages/web3-core/src/web3_context.ts b/packages/web3-core/src/web3_context.ts index b50f8ba30f7..867f10f2350 100644 --- a/packages/web3-core/src/web3_context.ts +++ b/packages/web3-core/src/web3_context.ts @@ -219,6 +219,9 @@ export class Web3Context< newContextChild.setConfig({ [event.name]: event.newValue }); }); + // @ts-expect-error No index signature with a parameter of type 'string' was found on type 'Web3Context' + this[ContextRef.name] = newContextChild; + return newContextChild; } diff --git a/packages/web3-core/src/web3_request_manager.ts b/packages/web3-core/src/web3_request_manager.ts index 1cbe9377988..21783229a5f 100644 --- a/packages/web3-core/src/web3_request_manager.ts +++ b/packages/web3-core/src/web3_request_manager.ts @@ -429,7 +429,6 @@ export class Web3RequestManager< ) { return this._buildResponse(payload, response, error); } - if (jsonRpc.isBatchRequest(payload) && !Array.isArray(response)) { throw new ResponseError(response, 'Got normal response for a batch request.'); } @@ -438,15 +437,6 @@ export class Web3RequestManager< throw new ResponseError(response, 'Got batch response for a normal request.'); } - if ( - (jsonRpc.isResponseWithError(response) || jsonRpc.isResponseWithResult(response)) && - !jsonRpc.isBatchRequest(payload) - ) { - if (response.id && payload.id !== response.id) { - throw new InvalidResponseError(response); - } - } - throw new ResponseError(response, 'Invalid response'); } diff --git a/packages/web3-core/src/web3_subscriptions.ts b/packages/web3-core/src/web3_subscriptions.ts index 3cf4cee9f50..21cd4fe21fe 100644 --- a/packages/web3-core/src/web3_subscriptions.ts +++ b/packages/web3-core/src/web3_subscriptions.ts @@ -30,7 +30,6 @@ import { Web3APISpec, } from 'web3-types'; import { jsonRpc } from 'web3-utils'; -import { SubscriptionError } from 'web3-errors'; // eslint-disable-next-line import/no-cycle import { Web3SubscriptionManager } from './web3_subscription_manager.js'; @@ -82,16 +81,6 @@ export abstract class Web3Subscription< super(); const { requestManager } = options as { requestManager: Web3RequestManager }; const { subscriptionManager } = options as { subscriptionManager: Web3SubscriptionManager }; - if (requestManager && subscriptionManager) { - throw new SubscriptionError( - 'Only requestManager or subscriptionManager should be provided at Subscription constructor', - ); - } - if (!requestManager && !subscriptionManager) { - throw new SubscriptionError( - 'Either requestManager or subscriptionManager should be provided at Subscription constructor', - ); - } if (requestManager) { // eslint-disable-next-line deprecation/deprecation this._subscriptionManager = new Web3SubscriptionManager(requestManager, {}, true); diff --git a/packages/web3-core/test/unit/fixtures/custom_transaction_type.ts b/packages/web3-core/test/unit/fixtures/custom_transaction_type.ts new file mode 100644 index 00000000000..12e4789a92d --- /dev/null +++ b/packages/web3-core/test/unit/fixtures/custom_transaction_type.ts @@ -0,0 +1,73 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +import { + BaseTransaction, + TxValuesArray, + AccessListEIP2930ValuesArray, + FeeMarketEIP1559ValuesArray, + JsonTx, +} from 'web3-eth-accounts'; + +export class CustomTransactionType extends BaseTransaction { + // eslint-disable-next-line class-methods-use-this + public getUpfrontCost(): bigint { + throw new Error('Method not implemented.'); + } + // eslint-disable-next-line class-methods-use-this + public raw(): TxValuesArray | AccessListEIP2930ValuesArray | FeeMarketEIP1559ValuesArray { + throw new Error('Method not implemented.'); + } + // eslint-disable-next-line class-methods-use-this + public serialize(): Uint8Array { + throw new Error('Method not implemented.'); + } + public getMessageToSign(hashMessage: false): Uint8Array | Uint8Array[]; + public getMessageToSign(hashMessage?: true | undefined): Uint8Array; + // eslint-disable-next-line class-methods-use-this + public getMessageToSign(): Uint8Array | Uint8Array[] { + throw new Error('Method not implemented.'); + } + // eslint-disable-next-line class-methods-use-this + public hash(): Uint8Array { + throw new Error('Method not implemented.'); + } + // eslint-disable-next-line class-methods-use-this + public getMessageToVerifySignature(): Uint8Array { + throw new Error('Method not implemented.'); + } + // eslint-disable-next-line class-methods-use-this + public getSenderPublicKey(): Uint8Array { + throw new Error('Method not implemented.'); + } + // eslint-disable-next-line class-methods-use-this + public toJSON(): JsonTx { + throw new Error('Method not implemented.'); + } + // eslint-disable-next-line class-methods-use-this + protected _processSignature(): unknown { + throw new Error('Method not implemented.'); + } + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility, class-methods-use-this + public errorStr(): string { + throw new Error('Method not implemented.'); + } + // eslint-disable-next-line class-methods-use-this + protected _errorMsg(): string { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/web3-core/test/unit/fixtures/example_subscription.ts b/packages/web3-core/test/unit/fixtures/example_subscription.ts index ee23ce2311e..019d4ccccc9 100644 --- a/packages/web3-core/test/unit/fixtures/example_subscription.ts +++ b/packages/web3-core/test/unit/fixtures/example_subscription.ts @@ -14,7 +14,7 @@ GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ - +import { DataFormat } from 'web3-types'; import { Web3Subscription } from '../../../src'; export class ExampleSubscription extends Web3Subscription< @@ -26,4 +26,7 @@ export class ExampleSubscription extends Web3Subscription< protected _buildSubscriptionParams() { return ['newHeads']; } + public getReturnFormat(): DataFormat { + return this.returnFormat; + } } diff --git a/packages/web3-core/test/unit/fixtures/example_subscription_build.ts b/packages/web3-core/test/unit/fixtures/example_subscription_build.ts new file mode 100644 index 00000000000..88fea953349 --- /dev/null +++ b/packages/web3-core/test/unit/fixtures/example_subscription_build.ts @@ -0,0 +1,28 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ +import { Web3Subscription } from '../../../src'; + +// subscription class that exposes buildSubscriptionParams +export class BuildSubscription extends Web3Subscription< + { data: string }, + { param1: string }, + { eth_subscribe: (newHeads: string) => void } +> { + public buildSubscriptionParams() { + this._buildSubscriptionParams(); + } +} diff --git a/packages/web3-core/test/unit/formatters.test.ts b/packages/web3-core/test/unit/formatters.test.ts index 105fb4fcf45..022d87f65cb 100644 --- a/packages/web3-core/test/unit/formatters.test.ts +++ b/packages/web3-core/test/unit/formatters.test.ts @@ -16,8 +16,9 @@ along with web3.js. If not, see . */ import * as utils from 'web3-utils'; -import { BlockTags } from 'web3-types'; +import { BlockTags, TransactionInput, Filter } from 'web3-types'; import { Iban } from 'web3-eth-iban'; +import { FormatterError } from 'web3-errors'; import { inputAddressFormatter, inputBlockNumberFormatter, @@ -268,6 +269,121 @@ describe('formatters', () => { ); }); + describe('inputCallFormatter', () => { + let txInput: any; + + beforeEach(() => { + jest.spyOn(utils, 'isAddress').mockReturnValue(true); + txInput = { + to: '0xabcd', + }; + }); + + it('should format "to" address if provided', () => { + expect(formatters.inputCallFormatter({ ...txInput, to: '0xABCD' })).toEqual( + expect.objectContaining({ to: '0xabcd' }), + ); + }); + + it('should format "from" if defaultAddress is provided', () => { + expect(formatters.inputCallFormatter({ ...txInput, to: '0xABCD' }, '0xABCDE')).toEqual( + expect.objectContaining({ from: '0xabcde', to: '0xabcd' }), + ); + }); + }); + + describe('inputTransactionFormatter', () => { + let txInput: any; + + beforeEach(() => { + jest.spyOn(utils, 'isAddress').mockReturnValue(true); + txInput = { + to: '0xabcd', + }; + }); + it('should format and populate "from"', () => { + expect( + formatters.inputTransactionFormatter({ ...txInput, to: '0xabcd', from: '0xABCDE' }), + ).toEqual(expect.objectContaining({ from: '0xabcde', to: '0xabcd' })); + }); + + it('should throw an error when from is undefined', () => { + expect(() => formatters.inputTransactionFormatter({ ...txInput })).toThrow( + new FormatterError('The send transactions "from" field must be defined!'), + ); + }); + }); + + describe('outputTransactionFormatter', () => { + it('should correctly format blockNumber from hex to number', () => { + const txInput: TransactionInput = { + to: '0x1234567890abcdef', + from: '0xabcdef1234567890', + gas: '0x123456', + gasPrice: '0x987654321', + nonce: '0x1', + value: '0x9876543210', + blockNumber: '0x123', + transactionIndex: '0x1', + maxFeePerGas: '0x87654321', + maxPriorityFeePerGas: '0x7654321', + type: '0x1', + }; + + const formattedTxOutput = formatters.outputTransactionFormatter(txInput); + // should return the mocked values; + expect(formattedTxOutput.blockNumber).toBe(123); + expect(formattedTxOutput.to).toBe('toChecksumAddress'); + expect(formattedTxOutput.from).toBe('toChecksumAddress'); + expect(formattedTxOutput.gas).toBe(123); + expect(formattedTxOutput.nonce).toBe(123); + expect(formattedTxOutput.transactionIndex).toBe(123); + expect(formattedTxOutput.value).toBe(12345); + expect(formattedTxOutput.maxFeePerGas).toBe(12345); + expect(formattedTxOutput.maxPriorityFeePerGas).toBe(12345); + expect(formattedTxOutput.type).toBe(123); + }); + + it('should make "to" property undefined', () => { + const txInput = { gas: '0x', nonce: '1', value: '0x' }; + const formattedTxOutput = formatters.outputTransactionFormatter(txInput); + + expect(formattedTxOutput.to).toBeUndefined(); + }); + }); + + describe('inputLogFormatter', () => { + beforeAll(() => { + const actualUtils = jest.requireActual('web3-utils'); + jest.spyOn(utils, 'mergeDeep').mockImplementation(actualUtils.mergeDeep); + }); + it('should correctly format a filter with all fields provided', () => { + const filter: Filter = { + fromBlock: '0x1', + toBlock: '0x2', + address: '0x1234567890abcdef1234567890abcdef12345678', + topics: ['0x123', ['0x456', '0x789']], + }; + + const formattedFilter = formatters.inputLogFormatter(filter); + + expect(formattedFilter.fromBlock).toBe('0x1'); + expect(formattedFilter.toBlock).toBe('0x2'); + expect(formattedFilter.address).toBe('0x1234567890abcdef1234567890abcdef12345678'); + expect(formattedFilter.topics).toEqual(['0x123', ['0x456', '0x789']]); + }); + it('should correctly format a filter with no fromBlock', () => { + const filter: Filter = { + address: ['0x123', '0x222'], + }; + + const formattedFilter = formatters.inputLogFormatter(filter); + + expect(formattedFilter.fromBlock).toBe('latest'); + expect(formattedFilter.address).toEqual(['0x123', '0x222']); + }); + }); + describe('outputLogFormatter', () => { it('should set log id from "blockHash", "transactionHash" and "logIndex"', () => { const result = outputLogFormatter({ diff --git a/packages/web3-core/test/unit/web3_config.test.ts b/packages/web3-core/test/unit/web3_config.test.ts index 0391eec851a..bd250d6386a 100644 --- a/packages/web3-core/test/unit/web3_config.test.ts +++ b/packages/web3-core/test/unit/web3_config.test.ts @@ -16,7 +16,8 @@ along with web3.js. If not, see . */ import { toHex } from 'web3-utils'; -import { DEFAULT_RETURN_FORMAT } from 'web3-types'; +import { DEFAULT_RETURN_FORMAT, Hardfork, ValidChains } from 'web3-types'; +import { ConfigHardforkMismatchError, ConfigChainMismatchError } from 'web3-errors'; import { Web3Config, Web3ConfigEvent } from '../../src/web3_config'; class MyConfigObject extends Web3Config {} @@ -46,6 +47,8 @@ const defaultConfig = { defaultTransactionType: '0x2', defaultMaxPriorityFeePerGas: toHex(2500000000), defaultReturnFormat: DEFAULT_RETURN_FORMAT, + transactionBuilder: undefined, + transactionTypeParser: undefined, }; const setValue = { string: 'newValue', @@ -123,6 +126,81 @@ describe('Web3Config', () => { ); }); + it('should throw when val does not match config.defaultCommon.defaultHardFork', () => { + const obj = new MyConfigObject(); + const defaultCommon = { + defaultHardfork: 'london' as Hardfork, + customChain: { + networkId: 1, + chainId: 1, + }, + }; + obj.setConfig({ + defaultCommon, + }); + const newDefaultCommon = { + hardfork: 'berlin' as Hardfork, + customChain: { + networkId: 1, + chainId: 1, + }, + }; + expect(() => { + obj.defaultCommon = newDefaultCommon; + }).toThrow( + new ConfigHardforkMismatchError( + defaultCommon.defaultHardfork, + newDefaultCommon.hardfork, + ), + ); + }); + + it('defaulthardfork should throw when val does not match config.defaultCommon.hardfork', () => { + const obj = new MyConfigObject(); + const defaultCommon = { + hardfork: 'london' as Hardfork, + customChain: { + networkId: 1, + chainId: 1, + }, + }; + const hardfork = 'berlin'; + obj.setConfig({ + defaultCommon, + }); + expect(() => { + obj.defaultHardfork = hardfork; + }).toThrow(new ConfigHardforkMismatchError(defaultCommon.hardfork, hardfork)); + }); + + it('should throw when val does not match config.defaultChain', () => { + const obj = new MyConfigObject(); + const defaultCommon = { + defaultHardfork: 'london' as Hardfork, + customChain: { + networkId: 1, + chainId: 1, + }, + defaultChain: 'mainnet', + }; + obj.setConfig({ + defaultCommon, + }); + const newDefaultCommon = { + hardfork: 'london' as Hardfork, + customChain: { + networkId: 1, + chainId: 1, + }, + baseChain: 'sepolia' as ValidChains, + }; + expect(() => { + obj.defaultCommon = newDefaultCommon; + }).toThrow( + new ConfigChainMismatchError(defaultCommon.defaultChain, newDefaultCommon.baseChain), + ); + }); + it('Updating transactionPollingInterval should update transactionReceiptPollingInterval and transactionConfirmationPollingInterval', () => { const obj = new MyConfigObject(); const configChange = jest.fn(); diff --git a/packages/web3-core/test/unit/web3_context.test.ts b/packages/web3-core/test/unit/web3_context.test.ts index c235a06cb83..b50c98e8661 100644 --- a/packages/web3-core/test/unit/web3_context.test.ts +++ b/packages/web3-core/test/unit/web3_context.test.ts @@ -24,10 +24,14 @@ import { JsonRpcResponse, Web3APIMethod, Web3APIReturnType, + Web3AccountProvider, + Web3BaseWalletAccount, + Web3BaseWallet, } from 'web3-types'; -import { Web3Context, Web3PluginBase } from '../../src/web3_context'; +import { Web3Context, Web3PluginBase, Web3ContextObject } from '../../src/web3_context'; import { Web3RequestManager } from '../../src/web3_request_manager'; import { RequestManagerMiddleware } from '../../src/types'; +import { CustomTransactionType } from './fixtures/custom_transaction_type'; // eslint-disable-next-line @typescript-eslint/ban-types class Context1 extends Web3Context<{}> {} @@ -64,13 +68,30 @@ describe('Web3Context', () => { expect(context.currentProvider).toBeInstanceOf(HttpProvider); }); + it('should return undefined when getting givenprovider', () => { + const context = new Web3Context('http://test.com'); + expect(context.givenProvider).toBeUndefined(); + }); it('should set return current provider for the request manager', () => { const context = new Web3Context('http://test.com'); - context.currentProvider = 'http://test/abc'; expect(context.currentProvider).toBeInstanceOf(HttpProvider); }); + it('should set httpProvider', () => { + const context = new Web3Context(); + const url = 'http://test/abc'; + context.setProvider(url); + const httpProvider = new HttpProvider(url); + expect(context.provider).toEqual(httpProvider); + }); + + it('get batch request', () => { + const context = new Web3Context(); + const BatchRequestClass = context.BatchRequest; + + expect(new BatchRequestClass()).toBeInstanceOf(context.BatchRequest); + }); it('should set middleware for the request manager', () => { const context = new Web3Context('http://test.com'); @@ -92,6 +113,82 @@ describe('Web3Context', () => { context.setRequestManagerMiddleware(middleware); expect(context.requestManager.middleware).toEqual(middleware); }); + it('should instantiate a new Web3Context object with provided context object', () => { + const config = { defaultNetworkId: 'my-network-id', defaultHardfork: 'my-fork' }; + const context = { + provider: 'http://test.com', + config, + } as Web3ContextObject; + const newContext = Web3Context.fromContextObject(context); + expect(newContext.currentProvider).toBeInstanceOf(HttpProvider); + expect(newContext.requestManager).toBeInstanceOf(Web3RequestManager); + expect(newContext.config.defaultHardfork).toEqual(config.defaultHardfork); + expect(newContext.config.defaultNetworkId).toEqual(config.defaultNetworkId); + }); + + describe('accountsProvider', () => { + const createAccountProvider = (): Web3AccountProvider => { + const accountProvider = { + privateKeyToAccount: jest.fn().mockImplementation(() => { + return ''; + }), + decrypt: jest.fn(), + create: jest.fn().mockImplementation(() => { + return ''; + }), + }; + return accountProvider; + }; + const accountProvider = createAccountProvider(); + + class WalletExample< + T extends Web3BaseWalletAccount = Web3BaseWalletAccount, + > extends Web3BaseWallet { + public create = jest.fn(() => this); + public add = jest.fn(() => this); + public get = jest.fn(() => { + return { + address: 'mockAddress', + privateKey: 'mockPrivateKey', + signTransaction: jest.fn(), + sign: jest.fn(), + encrypt: jest.fn(), + } as unknown as T; + }); + public remove = jest.fn(() => true); + public clear = jest.fn(() => this); + public encrypt = jest.fn(async () => Promise.resolve([])); + public decrypt = jest.fn(); + public save = jest.fn(); + public load = jest.fn(); + } + + const wallet = new WalletExample(accountProvider); + it('should set wallet in context', () => { + const context = new Web3Context({ + provider: 'http://test.com', + wallet, + }); + + expect(context.wallet).toEqual(wallet); + }); + it('should set the Accountsprovider when creating new context', () => { + const context = new Web3Context({ + provider: 'http://test.com', + accountProvider, + }); + + expect(context.accountProvider).toEqual(accountProvider); + }); + + it('should set wallet', () => { + const context = new Web3Context({ + provider: 'http://test.com', + wallet, + }); + expect(context.wallet).toEqual(wallet); + }); + }); }); describe('getContextObject', () => { @@ -255,6 +352,20 @@ describe('Web3Context', () => { }); describe('registerPlugin', () => { + it('should create a new type using registerNewTransactionType on a custom plugin', () => { + const context = new Context1('http://test/abc'); + const pluginNamespace = 'plugin'; + + class Plugin extends Web3PluginBase { + public constructor() { + super(); + this.registerNewTransactionType(3, CustomTransactionType); + } + public pluginNamespace = pluginNamespace; + } + + expect(() => context.registerPlugin(new Plugin())).not.toThrow(); + }); it('should throw ExistingPluginNamespaceError', () => { const context = new Context1('http://test/abc'); const pluginNamespace = 'plugin'; diff --git a/packages/web3-core/test/unit/web3_request_manager.test.ts b/packages/web3-core/test/unit/web3_request_manager.test.ts index 2db8c010fd8..9bbad51fdc9 100644 --- a/packages/web3-core/test/unit/web3_request_manager.test.ts +++ b/packages/web3-core/test/unit/web3_request_manager.test.ts @@ -730,6 +730,66 @@ describe('Web3RequestManager', () => { expect(myProvider.request).toHaveBeenCalledTimes(1); expect(myProvider.request).toHaveBeenCalledWith(payload); }); + it('should throw an error when payload batch request and response is not an array', async () => { + jest.spyOn(jsonRpc, 'toPayload').mockReturnValue([ + { method: 'my_method', params: [], id: 1, jsonrpc: '2.0' }, + ]); + const manager = new Web3RequestManager(); + + const pr = new Promise(resolve => { + resolve({ response: 'random' }); + }); + const myProvider = { + request: jest.fn().mockImplementation(async () => pr), + } as any; + manager.setProvider(myProvider); + + await expect(manager.send(request)).rejects.toThrow(); + + jest.clearAllMocks(); + }); + it('should throw an error when payload batch request is an array and response is not', async () => { + jest.spyOn(jsonRpc, 'toPayload').mockReturnValue({ + method: 'my_method', + params: [], + id: 1, + jsonrpc: '2.0', + }); + const manager = new Web3RequestManager(); + + const pr = new Promise(resolve => { + resolve([{ response: 'unknown response' }, { response: 'unknown response' }]); + }); + const myProvider = { + request: jest.fn().mockImplementation(async () => pr), + } as any; + manager.setProvider(myProvider); + + await expect(manager.send(request)).rejects.toThrow(); + + jest.clearAllMocks(); + }); + it('should throw an when invalid response', async () => { + jest.spyOn(jsonRpc, 'toPayload').mockReturnValue({ + method: 'my_method', + params: [], + id: 1, + jsonrpc: '2.0', + }); + const manager = new Web3RequestManager(); + + const pr = new Promise(resolve => { + resolve({ response: 'unknown response' }); + }); + const myProvider = { + request: jest.fn().mockImplementation(async () => pr), + } as any; + manager.setProvider(myProvider); + + await expect(manager.send(request)).rejects.toThrow(); + + jest.clearAllMocks(); + }); }); describe('legacy-request-provider', () => { diff --git a/packages/web3-core/test/unit/web3_subscription.test.ts b/packages/web3-core/test/unit/web3_subscription.test.ts index cab940b0f00..f04a8850af1 100644 --- a/packages/web3-core/test/unit/web3_subscription.test.ts +++ b/packages/web3-core/test/unit/web3_subscription.test.ts @@ -14,9 +14,10 @@ GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ - +import { JsonRpcNotification, Log, DEFAULT_RETURN_FORMAT } from 'web3-types'; import { Web3SubscriptionManager } from '../../src'; import { ExampleSubscription } from './fixtures/example_subscription'; +import { BuildSubscription } from './fixtures/example_subscription_build'; const subscriptions = { example: ExampleSubscription as never }; @@ -81,6 +82,10 @@ describe('Web3Subscription', () => { expect.any(Function), ); }); + + it('should return returnFormat', () => { + expect(sub.getReturnFormat()).toBe(DEFAULT_RETURN_FORMAT); + }); }); describe('unsubscribe', () => { @@ -89,6 +94,9 @@ describe('Web3Subscription', () => { sub['_id'] = 'sub-id'; subscriptionManager.subscriptions.set('sub-id', sub); }); + afterEach(() => { + jest.clearAllMocks(); + }); it('should invoke request manager to unsubscribe', async () => { await sub.unsubscribe(); @@ -105,6 +113,28 @@ describe('Web3Subscription', () => { await sub.unsubscribe(); expect(sub.id).toBeUndefined(); }); + it('should only call removeSubscription once after calling unsubscribe twice', async () => { + jest.spyOn(subscriptionManager, 'removeSubscription').mockImplementation(); + const newSub = new ExampleSubscription({ param1: 'value' }, { subscriptionManager }); + expect(newSub.id).toBeUndefined(); + await newSub.unsubscribe(); + expect(subscriptionManager.removeSubscription).toHaveBeenCalledTimes(0); + }); + }); + + describe('resubscribe', () => { + beforeEach(() => { + sub = new ExampleSubscription({ param1: 'value' }, { subscriptionManager }); + sub['_id'] = 'sub-id'; + subscriptionManager.subscriptions.set('sub-id', sub); + }); + it('should resubscribe', async () => { + jest.spyOn(sub, 'subscribe').mockImplementation(); + jest.spyOn(sub, 'unsubscribe').mockImplementation(); + await sub.resubscribe(); + expect(sub.subscribe).toHaveBeenCalledTimes(1); + expect(sub.unsubscribe).toHaveBeenCalledTimes(1); + }); }); }); @@ -156,6 +186,15 @@ describe('Web3Subscription without subscription manager - (deprecated)', () => { expect.any(Function), ); }); + it('should listen on error', () => { + const error = new Error(''); + const errorSpy = jest.fn(); + sub.on('error', errorSpy); + sub._processSubscriptionError(error); + + expect(errorSpy).toHaveBeenCalledTimes(1); + expect(errorSpy).toHaveBeenCalledWith(error); + }); }); describe('unsubscribe', () => { @@ -181,4 +220,55 @@ describe('Web3Subscription without subscription manager - (deprecated)', () => { expect(sub.id).toBeUndefined(); }); }); + + describe('lastblock', () => { + let subscriptionManager: Web3SubscriptionManager; + beforeEach(() => { + subscriptionManager = new Web3SubscriptionManager(requestManager, subscriptions); + sub = new ExampleSubscription({ param1: 'value' }, { subscriptionManager }); + sub['_id'] = 'sub-id'; + }); + it('should get last block', () => { + expect(sub.lastBlock).toBeUndefined(); + }); + }); + + describe('processSubscriptionData', () => { + let subscriptionManager: Web3SubscriptionManager; + beforeEach(() => { + subscriptionManager = new Web3SubscriptionManager(requestManager, subscriptions); + sub = new ExampleSubscription({ param1: 'value' }, { subscriptionManager }); + sub['_id'] = 'sub-id'; + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('should call _processSubscriptionResult when data is type JsonRpcNotification', () => { + const responseWithNotification: JsonRpcNotification = { + jsonrpc: '2.0', + params: { + subscription: '', + result: { id: '' }, + }, + method: '', + }; + jest.spyOn(sub, '_processSubscriptionResult').mockReturnValue(); + sub.processSubscriptionData(responseWithNotification); + expect(sub._processSubscriptionResult).toHaveBeenCalled(); + }); + }); + + describe('buildSubscriptionParams', () => { + let subscriptionManager: Web3SubscriptionManager; + let buildSub: BuildSubscription; + beforeEach(() => { + buildSub = new BuildSubscription({ param1: 'value' }, { subscriptionManager }); + }); + + it('should _buildSubscriptionParams', () => { + expect(() => { + buildSub.buildSubscriptionParams(); + }).toThrow(); + }); + }); }); diff --git a/packages/web3-core/test/unit/web3_subscription_manager.test.ts b/packages/web3-core/test/unit/web3_subscription_manager.test.ts index 403719b292a..fc671b35a7a 100644 --- a/packages/web3-core/test/unit/web3_subscription_manager.test.ts +++ b/packages/web3-core/test/unit/web3_subscription_manager.test.ts @@ -71,7 +71,6 @@ describe('Web3SubscriptionManager', () => { expect.any(Function), ); }); - it('should register the subscription types', () => { subManager = new Web3SubscriptionManager(requestManager, { example: ExampleSubscription as never, @@ -150,6 +149,12 @@ describe('Web3SubscriptionManager', () => { 'Subscription with id "123" already exists', ); }); + it('should error when there is no sub id', async () => { + (sub as any).id = undefined; + // const subManagers = new Web3SubscriptionManager(requestManager, subscriptions) as any + // // eslint-disable-next-line @typescript-eslint/no-unsafe-call + await expect(subManager.addSubscription(sub)).rejects.toThrow(); + }); it('should try to subscribe the subscription', async () => { sub = new ExampleSubscription( @@ -206,7 +211,18 @@ describe('Web3SubscriptionManager', () => { ); }); - it('should try to unsubscribe the subscription', async () => { + it('should unsubscribe to the subscription by id', async () => { + await subManager.unsubscribe(({ id }) => { + if (id === '123') { + return true; + } + return false; + }); + + expect(subManager.subscriptions).toEqual(new Map()); + }); + + it('should unsubscribe in the subscription under the condition', async () => { await subManager.removeSubscription(sub); expect(sub.sendUnsubscribeRequest).toHaveBeenCalledTimes(1); @@ -221,4 +237,28 @@ describe('Web3SubscriptionManager', () => { expect(subManager.subscriptions).toEqual(new Map()); }); }); + describe('messageListener', () => { + let subscription: ExampleSubscription; + + beforeEach(() => { + subManager = new Web3SubscriptionManager(requestManager, subscriptions); + jest.spyOn(subManager, 'supportsSubscriptions').mockReturnValue(true); + subscription = new ExampleSubscription( + { param1: 'param1' }, + { subscriptionManager: subManager }, + ); + (subscription as any).id = '123'; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + it('should error when no data is provided', () => { + const subManagers = new Web3SubscriptionManager(requestManager, subscriptions) as any; + expect(() => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + subManagers.messageListener(); + }).toThrow(); + }); + }); }); diff --git a/packages/web3-errors/CHANGELOG.md b/packages/web3-errors/CHANGELOG.md index 2acbb60bc4f..7f9b132283b 100644 --- a/packages/web3-errors/CHANGELOG.md +++ b/packages/web3-errors/CHANGELOG.md @@ -166,8 +166,10 @@ Documentation: - Fixed grammar and spelling in `transactionTimeoutHint` (#6559) -## [Unreleased] +## [1.2.0] ### Added -- Added `InvalidIntegerError` error for fromWei and toWei (#7052) \ No newline at end of file +- Added `InvalidIntegerError` error for fromWei and toWei (#7052) + +## [Unreleased] \ No newline at end of file diff --git a/packages/web3-errors/package.json b/packages/web3-errors/package.json index 08a2b5ebb7f..1b8587c81b3 100644 --- a/packages/web3-errors/package.json +++ b/packages/web3-errors/package.json @@ -1,6 +1,6 @@ { "name": "web3-errors", - "version": "1.1.4", + "version": "1.2.0", "description": "This package has web3 error classes", "main": "./lib/commonjs/index.js", "module": "./lib/esm/index.js", @@ -41,7 +41,7 @@ "test:integration": "jest --config=./test/integration/jest.config.js --passWithNoTests" }, "dependencies": { - "web3-types": "^1.3.1" + "web3-types": "^1.6.0" }, "devDependencies": { "@types/jest": "^28.1.6", diff --git a/packages/web3-eth-abi/CHANGELOG.md b/packages/web3-eth-abi/CHANGELOG.md index 47f57f4d2a4..4a7f1fe8add 100644 --- a/packages/web3-eth-abi/CHANGELOG.md +++ b/packages/web3-eth-abi/CHANGELOG.md @@ -170,4 +170,10 @@ Documentation: - Dependencies updated +## [4.2.2] + +### Changed + +- Dependencies updated + ## [Unreleased] \ No newline at end of file diff --git a/packages/web3-eth-abi/package.json b/packages/web3-eth-abi/package.json index 1ed38b13f05..4333ba616bb 100644 --- a/packages/web3-eth-abi/package.json +++ b/packages/web3-eth-abi/package.json @@ -1,6 +1,6 @@ { "name": "web3-eth-abi", - "version": "4.2.1", + "version": "4.2.2", "description": "Web3 module encode and decode EVM in/output.", "main": "./lib/commonjs/index.js", "module": "./lib/esm/index.js", @@ -43,10 +43,10 @@ }, "dependencies": { "abitype": "0.7.1", - "web3-errors": "^1.1.4", + "web3-errors": "^1.2.0", "web3-types": "^1.6.0", - "web3-utils": "^4.2.3", - "web3-validator": "^2.0.5" + "web3-utils": "^4.3.0", + "web3-validator": "^2.0.6" }, "devDependencies": { "@humeris/espresso-shot": "^4.0.0", diff --git a/packages/web3-eth-contract/CHANGELOG.md b/packages/web3-eth-contract/CHANGELOG.md index 8aa7c126ebb..8e275470925 100644 --- a/packages/web3-eth-contract/CHANGELOG.md +++ b/packages/web3-eth-contract/CHANGELOG.md @@ -380,8 +380,10 @@ Documentation: - Added a console warning in case of an ambiguous call to a solidity method with parameter overloading (#6942) - Added contract.deploy(...).decodeData(...) and contract.decodeMethodData(...) that decode data based on the ABI (#6950) -## [Unreleased] +## [4.5.0] ### Added - `defaultReturnFormat` was added to all methods that have `ReturnType` param. (#6947) + +## [Unreleased] \ No newline at end of file diff --git a/packages/web3-eth-contract/package.json b/packages/web3-eth-contract/package.json index 0c8cd166675..74edee8f90b 100644 --- a/packages/web3-eth-contract/package.json +++ b/packages/web3-eth-contract/package.json @@ -1,6 +1,6 @@ { "name": "web3-eth-contract", - "version": "4.4.0", + "version": "4.5.0", "description": "Web3 module to interact with Ethereum smart contracts.", "main": "./lib/commonjs/index.js", "module": "./lib/esm/index.js", @@ -45,13 +45,13 @@ "test:e2e:firefox": "npx cypress run --headless --browser firefox --env grep='ignore',invert=true" }, "dependencies": { - "web3-core": "^4.3.2", - "web3-errors": "^1.1.4", - "web3-eth": "^4.6.0", - "web3-eth-abi": "^4.2.1", + "web3-core": "^4.4.0", + "web3-errors": "^1.2.0", + "web3-eth": "^4.7.0", + "web3-eth-abi": "^4.2.2", "web3-types": "^1.6.0", - "web3-utils": "^4.2.3", - "web3-validator": "^2.0.5" + "web3-utils": "^4.3.0", + "web3-validator": "^2.0.6" }, "devDependencies": { "@humeris/espresso-shot": "^4.0.0", diff --git a/packages/web3-eth-ens/CHANGELOG.md b/packages/web3-eth-ens/CHANGELOG.md index c40b2727d5e..2eb11d79a13 100644 --- a/packages/web3-eth-ens/CHANGELOG.md +++ b/packages/web3-eth-ens/CHANGELOG.md @@ -153,7 +153,21 @@ Documentation: - Added function getText and getName in ENS and resolver classes (#6914) -## [Unreleased] +## [4.3.0] ### Added + - `defaultReturnFormat` was added to all methods that have `ReturnType` param. (#6947) + +## [4.4.0] + +### Added + +- `getText` now supports first param Address +- `getName` has optional second param checkInterfaceSupport + +### Fixed + +- `getName` reverse resolution + +## [Unreleased] \ No newline at end of file diff --git a/packages/web3-eth-ens/package.json b/packages/web3-eth-ens/package.json index db7fc9d2d03..e18474e0140 100644 --- a/packages/web3-eth-ens/package.json +++ b/packages/web3-eth-ens/package.json @@ -1,6 +1,6 @@ { "name": "web3-eth-ens", - "version": "4.2.0", + "version": "4.4.0", "description": "This package has ENS functions for interacting with Ethereum Name Service.", "main": "./lib/commonjs/index.js", "module": "./lib/esm/index.js", @@ -59,13 +59,13 @@ }, "dependencies": { "@adraffy/ens-normalize": "^1.8.8", - "web3-core": "^4.3.2", - "web3-errors": "^1.1.4", - "web3-eth": "^4.5.0", - "web3-eth-contract": "^4.3.0", - "web3-net": "^4.0.7", - "web3-types": "^1.5.0", - "web3-utils": "^4.2.2", - "web3-validator": "^2.0.5" + "web3-core": "^4.5.0", + "web3-errors": "^1.2.0", + "web3-eth": "^4.8.0", + "web3-eth-contract": "^4.5.0", + "web3-net": "^4.1.0", + "web3-types": "^1.7.0", + "web3-utils": "^4.3.0", + "web3-validator": "^2.0.6" } } diff --git a/packages/web3-eth-ens/src/ens.ts b/packages/web3-eth-ens/src/ens.ts index 254a33bc842..8f3e1806127 100644 --- a/packages/web3-eth-ens/src/ens.ts +++ b/packages/web3-eth-ens/src/ens.ts @@ -33,6 +33,7 @@ import { TransactionReceipt, Web3NetAPI, } from 'web3-types'; +import { isAddress } from 'web3-validator'; import { PublicResolverAbi } from './abi/ens/PublicResolver.js'; import { networkIds, registryAddresses } from './config.js'; import { Registry } from './registry.js'; @@ -174,8 +175,10 @@ export class ENS extends Web3Context { * @param key - The key to resolve https://github.com/ethereum/ercs/blob/master/ERCS/erc-634.md#global-keys * @returns - The value content stored in the resolver for the specified key */ - public async getText(ENSName: string, key: string): Promise { - return this._resolver.getText(ENSName, key); + public async getText(ENSNameOrAddr: string | Address, key: string): Promise { + if(isAddress(ENSNameOrAddr)) + return this._resolver.getText(await(this._resolver.getName(ENSNameOrAddr,false)), key); + return this._resolver.getText(ENSNameOrAddr, key); } /** @@ -183,8 +186,8 @@ export class ENS extends Web3Context { * @param ENSName - The node to resolve * @returns - The name */ - public async getName(ENSName: string): Promise { - return this._resolver.getName(ENSName); + public async getName(ENSName: string, checkInterfaceSupport = true): Promise { + return this._resolver.getName(ENSName, checkInterfaceSupport); } /** diff --git a/packages/web3-eth-ens/src/resolver.ts b/packages/web3-eth-ens/src/resolver.ts index c93d150e805..cb345055d95 100644 --- a/packages/web3-eth-ens/src/resolver.ts +++ b/packages/web3-eth-ens/src/resolver.ts @@ -130,12 +130,17 @@ export class Resolver { } public async getName( - address: string + address: string, + checkInterfaceSupport = true ) { - const resolverContract = await this.getResolverContractAdapter(address); - await this.checkInterfaceSupport(resolverContract, methodsInInterface.name); + const reverseName = `${address.toLowerCase().substring(2)}.addr.reverse`; + const resolverContract = await this.getResolverContractAdapter(reverseName); + + if(checkInterfaceSupport) + await this.checkInterfaceSupport(resolverContract, methodsInInterface.name); + return resolverContract.methods - .name(namehash(address)).call() + .name(namehash(reverseName)).call() } } diff --git a/packages/web3-eth-ens/test/unit/resolver.test.ts b/packages/web3-eth-ens/test/unit/resolver.test.ts index f0df74bd2a7..da189e5a45d 100644 --- a/packages/web3-eth-ens/test/unit/resolver.test.ts +++ b/packages/web3-eth-ens/test/unit/resolver.test.ts @@ -239,6 +239,8 @@ describe('resolver', () => { describe('name', () => { it('getName', async () => { + const address = "0x314159265dd8dbb310642f98f50c066173c1259b"; + const supportsInterfaceMock = jest .spyOn(contract.methods, 'supportsInterface') .mockReturnValue({ @@ -255,12 +257,13 @@ describe('resolver', () => { }); }); - await resolver.getName(ENS_NAME); + await resolver.getName(address); expect(supportsInterfaceMock).toHaveBeenCalledWith( interfaceIds[methodsInInterface.name], ); - expect(nameMock).toHaveBeenCalledWith(namehash(ENS_NAME)); + const reverseName = `${address.toLowerCase().substring(2)}.addr.reverse`; + expect(nameMock).toHaveBeenCalledWith(namehash(reverseName)); }) }) diff --git a/packages/web3-eth/CHANGELOG.md b/packages/web3-eth/CHANGELOG.md index 01294d0b449..2b6c0e08005 100644 --- a/packages/web3-eth/CHANGELOG.md +++ b/packages/web3-eth/CHANGELOG.md @@ -232,7 +232,7 @@ Documentation: - method `getBlock` now includes properties of eip 4844, 4895, 4788 when returning block (#6933) - update type `withdrawalsSchema`, `blockSchema` and `blockHeaderSchema` schemas to include properties of eip 4844, 4895, 4788 (#6933) -## [Unreleased] +## [4.7.0] ### Added @@ -247,3 +247,13 @@ Documentation: ### Fixed - Fixed issue with simple transactions, Within `checkRevertBeforeSending` if there is no data set in transaction, set gas to be `21000` (#7043) + +## [4.8.0] + +### Added + +- `sendTransaction` in `rpc_method_wrappers` accepts optional param of `TransactionMiddleware` (#7088) +- WebEth has `setTransactionMiddleware` and `getTransactionMiddleware` for automatically passing to `sentTransaction` (#7088) +- `TransactionMiddleware` and `TransactionMiddleware` data types are exported (#7088) + +## [Unreleased] \ No newline at end of file diff --git a/packages/web3-eth/package.json b/packages/web3-eth/package.json index 767f5697387..336ed74df1e 100644 --- a/packages/web3-eth/package.json +++ b/packages/web3-eth/package.json @@ -1,6 +1,6 @@ { "name": "web3-eth", - "version": "4.6.0", + "version": "4.8.0", "description": "Web3 module to interact with the Ethereum blockchain and smart contracts.", "main": "./lib/commonjs/index.js", "module": "./lib/esm/index.js", @@ -63,15 +63,15 @@ }, "dependencies": { "setimmediate": "^1.0.5", - "web3-core": "^4.3.2", - "web3-errors": "^1.1.4", - "web3-eth-abi": "^4.2.1", + "web3-core": "^4.5.0", + "web3-errors": "^1.2.0", + "web3-eth-abi": "^4.2.2", "web3-eth-accounts": "^4.1.2", - "web3-net": "^4.0.7", + "web3-net": "^4.1.0", "web3-providers-ws": "^4.0.7", - "web3-rpc-methods": "^1.2.0", - "web3-types": "^1.6.0", - "web3-utils": "^4.2.3", - "web3-validator": "^2.0.5" + "web3-rpc-methods": "^1.3.0", + "web3-types": "^1.7.0", + "web3-utils": "^4.3.0", + "web3-validator": "^2.0.6" } } diff --git a/packages/web3-eth/src/rpc_method_wrappers.ts b/packages/web3-eth/src/rpc_method_wrappers.ts index 20e832be892..336aef3a23d 100644 --- a/packages/web3-eth/src/rpc_method_wrappers.ts +++ b/packages/web3-eth/src/rpc_method_wrappers.ts @@ -70,6 +70,7 @@ import { SendSignedTransactionOptions, SendTransactionEvents, SendTransactionOptions, + TransactionMiddleware, } from './types.js'; // eslint-disable-next-line import/no-cycle import { getTransactionFromOrToAttr } from './utils/transaction_builder.js'; @@ -544,13 +545,14 @@ export function sendTransaction< ResolveType = FormatType, >( web3Context: Web3Context, - transaction: + transactionObj: | Transaction | TransactionWithFromLocalWalletIndex | TransactionWithToLocalWalletIndex | TransactionWithFromAndToLocalWalletIndex, returnFormat: ReturnFormat, options: SendTransactionOptions = { checkRevertBeforeSending: true }, + transactionMiddleware?: TransactionMiddleware ): Web3PromiEvent> { const promiEvent = new Web3PromiEvent>( (resolve, reject) => { @@ -563,6 +565,12 @@ export function sendTransaction< returnFormat, }); + let transaction = {...transactionObj}; + + if(!isNullish(transactionMiddleware)){ + transaction = await transactionMiddleware.processTransaction(transaction); + } + let transactionFormatted: | Transaction | TransactionWithFromLocalWalletIndex diff --git a/packages/web3-eth/src/types.ts b/packages/web3-eth/src/types.ts index e1bba319d69..9ed49badb39 100644 --- a/packages/web3-eth/src/types.ts +++ b/packages/web3-eth/src/types.ts @@ -33,6 +33,9 @@ import { Numbers, Transaction, TransactionReceipt, + TransactionWithFromAndToLocalWalletIndex, + TransactionWithFromLocalWalletIndex, + TransactionWithToLocalWalletIndex, } from 'web3-types'; export type InternalTransaction = FormatType; @@ -88,3 +91,17 @@ export interface RevertReasonWithCustomError extends RevertReason { customErrorDecodedSignature: string; customErrorArguments: Record; } + +export type TransactionMiddlewareData = Transaction +| TransactionWithFromLocalWalletIndex +| TransactionWithToLocalWalletIndex +| TransactionWithFromAndToLocalWalletIndex; + +export interface TransactionMiddleware{ + // for transaction processing before signing + processTransaction( + transaction: TransactionMiddlewareData, + options?: { [key: string]: unknown }, + ): Promise; + +} \ No newline at end of file diff --git a/packages/web3-eth/src/web3_eth.ts b/packages/web3-eth/src/web3_eth.ts index 959a7d5eddc..abce5c2c317 100644 --- a/packages/web3-eth/src/web3_eth.ts +++ b/packages/web3-eth/src/web3_eth.ts @@ -48,7 +48,7 @@ import { toChecksumAddress, isNullish, ethUnitMap } from 'web3-utils'; import { ethRpcMethods } from 'web3-rpc-methods'; import * as rpcMethodsWrappers from './rpc_method_wrappers.js'; -import { SendTransactionOptions } from './types.js'; +import { SendTransactionOptions, TransactionMiddleware } from './types.js'; import { LogsSubscription, NewPendingTransactionsSubscription, @@ -99,6 +99,9 @@ export const registeredSubscriptions = { * ``` */ export class Web3Eth extends Web3Context { + + private transactionMiddleware?: TransactionMiddleware; + public constructor( providerOrContext?: SupportedProviders | Web3ContextInitOptions | string, ) { @@ -126,6 +129,14 @@ export class Web3Eth extends Web3Context. +*/ + +export const blockMockResult = { + "jsonrpc": "2.0", + "id": "a40a81fa-1f8b-4bb2-a0ad-eef9b6d4636f", + "result": { + "baseFeePerGas": "0x44dab2983", + "blobGasUsed": "0x20000", + "difficulty": "0x0", + "excessBlobGas": "0x1c0000", + "extraData": "0x407273796e636275696c646572", + "gasLimit": "0x1c9c380", + "gasUsed": "0xb7a086", + "hash": "0xf2b1729965179032b17165678a1a212fa31cb008e30f4011ffe8ebdddbd02b95", + "logsBloom": "0xc3a70590c1c62524173d1892e33888067101934dc0891c2c9a898252b6f320215084a48906452960820188d32bba6fb82ec989018a0268603a00a4c6432a11276c9a038c676938eb68bc436c9905a9a1b08d238fb4458f48498215808bec81112e2a3a54869ff22422a8e491093da8a40f601d198417041cd22f799f9048865006e0b069ab049b852442b310396248088145e2810f230f9a44000c6868bc73e9afa8832a8ac92fd609007ac53c0a9cba0645ce298080184624e8040831dbc331f5e618072407050250021b3210e542781183a612d4618c1244000d421a6ca9c01a57e86a085402c55ab413f840a001e7117894d0469e20c2304a9655e344f60d", + "miner": "0x1f9090aae28b8a3dceadf281b0f12828e676c326", + "mixHash": "0x787ab1d511b72df60a705bb4cfc4e92e2f9d203e3e007ae3a0f757425951ca24", + "nonce": "0x0000000000000000", + "number": "0x131ad16", + "parentBeaconBlockRoot": "0x03bbca9fd0c7a0a020de04287f489112c79bc268220e9ff8e18957cd0d5c3cad", + "parentHash": "0xb1d8fa7b8346421d373a6d4c28575155516cea17c12a3df7201170c9e561b38c", + "receiptsRoot": "0x4ec500bdcd761ad505b2a989156c9a9628058d415acc93d800487c7c76308c59", + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size": "0xcb90", + "stateRoot": "0xafbb8743c0a5f4740e322217cb1f2780ee5c57c32bcd04e9256b09efc1a70568", + "timestamp": "0x6661ab8b", + "totalDifficulty": "0xc70d815d562d3cfa955", + "transactions": [ + "0x589956b75d19dbaf9911f246c23d4b3327ef234872ec1931c419041b23eb5b41", + "0x4d3793f20c25979bd329cafdd889ba6be015cfc999acce8642d6e757b5192e93", + "0x5ba5618ca5a14bab50862255dc69726a498346f9832bd0fd1394e8834e56790b", + "0x6df9678f350db7e30afc930b7246bf1c144b9acb7fd1d25d7e107d550ed5a061", + "0xb8f48ff2876cc393725ea4162421754dfb74ff2364a12d4d3de2c6269f1958c7", + "0x2e5cf7c0607025038b6ccd871dc9ce85af686fd5fa2c82e605198af9afa92cca", + "0x307fb855836feff5d8d0969fa4a44d3c6ae31d335da6577f48f9496d6fe9e0b9", + "0x1362bed1aa8a30d28b7b76c35c2a8601b257058beffa9490dcb20de12bcb15b2", + "0x234c7cc346c204022b2e5ead6d2e8c02317aeb0ec5ca82bd97c2b5d5e59a280b", + ], + "transactionsRoot": "0xc21a4d667b5f841538430b1e2c002c598f2178628ad1d61ea2fda462d1216607", + "uncles": [], + "withdrawals": [ + { + "address": "0xea97dc2523c0479484076660f150833e264c41e9", + "amount": "0x11b6d8c", + "index": "0x2dbe454", + "validatorIndex": "0x10f646" + }, + { + "address": "0xb3e84b6c6409826dc45432b655d8c9489a14a0d7", + "amount": "0x11b4ce2", + "index": "0x2dbe455", + "validatorIndex": "0x10f647" + }, + { + "address": "0x7e2a2fa2a064f693f0a55c5639476d913ff12d05", + "amount": "0x11ad733", + "index": "0x2dbe456", + "validatorIndex": "0x10f648" + }, + + ], + "withdrawalsRoot": "0x2914fa2f5ed93880ed45b58e8f6d14f20c645988400d83c59109964e2053fe1a" + } +}; + +export const receiptMockResult = { + "jsonrpc": "2.0", + "id": 1, + "result": { + "blockHash": "0xf4ad699b98241caf3930779b7d919a77f1727e67cef6ed1ce2a4c655ba812d54", + "blockNumber": "0x131ad35", + // eslint-disable-next-line no-null/no-null + "contractAddress": null, + "cumulativeGasUsed": "0x8cae7a", + "effectiveGasPrice": "0x4c9bc2d65", + "from": "0xab6fd3a7c6ce9db945889cd018e028e055f3bc2e", + "gasUsed": "0xa145", + "logs": [ + { + "address": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "blockHash": "0xf4ad699b98241caf3930779b7d919a77f1727e67cef6ed1ce2a4c655ba812d54", + "blockNumber": "0x131ad35", + "data": "0x000000000000000000000000000000000000000000000000000000000016e360", + "logIndex": "0xdf", + "removed": false, + "topics": [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x000000000000000000000000ab6fd3a7c6ce9db945889cd018e028e055f3bc2e", + "0x00000000000000000000000051112f9f08a2174fe3fc96aad8f07e82d1cccd00" + ], + "transactionHash": "0xdf7756865c2056ce34c4eabe4eff42ad251a9f920a1c620c00b4ea0988731d3f", + "transactionIndex": "0x82" + } + ], + "logsBloom": "0x00000000000000000000000002000000000000000000000000000000004000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000400000000000100000000000000000000000000080000000000000000000040000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000400000000000000000000000", + "status": "0x1", + "to": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "transactionHash": "0xdf7756865c2056ce34c4eabe4eff42ad251a9f920a1c620c00b4ea0988731d3f", + "transactionIndex": "0x82", + "type": "0x2" + } +}; \ No newline at end of file diff --git a/packages/web3-eth/test/unit/rpc_method_wrappers/send_transaction_middleware.test.ts b/packages/web3-eth/test/unit/rpc_method_wrappers/send_transaction_middleware.test.ts new file mode 100644 index 00000000000..63dddbb759f --- /dev/null +++ b/packages/web3-eth/test/unit/rpc_method_wrappers/send_transaction_middleware.test.ts @@ -0,0 +1,98 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +import { Web3Context } from "web3-core"; +import { DEFAULT_RETURN_FORMAT, Web3EthExecutionAPI } from "web3-types"; +import {blockMockResult,receiptMockResult } from "../../fixtures/transactions_data"; +import { TransactionMiddleware, sendTransaction } from "../../../src"; + +const mockTransactionMiddleware: TransactionMiddleware = { + processTransaction: jest.fn(async (transaction) => { + const tx = {...transaction} + tx.data = '0x123'; + return Promise.resolve(tx)} +), +}; + +describe('sendTransaction', () => { + let web3Context: Web3Context; + + beforeEach(() => { + let blockNum = 0; + web3Context = new Web3Context('http://127.0.0.1:8545'); + + web3Context.requestManager.send = jest.fn(async (request) => { + blockNum += 1; + + if(request.method === 'eth_getBlockByNumber'){ + + return Promise.resolve(blockMockResult.result); + } + if(request.method === 'eth_call'){ + + return Promise.resolve("0x"); + } + if(request.method === 'eth_blockNumber'){ + + return Promise.resolve(blockNum.toString(16)); + } + if(request.method === 'eth_sendTransaction'){ + + return Promise.resolve("0xdf7756865c2056ce34c4eabe4eff42ad251a9f920a1c620c00b4ea0988731d3f"); + } + if (request.method === 'eth_getTransactionReceipt') { + return Promise.resolve(receiptMockResult.result); + } + + return Promise.resolve("Unknown Request" as any); + }); + }); + + afterEach(() => jest.resetAllMocks()); + + it('should call processTransaction when transactionMiddleware is provided', async () => { + const transaction = { + from: '0x6E599DA0bfF7A6598AC1224E4985430Bf16458a4', + to: '0x6f1DF96865D09d21e8f3f9a7fbA3b17A11c7C53C', + value: '0x1', + data: '0x1' + }; + + await sendTransaction( + web3Context, + transaction, + DEFAULT_RETURN_FORMAT, + {}, + mockTransactionMiddleware, + ); + + expect(mockTransactionMiddleware.processTransaction).toHaveBeenCalledWith(transaction); + }); + + it('should not call processTransaction when transactionMiddleware is not provided', async () => { + const transaction = { + from: '0x6E599DA0bfF7A6598AC1224E4985430Bf16458a4', + to: '0x6f1DF96865D09d21e8f3f9a7fbA3b17A11c7C53C', + value: '0x1', + data: '0x1' + }; + + await sendTransaction(web3Context, transaction, DEFAULT_RETURN_FORMAT); + + expect(mockTransactionMiddleware.processTransaction).not.toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/packages/web3-eth/test/unit/web3_eth_methods_with_parameters.test.ts b/packages/web3-eth/test/unit/web3_eth_methods_with_parameters.test.ts index c2f017b24cf..76d753bde4b 100644 --- a/packages/web3-eth/test/unit/web3_eth_methods_with_parameters.test.ts +++ b/packages/web3-eth/test/unit/web3_eth_methods_with_parameters.test.ts @@ -17,7 +17,7 @@ along with web3.js. If not, see . import { ethRpcMethods } from 'web3-rpc-methods'; import { TransactionInfoAPI } from 'web3-types'; -import Web3Eth from '../../src/index'; +import Web3Eth, { TransactionMiddleware } from '../../src/index'; import * as rpcMethodWrappers from '../../src/rpc_method_wrappers'; import { getBlockNumberValidData, @@ -63,6 +63,20 @@ describe('web3_eth_methods_with_parameters', () => { web3Eth = new Web3Eth('http://127.0.0.1:8545'); }); + it('should set and unset the transactionMiddleware correctly', () => { + const mockTransactionMiddleware: TransactionMiddleware = { + processTransaction: jest.fn(), + }; + + web3Eth.setTransactionMiddleware(mockTransactionMiddleware); + + expect(web3Eth.getTransactionMiddleware()).toBe(mockTransactionMiddleware); + + web3Eth.setTransactionMiddleware(undefined as any); + + expect(web3Eth.getTransactionMiddleware()).toBeUndefined(); + }); + describe('should call RPC method with expected parameters', () => { describe('only has returnFormat parameter', () => { describe('getHashRate', () => { diff --git a/packages/web3-net/CHANGELOG.md b/packages/web3-net/CHANGELOG.md index d60abde6313..5e7ea9073b3 100644 --- a/packages/web3-net/CHANGELOG.md +++ b/packages/web3-net/CHANGELOG.md @@ -141,7 +141,10 @@ Documentation: - Dependencies updated -## [Unreleased] +## [4.1.0] ### Added + - `defaultReturnFormat` was added to all methods that have `ReturnType` param. (#6947) + +## [Unreleased] \ No newline at end of file diff --git a/packages/web3-net/package.json b/packages/web3-net/package.json index d3ca4f5a32b..952457f72be 100644 --- a/packages/web3-net/package.json +++ b/packages/web3-net/package.json @@ -1,6 +1,6 @@ { "name": "web3-net", - "version": "4.0.7", + "version": "4.1.0", "description": "Web3 module to interact with the Ethereum nodes networking properties.", "main": "./lib/commonjs/index.js", "module": "./lib/esm/index.js", @@ -56,9 +56,9 @@ "typescript": "^4.7.4" }, "dependencies": { - "web3-core": "^4.3.0", - "web3-rpc-methods": "^1.1.3", - "web3-types": "^1.3.0", - "web3-utils": "^4.0.7" + "web3-core": "^4.4.0", + "web3-rpc-methods": "^1.3.0", + "web3-types": "^1.6.0", + "web3-utils": "^4.3.0" } } diff --git a/packages/web3-rpc-methods/CHANGELOG.md b/packages/web3-rpc-methods/CHANGELOG.md index c57997c5ddc..050b6451539 100644 --- a/packages/web3-rpc-methods/CHANGELOG.md +++ b/packages/web3-rpc-methods/CHANGELOG.md @@ -138,8 +138,10 @@ Documentation: - Added `getMaxPriorityFeePerGas` method (#6748) -## [Unreleased] +## [1.3.0] ### Changed - Change `estimateGas` method to add possibility pass Transaction type (#7000) + +## [Unreleased] \ No newline at end of file diff --git a/packages/web3-rpc-methods/package.json b/packages/web3-rpc-methods/package.json index 8e9eec0e597..3a05060f2c2 100644 --- a/packages/web3-rpc-methods/package.json +++ b/packages/web3-rpc-methods/package.json @@ -1,6 +1,6 @@ { "name": "web3-rpc-methods", - "version": "1.2.0", + "version": "1.3.0", "description": "Ethereum RPC methods for Web3 4.x.x", "main": "./lib/commonjs/index.js", "module": "./lib/esm/index.js", @@ -56,8 +56,8 @@ "typescript": "^4.7.4" }, "dependencies": { - "web3-core": "^4.3.2", - "web3-types": "^1.5.0", - "web3-validator": "^2.0.4" + "web3-core": "^4.4.0", + "web3-types": "^1.6.0", + "web3-validator": "^2.0.6" } } diff --git a/packages/web3-rpc-methods/test/unit/eth_rpc_methods/get_maxPriorityFeePerGas.ts b/packages/web3-rpc-methods/test/unit/eth_rpc_methods/get_maxPriorityFeePerGas.test.ts similarity index 100% rename from packages/web3-rpc-methods/test/unit/eth_rpc_methods/get_maxPriorityFeePerGas.ts rename to packages/web3-rpc-methods/test/unit/eth_rpc_methods/get_maxPriorityFeePerGas.test.ts diff --git a/packages/web3-rpc-methods/test/unit/eth_rpc_methods/get_uncle_by_block_number.test.ts b/packages/web3-rpc-methods/test/unit/eth_rpc_methods/get_uncle_by_block_number.test.ts new file mode 100644 index 00000000000..93e8a633582 --- /dev/null +++ b/packages/web3-rpc-methods/test/unit/eth_rpc_methods/get_uncle_by_block_number.test.ts @@ -0,0 +1,42 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +import { Web3RequestManager } from 'web3-core'; +import { ethRpcMethods } from '../../../src/index'; + +jest.mock('web3-validator'); + +describe('getUncleByBlockNumberAndIndex', () => { + let requestManagerSendSpy: jest.Mock; + let requestManager: Web3RequestManager; + + beforeAll(() => { + requestManager = new Web3RequestManager('http://127.0.0.1:8545'); + requestManagerSendSpy = jest.fn(); + requestManager.send = requestManagerSendSpy; + }); + + it('should call requestManager.send with eth_getUncleByBlockNumberAndIndex method', async () => { + await ethRpcMethods.getUncleByBlockNumberAndIndex(requestManager, 0, '1' ); + expect(requestManagerSendSpy).toHaveBeenCalledWith({ + method: 'eth_getUncleByBlockNumberAndIndex', + params: [0,'1'], + }); + }, + ); + +}); diff --git a/packages/web3-rpc-methods/test/unit/personal_rpc_methods/eth_personal.test.ts b/packages/web3-rpc-methods/test/unit/personal_rpc_methods/eth_personal.test.ts new file mode 100644 index 00000000000..a487df07964 --- /dev/null +++ b/packages/web3-rpc-methods/test/unit/personal_rpc_methods/eth_personal.test.ts @@ -0,0 +1,145 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +import { Web3RequestManager } from 'web3-core'; +import { EthPersonalAPI } from 'web3-types'; +import { personalRpcMethods } from '../../../src/index'; + +describe('Eth Personal', () => { + let requestManagerSendSpy: jest.Mock; + let requestManager: Web3RequestManager; + + beforeAll(() => { + requestManager = new Web3RequestManager('http://127.0.0.1:8545'); + requestManagerSendSpy = jest.fn(); + requestManager.send = requestManagerSendSpy; + }); + + it('should call requestManager.send with personal_listAccounts method', async () => { + await personalRpcMethods.getAccounts(requestManager); + expect(requestManagerSendSpy).toHaveBeenCalledWith({ + method: 'personal_listAccounts', + params: [], + }); + }); + + it('should call requestManager.send with personal_newAccount method', async () => { + const pass = "ABC123"; + await personalRpcMethods.newAccount(requestManager, pass); + expect(requestManagerSendSpy).toHaveBeenCalledWith({ + method: 'personal_newAccount', + params: [pass], + }); + }); + + it('should call requestManager.send with personal_unlockAccount method', async () => { + const pass = "ABC123"; + const address = "0x4106486FB42F3Abf07CC07ef5DEE38f60319e789"; + const duration = 100; + await personalRpcMethods.unlockAccount(requestManager, address, pass, duration); + + expect(requestManagerSendSpy).toHaveBeenCalledWith({ + method: 'personal_unlockAccount', + params: [address, pass, duration], + }); + }); + + it('should call requestManager.send with personal_lockAccount method', async () => { + const address = "0x4106486FB42F3Abf07CC07ef5DEE38f60319e789"; + + await personalRpcMethods.lockAccount(requestManager, address ); + + expect(requestManagerSendSpy).toHaveBeenCalledWith({ + method: 'personal_lockAccount', + params: [address], + }); + }); + + it('should call requestManager.send with personal_importRawKey method', async () => { + const passPhrase = "123456"; + const keyData = "abe40cb08850da918ee951b237fa87946499b2d8643e4aa12b0610b050c731f6"; + await personalRpcMethods.importRawKey(requestManager, keyData, passPhrase ); + + expect(requestManagerSendSpy).toHaveBeenCalledWith({ + method: 'personal_importRawKey', + params: [keyData,passPhrase], + }); + }); + + it('should call requestManager.send with personal_sendTransaction method', async () => { + const passPhrase = "123456"; + const tx = { + from: "0x0d4aa485ecbc499c70860feb7e5aaeaf5fd8172e", + gasPrice: "20000", + gas: "21000", + to: "0x4106486FB42F3Abf07CC07ef5DEE38f60319e789", + value: "1000000", + data: "", + nonce: 0, + }; + await personalRpcMethods.sendTransaction(requestManager, tx, passPhrase ); + + expect(requestManagerSendSpy).toHaveBeenCalledWith({ + method: 'personal_sendTransaction', + params: [tx,passPhrase], + }); + }); + + it('should call requestManager.send with personal_signTransaction method', async () => { + const passPhrase = "123456"; + const tx = { + from: "0x0d4aa485ecbc499c70860feb7e5aaeaf5fd8172e", + gasPrice: "20000", + gas: "21000", + to: "0x4106486FB42F3Abf07CC07ef5DEE38f60319e789", + value: "1000000", + data: "", + nonce: 0, + }; + await personalRpcMethods.signTransaction(requestManager, tx, passPhrase ); + + expect(requestManagerSendSpy).toHaveBeenCalledWith({ + method: 'personal_signTransaction', + params: [tx,passPhrase], + }); + }); + + it('should call requestManager.send with personal_sign method', async () => { + const data = "Hello world"; + const address = "0x0D4Aa485ECbC499c70860fEb7e5AaeAf5fd8172E"; + const pass = "123456"; + + await personalRpcMethods.sign(requestManager, data,address,pass); + + expect(requestManagerSendSpy).toHaveBeenCalledWith({ + method: 'personal_sign', + params: [ data,address,pass], + }); + }); + + it('should call requestManager.send with personal_ecRecover method', async () => { + const data = "Hello world"; + const signature = "0x5d21d01b3198ac34d0585a9d76c4d1c8123e5e06746c8962318a1c08ffb207596e6fce4a6f377b7c0fc98c5f646cd73438c80e8a1a95cbec55a84c2889dca0301b"; + + await personalRpcMethods.ecRecover(requestManager,data, signature); + + expect(requestManagerSendSpy).toHaveBeenCalledWith({ + method: 'personal_ecRecover', + params: [ data, signature], + }); + }); +}); diff --git a/packages/web3-rpc-providers/.eslintignore b/packages/web3-rpc-providers/.eslintignore new file mode 120000 index 00000000000..94760d2888d --- /dev/null +++ b/packages/web3-rpc-providers/.eslintignore @@ -0,0 +1 @@ +../../templates/.eslintignore.tmpl \ No newline at end of file diff --git a/packages/web3-rpc-providers/.eslintrc.js b/packages/web3-rpc-providers/.eslintrc.js new file mode 100644 index 00000000000..12a507e5126 --- /dev/null +++ b/packages/web3-rpc-providers/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: '../../.eslintrc.js', + parserOptions: { + project: './tsconfig.esm.json', + tsconfigRootDir: __dirname, + }, +}; diff --git a/packages/web3-rpc-providers/.gitignore b/packages/web3-rpc-providers/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/web3-rpc-providers/.npmignore b/packages/web3-rpc-providers/.npmignore new file mode 120000 index 00000000000..8a0be70f3ed --- /dev/null +++ b/packages/web3-rpc-providers/.npmignore @@ -0,0 +1 @@ +../../templates/.npmignore.tmpl \ No newline at end of file diff --git a/packages/web3-rpc-providers/.npmrc b/packages/web3-rpc-providers/.npmrc new file mode 120000 index 00000000000..5cc817c4313 --- /dev/null +++ b/packages/web3-rpc-providers/.npmrc @@ -0,0 +1 @@ +../../templates/.npmrc.tmpl \ No newline at end of file diff --git a/packages/web3-rpc-providers/.prettierignore b/packages/web3-rpc-providers/.prettierignore new file mode 120000 index 00000000000..044e4a3df69 --- /dev/null +++ b/packages/web3-rpc-providers/.prettierignore @@ -0,0 +1 @@ +../../templates/.prettierignore.tmpl \ No newline at end of file diff --git a/packages/web3-rpc-providers/.prettierrc.json b/packages/web3-rpc-providers/.prettierrc.json new file mode 120000 index 00000000000..00ecd510aaf --- /dev/null +++ b/packages/web3-rpc-providers/.prettierrc.json @@ -0,0 +1 @@ +../../templates/.prettierrc.json.tmpl \ No newline at end of file diff --git a/packages/web3-rpc-providers/CHANGELOG.md b/packages/web3-rpc-providers/CHANGELOG.md new file mode 100644 index 00000000000..1909cc7a509 --- /dev/null +++ b/packages/web3-rpc-providers/CHANGELOG.md @@ -0,0 +1,44 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + + +## [1.0.0.rc.0] + +#### Added + +- RC release + +## [Unreleased] diff --git a/packages/web3-rpc-providers/README.md b/packages/web3-rpc-providers/README.md new file mode 100644 index 00000000000..59284dbb29b --- /dev/null +++ b/packages/web3-rpc-providers/README.md @@ -0,0 +1,58 @@ +

+ web3.js +

+ +# web3.js - Web3 Providers + +![ES Version](https://img.shields.io/badge/ES-2020-yellow) +![Node Version](https://img.shields.io/badge/node-14.x-green) +[![NPM Package][npm-image]][npm-url] +[![Downloads][downloads-image]][npm-url] + +This is a sub-package of [web3.js][repo]. + + +## Installation + +You can install the package either using [NPM](https://www.npmjs.com/package/web3-rpc-providers) or using [Yarn](https://yarnpkg.com/package/web3-rpc-providers) + +### Using NPM + +```bash +npm install web3-rpc-providers +``` + +### Using Yarn + +```bash +yarn add web3-rpc-providers +``` + +## Getting Started + +- :writing_hand: If you have questions [submit an issue](https://github.com/ChainSafe/web3.js/issues/new) or join us on [Discord](https://discord.gg/yjyvFRP) + ![Discord](https://img.shields.io/discord/593655374469660673.svg?label=Discord&logo=discord) + +## Prerequisites + +- :gear: [NodeJS](https://nodejs.org/) (LTS/Fermium) +- :toolbox: [Yarn](https://yarnpkg.com/)/[Lerna](https://lerna.js.org/) + +## Package.json Scripts + +| Script | Description | +| ---------------- | -------------------------------------------------- | +| clean | Uses `rimraf` to remove `dist/` | +| build | Uses `tsc` to build package and dependent packages | +| lint | Uses `eslint` to lint package | +| lint:fix | Uses `eslint` to check and fix any warnings | +| format | Uses `prettier` to format the code | +| test | Uses `jest` to run unit tests | +| test:integration | Uses `jest` to run tests under `/test/integration` | +| test:unit | Uses `jest` to run tests under `/test/unit` | + +[docs]: https://docs.web3js.org/ +[repo]: https://github.com/web3/web3.js/tree/4.x/tools/web3-rpc-providers +[npm-image]: https://img.shields.io/github/package-json/v/web3/web3.js/4.x?filename=tools%2Fweb3-rpc-providers%2Fpackage.json +[npm-url]: https://npmjs.org/package/web3-rpc-providers +[downloads-image]: https://img.shields.io/npm/dm/web3-rpc-providers?label=npm%20downloads diff --git a/packages/web3-rpc-providers/assets/logo/web3js.ai b/packages/web3-rpc-providers/assets/logo/web3js.ai new file mode 100644 index 00000000000..669ef50e7cd --- /dev/null +++ b/packages/web3-rpc-providers/assets/logo/web3js.ai @@ -0,0 +1,1516 @@ +%PDF-1.5 % +1 0 obj <>/OCGs[5 0 R 6 0 R 29 0 R 30 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + Print + + + + + 2017-01-27T15:51:37Z + 2017-01-27T15:51:37Z + 2017-01-27T14:51:33Z + Adobe Illustrator CS6 (Macintosh) + + + + 256 + 240 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgA8AEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A7F+an5hy6Ov6F0qTjqMq VubhTvCjDYL4Ow3r2Hz2IQ8x8teevMXl+cvaXBlgdi01pMS8Tk7k0rUMfFTXCr2Xyl+Z/l/X+FvI 31DUWoPqszCjn/iuTYN8tj7YKSzDArsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirs VdirsVdirsVdirsVdirsVdir5W1nUptU1a71CYkvdSvKa9gx2H0DbJIQeKtqrMwVQWZjRVG5JPYY q+ify70jzPpujKmvXrTu4BgtX+NoFp9lpDuT/k9BgKWV4FdirsVdirsVdirsVdirsVdirsVdirsV U57m2t053EqQp/NIwUfecjKQHM0yhCUjQFpZL5v8qxNxfV7OvQgTxtSnjQnKDrMI/jj83Kj2dqDy xz+RaTzl5TduK6vaV95kX8SRiNbhP8cfmk9m6gf5OXyKZ215aXSc7aeOdP5o2Vx96k5fGcZcjbiz xyhtIEe9VyTB2KuxV2KuxV2KuxV2KuxV2KuxV8lZJDsVejfkt5biv9Zn1a4TlFpoX0ARsZ3rRv8A YKCfmRgKh7fgS7FUDrOuaVotk17qVwtvbqaAtUlmP7KqKsx+WKqunanp+pWqXdhcJc27/ZkjNR8j 4H2OKonFXYq7FXYq7FXYq7FXEgCp2AxVhPmT81tA0pmgsv8AcldrsREwEKn3k3r/ALEHNVqe1seP aPqP2fN3ui7BzZd5+iPnz+X63nWr/mf5u1FmC3X1GE9IrUemR/s93/4bNLm7UzT68I8vxb0un7D0 2P8Ah4j/AEt/s5MXnuLi4kMlxK80h6vIxZj9JzAlIk2TbtYQjEUBQU8iydiq+KaaGQSQu0ci/ZdC VI+RGSEiDYRKIkKIsMq0T8z/ADXpjKslx9fgHWK6q5p7Sfb+8nM/B2pmhzPEPP8AW6jU9h6fLyHA f6P6uT07yv8AmVoGuMlu7fUb9qAW8xHFj4RvsG+Wx9s32l7Tx5dvpl3F5bXdi5sHqHqh3j9IZZmx dO7FXYq7FXYq7FXYq7FXYq+Sskh2Kvb/AMjEUeVrx6fE186k+ywxEf8AEsBUPRsCVO5uIba3luJ2 CQwo0krnoFUVY/QBir5s85ebL3zLrEl3MxW1QlbO2rtHH/zU3Vj/AApkkILQ/MOs6HdC50y5e3fb mo3RwOzofhYYq9g8pfnFpOo8LXWlXTrw7Cev+jufmd4/9lt74KW3oaOrqHQhkYAqwNQQehBwJbxV 2KuxV2Ksf8zeedB8voUupfVvKVWzio0m/Tl2UfPMPVa7Hh5m5dzsdD2Xm1H0io955fteQeaPzD17 Xy8LP9U089LSEkAj/ixur/q9s5vVdo5M23KPd+t7PQ9j4dPvXFPvP6O5i+a92rsVdiqZ2HljzFfo JLPTbiaNvsyLG3A/7Ijj+OZGPS5Z/TEn4OLl12HGalOIPvdqHlrzBpyGS9064giHWVo24D/ZAcfx wZNNkhvKJC4dbhyGoTiT70syhynYq7FXonkT8z7mxkj07W5TNYGixXbVMkPhyPVk/EZutB2oYHhy G49/c832r2HHIDPEKn3dD+17EjpIiyRsHRwGR1NQQdwQRnSg28WQQaLeFDsVdirsVdirsVdir5Ky SHYq9x/I3/lErv8A5j5P+TMOAqHomBLE/wA1LuS28i6kYzRpRHFX2eVQ33rUYQr52wodirI/I/ky 880ap9XRjDZQUe8uaV4qTsq/5TdsVfQ+k6TYaTp8On2MfpWsAoi1JO+5JJ6knIpReKuxVA6xruk6 Pam51K5S3i/ZDbsx8EUfEx+WU5s8MQuRpyNNpcmaXDAWXk/mn83NTvudtoqtYWpqDcGnrsPYjaP6 N/fOe1Xa857Y/SPt/Y9doPZ7Hj9WX1y7un7fxswB3d3LuxZ2NWYmpJPck5pybeiAAFBbgS7FU68u +UNc8wTcbCA+ippJdSfDEvzbufYVOZWm0eTMfSNu/o4Ws7Rxaces793V655W/LHQtFCT3KjUL8UP qygemh/yI9x9JqflnR6XsvHi3Pqk8br+3M2faPoh5c/iWY5s3SuIBFDuDiryb82vKGk2FtFrNii2 0kswhuIF2Ryylg6r2Pw7065zva+jhADJHbd7D2f7RyZJHFP1ACwXmOaF6l2KuxVm/lb81NT0TT4t PmtUvbaEkRFnKSKpNePKjCg7bZtdL2rPFERI4gHRa7sHHnmZiXDI/JnOkfm75XvWWO69XT5TtWVe Udf9dK/iBm2w9sYpbG4uh1Hs9qIbxqY8ufyLMrW7tbuBZ7WZJ4X3WWNgyn5EbZs4zEhYNh0k8coG pCj5quSYOxV2KuxV2KvkrJIdir3H8jf+USu/+Y+T/kzDgKh6JgSwz83v+UFvf+MkH/J1cIUvn3Ch 2Kvoj8rdGi0zydZMFpNfD61M3c+p9j7k44ClluBXYq8288fm5BplxJpuiqtxdxkrPdNvHGw2KqP2 2HfsPfIZYzMTwECXm24JYxMeICY9aeU3+q32qXLXd7cPczv1kc1NPADsPYZxusxZoz/e3fe+i9nZ tPPHWGqHTr8f1ofMRz3YqitN0vUdTultbC3e5nboiCtB4k9APc5ZixSmaiLLVmzwxR4pnhD1Pyt+ T9rb8LrX3FxKKEWUZIiH+u2xb5Db550Gk7HA3ybnueT1/tFKXpwjhH848/h3fjk9Hgggt4UggjWK GMcY40AVVA7ADYZu4xAFDYPMzmZGybJX4WLsVSDzN530Hy+hW7m9S7IqlnFRpD4V7KPdvormHqtd jw/Ud+52Oi7Lzag+kVHvPL9rxfzf5z1LzLdrJcAQ2kVfq9opJVa/tE/tMfHOX1mtlnlZ2A5B7fs7 s2GljQ3keZ/HRj+YbsXYq7FXYq7FUfpGvavo9wJ9Ount3rVgp+BvZkPwt9Iy7DqJ4zcTTj6jSY8w qcQXqvlT827C+KWmtqtldHZbla+gx/yq7x/Tt7jOh0na8Z7ZPSe/p+x5LX+z88fqxeqPd1/a9DBD AEGoO4I6UzcvNuxV2KuxV8lZJDsVe4/kb/yiV3/zHyf8mYcBUPRMCWGfm9/ygt7/AMZIP+Tq4Qpf PuFDsVfT/lD/AJRLRP8AmAtf+TK5FKbYqw780/M82heWmW1bhe37fV4XGxRaVkcfJdh7nCFfPeFD asymoNDkJ44zFSFhniyyxy4omiERBK0jrGFJkYhVCipJOwAAzQ6vsXri+X6j+t6rQe0f8Ocf5w/S P1fJ6R5W/KLUb3hc62zWNsdxbCnrsPfqE+nf2yjS9jylvk9I7urk672hhD04vVLv6fteraRomlaP ai1063S3iFOXEfExHd2O7H550OHBDGKiKeS1GqyZpcUzZR2WuO7FUDq+t6Vo9qbrUblLeL9nkfiY jsij4mPyyrNnhjFyNN+n0uTNLhgLLynzT+buo3vO20RTZWx2Ny1PXYe1Nk+jf3zntV2xKW2P0jv6 /seu0Hs9CHqy+qXd0/a8+kkkkdpJGLu5LM7GpJPUknNMTe5ejAAFBbgS7FU20Lyrr2uOV020aVFN HmNEjX5u1BX265k4NJky/SHE1Wvw4B65V5dfky+D8lNcaMGe/to3/lUO4+8hc2UexMlbyDpZe02I HaMj8kr1r8q/NOmxNPHGl/Cu7G2JZwPeNgrH/Y1zHz9lZoCx6h5OXpu3tPlNEmB8/wBf62HkEEgi hGxBzWu6awK7FWe/l3+YVxpNxFpepyGTSpCEjkY1MBPQg/yeI7dRm37O7ROMiE/o+79jz/bHY4zA 5MY/eD/Zfte1Agio3BzqXhnYq7FXyVkkOxV7j+Rv/KJXf/MfJ/yZhwFQ9EwJYZ+b3/KC3v8Axkg/ 5OrhCl8+4UOxV9P+UP8AlEtE/wCYC1/5MrkUptiryD8+nb6xoyV+EJcED3Jjr+rCEF5ThV2Kvbvy o8h2+n2EOu38YfUbpedqrD+5iYfCQP53G9ew28cBV6PgS7FVk88FvC888ixQxjlJI5CqoHck7DBK QAs7BlCBkaAsl5x5p/OC1t+droCC4lFQb2QERD/UXYt8zt880mr7YA2x7nvem0Hs7KXqzHhH80c/ j3fjk8s1LVNR1O6a6v7h7m4bq7mtB4AdFHsM5/LllkNyNl6zDghijwwHCELlba7FUVp2mahqV0tr YW73Nw3SOMVNPE9gPc5ZjxSmaiLLVmzwxR4pnhD1Hyt+T1vDwutfcTSbEWURogP+W43b5Db55v8A S9jgb5N/J5TX+0Uj6cIofzjz+AekW9vBbQpBbxrDDGKJFGAqqPAAbDN5GIiKGweYnMyNyNlUwsXY q8L/ADZXS182v9RCiX0kN8E6evU1/wBlw41/rXOT7WEPG9Pdv73vuwDk/LDj5X6fd/bbDM1bu3Yq 7FX0R5CuprnyfpcszFpPR4Fj1IjYotfoXO00EjLBEnufNu1oCOpmByv790/zMde7FXyVkkOxV7j+ Rv8AyiV3/wAx8n/JmHAVD0TAlhn5vf8AKC3v/GSD/k6uEKXz7hQ7FX0/5Q/5RLRP+YC1/wCTK5FK bYq8f/Pn/evR/wDjHP8A8STCEF5VhVHaDZpfa5p1lJ/d3V1DC/ykkCn9eKvqcAKAqigGwA6AZFLs VdirHPOfk2HzLaJGbuW2mhqYuJLRE/5cdaH5jfMLW6IZ41ZBHy+Ts+ze0jpZXwiQPz+BeK+YvKGu 6BLxv4D6JNI7qP4om+Tdj7GhzltTo8mE+obd/R7jR9o4tQPQd+7qkuYrnLkR3cIilnY0VQKkk9gB hAtBIAss/wDK35R6nfcLnWmawtTuIBT6ww9wdk+nf2zcaXsic98npH2/sed1/tDjx+nF65d/8P7f xu9X0fQtJ0a1FtptstvF1YjdmPi7H4mPzzocOCGIVEU8jqdVkzS4pmyj8ucd2KoLVta0vSLU3Wo3 KW8I6FjuxHZVHxMfYZVmzQxi5Gg36fTZM0uGAsvKvNP5vahec7bQ0NlbHY3TUM7D/J6hPxPuM5/V dsSltj9I7+v7HrtB7PQh6svql3dP2vO2ZnYsxLMxqzHcknqSc0pL0gFNYFdiqe+VfKGq+Yr1YrZC lorD6zeMPgRe/wDrNTov8My9Jo55pUOXUuBr+0cemhcj6ug7/wBnm+g7Cxt7CygsrZeMFuixxjvx UUFffOyxwEIiI5B85y5ZZJmUuZNq+Ta3Yq+UL62a1vbi2b7UEjxmvijFfbwySFHFXuP5G/8AKJXf /MfJ/wAmYcBUPRMCWGfm9/ygt7/xkg/5OrhCl8+4UOxV9P8AlD/lEtE/5gLX/kyuRSm2KvH/AM+f 969H/wCMc/8AxJMIQXlWFU28of8AKW6J/wAx9r/yeXFX0/kUuxV2KuxVZPBBcQvBPGssMg4yRuAy sD2IOxwSiCKO4ZQmYmwaIee+YPyc027uBPpFx9QDt+9gcGSMA9Sm4Yf6tafLNNqOxoyNwPC9HpPa OcI1kHH58j8WSeWfI2g+X0DW0XrXlKNeS0aT349lHyzO0ugx4eQuXe6zW9qZtQfUaj3Dl+1kOZjr XYqsmmhgieaZ1iiQcnkchVUDuSdhglIAWeTKMTI0BZedeafzfs7bna6Cgup+hvJAREv+ouxf8B88 0mq7YjHbHue/o9LoPZ2UvVm9I7uv7Hlep6tqWqXTXWoXD3E7ftOeg8FHRR7DNBlzSyG5Gy9Zg08M UeGAoITKm52KonT9Nv8AUbpbWxge4uH+zHGKn5nwHucsx45TNRFlqzZoY48UzQeoeVvyegj4XXmB /Vk6ixiNEH/GRxu3yX7zm+0vYwG+T5PK6/2jJ9OEV/SP6B+t6VbW1vbQJBbRJDBGKJFGoVVHgANs 3kYiIoCg8vOcpG5GyVTJMXYq7FXzl+ZumHT/ADrqSUolw4uUPj6w5sf+DLDJBDF8Ve0fkTeI+ial ZV+OG5ExHeksYUf8msBUPTcCWGfm9/ygt7/xkg/5OrhCl8+4UOxV9P8AlD/lEtE/5gLX/kyuRSm2 KvH/AM+f969H/wCMc/8AxJMIQXlWFU28of8AKW6J/wAx9r/yeXFX0/kUuxV2KuxV2KuxV2KuxViP mr8ytD0IvbRH69qK7G3jPwof+LH3A+Qqc12r7Sx4dvql3frdx2f2Ll1FSPph3/qDyHzH5x13zBLW +nItwax2kfwxL/se592qc5vU6zJmPqO3d0ez0fZ2HTj0Dfv6pJmI5zsVbVWdgqgszGiqNySegAwg KTTP/K35SapqHC51gmwtDuIf93uPkdk/2W/tm40vZE57z9I+39jzuv8AaDHj9OL1y7+n7fxu9X0b QNI0W2+r6bbLAm3Nhu7kd3Y7nOhw6eGIVEU8jqdXkzy4pm0wy5xnYq7FXYq7FXln55aC0tpZa5Et Tbn6tckfyOeUZ+Qao+nCEF47hVlH5dea18ueYUnnJ+o3K+hd0/ZUkEPT/IP4VxKvouGaKaJJoXWS KRQ0cikFWUioII6g5FLDfzfZR5GuwTQtLCFHifUB/UMIUvn7Ch2Kvp/yh/yiWif8wFr/AMmVyKU2 xV4/+fP+9ej/APGOf/iSYQgvKsKpt5Q/5S3RP+Y+1/5PLir6fyKXYqg4tXsJL2WxEoW6iNDG2xNR X4fHriqMxV2KuxV2KsR84eY5InbTbNuL0/0iVeor+wD+vChgV9ptnepSdKsNlkGzD5HMbU6PHmHq G/f1c3R9oZdObgdu7oxnUfLt5a1eL9/CP2lHxAe6/wBM5vV9l5MW49Ufx0ey0HbuLP6Zeifny+BS nNW7tk3lf8v9e18rLHH9VsD1vJgQpH+QvV/1e+Z+l7PyZtxtHvdXru18On2J4p9w/T3PX/LHkTQf L6q9vF697T4ryUAv78OyD5fTXOk0ugx4eQuXe8Zru1c2o2kaj3Dl+1kWZrrXYq7FXYq7FXYq7FUJ q+mW2q6Zc6ddCsF1G0b+Ir0Ye6ncYq+Y9c0e80bVbnTbtaTWzlSezL1Vx7Mu4ySEDirKvKf5j+YP LiC3hZbqwrX6pNUha9fTYbr+r2xpVTzv+Y2o+aYYLV7dLOyhb1PRVi5aShAZmIXoCaCmNKxHFXYq +n/KH/KJaJ/zAWv/ACZXIpTbFXj/AOfP+9ej/wDGOf8A4kmEILyrCqbeUP8AlLdE/wCY+1/5PLir 6fyKXYq8z80kjzBdkbEMtD/sBhQjtH86XtrxivK3UA25E/vFHz/a+n78aVmmn6pY6hF6lrKHH7S9 GX5r1GBKKxVbI4SNnPRQWPyAriryOeZ555JpDV5WLsfdjU4ULMKtxxvI6xxqWdyFVRuST0AxVmGj /lvokM0d/qNulxfD4vTO8QPXdejt7nMOWhwynxmIt2EO09RHH4YmeH8cjzZgAFAAFANgB0pmU4Ds VdirsVdirsVdirsVdirsVYR+ZvkMeYbAXtkoGr2in0x09aMbmM+/8v3d8IV4FJG8btHIpSRCVdGF CCNiCD3woaxV2KuxV2Kvo78tNUj1DyXprqavbR/VZV6lTD8AB+acT9ORKWT4q8f/AD5/3r0f/jHP /wASTCEF5VhVNvKH/KW6J/zH2v8AyeXFX0/kUuxV5n5q/wCO/ef6y/8AEBhQlWFWT+QP+Olcf8Yf +NhgKhnWBKje/wC8c/8Axjf/AIicVeSZJDsVZX5D01ZJ5r+QV9H93DX+ZhVj9A/XgKhm2BLsVdir sVdirsVdirsVdirsVdirsVYJ5/8AyxtPMAa/0/ja6uBVido56dnp0bwb7/Y2rw/VNK1HSrx7PULd 7a5j+1G4pt4g9CD4jChC4q7FXYqzX8svPK+XNRe2vSf0TeEesQK+lINhIB4U2b+zEq98t7iC5gSe 3kWWCUBo5UIZWU9CCOuRS8c/PW8hk1jTbRSDLBA7yAHp6rAKD/wGEILzHCqbeUP+Ut0T/mPtf+Ty 4q+n8il2KsP8zeVL24u5b+zImMlC8HRhQAfD2PTChh7o8blHUo6mjKwoQfcHCrJvIH/HSuP+MP8A xsMBUM6wJUb3/eOf/jG//ETiryTJIdirP/IoA0Vqd5nr/wACuApDIsCuxVL9V17TdMAFy5MrCqwo KuR407fTiqSf8rAtOVPqknDx5LX7v7cNItMLLzhol0QrSG3c9BMOI/4IEr95wUlOlZWUMpBU7gjc EYq3irsVdirsVdirsVdiqX615f0fW7X6tqdqlzH+wWFHQnujijKfkcVebaz+RSM7SaPqPBT9mC6W tP8Anon/ADThtFJVB+RfmUvSe+skjp9pGlc1+RjT9eNrTz/ULC70+9msruMxXNu5SWM9iP4eGFVD FU00fzT5h0YMumX8tsjbtGpqhPjwaq196Yqgb2+vL66kuryZ57mU1klkJZienU4qo4qm/k8E+bdE p/y323/J5cVfT2RS7FXYql+q6Fp2ppS4jpKBRZl2cfT3+nFUs8veXLrSdTndnEtu8XGOQbGvIGhX CrI8CqN7/vHP/wAY3/4icVeSZJDsVegeRf8Ajit/xmf9S4ClkOBVG9uktLSa5f7MKFyPGg6fTiry m7upru5kuJm5SysWY/wHywoUsKuxVMdJ1/UdMYehJyhr8UD7ofl4fRgVm+j+adN1HjGW9C5O3oue p/yW7/rwJTnFXYq7FXYq7FXYq7FXYq7FWBfmd+Xo163Op6cgGsW60KCgE6D9k/5Y/ZP0eFCCrwmW KWGV4pUaOWMlXjcFWVgaEEHcEYULcVdirsVZR+WVg17530xQKrC7TufARKWH/DADEq+jcil2KuxV 2KuxV2KqN7/vHP8A8Y3/AOInFXkmSQ7FXoHkX/jit/xmf9S4ClkOBUk85SlNAnA/3YyL/wAMD/DE K85ySHYq7FXYq7FU/wBH84ahZcYrmt1bDajH41Hs3f5HBSs103WNP1KPnayhiPtRnZ1+a4Eo3FXY q7FXYq7FXYq7FXYqw3zv+Wul+ZA11CRZ6sBtcgfDJToJVHXw5dfn0w2rxHX/ACvrmgXPoanbNFUk RzD4on90cbH5dfHChKsVdir2n8mfKU9jaTa7eIUmvVEdojChEFQxf/ZkCnsPfAVD0zAl2KsM1TzH qWleYLmMH1rUlW9F+lCin4T1XChkGk+YdN1NQIX4T03gfZvo8fowJTPFXYqo3v8AvHP/AMY3/wCI nFXkmSQ7FXoHkX/jit/xmf8AUuApZDgVIvOiFtBlI/ZdCf8AgqfxxCvO8kh2Kpxp/lTVr61FzEES Nv7v1GILDpsAD+OBUBfabe2EvpXcLRN2J3B+RGxxVDYVdiq+GaaGRZYXaORTVXU0I+kYFZXo/nll 4w6mvIdBcIN/9ko6/RjS2zPAl2KuxV2KuxV2KuxV2KqVza2t1A0F1Ck8D7PFIodT81NRirDtS/J/ yXeyNJHDNZM25FtJRa+yyCRR9Aw2tL9H/KTybps6zmGW+kQ1T624dQf9RFRT/sgcbVmeBXYq7FWC efbYpqUNwB8M0VK/5SHf8CMIQWMqzKwZSQw3BGxBwqzXyd5gvru4axu3EoWMvHK326ggUJ77HAVZ ZgSo3v8AvHP/AMY3/wCInFXkmSQ7FXoHkX/jit/xmf8AUuApZDgVL/MFsbnRbyIbkxllHiU+Mf8A EcVeXZJDsVeraRw/RVnw+z6EdPlwGRSrXNrb3URhuI1ljbqrCoxViGseRpE5TaY3Nept3PxD/VY9 fpw2imKSxSwyNHKjRyKaMjAgg+4OKrcKuxV7DkUuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVIPOl gbnSDMgrJat6n+xOzf1+jEK89ySEZo2oHT9Sguuqo1JB4o2zfgcCvU4pY5Y1ljYPG4DIw6EHocCV O9/3jn/4xv8A8ROKvJMkh2KvQPIv/HFb/jM/6lwFLIcCuIBFD0xV5XrNg1hqc9sRRVasZ8Ubdfww oQWFWe+StWS40/6k7fv7avEHq0ZOx+itMBSyTArsVQWp6Np+pR8LqIFgKLKuzr8jirCdY8oahY8p YK3VsN+Sj41H+Uv8RhQkOFXsORS7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FWpESRGjcckcFWU9wd iMVeWazpkmm6hLbNUqDyiY/tIehwoQWFU80HzTdaYBBIvr2la+nWjLXrxP8ADAqc6v51sJdOlis1 kM8ylPjAUKGFCep3p4Y0rCsKuxV6B5F/44rf8Zn/AFLgKWQ4FdirGfO2jm5tVv4VrNbikoHUx9f+ FwhDBMKqtrdT2s6XFu5jljNVYYFZzo3nOyugsN7S2uOnM/3bH5/s/T9+NJZGCCKjcHocCuxV2KpL rHlXTtR5SKPq9yd/VQbE/wCUvf8AXiqdYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqk3mbQhq lnWMAXcNTCenId0PzxV5w6OjsjqVdSQykUII7HJIaxV2KuxV2KvQvI6FdDBPR5XYfLYfwyJSn+Ku xVxAIIIqDsQcVed+aPLz6bcGeBSbKU/Af5GP7J/hhQkWFXYqmukeZdS00hEb1bYdYHNR/sT1XArO NI8x6dqYCxv6dx3gfZv9j/NgSmmKuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVj/mXy umog3NrRL0DcdBIB2Pv4HCrAZ4JoJWhmQxyoaMjChGKFmFXYq4AkgAVJ2AGKvVdHsvqWl21qdmjQ c/8AWPxN+JyKUZirsVdiqnc20FzA8E6CSKQUZT3xV575h8sXGmOZoqy2RO0ndPZ6frwoSTCrsVbR 2Rg6EqymqsDQgjuDirPvKvmX9IKLS6IF4g+Fv9+KO/8ArDvgSyLArsVdirsVdirsVdirsVdirsVd irsVdirsVdirsVdirsVQOqaLp+pR8LmOrAfBKuzr8j/XFWIah5G1KFi1my3MfZahHH0Hb8cNopK/ 8O65y4/Upa9Ps7ff0xVkvlvyhLbTpe6hT1E+KKAGtG/mY9KjtTG1ZZgS7FXEgUqaV2GKuxV2KtMq spVgGUihB3BGKsW1jyRBNym05hDJ1MDfYP8Aqn9n9Xyw2imHXlld2cxhuYmikHZh19wehHyxVRwq vgnlgmSaJiksZDIw7EYFepaRqMeo6fFdJsXFJF/lcbMMCUZirsVdirsVdirsVdirsVdirsVdirsV dirsVdirsVdirsVdirsVdirsVdiqQ+dWK6IzKSGEqEEdQa4hUh0fzrd23GK+BuYRt6n+7AP+Nvpw 0hmdhqVlfw+rayiRf2gOq+zA7jAlE4q7FULqWmWmo2zW9ynJT9lx9pT4qcVeZapps+nXslrNuU3V +zKejDChC4VZZ5CvytxPYsfhkHqxj/KXZvvH6sBUM1wJdirsVdirsVdirsVdirsVdirsVdirsVdi rsVdirsVdirsVdirsVdirsVSHzt/xwm/4yJ+vEK88ySFW1u7m1mE1vI0Uq9GU0+g+OBWX6P55jfj Dqa8G6C4QfCf9Ze30Y0tsrililjWSJw8bCqupBBHzGBK7FWJfmBaqYLW6A+JWMTHxBHIfqOEILC8 Kph5euDb61ZydAZAh+T/AAH/AIlgV6jgS7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYqk/muzubvR3itozJIGVuA6kDrTFXm7KyMVYFWBoVOxBySGsVdiqN0zWtQ02TlbSEI TVom3RvmP6YFZppnnTTLlAt0fqs3cNuh+Tf1xpKUedNcsryOG0tJBKqMZJHX7NaUAB79TiEMVwqu icxypIOqMGH0GuKvX8il2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2 Kpbq3l/TdTU+vHxm/ZnTZx8/H6cVYRrHlbUdNrJT17Yf7uQdB/lL2/VhQk+FXYq7FXYq7FXYq9hy KXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqx/WPJ1he8pba lrcHeqj4GPuvb5jDasK1LSNQ06ThdRFQfsyDdG+TYoQeFXYq7FUVpNq11qdtbqK85F5f6oNWP3DA r1fAl2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVZNDDPG 0UyLJG2zIwBB+g4qxXVvIsbsZdNkEZO5gkJK/wCxbcj6cNopjN1oWsWrUmtJAP5lHNf+CWoxVRi0 3UJXCR20rMegCN/TFWbeVfLL6fW7u6fW3HFYxuEU9d/E4pf/2Q== + + + + + + uuid:394c157d-8cf0-f94e-8261-98ccb840e1bc + xmp.did:106820F817276811822ACF3CAA06C913 + uuid:5D20892493BFDB11914A8590D31508C8 + proof:pdf + + uuid:1898a391-1dba-4249-9e7f-8804d46504b0 + xmp.did:FE7F11740720681183D1839CF7E6F44E + uuid:5D20892493BFDB11914A8590D31508C8 + proof:pdf + + + + + saved + xmp.iid:FE7F11740720681183D1839CF7E6F44E + 2017-01-06T15:27:37Z + Adobe Illustrator CS6 (Macintosh) + / + + + saved + xmp.iid:106820F817276811822ACF3CAA06C913 + 2017-01-27T14:51:33Z + Adobe Illustrator CS6 (Macintosh) + / + + + + + + Document + Print + + + False + False + 1 + + 3840.000000 + 2160.000000 + Pixels + + + + Cyan + Magenta + Yellow + Black + + + + + + Default Swatch Group + 0 + + + + White + RGB + PROCESS + 255 + 255 + 255 + + + Black + RGB + PROCESS + 35 + 31 + 32 + + + CMYK Red + RGB + PROCESS + 237 + 28 + 36 + + + CMYK Yellow + RGB + PROCESS + 255 + 242 + 0 + + + CMYK Green + RGB + PROCESS + 0 + 166 + 81 + + + CMYK Cyan + RGB + PROCESS + 0 + 174 + 239 + + + CMYK Blue + RGB + PROCESS + 46 + 49 + 146 + + + CMYK Magenta + RGB + PROCESS + 236 + 0 + 140 + + + C=15 M=100 Y=90 K=10 + RGB + PROCESS + 190 + 30 + 45 + + + C=0 M=90 Y=85 K=0 + RGB + PROCESS + 239 + 65 + 54 + + + C=0 M=80 Y=95 K=0 + RGB + PROCESS + 241 + 90 + 41 + + + C=0 M=50 Y=100 K=0 + RGB + PROCESS + 247 + 148 + 30 + + + C=0 M=35 Y=85 K=0 + RGB + PROCESS + 251 + 176 + 64 + + + C=5 M=0 Y=90 K=0 + RGB + PROCESS + 249 + 237 + 50 + + + C=20 M=0 Y=100 K=0 + RGB + PROCESS + 215 + 223 + 35 + + + C=50 M=0 Y=100 K=0 + RGB + PROCESS + 141 + 198 + 63 + + + C=75 M=0 Y=100 K=0 + RGB + PROCESS + 57 + 181 + 74 + + + C=85 M=10 Y=100 K=10 + RGB + PROCESS + 0 + 148 + 68 + + + C=90 M=30 Y=95 K=30 + RGB + PROCESS + 0 + 104 + 56 + + + C=75 M=0 Y=75 K=0 + RGB + PROCESS + 43 + 182 + 115 + + + C=80 M=10 Y=45 K=0 + RGB + PROCESS + 0 + 167 + 157 + + + C=70 M=15 Y=0 K=0 + RGB + PROCESS + 39 + 170 + 225 + + + C=85 M=50 Y=0 K=0 + RGB + PROCESS + 28 + 117 + 188 + + + C=100 M=95 Y=5 K=0 + RGB + PROCESS + 43 + 57 + 144 + + + C=100 M=100 Y=25 K=25 + RGB + PROCESS + 38 + 34 + 98 + + + C=75 M=100 Y=0 K=0 + RGB + PROCESS + 102 + 45 + 145 + + + C=50 M=100 Y=0 K=0 + RGB + PROCESS + 146 + 39 + 143 + + + C=35 M=100 Y=35 K=10 + RGB + PROCESS + 158 + 31 + 99 + + + C=10 M=100 Y=50 K=0 + RGB + PROCESS + 218 + 28 + 92 + + + C=0 M=95 Y=20 K=0 + RGB + PROCESS + 238 + 42 + 123 + + + C=25 M=25 Y=40 K=0 + RGB + PROCESS + 194 + 181 + 155 + + + C=40 M=45 Y=50 K=5 + RGB + PROCESS + 155 + 133 + 121 + + + C=50 M=50 Y=60 K=25 + RGB + PROCESS + 114 + 102 + 88 + + + C=55 M=60 Y=65 K=40 + RGB + PROCESS + 89 + 74 + 66 + + + C=25 M=40 Y=65 K=0 + RGB + PROCESS + 196 + 154 + 108 + + + C=30 M=50 Y=75 K=10 + RGB + PROCESS + 169 + 124 + 80 + + + C=35 M=60 Y=80 K=25 + RGB + PROCESS + 139 + 94 + 60 + + + C=40 M=65 Y=90 K=35 + RGB + PROCESS + 117 + 76 + 41 + + + C=40 M=70 Y=100 K=50 + RGB + PROCESS + 96 + 57 + 19 + + + C=50 M=70 Y=80 K=70 + RGB + PROCESS + 60 + 36 + 21 + + + + + + Grays + 1 + + + + C=0 M=0 Y=0 K=100 + RGB + PROCESS + 35 + 31 + 32 + + + C=0 M=0 Y=0 K=90 + RGB + PROCESS + 65 + 64 + 66 + + + C=0 M=0 Y=0 K=80 + RGB + PROCESS + 88 + 89 + 91 + + + C=0 M=0 Y=0 K=70 + RGB + PROCESS + 109 + 110 + 113 + + + C=0 M=0 Y=0 K=60 + RGB + PROCESS + 128 + 130 + 133 + + + C=0 M=0 Y=0 K=50 + RGB + PROCESS + 147 + 149 + 152 + + + C=0 M=0 Y=0 K=40 + RGB + PROCESS + 167 + 169 + 172 + + + C=0 M=0 Y=0 K=30 + RGB + PROCESS + 188 + 190 + 192 + + + C=0 M=0 Y=0 K=20 + RGB + PROCESS + 209 + 211 + 212 + + + C=0 M=0 Y=0 K=10 + RGB + PROCESS + 230 + 231 + 232 + + + C=0 M=0 Y=0 K=5 + RGB + PROCESS + 241 + 242 + 242 + + + + + + Brights + 1 + + + + C=0 M=100 Y=100 K=0 + RGB + PROCESS + 237 + 28 + 36 + + + C=0 M=75 Y=100 K=0 + RGB + PROCESS + 242 + 101 + 34 + + + C=0 M=10 Y=95 K=0 + RGB + PROCESS + 255 + 222 + 23 + + + C=85 M=10 Y=100 K=0 + RGB + PROCESS + 0 + 161 + 75 + + + C=100 M=90 Y=0 K=0 + RGB + PROCESS + 33 + 64 + 154 + + + C=60 M=90 Y=0 K=0 + RGB + PROCESS + 127 + 63 + 152 + + + + + + + + + Adobe PDF library 10.01 + + + + + + + + + + + + + + + + + + + + + + + + + endstream endobj 3 0 obj <> endobj 8 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/Thumb 36 0 R/TrimBox[0.0 0.0 3840.0 2160.0]/Type/Page>> endobj 32 0 obj <>stream +HlVI9 +TK"cN(rh4P@A*n(EJ\A*z/ϑ>Lۯ-RN%7޾[+ELD}~Q_,RC)i%mR=V{uS¡,YHzeۓ Q!5nJ9P5"v[ uWvU7nێ,FP^$$5DDpɁd4ڋۙ90q|dDZ a1439<0}f0{ꖐhE#R 5Y9$ +zX]@~3ϋ̬3L>س|cΏs%sZNVuAcp~Rc阏b^Aй><\I/wP;bJ@|n[fDڍ^ -[D@Dňށ(TIfйkUE'Gx"/Þ!QLwBq:p.RZ[Yh=j{9 ACIY8w;vuu-ӹ2/;@r fdk6z1FMǯk׊x]*C1ylDTAœu&4`pv)5bǸ+ +VEC04,Tp9p)1Rِ9ֿmXѭxhPxʐke&ډAeL9Jl1|)Y|zf*ѷ.G%:o]~Ljh(eMBC#\fch\ij\mr#Jܢo;oَ?I >stream +8;Z]!gMI$9&4O4Loo+g]Y:Y;L9GQ$:@3.,T.n*;fRief@%Sif_3dV)Ok+/'6SR3bl +9q7Fhkme7N\oK:^?"i4k6BkiO2r#nub+]#^#o2T&%;/3"U%t=#DK/;5(hd<.rJl^nXukH^04p_5&"H=J0K1J +F,$ endstream endobj 37 0 obj [/Indexed/DeviceRGB 255 38 0 R] endobj 38 0 obj <>stream +8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 29 0 obj <> endobj 30 0 obj <> endobj 41 0 obj [/View/Design] endobj 42 0 obj <>>> endobj 39 0 obj [/View/Design] endobj 40 0 obj <>>> endobj 35 0 obj <> endobj 34 0 obj [/ICCBased 43 0 R] endobj 43 0 obj <>stream +HyTSwoɞc [5laQIBHADED2mtFOE.c}08׎8GNg9w߽'0 ֠Jb  + 2y.-;!KZ ^i"L0- @8(r;q7Ly&Qq4j|9 +V)gB0iW8#8wթ8_٥ʨQQj@&A)/g>'Kt;\ ӥ$պFZUn(4T%)뫔0C&Zi8bxEB;Pӓ̹A om?W= +x-[0}y)7ta>jT7@tܛ`q2ʀ&6ZLĄ?_yxg)˔zçLU*uSkSeO4?׸c. R ߁-25 S>ӣVd`rn~Y&+`;A4 A9=-tl`;~p Gp| [`L`< "A YA+Cb(R,*T2B- +ꇆnQt}MA0alSx k&^>0|>_',G!"F$H:R!zFQd?r 9\A&G rQ hE]a4zBgE#H *B=0HIpp0MxJ$D1D, VĭKĻYdE"EI2EBGt4MzNr!YK ?%_&#(0J:EAiQ(()ӔWT6U@P+!~mD eԴ!hӦh/']B/ҏӿ?a0nhF!X8܌kc&5S6lIa2cKMA!E#ƒdV(kel }}Cq9 +N')].uJr + wG xR^[oƜchg`>b$*~ :Eb~,m,-ݖ,Y¬*6X[ݱF=3뭷Y~dó ti zf6~`{v.Ng#{}}jc1X6fm;'_9 r:8q:˜O:ϸ8uJqnv=MmR 4 +n3ܣkGݯz=[==<=GTB(/S,]6*-W:#7*e^YDY}UjAyT`#D="b{ų+ʯ:!kJ4Gmt}uC%K7YVfFY .=b?SƕƩȺy چ k5%4m7lqlioZlG+Zz͹mzy]?uuw|"űNwW&e֥ﺱ*|j5kyݭǯg^ykEklD_p߶7Dmo꿻1ml{Mś nLl<9O[$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! +zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)Km endstream endobj 33 0 obj <> endobj 44 0 obj <> endobj 45 0 obj <>stream +%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 16.0 %%AI8_CreatorVersion: 16.0.0 %%For: (Ian Meikle) () %%Title: (Web3_70.ai) %%CreationDate: 27/01/2017 15:51 %%Canvassize: 16383 %%BoundingBox: 1170 -1782 2670 -378 %%HiResBoundingBox: 1170 -1781.6396 2670 -378.3604 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 12.0 %AI12_BuildNumber: 682 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 ([Registration]) %AI3_Cropmarks: 0 -2160 3840 0 %AI3_TemplateBox: 1920.5 -1080.5 1920.5 -1080.5 %AI3_TileBox: 1517 -1359.5 2300 -800.5 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 6 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 2 %AI9_OpenToView: -1717.0015 636.9985 0.3333 2078 1150 18 0 0 32 191 0 0 0 1 1 0 1 1 0 1 %AI5_OpenViewLayers: 77 %%PageOrigin:1614 -1476 %AI7_GridSettings: 72 8 72 8 1 0 0.8 0.8 0.8 0.9 0.9 0.9 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 46 0 obj <>stream +%%BoundingBox: 1170 -1782 2670 -378 %%HiResBoundingBox: 1170 -1781.6396 2670 -378.3604 %AI7_Thumbnail: 128 120 8 %%BeginData: 14636 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C45FFFFFFA87D7DFD7CFFA8FD0452A8FD79FFA87D527D527D527DA8FD %77FF7D525352525253525259FD76FF527D527D527D527D527D52A8AFFD72 %FF7D52527D5252527D52525259527DA8FD71FF7D527D527D527D527D527D %527DA8FD72FF52535252525352525253527DA8FD39FFCACAA1C3A0C2A0C2 %A0C3A1CACAFD2DFF7D527D527D527D527D527DA8FD37FFC3C3BBBBBABBB4 %BBB4BBBABB92BBBBBBC2C9CAFD2AFF527D5252527DFD04527DFD35FFA1C2 %98BA92BB92BB92BB98BB92BB98BB92BB92BA92BB99CAFD28FF7D527D527D %527D527D52FD33FFCAC9BBBA92BBBBBB98BBBBBB98BBBBBB98BBBBBB98BB %BBBB98BBB4C2C9FD26FF5252525352525253527DFD0CFFA87D7DFD23FFA0 %BB92BB92BB92BB92BB92BB92BB92BB92BB92BB92BB92BB92BB92BB92BA99 %CAFD24FF7D527D527D527D52537DFD0BFFA87D525352A8FD1FFFCABBBBBA %BBBBBBBABBBBBBBABBBBBBBABBBBBBBABBBBBBBABBBBBBBABBBBBBBABBBA %C9FD23FF5252527D5252527D52A8FD0AFFA852527D5252527DA8FD1BFFCA %C292BB98BB92BB98BB92BB98BB92BB98BB92BB92BB92BB98BB92BB98BB92 %BB98BB92BB92C2CAFD21FF7D527D527D527D5253A8FD0AFF7D527D527D52 %7D52537DFD19FFC2BBB4BB98BBBBBB98BBBBBB98BBBBBB98BBB4BB92BBB4 %BA92BBBBBB98BBBBBB98BBBBBB98BBB4BBCAFD20FF525352525253525252 %A8FD09FFA8525352525253FD05527DA8FD14FFC998BA92BB92BB92BB92BB %92BB92BB92BB92BA99C3C3CAA7C9A0BB92BB92BB92BB92BB92BB92BB92BB %92BBA8FD1FFF7D527D527D527D5259A8FD09FF7D7D527D527D527D527D52 %7D527DFD12FFC9C2B4BBBABBBBBBBABBBBBBBABBBBBBBABBB4C2C9FD08FF %C9BBBBBABBBBBBBABBBBBBBABBBBBBB4C2FD1FFF527D5252527D525252A8 %FD09FF7D527D5252527D5252527D527DA8FD10FFCF9ABA92BB98BB92BB98 %BB92BB98BB92BB92BA98C9FD0BFFCF99BB92BB98BB92BB98BB92BB98BB92 %C3CAFD1DFF7D527D527D527D52527DFD09FF527D527D527D527D527D527D %FD11FFC398BBBABB98BBBBBB98BBBBBB98BBBBBB92BBC2FD0EFFC9B4BB98 %BBBBBB98BBBBBB98BBBBBB92BBBBC2C3CFFD19FF525252535252525352A8 %FD08FFA85952525253525252535252A8FD0FFFA1BB92BB92BB92BB92BB92 %BB92BB92BB92BB92C2A8FD0FFFA1BA92BB92BB92BB92BB92BB92BB92BB92 %BB92BA92C2C3FD17FF7D527D527D527D52527EFD09FF527D527D527D527D %52537DFD0EFFCABBBBBABBBABBBBBBBABBBBBBBABBBBBBBABBBBCAFD10FF %CFC2BABBBABBBBBBBABBBBBBBABBBBBBBABBBBBBBABBBABBBBC9FD15FF52 %52527D5252527D52A8FD08FFA87D5252527D5252525952FD0DFFA8C2B4BA %92BB98BB92BB98BB92BB98BB92BB92BBA0FD11FFCABB92BB98BB92BB98BB %92BB98BB92BB98BB92BB98BB92BB98BB92BBC3FD13FF7D527D527D527D52 %53A8FD09FF527D527D527D527D527DFD0CFFA87D75BBBABB98BBBBBB98BB %BBBB98BBBBBB92C3CFFD11FFC2BBBABB98BBBBBB98BBBBBB98BBBBBB98BB %BBBB98BBBBBB98BBBBBB92BBBBCAFD11FF525352525253525252A8FD08FF %A85252535252525352527DFD0BFF7D525253527698BA92BB92BB92BB92BB %92BA99CAFD11FFC999BA92BB92BB92BB92BB92BB92BB92BB92BB92BB92BB %92BB92BB92BB92BB92BB92C2CAFD0FFF7D527D527D527D5259A8FD09FF52 %7D527D527D527D52A8FD0AFFA87D527D527D537D99BBBABBBABBBBBB92C1 %C9FD11FFCAC2B4BBBABBBBBBBABBBBBBBABBBBBBBABBBBBBBABBBBBBBABB %BBBBBABBBBBBBABBBBBB92C2CAFD0EFF527D5252527D525252A8FD08FFA8 %53527D5252527D52527DFD0AFF7D527D5252527D52535299B4BB92BA98C3 %CAFD11FFC2BA92BB98BB92BB98BB92BB98BB92BB92BA92BB92BB92BA92BB %92BB98BB92BB98BB92BB98BB92BBCAFD0DFF7D527D527D527D52527DFD09 %FF527D527D527D527D52A8FD09FFA8527D527D527D527D5259527C99BBC2 %FD12FFC998BBBABB98BBBBBB98BBBBBB98BBBBBB92BBBBC9C9CFCACAC2C1 %B4BB98BBBBBB98BBBBBB98BBBBBB92BBCAFD0CFF525252535252525352A8 %FD08FFA85952525253FD04527DFD09FF59FD0452535252525352522E7DA8 %FD11FFA1BB92BB92BB92BB92BB92BB92BB92BB92BB92C2A7FD07FFCAC998 %BA92BB92BB92BB92BB92BB92BB92C1FD0CFF7D527D527D527D52527EFD09 %FF527D527D527D527D52A8FD09FF7D527D527D527D527D527D527DFD11FF %CFBBBBBABBBABBBBBBBABBBBBBBABBBBBBBABBBBCAFD0BFFCFFD04BBBABB %BBBBBABBBBBBBABBB4C9FD0BFF5252527D5252527D52A8FD08FFA87D5252 %527DFD04527DFD09FF52595252527D5252527D527DFD10FFCAC292BA92BB %98BB92BB98BB92BB98BB92BB92BBA0FD0EFFC992BB98BB92BB98BB92BB98 %BB92BB99FD0BFF7D527D527D527D5253A8FD09FF527D527D527D527D52A8 %FD08FFA87D527D527D527D527D527DFD10FFC3BB92BBBBBB98BBBBBB98BB %BBBB98BBBBBB92C2CAFD0FFFC3BB98BBBBBB98BBBBBB98BBBBBB92C2FD0B %FF525352525253525252A8FD08FFA85252535252525352527DFD09FF5252 %52535252525352527DFD0EFFCA99BA92BB92BB92BB92BB92BB92BB92BB92 %BA99CAFD10FFA8BB92BB92BB92BB92BB92BB92BB92BA99FD0BFF7D527D52 %7D527D5259A8FD09FF527D527D527D527D52A8FD09FF7D527D527D527D52 %7D52FD0DFFCFC292BBBBBBBABBBBBBBABBBBBBBABBBBBB92BBC9FD11FFCA %C1BABBBABBBBBBBABBBBBBBABBBBBB92C9FD0BFF527D5252527D525252A8 %FD08FFA853527D5252527D52527DFD09FF5252527D5252527D527DFD0EFF %C2BB92BB98BB92BB98BB92BB98BB92BA92C3CAFD11FFC2BB92BB98BB92BB %98BB92BB98BB92BB92BBCAFD0BFF7D527D527D527D52527DFD09FF527D52 %7D527D527D52A8FD08FFA87D527D527D527D52537EFD10FFC398BBBABB98 %BBBBBB98BBBABBBCFD12FFC998BBBABB98BBBBBB98BBBBBB98BBBBBB92BB %CAFD0CFF525252535252525352A8FD08FFA85952525253FD04527DFD09FF %FD055253525252A8FD11FFCA99BA92BB92BB92BA92BBC9FD11FFA1BB92BB %92BB92BB92BB92BB92BB92BB92BB92BBA8FD0DFF7D527D527D527D52527E %FD09FF527D527D527D527D52A8FD08FFA87D527D527D527D5259A8FD13FF %C9C292FD04BBC9FD11FFCFBCBBBABBBABBBBBBBABBBBBBBABBBBBBBABBBB %CAFD0FFF5252527D5252527D52A8FD08FFA87D5252527DFD04527DFD09FF %52595252527D525252A8FD15FFC992BBC2FD11FFCAC392BA92BB98BB92BB %98BB92BB98BB92BB92BBA0FD11FF7D527D527D527D5253A8FD09FF527D52 %7D527D527D52A8FD08FFA87D527D527D527D52527DFD17FFCAFD11FFC3BB %92BBBBBB98BBBBBB98BBBBBB98BBBBBB92C2CAFD12FF7DFD045253525252 %A8FD08FFA85252535252525352527DFD09FF525252535252525352A8FD27 %FFCA99BA92BB92BB92BB92BB92BB92BB92BB92BA99CAFD14FFA8527D527D %527D5259A8FD09FF527D527D527D527D52A8FD09FF7D527D527D527D5252 %7EFD25FFCAC2B4BBBBBBBABBBBBBBABBBBBBBABBBBBBB4BBC3FD16FFA853 %5252527D525252A8FD08FFA853527D5252527D52527DFD09FF5252527D52 %52527D52A8FD24FFA0BB92BB92BB98BB92BB98BB92BB98BB92BB92C2CAFD %18FF527D527D527D52527DFD09FF527D527D527D527D52A8FD08FFA87D52 %7D527D527D5253A8FD22FFCABBBA92BBBBBB98BBBBBB98BBBBBB98BBBABB %BBCFFD1AFF7D525352525253527DA8FD07FFA85952525253FD04527DFD09 %FFFD055253525252A8FD20FFA8BB92BB92BB92BB92BB92BB92BB92BB92BB %92BBC9FD1CFFA87D527D527D527D52FD09FF527D527D527D527D52A8FD08 %FFA87D527D527D527D5259A8FD1FFFA8A8A1C2BABBBABBBBBBBABBBBBBBA %FD04BBC9FD1FFF52535252527D52527DFD07FFA87D5252527DFD04527DFD %09FF52595252527D525252A8FD1EFFA8A87DA87DA199BA92BB98BB92BB98 %BB92BBC2FD21FF7E527D527D527D5252A8FD07FF527D527D527D527D52A8 %FD08FFA87D527D527D527D52527DFD1DFFA8A87DA87EA87EA8A0BBB4BBBB %BB98BBB4C2CAFD22FFA8525253525252535252A8FD05FFA8525253525252 %5352527DFD09FF525252535252525352A8FD08FFA8A8A8FD11FFA8A87DA8 %7DA87DA87DA87DA092BB92BA99C9FD25FF7D7D527D527D527D527DA8FD04 %FFA8527D527D527D527D52A8FD09FF7D527D527D527D52527EFD09FF7EA8 %7EFD11FF7DA87DA884A87DA884A884A799BBC2FD27FFA852595252527DFD %04527DA8A8A85252527D5252527D52527DFD09FF5252527D5252527D52A8 %FD08FFA8A87DA87DA8A8FD0DFF7DA87DA87DA87DA87DA87DA87DA8CAFD29 %FF7E527D527D527D527D5259527D527D527D527D527D527D52A8FD08FFA8 %7D527D527D527D5253A8FD09FF7DA87EA87DA8A8FD0CFFA87DA87EA87DA8 %7EA87DA87DA8FD2BFFA853525352525253525252535252525352525253FD %04527DFD09FFFD055253525252A8FD08FFA8A87DA87DA87DA87DA8A8FD09 %FF7DA87DA87DA87DA87DA87DA8FD2DFFA87D527D527D527D527D527D527D %527D527D527D527D52A8FD08FFA87D527D527D527D5259A8FD09FF7DA884 %A87DA884A87DFD0AFFA87DA884A87DA884A87DA8FD2FFF7DFD04527D5252 %527D5252527D5252527DFD04527DFD09FF52595252527D525252A8FD08FF %A8A87DA87DA87DA87D84A8FD09FF7DA87DA87DA87DA87D84A8FD30FF7D53 %527D527D527D527D527D527D527D527D527D527DFD08FFA87D527D527D52 %7D52527DFD09FF7EA87DA87EA87DA87DFD09FFA8A87DA87DA87EA87DA8A8 %FD32FFFD055253525252535252525352525253525252A8FD08FF52525253 %5252525352A8FD08FFA8A87DA87DA87DA87DA8A8FD09FF7DA87DA87DA87D %A87DA8FD33FF7D52527D527D527D527D527D527D527D527D527D7DFD08FF %7D527D527D527D52527EFD09FF7EA87DA884A87DA87DFD09FFA8A87EA87D %A884A87DA8A8FD34FF7D52527D5252527D5252527D5252527DFD0452A8FD %07FF5252527D5252527D52A8FD08FFA8A87DA87DA87DA87DA8A8FD09FF7D %A87DA87DA87DA87DFD36FFA859527D527D527D527D527D527D527D527D52 %7DA8FD05FFA87D527D527D527D5253A8FD0BFF7DA87DA87EA87DFD0AFFA8 %7DA87EA87DA87EA8A8FD0DFFFD05A8A9A8FD22FFA87DFD05525352525253 %5252525352525259A8FD04FF7D525352525253525252A8FD0CFFA87E7DA8 %7D7EA8FD09FF7DA87DA87DA87DA87DA9FD0CFF7DA87D847DA87D847DA8A8 %FD21FFA85953527D527D527D527D527D527D527D52597DFFFFA8527D527D %527D527D5259A8FD0DFFA8A87DA87DFD0AFFA87DA884A87DA884A8A8FD0B %FF7DA884A87DA884A87DA87DA8A8FD22FFA87DFD0552595252527D525252 %7D5252527D5252527D5252527D525252A8FD10FF7D7DA8FD09FF7DA87DA8 %7DA87DA87DFD0BFF7E847DA87DA87DA87DA87DA87D7E7DFD24FFFD05A87D %527D527D527D527D527D527D527D527D527D527D52527DFD11FFA8FD09FF %A8A87DA87DA87EA87DA8A8FD09FFA8A87EA87DA87EA87DA87EA87DA87EA8 %7DA8A8FD26FF84FD0452535252525352525253525252535252525352A8FD %1CFF7DA87DA87DA87DA87DFD0AFFA87DA87DA87DA87DA87DA87DA87DA87D %A87DA8A8FD26FF7D52527D527D527D527D527D527D527D527D527D5259A8 %FD1BFFA8A87EA87DA884A87DA8A8FD09FFA8A884A87DA884A87DA884A87D %A884A87DA87EA87DFD26FF7D52527D5252527D5252527D5252527D525252 %5952A8FD1CFF7DA87DA87DA87DA87DFD0AFFA87DA87DA87DA87DA87DA87D %A87DA87DA87DA87DA87DA8A8FD24FF7D52527D527D527D527D527D527D52 %7D527D527DFD1DFFA87DA87EA87DA87EA8A8FD09FF7EA87DA87EA87DA87E %A87DA87EA87DA87EA87DA87EA87DA8A8FD24FF7D52525352525253525252 %5352525253525253FD1DFF7DA87DA87DA87DA87DA9FD08FFA8A87DA87DA8 %7DA87DA87DA87DA87DA87DA87DA87DA87DA87DA87DA8A8FD22FF7D7D527D %527D527D527D527D527D527D52A8FD0AFFA8FD12FFA87DA884A87DA884A8 %A8FD09FF7EA87DA884A87DA884A87DA884A87DA884A87DA884A87DA884A8 %7DA8A8FD22FF7D535252527D5252527D5252527D5253A8FD09FFA8847EFD %11FF7DA87DA87DA87DA87DFD09FFA8A87DA87DA87DA87DA87DA87DA87DA8 %7DA87DA87DA87DA87DA87DA87DA87DFD22FFA87D5253527D527D527D527D %5252A8FD0BFF84A87DAFFD0EFFA8A87DA87DA87EA87DA8A8FD09FF7DA87E %A87DA87EA87DA8A8FFA8A87DA87DA87EA87DA87EA87DA87EA87DA87DA8A8 %FD21FFA8525252535252525352527DFD0CFFA87DA87DA8A8FD0DFF7DA87D %A87DA87DA87DFD09FFA8A87DA87DA87DA87D847DFFFFFFA8A87D7E7DA87D %A87DA87DA87DA87DA87DA87DA8FD23FFA87D52595259527D7DFD0DFFA8A8 %7DA87DA8A8FD0BFFA8A87EA87DA884A87DA8A8FD09FF84A884A87DA884A8 %7DFD07FFA8A87DA87DA884A87DA884A87DA884A87DFD26FFA8A87DA8A8FD %0EFFA87DA87DA87D847DA8FD0AFF7DA87DA87DA87DA87DFD0AFFA87DA87D %A87DA87D84A8FD07FFA8A87DA87DA87DA87DA87DA87DA87DA8FD39FFA8A8 %7DA87DA87EA87DFD0AFFA87DA87EA87DA87EA8A8FD09FFA8A87DA87EA87D %A87DFD0BFFA8A87DA87EA87DA87EA87DA87EFD3AFF7DA87DA87DA87D7EA8 %FD09FF7DA87DA87DA87DA87DA9FD0AFF7DA87DA87DA87DA8A8FD0BFFA8A8 %7DA87DA87DA87DA87DA8FD3AFFA87DA87DA884A87DA8FD09FFA87DA884A8 %7DA884A8A8FD0AFFA87DA884A87DA87DA8FD0DFFA87DA87DA884A87DA884 %FD3AFFA8A87DA87DA87DA87DFD09FF7DA87DA87DA87DA87DFD0BFFA8847D %A87DA87DA87DFD0FFF7E847DA87DA87DA8FD3BFF7DA87EA87DA87EA8A8FD %07FFA8A87DA87DA87EA87DA8A8FD0BFF7DA87DA87EA87DA8A8FD0FFFA8A8 %7DA87EA87DFD3BFFA87DA87DA87DA87D7EA8FD07FF7DA87DA87DA87DA87D %FD0CFFA87DA87DA87DA87DA8A8FD10FFA87DA87DA8FD3BFFA8A87EA87DA8 %84A87DA8FD06FFA8A884A87DA884A87DA8A8FD0BFFA8A87DA884A87DA87D %A8A9FD11FFA8A87DFD3CFF7DA87DA87DA87DA87DA8A8FD04FFA87DA87DA8 %7DA87DA87DFD0DFF7E847DA87DA87DA87DA8A8FD11FFA8A8FD3DFF7DA87E %A87DA87EA87DA8A8FFA8FF7DA87DA87EA87DA87EA8A8FD0DFF7DA87DA87E %A87DA87DA8A8FD4FFFA87DA87DA87DA87DA87DA87DA87DA87DA87DA87DA8 %7DA87DA9FD0DFFA87DA87DA87DA87DA87D7E7DA8FD4EFFA87DA87DA884A8 %7DA884A87DA884A87DA884A87DA884A8A8FD0EFFA87DA884A87DA884A87D %A87DA8A8FD4CFFA8A87DA87DA87DA87DA87DA87DA87DA87DA87DA87DA87D %FD0FFFA8A87DA87DA87DA87DA87DA87D7E7DFD4CFFA8A87EA87DA87EA87D %A87EA87DA87EA87DA87EA87DA8A8FD0FFFA8A87DA87EA87DA87EA87DA87E %A87DA8FD4BFF7DA87DA87DA87DA87DA87DA87DA87DA87DA87DA87DFD11FF %A87D7DA87DA87DA87DA87DA87DA87DA8A8FD4AFFA8A87DA884A87DA884A8 %7DA884A87DA884A87DA8FD12FFA8A87EA87DA884A87DA884A87DA87EA8A8 %FD4AFFA87E7DA87DA87DA87DA87DA87DA87DA87D84A8FD13FFA8A87DA87D %A87DA87DA87DA87DA87D7E7DFD4AFFA8A87DA87EA87DA87EA87DA87EA87D %A87DFD15FFA8A87DA87DA87EA87DA87EA87DA87EA8A8FD4AFFA8A87DA87D %A87DA87DA87DA87DA87DA8FD16FFA8A87DA87DA87DA87DA87DA87DA87D7D %7EFD4BFFA87DA87DA884A87DA884A87DA8A8FD18FFA87DA884A87DA884A8 %7DA884A87DA8A8FD4CFF7E847DA87DA87DA87D7EA8FD1AFFA87DA87DA87D %A87DA87DA87DA87DA8FD4DFFA8A87DA87DA87DA8A8FD1DFFA8A87DA87DA8 %7EA87DA87EA87DA8FD4FFFA8FFA8FFA8FD1FFFA8A87DA87DA87DA87DA87D %A87DFD62FFA8FD13FF7DA884A87DA884A87DA8A8FD60FFA8847DA8A8FD11 %FF7DA87DA87DA87DA87DA8FD61FF7DA87DA8A8FD11FF7DA87EA87DA87EA8 %7DFD60FFA8A87DA87DA87DA8FD0FFFA87DA87DA87DA87D7E84FD60FF7EA8 %7DA884A87DA8FD0EFFA8A87EA87DA884A87DA8FD5FFFA8A87DA87DA87DA8 %7DA8A8FD0DFF7DA87DA87DA87DA87DFD60FF7DA87EA87DA87EA87DA87DA9 %FD0BFFA87DA87EA87DA87EA8A8FD5EFFA8A87DA87DA87DA87DA87DA87DA8 %A8FD09FFA87D7DA87DA87DA87DA8FD5FFF7DA884A87DA884A87DA884A87D %A8A8FD09FF7DA884A87DA884A8A8FD5EFFA8A87DA87DA87DA87DA87DA87D %A87DA87DA8A8FD05FFA8847DA87DA87DA87DA8FD5FFF7DA87DA87EA87DA8 %7EA87DA87EA87DA87DA8A8FFFFFFA8A87DA87DA87EA87DA884FD60FF7E7E %7DA87DA87DA87DA87DA87DA87DA87D7E7DA8A8A87DA87DA87DA87DA87DA8 %FD61FFA8A87DA87DA884A87DA884A87DA884A87DA87DA87DA884A87DA884 %A87DA884FD63FFA87D847DA87DA87DA87DA87DA87DA87DA87DA87DA87DA8 %7DA87DA87DA8FD65FFA8A87DA87EA87DA87EA87DA87EA87DA87EA87DA87E %A87DA87EA87DFD66FFA8A87DA87DA87DA87DA87DA87DA87DA87DA87DA87D %A87DA87DA8FD69FFA8A87DA884A87DA884A87DA884A87DA884A87DA884A8 %84FD6AFFA8A87DA87DA87DA87DA87DA87DA87DA87DA87DA87DA8FD6CFFA8 %7DA87EA87DA87EA87DA87EA87DA87EA87DA8A8FD6EFF7DA87DA87DA87DA8 %7DA87DA87DA87DA87DFD70FFA8A87DA884A87DA884A87DA884A87DA9FD72 %FFA87DA87DA87DA87DA87DA87DA8A8FD74FFA8A87DA87DA87DA87DA8FD77 %FFA8FFA8A87DA8A8A8FFFFFFFF %%EndData endstream endobj 47 0 obj <>stream +%AI12_CompressedDataxyu' ;i`2cEo0ʣԲ-c0hPRc6`yVY7\ 4Y~gW_wwMo^;Żw?#.? MO717~xsO'?˿y_~ojO?/~Yi77~?+ÏyÇM߽9ˆ4=~ؾq2o'z͏ݽû'|o7wz݇޽~oC;^ >7wk~[׿|u ^IaEn?-/Mv߽oxv* csw7w}Wfa MygO_$fZWC bOϽVoNdWb)S?O~ȤÇo<܆D+ݏx;\-oB^7MpǷ߾}a0M,}|MY)f_q3W-|ŧOwuzhw-?|E/>;,8"Mz[̿^5ݯ>}>_ݏI7tiywAw7_~zowqvKzi}֯~_^w|{߼*}kv49߲z=7Tzċ^}?ہ˵ ?aywWq_;~{7 7I{ } ˿ޏ߽{훭l$~+?}?G/["rW|?߿{_]}f^ǟ|WaHM_~wx_>-1n_C}oiWXWGL4h7C"kH~=CvcΞOɝ=Iow O×b2Fp8o//?~ߪ0J @eYjr%5ۺF|8N11tx-x:Χssy:392?Um}G/?{>vhhBT!gic9I|ML{LOKӔƔSJ14=f;<׭l-qfpṣ<Ӽ̇4~%/2/rXi4i?H{{a:̇p8ta F#~3yv:BՆoVʝ.|;iG=L3<Ι:a~]iO4nMyJS)Tx;iގa܏(86|mamlvVVz=UzwUgno)δ43u6ЁJt&:b{:lG:vtCtG: [:oc]^xo{oKfoSFiIU Rb#D ñWt`CpޟX i?aOt~mGD*D!K !g?0 aĨiIoTpÉ-m!C +9b)a pp +pKdb1He sm~l`"\mfܹ% iUآm [۾|qۊT3>ofyyN43g|܇TGj왧{cܘA[Ж;ZMMX|y8|O 1 g!Jl%۲yK?i弔3aժmfgҕ?uM]rd&b^NѵeO ;+=vP CGەgWVM-U;6m7'"L4Ҷii(G{38Dfg y8vtDt>\^蘡! O/H N]jq]/9%h=sI0cMhH`J7я!L)WP=C_azWsdx3D "R +oR? / 1ӵ<ߐV{q+{('iç>/qs\>O26K!>QZ>=1X0ʕ=O2H)l7_cE/?Կ{7~ILF~OHi;zWtP,#dJ!Ap!-|l?kS,쬛unvMY/n.O7PH+ٻHiތy)Sq>f7<=cs}V/:i9W@/f A5[QN'@Sس}tcQ Bs{V%L@XR") (_:fJ<P8rPcvȎn¯3i"kGzLvM!4uO'TG`@S>t4Dϑ=@s a@,YYDi"B #i!t0pg~Y}AXuL z";K=K܁od\>M7(CYM^\YZ65hoQsRb_ҢjZżVW녻\% UK6UM8rH3%FCكJG AF,$AL%2|`  ( 4"bHqf D(*`Q +T,Gf zby{@ak m;g5Y<LpbKH$mh192P (İIC 㤉:^;C +8494d[7_*b}LB+l;2} +X 8g nb.2/ c6Ik~G#_ f#F?>ڻ S^ \G6pyˌ[Ŵ%ۣ獡Xay?l-0nɒc1w Է*f]EFt&C^j_:u ?WH-ݥV- Xf-mܴjz5tCpjZ$Ԍس;_mn-+}jc,5Zav+f0?Geծ_̿͠xfoUՂ%Vb(„Yw 5U:{hae.&2l8b]WܻhQU>vWlܿ+0wj ']EYy{z{ݪq얩I>ꊐPZ8o"ն-;9R,1V͗kͩKlWR ^p .pv}c6k[ڡiǪ[:R6򶨚w3[w8򳃓#όکLJ}P>d3$c.86#fvWZ\/uNGb6sj^fsmiX(+mܪ穜נwqY>tFA=Ǭf [5Po Le|~ytJ29=-Kuj̆2Jz^eN1Dܕ3ma|&\٨mz &.\> JTq)>I "OqV+j+oqXZrk̈́wXǤ{j 1OMD Ah,Boi +-D"F`3ρћ39P+TXx̓2sjf.0v`%\PRQ*yú]4Ɗi׶ >i8^T&m`(2v0( 9(LݑjȧG#ҩ*j\4jipqe HZ.N d:H|Ҙ9-N?*l^^~W[Z߯~*YP֎^$E!"h>{өiD،j;KטCX5nG_Ա{ +^v0Ε Sթ2Nj}?ؗض%OmMۍ]nEll&/iKh$uPvv)젒VZ`VWQ+Fp$ΕS >U]?WşS >EԥԈ;37c#Dmui}uvjZ_5-l*40J{1͗ם)fq;ea|ϻ9;`c~b\lS8O^xߑ;݂g?!`I4HGfFI_ٴ7yjas -*~Im*fÜe"Ζ1[~x\0M&J81VYdkp :(s9rʩD;qHH8&Y{}f4ӌpf`VX f о ؝m728@[c>*K?BaJ R9}DѝFw69C{8sw Z +TȾ|aAP),B P uT 8y6ܺmsǹōt[n+[߅®^tʩ"+2A3\j~ %'uXYξ,DFwш١u/s2!uL1LU1v DfΣAyVf`"^,XqvX|jOy X<9uS[9h&Z''<͟l˧=gU̝W{gk4.fZ,^P +uE.dRjhmQU  u,OoSZStX,S[gqYI5$$~7qCO2; =<~褚Ѹ.%hwL*f}iƖy2ļļļļļļļļļļļļļ<_bb^bbSݽr MF +{aIw}(}a[QmNjP}4s͢)d3]Ҭ~a`S<#u<~{f`?;8f&Tl 6uz_0aYT"y*8`/lR#c^"MXxi{dwrK0*ykWf S뱊UaY Vt%_M*\GIUKEkqU8v*ylyTxSv{&YwQ3!M+'J5ह2W2ݥ_!ӷ5_ Ԅƭ- hh k˜aQ@;k8Z'k 46ߚukMxM 6 bU5uXMA64뒍`Ln$ȫ[֩imqYhSV)"0 JY(m* ;IMIcFxIč!MxǶKM#G^iZI==ko6 GNyo7\;ATnUr|,LX@u'dd>bauICͭRF:yRw^ծdu4 +{hiŊT8@COSyMDzCWBU-u+T iTkm8e< +/AMђsf=bȉDP[?Hmj[gx+z= $@4fipɚ W2isdD%m ݊{K,;%cN5L%â})W%~E' ÝYb9lXAXUg4 < (8AhugNp*IY2(>1O)pKړSfRXΜhcW681hEgOǶLDӡ#Cʨ6u\`߬<=wl%AKE/ ^$(zIP%AKE/ ^$(zʟ$(zIP%AKE/ ^$(zIP7To4[e(JQ7=X7VS箱*bfϝXYظWAƓYYI]_:gZj<6QZbx Fj^k Ԡx_X42>Mi#RhR)<雨3A1Ufz[$Hdc1N-mqVkTKU g)l.yoOH]=o„\|$1s!, C {T 4m +YSNhKfY0Кqv,ym*kkz2x[=sytmAW$xiKR$ǁQ}n4$ gp +p$k]Rٞ^pLuV`yX'*@n(BA?U0l>J _})5c Ck8WQl˥ZId&* +s\i 3ZkpsKZoÅdy=ƭQ+mWQkUW+hx]{q4{iy@sB_B;9 ,>+PB4‹l JkchEtk׽a;ָ]a^nWV]ۭ3' iu߬k*kW@+z*?WH:1n8ϙk iOo3ٺds@'ssߋ:[YuVYx8nӴͰeJu ݆X]*]5>Zg2kX%*a53kQ;ƱU{U;.VUڡb蘏+kuٵB%]\y|v.dI&%-\K=( +;SX&]?qOk7gS(;,E F1Q5S+]ccNi+vAg闌ho]_EV(\ :Ա9f>JҶβʣ5*'EFdu%l.S)쯺솵&XpawEl!!#1ZZZe6{UY^WYG,E] ^{c\w{n:|xc#'y3xšLz'6rcl:cJ:4j X.{ 6!C8$a%酬VKp>v~X*ҙ6ѷ?UnS꠹uڹn:Yesc-^iZW~v񧻼?HS ϒ/rf%&YV +z]u#N3KG0vzM^y&y/ hGFĝXȝªX6֡eM5%K^Rjv -s^c]9Q2YEQ}KOKҊ~vDtz#zn-zKMUAעn3Ukѓۇ12D۟FJ;tMhV6.^TNzU;]ŗk/mݗ?_Tu:Om3%ne|uKX݆+QV|NCUɷV|[E=8 bohQƣz^< +z\.k|l.~tW3́Jg\wC(U|\MQЭt5ƍ!PA-e5v 6bYqWA3!zYVz:aY 4.5*+ylV|nVE]d_Tm;m+%8$iݕ$(/m] Z{N:,~m@G?r~)$Z`=6 ᇎЕ!"' +a/3f8d8<}GD0Ý6_O* +qӢE"4EM4<: 6?ReRVs4^Z"@H}8~V@%B$L]+yJͼRz:y,@ +1hPI><<˶ .08k}y/iьk$t&\t'1iG8$/n0SǍOw;{d7^X>@l1E@Xkۖ=m垒X6]6jb]ݶ P;\x^U4hE9=ozjBt i97]<_V0[/º:=+۩-nUWzXWh|UΗyxu}&Oq>SE]k +u*WrKu6k~_ЯklJ:t-e%*/jmuiUl끵,'Z1 ;nbLg>V?ׯV^Ẏm2ӓruW-vYI}$X2Tm"2Xm9kFf l֌2lþXdb(;XF0H]Kć6q[Mp1ϯraY+vu.'kMh=O>Ng Mǥ=m-Zk}~^YVJJ=~ -3\fSLjvs"e5:D +i9c) 2HEd! y+\+Ss UqtW߾^U]oW?}޾T>i7&Դ./fX|CX;GqO~_tw}#`}GW/޴ 6/6Ͽ~ͧ޿Çw>|@Z/׿y_߽7_Kh2 $ +l(q)½!vUHL@DZ(9 #'t7L~Bt3;s2'bc(CeoJ -BS`rlua}F15 }<ѕ|Dm(Z؜S@_5bx>֑Pq$:4Z:AP4~>Ow8 L#$'&jؑ3:w@盤 +[UDD^GMD2н2Sh!";dE<أHfz)ou]#~Rem@O ˅viLCSDJyBI"s?ϐljM!Nȋf & }%/0yDԅg]N+e { /@#Lo t\%=1>~_dH#Qqjj~ÿ9Pfk`uf:ݞ:2.ܣS=mB[D|fkLEFle[.Sd˪qi=Y]gP:Q $Bj~iSk~-{gb/Щ씓x# jLE*21Ȯd)䱡K6Sr0'N z'٩!+o3J)wR 0,g,pC_N_Fe'aj2YHDqۧaJXǙvqLYVֆ-܏LZ +#tv !_٭_4 as3X1zcg%C54;|E𝁴F`9Ww!m4qq "F02)+cU0,AncaL0 ӘZ3YkMUWəăn4z8mżj&#Eb08e:C/] ?qq=wcJ'"j='pK0Ch45j0H++4ud\bj Ĭe J1Ft03C&2B?_ \"zN8uͨDr9`@hKk8"`Gfӓ >uCàr2:CqZP=CO򅠨)K}%MĔ$PG`Lm%͉7zO\黽H6oA#Wf;'ve*d?SwjF@|y6tp%b437lzI]./dD6,;aȖoÁiěu,6~ږwm(r{@=臝$1b=i4s='3t>C\b/ G*ȫf * h:YgLj@ F%9=&Coƶ`N,+zbl&8$K9/aPsym9?h s?ca v/pn9/yvʪҢC5ދ=Ǣ}*saGh.x+&`a1AtHx {կ7bj%" ޤ.Zqldf+k&lϠ P&[LYcE* =XɎ]0w9' ӆ-E\+R- kX&Dy;x+(.DWaҺs)̕0Rf8i bR=.̅\'cMG UF +|hܸBֵ}h$C: pEIFQLLE%Pפ0Ա7UC̓k: չV8zTSe }eF ^t4HM5ebi:k0I˕\60h$EpUs~w #%J"H p\Y]E sq+ؒjq"Ф3vF t}ʕ媏͠2~ChV0M<>_5jpMI<85TYnî|K8ĵ:s7#jfu7csk|~xas$V[G=@1Xá5)Q'U9P:Gi"k4ކ:$$Qc%-7h'QSp yDZ8(ܽbα ]w|w&`3Rgn=p]L\5a./M+FTLuU%7ƸP6FxrNϬ?.+Q-%Ʉ:74F>tˑnrY `DV2\ݐ­r.+bn|Bݞnv4} 2,A]Iqci̸It7ZJyrXnu5McWܨlf]t[wIub67k5xḳ`c*5HLEz)<8 KVkunA`2HC Dx g_Y0M\&a1mL,-CSc7 Rc$.RJO7,7 a}`Ae* ,*SVJej |7|WF|1 *mpإ^ו>+{CJ_{pThF|2+ Q^ ~ E}glr  L.٧6.ƋO|N 3̋ګIT_]ʿ!fA]5Yǯޘ.W 1N{}Kߴc0gb=gc/}f !5$NǩLG(vd/# +G_Wcȏ;\b'7MIGʨKyeM>Q_`Gqeb lAE ; 9(wI,Էi!>Yf-$O~VBV{t`+V-Yh-\.hmp1Yg9|{ sTC/axMOAzhp#$:O >(̬A x%*pvypP\^#: 1lB^ ` YsKcG܋r EyWBӾq`lGJ\l$u/tͼAvuϳ#! Zf{|3@Gl_ v!mpNz H뻼nhH yQ ya1aOU2`MI]Md2M+ l'u r~ea0\`7>촲%hv%,yNׂȺa +m"5%rҍe~lL~lvo+֦jr,Df[Bp t!^(]SmKzo6 V%(`RCW%jD7*!"?F̡C g[81JHlۤzU7U&5xpd6h.Nd!Er5P;3Ũ 6r%v n #jiL>Ǩl臄'3Yp&q`"1p +A'Na8OF2Gq` &G#H8"?8Ѵ||Ҡ%8^l6Xc`[8E~_nϰƼ^P#΍\ffY웯w,=0r-4OAdvOOgxE,&b/y?@>?$mGi4ugC9q9I9Y3~K˵vOr`,pdE~bo8pbb)~9)C$1!^]ѧ<8,z!Y0,w  ^|iJM%kZ{W ꕽj:0]a3B 2|f_-,ڃL_=pe O>bwGZ¶+itD&`LA2b}0VH]-r]l}ʬjYهaTV+o?'" ^MQӾӶXm2~ۭws'rl >]6a⇯}Ow߀,5dhqr@`ydcl1H +@W R#=v:$,Aӡ]D!eֵ J 0̆Kr i 4@OM@4; -ύ#Xf 1]y,O]yuu[{ʣ%\GI/C)pQ6x%Y!Sj=P^ͯhk }tXHWГ4N`pΓX,_O0NUf8I p[SӒ^Ri;DԩyE ,5^ȊBj\ly.oypVMjhevxj9p!5y¦\RAm-5 ~<&5':ySO<<Y-5f,ѧЛ~f9Y%恳MbxK3%#*1@ +ub0Y< {!7y~8y^tr)mT52i+S\1Y0hsdLs2^eB c&C82^V#|zhPd2ܖXl##a9E4d#xR_ /vo6PT!83k \/FWzj +#\|π<"W;]Zt$Hb d_:x>\/isuUF&1WMNJfL~K[Hdq F_G{] +%^5:i&1ҁ2=UPHVM wŗ1[xXFY[f!Z | 5"fCtAǴ@@r!.ꋙ&fh!(KQä +J4M' >fΐĽ^膏0ÙHiK3xX4dO[Ȝڨj'| bɒgC +M7:gG^PcPv}3'TQ.H]p`72{lV·26ۿfX *fZ'Z/ MJ ؒ$ $X2^:+|| +` )u0c`x3I8I*|X.D fptLe%kI'pj`i) 1R';Vˁ9BJ @ N3Lmy7١JAZflL: +-IfciD%Ь$ZP}թM'8F0d* +XVMǂ +-yمbQKS|kayqԒ Uhr|5EB e mB/u!*Kp= 6AvGB̄Pqx8F=]ayS>w]j lҬ=ő\->_l1 qQUƹt%eYBA |2vrМeIUގ9TGS۔XU-o$)P o0QE 31k3|dx,n' ^#I#v2os-|4x09©4@9JI{#!b^'Yfl5a[ShNLtb*p.A5!}I17@^=YpJhui27bF Ӆ@=_I+ ^@8%zPwWd[cmz&oG; %8RLC#-y0&`SnԣqzT7J#<ԈHItscV^YTѴ&E]B>X[4؃vN7F=ɽ PE;c/l_vnq,^-ѣz䵳&B۹qu2lTQU7񠺠BD81J^Z?FBRnﲔ廴*5&yZ\HuVdՈns-f.7Z-^p7љFw{ nt EW2z[*u홦Ԙӵ7VN]}rkXnm07ֺ\Sۨ}6S|-mѮ{Ů]\F\A01 -DG͡ہv8NS22ZW)V1x`h;,U]DCN4XB.Y|X4g%ҏA$:2: 7e )U?eR &7ק| *+mե^~% uH { Дpc$+# GNA3Z*{CM[,Eye$ [|i+ؤ)5L}$?bsy& +UFr2πqD-;Mbdu>zjo^6F%* 5$4PfRt=GoO!>%(GX&Bώe/kh(^(J`J`-CKM6w),詬5:Sn թT~VBV*X%NvQP@Yy@X_$%<榉Lpa̿ QYensX.<)4-]h9.os@CL/L2*`%aŶ$ZUI_)} R}'~Sd\gFL@ iyM"85y3x W ʬ"!j|4p,kN/?\z*Ʌ];D rywd F D$e` ޒ2$ŦҨIӭqXZ@mi`ʖL1ee|Y߅7kƷd޻ѼJϙNcu~h}`D9h& O_9 +:iv'.(抬$5#%{K +$4M$0+L$J)zb$JafNc^cFTeamvd C40])Ur>&Uh4DU ȩ2X $WFe6AK$rp>KTR. g{.9Җ4}AR֩,eBU(X_(sail nZ|(o! +Yu>/<;u|}6i[gwϘff +sf>9LIyMMw% *GUOwEl&R[ +,.X,,j~]QKzqh?"-J>q&VwSU]e]AzT0j{b +r_wHWBK+9o'#5,f3^r|dƅ#$`]C!ٙVE![4 S؃)9 G@$%b59Ұ+9p$Ufr(z/G4jD1dJLI-[աժ*DZs]qq"CM?L*y!jZ55̥fODZmtH9{vZOZL(FJVo(,QXf*Ъyo{'[ hOB+/fB- */EͩUh +.Vʼn4NbT=E}'*6םQxeJZ_*bZG{PKUav dUU_&>AJc"^>R)1g+7GT,[.ŏk25Z2FQJFJfb^ṡv^ کvYJ֋O;!NuCԽܵ +g;o?`//<΋7Ћ;BVx~0qi]\~jKw72_wYJo\hWw+7W/YzʼZ}#yQ{KxkicؤJ\dU P K,t=pu3mddmMv}=*d|'ó +:&CW@C^ `9Nӱa~;\]8eRR:L$2'!h0&((4&V#9#iNC{$AeYѤ>Hrh& 6:?i ?s2cD;`2q\Ye ec kƠ5&8tf ++oPPS1+(e-]$ĔręM4]@G#_c8/]KXBԆʇdnN:$@\\ +$ń1YDУ .&؏LMH+B ~chBqJ¿ δ%ϔ ]Ƞ$Wq\CzIY i$Ey\TPF +IU7LL%I0x(j mj5<:!n%,Xҏde)8I)U &L&x194%|L(d(hUq_LY,0%ba~ijc/~(dž >Dƕ픃l2Q4:'mLOb_g !0-Ś)$PRzQ?25p"D+ a8 ڈˡ"ɞ +}x_JrfODW23(MaQ͔*`/^:^$9M./ebeojOBڈnIu9I/rZv]Mqpθ7&jFFr㴫Zn6m̭܍pW` +Eܧ[]tK~5`Jn<\iA01VfVGztE}\:pKQ' nJZ Z0" x Q gMcE +oL,,C2d7PI җ.ϼif0N{ؒjPʴS^| Q-_Qʪ@v[W] R Nþ{'G H(8ƓK @3⦐EB> #h/zcx$:e7]jp6kYYqF NYUflCGOm~e[XڸM5|uB&-ɵ`%A ,:{AvB>|ko^vn% @r8|l;Oi5_M\x"7c['tBe`vW^'NPk..02S]YTQ,=}ߛøcN +hox" d?Z} 5XmR8nUv]3gQسhƼsD"F$B7o#kӃj30lpRagvZ9hz2xOP"P;YgV:ۀc?Q."/- +Ѧ9840., ^q0Sݕwl߈A6.8߰kS}"2׹BW }-V N֬#81-7B8,w8l b0sRqqzϞt.<\,Z)s92_yjDǨQ +!N=l%1+ڊ3Z:Ioc/&W9TzOT˶*|lItW}h^HVΙݎcTmU(@=Rjb+TN:q/DDw +=-A3/hM:c^))A(߿?b6>lY/{AٓV#9s' @L' %@ !s xoA]6z ( }Ϩ,݁Lh`_pHdT4 HR>c'gc\gSMc +9gI'qW(^=a)T4˹/}H<V$F>7[,Ilôr  $6`y#ٳ+PuLZ6rNԓ +/DQ%"g F~8vO*nۭ:&DqVDGQ|GʝC 6}RKeY\~ LN>J^P4,Wj:enbJl +>ZztMbb3ldM_@%٧VgttӨ3l4,,2XY˚_&H~ _̑ +14B(`;T(솳c3餏VLD=v)~2`eƱ[!,slyȒm)JOy7&+#XFoJ>}f%[*]Ki@ B"1]zx Bu{Dm V^:m-N6Xo>77ָNjn>Ku>;FaZ^t,<g\uΌq#ֈ_y>ȹ|suq9IY3}2ɟ9EC̀be? ~2Rѐ]HtHqRp>yR}eQE[sDN=?'hIZBJ{o׳;r> 03kǙqA HbACt2dY}x룪RKO&}"6<|bժq_O|ly??˾`7%ۨC]*~T,l83iu.EY۾Yb9j@"b!>aqQ5цi 9NŴsoi}8pܲô*' HN;lRm}ځS!vyQӮħS3YGjg$fnY4䩧{6C<_OIfrN%~~yohqȊ9of?7IZ,DC^z{IͪHQY݃J{Se_MX-ߪ5tTVD|Y +YҾr+Uu8T9w:ZDmW9ZFi+kʾ__-\KY@M +gJThg\]]VBDr&p,ɷtChĔ@mRB̘TWhϸc7ڎd + @p͗ܨk.jlb4}i_Zc#A DP'-2CMY[Ьd'?aA@BhP!_l=CCFqIڽU|6 ;Bھ$U@v?xkc*+R.4ۙK!hiM&C/N 3eAa1)`Ō$$2B M + {jNܛ + "'n%Eu  oM4O6/V2?m>5*O1+yF!e<#z<N"K]#׬|QYVX/`k;O$8EҼl8%v@֒$ !jIQMEUjLax*Vc jL\U"uq5!s5k1q)F-E^Xt6-ŬF5X&QK1ojdbگm-&} +Sk1Gحbi6K#8j,XK۬pZ!ga-L{> GZ+ukqQЄ{k1vZ*i>ŅJK+Y%(kc~3=RV3psb)|U|{Ow: (l۴ kWO~?`!aq/H1uX϶NgIJq1nĨ/Mc17-ͼ7 +b˚2/t˸#P}1VW#Ÿ"֮b1n~_Ř&6 cbD]׾_MK"d&\yOdz;+jV:K[2˱Q<1 q1|=nTF&c؉C7kZ H1cz΄l`C17R ӌ%X1ϟr+)Q9%IneTN3FTNb=.r*;rfNTu*!WQkP9˱_G199TN`$l"&ǧp1$tP"&+3&Ǘ/5H19SY\e_J<&P#(ǥѽjž|dd`z^I]mz|#0#2=p t GA 뿈w ~g߮gbcn +6 +j[}wi 2 +b^M[*zvC%!ZA~êLiwIrwZ<=mo Kd0Ry[eETRIeNyrJVT˕7',G]oń:U3\}/[ [1'No-@-))m^1ӗ7EMKM 9kIH^8Yt3Z:X,kʇ.dVuBCCNj=GTqc҄ rn,IG^\vNښ)D +;})5ޕe5 ~%U޼Dd_YX"?|@65푲PBދU?>/+.T-^ׂS }pg"!lP% Ӌ +ŧm@|iN}v9 \@l3m + +u%Ep(!P^}y-ADBU+Iq7D=$1ypٯ^m"-~7ǭ--R-}@shDtQFWWƔ%(会M'%PNzu׬*A@eXLIx^" eNЈ +XWxz;#;u%lƒ:n7͆m6FcGdVZ/X6i!-i'Cg̯5xvkM K܇U4&KTs1K(#TlrUUosĹ8YmZBE)w9u%R~ýZlfXr^4DN؄pY{=UeYWdQ;vRrʣrNp;/kgQA kY-)LfwދkkoPeA[n*|[]?ZgEⵋgz8?RY ~}{e2a*b;i1?'h\*a_I-հUiC}ٍ Qu)(SّQ23}2嫥cC )fm.Wn +I +wQ/kӭ$,7O^@V%-PP?p!:bOl+3_^{w4< +"3x[#zkb^J͝e?st3>oJjsB7r^:!5f /%ƪr}_ų!^ +MqUHvfaSty]>7nqYnl-Yؾʀ +q+ܢ<̺ +-=t{1nGַtd޷2ddPBFXX CV[zB3"`9#+8L~\TgEХgg>NA Q*5iĄ݈Q޴1mPUNcH?}gwu x}CnYHg7eєq(lRF_uٹޤPqShU?$@5~eW{r*wF_Z֯W8(>) ־w{ l@M^;w9G|m|m>[ ?Ƭz]߫`LWb&yX\{~7~q0|r0)oHoWQ0K"BN/j7}[yKgj|MqBז;>= ⵤV@o'+ +FD +8s~#JkPC|8 ՘DaG~㏳S>]\p;^`U_$>=rn[(YPZ!'f0XJ) q!pK`;|G~\lLkD] J'},JkT;-YyX|Iw),߶|+_ooލ|D0ל0礒('Y$<yy1{|ϱ?9N{DwpZ` MϏ"?B>e ye y-[ȳ-[ȳ-[ȧ~l!~l!ݲ<޿@>eyeye  Sϛug[vg+[vOoA>}S[vg[vg[vO-oB[ޢE---S[g[GA~kC^mubW#e]W]W Ⱥ*K^aiϐW[#cA5W[)ljTgemWCSؐW[OwNy5>[/hjt5>j?}G WDkȃjK}5N}ue뫭Oؙ{Aڽ+\JAΫM5E֨ -[= +>sM`o ~J=.fC`S +g/y?OWX?y6j SNe|6,'$ϫF[A'EKLŌ%bFJiFJBb*'$(%b: +StS1TNG)15MG)1QkLQטkL|5kL|5b"S|Ub>*S1uq>2S1ucϜ,ǮhT"S5{G>L4A1EjaFզiM2_?C^ +FF&#'m:ߵuBg ^)NwiP:?K~ .*NwiJ!*NwiPb^=I~J~j*23=4?$Ӣ4U?ֽ)}|½w{=|jL:>cta?Cr&;455bjhES$l99>-_z:JX*]]&+IQ7,𵧟,๵k{ BzWvb4le. ʢqv=IOnmvIߪyFxPyet^Y_ꂲF_2lEv @١ .?p=g4Sey94VO[i i}ce yth][>!,iz=!~ątUI RwMj/㹤Vd]M`%^0R%u* 7i' b쳬ϽՉ,EZ*jY_lx4 X VIKşl=&d^j&bBaQ5{qj%R^ $.>[bVͫ US {ر^ޏuODNITSb;3%niDMJq_nl2XllrF5p;V#(YTa{(=jsְ$4v\3{ֹ|]&ġ+iPWc%\ҝ+O(+/k9H _^4?䀝$wf=gh ՜Z DN0m,pwH6rMR&~ZmCYn4&|޼^4T^Eq'f*HHu1?X;_ +HR|V# YŴ`D,`*FHYNF~yٍՙ_g³܎jyMwYۉuXf@,(l;L~WnYhsVWJ͂Kشƅíj^I޾'Ǒ6Lߙּ6fW5X1B~ew)6O^}S*VKM!zk ́qϙvZDyTwnǶGO1+nd{A4̔S1;#y\% f5 ǪH(VPBeJ_TtcգbL\kVi]]=KvZ=Z?G_LKNBH|2/iM7ZғT7qu3:|wBSK*B3mVzk-I[T'ot%Dw_K2ztQ6~wE&w;p/xݺNrZ$wvZm=l{Yʰ@-R-ŶDe޼찳 uyBF ̀ǼU:њ;*J_)۶7t‡mرy;B=w&fASdQn+HepYELK*#+btrv2iT %u0lV;F}$$7n,.sTuh;7 S"12Zb@(Alg ?}XݿXm592Z|ͣhs۴x7WPZeΐc5NG_jP;`a0M}`pܳ ڼ_f?GŴȄD 7ұbn;`%H' ˆrh]P< OuǷ!o{_ fò*% +ʾXmܣmOrc,5 VpU AQ +Qro3S}sbK@*b ] G*ٯ21wtf#12jE]~ŕ޶Up+y޿0`|Y}'v~Os P}e1Vc/ +?whyhFؑ6عS74MAk*|-!vN;PU\yhg3яXGLߙּ8+=ZEtvEgw?bH Ei~OWa~za~hP4ʛa|Hc)e ֻQ{xCG/`-sι*[uFڦsᒼ#vPq?nLl+CYEy<'QIAފt3C%VC̸fS@%MJha'_4҅x j8|$ڳfK|nK 7 ヨ=ou闎HN0TmցY ot{Zo{Gj|X.YG.;&viR5/V3xo[v;& i8h#[kO`8pH>/&|bc,/,a^|Gƨ{>W?Q3zl|Ϧcw{Ro~_Wo4!uٍ<¢qп7_?W?/_uΎkjE>ᄍ'_? }ϐ_Շ?k>W|__:g_|//~87O/{4o{ }??K?>s(W/d_7tſ?_dos ^³Ȣ/~bR3ަ֟mſ?/~amtm7}њW>m>?%ӟyX3~$/n':tĖ ^g w:fJ6|{aM)1kFoݨy5M{" K"4r e3 SAd;CV+#R2:H*D=b*,Ey 0s;VDH[jkJņBgLע߸Đ4-.|WE; ^`eJm|1b/{#3+BR],JM s("J-Vןr0u BXUϺ&ҮrD wJ}uso$`K@׭ Qh`5Uݻ2AޙxVUS|T+; D2=;#$G%$^74U֕xnCtJ G-Q຺lT HztrWê6%H+fi y|ӻ[h/ld/!{U_U3vw&u8Ԝ$7 ý4.j %k= mՠ(B>Gj7,J(YnuItNJMK-4tD[LhxPL@MF3ӔiC +^قtٽ暠e@Q- 3s^,*· w䪁_-pg<SGQ4l޸k񾏋8~|T$*M#i]G/75W#3ENA /*KjF>],z[|LE%ih8:ST)H~X(uSg,|uQ^1B<)?tw̯{ ^ej!?B5JՉR_Z0"WkcKfMJ$$RG!^24lf4 v| Kn=20.QԽlOF9:0hsoev;zOd7ZΦ5P :{KVd1YZ> >XR0.T yv)"A!۝Gݶnx@SxX>\{dڝZnqBY]86<ֺ3Py\N32E0h@NWt|#'0 +VT 0ovi|dg'E4u0pboD> w˝Na^XFW4KLB %q 3.&'6ĶpIT6^[/~Jdc0]J[\檩Nm7GX|g|Wx +"U.xiN/%8nm*m}u)HaCHujj4( ŎfPlhY%@j/5|P(s4 R5pmܸԽqjNW==Q6 k d@~xY$ܗU^LKmQp,oxμ\ y(%>t1-{7eu6yϰD=@K/ƪ UBEQwZ;6j۳.+t`%5P?#j,A G,74\f/&5Uv;y ೕbWmrS̶.!ӼY,?OAKW`J-fW>"YZf,_ڣ-[nSnY$D$KDӟ>ooPucVtc{]!e>Q*|wai[д_Um͕CZ>z408.YU#"PA^F}UdM҉ʊ40(>!Ij5L6Ym2Q(EZ!R(C"6\ĖOSQDga’zENRbp=ަhR;D~ +ķJGJUQӏ"eU%CI9>kK8e 8lnޑAE4IcgRnfU, +B@ę_#L{crL;Uc= g: +pQDA̰R?Zn직I氓GH]qy\-F J>ՇzO1ڑjK-DZ6v_q+7ahȱ=u𭟄N!"]*@>W¦n:mzz$jAz`(C3[ʿJ'ihLXw-Jř)G\LH,++PF,z3Eqx֖]DX-/Z9V;S?)hшvhE"Hȶ.NOoޭ,ÔHO)\l +reGwLgqU`WI3,njUie}C"&Lue) jP 3*Ӹ|m_Y,'TXa^tAZcT2ڌ8OTJ/);Y ןPTnW8GIW2(lmv8{~VW1\g!lϼ^:`b FQ\*dLgi/,N< + $j{srSWb4.x(5$J󉥳A")ǍlŒ>u+i5)k*RmSTQך#;"> +0釯dSjU9EMzSyCY-}l̛rqlӷY,Zkų6\)πZ$R5A~EY*0?ŷv}{XӔz #eRV0IZ7(̭JL1>\cmWVUD#`KˠOAbdogUN|7AZzNe&-LPB- &Vzq.fTuZV L ,$Zj=l+X>I\$g]l9&=^)1n(RuY)`(ݽTԔ30 MR觊, x_jҶַrȈ(3\|d Q줚{P8׶FpV vl#ekÊ%e4옘/1jӌrEBTnV> 5R': +)KlFt`"Rl%lee]qB0u<t@ 3L. \fZ3^K*ద"]\J' O+*qߧӨSJC<_{x DQD$UIVb;")b\K\F;mA.y0MX܇1ժ3i4|rPI^*b_[8CmPlU/p$R&^UtK9i7Ͽ DhVT:8 W<0oaR;8rgKwZx:2P 8%BY8%3}H|+YoE*1NQ>I60ьؖ)r pɶzsgj{Ж^mN"cLD'~e U-\bXfFڷQyEq(/%&jBҢv@?lb|MRqGg)jr\Ȣ,E9byː9UCN/L²J(+it{S$A;e(R3NʣԶC><3bPӯdΰUi>67_Qh͔1hakW^V\gYi'!*(b,pZ)7}"*py +_8Sf!#@Z*T)nDe&l'T +W/ fzH4ͻqEVҽ +YZcJN\;xת2AbΕݤ ۼ/zBMA tQ 'ޢԲm^dzB( 7&G"SIç{{TV hr^gʕn^k u=Kz r)3a+źSA毾ʍ]m6‚㝹7G]e)K̝30戤f$"$U bw:luO$,XC~%9/r(VUB yWrp7ETJ3>m]BXMnio+,x#wS4nRLlL*_,iL~ OIFo(Nd!Ҙel +?Ũk5>iE[ IsZwE9WS՗/#x][զ( -yzsbJScS+Zt$ +"QI0T%c,MqYf]8W'Z/u^ r;p;NuDԪD!u6YEp~Ir;x +׎:etNk_[*>>?!tՄuD +طn7'Gaz+ڗWVӃM@ͦV! b#"Ztġ#2b^_Umd=mAY= oB"vK4vBWcCeoW%hS:f#ʔHh|S>K=; ^k{VD.n.̲ YVIKKt,;SU0RdKMdYkAќR8JbޗwCN s=i*Vng1vnKڸ `b@qԤa;Ԃ> %1hW!r&R=^QXͫ*wYqea +#]][HKfUYdk"m_ .~}[Fj*DyYt/Nk'ɑaT^0 wJSIxbc)<iKA틗ՑYQU5Eq $WJr+/ʃp ]Pur`K՜bt>,Lb^$?`U4P @J c{"mjiԮi\UIhgqYrJ%ȁJ` [Cu I2P.e }Ϩ$_/Xz6%:_k7QF U(,-JpŬխW3BGH oBqlSVå."BQ/5/hS8W6/3GOdVo-S""KqnTʪF HMpV#جSD03/QNb,CDmWiD;Vvc6f]EK!v,i 2"V)0kWKJNtaNZ.tsi|"P[ M癕kQ#&f% wKVP3B.q6Zٽ4iU",V±mF1NltXC hK/71!RXb٤4 z=Q~ԱLBw'mCT~:` 96Ke/W"oVI +?i|+MfsTę9; ˉs^~Uq.ߌKݽ +Jǽv6}"Пn')S}/sKo-+SEMÅ%CjJ;X +}Qvvks| ZLXp[0U#/FEcJ+^?5*=83H)PUPFZ8vfJ"A_Rau s̲{wz<4Ji!u)ʰiæۊ7YlNtNM+_u Yn]/(i]{H +%n*qK5zU|W,e᠈rzT~=lTz2estVđ2ߴ[u;mM968Ʃ?6YOz+ê"-MTPv;X)L+=0*4t듷ue˄QBV*Y"'Z8QXvqZڏ,^Q2F>/ +GxrZfU$Z8e;G^2iJtWgqJ{RA6)ʥZtjҐau^nC{GAq.9-LVHߢi k"V/Z{G !h%&A6%#Lu$Rc +j'U?WjVp|'/fqu[f9rFy pYbe E4Vsڼq%Ra0wew; ?Z tB 48[#J<0@/>n,YPEvuɢAFkS\ +T4sXͅm//(Sv\z I1Jh~PhT#3zԶ@0L콅H +#x3zXtpMm%@N[ +h_MCDWkct†kbTosy) S_ܣ*)]1КKE򫧯DfjԄɸQjq-Mx(딑@j68MQhC],_,S +P[w{ PdGC%Z\)ȥmjZJi]+Dd< hV€tF4̧t!!ǺFy2if~b$ +Gz~Fl.Bջ`n*D¸:7ɒR|}ZYvÍh@wA=nmWQZ%PyȰ2,{: L։@&>@VR,dȅqI'l=@Fm& w5X@@FE [2ȸ@v`%Z0d\ eayȰ2,3 'GbS3l n {y{LrpdH[GVcd<7 9A&sf#LARf2 Ȱ2+Sx"PhdU2žoYYCi&'S A& à@&X0FrBU:'Y-uFU~҉ eBU?d AeFay#jiY-AvO *sBU5ZA& eDax#Ȱ2,3 AVigdUBA#(??d'd2l Ä ''6 Ȋ;\H +Tw2 AVz12 #@a-d& OYBpDtu[ԷH*w@aDugJ. c&dz( ddis~ǀ +2=dn3LwN{"ȊZY"d@ax!J-3Xs@uÀ Kc ABFY!h="0""IAV +Ba +lX Ɍ +W{#0Nb1"겼d' AV<#ȰM2F OYQa@1L,+U,/ AF΄ C 8!$4"$0Dd:iDc2v #D;&gM2mZl q@A/{ kdidi AVQy!ȪVAVNdoM2h'/80!0"2ɧ#kFUR#, #AVًvLk@mdR&y"dd27 0!Ȫj2iB [<"lL<dm̠PQ7zܻq@/#gB W<"0"vA&"AƳd8A4d`_ʦD6L2ędd&Y=A 0!AFrBU5}2m^2&3 A֍ b2yo`$a dy6tH > d#q"u@ KcȪ6#L/D H CE TH dؓ@H c20a Ye$u{rԙ@V;FYQa&;@v{3 7I m$a A 8d d2< d=02~ +H +"R7S{"i$adfd: H +"@FE c0Ȋ:, #,I r )BD /23 L +xȰN2 LGdEm#H$Z)_2 L Odd:s$_2~D + @@ OE +WdLda Y1}$^BD 6Ⱥe$ V'(tu~Ƌ@&D 2Ȱ dXg,L D R'Y9ۛ@sp|40Ⱥ5d#]@Fu&)8dD#, [@V2o֙@e& dXg,Lgd*wp&uH *Lr iydef`IY:l}$1 dpl&O֙@&D ә/H|$ayȸ@VȠM2Ⱥ5d(|Ȉp de;2KȊGNL #"qH +KXI@VJqT-O2Ⱥ5d%OA ;_2''@&ˋ@&D gM2L #R.': dȊ-#o@V5d^Y'dX«I c9@ "M c d%fYn L #" dčf78L 7L Cv&H$q"H KH K4$Q$q$х8^$AY‰@VA qI tO263&ʚ2 3 @8ȰL2^2 d/&ah {y* #@3ȈldX'd2YG' @\ @VL @F4i =ѱ<ǪO=3Lhq13 ?eacjcܞ7va⏁|ucb3L1?F0l握=ǘf?F:xfX?u2d!2ǔc\Ą7~n?VU<ǘ1*G ?c|1KU~idaL>k?ی?<ǨcD/1?ngQ'9ǰ2T3t u,fC޼,e3̶olP?FcɞcEO?V"'17Ss)f'c%+NJH^3.?~U;~L]I@1B #~ N2w#cE~L'~̚wv1E46?VR4c$oR~EcEnjSm%~ ?=q}?6[B+%*&~?& ?uƏjF7~sƏ)?X1Y' ?8ǔMcX1?eƏዼcEX?FgQ(jc:~̌2"Ό1Y\JN|0N1 }?1Sg9}acU>M:t',OXUsEcUԙ>> c*Bxy>c9Ǫk*>FL X-˃>ƙ3}‘{*]}=(|01Ĥ:ǰ1,oX 9e}xC1oN1]D>c;cc4!>cL})gQiS2J}Hӄ3x?'%^1YG>Vb)F}I3HS~o':l&.Ez>V2V15L1R(oΜcU1>GM*`cU^HS"IBC1z6fo'1h9xǪ(#}=aYH2ǰcXGz&,O#}Z#}L O?{.L1R/XR>1M3>F^}1g^1Rr>&>&B>Fx陎1F >{0ǪFc~T3Ǩ c,/ED|j6~p2&y>/+ʹ1#𱲭3|SW>Fh)0^q$SISj\1F[8dcv>c&|+cu;gS >?x'_/k|do=#{Lu"#{ Ë=&ads14'Um{IdILqvGgu5K;WLRF1ʑ=M1I+cE'z#>JG +V&,z̊Z1Y',zLDQ23x 3z[FYGX۝1c1tF/&F'/XjQx@ucT2(yǬNqDUncce @ͺOtFc*:zdc1 oXQsD5G7zL=& =fC+Rc܍=6X`uP'ro[''Xn 0o`FOc2:Ǩ}cEoO%3zD'@ǤBIq@IqB!:ǰ1Bf'ؽ]zinF>kB =6X%'1u1 3zLz̬#zL =f?+c3zl n XQ8aD=zz4uG{/Ȅ4cuxǴwcTnfz\ی'zdFB3z=fE#z,Sz tBU 71z111yBqq3z='< e ==@'zhB):QcĀg"z,-#žzLE#z=qBac8YOnDե4 豴zL=Vq1[> +$X{@1 =5x'Q'z ۈ#zL'=c21 z =uX{@q@JsD)DQO1Ǥ;Ǫg[G#BcKr1y <<֍NSHӷ=cJ1 yL'yLƁ<cYo5LjN1 nc)`d~T5O䱪‚yL'ycFX7 4yMc$1; 1էy W"AKcǤ7cg8yLcO1<^'yȉ_qQL޵&W{ P3:-$IjyR߀$D㍖eY 2M* y%J 4?F klg +c㸧r\\e>WTVV_=x&y >'g{/_w#!:@~QG6 Ejך1=ȯfY_ zvǸ2 %q̡|?0%ı{&~I;/1=G})m.wKnHϻK|%?W]b| Lgj|V^k]G9HɎ{-нt/;7^Lg| +.fںeOE#:ދ-{-ྲྀjx/z4Y^rq:F {O$bS}+K8RQJ{m5^z{s߽l*}Y9k rKrڋ:NnOd/^m%WMB9/g2(f׻^R.VSA WL'{2MAJJ"{m5^ .]g +KTlzGKLj>SԭK&2^wE%|FzaqZw$L%BuoD/ gxc_⦁26* O BP.==!uKXYtMd^z_uQ.F eܴ.R*ch]?BtM3ZW{=[~x-Ẻ^ %= J@G K&Еi] zȃ֥{B-`к +K8u1 )='TTമF[UcwZU[8 ᵿȸ.-SNEץo,K{PRBgQI|A@]e( X``4mGKoE%%qpЅn@C(_ҹ|.s]2|C0 <:Jw;ṡs;Id.v|r\/.VXW˅73FpU .AO*2EhpH~m. "cX,\(f2_?ں7o s'=ͥ+F,\ +Ruh.'ţ1Ϧ\Vd vsɲP%/( ,m3\A(v(;24\+ɥu\sNjMd=wNKwgISE|a{d.hFggYZi߽Ptzs5RY*ЉY ΙYTe\ ,Zi/ X̨],:ERռfe1Y8YaM8&3digo"1N)Y,T,V@ ENM"[hQl=:0Y@ξ)YpkHgJqrA+k d3_dUU)׆d]*,TbB!,d2$5 d5=}T=Ion HYsv&ɢ@`dRZ,)$Ku: $ l5"';#Km7`3,)ω !1:@cd6QtFΑfdn܌,oƋ>O`m;(Y5//HC@P.,uHegː,ސ,T[$T>nd5}nHVJDT%[dᒨ`@HnE2$ [юpFi@_I,))YkM2CCMצdIKbQLU[,TҀdaE YR,!Y͌dѕ|wCgeH[xwA[tVd>stream +87#K`pȐ,^?f*R!Y($GQ[PP9J:Lɲش-ij9FGARrʲr` ͈݋ʚ1$S9d)~eQ zJ256%AҟdJEؼvAɢ)+(Y~J"MDQP,qƦdq&LR8OPqS,RdJV{"',k)YJ]XFdq&d!,ߢd{غ$(YU37%-kd))w +LYG&d=tKժEiJz[4L h"Ԫ M՜ShJ")YdƘH7>(AZ YX!Y(%1dYB1Yt90%L~ d,2ůgm]AYeNqP>NplLϜ,S)n?6ɢCX!qQe +R,s2*KYŝUW}H`&ܚ-Vs-A1yUލҜ (͸iYPY(,+٩5q=XYYZo V` *뙾bpc@%j%Ё^zgdq>Di諅" 3*Iw5Gd!~➦i΃`dQ?e0W%Y*Fq⳴O FjMYY< D[gq%D SS~-#4c,lpW39#ڌK$PYT9# Aٜ`d9G2$Knb +~ KYXVeFVg3Y[H-:# QuL`[0"V91T,͚ȒYtY԰Pk`dIĒY k!kA$rdsv|^YK(=ddu\0* k ȒY 2 @dIdY),,Y@ûE;LB=~e%"!;x'%BQ/=_1 di/J-Y̑L18! ̬8hS"dIhhքiy'hH,KɚEXZըf,8!f*%3! Q d!沈a9 Ȣcftq2K{pbҥ>w?C45D$Ṉr>wc/6XTTuޒKjౖX[|:DGzc;Ҿ!bv" 7 +& c9E#=2 : c +2NO^E\'ca]imycOj븼+XIIu [ 2mb^B[Lfca@L~hCsX[ 6|s J 8Xf)!tWL;5@pݮM1lj:fRp,6^}E2vLBm=JmִXDN  e:jͺs\@ AȢl<+yFbdMmD:Ê,X['8yy]X4:=40aYk-uP "lPb[baD3Ԟ%f -$m(ҁXn\v'@mn%94, X  XXau; LBa)QF. k#5Z: /Nai^lYv0C?4: +)W!c牺дpޛ{E^m%sܫAY"Y}^A/x +롏mJhu 1JV 9J:vؕBf3XgGLsx +4gЕJ-+ fRxgW-^qux=^ML +-V,bj+؊0?_(T-!V[`+Sd mH;Jeg`~Q.)P+2: +SsċV+Jz΃h0Xt.gu-qT"pVjhe"K8arr^読{K*%X8/VbWx6ʟUhn4UfxwF¶H!.MR{YUPǝcLCy +ŮP" kxwt + ܁cWpWyœˬdaP\4-OX"Wa}rUQh3O\Y'8J#=Wѱ= pۮj+\e,p z\_Fk \Ex]g*V[%-}pſFl4snj +[8[*AVI.U8}'Utk*Ds[Y^ԋtWOlýA3EV1[ RRX$8?FuOHP2jARI~=c0{U[GG¹UK-:^]"Uh89ʖ zŧex$W!U[ v׽TZَ6ӫSW'ZZJW-1UsѫX@VU4)0B7U-ë`Vp(\kYJWhA;S\mV, WIU[ XU^EGk Eҝc*]a1[<Į +)U[ %t'8JEF'tFJ坍wAWXy + ]#,*3%&vbw2*]! u1{aW+ibW U߉]eLN"EnBWYGzuJm2 +Q:stb,2JG1.:$ATHQ]P2:yeeU[tv) v*dC߅BP'u]瘱!e2 +#-]ڢzvJ(plذ$]utK*DN2J-uXɃU ]ZaQ<*qyTYRp\m'q[WѮ`UԼO's}[hailPwָĬp//@V=Vu(@dU)|nv):*]hUK ZXkn_XUjx帿R U}BfUMs:J +mRf\SK;&U |`aʪWEA86jno`Us"rc`O; &59jbr1+&{;1?DV:5>WM%im Mk ]%aR, `5T!g)2oLJV%ZVn9JNvC%q$U3 P[h@XLvg aCj!XoGD=F1NlMWJt|mpjGh⓺vsU\,=sXZZK)oQƒU*5RI J8IgM{>ywVl/ʙVKUg Ic Z DbrXؕcЗxm|GBKU fZ +@E k(ɚn8=OTG?~ +wTD,h^N =QN^% U#g}2UkWnf WX)=*vV27}>JEPImQ8,X廉05a Kc/f)á+j(bueՌwSXx~_2i?Zsb+ܛU-9b#XMb\:U}>Ng1nE˰%󓰐b7}/!K5*UPmцƙ|yUƾ[*[@ +N +ho~KO_硷^\q +'434Rz\բ@sFEl~Oa7:d +}ch[ifl%4AS8[SZ^hK'av#_q[{mMuL߮/BTafF\"b0嫅9DZR[XlUcʇjݽ \J)VJB?=D$aŮF~Y" +։fݡdauFw!F1]?;YS!!ZZ#e>')Rj-BQV$ +#̞,n …jC++΃([2ymRk'qWB_qNU!MFxI"aY@'fFSY{iFqID=Y(D9PXۨ0x\Up&Q*dY1B.X*ٜhۮ۩ѭpp(MyvEwqJp}@^oRӦB9XA]D;e.%IȥmǤ,VT.\[P{Sd+(R^qrDyYSe ^8"ݔz,qQ=R,t*2ч_Y-OumJQ RwMnvϴ =+܊)r46ߜdUk}IXZ iszwbv鱺#-v ]%%you‹#ҮJ<,] +}֙Y8][UG4w8JcP p[zZ>NQWm"dKnR\RFDŽۖTjAZÔo;;i'}uFO_яdq\cC5# 8-f߼{3mBeSgN9KqXMkaλr[ݛ쥙uh/ͽ ) +͜NT!;r"TWLjp|cyʝ ; {Xq ;S<><qC>2R/%[Uo֕R{ +2UsE\^2*Ÿ8_Sg,ؠ/e㖦ID..(7ʹZ P{cN^f eRWfp(EEyRk +jT)R.[wNOGZO!!m)BV\÷]y|l'"uxMQy4 T[Z)2o;7c!MȍFŰBt)f[) lU'rWlFfLJ 3iQ!Z&NFz]G}G@qELZiˣLڄ0E^PÆA"lFRO31)²ZhEwbfjSԔ5%rԴ})Bϓ&7L@~+\Қ%:[d)i Ykgx&8N~Ra |ω)KcH;a ζaazs`.RWw g4!$7.%oEH} "˜*5)_8IKk;)* 3f Im v'l +Z^L*]ߍwK [ib_WZ^O'Xi6_e +ӈGއt5;<յrWܒil.T؂Ʃkqn)8*w͙D`@zXam*JD(IU T[uyid6LT}I"@[PMݕ=*`o/mQ6lL.H50c6ZNSmCǩ;0!,;1&542-iDeo2vKవWe((6HeJBrL@BU56=I|,~k-ޘvC;^LT{ +V"RȤ2@VYpiQs+BUm'P*f[ f봤౓z~ԴϿh{+ ubR1c)7qJH ~ UKN딆)K!=1?{%3'bߘR{핂F(2`wF(@]L<l!gw(Q I6 Qiinwnp]jJ'μ6IY=ķd&\&(+pQёPP*SSB}Zo\lܑKWў}#'F%uy5/j{Tg9[kLD=gPui]cӦ%+:5AUpIܫYU(**j Cn q| PmTAIy)_E֭Z,8R$Q  ,DMO/7^30B4)C;pJS(TTj^T:Pp_YL3AA ,FCA2=5Ǎe`3]ܵ]\0k3xW: F*LC;gT:Zu5$H5XQBi7aŰY?y^7U=\VO/T<*I8u)iu0 +:.bhrza*7*xg:!4~Zl%X-j#@uou?LQXZA : ;AMA_yyHC"W5)18ޛ6lTv8BKd )QOMtb鳀t;fs8S7Se./jG\N禫 l10."k.yrs%c/ Z_~z _3q97ӈT_[ DtK++KM^ٓ|c.tX9Bch'@:=?†==H"BZB%zBDrzRRtV,_E RB{{ aOUU̓C&5Uvf΂1_$ԫې|`ؔe0 WlU bhrAݷ\K4^|8larvJ"mo0JYp + +F 9vU@RK@.z*u1VJNj_]:I[ɒnU Qs}`n9HTD<0Va;mPCM Rh,PlS3 Ħj +}z3)gjIeFǻ`K9oyCn+!'Z;/6P 籓gO Zk춙>WRdܙTP +M @6U>A`-BdvTBiBv$& k %հ5j BUx(?}4ф93 :tU(WRT=v@&i+Z}Ipڨf`* EdNž7L$2=0<vEŜav̊ydd7cQ(k;[x?hYՇ[j\&ؙУ?v\]_Exvk\6-p]7b咽,;#͗X-y NS^UYr$Xi#YӅxކaಈQ kl>;n}>m˻Ϸ6NlZ2a}UW[ k4REC+vzx?¼RNH4_n' {–sX+l; $/vv;9p5 +ڦ ֵn,-A<`+~l㻝r rT`^!o'M>Fc1Po~oFhCà^A }w/"S5$zQ| 7=Nl;zےnjETUb~ڇX f;0PmM15*S`=ƽMr2ք (v^LڝgvOJŬZ}y* %M*Lzu !Q(}#h5b5!/KAS_KEp_ojxQuJP_ oJ8]T%ƽApvB=\4UvߒO8BMXхYskBzVyCRN`ɘݻϴ]i]DA9_ 2@ r[7u?lo0gTWmv˥oT\ h[,Oe~7 $E3g>n% i4] 4QTd%g9o+}ٰyl_:pEx…O },TkQ<81כGTCęktzQ'^>cćO϶|0f޸ uUΣ_Ec[hM{@"?p]uKYZNxj^ +XN3ŵDx,g47_ b (ww!T?i$TXn9XT$\B8kf]Ji;anZcZ>F/=_aV4RK.PfiE)wq`JkNV b*cݴrӚ=he& ʶ" l^Ui + q㚷*|O1R}JKS +;Zџӎ _+ $J2'ۑ7U'ף'afPbrZ=ꍏvI5ˌ)WW m)e5{r +U!*[#=Nv_J<TȬj+41-{5NOmWT{wk>V۶0'imP{z$x|=0@.JmT3?SZET7yh/bOׇ71t]6 m(8Y*eC6dn }-űr48?yqU[6۾_.R]JavP sK]670TP4D.: +<7onIFs`[mǽ6'Sn`7x +7'8rN'\j=|cv#>;\q[饔 TlO@)af +͜3k&`uH^fFS*] Ι;Q[I.E +GX5KW3E3u:o?=yLCyƍά,7VvՀ͜#ΔjfGtN^|9vN4~v^ۣ30F!ŠE!,{gk#*  47uN9L8eeZFL!4;7M R̷ږ(scL} ֶB[U3>,L 8zGFV +qk!+ɸ&'n^ey-T͛ns=}>]DŽTk $*]؅V^e1%#tg̗6 +jyf:Vqzqj'R xs*,Ҳs gw@!y}L(f~s%7dhn5cTJj.H3"WP3/ [PeÅNۅ+ʦ_u.\#qSx6$[շ%7=Hy{ɩ;}@Cm##nvėPup]귿+x>$BeD0yMSR.ˑ>TU6I蓤(w{&{Xn* Bӽ=K]ZJF%%,`Q^E0lDZkm:c1SXxhX<|pVf|heFNZÐPr}T\mY!! {mn24v+Z^*5R-[8ubzX)-4^'Na赎,/*QibD6)7rKč40JY:TZiS( 6 + A em9P3Fz%ncxv +u&{]JQt'ceh5s+6;F:f] A'_^Z+ԁAQ."ye9+pvgV!O6wAUG=9(QJE`Ki׆%^ |(=ᅿ}x6؎̾EY"nBBr+?/bnу<o0yIaÕ="= wI=݌de@:V>U;Kd$O.e.j4.Н>YeJ/{] +l`mqHIKVڴ^s4E r +”7 5cC#Cr7Bcb^YZVS8{@z}jؖ m( (ډӾޢZqZMQ*Zz)iI}Azfv^*v_,5albւê +BEã㪻8D< \2"^xbDӂK#Ux#+rx(K/O";E$/2Cē{=KAT%!)Do"/D$"V*Hw"J?o+D#2܇J!⡌+8CēZxL&I(-D< +o+T'/T"Vu_"=ej_x[D:O/x` Ou%m[x8~xdi +4}xR+OJ% x+P>DN$4 U"wR%:_"RRJēӗ'p*D<\>DD(.DO&♝BēRxRD@~^ģأ}x )ǙD23Sa/O +2/) u̝3 @<|xR+Cb ( (M[Q:I-@< /83OTSxK@%3e &t@<\2OCJſ3/iWx_  +ijxII@G1q% jI@<)_ ɠ|xR++/x{^Ъ񶒁x[ UxM ˖O #fZx[@ GGqQWx= +3+ OC^Q\x[@OD O +%IVV nSWx 'P +3?@<{3OJU J+o+Ɋ~xz+O/BiV & +SaG42@ d ^RoSp7?@<+|xg g~x+ J/)EuIGxxZ}x8TxV%g%%@<*;O_[x[@OOSH Uc)@<)FSxjK@<\?@<L Ϭ2o+UqxXx ?+͎87F;xWx~x<t@<7 Óq/ OJU2\SldPyx}yxBg=ȗ'8,$|yxG|yx9 +oh;A(<<ť*_4< TixVK^ixLK70)E 3 Cã. + ?4<-b* O_?B|CÓzQ48< + cp/8<ݍ#,)Pqx<tYdBAGDVqxtQOߤ?%j!u&]ÓVJXT;B5_BT,_G%ZpxA p"2ѣ'9f^UuGIюã'?8<* p"9_öPT^`"CQ iiB~i4cUefgOiS(4 83\/^ᑡ8< ?8<"X2 +gF8<g^8GN)[qxTS}px:G#ZpxjZ8<;3_^>qx*>8+4< ]1ixԡ~hx* .c&/ Bc /4<&1~hxAÓOY4<) ŅlЬhx8SѻEo\ /GyS᩵ Ek~ix*64Nti^/(4<~ix6.dz.@_X}hx$ _٪u1hx>4\.4<)_*'4  GBf!|XxYxzF+ j BCa + OJe :[~YxcGKaiX4?TS ۗuBe 4?_£䥠|QxRPx|/ +O›QxXXTh;($VBP]`Ap E!JAIf  +oೠ|Qx7ƌ›Gl KjXג(<Qwk@yRnRg8Ix72XSMxD&b$<vN“&!$<w/Hxz,|$$<W^~E£C3,$<å@u1^h`($<$<ۼdueD۪I}%=2 BCL$$}\&4gBJ&m5HxZT$<,D + 6&m!$<2 oD"!M³%e" %I,$PHxfuGC³.L{-$iVvVB7 K) <GZTAx|T᭚(* $^R7V+wlAx'˄/e[Jm5@x[L c <)+ϔ_V3c) eQIEdEt(~WvZqG2rdѓGg(}%"+Iv9 J-S@vڃ]9vl#9ŎZobsyotr8O"ر7OR\FGbiॎsj=&@t{/z߅AˑNG7NN7H~ȘnM݅,Şkuޅkޙ@,F6ŬY@(ao%5ճ<ǵ tR tR|@ה0l@7mǒ@)8z6d`Z.94ʅ?»i @'\,+\fe&- @.? @װKsj??qkohInٶs͢gI=@'8tmPuV]ï!E3݉ A:!ʮ ˨ {簒 ؜9ɽ =}N>}?='aԿp4乓6{.2uwnΉ(wtnc!aD!i{P{3#nnkF9U9Q\9B z*WNlJ+Goʡƈi:83">Gd9=Nwgd9^B&K=7Z{==/{ۗ0X\`9㰱ÒRWOY>ٛLNCog嶒r[  Pž֔]k.ub?,xklbr q Um%`9bf*k*Xt0#s, jy85Mc+qi͓8Ї^S:+_-jPGБjuȝv*#9ZN -6sZr+gD +3Īt!R[ ڗmTSx?-z7'mZ.) -@5B2xgEA`5;iC r[ > q +V *1M3S0ja;J-lw4Ng~m3`9)8OMO ,X E1@Ym%s\9nӟ +|`99X+"V̙,g6AD9-JV"ˑcz ^VE,LK:od<IO,!tfEbd96YW Ʃ[><؛ue7 ,V0 5~d/T3m&5XYoDr͈fN + dPR>o t"L۪Q䚐#*RLP>"'9㸗VomUdMNCyJ3Z$ %AUT[P䶒)r[ ^dZ3Rl} +,9Dn !%g?gjP2F{J99q |)zwm[IUmPNGa̙2@Uo5qf9)kJӑI V2@nk2~ý5*Xp:Jȟ+; < SeZw{g4t^^8|+_4w߶]D8puTGm!Q8eN潑q Uq;l`+޼RIQc"VymXC+'_L`[ؠ9't? O] KLpwg%D69'fiٟkᶐpK 0xsߢ];04 Jm5p'npb pʻY=@pl*3-$-3{)ؼp:=Upڱ]{cᴹ۱]&{ީjoi8$ +@ ۪ᬚ,>j[e0ݖװ#;K}6 +kÌ qd/0k S|`8l`8]~_ +>pIMT2S#"ISep賒Jp +Z`qV2!ޮM#(?C󚷴]p N gi#P(8|TE)d8TM Gq6pVwW4'&}khxV3d[ I^ᰔEG8!c/#:/UǬ\8l}/.JͅLSps gf+n+̅#^Ow hX8 , j$ ~e‘)^aQD%p$I*o˩p.+T8T,*6c2 +Gy+YIPPT}\p7kS%.p( X8;^5p [p6N c(e(w߃YN*NM͡pn@l˒3AAeDf,(f>BX1*)Oi:9ΓGl-NaV8s_φ` 6ͦ41FQ{ߕ̦% !䴟\6 +M1fo %R F[ 1xl?Yql!9͌Q熱)`ib#m#0[N}D2 ƦĆ?c  7MgKUg[p~/RqZIتٔbkQUXlKņB5Xl +>kؤ^4(~3Y)LYl JV|5S[XlzM +FbwbkfnYPl,Ql (6)Z&V+4\XPlLMaCMF{].AJ[=Pl*rؔQûbSQlņ,6TTؤZT&hlRlcvɖW'kt Æu!|IT" AooX+I/Pu3b'5xƆop 8I M*Ecq2M"yظņ4YN՜Ŧ\e [#;b0j4^GL,6XL2{st&bkғ0~~eBM5~DGM{b1 {t'<3L5pM|ih#36͔YlbCe,6)fI1f5k7XM/}VfشYl| bk]YlT+ pvؤlHb2'2ϴI|snJ|i$WE~d DnnYϔuE=7My33gM#;=fm۵`ltoK`l +^ ~? | c3rDVal8͇5h +wpͰUal(` 06dalt=!06\<c[all + +S [YR`l [#rwckdケZ%`l=7ԲBcShc +%p +c÷,6 Ʀe hy0k:czˊj j_i?(ؤZXlT5 휷c#M:hlZ'~ p86xǦ(kc345h86}c3cScӓnQG=Sz<6Z +[`Y5&Dd֊H6- FlGe۴*dӂ&dn}^=ld/DBi3^&nt͂֠uD~oNdSBpd1-eLXK#*hլ3MoRP?HlG޼ 6W: .kDbC(6q +Y ;Ŧ woXcw{h&VPl=EF%%(6zgoWZFj ņ-Pl(/tv:ݭ20DXo8 +kƝHlzbAlf>< 5f2-9U۬ߣsؔxshU8ll m+^,58l֌?qؠ}sXa pP2ob(6z~s(Un"9XauV,F pȚw<=z O+"Q6{8cVx1> 62i s bLŸ +M*AHrDR- b-S@ljaQfJFY bCe  6ÁmsxV9sؔ!6J`8DҴ}EPt?lӯ|ZV$SV)lpK e:э9+Mo9@l +Z(@lWM6#:&Xv^6FoFԇetC9#DmWW`ۃ勫vxv뱙j[hСjy"3 T#="Q4OM||SNSBB(mYjfD'I HGn?B'h*;g4NCZSiK)4Sr ~v.P9i"E,FnT<4z{wSϫZLNSX*rǜ#̚ig4l\v~24,KT<9i[HĴ%0F^R-j2.M_pRKS h1 -M KCJ;n/kW%ѿVm洖yI[bPhpF)X_GHSQ&HP`A ocTgB;Z*Nz/uZs"ѓK4k.DE#Ԯ֬!92 ywg7.|4mY ` ; MlLˏ;rr;u-@ +0@/#!HO?"d5!MO|4y[,|4ml >ЬNI|gѤ! >;/ F_BH+ +exDenҬjHSGSTܼ"m +lѤX%%LG5TB)Пh~u^hK\hRh=FqW:D/s<sc yh]!3x4%Ҽ1TƣI5_ѤLGGbŌGC89P+)JA5D>`ゎ֘ZNlњ$+y:]|1%Ѷp4m,mg:8vqVNGjx 爴 hݖ֦T$M`=#M$ HC it{~^~ݹ93$m{a}fHJfm5i憤iL%3N2HFG}HS,zNdFBn FThJf&iCٞ#PzHj#&z9;# XfInsICEs@Hǭ-Hnd:$MWqxؗ"-EH#~0;i&U!1r}'IG#m!Җ +%wA>8x[!Ѧj'l3 )K`AI},rXMBq"/@Fa8./D2@g +F9GG- L;,kψY!t"ɩy~޶/@Pd=3!L BB~2!M4B(eDujև2k" DV2"-Hu 6i狐FU#! ƙjbGYgBYEw5FsM'"(A3i4q 8"-BH[bdcztHao0..>)O48gG qq/KhࣩviMLG1;h*~fBɯ hwv#no, sM%8m Ā ϙhӹ SE5D{e\q_~w Mu!iR.U +I#O>vL0Բ">Zo?OS "iSZ⛪yE=&ڎ䠳8n;ڤ9Bm;-茸4⌎w*{蛣6?)gFZǂ +х׽Yjj΃uenӮ>Ԛz`sZxj?smÜ,,%՚Q:QM&Bq>+л<h|\?sռ mo,Gn-ޠ +Win!gκt*cXU>J 6ΏpZGVTXq{QR +qdh++TRR`wΨ^rmSfS唵sR8kKyz"mސVLH!OW\69^B#!^dwr]pWyf4kjm_<#M ͭm(lq8_,ը-L0FzKx\6` 1R]ö o)cx11 @OC”!.F5U;F.< b~]mP:T"m5N#0L3_"mRY8Wt!鍦Y _€jQ =Oʩx2 +O"%!mo;|`)ϰ2NͳլD"}UT'CUzԙP? o_ u"t(K+gEێh>'  +x^)*XAfPy~<ޟ +A[ ?gzM?7䣼 s2jjͤCYQ*BL3g޷iACh5˜67{Px?qȤ}VH)-ף_Ri91:P8e~8enTV퍆h[P{C߇LfEYJ\ٓzm*eUDM6~,tx-"RM* T u&t)OX>vjT6Z}3ls+v#Z襤n!IgE- isc+_ 8ž{_V6M׃ŻVc` P^):?޶U%عs_i%M:%tƳq†~uUq"Y.: j$Բ|Я ͤbJKc_RXf1)cf'5WR +hK7l:Kn=[7mi4|,㬵4՛uD]6 o1|SlCA3- 4IUۚJ:g-yWԬlVs43ٺok"V+wVT*{u$\S*Q9eB_VqPX&Zh<0c5ɫNPc7N-T 3`Z[ pV,E®oSH4NK۳cI_Ÿ֒Kљh+a݅`uw)'~ =iP%Ym8P& L8@*vZ6 U"wEq{?#~H9muz,ػL#M՜ٶJfډHySbF`J*[N{tШa+l +RJQ~G0ݴZGOYukۤU(".~yy?tI x4t3`K!G}<>T"+<=^a{ZIҙIp}^0fUG/)4ڀƛt7Ѫi7eݤ oXK6QvC'ΙWGlֱ_>iϒc * AM='dSx&6_NzJ,&̟iM;C_m)vQAqOWb[iqX'y6O6۸F(6RW %2$4>yKǖB.(Zz [L7,l=%~F5FV222b8YI(%˻p"ĻT-xv zە)))jjk5W*ⷚy(yx؃qKAKo#&%ʖ2FmUn#ݒfu)tm!ݤ8ni[)Jweз~s_xXL]ؽrg+ϱ;/UI\z'fYnoL=)+q lf+*!+@h9_wkzcne=udDq){7\NLY_-T"hq`kfk|n_`>i ?}@W>g^ְj*֬yD9%2鮿P. ';maR6bvm!pv3i~6i*$lCH-ndAwg FFg<⡀Gsubz)2WG>։g{]?>kxpR͖eQ,>lKatlYQf$,(,`o+սA*7"[܏ؒr~I4o xŀ禐yUNqTCHUEI(ϹU.Du;7V m <Fd>Y,&O1û$):z7A_=zNV2Mqao![WK0M*iAʣp%じ Po;zB/7)Q"BCQz7I"/ġx2jKZG1"(xd(X +8#*9]u3xF+EN*P3o'Q>OJ'C9B$I>[tACEJ9. *Fi-T5ڈqX{,aa,;]8=P5/#Cxcz2Q|Ukc)6#K涹C( :,"!]$e*'uY3,bna443,>jٖgBe|Ի sRvU>f'I(px lTwʅn3oV>.HѺia,78eޏ +YkK!=1k^, 74Jtͻ#[iCIJԈeJhFYL8feZcR_y yN;fqP2JM=WzU|;z˻=`a_ږ4h=gU4}$ S(߾6K8"y̿Fʂi=cgQ~:TU7+X:Nv͛M4--^❳Dd'QIDыXG]7a?ӌ};9N/b7`}px021i!\@7lCEy2FZy T h"0#}>Opt?YxE;(她r9A90'!F9a^fz% ߞV'͛Z +bE,hfxL "!g8(9t(?2`ڙć=(WEӉf±Ht-YV97P,Sc~"(T98v)Z_m6^;O}=eƆ|عˬqOT&E6F)8ۤ.c!z!v>_:IqϢC.Y̨<6qD {09En3ĆΝS f9 J21〝eNƞsRy.Nn4V1"4B1荐 +|N5z,s(ݎ+YA;U-'r%څ譥݂O$n2ӗ´vLzIǦ/F\(Xs$yw^C=kU| ޤZYk|J^2Zk"3^ HSUŝ^sH2>łE5C4G" f*xt 0v``We)TfkJw<7 E\L# *fEI +R)խ$$h!QhՒMi:2rDsئ6vC$a,]c^NIa9|ˢbwHa.%(;->f2}}HG7=̈́7ܡֵAdN;;9ԎfP'Oj&MXݶ0Ya9o~6NǮvTQ=#$F|Gy序nAʛnCd߈+LS#O\D:TV9X "F])V`h6bj:/6j&TOt-۔,"au'D BZ +"L[Q <^AcC lԒK@WC/{ޤ Sf US Rj$C.2B4R&Ȇ'wB>2ѫt3rtfj^EϾ!"{?Hg{=a4\VDL|wЌAT]9h} IR 6(+@:ɞXSRROD2qz4^UxS>ئU j[E?}n HGFnA,vh8*L,8D $yu,ŘeZ^qv Go`Àظ3c- i sQeFЖ1ptJZjI[.iec|{{4 t)mngm hT[^Im3}bDrXq3TiGΆB=JF}c<)ȯ"II! X%ۣ'ŠEQIOzV飉&ڴ2$.n;\@:Xw=VP_I d0;ClϺtxCż2kwMRfImvo+14V {ѦI*eQ2c.;feYFQ@w[dXL:l A*g*Ըl(mzJ JKh|ֺJv\P:-hCMp˥CTXXFKh_@y<$eAPOddRs:\5]mN1;;tGql+/?_ؙIMBs~bj)YC%UEM*F!jB10I{ۏ YW;R=׬m!'EXB5U&lٶ}#o 9i=ZKU]2Ewex*NNrB}R10!L$ >Z,W/$It-6KsCe> 4* EkJ+:( #qEB-(o'(Y\(y<5PN#3k.fcҀVcc9ɦ`PXNٗ[ĒχM-#w)dbl͸wE@ʴh>PD-KRoƨ;[Vf6}g합h>^;"H/-Ef=L*DCgwq|72,U9g*8<=4;4mrsHv7|ԮdJlK_"(G KA*jSNl֮{Lˏ&[\agmWiF7;2L r4;<w~ޮtx]x7y]睛i`Sގ{쩼GbƎ,䝤H'+Ǎh!6N\¡9iYUG<° [S3\s48W.[igmX +}Wt=ljf'x +1m"+ɸKw=1'K5gew,lϡa&R2pz3N7F|unMjX>EB0N}(mu1hr!7pZF>gZ~9ޫ'= :EB]c[;6w[춱~$gU(mkj H)Tm g\8651Y(H# V?|a]P"A"~(%:2 g:#Xw ~WXo8+)rn.ia;1BT iuY-C%??p1758 +0-3* IsWw>Rid+vi)-7BI$ɉJ{]/֙qy=unqOe h0}Ը}@{k`萯v}mS,x #n)"Dvʅ?cz0t;ކb<(%hFtR5*eBͪaUz~ ,+Qi̴lRoB@ʢߡjnJO:˜N{)kWғ|$<ޓpB.VW pDo8yBf -ik1/cHfc|s 3m|Z0+PVUK w[UnO@|5"sMArkV-5xF,-ᅐri?n<U]Y ְ&bgvq4'xiwQ8w^8f 5Jy)<n)Y\fZBcB0;[ӊ'B /\U`N x1DoAyOfk~-w"5"ϞxR+cDN+/"pD [B jX2P Ǡ"HHςj AHHE1$3B T$s G̳ T$W$c,HExguZxKD%3e"^盈ןQxR2/xDӋe(D0aYxK)D\!"J֣Rx[ "JSESDc\pxo(2OqxR͠ 8F/ru Љ9dD<<<~7On똽+)_yx=2dq#u +2/vxnCI<<ó{.Wx:ޟ#yx]oΫ<<ݍSSx$2O7+ZxxU^?U>/I*<<75 +c.<~óyx7O_cU<+뮒ׅ+<<^o r…yz^ +ì<<+WxxX OK7O7OjuZ@gO5( B4q4eUf/FÓ)oRyx7W^gPTxxi`u:ge)S#yx][9jJ;xx OQSãH'<< +<g8MÓXhx O|1BMÓZixR + Oʛ'𸆅Ma OJ|+ cuae 4 ϔL+'.jI4<)oX7>틆wMÀZhx|Bixzf O@iة4<4<:ף$hnhx<7($yP3 OBIx6)4VFC"Xp%/jAY{/Bh{:H³c}JAq 'eZ/ǂ3 +O&7 +OMD* +r£@C-(<B)SPx +_gETf>0d_(NJ08QxG$5$QxTjQxjF+(< ZPxR* +OG]~A-( +@A :'cH/(oRha:N/rrL/Ndu ͙g)fy [GmciA7*u5-n&Ub S[ΫUk} { 1icJb5sU״~ڳXu.WF53uC.kXSQu\FIFIkF5&|NGL2kj/IuMkcMk'T!zPx׃T'窝T'疜T'ynRuSIupoR]ҹAu2 -:y6^NJ'ěIuRv&IWۤGPakgb5VwBT_MN 稠:{ks9T: 'nͣb}S͸{CNRHOeԩ-#wh5.y1Uϓ{mn+AŠkH l~%\<a,ςj 9v-'GE5!fV{#Yt4j^;JƼr8X<@'&qy]@dz +.=9.QtlTl蒒t[ ]'=uE'z%Ex@'Px+t,bŜtk#8v_J:ML( +&_[n+A@u6W_ax5bxO:ȩMb Zh̳AGé{py4Z)ȇg{νD!ΊR0B5oU` #aZ,] 蚡gEi׬frش9B*ߥ8p: C@'͆R!0?0Cpdߜ>'m$4,.=홆Ö"FWKsC=msXfΝ9d3QJkcn蜖z +sBqoN#sܼ9ͨg?7n#Sisf߬9Ijn 4C@=8s3mȘ9=f䘹&قkT\cc fQ.V^q\ma$h p}3e OPP暈G&xY)saE3'5Ze*q,U3fN?;0sz^%d3טkbD2g)M{Tnm%9m׮6EɭP6/m撒hs[ \3n[9~O5P6mN +E2b6'pPϹis(ZeڜTVЋ6/{k[KI\9>&0DN涒is[ ڜ+n56טG>NO ?_;.P"#yhqs[ɸnyDV*4Z nN͝ ܜtn + jGP{ngSqsM#&7h;m%斺psz)&ők +)>-aBZ(ZeiPFkJ99ҏ8Is\VIsR~Uۤ9sw4z&I9[9)4 NkJ ˜4Bj} =5*9>-ǫP՟FA`W2eZ2wE's.!o1w]]8kS?+aNmuQm? W/ۢfigA) d9yh>kH+'X9}CQP0;mA)L9Wq&m̀r{6%N~O]Ca8r2Fd}9oơN3B(%AN{ޜX窕V΄[Jm5r'nb rʻY\-r +666?NQef8Ell~N|Xe~+McK8 +u评 r[:AB,Yߪ<^](#fA@?KkV9+A3AY7{䤐]+EI䴷=m9]~7$*} X$r9n'#f  s}V"|Akl r +7#ύS>XBq\!(5&BN bF!׭r )v;^2A.PrJ,? υ!GBbșsj!5j[^rP-4Bп!Gaž!`@cbȩR<r9+CNu$C_D G290I ҽr>ir(7@EW>\Q}cX)ne?ǁ¸Ώ7gW~2y?\B-8+,|\jAt^5^sl|beM鼂 e=.|'oPX8 ?K`[dL/`55 +~\͏ӐHߋqrnzC N{1~:z>gi .FkOv?L8 +G6G[P2"[Ҏ\-zf֬GKEc89)q4mjAa=)}4cS*8]EkF(c"3Z^$(MqyOSVayX 8}[}/ 9>'z^8y8,V'_iE]OiuL0q#6]gVh)O85҂j!N ?DϳqZE<yÿ#>_8iZ,X $ذ8箬8}ymT4Xޯ>}̤`zjpNrQWN-"8qZOk`. NM'ʉS9݁')1)Π836(N +CQPՅޛ8O8}ӱQqbdG,>:˙@.<1*\)N}A{߁gPlNϳ~a&) qg#%#4snB]v}'>p-*N5')3o2A}XBiEs2ǩsޘ_yw$d G ;܌Eu||L8@ N&@ Ahĩ +bCip|)5l6% +N TmM@p(2yfƩ1Td$Vij#w)N#h{|YnpBDBO-bn*㙑h$&\}<(xetzFp +M\&?(sp?~SV~Sal~4[70if"Dq^sakr{57M嫟)4|lYo/MIn$o2ue,TBiߞdM#F}/rb@C3-@=J%.QQUs8A{1%ZӜÓqo{h+:M=/۝鼵 z?X1op߼:;z`e|>H_C6 9Nq SQ-)?QooooO8d2g]|76}ۜn +c4 +n ߲$Iv33ʨd6sݎauSK8+v6jW{7nC )@ay0Ώ'U+ +w.@scv9G*MDF+ʍýAnR)vB7Dt@ܔnJ!- ։wথa?nf6=rm AQgbF]ٵmjn_AYmP!8:MmS'?C"9)A"mF2ڦ!iQHM:9V ڦ3M'[5mzmԶ|R-Զu6ж.mCe hnقIcI= жS7MUSeh&9ǽm$-s5c-6T= &Q@$\@$V 631ۚ-Ge5ʷk3ۤxlɓHZlk.H8M{&kؠ!1ۨ:c3۔{\{0(6ffxv^ m }Wf[نB mIlI%,mRX5M<)6W/ 5jr6& )DEmd6."!Jm4^@)vwʏΖm6=%=hrum:ӼmkIR3A<,m*#:m}m56i@ۚ|FIԶMW?mkHMDmHYɢ27m,\&PhbAۚ +mk#oh[.Y6Yچeo-ָPF#Ma'6?~V +h\ ڦqǢqm+ mcBw@Z,h[fesۖhX WV鱉m]`6T +&c3ۤ Ɖ m@3m" aqVjl3RP(Tmdf6jS۔8#cXmt$G6OܮEmΘmQmY6FCXxPۈO j[ekچ՜0mT0Umm(683Cwچg6mXyK2gpQ@e +Gڦ̊YK23AmkDpMfQM_*Ammyj3Mٸ++m@`TqzUqYcG*xuH0#km>QE5tY1XжNgϹmRM0 FYE>چr;nQH`S6v&„mۦ7LmSnf@jL1nG n fj)NI-Ep(ëmJۂ)m%NncXݦ5Nvn#LRa鍠۴೵I4|_|Wd %ӽm ?m]j@50Ͳu ݦnSF +sg5 +imZ[e۴g۔ǣ:&4:C*l\6! f++#z6Ql ن=TlC=SRݙmz6d6$zh %&"8lCQ b1W,4 +꒝&bl۸0T@+ǫMcY2M-8٦*k 7^Ai@l󄋿m(8/#se0"&_abq f8fY>*M->w` ֩w{1:Vm=Efm%C’6gf\of[gia`ۚ نH`%f+%CaI5B0ۺb<MlS+nJF& %!۶61d l}}%%ߋͺ'` = lC-6 l!`lQ6DrL)OmGc Q)PSE/y_Dl#fg")IkElk=ɤyVb4ubƙqbTAl#zy9;TD?U6g;Zg)p=pMa_<ĶX+. l>qm[û`XT$e7BjòVc:ܰ4pHkapsS0aLx;m:m%yMv&r4mTfLZh%d257M=AЍec](fDj32p? :dޔ6b%՝kl۵ kj}k}#lI|;S/yXGWq +],KV9jKh-BV;)JV ޹jǽ{̂U9SgC$0S_֮T-NTz-2OmS# շj. 8ǦWS>>+05a 6aj،4c|VeqۉԶ_J{;7@X!RB8KMIq;IّIj+Hqiho:^'j9۬$5SG㎱Ij\aV]*Xj(22Ks-MVcԴz +RRR6z$Tn 5)'Rk($5rV9 )po% +mRSTܼ"mݳI][Ԓ@j^Z*!@j4Fw gܛN]55%.5^5IO^@#pZ4qQrY_Aq#G+dƃDGM2G:I֬LtTwƉs]hMTFMx +QF1 禨5EyhwIEM3( F FMcx?8jII IM0tZ 2K 09MM6Zd4چ!05}S=Ђݹ3Mm>m%Զ05sԴOfajr'Y0/T>ς)m; Sk Skdk;B5dVp`jRwԤX3g!05>v8M ASk@%˶hjEFSU%R[ZH)QjLz])xPj[(FZ*H8xRQj 2@gF99Agm!L=,뷅H#f .7ϯ)}>V'bC+Kg@iAB)Ȋt.FhvHAWvʯ㚻HK-)`ẙ,5R#ݮ}Buz?FD(5T;K.gD=RèD)PjII(J̒#z-n(<>RS/*L(5T<ϳSccYjK(%Ih9;zxn R3I,5@jtd4@N#9R >n] 5kXm~dǖ1jC=EPT "3Ԉ"P n7"MM2)b4TB}MO[B-1i#:1$gR .Iᕱ;GHGڿI:͍Z'WX~H՞Ll٧x I:*y5#g t[52(qy~#`ȭ/rd{B),P^2ڢ {js:f8''I㰁{O.[^;B + <ҚQ;Ѱ?6 6EP- (Q dV`Hzƹ:HQPbY:ȚM$`路d--Glĩ%OJ>QjT DF1FU(5y:j(ԁұ^`Uc¥Zn! '5d'32}lnat MW. ,΀K$tk6d/"bHʔYoB~q1AzP$VEL-2$ҙ+C`ɍJD֬}O./dRJ>i\#!3g,K'D+1J4D+qTب}2XEGE,<)udLk_L5BÅ]ekl9*~s,BdK! `BYZ-⼾P. \,w d/+FET.<Q^"zO2l ⺤6 (Eg+1Z +R֕ rFyة,D<0,L|-ēwNN/ +3>%qK} E}mCȐ \Ȭ~P +AejSo +lOd\q.x<gxz <"Dz'r+*k t z0DL&.uyԫ pr- D}z t t7V=M5 "rUm渦5OyLu6J'я7Rj'7t:K?',9Yc?7PUJ4Ԅ>hԔG'ֻo֎AC2Bu'0 a! kT< Հq?T*A#N/xׁ y&%N"󳥁JN2> Ggy"JX?]a:Vb6lѕǃln=[G (~H)D[9Vl'k ϊX$2׻0h7X(JM=&D6>1Ь_cK\-if6m]1.,'ZY2Hy z67&gE?#f0:ĝLNP,W(<=|PƯY7dCRY]Qz!r"=\FN٪s. %zKp@Y`Mr ?Ǝ=}zYCkZmSD܄j ds )5&^`Ļ:S,bvdIbNK :kGm{$Cs DG%ਉiEvz ZE]HpIĎD곾 V[u֭ٙlY57J5sˠaPbDڠ\ .ƍ[؃덿Җ &3iZqlIT[b7X:$o<H./Bٶ޲'fh$IbtVi%[L| b3lJEG9D#UÐg!!X TG>m' ..܏i}VFEU{RmSowmZZ[r)nSAC}eOKS`}qX1⠞@^:K\-e#̅2C uZĪIRèWS[/Hԗp MQOH9!9 V\蚮u0Lk/.66?A_e0vi(1BNql"9|l{35y}yړCszzه?%˭f)XƒnM695<+M?@;oB5F۾{?kBdxaKt,"dz6Xё ׈)lXʾ_x6jIBIm;f 4vAZp +(!JpznvM<Ǿv] :\)aFlz,EJ(bmt~  +Ju4fnjs#: /R?FY[MEvSR2s蔨d\ܹE#1KJɦE0= zܖk1k-VI(i; õTg~v*v.vrl7"붨(z(V!u'T! ST@hT \QcxW}K3l,(Zy&!SEC#YSVJTGAaeį^Dp1N-$X+%Ypf%AYv uXw`ny^&N1qQ6Pgf ǀ&"=7Ŏ$|#Rdj6b/3kyU* T\v&MŒx䐛Zv`]Xy}a9' ITDDKvZUĈpٖT 2SȱorTbe .)Q5"ϬT @~hP/4 vɍR}nqAvb՝:(TW]RsfY9kHmE;n# +$ 1H*% ,>ܨǛn9O1 +[uҀ7ԇ@LC(8nE>"Y3'Ņ و6J<Bk@O$lQY[Qwe:i5g.Ԣ Eͱ fm>?5",x^~:]WLQ`Q|;Vt[!6'A$ص԰1|c]< +'y ) ?g0x}VCa.ЪrwFi` Ć_ܺ5r*1N҇%R /Eڀ\PݫKFm1%+- wn)IF4B#$wi *g)|4X\u|TZhoDExaX#:CA@ڣF*xC/̚jЏ{ǃ}To t%YPdLA@t*|jNO)5aAg?"J YYb'ggpv>Kg/jҔ>Q|`qk+^!`F c{쫔m{S+6&{rf1=fOSf凅d YM2m{Kj/{ñaQ.Y1F"\) h$ BX0/Kcdfjևm}x"ǒ롭ݠ^PJThC&)3h dJEOdcu?ނ~( @#Y29t ^?!͍^ L]94%)h'88:YN23n2u℘r͕\' Vz#Rz*;ownTth2mdh_16u͑RʄrȦJQ:C+jAXjM@.XJSNw(ƔHT=c {6+2z k %6k "9J0a{aPY*,I 7W:g"$ѢShцxCE1E&"˙"D\׃чl^7DR`M*5O&KSÓ,g JrWKVJgYUI@F? jQEogAAH/StYiQS\U8pjɭ"ntEBAҲͺfʰ@h )b=A!FղDaAVG3l +RF?_u9d۫9M-b D$IkkD 9Lt2$u=G"(/-yީ͔$ED(`ْ'=(xU'u5FvQbV"݅B/vRe 1fODk!ҕB0dJC3bY<F Nj +/xoO ,&Ӝ^9 I2> +U[K6 7tr0ZΈ}rj6.b֬7M"rcm! a )XE;pYX.}Ja$8RPQ`yYg!lϖdaTU6m2q7Ф%7(ۮrڋP3;Fl[?)6[2Rva?bTRФǓޖƎQzLRANLsb ͫaU7ӝ j֧w8bԞe~}6rQn@|Vx $ކZ"+`zu:esi+4'X0nwf&ƷP=F`jb  Ύ Bf0b6hNO ~; f$BT#+xuԫš8&0O_7?1ީB-B~D(6ַJ c6w*ͶZ_iH;hbg@HNɣ΂e{ dIb{?g0ԕh?_\7O*BNARAX!sK Y.E*[`7l#ZVpb4o 7|7e cPNiԦPhpE-bfmhs_6!do w~L]&IZ-ev!gNѸǍmqmQ9lq4*HUL{E5[<85Z9ŨeH tߏxQR*,2z@̔,M*J TfEp[r^YPKɒNkyry)GB̹׶Fט &ZfB +L[}@RTLj ?j`Dg\"QYM;QBQt5qp״3wBd`B deֽn,9Dm{6kΠmQ2rM`5~G%m+-#[dlf}&s:uCrXJ<z<4a\l.m>(XZ,0 $I/-:VLE(*޲-;z9ng +%])ٴY] Nqf[EBز$,Vnn3E `Q}O't;;_Is)8M -na(n +(skk Lqo-ePMJO5`m<d qPHXC-mҋp-azmPi?8 O5ŖcL8Ks[ΛN77tq|s--#9C']nzl۳ͭ|ٷ e ߗ. [޽BbAKRV/QE3G1eLa*V*kQ~̶umH"ŒLװ`y?[2r4QV [&d~>r c–z$SEq,a"4U MP,C +۫H!xh[M+=s;.Nso;2wl~=Ǫy~Lzdt6 0C}Ze''nBg,χtrwiyq|"*|nڔ҈01DB18~Jd+Qhr|`]~i1:)5 E4ͬTb0:*.7Pc3\n5 nrf{okTV'v4V!h$=`P6*P'k?Y6%+( Ė_VXezkƏ h`}6d/fftZj"Lm!&oԻ[~s?h#g^JlWf3K`yA*?E3 ) 66mE_AV, XƩgoGoy^:̐0: ʂ'lb!QHYKe^c`20b ,8*ۮoEyIw ּ3`شl t| ԧluA6l7-+wL~¤NF3^ö&㭅F4Sk0< R -͹PLłAVj̗}4k>pp 4ػk!/g2[O C#"qLM׾If[@S]r:b'S*h! 1h)6ꍈA7zfNZc (8Y9hs$ _F`1Va5 5Cg@}ѢtF6 ZAhyY~K@5z ـIE-9;X$4h,-2'[ AS~ѥcxط[[9l1Q Ie9qpu%Dc4cϴ6.oњ_˲,2Ibʳvᶢa \g{.k|x)Ӛ\Kh/.*-`z Y)oeɀ])8~>g;R)o nQ˵;3[rH$XFq8-ܓbC'{FNO50rne4{SURE}7Ʌ1"}Fܐ '%.$\y]H9 endstream endobj 49 0 obj <>stream + ĘI/MZ"蹛8;7FrߛiɚMDeO=ii-,G%i8SK>~5[ iiJ69LUG#s%һTh1[nn=ng}dN)ɵg)~Dz?&Z`^q*ol"fk͑r +G9Z3RjY)NFt>6^-ojVh $;FXWe=K(hP;#x\ĀJ)l^nj˵ 2jc +unQݙ5?VՍ]ov+< m`y=7hOwO(Uz5`K6;(&+}z,X#f+Kb ;gov=+'PjB^30 +3ڏgj +%B,HR74kI 邋A9 sCr/PQFgKGBhU8zViy-ZM~ K}~!6xjO dowq`魾@z y^tXC;FrJD2hPhut>W8k5Rp"fQb"$ۆ)/.`>`Ճ RQ4e:vZROVVe\l)Qz?H]`5%An'CfZX6< +\w[!=ɊcE]01پm\|~_g( lc{DA(̔ E,C<ᡟXRk>"O6KJ +l --{ET ۑ5^գ*ݳSMn@d4^3CkP;;ީ8V& hQwk8Oym$֮\r?4䪺}p;?͜-l~PG:LeY0% tMQʢUI@T횗(5X .$+/63̦ +-=4"W& +м2'6".,2j8)Pnvkp 4NǣzzL%DH*сJrpݒ%4dΤnb1 ,!d47^'}+Fy]c.I"2WZq3*YN \u֐ꮥleW^ n+|U@n\Ұ䒡&j~&9lr hT:8lrDюVGһQ':YJ[zoc+ MY&Im-)k3p^zr|Yjږ֟KjA pFO/XN67vD($ro)2& $'ldkh)95Ÿh񚎨Dsb,lc+0۷<:Tj8ƮvN#'* +&P3n&Po)R>D;V<vR0]b Vܡ@nUB,QhŔt,Ze&4B#d\KG4qlds 6[z" K+0"'MP?3eA$6fSRl︣hWdQ-ؖ.]9R7GZ*J7J5H3 %Dg$u3 '03&[ܔ}] +(Q")o|jA`Jo1r1ٳ!GJ.;pf +T6(%Ch!'S5UBdV8Qo7XvMhyl轑 \¯L %kǰbj3? M[.=]uNR3*a}"Ѳ`i,-NbKŏ¬’ ]7lg:QxJLV^|Mh}0ePV8L0[fdŖ@oNL/o:HzU @]?KB(} /*3` c:)warRZvloN(N ! ZI.]G: +l0ciE:\jƖ8ž]r fj r>8člArEKLJAoSE3MҀSN!潷QuuȖFg )(-hPJjI]KT\kfnTzg jvmXs]nt: #S, Ne#vGjbhՙXIV͞ΖVidfFlVN( GDk͌li-}%AEj1g|nQ+A/XT)G9-]qd +إ} IYII:׷zq,[ ̅4 o;ʭ+aS4H14 gt h{ۢ-g Sa_6[6b%)zBGz沚g̢)~,&NRZnE +Lnfé.ECP.G0ֆlC-8-E$6 ѫy8b]²,Jlu9߸4s2eFɃv1B>^1ƭMj4 nnp?Yz8yh|UZM $姬oEP.Ys&VFn1^zl+bHQ"g?7^~ jK,vUO)R%k mNɫ)"O{|-U< +}V6 j_`NIm^>G =_=H5mXn$-46# =hVqzgD^GgznՑփ)vlum\9Xe& 2DaJ㇧ő3m {zkdqD_WQj9m ۖFwk]-Q]j;3B$Gܝs{1TP/۪wP*0X{ UXh@WE@ӿτ)2{ƼD?m} dWh2 7` ]0ώs??Z9Me"e{)Ecj :HM5*vI]^h>ۛxezc⢷v7 r ; юjCޏWW_ +Gq鳿\]f~~<_w߽ďx=W?mI.__]/{0 o.񷟻/۫J{ߞ_gz? ֮kGW_?l=${Ǵ` ~7_՛##3>/ӻwoj>{}IQV}xZѮɇw_vz-žv_`48o.‹mo^=<3w#ܿ}ˋwgwoo_/y(on/?`>k ^]w_?jN]o8֧}[5޾j`7#Yjl;!\K Cq{ʲ<1BnVOq08L\خr?tW)"KlHxl}~t qM^ye'+~r_=|}s z}\8#tؕ'Z]u?[/ɏ׏zxvyu\鐂m +;:ջnoϷ K)voA._|A>\TOP}<[sپJۏh___wߙts^:ڷRwl=^~vi{b|kވwm!>vQ=a9v|lRv=zD?ؕ@S_^]_?[tz}u{y5yG;yCߝgm>?i|^~_ҜvHo\=\}F_x~zپb<1ͻ+Uym[I1Al;Rޚ#MG=Lonl?R~;.^lfKyb2'pخlf`3ۗY?̳_m_ΦpyAnmWp槨`>pgѾGYKe^1p kl?}}{ yrVyW~D"~O!/{\ boY/ۇWS2~ki^Wbɴ'Œ^eg,R"V-փ?ȷ|;ȷovo..vnOn  ^t>Zp Dp*JxDOD姼a64;N[ezp#yy}$ŭ3pv'n+$_5in\~yu蕨S,7~B_Tvl;㚽T͝W\m_LP%˻ljlWw.~UGGyɅlvEzwJ>'\/OO4a`WdFv=qiQϝnrwj#s/$l'vc=t %lHn:ydq{" }'7kz(_Q?ZC>ğ~Z!C>ğg|;_χcc;ğgۏ3MD}'zvAv9b·dg>{Rwuy}}ݶ㻾 zv7/߽TW9.(?nWwݯ|_ݟ>g?8[:[L#7CN=ܘuI/ cg~-fJeny7l3c (AOoM5>`/>b/'~)޲+98sΜ398s6gnętΜeΜp漄|mL}>uUfQDؾŽ~ ;rw|ݵ9x~Xpr® +Wv +WO_~z~I}8Aw=TzUVЏëjo?7wO+,\uɲCR;@<"xm?;Wv]}7O&l_Wl, +KOK(PSK!i-rp쁅oR8?vI^?`=?$t0ڄwWۣ-gƾC=)RG^Vg+u(*da'N?x]&=ҏI/]9CWx_><\vp~աО0}Am?Ơm?G'珔180^GaKmoOB>ۀO+`[`xw +Zk9X{mV G;p* gn=x0f <k4Wg>aDbb>e$;o +ﻻ7_ݟo/qv^ہ:uCUݰpT=`On +7C<Ƌɴ^s{Z+fձ9z׿?O_ۻ| 4}SFܿ-xy2~ -FSP泋'( k<;}-΁=+}ksP=}p]}Sp'-oɾi< +<}9z {n=!ٞ[k<ɵ>yţ%(֯x ZJUWمOV/?Q/t݃"V?ΈۃgAyєTDBnl7:b^P/y#v ;=GӁėg#ON^yeHݧw*%:r߶~k/ʮۯ>'B~ ۈu*qR8_?x:.p ?9O}e('XIM[vGiPIi)%jdf8CRs8щKŅ|ߝTr}ޝyyfΧr$ơt7iUJR׃$9MNC (9Ncr0Vh(.xC >ߗpїܺSWrX4FL }8v$Z4vpuz~̡ΕT)ݩ˱2)9ԏf%auT c0?;c:aX:ޮ].ǘ}4%c}SV;,i.e,w.]t#a9\JJ填0@(mC-WP8D|+ C*%r$-WY]v# Kal>+)LjĒSF+ O.V:%h央Jq~3Rv[ckG/rU _ ĴL\w5cx])WAd*>zLVtk®X຃:OAkpr W;0>kKsN߫jIQJߩ֩NXקJwFW | +qLh~@ﺁSZԅq7-bf &rN5n[U_ +,2aNVIX4>-:B:H|{=]9P¾FIu+IؚSN ҔjMQ5&~g*ac*3ݾjwHUU&ͣ#-nUkWz:= +L :_=1%%Oٙ_%$N/7} k:}k=7lB4$ec Y'| +HԡX Ef`Ufj4P +9Oشi +?nVef> wX\be`(T72_-BՕ)!AbBR <$qDKNb}/*>Cupvv-CoɾvM z(GrZ"=vh{lO sbg%),; 29 lرi qن/U嬎_rtKUçKaW_Yѓ&r7 ֽ$'!u Ӊ)Zd/AzIxegʭ8Y/wpfi;6Z|7p T{B_><cby,O|{4y/>R%_86l9?7G=9#p t4>zA7+YPHGsZUeURxӦ*6įJwWT7E{I15DCJXHXu?˩)'Sㄇ"E #?vYh{_8Aje {:|_-|7$HluAz8ѩZ#:]z +W. $_`*rE#,<3_bGXbV/6Nő1Xl^msEmt3u/"bV箣I +S0h0"Y:u`: ++"Z<E"H+% _8]r&jU DW`63|{ R,PWP{BC:1lE5_3V1~?pcb("Ai!VwH3@?X<p£q_]mr,b!ʷPt6<) R24t!=4WĤϼofYZpSLJD$*B@Hs1!`y8O{qs$UhODj /z9 +vؚT.pB+V.!b{pW xF[_b #l tZZ3iGpʆ;>ZeѡB}L0xci +(|<ίۑ hONyITUSPC@@CC HOWSeA$q Z"!l3O9,-?T;%_Nm',vuC{2>xaVu,BEꂦ0ohHqttr)ӆUxe )$J ^du8C1؛A=,]Tg Yg $VK>pܧ ‹ <àpB-\@58"xqj'/pixh&eqL]\g³QPpPu4jӰEOk=&+Ug~Um@YLY;/4@ $Wł0@PaE4Ep]G ½hqT{Bja]g*J$'P-'іŘrs|T#o$uK 8X†NFqA')ϏH';5S߇+w`AܻNŪ|aMmʤ"B+^?T;wf=ItΔEhO⣔@x%&ߡ _ +20im91k\EՆYNц \N|%.Ԋ^JJS y; Zyh` Xɞ)M*${ AƳz~lxP4_.Bu?K'D=MȪheW-o;6b^( E$qq8V ltM!zb/1@HN@~{ºGK)ObSXrȢ 9l#\dhcHcdVfȅ%aWm|fxQE]̞-qS#7kWnGt.2Ⲅ3LZذ?ROsKwBTFtesA"'R`-Ub ̫a8 t6@SSc:=Ѭ߻Z.b/6d CΘNc =D Txh]X$0a˫_ +M>z -U3ѱzRpO/RQ7 ցf8(AtNi@h ?hAֹ+/08X$`3]4m棇/dяVI*{i4S0f Y(H! +}u x@TQR <ͼY,|U 0  wB;њ`9z> @ 4$M2D?zCZYN&JGi9IFw2I=g%pLrs@ +5X9,j60lT d#nG8$S4Иx@`cz A9@(9Zi@<_E-[ 0{j AsS=$N(JCY#MbghYnP(Po<>J'DV`脂`PggS΅B˘Y6'WV0knNݵ. :?EeaWwNq 5Ԧ(OO V1M}؅AMUq}#W̥kۈS1bvM84}i"@g*LIp- 2ɠP]~@;AHO\"?0NDNR =F4x*P̰fo5Df0*K,ޠzZ HMj8LMь~ɘTMGxxOn1* Je@q`ȋ: +osXyY ꉮ_/6! +)&lZ rG̓Mx27|%+5_KThOCs &9GQ,/3깣 +VARvq>5Mf4xF1sL 5ɕ %ԆB-A;GAރ/&1jlp^6zauPD!(o":;Pay>&C >?Mz-zv A.Y7C83`G :r<[LAq!UNj!!JR`iǁ2,0/ \Eݲ +rcBwPP#fD28Sf9Bd ~%J3(Fdq-Vy)?@ha$fq9[!th" 8%h ݀% 4.@4#1jhEY͠^xi[f,|[ ׂ!s]0l=Nh@`@:ͺ,uT  ADY1G@4KC >DN$Ǯ3ý '% a/6`hK xYz=uNk~[ˠ2= gJܤ !m( xr` 5 AN%P aDP! +{ +v+zl$nqXRr4iQ }E_,&vXT">KH-{:jSQ5 IUiTSxy7Und%SI#M~D AcLưA|tNGrg(p3Sqq\qrdǪ8 nAړ%h!kP݅UgƂJ+@.`rtiݜQaUo[dAzuĴBz תCӜHIp`+EhYylHP,CBXLH&*fF`4uڙHUE̜ 3މ*sQ, +q UAO$A%J|m#6վ~}(趴VaЬSZ9q 9 #ᨔ554gR&L"(Ŷ2C@@DQUEOIYɋj7]Y O1WGduèA߲gbK6Kb<1/pȤP !M^?[yXNr=aԐ1 +륝:$\˚$FM_k_Ym6m˴p$P1EUJj|bwoj IW1jWWOͩG`tD8k&ydʞqVm<۞O_ծ&{"@z:*)T\SM 9yў,Ϳݐ|!~'-CnJ](OY D[*KeWSAzb{Z~p,L> +w f.8u<ٳf{>Z~S 9WXbw'E*Ws'8e v{kXmAĕt"iֆlU{ (RPusJ=K怒/4Lij|+\;)#)4SՆo26}Oj_gzRizC^_=|sۯ.P73HԀ sӊFz(3/wzAOLQՓeQ>`iWҢM:?-*9q #%O:ˮ=9n8sfgQL,ۼY`3ӋɫFa~zmjp+6+W86c|*Z㧟u6w)0lƴQ;yNN6Mq^= 5;yc}:On/;Ir&KU@G^B2ucfX"Ba&߾C Yk^~K桖}:$ٿ'I"C#1kzh#uiE֟rǓ'>qɢ-ps46rc-wl~v4775bNq6Jtѝ隮yFWRu$izGs/8}6^<|['Mg{O> 8>%9Bj]??<\?Y//?^ 绫7rӍ?]\}{ˋj\mɶ k KzWM0R$*K4I P;1L)J8;"K"eF +[M:=1c>ӤXW-R+S8&dOq]GDN{bpG^b >>64`:lcsr~.oϞ[2^7|Dn3=ӯ7E|d:`@GȗZ9w(S jF.˺*=͇[yk˖"d 43ku=n)Y;%/n_"2CRr=Cɕj>8lZ-Hjx ŭP pPW,/ $Ylw/DLRF^Kօqh |aN OUVbRNK +R֬EFf=Z2A<{͡lP28!x+W$.W6՟Ͼ>7uK~_.G OV?}zݽ= p<|§~rqOwdӺ5Y;SQ7rT{VVE9GTP$dS\-,#KZ UyJF+@@ǁbfyKA H$=񒼨\Z[o@SoC~'ʀs$u0)Ld>jvpJD 0]ND*#nKRWF9d-Y]n#Ln#6 %,9ϺBߵ?-X;8A Ay;0}Ctu)'uԣ<$ r#o&q񰉐(I% rchIW~Ah;bRr !1Cܢ Hfc* + +X ӌhVnB v^'Cxl4^wȡ$R.y Ĕs +XX%γ TbQGA }Hl)[ 0 +0D9%bI=,08'p1 +VM e'%Rxc$KTX8,FK%6PTc$uM<ʊcJ"ꍝrPK- б0^,N&rc2|{rri/tQ$yVӖdLƱT^DM$-s5<ݑD0W/iX(⬥lKܬ21UqkO9;9qUp,xDR[̟Ex{bqBxw*$Yj %)xˀaf' ulE>#%E.FZ5`B$sO`DN xsbNTZt.K ^!˜6-2yD +ٞEMzy ܱ&^ R,Idb[NsP"f|fD .!jXAHBaINl"u`Q5sJLͫAn`LY":LvR Y)g#7ZE1ڎ|!ɝ`BBgq >} Ǣ2‚es8-,\D,%e 'UJܷKӫZ;JG]U壐cr5ԤܿGy!p!S%IcͲ-Ł{R8Qrda|trI:5k*dD/* C,ZvA4S98Ѓő !2O_Gp#a`m+;(x͔j +Q3%N2@`9 + 3ZFl$(#Q=ePͧZF)=臤 ):pn8[HfAnₚ;z#P~ofC:ۑ=;я`K7G,UXDb:xskXdU ŢgV d@I=3DQ +#cסgvv= k$ka#2:M5OK2)JH ۽AB"LtBJE٤o)Q{:JL,-CU鎽pݡ]"bl;]aQp$^ot)3&{# ٳt0+%&!/L%b O@P)GY;lȌ-IL*8;Yv/"ASp# +-dubc^ 饤 +\-zX +LK/R efpc^ +ӱ\B 䇪dvȃ(q/u`dHV=$HL!Bz TRׅ] + QWJbzu@wxraαD%mnє[Ga˝2T2Nqzi­̌i'?{_T,C8ݧ,슢"M"!INBzKuum]]8bY :,Z|2MJ9|f,:kZ!I!{FK*Md=J' $+py1g$mdp䞷fz.l TP!=`,.|ܘ i袷,{mt4 *|J",sXw}wBQCrsRg֝sqJւIz٠9{Lx=VL'f?W8՗;#?>-.䛷k_~SIq<5JZI`)t&lNU~:@? %"K|onLV^)mx|?{9<y".#f,Q8v#f0!̡NOppw<"6~tgx0'0E/Y#KPhdq<6~#kE7 +pp @lϧ<84;?!)œ( cv+<npw9sp̓ Om`DQ?7DySJHy%u7 G?Ӥ(0TK69{$)̍XH\h +M9rc< g ƗpE$ԤJMnq~YF?()V0Px΃*oI~@iL)Ih4vhD?.k|o_€^}-p`SMA_p62d$ @1oDP`f A j H1.,]qQ?Pz>CM*;mje҂J$W4N[јdwH[_>HtN( ^0e{!(CR%GZP/P-Z ʒ܂ +£=4-"PcʟbyB7 !hACHp'4?\Q.hMNMcRʟ:`TdP0_B8b&KIrI^ BO\]R~I.rƱ 1$yvK5R% A]ĞDʢ BrD,Hh]A`8|%JFmd1R+;lR@$ agZvL;# φDDJUѧ4J|Y'!dbzByƽ&muK(i `"~ߜ2]uY*HJa;;17jT4a"𜶁BSjtrg)AJbA*{xK7] +lp$T!+SLfZҠxI(q5X7;"D9 hYFGN:&}]PDzGtɝ&hlc䫧)ۓp7r8XaqtRN kԖAV~Q>yr"-=$.q AJyʖU+/+]:`L* dA+.s:IC dz];aaxg(#E,&L) 'R*@ &J0Y!F;`B~aevP4D5B] &z%҃,3 [iQNL[qMiQkL[1.lRN%ҡmnF`:];kw< 6f Mel{/![V @* (aaE+-?zQ;)*}IZt - ȂijO'7pv, Ћ2{"̇+ Dv$@O'FAj젷$BV[h]qD( +8)OcwLTW/:Պ[EjqYV鷐p1)`?IT$.E1aP},ävbuŨ Axt!U[XD 4yIPMrR)X,Y V("Ԕ %'XD +Lv''Ɋ">ҫ +B7"-B +,?^6BhNL5JVšNcU #SӺ_=ojzsܪY ($q<(gYЧ^aYe#@9s(g;;xP4ٜ#'G$ `i 䗅,9˰%B8Eg>M0cŖT$#遙vfqBTr(7PQ9j +cX+PGF,j@Y,h$Εq0{<dފ2ʄď$M +1=9Y-QA yL +Ld#DeTiBbrd\TkH@<@QLV`~pH63;(Il’kh *y4ƸADURpʋ%!(4q" +Ǎ. +,4 "`vSϮP#@tVER9hX2 pidUxÝ#؍n.UVXDJa15k _"{_QXE9,vB5J]U+=H}qnu9l6KhQNX?j]T·werN_ ƷZ&.ݭrZ +Hn Y Av5"ȟla9>JkJwVT})ub.{[@RT@Tң iIz4pYu `-!%NϦ rRv#^p)Y @%M^p$ .i{e /{ _.z(WWɂSMq;ɁC8̙{%#hK"ˁ+;. !|/-BEՕ&YSQ#K$E-.@uivOQ7** b@K +im 0R= (JQШ[##;z+i1\ .C!$MG + .5vB"[ vUn]p)~*jS1AHfIi Ω]\ii,van?qjsspi6 { K]* f] nKRkv^qQٿzHr#VI%߂qx=5Z-|e6MI4! +"yt5 Wm5?>#P 2I S'INcA)]u! V$ZUUb/a> J +`<žՅ1OYD~9uI<@I6-$ FEi $N-3<2)TG7()Jv`Of ,| ؕsddpNbi}]$|~ 2 4oHa۹ z˲6^xuJa986#{\_ܬr i7?XjR5/>H)FIv4Z$:*6Zdlԥ kYfqmְ7|HnTQ.!%PJTJWʎQAV:mE迴ـZQq! O{gr@OyD#2^DHǘ$ѱ;(ņ@t=r ?VQq6F=>ۊ5@vCe/m,$q0QU`D>c&O!Yp(-Oi3@ +3V^@(wS(HP?()epiAᭅ:_ D}NvU.E2 +W@ǜ(&F⍔\Z9k(Վ@ѰKP_Y0 Y@Qs[9OHJ_櫧26L@<!, hδyp!cv݈nf5bZrfNʣ+D*}pW+E8e{~0 -̞2 +NSK2GcQ車O\V܁1@I(2YC zSa 11ihQq;4g@P†:|Ү`gGMrc#!5Įպ6 +L?)G4ƒoʹ>HF: mQg$םZ84 Q5-* iD` )* +&:8jGcZ% J9OOI\0sb +ce恞ވ`^ѩhY +*gl?=ر#@4nLEg{Or* |S;ȷ&|dmˋO=IhwzB ]@1]>JНo6J$ڹA[Yl.1!< =X)t 0ť.d)Ι!3IYjA1ҺD8.yWI V pC3N )Q,,EhC]g>ʤ0ܘ({cD;+ i02dEʴ UaiD|֐r;̃L8 g^y18XV2# ӊ+(Sf ^Fc{a)FTF9D"+8)ԧ!9<)Kw\ (HٱlzU +A@Uht9ɺ":qnL+GB ;GJMV!b'ݙe v療C-hgs^ǘ:!"  & :7TWN/ql6O*zz +A'!1rۙEp0߇`nv_* !9I"E (C^I7() `TU$I$ $ ¿~L{쯞 +\z4pEGWdH] \z$pE1+q-\zL#+RH=&pEjJB+?Ͷ)RH=&pE+h\"H#+R \ej +FNJ#+RH=&pEGW"H#+R \Q#+R \z$pEH8WH5&pEFW,zcʸ \j$pEo9pEFW\DHH[j$pEFW \ GYQiG+RH5"U5pE1+RH5""H +詹 LH5"HTcW \HLDSh6pEFW"u5pEH="HcW \z$pE1+ҿͶ+RH=6pX8 a<Ʊ1kXK5b-U!#V/ΪVTctVtJJJ>&+=TȰl`ahJx +:Xέ썵ң$+݂B+1 &ҭ(ҫ܊Ί.n]TbjbがtX騿V:JFQ+ +$٣Gt6qzU+[J+VzbkG[a`ˬl-(#`+Ո^iHz_ye`F^YZjpA#V:8q'b+Z%[BJYX;YݭZbUt'żSΪVzܪVz Q+Bd[Ͷ +-Y9-P obte(;)z ;DT~Y§>EH$ X +r{K]SɌHH_:K]3O!y+ߔ;dĘ]cg|BtݱYॽ g@ݗZl@[j"]EȖ: Z4eHN5ny4/WRWQfc:K]s, ,Y/\)35:A35Gc}(/t'RgPQoPEn=-ƭi`ېe\!g$`N,OLd$;=Z;=ss>Q ܇gngg!.dtK[/rv:\RyriTD4O菁LڗP@P׬-u,{xgA9§(9RT r+JG PPlÈPa&0ʠ*L`t:$ AAd`\2H"3a2`aE~&\dNﳁQ3J}ng$3JeA%*`ck7h,DЬ@ O ,KF S#Eó7&Q` f!SG5(,¨Az"¤KE(@5H6A Ŗ E +i}`9s!`ԛd*v R,$H &KN'Sq0a/yAF)E)1~vt&¬`60 +VX+ +$PZœ0)s8+({AI9'=kcj9~Dși3GXg7:Sz. +7҇ BѬOvDح :P2VBH,#7i| 5j4QiB,*+X{sp>hPckŅƒwTqhOA + +Ov FM\C +./ɜq):=qf0z/| rr=0\YkdRڎ_UfAdw!4zUےGpB +)9<$p@:nPL^HN䄤iqx[&6JSNPjh י6UgD(87GNb%AD>p +&H. l wOxe_=({`a_WRHUrM(].Ti.+( Ȕ08چɗP *>sd +<_Y_.2\V9-|Gldϔ%GB& `L?. g2 ^z K#xP:t2F7gù IukO)"XwgvIaסhnDXׁ]Joxb +A#!1;uc`)mWܖ +re[yHNe0dpsYS٣(8<S\UbE(!$Ny3UDXIc xeB\uhPJ0rLIm2: '|>lLI"DA%E⑇6!1p$I1jR3C%9,/(K6R"`V{C|BW/R]%h7(CY\ dH f{hU}ZZGfрJ2Z $~^Ŧ$ɷ ՊBO8/ Ei*d!!Ial\iXcM`eJ0 @ #%&e(!Fʇr h}XM &zxspDlP| +GA!t3ߑEǺ!le;Eƺ '"+l;*cL+@gRX$F $u3E^yX A!Rœ7]%S*~k;n E4$yHS 0O kBS~ GC=+C[Fi!+岉kQyDPM=2шbd\AbPC!Vtk^E1ôƔ%& Q $EE?0ʜk9X 9Gg~r#o TgMeI)Ʀ:ԁ*~MFBVP9TGe>,#nK,|*Bl epsP: R@M`+Y`y0?xy,>A1h6 APqW 5^xxJg L ( T( L$a +-e*҇+SD`ZMboq :5xn$f2% l&zT^IFr`31ËV JΠs"3Xtxp4Dzp9>B!XլC Dc*"h8586g./ WJ\A%%9čw< XWw@pf%)?Ģ + c2rVGTg9`H,\Pd ᅙ.,-PI/~칲Q~08/(_ 9ȱxz0g =*:a 22fŏqȔ 2eCN13lU05r,YG`OҷĐL&>`N&+) @XI˚.i| NbƇmr;o2E|RI!Ls NжR45 R6{9oΨDȦX/rpM|`-utPN bzy8S/SI*G@@p\!cJq_eF{AK,S$^5$65Q!X/Xу9"3̤SPr1=!(BI/CP&\EۇLB%D +z +/TjA:AGK)T Oc='`~y=5x(b?=7mFBdMP$n0mzhxQ!f)jECo.QbFPB/懅*C10R +Gp` 䰠29P)m:'cN)>)+Q2̎jȔ)zIIϺ΄q`R2:xx.T6aw7 +2_Q9} ^y-,`  @'D[,1fFe {ZBҫ604|cabf7pct }RLЌo0 D=9jTېQNo1R1!d\r@hci$Msș4,901A/(h]UL>Kjc=(#s@-`:&Z0D2H*LaU4[ hX0P.9$R<M<y< +3SJjJz)OXA-) Xpe?fQ]PWRit=1r( R!LYcxf; !FfSiF!y0@=ӧ"/U,e{(VƯ0 7)`$Ü15)12~rJ _eX4bZ(YYJ>WC-ꠀH2j1gɒg``0ABsPRRQ0rCdW&1K1 Ff 3Vсm!ӛP&H<Z RYI2^p§pFA b-Ѧ}n19rR39?O5b!9@S3|@fC>%316h$é5 !dvԾH!GH~z9@0ߍP*fR2e2 oQfQ@n[0SA +6ު+I*Й$ٲ,8d +F?<㞂6N$ځ Asn + c`kҗ贂Жt0qMiX ~`ROubΤ +(0|p3Ʒp)'CK _a7@ LXwecʜaLDɾ0G2DoOБ(92A씂߁ [a4ø"s9AJ,$aeJ68JbF+SnF"GBׄי)cIZrT -'/m[fTMWP*VK.rCDUVlEaD*PeT.*G Τ&mH92 )@EoDFITJ] &5궀#x$\-R=Z䮣g RwЋ?T4LH)>+m]ls0Ǵ\$,x4NH}<` !YJ[X},ڂٜ[}n  +k)kR\?"ᧈ50^ wҡD mr(<*Ypw_BN;d\ Gkb+I3wN0]pA.'2|(D= دĪ)'lr€?#EFaIL:!55'r:0Iic˵-ppMWR~0ĂFBJ,'QE) +~ +=5i&A ^4iMu+YQqɄFB@em"ŔRBT.)9:7sR`T~J&i"ߕ0$.M"67;=D4XI b=t(9p8``?7p#ѓ^kGjf`g1=z$t\)-9KxH+P1KV `:uHBڢV +d&z)9nʶQL ޞw9? g[,^1!1L%f31R 2+,>(;:3|vnOLB8OiJ.AF-spsAsi.{CtVG0 $]i@3"VTfh%f^ ``뛦^''|%ߏER>yZ."M#QvB0~P6ə`=˱[pxq2Oe+6@"ƫ7<CDq#.F)@Ώn%gk'jY:x1)|B>Jpsu}@q=6Ë0͖P2*  m4LѨS`0惾5XIIC:ݕ+Hx!il*x$8l\R"*'sg^茕 +o@ߨ@9M&|R]<\tS@)=1&P1hȠ JIO,9wIisA[ӂnN/Hs^RBO=Ww`F%*砗47Kb891hئ.Aă]KNo ܁ 4TrI.}:7A r*^lz*b52'HԒ5蘞9ΐ;ΔU+])%RP; +$I?!/5hH #Q2N ]NT›?TFj茛 +xn),s4C2)62I@PxOfzɭс? z=x!~4HE@ekX`0DwJpq~4`NvRȹX}E3=sEDb[r4 JF/*fn34< xnb1OoH(*B7/^ +` |'>\H6eP$Ø^ -z8 0 KvX/ #).^ɩpAΚ:pax?鼠S C,XRx$UBJ)F[!'4Iz :Dѥ RT::(|$:ŐFA4Cb +^+dM@6R + +Hx#W D,W8H٤;.m7֞9WvLK6Pqo 1+ ~% =IsKCZq`To2Nx|a=ƚU4[+ *p \quwi`X﷤qke-Ԟw:~-[j.Qx옑UDMLfP2cfcܿVla;'?l뚊bwڤ6 WhjLyPUV \ 7X:Hpu=@Bn= ,:Zo+@~W9w)Lmt*rK9IR3s|.&2!X[W7by7k/A qJzo =|}Askgsze@J 8D~<)ݢ_~4ߛa^dΧw^z9ЎtxMNVnN|2Y@fgʇI%9=YZkuyzzφۄlu>0ȂfѤݟa}Yi[mD۫qjl?tcx=Hӡ ;{`fFdV2=2?Zꂲ8)~Y( x@",,(g;@A/t{* )ڱ$AvvK ̝b}-b&PGG'j,TPRe2 -`z5sm$VC脺Bz@2 ]Z +3)J3 +b. 9\ۑ(s-RXeaWLɄ;iЅXp+qV,T<ѥse(:(U ze[^YL1 +|P j"Vk% +R2C8rl0~ܺ b*^ir *9i̡&K) +$5^сwO!9uM iƸQ# @7>Cg^*}?}s>5(mgLkHFhv;糿&qT\߮-~U^5Iƙm~kB%(q"ѤdJ3o(!8QmzM (| +J(ȴc(甞ӥ 1dDQie1 ǕSV٣[%$}@IzȔ8FA@rhSM[+a AW;UĚ):4*^WKR"mV!g lr4mjcd斢 3ڡӮEV'FQ(*KzaSA4ڡtJMH;f:~P)v[J(FBܟq4o3J.\ЦfӮ=c~hw*XlY3# eP3Ghk+dD*9ף4 +|`7YCXT$}`9%2 P0(S7* +aF -/!.z^i,Vi2?BWXZ&Md}(`1"HEg3ֲ+Z>l 54h$+.xJpk'W +9DU gwL14:@ShB`K~unۧ'N}ܯ0 q9zxP{7GkxT+Y@Dko1?ol%~*I3\P=#<~sofS4,q:vO~,'~txsP[[y]؁6/`i. Z}}r~Jyy:j}uٖ?xa6uFl6O-c\diE\zBGd13uj\d5_rގe#|ʒ1 16C `zw'eV9qְD?A~s0h~!6j=ް.ye%-yXYwT/O~'Htbl/luJ O _Z`'_ȓDn /-\q_sFĚď s} ^ipǏIsX߯wߜ}y 8J^}¯O0jU za9|fS({ymn&Q[:vk'AKBkǝn{:M[n>l W^6x>N~51[ýn_kx.řmjM i[`6 &pn[7Atmi/YW냝mYm9AvW֖x)<";5rq n'xPl|ЬmAY4칶)|4sf\Cϡ/sꕆ9;q{G|mg}X{i9a~S=C_[!U:4ڧFzxkFD}oEݮ?_[mlax9`T9n/Zy`ϡ$<<33Z33x2ѹ J2^| KykUg}jajk0ww!JQgNw|e<zNjBÀ>Nаrw./ EJ[7hi;n ρ.d.j#[}n ; սvc./lX̥Q ,7,`6B%9)$|nD3As'N YD۰ծwyd{8+۳?+2q5 f= WWEhUSyղ1 &/6B:;HӧvQrV]g-[9>$jk[;G4>OEmTlnyq秃aȓ_RNc&?G #9$q{~oz'΅'ۿpd}1n{>7 c7ִ  J* YrmŋvYC-~"ۆ:chޗjhTɛtB3:P賺3G;4 +#&u!1kqai9]}@|>ڎU']yV cwB˄@K'x(z9uۇkQ$G6:U}hG=H|C +Dؾm5v\4:IDl >Oz|

/hq-(ڱߎ.U'l& n0둹~Fn1`_|>Y0f|N; TBma);;VƦc|es=fv^>z7[]ci|)dtSٷj- +\P[puwqc<+Qe*hj׳ݎۯz4}I3FǦSt~SZSkK6]fᦔIa9hfmxԬXذ?ڀd{B ׀ +jncX;;= #u;5s\/84uwEQZ@7jf0iǬ6Bfz]f0j ΃LoF95[i +'O>vM׍~wmd9IRlo71Eޝږ[FEܘ"ږI0IHa5ɬ{QjN/Dw;Q{\5A)X8]?r.C%8f 磖 hFw$9횵FC('vcClfE޸aP=:=[6peA=agnR3B~G 0lXAl1\ZNxhЫat^❣rؑɗ'q"7d]p8:XP(a=W9Qj' +cAO^c>^uʩ;hw o[32<ϊ.2/v(dc߰A^|9,|#+j ª.$Rݫ7__S&/r(gIH3\T4!;.qp~1,scxI_#TT75C\qIC!ær˚K;+^m$+o~y+OOnv.=\tcJ>?5{`-|sս>^65J]Nɯ>_[}7X< SӋ6/m6oJÑ6_O՛K'՗Sӫýgo-̿8{а/̟\_1}>s`j9_GÕەՆx՗ԏ`ᇮՍKsŧ_v.whS姙덵~6r>޺4^oW>__i^[F~j]_;5vW?]_=Xxy^r.5K[6/ReW;u("ñh {]=YG|n^vz^(XV/G/]q}[~2^xw'Xjj;3m<7?s7.px:Nx `}o-Ut]6gfW7k{k~FܺsMᐱyCv{u8W7>u:GKO?,?^_9xX$kdcr; 7Y~{NeSfͧfgä͗V5O7{[I#6݃֠{%g/o<ռyĜY <|zuz.̬SWdK/v7kųWSӷ'k:zk7O+Z;vW7 {suiݻ{V`Uo]_zz35p+wE$2]Y=X!g/2 4͑{ߏ#I|}ݵ;^wd)uibgzKMvW=z0~{M_a-:|ra2{֟ϼ׺_4\_m\KG/=g|iϪflyg;O&[]X:V vZ[o I}^!};l i +MMƐPXYد~zq)[up2}hkoy߅y؝{ߕdɲȮ9ù{4oFQ{zu땅;7ybygOv^?xb^Iӽd^U|sleݥRy󏖏_7ց?7aX޹vbT<lc[ RǶG7v|֣׿Rӥ̾J܁;3 $w0|7|fCt;X{_dRupiiyf;Y;o-|؉ + >w[ n-Z|^m.5?a1/O5R,R<姧kA <)e^\,|uWaWıv/Qb߾:{}[&3N6=Du".L>}>5ޯkՃՃǧKzqZc=[Q3y8}ljK>Xxbiwsa97:3tik{ևkf_ѝC1׌[G A'f/-~h?^.{y -6glObosC.|)?[n$^dwo>}xWѥq;2/u{{{@ݷb͛fV^ug+;f}߭}4lrk{fo>]ݽr)"{MR1Yl;@\^_YzoTōWgH-vrT-E.'g6APSivscשgwRc%;^}v'2eSW/RV^f(O ]i|f:lL7(.=Y}5\zZyduY._bߎtpR1OVʇսkrvtV>\|nѣ g;sk`f/vC_οY_MF==[7ﷁί>epݾqz}1~4{[t0&s䤿cxM̒qt|H>qT{ry}utOy.Q,d~ +-;W,ھs=y]i8qzoB@BB׏?I6nƔkٌh4ͭyu8-Y,-ns6H1l-R|5$I n͢bf*OQY{"Sz5d~ /c)3#SQB;++Oj#^&cؽ rV"Cg9]Q2k +c.fAFZx}wWXi_Xy3! oTcYI'=gظ>Mq彆=HOymbe3賌[^D7.z +ĜuLyEFwod<>%Ny{rԙKVcvL <F +F1k.5~i,XƌfPuVWDž(pzʌ +E).opZ15mlr8pdbyCxE0A,1gF?=m۩oGXuY*M)fHh,4t\^wx5 5{39Y?O+?i=^_i;Z "8V59W&uc8LTf/aI,C@Q +~j̸ Z2Dkڄǒq=6 yڒy1\fx׌oJcodgY̐ k)m$~K8,bX{?cv`Erh;F:>2gwdt}-O.t5ӫYLm3 ͵w{}/o okGKBPEٗef`C#I5~ndO0^ч!I;WrT#@ +i7e[Hg^dL/h2K`7 FwTRZ)`a*slk-gLYC&bE`;SïON3E&LJk?ګ=BC)HF`9;lcq4Bf·7{@~/::tX`3li˅#91c~ GLckM5/TJgfܥpcw`Os9Bϩ/BHxK!c/)d`/w|QBB\䥙yA__ħp&EPm'3 +,M*g;g\~|q Yl}˂'˧olhaX#P)W%e&}8Q*JT m3s`;~2J@be0IaVc{Jʿky|W|gyǸ=`gL&5=(Z_n||O5Q[_d ï`ʚq;l/i߁Q : 쭆 ~E 8=~6WGzٙ $t@˼,`. +k3`=sRJxcw0d + xm`jS5D,C̷̤u؂L2Eq.*b;!bϺ yO{6Bנi*A6G2O\k}HZzOߜD6xϢDڥs߻-YMǚM28vݟO8 g.3OL?aTi.hKKlRKOZ`>}gEW2@߳[#Nk%@&esV=)jM9?*`}%ZT顭@ͥ 0SH_8wSD)="Ys}0,a>aSˌ:bV$h\̌57bn޶o?|䀲1qt'mSSx웂x3Gz%]`bȉـ[1a\?a _Ukq1Jwcԩ<놘1La; }~ƶD +, +c5aAk +Kt#,mLurmW:VU41͂U6nxش^fEJb39bK#VTYlaG;bI&ZcBgԚ֖Z0IHKƠ Zɬ }m4yݝ6xk3â[~n_mm[ mt?vTF +ʓڟ& mm՟MMN[uFwtL:GݛTׯ)οtЗ.K*\JeXWܺoء,& &^7tdWcЭߓ k+/ɞۆم>Rbd0sTܡ/ַjU*ǬӀNa`;o%ddm61bH_uCvQ[ŰP9c1 ;kʈ&ni9iC`c?q`z[Ec\F{QNm{zu4xh;LcrNSō۔gj?5[(]iUʏLe, +)ί@ xhNgBshEsc]7ɧyVؖ k>&Z)1W*E,iRKչY:d2`EOj!Yƞvhν(Z3n&f>N&}gг7bHfd S[j?[&ڃ[;mGOcG쯎MϞ>d~σT)JWeۡ]7!bs#k;_Gӧ~X:%ze8s[P2ҫ9)6sοϥ-P.<)7tUG, ]@=]u8+N5uKQ| % y3تěEeĔHnu" 8IM62IKv,9}䮤3QT}Myݖ1T՗(P>Fnmk n1is_}FwkӺ;V=?xlKhl2i4]{vvk&^?1y3 Wx?w}־ XfeŎ!~cfpm)W\n/&nG$)~J b50Zz`_AO fYl;_mD#B&Nf3Tt5RRh.B ~:k,Z]|_'u1;d8bYogxۂس}$"oEk>2(яlƨÑ\F#oA-U=o'u> XlZnpSO<=~i{o"P;-O Y]b-J%d)4J;YWy; +35h/\Oto},S |L+C ZY?-oYl}TuuHneH>n曽d7,'lzc1(,ZH\a>9} T7ŭE[XI$ˍYM7{{ommOULd=+T%]5ퟪX_?*Ac}ټwG_{ޚknlֳi}XlokF5h4_Ӧk&z`5вVy)߶%Qwk W7hH ~;9^?M G?GpQ~4*m)Wp[ŸX?i8 ziiW7`_9r6QLFmlqOXD'X=USZޫik֪N>)'3wVUg g^ $}~Uڝ+F}S$ԍa-:Vrq-Z3Y~}w/ƿٲ5]uO|"k}di_s7,-Wtj}4nlXAcϮhcAk?+ ppwx=7"nVKeir8w[[ 2Y0JgCB%?kXA9%[3٢ w[tq'Z R =Q1 +d9{x»)i {ʅ1Lq[d +-̾_`[q +1{x]U@ʠq;ZGP,ύIa֛iܾ}+ĊCğ{m;3w+੏L ß5[3P-鰾J^g_*С*qNwk9l6fF\l,fDGZ:]߅~u7%5J':%+߀$S+}aKWZ%n-r3`2z扩m^o鞂MX\t_Ix&QT{KX|չJ—6¨!Yi6G6dK~DcT)iZ8m S\K Խ`*LJ=hUI2]h?X2&^_FOHV:*i zKtt%ӯJRBW)(Ytn7Jo}EV*`WgXIJ'x>nIW㦹Ej bu(Sia183MAx `VQAo m[}Ld*|Yf(Wi +/[T):5 ~oC&JNRB9ȢA[zd-[6J򢯽,*n_+-Qf&t[JU +t25j2 UvI@AeG岯],#SiߏJ~JO*jGyZ{C݆TmUARC@xҜFS锲&4JISĜ8U +jŕ60S1jOzD }hF0P yWPix +sZ/]i(:Dоu+ &RX S~LװR\T~o[D M,.IǧӒm%N+_!c^]E9ڟ3^ii9_6xP_K=\ʶ>m.Li? 9&|lOmNE&j PנIGitG:3|ܯ1߷>Mӹcy9$zjkQi*8ks]K3S/蟱>2CF1;$(`ۀ,SIoL}Y)\u0sXDG +47N7#\ePZ#땭O O-@;i۽VcE;J:IW~V +_Bdv:%SRwyu^3QURJ^h= *t_aRRdT +r:0Zj _Hs*UZ2Vl +R8B!y\= %1[yoxZs_Mn7Lw*W,cEgW;<M$s3XfGs"[l֡NuR4#@NZ d{)&iX3ũ&H7L>Wږ=s-^µ w*)XKq!@UP`zm3׃Q@<-%I5jȋod[(܁@[N5?LͼDLl4ǯ>~:VFƤzצԏ'P,b5%Q#gf7I'Wlw;%F<gPX.U0-T=\t?3mFZHn6 `v;b-ͻUr$5*:GE[5VgF4A!)WU׫yidFBA-M0@vv-; r>A:)aoޔCmw2_[,9LOF/rE jȶ'W=!&D}wÞ>=0uS| +dlI!3AfpKtw*Snp4(/j@Sj3`Fuؒ|}yx~PDzprA 'ٓ_Pj9Rb).A$YGK8 /).]RU!Q%.Yqk\"ɪ e6]kČnK(uo$qS +'|{DB*~DkGcPҭ +p庯O +d5;^UӛOV`JNbSȁ3*iؔw=cPnZmL"nZ"KYv]΋Ǯ/wxE'!AAҁ{@ih:ƌ85NΖ-t!-K*?~ +YǓ$h6e;uNx*Jy[靅aI>@iTn$ok s&NytT$G嬓os% No[D&d]B3TrDibHg|2F崪b% X vXTh4II0&{T59v޽b]h'H  Fy:k Mq<~(fǥ1HYEQAm]o <qXy}2ՐJ, y(5imjka7)YAV -1P4u:L{Y*<lE'U%Ng jJXd*9U5JD\L.fE.E<΂;|cumtLc85q DЉCwctH'?C'[)Bɢo)#hG0t:NA'@ >SF)o)#Z :l+@б;1t:d] fUSFږG]T4IӛmUʈwæ֯s@ /%ظO!@\XQv7kN18,uCk9y.zno JIR9ؗ@stMZ +: +BǏI6sWu:iT݄:$1cLmk`7O[A[kې @ +#& ".OlQΚdnY)R!7ALux}o%uckå+r6%f8ʚ](0$Ɓ]5TNboM_t<4MlJ_ \bRM:`z0%O-ϿQat:$4;CBī ǮDwhwqL%CK ٲ>m;ߥx9 KpE 1&3V9=vweSٓ# QĵQ8j5B#r6i]w2h]&{% +Yő''핂<\SG,睌Io6V.<"(axnc;0;q|Z\4hD[ y##h EPhp2__V$8&p4$<,a + sw:xdx@q Zq <2rkn\LE6 +aP$nωd] __ L޻t|"tA-x Wwse*թS +HNТ%U˳-Cw2 .<1ux']]=ZG[/+^ +* vhTez>1rT^H=ګ x/T0DλH'^P|*BnmMEpFT5n*کh\LQׂSyлgfJ;"q +R&p+hHt' q}ג)+Id'Zk@5ӯݥ@8%;OD$64}~[tRs.T N'(/餰t"tRxHGt0sVtRrF49 V> M'ҩM'k?g`Njth:);4NtMZ-{tRƩl4IDC=S.4m\4z=? MwnF)bV?M'Ed4i( h:) ܽ>MwdIh3tRg!{|6b7]$2I87Jd0߽es&)ϳ[tn%A$$d^5,T zkIjUrƒ9uSe&IRj>)]QpD QJbse-K)ix\4Is'] +2͝KNUr|2vj(zTQy8c('{8}NѶ{hPD8BEOSj50:yVҩh\GqZo"ܑu %Y]-P$KSe,u7yO%Bu%\u4 u5~Lu4 .[9龧 ? +gF_%U]"Ϙ])|ˎkK`m}[!$2F2ZF.YV6Xy5߫R H".#wr1)-KRӳ2U#bsySj7xasy'@y4[5M< n8-Vp &1 [* <g;+r1 %+[}[;%,hLdʻ'R)Q<)iNKR'ÝRW wjo5ǁg^UkfFC离X+F>zA|aY}=WzH)x2n/v<Vb\f˻.b⟊BaAP:}S cRb2(oScv#Ŏ,)hVPWܶ2hV3+ՀLw_ٹ $As'*"PN k)ܿܚtXyWSuۓ0]٫na=^ɔʛl +u+hjԳƕ(;~ 7RK"p)ݼPRQ[ތm^)[EL6E/<ȵ3 ;RNc8Z*U%L2x8<o Npϫ-9^_RmfK>S\6 W)\JдgR"IU1@s `x1kUR~K97R4W>*Ey9R\RbKtDvjĀ0OְK82QI\=>[qm1-]dkopL*مNe$`ǂNY̮NXtC"I mdF%@̂W2$Cd"3 ~KNt+44ItukX5ݜeQIQZ7Eii J$-㴘\ƊU +ӗ9SJyW__Ϻp^V{3L?r-I2.MZwzhBࣗJ'k+Na$HN27;0'ԛctbu=}eb?u'Uq.L'S#8x"iٔL7a7sk2*Z&Bjެ +ԫrR^Ki|eY:PG0Kfص,:&1VhUycW<;)<|cWı/C.,e29* Bt +Z ?Z Qj1k}7 +P * +;Lqm(@@ȱQ?cu+ +P-#߅i^IW'S푤|]x'Ied{NR>3l||j@Lnoո-X^?c"z< R+p^?e3GFn맜[~TS{}B^?~7 +Җyjy(ߥ+O[)wV):|Jw^V?{o)'}y8%AQ^?Eey,aH^ eV ;)waJOm+7Yʆy8$/pе2S:⠈| ^]ÇViJG|Z7*JP*Ut1&qo,X!12|DK}# ,+{GT;qs}nj(d"@i5;VSbsoqbɌ1˕ݻg43gѹ<{x_6z^4o]<ҷwWx/.zII7fIZ8C}~j5$bLaW7B}+>k$4[y]jM!yRy%L[r0 ee5ݎQCu$9/6tZ3Y:}-}B%lz_m$NjȖnurp Avz[th焻f0:ҚqΟ3ϊP@k؎4V$f\4Y9ܖNs.'*pWgA $JL\xLɰp~tpG3z~g+s3c,@elDe6e#NvaPAw~I;]F< +doO9GswjVdO?SUZ#>3Sк uZlEL6nDe0q1ӴFw?τF%B~gSpD'r$0g=@Sh,>h}P-LdgeʖZS+37ݵ%Ύ xttyYyYSsY_ j\ uWoq`W6nlspsg٪̱8hf=#vd$5̂eYvq,`{R CeBτS²@t]PLb5a$Z LL`*CYX OX,rBx0/OgW,v_:񱗋- 1lιΛ첫o w9xzÍ }a +fz0b^ 1_fhyג! 5=sE<7w3Fy0hރ&zN@c`}ggWmm cAd l6e1:$Aѽp=(-]0 ++$!m05'CWΰw7'+3Ւ.2V}ǡ";T;4Y4XrvTnʼnj~-_Ʌ7SIu~V493\r>]I_x8G 14N.|m_#WLCag sk Mp=vð, im9'r%< C{gktBC-7ӟ+irS|} +@<~8Rɫ1653i%۩1q5&huռes;z.Ov)TԚ--jcK}k;:4=HfffH ON` yF$];h3΁OCg$QC'َ^a?b+q'#,!Nd|O3ΔdV5c+cDBC[Όqtᅬ  KPnq`Կufo @);S +U*YY Z\SSNXK:³r-d8Gz\aBGP[mY5ڒ936@eiGSd~:G'}c9coEM\:VZ ) ҫ@T2[ƈ}c8w)hq5{"4CM4{s2>FDhE+O6ZcƭrA@s/#2QU:-=~5?޺;熟|`IT/Y;_77\MRGwBjt) Wԙk#@d:Y2 ̴N\?fP"HeݹwT6.980/L~cuOtynw!/?Ob2agr21,F_7?Ywɟs2 CC#f΀8GLL18$~@ *⦮ t,% nF3(rVA1NDǵnL^8TQ1}%pGO m6;4koOxof; f旰Id`|22=(c@1?&c%# +c$1Q3-̏Șj% Nels>2ȵ$EȘZْt&ĭBOn)JYru4yu+F-ojYnE%*kn6#."GgGb:4<Lm66P +mB%j3ŭIħ#XӖWV"> oRr˶R[~FQ\%ZkƙbASpP[ӝFi-%W= DV8vyh +`{y?BE`bO?IYCvl]Zo(&[g@goRسR[*ǀC6F0n6&#&l٧2[{yC@!rWs1h'@Y̍(1Dc!!sER%vOZrכ.)Jahs;^?*:o0v>R-[>VXUQKB5v hOp&ή6:RGm C+/vId|' Z._{cXNRh@nیdl@en LM-3nu)ߒɤ-iЋ=Yu zAm;M;j")GD1@I}~Pހ!qA TC:^KWCx}8% N +O8N9) +6Ƈ;Al%zJv7ѐ, µ祜'kfJAR/Art)J? +!|$;>/] +>{I,9 wxyN p(*>7>Ḣ6q$`p/jxCadZDYF5 5ދ.q5I?/陉f LII(n->4$&>x'5!# MN@jq l)K^o=ye@Ddđ+&Mƒ,@wӇS$K3  ^ad#\).7m#NV2BBঘ_<>Cd'f7/$d=g܀ + endstream endobj 50 0 obj <>stream +ā. 8AB&H%XJ 7` X˜W@s%x 8h򿒼Fc@A8h//Ts NISXkN pe@hA!ʅ_L|Sb-4"⨘]0&!酑_B+>]/}:ZbTy$Ey|  +Z{8~_ Cډ8m^>7/T#7VR0T'~}3כ8߂'/_` u1d4 endstream endobj 5 0 obj <> endobj 6 0 obj <> endobj 18 0 obj [/View/Design] endobj 19 0 obj <>>> endobj 16 0 obj [/View/Design] endobj 17 0 obj <>>> endobj 31 0 obj [30 0 R 29 0 R] endobj 51 0 obj <> endobj xref 0 52 0000000004 65535 f +0000000016 00000 n +0000000185 00000 n +0000047379 00000 n +0000000000 00000 f +0000266555 00000 n +0000266625 00000 n +0000000000 00000 f +0000047430 00000 n +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000266816 00000 n +0000266847 00000 n +0000266700 00000 n +0000266731 00000 n +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000050006 00000 n +0000050077 00000 n +0000266932 00000 n +0000047801 00000 n +0000053181 00000 n +0000050498 00000 n +0000050385 00000 n +0000048830 00000 n +0000049445 00000 n +0000049493 00000 n +0000050269 00000 n +0000050300 00000 n +0000050153 00000 n +0000050184 00000 n +0000050533 00000 n +0000053249 00000 n +0000053489 00000 n +0000054522 00000 n +0000069362 00000 n +0000134950 00000 n +0000200538 00000 n +0000266126 00000 n +0000266964 00000 n +trailer <<64A8AB8E858948269A0BCA92EF85B468>]>> startxref 267136 %%EOF \ No newline at end of file diff --git a/packages/web3-rpc-providers/assets/logo/web3js.jpg b/packages/web3-rpc-providers/assets/logo/web3js.jpg new file mode 100644 index 00000000000..3b4cf23de26 Binary files /dev/null and b/packages/web3-rpc-providers/assets/logo/web3js.jpg differ diff --git a/packages/web3-rpc-providers/assets/logo/web3js.svg b/packages/web3-rpc-providers/assets/logo/web3js.svg new file mode 100644 index 00000000000..df8a2fcb868 --- /dev/null +++ b/packages/web3-rpc-providers/assets/logo/web3js.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + diff --git a/packages/web3-rpc-providers/package.json b/packages/web3-rpc-providers/package.json new file mode 100644 index 00000000000..6904037773a --- /dev/null +++ b/packages/web3-rpc-providers/package.json @@ -0,0 +1,64 @@ +{ + "name": "web3-rpc-providers", + "version": "1.0.0-rc.0", + "description": "Web3 Providers package", + "main": "./lib/commonjs/index.js", + "module": "./lib/esm/index.js", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "import": "./lib/esm/index.js", + "require": "./lib/commonjs/index.js" + } + }, + "repository": "https://github.com/ChainSafe/web3.js", + "author": "ChainSafe Systems", + "license": "LGPL-3.0", + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + }, + "files": [ + "lib/**/*", + "src/**/*", + "dist/**/*" + ], + "scripts": { + "clean": "rimraf dist && rimraf lib", + "prebuild": "yarn clean", + "build": "concurrently --kill-others-on-fail \"yarn:build:*(!check)\"", + "build:cjs": "tsc --build tsconfig.cjs.json && echo '{\"type\": \"commonjs\"}' > ./lib/commonjs/package.json", + "build:esm": "tsc --build tsconfig.esm.json && echo '{\"type\": \"module\"}' > ./lib/esm/package.json", + "build:types": "tsc --build tsconfig.types.json", + "build:check": "node -e \"require('./lib')\"", + "lint": "eslint --cache --cache-strategy content --ext .ts .", + "lint:fix": "eslint --fix --ext .js,.ts .", + "format": "prettier --write '**/*'", + "test": "jest --config=./test/unit/jest.config.js", + "test:coverage:unit": "jest --config=./test/unit/jest.config.js --coverage=true --coverage-reporters=text", + "test:ci": "jest --coverage=true --coverage-reporters=json --verbose", + "test:watch": "npm test -- --watch", + "test:unit": "jest --config=./test/unit/jest.config.js", + "test:integration": "jest --config=./test/integration/jest.config.js --passWithNoTests" + }, + "devDependencies": { + "@types/jest": "^28.1.6", + "@typescript-eslint/eslint-plugin": "^5.30.7", + "@typescript-eslint/parser": "^5.30.7", + "eslint": "^8.20.0", + "eslint-config-base-web3": "0.1.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-import": "^2.26.0", + "jest": "^29.7.0", + "jest-extended": "^3.0.1", + "prettier": "^2.7.1", + "ts-jest": "^29.1.1", + "typescript": "^4.7.4" + }, + "dependencies": { + "web3-providers-http": "^4.1.0", + "web3-providers-ws": "^4.0.7", + "web3-types": "^1.7.0", + "web3-utils": "^4.3.0" + } +} diff --git a/packages/web3-rpc-providers/src/index.ts b/packages/web3-rpc-providers/src/index.ts new file mode 100644 index 00000000000..eea79f433cb --- /dev/null +++ b/packages/web3-rpc-providers/src/index.ts @@ -0,0 +1,25 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +import { QuickNodeProvider } from './web3_provider_quicknode.js'; + +export * from './types.js'; +export * from './web3_provider_quicknode.js'; +export * from './web3_provider.js'; + +// default providers +export const mainnet = new QuickNodeProvider(); \ No newline at end of file diff --git a/packages/web3-rpc-providers/src/types.ts b/packages/web3-rpc-providers/src/types.ts new file mode 100644 index 00000000000..9da4c1dbfe7 --- /dev/null +++ b/packages/web3-rpc-providers/src/types.ts @@ -0,0 +1,44 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +export enum Transport { + HTTPS = "https", + WebSocket = "wss" +}; + +export enum Network { + ETH_MAINNET = "eth_mainnet", + ETH_GOERLI = "eth_goerli", + ETH_SEPOLIA = "eth_sepolia", + ETH_HOLESKY = "eth_holesky", + + POLYGON_MAINNET= "polygon_mainnet", + POLYGON_MUMBAI= "polygon_mumbai", + POLYGON_AMONY= "polygon_amony", + + ARBITRUM_MAINNET = "arbitrum_mainnet", + ARBITRUM_SEPOLIA = "arbitrum_sepolia", + + BASE_MAINNET = "base_mainnet", + BASE_SEPOLIA = "base_sepolia", + + OPTIMISM_MAINNET = "optimism_mainnet", + OPTIMISM_SEPOLIA = "optimism_sepolia", + + BNB_MAINNET = "bnb_mainnet", + BNB_TESTNET = "bnb_testnet" +}; \ No newline at end of file diff --git a/packages/web3-rpc-providers/src/web3_provider.ts b/packages/web3-rpc-providers/src/web3_provider.ts new file mode 100644 index 00000000000..2bba9a7b880 --- /dev/null +++ b/packages/web3-rpc-providers/src/web3_provider.ts @@ -0,0 +1,135 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +import HttpProvider from "web3-providers-http"; +import WebSocketProvider from "web3-providers-ws"; +import { + EthExecutionAPI, JsonRpcResult, ProviderConnectInfo, ProviderMessage, + ProviderRpcError, Web3APIMethod, Web3APIPayload, Web3APIReturnType, Web3APISpec, Web3BaseProvider, + Web3Eip1193ProviderEventCallback, + Web3ProviderEventCallback, + Web3ProviderMessageEventCallback, + Web3ProviderStatus +} from "web3-types"; +import { Eip1193Provider } from "web3-utils"; +import { Transport, Network } from "./types.js"; + +/* +This class can be used to create new providers only when there is custom logic required in each Request method like +checking specific HTTP status codes and performing any action, throwing new error types or setting additional HTTP headers in requests, or even modifying requests. + +Another simpler approach can be a function simply returning URL strings instead of using the following class in case if +no additional logic implementation is required in the provider. +*/ + +export abstract class Web3ExternalProvider < +API extends Web3APISpec = EthExecutionAPI, +> extends Eip1193Provider { + + public provider!: Web3BaseProvider; + public readonly transport: Transport; + + public abstract getRPCURL(network: Network,transport: Transport,token: string, host: string): string; + + public constructor( + network: Network, + transport: Transport, + token: string, + host: string) { + + super(); + + this.transport = transport; + if (transport === Transport.HTTPS) { + this.provider = new HttpProvider(this.getRPCURL(network, transport, token, host)); + } + else if (transport === Transport.WebSocket) { + this.provider = new WebSocketProvider(this.getRPCURL(network, transport, token, host)); + } + } + + public async request< + Method extends Web3APIMethod, + ResultType = Web3APIReturnType, + >( + payload: Web3APIPayload, + requestOptions?: RequestInit, + ): Promise { + + if (this.transport === Transport.HTTPS) { + return ( (this.provider as HttpProvider).request(payload, requestOptions)) as unknown as Promise; + } + + return ( (this.provider as WebSocketProvider).request(payload)) as unknown as Promise; + + } + + public getStatus(): Web3ProviderStatus { + return this.provider.getStatus(); + } + public supportsSubscriptions(): boolean { + return this.provider.supportsSubscriptions(); + } + public once(type: "disconnect", listener: Web3Eip1193ProviderEventCallback): void; + public once(type: string, listener: Web3Eip1193ProviderEventCallback | Web3ProviderEventCallback): void; + public once(type: "connect", listener: Web3Eip1193ProviderEventCallback): void; + public once(type: "chainChanged", listener: Web3Eip1193ProviderEventCallback): void; + public once(type: "accountsChanged", listener: Web3Eip1193ProviderEventCallback): void; + public once(_type: string, _listener: unknown): void { + if (this.provider?.once) + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this.provider.once(_type, _listener as any); + } + public removeAllListeners?(_type: string): void { + if (this.provider?.removeAllListeners) + this.provider.removeAllListeners(_type); + } + public connect(): void { + if (this.provider?.connect) + this.provider.connect(); + } + public disconnect(_code?: number | undefined, _data?: string | undefined): void { + if (this.provider?.disconnect) + this.provider.disconnect(_code, _data); + } + public reset(): void { + if (this.provider?.reset) + this.provider.reset(); + } + + public on(type: "disconnect", listener: Web3Eip1193ProviderEventCallback): void; + public on(type: string, listener: Web3Eip1193ProviderEventCallback | Web3ProviderMessageEventCallback): void; + public on(type: string, listener: Web3Eip1193ProviderEventCallback | Web3ProviderMessageEventCallback): void; + public on(type: "connect", listener: Web3Eip1193ProviderEventCallback): void; + public on(type: "chainChanged", listener: Web3Eip1193ProviderEventCallback): void; + public on(type: "accountsChanged", listener: Web3Eip1193ProviderEventCallback): void; + public on(_type: unknown, _listener: unknown): void { + if (this.provider) + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this.provider.on(_type as any, _listener as any); + } + public removeListener(type: "disconnect", listener: Web3Eip1193ProviderEventCallback): void; + public removeListener(type: string, listener: Web3Eip1193ProviderEventCallback | Web3ProviderEventCallback): void; + public removeListener(type: "connect", listener: Web3Eip1193ProviderEventCallback): void; + public removeListener(type: "chainChanged", listener: Web3Eip1193ProviderEventCallback): void; + public removeListener(type: "accountsChanged", listener: Web3Eip1193ProviderEventCallback): void; + public removeListener(_type: unknown, _listener: unknown): void { + if (this.provider) + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this.provider.removeListener(_type as any, _listener as any); + } +} \ No newline at end of file diff --git a/packages/web3-rpc-providers/src/web3_provider_quicknode.ts b/packages/web3-rpc-providers/src/web3_provider_quicknode.ts new file mode 100644 index 00000000000..a1b66880110 --- /dev/null +++ b/packages/web3-rpc-providers/src/web3_provider_quicknode.ts @@ -0,0 +1,91 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +import { Transport, Network } from "./types.js"; +import { Web3ExternalProvider } from "./web3_provider.js"; + +const isValid = (str: string) => str !== undefined && str.trim().length > 0; + +export class QuickNodeProvider extends Web3ExternalProvider { + + public constructor( + network: Network = Network.ETH_MAINNET, + transport: Transport = Transport.HTTPS, + token = "", + host = "") { + + super(network, transport, token, host); + + } + + // eslint-disable-next-line class-methods-use-this + public getRPCURL(network: Network, + transport: Transport, + _token: string, + _host: string) { + + let host = ""; + let token = ""; + + switch (network) { + case Network.ETH_MAINNET: + host = isValid(_host) ? _host : "powerful-holy-bush.quiknode.pro"; + token = isValid(_token) ? _token : "3240624a343867035925ff7561eb60dfdba2a668"; + break; + case Network.ETH_SEPOLIA: + host = isValid(_host) ? _host : "dimensional-fabled-glitter.ethereum-sepolia.quiknode.pro"; + token = isValid(_token) ? _token : "382a3b5a4b938f2d6e8686c19af4b22921fde2cd"; + break + case Network.ETH_HOLESKY: + host = isValid(_host) ? _host : "yolo-morning-card.ethereum-holesky.quiknode.pro"; + token = isValid(_token) ? _token : "481ebe70638c4dcf176af617a16d02ab866b9af9"; + break; + + case Network.ARBITRUM_MAINNET: + host = isValid(_host) ? _host : "autumn-divine-dinghy.arbitrum-mainnet.quiknode.pro"; + token = isValid(_token) ? _token : "a5d7bfbf60b5ae9ce3628e53d69ef50d529e9a8c"; + break; + case Network.ARBITRUM_SEPOLIA: + host = isValid(_host) ? _host : "few-patient-pond.arbitrum-sepolia.quiknode.pro"; + token = isValid(_token) ? _token : "3be985450970628c860b959c65cd2642dcafe53c"; + break; + + case Network.BNB_MAINNET: + host = isValid(_host) ? _host : "purple-empty-reel.bsc.quiknode.pro"; + token = isValid(_token) ? _token : "ebf6c532961e21f092ff2facce1ec4c89c540158"; + break; + case Network.BNB_TESTNET: + host = isValid(_host) ? _host : "floral-rough-scion.bsc-testnet.quiknode.pro"; + token = isValid(_token) ? _token : "5b297e5acff5f81f4c37ebf6f235f7299b6f9d28"; + break; + + case Network.POLYGON_MAINNET: + host = isValid(_host) ? _host : "small-chaotic-moon.matic.quiknode.pro"; + token = isValid(_token) ? _token : "847569f8a017e84d985e10d0f44365d965a951f1"; + break; + case Network.POLYGON_AMONY: + host = isValid(_host) ? _host : "prettiest-side-shape.matic-amoy.quiknode.pro"; + token = isValid(_token) ? _token : "79a9476eea661d4f82de614db1d8a895b14b881c"; + break; + default: + throw new Error("Network info not avalible."); + } + + return `${transport}://${host}/${token}`; + } +} + diff --git a/packages/web3-rpc-providers/test/.eslintrc.js b/packages/web3-rpc-providers/test/.eslintrc.js new file mode 100644 index 00000000000..a98dfb6d823 --- /dev/null +++ b/packages/web3-rpc-providers/test/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: '../../../.eslintrc.test.js', + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, +}; diff --git a/packages/web3-rpc-providers/test/config/jest.config.js b/packages/web3-rpc-providers/test/config/jest.config.js new file mode 120000 index 00000000000..b875b48129d --- /dev/null +++ b/packages/web3-rpc-providers/test/config/jest.config.js @@ -0,0 +1 @@ +../../../../templates/jest.config.js.tmpl \ No newline at end of file diff --git a/packages/web3-rpc-providers/test/config/setup.js b/packages/web3-rpc-providers/test/config/setup.js new file mode 100644 index 00000000000..0b6b9109ce0 --- /dev/null +++ b/packages/web3-rpc-providers/test/config/setup.js @@ -0,0 +1,24 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +// Have to use `require` because of Jest issue https://jestjs.io/docs/ecmascript-modules +// eslint-disable-next-line @typescript-eslint/no-require-imports +require('jest-extended'); + +// @todo extend jest to have "toHaveBeenCalledOnceWith" matcher. + +process.env.NODE_ENV = 'test'; diff --git a/packages/web3-rpc-providers/test/integration/jest.config.js b/packages/web3-rpc-providers/test/integration/jest.config.js new file mode 100644 index 00000000000..1d95890206a --- /dev/null +++ b/packages/web3-rpc-providers/test/integration/jest.config.js @@ -0,0 +1,33 @@ +'use strict'; + +const base = require('../config/jest.config'); + +module.exports = { + ...base, + setupFilesAfterEnv: ['/test/integration/setup.js'], + testMatch: ['/test/integration/**/*.(spec|test).(js|ts)'], + /** + * restoreMocks [boolean] + * + * Default: false + * + * Automatically restore mock state between every test. + * Equivalent to calling jest.restoreAllMocks() between each test. + * This will lead to any mocks having their fake implementations removed + * and restores their initial implementation. + */ + restoreMocks: true, + + /** + * resetModules [boolean] + * + * Default: false + * + * By default, each test file gets its own independent module registry. + * Enabling resetModules goes a step further and resets the module registry before running each individual test. + * This is useful to isolate modules for every test so that local module state doesn't conflict between tests. + * This can be done programmatically using jest.resetModules(). + */ + resetModules: true, + coverageDirectory: '.coverage/integration', +}; diff --git a/packages/web3-rpc-providers/test/integration/setup.js b/packages/web3-rpc-providers/test/integration/setup.js new file mode 100644 index 00000000000..5be1bccf7cc --- /dev/null +++ b/packages/web3-rpc-providers/test/integration/setup.js @@ -0,0 +1,24 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +// Have to use `require` because of Jest issue https://jestjs.io/docs/ecmascript-modules +// eslint-disable-next-line @typescript-eslint/no-require-imports +require('../config/setup'); + +const jestTimeout = 15000; + +jest.setTimeout(jestTimeout); diff --git a/packages/web3-rpc-providers/test/tsconfig.json b/packages/web3-rpc-providers/test/tsconfig.json new file mode 120000 index 00000000000..c73c54e77b4 --- /dev/null +++ b/packages/web3-rpc-providers/test/tsconfig.json @@ -0,0 +1 @@ +../../../templates/test/tsconfig.json.tmpl \ No newline at end of file diff --git a/packages/web3-rpc-providers/test/unit/constructor.test.ts b/packages/web3-rpc-providers/test/unit/constructor.test.ts new file mode 100644 index 00000000000..eb64b29163a --- /dev/null +++ b/packages/web3-rpc-providers/test/unit/constructor.test.ts @@ -0,0 +1,62 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ +/* eslint-disable max-classes-per-file */ + +import HttpProvider from 'web3-providers-http'; +import WebSocketProvider from 'web3-providers-ws'; +import { Web3ExternalProvider } from '../../src/web3_provider'; +import { Network, Transport } from '../../src/types'; + +class MockWeb3ExternalProviderA extends Web3ExternalProvider { + public constructor(network: Network, transport: Transport, token: string){ + super(network, transport, token, ""); + } + // eslint-disable-next-line class-methods-use-this + public getRPCURL(_network: Network, _transport: Transport, _token: string, _host=""): string { + let transport = ""; + if (_transport === Transport.HTTPS) + transport = "http://"; + else if (_transport === Transport.WebSocket) + transport = "wss://"; + + return `${transport}example.com/`; + } +} + +describe('Web3ExternalProvider', () => { + it('should initialize the provider correctly', () => { + const network: Network = Network.ETH_MAINNET; + const transport: Transport = Transport.HTTPS; + const token = 'your-token'; + + const provider = new MockWeb3ExternalProviderA(network, transport, token); + + expect(provider.provider).toBeInstanceOf(HttpProvider); + }); + + it('should initialize the provider with WebSocketProvider for WebSocket transport', () => { + const network: Network = Network.ETH_MAINNET; + const transport: Transport = Transport.WebSocket; + const token = 'your-token'; + + const provider = new MockWeb3ExternalProviderA(network, transport, token); + + expect(provider.provider).toBeInstanceOf(WebSocketProvider); + }); + +}); +/* eslint-enable max-classes-per-file */ \ No newline at end of file diff --git a/packages/web3-rpc-providers/test/unit/jest.config.js b/packages/web3-rpc-providers/test/unit/jest.config.js new file mode 100644 index 00000000000..ceac341e332 --- /dev/null +++ b/packages/web3-rpc-providers/test/unit/jest.config.js @@ -0,0 +1,9 @@ +const base = require('../config/jest.config'); + +module.exports = { + ...base, + testMatch: ['/test/unit/**/*.(spec|test).(js|ts)'], + + coverageDirectory: '.coverage/unit', + collectCoverageFrom: ['src/**'], +}; diff --git a/packages/web3-rpc-providers/test/unit/request.test.ts b/packages/web3-rpc-providers/test/unit/request.test.ts new file mode 100644 index 00000000000..ccbaa0f7cc7 --- /dev/null +++ b/packages/web3-rpc-providers/test/unit/request.test.ts @@ -0,0 +1,81 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ +import { Web3APIPayload, EthExecutionAPI, Web3APIMethod } from "web3-types"; +import { Network, Transport } from "../../src/types"; +import { Web3ExternalProvider } from "../../src/web3_provider"; + +jest.mock('web3-providers-ws', () => { + return { + __esModule: true, + default: jest.fn().mockImplementation(() => ({ + request: jest.fn().mockResolvedValue({ result: 'mock-result' }), + })), + }; +}); + +class MockWeb3ExternalProvider extends Web3ExternalProvider { + public constructor(network: Network, transport: Transport, token: string){ + super(network, transport, token, ""); + } + // eslint-disable-next-line class-methods-use-this + public getRPCURL(_network: Network, _transport: Transport, _token: string): string { + return 'https://example.com/'; + } + } + +describe('Web3ExternalProvider', () => { + it('should make a request using the HTTPS provider', async () => { + const network: Network = Network.ETH_MAINNET; + const transport: Transport = Transport.HTTPS; + const token = 'your-token'; + + const mockHttpProvider = { + request: jest.fn(), + }; + + const mockResponse = { result: 'mock-result' }; + mockHttpProvider.request.mockResolvedValue(mockResponse); + + const provider = new MockWeb3ExternalProvider(network, transport, token); + (provider as any).provider = mockHttpProvider; + + const payload: Web3APIPayload> = { + method: 'eth_getBalance', + params: ['0x0123456789012345678901234567890123456789', 'latest'], + }; + + const result = await provider.request(payload); + expect(result).toEqual(mockResponse); + }); + + it('should make a request using the WebSocket provider', async () => { + const network: Network = Network.ETH_MAINNET; + const transport: Transport = Transport.WebSocket; + const token = 'your-token'; + + const provider = new MockWeb3ExternalProvider(network, transport, token); + (provider as any).getRPCURL = jest.fn().mockReturnValue('ws://mock-rpc-url.com'); + + const payload: Web3APIPayload> = { + method: 'eth_getBalance', + params: ['0x0123456789012345678901234567890123456789', 'latest'], + }; + + const result = await provider.request(payload); + expect(result).toEqual({ result: 'mock-result' }); + }); +}); \ No newline at end of file diff --git a/packages/web3-rpc-providers/tsconfig.cjs.json b/packages/web3-rpc-providers/tsconfig.cjs.json new file mode 120000 index 00000000000..f8b17044cd5 --- /dev/null +++ b/packages/web3-rpc-providers/tsconfig.cjs.json @@ -0,0 +1 @@ +../../config/tsconfig.cjs.json \ No newline at end of file diff --git a/packages/web3-rpc-providers/tsconfig.esm.json b/packages/web3-rpc-providers/tsconfig.esm.json new file mode 120000 index 00000000000..f5fab722b38 --- /dev/null +++ b/packages/web3-rpc-providers/tsconfig.esm.json @@ -0,0 +1 @@ +../../config/tsconfig.esm.json \ No newline at end of file diff --git a/packages/web3-rpc-providers/tsconfig.types.json b/packages/web3-rpc-providers/tsconfig.types.json new file mode 120000 index 00000000000..c67a7816f18 --- /dev/null +++ b/packages/web3-rpc-providers/tsconfig.types.json @@ -0,0 +1 @@ +../../config/tsconfig.types.json \ No newline at end of file diff --git a/packages/web3-rpc-providers/tsdoc.json b/packages/web3-rpc-providers/tsdoc.json new file mode 100644 index 00000000000..776fc76a3d3 --- /dev/null +++ b/packages/web3-rpc-providers/tsdoc.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "extends": ["../../tsdoc.json"] +} diff --git a/packages/web3-types/CHANGELOG.md b/packages/web3-types/CHANGELOG.md index 3f96fe9d53d..9fba5349a3c 100644 --- a/packages/web3-types/CHANGELOG.md +++ b/packages/web3-types/CHANGELOG.md @@ -189,9 +189,18 @@ Documentation: - Type `FeeData` to be filled by `await web3.eth.calculateFeeData()` to be used with EIP-1559 transactions (#6795) -## [Unreleased] +## [1.6.0] ### Added - Added `signature` to type `AbiFunctionFragment` (#6922) - update type `Withdrawals`, `block` and `BlockHeaderOutput` to include properties of eip 4844, 4895, 4788 (#6933) + +## [1.7.0] + +### Added + +- Added `result` as optional `never` and `error` as optional `never in type `JsonRpcNotification` (#7091) +- Added `JsonRpcNotfication` as a union type in `JsonRpcResponse` (#7091) + +## [Unreleased] diff --git a/packages/web3-types/package.json b/packages/web3-types/package.json index 8cec2cee0b0..b2e7d7b3ff7 100644 --- a/packages/web3-types/package.json +++ b/packages/web3-types/package.json @@ -1,6 +1,6 @@ { "name": "web3-types", - "version": "1.6.0", + "version": "1.7.0", "description": "Provide the common data structures and interfaces for web3 modules.", "main": "./lib/commonjs/index.js", "module": "./lib/esm/index.js", diff --git a/packages/web3-types/src/json_rpc_types.ts b/packages/web3-types/src/json_rpc_types.ts index a361d018700..9cb98648dc8 100644 --- a/packages/web3-types/src/json_rpc_types.ts +++ b/packages/web3-types/src/json_rpc_types.ts @@ -55,8 +55,9 @@ export interface JsonRpcNotification { readonly jsonrpc: JsonRpcIdentifier; readonly method: string; // for subscription readonly params: SubscriptionParams; // for subscription results - readonly result: never; + readonly result?: never; readonly data?: never; + readonly error?: never; } export interface JsonRpcSubscriptionResult { @@ -91,4 +92,5 @@ export type JsonRpcBatchResponse export type JsonRpcResponse = | JsonRpcResponseWithError | JsonRpcResponseWithResult - | JsonRpcBatchResponse; + | JsonRpcBatchResponse + | JsonRpcNotification; diff --git a/packages/web3-utils/CHANGELOG.md b/packages/web3-utils/CHANGELOG.md index 763f7f1d462..5fcf44ccec5 100644 --- a/packages/web3-utils/CHANGELOG.md +++ b/packages/web3-utils/CHANGELOG.md @@ -216,7 +216,7 @@ Documentation: - fixed toHex incorrectly hexing Uint8Arrays and Buffer (#6957) - fixed isUint8Array not returning true for Buffer (#6957) -## [Unreleased] +## [4.3.0] ### Added @@ -227,3 +227,5 @@ Documentation: - `toWei` support numbers in scientific notation (#6908) - `toWei` and `fromWei` trims according to ether unit successfuly (#7044) + +## [Unreleased] \ No newline at end of file diff --git a/packages/web3-utils/package.json b/packages/web3-utils/package.json index 511ce216fc2..c07e9b85821 100644 --- a/packages/web3-utils/package.json +++ b/packages/web3-utils/package.json @@ -1,7 +1,7 @@ { "name": "web3-utils", "sideEffects": false, - "version": "4.2.3", + "version": "4.3.0", "description": "Collection of utility functions used in web3.js.", "main": "./lib/commonjs/index.js", "module": "./lib/esm/index.js", @@ -65,8 +65,8 @@ "dependencies": { "ethereum-cryptography": "^2.0.0", "eventemitter3": "^5.0.1", - "web3-errors": "^1.1.4", + "web3-errors": "^1.2.0", "web3-types": "^1.6.0", - "web3-validator": "^2.0.5" + "web3-validator": "^2.0.6" } } diff --git a/packages/web3-utils/src/converters.ts b/packages/web3-utils/src/converters.ts index 6649bef1481..9c94a4332b0 100644 --- a/packages/web3-utils/src/converters.ts +++ b/packages/web3-utils/src/converters.ts @@ -79,7 +79,8 @@ export const ethUnitMap = { tether: BigInt('1000000000000000000000000000000'), }; -const PrecisionLossWarning = 'Warning: Using type `number` with values that are large or contain many decimals may cause loss of precision, it is recommended to use type `string` or `BigInt` when using conversion methods'; +const PrecisionLossWarning = + 'Warning: Using type `number` with values that are large or contain many decimals may cause loss of precision, it is recommended to use type `string` or `BigInt` when using conversion methods'; export type EtherUnits = keyof typeof ethUnitMap; /** @@ -366,7 +367,7 @@ export const toHex = ( return returnType ? 'bigint' : numberToHex(value); } - if(isUint8Array(value)) { + if (isUint8Array(value)) { return returnType ? 'bytes' : bytesToHex(value); } @@ -386,6 +387,15 @@ export const toHex = ( return returnType ? 'bytes' : `0x${value}`; } if (isHex(value) && !isInt(value) && isUInt(value)) { + // This condition seems problematic because meeting + // both conditions `!isInt(value) && isUInt(value)` should be impossible. + // But a value pass for those conditions: "101611154195520776335741463917853444671577865378275924493376429267637792638729" + // Note that according to the docs: it is supposed to be treated as a string (https://docs.web3js.org/guides/web3_upgrade_guide/x/web3_utils_migration_guide#conversion-to-hex) + // In short, the strange is that isInt(value) is false but isUInt(value) is true for the value above. + // TODO: isUInt(value) should be investigated. + + // However, if `toHex('101611154195520776335741463917853444671577865378275924493376429267637792638729', true)` is called, it will return `true`. + // But, if `toHex('101611154195520776335741463917853444671577865378275924493376429267637792638729')` is called, it will throw inside `numberToHex`. return returnType ? 'uint' : numberToHex(value); } @@ -419,14 +429,14 @@ export const toHex = ( */ export const toNumber = (value: Numbers): number | bigint => { if (typeof value === 'number') { - if (value > 1e+20) { - console.warn(PrecisionLossWarning) - // JavaScript converts numbers >= 10^21 to scientific notation when coerced to strings, - // leading to potential parsing errors and incorrect representations. - // For instance, String(10000000000000000000000) yields '1e+22'. - // Using BigInt prevents this - return BigInt(value); - } + if (value > 1e20) { + console.warn(PrecisionLossWarning); + // JavaScript converts numbers >= 10^21 to scientific notation when coerced to strings, + // leading to potential parsing errors and incorrect representations. + // For instance, String(10000000000000000000000) yields '1e+22'. + // Using BigInt prevents this + return BigInt(value); + } return value; } @@ -506,10 +516,9 @@ export const fromWei = (number: Numbers, unit: EtherUnits | number): string => { if (unit < 0 || !Number.isInteger(unit)) { throw new InvalidIntegerError(unit); } - denomination = bigintPower(BigInt(10),BigInt(unit)); + denomination = bigintPower(BigInt(10), BigInt(unit)); } - // value in wei would always be integer // 13456789, 1234 const value = String(toNumber(number)); @@ -575,8 +584,8 @@ export const toWei = (number: Numbers, unit: EtherUnits | number): string => { if (unit < 0 || !Number.isInteger(unit)) { throw new InvalidIntegerError(unit); } - - denomination = bigintPower(BigInt(10),BigInt(unit)); + + denomination = bigintPower(BigInt(10), BigInt(unit)); } let parsedNumber = number; diff --git a/packages/web3-utils/src/formatter.ts b/packages/web3-utils/src/formatter.ts index 42062add5c6..665965a8eed 100644 --- a/packages/web3-utils/src/formatter.ts +++ b/packages/web3-utils/src/formatter.ts @@ -57,10 +57,8 @@ const findSchemaByDataPath = ( for (const dataPart of dataPath) { if (result.oneOf && previousDataPath) { - const path = oneOfPath.find(function (element: [string, number]) { - return (this as unknown as string) === element[0]; - }, previousDataPath ?? ''); - + const currentDataPath = previousDataPath; + const path = oneOfPath.find(([key]) => key === currentDataPath); if (path && path[0] === previousDataPath) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access result = result.oneOf[path[1]]; @@ -75,10 +73,6 @@ const findSchemaByDataPath = ( } else if (result.items && (result.items as JsonSchema).properties) { const node = (result.items as JsonSchema).properties as Record; - if (!node) { - return undefined; - } - result = node[dataPart]; } else if (result.items && isObject(result.items)) { result = result.items; @@ -307,7 +301,7 @@ export const convert = ( // If value is an object, recurse into it if (isObject(value)) { - convert(value, schema, dataPath, format); + convert(value, schema, dataPath, format, oneOfPath); dataPath.pop(); continue; } diff --git a/packages/web3-utils/src/hash.ts b/packages/web3-utils/src/hash.ts index 6e498138c29..0289a429a52 100644 --- a/packages/web3-utils/src/hash.ts +++ b/packages/web3-utils/src/hash.ts @@ -17,25 +17,25 @@ along with web3.js. If not, see . /** * This package provides utility functions for Ethereum dapps and other web3.js packages. - * + * * For using Utils functions, first install Web3 package using `npm i web3` or `yarn add web3`. - * After that, Web3 Utils functions will be available as mentioned below. + * After that, Web3 Utils functions will be available as mentioned below. * ```ts * import { Web3 } from 'web3'; * const web3 = new Web3(); - * + * * const value = web3.utils.fromWei("1", "ether") - * + * * ``` - * + * * For using individual package install `web3-utils` package using `npm i web3-utils` or `yarn add web3-utils` and only import required functions. - * This is more efficient approach for building lightweight applications. + * This is more efficient approach for building lightweight applications. * ```ts * import { fromWei, soliditySha3Raw } from 'web3-utils'; - * + * * console.log(fromWei("1", "ether")); * console.log(soliditySha3Raw({ type: "string", value: "helloworld" })) - * + * * ``` * @module Utils */ @@ -172,7 +172,6 @@ const getType = (arg: Sha3Input): [string, EncodingTypes] => { if (Array.isArray(arg)) { throw new Error('Autodetection of array types is not supported.'); } - let type; let value; // if type is given diff --git a/packages/web3-utils/src/json_rpc.ts b/packages/web3-utils/src/json_rpc.ts index 573d39197e3..495a425611f 100644 --- a/packages/web3-utils/src/json_rpc.ts +++ b/packages/web3-utils/src/json_rpc.ts @@ -99,7 +99,7 @@ let requestIdSeed: number | undefined; /** * Optionally use to make the jsonrpc `id` start from a specific number. * Without calling this function, the `id` will be filled with a Uuid. - * But after this being called with a number, the `id` will be a number staring from the provided `start` variable. + * But after this being called with a number, the `id` will be a number starting from the provided `start` variable. * However, if `undefined` was passed to this function, the `id` will be a Uuid again. * @param start - a number to start incrementing from. * Or `undefined` to use a new Uuid (this is the default behavior) diff --git a/packages/web3-utils/src/promise_helpers.ts b/packages/web3-utils/src/promise_helpers.ts index c8e211fbbb7..a2140a2d1c5 100644 --- a/packages/web3-utils/src/promise_helpers.ts +++ b/packages/web3-utils/src/promise_helpers.ts @@ -20,7 +20,6 @@ import { isNullish } from 'web3-validator'; export type Timer = ReturnType; export type Timeout = ReturnType; - /** * An alternative to the node function `isPromise` that exists in `util/types` because it is not available on the browser. * @param object - to check if it is a `Promise` @@ -74,7 +73,6 @@ export async function waitWithTimeout( return result; } - /** * Repeatedly calls an async function with a given interval until the result of the function is defined (not undefined or null), * or until a timeout is reached. It returns promise and intervalId. @@ -85,25 +83,27 @@ export function pollTillDefinedAndReturnIntervalId( func: AsyncFunction, interval: number, ): [Promise>, Timer] { - let intervalId: Timer | undefined; const polledRes = new Promise>((resolve, reject) => { - intervalId = setInterval(function intervalCallbackFunc(){ - (async () => { - try { - const res = await waitWithTimeout(func, interval); - - if (!isNullish(res)) { + intervalId = setInterval( + (function intervalCallbackFunc() { + (async () => { + try { + const res = await waitWithTimeout(func, interval); + + if (!isNullish(res)) { + clearInterval(intervalId); + resolve(res as unknown as Exclude); + } + } catch (error) { clearInterval(intervalId); - resolve(res as unknown as Exclude); + reject(error); } - } catch (error) { - clearInterval(intervalId); - reject(error); - } - })() as unknown; - return intervalCallbackFunc;}() // this will immediate invoke first call - , interval); + })() as unknown; + return intervalCallbackFunc; + })(), // this will immediate invoke first call + interval, + ); }); return [polledRes as unknown as Promise>, intervalId!]; @@ -113,7 +113,7 @@ export function pollTillDefinedAndReturnIntervalId( * Repeatedly calls an async function with a given interval until the result of the function is defined (not undefined or null), * or until a timeout is reached. * pollTillDefinedAndReturnIntervalId() function should be used instead of pollTillDefined if you need IntervalId in result. - * This function will be deprecated in next major release so use pollTillDefinedAndReturnIntervalId(). + * This function will be deprecated in next major release so use pollTillDefinedAndReturnIntervalId(). * @param func - The function to call. * @param interval - The interval in milliseconds. */ @@ -146,7 +146,7 @@ export function rejectIfTimeout(timeout: number, error: Error): [Timer, Promise< /** * Sets an interval that repeatedly executes the given cond function with the specified interval between each call. * If the condition is met, the interval is cleared and a Promise that rejects with the returned value is returned. - * @param cond - The function/confition to call. + * @param cond - The function/condition to call. * @param interval - The interval in milliseconds. * @returns - an array with the interval ID and the Promise. */ @@ -168,4 +168,3 @@ export function rejectIfConditionAtInterval( }); return [intervalId!, rejectIfCondition]; } - diff --git a/packages/web3-utils/src/socket_provider.ts b/packages/web3-utils/src/socket_provider.ts index d847dcbccc9..0a73fb8378f 100644 --- a/packages/web3-utils/src/socket_provider.ts +++ b/packages/web3-utils/src/socket_provider.ts @@ -187,13 +187,13 @@ export abstract class SocketProvider< protected _validateProviderPath(path: string): boolean { return !!path; } - + /** * * @returns the pendingRequestQueue size */ // eslint-disable-next-line class-methods-use-this - public getPendingRequestQueueSize() { + public getPendingRequestQueueSize() { return this._pendingRequestsQueue.size; } @@ -350,32 +350,34 @@ export abstract class SocketProvider< /** * Safely disconnects the socket, async and waits for request size to be 0 before disconnecting - * @param forceDisconnect - If true, will clear queue after 5 attempts of waiting for both pending and sent queue to be 0 + * @param forceDisconnect - If true, will clear queue after 5 attempts of waiting for both pending and sent queue to be 0 * @param ms - Determines the ms of setInterval * @param code - The code to be sent to the server * @param data - The data to be sent to the server */ - public async safeDisconnect(code?: number, data?: string, forceDisconnect = false,ms = 1000) { + public async safeDisconnect(code?: number, data?: string, forceDisconnect = false, ms = 1000) { let retryAttempt = 0; - const checkQueue = async () => + const checkQueue = async () => new Promise(resolve => { const interval = setInterval(() => { - if (forceDisconnect && retryAttempt === 5) { + if (forceDisconnect && retryAttempt >= 5) { this.clearQueues(); } - if (this.getPendingRequestQueueSize() === 0 && this.getSentRequestsQueueSize() === 0) { + if ( + this.getPendingRequestQueueSize() === 0 && + this.getSentRequestsQueueSize() === 0 + ) { clearInterval(interval); resolve(true); } - retryAttempt+=1; - }, ms) - }) - + retryAttempt += 1; + }, ms); + }); + await checkQueue(); this.disconnect(code, data); } - /** * Removes all listeners for the specified event type. * @param type - The event type to remove the listeners for @@ -512,7 +514,7 @@ export abstract class SocketProvider< if (isNullish(responses) || responses.length === 0) { return; } - + for (const response of responses) { if ( jsonRpc.isResponseWithNotification(response as JsonRpcNotification) && @@ -544,7 +546,7 @@ export abstract class SocketProvider< this._sentRequestsQueue.delete(requestId); } } - + public clearQueues(event?: ConnectionEvent) { this._clearQueues(event); } diff --git a/packages/web3-utils/test/fixtures/converters.ts b/packages/web3-utils/test/fixtures/converters.ts index 9f0324b68ad..cecb0b3fafb 100644 --- a/packages/web3-utils/test/fixtures/converters.ts +++ b/packages/web3-utils/test/fixtures/converters.ts @@ -235,21 +235,20 @@ export const toHexValidData: [Numbers | Bytes | Address | boolean, [HexString, V ], ['-0x01', ['-0x1', 'int256']], ['123c', ['0x123c', 'bytes']], - [new Uint8Array([ - 221, 128, 128, 128, 148, 186, 248, - 242, 159, 130, 231, 84, 254, 199, - 252, 69, 21, 58, 104, 102, 201, - 137, 255, 3, 196, 10, 128, 128, - 128, 128 - ]), ['0xdd80808094baf8f29f82e754fec7fc45153a6866c989ff03c40a80808080', 'bytes']], - [Buffer.from([ - 221, 128, 128, 128, 148, 186, 248, - 242, 159, 130, 231, 84, 254, 199, - 252, 69, 21, 58, 104, 102, 201, - 137, 255, 3, 196, 10, 128, 128, - 128, 128 - ]), ['0xdd80808094baf8f29f82e754fec7fc45153a6866c989ff03c40a80808080', 'bytes']] - + [ + new Uint8Array([ + 221, 128, 128, 128, 148, 186, 248, 242, 159, 130, 231, 84, 254, 199, 252, 69, 21, 58, + 104, 102, 201, 137, 255, 3, 196, 10, 128, 128, 128, 128, + ]), + ['0xdd80808094baf8f29f82e754fec7fc45153a6866c989ff03c40a80808080', 'bytes'], + ], + [ + Buffer.from([ + 221, 128, 128, 128, 148, 186, 248, 242, 159, 130, 231, 84, 254, 199, 252, 69, 21, 58, + 104, 102, 201, 137, 255, 3, 196, 10, 128, 128, 128, 128, + ]), + ['0xdd80808094baf8f29f82e754fec7fc45153a6866c989ff03c40a80808080', 'bytes'], + ], ]; export const toHexInvalidData: [any, string][] = [ @@ -320,15 +319,15 @@ const conversionBaseData: [[Numbers, EtherUnits | number], string][] = [ export const fromWeiValidData: [[Numbers, EtherUnits | number], Numbers][] = [ ...conversionBaseData, - [['0xff', 'wei'], '255'], - [[1e+22, 'ether'], '10000'], - [[19999999999999991611392, 'ether'], '19999.999999999991611392'], - [[1.9999999999999991611392e+22, 'ether'], '19999.999999999991611392'], + [['0xff', 'wei'], '255'], + [[1e22, 'ether'], '10000'], + [[19999999999999991611392, 'ether'], '19999.999999999991611392'], + [[1.9999999999999991611392e22, 'ether'], '19999.999999999991611392'], [['1000000', 'ether'], '0.000000000001'], [['1123456789123456789', 'ether'], '1.123456789123456789'], [['1123', 'kwei'], '1.123'], - [['1234100' ,'kwei'], '1234.1'], - [['3308685546611893', 'ether'], '0.003308685546611893'] + [['1234100', 'kwei'], '1234.1'], + [['3308685546611893', 'ether'], '0.003308685546611893'], ]; export const toWeiValidData: [[Numbers, EtherUnits | number], Numbers][] = [ @@ -339,17 +338,24 @@ export const toWeiValidData: [[Numbers, EtherUnits | number], Numbers][] = [ [['1000000', 'ether'], 0.000000000001], [['1123456789123456789', 'ether'], '1.123456789123456789123'], [['1123', 'kwei'], '1.12345'], - [['1234100' ,'kwei'], '1234.1'], + [['1234100', 'kwei'], '1234.1'], [['3308685546611893', 'ether'], '0.0033086855466118933'], [['1123', 'kwei'], 1.12345], - ]; export const toWeiValidDataWarnings: [[Numbers, EtherUnits], string][] = [ - [[0.0000000000000000000001, 'ether'], 'Warning: Using type `number` with values that are large or contain many decimals may cause loss of precision, it is recommended to use type `string` or `BigInt` when using conversion methods'], - [[0.0000000000000000000001, 'ether'], 'Warning: Using type `number` with values that are large or contain many decimals may cause loss of precision, it is recommended to use type `string` or `BigInt` when using conversion methods'], - [[1999999000000009900000, 'kwei'], 'Warning: Using type `number` with values that are large or contain many decimals may cause loss of precision, it is recommended to use type `string` or `BigInt` when using conversion methods'], - + [ + [0.0000000000000000000001, 'ether'], + 'Warning: Using type `number` with values that are large or contain many decimals may cause loss of precision, it is recommended to use type `string` or `BigInt` when using conversion methods', + ], + [ + [0.0000000000000000000001, 'ether'], + 'Warning: Using type `number` with values that are large or contain many decimals may cause loss of precision, it is recommended to use type `string` or `BigInt` when using conversion methods', + ], + [ + [1999999000000009900000, 'kwei'], + 'Warning: Using type `number` with values that are large or contain many decimals may cause loss of precision, it is recommended to use type `string` or `BigInt` when using conversion methods', + ], ]; export const fromWeiInvalidData: [[any, any], string][] = [ @@ -362,8 +368,14 @@ export const fromWeiInvalidData: [[any, any], string][] = [ [[{}, 'kwei'], 'Invalid value given "{}". Error: can not parse as number data'], [['data', 'kwei'], 'Invalid value given "data". Error: can not parse as number data.'], [['1234', 'uwei'], 'Invalid value given "uwei". Error: invalid unit.'], - [['1234', -1], 'Invalid value given "-1". Error: not a valid unit. Must be a positive integer.'], - [['1234', 3.3], 'Invalid value given "3.3". Error: not a valid unit. Must be a positive integer.'] + [ + ['1234', -1], + 'Invalid value given "-1". Error: not a valid unit. Must be a positive integer.', + ], + [ + ['1234', 3.3], + 'Invalid value given "3.3". Error: not a valid unit. Must be a positive integer.', + ], ]; export const toWeiInvalidData: [[any, any], string][] = [ @@ -374,6 +386,7 @@ export const toWeiInvalidData: [[any, any], string][] = [ [[{}, 'kwei'], 'value "{}" at "/0" must pass "number" validation'], [['data', 'kwei'], 'value "data" at "/0" must pass "number" validation'], [['1234', 'uwei'], 'Invalid value given "uwei". Error: invalid unit.'], + [['123', -1], 'Invalid value given "-1". Error: not a valid unit. Must be a positive integer.'], ]; export const toCheckSumValidData: [string, string][] = [ ['0x0089d53f703f7e0843953d48133f74ce247184c2', '0x0089d53F703f7E0843953D48133f74cE247184c2'], @@ -397,6 +410,7 @@ export const bytesToUint8ArrayValidData: [Bytes, Uint8Array][] = [ ['0x1234', new Uint8Array([18, 52])], ['0x1234', new Uint8Array([18, 52])], [new Uint8Array(hexToBytes('0c12')), new Uint8Array(hexToBytes('0c12'))], + [[72, 12] as any, new Uint8Array([72, 12])], ]; export const toBigIntValidData: [any, bigint][] = [ diff --git a/packages/web3-utils/test/fixtures/formatter.ts b/packages/web3-utils/test/fixtures/formatter.ts index 7437373d708..5d44b05df2d 100644 --- a/packages/web3-utils/test/fixtures/formatter.ts +++ b/packages/web3-utils/test/fixtures/formatter.ts @@ -55,4 +55,5 @@ export const convertScalarValueValid: [[any, any, any], any][] = [ hexToBytes('0x00000000000000000000000000000000000000000000000000000000000000ff'), ), ], + [[255, 'bytes32', { bytes: 'invalidFormat' }], 255], // return original value when erroring ]; diff --git a/packages/web3-utils/test/fixtures/hash.ts b/packages/web3-utils/test/fixtures/hash.ts index 6bfe9f63f33..1869dd10fec 100644 --- a/packages/web3-utils/test/fixtures/hash.ts +++ b/packages/web3-utils/test/fixtures/hash.ts @@ -256,6 +256,7 @@ export const encodePackData: [TypedObject[] | TypedObjectAbbreviated[], any][] = '0x12480000000000000000000000000000000000000000000000000000000000003c69a194aaf415ba5d6afca734660d0a3d45acdc05d54cd1ca89a8988e7625b4', ], [[{ type: 'bytes4[]', value: ['0x11223344', '0x22334455'] }], '0x1122334422334455'], + [[{ type: '', value: '31323334' }], '0x'], ]; export const encodePackedInvalidData: [any, string][] = [ @@ -285,9 +286,13 @@ export const encodePackedInvalidData: [any, string][] = [ { type: 'bytes32', value: '0x1' }, 'Invalid value given "0x1". Error: can not parse as byte data.', ], + [ + [[{ type: 'string', value: '31323334' }], [{ type: '', value: '31323334' }]], + 'Autodetection of array types is not supported.', + ], ]; -export const keccak256ValidData: [string | Uint8Array | bigint, string][] = [ +export const keccak256ValidData: [string | Uint8Array | bigint | number[], string][] = [ ['my data', '0x8e0c48154711500d6fa119cc31df4dec339091e8b426cf4109a769fe89baad31'], [ new Uint8Array(Buffer.from('my data')), @@ -298,6 +303,8 @@ export const keccak256ValidData: [string | Uint8Array | bigint, string][] = [ '0x2d19cd91fbcc44e6412f92c11da7907cdedb1ace04c47447b42a61f1cd63b85a', ], [BigInt(3), '0x2a80e1ef1d7842f27f2e6be0972bb708b9a135c38860dbe73c27c3486c34f4de'], + [[0x3], '0x69c322e3248a5dfc29d73c5b0553b0185a35cd5bb6386747517ef7e53b15e287'], + [new Uint8Array([0x3]), '0x69c322e3248a5dfc29d73c5b0553b0185a35cd5bb6386747517ef7e53b15e287'], ]; export const elementaryNameValidData: [any, string][] = [ @@ -349,3 +356,8 @@ export const soliditySha3BigIntValidData: [Sha3Input[], string][] = [ return keccak256(abi.encodePacked(int(90071992547409))) ;} */ ]; + +export const getStorageSlotNumForLongStringValidData: [string | number, string | undefined][] = [ + [0, '0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563'], + ['0', '0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563'], +]; diff --git a/packages/web3-utils/test/fixtures/json_rpc.ts b/packages/web3-utils/test/fixtures/json_rpc.ts index 516711abceb..d801fa91587 100644 --- a/packages/web3-utils/test/fixtures/json_rpc.ts +++ b/packages/web3-utils/test/fixtures/json_rpc.ts @@ -18,6 +18,7 @@ import { JsonRpcNotification, SubscriptionParams } from 'web3-types'; const responseWithResult = { jsonrpc: '2.0', id: 1, result: '' }; const responseWithError = { jsonrpc: '2.0', id: 1, error: { code: 1, message: 'string' } }; +const responseWithRpcError = { jsonrpc: '2.0', id: 1, error: { code: -32000, message: 'string' } }; const responseWithSubscription = { id: 1, jsonrpc: '2.0', result: '' }; const responseWithNotfication = { jsonrpc: '2.0', @@ -27,11 +28,13 @@ const responseWithNotfication = { export const isResponseWithResultValidTest: [any, boolean][] = [ [responseWithResult, true], [responseWithError, false], + [{ ...responseWithResult, id: '1' }, true], ]; export const isResponseWithErrorValidTest: [any, boolean][] = [ [responseWithResult, false], [responseWithError, true], + [{ ...responseWithError, id: '1' }, true], ]; export const isResponseWithNotificationValidTest: [JsonRpcNotification, boolean][] = [ @@ -63,4 +66,41 @@ export const toPayloadValidTest: [any, any][] = [ params: undefined, }, ], + [ + { method: 'add', jsonrpc: '1.0', id: 1 }, + { + method: 'add', + id: 1, + jsonrpc: '1.0', + params: undefined, + }, + ], +]; + +export const isResponseRpcErrorValidData: [any, boolean][] = [ + [responseWithRpcError, true], + [responseWithError, false], +]; + +export const isBatchRequestValidData: [any, boolean][] = [ + [ + [ + { + method: 'add', + id: 1, + jsonrpc: '1.0', + params: undefined, + }, + ], + true, + ], + [ + { + method: 'add', + id: 1, + jsonrpc: '1.0', + params: undefined, + }, + false, + ], ]; diff --git a/packages/web3-utils/test/fixtures/string_manipulation.ts b/packages/web3-utils/test/fixtures/string_manipulation.ts index be5b298c803..dde49247677 100644 --- a/packages/web3-utils/test/fixtures/string_manipulation.ts +++ b/packages/web3-utils/test/fixtures/string_manipulation.ts @@ -58,7 +58,7 @@ export const padRightData: [[Numbers, number, string], HexString][] = [ [['15.5', 8, '0'], '15.50000'], ]; -export const toTwosComplementData: [[Numbers, number], HexString][] = [ +export const toTwosComplementData: [[Numbers, number | undefined], HexString][] = [ [[13, 32], '0x0000000000000000000000000000000d'], [[256, 30], '0x000000000000000000000000000100'], [[0, 32], '0x00000000000000000000000000000000'], @@ -69,9 +69,10 @@ export const toTwosComplementData: [[Numbers, number], HexString][] = [ [['13', 32], '0x0000000000000000000000000000000d'], [['-13', 32], '0xfffffffffffffffffffffffffffffff3'], [[-16, 2], '0xf0'], + [['0x1', undefined], '0x0000000000000000000000000000000000000000000000000000000000000001'], ]; -export const fromTwosComplementData: [[Numbers, number], number | bigint][] = [ +export const fromTwosComplementData: [[Numbers, number | undefined], number | bigint][] = [ [['0x0000000000000000000000000000000d', 32], 13], [['0x000000000000000000000000000100', 30], 256], [['0x00000000000000000020000000000000', 32], BigInt('9007199254740992')], @@ -81,6 +82,7 @@ export const fromTwosComplementData: [[Numbers, number], number | bigint][] = [ [[1000, 64], 1000], [[-1000, 64], -1000], [[BigInt(9), 1], -7], + [['0x0000000000000000000000000000000000000000000000000000000000000001', undefined], 1], ]; export const toTwosComplementInvalidData: [[Numbers, number], string][] = [ diff --git a/packages/web3-utils/test/unit/chunk_response_parser.test.ts b/packages/web3-utils/test/unit/chunk_response_parser.test.ts index 22100a3e6a8..bb4c70e8f1b 100644 --- a/packages/web3-utils/test/unit/chunk_response_parser.test.ts +++ b/packages/web3-utils/test/unit/chunk_response_parser.test.ts @@ -71,4 +71,18 @@ describe('chunk_response_parser', () => { }), ); }); + + it('lastChunkTimeout return empty when auto reconnect true', async () => { + const p = new ChunkResponseParser(eventEmiter, true); + // @ts-expect-error set private property + p.chunkTimeout = 10; + const result = p.parseResponse( + '{"jsonrpc":"2.0","id":"96aa3f13-077c-4c82-a64a-64b8626f8192","result":"0x141414141', + ); + const onError = jest.fn(); + eventEmiter.on('error', onError); + // eslint-disable-next-line no-promise-executor-return + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(result).toEqual([]); + }); }); diff --git a/packages/web3-utils/test/unit/converters.test.ts b/packages/web3-utils/test/unit/converters.test.ts index 940590f06e7..84073f4a4d7 100644 --- a/packages/web3-utils/test/unit/converters.test.ts +++ b/packages/web3-utils/test/unit/converters.test.ts @@ -15,6 +15,9 @@ You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ +import { InvalidBytesError } from 'web3-errors'; +import { validator, utils } from 'web3-validator'; + import { asciiToHex, bytesToHex, @@ -325,6 +328,16 @@ describe('converters', () => { it.each(toHexValidData)('%s', (input, output) => { expect(toHex(input, true)).toEqual(output[1]); }); + + it('an interesting case that needs investigation', () => { + // TODO: This case is to be investigated further + expect( + toHex( + '101611154195520776335741463917853444671577865378275924493376429267637792638729', + true, + ), + ).toBe('uint'); + }); }); describe('invalid cases', () => { @@ -341,6 +354,14 @@ describe('converters', () => { }); describe('fromWei', () => { + beforeEach(() => { + jest.spyOn(console, 'warn').mockImplementation(() => { + // do nothing + }); + }); + afterAll(() => { + jest.restoreAllMocks(); + }); describe('valid cases', () => { it.each(fromWeiValidData)('%s', (input, output) => { expect(fromWei(input[0], input[1])).toEqual(output); @@ -372,13 +393,14 @@ describe('converters', () => { // do nothing }); }); + afterAll(() => { + jest.restoreAllMocks(); + }); it.each(toWeiValidDataWarnings)('%s', (input, output) => { toWei(input[0], input[1]); - // expect(() => toWei(input[0], input[1])).toThrow(output); - expect(console.warn).toHaveBeenCalledWith(output) + expect(console.warn).toHaveBeenCalledWith(output); }); - - }) + }); }); describe('toChecksumAddress', () => { describe('valid cases', () => { @@ -391,6 +413,33 @@ describe('converters', () => { expect(() => toChecksumAddress(input)).toThrow(output); }); }); + it('should return an empty string if hash is nullish', () => { + const address = '0xc1912fee45d61c87cc5ea59dae31190fffff232d'; + + // mock utils.uint8ArrayToHexString to return an empty string + jest.mock('web3-validator'); + jest.spyOn(utils, 'uint8ArrayToHexString').mockReturnValue( + undefined as unknown as string, + ); + + const result = toChecksumAddress(address); + expect(result).toBe(''); + + jest.mock('web3-validator').restoreAllMocks(); + }); + + it('should return an empty string if hash is equal to "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"', () => { + const address = '0xc1912fee45d61c87cc5ea59dae31190fffff232d'; + + // mock utils.uint8ArrayToHexString to return '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470' + jest.mock('web3-validator'); + const hash = '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470'; + jest.spyOn(utils, 'uint8ArrayToHexString').mockReturnValue(hash); + const result = toChecksumAddress(address); + expect(result).toBe(''); + + jest.mock('web3-validator').restoreAllMocks(); + }); }); describe('bytesToUint8Array', () => { describe('bytesToUint8Array', () => { @@ -404,6 +453,18 @@ describe('converters', () => { it.each(bytesToUint8ArrayInvalidData)('%s', (input, output) => { expect(() => bytesToUint8Array(input)).toThrow(output); }); + + it('should throw InvalidBytesError for invalid input even if it passed the validator', () => { + const invalidData = 8; + // the package 'web3-validator' contains `validator`. + // Mock mock the `validator.validate(...)` to not throw an error, but return `false` instead. + jest.mock('web3-validator'); + + jest.spyOn(validator, 'validate').mockReturnValue(undefined); + + expect(() => bytesToUint8Array(invalidData as any)).toThrow(InvalidBytesError); + jest.mock('web3-validator').restoreAllMocks(); + }); }); }); }); diff --git a/packages/web3-utils/test/unit/formatter.test.ts b/packages/web3-utils/test/unit/formatter.test.ts index bed8b55de8e..aa1a14fb6c9 100644 --- a/packages/web3-utils/test/unit/formatter.test.ts +++ b/packages/web3-utils/test/unit/formatter.test.ts @@ -24,9 +24,10 @@ import { HexString, Numbers, } from 'web3-types'; +import { FormatterError } from 'web3-errors'; import { expectTypeOf, typecheck } from '@humeris/espresso-shot'; import { isDataFormatValid, convertScalarValueValid } from '../fixtures/formatter'; -import { format, isDataFormat, convertScalarValue } from '../../src/formatter'; +import { format, isDataFormat, convertScalarValue, convert } from '../../src/formatter'; import { hexToBytes } from '../../src/converters'; type TestTransactionInfoType = { @@ -738,7 +739,6 @@ describe('formatter', () => { ).toEqual(result); }); }); - describe('object values', () => { it('should format simple object', () => { const schema = { @@ -776,6 +776,13 @@ describe('formatter', () => { expect(result).toEqual(expected); }); + it('should throw FormatterError when jsonSchema is invalid', () => { + const invalidSchema1 = {}; + const data = { key: 'value' }; + + expect(() => format(invalidSchema1, data)).toThrow(FormatterError); + }); + it('should format nested objects', () => { const schema = { type: 'object', @@ -846,4 +853,16 @@ describe('formatter', () => { }); }); }); + + describe('convert', () => { + it('should return empty when no properties or items', () => { + const data = { key: 'value' }; + const schema = { + type: 'object', + }; + const f = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }; + const result = convert(data, schema, [], f, []); + expect(result).toEqual({}); + }); + }); }); diff --git a/packages/web3-utils/test/unit/hash.test.ts b/packages/web3-utils/test/unit/hash.test.ts index d381e3c4240..814dacd45f2 100644 --- a/packages/web3-utils/test/unit/hash.test.ts +++ b/packages/web3-utils/test/unit/hash.test.ts @@ -23,6 +23,7 @@ import { soliditySha3Raw, encodePacked, keccak256 as web3keccak256, + getStorageSlotNumForLongString, } from '../../src/hash'; import { sha3Data, @@ -37,6 +38,7 @@ import { encodePackedInvalidData, keccak256ValidData, soliditySha3BigIntValidData, + getStorageSlotNumForLongStringValidData, } from '../fixtures/hash'; describe('hash', () => { @@ -149,4 +151,10 @@ describe('hash', () => { }); }); }); + + describe('getStorageSlotNumForLongString', () => { + it.each(getStorageSlotNumForLongStringValidData)('%s', (input, output) => { + expect(getStorageSlotNumForLongString(input)).toEqual(output); + }); + }); }); diff --git a/packages/web3-utils/test/unit/index.test.ts b/packages/web3-utils/test/unit/index.test.ts new file mode 100644 index 00000000000..8682408801a --- /dev/null +++ b/packages/web3-utils/test/unit/index.test.ts @@ -0,0 +1,64 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +import * as web3Utils from '../../src'; + +import * as converters from '../../src/converters.js'; +import * as eventEmitter from '../../src/event_emitter.js'; +import * as validation from '../../src/validation.js'; +import * as formatter from '../../src/formatter.js'; +import * as hash from '../../src/hash.js'; +import * as random from '../../src/random.js'; +import * as stringManipulation from '../../src/string_manipulation.js'; +import * as objects from '../../src/objects.js'; +import * as promiseHelpers from '../../src/promise_helpers.js'; +import * as jsonRpc from '../../src/json_rpc.js'; +import * as web3DeferredPromise from '../../src/web3_deferred_promise.js'; +import * as ChunkResponseParser from '../../src/chunk_response_parser.js'; +import * as uuid from '../../src/uuid.js'; +import * as web3Eip1193Provider from '../../src/web3_eip1193_provider.js'; +import * as socketProvider from '../../src/socket_provider.js'; +import * as uint8array from '../../src/uint8array.js'; + +describe('web3-utils exports', () => { + it('should export all modules', () => { + const modules = [ + converters, + eventEmitter, + validation, + formatter, + hash, + random, + stringManipulation, + objects, + promiseHelpers, + jsonRpc, + web3DeferredPromise, + ChunkResponseParser, + uuid, + web3Eip1193Provider, + socketProvider, + uint8array, + ]; + + modules.forEach(module => { + Object.keys(module).forEach((property: string | any[]) => { + expect(web3Utils).toHaveProperty(property); + }); + }); + }); +}); diff --git a/packages/web3-utils/test/unit/json_rpc.test.ts b/packages/web3-utils/test/unit/json_rpc.test.ts index 89261009916..ea16cd646d1 100644 --- a/packages/web3-utils/test/unit/json_rpc.test.ts +++ b/packages/web3-utils/test/unit/json_rpc.test.ts @@ -16,22 +16,28 @@ along with web3.js. If not, see . */ import { + isResponseRpcError, isResponseWithResult, isResponseWithError, isResponseWithNotification, isSubscriptionResult, isValidResponse, isBatchResponse, + setRequestIdStart, + toBatchPayload, + isBatchRequest, toPayload, } from '../../src/json_rpc'; import { isResponseWithResultValidTest, isResponseWithErrorValidTest, + isResponseRpcErrorValidData, isResponseWithNotificationValidTest, isSubscriptionResultValidTest, toPayloadValidTest, isValidResponseValidTest, isBatchResponseValidTest, + isBatchRequestValidData, } from '../fixtures/json_rpc'; describe('json rpc tests', () => { @@ -51,6 +57,14 @@ describe('json rpc tests', () => { }); }); }); + describe('isResponseRpcError', () => { + describe('valid cases', () => { + it.each(isResponseRpcErrorValidData)('%s', (input, output) => { + const result = isResponseRpcError(input); + expect(result).toBe(output); + }); + }); + }); describe('isResponseWithNotification', () => { describe('valid cases', () => { it.each(isResponseWithNotificationValidTest)('should have notify', (input, output) => { @@ -77,18 +91,57 @@ describe('json rpc tests', () => { }); describe('isBatchResponseValid', () => { describe('valid cases', () => { - it.each(isBatchResponseValidTest)('isValidresponse valid test', (input, output) => { - const result = isBatchResponse(input); - expect(result).toBe(output); + it.each(isBatchResponseValidTest)( + 'isBatchResponseValid valid test', + (input, output) => { + const result = isBatchResponse(input); + expect(result).toBe(output); + }, + ); + }); + }); + describe('isBatchRequest', () => { + describe('valid cases', () => { + it.each(isBatchRequestValidData)('isBatchRqeuest valid data', (input, output) => { + expect(isBatchRequest(input)).toBe(output); }); }); }); describe('toPayloadValid', () => { describe('valid cases', () => { - it.each(toPayloadValidTest)('isValidresponse valid test', (input, output) => { - const result = toPayload(input); + beforeEach(() => { + setRequestIdStart(undefined); + }); + it.each(toPayloadValidTest)('toPayload valid test', async (input, output) => { + const result = await new Promise(resolve => { + resolve(toPayload(input)); + }); expect(result).toStrictEqual(output); }); + it('should give payload that has requestid set', async () => { + setRequestIdStart(1); + const result = await new Promise(resolve => { + resolve(toPayload({ method: 'delete' })); + }); + expect(result).toStrictEqual({ + method: 'delete', + id: 2, + params: undefined, + jsonrpc: '2.0', + }); + }); + }); + }); + describe('toBatchPayload', () => { + it('should batch payload', async () => { + setRequestIdStart(0); + const result = await new Promise(resolve => { + resolve(toBatchPayload([{ method: 'delete' }, { method: 'add' }])); + }); + expect(result).toStrictEqual([ + { method: 'delete', id: 1, params: undefined, jsonrpc: '2.0' }, + { method: 'add', id: 2, params: undefined, jsonrpc: '2.0' }, + ]); }); }); }); diff --git a/packages/web3-utils/test/unit/objects.test.ts b/packages/web3-utils/test/unit/objects.test.ts index 1201b3b7345..cca9d46cba9 100644 --- a/packages/web3-utils/test/unit/objects.test.ts +++ b/packages/web3-utils/test/unit/objects.test.ts @@ -79,5 +79,14 @@ describe('objects', () => { expect(result.a).toStrictEqual(new Uint8Array([1, 2])); }); + + it('should return the destination object if it is not iterable', () => { + const destination = 123; // Replace with your desired destination object + const sources: Record[] = []; // Replace with your desired sources array + + const result = mergeDeep(destination as unknown as Record, ...sources); + + expect(result).toBe(destination); + }); }); }); diff --git a/packages/web3-utils/test/unit/promise_helpers.test.ts b/packages/web3-utils/test/unit/promise_helpers.test.ts index 7492c51fc24..14ccc00488d 100644 --- a/packages/web3-utils/test/unit/promise_helpers.test.ts +++ b/packages/web3-utils/test/unit/promise_helpers.test.ts @@ -60,6 +60,14 @@ describe('promise helpers', () => { new Error('time out'), ); }); + it('throws if result is an instance of Error', async () => { + const dummyError = new Error('dummy error'); + const asyncHelper = async () => { + return dummyError; + }; + + await expect(waitWithTimeout(asyncHelper(), 1000)).rejects.toThrow(dummyError); + }); }); describe('rejectIfTimeout', () => { it('%s', async () => { diff --git a/packages/web3-utils/test/unit/socket_provider.test.ts b/packages/web3-utils/test/unit/socket_provider.test.ts index a09f1467ce0..c2c3756c755 100644 --- a/packages/web3-utils/test/unit/socket_provider.test.ts +++ b/packages/web3-utils/test/unit/socket_provider.test.ts @@ -15,7 +15,14 @@ You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ -import { Web3APIPayload, EthExecutionAPI, JsonRpcResponse, Web3ProviderStatus } from 'web3-types'; +import { + Web3APIPayload, + EthExecutionAPI, + JsonRpcResponse, + Web3ProviderStatus, + JsonRpcIdentifier, +} from 'web3-types'; +import { MaxAttemptsReachedOnReconnectingError, InvalidClientError } from 'web3-errors'; import { EventEmitter } from '../../src/event_emitter'; // eslint-disable-next-line import/no-relative-packages import { sleep } from '../../../../fixtures/utils'; @@ -40,8 +47,27 @@ class TestProvider extends SocketProvider { // eslint-disable-next-line protected _sendToSocket(_payload: Web3APIPayload): void {} // eslint-disable-next-line - protected _parseResponses(_event: any): JsonRpcResponse[] { - return [] as JsonRpcResponse[]; + protected _parseResponses(_event: { data: string } | undefined): JsonRpcResponse[] { + if (!_event || !_event.data) { + return []; + } + const returnValues: JsonRpcResponse[] = []; + + // DE-CHUNKER + const dechunkedData = _event.data + .replace(/\}[\n\r]?\{/g, '}|--|{') // }{ + .replace(/\}\][\n\r]?\[\{/g, '}]|--|[{') // }][{ + .replace(/\}[\n\r]?\[\{/g, '}|--|[{') // }[{ + .replace(/\}\][\n\r]?\{/g, '}]|--|{') // }]{ + .split('|--|'); + + dechunkedData.forEach((chunkData: string) => { + const result = JSON.parse(chunkData) as unknown as JsonRpcResponse; + + if (result) returnValues.push(result); + }); + + return returnValues; } public message(_event: any): void { this._onMessage(_event); @@ -74,6 +100,32 @@ describe('SocketProvider', () => { expect(provider).toBeInstanceOf(SocketProvider); expect(provider.SocketConnection).toEqual(dummySocketConnection); }); + it('should call _clearQueues when chunkResponseParser emits an error', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + const clearQueuesSpy = jest.spyOn(provider as any, '_clearQueues'); + + try { + // @ts-expect-error access readonly method + provider['chunkResponseParser']['autoReconnect'] = false; + // @ts-expect-error access readonly method + provider['chunkResponseParser']['chunkTimeout'] = 0; + + provider['chunkResponseParser'].parseResponse('invalid-json'); + } catch (error) { + // nothing + } + + // wait 1 second for the timeout to trigger + await sleep(100); + + expect(clearQueuesSpy).toHaveBeenCalled(); + }); + it('should error when failing to _validateProviderPath', () => { + expect(() => { + // eslint-disable-next-line no-new + new TestProvider('', socketOption, { delay: 0 }); + }).toThrow(InvalidClientError); + }); }); describe('testing _reconnect() method', () => { it('should not be called when { autoReconnect: false }', () => { @@ -94,6 +146,111 @@ describe('SocketProvider', () => { // @ts-expect-error run protected method expect(provider._reconnect).not.toHaveBeenCalled(); }); + it('should call _reconnect when isReconnecting is true and an error happens', () => { + const provider = new TestProvider(socketPath, socketOption); + provider['_reconnect'] = jest.fn(); + provider['isReconnecting'] = true; + + provider['_onError']({}); + + expect(provider['_reconnect']).toHaveBeenCalled(); + }); + + it('should call _reconnect when isReconnecting is false and an error happens', () => { + const provider = new TestProvider(socketPath, socketOption); + provider['_reconnect'] = jest.fn(); + provider['isReconnecting'] = false; + + provider['_onError']({}); + expect(provider['_reconnect']).not.toHaveBeenCalled(); + }); + + it('should return if the provider is already isReconnecting', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + // just to run the test faster moke `connect` + jest.spyOn(provider, 'connect'); + + // @ts-expect-error access protected method + expect(provider._reconnectAttempts).toBe(0); + provider['_reconnect'](); + // @ts-expect-error access protected method + expect(provider._reconnectAttempts).toBe(1); + + // after the first call provider.isReconnecting will set to true and so the `_reconnectAttempts` will not be incremented + provider['_reconnect'](); + + // @ts-expect-error access protected method + expect(provider._reconnectAttempts).toBe(1); + }); + + it('should reconnect the socket when the number of reconnect attempts is less than the maximum attempts', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + // @ts-expect-error access protected method + const openSocketConnectionSpy = jest.spyOn(provider, '_openSocketConnection'); + // @ts-expect-error access protected method + const removeSocketListenersSpy = jest.spyOn(provider, '_removeSocketListeners'); + const connectSpy = jest.spyOn(provider, 'connect'); + + // Set the reconnect attempts to less than the maximum attempts + provider['_reconnectAttempts'] = 2; + + provider['_reconnect'](); + + // wait for the timeout to trigger + await sleep(100); + + expect(openSocketConnectionSpy).toHaveBeenCalled(); + expect(removeSocketListenersSpy).toHaveBeenCalled(); + expect(connectSpy).toHaveBeenCalled(); + }); + + it('should clear the queues and emit an error event when the number of reconnect attempts reaches the maximum attempts', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + const clearQueuesSpy = jest.spyOn(provider as any, '_clearQueues'); + // @ts-expect-error access protected method + const removeSocketListenersSpy = jest.spyOn(provider, '_removeSocketListeners'); + const errorEventSpy = jest.spyOn(provider['_eventEmitter'], 'emit'); + + // Set the reconnect attempts to the maximum attempts + provider['_reconnectAttempts'] = 5; + + provider['_reconnect'](); + + // wait for the timeout to trigger + await sleep(100); + + expect(clearQueuesSpy).toHaveBeenCalled(); + expect(removeSocketListenersSpy).toHaveBeenCalled(); + expect(errorEventSpy).toHaveBeenCalledWith( + 'error', + expect.any(MaxAttemptsReachedOnReconnectingError), + ); + }); + + it('should keep pending requests but clear the sent requests queue when reconnecting', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + + provider.setStatus('connected'); + // Add a sent request + provider.request({ id: 2, method: 'some_rpc_method' }).catch(() => { + // it will throw with "Connection not open" because no actual connection is used in the test. So ignore the error + }); + // @ts-expect-error run protected method + expect(provider._sentRequestsQueue.size).toBe(1); + + // @ts-expect-error access protected method + const rejectSpy = jest.spyOn(provider['_pendingRequestsQueue'], 'delete'); + const deleteSpy = jest.spyOn(provider['_sentRequestsQueue'], 'delete'); + + const pendingRequestsQueueSize = provider['_pendingRequestsQueue'].size; + const sentRequestsQueueSize = provider['_sentRequestsQueue'].size; + + provider['_reconnect'](); + + expect(provider['_pendingRequestsQueue'].size).toEqual(pendingRequestsQueueSize); + + expect(deleteSpy).toHaveBeenCalledTimes(sentRequestsQueueSize); + }); }); describe('testing connect() method', () => { @@ -298,6 +455,46 @@ describe('SocketProvider', () => { // @ts-expect-error run protected method expect(provider._sentRequestsQueue.get(payload.id).payload).toBe(payload); }); + + it('should add request to the `_sentRequestsQueue` when the status is `connected` for batch requests', () => { + const provider = new TestProvider(socketPath, socketOption); + const payload = [ + { id: 1, method: 'some_rpc_method', jsonrpc: '2.0' as JsonRpcIdentifier }, + { id: 2, method: 'some_rpc_method', jsonrpc: '2.0' as JsonRpcIdentifier }, + ]; + provider.setStatus('connected'); + const reqPromise = provider.request(payload as any); + expect(reqPromise).toBeInstanceOf(Promise); + + // the id of the first request in the batch is the one used to identify the batch request + // @ts-expect-error run protected method + expect(provider._sentRequestsQueue.get(payload[0].id).payload).toBe(payload); + }); + + it('should clear _sentRequestsQueue in case `_sendToSocket` had an error', async () => { + // Create a mock SocketProvider instance + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + + const deleteSpy = jest.spyOn(provider['_sentRequestsQueue'], 'delete'); + + provider.setStatus('connected'); + // Assert that the _sendToSocket method was called with the correct payload + // @ts-expect-error access protected method + provider._sendToSocket = () => { + throw new Error('any error'); + }; + // Call the request method + provider + .request({ id: 1, method: 'some_rpc_method' }) + .then(() => { + // nothing + }) + .catch(() => { + // nothing + }); + + expect(deleteSpy).toHaveBeenCalled(); + }); }); describe('testing _clearQueues() method', () => { @@ -339,4 +536,330 @@ describe('SocketProvider', () => { }); }); }); + + describe('safeDisconnect', () => { + it('should disconnect the socket when there are no pending or sent requests', async () => { + const provider = new TestProvider(socketPath, socketOption); + const disconnectSpy = jest.spyOn(provider, 'disconnect'); + await provider.safeDisconnect(); + expect(disconnectSpy).toHaveBeenCalled(); + }); + + it('should disconnect the socket after waiting for pending and sent requests to be empty', async () => { + const provider = new TestProvider(socketPath, socketOption); + const disconnectSpy = jest.spyOn(provider, 'disconnect'); + + // Add a pending request + provider.request({ id: 1, method: 'some_rpc_method' }).catch(() => { + // it will throw with "Connection not open" because no actual connection is used in the test. So ignore the error + }); + // Add a sent request + provider.request({ id: 2, method: 'some_rpc_method' }).catch(() => { + // it will throw with "Connection not open" because no actual connection is used in the test. So ignore the error + }); + expect(provider.getPendingRequestQueueSize()).toBe(2); + + provider.clearQueues(); + // Call safeDisconnect and wait for the queues to be empty + await provider.safeDisconnect(undefined, undefined, false, 100); + + expect(disconnectSpy).toHaveBeenCalled(); + expect(provider.getPendingRequestQueueSize()).toBe(0); + expect(provider.getSentRequestsQueueSize()).toBe(0); + }); + + it('should force disconnect the socket after waiting for 5 attempts', async () => { + const provider = new TestProvider(socketPath, socketOption); + const disconnectSpy = jest.spyOn(provider, 'disconnect'); + const clearQueuesSpy = jest.spyOn(provider as any, 'clearQueues'); + + // Add a pending request + provider.request({ id: 1, method: 'some_rpc_method' }).catch(() => { + // it will throw with "Connection not open" because no actual connection is used in the test. So ignore the error + }); + expect(provider.getPendingRequestQueueSize()).toBe(1); + + // Add a sent request + provider.request({ id: 2, method: 'some_rpc_method' }).catch(() => { + // it will throw with "Connection not open" because no actual connection is used in the test. So ignore the error + }); + // expect(provider.getSentRequestsQueueSize()).toBe(1); + + // Call safeDisconnect with forceDisconnect set to true and a small interval + await provider.safeDisconnect(undefined, undefined, true, 100); + + expect(disconnectSpy).toHaveBeenCalled(); + expect(clearQueuesSpy).toHaveBeenCalled(); + }); + }); + describe('removeAllListeners', () => { + it('should remove all listeners for the specified event type', () => { + const provider = new TestProvider(socketPath, socketOption); + const listener1 = jest.fn(); + const listener2 = jest.fn(); + const listener3 = jest.fn(); + provider.on('event', listener1); + provider.on('event', listener2); + provider.on('otherEvent', listener3); + + provider.removeAllListeners('event'); + + provider['_eventEmitter'].emit('event'); + provider['_eventEmitter'].emit('otherEvent'); + + expect(listener1).not.toHaveBeenCalled(); + expect(listener2).not.toHaveBeenCalled(); + expect(listener3).toHaveBeenCalled(); + }); + }); + + describe('_sendPendingRequests', () => { + it('should send pending requests to the socket', () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + + const payload1 = { id: 1, method: 'method1', params: [] }; + const payload2 = { id: 2, method: 'method2', params: [] }; + // Add a pending request + provider.request(payload1).catch(() => { + // it will throw with "Connection not open" because no actual connection is used in the test. So ignore the error + }); + // Add a sent request + provider.request(payload2).catch(() => { + // it will throw with "Connection not open" because no actual connection is used in the test. So ignore the error + }); + expect(provider.getPendingRequestQueueSize()).toBe(2); + + provider['_sendToSocket'] = jest.fn(); + + provider['_sendPendingRequests'](); + + expect(provider['_sendToSocket']).toHaveBeenCalledTimes(2); + expect(provider['_sendToSocket']).toHaveBeenCalledWith(payload1); + expect(provider['_sendToSocket']).toHaveBeenCalledWith(payload2); + expect(provider['_pendingRequestsQueue'].size).toBe(0); + expect(provider['_sentRequestsQueue'].size).toBe(2); + }); + + it('should not send any requests if the pending requests queue is empty', () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + provider['_sendToSocket'] = jest.fn(); + + provider['_sendPendingRequests'](); + + expect(provider['_sendToSocket']).not.toHaveBeenCalled(); + expect(provider['_pendingRequestsQueue'].size).toBe(0); + expect(provider['_sentRequestsQueue'].size).toBe(0); + }); + }); + + describe('_onConnect', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => { + // do nothing + }); // Spy on console.error to suppress and check calls + }); + + afterEach(() => { + jest.restoreAllMocks(); // Restore all mocks after each test + }); + + it('should set the connection status to "connected"', () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + + // Act + provider['_onConnect'](); + + expect(provider['_connectionStatus']).toBe('connected'); + }); + it('should set _accounts and _chainId when _getAccounts and _getChainId resolve', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + jest.spyOn(provider as any, '_getAccounts').mockResolvedValueOnce([123]); + jest.spyOn(provider as any, '_getChainId').mockResolvedValueOnce('1'); + + await new Promise(resolve => { + provider['_onConnect'](); + resolve(''); + }); + expect((provider as any)._chainId).toBe('1'); + expect((provider as any)._accounts).toEqual([123]); + }); + it('chainID should change when connecting twice', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + + await new Promise(resolve => { + jest.spyOn(provider as any, '_getAccounts').mockResolvedValueOnce([123]); + jest.spyOn(provider as any, '_getChainId').mockResolvedValueOnce('1'); + provider['_onConnect'](); + resolve(''); + }); + expect((provider as any)._chainId).toBe('1'); + expect((provider as any)._accounts).toEqual([123]); + + await new Promise(resolve => { + jest.spyOn(provider as any, '_getAccounts').mockResolvedValueOnce([123]); + jest.spyOn(provider as any, '_getChainId').mockResolvedValueOnce('2'); + provider['_onConnect'](); + resolve(''); + }); + expect((provider as any)._chainId).toBe('2'); + expect((provider as any)._accounts).toEqual([123]); + }); + it('should catch errors when _getAccounts and _getChainId throws', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + jest.spyOn(provider as any, '_getChainId').mockRejectedValueOnce(new Error('')); + jest.spyOn(provider as any, '_getAccounts').mockRejectedValueOnce(new Error('')); + jest.spyOn(provider, 'request').mockReturnValue(new Error() as unknown as Promise); + + await new Promise(resolve => { + provider['_onConnect'](); + resolve(''); + }); + expect((provider as any)._chainId).toBe(''); + expect((provider as any)._accounts).toEqual([]); + }); + it('should catch when connect emit fails', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + jest.spyOn(provider as any, '_getChainId').mockResolvedValueOnce(1); + jest.spyOn(provider as any, '_getAccounts').mockResolvedValueOnce([]); + (provider as any)._eventEmitter.emit = jest.fn(() => { + throw new Error('event emitter failed'); + }); + + await new Promise(resolve => { + provider['_onConnect'](); + resolve(''); + }); + // I would check if console.error is called, but facing a race condition + expect((provider as any)._eventEmitter.emit).toHaveBeenCalledTimes(1); + }); + }); + + describe('_getChainId', () => { + it('should return data from the chainId method', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + const chainId = 1; + jest.spyOn(provider as any, 'request').mockResolvedValueOnce({ result: chainId }); + const result = await provider['_getChainId'](); + expect(result).toBe(chainId); + }); + + it('should be returning undefined from the chainId method', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + jest.spyOn(provider as any, 'request').mockResolvedValueOnce({ result: undefined }); + const result = await provider['_getChainId'](); + expect(result).toBe(''); + }); + + it('should return empty from the chainId method', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + jest.spyOn(provider as any, 'request').mockResolvedValueOnce(undefined); + const result = await provider['_getChainId'](); + expect(result).toBe(''); + }); + }); + + describe('_getAccounts', () => { + it('should return data from the _getAccounts method', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + const accounts = [1]; + jest.spyOn(provider as any, 'request').mockResolvedValueOnce({ result: accounts }); + const result = await provider['_getAccounts'](); + expect(result).toBe(accounts); + }); + + it('should returning undefined from the _getAccounts method', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + jest.spyOn(provider as any, 'request').mockResolvedValueOnce({ result: undefined }); + const result = await provider['_getAccounts'](); + expect(result).toEqual([]); + }); + + it('should return empty from the _getAccounts method', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + jest.spyOn(provider as any, 'request').mockResolvedValueOnce(undefined); + const result = await provider['_getAccounts'](); + expect(result).toEqual([]); + }); + }); + + describe('_onMessage', () => { + it('should resolve the deferred promise for valid responses with errors', () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + + const payload1 = { + id: 1, + method: 'method1', + params: [], + jsonrpc: '2.0' as JsonRpcIdentifier, + error: { code: -32601, message: 'Method not found' }, + }; + const event = { + data: JSON.stringify(payload1), + }; + // Add a pending request + provider.request(payload1).catch(() => { + // it will throw with "Connection not open" because no actual connection is used in the test. So ignore the error + }); + expect(provider.getPendingRequestQueueSize()).toBe(1); + + // @ts-expect-error access protected method + provider['_sentRequestsQueue'] = provider['_pendingRequestsQueue']; + + const deferredPromiseResolveSpy = jest.spyOn( + provider['_sentRequestsQueue'].get(1)!.deferredPromise, + 'resolve', + ); + provider['_onMessage']({ + ...event, + }); + + expect(deferredPromiseResolveSpy).toHaveBeenCalledWith(payload1); + }); + + it('should not emit "message" event for invalid responses', () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + const event = { + data: JSON.stringify([ + { id: 1, jsonrpc: '2.0', error: { code: -32601, message: 'Method not found' } }, + { id: 2, jsonrpc: '2.0', error: { code: -32601, message: 'Method not found' } }, + ]), + }; + + const eventEmitterSpy = jest.spyOn(provider['_eventEmitter'], 'emit'); + + provider['_onMessage'](event); + + expect(eventEmitterSpy).not.toHaveBeenCalledWith('message', { + id: 1, + jsonrpc: '2.0', + error: { code: -32601, message: 'Method not found' }, + }); + expect(eventEmitterSpy).not.toHaveBeenCalledWith('message', { + id: 2, + jsonrpc: '2.0', + error: { code: -32601, message: 'Method not found' }, + }); + }); + + it('should emit "message" event for notifications', () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + const event = { + data: JSON.stringify({ + jsonrpc: '2.0', + method: 'notification_1_subscription', + params: {}, + }), + }; + + const eventEmitterSpy = jest.spyOn(provider['_eventEmitter'], 'emit'); + + provider['_onMessage'](event); + + expect(eventEmitterSpy).toHaveBeenCalledWith('message', { + jsonrpc: '2.0', + method: 'notification_1_subscription', + params: {}, + }); + }); + }); }); diff --git a/packages/web3-utils/test/unit/web3_deferred_promise.test.ts b/packages/web3-utils/test/unit/web3_deferred_promise.test.ts index b8180705969..6fe87129e73 100644 --- a/packages/web3-utils/test/unit/web3_deferred_promise.test.ts +++ b/packages/web3-utils/test/unit/web3_deferred_promise.test.ts @@ -53,4 +53,17 @@ describe('Web3DeferredPromise', () => { expect(promise.state).toBe('rejected'); }); }); + + describe('Web3DeferredPromise finally', () => { + it('should execute the callback when the promise is settled', async () => { + const promise = new Web3DeferredPromise(); + let callbackExecuted = false; + promise.resolve(1); + await promise.finally(() => { + callbackExecuted = true; + }); + + expect(callbackExecuted).toBe(true); + }); + }); }); diff --git a/packages/web3-validator/CHANGELOG.md b/packages/web3-validator/CHANGELOG.md index ea8cd0259ff..da4cc2c0136 100644 --- a/packages/web3-validator/CHANGELOG.md +++ b/packages/web3-validator/CHANGELOG.md @@ -168,9 +168,11 @@ Documentation: - Multi-dimensional arrays(with a fix length) are now handled properly when parsing ABIs (#6798) -## [Unreleased] +## [2.0.6] ### Fixed -- The JSON schema conversion process now correctly assigns an id when the `abi.name` is not available, for example, in the case of public mappings. +- The JSON schema conversion process now correctly assigns an id when the `abi.name` is not available, for example, in the case of public mappings. (#6981) - `browser` entry point that was pointing to an non-existing bundle file was removed from `package.json` (#7015) + +## [Unreleased] \ No newline at end of file diff --git a/packages/web3-validator/package.json b/packages/web3-validator/package.json index c416a1fa235..4a12b604981 100644 --- a/packages/web3-validator/package.json +++ b/packages/web3-validator/package.json @@ -1,6 +1,6 @@ { "name": "web3-validator", - "version": "2.0.5", + "version": "2.0.6", "description": "JSON-Schema compatible validator for web3", "main": "./lib/commonjs/index.js", "module": "./lib/esm/index.js", @@ -46,8 +46,8 @@ "dependencies": { "ethereum-cryptography": "^2.0.0", "util": "^0.12.5", - "web3-errors": "^1.1.4", - "web3-types": "^1.5.0", + "web3-errors": "^1.2.0", + "web3-types": "^1.6.0", "zod": "^3.21.4" }, "devDependencies": { diff --git a/packages/web3/CHANGELOG.md b/packages/web3/CHANGELOG.md index 15fca59a531..2e649c3ee0f 100644 --- a/packages/web3/CHANGELOG.md +++ b/packages/web3/CHANGELOG.md @@ -262,8 +262,123 @@ Documentation: - Added `signature` to type `AbiFunctionFragment` (#6922) - update type `Withdrawals`, `block` and `BlockHeaderOutput` to include properties of eip 4844, 4895, 4788 (#6933) -## [Unreleased] +## [4.9.0] ### Added +#### web3 + - Updated type `Web3EthInterface.accounts` to includes `privateKeyToAccount`,`privateKeyToAddress`,and `privateKeyToPublicKey` (#6762) + +#### web3-core + +- `defaultReturnFormat` was added to the configuration options. (#6947) + +#### web3-errors + +- Added `InvalidIntegerError` error for fromWei and toWei (#7052) + +#### web3-eth + +- `defaultReturnFormat` was added to all methods that have `ReturnType` param. (#6947) +- `getTransactionFromOrToAttr`, `waitForTransactionReceipt`, `trySendTransaction`, `SendTxHelper` was exported (#7000) + +#### web3-eth-contract + +- `defaultReturnFormat` was added to all methods that have `ReturnType` param. (#6947) + +#### web3-eth-ens + +- `defaultReturnFormat` was added to all methods that have `ReturnType` param. (#6947) + +#### web3-net + +- `defaultReturnFormat` was added to all methods that have `ReturnType` param. (#6947) + +#### web3-types + +- Added `signature` to type `AbiFunctionFragment` (#6922) +- update type `Withdrawals`, `block` and `BlockHeaderOutput` to include properties of eip 4844, 4895, 4788 (#6933) + +#### web3-utils + +- `toWei` add warning when using large numbers or large decimals that may cause precision loss (#6908) +- `toWei` and `fromWei` now supports integers as a unit. (#7053) + +### Fixed + +#### web3-eth + +- Fixed issue with simple transactions, Within `checkRevertBeforeSending` if there is no data set in transaction, set gas to be `21000` (#7043) + +#### web3-utils + +- `toWei` support numbers in scientific notation (#6908) +- `toWei` and `fromWei` trims according to ether unit successfuly (#7044) + +#### web3-validator + +- The JSON schema conversion process now correctly assigns an id when the `abi.name` is not available, for example, in the case of public mappings. (#6981) +- `browser` entry point that was pointing to an non-existing bundle file was removed from `package.json` (#7015) + +#### web3-core + +- Set a try catch block if processesingError fails (#7022) + +### Changed + +#### web3-core + +- Interface `RequestManagerMiddleware` was changed (#7003) + +#### web3-eth + +- Added parameter `customTransactionReceiptSchema` into methods `emitConfirmation`, `waitForTransactionReceipt`, `watchTransactionByPolling`, `watchTransactionBySubscription`, `watchTransactionForConfirmations` (#7000) +- Changed functionality: For networks that returns `baseFeePerGas===0x0` fill `maxPriorityFeePerGas` and `maxFeePerGas` by `getGasPrice` method (#7050) + +#### web3-eth-abi + +- Dependencies updated + +#### web3-rpc-methods + +- Change `estimateGas` method to add possibility pass Transaction type (#7000) + +## [4.10.0] + +### Added + +#### web3 + +- Now when existing packages are added in web3, will be avalible for plugins via context. (#7088) + +#### web3-core + +- Now when existing packages are added in web3, will be avalible for plugins via context. (#7088) + +#### web3-eth + +- `sendTransaction` in `rpc_method_wrappers` accepts optional param of `TransactionMiddleware` (#7088) +- WebEth has `setTransactionMiddleware` and `getTransactionMiddleware` for automatically passing to `sentTransaction` (#7088) + +#### web3-eth-ens + +- `getText` now supports first param Address +- `getName` has optional second param checkInterfaceSupport + +### web3-types + +- Added `result` as optional `never` and `error` as optional `never in type `JsonRpcNotification` (#7091) +- Added `JsonRpcNotfication` as a union type in `JsonRpcResponse` (#7091) + +### web3-rpc-providers + +- RC release + +### Fixed + +#### web3-eth-ens + +- `getName` reverse resolution + +## [Unreleased] diff --git a/packages/web3/package.json b/packages/web3/package.json index 52bb78ce8c8..5d05fa85923 100644 --- a/packages/web3/package.json +++ b/packages/web3/package.json @@ -1,6 +1,6 @@ { "name": "web3", - "version": "4.8.0", + "version": "4.10.0", "description": "Ethereum JavaScript API", "main": "./lib/commonjs/index.js", "module": "./lib/esm/index.js", @@ -86,21 +86,22 @@ "web3-providers-ipc": "^4.0.7" }, "dependencies": { - "web3-core": "^4.3.2", - "web3-errors": "^1.1.4", - "web3-eth": "^4.6.0", - "web3-eth-abi": "^4.2.1", + "web3-core": "^4.5.0", + "web3-errors": "^1.2.0", + "web3-eth": "^4.8.0", + "web3-eth-abi": "^4.2.2", "web3-eth-accounts": "^4.1.2", - "web3-eth-contract": "^4.4.0", - "web3-eth-ens": "^4.2.0", + "web3-eth-contract": "^4.5.0", + "web3-eth-ens": "^4.4.0", "web3-eth-iban": "^4.0.7", "web3-eth-personal": "^4.0.8", - "web3-net": "^4.0.7", + "web3-net": "^4.1.0", "web3-providers-http": "^4.1.0", "web3-providers-ws": "^4.0.7", - "web3-rpc-methods": "^1.2.0", - "web3-types": "^1.6.0", - "web3-utils": "^4.2.3", - "web3-validator": "^2.0.5" + "web3-rpc-methods": "^1.3.0", + "web3-rpc-providers": "^1.0.0-rc.0", + "web3-types": "^1.7.0", + "web3-utils": "^4.3.0", + "web3-validator": "^2.0.6" } } diff --git a/packages/web3/src/version.ts b/packages/web3/src/version.ts index 8e4910766d8..8b07e0d3591 100644 --- a/packages/web3/src/version.ts +++ b/packages/web3/src/version.ts @@ -1 +1 @@ -/* eslint-disable header/header */ export const Web3PkgInfo = { version: '4.8.0' }; +/* eslint-disable header/header */ export const Web3PkgInfo = { version: '4.10.0' }; diff --git a/packages/web3/src/web3.ts b/packages/web3/src/web3.ts index 0fda2c10521..a3b6b462842 100644 --- a/packages/web3/src/web3.ts +++ b/packages/web3/src/web3.ts @@ -30,6 +30,7 @@ import { Personal } from 'web3-eth-personal'; import { Net } from 'web3-net'; import * as utils from 'web3-utils'; import { isNullish, isDataFormat, isContractInitOptions } from 'web3-utils'; +import { mainnet } from 'web3-rpc-providers'; import { Address, ContractAbi, @@ -67,10 +68,10 @@ export class Web3< public eth: Web3EthInterface; public constructor( - providerOrContext?: + providerOrContext: | string | SupportedProviders - | Web3ContextInitOptions, + | Web3ContextInitOptions = mainnet, ) { if ( isNullish(providerOrContext) || diff --git a/packages/web3/test/e2e/estimate_gas.test.ts b/packages/web3/test/e2e/estimate_gas.test.ts index cfad9595b02..06a587027fd 100644 --- a/packages/web3/test/e2e/estimate_gas.test.ts +++ b/packages/web3/test/e2e/estimate_gas.test.ts @@ -99,7 +99,9 @@ describe(`${getSystemTestBackend()} tests - estimateGas`, () => { break; case 'NUMBER_BIGINT': // eslint-disable-next-line jest/no-conditional-expect - expect(result).toBe(BigInt(expectedGasEstimate)); + expect(typeof result).toBe('bigint'); + // eslint-disable-next-line jest/no-conditional-expect + expect(result.toString()).toBe(expectedGasEstimate.toString()); break; default: throw new Error('Unhandled format'); diff --git a/packages/web3/test/integration/web3.test.ts b/packages/web3/test/integration/web3.test.ts index 18a49dd0fcc..37587954bb0 100644 --- a/packages/web3/test/integration/web3.test.ts +++ b/packages/web3/test/integration/web3.test.ts @@ -124,13 +124,13 @@ describe('Web3 instance', () => { expect(typeof web3Instance.eth.currentProvider?.disconnect).toBe('function'); }); - it('should be able use "utils" without provider', () => { + it('should be able use "utils"', () => { web3 = new Web3(); expect(web3.utils.hexToNumber('0x5')).toBe(5); }); - it('should be able use "abi" without provider', () => { + it('should be able use "abi"', () => { web3 = new Web3(); const validData = validEncodeParametersData[0]; @@ -141,12 +141,6 @@ describe('Web3 instance', () => { expect(encodedParameters).toEqual(validData.output); }); - it('should throw error when we make a request when provider not available', async () => { - web3 = new Web3(); - - await expect(web3.eth.getChainId()).rejects.toThrow('Provider not available'); - }); - describeIf(isHttp)('Create Web3 class instance with http string providers', () => { it('should create instance with string provider', () => { web3 = new Web3(provider); diff --git a/packages/web3/test/integration/web3RPCProviders.test.ts b/packages/web3/test/integration/web3RPCProviders.test.ts new file mode 100644 index 00000000000..24bc312610d --- /dev/null +++ b/packages/web3/test/integration/web3RPCProviders.test.ts @@ -0,0 +1,61 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +import { mainnet , Network, QuickNodeProvider, Transport } from "web3-rpc-providers"; +import { Web3 } from '../../src/index'; + +describe('Web3 RPC Provider Integration tests', () => { + const transports = Object.values(Transport); + const networks = [Network.ETH_MAINNET, Network.ETH_HOLESKY, Network.ETH_SEPOLIA, + Network.ARBITRUM_MAINNET, Network.ARBITRUM_SEPOLIA, + Network.BNB_MAINNET, Network.BNB_TESTNET, + Network.POLYGON_MAINNET, Network.POLYGON_AMONY]; + + transports.forEach((transport) => { + networks.forEach((network) => { + it(`QuickNodeProvider should work with ${transport} transport and ${network} network`, async () => { + + const provider = new QuickNodeProvider(network, transport); + const web3 = new Web3(provider); + const result = await web3.eth.getBlockNumber() + + expect(typeof result).toBe('bigint'); + expect(result > 0).toBe(true); + + if (transport === Transport.WebSocket) { + web3.provider?.disconnect(); + } + + }); + }); + }); + it(`should work with mainnet provider`, async () => { + const web3 = new Web3(mainnet); + const result = await web3.eth.getBlockNumber() + expect(typeof result).toBe('bigint'); + expect(result > 0).toBe(true); + + }); + + it(`should work with default provider`, async () => { + const web3 = new Web3(); + const result = await web3.eth.getBlockNumber() + expect(typeof result).toBe('bigint'); + expect(result > 0).toBe(true); + + }); +}); \ No newline at end of file diff --git a/packages/web3/test/unit/web3.test.ts b/packages/web3/test/unit/web3.test.ts index 3d520f38e36..3cd00a9e718 100644 --- a/packages/web3/test/unit/web3.test.ts +++ b/packages/web3/test/unit/web3.test.ts @@ -21,9 +21,9 @@ import { Web3 } from '../../src/web3'; describe('Web3 object', () => { it('should be able to set and read web3 providers', () => { - const web3NoProvider = new Web3(); + const web3NoProvider = new Web3(""); expect(web3NoProvider).toBeTruthy(); - expect(web3NoProvider.provider).toBeUndefined(); + expect(web3NoProvider.provider).toBe(""); const web3 = new Web3('http://somenode'); expect(web3).toBeTruthy(); diff --git a/templates/.eslintignore.tmpl b/templates/.eslintignore.tmpl index ffb881be6b5..c2d4ae29304 100644 --- a/templates/.eslintignore.tmpl +++ b/templates/.eslintignore.tmpl @@ -1,9 +1,10 @@ dist lib +hardhat.config.js jest.config.js -.eslintrc.js cypress cypress.config.js +.eslintrc.js src/common/chains/** src/common/eips/** -src/common/hardforks/** \ No newline at end of file +src/common/hardforks/** diff --git a/tools/web3-plugin-example/CHANGELOG.md b/tools/web3-plugin-example/CHANGELOG.md index 65e4434200c..6762e7f8b1b 100644 --- a/tools/web3-plugin-example/CHANGELOG.md +++ b/tools/web3-plugin-example/CHANGELOG.md @@ -88,4 +88,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Dependencies updated +### Added + +Transaction middleware (#7088) + ## [Unreleased] \ No newline at end of file diff --git a/tools/web3-plugin-example/package.json b/tools/web3-plugin-example/package.json index 3832f5ef30d..c2463255ac8 100644 --- a/tools/web3-plugin-example/package.json +++ b/tools/web3-plugin-example/package.json @@ -1,6 +1,6 @@ { "name": "web3-plugin-example", - "version": "1.0.6", + "version": "1.1.0", "description": "Example implementations of Web3.js' 4.x plugin system", "repository": "https://github.com/ChainSafe/web3.js", "engines": { @@ -45,15 +45,16 @@ "prettier": "^2.7.1", "ts-jest": "^29.1.1", "typescript": "^4.7.4", - "web3": "^4.3.0", - "web3-core": "^4.3.2", - "web3-eth-abi": "^4.1.4", - "web3-eth-contract": "^4.1.4", - "web3-types": "^1.3.1", - "web3-utils": "^4.1.0" + "web3": "^4.10.0", + "web3-core": "^4.5.0", + "web3-eth-abi": "^4.2.2", + "web3-eth-contract": "^4.5.0", + "web3-types": "^1.7.0", + "web3-utils": "^4.3.0" }, "peerDependencies": { "web3-core": ">= 4.1.1 < 5", + "web3-eth": ">= 4.7.0 < 5", "web3-eth-abi": ">= 4.1.1 < 5", "web3-eth-contract": ">= 4.0.5 < 5", "web3-types": ">= 1.1.1 < 5", diff --git a/tools/web3-plugin-example/src/custom_rpc_methods.ts b/tools/web3-plugin-example/src/custom_rpc_methods.ts index ed1c2fbee23..d5ff583ddf5 100644 --- a/tools/web3-plugin-example/src/custom_rpc_methods.ts +++ b/tools/web3-plugin-example/src/custom_rpc_methods.ts @@ -18,7 +18,7 @@ import { Web3PluginBase } from 'web3-core'; // eslint-disable-next-line require-extensions/require-extensions import { Web3Context } from './reexported_web3_context'; // eslint-disable-next-line require-extensions/require-extensions -import { Web3Middleware } from './middleware'; +import { Web3Middleware } from './request_manager_middleware'; type CustomRpcApi = { custom_rpc_method: () => string; diff --git a/tools/web3-plugin-example/src/middleware.ts b/tools/web3-plugin-example/src/request_manager_middleware.ts similarity index 100% rename from tools/web3-plugin-example/src/middleware.ts rename to tools/web3-plugin-example/src/request_manager_middleware.ts diff --git a/tools/web3-plugin-example/src/transaction_middleware.ts b/tools/web3-plugin-example/src/transaction_middleware.ts new file mode 100644 index 00000000000..d38eec8127b --- /dev/null +++ b/tools/web3-plugin-example/src/transaction_middleware.ts @@ -0,0 +1,38 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +import { TransactionMiddleware, TransactionMiddlewareData } from "web3-eth"; + +// Sample Transaction Middleware +export class Web3TransactionMiddleware implements TransactionMiddleware { + + // eslint-disable-next-line class-methods-use-this + public async processTransaction(transaction: TransactionMiddlewareData, + _options?: { [key: string]: unknown } | undefined): + + Promise { + + // eslint-disable-next-line prefer-const + let txObj = { ...transaction }; + + // Add your logic here for transaction modification + txObj.data = '0x123'; + + return Promise.resolve(txObj); + } + +} diff --git a/tools/web3-plugin-example/src/transaction_middleware_plugin.ts b/tools/web3-plugin-example/src/transaction_middleware_plugin.ts new file mode 100644 index 00000000000..3dd86ed7e26 --- /dev/null +++ b/tools/web3-plugin-example/src/transaction_middleware_plugin.ts @@ -0,0 +1,44 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ +import { Web3PluginBase } from 'web3-core'; +// eslint-disable-next-line require-extensions/require-extensions +import { Web3Context } from './reexported_web3_context'; +// eslint-disable-next-line require-extensions/require-extensions +import { Web3TransactionMiddleware } from './transaction_middleware'; + +// Sample Transaction middleware plugin +export class TransactionMiddlewarePlugin extends Web3PluginBase { + public pluginNamespace = 'TransactionsPlugIn'; + public txMiddleware: Web3TransactionMiddleware; + + public constructor() { + super(); + this.txMiddleware = new Web3TransactionMiddleware(); + } + + public link(parentContext: Web3Context): void { + + if (this.txMiddleware){ + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + (parentContext as any).Web3Eth.setTransactionMiddleware(this.txMiddleware); + } + + super.link(parentContext); + } + +} + diff --git a/tools/web3-plugin-example/test/unit/fixtures/transactions_data.ts b/tools/web3-plugin-example/test/unit/fixtures/transactions_data.ts new file mode 100644 index 00000000000..189e2135ae4 --- /dev/null +++ b/tools/web3-plugin-example/test/unit/fixtures/transactions_data.ts @@ -0,0 +1,117 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +export const blockMockResult = { + "jsonrpc": "2.0", + "id": "a40a81fa-1f8b-4bb2-a0ad-eef9b6d4636f", + "result": { + "baseFeePerGas": "0x44dab2983", + "blobGasUsed": "0x20000", + "difficulty": "0x0", + "excessBlobGas": "0x1c0000", + "extraData": "0x407273796e636275696c646572", + "gasLimit": "0x1c9c380", + "gasUsed": "0xb7a086", + "hash": "0xf2b1729965179032b17165678a1a212fa31cb008e30f4011ffe8ebdddbd02b95", + "logsBloom": "0xc3a70590c1c62524173d1892e33888067101934dc0891c2c9a898252b6f320215084a48906452960820188d32bba6fb82ec989018a0268603a00a4c6432a11276c9a038c676938eb68bc436c9905a9a1b08d238fb4458f48498215808bec81112e2a3a54869ff22422a8e491093da8a40f601d198417041cd22f799f9048865006e0b069ab049b852442b310396248088145e2810f230f9a44000c6868bc73e9afa8832a8ac92fd609007ac53c0a9cba0645ce298080184624e8040831dbc331f5e618072407050250021b3210e542781183a612d4618c1244000d421a6ca9c01a57e86a085402c55ab413f840a001e7117894d0469e20c2304a9655e344f60d", + "miner": "0x1f9090aae28b8a3dceadf281b0f12828e676c326", + "mixHash": "0x787ab1d511b72df60a705bb4cfc4e92e2f9d203e3e007ae3a0f757425951ca24", + "nonce": "0x0000000000000000", + "number": "0x131ad16", + "parentBeaconBlockRoot": "0x03bbca9fd0c7a0a020de04287f489112c79bc268220e9ff8e18957cd0d5c3cad", + "parentHash": "0xb1d8fa7b8346421d373a6d4c28575155516cea17c12a3df7201170c9e561b38c", + "receiptsRoot": "0x4ec500bdcd761ad505b2a989156c9a9628058d415acc93d800487c7c76308c59", + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size": "0xcb90", + "stateRoot": "0xafbb8743c0a5f4740e322217cb1f2780ee5c57c32bcd04e9256b09efc1a70568", + "timestamp": "0x6661ab8b", + "totalDifficulty": "0xc70d815d562d3cfa955", + "transactions": [ + "0x589956b75d19dbaf9911f246c23d4b3327ef234872ec1931c419041b23eb5b41", + "0x4d3793f20c25979bd329cafdd889ba6be015cfc999acce8642d6e757b5192e93", + "0x5ba5618ca5a14bab50862255dc69726a498346f9832bd0fd1394e8834e56790b", + "0x6df9678f350db7e30afc930b7246bf1c144b9acb7fd1d25d7e107d550ed5a061", + "0xb8f48ff2876cc393725ea4162421754dfb74ff2364a12d4d3de2c6269f1958c7", + "0x2e5cf7c0607025038b6ccd871dc9ce85af686fd5fa2c82e605198af9afa92cca", + "0x307fb855836feff5d8d0969fa4a44d3c6ae31d335da6577f48f9496d6fe9e0b9", + "0x1362bed1aa8a30d28b7b76c35c2a8601b257058beffa9490dcb20de12bcb15b2", + "0x234c7cc346c204022b2e5ead6d2e8c02317aeb0ec5ca82bd97c2b5d5e59a280b", + ], + "transactionsRoot": "0xc21a4d667b5f841538430b1e2c002c598f2178628ad1d61ea2fda462d1216607", + "uncles": [], + "withdrawals": [ + { + "address": "0xea97dc2523c0479484076660f150833e264c41e9", + "amount": "0x11b6d8c", + "index": "0x2dbe454", + "validatorIndex": "0x10f646" + }, + { + "address": "0xb3e84b6c6409826dc45432b655d8c9489a14a0d7", + "amount": "0x11b4ce2", + "index": "0x2dbe455", + "validatorIndex": "0x10f647" + }, + { + "address": "0x7e2a2fa2a064f693f0a55c5639476d913ff12d05", + "amount": "0x11ad733", + "index": "0x2dbe456", + "validatorIndex": "0x10f648" + }, + + ], + "withdrawalsRoot": "0x2914fa2f5ed93880ed45b58e8f6d14f20c645988400d83c59109964e2053fe1a" + } +}; + +export const receiptMockResult = { + "jsonrpc": "2.0", + "id": 1, + "result": { + "blockHash": "0xf4ad699b98241caf3930779b7d919a77f1727e67cef6ed1ce2a4c655ba812d54", + "blockNumber": "0x131ad35", + // eslint-disable-next-line no-null/no-null + "contractAddress": null, + "cumulativeGasUsed": "0x8cae7a", + "effectiveGasPrice": "0x4c9bc2d65", + "from": "0xab6fd3a7c6ce9db945889cd018e028e055f3bc2e", + "gasUsed": "0xa145", + "logs": [ + { + "address": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "blockHash": "0xf4ad699b98241caf3930779b7d919a77f1727e67cef6ed1ce2a4c655ba812d54", + "blockNumber": "0x131ad35", + "data": "0x000000000000000000000000000000000000000000000000000000000016e360", + "logIndex": "0xdf", + "removed": false, + "topics": [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x000000000000000000000000ab6fd3a7c6ce9db945889cd018e028e055f3bc2e", + "0x00000000000000000000000051112f9f08a2174fe3fc96aad8f07e82d1cccd00" + ], + "transactionHash": "0xdf7756865c2056ce34c4eabe4eff42ad251a9f920a1c620c00b4ea0988731d3f", + "transactionIndex": "0x82" + } + ], + "logsBloom": "0x00000000000000000000000002000000000000000000000000000000004000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000400000000000100000000000000000000000000080000000000000000000040000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000400000000000000000000000", + "status": "0x1", + "to": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "transactionHash": "0xdf7756865c2056ce34c4eabe4eff42ad251a9f920a1c620c00b4ea0988731d3f", + "transactionIndex": "0x82", + "type": "0x2" + } +}; \ No newline at end of file diff --git a/tools/web3-plugin-example/test/unit/middleware.test.ts b/tools/web3-plugin-example/test/unit/request_manager_middleware.test.ts similarity index 100% rename from tools/web3-plugin-example/test/unit/middleware.test.ts rename to tools/web3-plugin-example/test/unit/request_manager_middleware.test.ts diff --git a/tools/web3-plugin-example/test/unit/transaction_middleware.test.ts b/tools/web3-plugin-example/test/unit/transaction_middleware.test.ts new file mode 100644 index 00000000000..2009e028283 --- /dev/null +++ b/tools/web3-plugin-example/test/unit/transaction_middleware.test.ts @@ -0,0 +1,76 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +import { Web3, Transaction } from 'web3'; +import { TransactionMiddlewarePlugin } from '../../src/transaction_middleware_plugin'; +import { blockMockResult, receiptMockResult } from './fixtures/transactions_data'; + +describe('Transaction Middleware', () => { + + // This will allow Transaction modification before signing and gas estimations + it('should modify transaction before signing', async () => { + const web3 = new Web3('http://127.0.0.1:8545'); + const plugin = new TransactionMiddlewarePlugin(); + + /// Mock block starts - Mock web3 internal calls for test + let blockNum = 1000; + + web3.requestManager.send = jest.fn(async (request) => { + blockNum += 1; + + if(request.method === 'eth_getBlockByNumber'){ + + return Promise.resolve(blockMockResult.result); + } + if(request.method === 'eth_call'){ + + return Promise.resolve("0x"); + } + if(request.method === 'eth_blockNumber'){ + + return Promise.resolve(blockNum.toString(16)); + } + if(request.method === 'eth_sendTransaction'){ + + // Test that middleware modified transaction + // eslint-disable-next-line jest/no-conditional-expect + expect((request.params as any)[0].data).toBe("0x123"); + + return Promise.resolve("0xdf7756865c2056ce34c4eabe4eff42ad251a9f920a1c620c00b4ea0988731d3f"); + } + if (request.method === 'eth_getTransactionReceipt') { + return Promise.resolve(receiptMockResult.result); + } + + return Promise.resolve("Unknown Request" as any); + }); + + /// Mock block ends here + + web3.registerPlugin(plugin); + + const transaction: Transaction = { + from: '0x6E599DA0bfF7A6598AC1224E4985430Bf16458a4', + to: '0x6f1DF96865D09d21e8f3f9a7fbA3b17A11c7C53C', + value: '0x1', + data: '0x1' + }; + + await web3.eth.sendTransaction(transaction as any); + + }); +});