Skip to content

Commit

Permalink
Plutus Serialization and contract tests (#60)
Browse files Browse the repository at this point in the history
* adaToLovelace helper method added

* Fix policyId generation for Plutus script and serialization issues

* More address tests

* Contract transaction tests

* ScriptUtxoSelection interface and default impl

* Fixed script data hash generation. PlutusObjectConverter interface

* New test added for multi token transfer in one transaction
  • Loading branch information
satran004 authored Jan 5, 2022
1 parent 2c46561 commit 8284f9c
Show file tree
Hide file tree
Showing 18 changed files with 1,593 additions and 350 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,71 @@ void transferMultiAssetMultiPayments_whenSingleSender() throws CborSerialization
assertThat(result.isSuccessful(), is(true));
}

@Test
void transferMultiAssetMultiPayments_whenSingleSender_multipleToken() throws CborSerializationException, AddressExcepion, ApiException {
//Sender address : addr_test1qzx9hu8j4ah3auytk0mwcupd69hpc52t0cw39a65ndrah86djs784u92a3m5w475w3w35tyd6v3qumkze80j8a6h5tuqq5xe8y
String senderMnemonic = "damp wish scrub sentence vibrant gauge tumble raven game extend winner acid side amused vote edge affair buzz hospital slogan patient drum day vital";
Account sender = new Account(Networks.testnet(), senderMnemonic);

String receiver1 = "addr_test1qqwpl7h3g84mhr36wpetk904p7fchx2vst0z696lxk8ujsjyruqwmlsm344gfux3nsj6njyzj3ppvrqtt36cp9xyydzqzumz82";
String receiver2 = "addr_test1qz7r5eu2jg0hx470mmf79vpgueaggh22pmayry8xrre5grtpyy9s8u2heru58a4r68wysmdw9v40zznttmwrg0a6v9tq36pjak";
String receiver3 = "addr_test1qrp6x6aq2m28xhvxhqzufl0ff7x8gmzjejssrk29mx0q829dsty3hzmrl2k8jhwzghgxuzfjatgxlhg9wtl6ecv0v3cqf92rnh";
String receiver4 = "addr_test1qzj02w9f2lv3ayekxwr3hdxvn2umf3hyw6azze4rtczxzuwljt3yugepv88k3htyr60llrcj4a52hlytaka7jj3kf85szkyp6l";

PaymentTransaction paymentTransaction1 =
PaymentTransaction.builder()
.sender(sender)
.receiver(receiver1)
.unit(LOVELACE)
.amount(BigInteger.valueOf(1000000))
.fee(BigInteger.valueOf(4000)) //some low fee (invalid). Just for testing
.build();

PaymentTransaction paymentTransaction2 =
PaymentTransaction.builder()
.sender(sender)
.receiver(receiver2)
.unit("6b248bf1bbfac692610ca7e9873f988dc5e358b9229be8d6363aedd34d59546f6b656e")
.amount(BigInteger.valueOf(15))
.build();

PaymentTransaction paymentTransaction3 =
PaymentTransaction.builder()
.sender(sender)
.receiver(receiver3)
.amount(BigInteger.valueOf(4))
.unit("329728f73683fe04364631c27a7912538c116d802416ca1eaf2d7a96736174636f696e")
.build();

PaymentTransaction paymentTransaction4 =
PaymentTransaction.builder()
.sender(sender)
.receiver(receiver4)
.amount(BigInteger.valueOf(2))
.unit("b9bd3fb4511908402fbef848eece773bb44c867c25ac8c08d9ec3313696e746a")
.build();

//Calculate total fee for all 4 payment transactions and set in one of the payment transaction
BigInteger fee = feeCalculationService.calculateFee(Arrays.asList(paymentTransaction1, paymentTransaction2, paymentTransaction3, paymentTransaction4),
TransactionDetailsParams
.builder()
.ttl(getTtl())
.build(), null);
paymentTransaction1.setFee(fee);

Result<TransactionResult> result = transactionHelperService.transfer(Arrays.asList(paymentTransaction1, paymentTransaction2, paymentTransaction3, paymentTransaction4),
TransactionDetailsParams.builder().ttl(getTtl()).build());

if(result.isSuccessful())
System.out.println("Transaction Id: " + result.getValue());
else
System.out.println("Transaction failed: " + result);

System.out.println(result);
waitForTransaction(result);
assertThat(result.isSuccessful(), is(true));
}

