Martify marketplace using cardano-serialization-lib

Hi guys,

I try using
GitHub - MartifyLabs/Martify: NFT Marketplace on the Cardano Blockchain. Powered by Plutus Smart Contracts

when I interact with the smart contract using CLI everything works as expected.
I was able to lock token - offer others to buy it and later I was able to cancel the offer and NFT was back in my wallet.

locking token:

cardano-cli transaction build \
    --alonzo-era \
    --$testnet \
    --tx-in 1faeef0ce92b02ff90348b4f86f54ac02d6ad6b0270602573130a872dd37af67#0 \
    --tx-in 1faeef0ce92b02ff90348b4f86f54ac02d6ad6b0270602573130a872dd37af67#1 \
    --tx-out "$(cat script.addr) + 1724100 lovelace + 1 $(cat policy/policyID).AdamNFT" \
    --tx-out-datum-hash $(cat dhash) \
    --change-address $(cat payment.addr) \
    --protocol-params-file protocol.json \
    --out-file tx.02 

unlocking token:

cardano-cli transaction build \
    --alonzo-era \
    --$testnet \
    --tx-in d10fcf51e2bc75dd499ff747bfb8f4112e75ced61c598711a00554d08dc930ed#0 \
    --tx-in d10fcf51e2bc75dd499ff747bfb8f4112e75ced61c598711a00554d08dc930ed#1 \
    --tx-in-script-file market.plutus \
    --tx-in-datum-file datum-f9160cfb676909cbd6a6a9bdd7868abd1e3b988fd32ceee05b95246d-AdamNFT.json \
    --tx-in-redeemer-file close.json \
    --required-signer payment.skey \
    --tx-in-collateral d10fcf51e2bc75dd499ff747bfb8f4112e75ced61c598711a00554d08dc930ed#0 \
    --tx-out "$(cat payment.addr) + 1724100 lovelace + 1 f9160cfb676909cbd6a6a9bdd7868abd1e3b988fd32ceee05b95246d.AdamNFT" \
    --change-address $(cat payment.addr) \
    --protocol-params-file protocol.json \
    --out-file unlock-body.02

After signing and submitting transaction everything works as expected.

When I try to do lock and cancel using cardano-serialization-lib and hosky code
HOSKYSWAP.Validator/hosky-swap-ui-prototype at master · ADAPhilippines/HOSKYSWAP.Validator · GitHub

import { Value, TransactionUnspentOutput, BigNum, Vkeywitnesses, PlutusData, Ed25519KeyHash, BaseAddress, Redeemer, PlutusScripts, TransactionBuilder, Address, TransactionOutput } from './custom_modules/@emurgo/cardano-serialization-lib-browser';
import CardanoLoader from './CardanoLoader';
import CardanoProtocolParameters from './Types/CardanoProtocolParam';
import { contractCbor } from "./contract";
import { languageViews } from './languageViews'; import { CoinSelection, setCardanoSerializationLib as setCoinSelectionCardanoSerializationLib } from './coinSelection';
import { PlutusDataObject } from './Types/PlutusDataObject';
import { PlutusField, PlutusFieldType } from './Types/PlutusField';

const BLOCKFROST_PROJECT_ID = "";
const CardanoSerializationLib = async () => {
    return await import("./custom_modules/@emurgo/cardano-serialization-lib-browser/cardano_serialization_lib");
}

const GetWalletAddressAsync = async () => (await window.cardano.getUsedAddresses())[0];
const toHex = (bytes: Uint8Array) => Buffer.from(bytes).toString("hex");
const fromHex = (hex: string) => Buffer.from(hex, "hex");
const toByteArray = (name: string) => Buffer.from(name, "utf8");

const toBigNum = async (value: any) => {
    let Cardano = await CardanoSerializationLib();
    return Cardano?.BigNum.from_str(value.toString()) as BigNum;
}

let btnOffer: HTMLButtonElement;
let btnCancel: HTMLButtonElement;
const policyId = "26e3d767b97e9b61f3bfc294b84b67f6a569aae89666f239eceb7466"
const askingPrice = "5000000";
//const assetName = "StressTestCol04294"
// const scriptTxId = "b77f268a3c55f4919ef1d34be6139ae70c07efe024e3f2d5135b839f14e9fc04"
// const scriptTxIndex = 0
// const amountToTransfer = "2000000"
const assetName = "StressTestCol04256"
const scriptTxId = "1cf640e2672a581a780636c546423f9b10ddb3661d6eb065994e0619a062af24"
const scriptTxIndex = 0
const amountToTransfer = "3000000"
const assetNameInHex = toHex(toByteArray(assetName));

