cardano-serialization-lib icon indicating copy to clipboard operation
cardano-serialization-lib copied to clipboard

Mint tokens in the "plutus way"

Open mateusap1 opened this issue 2 years ago • 23 comments

As I understand, in order to mint a token, you must use the function set_mint_scripts, providing a NativeScripts object. This NativeScript object determines until what time a token can be minted and by whom.

With the introduction of Plutus, though, we have a new (and better) way of minting assets, which is using Plutus scripts that allow or not the minting and burning of the assets based on arbitrary logic. I need to use this approach for my project, but I can't find any similar (to set_mint_scripts) function that require a PlutusScript (not native one).

Is there a workaround I could use (or maybe an actual implementation that I missed)? If not, are there plans to add support for minting of tokens using Plutus scripts?

mateusap1 avatar Dec 21 '21 12:12 mateusap1

As I understand, in order to mint a token, you must use the function set_mint_scripts, providing a NativeScripts object. This NativeScript object determines until what time a token can be minted and by whom.

With the introduction of Plutus, though, we have a new (and better) way of minting assets, which is using Plutus scripts that allow or not the minting and burning of the assets based on arbitrary logic. I need to use this approach for my project, but I can't find any similar (to set_mint_scripts) function that require a PlutusScript (not native one).

Is there a workaround I could use (or maybe an actual implementation that I missed)? If not, are there plans to add support for minting of tokens using Plutus scripts?

Hi, @mateusap1! Right now there's no separate support for any Plutus patterns specifically, only fundamental protocol types for Plutus are supported at the moment, and the TransactionBuilder has no special support for Plutus features yet at all.

By Plutus-controlled minting do you mean you would have posted some UTxO with your script and then every time you want to mint mroe of that token you would have to spend that UTxO in the tx for the script to validate the tx and the included mint, or something like that? I am curious how would you protect the minting policy from being mintable without havign to include that script input anyways, by just providing the key-hash witnesses.

Overall at the moment you can produce script outputs with no problem even by using TransactionBuilder if I am not mistaken as it does not care about the output types explicitly, so you can "deploy" a Plutus script with no problem. But support for spending script inputs is still being worked on for the transaction-builder

vsubhuman avatar Dec 21 '21 19:12 vsubhuman

Thank you for your answer.

By Plutus-controlled minting do you mean you would have posted some UTxO with your script and then every time you want to mint mroe of that token you would have to spend that UTxO in the tx for the script to validate the tx and the included mint, or something like that?

Well, kind of, the way it's implemented you don't have to spend any UTxO, it just executes the minting policy script (it has no datum as well). If you've seen the plutus pioneer program lectures, you saw that we are presented with a new way of minting tokens. You can make a minting policy script that runs every time someone tries to mint (or burn) this token (represented by an arbitrary token name and with the currency symbol corresponding to the script hash).

For example, you can make a script that only validates if the transaction is signed by someone (see mkPolicy function here). But, of course, there are many more interesting applications to this new way of handling minting policies (like making sure a certain value is sent to a certain address before minting), and this approach is fundamental for the application I'm trying to build.

I am curious how would you protect the minting policy from being mintable without havign to include that script input anyways, by just providing the key-hash witnesses.

