diff --git a/.gitignore b/.gitignore index 6143e53..1bc5b22 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,15 @@ # Mobile Tools for Java (J2ME) .mtj.tmp/ +# Eclipse IDE files +*.classpath +*.project +.settings +.settings/* +*/.settings/ +*/.settings/* +*/target + # Package Files # *.jar *.war @@ -20,3 +29,6 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* + +# system files +*.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..f7fcaee --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# Java Examples for NEO + +This repository contains resources and quick starts to help Java developers get started on the NEO blockchain. + +## Setup +You will need to setup a few things before getting started with these examples. + +**Required** + * [neo-devpack-java](https://github.com/neo-project/neo-devpack-java) – Java Native Interfaces (JNI) for Neo's methods + * [neo-compiler](https://github.com/neo-project/neo-compiler) – Converter from Java bytecode to Neo bytecode + + + **Advised** + * [neo-privatenet](https://hub.docker.com/r/cityofzion/neo-privatenet/) – Docker image for local testing  + * [neo-python](https://github.com/CityOfZion/neo-python) – CLI for NEO + + +For instructions on the setup process, see the [NEO docs](http://docs.neo.org) + +## Development Notes + +The Java source code that is written is converted to an avm file (Neo's Virtual Machine bytecode) by the neoj neo-compiler. There are a few limitations with this compiler in its current state that are important to mention. + +1. Method calls while instantiating a field variable will result in runtime errors. Example: +Do not do the following +```java +public static final byte[] OWNER = + Helper.asByteArray"AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y"; +``` + +2. Package names cannot start with **org.neo** as they will be ignored by the neoj neo-compiler. +```csharp + var bskip = cc.classfile.Name.IndexOf("org.neo.") == 0 || cc.classfile.Name.IndexOf("src.org.neo.") == 0; +``` +See [neo-compiler's JAVAModule](https://github.com/neo-project/neo-compiler/blob/92533c40058cdda9be67e94a0e13712f16017b8c/neoj/JVM/JAVAModule.cs#L55) diff --git a/ico-template/README.md b/ico-template/README.md new file mode 100644 index 0000000..e523d9a --- /dev/null +++ b/ico-template/README.md @@ -0,0 +1,9 @@ +# ICO Template +To run this ICO Template, you must first setup [neo-devpack-java](https://github.com/neo-project/neo-devpack-java) and install the artifact to your local maven .m2 folder as well as your system's dotnet folder. + + +## Additional Resources +### Promotion process +**Mac OS** + +There is a bash script file called *promote.sh* that simplifies compiling the template but makes some assumptions on the location of the neo-compiler and neo-python projects. diff --git a/ico-template/pom.xml b/ico-template/pom.xml new file mode 100644 index 0000000..c5ec847 --- /dev/null +++ b/ico-template/pom.xml @@ -0,0 +1,37 @@ + + 4.0.0 + org.neo.ico + ico-template + 1.0.0 + + + UTF-8 + 1.8 + 3.6.1 + + + + + org.neo.smartcontract.framework + neo-devpack-java + 2.3.0 + + + + + src/main/java + + + maven-compiler-plugin + ${maven.compiler.plugin.version} + + ${java.version} + ${java.version} + ${project.build.sourceEncoding} + + + + + + diff --git a/ico-template/scripts/promote.sh b/ico-template/scripts/promote.sh new file mode 100755 index 0000000..3e9e990 --- /dev/null +++ b/ico-template/scripts/promote.sh @@ -0,0 +1,17 @@ +#bin/bash + +artifactId=ico-template +version=1.0.0 +jarFile=$artifactId-$version.jar +neocompilerLocation=../../neo-compiler +neoj=$neocompilerLocation/neoj +neopythonLocation=~/Documents/neo-python/ + +cd ../ +mvn clean install +cp target/$jarFile $neoj/$jarFile + +cd $neoj +dotnet run $jarFile + +cp $artifactId-$version.avm $neopythonLocation diff --git a/ico-template/src/main/java/org/ico/main/ICOTemplate.java b/ico-template/src/main/java/org/ico/main/ICOTemplate.java new file mode 100644 index 0000000..ba0b22e --- /dev/null +++ b/ico-template/src/main/java/org/ico/main/ICOTemplate.java @@ -0,0 +1,203 @@ +package org.ico.main; + +import org.neo.smartcontract.framework.services.neo.TriggerType; +import org.neo.smartcontract.framework.services.system.ExecutionEngine; + +import java.math.BigInteger; + +import org.ico.token.NEP5Template; +import org.ico.token.TemplateToken; +import org.neo.smartcontract.framework.Helper; +import org.neo.smartcontract.framework.SmartContract; +import org.neo.smartcontract.framework.services.neo.Runtime; +import org.neo.smartcontract.framework.services.neo.Storage; +import org.neo.smartcontract.framework.services.neo.Transaction; +import org.neo.smartcontract.framework.services.neo.TransactionOutput; + +public class ICOTemplate extends SmartContract { + + /** + * The EPOC start time for the ICO. The associated time unit is seconds + */ + private final static long ICO_START_TIME = 1521003600; + + /** + * The EPOC end time for the ICO. The associated time unit is seconds + */ + private final static long ICO_END_TIME = 1523682000; + + /** + * The Entry point for the ICO Smart Contract. The neoj neo-compiler looks + * for a method named: Main with an uppercase M. + */ + public static Object Main(String operation, Object[] args) { + TriggerType trigger = Runtime.trigger(); + + if (trigger == TriggerType.Verification) { + boolean isOwner = Runtime.checkWitness(TemplateToken.getOwner()); + + if (isOwner) { + return true; + } + return false; + + } else if (trigger == TriggerType.Application) { + + if (operation.equals("deploy")) { + return deploy(); + } else if (operation.equals("mintTokens")) { + return mintTokens(); + } else if (operation.equals(NEP5Template.TOTAL_SUPPLY)) { + return NEP5Template.totalSupply(); + } else if (operation.equals(NEP5Template.NAME)) { + return NEP5Template.name(); + } else if (operation.equals(NEP5Template.SYMBOL)) { + return NEP5Template.symbol(); + } else if (operation.equals(NEP5Template.DECIMALS)) { + return NEP5Template.decimalsString(); + } else if (operation.equals(NEP5Template.BALANCE_OF)) { + if (args.length < 1) { + Runtime.log("No account argument-No action"); + return false; + } + + byte[] account = Helper.asByteArray((String) args[0]); + return NEP5Template.balanceOf(account); + } else if (operation.equals(NEP5Template.TRANSFER)) { + if (args.length != 3) { + Runtime.log("Insufficient number of arguments-No action"); + return false; + } + + byte[] from = Helper.asByteArray((String) args[0]); + byte[] to = Helper.asByteArray((String) args[1]); + BigInteger amount = BigInteger.valueOf(Long.valueOf((String) args[2])); + + return NEP5Template.transfer(from, to, amount); + } + } + + return false; + } + + public static Object deploy() { + if (getTotalSupply().length != 0) { + Runtime.log("Insufficient token supply-No action"); + return false; + } + + Storage.put(Storage.currentContext(), TemplateToken.getOwner(), TemplateToken.getPreIcoCap()); + Storage.put(Storage.currentContext(), NEP5Template.TOTAL_SUPPLY, TemplateToken.getPreIcoCap()); + + return true; + } + + public static Object mintTokens() { + byte[] sender = getSender(); + + if (sender.length == 0) { + Runtime.log("Asset is not neo-No action"); + return false; + } + + // The current exchange rate between ICO tokens and Neo during the token + // swap period + BigInteger swapRate = currentSwapRate(); + if (swapRate.equals(BigInteger.ZERO)) { + Runtime.log("Crowd funding failure-No action"); + return false; + } + + BigInteger contributionAmount = getContributionAmount(); + BigInteger tokenTransferAmount = currentSwapToken(sender, contributionAmount, swapRate); + if (tokenTransferAmount.equals(BigInteger.ZERO)) { + Runtime.log("Zero token transfer amount-No action"); + return false; + } + + byte[] senderBalanceByteArray = Storage.get(Storage.currentContext(), sender); + BigInteger senderBalance = new BigInteger(senderBalanceByteArray); + BigInteger postTransferAmount = senderBalance.add(tokenTransferAmount); + Storage.put(Storage.currentContext(), sender, postTransferAmount); + + BigInteger tokenAndSupply = tokenTransferAmount.add(NEP5Template.totalSupply()); + Storage.put(Storage.currentContext(), NEP5Template.TOTAL_SUPPLY, tokenAndSupply); + + return true; + } + + private static BigInteger currentSwapRate() { + long icoDuration = ICO_END_TIME - ICO_START_TIME; + + long now = Runtime.time(); + long time = now - ICO_START_TIME; + + if (time < 0) { + return BigInteger.ZERO; + } else if (time < icoDuration) { + return TemplateToken.getBasicRate(); + } + + return BigInteger.ZERO; + } + + private static BigInteger getContributionAmount() { + Transaction tx = (Transaction) ExecutionEngine.scriptContainer(); + TransactionOutput[] outputs = tx.outputs(); + BigInteger contributionAmount = BigInteger.ZERO; + + for (TransactionOutput transaction : outputs) { + if (transaction.scriptHash() == getReceiver() && transaction.assetId() == neoAssetId()) { + BigInteger transactionAmount = BigInteger.valueOf(transaction.value()); + contributionAmount = contributionAmount.add(transactionAmount); + } + } + + return contributionAmount; + } + + private static BigInteger currentSwapToken(byte[] sender, BigInteger contributedAmount, BigInteger swapRate) { + BigInteger tokenAmount = contributedAmount.divide(TemplateToken.getDecimals()).multiply(swapRate); + BigInteger tokenBalance = TemplateToken.getTotalAmount().subtract(NEP5Template.totalSupply()); + + if (tokenBalance.compareTo(BigInteger.ZERO) <= 0) { + return BigInteger.ZERO; + } else if (tokenBalance.compareTo(tokenAmount) < 0) { + tokenAmount = tokenBalance; + } + + return tokenAmount; + } + + /** + * @return smart contract script hash + */ + private static byte[] getReceiver() { + return ExecutionEngine.executingScriptHash(); + } + + private static byte[] getSender() { + Transaction tx = (Transaction) ExecutionEngine.scriptContainer(); + TransactionOutput[] reference = tx.references(); + + for (TransactionOutput output : reference) { + if (output.assetId() == neoAssetId()) { + return output.scriptHash(); + } + } + return Helper.asByteArray(""); + } + + private static byte[] getTotalSupply() { + return Storage.get(Storage.currentContext(), NEP5Template.TOTAL_SUPPLY); + } + + private static byte[] neoAssetId() { + byte[] assetId = new byte[] { (byte) 155, (byte) 124, (byte) 255, (byte) 218, (byte) 166, (byte) 116, + (byte) 190, (byte) 174, (byte) 15, (byte) 147, (byte) 14, (byte) 190, (byte) 96, (byte) 133, (byte) 175, + (byte) 144, (byte) 147, (byte) 229, (byte) 254, (byte) 86, (byte) 179, (byte) 74, (byte) 92, (byte) 34, + (byte) 12, (byte) 205, (byte) 207, (byte) 110, (byte) 252, (byte) 51, (byte) 111, (byte) 197 }; + return assetId; + } + +} diff --git a/ico-template/src/main/java/org/ico/token/NEP5Template.java b/ico-template/src/main/java/org/ico/token/NEP5Template.java new file mode 100644 index 0000000..5e5816f --- /dev/null +++ b/ico-template/src/main/java/org/ico/token/NEP5Template.java @@ -0,0 +1,107 @@ +package org.ico.token; + +import java.math.BigInteger; + +import org.neo.smartcontract.framework.services.neo.Runtime; +import org.neo.smartcontract.framework.services.neo.Storage; + +/** + * NEP-5 outlines a token standard for the NEO blockchain that will provide + * systems with a generalized interaction mechanism for tokenized Smart + * Contracts. + */ +public class NEP5Template { + public final static String TOTAL_SUPPLY = "totalSupply"; + public final static String NAME = "name"; + public final static String SYMBOL = "symbol"; + public final static String DECIMALS = "decimals"; + public final static String BALANCE_OF = "balanceOf"; + public final static String TRANSFER = "transfer"; + + /** + * @return the total token supply deployed in the system + */ + public static BigInteger totalSupply() { + byte[] totalSupply = Storage.get(Storage.currentContext(), "totalSupply"); + return new BigInteger(totalSupply); + } + + /** + * @return the token name + */ + public static String name() { + return TemplateToken.getName(); + } + + /** + * @return the token symbol + */ + public static String symbol() { + return TemplateToken.getSymbol(); + } + + /** + * @return the number of decimals used by the token + */ + public static byte decimals() { + return TemplateToken.decimals(); + } + + /** + * @return the number of decimals used by the token as a String + */ + public static String decimalsString() { + return String.valueOf(TemplateToken.decimals()); + } + + /** + * @return the token balance of the account + */ + public static BigInteger balanceOf(byte[] account) { + return new BigInteger(Storage.get(Storage.currentContext(), account)); + } + + /** + * Will transfer an amount of tokens from the from account to the to account + */ + public static Boolean transfer(byte[] from, byte[] to, BigInteger amount) { + if (amount.compareTo(BigInteger.ZERO) <= 0) { + Runtime.log("No transfer amount-No action"); + return false; + } + + if (!Runtime.checkWitness(from)) { + Runtime.log("Not transfering from self-No action"); + return false; + } + + if (from == to) { + Runtime.log("Transfering to self-No action"); + return true; + } + + byte[] senderSupplyByteArray = Storage.get(Storage.currentContext(), from); + BigInteger senderSupply = new BigInteger(senderSupplyByteArray); + + if (senderSupply.compareTo(amount) < 0) { + Runtime.log("Insufficient funds for sender-No action"); + return false; + } + + if (senderSupply == amount) { + Storage.delete(Storage.currentContext(), from); + } else { + BigInteger remainingSupply = senderSupply.subtract(amount); + Storage.put(Storage.currentContext(), from, remainingSupply); + } + + byte[] receiverSupplyByteArray = Storage.get(Storage.currentContext(), to); + BigInteger receiverSupply = new BigInteger(receiverSupplyByteArray); + + BigInteger postTransferAmount = receiverSupply.add(amount); + Storage.put(Storage.currentContext(), to, postTransferAmount); + + return true; + } + +} diff --git a/ico-template/src/main/java/org/ico/token/TemplateToken.java b/ico-template/src/main/java/org/ico/token/TemplateToken.java new file mode 100644 index 0000000..76ef1b1 --- /dev/null +++ b/ico-template/src/main/java/org/ico/token/TemplateToken.java @@ -0,0 +1,85 @@ +package org.ico.token; + +import java.math.BigInteger; + +import org.neo.smartcontract.framework.Helper; + +public class TemplateToken { + + /** + * The full name of the token + */ + private final static String NAME = "Test Token"; + + /** + * The symbol that will be used to identify the token + */ + private final static String SYMBOL = "TST"; + + /** + * The unique id to the owner of the token + */ + private final static String OWNER = "AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y"; + + /** + * The factor is the value of a single token after the factor is applied + */ + private final static long FACTOR = 1000000000; + + private final static long NEO_DECIMALS = 1000000000; + + private final static long TOTAL_AMOUNT = 100000000 * FACTOR; + + private final static long PRE_ICO_CAP = 30000000 * FACTOR; + + private final static long BASIC_RATE = 1000 * FACTOR; + + public static String getName() { + return NAME; + } + + public static String getSymbol() { + return SYMBOL; + } + + public static byte[] getOwner() { + return Helper.asByteArray(OWNER); + } + + /** + * + * Because NEO is traded in whole numbers we set a decimal value so that + * smaller amounts of the token can be traded. 1 token = 1*10^8 = 100000000 + */ + public static byte decimals() { + return 8; + } + + /** + * The neo decimals is the value of a single token after the factor is + * applied + */ + public static BigInteger getDecimals() { + return BigInteger.valueOf(NEO_DECIMALS); + } + + /** + * The total number of token that exist + */ + public static BigInteger getTotalAmount() { + return BigInteger.valueOf(TOTAL_AMOUNT); + } + + public static BigInteger getPreIcoCap() { + return BigInteger.valueOf(PRE_ICO_CAP); + } + + /** + * The current exchange rate between the ICO tokens and neo during the token + * swap period + */ + public static BigInteger getBasicRate() { + return BigInteger.valueOf(BASIC_RATE); + } + +}