From 64313ce30dbe969424bb115125bef5fcd2b0322b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Kh=C3=A1nh?= <108068667+independenceee@users.noreply.github.com> Date: Tue, 5 Nov 2024 05:53:23 +0700 Subject: [PATCH] develop: onchain code with smart contract --- contract/README.md | 128 +++++++++++++++++++++- contract/lib/cip68generator/types.ak | 20 +--- contract/lib/cip68generator/utils.ak | 100 ++++++++++++++++- contract/src/txbuilder/cip68.txbuilder.ts | 58 +++++++--- contract/tests/cip68.test.ts | 38 ++++--- contract/validators/mint.ak | 78 ++++++++++++- contract/validators/store.ak | 47 ++++++-- 7 files changed, 397 insertions(+), 72 deletions(-) diff --git a/contract/README.md b/contract/README.md index 593d3ea2..66cc1991 100644 --- a/contract/README.md +++ b/contract/README.md @@ -1 +1,127 @@ -# CIP68Generator - Contract +use aiken/collection/list +use aiken/crypto.{ScriptHash, VerificationKeyHash} +use cardano/address +use cardano/assets.{AssetName, PolicyId, without_lovelace} +use cardano/minting +use cardano/transaction.{Transaction} +use cardano/tx.{verify_signature} +use cardano/value +use cip68generator/types.{Burn, Mint, MintRedeemer} +use cip68generator/utils +use types/cip68 +use validation/find + +// validator - mint +// parameters (exchange_address, store_validator) +validator mint(store: ScriptHash) { +mint(redeemer: MintRedeemer, policy_id: PolicyId, transaction: Transaction) { +let Transaction { inputs, outputs, extra_signatories, mint, .. } = +transaction + + let mint_flatten = + mint + |> without_lovelace() + |> assets.flatten() + + when redeemer is { + Mint -> { + let first_tx_id = find.first_input_txid(inputs) + let first_tx_index = find.first_input_index(inputs) + + let reference_token = utils.token_prefix(mint_flatten, cip68.prefix_100) + let user_token = utils.token_prefix(mint_flatten, cip68.prefix_222) + + let check_none_token = + utils.check_none_token(user_token, reference_token) + + when check_none_token is { + False -> False + True -> { + let reference_value = + assets.from_asset(policy_id, reference_token, 1) + let store_address = address.from_script(store) + let output_utxo = + find.output_by_addr_value(outputs, store_address, reference_value) + + and { + first_tx_index < 256, + list.length(mint_flatten) >= 2, + minting.exact(mint_flatten, policy_id, reference_token, 1)?, + utils.check_output_utxo(output_utxo, extra_signatories)?, + } + } + } + } + + Burn -> True + } + +} + +else(\_) { +fail +} +} + +use aiken/collection/list +use aiken/crypto.{ScriptHash, VerificationKeyHash} +use cardano/address +use cardano/assets.{AssetName, PolicyId, without_lovelace} +use cardano/minting +use cardano/transaction.{Transaction} +use cardano/tx.{verify_signature} +use cardano/value +use cip68generator/types.{Burn, Mint, MintRedeemer} +use cip68generator/utils +use types/cip68 +use validation/find + +// validator - mint +// parameters (exchange_address, store_validator) +validator mint(store: ScriptHash) { +mint(redeemer: MintRedeemer, policy_id: PolicyId, transaction: Transaction) { +let Transaction { inputs, outputs, extra_signatories, mint, .. } = +transaction + + let mint_flatten = + mint + |> without_lovelace() + |> assets.flatten() + + when redeemer is { + Mint -> { + let first_tx_id = find.first_input_txid(inputs) + let first_tx_index = find.first_input_index(inputs) + + let reference_token = utils.token_prefix(mint_flatten, cip68.prefix_100) + let user_token = utils.token_prefix(mint_flatten, cip68.prefix_222) + + let check_none_token = utils.check_none_token(user_token, reference_token) + + when reference_token is { + Some(reference_token) -> { + let reference_value = + assets.from_asset(policy_id, reference_token, 1) + let store_address = address.from_script(store) + let output_utxo = + find.output_by_addr_value(outputs, store_address, reference_value) + + and { + first_tx_index < 256, + list.length(mint_flatten) >= 2, + minting.exact(mint_flatten, policy_id, reference_token, 1)?, + utils.check_output_utxo(output_utxo, extra_signatories)?, + } + } + _ -> fail @"No matching asset found for the given prefix" + } + } + Burn -> True + } + +} + +else(\_) { +fail +} +} diff --git a/contract/lib/cip68generator/types.ak b/contract/lib/cip68generator/types.ak index 63f176de..6fcf1fc8 100644 --- a/contract/lib/cip68generator/types.ak +++ b/contract/lib/cip68generator/types.ak @@ -1,30 +1,14 @@ use aiken/crypto.{VerificationKeyHash} use cardano/assets.{AssetName, PolicyId} -pub type Asset { - policy_id: PolicyId, - asset_name: AssetName, -} - -pub type Extra { - address: VerificationKeyHash, - has_change: Bool, - asset: Asset, -} - -pub type StoreDatum { - metadata: Pair, - version: Int, - extra: Extra, -} - +// the redeemer using mint validator pub type MintRedeemer { Mint Burn } +// the redeemer using store validator pub type StoreRedeemer { Update Remove - Redeem } diff --git a/contract/lib/cip68generator/utils.ak b/contract/lib/cip68generator/utils.ak index 0b7502c1..4b565b26 100644 --- a/contract/lib/cip68generator/utils.ak +++ b/contract/lib/cip68generator/utils.ak @@ -1,15 +1,105 @@ use aiken/collection/list -use cardano/assets.{flatten, without_lovelace} +use aiken/crypto.{ScriptHash, VerificationKeyHash} +use aiken/primitive/bytearray +use cardano/address.{Address} +use cardano/assets.{AssetName, PolicyId, flatten, lovelace_of, without_lovelace} use cardano/transaction.{InlineDatum, Output} -use cip68generator/types.{StoreDatum} +use cardano/tx +use types/cip68.{CIP68} -pub fn check_output_utxo(output: Output) -> Bool { +// check the output utxos containing the reference nft +pub fn check_output_utxo(output: Output, vks: List) -> Bool { expect InlineDatum(data) = output.datum - expect _metadatum: StoreDatum = data + expect metadatum: CIP68 = data + expect name: ByteArray = cip68.get(metadatum, "name") + expect image: ByteArray = cip68.get(metadatum, "image") + expect media_type: ByteArray = cip68.get(metadatum, "mediaType") + expect author: VerificationKeyHash = cip68.get(metadatum, "author") + let output_value = output.value |> without_lovelace() |> flatten() - list.length(output_value) == 1 + and { + bytearray.length(name) > 0, + bytearray.length(image) > 0, + bytearray.length(media_type) > 0, + tx.verify_signature(vks, author), + list.length(output_value) == 1, + } +} + +// get asset name from mint flatten +pub fn token_prefix( + flat: List<(PolicyId, AssetName, Int)>, + prefix: ByteArray, +) -> Option { + let exist = + list.find( + flat, + fn((policy_id, asset_name, amount)) { + bytearray.starts_with(asset_name, prefix) + }, + ) + when exist is { + Some((policy_id, asset_name, amount)) -> Some(asset_name) + None -> None + } +} + +pub fn check_none_token( + user_token: Option, + reference_token: Option, +) -> Bool { + if user_token == None || reference_token == None { + False + } else { + True + } +} + +pub fn check_address(output: Output, address: Address) -> Bool { + output.address.payment_credential == address.payment_credential +} + +// The function checks whether the output exists or not +pub fn check_none_output( + output_store: Option, + output_exchange: Option, +) -> Bool { + // If one of the 3 outputs does not exist, the function will return False + if output_store == None || output_exchange == None { + False + } else { + // Otherwise, the function will return True + True + } +} + +pub fn find_output( + outputs: List, + price: Int, + address: Address, +) -> Option { + list.find( + outputs, + fn(output) { check_amount(output, price) && check_address(output, address) }, + ) +} + +pub fn check_price_duplicate(out_sell: Output, out_royal: Output) -> Bool { + lovelace_of(out_sell.value) > lovelace_of(out_royal.value) +} + +pub fn check_address_duplicate( + output_author: Output, + output_exchange: Output, +) -> Bool { + output_author.address.payment_credential == output_exchange.address.payment_credential +} + +// The function checks the amount +pub fn check_amount(output: Output, price: Int) -> Bool { + lovelace_of(output.value) >= price } diff --git a/contract/src/txbuilder/cip68.txbuilder.ts b/contract/src/txbuilder/cip68.txbuilder.ts index d3548590..879e0abb 100644 --- a/contract/src/txbuilder/cip68.txbuilder.ts +++ b/contract/src/txbuilder/cip68.txbuilder.ts @@ -11,6 +11,7 @@ import { AssetMetadata, metadataToCip68, mConStr1, + deserializeAddress } from "@meshsdk/core"; import { ICip68Contract } from "../interface/icip68.interface"; import { MeshAdapter } from "../adapters/mesh.adapter"; @@ -28,25 +29,28 @@ export class Cip68Contract extends MeshAdapter implements ICip68Contract { protected mintCompileCode: string = this.readValidator(plutus, title.mint); protected storeCompileCode: string = this.readValidator(plutus, title.store); - protected mintScriptCbor = applyParamsToScript(this.mintCompileCode, []); - protected storeScriptCbor = applyParamsToScript(this.storeCompileCode, []); + protected storeScriptCbor = applyParamsToScript(this.storeCompileCode, ["9dcd4b00b1d25d24c07a82c02af5e955e42271a2548136df4af35b38", BigInt(1)]); - protected mintScript: PlutusScript = { - code: this.mintScriptCbor, - version: "V3", - }; protected storeScript: PlutusScript = { code: this.storeScriptCbor, version: "V3", }; - - protected policyId = resolveScriptHash(this.mintScriptCbor, "V3"); - protected storeAddress = serializePlutusScript( + protected storeAddress = serializePlutusScript( this.storeScript, undefined, APP_NETWORK, false, ).address; + protected storeScriptHash = deserializeAddress(this.storeAddress).scriptHash; + protected mintScriptCbor = applyParamsToScript(this.mintCompileCode, [ + "9dcd4b00b1d25d24c07a82c02af5e955e42271a2548136df4af35b38", BigInt(1),this.storeScriptHash + ]); + protected mintScript: PlutusScript = { + code: this.mintScriptCbor, + version: "V3", + }; + protected policyId = resolveScriptHash(this.mintScriptCbor, "V3"); + /** * @@ -63,19 +67,24 @@ export class Cip68Contract extends MeshAdapter implements ICip68Contract { quantity: string; }) => { const { utxos, walletAddress, collateral } = await this.getWalletForTx(); + const utxoRef: UTxO = await this.getUtxoForTx( MINT_REFERENCE_SCRIPT_ADDRESS, MINT_REFERENCE_SCRIPT_HASH, ); + + console.log(deserializeAddress(walletAddress).pubKeyHash) const unsignedTx = this.meshTxBuilder .mintPlutusScriptV3() .mint(quantity, this.policyId, CIP68_222(stringToHex(assetName))) - .mintTxInReference(utxoRef.input.txHash, utxoRef.input.outputIndex) + // .mintTxInReference(utxoRef.input.txHash, utxoRef.input.outputIndex) + .mintingScript(this.mintScriptCbor) .mintRedeemerValue(mConStr0([])) .mintPlutusScriptV3() .mint("1", this.policyId, CIP68_100(stringToHex(assetName))) - .mintTxInReference(utxoRef.input.txHash, utxoRef.input.outputIndex) + .mintingScript(this.mintScriptCbor) + // .mintTxInReference(utxoRef.input.txHash, utxoRef.input.outputIndex) .mintRedeemerValue(mConStr0([])) .txOut(this.storeAddress, [ @@ -85,7 +94,15 @@ export class Cip68Contract extends MeshAdapter implements ICip68Contract { }, ]) .txOutInlineDatumValue(metadataToCip68(metadata)) + .txOut("addr_test1qzwu6jcqk8f96fxq02pvq2h4a927ggn35f2gzdklfte4kwx0sd5zdvsat2chsyyjxkjxcg6uz2y46avd46mzqdgdy3dsckqxs4", [ + { + unit: "lovelace", + quantity: "1500000", + }, + ]) + .changeAddress(walletAddress) + .requiredSignerHash(deserializeAddress(walletAddress).pubKeyHash) .selectUtxosFrom(utxos) .txInCollateral( collateral.input.txHash, @@ -124,10 +141,11 @@ export class Cip68Contract extends MeshAdapter implements ICip68Contract { .mintPlutusScriptV3() .mint(quantity, this.policyId, CIP68_222(stringToHex(assetName))) - .mintTxInReference( - mintUtxoRef.input.txHash, - mintUtxoRef.input.outputIndex, - ) + .mintingScript(this.mintScriptCbor) + // .mintTxInReference( + // mintUtxoRef.input.txHash, + // mintUtxoRef.input.outputIndex, + // ) .mintRedeemerValue(mConStr1([])) .spendingPlutusScriptV3() @@ -154,7 +172,7 @@ export class Cip68Contract extends MeshAdapter implements ICip68Contract { mintUtxoRef.input.outputIndex, ) .mintRedeemerValue(mConStr1([])) - + .requiredSignerHash(deserializeAddress(walletAddress).pubKeyHash) .changeAddress(walletAddress) .selectUtxosFrom(utxos) .txInCollateral( @@ -207,6 +225,14 @@ export class Cip68Contract extends MeshAdapter implements ICip68Contract { }, ]) .txOutInlineDatumValue(metadataToCip68(metadata)) + + .txOut("addr_test1qzwu6jcqk8f96fxq02pvq2h4a927ggn35f2gzdklfte4kwx0sd5zdvsat2chsyyjxkjxcg6uz2y46avd46mzqdgdy3dsckqxs4", [ + { + unit: "lovelace", + quantity: "1000000", + }, + ]) + .requiredSignerHash(deserializeAddress(walletAddress).pubKeyHash) .changeAddress(walletAddress) .selectUtxosFrom(utxos) .txInCollateral( diff --git a/contract/tests/cip68.test.ts b/contract/tests/cip68.test.ts index 4ccd42a3..b4d6e849 100644 --- a/contract/tests/cip68.test.ts +++ b/contract/tests/cip68.test.ts @@ -3,6 +3,7 @@ import { describe, test, expect, beforeEach } from "@jest/globals"; import { BlockfrostProvider, BrowserWallet, + deserializeAddress, KoiosProvider, MeshTxBuilder, MeshWallet, @@ -45,12 +46,13 @@ describe("Mint, Burn, Update, Remove Assets (NFT/TOKEN) CIP68", function () { }); const unsignedTx: string = await cip68Contract.mint({ - assetName: "CIP68 Generators", + assetName: "CIP68 Generators000001", metadata: { name: "CIP68 Generators", image: "ipfs://QmRzicpReutwCkM6aotuKjErFCUD213DpwPq6ByuzMJaua", mediaType: "image/jpg", description: "Open source dynamic assets (Token/NFT) generator (CIP68)", + author: deserializeAddress(await wallet.getChangeAddress()).pubKeyHash, }, quantity: "1", }); @@ -60,24 +62,24 @@ describe("Mint, Burn, Update, Remove Assets (NFT/TOKEN) CIP68", function () { expect(txHash.length).toBe(64); }); - test("Burn", async function () { - const cip68Contract: Cip68Contract = new Cip68Contract({ - fetcher: blockfrostProvider, - wallet: wallet, - meshTxBuilder: meshTxBuilder, - }); + // test("Burn", async function () { + // const cip68Contract: Cip68Contract = new Cip68Contract({ + // fetcher: blockfrostProvider, + // wallet: wallet, + // meshTxBuilder: meshTxBuilder, + // }); - const unsignedTx: string = await cip68Contract.burn({ - assetName: "CIP68 Generators", - txHash: - "728fa7f14b3652a34dfd0f920b5739ede6bda1a88e9ffc52d74636bb41007235", - quantity: "-1", - }); - const signedTx = await wallet.signTx(unsignedTx, true); - const txHash = await wallet.submitTx(signedTx); - console.log(txHash); - expect(txHash.length).toBe(64); - }); + // const unsignedTx: string = await cip68Contract.burn({ + // assetName: "CIP68 Generators", + // txHash: + // "728fa7f14b3652a34dfd0f920b5739ede6bda1a88e9ffc52d74636bb41007235", + // quantity: "-1", + // }); + // const signedTx = await wallet.signTx(unsignedTx, true); + // const txHash = await wallet.submitTx(signedTx); + // console.log(txHash); + // expect(txHash.length).toBe(64); + // }); // test("Update", async function () { // const cip68Contract: Cip68Contract = new Cip68Contract({ diff --git a/contract/validators/mint.ak b/contract/validators/mint.ak index aa8fbb6b..6583268d 100644 --- a/contract/validators/mint.ak +++ b/contract/validators/mint.ak @@ -1,18 +1,84 @@ -use aiken/crypto.{VerificationKeyHash} -use cardano/assets.{PolicyId} +use aiken/collection/list +use aiken/crypto.{ScriptHash, VerificationKeyHash} +use cardano/address +use cardano/assets.{AssetName, PolicyId, without_lovelace} +use cardano/minting use cardano/transaction.{Transaction} use cardano/tx.{verify_signature} +use cardano/value use cip68generator/types.{Burn, Mint, MintRedeemer} +use cip68generator/utils +use types/cip68 +use validation/find // validator - mint // parameters (exchange_address, store_validator) -validator mint(exchange: VerificationKeyHash, store: VerificationKeyHash) { - mint(redeemer: MintRedeemer, _policy_id: PolicyId, transaction: Transaction) { - let Transaction { inputs, outputs, extra_signatories, .. } = transaction +validator mint( + exchange: VerificationKeyHash, + exchange_fee: Int, + store: ScriptHash, +) { + mint(redeemer: MintRedeemer, policy_id: PolicyId, transaction: Transaction) { + let Transaction { inputs, outputs, extra_signatories, mint, .. } = + transaction + + let mint_flatten = + mint + |> without_lovelace() + |> assets.flatten() + let exchange_address = address.from_verification_key(exchange) + let output_utxo_exchange = + utils.find_output(outputs, exchange_fee, exchange_address) + when redeemer is { Mint -> { - + let reference_token_option = + utils.token_prefix(mint_flatten, cip68.prefix_100) + let user_token_option = + utils.token_prefix(mint_flatten, cip68.prefix_222) + let amount_mint_token: Int = list.length(mint_flatten) + let check_none_token = + utils.check_none_token(user_token_option, reference_token_option) + + when check_none_token is { + False -> False + True -> + when (reference_token_option, user_token_option) is { + (Some(reference_token), Some(user_token)) -> { + let reference_value = + assets.from_asset(policy_id, reference_token, 1) + let user_value = + assets.from_asset( + policy_id, + user_token, + amount_mint_token - 1, + ) + let store_address = address.from_script(store) + let output_utxo_store = + find.output_by_addr_value( + outputs, + store_address, + reference_value, + ) + // let output_utxo_user = find.output_by_addr_value(outputs) + and { + amount_mint_token >= 2, + minting.exact(mint_flatten, policy_id, reference_token, 1)?, + minting.exact( + mint_flatten, + policy_id, + user_token, + amount_mint_token - 1, + )?, + utils.check_output_utxo(output_utxo_store, extra_signatories)?, + output_utxo_exchange != None, + } + } + _ -> False + } + } } + Burn -> True } } diff --git a/contract/validators/store.ak b/contract/validators/store.ak index c12b73bb..d4f40757 100644 --- a/contract/validators/store.ak +++ b/contract/validators/store.ak @@ -1,18 +1,49 @@ -use cardano/transaction.{OutputReference, Transaction} -use cip68generator/types.{Redeem, Remove, StoreRedeemer, Update} +use aiken/crypto.{ScriptHash, VerificationKeyHash} +use cardano/address +use cardano/assets.{flatten, without_lovelace} +use cardano/transaction.{ + InlineDatum, Output, OutputReference, Transaction, find_input, +} +use cip68generator/types.{Remove, StoreRedeemer, Update} +use cip68generator/utils use types/cip68.{CIP68} +use validation/find.{output_by_addr_value} -validator store { +validator store(exchange: VerificationKeyHash, exchange_fee: Int) { spend( - _datum: Option, + datum: Option, redeemer: StoreRedeemer, - _output_reference: OutputReference, - _transaction: Transaction, + output_reference: OutputReference, + transaction: Transaction, ) { + expect Some(datum_output) = datum + let Transaction { inputs, outputs, extra_signatories, .. } = transaction + expect Some(input) = find_input(inputs, output_reference) + let script_address = input.output.address + let reference_token = + input.output.value + |> without_lovelace() + let validator_output = + output_by_addr_value(outputs, script_address, reference_token) + let exchange_address = address.from_verification_key(exchange) + let output_utxo_exchange = + utils.find_output(outputs, exchange_fee, exchange_address) when redeemer is { - Update -> True + Update -> { + expect InlineDatum(datum_input) = validator_output.datum + let meradatum_output: CIP68 = datum_output + expect metadatum_input: CIP68 = datum_input + expect author_input: ByteArray = cip68.get(metadatum_input, "author") + expect author_output: ByteArray = cip68.get(meradatum_output, "author") + + let check_author = author_input == author_output + and { + utils.check_output_utxo(validator_output, extra_signatories)?, + output_utxo_exchange != None, + check_author, + } + } Remove -> True - Redeem -> True } }