In fact, this is already possible with cardano-cli and this blog post goes over how to mint tokens in this new way using cardano-cli. The mint_nft.sh script he uses is this one and, as you can see, mint requires a script file and a redeemer. So, to answer your question, you would actually need a script file (not as an input though, since it's not an UTxO) as one of your parameters.

If you could point me in a direction of how I would be able to implement this using the serialization-lib (even if I needed to modify the low-level stuff), I would greatly appreciate (of course, if it's actually possible). Also, it might be a good idea to add support for this in the future (if I'm right in assuming it's not supported yet)

mateusap1 avatar Dec 21 '21 20:12 mateusap1

Thank you for your answer.

By Plutus-controlled minting do you mean you would have posted some UTxO with your script and then every time you want to mint mroe of that token you would have to spend that UTxO in the tx for the script to validate the tx and the included mint, or something like that?

Well, kind of, the way it's implemented you don't have to spend any UTxO, it just executes the minting policy script (it has no datum as well). If you've seen the plutus pioneer program lectures, you saw that we are presented with a new way of minting tokens. You can make a minting policy script that runs every time someone tries to mint (or burn) this token (represented by an arbitrary token name and with the currency symbol corresponding to the script hash).

For example, you can make a script that only validates if the transaction is signed by someone (see mkPolicy function here). But, of course, there are many more interesting applications to this new way of handling minting policies (like making sure a certain value is sent to a certain address before minting), and this approach is fundamental for the application I'm trying to build.

I am curious how would you protect the minting policy from being mintable without havign to include that script input anyways, by just providing the key-hash witnesses.

In fact, this is already possible with cardano-cli and this blog post goes over how to mint tokens in this new way using cardano-cli. The mint_nft.sh script he uses is this one and, as you can see, mint requires a script file and a redeemer. So, to answer your question, you would actually need a script file (not as an input though, since it's not an UTxO) as one of your parameters.

If you could point me in a direction of how I would be able to implement this using the serialization-lib (even if I needed to modify the low-level stuff), I would greatly appreciate (of course, if it's actually possible). Also, it might be a good idea to add support for this in the future (if I'm right in assuming it's not supported yet)

Hi, I've had this issue myself and have recently found a workaround to mint tokens using plutus scripts. It uses the 'SpaceBudz' modified version of the Serialization Lib that can be found here: https://github.com/Berry-Pool/spacebudz/tree/main/src/cardano/market/custom_modules/%40emurgo.

There are some slight differences to how the transaction builder is used, but you can follow the example here: https://github.com/Berry-Pool/spacebudz/blob/main/src/cardano/market/index.js


The idea is to create a transaction as you normally would with the transaction builder and build the minting part of the script manually using the Mint and MintAsset classes of the serialization lib. Note that a downside of this method is that you need to manually set the fee, either by setting it to a larger value or by reliably estimating it from a different source (e.g maybe a dummy transaction in the CLI itself).

To mimic the --mint line of the cli, the minted assets need to be specified. Create the minted value as a multi-asset (to be included as output in the transaction builder) eg:

	const mintedAssets = Cardano.Assets.new();
	mintedAssets.insert(
		Cardano.AssetName.new(Buffer.from("MONT")), // Name
		Cardano.BigNum.from_str("1") // Quantity
	);

        // Add to multi asset with the policyId of the policy script 
        // (you can just hardcode value returned by the CLI)        
	const multiAsset = Cardano.MultiAsset.new();
	multiAsset.insert(
		Cardano.ScriptHash.from_bytes(
			Cardano.Ed25519KeyHash.from_bytes(
				fromHex(policyId)
			).to_bytes()
		),
		mintedAssets
	);

Add this multi asset to a Value object (this is the value object that you use in one of your outputs):

	let mintedValue = Cardano.Value.new(
		Cardano.BigNum.from_str("0")
	); // Tally minted value

	mintedValue.set_multiasset(multiAsset);

Create a 'Mint' object, which signals that this transaction includes a minting operation, balancing the output. Add the minted assets to this object (the code is essentially identical to above):

	const mint = Cardano.Mint.new();
	const mintAssets = Cardano.MintAssets.new();
	mintAssets.insert(
		Cardano.AssetName.new(Buffer.from("MONT")), // Name
		Cardano.Int.new(Cardano.BigNum.from_str("1")) // Quantity
	);

	mint.insert(
		Cardano.ScriptHash.from_bytes(
			Cardano.Ed25519KeyHash.from_bytes(
				fromHex(policyId)
			).to_bytes()
		),
		mintAssets
	);

Create a redeemer for the transaction in a similar way to the following:

function mintRedeemer(index) {
        // Create the redeemer data
	const redeemerData = Cardano.PlutusData.new_constr_plutus_data(
		Cardano.ConstrPlutusData.new(
		Cardano.Int.new_i32(REDEEMER_VALUE),
		Cardano.PlutusList.new()
		)
	);
        
        // redeemer itself
	const redeemer = Cardano.Redeemer.new(
	        Cardano.RedeemerTag.new_mint(),
		Cardano.BigNum.from_str(index),
		redeemerData,
		Cardano.ExUnits.new(
			Cardano.BigNum.from_str(EX_UNIT_A),
			Cardano.BigNum.from_str(EX_UNIT_B)
		)
	);

	return redeemer;
}

The difference here is the redeemer tag, which you set to Cardano.RedeemerTag.new_mint(). The index should correspond to the order of your inputs (if this is incorrectly set you will get a missing or extra redeemer error). If you are not spending any other script inputs, this is just 0.

You can then create your transaction in the normal way using the transaction builder, making sure all the inputs and outputs balance and setting the fee using TransactionBuilder.set_fee(). You include the policy script the same was as a validator script with transactionBuilder.set_plutus_scripts(YOUR_SCRIPTS). The minting redeemer with transactionBuilder.set_redeemers(YOUR_REDEEMERS). The witnesses need to be made manually, the example link I included shows how this is done.

You build the transaction body with TransactionBuilder.build() which returns the generated body. Using this returned object, the final part is to do transactionBody.set_mint(mint);, where mint is the Mint object you created earlier.

I know this was brief and maybe not ideal for your situation but let me know if you need any clarification!

Montel98 avatar Dec 22 '21 13:12 Montel98

Thank you very much @Montel98, that was very clear.

mateusap1 avatar Dec 22 '21 20:12 mateusap1

Thank you very much @Montel98, that was very clear.

No problem, also I forgot to mention to add collateral by using transactionBuilder.set_collateral(inputs).

Montel98 avatar Dec 22 '21 21:12 Montel98

I am getting an "INPUTS_EXHAUSTED" error when the app tries to do coin selection. Am I missing something? Do I need to add ADA to an additional output?

mateusap1 avatar Dec 23 '21 14:12 mateusap1

Oh I see now, I added the output with the minted values before making the coin selection, so the app went crazy trying to find an input with the assets I provided (which will be minted) and, of course, couldn't find any. So, for anyone that may find this thread, don't be stupid like me and add the minted value to the output after the coin selection is made.

mateusap1 avatar Dec 23 '21 15:12 mateusap1

The index should correspond to the order of your inputs (if this is incorrectly set you will get a missing or extra redeemer error). If you are not spending any other script inputs, this is just 0.

Sorry to bother you @Montel98, but when we mint a token we don't necessarily spend a script input, do we? In that case I am not sure what value my index should be, I'm using "0", but the console is giving me a web assembly "unreachable" error

mateusap1 avatar Dec 23 '21 22:12 mateusap1

The index should correspond to the order of your inputs (if this is incorrectly set you will get a missing or extra redeemer error). If you are not spending any other script inputs, this is just 0.

Sorry to bother you @Montel98, but when we mint a token we don't necessarily spend a script input, do we? In that case I am not sure what value my index should be, I'm using "0", but the console is giving me a web assembly "unreachable" error

No you're right you don't spend a script input, but the index is still required. Are you able to post the full error message? I might be able to help. I used 0 for my initial script, but that was without any script inputs.

Montel98 avatar Dec 23 '21 22:12 Montel98

Of course, I don't think it will be very helpful, but here it is:

Uncaught (in promise) RuntimeError: unreachable
    at 35c481a3c170e5a6ce28.wasm:0xb3eee
    at 35c481a3c170e5a6ce28.wasm:0xd494f
    at 35c481a3c170e5a6ce28.wasm:0xe5927
    at 35c481a3c170e5a6ce28.wasm:0xe4580
    at 35c481a3c170e5a6ce28.wasm:0xe598b
    at 35c481a3c170e5a6ce28.wasm:0xe33c1
    at 35c481a3c170e5a6ce28.wasm:0x3ebd7
    at 35c481a3c170e5a6ce28.wasm:0x98f58
    at 35c481a3c170e5a6ce28.wasm:0xa6527
    at 35c481a3c170e5a6ce28.wasm:0xe4717
    at 35c481a3c170e5a6ce28.wasm:0x106b4
    at 35c481a3c170e5a6ce28.wasm:0xd02ec
    at TransactionBuilder.add_change_if_needed (cardano_serialization_lib_bg.js?45ba:10344)
    at _callee2$ (buy.js?5930:349)
    at tryCatch (runtime.js?96cf:63)
    at Generator.invoke [as _invoke] (runtime.js?96cf:294)
    at Generator.eval [as next] (runtime.js?96cf:119)
    at asyncGeneratorStep (asyncToGenerator.js?1bd0:3)
    at _next (asyncToGenerator.js?1bd0:25)

mateusap1 avatar Dec 23 '21 22:12 mateusap1

I know this error is related to the redeemer, because when I comment the redeemer lines, the error disappears

mateusap1 avatar Dec 23 '21 22:12 mateusap1

This is the code I'm using

const redeemers = Loader.Cardano.Redeemers.new();
redeemers.add(mintRedeemer("0"));
txBuilder.set_redeemers(Loader.Cardano.Redeemers.from_bytes(redeemers.to_bytes()));

Where mintRedeemer is defined as

function mintRedeemer(index) {
  // Create the redeemer data
  const redeemerData = Loader.Cardano.PlutusData.new_constr_plutus_data(
    Loader.Cardano.ConstrPlutusData.new(
      Loader.Cardano.Int.new_i32(0),
      Loader.Cardano.PlutusList.new()
    )
  );

  // redeemer itself
  const redeemer = Loader.Cardano.Redeemer.new(
    Loader.Cardano.RedeemerTag.new_mint(),
    Loader.Cardano.BigNum.from_str(index),
    redeemerData,
    Loader.Cardano.ExUnits.new(
      Loader.Cardano.BigNum.from_str(EX_UNIT_A),
      Loader.Cardano.BigNum.from_str(EX_UNIT_B)
    )
  );

  return redeemer;
}

mateusap1 avatar Dec 23 '21 22:12 mateusap1

Of course, I don't think it will be very helpful, but here it is:

Uncaught (in promise) RuntimeError: unreachable
    at 35c481a3c170e5a6ce28.wasm:0xb3eee
    at 35c481a3c170e5a6ce28.wasm:0xd494f
    at 35c481a3c170e5a6ce28.wasm:0xe5927
    at 35c481a3c170e5a6ce28.wasm:0xe4580
    at 35c481a3c170e5a6ce28.wasm:0xe598b
    at 35c481a3c170e5a6ce28.wasm:0xe33c1
    at 35c481a3c170e5a6ce28.wasm:0x3ebd7
    at 35c481a3c170e5a6ce28.wasm:0x98f58
    at 35c481a3c170e5a6ce28.wasm:0xa6527
    at 35c481a3c170e5a6ce28.wasm:0xe4717
    at 35c481a3c170e5a6ce28.wasm:0x106b4
    at 35c481a3c170e5a6ce28.wasm:0xd02ec
    at TransactionBuilder.add_change_if_needed (cardano_serialization_lib_bg.js?45ba:10344)
    at _callee2$ (buy.js?5930:349)
    at tryCatch (runtime.js?96cf:63)
    at Generator.invoke [as _invoke] (runtime.js?96cf:294)
    at Generator.eval [as next] (runtime.js?96cf:119)
    at asyncGeneratorStep (asyncToGenerator.js?1bd0:3)
    at _next (asyncToGenerator.js?1bd0:25)

I'm not 100% sure but it looks like you are using add_change_if_needed()? You can't use that function as the transaction builder technically doesn't support minting and will freak out. You have to manually calculate the change and add that as an output. It's strange that it goes away when you comment out the redeemer though, the code is basically identical to how I did it.

Montel98 avatar Dec 23 '21 22:12 Montel98

Thank you again for your answer, I did change to set_fee and manually created the outputs (simulating add_change), but I still receive the same error (but in a different method).

Uncaught (in promise) RuntimeError: unreachable
    at 35c481a3c170e5a6ce28.wasm:0xb3eee
    at 35c481a3c170e5a6ce28.wasm:0xd494f
    at 35c481a3c170e5a6ce28.wasm:0xe5927
    at 35c481a3c170e5a6ce28.wasm:0xe4580
    at 35c481a3c170e5a6ce28.wasm:0xe598b
    at 35c481a3c170e5a6ce28.wasm:0xe33c1
    at 35c481a3c170e5a6ce28.wasm:0x3ebd7
    at 35c481a3c170e5a6ce28.wasm:0x98f58
    at 35c481a3c170e5a6ce28.wasm:0xc542b
    at TransactionBuilder.build (cardano_serialization_lib_bg.js?45ba:10374)
    at _callee2$ (buy.js?5930:378)
    at tryCatch (runtime.js?96cf:63)
    at Generator.invoke [as _invoke] (runtime.js?96cf:294)
    at Generator.eval [as next] (runtime.js?96cf:119)
    at asyncGeneratorStep (asyncToGenerator.js?1bd0:3)
    at _next (asyncToGenerator.js?1bd0:25)

mateusap1 avatar Dec 28 '21 10:12 mateusap1

The full createAccount function:

export async function createAccount(endpoint, project_id) {
  // Get Nami handler
  const cardano = window.cardano;

  // Get protocol parameters
  const protocolParameters = await getProtocolParameters(endpoint, project_id);

  // Converts the wallet address into a BECH32 format
  const paymentAddr = Loader.Cardano.Address.from_bytes(
    fromHex(await cardano.getChangeAddress())
  ).to_bech32();

  // Get's a list of UTxOs that belong to our user
  const rawUtxo = await cardano.getUtxos();

  // Go over each of these raw UTxOs and convert them to a better format
  const utxos = rawUtxo.map((u) =>
    Loader.Cardano.TransactionUnspentOutput.from_bytes(fromHex(u))
  );

  const mintedValue = getMintedValue(freeAddr, "token", 100);

  // Create an empty outputs variable
  const fakeOutputs = Loader.Cardano.TransactionOutputs.new();

  fakeOutputs.add(
    createOutput(
      Loader.Cardano.Address.from_bech32(testAddr),
      Loader.Cardano.Value.new(
        Loader.Cardano.min_ada_required(mintedValue, protocolParameters.minUtxo)
      ),
      protocolParameters.minUtxo
    )
  );

  const MULTIASSET_SIZE = 5848;
  const VALUE_SIZE = 5860;
  const totalAssets = 0;

  CoinSelection.setProtocolParameters(
    protocolParameters.minUtxo.to_str(),
    protocolParameters.linearFee.coefficient().to_str(),
    protocolParameters.linearFee.constant().to_str(),
    protocolParameters.maxTxSize.toString()
  );

  const selection = await CoinSelection.randomImprove(
    utxos,
    fakeOutputs,
    20 + totalAssets,
    protocolParameters.minUtxo.to_str()
  );

  const inputs = selection.input;
  const txBuilder = Loader.Cardano.TransactionBuilder.new(
    protocolParameters.linearFee,
    protocolParameters.minUtxo,
    protocolParameters.poolDeposit,
    protocolParameters.keyDeposit,
    protocolParameters.maxValSize,
    protocolParameters.maxTxSize
  );

  const collateral = (await window.cardano.getCollateral()).map((utxo) =>
    Loader.Cardano.TransactionUnspentOutput.from_bytes(fromHex(utxo))
  );
  if (collateral.length <= 0) throw new Error("NO_COLLATERAL");

  let value = Loader.Cardano.Value.new(Loader.Cardano.BigNum.from_str("0"));

  for (let i = 0; i < inputs.length; i++) {
    const utxo = inputs[i];
    txBuilder.add_input(
      utxo.output().address(),
      utxo.input(),
      utxo.output().amount()
    );

    value = value.checked_add(utxo.output().amount());
  }

  // Create an empty outputs variable
  const realOutputs = Loader.Cardano.TransactionOutputs.new();

  value = value.checked_sub(
    Loader.Cardano.Value.new(Loader.Cardano.BigNum.from_str(FEE))
  );
  value = value.checked_add(mintedValue);

  realOutputs.add(
    createOutput(
      Loader.Cardano.Address.from_bech32(paymentAddr),
      value,
      protocolParameters.minUtxo
    )
  );

  txBuilder.add_output(realOutputs.get(0));

  const redeemers = Loader.Cardano.Redeemers.new();

  redeemers.add(mintRedeemer("0"));
  txBuilder.set_redeemers(
    Loader.Cardano.Redeemers.from_bytes(redeemers.to_bytes())
  );

  const scripts = Loader.Cardano.PlutusScripts.new();

  scripts.add(serializeScript(freeScript));
  txBuilder.set_plutus_scripts(scripts);

  addCollateral(txBuilder, collateral);

  txBuilder.set_fee(Loader.Cardano.BigNum.from_str(FEE));

  const txBody = txBuilder.build();

  const mint = getMint(freeAddr, "token", 100);
  txBody.set_mint(mint);

  const transaction = Loader.Cardano.Transaction.new(
    txBody,
    Loader.Cardano.TransactionWitnessSet.new()
  );

  const size = transaction.to_bytes().length * 2;
  if (size > protocolParameters.maxTxSize)
    throw new Error("Transaction too big");

  const signatureWitness = await cardano.signTx(
    Buffer.from(transaction.to_bytes(), "hex").toString("hex")
  );

  const witnessSet = Loader.Cardano.TransactionWitnessSet.from_bytes(
    Buffer.from(signatureWitness, "hex")
  );

  witnessSet.set_plutus_scripts(scripts);
  witnessSet.set_redeemers(redeemers);

  const signedTx = Loader.Cardano.Transaction.new(
    transaction.body(),
    witnessSet
  );

  const txhash = await cardano.submitTx(
    Buffer.from(signedTx.to_bytes(), "hex").toString("hex")
  );

  return txhash;
}

mateusap1 avatar Dec 28 '21 10:12 mateusap1

Thank you again for your answer, I did change to set_fee and manually created the outputs (simulating add_change), but I still receive the same error (but in a different method).

Uncaught (in promise) RuntimeError: unreachable
    at 35c481a3c170e5a6ce28.wasm:0xb3eee
    at 35c481a3c170e5a6ce28.wasm:0xd494f
    at 35c481a3c170e5a6ce28.wasm:0xe5927
    at 35c481a3c170e5a6ce28.wasm:0xe4580
    at 35c481a3c170e5a6ce28.wasm:0xe598b
    at 35c481a3c170e5a6ce28.wasm:0xe33c1
    at 35c481a3c170e5a6ce28.wasm:0x3ebd7
    at 35c481a3c170e5a6ce28.wasm:0x98f58
    at 35c481a3c170e5a6ce28.wasm:0xc542b
    at TransactionBuilder.build (cardano_serialization_lib_bg.js?45ba:10374)
    at _callee2$ (buy.js?5930:378)
    at tryCatch (runtime.js?96cf:63)
    at Generator.invoke [as _invoke] (runtime.js?96cf:294)
    at Generator.eval [as next] (runtime.js?96cf:119)
    at asyncGeneratorStep (asyncToGenerator.js?1bd0:3)
    at _next (asyncToGenerator.js?1bd0:25)

Did you manage to get it working? It looks like you aren't using the custom serialisation lib used in SpaceBudz? It won't work without it and is most likely what is causing the error as the rest of your code looks fine to me.

Montel98 avatar Jan 07 '22 22:01 Montel98

I just gave up and I'm trying to use the PAB now, I'll wait for official support. I am sure I am using the custom module since I even removed serialization-lib with npm. In any case, thanks for all the help.

mateusap1 avatar Jan 13 '22 19:01 mateusap1

I'll leave the issue open since I think integration with plutus minting policies is still a critical feature, which is not currently supported (officially)

mateusap1 avatar Jan 13 '22 19:01 mateusap1

I agree with this. I am using the spacebudz custom implementation, but I would rather be using the officially supported libraries to make sure updates are included. However, currently, there is no option in this fork.

kyle-manuel-franz avatar Mar 23 '22 20:03 kyle-manuel-franz

Is there any way to mint token with plutus script and serialization-lib by Nami wallet for now

nmaddp1995 avatar Jul 15 '22 08:07 nmaddp1995

Is there any way to mint token with plutus script and serialization-lib by Nami wallet for now

Not yet, but we will add it right in the next version

vsubhuman avatar Jul 15 '22 09:07 vsubhuman

Got it, but is there any way to mint nft with plutus script by Nami wallet right now (any lib can be oke)

nmaddp1995 avatar Jul 15 '22 09:07 nmaddp1995

how to add metadata.json and mint NFT?

lynch- avatar Sep 11 '22 12:09 lynch-

Plutus minting now available in the new CSL version 11.2.0

lisicky avatar Dec 08 '22 22:12 lisicky