async function Main() {
    await CardanoLoader.LoadAsync();
    btnOffer = document.getElementById("btnOffer") as HTMLButtonElement;
    btnCancel = document.getElementById("btnCancel") as HTMLButtonElement;

    btnOffer.addEventListener("click", OfferToken);
    btnCancel.addEventListener("click", CancelOffer);
}

async function OfferToken() {
    await BuildOfferTxAsync();
}

async function CancelOffer() {
    await CancelOfferTxAsync();
}

async function BuildOfferTxAsync() {
    let Cardano = await CardanoSerializationLib();
    if (Cardano !== null) {
        setCoinSelectionCardanoSerializationLib(Cardano);
        const protocolParameters = await GetProtocolProtocolParamsAsync();
        CoinSelection.setProtocolParameters(
            protocolParameters.min_utxo.toString(),
            protocolParameters.min_fee_a.toString(),
            protocolParameters.min_fee_b.toString(),
            protocolParameters.max_tx_size.toString()
        );

        if (!await window.cardano.isEnabled()) await window.cardano.enable();
        const txBuilder = await CreateTransactionBuilderAsync() as TransactionBuilder;
        const selfAddress = Cardano.Address.from_bytes(fromHex(await GetWalletAddressAsync()));
        const baseAddress = Cardano.BaseAddress.from_address(selfAddress) as BaseAddress;
        const pkh = toHex(baseAddress.payment_cred().to_keyhash()?.to_bytes() as Uint8Array);

        const transactionWitnessSet = Cardano.TransactionWitnessSet.new();
        const hoskyDatumObject = OfferDatum(pkh, askingPrice, policyId, assetName) as PlutusDataObject;
        const datumHash = Cardano.hash_plutus_data(await ToPlutusData(hoskyDatumObject) as PlutusData);

        console.log("datumHash", toHex(datumHash.to_bytes()));
        console.log("pkh", pkh);
        console.log("value", (await GetContractOutput())?.coin().to_str());

        const contractOutput = Cardano.TransactionOutput.new(
            await ContractAddress() as Address,
            await GetContractOutput() as Value
        );
        contractOutput.set_data_hash(datumHash);
        const transactionOutputs = Cardano.TransactionOutputs.new();
        transactionOutputs.add(contractOutput);

        const utxos = await window.cardano.getUtxos();
        const csResult = CoinSelection.randomImprove(
            utxos.map(utxo => Cardano?.TransactionUnspentOutput.from_bytes(fromHex(utxo)) as TransactionUnspentOutput),
            transactionOutputs,
            8
        );

        csResult.inputs.forEach((utxo) => {
            txBuilder.add_input(
                utxo.output().address(),
                utxo.input(),
                utxo.output().amount()
            );
        });

        txBuilder.add_output(contractOutput);
        txBuilder.add_change_if_needed(selfAddress);
        const txBody = txBuilder.build();

        const transaction = Cardano.Transaction.new(
            Cardano.TransactionBody.from_bytes(txBody.to_bytes()),
            Cardano.TransactionWitnessSet.from_bytes(
                transactionWitnessSet.to_bytes()
            )
        );

        const serializedTx = toHex(transaction.to_bytes());

        const txVkeyWitnesses = await window.cardano.signTx(serializedTx, true);

        let signedtxVkeyWitnesses = Cardano.TransactionWitnessSet.from_bytes(
            fromHex(txVkeyWitnesses)
        );

        transactionWitnessSet.set_vkeys(signedtxVkeyWitnesses.vkeys() as Vkeywitnesses);

        const signedTx = Cardano.Transaction.new(
            Cardano.TransactionBody.from_bytes(txBody.to_bytes()),
            Cardano.TransactionWitnessSet.from_bytes(
                transactionWitnessSet.to_bytes()
            )
        );

        console.log("full tx size", signedTx.to_bytes().length);
        console.log("datumObj", hoskyDatumObject);
        const txHash = await window.cardano.submitTx(toHex(signedTx.to_bytes()));
        console.log(txHash);
    }
}

