From 479120dcb8e6f5d0e27d62999da8c1ff89374c1f Mon Sep 17 00:00:00 2001 From: Alexander Garzon Date: Tue, 22 Aug 2017 17:44:37 -0400 Subject: [PATCH] Adding POTCOIN --- .../potcoin/PotcoinAddressValidator.java | 35 ++++ .../extra/potcoin/PotcoinExtension.java | 107 +++++++++++ .../potcoin/sources/FixPriceRateSource.java | 53 ++++++ .../CoinmarketcapRateSource.java | 100 +++++++++++ .../coinmarketcap/CoinmarketcapResponse.java | 34 ++++ .../coinmarketcap/ICoinmarketcapRateAPI.java | 15 ++ .../potcoin/wallets/potwallet/Potwallet.java | 168 ++++++++++++++++++ .../wallets/potwallet/PotwalletAPI.java | 21 +++ .../wallets/potwallet/PotwalletResponse.java | 24 +++ .../src/main/resources/pot.png | Bin 0 -> 7274 bytes 10 files changed, 557 insertions(+) create mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/PotcoinAddressValidator.java create mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/PotcoinExtension.java create mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/sources/FixPriceRateSource.java create mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/sources/coinmarketcap/CoinmarketcapRateSource.java create mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/sources/coinmarketcap/CoinmarketcapResponse.java create mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/sources/coinmarketcap/ICoinmarketcapRateAPI.java create mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/wallets/potwallet/Potwallet.java create mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/wallets/potwallet/PotwalletAPI.java create mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/wallets/potwallet/PotwalletResponse.java create mode 100644 server_extensions_extra/src/main/resources/pot.png diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/PotcoinAddressValidator.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/PotcoinAddressValidator.java new file mode 100644 index 0000000..e2e9898 --- /dev/null +++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/PotcoinAddressValidator.java @@ -0,0 +1,35 @@ +package com.generalbytes.batm.server.extensions.extra.potcoin; + +import com.generalbytes.batm.server.coinutil.AddressFormatException; +import com.generalbytes.batm.server.coinutil.Base58; +import com.generalbytes.batm.server.extensions.ExtensionsUtil; +import com.generalbytes.batm.server.extensions.ICryptoAddressValidator; + +public class PotcoinAddressValidator implements ICryptoAddressValidator { + + @Override + public boolean isAddressValid(String address) { + if (address.startsWith("P")) { + try { + Base58.decodeToBigInteger(address); + Base58.decodeChecked(address); + } catch (AddressFormatException e) { + e.printStackTrace(); + return false; + } + return true; + }else{ + return false; + } + } + + @Override + public boolean isPaperWalletSupported() { + return false; + } + + @Override + public boolean mustBeBase58Address() { + return true; + } +} diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/PotcoinExtension.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/PotcoinExtension.java new file mode 100644 index 0000000..87c78f6 --- /dev/null +++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/PotcoinExtension.java @@ -0,0 +1,107 @@ +package com.generalbytes.batm.server.extensions.extra.potcoin; + +import com.generalbytes.batm.server.extensions.*; +import com.generalbytes.batm.server.extensions.extra.potcoin.sources.FixPriceRateSource; +import com.generalbytes.batm.server.extensions.extra.potcoin.sources.coinmarketcap.CoinmarketcapRateSource; +import com.generalbytes.batm.server.extensions.extra.potcoin.wallets.potwallet.Potwallet; +import com.generalbytes.batm.server.extensions.watchlist.IWatchList; + +import java.math.BigDecimal; +import java.util.*; + +public class PotcoinExtension implements IExtension{ + @Override + public String getName() { + return "BATM Potcoin extension"; + } + + @Override + public IExchange createExchange(String exchangeLogin) { + return null; + } + + @Override + public IWallet createWallet(String walletLogin) { + if (walletLogin !=null && !walletLogin.trim().isEmpty()) { + StringTokenizer st = new StringTokenizer(walletLogin,":"); + String walletType = st.nextToken(); + + if ("potwallet".equalsIgnoreCase(walletType)) { + String publicKey = st.nextToken(); + String privateKey = st.nextToken(); + String walletId = st.nextToken(); + + if (publicKey != null && privateKey != null && walletId != null) { + return new Potwallet(publicKey,privateKey,walletId); + } + } + } + return null; + } + + @Override + public ICryptoAddressValidator createAddressValidator(String cryptoCurrency) { + if (ICurrencies.POT.equalsIgnoreCase(cryptoCurrency)) { + return new PotcoinAddressValidator(); + } + return null; + } + + @Override + public IPaperWalletGenerator createPaperWalletGenerator(String cryptoCurrency) { + return null; + } + + @Override + public IPaymentProcessor createPaymentProcessor(String paymentProcessorLogin) { + return null; //no payment processors available + } + + @Override + public IRateSource createRateSource(String sourceLogin) { + if (sourceLogin != null && !sourceLogin.trim().isEmpty()) { + StringTokenizer st = new StringTokenizer(sourceLogin,":"); + String rsType = st.nextToken(); + + if ("potfix".equalsIgnoreCase(rsType)) { + BigDecimal rate = BigDecimal.ZERO; + if (st.hasMoreTokens()) { + try { + rate = new BigDecimal(st.nextToken()); + } catch (Throwable e) { + } + } + String preferedFiatCurrency = ICurrencies.CAD; + if (st.hasMoreTokens()) { + preferedFiatCurrency = st.nextToken().toUpperCase(); + } + return new FixPriceRateSource(rate,preferedFiatCurrency); + } else if ("coinmarketcap".equalsIgnoreCase(rsType)) { + String preferredFiatCurrency = ICurrencies.CAD; + if (st.hasMoreTokens()) { + preferredFiatCurrency = st.nextToken(); + } + return new CoinmarketcapRateSource(preferredFiatCurrency); + } + + } + return null; + } + + @Override + public Set getSupportedCryptoCurrencies() { + Set result = new HashSet(); + result.add(ICurrencies.POT); + return result; + } + + @Override + public Set getSupportedWatchListsNames() { + return null; + } + + @Override + public IWatchList getWatchList(String name) { + return null; + } +} diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/sources/FixPriceRateSource.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/sources/FixPriceRateSource.java new file mode 100644 index 0000000..6ed0301 --- /dev/null +++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/sources/FixPriceRateSource.java @@ -0,0 +1,53 @@ +package com.generalbytes.batm.server.extensions.extra.potcoin.sources; + +import com.generalbytes.batm.server.extensions.ICurrencies; +import com.generalbytes.batm.server.extensions.IRateSource; + +import java.math.BigDecimal; +import java.util.HashSet; +import java.util.Set; + +public class FixPriceRateSource implements IRateSource { + private BigDecimal rate = BigDecimal.ZERO; + + private String preferedFiatCurrency = ICurrencies.CAD; + + public FixPriceRateSource(BigDecimal rate,String preferedFiatCurrency) { + this.rate = rate; + if (ICurrencies.CAD.equalsIgnoreCase(preferedFiatCurrency)) { + this.preferedFiatCurrency = ICurrencies.CAD; + } + if (ICurrencies.USD.equalsIgnoreCase(preferedFiatCurrency)) { + this.preferedFiatCurrency = ICurrencies.USD; + } + } + + @Override + public Set getCryptoCurrencies() { + Set result = new HashSet(); + result.add(ICurrencies.POT); + return result; + } + + @Override + public BigDecimal getExchangeRateLast(String cryptoCurrency, String fiatCurrency) { + if (ICurrencies.POT.equalsIgnoreCase(cryptoCurrency)) { + return rate; + } + return null; + } + + @Override + public Set getFiatCurrencies() { + Set result = new HashSet(); + result.add(ICurrencies.USD); + result.add(ICurrencies.CAD); + return result; + } + + @Override + public String getPreferredFiatCurrency() { + return preferedFiatCurrency; + } + +} diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/sources/coinmarketcap/CoinmarketcapRateSource.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/sources/coinmarketcap/CoinmarketcapRateSource.java new file mode 100644 index 0000000..1b602bd --- /dev/null +++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/sources/coinmarketcap/CoinmarketcapRateSource.java @@ -0,0 +1,100 @@ +package com.generalbytes.batm.server.extensions.extra.potcoin.sources.coinmarketcap; + +import com.generalbytes.batm.server.extensions.ICurrencies; +import com.generalbytes.batm.server.extensions.IRateSource; +import si.mazi.rescu.RestProxyFactory; + +import java.math.BigDecimal; +import java.util.HashSet; +import java.util.Set; + +public class CoinmarketcapRateSource implements IRateSource { + + private String preferedFiatCurrency = ICurrencies.USD; + private ICoinmarketcapRateAPI api; + + /** + * Constructor + */ + public CoinmarketcapRateSource(String preferedFiatCurrency) { + if (ICurrencies.USD.equalsIgnoreCase(preferedFiatCurrency)) { + this.preferedFiatCurrency = ICurrencies.USD; + } + if (ICurrencies.CAD.equalsIgnoreCase(preferedFiatCurrency)) { + this.preferedFiatCurrency = ICurrencies.CAD; + } + if (ICurrencies.EUR.equalsIgnoreCase(preferedFiatCurrency)) { + this.preferedFiatCurrency = ICurrencies.EUR; + } + api = RestProxyFactory.createProxy(ICoinmarketcapRateAPI.class, "https://api.coinmarketcap.com"); + } + + /** + * This method returns list of supported crypto currencies + * @return + */ + @Override + public Set getCryptoCurrencies() { + Set result = new HashSet(); + result.add(ICurrencies.POT); + //result.add(ICurrencies.BTC); + return result; + } + + /** + * Returns current price of cryptocurrency in specified fiat currency + * @param cryptoCurrency + * @param fiatCurrency + * @return + */ + @Override + public BigDecimal getExchangeRateLast(String cryptoCurrency, String fiatCurrency) { + // coinmarketcap uses coin name instead coin symbol + if (ICurrencies.BTC.equalsIgnoreCase(cryptoCurrency)) { + cryptoCurrency = "bitcoin"; + } else if (ICurrencies.POT.equalsIgnoreCase(cryptoCurrency)) { + cryptoCurrency = "potcoin"; + } + + CoinmarketcapResponse[] response = api.getRequest(cryptoCurrency, fiatCurrency); + + if (response != null) { + if (ICurrencies.USD.equalsIgnoreCase(fiatCurrency)) { + BigDecimal rate = response[0].getPrice_usd(); + return rate; + } else if (ICurrencies.CAD.equalsIgnoreCase(fiatCurrency)) { + BigDecimal rate = response[0].getPrice_cad(); + return rate; + } else if (ICurrencies.EUR.equalsIgnoreCase(fiatCurrency)) { + BigDecimal rate = response[0].getPrice_eur(); + return rate; + } else { + return null; + } + } + return null; + } + + /** + * This method returns list of supported fiat currencies + * @return + */ + @Override + public Set getFiatCurrencies() { + Set result = new HashSet(); + result.add(ICurrencies.USD); + result.add(ICurrencies.CAD); + result.add(ICurrencies.EUR); + return result; + } + + /** + * Returns fiat currency that is used for actual purchases of cryptocurrency by server + * @return + */ + @Override + public String getPreferredFiatCurrency() { + return preferedFiatCurrency; + } + +} diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/sources/coinmarketcap/CoinmarketcapResponse.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/sources/coinmarketcap/CoinmarketcapResponse.java new file mode 100644 index 0000000..a63dca5 --- /dev/null +++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/sources/coinmarketcap/CoinmarketcapResponse.java @@ -0,0 +1,34 @@ +package com.generalbytes.batm.server.extensions.extra.potcoin.sources.coinmarketcap; + +import java.math.BigDecimal; + +public class CoinmarketcapResponse { + + private BigDecimal price_usd; + private BigDecimal price_cad; + private BigDecimal price_eur; + + public BigDecimal getPrice_usd() { + return price_usd; + } + + public void setPrice_usd(BigDecimal price_usd) { + this.price_usd = price_usd; + } + + public BigDecimal getPrice_cad() { + return price_cad; + } + + public void setPrice_cad(BigDecimal price_cad) { + this.price_cad = price_cad; + } + + public BigDecimal getPrice_eur() { + return price_eur; + } + + public void setPrice_eur(BigDecimal price_eur) { + this.price_eur = price_eur; + } +} diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/sources/coinmarketcap/ICoinmarketcapRateAPI.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/sources/coinmarketcap/ICoinmarketcapRateAPI.java new file mode 100644 index 0000000..16251e4 --- /dev/null +++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/sources/coinmarketcap/ICoinmarketcapRateAPI.java @@ -0,0 +1,15 @@ +package com.generalbytes.batm.server.extensions.extra.potcoin.sources.coinmarketcap; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/v1") +@Produces(MediaType.APPLICATION_JSON) +public interface ICoinmarketcapRateAPI { + @GET + @Path("/ticker/{cryptocurrency}/?convert={fiatcurrency}") + CoinmarketcapResponse[] getRequest(@PathParam("cryptocurrency") String cryptoCurrency, @PathParam("fiatcurrency") String fiatCurrency); +} diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/wallets/potwallet/Potwallet.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/wallets/potwallet/Potwallet.java new file mode 100644 index 0000000..ad47524 --- /dev/null +++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/potcoin/wallets/potwallet/Potwallet.java @@ -0,0 +1,168 @@ +package com.generalbytes.batm.server.extensions.extra.potcoin.wallets.potwallet; + +import com.generalbytes.batm.server.extensions.ICurrencies; +import com.generalbytes.batm.server.extensions.IWallet; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import si.mazi.rescu.HttpStatusIOException; +import si.mazi.rescu.RestProxyFactory; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.HashSet; +import java.util.Set; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.UnsupportedEncodingException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import java.util.Random; + +public class Potwallet implements IWallet { + private static final Logger log = LoggerFactory.getLogger("batm.master.Potwallet"); + + private String nonce; + private String accessHash; + + private String publicKey; + private String privateKey; + private String walletId; + + private PotwalletAPI api; + + public Potwallet(String publicKey, String privateKey, String walletId) { + this.nonce = generateRandomString(8); + + this.publicKey = publicKey; + this.privateKey = privateKey; + this.walletId = walletId; + api = RestProxyFactory.createProxy(PotwalletAPI.class, "https://api.potwallet.com"); + } + + @Override + public String getPreferredCryptoCurrency() { + return ICurrencies.POT; + } + + @Override + public Set getCryptoCurrencies() { + Set result = new HashSet(); + result.add(ICurrencies.POT); + return result; + } + + @Override + public String getCryptoAddress(String cryptoCurrency) { + if (!getCryptoCurrencies().contains(cryptoCurrency)) { + log.error("Cryptocurrency " + cryptoCurrency + " not supported."); + return null; + } + if (walletId != null) { + return walletId; + } + try { + accessHash = generateHash(privateKey, "https://api.potwallet.com/v1/address", nonce); + PotwalletResponse response = api.getAddress(publicKey, accessHash, nonce); + if (response != null && response.getMessage() != null && response.getSuccess()) { + return new String(response.getMessage()); + } + } catch (HttpStatusIOException e) { + log.error(e.getHttpBody()); + } catch (IOException e) { + log.error("", e); + } + return null; + } + + @Override + public BigDecimal getCryptoBalance(String cryptoCurrency) { + if (!getCryptoCurrencies().contains(cryptoCurrency)) { + log.error("Cryptocurrency " + cryptoCurrency + " not supported."); + return null; + } + try { + accessHash = generateHash(privateKey, "https://api.potwallet.com/v1/balance", nonce); + PotwalletResponse response = api.getCryptoBalance(publicKey, accessHash, nonce); + if (response != null && response.getMessage() != null && response.getSuccess()) { + log.debug("Transaction " + response.getMessage() + " sent."); + return new BigDecimal(response.getMessage()); + } + } catch (HttpStatusIOException e) { + log.error(e.getHttpBody()); + } catch (IOException e) { + log.error("", e); + } + return null; + } + + @Override + public String sendCoins(String destinationAddress, BigDecimal amount, String cryptoCurrency, String description) { + if (!getCryptoCurrencies().contains(cryptoCurrency)) { + log.error("Cryptocurrency " + cryptoCurrency + " not supported."); + return null; + } + try{ + accessHash = generateHash(privateKey, "https://api.potwallet.com/v1/send", nonce); + PotwalletResponse response = api.sendPots(publicKey, accessHash, nonce, destinationAddress, amount.stripTrailingZeros()); + if (response != null && response.getMessage() != null && response.getSuccess()) { + return new String(response.getMessage()); + } + } catch (HttpStatusIOException e) { + log.error(e.getHttpBody()); + } catch (IOException e) { + log.error("", e); + } + return null; + } + + private static String generateRandomString(int length) + { + String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + Random rng = new Random(); + char[] text = new char[length]; + for (int i = 0; i < length; i++) + { + text[i] = characters.charAt(rng.nextInt(characters.length())); + } + return new String(text); + } + + private static String generateHash(String privateHash, String endPoint, String nonce ) + { + String digest = null; + try + { + String content = endPoint + nonce; + + SecretKeySpec key = new SecretKeySpec((privateHash).getBytes("UTF-8"), "HmacSHA256"); + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(key); + + byte[] bytes = mac.doFinal(content.getBytes("ASCII")); + + StringBuffer hash = new StringBuffer(); + + for (int i=0; i zK~#9!?VWjaTjiPmKkt=fTizsRb)3!FS4&BNDJ7ICWt+Csp$m}MW*aEe!;khi=X9o} zhdCWMGhOHmbEY%2WuT?#aGh3 zvSdrLllbw`k$iP^?|a|x`>gNtye~}BB%QZ(-&y=WKRgL=vB=);tOe>N@(O|22>dxv zS;i)GA^C+w{+AG9rwRkTE>AGU-K`FKJIcr1x83$xe)hBH&Od-{Z4Lr%uNK&q5-t;Q zKtG<~(($Hl;OD?ANV(JR_I4VzzZ7)uy`SW<$9_p$+l|!J*r}>&;L%6FJFx+_wK)m6 zJz8Kt1$+)z4=kRbf<*%V3%mhr_Pac9mFvb|`qB;b_0=j;)=*h#RiAj`aozLG6Q!q; z;M&`rw*dD83jzCipyJjgUbouYoqt5ihx~5uJMm|6;CGdVn-xVtQNl77lLUivC#HzD zwL1v7yx7{ELLmPH#ACqP3&45%CCD%OUEagm=Pb5}zWeSQ0NAwYm5B+keS_1CnS~Pm zLBNI!&`W<9DSzO1dp`iMw>j{;y=50Me~Qw|b2FzV5RjUpMwP2SH!=`2n!4Z8&UaZ+CuC2p$7wPU1SO zlLcZP@aZZ?ZZGw%jw(eFj{x^gVppKtma;YA_HKsaMZb!o zgX=b~rmw?CU8}=lQp7{RmnY#G1-A)tQ>CNvaLCu~3HiFQwL1unQ74LBv(G9n*@o_(>H`bATTKcp(vV1lu0p&A`}pysxtn?aVB@&A=5FDZ}*iSGLCKwJ8 zQDdkwp6Hjax)h^&AT-3| zfAtUCaqTTcW6_d6m>kxJsrO(wNZ(*TfuVlR_WL<`_B1EYo#MpVZjSbxVE>6uj`o}& z7zrbUNDY87!hHz1+VAol9akgE-sWgQh+hEBr4n1cgZ=FN_g7dtXHlk6f)FAFMnaFJ zeW!PR4kIV8>lo3|QzzKjxtn(mc-gw=9gd#uVlX(wP&k-&%>sm(m+V+M=j8Lz$Aa8Jtq@PDJ6(uA)ESM2*~)2 z_IxzQGO6p_DPG;Vl~;DY!Q1b9_~6V*D$N!70xc8x{EOe^`CEXx))x8_(ZP}$S+;g3 zAzwFDj+tK*h7WjGe0ql`&({JWgh(6SLI^|>v|PW@ zd?$u~oW?21Hq}~da4uNEXD+{ibqiNx5`tYv_n}HPZU^MYd{$5sKQ=FnPvh>7n7I({ZAkh`eGIgHb z{fye6WkDeX^QJd(t#dsq<}Buo-S03I2^Au;*H$9#jFdqcmo|im;nNw zEr;6>jm22EU?qZK&ehYEM!Hl_S7(ZIBmEm^Ba!vnT<0ghOIr2X^QtPU_}mpA=f%JK zA=By`3h9P<3Vhw(zS>f#9?l!!>W#w_LH6sM-v^pX9@#-+wN$XUX#t9&F_1Gv;Eesn_Mwlq-{bX4ev^K38n~HaT-Sw)Xa6}D5Q z%A&^k*sAMvxheI32A3LftM*+xIAkWL8%YDCF83#|Wz>euY_8?X8lOJ+5vB4v5d z!}it|TfW(uc>`>3bJ&5qC}S>_Kw-g*IaF9GvN%xaaHkQs=5lv*ua)t?mIE_gC@pwK zZOG-OEp1xBH7y@5L>E_}D326PMAx^ij6aAF4&YOik!&?}%$?q(uYYss(ShI~1HnNC zLqiM%hZqPAF&G?59g80yjDHs#jDMeeY0e8qLPVkwqOn+p0!G)zHfp0$JEfFZ%oZ+R zwvO7Wg0$XUwl?Pi79nOOncuA_Y8>F-fx zsZ5&*N(MlZoG6ircM|_;WkcM>v6wAXm@BZD%v4oWQfIBD-de+yIy*CLQ<*z$7W1ah zHnze?2y-UCYni`{>Ge}Mb|&v2h7f|k2mW%TWW5mq_CNJ5wsz;Gz_pYWVM#-sjroZV zG%|`Zt<~Rp-{i-?dzR`-Ymu5UY*{M^EM_y+mDSW+YiX#pF?;GvHm?69H?9AqfsvIQ zO2)3-;#tkus_P2^82FgI-MQTF^1N5ywwhk|uEGIMW)%o{ka3`Cb#|uNrlh+mMseZ9 z=eOVYP+e)I!d$vcp`mb)!Elf+|7rFfKfu<#+j-&b*Ld`vhp^Y$vL#NM|C|v&2dtG< zOs%sO+KjotXMt}SftynY*KSyyC=7En5wFcHE147%%bFIZca}~(IK{yUhIsS6cQKnx zC2ya0?l}|*^UPZ>^W`6XgTA4DLqA&^KhN~1XE)3&bhb$V*Vx;fGe%`(S*;aGef6c% zNnuG$Rf(ywtd|gimifzd-I}y;U=m~m{X_k9o<5$qZfw-AJOdADRAs5;r~mUjue|$u zTqtK)+DUFrewRrz8>WpWVc-e`D@Ub+Pycuawl;?uxUp2=+H2~VUOyE@D2Sv%6t{7x zs;Fe$!d1FDH)SY^bPM(1$s;)CFUO=PIgK|mrUo;kQKtiE=;`yPnO=E;Tv5fpzPO2- zuKYxn>y#G#(!sIbS{ILg7I;gfyvE+zven<=i{=e*-Nx1QZQg|t;%cC=6aWV!A#T0u zHtxFi7EC5nqH{GKun^2@m@#t1N%Pg>*$a63tKUoOL7A|KbJ%i;i%NtPSSzde+E2g3 zGk4lTu0m4jVJ zxTMaJ*Rv$sF%SwCEHM(aR7$xp;P!e8(!sVihYfI+N<2@mpTew$=~*LMZ>rBI25l5% z(3^0`)Z1B|oOtg#cbZdyGg-4mg*d3^278SsMhx`(iL>3K(*22>wgS{|Sd~A(LWqUH z^3nt`B(oZ(Gp&A#Ay*kate$(6>s+JE4x@W@`+MjKoE<|hlcdsIp(}=SeeUv~9yP!s zzE};tTXq)|s$4*PwYXBmFqmbZ!3+(+`OM)a4jBF>6Ucm`NSU9z13jDy^o&Y8$0tZ6 zOPiW=-Y@&OZxLPJ+G3%kb-a?94bzxX-(WaHqc1qVlU&6pZvMbjdt+o$QO1|yH)SWg$dLjz;#QAJU1$QxiqQL2b94O9}c zn9Rc=NSPJ_9?{^+1SQko)B{gfC(B{Qo4igi9O6*d(NXK)U?j}#AN@2!DA@y3Go_gM zjEB08(mU8U)^7b605{LcZAB+$Cn`&2U;$KDT3I<~v7yVOC!Y0;s(jt2d?)(D(FpIK zJe<>y&SgE%ZkWM_k9;bA>x{rmF14`t*uk-iXn-pLc=}jwfCKSLg*xIZwU&(b96LaC z+pmbJu{5z6jYhd*#U(6ioS&Bt1325~XY1bW36u6(aZ_^2YT6i z{NShnR)paHfAUV|Oq)fPX>McyA_&LAc#rHQ7!G0?Tl{=xeS3?o*X0Y0=m$$Fmyg!S z$cOragM8=t$5C}Axd#S%dHvhZqyWyxAN3vC#}}V?kebfMGNYeU_!?kfHktFu zSNE=A>jC#Zr(ZILs7;YKUWl#Rlu7g0xX1BUFu-HNin5$ zMzSO7>l#=!f0>~#tJhH{&z<7jK!8e11?8rgs<~IMX7HsWCdcKOmB4QCcxcm6BC+ z7W4aWb}+TRLDMdgG9Yu+NQI(de)hZP5K2*`rIK)M;pQbH0^B?v07umrj`_<{oSlzv zJP;gY-|<6P(x7yp#%I6ktTo(z)u-9^@H5m}YvQUvh^*LmuK4r5V+VQu?N?KQks@$# zJnsAS`}!if^5&(4p59OD#By_REJjQ7a?B=k&S;dDhQcBCpXf~K-eQMT<3;8%qG}9P zRar8tnUAczjGM3c1lKs%XSFeFzBdQm|C8^eJ?j`r09kXxX;)8x3*)rpk=bNs`Rqlh z3Hy9Hs()yJcMtlAN|iBHjo|rbQsieHsgA&ZDJy8J90z%d*)ES=2Z`r8D(J zaISD`^88J|-on=Rwii3|q%J4Gac^pd0P4p>pUtvQPfO9s$C95neKv=m*c$hCM7kTK zwV2cE{I!^hRA3~PN~1u9sREP9gh`24@S9EMG%%6|R6ey)nc#MwJi@o1`@vYDA;pnaySzr%cbP6VBu$qe8RHu%YH) zT6s#`h0bip$k)yUHy94_H$VOsXT~1b(2XGhQvqXum2q0yS4zpe>9d$x*O1leS=p1x zj0~Y)A3U5^tM8`hWn=0Vi)jZ)MszImoYehtOe)0G7!N=D7{A;7$C7!ULI`tiH%Ukn z<3#hOHBn=b0G`?Lx%69RpdlZTZxoEs26_YI!NGOawOJgjXVm1htVQ-bvNnY^*X)kgRmzH(Weg0xx#Z03 zlBgPE$H85E?CWj3yz}+Zr{fexL#Uj~&A~4oR%v(k>G@QpBztvwp_#r;)L-#NBc8G= z_sQmdqa=gm0LsOc`K%1E@Ax5}f9ny_5B7HWdJO@t_}n7G&mM&M z((`2lSO~$A#%4qA(w2E*l*&WKDm&;ep_DR)MzDBCYOfw0I(Y>58!z(W_TR9xa}QM& zmE{B%t$U;kZ~|O_qd?g<)m4OG?ZTBgg9V1`UuCRqr3HB;KXTrKWS9gBSoSlC1d(O%fCgUs%mPb+GsRNRE-gdMi>f*IC}axJ3IHV^y38cU^1X#kJay40%lDShFu~t@%n7yI>?z_L(M3?_` zDwZsSKuHvIr-3dqgxGLE2QBQ;Sd?%iOgI{0Fcc&()W^BL0Nwr`x&u7~BVkM?6K2JP z#cZa|I^p*6ib;4sFA*(ZPuW_f#`y5Eby;o-Xwz=eb>z7|KR^Eca~wH!ydVW7vaXwb zTal;{O1f5x5Lhf0D#jsOq+lH^)jcBuY-@7@^a3Y=S>*uu@?~qXQP6zM6u?pZRk&=e$dKNV`8z-vE>|+N`9$_#XoP;!;eha{Fj*kqm-{pxA-`TpP6pck$w{Ue9 z_h$OBS|ckKi*dB)M0{zZNg6g*Rr!)`j3LKwI)M*L*{Fybsm=Spd4Nx-6*ST$!!7T0P`=uD7%de3swf0{{r)0km1K<}1a`2k$p+M-q5{?h1- z7>j4krzR0F$f_Dde@^$)nRVmJ>d0^(J_%NH>^!4P2g(W6Q3Db04|QnIvZK`J*mnJJOQ z$r?vE9F6ea2L~qs9;QDqLRi@l;6AsPnYXPW;P!T=oaiD3IHuM$&@?5zfI@FQX6?j% z|Ih#jPaPdq{qBVt-0-_RCr1S^KzA_3`9)nySR!d^n86I&)U+UmkseOYGIfs3Ou6Gv@Vh*30^5oxotZTy9pGGfPRakbAM{}teLdVzSUYV~)nSGT0U;Ba7qRL%WJuZh1b_=78I~7r^iKj1Ax<`CXo~ zz^1rqJ~pbZtYT@?0!;*q^b(L%a@#N+?LNl2fxu*l5>yrSuk$^*AagXP@=`G@>gq}> z%Vsas)w4$W`0WGUN%tsDf8Xo!owBtTdEX>^>l*qxJbfS^AwFL`YO+>Tvp4RB-zHKYlEhGBB+Hj?}b1zc8s3Tb4^t*i9r{2DnfZJPIfaAWG zHwo517h+XQDX~}AF@I)Z%V2sF+oE5LB#}?}U7jBRgu~&&BO+sMfW6)MA>uL7+4-Dr zHkoLiJ}2epnoTAY#iZGXi1@mVB3_hcGMPDW@-Y5^zKalC-T`@4z~v3v+MNVkp7H`b z{f^5x{gZdFwL9(+h<^jDd5+4orx>x37iVzeBygqw%)UdsJbJHmOpfK?JZSjHQv5&J zxueR_SOa_{zfq<66C&?zVlN`{19$mdok5ZZN$`c@~QUENf+%-kx3r~xjY#A(!e_lp6S=RY8^IcR}abI}2g zgWQd+%|XEJjaIhI*d~OSLkYJ8xNy;x_XgbF$Kv2R^A1Or9AJQuFA1_QY;sKfl?l^o z;_FUN=C|epE%L7eZtoB5Z4QFIZbH89qFlJ>ouAxuIN?G)3EVx2Ury==zUFs%zF&%q z7d^NeaCu24Vf$TIejfM=5T3+Qm(BrqCc(9j(V!}+h|bo@PC~vHsaP@fEnzbGfa{2_ zhB`@c+6Ua=cX?h<>0GzB=r<{;ezdi=5J*%>*xQ|}f#-q6li2bck~{ovPk-DMawZG! zC%%YI6W7Dw;&*v=5e}~+9@e~A%k$3y4+1y&-JX71q8hMN;Fg?&wUn5FA~9m`5Q48l z?(NAhF#0X-JQ(G`=JR5#Wjo9M~@40pP)8 z9<#00Q4)PSt{kk()$L9TQZ7e`e+NE#0fPG@DfwnhMmzgEcFS^cZC*O~qaVFQ_j8fm z`-tD|_4!?%t0mkB9J)Y&-Hw!(`CXp718&bDlQ7G;+gLnszwm{Px}OW;;5CkBc6tw& ztyj$LPNf#^O9V$20k!9uPwSEJFM-5F!Ibv3oDHAO+7WcDbl+F*;`{&nBR>C^H&In> z;(OoUQW(JN*RN;Wwr$*c=S`x@QjbZo$j2XlyrjLOozS*#bXqa-S>O}E)s$gDwlUCN z;P=1_ewXK!hih@O$ zOp2roiWfN#Jt1CY}{t2vxVPVsN++ zj=GyyR+%cy*MqnoxC)pLSQ3V#De+!NAV(#>ByaLPk`TYY0bcgIJX_OL%7#@0w(Oqp z{dQhXD+O3_QKp zuv*f#aW#R>yO^X2NL3qF6m)*g=T=R2+9XZVBuy~-KRk`RR`lr8n*aa+07*qoM6N<$ Ef-(&@bpQYW literal 0 HcmV?d00001