@Test
void mintToken() throws CborSerializationException, AddressExcepion, ApiException {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,8 @@ public static BigInteger assetFromDecimal(BigDecimal doubleAmout, long decimals)

return amount.toBigInteger();
}

public static BigInteger adaToLovelace(double amount) {
return adaToLovelace(BigDecimal.valueOf(amount));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.bloxbean.cardano.client.plutus.api;

import com.bloxbean.cardano.client.transaction.spec.ConstrPlutusData;
import com.bloxbean.cardano.client.transaction.spec.PlutusData;

public interface PlutusObjectConverter {

public ConstrPlutusData convertToPlutusData(Object o);

public <T> T convertToObject(PlutusData plutusData, Class<T> tClass);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.bloxbean.cardano.client.plutus.api;

import com.bloxbean.cardano.client.backend.exception.ApiException;
import com.bloxbean.cardano.client.backend.model.Utxo;

import java.util.List;
import java.util.Set;
import java.util.function.Predicate;

public interface ScriptUtxoSelection {
/**
* Find the first utxo matching the predicate
* @param scriptAddress Script address
* @param predicate Predicate to filter utxos
* @return Utxo
* @throws ApiException
*/
Utxo findFirst(String scriptAddress, Predicate<Utxo> predicate) throws ApiException;

/**
* Find the first utxo matching the predicate
* @param scriptAddress Script address
* @param predicate Predicate to filter utxos
* @param excludeUtxos Utxos to exclude
* @return Utxo
* @throws ApiException
*/
Utxo findFirst(String scriptAddress, Predicate<Utxo> predicate, Set<Utxo> excludeUtxos) throws ApiException;

/**
* Find all utxos matching the predicate
* @param scriptAddress Script address
* @param predicate Predicate to filter utxos
* @return List of Utxos
* @throws ApiException
*/
List<Utxo> findAll(String scriptAddress, Predicate<Utxo> predicate) throws ApiException;

/**
* Find all utxos matching the predicate
* @param scriptAddress ScriptAddress Script address
* @param predicate Predicate Predicate to filter utxos
* @param excludeUtxos Utxos to exclude
* @return List of Utxos
* @throws ApiException
*/
List<Utxo> findAll(String scriptAddress, Predicate<Utxo> predicate, Set<Utxo> excludeUtxos) throws ApiException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.bloxbean.cardano.client.plutus.impl;

import com.bloxbean.cardano.client.backend.api.UtxoService;
import com.bloxbean.cardano.client.backend.common.OrderEnum;
import com.bloxbean.cardano.client.backend.exception.ApiException;
import com.bloxbean.cardano.client.backend.model.Result;
import com.bloxbean.cardano.client.backend.model.Utxo;
import com.bloxbean.cardano.client.plutus.api.ScriptUtxoSelection;

import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

//TODO -- Unit tests pending
public class DefaultScriptUtxoSelection implements ScriptUtxoSelection {
private UtxoService utxoService;

public DefaultScriptUtxoSelection(UtxoService utxoService) {
this.utxoService = utxoService;
}

@Override
public Utxo findFirst(String scriptAddress, Predicate<Utxo> predicate) throws ApiException {
return findFirst(scriptAddress, predicate, Collections.EMPTY_SET);
}

@Override
public Utxo findFirst(String scriptAddress, Predicate<Utxo> predicate, Set<Utxo> excludeUtxos) throws ApiException {
boolean canContinue = true;
int i = 1;

while(canContinue) {
Result<List<Utxo>> result = utxoService.getUtxos(scriptAddress, getUtxoFetchSize(),
i++, getUtxoFetchOrder());
if(result.code() == 200) {
List<Utxo> fetchData = result.getValue();

List<Utxo> data = (fetchData);
if(data == null || data.isEmpty())
canContinue = false;

Optional<Utxo> option = data.stream().filter(predicate).findFirst();
if (option.isPresent())
return option.get();

} else {
canContinue = false;
throw new ApiException(String.format("Unable to get enough Utxos for address : %s, reason: %s", scriptAddress, result.getResponse()));
}
}

return null;
}

@Override
public List<Utxo> findAll(String scriptAddress, Predicate<Utxo> predicate) throws ApiException {
return findAll(scriptAddress, predicate);
}

@Override
public List<Utxo> findAll(String scriptAddress, Predicate<Utxo> predicate, Set<Utxo> excludeUtxos) throws ApiException {
boolean canContinue = true;
int i = 1;

List<Utxo> utxoList = new ArrayList<>();
while(canContinue) {
Result<List<Utxo>> result = utxoService.getUtxos(scriptAddress, getUtxoFetchSize(),
i++, getUtxoFetchOrder());
if(result.code() == 200) {
List<Utxo> fetchData = result.getValue();

List<Utxo> data = (fetchData);
if(data == null || data.isEmpty())
canContinue = false;

List<Utxo> filterUtxos = data.stream().filter(predicate).collect(Collectors.toList());
utxoList.addAll(filterUtxos);

} else {
canContinue = false;
throw new ApiException(String.format("Unable to get enough Utxos for address : %s, reason: %s", scriptAddress, result.getResponse()));
}
}

return null;
}

private OrderEnum getUtxoFetchOrder() {
return OrderEnum.asc;
}

private int getUtxoFetchSize() {
return 100;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,30 @@
@NoArgsConstructor
@Builder
public class ConstrPlutusData implements PlutusData {
private static final long GENERAL_FORM_TAG = 102;
private long tag;
private long alternative;
private ListPlutusData data;

// see: https://github.com/input-output-hk/plutus/blob/1f31e640e8a258185db01fa899da63f9018c0e85/plutus-core/plutus-core/src/PlutusCore/Data.hs#L61
// We don't directly serialize the alternative in the tag, instead the scheme is:
// - Alternatives 0-6 -> tags 121-127, followed by the arguments in a list
// - Alternatives 7-127 -> tags 1280-1400, followed by the arguments in a list
// - Any alternatives, including those that don't fit in the above -> tag 102 followed by a list containing
// an unsigned integer for the actual alternative, and then the arguments in a (nested!) list.
private static final long GENERAL_FORM_TAG = 102;

@Override
public DataItem serialize() throws CborSerializationException {
boolean isCompact = isTagCompact();
Long cborTag = alternativeToCompactCborTag(alternative);
DataItem dataItem = null;

if (isCompact) {
if (cborTag != null) {
// compact form
dataItem = data.serialize();
dataItem.setTag(tag);
dataItem.setTag(cborTag);
} else {
//general form
Array constrArray = new Array();
constrArray.add(new UnsignedInteger(tag));
constrArray.add(new UnsignedInteger(alternative));
constrArray.add(data.serialize());
dataItem = constrArray;
dataItem.setTag(GENERAL_FORM_TAG);
Expand All @@ -43,35 +51,46 @@ public DataItem serialize() throws CborSerializationException {
}

public static ConstrPlutusData deserialize(DataItem di) throws CborDeserializationException {
Tag diTag = di.getTag();
Long tag = null;
Tag tag = di.getTag();
Long alternative = null;
ListPlutusData data = null;

if (GENERAL_FORM_TAG == diTag.getValue()) { //general form
if (GENERAL_FORM_TAG == tag.getValue()) { //general form
Array constrArray = (Array) di;
List<DataItem> dataItems = constrArray.getDataItems();

if (dataItems.size() != 2)
throw new CborDeserializationException("Cbor deserialization failed. Expected 2 DataItem, found : " + dataItems.size());

tag = ((UnsignedInteger) dataItems.get(0)).getValue().longValue();
alternative = ((UnsignedInteger) dataItems.get(0)).getValue().longValue();
data = ListPlutusData.deserialize((Array) dataItems.get(1));

} else { //concise form
tag = diTag.getValue();
alternative = compactCborTagToAlternative(tag.getValue());
data = ListPlutusData.deserialize((Array) di);
}

return ConstrPlutusData.builder()
.tag(tag)
.alternative(alternative)
.data(data)
.build();
}

private boolean isTagCompact() {
if ((tag >= 121 && tag <= 127) || (tag >= 1280 && tag <= 1400))
return true;
else
return false;
private static Long alternativeToCompactCborTag(long alt) {
if (alt <= 6) {
return 121 + alt;
} else if (alt >= 7 && alt <= 127) {
return 1280 - 7 + alt;
} else
return null;
}

private static Long compactCborTagToAlternative(long cborTag) {
if (cborTag >= 121 && cborTag <= 127) {
return cborTag - 121;
} else if (cborTag >= 1280 && cborTag <= 1400) {
return cborTag - 1280 + 7;
} else
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import co.nstant.in.cbor.model.Array;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.Special;
import com.bloxbean.cardano.client.exception.CborDeserializationException;
import com.bloxbean.cardano.client.exception.CborSerializationException;
import lombok.AllArgsConstructor;
Expand Down Expand Up @@ -32,6 +33,11 @@ public DataItem serialize() throws CborSerializationException {
return null;

Array plutusDataArray = new Array();

if (plutusDataList.size() == 0)
return plutusDataArray;

plutusDataArray.setChunked(true);
for (PlutusData plutusData : plutusDataList) {
DataItem di = plutusData.serialize();
if (di == null) {
Expand All @@ -40,6 +46,7 @@ public DataItem serialize() throws CborSerializationException {

plutusDataArray.add(di);
}
plutusDataArray.add(Special.BREAK);

return plutusDataArray;
}
Expand All @@ -50,6 +57,8 @@ public static ListPlutusData deserialize(Array arrayDI) throws CborDeserializati

ListPlutusData listPlutusData = new ListPlutusData();
for (DataItem di : arrayDI.getDataItems()) {
if (di == Special.BREAK)
break;
PlutusData plutusData = PlutusData.deserialize(di);
if (plutusData == null)
throw new CborDeserializationException("Null value found during PlutusData de-serialization");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ public interface PlutusData {
// / big_int
// / bounded_bytes

// big_int = int / big_uint / big_nint ; New
// big_uint = #6.2(bounded_bytes) ; New
// big_nint = #6.3(bounded_bytes) ; New

DataItem serialize() throws CborSerializationException;

static PlutusData deserialize(DataItem dataItem) throws CborDeserializationException {
Expand All @@ -29,7 +33,11 @@ static PlutusData deserialize(DataItem dataItem) throws CborDeserializationExcep
} else if (dataItem instanceof ByteString) {
return BytesPlutusData.deserialize((ByteString) dataItem);
} else if (dataItem instanceof Array) {
return ListPlutusData.deserialize((Array) dataItem);
if (dataItem.getTag() == null) {
return ListPlutusData.deserialize((Array) dataItem);
} else { //Tag found .. try Constr
return ConstrPlutusData.deserialize(dataItem);
}
} else if (dataItem instanceof Map) {
return MapPlutusData.deserialize((Map) dataItem);
} else
Expand Down
Loading

0 comments on commit 8284f9c

Please sign in to comment.