├── .gitignore ├── README.md ├── contracts ├── Typeface.sol ├── TypefaceExpandable.sol ├── examples │ ├── TestTypeface.sol │ └── TestTypefaceExpandable.sol └── interfaces │ ├── ITypeface.sol │ └── ITypefaceExpandable.sol ├── fonts.ts ├── hardhat.config.ts ├── package.json ├── test ├── TestTypeface.ts ├── TestTypefaceExpandable.ts └── utils.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | artifacts 4 | cache 5 | mnemonic.txt 6 | pk.txt 7 | deployments/localhost 8 | typechain-types 9 | build 10 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Typeface Contract 2 | 3 | The Typeface contract allows storing and retrieving font source data (base-64 or otherwise encoded) on-chain. 4 | 5 | ## Storing font sources 6 | 7 | Font source data can be large and cost large amounts of gas to store. To avoid surpassing gas limits in deploying a contract with included font source data, only a keccak256 hash of the data is stored when the contract is deployed. This allows font sources to be stored later in separate transactions, provided the hash of the data matches the hash previously stored for that font. 8 | 9 | Fonts are identified by the Font struct, which specifies `style` and `weight` properties. 10 | 11 | ## Supported characters 12 | 13 | The function `supportsCodePoint(bytes3)` allows specifying which characters are supported by the stored typeface. All possible unicodes can be encoded using no more than 3 bytes. 14 | 15 | ## TypefaceExpandable 16 | 17 | The TypefaceExpandable contract allows font hashes to be added or modified after deployment by an operator address. Hashes can only be modified until a source has been stored for that font. -------------------------------------------------------------------------------- /contracts/Typeface.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.8; 3 | 4 | import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; 5 | import "./interfaces/ITypeface.sol"; 6 | 7 | /** 8 | @title Typeface 9 | 10 | @author peri 11 | 12 | @notice The Typeface contract allows storing and retrieving a "source", such as a base64-encoded file, for all fonts in a typeface. 13 | 14 | Sources may be large and require a high gas fee to store. To reduce gas costs while deploying a contract containing source data, or to avoid surpassing gas limits of the deploy transaction block, only a hash of each source is stored when the contract is deployed. This allows sources to be stored in later transactions, ensuring the hash of the source matches the hash already stored for that font. 15 | 16 | Once the Typeface contract has been deployed, source hashes can't be added or modified. 17 | 18 | Fonts are identified by the Font struct, which includes "style" and "weight" properties. 19 | */ 20 | 21 | abstract contract Typeface is ITypeface, ERC165 { 22 | modifier onlyDonationAddress() { 23 | if (msg.sender != _donationAddress) { 24 | revert("Typeface: Not donation address"); 25 | } 26 | _; 27 | } 28 | 29 | /// @notice Mapping of style => weight => font source data as bytes. 30 | mapping(string => mapping(uint256 => bytes)) private _source; 31 | 32 | /// @notice Mapping of style => weight => keccack256 hash of font source data as bytes. 33 | mapping(string => mapping(uint256 => bytes32)) private _sourceHash; 34 | 35 | /// @notice Mapping of style => weight => true if font source has been stored. 36 | /// @dev This serves as a gas-efficient way to check if a font source has been stored without getting the entire source data. 37 | mapping(string => mapping(uint256 => bool)) private _hasSource; 38 | 39 | /// @notice Address to receive donations. 40 | address _donationAddress; 41 | 42 | /// @notice Typeface name 43 | string private _name; 44 | 45 | /// @notice Return typeface name. 46 | /// @return name Name of typeface 47 | function name() public view virtual override returns (string memory) { 48 | return _name; 49 | } 50 | 51 | /// @notice Return source bytes for font. 52 | /// @param font Font to check source of. 53 | /// @return source Font source data as bytes. 54 | function sourceOf( 55 | Font memory font 56 | ) public view virtual returns (bytes memory) { 57 | return _source[font.style][font.weight]; 58 | } 59 | 60 | /// @notice Return true if font source exists. 61 | /// @param font Font to check if source exists for. 62 | /// @return true True if font source exists. 63 | function hasSource(Font memory font) public view virtual returns (bool) { 64 | return _hasSource[font.style][font.weight]; 65 | } 66 | 67 | /// @notice Return hash of source bytes for font. 68 | /// @param font Font to return source hash of. 69 | /// @return sourceHash Hash of source for font. 70 | function sourceHash( 71 | Font memory font 72 | ) public view virtual returns (bytes32) { 73 | return _sourceHash[font.style][font.weight]; 74 | } 75 | 76 | /// @notice Returns the address to receive donations. 77 | /// @return donationAddress The address to receive donations. 78 | function donationAddress() external view returns (address) { 79 | return _donationAddress; 80 | } 81 | 82 | /// @notice Allows the donation address to set a new donation address. 83 | /// @param __donationAddress New donation address. 84 | function setDonationAddress( 85 | address __donationAddress 86 | ) external onlyDonationAddress { 87 | _setDonationAddress(__donationAddress); 88 | } 89 | 90 | function _setDonationAddress(address __donationAddress) internal { 91 | _donationAddress = payable(__donationAddress); 92 | 93 | emit SetDonationAddress(__donationAddress); 94 | } 95 | 96 | /// @notice Sets source for Font. 97 | /// @dev The keccack256 hash of the source must equal the sourceHash of the font. 98 | /// @param font Font to set source for. 99 | /// @param source Font source as bytes. 100 | function setSource(Font calldata font, bytes calldata source) public { 101 | require(!hasSource(font), "Typeface: Source already exists"); 102 | 103 | require( 104 | keccak256(source) == sourceHash(font), 105 | "Typeface: Invalid font" 106 | ); 107 | 108 | _beforeSetSource(font, source); 109 | 110 | _source[font.style][font.weight] = source; 111 | _hasSource[font.style][font.weight] = true; 112 | 113 | emit SetSource(font); 114 | 115 | _afterSetSource(font, source); 116 | } 117 | 118 | /// @notice Sets hash of source data for each font in a list. 119 | /// @dev Length of fonts and hashes arrays must be equal. Each hash from hashes array will be set for the font with matching index in the fonts array. 120 | /// @param fonts Array of fonts to set hashes for. 121 | /// @param hashes Array of hashes to set for fonts. 122 | function _setSourceHashes( 123 | Font[] memory fonts, 124 | bytes32[] memory hashes 125 | ) internal { 126 | require( 127 | fonts.length == hashes.length, 128 | "Typeface: Unequal number of fonts and hashes" 129 | ); 130 | 131 | for (uint256 i; i < fonts.length; i++) { 132 | _sourceHash[fonts[i].style][fonts[i].weight] = hashes[i]; 133 | 134 | emit SetSourceHash(fonts[i], hashes[i]); 135 | } 136 | } 137 | 138 | constructor(string memory __name, address __donationAddress) { 139 | _name = __name; 140 | _setDonationAddress(__donationAddress); 141 | } 142 | 143 | /// @dev See {IERC165-supportsInterface}. 144 | function supportsInterface( 145 | bytes4 interfaceId 146 | ) public view virtual override(ERC165) returns (bool) { 147 | return 148 | interfaceId == type(ITypeface).interfaceId || 149 | super.supportsInterface(interfaceId); 150 | } 151 | 152 | /// @notice Function called before setSource() is called. 153 | function _beforeSetSource( 154 | Font calldata font, 155 | bytes calldata src 156 | ) internal virtual {} 157 | 158 | /// @notice Function called after setSource() is called. 159 | function _afterSetSource( 160 | Font calldata font, 161 | bytes calldata src 162 | ) internal virtual {} 163 | } 164 | -------------------------------------------------------------------------------- /contracts/TypefaceExpandable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.8; 3 | 4 | import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; 5 | import "./Typeface.sol"; 6 | import "./interfaces/ITypefaceExpandable.sol"; 7 | 8 | /** 9 | @title TypefaceExpandable 10 | 11 | @author peri 12 | 13 | @notice TypefaceExpandable is an extension of the Typeface contract that allows an operator to add or modify font hashes after the contract has been deployed, as long as a source for the font hasn't been stored yet. 14 | */ 15 | 16 | abstract contract TypefaceExpandable is Typeface, ITypefaceExpandable { 17 | /// @notice Require that the sender is the operator address. 18 | modifier onlyOperator() { 19 | if (msg.sender != _operator) revert("TypefaceExpandable: Not operator"); 20 | _; 21 | } 22 | 23 | /// @notice Require that all fonts have not been stored. 24 | modifier onlyUnstoredFonts(Font[] calldata fonts) { 25 | for (uint256 i; i < fonts.length; i++) { 26 | Font memory font = fonts[i]; 27 | if (hasSource(font)) { 28 | revert("TypefaceExpandable: Source already exists"); 29 | } 30 | } 31 | _; 32 | } 33 | 34 | /// Address with permission to add or modify font hashes, as long as no source has been stored for that font. 35 | address internal _operator; 36 | 37 | /// @notice Allows operator to set new font hashes. 38 | /// @dev Equal number of fonts and hashes must be provided. 39 | /// @param fonts Array of fonts to set hashes for. 40 | /// @param hashes Array of hashes to set for fonts. 41 | function setSourceHashes(Font[] calldata fonts, bytes32[] calldata hashes) 42 | external 43 | onlyOperator 44 | onlyUnstoredFonts(fonts) 45 | { 46 | _setSourceHashes(fonts, hashes); 47 | } 48 | 49 | /// @notice Returns operator of contract. Operator has permission to add or modify font hashes, as long as no source has been stored for that font. 50 | /// @return operator Operator address. 51 | function operator() external view returns (address) { 52 | return _operator; 53 | } 54 | 55 | /// @notice Allows operator to set new operator. 56 | /// @param __operator New operator address. 57 | function setOperator(address __operator) external onlyOperator { 58 | _setOperator(__operator); 59 | } 60 | 61 | constructor( 62 | string memory __name, 63 | address donationAddress, 64 | address __operator 65 | ) Typeface(__name, donationAddress) { 66 | _setOperator(__operator); 67 | } 68 | 69 | /// @dev See {IERC165-supportsInterface}. 70 | function supportsInterface(bytes4 interfaceId) 71 | public 72 | view 73 | virtual 74 | override(Typeface) 75 | returns (bool) 76 | { 77 | return 78 | interfaceId == type(ITypefaceExpandable).interfaceId || 79 | super.supportsInterface(interfaceId); 80 | } 81 | 82 | function _setOperator(address __operator) internal { 83 | _operator = __operator; 84 | 85 | emit SetOperator(__operator); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /contracts/examples/TestTypeface.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | /** 4 | @title ASCIITypeface 5 | @author peri 6 | @notice Typeface contract implementation for storing a typeface with characters that require only 1 byte to encode. 7 | */ 8 | 9 | pragma solidity ^0.8.0; 10 | 11 | import "../Typeface.sol"; 12 | 13 | contract TestTypeface is Typeface { 14 | /// For testing 15 | event BeforeSetSource(); 16 | 17 | /// For testing 18 | event AfterSetSource(); 19 | 20 | constructor( 21 | Font[] memory fonts, 22 | bytes32[] memory hashes, 23 | address donationAddress 24 | ) Typeface("TestTypeface", donationAddress) { 25 | _setSourceHashes(fonts, hashes); 26 | } 27 | 28 | function supportsCodePoint(bytes3 cp) external pure returns (bool) { 29 | return cp >= 0x000020 && cp <= 0x00007A; 30 | } 31 | 32 | function _beforeSetSource(Font calldata, bytes calldata) internal override { 33 | emit BeforeSetSource(); 34 | } 35 | 36 | function _afterSetSource(Font calldata, bytes calldata) internal override { 37 | emit AfterSetSource(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /contracts/examples/TestTypefaceExpandable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | /** 4 | @title ASCIITypeface 5 | @author peri 6 | @notice Typeface contract implementation for storing a typeface with characters that require only 1 byte to encode. 7 | */ 8 | 9 | pragma solidity ^0.8.0; 10 | 11 | import "../TypefaceExpandable.sol"; 12 | 13 | contract TestTypefaceExpandable is TypefaceExpandable { 14 | /// For testing 15 | event BeforeSetSource(); 16 | 17 | /// For testing 18 | event AfterSetSource(); 19 | 20 | constructor( 21 | Font[] memory fonts, 22 | bytes32[] memory hashes, 23 | address donationAddress, 24 | address operator 25 | ) TypefaceExpandable("TestTypeface", donationAddress, operator) { 26 | _setSourceHashes(fonts, hashes); 27 | } 28 | 29 | function supportsCodePoint(bytes3 cp) external pure returns (bool) { 30 | return cp >= 0x000020 && cp <= 0x00007A; 31 | } 32 | 33 | function _beforeSetSource(Font calldata, bytes calldata) internal override { 34 | emit BeforeSetSource(); 35 | } 36 | 37 | function _afterSetSource(Font calldata, bytes calldata) internal override { 38 | emit AfterSetSource(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /contracts/interfaces/ITypeface.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | /** 4 | @title ITypeface 5 | 6 | @author peri 7 | 8 | @notice Interface for Typeface contract 9 | */ 10 | 11 | pragma solidity ^0.8.8; 12 | 13 | struct Font { 14 | uint256 weight; 15 | string style; 16 | } 17 | 18 | interface ITypeface { 19 | /// @notice Emitted when the source is set for a font. 20 | /// @param font The font the source has been set for. 21 | event SetSource(Font font); 22 | 23 | /// @notice Emitted when the source hash is set for a font. 24 | /// @param font The font the source hash has been set for. 25 | /// @param sourceHash The source hash that was set. 26 | event SetSourceHash(Font font, bytes32 sourceHash); 27 | 28 | /// @notice Emitted when the donation address is set. 29 | /// @param donationAddress New donation address. 30 | event SetDonationAddress(address donationAddress); 31 | 32 | /// @notice Returns the typeface name. 33 | function name() external view returns (string memory); 34 | 35 | /// @notice Check if typeface includes a glyph for a specific character code point. 36 | /// @dev 3 bytes supports all possible unicodes. 37 | /// @param codePoint Character code point. 38 | /// @return true True if supported. 39 | function supportsCodePoint(bytes3 codePoint) external view returns (bool); 40 | 41 | /// @notice Return source data of Font. 42 | /// @param font Font to return source data for. 43 | /// @return source Source data of font. 44 | function sourceOf(Font memory font) external view returns (bytes memory); 45 | 46 | /// @notice Checks if source data has been stored for font. 47 | /// @param font Font to check if source data exists for. 48 | /// @return true True if source exists. 49 | function hasSource(Font memory font) external view returns (bool); 50 | 51 | /// @notice Stores source data for a font. 52 | /// @param font Font to store source data for. 53 | /// @param source Source data of font. 54 | function setSource(Font memory font, bytes memory source) external; 55 | 56 | /// @notice Sets a new donation address. 57 | /// @param donationAddress New donation address. 58 | function setDonationAddress(address donationAddress) external; 59 | 60 | /// @notice Returns donation address 61 | /// @return donationAddress Donation address. 62 | function donationAddress() external view returns (address); 63 | } 64 | -------------------------------------------------------------------------------- /contracts/interfaces/ITypefaceExpandable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | /** 4 | @title ITypeface 5 | 6 | @author peri 7 | 8 | @notice Interface for Typeface contract 9 | */ 10 | 11 | pragma solidity ^0.8.8; 12 | 13 | import "./ITypeface.sol"; 14 | 15 | interface ITypefaceExpandable is ITypeface { 16 | event SetOperator(address operator); 17 | 18 | function operator() external view returns (address); 19 | 20 | function setSourceHashes(Font[] memory fonts, bytes32[] memory hashes) 21 | external; 22 | 23 | function setOperator(address operator) external; 24 | } 25 | -------------------------------------------------------------------------------- /fonts.ts: -------------------------------------------------------------------------------- 1 | // Encoded font is Jetbrains Mono https://www.jetbrains.com/lp/mono/ 2 | 3 | export const FONTS = { 4 | normal: { 5 | 400: "xIDwkYCA0IDEgAAAyIAAxIAAAADEgOi0gOWgguWgguSAgQD0j7yC7q+44KCA7aiC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCCAOWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgggDloILvv7/mnILuqIEAAAAAAOmHvu+ogADju77kh77jv74AAADkr6IA44ei6JOi7p+gAOezoOevoADvi5/in6Dis53mr5wA6LiDAAAAAAAA6YiD6bCD7JCD7aiD46CE5ICEAAAAAAAAAO24hADviITvoIQA76iEyIUABQDQhQDimIYAAAAAAAAAAADlqILygICI8Yuho/CQgrLwkIKy84C2s/GLoKDwkaCYAPOLhaHygICI64is64is64ys8JuMrOuMrPCbjKzriKzriKzrjKzwm4ys64ys8JuMrOuZgvCwgrnwsIK58aaQheSRhAAAAAAAAAAAAAAA7aiC4piC7aiC7aiC4Zqw8bCAiOOIgOOIgOOIgOWgguWgguOIgOOIgOOIgOOIgOOIgOOIgOWgguOIgOOIgOOIgOOIgOOIgOOIgOOIgPGghJzxoICfaOaMg+GBgMSA5aCCxIDloILkuIXhoIDmiIJN5bCA8LCMgF/kuIHwoICA8JCcgeS4g+WwgOCogPCQhIBf5LiF4pSA55iC4pSA5rCC4pSA5rCC4pSA5rCC4pSA5rCC4pSA5rCC5rCC4pSA5rCC4pSA5rCC4pSA5rCC4pSA5rCC4pSA5rCC4pSA5rCC4pSA5oiC4pSA5rCC4pSA5rCC8KCAqeW8gPCggKbxoIGn8JCEgF/hhYDmkIDlvIDykICA8JS4gOOggOaIguOggOWgguOggOWgguOggOWgguOggOWwguOggOWggsSC5LiD47yA5aCC5qSA5qSA5qSA5qSA5qSA5qSA5qSA5qSA8aCAoOSIgOWggvCUuIDlsIDEguS4g+eMgOGxgPGQgIDmnIDnjIDnjIDhkIDksIDwsIyG5aiA8JCFjOWkgOaIguWkgOWgguWkgOWgguWkgOWggvCvm7/wr5u/5byA5oiC8K+bv+W8gOWggvCvm7/lvIDloILlvIDloILwoq+/5byA5aCC5aCC5byA5aCC5byA5aCC8K+bv+W8gOWggvCvm7/lvIDloILwoq+/5byA5aCC8K+bv+W8gOS4gvCvm7/lvIDloILwsIyB8JCVofCRmJfmsIDloILmsIDloILmsIDkkILmsIDkuILmsIDkuILlvIDloILwr5u/5byA5aCC8KOXv/CwjIDwkJmf8JCYg/CRuJ/xgISJ8YCQgPCQhaHmhIDwkaSByIrIivKggIvlqIDmhIDlsIDlsIDlsIDEhvCwkIbogIDmiILogIDloILxsISA8JCcgBHlsIDwkIGM5byA8YCUiMSC5LiD45yA8JCYgeS4h+iggOWggvGAgoDEgg/ooIDloILwlLiA6LSA5oiC6LSA5aCC6LSA5aCC6LSA5aCC6LSA5aCC6LSA5LiC6LSA5aCC8YCUiMSG8JGNgOi0gOWggui0gOWggvCghIbzgICAyIHkuIPqhIDmiILqhIDloILqhIDloILqhIDkuILwkIWMxIFO6pyA5oiC6pyA5aCC6pyA5aCC6pyA5aCC6pyA5LiC6pyA5aCC6pyA5aCC6pyA5aCCTuuAgOaIguuAgOWgguuAgOWggvCQgoDwkbCd8JGYifCQgoDygKCB8JCCgPGQgafygKCB8JCpofCSrKzsrIDmiILwoJiB8YCQgfCQmaHxkJiH7LiA5piC7LiA5bCC8bCEgPCQnIAR8YCUmfCwgJjwkIGn8KCoh/CQgafygKCB8JCpofGgoI3wsIyA8JCdofGwgoDEh/CQgIXktYHusIDlvILusIDllILusIDllILusIDllILusIDllILusIDllILksIFO77+/7KiB74yA5ZSA5ZSAxINO5ZSA5ZSA5ZSA5ZSA5ZSA5ZSA5ZSA5ZSA8JCorfCQkIDxsIGp8JCwjO+cgOacgtCB6rSC5LS75LiD5byA5LiD2IHloILqsIHwlLiA4KSB44CC4KSB5aCC4KSB5rCC8JCIh0zIgeGpgPSEgZnwsIyA8JCFoeGdgOWwgO+bv+WwgOS0uU3lsIDEhPGgoJvxgJCB8JCZofGgoInwsICB8YCEieaEgOK0geaIguGcgeWggvGAkILxoKCPxIHChuaEgOaEgPCwjIDwkIWhwoDgtYDjkIHnmILjkIHmsILjkIHrjIFN5bCA8ZCUgGHqlIDysICA8YCUhuOcgPKAnIJO45yA8ZCAgNCF8oCEgE3ygISA8KCIgPCQkaHEguS0geaIguS0geWcguS0geS4guS0geWgguS0geWgguSUgeWgguSUgeWgguSUgeWgguSUgeWggvCghIbzgICA8KCEguKMgOKMgOKMgOKMgOOIgE7wkIWM5LiC5byB5oiC5byB5aCC5byB5aCC5byB76CC5byB5LiC5byB5aCC5byB5aCC5byB5aCCTuaggeaIguaggeWgguaggeWggvGQmIDxgIKA8JCBqfGQnIrwoICB44iA5biA8JCEgF/kuIPhqIDwkISAX07muIDmuIDlvIDwkICW4LGAyILmkIDmkIDgsIDIgvCUuIBO56SB5aCC5YCA5YCA5oiC5LSbDeWggOO8gPCvm7/lvIDwoIiAX+S4geekgOGsgOiggOiUgeWgguqYgOaAgOSAgOS4heS4gOachPOAgI3mnIXkhIDwoICg8JCZjeOUgOiAgPCUuIDwoIiA8JChoeSIgOSIgOWgguS4gMSA5LiE8YCAofGAgKnjoIDzsYGO8aCBjE1O4oyA5bCA6IiA8ZCAgOqIgeiAgvCQgJfwoIiAX+S4hO2ggO2ggOWggk7wlLiATuW4gOaEgMSC5LiD5oSATfOUgZnzsICAxIHkuILEguS4g+GcgfCgiIBf5LiB44SBxILkuIPjiIDkuIDIgE7kuIDwoIiE5oCA1IHwoIiA8JCRoMSE4LmA5oiA8rC0kciA5pyFxIPgrIvwoICg8KCAq/GwgILEguOggfCUuIDwlLiA75yA5pyC75yA5pyC0IHqtILEgPGAgadO8KCggvGgmIHwkKGh8YCUhvGAhIflvIDzkICWxIDktJvkuInvkIHEhvGgoI7jiIDltIDmuIDjiIDwoISK5LCB8ZCYgOKUgOuAgOO8gPCvm7/woIiB8JCZoeakgOWwgOOIgOGhgPOAgID0j7mH5aCA5aSA5biA5bCA45yA8JCEgF/kuIPooIDqnIDonIHqmIDjiIDxkJCB8JCJjfCQiY3hgYDgqIDot7/ot7/qs77ot7/qs77sv77oj7/qi77rg7/mpIDloILijIDloILEgvCgmIPxgJCB8JCZofGgoIzxkISH5LCC8YCUgPCkuIDwsIyA8JCFoeGtgOOcgPCwjIDwkJGf5oSA44iATOS4gcSC5aCB4ZyB8YCUhvCgiIBh4ZGA45yA8YCUhvCgiIHwkJGh5LiF8KCIgfCQmaLkuIfwoJSE76iB45yA45yA4LyE5JSB4LyE4ZyB8JCAgPGAlJjwsICX75iB8ZCAgvCQhYzxgJSAzIHwsICA8pCAi/CgiIHwkJmh8JCJjeS4g+SggOS4hPGggoDEh+WAgPCggYzwoJCA5LiE8LCMgfCQlaHwkZiX8LCAgfOEgZnygICA8LCEhvCwiIDivILjhILjjILjlILjnILkuITxsIG+xIjwsJCG8LCAgfOEgZnygICA8LCEhvCwiIDlqIDhmYDkuIHxsIKF4aiA8ZCEhvKwgInwoKSA8auEq/CUuIDwkICACsSE8ZCcl/CwgILwkJKE8JC4jvCQhILxkJiC8aCdjnLhlJbxgICmxIfwsICE8YCApPCwgoDwkJ2j8ZCgocSA8JCAgfCQhIBX47SC5ZyAT/CQgIDnmIHwkICA55iB8KCIgPCQkZ/wkIyA1IDxkICB5pyET+acgfGwgIB2duakgOWgguGtgPCwgIDhrYDwsICA4a2A8LCAgOWwguWcgE/wkIiP8JCAgfCwgIXxgJG28ZCYh/CwgIXYhvCQiYzEgvCQjITwkISAV/CQhILkvIHEgvGQgIhO8JCMgeSwgfCQjIHksIHjtILygIKG8qCogPCQmZ/wsISE5LyD8YCEhciB8JCAmuaEgPCQgafwoLCH8JCwiPCRtJ7zgLCA8JC5oeS0uOS4h+G4gPCQnIHxsISB8JCYgeS4iPCQiYzijIBO47SC8YCQgPCQmaLwkIG+8YCBqfCgiITwoIiET++8gOGRgOWogPGQlIDwkJmf8oCoi+eggOGRgOO1gPGQmIflgIDmnIHysICA86CAmsiB8JGAkeKsgE/xsICF84CAifCwgobxkIGpavGgmIDwkK2X8aCIiPCQrafEi/CQiobwkIqGxIbwkJiB4rGA0ITEgeS8gOWcgOWcgOSEgPCQmacQ6byC8KCAqeiAg/CQkIAOxIDwkIKG8JCAgPCQhIjksIF28auEq9CD8YCAgvCQhIjksIF284CAgOqYguKsgOacgvCQgIJPT/CQgoXwkIiA5LyE8LCAgfGgoJFO6ayA8YCRtvGQmIfxgJyA8bCBqfCQqITEhPKwgIDwsKCC47yC8JGYluG4gMSA8JCAgPGwgIDwoICB6byC8LCMgPCQkZ/ygKiL5pyB5ZyAT8SA8LCEhPCQhIDwkImf8bCAgO+/v+uwgvCQgYzxgIGp8JGAkfCggIHwkIKAyIHwkYCR4KiA8KCAgeeYgvCghJLlvIDzooiK5byA8JCBiuiUgfCQgYzwkISGT+Swg0/kpIDwkICB8oCEgvCQjoXgvIDxgJSA5LyD8LCMh+WcgOSwg0/wkICB5ZyAT/CQgIDnmIHhkYDwkICA55iB4ZGA8JCAgOeYgeGRgPCQgIDnmIHhkYDlnIBP8JCAgOeYgeGRgPCQgIDnmIHhkYDwkICA55iB4ZGA8JCAgOeYgeGRgOGRgPCQgIDnmIHhkYDhkYDwkICA8ZCAgPCQgIDxgJG28ZCYh/CQgIDxkICAduGRgPCQgIDxgJG28ZCYh/GQgIDtrIDmuIDrnIHttIDwkIqF8JeYgPCggIHwoICB8bCknfCwiIDEhPCggIHwoISF8KCIgfCQkaHxkJyX8JCAgOeYgeGRgPGAlIbwkICA55iB8YCUhuWggsiA4p2A8KCMgPCwgITwoISG5LCB5LCB8LCAhOK5gOS8gfCggIDwkIGK5LyA4ZWA8JCBn/CggIBA6ZCC8JCBiuS8gOWcgOiYhNCExIDYhtiG4rGA0ITxkICH8ZCEh/CgiIBX2IbYhvGAkbbxkJiH8bCgifGQmIfxgJCK8LCAhfCQgIJP8JCEg+S8gfCwhITwkISA8JCJn/GwgIDEgOW8gPKQgIDomILwkIGf5LyBT/CwhITwkISA8JCJn/GwgIDwoISD8JCEgOS8gOWcgPCggobkvIDEgeS8gOiYhdCExIDxgJGP8ZCYh+KdgPCwgIXhkYDwsISE8JCEgPCQiZ/xsICAxIDlnIDhrYDwsICA4a2A8LCAgOKVgPGwgIDwkISA8YCQiPGQmIfkuYDhrYDwsICA8YCQiPCwgIXjqYDQhOGRgPCQioXEgvCgiIBX8JCKhfCQgIDkvILhkYD0j6uwxIHkvIDlvIDxsICA4p2AT/GAhIHwkICAT/GAhIXwkISA5LyA8YCEheS8hPCwgZflvIDysICAxIDkvIPwkIGX8JCQgE/xgICD5LyAxIDomILwkIGh5YSB8KCIgFnwoIKF8JWEgPCggIHzsICA8ZCAgPGQgIDxgJGP8ZCYh+KdgPCwgIXhkYDwsISE8JCEgPCQiZ/xsICA8KCEg/CQhIDkvIDomILwkIGf5LyB5ZyAT+WcgE9P8LCIhPCwgIXQhOWcgE/woIiE8KCIhPGQmIfwkICA8JCAgOeYgfCwgIHnmIPwsISE5LyD8KCIgFfihYDhkYD0j6uwxIHkvIDlvIDxsICA9I+qvPCQgIDxsICAT8SA5LyD5LyE8JCBl/CQkIDysICA8YCEhfCQhIDkvIDxgIiA8KCBl2fxgICD8JCAgE/wkICC5LyA8KCIhPCgiIRP8pCAgE/wkIqFxILwkIKG5LyA8KCEg+S8gvCggobkvIDwkICA5LyC8JCKhcSCxIHwkICAT/CgiIBX8JCKhsSC8pCAgPCQgIDkvILwoIKF8YCEheS8hOKtgPCgmIjEgOCgiOCgiPCQgobwkICAT/SPqrzwkICA8bCAgPCQgobwkICAT+KdgE/EgeS8gOW8gPKwgID0j6mY8JCAgPKwgIDomILwsJCA4YiT75u/8JCEgPCQmZ/xoJiM8LCQiOWcgPGQgoDxoIGp8oCggPCQraHIh/CgnIXxoJiB8JCdoeCsi/CQgb7xkIGp8KCAgfGrhKvIgPCggIHxq4Sr8KGkovCwgIfwkIiE4KCI8KCAgfGrhKvhkYDxgJG28ZCYh/CQgIDnmIHisYDQhMSB5LyA4biA8aCQgPCQgoDxkIGp8LCBp/CVhIDwkICA8aCAgOCogOCgiPCQkoXwkLiP8KCAgfCghIXwoIiB8JCRofKQrKPwoJyP5ZyA8YCRj/CwgIXQgPCgkIDwsIKA8auEq+S4gOO4gvCQgIDnmIHxq4Sr8LCAheWggvCQgIDnmIHxq4Sr8LCQgvGQnJfwkISA8JCJofGQnIDhkYDwkICA55iB8auEq/GAkY/xkJiH5byA8JCEheSwgXbxq4Sr8aCAgPCgiIDwkJGh8LCQhvCghIXwoIiB8JCRofGQnJfxgJCA8YCYiOWcgE/EgPCQgobwkICA8JCEg/CQhIHwkImf8JCAl/CgiIBZ5aSA8JCEgPCQiaHxkJyA8KCEhfCgiIHwkJGh8ZCcl+GRgOakgfCggoXwlYCA5ZyAT/CQgIDnmIHxq4Sr4ZGA5aCC5aCC5aCC5aCC5aCC5aCC8JCEg/CQhIHwkICDxIDwkICB8JCAgOeYgeGRgPCQhIDwkImX8LCAhfCQgIDxoICA8KCEg3bIgPCggIHwoICB0IDwoJCA8LCCgOGtgPCwgIDIgfCQhIDlvIDgrYDwkICB8KCIgFnzkICA8JCEgvCQhIHkvIDEgPCQgIHwsJCC8ZCcl/CQgIDnmIHmpIHwoICZ8LCEhAzwoIiA8JCNoMiA8KCAgciA8KCAgeWcgE/loILloILloILloILloILhkYDlvIDwsICA0IXxq4Sr8KCIgPCQkaHwsJCG8LCAgOOhgNCF8JCBsPCQkIDwsISF8YCcgOSwgdCE57iD0ITwkIKE8JCAgPCQhIDzsLyV86CAkPCwhIXxgIGp8KCkgvCgkIDxgICB8JGomvCwgIDjoYDQhfCQgbDwkJCA8aCAhfCQgILwkKCD8KCAgOWcgPCwiITmhIDEgPSBhJLwsIyA8JCdofCwgIHkkYDwsISF8ZCAh/CggYzxkICAxIDxkICA5LCAyIAAAAAAAOKohPCfuIDkjZDEgOCwgADwoISAzIAA", 6 | 600: "xIDwkYCA0IDEgAAAyIAAxIAAAADEgOi0gOWgguWgguSAgQD0j7yC7q+44KCA7aiC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCCAOWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgguWgggDloILvv7/mnILuqIEAAAAAAOmHvu+ogADju77kh77jv74AAADkr6IA44ei6JOi7p+gAOezoOevoADvi5/in6Dis53mr5wA6LiDAAAAAAAA6YiD6bCD7JCD7aiD46CE5ICEAAAAAAAAAO24hADviITvoIQA76iEyIUABQDQhQDimIYAAAAAAAAAAADlqILygICI8Yuho/CQgrLwkIKy84C2s/GLoKDwkaCYAPOLhaHygICI64is64is64ys8JuMrOuMrPCbjKzriKzriKzrjKzwm4ys64ys8JuMrOuZgvCwgrnwsIK58aaQheSRhAAAAAAAAAAAAAAA7aiC4piC7aiC7aiC74ay8bCAiOKMgOKMgOKMgOWgguWgguKMgOKMgOKMgOKMgOKMgOKMgOWgguKMgOKMgOKMgOKMgOKMgOKMgOKMgPGghJvmjIPijIDijIDgsIDlvIDgsIDktIDwsIyAX+S4gcSG8oCojeGsgOacguGsgOW0gueIgfKAhInxsISAEeSsgPCwjIBf5LiB8JCcgeS4g+SsgOCogPCQhIBf5LiF4pSA55iC4pSA5rCC4pSA5rCC4pSA5rCC4pSA5rCC4pSA5rCC5rCC4pSA5rCC4pSA5rCC4pSA5rCC4pSA5rCC4pSA5rCC4pSA5rCC4pSA5oiC4pSA5rCC4pSA5rCC5LiF5aCA5YCA8pCAgPCUuIDjoIDmiILjoIDloILjoIDloILjoIDloILjoIDlsILjoIDloILEguS4g+O8gOWgguWogOWogOWogOWogOWogOWogOWogOWogMSE5LiF5aiA8KCIgPCQlaHkuIDpkILlgIDloIJO5YiA7bCB5YiA0ILliIDmsILliIBM5LiC46CA8LCMhk7koIDkoIBNTuSggMSE8aCgm8SF4bSe55SA5oiC5byA5aCC8JGkgfCRpILxoJiC8YCArfGgmILxgIC2yIrwkbif5LSA8YCQgF/zkLyT5LiA4ZiX5LCA5LCA5LCAxIbwsJCG6ICA5oiC6ICA5aCC8bCEgOiAgPKAhInhhYDogIDloILogIDloILxgJCAX8SH8JGkmuK4gOiggOWggvCgiIDwkJGixIXxoKCL6LSA5aCC6LSA5aCC8KCEhvOAgIDwoISC4YCA4YCA4YCA4YCA4bCATuGcgPCgjIHzoICA6pyA5oiC6pyA5aCC6pyA5aCC6pyA5aCC6pyA5LiC6pyA5aCC6pyA5aCC6pyA5aCCTuuAgOaIguuAgOWgguuAgOWggvCQgoDwkbCd8JGYifCQgoDxkIGn8KCoh+uQgOW0guuQgOW0gvCguInwoLiJ8KC4ifCTtL7EhvKAqIzsuIDmlILsuIDlrILxsISA6ICA8oCEieGFgOy4gOWsguy4gOWsgvCgmIPxgJCB8JCZofGgoI3xgJCB8JCZofCShKLwkISB8JCRn8SE8JS4gO2ggOaIgu2ggOWggu2ggOWggu2ggOWggu2ggOWggu2ggOWgguWggu2ggOWggu2ggOWggu2ggOWggu2ggOWggu2ggOWggu2ggOWggu2ggOS4gu2ggOWggu2ggOWggsSH45iA8JCUgU5N8ZCEiOS0gO+/v+yoge+MgOSwgOSwgMSDTuSwgOSwgOSwgOSwgOSwgOSwgOSwgOSwgPGwgKrEiuSwgNCB64yC5pyE5YSA4ayA8LCMgPCQkZ/hrIDhrIDcgOGsgPGAiIPIgeGpgPSEgZnwsIyA8JCFoeGdgOS0gO63v+S0gOS0uU3hgIHltILhnIHmiILhnIHloILhnIHloILhnIHloILhnIHloILloILhnIHloILhnIHloILhnIHloILhnIHloILhnIHloILhnIHkuILhnIHloILEhvCQgJ3wkICh8YCQgGHzv6CI4rSB5oiC4ZyB5aCCyIvxgJCC8JKkqsSBIeW4gOOQgeeYguOQgeawguOQgeuMgfCgiIDwkJmi8KCIgPCQmaLwoIiA8JCZouiAgPKAhInkpIDxkJSAYemMgPKwgIDxgJSG4qyA8oCcgk7irIDxgJWO8LCAlvKAhIDogIDykISK8KCIgPCQkaLEguS0geWYguS0geSwguS0geSIguS0geSwguS0geSwguSUgeWgguSUgeWgguSUgeWgguSUgeWggvCghIbzgICA8KCEguGQgOGQgOGQgOGQgOKQgE7wkIWM5LiC5byB5oiC5byB5aCC5byB5aCC5byB4LyD5byB5LiC5byB5aCC5byB5aCC5byB5aCCTuaggeaIguaggeWgguaggeWggvGQmIDxgIKA8JCBqfGQnIrwoISF8KCIgfCQkaHxoKCbxIDhqIDwkISAX07mlIDmlIDlvIDwkICW4LGAyILloIDloIDgrIDIgvCgiIDwkKGhTuekgeWgguWAgOWAgOaIguS0mw3loIDjvIDlvIDwoIiAX+S4geekgOGsgOiggOiUgeWggtSB8bCAgeqYgOaAgOOIgOS4heS4gOachPOAgI3mnIXjiIDxsICC8JCZjcSGxILxoJiA8JChofCUuIDlqIDlqIAAxILwoKCC8aCYgfCQoaHxgJSG8YCEh+W8gPOggJfqnIDEhvGgoI7rkIDEhvGgoInxgJCAX+S4gvCQhIBfTuekgOekgOW8gPCQgJbgsYDIguCkgMiC8KCIgPCQoaHxgIKAxIjwsJCG5LiA8JCFjOS4gOWEgMSC5LiD5YSATfOUgZnzsICAxIHkuILkuIDkuIDjhIDlvIHlvIHloILEhOW4geS4gPCgiITmgIDUgfCgiIDwkJGgxITguYDliIDysLSRyIDmnIXEg+Csi/CggKDwkJmN44iA8aCYgPCQoaHwoIiA8JChoeSwgOSwgADwoKCC8aCYgfCQoaHxgJSG8YCEh+W8gPOQgJbEgOS0m+S4ieKMgOS0gOaUgOKMgPCghIrksIHxkJiA5aCA5LyA5LSA8JC4j+WogOSwgOKMgOGhgPOggIDloIDlpIDkuIDktIDjnIDwkISAX+S4g+iggOqcgOicgeqYgOOIgPGQkIHwkImN8JCJjeGBgMSA4au/4pSA6pu+47yA5IiA5byA6pyA7pyB7Ju+5IiA6pyA5LiE8ZCEhuSwgvGAlIDxoJiA8JChofGAgoDEiPOAvJHwsIyA8JCFoeGtgOK4gPCwjIDwkJGf5YSA4oyATOS4gcSC5aCB8KCcgvGgmIDwoIiAYeW8gMSF5JSB6piA8KCUhO+ogeK4gOK4gOC8hOSUgeC8hOGcgfCwhIHiqYDxgJSY8LCAl++YgfGQgILlqIDkvILwkYyE8JCChuaEgOWsgPCQkIHyoKmP8rCwjfCRiJPljIDogIPwlLiA8bCBvsSI8LCQhuOQgPCQnrTwkICY8LCEhvCwiIDwsIyB8JCRofCRmJfivILjhILjjILjlILjnILkuITxsIG+xIjwsJCG8LCAgfOEgZnygICA8LCEhvCwiIDkuIDhmYDkuIHEgfCQhIPwkKCA8oCBqOKIgPKQgIfEgfKQgbDyoIGX8ZCEhvKwgInwoKSA8JS4gAzlnIDwkJCX8JC4j+W8gPGgnJLEhPGQoJBywoDhlJbxgICmxIfwsICE8YCApPCwgoDwkJ2j8ZCgosSA8JCAgfCQhIBX4aiA8KCojOO0guWcgE/wkICA55iB8JCAgOeYgcSE8JC9gPCQsI3xkJSA8JCZofGgoInmnIRP5pyB8bCAgHZ25qSA5aCC4a2A8LCAgOGtgPCwgIDhrYDwsICA5bCC5ZyAT/CQiI/wsICF8YCRj/GQmIfwkISA8JCJl/CwgIXYhvCQiYzEgvCQjITwkISAV/CQhILkvIFO8JCAgOeYgfCQgIDnmIHkgILkuIDwsIiA8LCEgFfxgISFyIHwkICa5oSA8KCEgE/wkIGn8KCwh/CQsIjwkbSe84CwgPCQuaHEgPGwhIHwkJiB5LiI8JCJjOGcgE7QgPGAgIHxgJCA8JCZovGgoIvwoIiE8KCIhE/utIDhkYDkuIDxkJSA8JCZn/KAqIvwkICA55iB8YCQiPCwgIXwoICA5ZyAT/OggJrIgfCRgJHhiIBP8pCEhuaoh/KAsIAa8aCYgPCQrZnxoIiI8JCtp8SL8JCAsPCQiobhnYDwkIqG5bCC8JCIgPGQoI/ksIDkqIDksIDkqIDwkISFxIbwkISF9ISBmfOwgIDwkIib8JCQgPGAgILzpIGZyIHwkISA5byA4K2A8JCAgPOAgIDwoKCD8aCgjvCQhIjksIF284CAgOqYguGIgOacgvCQgobwoIiE8rCAgOSwgfCghIDzgICA8LCEiPCQgobwkICAT/CQgoXwkaycTuiQgPGAkbbxkJiH8YCcgPGwganwkKiExITysICA8LCgguO8gvCRmJbhrIDEgPCQgIDxsICA8KCEhfCgiIHwkJGh8ZCcl/GAhIfwkIWJzIDwsICB5ZyAT+WcgE/EgPCwhITwkISA8JCJn/GwgIDvv7/rsILwoISI8KCIgfCQnaHIgciB8JGAkfCghIXwoIiB8JCRofGwpJ/vm7/IgfCRgJHwkKmM8JCFivCQgobwkICA8LCEhPCQgobwkICAT+WcgE/xgISCxILwoKyM8JCEh/CQgIDwoICA8JCJjOS8hPGQgIHkvIPwsIyH5ZyA5LCDT/CQgIHlnIBP8JCAgOeYgeGRgPCQgIDnmIHhkYDwkICA55iB4ZGA8JCAgOeYgeGRgOWcgE/wkICA55iB4ZGA8JCAgOeYgeGRgPCQgIDnmIHhkYDwkICA55iB4ZGA4ZGA8JCAgOeYgeGRgOGRgPCQgIDxkICA8JCAgPGAkbbxkJiH8JCAgPGQgIB24ZGA8JCAgPGAkbbxkJiH8ZCAgO2sgOa4gOucge20gPCQioXwl5iA8KCAgfCggIHxsKSd8LCIgMSE8KCAgfCghIXwoIiB8JCRofGQnJfwkICA55iB4ZGA8YCUhvCQgIDnmIHxgJSG5aCCyIDinYDwoIyA8LCAhPCghIbksIHksIHwsICE4rmA5LyB8KCAgPCQgYrkvIDhlYDwkIGf8KCAgEDpkILwkIGK5LyA5ZyA6JiE0ITEgNiG2IbisYDQhPGQgIfxkISH8KCIgFfYhtiG8YCRtvGQmIfxsKCJ8ZCYh/GAkIrwsICF8JCAgk/wkISD5LyB8LCEhPCQhIDwkImf8bCAgMSA5byA8pCAgOiYgvCQgZ/kvIFP8LCEhPCQhIDwkImf8bCAgPCghIPwkISA5LyA5ZyA8KCChuS8gMSB5LyA6JiF0ITEgPGAkY/xkJiH4p2A8LCAheGRgPCwhITwkISA8JCJn/GwgIDEgOWcgOGtgPCwgIDhrYDwsICA4pWA8bCAgPCQhIDxgJCI8ZCYh+S5gOGtgPCwgIDxgJCI8LCAheOpgNCE4ZGA8JCKhcSC8KCIgFfwkIqF8JCAgOS8guGRgPSPq7DEgeS8gOW8gPGwgIDinYBP8YCEgfCQgIBP8YCEhfCQhIDkvIDxgISF5LyE8LCBl+W8gPKwgIDEgOS8g/CQgZfwkJCAT/GAgIPkvIDEgOiYgvCQgaHlhIHwoIiAWfCggoXwlYSA8KCAgfOwgIDxkICA8ZCAgPGAkY/xkJiH4p2A8LCAheGRgPCwhITwkISA8JCJn/GwgIDwoISD8JCEgOS8gOiYgvCQgZ/kvIHlnIBP5ZyAT0/wsIiE8LCAhdCE5ZyAT/CgiITwoIiE8ZCYh/CQgIDwkICA55iB8LCAgeeYg/CwhITkvIPwoIiAV+KFgOGRgPSPq7DEgeS8gOW8gPGwgID0j6q88JCAgPGwgIBPxIDkvIPkvITwkIGX8JCQgPKwgIDxgISF8JCEgOS8gPGAiIDwoIGXZ/GAgIPwkICAT/CQgILkvIDwoIiE8KCIhE/ykICAT/CQioXEgvCQgobkvIDwoISD5LyC8KCChuS8gPCQgIDkvILwkIqFxILEgfCQgIBP8KCIgFfwkIqGxILykICA8JCAgOS8gvCggoXxgISF5LyE4q2A8KCYiMSA4KCI4KCI8JCChvCQgIBP9I+qvPCQgIDxsICA8JCChvCQgIBP4p2AT8SB5LyA5byA8rCAgPSPqZjwkICA8rCAgOiYgvCwkIDhjJTvm7/wkISA8JCZn/GgmIzwsJCI5ZyA8ZCCgPGgganygKCA8JCtociHyIfEh+KdgPCwkIbIgfKAqKPwoIiA8JCpofKAqJXwoaij8LCAh/CQiITgoIjwoICB8auEq+GtgPCwgIDwkISA8JCJl/CwgIXhrYDwsICAT/CgiIDwkJGh8qCwlk/hoIDxoJCA8JCCgPGQganwsIGn8JWEgPCQgIDxoICA4KiA4KCI8LCAgfCghIfwoIiB8JCZofKQrKPwoICB8JCKhfCXmIDokIPxkJiH7ZiD8YCQgPCwgZnxgJiI5ZyA4a2A8LCAgPCQhIDwkImX8LCAheWgguWcgE/wkISD8JCEgfCQgIPxq4SrxIDwkICB8auEq+WcgE/hrYDwsICA8JCEgPCQiZfwsICF5LiB8JCAgPGggIDwoISDdvGrhKvIgPCggIHxq4Sr8KCAgfGrhKvwoISA8auEq+GtgPCwgIDIgfCQhIDlvIDgrYDwkICB8auEq/CQhILwkISB5LyA8auEq/CghIPwkICW6JSB4K2AxIDwkICB8auEq/CggIHxq4Sr4a2A8LCAgOWcgE/hrYDwsICA4ZGA8JCAgOeYgfGrhKvloILloILloILloILloILloILloILtnIPwsJCC8ZCcl/CQhIDwkImh8ZCcgOGtgPCwgIDlnIBP8YCRj/GQmIfwkISF5LCBdvGggIDwoIiA8JCRofCwkIbwoISF8KCIgfCQkaHxkJyX8YCQgPCwgZnxgJiI5ZyAT8SA8JCChvCQgIDwkISD8JCEgfCQiZ9R8JCAkvCgiIFh8KCIgWHgrYDEgPCQgIHwsJCC8ZCcl+GtgPCwgIDlnIBPyIDwoICB5ZyAT+WgguWgguWgguWgguWgguGtgPCwgIDkuIHwoISByIDwoICB0IXxkJCBxIDEgPOxgJfwsISF8oCAh/CQqITxgJyA8YCYiPCggIDlnIDwsIiE5oSAxID0gYSS8LCEhfGAgILxoIKA8LCBmfGAgILwkIiA8YCQgPCQpaLwsICA8JCAg/GAlZHwkIGw8JCQgPCggbLxoICF8JCAgvCQoIPwkaCZ8JCEheeEg/CQhIXxgJCU8LCAhfCQgoTwkICA8JCEgPOwvJXzoICQ8LCUi/CwgIHkkYDwsISF8ZCAh/CggYzxsqCixIDEgHbEgHbpn73ksIDIgAAAAAAA4qiE8J+4gOSNkMSA4LCAAPCghIDMgAA=", 7 | }, 8 | italic: { 9 | 400: "xIDwkYCA0IDEgAAAyIAAxIDpuIAAAMSA6LSA5ICBAPSPvILur7jgoIDtqILloILloILloILloIIA5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCCAOWggu+/v+acgu6ogQAAAAAA6Ye+76iAAOO7vuSHvuO/vgAAAOSvogDjh6Lok6Lun6AA57Og56+gAO+Ln+KfoOKzneavnADouIMAAAAAAADpiIPpsIPskIPtqIPjoITkgIQAAAAAAAAA7biEAO+IhO+ghADvqITIhQAFANCFAOKYhgAAAAAAAAAAAOWogvKAgIjxi6Gj8JCCsvCQgrLzgLaz8YugoPCRoJgA84uFofKAgIjriKzriKzrjKzwm4ys64ys8JuMrOuIrOuIrOuMrPCbjKzrjKzwm4ys65mC8LCCufCwgrnxppCF5JGEAAAAAAAAAAAAAAAA7aiC4piCAO2oggDtqIIAAMK48bCAiNiA2IDYgNiA2IDYgNiA2IDYgNiA2IDYgNiA2IDYgNiA8aCEnPGggJ9o5oyD4YGA2IDYgO+Lv+W8gO+Lv+OEgPCwjIBf5LiB5LS/8bCEgBHjgIDwsIyAX+S4geCogOOAgOCogPCQhIBf5LiF46CA46CA46CA46CA46CA46CA46CA46CA46CA46CA46CA46CA46CA46CA44yA8pCAgPGggoDEh/CRlYDjhIDjvIDlnILkiIDmjILkiIDlnILkiIDlnILkiIDlnILkiIDlnILkiIDloILkiIDkqILkiIDloILkiIDloILEhPCQkY3hhYDkiIDloILwlLiA5LiA6YyC5YCA5aCCTuWIgO2ggeWIgO+8geWIgOawguWIgEzkuILigIDwsIyGTk7lpIDmjILlpIDlnILlpIDloILlpIDloILlvIDmjILlvIDlnILlvIDlnILlvIDloILlvIDloILlnILlvIDloILlvIDloILlvIDloILlvIDlnILlvIDlnILlvIDkqILlvIDloILwsIyB8JCVofCRmJfmsIDlpILmsIDloILmsIDkgILmsIDkuILmsIDkuILlvIDloILlvIDloILwsIyA8JCZn/CQmIPwkbSe4bSe55SA5oyC5byA5aCC8aCYgvGAgLjwoKiH8oCggcSK8aCgjfKwgIzmnILkuIPhkJXxkJSAX+S4geewgOaMguewgOWcguewgOWgguiAgOaMguiAgOWcguS0v+iAgPKAhInjgIDlvIDxgJSI54iA54iA8YCCgMSCD+iggOWggvCUuIDotIDmjILotIDlnILotIDlnILotIDlnILotIDlnILotIDkqILotIDloILEhemUgOaMgumUgOWggumUgOSogumUgOWggumUgOWggui0gOWggui0gOWggui0gOWcgui0gOWggvCghIbzgICAyIHkuIPqhIDmjILqhIDlnILqhIDlnILqhIDkqILwkIWM5qyA8JCFjOS4guqcgOaMguqcgOWcguqcgOWcguqcgOWgguqcgOSoguqcgOWgguqcgOWgguqcgOWgguS4g+uAgOaMguuAgOWcguuAgOWcgvCgmIPxgJCB8JCZofGgoIzrkIDmoILrkIDlsILrkIDlsILrkIDlsILlsILrkIDlsILrkIDlsILrkIDlsILrkIDltILrkIDlsILrkIDlsILrkIDlsILlsILrkIDlsILrkIDlsILrkIDlsILrkIDltILrkIDlsILrkIDkvILrkIDltILrkIDltILwoKCF8LCAqPGwnILEhvGgoIrsuIDmnILsuIDlsILxsISA8JCcgBHxgJCB8JCZofCShKLwkISB8JCRn8SEaPCQhIXwsIyA8JCdofGwgoDEh03xkISI44CA7KiB74yA4pCA4pCAxINO75yA54iC75yA5piC75yA5piC75yA5pyC75yA5pyC75yA5pyC5piC75yA5aSC75yA5pyC75yA5pyC8JCQgO+cgOacgtCB6rSC5LS75LiD44yA5LiD45SA5oyA8LCMgPCQkZ/gpIHinILgpIHkuILgpIHkoILIgeGpgPSEgZnwsIyA8JCFoeGdgOOAgOKYgOOAgOS0uU3jgIDEhPGgoJvxgJCB8JCZofGgoInikIHlpILikIHlnILikIHkgILikIHkuILikIHkuILhnIHloILhnIHloILwsIyA8JCZn/CQmIPwkbyg8YCEieaEgOK0geaMguGcgeWggvGAkILxoKCPxIFM5IyA5IyA5IyA8JGNgPGwgIjwoIiA8JClovKAgb7xsIKA8JGlgOOggeWgguOggeWkguS0ueS4h+S0uOS4g/GAlIbkvIDygJyCTuSRgPCghIDkgIHpkILwlLiAxILEhvCRjYDmnIDEgU7kuIDwsIyGTuS4gOS4gOS4gOS4gOCsgE7ktLvkuIXiuIDiiYDiuIDiuIDiuIDxoISJ8aCYgfGQmIHwoISF8KCIgfCQkaHxoKCbxIDhqIDwkISAX07nhIHmjILwkICX8KCIgF/kuITilIDilIDmrIJO8KCIgPCQoaHiuIDwkIWM56SB5ZyC5YCA5YCA5LSbDeKAgOOEgOOIgOOAgOeIgOeogPCQhYzkuILnqIDcgvGwnILwkJGh8bCIi++zv+WwgMiATuaAgPCgiITkuIDjiIDzkLyT5pyFxIPgsIzwoICg8JCZjcSC8aCYgPCQoaHxgIKAxIjwkZ2A5IiA5IiA5LiATvGAgKHxgICp4LCA87GBjuS0m+S4h+qcgMSG8aCgjuuQgMSG8aCgieOEgMSG85GAkuqIgeiEgvCQgJfwoIiAX+2ggO2ggE7wlLiA8KCIgMSF8JGFgOqogeWgguqwgeaMguS0mw3igIDwsIyGTsSC5LiD44iA5ZiA5byB5byB8pCkgvCQlaHluIHkuIDhmIDkuIXmgIDUgfCgiIDwkJGgxITguYDmoITyoICL5pyFxIPgrIvwoICg8KCAq/GwgILEguOggfGAgoDEiPCRnYDxgIKAxIjwkZ2A75yA5pyC75yA5pyC0IHqtILxgIGoTvCgoILxoJiB8JChofGAlIbxgISH5byA85CAluS0m+S4ie+QgcSG8aCgjtiA44SA5IiA2IDwoISJ5LCB8YCUgOOggOKkgOOEgPCQuI/kiIDlgIBO4oCA4riA4pCA8JCEgV/QhOOIgOOAgOCsgOW8gPOggIDniIDmrIDvs7/jnIDwkJiC5LiH8YCUgNiA66+/66+/6rO+66+/6rO+7L++64e/6ou+7bu/47SA5qyAxILwoISB86CAgPGwnIDwkKWi8LCMgPCQhaHhrYDllIDwsIyA8JCRn+qwgU7mnIDwoIiAYeGcgfGAlIbwoIiAYeGRgOWUgPGAlIbwoIiB8JCRouS4hfCgiIHwkJmi5LiH8JCEgfOhgJPllIDvqIHlpILvqIHklIHloILklIHgqILrkIDvmIHwkIWM8YCUgMyB8LCAgPKQgIvwoIiB8JCZofCgjITEgk7jgIDogIPIgPGwgb7EiPCwkIbwsIyB8JCVofCRmJfwkICY8LCEhvCwiIDwsIyB8JCRofCRmJfjhILyoICA6ICD8KCIgPCQmaLwoIiA8JCZovCwkIbwoIKA8KCQgPCQgJjwsISG8LCIgOK4gOGZgOS4gcSB4riA8JC8gsSB8oCkgMSB4YyT8auEq/KQgbDxkISG8KCkgPKwgInwoKSA8rCAifCQtILwsJCG8JS4gPCQgIAKxITxkJyX8LCAgvCQkoTwkLiO8JCEgvGQmILlvIDxkICSwoDxgICmxIfwsICE8YCApPCwgoDwkJ2j8ZCgoeO0gvCQhIDwkImh8ZCcgOGcgPCQrIDwoKiM4a2A8LCAgPCQgIDnmIHwkICA55iB8JS4gPCQsI3xkJSA8JCZofGgoInmnITilYDwoISA8bCAgOWcgE/muIDxkICA6ZiA8ZCAgOmMgOWgguWcgE/hrYDwsICA5ZyAT+GtgPCwgIDwkIiP8LCAhfCQhIDwkImX8LCAhfGAkY/xkJiH8JCJjMSC8JCMhNiG8JCEguS8gfCQhIBX8aCZjvKApIvlvIDwkIyB5LCB8JCMgeSwgfKAgobyoKiA8JCZn/GAhIXomITwoICAyIHwoICA8LCIgfCwgIDnmIPwkIGo8JS4gOG8gPCQmIHjpIBO0IDxgICB8aCgi/CQhIPysICA7YyA4ZGA46SC1IDxkICB8JCAgOeYgfGAkIjwsICF4pCA5pyB8rCAgOakhfOxgZHkmIDxsICA8qCEvvGQgobxsIGp8KCBqfCgqIBR8YCChvCQhIrhnYDwkICw8JCKhuGdgMSB5LyAxIEO5bCC8ZCgj+KxgNCExIHkvIDlnIDlnIDhqIDwkJmoEPCggKnogIPwkJCADsSA8JCChvCQgIDwkISH5LCBdvGrhKvQg/GAgILwkISF5LCBdvGggIDzj7qv6piC4qyA5pyC8JCChvCgiITysICATOS8g+OwgOW8gPKwgIDMgcSC8LCAhfGgoI/xgJyA8YCcgPGgoI/UgcSEDvCwoILwkLCN2IDwsJCA5pyB4ZiWxIDwkICA8bCAgPCggIHpvILwsIyA8JCRn/KAqIvlnIBP5ZyAT8SA8LCEhPCQhIDwkImf8bCAgOS8hADIgciB8JGAkfCghIXwoIiB8JCRofGwpJ/vm7/IgfCRgJHwkKmM8JCFivCQgobwkICA8LCEhPCQgobwkICAT+WcgE/xgISCxILwoKyM8JCEh/CQgIDwoICA8JCJjOS8hOiYgvCwgZ/wkISH8JCAgPCQiYzkvIThrYDwsICA4ZGA8JCAgOeYgeGRgPCQgIDnmIHhkYDwkICA55iB4ZGA8JCAgOeYgeGtgPCwgIDhkYDwkICA55iB4ZGA8JCAgOeYgeGRgPCQgIDnmIHhkYDwkICA55iB8JCAgOeYgeGRgPCQgIDnmIHwkICA55iB4amAduKdgPCwgIXhqYB28ZCAgPCQgIDnmIHinYDwsICFduKkgPCghIXwoIiB8JCRofGwpJ/woISG8KCIgfCQlaHxsKSQ8JCMhOSwg/CghIDyo4CE8KCEifCgiIHwkKGh8bCkn/CggIHjsIDwkKSB5LCBxIDwkICA55iByIDinYDwoIyA8LCAhOGRgOKdgPCgjIDwsICE77+/8YCUhsiA8KCAgOS8gciA8KCAgPGAlIDMgPOPu7TEgXbEgMK3yIDlnIDEgOWggsiA5aCCxIF247OE4YyDw4DhlYDwkIGf6JiC8LCEgPGgmIzxsKCJ8KCEhvCwgIXwkISD8aCIg/GgmIzxkICH8YCQivCwgIXxkJiH8JCChvCgiITysICA8JCChuS8gPCQhIBXxIDwkICBT/CghIPwkISA5LyAxIHwkICAT+WcgPCQiobEgvKQgIDEgeS8gMSA8JCAgU/EgPCQgobkvIDomILwkIGf5LyB8YCEhfCQhIDkvIDomITwoISA5LyD8JCEgPCQiZfwsICF8JCAgPGAkbbxkJiH8JCAgOeYgcSA8JCAgU/woISD8JCEgOS8gOiYgvCQgZ/kvIHlnIBP5ZyAT+acgk/wsIiE8LCAhdCE5ZyAT/CgiITwoIiE8ZCYh/CQgIDwkICA55iB8LCEhOS8g/CghIPkvILwsICB55iD4oWA8JCAgOeYgfCQgobwkICAT/SPqorwkICA8bCAgOW8gPOQgIDwoISA8YCIgPCggZdn8YCAg/CQgIBP5LyE8JCBl/CQkIDysICA8LCBl/CghIPwkISA5YSA5aSA8KCEg+WEgvCQhYx2dnbwkISA8JCJl/CwgIXwkICA8YCRtvGQmIfwkICA55iBxIDwkICBT8SA5ZyA4a2A8LCAgOGtgPCwgIDEgPGwgIDwkISA8YCQiPGQmIfkuYDhrYDwsICA8YCQiPCwgIXjqYDQhOGRgPCQioXwkIqFxILwoISD5LyC8JCAgOS8gvCQgIDnmIHwkIKG8JCAgE/inYBPxIDzkICA8JCBl/CQkIBP8KCEgPGAhIXkvITwsIGX5byA8rCAgPCggZfwkISD8rCAgPCQhIPxgJCA8JCJl0/ykICAxIHkvIDwsISE5LyD8KCIgFfihYDEgeS8gPCwhITkvIPlvIDykICA8KCEg+S8gk/ihYDwkIqF4KCI8JCAgvGAkIBX8JCAgPCQgoXkvITwkIiD8JCAgk/wsIyG8JCEgMSB5LyA5byA8bCAgOKdgE/EgeS8gOW8gPGwgID0j6qK8JCAgPGwgIDwsIKG8JCBp/CQgIBP44WA8JCChk/EgPCQgIHwoICD5KSB8JCBn/GQgoDxoIGp8oCggPCQraHIh/CgnIXIh/CgnIXxoJiB8JCdofCSjKTiqYDwkIiB84CxjvOQuI/wsJCG8bOcpvCghIvwoIiB8JCpofCQgb7xkIGp8KCAgfCghIvwoIiB8JCpofKAqKHwoaSi8pCokvCggIHxq4Sr4ZGA8YCRj/GQmIfwkICA55iByIDwoICBxIHkvIDvi7/xoJCA8JCEheSwgXblhIDwkIGK5LyA8JCIgPCwgIHwoISH8KCIgfCQmaHykKyj8KCAgfCQioXwl5iA6JCD8ZCYh9CA8KCQgPCwgoDxq4Sr5LiA47iC8JCAgOeYgfGrhKvwsICF5aCC8JCAgOeYgfGrhKvwsJCC8ZCcl/CQhIDwkImh8ZCcgOGRgPCQgIDnmIHxq4Sr8YCRj/GQmIflvIDwkISF5LCBdvGrhKvxoICAyIDwoICB8KCChfCwkIbwoISF8KCIgfCQkaHxkJyX0IDwoJCA8LCCgPGrhKvhrYDwsICAxIDwkIKG8JCAgPCQhIPwoISD5YSCxIDwkICB8auEq/CggIHxq4Sr8JCAgOeYgfGrhKvwkIKF6JSB8KCChfCVgIDlnIBP4ZGA8JCAgOeYgfGrhKvloILloILloILloILloILloILloILtnIPwsJCC8ZCcl/CQhIDwkImh8ZCcgOGRgPCQgIDnmIHxgJGP8ZCYh/CQhIXksIF28aCAgMiA8KCAgfCgiIDwkJGi8JC9gPCggIHxgJCA8YCYieGtgPCwgIDEgPCQgobwkICA8JCEg/CgiIBZ8JCEguiUgeeYgPCgiIFh8rCAgMSA8JCAgfCwkILxkJyX4ZGA8JCCheiUgfCwjIBZ84SBmfCgiIDwkI2g8JC1gMiA8KCAgfCgiIDwkJGi8JC9gOWcgE/loILloILloILloILloILloILwkICA55iB8auEq+S4gfCwjIDijYDwoIiA8JCRosiA8KCAgfCwgIDwsICAxIDgvJDxgJyA8KCQgPGAmIjwoICA5ZyA8LCIhOaEgMSA9IGEkvCwhIXxgIGp8KCkgvCgkIDxgICB8JGomtCF8YCEgfCQgIPQhfGQiIHxkIiBxID0gYSX8KCBsvGggIXwkICC8JCgg/CRqJvksIHQhOe4g9CE8JCChPCQgIDwkISA87C8lfOggJDwkIyA8LCAgfGAmIzwsICB5JGA8LCEhfGQgIfwoIGM8ZCAgMSA8ZCAgOSwgMiA75+/AAAAAOKohPCfuIDkjZDEgOCwgADwoICCzIAA", 10 | 600: "xIDwkYCA0IDEgAAAyIAAxIDpuIAAAMSA6LSA5ICBAPSPvILur7jgoIDtqILloILloILloIIA5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCC5aCCAOWggu+/v+acgu6ogQAAAAAA6Ye+76iAAOO7vuSHvuO/vgAAAOSvogDjh6Lok6Lun6AA57Og56+gAO+Ln+KfoOKzneavnADouIMAAAAAAADpiIPpsIPskIPtqIPjoITkgIQAAAAAAAAA7biEAO+IhO+ghADvqITIhQAFANCFAOKYhgAAAAAAAAAAAOWogvKAgIjxi6Gj8JCCsvCQgrLzgLaz8YugoPCRoJgA84uFofKAgIjriKzriKzrjKzwm4ys64ys8JuMrOuIrOuIrOuMrPCbjKzrjKzwm4ys65mC8LCCufCwgrnxppCF5JGEAAAAAAAAAAAAAAAA7aiC4piCAO2oggDtqIIAAO2uuvGwgIjut7/ut7/ut7/ut7/ut7/ut7/ut7/ut7/ut7/ut7/ut7/ut7/ut7/ut7/ut7/ut7/xoIScY+S4ge63v+63v+2Tv+W8gOGggOaQgvGAkIBf5LiCxIbEhvCRjYDnuIDykISK8JCCgOe4gPKQhIrhrIDlsILhrIDlsILwkJyB5LiD4YyA4KiA4pCA4pSA56CC4pSA5rCC4pSA5qyC4pSA5qyC4pSA5rCC4pSA6aCC5qyC4pSA5rCC4pSA5rCC4pSA5rCC4pSA5qyC4pSA5qyC4pSA6aCC4pSA5biC4pSA5rCC4pSA5rCC5LiF4pSA5rCC8JCEgF9O8YCQgPCQnaHhmIDjvIDlnILkiIDmkILkiIDloILkiIDlnILkiIDlnILkiIDlnILkiIDloILkiIDkqILkiIDloILkiIDloILikIDwoIiA8JCVouS4gOmMguWAgOWggk7liIDuiIHliIDgoILliIDmsILliIBM5LiCxIHkuIJO5aSA5pCC5aSA5ZyC5aSA5aCC4YCAxITygKib5byA5pCC5byA5aCC5byA5ZyC5byA5aCC5byA5aCC5ZyC5byA5aCC5byA5aCC5byA5aCC5byA5ZyC5byA5ZyC5byA5KiC5byA5aCC8oCojvGAkIDwkIWh5oSA8JGkgfGgmILygJiA8YCCgPOAgI3hqIDhlJbxkJSAX+S4geewgOaQguewgOWcguewgOWggvCgiIDwkJmi8KCIgPCQmaLwoIiA8JCZouiAgOaQguiAgOWcgvCQnIjmpIHykISK5qSB8pCEiuakgfKQhIrhgIDxgJCAX8SHxILkuIPlpIDwkJiB5LiH5aSA8ZCBvsSC6KCA5aCC8JS4gPGAlIjplIDmkILplIDloILplIDkqILplIDloILplIDloILotIDloILotIDloILotIDlnILotIDloILiiIDIgeS4g+qEgOaQguqEgOWcguqEgOWcguqEgOSogvCQhYzlnIDwkIWM5LiC5ZyA5ZyA5ZyA5ZyA5ZyA5ZyA5ZyA4oCA4omA4oCA4oCA4oCA8JGAgfGAkIHwkZCV8JKAgfCgoIXEiOysgOaQgsiGxIbnuIDykISK8JCCgOe4gPKQhIrwkYiB8YCQgfCRmJfhmIDwsIyB8JCVofCSiKPwkZiGxITykISH8JCEhWjwlLiA8ZCYjcSH8ZCEiO6wgOaEgu6wgOWQgu6wgOWUgu6wgOWQgu6wgOWUgu6wgOWUguSwgU7hmIDQgO+cgOawgvKQgIDhqIDhqIDhqIDhqIDhqIDhqIDhqIDhqIDxsICqxIrhqIDhpIDkuIPhpIDkrIDwsIyA8JCRn+CkgeSYguCkgeawguCkgeeAgvGAiIPIgeGpgPSEgZnwsIyA8JCFoeGdgOGYgOGYgOGYgOS0uU3hmIDEhPKAqJ3EheaEgPKdkIHitIHmkILhnIHloILIi/GAkILwkqCp8ZCEgyHksILwkIiB8YCQgWHxgJCBYeGYl+KYgMSB4LWA4piA4piA8JGNgPCggbLygIGp8aCBsvCggoDygIGp8aCCgPCggoDygIGp8JGtgOS0uOS4g/GAlIbwkJqF46SA8YCVjvCwgJbkgIHpkILwlLiAxILktIHlgILktIHkjILktIHjmILktIHkkILktIHkkILklIHloILklIHloILklIHloILklIHloILwoISG84CAgPCghILlpIHmkILlpIHlnILlpIHlnILlpIHkqILwkIWM4aiA4omA4aiA4aiA4aiA8aCEifGgmIHxkJiB8KCAge63v+GYgPCQhIBf5LiD4aiA8JCEgF9O54SB5pCC8JCAl/CgiIBf5LiE4pCA4pCA7bu/yILxgIGyxIjwoIiA8JChovCwkIbhgIDwkIWM56SB5aCC5YCA5YCA5LSbDeWggOO8gOW8gPCgiIBf5LiB56SA4ayA6KCA6JSB5aCC4aCY7qO/5IiAyIBO5oCA77O/5LiF5oCA1IHwoIiA8JCRoMSE4LmA5qCE84CAjeOAgOS4g8SD4LCM8bCAgvCQmY3EhsSC8aCYgPCQoaHwoIiA8JChouSIgOSIgOS4gE7xgICh8YCAqe+nv/OxgY7lnIDrkIDEhvGgoInxgJCAX+S4guqIgeiIgvCQgJfwoIiAX+Wggu2fv8iC8YCBssSI8KCIgPCQoaLwsJCGyIXhpIDEguS4g+GkgE3zlIGZ9ICAgO+7v/CwjIZO4ZiA4ZyB8KCIgF/kuIHjhIHsuIDEguS4g+Wggu67v+OEgMiATuS4gPCgiITkuIDhqIDysLSQ4ZSA5LiD77O/8KCAoPCQmY3vs7/joIHxgIKAxIjwkZ2A8KCIgPCQoaLvnIDmsILvnIDmrILQgeucgvGAgahO8KCggvGgmIHwkKGh8YCUhtiA86C9jsSA4aiA54SB8LCAhOKUgOuAgOO8gPCgiIHwkJmh5IiA5YCATuWggOWkgOW8gOiEgeekgPCQhIBf5LiD5aSA5ZyA7qO/4oCA8JCYguS4h9iA8ZCQgfCQiY3wkImN4YGAxIDhg7/ilIDqi77jvIDkiIDlvIDqnIDunIHsg77kiIDlnILqnIDlnILkuITwoISB9ICAgEviiYDwsIyA8JCFoeGtgOO4gPCwjIDwkJGf6rCB8YCQgPCQiZ/kmIDwsIGy5oSA4ZyB8YCUhvGAkIBh4ZSW8KCIgGHmhIDjuIDxgJSG5JSB6piAyIXvqIHjuIDjuIDgvITklIHgvIThnIHzkLSE76CB5aiA5LyC8JGMhPCQgobmhIDlrIDwkJCB8qCpj/KwsI3wkYiT8KCMhMSCTuGogOiAg8iA8KCIgPCQoaLwoIiA8JChovCwkIbwsIyB8JCVofCRlJbwkICY5JyA4ZiX8Je0lvCwjIHwkJGh8JGom+OAguOIgsSE8aCgm/CgjITEgk7kuITEhsSGxIbwsJCGyIDgtYDwkICY4YiT4aiA4aiA8pCAh/GwgoXykICH8pCBsPGQhIbwoKSA8rCAifCgpIDysICJ8JC0gvCwkIbwlLiA8JCAgArEhPGQnJfwsICC8JCShPCQuI7wkISC8ZCYguW8gPGQgJLEhPGQoJBy4ZCV8YCAp/CwgoDwkJ2o8LCAhOO0gvCQhIDwkImh8ZCcgPGQhIbwkIGJ47SC5ZyAT+GRgOGRgMSE8JC9gPCQjIDxkJSA8JCZn/GwpIrlpIBP4pWA8KCEgPGwgIDlnIBP5riA8ZCAgOmYgPGQgIDpjIDloILlnIBP4a2A8LCAgOWcgE/hrYDwsICA8JCIj/CwgIXwkISA8JCJl/CwgIXxgJGP8ZCYh/CQiYzEgvCQjITYhvCQhILkvIHwkISAV/CQhIPwkImf2IbwkISB5LiB8JCAgOeYgfCQgIDnmIHkgILkuIDksILnmIPxgISF8KCAgMiB8KCAgPCwiIHwsICA55iDzIDwkaSB8oCogPKAqIDwkbSexI7wko2A75+/8JCcgeG8gPCQmIHipIBO47SC8YCQgPCQmaLwkIG+8YCBqdCA8YCAgfCQhIPysICA65SA4ZGA46SC1IDxkICB56CA4ZGA47WA8ZCYh+WcgE/mpIXzsYGR5JiAxIDxsICA8ZCChvGwganwoIGp8KCogFHxgIKG8JCEiuGdgPCQiobwkIqGxIHlhIDwkJSA86SBmfKwgIDxgJSJ8JCEguGogPCQhIBX4ZSA8JCZqBDIgfCQhIDlvIDgrYDlnIDwkIiC5LyB0IPxgICC8JCEheSwgXbxoICA84+5te+/v+qYggDlnIBP8JCAgk9P4ayA5byA8rCAgMyB8YCQgPCQiZ/knIDxgJG28ZCYh/GAnIDxsIGp8JCohNSBxIQO8LCggvCQsI3vs7/wsJCA7re/AOKdgE/woICB6byC8LCMgPCQkZ/ygKiL5ZyAT+WcgE/EgPCwhITwkISA8JCJn/GwgIDvv7/rsILwkIG+8JGAkciB8JGAkfCghIXwoIiB8JCRofGwpJ/vm7/IgfCRgJHwkKmM8JCFivCQgobwkICA8LCEhPCQgobwkICAT+WcgE/xgISCxILwoKyM8JCEh/CQgIDwoICA8JCJjOS8hPGAiIbyoKmP8pCAjOiYgvCwgZ/wkISH8JCAgPCQiYzkvIThrYDwsICA4ZGA8JCAgOeYgeGRgPCQgIDnmIHhkYDwkICA55iB4ZGA8JCAgOeYgeGtgPCwgIDhkYDwkICA55iB4ZGA8JCAgOeYgeGRgPCQgIDnmIHhkYDwkICA55iB8JCAgOeYgeGRgPCQgIDnmIHwkICA55iB4amAduKdgPCwgIXhqYB28ZCAgPCQgIDnmIHinYDwsICFduKkgPCghIXwoIiB8JCRofGwpJ/woISG8KCIgfCQlaHxsKSQ8JCMhOSwg/CghIDyo4CE8KCEifCgiIHwkKGh8bCkn/CggIHioIDwkKSB5LCBxIDwkICA55iByIDinYDwoIyA8LCAhOGRgOKdgPCgjIDwsICE77+/8YCUhsiA8KCAgOS8gciA8KCAgPGAlIDMgPOPu7TEgXbEgMK3yIDlnIDEgOWggsiA5aCCxIF247OE4YyDw4DhlYDwkIGf6JiC8LCEgPGgmIzxsKCJ8KCEhvCwgIXwkISD8aCIg/GgmIzxkICH8YCQivCwgIXxkJiH8JCChvCgiITysICA8JCChuS8gPCQhIBXxIDwkICBT/CghIPwkISA5LyAxIHwkICAT+WcgPCQiobEgvKQgIDEgeS8gMSA8JCAgU/EgPCQgobkvIDomILwkIGf5LyB8YCEhfCQhIDkvIDomITwoISA5LyD8JCEgPCQiZfwsICF8JCAgPGAkbbxkJiH8JCAgOeYgcSA8JCAgU/woISD8JCEgOS8gOiYgvCQgZ/kvIHlnIBP5ZyAT+acgk/wsIiE8LCAhdCE5ZyAT/CgiITwoIiE8ZCYh/CQgIDwkICA55iB8LCEhOS8g/CghIPkvILwsICB55iD4oWA8JCAgOeYgfCQgobwkICAT/SPqorwkICA8bCAgOW8gPOQgIDwoISA8YCIgPCggZdn8YCAg/CQgIBP5LyE8JCBl/CQkIDysICA8LCBl/CghIPwkISA5YSA5aSA8KCEg+WEgvCQhYx2dnbwkISA8JCJl/CwgIXwkICA8YCRtvGQmIfwkICA55iBxIDwkICBT8SA5ZyA4a2A8LCAgOGtgPCwgIDEgPGwgIDwkISA8YCQiPGQmIfkuYDhrYDwsICA8YCQiPCwgIXjqYDQhOGRgPCQioXwkIqFxILwoISD5LyC8JCAgOS8gvCQgIDnmIHwkIKG8JCAgE/inYBPxIDzkICA8JCBl/CQkIBP8KCEgPGAhIXkvITwsIGX5byA8rCAgPCggZfwkISD8rCAgPCQhIPxgJCA8JCJl0/ykICAxIHkvIDwsISE5LyD8KCIgFfihYDEgeS8gPCwhITkvIPlvIDykICA8KCEg+S8gk/ihYDwkIqF4KCI8JCAgvGAkIBX8JCAgPCQgoXkvITwkIiD8JCAgk/wsIyG8JCEgMSB5LyA5byA8bCAgOKdgE/EgeS8gOW8gPGwgID0j6qK8JCAgPGwgIDwsIKG8JCBp/CQgIBP44WA8JCChk/vm7/wkISA8JCZn/GgmIzwsJCI5ZyA8oCggPCQraHygKiOyIfIh8SH8rCAjfCQmILwsJCG8KCEi/CgiIHwkKmh8JCBvvGQganwoICB8KCEi/CgiIHwkKmh8oCoofCQnIDygKGP8bCAifCggIHxq4Sr4ZGA8YCRj/GQmIfwkICA55iB8JG0g8SB5LyA8JC4jsKA8ZCBqfCQgIDxoICA4KiA4KCI8LCAgfCghIfwoIiB8JCZofKQrKPwoICB8JCKhfCXmIDokIPxkJiH8KCEgPGrhKvwkICACuGRgPGAkY/xkJiH4ZGA8JCEg/CQhIHwkICD8auEq8SA8JCAgfGrhKvlnIBP4a2A8LCAgPCQhIDwkImX8LCAheS4geWcgPCQiILkvIHwoISD8KCIgPCQhZdP8auEq/CgiIDwkJGiyIDwoICB8KCEhfCgiIHwkJGh8ZCcl/CghIDxq4Sr4a2A8LCAgMSA8JCChvCQgIDwkICB8auEq/CQhILwkISB5LyA8auEq/CghIPwkICW6JSB4K2AxIDwkICB8auEq/CggIHxq4Sr4a2A8LCAgPCQjIDxgJCAWPCQgoXolIHgtYDIgPCggIHlnIBP4ZGA8JCAgOeYgfGrhKvloILloILloILloILloILloILloILtnIPwsJCC8ZCcl/CQhIDwkImh8ZCcgOGtgPCwgIDlnIBP8YCRj/GQmIfwkISF5LCB8aCAgMiA8KCAgfCgiIDwkJGi8JC9gPCggIHxgJCA8LCBmfGAmIjlnIBPyIHwkISA5byA4K2A8JCEg/CQhIHwkImf8KCIgFnwkISC8JCEgeS8gPCQhIDwkImh8ZCcgPCQhIPwkISB8JCAg+WcgE/xgISF5aCA8KCBsPCgiIDwkI2g8JC1gPCgiIDwkJGiyIDwoICB4a2A8LCAgOWgguWgguWgguWgguWgguGtgPCwgIDkuIHjoYDwoIiA8JCRosiA8KCAgeG1gPCwhIHogITEgPCghIfEgMSA4YCR8KCogvGwhInwoKiC8YCcgPCggIDQhPGAkY/wsICFxID0gYSS8JCAg/GAmIDwoKSC8YCYiPCwhIXxgISB87CAnvCgqILxsISJ8LCBsvGAnIDwoISA8LCUhuWcgvGQmIflnILxkJiH4omA8KCMgfSBgJXzsICR8JCAg/GAmIDwoKSC8YCYgPCQiIDxgJiA8LCUhvCwgIHkkYDwsISF8ZCAh/CggYzxsqCixIDEgHbEgHbpn73IgO+fvwAAAADiqITwn7iA5I2QxIDgsIAA8KCAgsyAAA==", 11 | }, 12 | } as const; 13 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import "hardhat-deploy"; 2 | import "hardhat-deploy-ethers"; 3 | import "hardhat-contract-sizer"; 4 | import "hardhat-gas-reporter"; 5 | import "@typechain/hardhat"; 6 | import "@nomiclabs/hardhat-ethers"; 7 | import "@nomiclabs/hardhat-waffle"; 8 | import "@nomiclabs/hardhat-etherscan"; 9 | 10 | const coinmarketcapApiKey = process.env.COINMARKETCAP_API_KEY; 11 | 12 | module.exports = { 13 | solidity: { 14 | version: "0.8.14", 15 | settings: { 16 | optimizer: { 17 | enabled: true, 18 | runs: 1000, 19 | }, 20 | }, 21 | }, 22 | paths: { 23 | sources: "./contracts", 24 | cache: "./cache", 25 | artifacts: "./artifacts", 26 | }, 27 | mocha: { 28 | timeout: 6000000, 29 | }, 30 | typechain: { 31 | outDir: "typechain-types", 32 | target: "ethers-v5", 33 | alwaysGenerateOverloads: false, 34 | }, 35 | gasReporter: { 36 | currency: "USD", 37 | gasPrice: 20, 38 | coinmarketcap: coinmarketcapApiKey, 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typeface", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "compile": "npx hardhat compile", 7 | "test": "npx hardhat test" 8 | }, 9 | "dependencies": { 10 | "@nomiclabs/hardhat-ethers": "^2.1.1", 11 | "@nomiclabs/hardhat-waffle": "^2.0.3", 12 | "@typechain/ethers-v5": "^9.0.0", 13 | "@typechain/hardhat": "^5.0.0", 14 | "@types/chai": "^4.3.0", 15 | "@types/mocha": "^9.1.0", 16 | "@types/node": "^17.0.21", 17 | "chai": "^4.3.6", 18 | "ethereum-waffle": "^3.4.0", 19 | "ethereumjs-wallet": "^1.0.1", 20 | "ethers": "^5.4.0", 21 | "ts-node": "^10.7.0", 22 | "typechain": "^7.0.1", 23 | "typescript": "^4.6.2" 24 | }, 25 | "devDependencies": { 26 | "@nomiclabs/hardhat-etherscan": "^3.1.0", 27 | "@openzeppelin/contracts": "4.5.0", 28 | "chalk": "^4.1.1", 29 | "fs": "^0.0.1-security", 30 | "hardhat": "^2.10.1", 31 | "hardhat-contract-sizer": "^2.5.1", 32 | "hardhat-deploy": "^0.8.9", 33 | "hardhat-deploy-ethers": "^0.3.0-beta.10", 34 | "hardhat-gas-reporter": "^1.0.8", 35 | "sol-merger": "^3.1.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/TestTypeface.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import chalk from "chalk"; 3 | import { Contract, Signer } from "ethers"; 4 | import * as fs from "fs"; 5 | import { ethers } from "hardhat"; 6 | import { Typeface } from "../typechain-types"; 7 | 8 | import { fontHashes, fonts, fontSources, wallets } from "./utils"; 9 | 10 | export let typeface: Typeface; 11 | 12 | export const typefaceContract = (signer?: Signer) => 13 | new Contract( 14 | typeface.address, 15 | JSON.parse( 16 | fs 17 | .readFileSync( 18 | "./artifacts/contracts/examples/TestTypeface.sol/TestTypeface.json" 19 | ) 20 | .toString() 21 | ).abi, 22 | signer ?? ethers.provider 23 | ) as Typeface; 24 | 25 | export async function deployTypeface() { 26 | const { donationAddress } = await wallets(); 27 | 28 | const TestTypeface = await ethers.getContractFactory("TestTypeface"); 29 | const testTypeface = (await TestTypeface.deploy( 30 | fonts, 31 | fontHashes, 32 | donationAddress.address 33 | )) as Typeface; 34 | 35 | console.log("Deployed TestTypeface " + chalk.magenta(testTypeface.address)); 36 | 37 | return testTypeface; 38 | } 39 | 40 | describe("TestTypeface", async () => { 41 | before(async () => { 42 | typeface = await deployTypeface(); 43 | }); 44 | 45 | it("Should return correct font hashes", async () => { 46 | const { rando } = await wallets(); 47 | 48 | for (let i = 0; i < fonts.length; i++) { 49 | expect(await typefaceContract(rando).sourceHash(fonts[i])).to.equal( 50 | fontHashes[i] 51 | ); 52 | } 53 | }); 54 | 55 | it("Should return correct font name", async () => { 56 | const { rando } = await wallets(); 57 | 58 | for (let i = 0; i < fonts.length; i++) { 59 | expect(await typefaceContract(rando).name()).to.equal("TestTypeface"); 60 | } 61 | }); 62 | 63 | it("Should return false hasSource() for all fonts", async () => { 64 | const { rando } = await wallets(); 65 | 66 | for (let i = 0; i < fonts.length; i++) { 67 | const font = fonts[i]; 68 | 69 | return expect(await typefaceContract(rando).hasSource(font)).to.be.false; 70 | } 71 | }); 72 | 73 | it("Store font source with invalid weight should revert", async () => { 74 | const { rando } = await wallets(); 75 | 76 | return expect( 77 | typefaceContract(rando).setSource( 78 | { 79 | ...fonts[0], 80 | weight: 69, 81 | }, 82 | fontSources[0] 83 | ) 84 | ).to.be.revertedWith("Typeface: Invalid font"); 85 | }); 86 | 87 | it("Store font source with invalid style should revert", async () => { 88 | const { rando } = await wallets(); 89 | 90 | return expect( 91 | typefaceContract(rando).setSource( 92 | { 93 | ...fonts[0], 94 | style: "asdf", 95 | }, 96 | fontSources[0] 97 | ) 98 | ).to.be.revertedWith("Typeface: Invalid font"); 99 | }); 100 | 101 | it("Store font source with invalid source should revert", async () => { 102 | const { rando } = await wallets(); 103 | 104 | return expect( 105 | typefaceContract(rando).setSource(fonts[0], fontSources[2]) 106 | ).to.be.revertedWith("Typeface: Invalid font"); 107 | }); 108 | 109 | it("Should store all font sources", async () => { 110 | const { rando } = await wallets(); 111 | 112 | for (let i = 0; i < fonts.length; i++) { 113 | const font = fonts[i]; 114 | 115 | await expect(typefaceContract(rando).setSource(font, fontSources[i])) 116 | .to.emit(typeface, "SetSource") 117 | .withArgs([font.weight, font.style]); 118 | } 119 | }); 120 | 121 | it("Should return true hasSource() for all stored fonts", async () => { 122 | const { rando } = await wallets(); 123 | 124 | for (let i = 0; i < fonts.length; i++) { 125 | const font = fonts[i]; 126 | 127 | return expect(await typefaceContract(rando).hasSource(font)).to.be.true; 128 | } 129 | }); 130 | 131 | it("setFontSource should revert if already set", async () => { 132 | const { rando } = await wallets(); 133 | 134 | return expect( 135 | typefaceContract(rando).setSource(fonts[0], fontSources[0]) 136 | ).to.be.revertedWith("Typeface: Source already exists"); 137 | }); 138 | 139 | it("Should return correct font sources", async () => { 140 | const { rando } = await wallets(); 141 | 142 | for (let i = 0; i < fonts.length; i++) { 143 | expect(await typefaceContract(rando).sourceOf(fonts[i])).to.equal( 144 | "0x" + fontSources[i].toString("hex") 145 | ); 146 | } 147 | }); 148 | 149 | it("Should return true for supported codepoint", async () => { 150 | const { rando } = await wallets(); 151 | 152 | expect(await typefaceContract(rando).supportsCodePoint("0x000020")).to.be 153 | .true; 154 | }); 155 | 156 | it("Should return false for unsupported codepoint", async () => { 157 | const { rando } = await wallets(); 158 | 159 | expect(await typefaceContract(rando).supportsCodePoint("0x000000")).to.be 160 | .false; 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /test/TestTypefaceExpandable.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import chalk from "chalk"; 3 | import { Contract, Signer } from "ethers"; 4 | import * as fs from "fs"; 5 | import { ethers } from "hardhat"; 6 | import { Typeface, TypefaceExpandable } from "../typechain-types"; 7 | 8 | import { fontHashes, fonts, fontSources, wallets } from "./utils"; 9 | 10 | export let typeface: Typeface; 11 | 12 | export const typefaceContract = (signer?: Signer) => 13 | new Contract( 14 | typeface.address, 15 | JSON.parse( 16 | fs 17 | .readFileSync( 18 | "./artifacts/contracts/examples/TestTypefaceExpandable.sol/TestTypefaceExpandable.json" 19 | ) 20 | .toString() 21 | ).abi, 22 | signer ?? ethers.provider 23 | ) as TypefaceExpandable; 24 | 25 | export async function deployTypeface() { 26 | const { operator, donationAddress } = await wallets(); 27 | 28 | const TestTypefaceExpandable = await ethers.getContractFactory( 29 | "TestTypefaceExpandable" 30 | ); 31 | const contract = (await TestTypefaceExpandable.deploy( 32 | fonts, 33 | fontHashes, 34 | donationAddress.address, 35 | operator.address 36 | )) as Typeface; 37 | 38 | console.log( 39 | "Deployed TestTypefaceExpandable " + chalk.magenta(contract.address) 40 | ); 41 | 42 | return contract; 43 | } 44 | 45 | describe("TestTypefaceExpandable", async () => { 46 | before(async () => { 47 | typeface = await deployTypeface(); 48 | }); 49 | 50 | it("Should return correct font name", async () => { 51 | const { rando } = await wallets(); 52 | 53 | for (let i = 0; i < fonts.length; i++) { 54 | expect(await typefaceContract(rando).name()).to.equal("TestTypeface"); 55 | } 56 | }); 57 | 58 | it("Set font hashes should fail for rando", async () => { 59 | const { rando } = await wallets(); 60 | 61 | const font = fonts[0]; 62 | 63 | await expect( 64 | typefaceContract(rando).setSourceHashes([font], [fontHashes[0]]) 65 | ).to.be.revertedWith("TypefaceExpandable: Not operator"); 66 | }); 67 | 68 | it("Set font hashes should fail if unequal number of fonts & hashes", async () => { 69 | const { operator } = await wallets(); 70 | 71 | await expect( 72 | typefaceContract(operator).setSourceHashes( 73 | [fonts[0]], 74 | [fontHashes[0], fontHashes[1]] 75 | ) 76 | ).to.be.revertedWith("Typeface: Unequal number of fonts and hashes"); 77 | 78 | await expect( 79 | typefaceContract(operator).setSourceHashes( 80 | [fonts[0], fonts[1]], 81 | [fontHashes[0]] 82 | ) 83 | ).to.be.revertedWith("Typeface: Unequal number of fonts and hashes"); 84 | }); 85 | 86 | it("Change font hash should succeed for operator if no source", async () => { 87 | const { operator } = await wallets(); 88 | 89 | const font = fonts[0]; 90 | 91 | await expect( 92 | typefaceContract(operator).setSourceHashes([font], [fontHashes[0]]) 93 | ) 94 | .to.emit(typeface, "SetSourceHash") 95 | .withArgs([font.weight, font.style], fontHashes[0]); 96 | }); 97 | 98 | it("Should return false hasSource() for unstored font", async () => { 99 | const { rando } = await wallets(); 100 | 101 | const font = fonts[0]; 102 | 103 | return expect(await typefaceContract(rando).hasSource(font)).to.be.false; 104 | }); 105 | 106 | it("Store font source with invalid weight should revert", async () => { 107 | const { rando } = await wallets(); 108 | 109 | const font = fonts[0]; 110 | 111 | return expect( 112 | typefaceContract(rando).setSource( 113 | { 114 | ...font, 115 | weight: 69, 116 | }, 117 | fontSources[0] 118 | ) 119 | ).to.be.revertedWith("Typeface: Invalid font"); 120 | }); 121 | 122 | it("Store font source with invalid style should revert", async () => { 123 | const { rando } = await wallets(); 124 | 125 | const font = fonts[0]; 126 | 127 | return expect( 128 | typefaceContract(rando).setSource( 129 | { 130 | ...font, 131 | style: "asdf", 132 | }, 133 | fontSources[0] 134 | ) 135 | ).to.be.revertedWith("Typeface: Invalid font"); 136 | }); 137 | 138 | it("Store font source with invalid source should revert", async () => { 139 | const { rando } = await wallets(); 140 | 141 | const font = fonts[0]; 142 | 143 | return expect( 144 | typefaceContract(rando).setSource(font, fontSources[2]) 145 | ).to.be.revertedWith("Typeface: Invalid font"); 146 | }); 147 | 148 | it("Store font source with valid source should succeed", async () => { 149 | const { rando } = await wallets(); 150 | 151 | const font = fonts[0]; 152 | 153 | return expect(typefaceContract(rando).setSource(font, fontSources[0])) 154 | .to.emit(typeface, "SetSource") 155 | .withArgs([font.weight, font.style]); 156 | }); 157 | 158 | it("Should return correct font hash", async () => { 159 | const { rando } = await wallets(); 160 | 161 | expect(await typefaceContract(rando).sourceHash(fonts[0])).to.equal( 162 | fontHashes[0] 163 | ); 164 | }); 165 | 166 | it("Should return true hasSource() for stored font", async () => { 167 | const { rando } = await wallets(); 168 | 169 | const font = fonts[0]; 170 | 171 | return expect(await typefaceContract(rando).hasSource(font)).to.be.true; 172 | }); 173 | 174 | it("Should return correct font source", async () => { 175 | const { rando } = await wallets(); 176 | 177 | const font = fonts[0]; 178 | 179 | expect(await typefaceContract(rando).sourceOf(font)).to.equal( 180 | "0x" + fontSources[0].toString("hex") 181 | ); 182 | }); 183 | 184 | it("Change font hash should revert if source already exists", async () => { 185 | const { operator } = await wallets(); 186 | 187 | const font = fonts[0]; 188 | 189 | await expect( 190 | typefaceContract(operator).setSourceHashes([font], [fontHashes[0]]) 191 | ).to.be.revertedWith("TypefaceExpandable: Source already exists"); 192 | }); 193 | 194 | it("setFontSource should revert if already set", async () => { 195 | const { rando } = await wallets(); 196 | 197 | return expect( 198 | typefaceContract(rando).setSource(fonts[0], fontSources[0]) 199 | ).to.be.revertedWith("Typeface: Source already exists"); 200 | }); 201 | 202 | it("Should return false hasSource() for unset font", async () => { 203 | const { rando } = await wallets(); 204 | 205 | const font = fonts[1]; 206 | 207 | return expect(await typefaceContract(rando).hasSource(font)).to.be.false; 208 | }); 209 | 210 | it("setOperator should revert for rando", async () => { 211 | const { rando } = await wallets(); 212 | 213 | return expect( 214 | typefaceContract(rando).setOperator(rando.address) 215 | ).to.be.revertedWith("TypefaceExpandable: Not operator"); 216 | }); 217 | 218 | it("setOperator should succceed for rando", async () => { 219 | const { operator, rando } = await wallets(); 220 | 221 | await expect(typefaceContract(operator).setOperator(rando.address)) 222 | .to.emit(typeface, "SetOperator") 223 | .withArgs(rando.address); 224 | 225 | await expect(await typefaceContract(operator).operator()).to.equal( 226 | rando.address 227 | ); 228 | }); 229 | }); 230 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import { keccak256 } from "ethers/lib/utils"; 2 | import { ethers } from "hardhat"; 3 | 4 | import { FONTS } from "../fonts"; 5 | 6 | export async function wallets() { 7 | const [deployer, donationAddress, operator, rando] = 8 | await ethers.getSigners(); 9 | 10 | return { deployer, donationAddress, operator, rando }; 11 | } 12 | 13 | export type Font = { 14 | weight: keyof typeof FONTS[keyof typeof FONTS]; 15 | style: keyof typeof FONTS; 16 | }; 17 | 18 | export const fonts: Font[] = [ 19 | { 20 | style: "normal", 21 | weight: 400, 22 | }, 23 | { 24 | style: "normal", 25 | weight: 600, 26 | }, 27 | { 28 | style: "italic", 29 | weight: 400, 30 | }, 31 | { 32 | style: "italic", 33 | weight: 600, 34 | }, 35 | ]; 36 | 37 | export const fontSources = fonts.map((f) => 38 | Buffer.from(FONTS[f.style][f.weight]) 39 | ); 40 | 41 | export const fontHashes = fontSources.map((f) => keccak256(f)); 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "outDir": "dist", 8 | "resolveJsonModule": true 9 | }, 10 | "include": ["./scripts", "./test", "./typechain-types", "fonts.ts", "reservedColors.ts"], 11 | "files": ["./hardhat.config.ts"] 12 | } --------------------------------------------------------------------------------