async function CancelOfferTxAsync() {
    let Cardano = await CardanoSerializationLib();
    if (Cardano !== null) {

        setCoinSelectionCardanoSerializationLib(Cardano);
        const protocolParameters = await GetProtocolProtocolParamsAsync();
        CoinSelection.setProtocolParameters(
            protocolParameters.min_utxo.toString(),
            protocolParameters.min_fee_a.toString(),
            protocolParameters.min_fee_b.toString(),
            protocolParameters.max_tx_size.toString()
        );

        const txBuilder = await CreateTransactionBuilderAsync() as TransactionBuilder;
        const transactionWitnessSet = Cardano.TransactionWitnessSet.new();

        const selfAddress = Cardano.Address.from_bytes(fromHex(await GetWalletAddressAsync()));
        const baseAddress = Cardano.BaseAddress.from_address(selfAddress) as BaseAddress;
        console.log(`selfAddress: ${selfAddress.to_bech32()}`);
        // const pkh = "3e4a2ec70fcef9e54c437a173714d1f82b96242379816bea3dd387dd";
        const pkh = toHex(baseAddress.payment_cred().to_keyhash()?.to_bytes() as Uint8Array);
        console.log(`pkh: ${pkh}`)
        const scriptUtxo = Cardano.TransactionUnspentOutput.new(
             Cardano.TransactionInput.new(
                Cardano.TransactionHash.from_bytes(fromHex(scriptTxId)), scriptTxIndex
             ),
             Cardano.TransactionOutput.new(
                await ContractAddress() as Address,
                await GetContractOutput() as Value
            )
        );
        const utxos = (await window.cardano.getUtxos()).map((utxo) =>
             Cardano.TransactionUnspentOutput.from_bytes(fromHex(utxo))
        );
        const outputs: TransactionOutput[] = [
            Cardano.TransactionOutput.new(
                selfAddress, 
                await GetContractOutput() as Value
            )
        ];
        const transactionOutputs = Cardano.TransactionOutputs.new();
        outputs.forEach(output => transactionOutputs.add(output));
        const csResult = CoinSelection.randomImprove(
            utxos,
            transactionOutputs,
            8,
            [scriptUtxo]
        );

        csResult.inputs.forEach((utxo) => {
            console.log(`output address: ${utxo.output().address().to_bech32()}`)
            console.log(`input: txId ${toHex(utxo.input().transaction_id().to_bytes())} txIndex ${utxo.input().index()}`)
            console.log(`output amount: ${utxo.output().amount().coin().to_str()} lovelace, other len: ${utxo.output().amount().multiasset()?.len()}`)
            txBuilder.add_input(
                utxo.output().address(),
                utxo.input(),
                utxo.output().amount()
            );
        });

        const scriptInputIndex = txBuilder.index_of_input(scriptUtxo.input());

        outputs.forEach(output => txBuilder.add_output(output));
        const requiredSigners = Cardano.Ed25519KeyHashes.new();
        requiredSigners.add(baseAddress.payment_cred().to_keyhash() as Ed25519KeyHash);
        txBuilder.set_required_signers(requiredSigners);

        const datumObj = OfferDatum(pkh, askingPrice, policyId, assetName);
        const datum = await ToPlutusData(datumObj as PlutusDataObject) as PlutusData;
        const datumHash = Cardano.hash_plutus_data(datum);
        console.log("datumHash", toHex(datumHash.to_bytes()));

        const datumList = Cardano.PlutusList.new();
        datumList.add(datum);

        const redeemers = Cardano.Redeemers.new();
        // not passing datum because close.json content is {"constructor":2,"fields":[]}
        redeemers.add(await SimpleRedeemer(scriptInputIndex) as Redeemer);

        txBuilder.set_plutus_scripts(await ContractScript() as PlutusScripts);
        txBuilder.set_plutus_data(datumList);
        txBuilder.set_redeemers(redeemers);

        transactionWitnessSet.set_plutus_scripts(await ContractScript() as PlutusScripts);
        transactionWitnessSet.set_plutus_data(datumList);
        transactionWitnessSet.set_redeemers(redeemers);

        const collateralUnspentTransactions = (await GetCollateralUnspentTransactionOutputAsync()) as TransactionUnspentOutput[];
        const collateralInputs = Cardano.TransactionInputs.new();
        collateralUnspentTransactions.forEach(c => collateralInputs.add(c.input()));
        txBuilder.set_collateral(collateralInputs);
        txBuilder.add_change_if_needed(selfAddress);

        const txBody = txBuilder.build();

        const transaction = Cardano.Transaction.new(
            Cardano.TransactionBody.from_bytes(txBody.to_bytes()),
            transactionWitnessSet
        );

        const serializedTx = toHex(transaction.to_bytes());

        const txVkeyWitnesses = await window.cardano.signTx(serializedTx, true);

        let signedtxVkeyWitnesses = Cardano.TransactionWitnessSet.from_bytes(
            fromHex(txVkeyWitnesses)
        );

        transactionWitnessSet.set_vkeys(signedtxVkeyWitnesses.vkeys() as Vkeywitnesses);

        const signedTx = Cardano.Transaction.new(
            Cardano.TransactionBody.from_bytes(txBody.to_bytes()),
            transactionWitnessSet
            // Cardano.TransactionWitnessSet.from_bytes(
            //     transactionWitnessSet.to_bytes()
            // )
        );

        console.log("Full Tx Size", signedTx.to_bytes().length);
        let result = await window.cardano.submitTx(toHex(signedTx.to_bytes()));
        console.log("tx submitted", result);
    }
}

