Skip to content

Commit 4af2afa

Browse files
committed
feat: minting using plutus scripts; rework hashscriptdata; added simple mint validator as spec; replace string use data as redeemer
1 parent 65b7259 commit 4af2afa

File tree

14 files changed

+1132
-142
lines changed

14 files changed

+1132
-142
lines changed
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
/**
2+
* @fileoverview Devnet tests for Plutus script minting
3+
*
4+
* Tests minting with PlutusV3 simple_mint script from the spec project.
5+
* Uses the simple_mint.simple_mint.mint validator which requires:
6+
* - MintRedeemer: Constr(0, [idx: Int]) where idx == 1 to succeed
7+
*/
8+
9+
import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest"
10+
import * as Cluster from "@evolution-sdk/devnet/Cluster"
11+
import * as Config from "@evolution-sdk/devnet/Config"
12+
import * as Genesis from "@evolution-sdk/devnet/Genesis"
13+
import { Core } from "@evolution-sdk/evolution"
14+
import * as CoreAddress from "@evolution-sdk/evolution/core/Address"
15+
import * as AssetName from "@evolution-sdk/evolution/core/AssetName"
16+
import * as Bytes from "@evolution-sdk/evolution/core/Bytes"
17+
import * as Data from "@evolution-sdk/evolution/core/Data"
18+
import * as PlutusV3 from "@evolution-sdk/evolution/core/PlutusV3"
19+
import * as PolicyId from "@evolution-sdk/evolution/core/PolicyId"
20+
import * as ScriptHash from "@evolution-sdk/evolution/core/ScriptHash"
21+
import * as Text from "@evolution-sdk/evolution/core/Text"
22+
import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl"
23+
24+
const CoreAssets = Core.Assets
25+
26+
describe("TxBuilder Plutus Minting (Devnet Submit)", () => {
27+
// ============================================================================
28+
// Devnet Setup
29+
// ============================================================================
30+
31+
let devnetCluster: Cluster.Cluster | undefined
32+
let genesisConfig: Config.ShelleyGenesis
33+
let genesisUtxos: ReadonlyArray<Core.UTxO.UTxO> = []
34+
35+
const TEST_MNEMONIC = "test test test test test test test test test test test test test test test test test test test test test test test sauce"
36+
37+
/**
38+
* simple_mint.simple_mint.mint validator from plutus.json
39+
*
40+
* This is a PlutusV3 minting policy that succeeds when:
41+
* - Redeemer is MintRedeemer { idx: 1 }
42+
*
43+
* Source: packages/evolution/test/spec/validators/simple_mint.ak
44+
*/
45+
const SIMPLE_MINT_COMPILED_CODE = "59012901010029800aba2aba1aab9faab9eaab9dab9a488888966002646465300130053754003370e90004c02000e601000491112cc004cdc3a400800913259800980218051baa002899192cc004c04000a266e3cdd7180798069baa0044890131008b201c375c601c00260166ea800a2c8048c030c028dd5002c56600266e1d200600489919912cc004c018c030dd500244c8c966002602400513371e6eb8c044c03cdd500324410131008b2020375c6020002601a6ea80122c8058dd698068009806980700098051baa0058acc004c00c012264b30013004300a37540051323259800980800144cdc39bad300f300d3754008900145900e1bad300e001300b37540051640246eb8c030c028dd5002c59008201040203007300800130070013003375400f149a26cac80081"
46+
47+
// Hash from plutus.json - this is the policy ID
48+
const SIMPLE_MINT_POLICY_ID_HEX = "5cee358e512c8064024b140fcdb7bc35bb4694d11ccccb7acb182b5c"
49+
50+
// Helper to create MintRedeemer PlutusData: Constr(0, [Int])
51+
const makeMintRedeemer = (idx: bigint): Data.Data =>
52+
Data.constr(0n, [Data.int(idx)])
53+
54+
// Create PlutusV3 script from compiled CBOR hex
55+
const makeSimpleMintScript = (): PlutusV3.PlutusV3 => {
56+
return new PlutusV3.PlutusV3({ bytes: Bytes.fromHex(SIMPLE_MINT_COMPILED_CODE) })
57+
}
58+
59+
const simpleMintScript = makeSimpleMintScript()
60+
61+
// Verify script hash matches expected policy ID
62+
const scriptHash = ScriptHash.fromScript(simpleMintScript)
63+
const calculatedPolicyId = ScriptHash.toHex(scriptHash)
64+
65+
const createTestClient = () =>
66+
createClient({
67+
network: 0,
68+
provider: {
69+
type: "kupmios",
70+
kupoUrl: "http://localhost:1444",
71+
ogmiosUrl: "http://localhost:1339"
72+
},
73+
wallet: {
74+
type: "seed",
75+
mnemonic: TEST_MNEMONIC,
76+
accountIndex: 0
77+
}
78+
})
79+
80+
beforeAll(async () => {
81+
// Verify our script hash calculation matches the blueprint
82+
expect(calculatedPolicyId).toBe(SIMPLE_MINT_POLICY_ID_HEX)
83+
84+
const testClient = createClient({
85+
network: 0,
86+
wallet: { type: "seed", mnemonic: TEST_MNEMONIC, accountIndex: 0 }
87+
})
88+
89+
const testAddress = await testClient.address()
90+
const testAddressHex = CoreAddress.toHex(testAddress)
91+
92+
genesisConfig = {
93+
...Config.DEFAULT_SHELLEY_GENESIS,
94+
slotLength: 0.02,
95+
epochLength: 50,
96+
activeSlotsCoeff: 1.0,
97+
initialFunds: { [testAddressHex]: 900_000_000_000 }
98+
}
99+
100+
// Pre-calculate genesis UTxOs
101+
genesisUtxos = await Genesis.calculateUtxosFromConfig(genesisConfig)
102+
103+
devnetCluster = await Cluster.make({
104+
clusterName: "plutus-minting-test",
105+
ports: { node: 6002, submit: 9003 },
106+
shelleyGenesis: genesisConfig,
107+
kupo: { enabled: true, port: 1444, logLevel: "Info" },
108+
ogmios: { enabled: true, port: 1339, logLevel: "info" }
109+
})
110+
111+
await Cluster.start(devnetCluster)
112+
await new Promise((resolve) => setTimeout(resolve, 3_000))
113+
}, 180_000)
114+
115+
afterAll(async () => {
116+
if (devnetCluster) {
117+
await Cluster.stop(devnetCluster)
118+
await Cluster.remove(devnetCluster)
119+
}
120+
}, 60_000)
121+
122+
// ============================================================================
123+
// Submit Tests
124+
// ============================================================================
125+
126+
it("should mint tokens with PlutusV3 simple_mint script", { timeout: 60_000 }, async () => {
127+
if (genesisUtxos.length === 0) {
128+
throw new Error("Genesis UTxOs not calculated")
129+
}
130+
131+
const client = createTestClient()
132+
const address = await client.address()
133+
134+
// Use pre-calculated genesis UTxOs (Kupo may not have synced yet)
135+
const genesisUtxo = genesisUtxos.find((u) => CoreAddress.toBech32(u.address) === CoreAddress.toBech32(address))
136+
if (!genesisUtxo) {
137+
throw new Error("Genesis UTxO not found for wallet address")
138+
}
139+
140+
const assetNameHex = Text.toHex("PlutusToken")
141+
const unit = SIMPLE_MINT_POLICY_ID_HEX + assetNameHex
142+
143+
// Create redeemer that will pass validation (idx == 1)
144+
const mintRedeemer = makeMintRedeemer(1n)
145+
146+
// Build, sign, and submit transaction with Plutus minting
147+
const signBuilder = await client
148+
.newTx()
149+
.attachScript({ script: simpleMintScript })
150+
.mintAssets({
151+
assets: CoreAssets.fromRecord({ [unit]: 1000n }),
152+
redeemer: mintRedeemer
153+
})
154+
.payToAddress({
155+
address,
156+
assets: CoreAssets.fromRecord({
157+
lovelace: 3_000_000n,
158+
[unit]: 1000n
159+
})
160+
})
161+
.build({ availableUtxos: [genesisUtxo] })
162+
163+
const tx = await signBuilder.toTransaction()
164+
expect(tx.body.mint).toBeDefined()
165+
166+
// Verify Plutus script is in witness set
167+
expect(tx.witnessSet.plutusV3Scripts).toBeDefined()
168+
expect(tx.witnessSet.plutusV3Scripts!.length).toBe(1)
169+
170+
// Verify redeemers with evaluated exUnits
171+
expect(tx.witnessSet.redeemers).toBeDefined()
172+
expect(tx.witnessSet.redeemers!.length).toBe(1)
173+
174+
const redeemer = tx.witnessSet.redeemers![0]
175+
expect(redeemer.tag).toBe("mint")
176+
expect(redeemer.exUnits.mem).toBeGreaterThan(0n)
177+
expect(redeemer.exUnits.steps).toBeGreaterThan(0n)
178+
179+
// Submit transaction
180+
const submitBuilder = await signBuilder.sign()
181+
let txHash: string
182+
try {
183+
txHash = await submitBuilder.submit()
184+
} catch (err) {
185+
// eslint-disable-next-line no-console
186+
console.error("Submission error:", err)
187+
// eslint-disable-next-line no-console
188+
console.error("Cause:", (err as { cause?: unknown }).cause)
189+
throw err
190+
}
191+
expect(txHash.length).toBe(64)
192+
193+
// eslint-disable-next-line no-console
194+
console.log(`✓ Submitted Plutus mint tx: ${txHash}`)
195+
196+
const confirmed = await client.awaitTx(txHash, 1000)
197+
expect(confirmed).toBe(true)
198+
199+
// Query wallet UTxOs and verify minted asset
200+
const utxos = await client.getWalletUtxos()
201+
let foundMintedAsset = false
202+
let mintedAmount = 0n
203+
204+
for (const utxo of utxos) {
205+
if (!utxo.assets.multiAsset) continue
206+
207+
for (const [policyIdKey, assetMap] of utxo.assets.multiAsset.map.entries()) {
208+
if (PolicyId.toHex(policyIdKey) === SIMPLE_MINT_POLICY_ID_HEX) {
209+
for (const [assetName, amount] of assetMap.entries()) {
210+
if (AssetName.toHex(assetName) === assetNameHex) {
211+
foundMintedAsset = true
212+
mintedAmount = amount
213+
}
214+
}
215+
}
216+
}
217+
}
218+
219+
expect(foundMintedAsset).toBe(true)
220+
expect(mintedAmount).toBe(1000n)
221+
})
222+
223+
it("should mint then burn tokens with PlutusV3 simple_mint script", { timeout: 60_000 }, async () => {
224+
const client = createTestClient()
225+
const address = await client.address()
226+
227+
const assetNameHex = Text.toHex("BurnToken")
228+
const unit = SIMPLE_MINT_POLICY_ID_HEX + assetNameHex
229+
230+
// Use pre-calculated genesis UTxOs or wallet UTxOs (from prior test)
231+
let availableUtxos = await client.getWalletUtxos()
232+
if (availableUtxos.length === 0) {
233+
// Fall back to genesis UTxOs if Kupo hasn't synced
234+
const genesisUtxo = genesisUtxos.find((u) => CoreAddress.toBech32(u.address) === CoreAddress.toBech32(address))
235+
if (!genesisUtxo) {
236+
throw new Error("Genesis UTxO not found for wallet address")
237+
}
238+
availableUtxos = [genesisUtxo]
239+
}
240+
241+
// Step 1: First mint tokens
242+
const mintRedeemer = makeMintRedeemer(1n)
243+
244+
const mintBuilder = await client
245+
.newTx()
246+
.attachScript({ script: simpleMintScript })
247+
.mintAssets({
248+
assets: CoreAssets.fromRecord({ [unit]: 1000n }),
249+
redeemer: mintRedeemer
250+
})
251+
.payToAddress({
252+
address,
253+
assets: CoreAssets.fromRecord({
254+
lovelace: 3_000_000n,
255+
[unit]: 1000n
256+
})
257+
})
258+
.build({ availableUtxos })
259+
260+
const mintTx = await mintBuilder.toTransaction()
261+
expect(mintTx.body.mint).toBeDefined()
262+
263+
// Submit and wait for confirmation
264+
const mintSubmitBuilder = await mintBuilder.sign()
265+
const mintTxHash = await mintSubmitBuilder.submit()
266+
// eslint-disable-next-line no-console
267+
console.log(`✓ Submitted Plutus mint tx (for burn test): ${mintTxHash}`)
268+
const mintConfirmed = await client.awaitTx(mintTxHash, 1000)
269+
expect(mintConfirmed).toBe(true)
270+
271+
// Step 2: Get UTxOs after minting - we need one with tokens to spend, and another for collateral
272+
const utxos = await client.getWalletUtxos()
273+
const utxoWithTokens = utxos.find((u) => {
274+
const hasToken = CoreAssets.getByUnit(u.assets, unit) > 0n
275+
return hasToken
276+
})
277+
278+
if (!utxoWithTokens) {
279+
throw new Error("UTxO with minted tokens not found")
280+
}
281+
282+
// Step 3: Now burn some of those tokens
283+
const burnRedeemer = makeMintRedeemer(1n)
284+
285+
// Build burn transaction - let client fetch UTxOs for collateral
286+
const burnBuilder = await client
287+
.newTx()
288+
.attachScript({ script: simpleMintScript })
289+
.collectFrom({ inputs: [utxoWithTokens] })
290+
.mintAssets({
291+
assets: CoreAssets.fromRecord({ [unit]: -500n }),
292+
redeemer: burnRedeemer
293+
})
294+
.payToAddress({
295+
address,
296+
assets: CoreAssets.fromRecord({
297+
lovelace: 1_500_000n,
298+
[unit]: 500n
299+
})
300+
})
301+
.build()
302+
303+
const burnTx = await burnBuilder.toTransaction()
304+
expect(burnTx.body.mint).toBeDefined()
305+
306+
// Verify the mint shows negative (burning)
307+
const mint = burnTx.body.mint!
308+
let foundBurn = false
309+
for (const [policyIdKey, assetMap] of mint.map.entries()) {
310+
if (PolicyId.toHex(policyIdKey) === SIMPLE_MINT_POLICY_ID_HEX) {
311+
for (const [assetName, amount] of assetMap.entries()) {
312+
if (AssetName.toHex(assetName) === assetNameHex && amount === -500n) {
313+
foundBurn = true
314+
}
315+
}
316+
}
317+
}
318+
expect(foundBurn).toBe(true)
319+
320+
// Submit burn transaction
321+
const burnSubmitBuilder = await burnBuilder.sign()
322+
const burnTxHash = await burnSubmitBuilder.submit()
323+
expect(burnTxHash.length).toBe(64)
324+
325+
// eslint-disable-next-line no-console
326+
console.log(`✓ Submitted Plutus burn tx: ${burnTxHash}`)
327+
328+
const burnConfirmed = await client.awaitTx(burnTxHash, 1000)
329+
expect(burnConfirmed).toBe(true)
330+
331+
// Verify the remaining tokens in wallet
332+
const utxosAfterBurn = await client.getWalletUtxos()
333+
let remainingTokenAmount = 0n
334+
for (const utxo of utxosAfterBurn) {
335+
remainingTokenAmount += CoreAssets.getByUnit(utxo.assets, unit)
336+
}
337+
expect(remainingTokenAmount).toBe(500n)
338+
})
339+
})

0 commit comments

Comments
 (0)