Skip to content

Commit 3e5129a

Browse files
committed
Fix Crowdfund validator tests and remove double cast
LinkedList validator demo
1 parent b04945c commit 3e5129a

7 files changed

Lines changed: 1401 additions & 9 deletions

File tree

src/main/java/com/example/cftemplates/factory/onchain/CfFactoryValidator.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,8 @@ static boolean checkSeedConsumed(JulcList<TxInInfo> inputs) {
9999

100100
static boolean listContainsBytes(JulcList<byte[]> list, byte[] target) {
101101
boolean found = false;
102-
PlutusData targetData = Builtins.bData(target);
103102
for (var item : list) {
104-
PlutusData itemData = (PlutusData)(Object) item;
105-
if (Builtins.equalsData(itemData, targetData)) {
103+
if (item.equals(target)) {
106104
found = true;
107105
break;
108106
}

src/main/java/com/example/linkedlist/offchain/LinkedListDemo.java

Lines changed: 430 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package com.example.linkedlist.onchain;
2+
3+
import com.bloxbean.cardano.julc.core.PlutusData;
4+
import com.bloxbean.cardano.julc.ledger.*;
5+
import com.bloxbean.cardano.julc.stdlib.Builtins;
6+
import com.bloxbean.cardano.julc.stdlib.annotation.OnchainLibrary;
7+
import com.bloxbean.cardano.julc.stdlib.lib.ByteStringLib;
8+
import com.bloxbean.cardano.julc.stdlib.lib.OutputLib;
9+
import com.bloxbean.cardano.julc.stdlib.lib.ValuesLib;
10+
11+
import java.math.BigInteger;
12+
13+
/**
14+
* Reusable on-chain validation logic for a linked list stored as UTXOs.
15+
* Each node is a separate UTXO holding a unique NFT; nodes link via datum fields.
16+
* <p>
17+
* Methods are ordered bottom-up (helpers before callers) for single-pass compilation.
18+
*/
19+
@OnchainLibrary
20+
public class LinkedListLib {
21+
22+
record ListElement(PlutusData userData, byte[] nextKey) {}
23+
24+
// === Helpers ===
25+
26+
static byte[] extractNodeKey(byte[] tokenName, int prefixLen) {
27+
return ByteStringLib.drop(tokenName, prefixLen);
28+
}
29+
30+
static byte[] buildTokenName(byte[] prefix, byte[] key) {
31+
return ByteStringLib.append(prefix, key);
32+
}
33+
34+
static boolean isRootToken(byte[] tokenName, byte[] rootKey) {
35+
return tokenName.equals(rootKey);
36+
}
37+
38+
static boolean requireListTokensMintedOrBurned(Value mint, byte[] policyId) {
39+
return ValuesLib.containsPolicy(mint, policyId);
40+
}
41+
42+
// === Validation ===
43+
44+
static boolean validateInit(TxOut rootOutput, Value mint, byte[] policyId,
45+
byte[] rootKey, Address scriptAddr) {
46+
boolean atScript = Builtins.equalsData(rootOutput.address(), scriptAddr);
47+
48+
ListElement datum = (ListElement)(Object) OutputLib.getInlineDatum(rootOutput);
49+
boolean emptyNext = datum.nextKey().equals(Builtins.emptyByteString());
50+
51+
BigInteger rootQty = ValuesLib.assetOf(mint, policyId, rootKey);
52+
boolean rootMinted = rootQty.compareTo(BigInteger.ONE) == 0;
53+
54+
BigInteger mintCount = ValuesLib.countTokensWithQty(mint, policyId, BigInteger.ONE);
55+
boolean onlyOne = mintCount.compareTo(BigInteger.ONE) == 0;
56+
57+
return atScript && emptyNext && rootMinted && onlyOne;
58+
}
59+
60+
static boolean validateDeinit(TxOut rootInputResolved, Value mint, byte[] policyId,
61+
byte[] rootKey) {
62+
ListElement datum = (ListElement)(Object) OutputLib.getInlineDatum(rootInputResolved);
63+
boolean emptyNext = datum.nextKey().equals(Builtins.emptyByteString());
64+
65+
BigInteger rootQty = ValuesLib.assetOf(mint, policyId, rootKey);
66+
boolean rootBurned = rootQty.compareTo(BigInteger.ONE.negate()) == 0;
67+
68+
BigInteger burnCount = ValuesLib.countTokensWithQty(mint, policyId, BigInteger.ONE.negate());
69+
boolean onlyOne = burnCount.compareTo(BigInteger.ONE) == 0;
70+
71+
return emptyNext && rootBurned && onlyOne;
72+
}
73+
74+
static boolean validateInsert(TxOut anchorInputResolved, TxOut contAnchorOutput,
75+
TxOut newElementOutput, Value mint, byte[] policyId,
76+
byte[] rootKey, byte[] prefix, int prefixLen,
77+
Address scriptAddr) {
78+
// Discover anchor's NFT token name
79+
byte[] anchorTokenName = ValuesLib.findTokenName(
80+
anchorInputResolved.value(), policyId, BigInteger.ONE);
81+
boolean anchorIsRoot = isRootToken(anchorTokenName, rootKey);
82+
83+
// Discover newly minted NFT token name and extract key
84+
byte[] newTokenName = ValuesLib.findTokenName(mint, policyId, BigInteger.ONE);
85+
byte[] newKey = extractNodeKey(newTokenName, prefixLen);
86+
87+
// Token name must be prefix + key
88+
byte[] expectedName = buildTokenName(prefix, newKey);
89+
boolean nameCorrect = newTokenName.equals(expectedName);
90+
91+
// Anchor NFT preserved in continuing output
92+
BigInteger contQty = ValuesLib.assetOf(contAnchorOutput.value(), policyId, anchorTokenName);
93+
boolean anchorPreserved = contQty.compareTo(BigInteger.ONE) == 0;
94+
95+
// Continuing anchor and new element must be at script address
96+
boolean contAtScript = Builtins.equalsData(contAnchorOutput.address(), scriptAddr);
97+
boolean newAtScript = Builtins.equalsData(newElementOutput.address(), scriptAddr);
98+
99+
// Extract datums
100+
ListElement anchorOld = (ListElement)(Object) OutputLib.getInlineDatum(anchorInputResolved);
101+
ListElement contAnchor = (ListElement)(Object) OutputLib.getInlineDatum(contAnchorOutput);
102+
ListElement newElement = (ListElement)(Object) OutputLib.getInlineDatum(newElementOutput);
103+
104+
// Anchor userData unchanged
105+
boolean dataUnchanged = Builtins.equalsData(anchorOld.userData(), contAnchor.userData());
106+
107+
// Continuing anchor's nextKey = new node's key
108+
boolean contNextOk = contAnchor.nextKey().equals(newKey);
109+
110+
// New element's nextKey = anchor's old nextKey
111+
boolean newNextOk = newElement.nextKey().equals(anchorOld.nextKey());
112+
113+
// Ordering: anchorKey < newKey (skip if root), newKey < oldNextKey (skip if end)
114+
byte[] oldNextKey = anchorOld.nextKey();
115+
boolean insertAtEnd = oldNextKey.equals(Builtins.emptyByteString());
116+
byte[] anchorKey = extractNodeKey(anchorTokenName, prefixLen);
117+
boolean orderOk = (anchorIsRoot || ByteStringLib.lessThan(anchorKey, newKey))
118+
&& (insertAtEnd || ByteStringLib.lessThan(newKey, oldNextKey));
119+
120+
// Exactly 1 new NFT minted under this policy
121+
BigInteger mintCount = ValuesLib.countTokensWithQty(mint, policyId, BigInteger.ONE);
122+
boolean exactlyOne = mintCount.compareTo(BigInteger.ONE) == 0;
123+
124+
return nameCorrect && anchorPreserved && contAtScript && newAtScript
125+
&& dataUnchanged && contNextOk && newNextOk && orderOk && exactlyOne;
126+
}
127+
128+
static boolean validateRemove(TxOut anchorInputResolved, TxOut removingInputResolved,
129+
TxOut contAnchorOutput, Value mint, byte[] policyId,
130+
byte[] rootKey, byte[] prefix, int prefixLen,
131+
Address scriptAddr) {
132+
// Discover token names
133+
byte[] anchorTokenName = ValuesLib.findTokenName(
134+
anchorInputResolved.value(), policyId, BigInteger.ONE);
135+
byte[] removingTokenName = ValuesLib.findTokenName(
136+
removingInputResolved.value(), policyId, BigInteger.ONE);
137+
byte[] removingKey = extractNodeKey(removingTokenName, prefixLen);
138+
139+
// Extract datums
140+
ListElement anchorDatum = (ListElement)(Object) OutputLib.getInlineDatum(anchorInputResolved);
141+
ListElement removingDatum = (ListElement)(Object) OutputLib.getInlineDatum(removingInputResolved);
142+
ListElement contDatum = (ListElement)(Object) OutputLib.getInlineDatum(contAnchorOutput);
143+
144+
// Anchor must link to the removing node
145+
boolean anchorLinksToRemoving = anchorDatum.nextKey().equals(removingKey);
146+
147+
// Anchor NFT preserved in continuing output
148+
BigInteger contQty = ValuesLib.assetOf(contAnchorOutput.value(), policyId, anchorTokenName);
149+
boolean anchorPreserved = contQty.compareTo(BigInteger.ONE) == 0;
150+
151+
// Continuing anchor at script address
152+
boolean contAtScript = Builtins.equalsData(contAnchorOutput.address(), scriptAddr);
153+
154+
// Anchor userData unchanged
155+
boolean dataUnchanged = Builtins.equalsData(anchorDatum.userData(), contDatum.userData());
156+
157+
// Continuing anchor's nextKey = removing node's nextKey (skip over)
158+
boolean skipCorrect = contDatum.nextKey().equals(removingDatum.nextKey());
159+
160+
// Removing node's NFT burned
161+
BigInteger burnQty = ValuesLib.assetOf(mint, policyId, removingTokenName);
162+
boolean nftBurned = burnQty.compareTo(BigInteger.ONE.negate()) == 0;
163+
164+
// Exactly 1 token burned
165+
BigInteger burnCount = ValuesLib.countTokensWithQty(mint, policyId, BigInteger.ONE.negate());
166+
boolean exactlyOneBurned = burnCount.compareTo(BigInteger.ONE) == 0;
167+
168+
return anchorLinksToRemoving && anchorPreserved && contAtScript && dataUnchanged
169+
&& skipCorrect && nftBurned && exactlyOneBurned;
170+
}
171+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package com.example.linkedlist.onchain;
2+
3+
import com.bloxbean.cardano.julc.core.PlutusData;
4+
import com.bloxbean.cardano.julc.ledger.*;
5+
import com.bloxbean.cardano.julc.stdlib.annotation.Entrypoint;
6+
import com.bloxbean.cardano.julc.stdlib.annotation.MultiValidator;
7+
import com.bloxbean.cardano.julc.stdlib.annotation.Param;
8+
import com.bloxbean.cardano.julc.stdlib.annotation.Purpose;
9+
import com.bloxbean.cardano.julc.stdlib.lib.ContextsLib;
10+
11+
import java.math.BigInteger;
12+
import java.util.Optional;
13+
14+
/**
15+
* On-chain linked list validator — MINT + SPEND.
16+
* <p>
17+
* Each list element is a UTXO holding a unique NFT. Nodes link via datum nextKey fields.
18+
* The MINT policy validates structural mutations (init/deinit/insert/remove).
19+
* The SPEND validator delegates to the MINT policy via the coupling pattern.
20+
*/
21+
@MultiValidator
22+
public class LinkedListValidator {
23+
24+
@Param static byte[] rootKey; // e.g. "ROOT" — token name for the root NFT
25+
@Param static byte[] prefix; // e.g. "NODE" — prefix for node token names
26+
@Param static BigInteger prefixLen; // e.g. 4 — byte length of prefix
27+
28+
// --- Redeemer variants (tag order = position in permits) ---
29+
30+
sealed interface ListAction permits InitList, DeinitList, InsertNode, RemoveNode {}
31+
record InitList(BigInteger rootOutputIndex) implements ListAction {} // tag 0
32+
record DeinitList(BigInteger rootInputIndex) implements ListAction {} // tag 1
33+
record InsertNode(BigInteger anchorInputIndex,
34+
BigInteger contAnchorOutputIndex,
35+
BigInteger newElementOutputIndex) implements ListAction {} // tag 2
36+
record RemoveNode(BigInteger anchorInputIndex,
37+
BigInteger removingInputIndex,
38+
BigInteger contAnchorOutputIndex) implements ListAction {} // tag 3
39+
40+
@Entrypoint(purpose = Purpose.MINT)
41+
public static boolean mint(ListAction redeemer, ScriptContext ctx) {
42+
TxInfo txInfo = ctx.txInfo();
43+
ScriptInfo.MintingScript mintInfo = (ScriptInfo.MintingScript) ctx.scriptInfo();
44+
byte[] policyBytes = (byte[])(Object) mintInfo.policyId();
45+
46+
return switch (redeemer) {
47+
case InitList init -> {
48+
Address scriptAddr = new Address(
49+
new Credential.ScriptCredential((ScriptHash)(Object) policyBytes),
50+
Optional.empty());
51+
TxOut rootOutput = txInfo.outputs().get(init.rootOutputIndex().intValue());
52+
yield LinkedListLib.validateInit(
53+
rootOutput, txInfo.mint(), policyBytes, rootKey, scriptAddr);
54+
}
55+
case DeinitList deinit -> {
56+
TxInInfo rootInput = txInfo.inputs().get(deinit.rootInputIndex().intValue());
57+
yield LinkedListLib.validateDeinit(
58+
rootInput.resolved(), txInfo.mint(), policyBytes, rootKey);
59+
}
60+
case InsertNode insert -> {
61+
Address scriptAddr = new Address(
62+
new Credential.ScriptCredential((ScriptHash)(Object) policyBytes),
63+
Optional.empty());
64+
TxInInfo anchorInput = txInfo.inputs().get(insert.anchorInputIndex().intValue());
65+
TxOut contAnchorOutput = txInfo.outputs().get(insert.contAnchorOutputIndex().intValue());
66+
TxOut newElementOutput = txInfo.outputs().get(insert.newElementOutputIndex().intValue());
67+
yield LinkedListLib.validateInsert(
68+
anchorInput.resolved(), contAnchorOutput, newElementOutput,
69+
txInfo.mint(), policyBytes, rootKey, prefix, prefixLen.intValue(), scriptAddr);
70+
}
71+
case RemoveNode remove -> {
72+
Address scriptAddr = new Address(
73+
new Credential.ScriptCredential((ScriptHash)(Object) policyBytes),
74+
Optional.empty());
75+
TxInInfo anchorInput = txInfo.inputs().get(remove.anchorInputIndex().intValue());
76+
TxInInfo removingInput = txInfo.inputs().get(remove.removingInputIndex().intValue());
77+
TxOut contAnchorOutput = txInfo.outputs().get(remove.contAnchorOutputIndex().intValue());
78+
yield LinkedListLib.validateRemove(
79+
anchorInput.resolved(), removingInput.resolved(), contAnchorOutput,
80+
txInfo.mint(), policyBytes, rootKey, prefix, prefixLen.intValue(), scriptAddr);
81+
}
82+
};
83+
}
84+
85+
@Entrypoint(purpose = Purpose.SPEND)
86+
public static boolean spend(Optional<PlutusData> datum, PlutusData redeemer, ScriptContext ctx) {
87+
TxInfo txInfo = ctx.txInfo();
88+
byte[] ownHash = ContextsLib.ownHash(ctx);
89+
return LinkedListLib.requireListTokensMintedOrBurned(txInfo.mint(), ownHash);
90+
}
91+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.example.cftemplates.crowdfund;
2+
3+
import com.bloxbean.cardano.julc.core.PlutusData;
4+
import com.bloxbean.cardano.julc.testkit.JulcEval;
5+
import com.example.cftemplates.crowdfund.onchain.CfCrowdfundValidator;
6+
import org.junit.jupiter.api.Test;
7+
8+
import java.math.BigInteger;
9+
10+
import static org.junit.jupiter.api.Assertions.*;
11+
12+
/**
13+
* JulcEval unit tests for {@code sumMapValues} and {@code sumSignerDonations}
14+
* logic from CfCrowdfundValidator, compiled and run on the UPLC VM.
15+
* <p>
16+
* Uses {@code forClass()} with {@code @Param} support to compile the validator's
17+
* methods in their full context, avoiding the workarounds needed with
18+
* isolated {@code @OnchainLibrary} compilation.
19+
*/
20+
class CrowdfundMethodTest {
21+
22+
private final JulcEval eval = JulcEval.forClass(CfCrowdfundValidator.class,
23+
PlutusData.bytes(new byte[28]), // beneficiary (dummy)
24+
PlutusData.integer(10_000_000), // goal (dummy)
25+
PlutusData.integer(1_000_000_000)); // deadline (dummy)
26+
27+
// -- Test data --------------------------------------------------------
28+
29+
private static final byte[] PKH1 = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
30+
11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28};
31+
private static final byte[] PKH2 = new byte[]{28, 27, 26, 25, 24, 23, 22, 21, 20, 19,
32+
18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
33+
private static final byte[] PKH_UNKNOWN = new byte[]{99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
34+
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99};
35+
36+
/** Map { pkh1 -> 5_000_000, pkh2 -> 3_000_000 } */
37+
private static PlutusData twoEntryMap() {
38+
return PlutusData.map(
39+
new PlutusData.Pair(PlutusData.bytes(PKH1), PlutusData.integer(5_000_000)),
40+
new PlutusData.Pair(PlutusData.bytes(PKH2), PlutusData.integer(3_000_000)));
41+
}
42+
43+
/** Map { pkh1 -> 5_000_000 } */
44+
private static PlutusData singleEntryMap() {
45+
return PlutusData.map(
46+
new PlutusData.Pair(PlutusData.bytes(PKH1), PlutusData.integer(5_000_000)));
47+
}
48+
49+
/** Empty map */
50+
private static PlutusData emptyMap() {
51+
return PlutusData.map();
52+
}
53+
54+
private static PlutusData signersList(byte[]... pkhs) {
55+
PlutusData[] items = new PlutusData[pkhs.length];
56+
for (int i = 0; i < pkhs.length; i++) {
57+
items[i] = PlutusData.bytes(pkhs[i]);
58+
}
59+
return PlutusData.list(items);
60+
}
61+
62+
// -- sumMapValues tests -----------------------------------------------
63+
64+
@Test
65+
void sumMapValues_twoEntries() {
66+
BigInteger result = eval.call("sumMapValues", twoEntryMap()).asInteger();
67+
assertEquals(BigInteger.valueOf(8_000_000), result);
68+
}
69+
70+
@Test
71+
void sumMapValues_singleEntry() {
72+
BigInteger result = eval.call("sumMapValues", singleEntryMap()).asInteger();
73+
assertEquals(BigInteger.valueOf(5_000_000), result);
74+
}
75+
76+
@Test
77+
void sumMapValues_empty() {
78+
BigInteger result = eval.call("sumMapValues", emptyMap()).asInteger();
79+
assertEquals(BigInteger.ZERO, result);
80+
}
81+
82+
// -- sumSignerDonations tests -----------------------------------------
83+
84+
@Test
85+
void sumSignerDonations_oneSignerMatches() {
86+
BigInteger result = eval.call("sumSignerDonations",
87+
twoEntryMap(), signersList(PKH1)).asInteger();
88+
assertEquals(BigInteger.valueOf(5_000_000), result);
89+
}
90+
91+
@Test
92+
void sumSignerDonations_noSignerMatches() {
93+
BigInteger result = eval.call("sumSignerDonations",
94+
twoEntryMap(), signersList(PKH_UNKNOWN)).asInteger();
95+
assertEquals(BigInteger.ZERO, result);
96+
}
97+
98+
@Test
99+
void sumSignerDonations_allSignersMatch() {
100+
BigInteger result = eval.call("sumSignerDonations",
101+
twoEntryMap(), signersList(PKH1, PKH2)).asInteger();
102+
assertEquals(BigInteger.valueOf(8_000_000), result);
103+
}
104+
}

0 commit comments

Comments
 (0)