async function GetProtocolProtocolParamsAsync(): Promise<CardanoProtocolParameters> {
    let protocolParamsResult = await fetch("https://cardano-testnet.blockfrost.io/api/v0/epochs/latest/parameters", {
        "headers": {
            "project_id": BLOCKFROST_PROJECT_ID
        }
    });
    return await protocolParamsResult.json();
}

const GetContractOutput = async () => {
    let Cardano = await CardanoSerializationLib();
    if (Cardano !== null) {
        return AssetValue(
                await toBigNum(amountToTransfer),
                policyId,
                assetNameInHex,
                await toBigNum("1")
            );
    }
}

const AssetValue = async (lovelace: BigNum, policyIdHex: string, assetNameHex: string, amount: BigNum) => {
    let Cardano = await CardanoSerializationLib();
    if (Cardano !== null) {
        const assetVal = Cardano.Value.new(lovelace);
        const assetMA = Cardano.MultiAsset.new();
        const asset = Cardano.Assets.new();

        asset.insert(
            Cardano.AssetName.new(fromHex(assetNameHex)),
            amount
        );

        assetMA.insert(
            Cardano.ScriptHash.from_bytes(fromHex(policyIdHex)),
            asset
        );

        assetVal.set_multiasset(assetMA)
        return assetVal;
    }
}

const ToPlutusData = async (plutusDataObj: PlutusDataObject) => {
    let Cardano = await CardanoSerializationLib();
    if (Cardano !== null) {

        const datumFields = Cardano.PlutusList.new();
        plutusDataObj.Fields.sort((a, b) => a.Index - b.Index);
        plutusDataObj.Fields.forEach(f => {
            if (Cardano === null) return;
            switch (f.Type) {
                case PlutusFieldType.Integer:
                    datumFields.add(Cardano.PlutusData.new_integer(Cardano.BigInt.from_str(f.Value.toString())));
                    break;
                // case PlutusFieldType.Data:
                //     datumFields.add(ToPlutusData(f.Value) as PlutusData);
                case PlutusFieldType.Bytes:
                    datumFields.add(Cardano.PlutusData.new_bytes(f.Value));
            }
        })

        return Cardano.PlutusData.new_constr_plutus_data(
            Cardano.ConstrPlutusData.new(
                Cardano.Int.new_i32(plutusDataObj.ConstructorIndex),
                datumFields
            )
        );
    }
}

const OfferDatum = (pkh: string, price: string, policyId: string, assetName: string) => {
    let Cardano = CardanoSerializationLib();
    if (Cardano !== null) {

        const offerDatum = new PlutusDataObject(0);
        offerDatum.Fields = [
            {
                Index: 0,
                Type: PlutusFieldType.Bytes,
                Key: "pkh",
                Value: fromHex(pkh)
            } as PlutusField,
            {
                Index: 0,
                Type: PlutusFieldType.Integer,
                Key: "price",
                Value: price
            } as PlutusField,
            {
                Index: 0,
                Type: PlutusFieldType.Bytes,
                Key: "policyId",
                Value: toByteArray(policyId)
            } as PlutusField,
            {
                Index: 0,
                Type: PlutusFieldType.Bytes,
                Key: "assetName",
                Value: toByteArray(assetName)
            } as PlutusField,
            
        ];

        return offerDatum;
    }
}

