![JAR search and dependency download from the Maven repository](/logo.png)
io.neow3j.script.ScriptBuilder Maven / Gradle / Ivy
package io.neow3j.script;
import io.neow3j.types.ContractParameter;
import io.neow3j.types.Hash160;
import io.neow3j.types.Hash256;
import io.neow3j.types.CallFlags;
import io.neow3j.utils.ArrayUtils;
import io.neow3j.utils.BigIntegers;
import io.neow3j.utils.Numeric;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static java.nio.charset.StandardCharsets.UTF_8;
@SuppressWarnings("unchecked")
public class ScriptBuilder {
private DataOutputStream stream;
private ByteBuffer buffer;
private ByteArrayOutputStream byteStream;
public ScriptBuilder() {
byteStream = new ByteArrayOutputStream();
stream = new DataOutputStream(byteStream);
buffer = ByteBuffer.wrap(new byte[8]).order(ByteOrder.LITTLE_ENDIAN);
}
/**
* Appends an OpCode to the script.
*
* @param opCode The OpCode to append.
* @return this ScriptBuilder object.
*/
public ScriptBuilder opCode(OpCode opCode) {
writeByte(opCode.getCode());
return this;
}
/**
* Appends an OpCode and a belonging argument to the script.
*
* @param opCode The OpCode to append.
* @param argument The argument of the OpCode.
* @return this ScriptBuilder object.
*/
public ScriptBuilder opCode(OpCode opCode, byte[] argument) {
writeByte(opCode.getCode());
write(argument);
return this;
}
/**
* Appends a call to the contract denoted by the given script hash. Uses {@link CallFlags#ALL}
* for the call.
*
* @param hash160 The script hash of the contract to call.
* @param method The method to call.
* @param params The parameters that will be used in the call. Need to be in correct order.
* @return this ScriptBuilder object.
*/
public ScriptBuilder contractCall(Hash160 hash160, String method,
List params) {
return contractCall(hash160, method, params, CallFlags.ALL);
}
/**
* Appends a call to the contract denoted by the given script hash.
*
* @param hash160 The script hash of the contract to call.
* @param method The method to call.
* @param params The parameters that will be used in the call. Need to be in correct order.
* @param callFlags The call flags to use for the contract call.
* @return this ScriptBuilder object.
*/
public ScriptBuilder contractCall(Hash160 hash160, String method,
List params, CallFlags callFlags) {
if (params != null && params.size() > 0) {
pushParams(params);
} else {
opCode(OpCode.NEWARRAY0);
}
pushInteger(callFlags.getValue());
pushData(method);
pushData(hash160.toLittleEndianArray());
sysCall(InteropService.SYSTEM_CONTRACT_CALL);
return this;
}
public ScriptBuilder sysCall(InteropService operation) {
writeByte(OpCode.SYSCALL.getCode());
write(Numeric.hexStringToByteArray(operation.getHash()));
return this;
}
/**
* Adds the given contract parameters to the script.
*
* Example with two parameters in the list:
*
* PUSHBYTES4 a3b00183
* PUSHBYTES20 0100000000000000000000000000000000000000
* PUSH2
* PACK
*
*
* This method should also be used if the parameters list is empty. In that case the script
* looks like the following:
*
* PUSH0
* PACK
*
*
* @param params The list of parameters to add.
* @return this
*/
public ScriptBuilder pushParams(List params) {
// Push params in reverse order.
for (int i = params.size() - 1; i >= 0; i--) {
pushParam(params.get(i));
}
// Even if the parameter list is empty we need to push PUSH0 and PACK OpCodes.
pushInteger(params.size());
opCode(OpCode.PACK);
return this;
}
public ScriptBuilder pushParam(ContractParameter param) {
if (param == null) {
opCode(OpCode.PUSHNULL);
} else {
Object value = param.getValue();
switch (param.getParamType()) {
case BYTE_ARRAY:
case SIGNATURE:
case PUBLIC_KEY:
pushData((byte[]) value);
break;
case BOOLEAN:
pushBoolean((boolean) value);
break;
case INTEGER:
pushInteger((BigInteger) value);
break;
case HASH160:
pushData(((Hash160) value).toLittleEndianArray());
break;
case HASH256:
pushData(((Hash256) value).toLittleEndianArray());
break;
case STRING:
pushData((String) value);
break;
case ARRAY:
pushArray((ContractParameter[]) value);
break;
case MAP:
pushMap((HashMap) value);
break;
case ANY:
if (value == null) {
opCode(OpCode.PUSHNULL);
}
break;
default:
throw new IllegalArgumentException("Parameter type '" + param.getParamType() +
"' not supported.");
}
}
return this;
}
/**
* Adds a push operation with the given integer to the script.
*
* @param v The number to push.
* @return this.
* @throws IllegalArgumentException if the given number is smaller than -1.
*/
public ScriptBuilder pushInteger(long v) {
return pushInteger(BigInteger.valueOf(v));
}
private static final BigInteger minusOne = BigInteger.valueOf(-1);
private static final BigInteger sixteen = BigInteger.valueOf(16);
/**
* Adds a push operation with the given integer to the script. The integer is encoded in its
* two's complement and in little-endian order.
*
* The integer can be up to 32 bytes long.
*
* @param v The integer to push.
* @return this.
* @throws IllegalArgumentException if the given integer is smaller than -1 or takes more space
* than 32 bytes.
*/
public ScriptBuilder pushInteger(BigInteger v) {
int i = v.intValue();
if (v.compareTo(minusOne) >= 0 && v.compareTo(sixteen) <= 0) {
int opcode = OpCode.PUSH0.getCode() + i;
return this.opCode(OpCode.get(opcode));
}
byte[] bytes = BigIntegers.toLittleEndianByteArray(v);
if (bytes.length == 1) {
return this.opCode(OpCode.PUSHINT8, bytes);
}
if (bytes.length == 2) {
return this.opCode(OpCode.PUSHINT16, bytes);
}
if (bytes.length <= 4) {
return this.opCode(OpCode.PUSHINT32, padNumber(v, 4));
}
if (bytes.length <= 8) {
return this.opCode(OpCode.PUSHINT64, padNumber(v, 8));
}
if (bytes.length <= 16) {
return this.opCode(OpCode.PUSHINT128, padNumber(v, 16));
}
if (bytes.length <= 32) {
return this.opCode(OpCode.PUSHINT256, padNumber(v, 32));
}
throw new IllegalArgumentException("The given number (" + v + ") is out of range.");
}
private byte[] padNumber(BigInteger v, int desiredLength) {
if (v.toByteArray().length == desiredLength) {
return BigIntegers.toLittleEndianByteArray(v);
}
if (v.signum() == -1) {
// If the number is negative we need to pad it with 1's to keep it a negative number.
byte[] data = v.toByteArray();
byte[] paddedData = new byte[desiredLength];
System.arraycopy(data, 0, paddedData, paddedData.length - data.length, data.length);
for (int i = 0; i < paddedData.length - data.length; i++) {
paddedData[i] = (byte) 255;
}
return ArrayUtils.reverseArray(paddedData);
}
else {
// If the number is positive we just pad it with zeros.
byte[] data = BigIntegers.toLittleEndianByteArray(v);
byte[] paddedData = new byte[desiredLength];
System.arraycopy(data, 0, paddedData, 0, data.length);
return paddedData;
}
}
public ScriptBuilder pushBoolean(boolean bool) {
if (bool) {
writeByte(OpCode.PUSH1.getCode());
} else {
writeByte(OpCode.PUSH0.getCode());
}
return this;
}
/**
* Adds the data to the script, prefixed with the correct code for its length.
*
* @param data The data to add to the script.
* @return this ScriptBuilder object.
*/
public ScriptBuilder pushData(String data) {
if (data != null) {
pushData(data.getBytes(UTF_8));
} else {
pushData("".getBytes());
}
return this;
}
/**
* Adds the data to the script, prefixed with the correct code for its length.
*
* @param data The data to add to the script.
* @return this ScriptBuilder object.
*/
public ScriptBuilder pushData(byte[] data) {
if (data == null) {
throw new IllegalArgumentException("Data must not be null.");
}
if (data.length < 256) {
this.opCode(OpCode.PUSHDATA1);
this.writeByte((byte) data.length);
this.write(data);
} else if (data.length < 65536) {
this.opCode(OpCode.PUSHDATA2);
this.writeShort(data.length);
this.write(data);
} else {
this.opCode(OpCode.PUSHDATA4);
this.writeInt(data.length);
this.write(data);
}
return this;
}
public ScriptBuilder pushArray(ContractParameter[] params) {
for (int i = params.length - 1; i >= 0; i--) {
pushParam(params[i]);
}
pushInteger(params.length);
pack();
return this;
}
public ScriptBuilder pushMap(HashMap map) {
opCode(OpCode.NEWMAP);
if (map != null) {
for (Map.Entry entry : map.entrySet()) {
opCode(OpCode.DUP);
pushParam(entry.getKey());
pushParam(entry.getValue());
opCode(OpCode.SETITEM);
}
}
return this;
}
public ScriptBuilder pack() {
opCode(OpCode.PACK);
return this;
}
private void writeByte(int v) {
try {
stream.writeByte(v);
} catch (IOException e) {
throw new IllegalStateException("Got IOException without doing IO.");
}
}
private void writeShort(int v) {
buffer.putInt(0, v);
try {
stream.write(buffer.array(), 0, 2);
} catch (IOException e) {
throw new IllegalStateException("Got IOException without doing IO.");
}
}
private void writeInt(int v) {
buffer.putInt(0, v);
try {
stream.write(buffer.array(), 0, 4);
} catch (IOException e) {
throw new IllegalStateException("Got IOException without doing IO.");
}
}
private void write(byte[] data) {
try {
stream.write(data);
} catch (IOException e) {
throw new IllegalStateException("Got IOException without doing IO.");
}
}
public byte[] toArray() {
try {
stream.flush();
} catch (IOException e) {
throw new IllegalStateException("Got IOException without doing IO.");
}
return byteStream.toByteArray();
}
/**
* Builds a verification script for the given public key.
*
* @param encodedPublicKey The public key encoded in compressed format.
* @return the script.
*/
public static byte[] buildVerificationScript(byte[] encodedPublicKey) {
return new ScriptBuilder()
.pushData(encodedPublicKey)
.sysCall(InteropService.SYSTEM_CRYPTO_CHECKSIG)
.toArray();
}
/**
* Builds a verification script for a multi signature account from the given public keys.
*
* @param encodedPublicKeys The public keys encoded in compressed format.
* @param signingThreshold The desired minimum number of signatures required when using the
* multi-sig account.
* @return the script.
*/
public static byte[] buildVerificationScript(List encodedPublicKeys,
int signingThreshold) {
ScriptBuilder builder = new ScriptBuilder().pushInteger(signingThreshold);
encodedPublicKeys.forEach(builder::pushData);
return builder
.pushInteger(encodedPublicKeys.size())
.sysCall(InteropService.SYSTEM_CRYPTO_CHECKMULTISIG)
.toArray();
}
}