const CreateTransactionBuilderAsync = async () => {
    let Cardano = await CardanoSerializationLib();
    if (Cardano !== null) {
        let protocolParams = await GetProtocolProtocolParamsAsync();
        const txBuilder = Cardano.TransactionBuilder.new(
            Cardano.LinearFee.new(
                await toBigNum(protocolParams.min_fee_a),
                await toBigNum(protocolParams.min_fee_b)
            ),
            await toBigNum("1000000"),
            await toBigNum("500000000"),
            await toBigNum("2000000"),
            parseInt("5000"),
            16384,
            5.77e-2,
            7.21e-5,
            Cardano.LanguageViews.new(fromHex(languageViews))
        );
        return txBuilder;
    }
}

const SimpleRedeemer = async (index: number) => {
    let Cardano = await CardanoSerializationLib();
    if (Cardano !== null) {
        //close.json - {"constructor":2,"fields":[]} - this is why I pyt new_i32(2), maybe I'm wrong here
        const redeemerData = Cardano.PlutusData.new_constr_plutus_data(
            Cardano.ConstrPlutusData.new(
                Cardano.Int.new_i32(2),
                Cardano.PlutusList.new()
            )
        );

        const r = Cardano.Redeemer.new(
            Cardano.RedeemerTag.new_spend(),
            await toBigNum(index),
            redeemerData,
            Cardano.ExUnits.new(
                Cardano.BigNum.from_str("1754991"),
                Cardano.BigNum.from_str("652356532")
            )
        )

        return r;
    }
}

const ContractScript = async () => {
    let Cardano = await CardanoSerializationLib();
    if (Cardano !== null) {
        const scripts = Cardano.PlutusScripts.new();
        scripts.add(Cardano.PlutusScript.new(fromHex(contractCbor)));
        return scripts;
    }
};

const GetCollateralUnspentTransactionOutputAsync = async () => {
    let Cardano = await CardanoSerializationLib();
    if (Cardano !== null) {
        const utxosHex = await window.cardano.getCollateral();
        let utxos = utxosHex.map((utxoHex: string) => Cardano?.TransactionUnspentOutput.from_bytes(fromHex(utxoHex) ?? null));
        utxos = utxos.filter((utxo) => utxo !== undefined);
        return utxos as TransactionUnspentOutput[];
    }
};

const ContractAddress = async () => {
    let Cardano = await CardanoSerializationLib();
    if (Cardano !== null) {
        return Cardano.Address.from_bech32("addr_test1wrgd386qdtskfe5uhtk3ur9yx4mnjr5qfn4w0n6uhc9zmwq2qa6n4")
    }
}

document.onreadystatechange = async () => {
    if (document.readyState === "complete") await Main();
};

I’m getting exception:

transaction submit error ShelleyTxValidationError ShelleyBasedEraAlonzo (ApplyTxError 
[ UtxowFailure (NonOutputSupplimentaryDatums (fromList [SafeHash \"8ac77ecbddbc22497349be7ef08ea61bccc57733b588548ebe1d8c6c0a68f9c8\"]) (fromList [])),
 UtxowFailure (ExtraRedeemers [RdmrPtr Spend 0]),UtxowFailure (WrappedShelleyEraFailure (MissingScriptWitnessesUTXOW (fromList [ScriptHash \"d0d89f406ae164e69cbaed1e0ca43577390e804ceae7cf5cbe0a2db8\"]))),
 UtxowFailure (WrappedShelleyEraFailure (UtxoFailure (UtxosFailure (CollectErrors [NoWitness (ScriptHash \"d0d89f406ae164e69cbaed1e0ca43577390e804ceae7cf5cbe0a2db8\")]))))])"

Any idea why I cannot unlock and why it is complaining that I’m missing signature?

1 Like

I spoke with Leni from the project and he told me to check what is the cbor for smart contract when transaction is executed using cli. I went to test.cardanoscan.io and checked
Transaction 17862d2cf34f660aef46a3bca3aff8ba13b0e18769b85feb41cb691ad7e38c70 - Cardanoscan

apparently the bytecode was different than the one in market.plutus file. In the file the cboxHex had some extra bytes “591561” that were not present on the cardanoscan.
Once I took the bytecode from cardanoscan and used it with cardano-serialization-lib I was able to cancel sale.

2 Likes

Are you sure the pkh is correctly used? :slight_smile: … i am just at that step

yes, this is public key hash of the seller. This value has to be passed to datum when used for cancellation, updating offer and eventually by other party that wants to buy needs to pass public key hash of the owner of the NFT. It is validated by onchain code.

2 Likes

I did not underestand what did you do for making it work, the extra bytes in the cborHex where did they came from? did the Cardano Serialization Lib put them? or did the haskell compiler added them?