From e94fac570ddf7e11548f5c8fe66a0965b88c536c Mon Sep 17 00:00:00 2001 From: samdelaney Date: Mon, 9 Oct 2023 16:18:11 -0700 Subject: [PATCH 01/24] spec outline & todo --- CIP-0102/ref_impl/Spec.md | 63 +++++++++++++++++++++++++++++++++++++++ CIP-0102/ref_impl/TODO.md | 5 ++++ 2 files changed, 68 insertions(+) create mode 100644 CIP-0102/ref_impl/Spec.md diff --git a/CIP-0102/ref_impl/Spec.md b/CIP-0102/ref_impl/Spec.md new file mode 100644 index 000000000..b5221d197 --- /dev/null +++ b/CIP-0102/ref_impl/Spec.md @@ -0,0 +1,63 @@ +# Minting a CIP 102 compliant NFT with royalties + +## Minting Policy +- Checks + - Reference NFT created + - User NFT created + - Royalty NFT created (optionally?) + - Royalty datum is valid + - Reference datum is valid + - User datum is valid + +## Validators + +### Always Fails +- Checks nothing +- Returns false or throws error + +### Reducible +- Checks + - tx is signed by recipient + - amount sent to recipient is < previous amount + - all other recipients remain the same + - amount + - address + +## Offchain + +- Sends reference NFT & datum to arbitrary address +- Sends royalty NFT & datum to + - arbitrary address + - always-fails script + - reducible validator + +# Reading a CIP 102 NFT’s royalties off chain + +- Querying the datum + - Check if datum is required + - Request datum + - Fail depending on whether datum is required +- Parsing the datum + + +# Reading and validating against CIP 102 NFT royalties on chain + +A simple listing contract which locks a CIP 102 NFT & a datum with a price & seller and must pay out royalties according to that price to be withdrawn. + +## Onchain + +- Royalty datum + - If empty, check if royalty is required + - Parse reference datum + - Fail if royalty field > 1 + - If not, check if royalties are being paid correctly + - Fail if not +- Listing datum + - Confirm the amount is getting paid to the seller, minus royalties + +## Offchain + +- Find & read the requested listing datum +- Find & read the associated royalty datum using the approach above +- Calculate payments +- Construct & Submit tx \ No newline at end of file diff --git a/CIP-0102/ref_impl/TODO.md b/CIP-0102/ref_impl/TODO.md index e69de29bb..f81bbbe97 100644 --- a/CIP-0102/ref_impl/TODO.md +++ b/CIP-0102/ref_impl/TODO.md @@ -0,0 +1,5 @@ +- [ ] Create Specification +- [ ] Select Onchain Language +- [ ] Minting a CIP 102 compliant NFT with royalties +- [ ] Reading a CIP 102 NFT’s royalties off chain +- [ ] Reading and validating against CIP 102 NFT royalties on chain \ No newline at end of file From c7142b006ee4eb788f3ff190a3827b6cc0665a49 Mon Sep 17 00:00:00 2001 From: SamDelaney Date: Tue, 5 Dec 2023 15:37:50 -0800 Subject: [PATCH 02/24] spec final pass --- CIP-0102/ref_impl/Spec.md | 12 ++++-------- CIP-0102/ref_impl/TODO.md | 4 ++-- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/CIP-0102/ref_impl/Spec.md b/CIP-0102/ref_impl/Spec.md index b5221d197..1d0b8c812 100644 --- a/CIP-0102/ref_impl/Spec.md +++ b/CIP-0102/ref_impl/Spec.md @@ -1,13 +1,9 @@ # Minting a CIP 102 compliant NFT with royalties -## Minting Policy +## Timelocked Minting Policy - Checks - - Reference NFT created - - User NFT created - - Royalty NFT created (optionally?) - - Royalty datum is valid - - Reference datum is valid - - User datum is valid + - Owner has signed tx + - Tx validity window is before MP deadline ## Validators @@ -24,7 +20,7 @@ - address ## Offchain - +- Constructs well formed CIP-68 & CIP-102 Datums - Sends reference NFT & datum to arbitrary address - Sends royalty NFT & datum to - arbitrary address diff --git a/CIP-0102/ref_impl/TODO.md b/CIP-0102/ref_impl/TODO.md index f81bbbe97..b7e665e59 100644 --- a/CIP-0102/ref_impl/TODO.md +++ b/CIP-0102/ref_impl/TODO.md @@ -1,5 +1,5 @@ -- [ ] Create Specification -- [ ] Select Onchain Language +- [x] Create Specification +- [x] Select Onchain Language - [ ] Minting a CIP 102 compliant NFT with royalties - [ ] Reading a CIP 102 NFT’s royalties off chain - [ ] Reading and validating against CIP 102 NFT royalties on chain \ No newline at end of file From 98826c1aa76aaf7e71d49e76891941bdaf9d7c40 Mon Sep 17 00:00:00 2001 From: SamDelaney Date: Tue, 5 Dec 2023 16:55:05 -0800 Subject: [PATCH 03/24] timelocked MP & always-fails validator --- CIP-0102/ref_impl/TODO.md | 3 + .../.github/workflows/tests.yml | 20 +++ .../ref_impl/onchain-reference/.gitignore | 6 + CIP-0102/ref_impl/onchain-reference/README.md | 55 ++++++++ .../ref_impl/onchain-reference/aiken.lock | 15 +++ .../ref_impl/onchain-reference/aiken.toml | 14 ++ .../ref_impl/onchain-reference/plutus.json | 86 ++++++++++++ .../validators/always_fails.ak | 5 + .../onchain-reference/validators/minting.ak | 126 ++++++++++++++++++ 9 files changed, 330 insertions(+) create mode 100644 CIP-0102/ref_impl/onchain-reference/.github/workflows/tests.yml create mode 100644 CIP-0102/ref_impl/onchain-reference/.gitignore create mode 100644 CIP-0102/ref_impl/onchain-reference/README.md create mode 100644 CIP-0102/ref_impl/onchain-reference/aiken.lock create mode 100644 CIP-0102/ref_impl/onchain-reference/aiken.toml create mode 100644 CIP-0102/ref_impl/onchain-reference/plutus.json create mode 100644 CIP-0102/ref_impl/onchain-reference/validators/always_fails.ak create mode 100644 CIP-0102/ref_impl/onchain-reference/validators/minting.ak diff --git a/CIP-0102/ref_impl/TODO.md b/CIP-0102/ref_impl/TODO.md index b7e665e59..332ce9270 100644 --- a/CIP-0102/ref_impl/TODO.md +++ b/CIP-0102/ref_impl/TODO.md @@ -1,5 +1,8 @@ - [x] Create Specification - [x] Select Onchain Language - [ ] Minting a CIP 102 compliant NFT with royalties + - [x] Timelocked MP + - [x] Always-Fails Validator + - [ ] Reducible Validator - [ ] Reading a CIP 102 NFT’s royalties off chain - [ ] Reading and validating against CIP 102 NFT royalties on chain \ No newline at end of file diff --git a/CIP-0102/ref_impl/onchain-reference/.github/workflows/tests.yml b/CIP-0102/ref_impl/onchain-reference/.github/workflows/tests.yml new file mode 100644 index 000000000..89136858e --- /dev/null +++ b/CIP-0102/ref_impl/onchain-reference/.github/workflows/tests.yml @@ -0,0 +1,20 @@ +name: Tests + +on: + push: + branches: ["main"] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: aiken-lang/setup-aiken@v0.1.0 + with: + version: v1 + + - run: aiken fmt --check + - run: aiken check -D + - run: aiken build diff --git a/CIP-0102/ref_impl/onchain-reference/.gitignore b/CIP-0102/ref_impl/onchain-reference/.gitignore new file mode 100644 index 000000000..ff7811b15 --- /dev/null +++ b/CIP-0102/ref_impl/onchain-reference/.gitignore @@ -0,0 +1,6 @@ +# Aiken compilation artifacts +artifacts/ +# Aiken's project working directory +build/ +# Aiken's default documentation export +docs/ diff --git a/CIP-0102/ref_impl/onchain-reference/README.md b/CIP-0102/ref_impl/onchain-reference/README.md new file mode 100644 index 000000000..d70412214 --- /dev/null +++ b/CIP-0102/ref_impl/onchain-reference/README.md @@ -0,0 +1,55 @@ +# onchain-reference + +Write validators in the `validators` folder, and supporting functions in the `lib` folder using `.ak` as a file extension. + +For example, as `validators/always_true.ak` + +```gleam +validator { + fn spend(_datum: Data, _redeemer: Data, _context: Data) -> Bool { + True + } +} +``` + +## Building + +```sh +aiken build +``` + +## Testing + +You can write tests in any module using the `test` keyword. For example: + +```gleam +test foo() { + 1 + 1 == 2 +} +``` + +To run all tests, simply do: + +```sh +aiken check +``` + +To run only tests matching the string `foo`, do: + +```sh +aiken check -m foo +``` + +## Documentation + +If you're writing a library, you might want to generate an HTML documentation for it. + +Use: + +```sh +aiken docs +``` + +## Resources + +Find more on the [Aiken's user manual](https://aiken-lang.org). diff --git a/CIP-0102/ref_impl/onchain-reference/aiken.lock b/CIP-0102/ref_impl/onchain-reference/aiken.lock new file mode 100644 index 000000000..0a72eb3c6 --- /dev/null +++ b/CIP-0102/ref_impl/onchain-reference/aiken.lock @@ -0,0 +1,15 @@ +# This file was generated by Aiken +# You typically do not need to edit this file + +[[requirements]] +name = "aiken-lang/stdlib" +version = "1.7.0" +source = "github" + +[[packages]] +name = "aiken-lang/stdlib" +version = "1.7.0" +requirements = [] +source = "github" + +[etags] diff --git a/CIP-0102/ref_impl/onchain-reference/aiken.toml b/CIP-0102/ref_impl/onchain-reference/aiken.toml new file mode 100644 index 000000000..7a07c2757 --- /dev/null +++ b/CIP-0102/ref_impl/onchain-reference/aiken.toml @@ -0,0 +1,14 @@ +name = "0102/onchain-reference" +version = "0.0.0" +license = "Apache-2.0" +description = "Aiken contracts for project '0102/onchain-reference'" + +[repository] +user = "0102" +project = "onchain-reference" +platform = "github" + +[[dependencies]] +name = "aiken-lang/stdlib" +version = "1.7.0" +source = "github" diff --git a/CIP-0102/ref_impl/onchain-reference/plutus.json b/CIP-0102/ref_impl/onchain-reference/plutus.json new file mode 100644 index 000000000..46df487e3 --- /dev/null +++ b/CIP-0102/ref_impl/onchain-reference/plutus.json @@ -0,0 +1,86 @@ +{ + "preamble": { + "title": "0102/onchain-reference", + "description": "Aiken contracts for project '0102/onchain-reference'", + "version": "0.0.0", + "plutusVersion": "v2", + "compiler": { + "name": "Aiken", + "version": "v1.0.20-alpha+unknown" + }, + "license": "Apache-2.0" + }, + "validators": [ + { + "title": "always_fails.spend", + "datum": { + "title": "_datum", + "schema": { + "$ref": "#/definitions/Data" + } + }, + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/Data" + } + }, + "compiledCode": "510100003222253330044a029309b2b2b9a1", + "hash": "66a67769edd0c54d5f6ec8ba3925394bf0e4fc1f8bfbe3a131eb523c" + }, + { + "title": "minting.minting_validator", + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/minting~1Redeemer" + } + }, + "parameters": [ + { + "title": "lock_time", + "schema": { + "$ref": "#/definitions/Int" + } + }, + { + "title": "owner", + "schema": { + "$ref": "#/definitions/ByteArray" + } + } + ], + "compiledCode": "5902580100003232323232323232322322322232533300a32533300b533300b33223232533300f3370e9001000899b89375a6028601a0040062940c034004c00cc02cc048c04cc02cc048c04cc04cc04cc04cc04cc04cc04cc02c008c004c02400c01c4cc88c8cc00400400c894ccc04800452809919299980899b8f00200514a2266008008002602c0046eb8c050004dd61808180898089808980898089808980898089804980098048018028a50132533300c3370e900018058008991919191919299980919b87480000044cdc4a4004004266e240092001301000a375a602a002602a00260280026eb0c048004c02800458c8c8c8ccc8c0040048894ccc050008530103d87a80001323253330133370e0069000099ba548000cc05cdd380125eb804ccc014014004cdc0801a400460300066eb0c0580080052000323300100100222533301200114bd70099199911191980080080191299980c00088018991999111980e9ba73301d37520126603a6ea400ccc074dd400125eb80004dd7180b8009bad301800133003003301c002301a001375c60220026eacc048004cc00c00cc058008c050004c8cc004004008894ccc04400452f5bded8c0264646464a66602466e3d221000021003133016337606ea4008dd3000998030030019bab3013003375c6022004602a00460260026eacc040c044c044c044c044c024c004c02400c588c0400045261365632533300a3370e90000008a99980698040018a4c2c2a66601466e1d20020011533300d300800314985858c020008dd70009bad001230053754002460066ea80055cd2ab9d5573caae7d5d02ba157441", + "hash": "da3ed2bc3adda27c9012abaffa4d0dfb32d0181ad38caf0e12a8daca" + } + ], + "definitions": { + "ByteArray": { + "dataType": "bytes" + }, + "Data": { + "title": "Data", + "description": "Any Plutus data." + }, + "Int": { + "dataType": "integer" + }, + "minting/Redeemer": { + "title": "Redeemer", + "anyOf": [ + { + "title": "MintNft", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "BurnNft", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + } + } +} \ No newline at end of file diff --git a/CIP-0102/ref_impl/onchain-reference/validators/always_fails.ak b/CIP-0102/ref_impl/onchain-reference/validators/always_fails.ak new file mode 100644 index 000000000..c62c006fa --- /dev/null +++ b/CIP-0102/ref_impl/onchain-reference/validators/always_fails.ak @@ -0,0 +1,5 @@ +validator { + fn spend(_datum: Data, _redeemer: Data, _context: Data) -> Bool { + False + } +} diff --git a/CIP-0102/ref_impl/onchain-reference/validators/minting.ak b/CIP-0102/ref_impl/onchain-reference/validators/minting.ak new file mode 100644 index 000000000..526a005a9 --- /dev/null +++ b/CIP-0102/ref_impl/onchain-reference/validators/minting.ak @@ -0,0 +1,126 @@ +use aiken/hash.{Blake2b_224, Hash} +use aiken/interval.{Finite, Interval, IntervalBound} +use aiken/list +use aiken/time.{PosixTime} +use aiken/transaction.{Mint, ScriptContext, Transaction} +use aiken/transaction/credential.{VerificationKey} +use aiken/transaction/value + +type Redeemer { + MintNft + BurnNft +} + +validator(lock_time: PosixTime, owner: Hash) { + fn minting_validator(redeemer: Redeemer, ctx: ScriptContext) -> Bool { + expect + is_not_expired(ctx.transaction, lock_time)? && list.has( + ctx.transaction.extra_signatories, + owner, + )? + expect Some((_, _, quantity)) = + ctx.transaction.mint + |> value.from_minted_value + |> value.flatten + |> list.at(0) + + when redeemer is { + MintNft -> (quantity >= 1)? + BurnNft -> (quantity <= -1)? + } + } +} + +fn is_not_expired(tx: Transaction, lock_time: PosixTime) { + when tx.validity_range.upper_bound.bound_type is { + Finite(bound_time) -> bound_time <= lock_time + _ -> False + } +} + +// ############### TESTS ############### + +const mock_timestamp = 1704063599000 + +// 2023-12-31 23:59:59 +const mock_owner = "some_fake_address_hash" + +const mock_policy_id = "some_fake_policy_id" + +const mock_asset_name = "some_fake_asset_name" + +test should_mint_nft() { + // GIVEN + let redeemer = MintNft + let ctx = + ScriptContext { + purpose: Mint(mock_policy_id), + transaction: get_base_transaction(1604063500000, 1704063500000, 1), + } + // THEN + minting_validator(mock_timestamp, mock_owner, redeemer, ctx) == True +} + +test should_burn_nfg() { + // GIVEN + let redeemer = BurnNft + let ctx = + ScriptContext { + purpose: Mint(mock_policy_id), + transaction: get_base_transaction(1604063500000, 1704063500000, -1), + } + // THEN + minting_validator(mock_timestamp, mock_owner, redeemer, ctx) == True +} + +test should_succeed_mint_multiple_asset_amount() { + // GIVEN + let redeemer = MintNft + let ctx = + ScriptContext { + purpose: Mint(mock_policy_id), + transaction: get_base_transaction(1604063500000, 1704063500000, 5), + } + // THEN + minting_validator(mock_timestamp, mock_owner, redeemer, ctx) == True +} + +test should_throw_error_by_exceeded_time_lock() fail { + // GIVEN + let redeemer = MintNft + let ctx = + ScriptContext { + purpose: Mint(mock_policy_id), + transaction: get_base_transaction(1604063500000, mock_timestamp, 1), + } + // THEN + minting_validator(1704063500000, mock_owner, redeemer, ctx) +} + +test should_throw_error_by_missing_signer() fail { + // GIVEN + let redeemer = MintNft + let ctx = + ScriptContext { + purpose: Mint(mock_policy_id), + transaction: get_base_transaction(1604063500000, 1704063500000, 1), + } + // THEN + minting_validator(mock_timestamp, "some_other_owner", redeemer, ctx) +} + +fn get_base_transaction(from: Int, to: Int, quantity: Int) { + Transaction { + ..transaction.placeholder(), + extra_signatories: [mock_owner], + mint: value.from_asset(mock_policy_id, mock_asset_name, quantity) + |> value.to_minted_value, + validity_range: Interval { + lower_bound: IntervalBound { + bound_type: Finite(from), + is_inclusive: True, + }, + upper_bound: IntervalBound { bound_type: Finite(to), is_inclusive: True }, + }, + } +} From 347aadf4394b4b3b32707a3f54a06404eb87261d Mon Sep 17 00:00:00 2001 From: SamDelaney Date: Sat, 9 Dec 2023 00:27:51 -0800 Subject: [PATCH 04/24] offchain utils for env, wallet generation --- .../ref_impl/offchain-reference/.env.example | 5 +++++ .../ref_impl/offchain-reference/.gitignore | 1 + .../ref_impl/offchain-reference/deno.json | 5 +++++ CIP-0102/ref_impl/offchain-reference/index.ts | 1 + .../ref_impl/offchain-reference/utils/env.ts | 22 +++++++++++++++++++ .../utils/generate-wallet.js | 15 +++++++++++++ 6 files changed, 49 insertions(+) create mode 100644 CIP-0102/ref_impl/offchain-reference/.env.example create mode 100644 CIP-0102/ref_impl/offchain-reference/.gitignore create mode 100644 CIP-0102/ref_impl/offchain-reference/deno.json create mode 100644 CIP-0102/ref_impl/offchain-reference/index.ts create mode 100644 CIP-0102/ref_impl/offchain-reference/utils/env.ts create mode 100644 CIP-0102/ref_impl/offchain-reference/utils/generate-wallet.js diff --git a/CIP-0102/ref_impl/offchain-reference/.env.example b/CIP-0102/ref_impl/offchain-reference/.env.example new file mode 100644 index 000000000..f54a30946 --- /dev/null +++ b/CIP-0102/ref_impl/offchain-reference/.env.example @@ -0,0 +1,5 @@ +PUBLIC_CARDANO_NETWORK=Preprod +BLOCKFROST_URL=https://cardano-preprod.blockfrost.io/api/v0 +BLOCKFROST_PROJECT_KEY=PLACEHOLDER +SERVICE_WALLET_ADDRESS=PLACEHOLDER +SERVICE_WALLET_PRIVATE_KEY=PLACEHOLDER \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/.gitignore b/CIP-0102/ref_impl/offchain-reference/.gitignore new file mode 100644 index 000000000..2eea525d8 --- /dev/null +++ b/CIP-0102/ref_impl/offchain-reference/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/deno.json b/CIP-0102/ref_impl/offchain-reference/deno.json new file mode 100644 index 000000000..0d4dc59e0 --- /dev/null +++ b/CIP-0102/ref_impl/offchain-reference/deno.json @@ -0,0 +1,5 @@ +{ + "tasks": { + "generate-wallet": "deno run --allow-net --allow-read --allow-env --allow-write utils/generate-wallet.js" + } +} diff --git a/CIP-0102/ref_impl/offchain-reference/index.ts b/CIP-0102/ref_impl/offchain-reference/index.ts new file mode 100644 index 000000000..e027b46bc --- /dev/null +++ b/CIP-0102/ref_impl/offchain-reference/index.ts @@ -0,0 +1 @@ +console.log('Hello, world!') \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/utils/env.ts b/CIP-0102/ref_impl/offchain-reference/utils/env.ts new file mode 100644 index 000000000..758e6e09e --- /dev/null +++ b/CIP-0102/ref_impl/offchain-reference/utils/env.ts @@ -0,0 +1,22 @@ +import { load } from "https://deno.land/std@0.208.0/dotenv/mod.ts"; +import { writeFileSync } from "https://deno.land/std@0.110.0/node/fs.ts"; + +const env = await load({ allowEmptyValues: true }); + +export function getEnv(variable: string) { + const value = env[variable]; + if (!value) { + throw Error( + `Must set ${variable} is a required environment variable. Did you use 'pnpm run .env '?` + ); + } + + return value; +} + +export function updateEnv(config = {}, eol = '\n'){ + const envContents = Object.entries({...env, ...config}) + .map(([key,val]) => `${key}=${val}`) + .join(eol) + writeFileSync('.env', envContents); +} \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/utils/generate-wallet.js b/CIP-0102/ref_impl/offchain-reference/utils/generate-wallet.js new file mode 100644 index 000000000..6109cc006 --- /dev/null +++ b/CIP-0102/ref_impl/offchain-reference/utils/generate-wallet.js @@ -0,0 +1,15 @@ +import { Lucid } from 'https://deno.land/x/lucid@0.10.7/mod.ts'; +import { getEnv, updateEnv } from './env.ts'; + +const network = getEnv('PUBLIC_CARDANO_NETWORK'); +const lucid = await Lucid.new(undefined, network); + +const privateKey = lucid.utils.generatePrivateKey(); +const address = await lucid + .selectWalletFromPrivateKey(privateKey) + .wallet.address(); +const envUpdate = { + SERVICE_WALLET_ADDRESS: address, + SERVICE_WALLET_PRIVATE_KEY: privateKey, +} +updateEnv(envUpdate); From 1f675f5f8745cd88aafd296decad0e16b01a4715 Mon Sep 17 00:00:00 2001 From: samdelaney Date: Sat, 16 Dec 2023 11:21:30 -0800 Subject: [PATCH 05/24] print-utxos util --- CIP-0102/ref_impl/offchain-reference/deno.json | 3 ++- .../offchain-reference/utils/print-utxos.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 CIP-0102/ref_impl/offchain-reference/utils/print-utxos.ts diff --git a/CIP-0102/ref_impl/offchain-reference/deno.json b/CIP-0102/ref_impl/offchain-reference/deno.json index 0d4dc59e0..09abd465a 100644 --- a/CIP-0102/ref_impl/offchain-reference/deno.json +++ b/CIP-0102/ref_impl/offchain-reference/deno.json @@ -1,5 +1,6 @@ { "tasks": { - "generate-wallet": "deno run --allow-net --allow-read --allow-env --allow-write utils/generate-wallet.js" + "generate-wallet": "deno run --allow-net --allow-read --allow-env --allow-write utils/generate-wallet.js", + "print-utxos": "deno run --allow-net --allow-read --allow-env --allow-write utils/print-utxos.ts" } } diff --git a/CIP-0102/ref_impl/offchain-reference/utils/print-utxos.ts b/CIP-0102/ref_impl/offchain-reference/utils/print-utxos.ts new file mode 100644 index 000000000..aa1cef1a3 --- /dev/null +++ b/CIP-0102/ref_impl/offchain-reference/utils/print-utxos.ts @@ -0,0 +1,16 @@ +import { Lucid, Blockfrost } from 'https://deno.land/x/lucid@0.10.7/mod.ts'; + +import { getEnv } from './env.ts'; + +const network = getEnv('PUBLIC_CARDANO_NETWORK'); +const address = getEnv('SERVICE_WALLET_ADDRESS'); +const blockfrostUrl = getEnv(`BLOCKFROST_URL`); +const blockfrostKey = getEnv(`BLOCKFROST_PROJECT_KEY`); + +console.log(address); + +const blockfrost = new Blockfrost(blockfrostUrl, blockfrostKey); +const lucid = await Lucid.new(blockfrost, network); +const utxos = await lucid.utxosAt(address); + +console.log(utxos); \ No newline at end of file From 82bd0eb001c7cfd7136e5b3ba1b46e3bc023a679 Mon Sep 17 00:00:00 2001 From: SamDelaney Date: Tue, 16 Jan 2024 18:44:41 -0800 Subject: [PATCH 06/24] WIP - initial offchain for mint & read --- .../ref_impl/offchain-reference/deno.json | 3 +- CIP-0102/ref_impl/offchain-reference/index.ts | 6 +- CIP-0102/ref_impl/offchain-reference/mint.ts | 214 ++++++++++++++++++ CIP-0102/ref_impl/offchain-reference/read.ts | 141 ++++++++++++ CIP-0102/ref_impl/offchain-reference/types.ts | 111 +++++++++ 5 files changed, 473 insertions(+), 2 deletions(-) create mode 100644 CIP-0102/ref_impl/offchain-reference/mint.ts create mode 100644 CIP-0102/ref_impl/offchain-reference/read.ts create mode 100644 CIP-0102/ref_impl/offchain-reference/types.ts diff --git a/CIP-0102/ref_impl/offchain-reference/deno.json b/CIP-0102/ref_impl/offchain-reference/deno.json index 09abd465a..fc9cb2daf 100644 --- a/CIP-0102/ref_impl/offchain-reference/deno.json +++ b/CIP-0102/ref_impl/offchain-reference/deno.json @@ -1,6 +1,7 @@ { "tasks": { "generate-wallet": "deno run --allow-net --allow-read --allow-env --allow-write utils/generate-wallet.js", - "print-utxos": "deno run --allow-net --allow-read --allow-env --allow-write utils/print-utxos.ts" + "print-utxos": "deno run --allow-net --allow-read --allow-env --allow-write utils/print-utxos.ts", + "playground": "deno run --allow-net --allow-read --allow-env --allow-write index.ts" } } diff --git a/CIP-0102/ref_impl/offchain-reference/index.ts b/CIP-0102/ref_impl/offchain-reference/index.ts index e027b46bc..9e7f42db2 100644 --- a/CIP-0102/ref_impl/offchain-reference/index.ts +++ b/CIP-0102/ref_impl/offchain-reference/index.ts @@ -1 +1,5 @@ -console.log('Hello, world!') \ No newline at end of file +import { getAssetsTransactions } from "./read.ts" + +const response = await getAssetsTransactions("2b29ff2309668a2e58d96da26bd0e1c0dd4013e5853e96df2be27e42001f4d70526f79616c7479") + +console.log(response) \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/mint.ts b/CIP-0102/ref_impl/offchain-reference/mint.ts new file mode 100644 index 000000000..61f5287c5 --- /dev/null +++ b/CIP-0102/ref_impl/offchain-reference/mint.ts @@ -0,0 +1,214 @@ +import { + Assets, + Constr, + Data, + Lucid, + Script, + Tx, + applyParamsToScript, + fromText, + toUnit +} from "https://deno.land/x/lucid@0.10.7/mod.ts"; + +import { + RoyaltyRecipient, + DatumMetadata, + Metadata, + RoyaltyFlag, + MediaAssets +} from "./types.ts"; + +const timeLockMintingContract = '5906e90100003232323232323232323232322322322232533300b53300c49107656e746572656400132533300c533300c533300c3322323253330103370e9001000899b89375a602c601a0040062940c034004c00cc02cc050c054c02cc050c054c054c054c054c054c054c054c02c008c004c02400c01c5288a99806a493269735f6e6f745f65787069726564286374782e7472616e73616374696f6e2c206c6f636b5f74696d6529203f2046616c73650014a02a6660186644646600200200644a66602800229404c8c94ccc048cdc78010028a511330040040013018002375c602c0026eb0c048c04cc04cc04cc04cc04cc04cc04cc04cc024c004c02400c0145288a99806a493a6c6973742e686173286374782e7472616e73616374696f6e2e65787472615f7369676e61746f726965732c206f776e657229203f2046616c73650014a0294054cc0352410670617373656400132533300d3370e9000180580089919191919299809a490a7175616e746974793a20001533013332323232323223732660046ea0005221003001001222533333302100213232323233009533301d337100069007099b80483c80400c54ccc074cdc4001a410004266e04cdc0241002800690070b19b8a4890128000015333020001133714911035b5d2900004133714911035b5f2000375c603e66600e00266ec1300102415d00375266e292210129000042233760980103422c2000375266601001000466e28dd718100009bae30210013758603c0046eb4c070004c8cdd81ba8301c001374e603a0026ea80084c94ccc0780044cdc5245027b7d00002133714911037b5f2000375c603a6644646600200200844a6660440022008266006604800266004004604a00266ec130010342207d0037520044466ec1300103422c2000375266600c00c603c00466e29221023a2000333006006301f002337146eb8c078004dd7180f8009bab002132533301e0011337149101025b5d00002133714911035b5f2000375c603a66600a00266ec1300102415d0037520044466ec1300103422c2000375266600c00c00466e28dd7180f0009bae301f001375800426600a6eb40080044c8cdc5244102682700332232333001001003002222533301f3371000490000800899191919980300319b8100548008cdc599b80002533302233710004900a0a40c02903719b8b33700002a66604466e2000520141481805206e0043370c004901019b8300148080cdc700300119b81371a002900119b8a4881012700002375c004444646600200200844a66603c0022008266006604000266004004604200244646600200200644a66603066e1c0052000133714910101300000315333018337100029000099b8a489012d003300200233702900000089980299b8400148050cdc599b803370a002900a240c00066002002444a66602a66e2400920001001133300300333708004900a19b8b3370066e140092014481800040044c94ccc04ccdc3a40000022a66602666e25200200214a22a66028921157175616e74697479203e3d2031203f2046616c73650014a02a66602666e24009200114a22a66028921167175616e74697479203c3d202d31203f2046616c73650014a060200146eb4c05c004c05c004c058004dd6180a00098050008a998072481244578706563746564206f6e20696e636f727265637420436f6e7374722076617269616e74001632323233323001001222533301600214c103d87a80001323253330143370e0069000099ba548000cc064dd380125eb804ccc014014004cdc0801a400460340066eb0c0600080052000323300100100222533301400114bd70099199911191980080080191299980d00088018991999111980f9ba73301f37520126603e6ea400ccc07cdd400125eb80004dd7180c8009bad301a00133003003301e002301c001375c60260026eacc050004cc00c00cc060008c058004c8cc004004008894ccc04c00452f5bded8c0264646464a66602666e3d221000021003133018337606ea4008dd3000998030030019bab3015003375c6026004602e004602a0026eacc048c04cc04cc04cc04cc024c004c02400c54cc0352401254578706563746564206f6e20696e636f727265637420426f6f6c65616e2076617269616e74001623012001149854cc0312411856616c696461746f722072657475726e65642066616c7365001365632533300b3370e90000008a99980798040018a4c2a660180142c2a66601666e1d20020011533300f3008003149854cc0300285854cc03124128436f6e73747220696e646578206469646e2774206d61746368206120747970652076617269616e7400163008002375c0026eb40048c01cdd5000918029baa00149011d4578706563746564206e6f206669656c647320666f7220436f6e737472005734ae7155ceaab9e5573eae815d0aba257481' + +export type NFTData = { + name: string, + description: string, + image: string, +} + +export function createTimelockedMP( + lucid: Lucid, + timestamp: number, + walletAddress: string +) { + const paymentHash = lucid.utils.getAddressDetails(walletAddress).paymentCredential?.hash; + + if(!paymentHash) + throw new Error("Payment hash not found") + + const policy: Script = { + type: "PlutusV2", + script: applyParamsToScript(timeLockMintingContract, [BigInt(timestamp), paymentHash]), + } + return policy; +} + +export async function createRoyalty(lucid: Lucid, policy: Script, validator: Script, royalty: RoyaltyRecipient) { + const royaltyDatum = Data.to({ + metadata: Data.castFrom(Data.fromJson([royalty]), Metadata), + version: BigInt(1), + extra: Data.from(Data.void()) + }, DatumMetadata) + + const validTo = new Date(); + validTo.setHours(validTo.getHours() + 1); + + const utxos = await lucid.wallet.getUtxos(); + const walletAddress = await lucid.wallet.address(); + + const policyId = lucid.utils.mintingPolicyToId(policy); + + const nftUnit = toUnit(policyId, fromText("Royalty"), 500); + const validatorAddress = lucid.utils.validatorToAddress(validator) + + const transaction = await lucid + .newTx() + .collectFrom(utxos) + .attachMintingPolicy(policy) + .mintAssets( + { [nftUnit]: BigInt(1) }, + Data.to(new Constr(0, [])) // Constr(1, []) to Burn + ) + .validTo(validTo.getTime()) + .addSigner(walletAddress) + .payToContract( + validatorAddress, + { inline: royaltyDatum }, + { [toUnit(policyId, fromText("Royalty"), 500)]: 1n } + ).complete() + + return transaction; + } + + export async function createTimelockedTransaction( + lucid: Lucid, + policy: Script, + validator: Script, + sanitizedMetadataAssets: MediaAssets, + cip102?: "NoRoyalty" | "Premade" | RoyaltyRecipient +) { + const royalty = cip102 === "NoRoyalty" || cip102 === "Premade" ? undefined : cip102; + + const utxos = await lucid.wallet.getUtxos(); + + // Grab any utxo to use to create a unique policy + if (!utxos || !utxos.length || !utxos[0]) { + return { error: 'empty-wallet' }; + } + + const referenceUtxo = utxos[0]; + + // TODO: May need more utxos to pay for the transaction if the one found wasn't big enough + + const tokenNames = Object.keys(sanitizedMetadataAssets); + const policyId = lucid.utils.mintingPolicyToId(policy); + + const validatorAddress = lucid.utils.validatorToAddress(validator) + + const assets: Assets = {}; + if(royalty) + assets[toUnit(policyId, fromText("Royalty"), 500)] = 1n; + tokenNames.forEach((tokenName) => { + assets[toUnit(policyId, fromText(tokenName), 100)] = 1n; + assets[toUnit(policyId, fromText(tokenName), 222)] = 1n; + }); + + const validTo = new Date(); + validTo.setHours(validTo.getHours(), validTo.getMinutes() + 5); + + const walletAddress = await lucid.wallet.address() + + let incompleteTx = await lucid + .newTx() + .collectFrom([...utxos, referenceUtxo]) + .attachMintingPolicy(policy) + .mintAssets(assets, Data.to(new Constr(0, []))) + .validTo(validTo.getTime()) + .addSigner(walletAddress) + + const attachDatums = (tx: Tx): Tx => { + if(royalty) { + const royaltyDatum = Data.to({ + metadata: Data.castFrom(Data.fromJson([royalty]), Metadata), + version: BigInt(1), + extra: Data.from(Data.void()) + }, DatumMetadata) + + tx = tx.payToContract( + validatorAddress, + { inline: royaltyDatum }, + { [toUnit(policyId, fromText("Royalty"), 500)]: 1n } + ) + } + + const has102Royalty = cip102 === "Premade" || royalty !== undefined; + const extra = has102Royalty + ? Data.to({ royalty_included: BigInt(1) }, RoyaltyFlag) + : Data.from(Data.void()); + + for(const tokenName of tokenNames) { + const metadataDatum = Data.to({ + metadata: Data.castFrom(Data.fromJson(sanitizedMetadataAssets[tokenName]), Metadata), + version: BigInt(1), + extra + }, DatumMetadata) + + tx = tx.payToContract( + validatorAddress, + { inline: metadataDatum }, + { [toUnit(policyId, fromText(tokenName), 100)]: BigInt(1) } + ) + } + + return tx; + } + + if(cip102) + incompleteTx = await attachDatums(incompleteTx) + + const tx = await incompleteTx.complete() + + return { tx, policyId }; +} + +/// CIP-25 minting. Don't use unless you're certain this is what you want. +export async function timeLockMintNft(lucid: Lucid, timestamp: number, nftData: NFTData) { + const validTo = new Date(); + validTo.setHours(validTo.getHours() + 1); + + const utxos = await lucid.wallet.getUtxos(); + const walletAddress = await lucid.wallet.address(); + + const policy = createTimelockedMP(lucid, timestamp, walletAddress); + const policyId = lucid.utils.mintingPolicyToId(policy); + + const nftName = nftData.name.replace(/\s/g,''); + const nftUnit1 = policyId + fromText(nftName); + const nftUnit2 = policyId + fromText(nftName + "a"); + + + const transaction = await lucid + .newTx() + .collectFrom(utxos) + .attachMintingPolicy(policy) + .mintAssets( + { [nftUnit1]: BigInt(1), + [nftUnit2]: BigInt(1), }, + Data.to(new Constr(0, [])) // Constr(1, []) to Burn + ) + .validTo(validTo.getTime()) + .addSigner(walletAddress) + .attachMetadata(721, { + [policyId]: { + [nftName]: { + id: 1, + name: nftData.name, + description: nftData.description, + image: nftData.image, + } + } + }) + .complete() + + return transaction; + } \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/read.ts b/CIP-0102/ref_impl/offchain-reference/read.ts new file mode 100644 index 000000000..f7eeb7e2a --- /dev/null +++ b/CIP-0102/ref_impl/offchain-reference/read.ts @@ -0,0 +1,141 @@ +import { getEnv } from "./utils/env.ts" +import type { RoyaltyRecipient, asset_transactions, output } from './types.ts'; +import { Constr, Data, toText } from 'https://deno.land/x/lucid@0.10.7/mod.ts'; + +export const CIP68_ROYALTY_TOKEN_HEX = '001f4d70526f79616c7479'; // (500)Royalty +export const CIP68_222_TOKEN_HEX = '000de140'; + +type RoyaltyConstr = Constr< + Map | Map[] +>; + +// TODO: this conversion does't work wor bigger values. +// Example for input == 57% it loose 1% during backward conversion. +export function toCip68ContractRoyalty(percentage: number): number { + return Math.floor(1 / (percentage / 1000)); +} + +export function percentageFromCip68Royalty(value: number): number { + return Math.trunc((10 / value) * 1000) / 10; +} + +export function getCip68BlockfrostVersion(version: string) { + switch (version) { + case 'CIP68v1': + return 1; + case 'CIP68v2': + return 2; + default: + return undefined; + } +} + +export async function getCip68RoyaltyMetadata( + policyId: string +): Promise { + const assetName = policyId + CIP68_ROYALTY_TOKEN_HEX; + // ------ uncomment when blockfrost will be ready ---------- + // const asset = await CardanoAssetsService.getAssets1(assetName); + // if (asset.metadata) { + // return [asset.metadata] + // } + try { + const lastTx = await getAssetsTransactions( + assetName, + 1, + 1, + 'desc' + ); + const lastTxHash = lastTx?.at(0)?.tx_hash; + if (lastTxHash) { + const txUtxos = await getTxsUtxos(lastTxHash); + const royaltyData = await Promise.all( + txUtxos.outputs.map(async (output: output) => + await getRoyaltyMetadata(output) + )); + return royaltyData.flat(); + } + } catch (err) { + // Best effort just silently fail + } + return []; +} + + +async function getRoyaltyMetadata(out: output): Promise { + let datum; + if(!out.inline_datum && out.data_hash) + datum = out.inline_datum ?? await getScriptsDatumCbor(out.data_hash).then(dat => dat.cbor) + else + datum = out.inline_datum + if (datum) { + try { + const datumConstructor: RoyaltyConstr = Data.from(datum); + const datumData = datumConstructor.fields[0]; + if (Array.isArray(datumData)) { + const royalties = datumData + .map((royaltyMap) => decodeDatumMap(royaltyMap)) + .filter((royalty) => royalty); + return royalties as RoyaltyRecipient[]; + } + } catch (error) { + // do nothing + } + } + return []; +} + +function decodeDatumMap( + data: Map +): RoyaltyRecipient | undefined { + const model: { [key: string]: string | number } = {}; + data.forEach((value, key) => { + model[toText(key)] = + typeof value === 'string' ? toText(value) : Number(value); + }); + if ('address' in model && 'fee' in model) { + return model as unknown as RoyaltyRecipient; + } +} + +// based on Blockfrost's openAPI +const blockfrost_url = getEnv("BLOCKFROST_URL"); +const headers = { + project_id: getEnv("BLOCKFROST_PROJECT_KEY"), lucid: "0.10.7" +} + +export async function getAssetsTransactions( + asset: string, + count: number = 100, + page: number = 1, + order: 'asc' | 'desc' = 'asc', + ): Promise +{ + const response = + await fetch( + blockfrost_url + "/assets/" + asset + "/transactions", + { headers }, + ).then((res) => res.json()); + + return response; +} + +async function getTxsUtxos(txHash: string): Promise<{ outputs: output[] }> { + const response = + await fetch( + blockfrost_url + "/txs/" + txHash, + { headers }, + ).then((res) => res.json()); + + return response; +} + +async function getScriptsDatumCbor(datumHash: string): Promise<{ cbor: string }> { + const response = + await fetch( + blockfrost_url + "/scripts/datum/" + datumHash + "/cbor", + { headers }, + ).then((res) => res.json()); + + return response; +} \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/types.ts b/CIP-0102/ref_impl/offchain-reference/types.ts new file mode 100644 index 000000000..c81b6451e --- /dev/null +++ b/CIP-0102/ref_impl/offchain-reference/types.ts @@ -0,0 +1,111 @@ +/* + needs + RoyaltyRecipient, + DatumMetadata, + Metadata, + RoyaltyFlag, + MediaAssets +*/ + +import { Data } from "https://deno.land/x/lucid@0.10.7/mod.ts"; + +export const MetadataSchema = Data.Map(Data.Bytes(), Data.Any()); +export type Metadata = Data.Static; +export const Metadata = MetadataSchema as unknown as Metadata; + +export const DatumMetadataSchema = Data.Object({ + metadata: MetadataSchema, + version: Data.Integer({ minimum: 1, maximum: 1 }), + extra: Data.Any(), +}); +export type DatumMetadata = Data.Static; +export const DatumMetadata = DatumMetadataSchema as unknown as DatumMetadata; + +export const RoyaltyFlagSchema = Data.Object({ royalty_included: Data.Integer() }) +export type RoyaltyFlag = Data.Static; +export const RoyaltyFlag = RoyaltyFlagSchema as unknown as RoyaltyFlag; + +export type RoyaltyRecipient = { + address: string; + fee: number; + maxFee?: number; + minFee?: number; +} + +// NOTE: these are ripped from `lucid-cardano` and modified because his types don't match the CIP-25 standard!!! +export type MediaAsset = { + name: string + image: string | string[] + mediaType?: string + description?: string | string[] + files?: MediaAssetFile[] + [key: string]: unknown +} + +declare type MediaAssetFile = { + name?: string + mediaType: string + src: string | string[] +} + +export type MediaAssets = { + [key: string]: MediaAsset; +}; + +// based on Blockfrost's openAPI +export type asset_transactions = Array<{ + /** + * Hash of the transaction + */ + tx_hash: string; + /** + * Transaction index within the block + */ + tx_index: number; + /** + * Block height + */ + block_height: number; + /** + * Block creation time in UNIX time + */ + block_time: number; + }>; + + export type output = { + /** + * Output address + */ + address: string; + amount: Array<{ + /** + * The unit of the value + */ + unit: string; + /** + * The quantity of the unit + */ + quantity: string; + }>; + /** + * UTXO index in the transaction + */ + output_index: number; + /** + * The hash of the transaction output datum + */ + data_hash: string | null; + /** + * CBOR encoded inline datum + */ + inline_datum: string | null; + /** + * Whether the output is a collateral output + */ + collateral: boolean; + /** + * The hash of the reference script of the output + */ + reference_script_hash: string | null; + }; + \ No newline at end of file From 0a8a0e101792abcb60a08d3a35d71e7de3448caf Mon Sep 17 00:00:00 2001 From: SamDelaney Date: Tue, 27 Feb 2024 15:57:09 -0800 Subject: [PATCH 07/24] minor cleanup --- .../ref_impl/offchain-reference/deno.lock | 116 ++++++++++++++++++ CIP-0102/ref_impl/offchain-reference/index.ts | 2 +- CIP-0102/ref_impl/offchain-reference/mint.ts | 10 +- CIP-0102/ref_impl/offchain-reference/query.ts | 44 +++++++ CIP-0102/ref_impl/offchain-reference/read.ts | 61 ++------- 5 files changed, 177 insertions(+), 56 deletions(-) create mode 100644 CIP-0102/ref_impl/offchain-reference/deno.lock create mode 100644 CIP-0102/ref_impl/offchain-reference/query.ts diff --git a/CIP-0102/ref_impl/offchain-reference/deno.lock b/CIP-0102/ref_impl/offchain-reference/deno.lock new file mode 100644 index 000000000..0ac40fafb --- /dev/null +++ b/CIP-0102/ref_impl/offchain-reference/deno.lock @@ -0,0 +1,116 @@ +{ + "version": "3", + "remote": { + "https://deno.land/std@0.100.0/encoding/hex.ts": "f952e0727bddb3b2fd2e6889d104eacbd62e92091f540ebd6459317a61932d9b", + "https://deno.land/std@0.110.0/_util/assert.ts": "2f868145a042a11d5ad0a3c748dcf580add8a0dbc0e876eaa0026303a5488f58", + "https://deno.land/std@0.110.0/_util/os.ts": "dfb186cc4e968c770ab6cc3288bd65f4871be03b93beecae57d657232ecffcac", + "https://deno.land/std@0.110.0/async/deadline.ts": "1d6ac7aeaee22f75eb86e4e105d6161118aad7b41ae2dd14f4cfd3bf97472b93", + "https://deno.land/std@0.110.0/async/debounce.ts": "b2f693e4baa16b62793fd618de6c003b63228db50ecfe3bd51fc5f6dc0bc264b", + "https://deno.land/std@0.110.0/async/deferred.ts": "ab60d46ba561abb3b13c0c8085d05797a384b9f182935f051dc67136817acdee", + "https://deno.land/std@0.110.0/async/delay.ts": "db68b7c22518ea9805be110cdc914017d741894d2bececf4d78607fd2f0548e7", + "https://deno.land/std@0.110.0/async/mod.ts": "78425176fabea7bd1046ce3819fd69ce40da85c83e0f174d17e8e224a91f7d10", + "https://deno.land/std@0.110.0/async/mux_async_iterator.ts": "62abff3af9ff619e8f2adc96fc70d4ca020fa48a50c23c13f12d02ed2b760dbe", + "https://deno.land/std@0.110.0/async/pool.ts": "353ce4f91865da203a097aa6f33de8966340c91b6f4a055611c8c5d534afd12f", + "https://deno.land/std@0.110.0/async/tee.ts": "63811ea47268825db2b15e973dc5c37bab37b749ffa00d2b7bbb6c6f568412cb", + "https://deno.land/std@0.110.0/bytes/mod.ts": "440684e07e8f57a19a43b34d57eb63af0b36fc92b6657b6dcdbf9d5612d62e29", + "https://deno.land/std@0.110.0/encoding/base64.ts": "eecae390f1f1d1cae6f6c6d732ede5276bf4b9cd29b1d281678c054dc5cc009e", + "https://deno.land/std@0.110.0/encoding/hex.ts": "5bc7df19af498c315cdaba69e2fce1b2aef5fc57344e8c21c08991aa8505a260", + "https://deno.land/std@0.110.0/fmt/colors.ts": "8368ddf2d48dfe413ffd04cdbb7ae6a1009cf0dccc9c7ff1d76259d9c61a0621", + "https://deno.land/std@0.110.0/fs/exists.ts": "b0d2e31654819cc2a8d37df45d6b14686c0cc1d802e9ff09e902a63e98b85a00", + "https://deno.land/std@0.110.0/io/buffer.ts": "3ead6bb11276ebcf093c403f74f67fd2205a515dbbb9061862c468ca56f37cd8", + "https://deno.land/std@0.110.0/io/util.ts": "85c33d61b20fd706acc094fe80d4c8ae618b04abcf3a96ca2b47071842c1c8ac", + "https://deno.land/std@0.110.0/node/_errors.ts": "74d1e7c7aad0f4a04df20be1f25f8a0a1d39483a75daabefa2cb285b0090e6e5", + "https://deno.land/std@0.110.0/node/_fs/_fs_access.ts": "7cbbfd1309e47983065dc7f186fd128eea0854c6693d58b39be2fd0f4de8d0c0", + "https://deno.land/std@0.110.0/node/_fs/_fs_appendFile.ts": "bd802989a76d2e87a92034cbbbf6ad3154bf925b68e1a0a43772b3f4fc2a3728", + "https://deno.land/std@0.110.0/node/_fs/_fs_chmod.ts": "101ea1cfc63b518f7434357e6e559c163b86c836826e87273ad4fbdd17153c6d", + "https://deno.land/std@0.110.0/node/_fs/_fs_chown.ts": "d314d0b3c33422c980e8b8d2abe95433d5d3ec6e0699b53de6e03c13bb37769e", + "https://deno.land/std@0.110.0/node/_fs/_fs_close.ts": "6a175fef187fa06ad7fd3c82a7a44410f947f5d20ec6673f871d4d327d98753e", + "https://deno.land/std@0.110.0/node/_fs/_fs_common.ts": "45f0b63e864d24c6c6c492a931e6b794f61ed125157e8f74282350557ec7e971", + "https://deno.land/std@0.110.0/node/_fs/_fs_constants.ts": "20e2d62e5c5bc1f94b68c14d686c0f61a683d405f31523456b863bb525c9cdee", + "https://deno.land/std@0.110.0/node/_fs/_fs_copy.ts": "ce349beecec9f833e8cbe02eaa6c79119cbdbe788093c2b9543ce4d865f2b941", + "https://deno.land/std@0.110.0/node/_fs/_fs_dir.ts": "b122324330d4d754a595618986039a2841f9551c16a20be21e93f371cc8e65ce", + "https://deno.land/std@0.110.0/node/_fs/_fs_dirent.ts": "c9f585db140749426bcbcd735801198e7c517d654cd72795a20dfa349e4bac05", + "https://deno.land/std@0.110.0/node/_fs/_fs_exists.ts": "ec185609b68149e259d4fb1d7872e70bb2f9aff135236a3c2e14109e579393cc", + "https://deno.land/std@0.110.0/node/_fs/_fs_fdatasync.ts": "a62e70004a0f9c2f49e369b44a9ff788bb13d7b8160c61cec942e5dc42da4181", + "https://deno.land/std@0.110.0/node/_fs/_fs_fstat.ts": "6e2280db419a521dd3ea4525982c02f84677493561d1bcbaa271abadc8e7f844", + "https://deno.land/std@0.110.0/node/_fs/_fs_fsync.ts": "c68c74966342d5a98437036a634359ac6feab85f114310892f14553c02fad5d4", + "https://deno.land/std@0.110.0/node/_fs/_fs_ftruncate.ts": "2964fe380c60db9e802a9ae65c72cbfee8d710abd0e078afbcfc2e0cc442599c", + "https://deno.land/std@0.110.0/node/_fs/_fs_futimes.ts": "88c1e0d7b4012bcee23f586b2b7282ac21725824f3961ffa235d96501afcb65f", + "https://deno.land/std@0.110.0/node/_fs/_fs_link.ts": "93c8997574a6f42aa2699012236b53f7ee2105400c207e0b8156dce04772ab81", + "https://deno.land/std@0.110.0/node/_fs/_fs_lstat.ts": "3d81117febd0abcab4e92ecbe70885179f62d10d09608d8ac6acfb7c2db34290", + "https://deno.land/std@0.110.0/node/_fs/_fs_mkdir.ts": "a98590e4a07416e4d7af1342d062b9c8e01997e6f047ed95411ef1215aa154b3", + "https://deno.land/std@0.110.0/node/_fs/_fs_mkdtemp.ts": "683c0e0fbe2583c318b2d67cd986cf55f8f7410199537edb76f3af0ec8b54a80", + "https://deno.land/std@0.110.0/node/_fs/_fs_open.ts": "6a08a3e5edf17f49589fa34223d2ea62193ea1e17392f4e84bfa49fc9d0ca532", + "https://deno.land/std@0.110.0/node/_fs/_fs_readFile.ts": "aa972cc3a6a13ddad8e7319c3908d089be56136ef11e2a8b8bfb48cfc368fde3", + "https://deno.land/std@0.110.0/node/_fs/_fs_readdir.ts": "d3b0c2b3d3b243e63f0c8df5294252edbb6a732c07f540044af3e89d6d75245f", + "https://deno.land/std@0.110.0/node/_fs/_fs_readlink.ts": "919df3481f3e339cfb153e899941fe93d4284287794fed3d92f071a055d473e9", + "https://deno.land/std@0.110.0/node/_fs/_fs_realpath.ts": "9dad0fe43e8c4cda6953bc297cd605c9647162e4a9d29cf91e435d96a741d8e1", + "https://deno.land/std@0.110.0/node/_fs/_fs_rename.ts": "98852b8f576e2bbdaede8b85abf8c459c3d0b50d8f6c8aff030a72f20ed26710", + "https://deno.land/std@0.110.0/node/_fs/_fs_rmdir.ts": "89458f749b97bf2e23e485cba09fbb18f57041141e1cbbcfa9e44bde75953e51", + "https://deno.land/std@0.110.0/node/_fs/_fs_stat.ts": "71c770228b0f6eb0bfa8b5db88e0313940f57fa3e5dccfbf46d8bd838d1a9497", + "https://deno.land/std@0.110.0/node/_fs/_fs_symlink.ts": "174e41dbb2dbd0af79ba6460f3ab849c48099b8e824beed7c7f7c5d6d616b36d", + "https://deno.land/std@0.110.0/node/_fs/_fs_truncate.ts": "fd016bc0b5ac236c4e88b326d0a9de0df6d56a512f20e927384fb4bd088d4e01", + "https://deno.land/std@0.110.0/node/_fs/_fs_unlink.ts": "95cb6d81c647ac46826a86b9ebc8ebd8008fd99080dd36ddb62c5ddc54339c5f", + "https://deno.land/std@0.110.0/node/_fs/_fs_utimes.ts": "5a87c6cb903f072a10a1917195e55f8673f4e1023964e16648e4146a778160e0", + "https://deno.land/std@0.110.0/node/_fs/_fs_watch.ts": "7f7757775b6cdeb2e66bf5dbb5e1eb15e105fce4429eecd1026e1301e696babe", + "https://deno.land/std@0.110.0/node/_fs/_fs_writeFile.ts": "6bf2646358e6e840353f1c1da184493121a1e57ab20e6ba6e8b1cf21574d4bf2", + "https://deno.land/std@0.110.0/node/_util/_util_callbackify.ts": "947aa66d148c10e484c5c5e65ca4041cdd65085d7045fb26d388269a63c4d079", + "https://deno.land/std@0.110.0/node/_util/_util_promisify.ts": "2ad6efe685f73443d5ed6ae009999789a8de4a0f01e6d2afdf242b4515477ee2", + "https://deno.land/std@0.110.0/node/_util/_util_types.ts": "ae3d21e07c975f06590ab80bbde8173670d70ff40546267c0c1df869fc2ff00c", + "https://deno.land/std@0.110.0/node/_utils.ts": "c32d3491e380488728d65ad471698ed0aadff7fe35bde0a26ba4dd8f434ed0e7", + "https://deno.land/std@0.110.0/node/buffer.ts": "29e7a8849479c9d325a1be899fd79d1289a858afe211ab5a0c78e57f3493dfea", + "https://deno.land/std@0.110.0/node/events.ts": "f92ba300cb0a6efa4ff4d8a267a507ee725858d6a250a2eeb829ec99b21f90b1", + "https://deno.land/std@0.110.0/node/fs.ts": "0f283d57d7d19b48845d80d9517c1a69c9b66cf29787ad1e182939e95c6a9c07", + "https://deno.land/std@0.110.0/node/fs/promises.ts": "88258c223ef5774e8467e1b7707be8ebf490f62b024cb56cf2b4aae29a81a6dd", + "https://deno.land/std@0.110.0/node/path.ts": "86b262d6957fba13d4f3d58a92ced49de4f40169d06542326b5547ff97257f0d", + "https://deno.land/std@0.110.0/node/util.ts": "23878bd3ee67a52e67cfe5acb78c7ccce9c54735c6d280b069577605e8679935", + "https://deno.land/std@0.110.0/path/_constants.ts": "1247fee4a79b70c89f23499691ef169b41b6ccf01887a0abd131009c5581b853", + "https://deno.land/std@0.110.0/path/_interface.ts": "1fa73b02aaa24867e481a48492b44f2598cd9dfa513c7b34001437007d3642e4", + "https://deno.land/std@0.110.0/path/_util.ts": "2e06a3b9e79beaf62687196bd4b60a4c391d862cfa007a20fc3a39f778ba073b", + "https://deno.land/std@0.110.0/path/common.ts": "f41a38a0719a1e85aa11c6ba3bea5e37c15dd009d705bd8873f94c833568cbc4", + "https://deno.land/std@0.110.0/path/glob.ts": "46708a3249cb5dc4a116cae3055114d6339bd5f0c1f412db6a4e0cb44c828a7d", + "https://deno.land/std@0.110.0/path/mod.ts": "4465dc494f271b02569edbb4a18d727063b5dbd6ed84283ff906260970a15d12", + "https://deno.land/std@0.110.0/path/posix.ts": "34349174b9cd121625a2810837a82dd8b986bbaaad5ade690d1de75bbb4555b2", + "https://deno.land/std@0.110.0/path/separator.ts": "8fdcf289b1b76fd726a508f57d3370ca029ae6976fcde5044007f062e643ff1c", + "https://deno.land/std@0.110.0/path/win32.ts": "2edb2f71f10578ee1168de01a8cbd3c65483e45a46bc2fa3156a0c6bfbd2720d", + "https://deno.land/std@0.110.0/testing/_diff.ts": "ccd6c3af6e44c74bf1591acb1361995f5f50df64323a6e7fb3f16c8ea792c940", + "https://deno.land/std@0.110.0/testing/asserts.ts": "6b0d6ba564bdff807bd0f0e93e02c48aa3177acf19416bf84a7f420191ef74cd", + "https://deno.land/std@0.148.0/bytes/equals.ts": "3c3558c3ae85526f84510aa2b48ab2ad7bdd899e2e0f5b7a8ffc85acb3a6043a", + "https://deno.land/std@0.148.0/bytes/mod.ts": "763f97d33051cc3f28af1a688dfe2830841192a9fea0cbaa55f927b49d49d0bf", + "https://deno.land/std@0.153.0/hash/sha256.ts": "aa9479c260f41b72c639f36c3e4bc9319940b5d2e52fe793ebe3dc646d12832f", + "https://deno.land/std@0.208.0/dotenv/mod.ts": "039468f5c87d39b69d7ca6c3d68ebca82f206ec0ff5e011d48205eea292ea5a6", + "https://deno.land/x/lucid@0.10.7/mod.ts": "9473507398048cb24dbb37b3a220777106c69c28d573898648deb2bb84a7e131", + "https://deno.land/x/lucid@0.10.7/package.json": "402ef25f1bcbf29c57b24f4737637db8470dc9197e80478455ec132a7a08a0cc", + "https://deno.land/x/lucid@0.10.7/src/core/core.ts": "5b2f6d4746933a425bfcb134a55b84c946bc2ddd98b795aa9189f345778b113c", + "https://deno.land/x/lucid@0.10.7/src/core/libs/cardano_message_signing/cardano_message_signing.generated.js": "b3cae6f286d855a1cf9c84987026bfa46f577e9edd6b269127e88c0f41d68bfa", + "https://deno.land/x/lucid@0.10.7/src/core/libs/cardano_multiplatform_lib/cardano_multiplatform_lib.generated.js": "286312109897e46032173ab3a0a233212ea0741051bcba936914ac7af8f22227", + "https://deno.land/x/lucid@0.10.7/src/core/mod.ts": "978b94101791fdad3113e5a9fa3813b1f48a8e505f68da14bcad72668b294926", + "https://deno.land/x/lucid@0.10.7/src/lucid/lucid.ts": "80f06b1965074a6c3504d509df676f2508d49cc0a8050dee1b1f8ea804fb565a", + "https://deno.land/x/lucid@0.10.7/src/lucid/message.ts": "b16bb7c06cbc81c777ac116d95db32959af42bc093ebe86b65f2dbc28b75f9b2", + "https://deno.land/x/lucid@0.10.7/src/lucid/mod.ts": "94bfe48ee683e932a67347786a1253a2e7277e53b1dc192d045d70bc5ce9bc10", + "https://deno.land/x/lucid@0.10.7/src/lucid/tx.ts": "5833f0cb10bcd83439e7caff03c20296b8e646ce1d3797e9db56f3fdcfe33e19", + "https://deno.land/x/lucid@0.10.7/src/lucid/tx_complete.ts": "a687e19df52072fe239af33081cfddf2fba40011324ff86f3352042940a65362", + "https://deno.land/x/lucid@0.10.7/src/lucid/tx_signed.ts": "58edce3295e5fa14977120d887b43796ebd40893caaf012d441a8b56909c8ae1", + "https://deno.land/x/lucid@0.10.7/src/misc/bip39.ts": "7dc0f49f96d43a254a048a956cac31ef8c8045f69f3b8dbc5350bc6c601441eb", + "https://deno.land/x/lucid@0.10.7/src/misc/crc8.ts": "c9abda52851d5c575f6a8714c3600d2e63bb8cfc942e035486b1ab03b780e2f9", + "https://deno.land/x/lucid@0.10.7/src/misc/sign_data.ts": "6f5066d19455ade77615b5cbbf5dd9947ea691a7b764f96c81ba911cc84183e5", + "https://deno.land/x/lucid@0.10.7/src/misc/wallet.ts": "7a8d63c234f0741dc49d3134554b7c455e24f4692ba42a3843aad7e37bcc7d2b", + "https://deno.land/x/lucid@0.10.7/src/mod.ts": "f4156883dc0dc394e9fd9e9e7222537a61ff8b2781a88761d7bcb65123a16bcf", + "https://deno.land/x/lucid@0.10.7/src/plutus/data.ts": "b6eb205124e6f340f47dd789d94d91146b2bf038c6a76b57ede12874b60d5920", + "https://deno.land/x/lucid@0.10.7/src/plutus/mod.ts": "3a7f784e950348d447cd6bcb27546b20902fae167e04fd42e67bfad57216e1a7", + "https://deno.land/x/lucid@0.10.7/src/plutus/time.ts": "f142f03f897a6e57625e8c0752bb73397818090871f3cff123ec64311590b6f9", + "https://deno.land/x/lucid@0.10.7/src/provider/blockfrost.ts": "7cd49ed72464f3faf3c4bc93b54a1c30dbe0f34035b3c5e844adfdeb3ca74e32", + "https://deno.land/x/lucid@0.10.7/src/provider/emulator.ts": "066892a5dfbe27bde86d449650913da18da8025bd618c7cfed0a6fc16e07bb29", + "https://deno.land/x/lucid@0.10.7/src/provider/kupmios.ts": "e15178e252d5a87b6c6123e51c3dc426ec7ebe02bac920dea26213f62eabcbd0", + "https://deno.land/x/lucid@0.10.7/src/provider/maestro.ts": "76c379f946bdc2198ccf6898ce1b09ed5a6ddb1d858f0abe2b39e5300aa7a6c1", + "https://deno.land/x/lucid@0.10.7/src/provider/mod.ts": "76b36094551556045e851fe0ba6012626e1a08c03dec62a21b69d22311266379", + "https://deno.land/x/lucid@0.10.7/src/types/global.ts": "3ea23ebcf9af819a01cbcf682856df2b178929ffec362113d24eb4a80a966ac3", + "https://deno.land/x/lucid@0.10.7/src/types/mod.ts": "2e6e4ffd7077025d1b45af726756455fc9c915666dd7f23a041be058039b49c6", + "https://deno.land/x/lucid@0.10.7/src/types/types.ts": "b637a944c04576c42c2bb5df98aa8128cd4677e04fa0ec911e042ce96e764fa2", + "https://deno.land/x/lucid@0.10.7/src/utils/cost_model.ts": "f04e4d393062b0057f703e37ddcb1aa9fe378b589134c679688f9786235a25ee", + "https://deno.land/x/lucid@0.10.7/src/utils/merkle_tree.ts": "af39a9167eb8b083a19a980916c95ab40332e959ef20bc43fdfef69eef08e594", + "https://deno.land/x/lucid@0.10.7/src/utils/mod.ts": "7e405bfa0db96d9e995e67fb77251e8465843addf141caf4ce64b3efa6077822", + "https://deno.land/x/lucid@0.10.7/src/utils/utils.ts": "177ec0578b788f947f6afb6cc911c4eae776aaf98b5249412a750b8f366b4d11", + "https://deno.land/x/typebox@0.25.13/src/typebox.ts": "9b20b62c0bf31f1a9128b6dc6dfd470a5d956a48f0b0ef0a1ccc4d794fb55dcc" + } +} diff --git a/CIP-0102/ref_impl/offchain-reference/index.ts b/CIP-0102/ref_impl/offchain-reference/index.ts index 9e7f42db2..8cf8bdf00 100644 --- a/CIP-0102/ref_impl/offchain-reference/index.ts +++ b/CIP-0102/ref_impl/offchain-reference/index.ts @@ -1,4 +1,4 @@ -import { getAssetsTransactions } from "./read.ts" +import { getAssetsTransactions } from "./query.ts" const response = await getAssetsTransactions("2b29ff2309668a2e58d96da26bd0e1c0dd4013e5853e96df2be27e42001f4d70526f79616c7479") diff --git a/CIP-0102/ref_impl/offchain-reference/mint.ts b/CIP-0102/ref_impl/offchain-reference/mint.ts index 61f5287c5..7fe01fd34 100644 --- a/CIP-0102/ref_impl/offchain-reference/mint.ts +++ b/CIP-0102/ref_impl/offchain-reference/mint.ts @@ -80,7 +80,13 @@ export async function createRoyalty(lucid: Lucid, policy: Script, validator: Scr return transaction; } - export async function createTimelockedTransaction( +/** Mints CIP-68 nfts specified in the sanitizedMetadataAssets parameter. + * The cip102 parameter allows for 3 cases: + * - "NoRoyalty" - no royalty is attached to the collection - obviously not recommended but hey, it's your collection + * - "Premade" - the royalty has already been minted to the policy - no need to mint new tokens + * - RoyaltyRecipient - a new royalty token is minted to the policy in this transaction according to the information in the parameter + */ + export async function mintNFTs( lucid: Lucid, policy: Script, validator: Script, @@ -172,7 +178,7 @@ export async function createRoyalty(lucid: Lucid, policy: Script, validator: Scr } /// CIP-25 minting. Don't use unless you're certain this is what you want. -export async function timeLockMintNft(lucid: Lucid, timestamp: number, nftData: NFTData) { +export async function mintWithCip25(lucid: Lucid, timestamp: number, nftData: NFTData) { const validTo = new Date(); validTo.setHours(validTo.getHours() + 1); diff --git a/CIP-0102/ref_impl/offchain-reference/query.ts b/CIP-0102/ref_impl/offchain-reference/query.ts new file mode 100644 index 000000000..49972d2c2 --- /dev/null +++ b/CIP-0102/ref_impl/offchain-reference/query.ts @@ -0,0 +1,44 @@ +import { asset_transactions, output } from "./types.ts"; +import { getEnv } from "./utils/env.ts"; + +// based on Blockfrost's openAPI +const blockfrost_url = getEnv("BLOCKFROST_URL"); +const headers = { + project_id: getEnv("BLOCKFROST_PROJECT_KEY"), lucid: "0.10.7" +} + +export async function getAssetsTransactions( + asset: string, + _count: number = 100, + _page: number = 1, + _order: 'asc' | 'desc' = 'asc', + ): Promise +{ + const response = + await fetch( + blockfrost_url + "/assets/" + asset + "/transactions", + { headers }, + ).then((res) => res.json()); + + return response; +} + +export async function getTxsUtxos(txHash: string): Promise<{ outputs: output[] }> { + const response = + await fetch( + blockfrost_url + "/txs/" + txHash, + { headers }, + ).then((res) => res.json()); + + return response; +} + +export async function getScriptsDatumCbor(datumHash: string): Promise<{ cbor: string }> { + const response = + await fetch( + blockfrost_url + "/scripts/datum/" + datumHash + "/cbor", + { headers }, + ).then((res) => res.json()); + + return response; +} \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/read.ts b/CIP-0102/ref_impl/offchain-reference/read.ts index f7eeb7e2a..f135f2e52 100644 --- a/CIP-0102/ref_impl/offchain-reference/read.ts +++ b/CIP-0102/ref_impl/offchain-reference/read.ts @@ -1,17 +1,14 @@ -import { getEnv } from "./utils/env.ts" -import type { RoyaltyRecipient, asset_transactions, output } from './types.ts'; +import type { RoyaltyRecipient, output } from './types.ts'; import { Constr, Data, toText } from 'https://deno.land/x/lucid@0.10.7/mod.ts'; +import { getAssetsTransactions, getScriptsDatumCbor, getTxsUtxos } from "./query.ts"; -export const CIP68_ROYALTY_TOKEN_HEX = '001f4d70526f79616c7479'; // (500)Royalty -export const CIP68_222_TOKEN_HEX = '000de140'; +export const CIP102_ROYALTY_TOKEN_HEX = '001f4d70526f79616c7479'; // (500)Royalty type RoyaltyConstr = Constr< Map | Map[] >; -// TODO: this conversion does't work wor bigger values. -// Example for input == 57% it loose 1% during backward conversion. -export function toCip68ContractRoyalty(percentage: number): number { +export function toCip102ContractRoyalty(percentage: number): number { return Math.floor(1 / (percentage / 1000)); } @@ -30,10 +27,10 @@ export function getCip68BlockfrostVersion(version: string) { } } -export async function getCip68RoyaltyMetadata( +export async function getCip102RoyaltyMetadata( policyId: string ): Promise { - const assetName = policyId + CIP68_ROYALTY_TOKEN_HEX; + const assetName = policyId + CIP102_ROYALTY_TOKEN_HEX; // ------ uncomment when blockfrost will be ready ---------- // const asset = await CardanoAssetsService.getAssets1(assetName); // if (asset.metadata) { @@ -55,7 +52,7 @@ export async function getCip68RoyaltyMetadata( )); return royaltyData.flat(); } - } catch (err) { + } catch (_error) { // Best effort just silently fail } return []; @@ -78,7 +75,7 @@ async function getRoyaltyMetadata(out: output): Promise { .filter((royalty) => royalty); return royalties as RoyaltyRecipient[]; } - } catch (error) { + } catch (_error) { // do nothing } } @@ -96,46 +93,4 @@ function decodeDatumMap( if ('address' in model && 'fee' in model) { return model as unknown as RoyaltyRecipient; } -} - -// based on Blockfrost's openAPI -const blockfrost_url = getEnv("BLOCKFROST_URL"); -const headers = { - project_id: getEnv("BLOCKFROST_PROJECT_KEY"), lucid: "0.10.7" -} - -export async function getAssetsTransactions( - asset: string, - count: number = 100, - page: number = 1, - order: 'asc' | 'desc' = 'asc', - ): Promise -{ - const response = - await fetch( - blockfrost_url + "/assets/" + asset + "/transactions", - { headers }, - ).then((res) => res.json()); - - return response; -} - -async function getTxsUtxos(txHash: string): Promise<{ outputs: output[] }> { - const response = - await fetch( - blockfrost_url + "/txs/" + txHash, - { headers }, - ).then((res) => res.json()); - - return response; -} - -async function getScriptsDatumCbor(datumHash: string): Promise<{ cbor: string }> { - const response = - await fetch( - blockfrost_url + "/scripts/datum/" + datumHash + "/cbor", - { headers }, - ).then((res) => res.json()); - - return response; } \ No newline at end of file From 01a05c321731916aa81dfbe5c32d562bc55b595b Mon Sep 17 00:00:00 2001 From: SamDelaney Date: Thu, 7 Mar 2024 14:48:28 -0800 Subject: [PATCH 08/24] fix asset txs query --- CIP-0102/ref_impl/offchain-reference/.gitignore | 3 ++- CIP-0102/ref_impl/offchain-reference/index.ts | 5 ++++- CIP-0102/ref_impl/offchain-reference/query.ts | 2 +- CIP-0102/ref_impl/offchain-reference/read.ts | 6 +++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CIP-0102/ref_impl/offchain-reference/.gitignore b/CIP-0102/ref_impl/offchain-reference/.gitignore index 2eea525d8..c2111fc33 100644 --- a/CIP-0102/ref_impl/offchain-reference/.gitignore +++ b/CIP-0102/ref_impl/offchain-reference/.gitignore @@ -1 +1,2 @@ -.env \ No newline at end of file +.env +.vscode \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/index.ts b/CIP-0102/ref_impl/offchain-reference/index.ts index 8cf8bdf00..6a2b37ef0 100644 --- a/CIP-0102/ref_impl/offchain-reference/index.ts +++ b/CIP-0102/ref_impl/offchain-reference/index.ts @@ -1,5 +1,8 @@ import { getAssetsTransactions } from "./query.ts" +import { getCip102RoyaltyMetadata } from "./read.ts"; -const response = await getAssetsTransactions("2b29ff2309668a2e58d96da26bd0e1c0dd4013e5853e96df2be27e42001f4d70526f79616c7479") +// const response = await getAssetsTransactions("2b29ff2309668a2e58d96da26bd0e1c0dd4013e5853e96df2be27e42001f4d70526f79616c7479") + +const response = await getCip102RoyaltyMetadata("60a75923cfc6e241b5da7fd6328d8846275ce94c15fcfc903538e012") console.log(response) \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/query.ts b/CIP-0102/ref_impl/offchain-reference/query.ts index 49972d2c2..1f550ba84 100644 --- a/CIP-0102/ref_impl/offchain-reference/query.ts +++ b/CIP-0102/ref_impl/offchain-reference/query.ts @@ -26,7 +26,7 @@ export async function getAssetsTransactions( export async function getTxsUtxos(txHash: string): Promise<{ outputs: output[] }> { const response = await fetch( - blockfrost_url + "/txs/" + txHash, + blockfrost_url + "/txs/" + txHash + "/utxos", { headers }, ).then((res) => res.json()); diff --git a/CIP-0102/ref_impl/offchain-reference/read.ts b/CIP-0102/ref_impl/offchain-reference/read.ts index f135f2e52..75e436201 100644 --- a/CIP-0102/ref_impl/offchain-reference/read.ts +++ b/CIP-0102/ref_impl/offchain-reference/read.ts @@ -12,7 +12,7 @@ export function toCip102ContractRoyalty(percentage: number): number { return Math.floor(1 / (percentage / 1000)); } -export function percentageFromCip68Royalty(value: number): number { +export function percentageFromCip102Royalty(value: number): number { return Math.trunc((10 / value) * 1000) / 10; } @@ -30,7 +30,7 @@ export function getCip68BlockfrostVersion(version: string) { export async function getCip102RoyaltyMetadata( policyId: string ): Promise { - const assetName = policyId + CIP102_ROYALTY_TOKEN_HEX; + const asset_unit = policyId + CIP102_ROYALTY_TOKEN_HEX; // ------ uncomment when blockfrost will be ready ---------- // const asset = await CardanoAssetsService.getAssets1(assetName); // if (asset.metadata) { @@ -38,7 +38,7 @@ export async function getCip102RoyaltyMetadata( // } try { const lastTx = await getAssetsTransactions( - assetName, + asset_unit, 1, 1, 'desc' From c248735c88546adef577dca1be682ab6c2c80e16 Mon Sep 17 00:00:00 2001 From: SamDelaney Date: Wed, 13 Mar 2024 18:54:27 -0700 Subject: [PATCH 09/24] organization, polish & minor logic improvements --- .../ref_impl/offchain-reference/conversion.ts | 11 ++ CIP-0102/ref_impl/offchain-reference/index.ts | 4 +- CIP-0102/ref_impl/offchain-reference/mint.ts | 153 ++++++++---------- CIP-0102/ref_impl/offchain-reference/query.ts | 21 ++- CIP-0102/ref_impl/offchain-reference/read.ts | 71 ++++---- CIP-0102/ref_impl/offchain-reference/types.ts | 19 +-- 6 files changed, 146 insertions(+), 133 deletions(-) create mode 100644 CIP-0102/ref_impl/offchain-reference/conversion.ts diff --git a/CIP-0102/ref_impl/offchain-reference/conversion.ts b/CIP-0102/ref_impl/offchain-reference/conversion.ts new file mode 100644 index 000000000..4a46e86b5 --- /dev/null +++ b/CIP-0102/ref_impl/offchain-reference/conversion.ts @@ -0,0 +1,11 @@ +export const CIP102_ROYALTY_TOKEN_NAME = '001f4d70526f79616c7479'; // (500)Royalty + +// convert from percentage to onchain royalty value +export function toOnchainRoyalty(percentage: number): number { + return Math.floor(1 / (percentage / 1000)); +} + +// convert from onchain royalty value to percentage +export function fromOnchainRoyalty(value: number): number { + return Math.trunc((10 / value) * 1000) / 10; +} \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/index.ts b/CIP-0102/ref_impl/offchain-reference/index.ts index 6a2b37ef0..f10357cc9 100644 --- a/CIP-0102/ref_impl/offchain-reference/index.ts +++ b/CIP-0102/ref_impl/offchain-reference/index.ts @@ -1,8 +1,8 @@ import { getAssetsTransactions } from "./query.ts" -import { getCip102RoyaltyMetadata } from "./read.ts"; +import { getRoyaltyPolicy } from "./read.ts"; // const response = await getAssetsTransactions("2b29ff2309668a2e58d96da26bd0e1c0dd4013e5853e96df2be27e42001f4d70526f79616c7479") -const response = await getCip102RoyaltyMetadata("60a75923cfc6e241b5da7fd6328d8846275ce94c15fcfc903538e012") +const response = await getRoyaltyPolicy("60a75923cfc6e241b5da7fd6328d8846275ce94c15fcfc903538e012") console.log(response) \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/mint.ts b/CIP-0102/ref_impl/offchain-reference/mint.ts index 7fe01fd34..ea1140c0f 100644 --- a/CIP-0102/ref_impl/offchain-reference/mint.ts +++ b/CIP-0102/ref_impl/offchain-reference/mint.ts @@ -18,31 +18,24 @@ import { MediaAssets } from "./types.ts"; +/** + * Included in this file: + * - createRoyalty() - creates a royalty token with no other assets + * - mintNFTs() - mints CIP-68 nfts, may include a royalty token in the transaction as well + * - createTimelockedMP() - utility to create a parameterized minting policy + */ + +// generated from the onchain-reference directory const timeLockMintingContract = '5906e90100003232323232323232323232322322322232533300b53300c49107656e746572656400132533300c533300c533300c3322323253330103370e9001000899b89375a602c601a0040062940c034004c00cc02cc050c054c02cc050c054c054c054c054c054c054c054c02c008c004c02400c01c5288a99806a493269735f6e6f745f65787069726564286374782e7472616e73616374696f6e2c206c6f636b5f74696d6529203f2046616c73650014a02a6660186644646600200200644a66602800229404c8c94ccc048cdc78010028a511330040040013018002375c602c0026eb0c048c04cc04cc04cc04cc04cc04cc04cc04cc024c004c02400c0145288a99806a493a6c6973742e686173286374782e7472616e73616374696f6e2e65787472615f7369676e61746f726965732c206f776e657229203f2046616c73650014a0294054cc0352410670617373656400132533300d3370e9000180580089919191919299809a490a7175616e746974793a20001533013332323232323223732660046ea0005221003001001222533333302100213232323233009533301d337100069007099b80483c80400c54ccc074cdc4001a410004266e04cdc0241002800690070b19b8a4890128000015333020001133714911035b5d2900004133714911035b5f2000375c603e66600e00266ec1300102415d00375266e292210129000042233760980103422c2000375266601001000466e28dd718100009bae30210013758603c0046eb4c070004c8cdd81ba8301c001374e603a0026ea80084c94ccc0780044cdc5245027b7d00002133714911037b5f2000375c603a6644646600200200844a6660440022008266006604800266004004604a00266ec130010342207d0037520044466ec1300103422c2000375266600c00c603c00466e29221023a2000333006006301f002337146eb8c078004dd7180f8009bab002132533301e0011337149101025b5d00002133714911035b5f2000375c603a66600a00266ec1300102415d0037520044466ec1300103422c2000375266600c00c00466e28dd7180f0009bae301f001375800426600a6eb40080044c8cdc5244102682700332232333001001003002222533301f3371000490000800899191919980300319b8100548008cdc599b80002533302233710004900a0a40c02903719b8b33700002a66604466e2000520141481805206e0043370c004901019b8300148080cdc700300119b81371a002900119b8a4881012700002375c004444646600200200844a66603c0022008266006604000266004004604200244646600200200644a66603066e1c0052000133714910101300000315333018337100029000099b8a489012d003300200233702900000089980299b8400148050cdc599b803370a002900a240c00066002002444a66602a66e2400920001001133300300333708004900a19b8b3370066e140092014481800040044c94ccc04ccdc3a40000022a66602666e25200200214a22a66028921157175616e74697479203e3d2031203f2046616c73650014a02a66602666e24009200114a22a66028921167175616e74697479203c3d202d31203f2046616c73650014a060200146eb4c05c004c05c004c058004dd6180a00098050008a998072481244578706563746564206f6e20696e636f727265637420436f6e7374722076617269616e74001632323233323001001222533301600214c103d87a80001323253330143370e0069000099ba548000cc064dd380125eb804ccc014014004cdc0801a400460340066eb0c0600080052000323300100100222533301400114bd70099199911191980080080191299980d00088018991999111980f9ba73301f37520126603e6ea400ccc07cdd400125eb80004dd7180c8009bad301a00133003003301e002301c001375c60260026eacc050004cc00c00cc060008c058004c8cc004004008894ccc04c00452f5bded8c0264646464a66602666e3d221000021003133018337606ea4008dd3000998030030019bab3015003375c6026004602e004602a0026eacc048c04cc04cc04cc04cc024c004c02400c54cc0352401254578706563746564206f6e20696e636f727265637420426f6f6c65616e2076617269616e74001623012001149854cc0312411856616c696461746f722072657475726e65642066616c7365001365632533300b3370e90000008a99980798040018a4c2a660180142c2a66601666e1d20020011533300f3008003149854cc0300285854cc03124128436f6e73747220696e646578206469646e2774206d61746368206120747970652076617269616e7400163008002375c0026eb40048c01cdd5000918029baa00149011d4578706563746564206e6f206669656c647320666f7220436f6e737472005734ae7155ceaab9e5573eae815d0aba257481' -export type NFTData = { - name: string, - description: string, - image: string, -} - -export function createTimelockedMP( - lucid: Lucid, - timestamp: number, - walletAddress: string -) { - const paymentHash = lucid.utils.getAddressDetails(walletAddress).paymentCredential?.hash; - - if(!paymentHash) - throw new Error("Payment hash not found") - - const policy: Script = { - type: "PlutusV2", - script: applyParamsToScript(timeLockMintingContract, [BigInt(timestamp), paymentHash]), - } - return policy; -} - +/** + * Create a royalty token with no other assets + * @param lucid a lucid instance with a wallet already selected + * @param policy the minting policy to mint off of + * @param validator the validator to lock reference & royalty datums ator + * @param royalty the royalty metadata + * @returns a TxComplete object, ready to sign & submit + */ export async function createRoyalty(lucid: Lucid, policy: Script, validator: Script, royalty: RoyaltyRecipient) { const royaltyDatum = Data.to({ metadata: Data.castFrom(Data.fromJson([royalty]), Metadata), @@ -50,15 +43,19 @@ export async function createRoyalty(lucid: Lucid, policy: Script, validator: Scr extra: Data.from(Data.void()) }, DatumMetadata) + // generous tx validity window const validTo = new Date(); validTo.setHours(validTo.getHours() + 1); + // user wallet info const utxos = await lucid.wallet.getUtxos(); const walletAddress = await lucid.wallet.address(); + // the royalty token const policyId = lucid.utils.mintingPolicyToId(policy); - const nftUnit = toUnit(policyId, fromText("Royalty"), 500); + + // the royalty validator address const validatorAddress = lucid.utils.validatorToAddress(validator) const transaction = await lucid @@ -80,12 +77,18 @@ export async function createRoyalty(lucid: Lucid, policy: Script, validator: Scr return transaction; } -/** Mints CIP-68 nfts specified in the sanitizedMetadataAssets parameter. - * The cip102 parameter allows for 3 cases: - * - "NoRoyalty" - no royalty is attached to the collection - obviously not recommended but hey, it's your collection - * - "Premade" - the royalty has already been minted to the policy - no need to mint new tokens - * - RoyaltyRecipient - a new royalty token is minted to the policy in this transaction according to the information in the parameter - */ +/** + * Mints CIP-68 nfts as specified in the sanitizedMetadataAssets parameter. + * @param lucid a lucid instance with a wallet already selected + * @param policy the minting policy to mint off of + * @param validator the validator to lock reference & royalty datums at + * @param sanitizedMetadataAssets the asset specification to mint off of + * @param cip102 The cip102 parameter allows for 3 cases: + * - "NoRoyalty" - no royalty is attached to the collection - obviously not recommended here but hey, it's your collection + * - "Premade" - the royalty has already been minted to the policy - no need to mint new tokens, but include the flag to indicate a royalty token exists + * - RoyaltyRecipient - a new royalty token is minted to the policy in this transaction according to the information in the parameter + * @returns an object containing a TxComplete object & the policyId of the minted assets + */ export async function mintNFTs( lucid: Lucid, policy: Script, @@ -93,24 +96,19 @@ export async function createRoyalty(lucid: Lucid, policy: Script, validator: Scr sanitizedMetadataAssets: MediaAssets, cip102?: "NoRoyalty" | "Premade" | RoyaltyRecipient ) { + // extract royalty information or mark undefined const royalty = cip102 === "NoRoyalty" || cip102 === "Premade" ? undefined : cip102; + // get utxos from users wallet const utxos = await lucid.wallet.getUtxos(); - - // Grab any utxo to use to create a unique policy if (!utxos || !utxos.length || !utxos[0]) { return { error: 'empty-wallet' }; } - const referenceUtxo = utxos[0]; - // TODO: May need more utxos to pay for the transaction if the one found wasn't big enough - + // calculate the assets to mint const tokenNames = Object.keys(sanitizedMetadataAssets); const policyId = lucid.utils.mintingPolicyToId(policy); - - const validatorAddress = lucid.utils.validatorToAddress(validator) - const assets: Assets = {}; if(royalty) assets[toUnit(policyId, fromText("Royalty"), 500)] = 1n; @@ -119,10 +117,13 @@ export async function createRoyalty(lucid: Lucid, policy: Script, validator: Scr assets[toUnit(policyId, fromText(tokenName), 222)] = 1n; }); - const validTo = new Date(); - validTo.setHours(validTo.getHours(), validTo.getMinutes() + 5); + // addresses to send assets to + const validatorAddress = lucid.utils.validatorToAddress(validator) + const walletAddress = await lucid.wallet.address() - const walletAddress = await lucid.wallet.address() + // generous tx validity window + const validTo = new Date(); + validTo.setHours(validTo.getHours() + 1); let incompleteTx = await lucid .newTx() @@ -132,14 +133,17 @@ export async function createRoyalty(lucid: Lucid, policy: Script, validator: Scr .validTo(validTo.getTime()) .addSigner(walletAddress) + // send assets & datums to associated addresses const attachDatums = (tx: Tx): Tx => { if(royalty) { + // construct the royalty datum const royaltyDatum = Data.to({ metadata: Data.castFrom(Data.fromJson([royalty]), Metadata), version: BigInt(1), extra: Data.from(Data.void()) }, DatumMetadata) + // attach the royalty token & datum to the transaction tx = tx.payToContract( validatorAddress, { inline: royaltyDatum }, @@ -147,18 +151,22 @@ export async function createRoyalty(lucid: Lucid, policy: Script, validator: Scr ) } + // attach the royalty flag to the reference tokens if a royalty policy exists const has102Royalty = cip102 === "Premade" || royalty !== undefined; const extra = has102Royalty ? Data.to({ royalty_included: BigInt(1) }, RoyaltyFlag) : Data.from(Data.void()); for(const tokenName of tokenNames) { + + // construct the reference datum const metadataDatum = Data.to({ metadata: Data.castFrom(Data.fromJson(sanitizedMetadataAssets[tokenName]), Metadata), version: BigInt(1), extra }, DatumMetadata) + // attach the reference token & datum to the transaction tx = tx.payToContract( validatorAddress, { inline: metadataDatum }, @@ -169,52 +177,33 @@ export async function createRoyalty(lucid: Lucid, policy: Script, validator: Scr return tx; } - if(cip102) - incompleteTx = await attachDatums(incompleteTx) + incompleteTx = await attachDatums(incompleteTx) const tx = await incompleteTx.complete() return { tx, policyId }; } -/// CIP-25 minting. Don't use unless you're certain this is what you want. -export async function mintWithCip25(lucid: Lucid, timestamp: number, nftData: NFTData) { - const validTo = new Date(); - validTo.setHours(validTo.getHours() + 1); +/** + * parameterize the timelocked minting policy with your wallet address & minting deadline + * @param lucid a lucid instance with a wallet already selected + * @param timestamp the minting deadline + * @param walletAddress the wallet address allowed to mint + * @returns the minting policy in a Script object + */ +export function createTimelockedMP( + lucid: Lucid, + timestamp: number, + walletAddress: string +) { + const paymentHash = lucid.utils.getAddressDetails(walletAddress).paymentCredential?.hash; - const utxos = await lucid.wallet.getUtxos(); - const walletAddress = await lucid.wallet.address(); - - const policy = createTimelockedMP(lucid, timestamp, walletAddress); - const policyId = lucid.utils.mintingPolicyToId(policy); - - const nftName = nftData.name.replace(/\s/g,''); - const nftUnit1 = policyId + fromText(nftName); - const nftUnit2 = policyId + fromText(nftName + "a"); + if(!paymentHash) + throw new Error("Payment hash not found") - - const transaction = await lucid - .newTx() - .collectFrom(utxos) - .attachMintingPolicy(policy) - .mintAssets( - { [nftUnit1]: BigInt(1), - [nftUnit2]: BigInt(1), }, - Data.to(new Constr(0, [])) // Constr(1, []) to Burn - ) - .validTo(validTo.getTime()) - .addSigner(walletAddress) - .attachMetadata(721, { - [policyId]: { - [nftName]: { - id: 1, - name: nftData.name, - description: nftData.description, - image: nftData.image, - } - } - }) - .complete() - - return transaction; - } \ No newline at end of file + const policy: Script = { + type: "PlutusV2", + script: applyParamsToScript(timeLockMintingContract, [BigInt(timestamp), paymentHash]), + } + return policy; +} \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/query.ts b/CIP-0102/ref_impl/offchain-reference/query.ts index 1f550ba84..6ebf4c715 100644 --- a/CIP-0102/ref_impl/offchain-reference/query.ts +++ b/CIP-0102/ref_impl/offchain-reference/query.ts @@ -1,12 +1,21 @@ import { asset_transactions, output } from "./types.ts"; import { getEnv } from "./utils/env.ts"; -// based on Blockfrost's openAPI +// Barebones Blockfrost query wrappers. Based on Blockfrost's openAPI. + const blockfrost_url = getEnv("BLOCKFROST_URL"); const headers = { project_id: getEnv("BLOCKFROST_PROJECT_KEY"), lucid: "0.10.7" } +/** + * Get the transactions of an asset + * @param asset the asset to query for + * @param _count unused for now + * @param _page unused for now + * @param _order unused for now + * @returns + */ export async function getAssetsTransactions( asset: string, _count: number = 100, @@ -23,6 +32,11 @@ export async function getAssetsTransactions( return response; } +/** + * Get the utxos of a transaction + * @param txHash the hash of the transaction + * @returns + */ export async function getTxsUtxos(txHash: string): Promise<{ outputs: output[] }> { const response = await fetch( @@ -33,6 +47,11 @@ export async function getTxsUtxos(txHash: string): Promise<{ outputs: output[] } return response; } +/** + * Get a cbor datum from its hash + * @param datumHash the hash of the datum + * @returns + */ export async function getScriptsDatumCbor(datumHash: string): Promise<{ cbor: string }> { const response = await fetch( diff --git a/CIP-0102/ref_impl/offchain-reference/read.ts b/CIP-0102/ref_impl/offchain-reference/read.ts index 75e436201..f6b1627e6 100644 --- a/CIP-0102/ref_impl/offchain-reference/read.ts +++ b/CIP-0102/ref_impl/offchain-reference/read.ts @@ -1,42 +1,19 @@ -import type { RoyaltyRecipient, output } from './types.ts'; -import { Constr, Data, toText } from 'https://deno.land/x/lucid@0.10.7/mod.ts'; +import type { RoyaltyRecipient, RoyaltyConstr, output } from './types.ts'; +import { Data, toText } from 'https://deno.land/x/lucid@0.10.7/mod.ts'; import { getAssetsTransactions, getScriptsDatumCbor, getTxsUtxos } from "./query.ts"; +import { CIP102_ROYALTY_TOKEN_NAME, fromOnchainRoyalty } from './conversion.ts'; -export const CIP102_ROYALTY_TOKEN_HEX = '001f4d70526f79616c7479'; // (500)Royalty - -type RoyaltyConstr = Constr< - Map | Map[] ->; - -export function toCip102ContractRoyalty(percentage: number): number { - return Math.floor(1 / (percentage / 1000)); -} - -export function percentageFromCip102Royalty(value: number): number { - return Math.trunc((10 / value) * 1000) / 10; -} - -export function getCip68BlockfrostVersion(version: string) { - switch (version) { - case 'CIP68v1': - return 1; - case 'CIP68v2': - return 2; - default: - return undefined; - } -} - -export async function getCip102RoyaltyMetadata( +/** + * Queries the blockchain for the royalty metadata of a CIP-102 collection + * @param policyId the policy id of the collection + * @returns the royalty metadata in the form of an array of royalty recipients + */ +export async function getRoyaltyPolicy( policyId: string ): Promise { - const asset_unit = policyId + CIP102_ROYALTY_TOKEN_HEX; - // ------ uncomment when blockfrost will be ready ---------- - // const asset = await CardanoAssetsService.getAssets1(assetName); - // if (asset.metadata) { - // return [asset.metadata] - // } + const asset_unit = policyId + CIP102_ROYALTY_TOKEN_NAME; try { + // get the last tx of the royalty token const lastTx = await getAssetsTransactions( asset_unit, 1, @@ -45,12 +22,19 @@ export async function getCip102RoyaltyMetadata( ); const lastTxHash = lastTx?.at(0)?.tx_hash; if (lastTxHash) { + // get the utxos of the last tx const txUtxos = await getTxsUtxos(lastTxHash); + + // parse the utxos to find the royalty metadata const royaltyData = await Promise.all( txUtxos.outputs.map(async (output: output) => await getRoyaltyMetadata(output) )); - return royaltyData.flat(); + + // return the royalty metadata in the form of an array of royalty recipients with the fee converted to percentage + return royaltyData.flat().map((royaltyData) => { + return { ...royaltyData, fee: fromOnchainRoyalty(royaltyData.fee) } + }); } } catch (_error) { // Best effort just silently fail @@ -58,15 +42,21 @@ export async function getCip102RoyaltyMetadata( return []; } - +/** + * Checks for royalty metadata in a given utxo + * @param out the utxo to examine + * @returns an array of royalty recipients if the utxo contains royalty metadata, [] if not + */ async function getRoyaltyMetadata(out: output): Promise { let datum; + // get the datum of the utxo if(!out.inline_datum && out.data_hash) datum = out.inline_datum ?? await getScriptsDatumCbor(out.data_hash).then(dat => dat.cbor) else datum = out.inline_datum if (datum) { try { + // try to parse the datum, return it as an array of royalty recipients if successful const datumConstructor: RoyaltyConstr = Data.from(datum); const datumData = datumConstructor.fields[0]; if (Array.isArray(datumData)) { @@ -82,14 +72,23 @@ async function getRoyaltyMetadata(out: output): Promise { return []; } +/** + * Parse a datum & check for royalty metadata + * @param data the datum to parse + * @returns a royalty recipient if the datum contains royalty metadata, undefined if not + */ function decodeDatumMap( data: Map ): RoyaltyRecipient | undefined { const model: { [key: string]: string | number } = {}; + + // parse the datum to a javascript object data.forEach((value, key) => { model[toText(key)] = typeof value === 'string' ? toText(value) : Number(value); }); + + // check if the object contains the required fields. This could be more robust, but it's functional as is. if ('address' in model && 'fee' in model) { return model as unknown as RoyaltyRecipient; } diff --git a/CIP-0102/ref_impl/offchain-reference/types.ts b/CIP-0102/ref_impl/offchain-reference/types.ts index c81b6451e..a084b2bd0 100644 --- a/CIP-0102/ref_impl/offchain-reference/types.ts +++ b/CIP-0102/ref_impl/offchain-reference/types.ts @@ -1,13 +1,4 @@ -/* - needs - RoyaltyRecipient, - DatumMetadata, - Metadata, - RoyaltyFlag, - MediaAssets -*/ - -import { Data } from "https://deno.land/x/lucid@0.10.7/mod.ts"; +import { Constr, Data } from "https://deno.land/x/lucid@0.10.7/mod.ts"; export const MetadataSchema = Data.Map(Data.Bytes(), Data.Any()); export type Metadata = Data.Static; @@ -27,12 +18,16 @@ export const RoyaltyFlag = RoyaltyFlagSchema as unknown as RoyaltyFlag; export type RoyaltyRecipient = { address: string; - fee: number; + fee: number; // in percentage maxFee?: number; minFee?: number; } -// NOTE: these are ripped from `lucid-cardano` and modified because his types don't match the CIP-25 standard!!! +export type RoyaltyConstr = Constr< + Map | Map[] +>; + +// NOTE: these are ripped from `lucid-cardano` and modified export type MediaAsset = { name: string image: string | string[] From 2b0a8fdfa3105fb9fe69a6f534f1ca60243d1973 Mon Sep 17 00:00:00 2001 From: SamDelaney Date: Thu, 21 Mar 2024 15:47:48 -0700 Subject: [PATCH 10/24] reducible validator & common file --- .../lib/onchain-reference/common.ak | 32 +++++++++ .../validators/reducible_royalty.ak | 67 +++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak create mode 100644 CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak diff --git a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak new file mode 100644 index 000000000..bf8376444 --- /dev/null +++ b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak @@ -0,0 +1,32 @@ +use aiken/dict +use aiken/transaction.{Datum, DatumHash, InlineDatum, NoDatum, Transaction} +use aiken/transaction/credential.{Address} + +pub const royalty_tn: ByteArray = "" + +pub type RoyaltyInfo { + recipients: List, + version: Int, + extra: Data, +} + +pub type RoyaltyRecipient { + address: Address, + // percentage (fraction) + fee: Int, + // fixed (absolute) + min_fee: Option, + // fixed (absolute) + max_fee: Option, +} + +pub fn get_data(tx: Transaction, datum: Datum) -> Data { + when datum is { + NoDatum -> fail + DatumHash(h) -> { + expect Some(d) = dict.get(tx.datums, h) + d + } + InlineDatum(d) -> d + } +} diff --git a/CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak b/CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak new file mode 100644 index 000000000..592e7aca4 --- /dev/null +++ b/CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak @@ -0,0 +1,67 @@ +use aiken/list +use aiken/transaction.{Output, ScriptContext, Transaction} +use aiken/transaction/credential.{VerificationKeyCredential} +use aiken/transaction/value.{PolicyId} +use onchain_reference/common.{RoyaltyInfo, get_data, royalty_tn} + +type ReduceRedeemer { + policy_id: PolicyId, +} + +validator { + fn spend(_d: Data, redeemer: ReduceRedeemer, context: ScriptContext) -> Bool { + let tx = context.transaction + + // find output datum + expect Some(royalty_output) = + list.find( + tx.outputs, + fn(output) { + value.quantity_of(output.value, redeemer.policy_id, royalty_tn) >= 1 + }, + ) + + expect out_royalty_info: RoyaltyInfo = get_data(tx, royalty_output.datum) + + // find input datum + expect Some(royalty_input) = + list.find( + tx.inputs, + fn(input) { + value.quantity_of(input.output.value, redeemer.policy_id, royalty_tn) >= 1 + }, + ) + + expect in_royalty_info: RoyaltyInfo = + get_data(tx, royalty_input.output.datum) + + // check that for each in recipient, there is a corresponding out recipient + expect + list.all( + // for each in recipient + in_royalty_info.recipients, + fn(in_recipient) { + // unwrap the vck + expect VerificationKeyCredential(sig) = + in_recipient.address.payment_credential + list.any( + // for each out recipient + out_royalty_info.recipients, + fn(out_recipient) { + // find the corresponding out recipient + if in_recipient.address == out_recipient.address { + // the fee has not changed OR the fee has decreased AND the recipient has signed the transaction + in_recipient.fee == out_recipient.fee || in_recipient.fee > out_recipient.fee && list.has( + context.transaction.extra_signatories, + sig, + ) + } else { + False + } + }, + ) + }, + ) + True + } +} From 212d32359f01798f0bd4a2179236b5f9615fd8f0 Mon Sep 17 00:00:00 2001 From: SamDelaney Date: Thu, 18 Apr 2024 15:43:54 -0700 Subject: [PATCH 11/24] WIP: reducible tests --- .../validators/reducible_royalty.ak | 62 ++++++++++++++++--- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak b/CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak index 592e7aca4..d1a9177ea 100644 --- a/CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak +++ b/CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak @@ -1,8 +1,10 @@ use aiken/list -use aiken/transaction.{Output, ScriptContext, Transaction} -use aiken/transaction/credential.{VerificationKeyCredential} +use aiken/transaction.{Input, Output, ScriptContext, Transaction} +use aiken/transaction/credential.{Address, VerificationKeyCredential} use aiken/transaction/value.{PolicyId} -use onchain_reference/common.{RoyaltyInfo, get_data, royalty_tn} +use onchain_reference/common.{ + RoyaltyInfo, RoyaltyRecipient, get_data, royalty_tn, +} type ReduceRedeemer { policy_id: PolicyId, @@ -50,11 +52,10 @@ validator { fn(out_recipient) { // find the corresponding out recipient if in_recipient.address == out_recipient.address { - // the fee has not changed OR the fee has decreased AND the recipient has signed the transaction - in_recipient.fee == out_recipient.fee || in_recipient.fee > out_recipient.fee && list.has( - context.transaction.extra_signatories, - sig, - ) + // the fee has not changed OR + in_recipient.fee == out_recipient.fee || // the fee has decreased (encoding reverses this) AND + in_recipient.fee < out_recipient.fee && // the recipient has signed the transaction + list.has(tx.extra_signatories, sig) } else { False } @@ -65,3 +66,48 @@ validator { True } } + +// ############### TESTS ############### +const mock_owner = "some_fake_address_hash" + +const mock_policy_id = "some_fake_policy_id" + +test should_reduce_recipient() { + let redeemer = ReduceRedeemer { policy_id: mock_policy_id } + + let in_royalty_info = + RoyaltyInfo { + recipients: [ + RoyaltyRecipient { + address: Address { + payment_credential: VerificationKeyCredential(mock_owner), + stake_credential: None, + }, + fee: 125, + min_fee: None, + max_fee: None, + }, + ], + version: 1, + extra: None, + } + + True +} +// fn get_base_transaction(recipients_in: List, recipients_out: List) -> Transaction { +// Transaction { +// ..transaction.placeholder() +// extra_signatories: [], +// inputs: [ +// Output { +// value: value.new( +// value.new_map([ +// (mock_policy_id, value.new_integer(1)) +// ]), +// royalty_tn +// ), +// datum: 0 +// } +// ] +// } +// } From b7eb1d18cfcd8ad5c2f2331b16d7fcfc6a6f1cab Mon Sep 17 00:00:00 2001 From: samdelaney Date: Fri, 26 Apr 2024 10:33:00 -0700 Subject: [PATCH 12/24] restructure directory --- CIP-0102/.vscode/settings.json | 9 +++ CIP-0102/ref_impl/TODO.md | 6 +- .../ref_impl/offchain-reference/.env.example | 4 +- .../ref_impl/offchain-reference/deno.json | 6 +- CIP-0102/ref_impl/offchain-reference/index.ts | 11 +-- .../offchain-reference/{ => lib}/mint.ts | 32 ++++---- .../offchain-reference/{ => lib}/read.ts | 4 +- .../offchain-reference/{ => lib}/types.ts | 36 ++++++--- .../{utils => scripts}/env.ts | 2 +- .../{utils => scripts}/generate-wallet.js | 4 +- .../scripts/mock-frontend.ts | 78 +++++++++++++++++++ .../{utils => scripts}/print-utxos.ts | 6 +- .../offchain-reference/{ => utils}/query.ts | 4 +- 13 files changed, 151 insertions(+), 51 deletions(-) create mode 100644 CIP-0102/.vscode/settings.json rename CIP-0102/ref_impl/offchain-reference/{ => lib}/mint.ts (59%) rename CIP-0102/ref_impl/offchain-reference/{ => lib}/read.ts (98%) rename CIP-0102/ref_impl/offchain-reference/{ => lib}/types.ts (61%) rename CIP-0102/ref_impl/offchain-reference/{utils => scripts}/env.ts (91%) rename CIP-0102/ref_impl/offchain-reference/{utils => scripts}/generate-wallet.js (83%) create mode 100644 CIP-0102/ref_impl/offchain-reference/scripts/mock-frontend.ts rename CIP-0102/ref_impl/offchain-reference/{utils => scripts}/print-utxos.ts (64%) rename CIP-0102/ref_impl/offchain-reference/{ => utils}/query.ts (93%) diff --git a/CIP-0102/.vscode/settings.json b/CIP-0102/.vscode/settings.json new file mode 100644 index 000000000..84bfcc7e5 --- /dev/null +++ b/CIP-0102/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "deno.cacheOnSave": true, + "deno.codeLens.test": false, + "deno.codeLens.referencesAllFunctions": false, + "deno.codeLens.references": false, + "deno.codeLens.implementations": false, + "deno.enable": false, + "deno.unstable": false +} \ No newline at end of file diff --git a/CIP-0102/ref_impl/TODO.md b/CIP-0102/ref_impl/TODO.md index 332ce9270..774343bd8 100644 --- a/CIP-0102/ref_impl/TODO.md +++ b/CIP-0102/ref_impl/TODO.md @@ -1,8 +1,8 @@ - [x] Create Specification - [x] Select Onchain Language -- [ ] Minting a CIP 102 compliant NFT with royalties +- [x] Minting a CIP 102 compliant NFT with royalties - [x] Timelocked MP - [x] Always-Fails Validator - - [ ] Reducible Validator -- [ ] Reading a CIP 102 NFT’s royalties off chain + - [x] Reducible Validator +- [x] Reading a CIP 102 NFT’s royalties off chain - [ ] Reading and validating against CIP 102 NFT royalties on chain \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/.env.example b/CIP-0102/ref_impl/offchain-reference/.env.example index f54a30946..cbf186adc 100644 --- a/CIP-0102/ref_impl/offchain-reference/.env.example +++ b/CIP-0102/ref_impl/offchain-reference/.env.example @@ -1,5 +1,5 @@ PUBLIC_CARDANO_NETWORK=Preprod BLOCKFROST_URL=https://cardano-preprod.blockfrost.io/api/v0 BLOCKFROST_PROJECT_KEY=PLACEHOLDER -SERVICE_WALLET_ADDRESS=PLACEHOLDER -SERVICE_WALLET_PRIVATE_KEY=PLACEHOLDER \ No newline at end of file +WALLET_ADDRESS=PLACEHOLDER +WALLET_PRIVATE_KEY=PLACEHOLDER \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/deno.json b/CIP-0102/ref_impl/offchain-reference/deno.json index fc9cb2daf..b12131a42 100644 --- a/CIP-0102/ref_impl/offchain-reference/deno.json +++ b/CIP-0102/ref_impl/offchain-reference/deno.json @@ -1,7 +1,7 @@ { "tasks": { - "generate-wallet": "deno run --allow-net --allow-read --allow-env --allow-write utils/generate-wallet.js", - "print-utxos": "deno run --allow-net --allow-read --allow-env --allow-write utils/print-utxos.ts", - "playground": "deno run --allow-net --allow-read --allow-env --allow-write index.ts" + "generate-wallet": "deno run --allow-net --allow-read --allow-env --allow-write scripts/generate-wallet.js", + "print-utxos": "deno run --allow-net --allow-read --allow-env --allow-write scripts/print-utxos.ts", + "playground": "deno run --allow-net --allow-read --allow-env --allow-write scripts/mock-frontend.ts" } } diff --git a/CIP-0102/ref_impl/offchain-reference/index.ts b/CIP-0102/ref_impl/offchain-reference/index.ts index f10357cc9..ce8cabfc8 100644 --- a/CIP-0102/ref_impl/offchain-reference/index.ts +++ b/CIP-0102/ref_impl/offchain-reference/index.ts @@ -1,8 +1,3 @@ -import { getAssetsTransactions } from "./query.ts" -import { getRoyaltyPolicy } from "./read.ts"; - -// const response = await getAssetsTransactions("2b29ff2309668a2e58d96da26bd0e1c0dd4013e5853e96df2be27e42001f4d70526f79616c7479") - -const response = await getRoyaltyPolicy("60a75923cfc6e241b5da7fd6328d8846275ce94c15fcfc903538e012") - -console.log(response) \ No newline at end of file +export * from "./lib/mint.ts" +export * from "./lib/read.ts" +export * from "./lib/types.ts" \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/mint.ts b/CIP-0102/ref_impl/offchain-reference/lib/mint.ts similarity index 59% rename from CIP-0102/ref_impl/offchain-reference/mint.ts rename to CIP-0102/ref_impl/offchain-reference/lib/mint.ts index ea1140c0f..437b42639 100644 --- a/CIP-0102/ref_impl/offchain-reference/mint.ts +++ b/CIP-0102/ref_impl/offchain-reference/lib/mint.ts @@ -12,8 +12,10 @@ import { import { RoyaltyRecipient, - DatumMetadata, - Metadata, + NFTDatumMetadata, + NFTMetadata, + RoyaltyDatumMetadata, + RoyaltyMetadata, RoyaltyFlag, MediaAssets } from "./types.ts"; @@ -25,9 +27,6 @@ import { * - createTimelockedMP() - utility to create a parameterized minting policy */ -// generated from the onchain-reference directory -const timeLockMintingContract = '5906e90100003232323232323232323232322322322232533300b53300c49107656e746572656400132533300c533300c533300c3322323253330103370e9001000899b89375a602c601a0040062940c034004c00cc02cc050c054c02cc050c054c054c054c054c054c054c054c02c008c004c02400c01c5288a99806a493269735f6e6f745f65787069726564286374782e7472616e73616374696f6e2c206c6f636b5f74696d6529203f2046616c73650014a02a6660186644646600200200644a66602800229404c8c94ccc048cdc78010028a511330040040013018002375c602c0026eb0c048c04cc04cc04cc04cc04cc04cc04cc04cc024c004c02400c0145288a99806a493a6c6973742e686173286374782e7472616e73616374696f6e2e65787472615f7369676e61746f726965732c206f776e657229203f2046616c73650014a0294054cc0352410670617373656400132533300d3370e9000180580089919191919299809a490a7175616e746974793a20001533013332323232323223732660046ea0005221003001001222533333302100213232323233009533301d337100069007099b80483c80400c54ccc074cdc4001a410004266e04cdc0241002800690070b19b8a4890128000015333020001133714911035b5d2900004133714911035b5f2000375c603e66600e00266ec1300102415d00375266e292210129000042233760980103422c2000375266601001000466e28dd718100009bae30210013758603c0046eb4c070004c8cdd81ba8301c001374e603a0026ea80084c94ccc0780044cdc5245027b7d00002133714911037b5f2000375c603a6644646600200200844a6660440022008266006604800266004004604a00266ec130010342207d0037520044466ec1300103422c2000375266600c00c603c00466e29221023a2000333006006301f002337146eb8c078004dd7180f8009bab002132533301e0011337149101025b5d00002133714911035b5f2000375c603a66600a00266ec1300102415d0037520044466ec1300103422c2000375266600c00c00466e28dd7180f0009bae301f001375800426600a6eb40080044c8cdc5244102682700332232333001001003002222533301f3371000490000800899191919980300319b8100548008cdc599b80002533302233710004900a0a40c02903719b8b33700002a66604466e2000520141481805206e0043370c004901019b8300148080cdc700300119b81371a002900119b8a4881012700002375c004444646600200200844a66603c0022008266006604000266004004604200244646600200200644a66603066e1c0052000133714910101300000315333018337100029000099b8a489012d003300200233702900000089980299b8400148050cdc599b803370a002900a240c00066002002444a66602a66e2400920001001133300300333708004900a19b8b3370066e140092014481800040044c94ccc04ccdc3a40000022a66602666e25200200214a22a66028921157175616e74697479203e3d2031203f2046616c73650014a02a66602666e24009200114a22a66028921167175616e74697479203c3d202d31203f2046616c73650014a060200146eb4c05c004c05c004c058004dd6180a00098050008a998072481244578706563746564206f6e20696e636f727265637420436f6e7374722076617269616e74001632323233323001001222533301600214c103d87a80001323253330143370e0069000099ba548000cc064dd380125eb804ccc014014004cdc0801a400460340066eb0c0600080052000323300100100222533301400114bd70099199911191980080080191299980d00088018991999111980f9ba73301f37520126603e6ea400ccc07cdd400125eb80004dd7180c8009bad301a00133003003301e002301c001375c60260026eacc050004cc00c00cc060008c058004c8cc004004008894ccc04c00452f5bded8c0264646464a66602666e3d221000021003133018337606ea4008dd3000998030030019bab3015003375c6026004602e004602a0026eacc048c04cc04cc04cc04cc024c004c02400c54cc0352401254578706563746564206f6e20696e636f727265637420426f6f6c65616e2076617269616e74001623012001149854cc0312411856616c696461746f722072657475726e65642066616c7365001365632533300b3370e90000008a99980798040018a4c2a660180142c2a66601666e1d20020011533300f3008003149854cc0300285854cc03124128436f6e73747220696e646578206469646e2774206d61746368206120747970652076617269616e7400163008002375c0026eb40048c01cdd5000918029baa00149011d4578706563746564206e6f206669656c647320666f7220436f6e737472005734ae7155ceaab9e5573eae815d0aba257481' - /** * Create a royalty token with no other assets * @param lucid a lucid instance with a wallet already selected @@ -37,11 +36,11 @@ const timeLockMintingContract = '5906e901000032323232323232323232323223223222325 * @returns a TxComplete object, ready to sign & submit */ export async function createRoyalty(lucid: Lucid, policy: Script, validator: Script, royalty: RoyaltyRecipient) { - const royaltyDatum = Data.to({ - metadata: Data.castFrom(Data.fromJson([royalty]), Metadata), + const royaltyDatum = Data.to({ + metadata: Data.castFrom(Data.fromJson([royalty]), RoyaltyMetadata), version: BigInt(1), extra: Data.from(Data.void()) - }, DatumMetadata) + }, RoyaltyDatumMetadata) // generous tx validity window const validTo = new Date(); @@ -108,6 +107,7 @@ export async function createRoyalty(lucid: Lucid, policy: Script, validator: Scr // calculate the assets to mint const tokenNames = Object.keys(sanitizedMetadataAssets); + console.log(policy) const policyId = lucid.utils.mintingPolicyToId(policy); const assets: Assets = {}; if(royalty) @@ -137,11 +137,11 @@ export async function createRoyalty(lucid: Lucid, policy: Script, validator: Scr const attachDatums = (tx: Tx): Tx => { if(royalty) { // construct the royalty datum - const royaltyDatum = Data.to({ - metadata: Data.castFrom(Data.fromJson([royalty]), Metadata), + const royaltyDatum = Data.to({ + metadata: Data.castFrom(Data.fromJson([royalty]), RoyaltyMetadata), version: BigInt(1), extra: Data.from(Data.void()) - }, DatumMetadata) + }, RoyaltyDatumMetadata) // attach the royalty token & datum to the transaction tx = tx.payToContract( @@ -158,13 +158,12 @@ export async function createRoyalty(lucid: Lucid, policy: Script, validator: Scr : Data.from(Data.void()); for(const tokenName of tokenNames) { - // construct the reference datum - const metadataDatum = Data.to({ - metadata: Data.castFrom(Data.fromJson(sanitizedMetadataAssets[tokenName]), Metadata), + const metadataDatum = Data.to({ + metadata: Data.castFrom(Data.fromJson(sanitizedMetadataAssets[tokenName]), NFTMetadata), version: BigInt(1), extra - }, DatumMetadata) + }, NFTDatumMetadata) // attach the reference token & datum to the transaction tx = tx.payToContract( @@ -193,6 +192,7 @@ export async function createRoyalty(lucid: Lucid, policy: Script, validator: Scr */ export function createTimelockedMP( lucid: Lucid, + mp: string, timestamp: number, walletAddress: string ) { @@ -203,7 +203,7 @@ export function createTimelockedMP( const policy: Script = { type: "PlutusV2", - script: applyParamsToScript(timeLockMintingContract, [BigInt(timestamp), paymentHash]), + script: applyParamsToScript(mp, [BigInt(timestamp), paymentHash]), } return policy; } \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/read.ts b/CIP-0102/ref_impl/offchain-reference/lib/read.ts similarity index 98% rename from CIP-0102/ref_impl/offchain-reference/read.ts rename to CIP-0102/ref_impl/offchain-reference/lib/read.ts index f6b1627e6..6b7303dae 100644 --- a/CIP-0102/ref_impl/offchain-reference/read.ts +++ b/CIP-0102/ref_impl/offchain-reference/lib/read.ts @@ -1,7 +1,7 @@ import type { RoyaltyRecipient, RoyaltyConstr, output } from './types.ts'; import { Data, toText } from 'https://deno.land/x/lucid@0.10.7/mod.ts'; -import { getAssetsTransactions, getScriptsDatumCbor, getTxsUtxos } from "./query.ts"; -import { CIP102_ROYALTY_TOKEN_NAME, fromOnchainRoyalty } from './conversion.ts'; +import { getAssetsTransactions, getScriptsDatumCbor, getTxsUtxos } from "../utils/query.ts"; +import { CIP102_ROYALTY_TOKEN_NAME, fromOnchainRoyalty } from '../conversion.ts'; /** * Queries the blockchain for the royalty metadata of a CIP-102 collection diff --git a/CIP-0102/ref_impl/offchain-reference/types.ts b/CIP-0102/ref_impl/offchain-reference/lib/types.ts similarity index 61% rename from CIP-0102/ref_impl/offchain-reference/types.ts rename to CIP-0102/ref_impl/offchain-reference/lib/types.ts index a084b2bd0..cbfc8ab1f 100644 --- a/CIP-0102/ref_impl/offchain-reference/types.ts +++ b/CIP-0102/ref_impl/offchain-reference/lib/types.ts @@ -1,23 +1,41 @@ -import { Constr, Data } from "https://deno.land/x/lucid@0.10.7/mod.ts"; +import { Constr, Data, Address } from "https://deno.land/x/lucid@0.10.7/mod.ts"; -export const MetadataSchema = Data.Map(Data.Bytes(), Data.Any()); -export type Metadata = Data.Static; -export const Metadata = MetadataSchema as unknown as Metadata; +// NFT Metadata Schema -export const DatumMetadataSchema = Data.Object({ - metadata: MetadataSchema, +export const NFTMetadataSchema = Data.Map(Data.Bytes(), Data.Any()); +export type NFTMetadata = Data.Static; +export const NFTMetadata = NFTMetadataSchema as unknown as NFTMetadata; + +export const NFTDatumMetadataSchema = Data.Object({ + metadata: NFTMetadataSchema, + version: Data.Integer({ minimum: 1, maximum: 1 }), + extra: Data.Any(), +}); +export type NFTDatumMetadata = Data.Static; +export const NFTDatumMetadata = NFTDatumMetadataSchema as unknown as NFTDatumMetadata; + +// Royalty Metadata Schema + +export const RoyaltyMetadataSchema = Data.Array(Data.Map(Data.Bytes(), Data.Any())); +export type RoyaltyMetadata = Data.Static; +export const RoyaltyMetadata = RoyaltyMetadataSchema as unknown as RoyaltyMetadata; + +export const RoyaltyDatumMetadataSchema = Data.Object({ + metadata: RoyaltyMetadataSchema, version: Data.Integer({ minimum: 1, maximum: 1 }), extra: Data.Any(), }); -export type DatumMetadata = Data.Static; -export const DatumMetadata = DatumMetadataSchema as unknown as DatumMetadata; +export type RoyaltyDatumMetadata = Data.Static; +export const RoyaltyDatumMetadata = RoyaltyDatumMetadataSchema as unknown as RoyaltyDatumMetadata; + +// Royalty Flag Schema export const RoyaltyFlagSchema = Data.Object({ royalty_included: Data.Integer() }) export type RoyaltyFlag = Data.Static; export const RoyaltyFlag = RoyaltyFlagSchema as unknown as RoyaltyFlag; export type RoyaltyRecipient = { - address: string; + address: Address; fee: number; // in percentage maxFee?: number; minFee?: number; diff --git a/CIP-0102/ref_impl/offchain-reference/utils/env.ts b/CIP-0102/ref_impl/offchain-reference/scripts/env.ts similarity index 91% rename from CIP-0102/ref_impl/offchain-reference/utils/env.ts rename to CIP-0102/ref_impl/offchain-reference/scripts/env.ts index 758e6e09e..7d08bc621 100644 --- a/CIP-0102/ref_impl/offchain-reference/utils/env.ts +++ b/CIP-0102/ref_impl/offchain-reference/scripts/env.ts @@ -3,7 +3,7 @@ import { writeFileSync } from "https://deno.land/std@0.110.0/node/fs.ts"; const env = await load({ allowEmptyValues: true }); -export function getEnv(variable: string) { +export function getEnv(variable: string): string { const value = env[variable]; if (!value) { throw Error( diff --git a/CIP-0102/ref_impl/offchain-reference/utils/generate-wallet.js b/CIP-0102/ref_impl/offchain-reference/scripts/generate-wallet.js similarity index 83% rename from CIP-0102/ref_impl/offchain-reference/utils/generate-wallet.js rename to CIP-0102/ref_impl/offchain-reference/scripts/generate-wallet.js index 6109cc006..0e1815fe1 100644 --- a/CIP-0102/ref_impl/offchain-reference/utils/generate-wallet.js +++ b/CIP-0102/ref_impl/offchain-reference/scripts/generate-wallet.js @@ -9,7 +9,7 @@ const address = await lucid .selectWalletFromPrivateKey(privateKey) .wallet.address(); const envUpdate = { - SERVICE_WALLET_ADDRESS: address, - SERVICE_WALLET_PRIVATE_KEY: privateKey, + WALLET_ADDRESS: address, + WALLET_PRIVATE_KEY: privateKey, } updateEnv(envUpdate); diff --git a/CIP-0102/ref_impl/offchain-reference/scripts/mock-frontend.ts b/CIP-0102/ref_impl/offchain-reference/scripts/mock-frontend.ts new file mode 100644 index 000000000..e469e01e5 --- /dev/null +++ b/CIP-0102/ref_impl/offchain-reference/scripts/mock-frontend.ts @@ -0,0 +1,78 @@ +import { createTimelockedMP, mintNFTs } from "../lib/mint.ts"; +import { MediaAssets, RoyaltyRecipient } from "../lib/types.ts"; +import { getAssetsTransactions } from "../utils/query.ts" +import { getRoyaltyPolicy } from "../lib/read.ts"; +import { Lucid, Blockfrost, Network, Script, PlutusVersion } from "https://deno.land/x/lucid@0.10.7/mod.ts"; +import { getEnv } from "./env.ts"; +import contracts from "../../onchain-reference/plutus.json" with { type: "json" }; + +// get environment variables +const blockfrostUrl = getEnv("BLOCKFROST_URL") +const projectId = getEnv("BLOCKFROST_PROJECT_KEY") +const cardanoNetwork = getEnv("PUBLIC_CARDANO_NETWORK") +const walletAddress = getEnv("WALLET_ADDRESS") +const privateKey = getEnv("WALLET_PRIVATE_KEY") + +// set up a blockfrost-connected lucid instance +const bf: Blockfrost = new Blockfrost(blockfrostUrl, projectId) +const lucid: Lucid = await Lucid.new(bf, cardanoNetwork as Network) +lucid.selectWalletFromPrivateKey(privateKey) + +// isolate the contracts' cbor +const type: PlutusVersion = "PlutusV2"; +const alwaysFails = { + type, + script: contracts.validators.find((v) => v.title === "always_fails.spend")?.compiledCode ?? "" +} + +const timelockedMP = { + type, + script: contracts.validators.find((v) => v.title === "minting.minting_validator")?.compiledCode ?? "" +} + + +// const response = await getAssetsTransactions("2b29ff2309668a2e58d96da26bd0e1c0dd4013e5853e96df2be27e42001f4d70526f79616c7479") +// const response = await getRoyaltyPolicy("60a75923cfc6e241b5da7fd6328d8846275ce94c15fcfc903538e012") + +const response = await testTimelockedMint(lucid, timelockedMP, alwaysFails, walletAddress) + +console.log(response) + +// these are examples of constructing transactions from the frontend. + +export async function testTimelockedMint(lucid: Lucid, mp: Script, validator: Script, walletAddress: string) { + + // configuration - these would come from user input. Adjust these however you wish. + const mock_image = "ipfs://QmeTkA5bY4P3DUjhdtPc2MsT8G8keb7HAxjccKrLJN2xTz" + const mock_name = "test" + const mock_deadline = new Date("2024-12-31T23:59:59Z").getTime() + const mock_size = 5 + const mock_fee = 625 // 10 / 0.016 + + // parameterize minting policy + const parameterized_mp = createTimelockedMP(lucid, mp.script, mock_deadline, walletAddress); + + // define media assets for each nft + let assets: MediaAssets = {} + for(let i = 0; i < mock_size; i++) { + let tempdetails = { + name: mock_name + i, + image: mock_image + }; + assets[mock_name + i] = tempdetails + } + + // define royalty policy + const royalties: RoyaltyRecipient[] = [{ + address: walletAddress, + fee: mock_fee + }] + + return mintNFTs( + lucid, + parameterized_mp, + validator, + assets, + royalties[0] + ) +} \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/utils/print-utxos.ts b/CIP-0102/ref_impl/offchain-reference/scripts/print-utxos.ts similarity index 64% rename from CIP-0102/ref_impl/offchain-reference/utils/print-utxos.ts rename to CIP-0102/ref_impl/offchain-reference/scripts/print-utxos.ts index aa1cef1a3..6edfd4497 100644 --- a/CIP-0102/ref_impl/offchain-reference/utils/print-utxos.ts +++ b/CIP-0102/ref_impl/offchain-reference/scripts/print-utxos.ts @@ -1,16 +1,16 @@ -import { Lucid, Blockfrost } from 'https://deno.land/x/lucid@0.10.7/mod.ts'; +import { Lucid, Blockfrost, Network } from 'https://deno.land/x/lucid@0.10.7/mod.ts'; import { getEnv } from './env.ts'; const network = getEnv('PUBLIC_CARDANO_NETWORK'); -const address = getEnv('SERVICE_WALLET_ADDRESS'); +const address = getEnv('WALLET_ADDRESS'); const blockfrostUrl = getEnv(`BLOCKFROST_URL`); const blockfrostKey = getEnv(`BLOCKFROST_PROJECT_KEY`); console.log(address); const blockfrost = new Blockfrost(blockfrostUrl, blockfrostKey); -const lucid = await Lucid.new(blockfrost, network); +const lucid = await Lucid.new(blockfrost, network as Network); const utxos = await lucid.utxosAt(address); console.log(utxos); \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/query.ts b/CIP-0102/ref_impl/offchain-reference/utils/query.ts similarity index 93% rename from CIP-0102/ref_impl/offchain-reference/query.ts rename to CIP-0102/ref_impl/offchain-reference/utils/query.ts index 6ebf4c715..c5d7340ee 100644 --- a/CIP-0102/ref_impl/offchain-reference/query.ts +++ b/CIP-0102/ref_impl/offchain-reference/utils/query.ts @@ -1,5 +1,5 @@ -import { asset_transactions, output } from "./types.ts"; -import { getEnv } from "./utils/env.ts"; +import { asset_transactions, output } from "../lib/types.ts"; +import { getEnv } from "../scripts/env.ts"; // Barebones Blockfrost query wrappers. Based on Blockfrost's openAPI. From cad9a9e98095d08aa4fc6ff760dddaef1061ee44 Mon Sep 17 00:00:00 2001 From: samdelaney Date: Fri, 26 Apr 2024 17:28:26 -0700 Subject: [PATCH 13/24] mock frontend action routing --- CIP-0102/ref_impl/offchain-reference/README | 26 +++++++++ .../ref_impl/offchain-reference/deno.json | 3 +- .../ref_impl/offchain-reference/lib/mint.ts | 5 +- .../ref_impl/offchain-reference/lib/types.ts | 13 ++++- .../scripts/mock-frontend.ts | 58 +++++++++++++++---- 5 files changed, 91 insertions(+), 14 deletions(-) create mode 100644 CIP-0102/ref_impl/offchain-reference/README diff --git a/CIP-0102/ref_impl/offchain-reference/README b/CIP-0102/ref_impl/offchain-reference/README new file mode 100644 index 000000000..d05ff96bd --- /dev/null +++ b/CIP-0102/ref_impl/offchain-reference/README @@ -0,0 +1,26 @@ +# Using the CIP-102 Offchain Library + +## Importing +The contents of this library are available to be imported from `index.ts` and will eventually be published to a public package manager (or two). I will add instructions here when I do. + +In the meantime, you can clone or copy the contents of the `/lib/` folder wherever you need it. + +## Direct Use + +If you want to interact with the library directly without setting up your own project, you can run the code directly with the mock frontend defined in `scripts/mock-frontend.ts` + +### Setup + +### Reading Royalties +You can query the royalty information for a given collection with the following query: + +`deno task get-royalties [policyId]` + +This does not return CIP-27 royalties. + +### Minting CIP-102 Compliant NFTs +You can mint a collection with multiple CIP-68 nfts & a royalty policy with + +`deno task mint-collection` + +Instead of using the command line, this takes its parameters from the `mock_` prefixed consts at the start of the `testTimelockedMint` function in `mock-frontend.ts`. \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/deno.json b/CIP-0102/ref_impl/offchain-reference/deno.json index b12131a42..a60f127fb 100644 --- a/CIP-0102/ref_impl/offchain-reference/deno.json +++ b/CIP-0102/ref_impl/offchain-reference/deno.json @@ -2,6 +2,7 @@ "tasks": { "generate-wallet": "deno run --allow-net --allow-read --allow-env --allow-write scripts/generate-wallet.js", "print-utxos": "deno run --allow-net --allow-read --allow-env --allow-write scripts/print-utxos.ts", - "playground": "deno run --allow-net --allow-read --allow-env --allow-write scripts/mock-frontend.ts" + "mint-collection": "deno run --allow-net --allow-read --allow-env --allow-write scripts/mock-frontend.ts mint-collection", + "get-royalties": "deno run --allow-net --allow-read --allow-env --allow-write scripts/mock-frontend.ts get-royalties" } } diff --git a/CIP-0102/ref_impl/offchain-reference/lib/mint.ts b/CIP-0102/ref_impl/offchain-reference/lib/mint.ts index 437b42639..2ced09828 100644 --- a/CIP-0102/ref_impl/offchain-reference/lib/mint.ts +++ b/CIP-0102/ref_impl/offchain-reference/lib/mint.ts @@ -17,7 +17,8 @@ import { RoyaltyDatumMetadata, RoyaltyMetadata, RoyaltyFlag, - MediaAssets + MediaAssets, + TxBuild } from "./types.ts"; /** @@ -94,7 +95,7 @@ export async function createRoyalty(lucid: Lucid, policy: Script, validator: Scr validator: Script, sanitizedMetadataAssets: MediaAssets, cip102?: "NoRoyalty" | "Premade" | RoyaltyRecipient -) { +): Promise { // extract royalty information or mark undefined const royalty = cip102 === "NoRoyalty" || cip102 === "Premade" ? undefined : cip102; diff --git a/CIP-0102/ref_impl/offchain-reference/lib/types.ts b/CIP-0102/ref_impl/offchain-reference/lib/types.ts index cbfc8ab1f..b9798cbe9 100644 --- a/CIP-0102/ref_impl/offchain-reference/lib/types.ts +++ b/CIP-0102/ref_impl/offchain-reference/lib/types.ts @@ -1,4 +1,4 @@ -import { Constr, Data, Address } from "https://deno.land/x/lucid@0.10.7/mod.ts"; +import { Constr, Data, Address, TxComplete } from "https://deno.land/x/lucid@0.10.7/mod.ts"; // NFT Metadata Schema @@ -45,6 +45,17 @@ export type RoyaltyConstr = Constr< Map | Map[] >; +// Tx Builder Output +export type TxBuild = { + error: string; + tx?: undefined; + policyId?: undefined; +} | { + tx: TxComplete; + policyId: string; + error?: undefined; +} + // NOTE: these are ripped from `lucid-cardano` and modified export type MediaAsset = { name: string diff --git a/CIP-0102/ref_impl/offchain-reference/scripts/mock-frontend.ts b/CIP-0102/ref_impl/offchain-reference/scripts/mock-frontend.ts index e469e01e5..888639f90 100644 --- a/CIP-0102/ref_impl/offchain-reference/scripts/mock-frontend.ts +++ b/CIP-0102/ref_impl/offchain-reference/scripts/mock-frontend.ts @@ -1,11 +1,18 @@ import { createTimelockedMP, mintNFTs } from "../lib/mint.ts"; -import { MediaAssets, RoyaltyRecipient } from "../lib/types.ts"; -import { getAssetsTransactions } from "../utils/query.ts" +import { MediaAssets, RoyaltyRecipient, TxBuild } from "../lib/types.ts"; import { getRoyaltyPolicy } from "../lib/read.ts"; import { Lucid, Blockfrost, Network, Script, PlutusVersion } from "https://deno.land/x/lucid@0.10.7/mod.ts"; import { getEnv } from "./env.ts"; import contracts from "../../onchain-reference/plutus.json" with { type: "json" }; +/** + * MAIN + * Sets up a mock frontend with a collection of required variables + * Then selects an action based on the parameter passed in by the user + */ + +const purpose = Deno.args[0]; + // get environment variables const blockfrostUrl = getEnv("BLOCKFROST_URL") const projectId = getEnv("BLOCKFROST_PROJECT_KEY") @@ -30,17 +37,48 @@ const timelockedMP = { script: contracts.validators.find((v) => v.title === "minting.minting_validator")?.compiledCode ?? "" } +// interpret the user input and execute the requested action +selectAction().then(console.log) -// const response = await getAssetsTransactions("2b29ff2309668a2e58d96da26bd0e1c0dd4013e5853e96df2be27e42001f4d70526f79616c7479") -// const response = await getRoyaltyPolicy("60a75923cfc6e241b5da7fd6328d8846275ce94c15fcfc903538e012") +// Frontend Utilities -const response = await testTimelockedMint(lucid, timelockedMP, alwaysFails, walletAddress) +/** + * Utility to select an action based on the purpose parameter passed in by the user + */ +async function selectAction() { + switch(purpose) { + case "mint-collection": + return await runTx(() => testTimelockedMint(lucid, timelockedMP, alwaysFails, walletAddress)) + case "get-royalties": + return await getRoyaltyPolicy(Deno.args[1]) + default: + return { error: "no transaction selected" } + } +} -console.log(response) +/** + * A simple function to attempt to build a tx, sign it, and submit it to the blockchain + * @param txBuilder the frontend transaction builder, often just a simple tunnel to the offchain library + */ +async function runTx(txBuilder: () => Promise) { + const txBuild = await txBuilder() + if(!txBuild.tx) { + return txBuild.error; + } + else { + //console.log(txBuild) + const signedTx = await txBuild.tx.sign().complete(); + const txHash = await signedTx.submit(); + return txHash; + } +} -// these are examples of constructing transactions from the frontend. +/** + * Examples of constructing transactions from the frontend using the endpoints defined in this library. + * - testTimelockedMint: a collection with CIP 68 NFTs and a CIP 102 royalty + * */ -export async function testTimelockedMint(lucid: Lucid, mp: Script, validator: Script, walletAddress: string) { +function testTimelockedMint(lucid: Lucid, mp: Script, validator: Script, walletAddress: string): Promise { // configuration - these would come from user input. Adjust these however you wish. const mock_image = "ipfs://QmeTkA5bY4P3DUjhdtPc2MsT8G8keb7HAxjccKrLJN2xTz" @@ -53,9 +91,9 @@ export async function testTimelockedMint(lucid: Lucid, mp: Script, validator: Sc const parameterized_mp = createTimelockedMP(lucid, mp.script, mock_deadline, walletAddress); // define media assets for each nft - let assets: MediaAssets = {} + const assets: MediaAssets = {} for(let i = 0; i < mock_size; i++) { - let tempdetails = { + const tempdetails = { name: mock_name + i, image: mock_image }; From eff35b4d288180c79017e7a18d0dd2da5cae7e22 Mon Sep 17 00:00:00 2001 From: samdelaney Date: Fri, 26 Apr 2024 17:51:59 -0700 Subject: [PATCH 14/24] barebones offchain README --- CIP-0102/ref_impl/offchain-reference/README | 30 +++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CIP-0102/ref_impl/offchain-reference/README b/CIP-0102/ref_impl/offchain-reference/README index d05ff96bd..c05ea744f 100644 --- a/CIP-0102/ref_impl/offchain-reference/README +++ b/CIP-0102/ref_impl/offchain-reference/README @@ -11,6 +11,36 @@ If you want to interact with the library directly without setting up your own pr ### Setup +- Make sure you have [Deno](https://deno.com/) installed. + +- Create a Blockfrost project if you don't already have one. These are free up to a certain number of queries. + +- Create a `.env` file in the parent directory with the structure defined in `.env.example`: + - Fill in the `PUBLIC_CARDANO_NETWORK` variable with "Preview", "Preprod" or "Mainnet" depending on which network you want to use. + - Fill in the corresponding `BLOCKFROST_URL`. + - Fill in the + +- Set up your wallet by filling in the `WALLET_ADDRESS` and `WALLET_PRIVATE_KEY` variables. + - I recommend using `deno task generate-wallet` to generate a new wallet for testing this specifically. Once generated you can send the minimal funds you need for testing to the wallet from your hot wallet or from a testnet faucet. + +To verify your setup worked, or if you want to check the contents of the wallet you have connected, I've included a handy `deno task print-utxos` script. + +You should see something like this: +``` +addr_test1vqhjcudw5m5pmehwtwduts2ayz2rlpm7vjq0ql6exsz6czq2gr7h8 +[ + { + txHash: "6b369bac955857261566812acde749t9d438032ac6633a004cceeb2c4dc5b287", + outputIndex: 7, + assets: { lovelace: 9980876490n }, + address: "addr_test1vqhjcudw5m5pmehwtwduts2ayz2rlpm7vjq0ql6exsz6czq2gr7h8", + datumHash: undefined, + datum: undefined, + scriptRef: undefined + } +] +``` + ### Reading Royalties You can query the royalty information for a given collection with the following query: From b7762e1a25c8788474eccf734bbf64d0045e420a Mon Sep 17 00:00:00 2001 From: samdelaney Date: Fri, 17 May 2024 13:58:20 -0700 Subject: [PATCH 15/24] type system improvements --- .../ref_impl/offchain-reference/lib/mint.ts | 41 +++--- .../ref_impl/offchain-reference/lib/read.ts | 13 +- .../ref_impl/offchain-reference/lib/types.ts | 135 ------------------ .../offchain-reference/lib/types/chain.ts | 130 +++++++++++++++++ .../offchain-reference/lib/types/index.ts | 3 + .../offchain-reference/lib/types/royalties.ts | 112 +++++++++++++++ .../offchain-reference/lib/types/utility.ts | 32 +++++ .../scripts/mock-frontend.ts | 6 +- .../offchain-reference/utils/query.ts | 2 +- 9 files changed, 307 insertions(+), 167 deletions(-) delete mode 100644 CIP-0102/ref_impl/offchain-reference/lib/types.ts create mode 100644 CIP-0102/ref_impl/offchain-reference/lib/types/chain.ts create mode 100644 CIP-0102/ref_impl/offchain-reference/lib/types/index.ts create mode 100644 CIP-0102/ref_impl/offchain-reference/lib/types/royalties.ts create mode 100644 CIP-0102/ref_impl/offchain-reference/lib/types/utility.ts diff --git a/CIP-0102/ref_impl/offchain-reference/lib/mint.ts b/CIP-0102/ref_impl/offchain-reference/lib/mint.ts index 2ced09828..50d7dc806 100644 --- a/CIP-0102/ref_impl/offchain-reference/lib/mint.ts +++ b/CIP-0102/ref_impl/offchain-reference/lib/mint.ts @@ -10,16 +10,21 @@ import { toUnit } from "https://deno.land/x/lucid@0.10.7/mod.ts"; -import { - RoyaltyRecipient, - NFTDatumMetadata, +import { + toCip102RoyaltyDatum, + Royalty, + RoyaltyFlag +} from './types/royalties.ts' + +import { NFTMetadata, - RoyaltyDatumMetadata, - RoyaltyMetadata, - RoyaltyFlag, - MediaAssets, - TxBuild -} from "./types.ts"; + NFTDatumMetadata +} from './types/chain.ts' + +import { + MediaAssets, + TxBuild +} from './types/utility.ts' /** * Included in this file: @@ -36,12 +41,8 @@ import { * @param royalty the royalty metadata * @returns a TxComplete object, ready to sign & submit */ -export async function createRoyalty(lucid: Lucid, policy: Script, validator: Script, royalty: RoyaltyRecipient) { - const royaltyDatum = Data.to({ - metadata: Data.castFrom(Data.fromJson([royalty]), RoyaltyMetadata), - version: BigInt(1), - extra: Data.from(Data.void()) - }, RoyaltyDatumMetadata) +export async function createRoyalty(lucid: Lucid, policy: Script, validator: Script, royalty: Royalty) { + const royaltyDatum = toCip102RoyaltyDatum([royalty]) // generous tx validity window const validTo = new Date(); @@ -86,7 +87,7 @@ export async function createRoyalty(lucid: Lucid, policy: Script, validator: Scr * @param cip102 The cip102 parameter allows for 3 cases: * - "NoRoyalty" - no royalty is attached to the collection - obviously not recommended here but hey, it's your collection * - "Premade" - the royalty has already been minted to the policy - no need to mint new tokens, but include the flag to indicate a royalty token exists - * - RoyaltyRecipient - a new royalty token is minted to the policy in this transaction according to the information in the parameter + * - Royalty - a new royalty token is minted to the policy in this transaction according to the information in the parameter * @returns an object containing a TxComplete object & the policyId of the minted assets */ export async function mintNFTs( @@ -94,7 +95,7 @@ export async function createRoyalty(lucid: Lucid, policy: Script, validator: Scr policy: Script, validator: Script, sanitizedMetadataAssets: MediaAssets, - cip102?: "NoRoyalty" | "Premade" | RoyaltyRecipient + cip102?: "NoRoyalty" | "Premade" | Royalty ): Promise { // extract royalty information or mark undefined const royalty = cip102 === "NoRoyalty" || cip102 === "Premade" ? undefined : cip102; @@ -138,11 +139,7 @@ export async function createRoyalty(lucid: Lucid, policy: Script, validator: Scr const attachDatums = (tx: Tx): Tx => { if(royalty) { // construct the royalty datum - const royaltyDatum = Data.to({ - metadata: Data.castFrom(Data.fromJson([royalty]), RoyaltyMetadata), - version: BigInt(1), - extra: Data.from(Data.void()) - }, RoyaltyDatumMetadata) + const royaltyDatum = toCip102RoyaltyDatum([royalty]) // attach the royalty token & datum to the transaction tx = tx.payToContract( diff --git a/CIP-0102/ref_impl/offchain-reference/lib/read.ts b/CIP-0102/ref_impl/offchain-reference/lib/read.ts index 6b7303dae..5b0efa7fb 100644 --- a/CIP-0102/ref_impl/offchain-reference/lib/read.ts +++ b/CIP-0102/ref_impl/offchain-reference/lib/read.ts @@ -1,4 +1,5 @@ -import type { RoyaltyRecipient, RoyaltyConstr, output } from './types.ts'; +import type { Royalty, RoyaltyConstr } from './types/royalties.ts'; +import type { output } from "./types/chain.ts"; import { Data, toText } from 'https://deno.land/x/lucid@0.10.7/mod.ts'; import { getAssetsTransactions, getScriptsDatumCbor, getTxsUtxos } from "../utils/query.ts"; import { CIP102_ROYALTY_TOKEN_NAME, fromOnchainRoyalty } from '../conversion.ts'; @@ -10,7 +11,7 @@ import { CIP102_ROYALTY_TOKEN_NAME, fromOnchainRoyalty } from '../conversion.ts' */ export async function getRoyaltyPolicy( policyId: string -): Promise { +): Promise { const asset_unit = policyId + CIP102_ROYALTY_TOKEN_NAME; try { // get the last tx of the royalty token @@ -47,7 +48,7 @@ export async function getRoyaltyPolicy( * @param out the utxo to examine * @returns an array of royalty recipients if the utxo contains royalty metadata, [] if not */ -async function getRoyaltyMetadata(out: output): Promise { +async function getRoyaltyMetadata(out: output): Promise { let datum; // get the datum of the utxo if(!out.inline_datum && out.data_hash) @@ -63,7 +64,7 @@ async function getRoyaltyMetadata(out: output): Promise { const royalties = datumData .map((royaltyMap) => decodeDatumMap(royaltyMap)) .filter((royalty) => royalty); - return royalties as RoyaltyRecipient[]; + return royalties as Royalty[]; } } catch (_error) { // do nothing @@ -79,7 +80,7 @@ async function getRoyaltyMetadata(out: output): Promise { */ function decodeDatumMap( data: Map -): RoyaltyRecipient | undefined { +): Royalty | undefined { const model: { [key: string]: string | number } = {}; // parse the datum to a javascript object @@ -90,6 +91,6 @@ function decodeDatumMap( // check if the object contains the required fields. This could be more robust, but it's functional as is. if ('address' in model && 'fee' in model) { - return model as unknown as RoyaltyRecipient; + return model as unknown as Royalty; } } \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/lib/types.ts b/CIP-0102/ref_impl/offchain-reference/lib/types.ts deleted file mode 100644 index b9798cbe9..000000000 --- a/CIP-0102/ref_impl/offchain-reference/lib/types.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { Constr, Data, Address, TxComplete } from "https://deno.land/x/lucid@0.10.7/mod.ts"; - -// NFT Metadata Schema - -export const NFTMetadataSchema = Data.Map(Data.Bytes(), Data.Any()); -export type NFTMetadata = Data.Static; -export const NFTMetadata = NFTMetadataSchema as unknown as NFTMetadata; - -export const NFTDatumMetadataSchema = Data.Object({ - metadata: NFTMetadataSchema, - version: Data.Integer({ minimum: 1, maximum: 1 }), - extra: Data.Any(), -}); -export type NFTDatumMetadata = Data.Static; -export const NFTDatumMetadata = NFTDatumMetadataSchema as unknown as NFTDatumMetadata; - -// Royalty Metadata Schema - -export const RoyaltyMetadataSchema = Data.Array(Data.Map(Data.Bytes(), Data.Any())); -export type RoyaltyMetadata = Data.Static; -export const RoyaltyMetadata = RoyaltyMetadataSchema as unknown as RoyaltyMetadata; - -export const RoyaltyDatumMetadataSchema = Data.Object({ - metadata: RoyaltyMetadataSchema, - version: Data.Integer({ minimum: 1, maximum: 1 }), - extra: Data.Any(), -}); -export type RoyaltyDatumMetadata = Data.Static; -export const RoyaltyDatumMetadata = RoyaltyDatumMetadataSchema as unknown as RoyaltyDatumMetadata; - -// Royalty Flag Schema - -export const RoyaltyFlagSchema = Data.Object({ royalty_included: Data.Integer() }) -export type RoyaltyFlag = Data.Static; -export const RoyaltyFlag = RoyaltyFlagSchema as unknown as RoyaltyFlag; - -export type RoyaltyRecipient = { - address: Address; - fee: number; // in percentage - maxFee?: number; - minFee?: number; -} - -export type RoyaltyConstr = Constr< - Map | Map[] ->; - -// Tx Builder Output -export type TxBuild = { - error: string; - tx?: undefined; - policyId?: undefined; -} | { - tx: TxComplete; - policyId: string; - error?: undefined; -} - -// NOTE: these are ripped from `lucid-cardano` and modified -export type MediaAsset = { - name: string - image: string | string[] - mediaType?: string - description?: string | string[] - files?: MediaAssetFile[] - [key: string]: unknown -} - -declare type MediaAssetFile = { - name?: string - mediaType: string - src: string | string[] -} - -export type MediaAssets = { - [key: string]: MediaAsset; -}; - -// based on Blockfrost's openAPI -export type asset_transactions = Array<{ - /** - * Hash of the transaction - */ - tx_hash: string; - /** - * Transaction index within the block - */ - tx_index: number; - /** - * Block height - */ - block_height: number; - /** - * Block creation time in UNIX time - */ - block_time: number; - }>; - - export type output = { - /** - * Output address - */ - address: string; - amount: Array<{ - /** - * The unit of the value - */ - unit: string; - /** - * The quantity of the unit - */ - quantity: string; - }>; - /** - * UTXO index in the transaction - */ - output_index: number; - /** - * The hash of the transaction output datum - */ - data_hash: string | null; - /** - * CBOR encoded inline datum - */ - inline_datum: string | null; - /** - * Whether the output is a collateral output - */ - collateral: boolean; - /** - * The hash of the reference script of the output - */ - reference_script_hash: string | null; - }; - \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/lib/types/chain.ts b/CIP-0102/ref_impl/offchain-reference/lib/types/chain.ts new file mode 100644 index 000000000..1f4ad65d4 --- /dev/null +++ b/CIP-0102/ref_impl/offchain-reference/lib/types/chain.ts @@ -0,0 +1,130 @@ +import { Data, Address, getAddressDetails } from "https://deno.land/x/lucid@0.10.7/mod.ts"; + +// NFT Metadata Schema + +export const NFTMetadataSchema = Data.Map(Data.Bytes(), Data.Any()); +export type NFTMetadata = Data.Static; +export const NFTMetadata = NFTMetadataSchema as unknown as NFTMetadata; + +export const NFTDatumMetadataSchema = Data.Object({ + metadata: NFTMetadataSchema, + version: Data.Integer({ minimum: 1, maximum: 1 }), + extra: Data.Any(), +}); +export type NFTDatumMetadata = Data.Static; +export const NFTDatumMetadata = NFTDatumMetadataSchema as unknown as NFTDatumMetadata + +// Address Schema + +export const ChainCredentialSchema = Data.Enum([ + Data.Object({ + VerificationKeyCredential: Data.Tuple([Data.Bytes({ minLength: 28, maxLength: 28 })]), + }), + Data.Object({ + ScriptCredential: Data.Tuple([Data.Bytes({ minLength: 28, maxLength: 28 })]), + }), +]); + +export const ChainAddressSchema = Data.Object({ + paymentCredential: ChainCredentialSchema, + stakeCredential: Data.Nullable( + Data.Enum([ + Data.Object({ Inline: Data.Tuple([ChainCredentialSchema]) }), + Data.Object({ + Pointer: Data.Object({ + slotNumber: Data.Integer(), + transactionIndex: Data.Integer(), + certificateIndex: Data.Integer(), + }), + }), + ]) + ), +}); + +export type ChainAddress = Data.Static; +export const ChainAddress = ChainAddressSchema as unknown as ChainAddress; + +/// Converts a Bech32 address to the aiken representation of a chain address +export function asChainAddress(address: Address): ChainAddress { + const { paymentCredential, stakeCredential } = getAddressDetails(address); + + if (!paymentCredential) throw new Error('Not a valid payment address.'); + + return { + paymentCredential: + paymentCredential?.type === 'Key' + ? { + VerificationKeyCredential: [paymentCredential.hash], + } + : { ScriptCredential: [paymentCredential.hash] }, + stakeCredential: stakeCredential + ? { + Inline: [ + stakeCredential.type === 'Key' + ? { + VerificationKeyCredential: [stakeCredential.hash], + } + : { ScriptCredential: [stakeCredential.hash] }, + ], + } + : null, + }; +} + +// based on Blockfrost's openAPI +export type asset_transactions = Array<{ + /** + * Hash of the transaction + */ + tx_hash: string; + /** + * Transaction index within the block + */ + tx_index: number; + /** + * Block height + */ + block_height: number; + /** + * Block creation time in UNIX time + */ + block_time: number; + }>; + + export type output = { + /** + * Output address + */ + address: string; + amount: Array<{ + /** + * The unit of the value + */ + unit: string; + /** + * The quantity of the unit + */ + quantity: string; + }>; + /** + * UTXO index in the transaction + */ + output_index: number; + /** + * The hash of the transaction output datum + */ + data_hash: string | null; + /** + * CBOR encoded inline datum + */ + inline_datum: string | null; + /** + * Whether the output is a collateral output + */ + collateral: boolean; + /** + * The hash of the reference script of the output + */ + reference_script_hash: string | null; + }; + \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/lib/types/index.ts b/CIP-0102/ref_impl/offchain-reference/lib/types/index.ts new file mode 100644 index 000000000..89dafceb2 --- /dev/null +++ b/CIP-0102/ref_impl/offchain-reference/lib/types/index.ts @@ -0,0 +1,3 @@ +export * from './chain.ts' +export * from './royalties.ts' +export * from './utility.ts' \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/lib/types/royalties.ts b/CIP-0102/ref_impl/offchain-reference/lib/types/royalties.ts new file mode 100644 index 000000000..1954d2eb6 --- /dev/null +++ b/CIP-0102/ref_impl/offchain-reference/lib/types/royalties.ts @@ -0,0 +1,112 @@ +import { fromText, toUnit, type Tx, Data, Address, Constr } from "https://deno.land/x/lucid@0.10.7/mod.ts"; + +import { asChainAddress, ChainAddressSchema } from './chain.ts'; + +export type Royalty = { + address: Address; + fee: number; // in percentage + maxFee?: number; + minFee?: number; +} + +export const ROYALTY_TOKEN_LABEL = 500; +export const ROYALTY_TOKEN_NAME = fromText('Royalty'); + +export const RoyaltyRecipientSchema = Data.Object({ + address: ChainAddressSchema, + fee: Data.Integer({ minimum: 1 }), + minFee: Data.Nullable(Data.Integer()), + maxFee: Data.Nullable(Data.Integer()), +}); + +export type RoyaltyRecipientType = Data.Static; +export const RoyaltyRecipientShape = RoyaltyRecipientSchema as unknown as RoyaltyRecipientType; + +export const RoyaltyInfoSchema = Data.Object({ + metadata: Data.Array(RoyaltyRecipientSchema), + version: Data.Integer({ minimum: 1, maximum: 1 }), + extra: Data.Any(), +}); +export type RoyaltyInfoType = Data.Static; +export const RoyaltyInfoShape = RoyaltyInfoSchema as unknown as RoyaltyInfoType; + +export function toRoyaltyUnit(policyId: string) { + return toUnit(policyId, ROYALTY_TOKEN_NAME, ROYALTY_TOKEN_LABEL); +} + +export type RoyaltyConstr = Constr< + Map | Map[] +>; + +// + +export const RoyaltyFlagSchema = Data.Object({ royalty_included: Data.Integer() }) +export type RoyaltyFlag = Data.Static; +export const RoyaltyFlag = RoyaltyFlagSchema as unknown as RoyaltyFlag; + +/// Converts a percentage between 0 and 100 inclusive to the CIP-102 fee format +export function asChainVariableFee(percent: number) { + if (percent < 0.1 || percent > 100) { + throw new Error('Royalty fee must be between 0.1 and 100 percent'); + } + + return BigInt(Math.floor(1 / (percent / 1000))); +} + +/// Converts from a on chain royalty to a percent between 0 and 100 +export function fromChainVariableFee(fee: bigint) { + return Math.ceil(Number(10000n / fee)) / 10; +} + +/// Confirms the fee is a positive integer and casts it to a bigint otherwise returns null +export function asChainFixedFee(fee?: number) { + if (fee) { + if (fee < 0 || !Number.isInteger(fee)) { + throw new Error('Fixed fee must be an positive integer or 0'); + } + + return BigInt(fee); + } else { + return null; + } +} + +// Converts the offchain representation of royaltyies into the format used for storing the royalties in datum on chain +export function toCip102RoyaltyDatum(royalties: Royalty[]) { + const metadata: RoyaltyRecipientType[] = royalties.map((royalty) => { + const address = asChainAddress(royalty.address); + const fee = asChainVariableFee(royalty.fee); + const minFee = asChainFixedFee(royalty.minFee); + const maxFee = asChainFixedFee(royalty.maxFee); + + return { + address, + fee, + minFee, + maxFee, + }; + }); + + const info: RoyaltyInfoType = { + metadata, + version: BigInt(1), + extra: '', + }; + + return Data.to(info, RoyaltyInfoShape); +} + +export function addCip102RoyaltyToTransaction( + tx: Tx, + policyId: string, + address: string, + royalties: Royalty[], + redeemer?: string +) { + const royaltyUnit = toRoyaltyUnit(policyId); + const royaltyAsset = { [royaltyUnit]: 1n }; + const royaltyDatum = toCip102RoyaltyDatum(royalties); + const royaltyOutputData = { inline: royaltyDatum }; + + tx.mintAssets(royaltyAsset, redeemer).payToAddressWithData(address, royaltyOutputData, royaltyAsset); +} \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/lib/types/utility.ts b/CIP-0102/ref_impl/offchain-reference/lib/types/utility.ts new file mode 100644 index 000000000..316e91f13 --- /dev/null +++ b/CIP-0102/ref_impl/offchain-reference/lib/types/utility.ts @@ -0,0 +1,32 @@ +import { TxComplete } from 'https://deno.land/x/lucid@0.10.7/mod.ts' + +// Tx Builder Output +export type TxBuild = { + error: string; + tx?: undefined; + policyId?: undefined; +} | { + tx: TxComplete; + policyId: string; + error?: undefined; +} + +// NOTE: these are ripped from `lucid-cardano` and modified +export type MediaAsset = { + name: string + image: string | string[] + mediaType?: string + description?: string | string[] + files?: MediaAssetFile[] + [key: string]: unknown +} + +declare type MediaAssetFile = { + name?: string + mediaType: string + src: string | string[] +} + +export type MediaAssets = { + [key: string]: MediaAsset; +}; \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/scripts/mock-frontend.ts b/CIP-0102/ref_impl/offchain-reference/scripts/mock-frontend.ts index 888639f90..63bd138b6 100644 --- a/CIP-0102/ref_impl/offchain-reference/scripts/mock-frontend.ts +++ b/CIP-0102/ref_impl/offchain-reference/scripts/mock-frontend.ts @@ -1,5 +1,5 @@ import { createTimelockedMP, mintNFTs } from "../lib/mint.ts"; -import { MediaAssets, RoyaltyRecipient, TxBuild } from "../lib/types.ts"; +import { MediaAssets, Royalty, TxBuild } from "../lib/types/index.ts"; import { getRoyaltyPolicy } from "../lib/read.ts"; import { Lucid, Blockfrost, Network, Script, PlutusVersion } from "https://deno.land/x/lucid@0.10.7/mod.ts"; import { getEnv } from "./env.ts"; @@ -85,7 +85,7 @@ function testTimelockedMint(lucid: Lucid, mp: Script, validator: Script, walletA const mock_name = "test" const mock_deadline = new Date("2024-12-31T23:59:59Z").getTime() const mock_size = 5 - const mock_fee = 625 // 10 / 0.016 + const mock_fee = 1.6 // parameterize minting policy const parameterized_mp = createTimelockedMP(lucid, mp.script, mock_deadline, walletAddress); @@ -101,7 +101,7 @@ function testTimelockedMint(lucid: Lucid, mp: Script, validator: Script, walletA } // define royalty policy - const royalties: RoyaltyRecipient[] = [{ + const royalties: Royalty[] = [{ address: walletAddress, fee: mock_fee }] diff --git a/CIP-0102/ref_impl/offchain-reference/utils/query.ts b/CIP-0102/ref_impl/offchain-reference/utils/query.ts index c5d7340ee..878d97f77 100644 --- a/CIP-0102/ref_impl/offchain-reference/utils/query.ts +++ b/CIP-0102/ref_impl/offchain-reference/utils/query.ts @@ -1,4 +1,4 @@ -import { asset_transactions, output } from "../lib/types.ts"; +import { asset_transactions, output } from "../lib/types/chain.ts"; import { getEnv } from "../scripts/env.ts"; // Barebones Blockfrost query wrappers. Based on Blockfrost's openAPI. From 03a6bd63f86971f4f08af9266d471d788877e2c2 Mon Sep 17 00:00:00 2001 From: samdelaney Date: Mon, 20 May 2024 12:12:13 -0700 Subject: [PATCH 16/24] initial reducible validator tests --- .../lib/onchain-reference/common.ak | 4 +- .../lib/onchain-reference/tests.ak | 37 ++++++ .../validators/reducible_royalty.ak | 123 ++++++++++++------ 3 files changed, 121 insertions(+), 43 deletions(-) create mode 100644 CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/tests.ak diff --git a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak index bf8376444..33bbb8548 100644 --- a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak +++ b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak @@ -2,9 +2,9 @@ use aiken/dict use aiken/transaction.{Datum, DatumHash, InlineDatum, NoDatum, Transaction} use aiken/transaction/credential.{Address} -pub const royalty_tn: ByteArray = "" +pub const royalty_tn: ByteArray = "001f4d70526f79616c7479" -pub type RoyaltyInfo { +pub type RoyaltyDatum { recipients: List, version: Int, extra: Data, diff --git a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/tests.ak b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/tests.ak new file mode 100644 index 000000000..30a0b1f51 --- /dev/null +++ b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/tests.ak @@ -0,0 +1,37 @@ +use aiken/transaction.{InlineDatum, Input, Output, OutputReference} +use aiken/transaction/credential.{Address, VerificationKeyCredential} +use aiken/transaction/value.{PolicyId} +use onchain_reference/common.{RoyaltyDatum, RoyaltyRecipient, royalty_tn} + +pub fn recipients_to_input( + recipients: List, + policy_id: PolicyId, +) -> Input { + let mock_outref = + OutputReference { + transaction_id: transaction.placeholder().id, + output_index: 0, + } + + Input { + output_reference: mock_outref, + output: recipients_to_output(recipients, policy_id), + } +} + +pub fn recipients_to_output( + recipients: List, + policy_id: PolicyId, +) -> Output { + let out_royalty_info = RoyaltyDatum { recipients, version: 1, extra: None } + + Output { + address: Address { + payment_credential: VerificationKeyCredential(""), + stake_credential: None, + }, + value: value.from_asset(policy_id, royalty_tn, 1), + datum: InlineDatum(out_royalty_info), + reference_script: None, + } +} diff --git a/CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak b/CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak index d1a9177ea..218eeedb6 100644 --- a/CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak +++ b/CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak @@ -1,17 +1,18 @@ use aiken/list -use aiken/transaction.{Input, Output, ScriptContext, Transaction} +use aiken/transaction.{Input, Output, ScriptContext, Spend, Transaction} use aiken/transaction/credential.{Address, VerificationKeyCredential} use aiken/transaction/value.{PolicyId} use onchain_reference/common.{ - RoyaltyInfo, RoyaltyRecipient, get_data, royalty_tn, + RoyaltyDatum, RoyaltyRecipient, get_data, royalty_tn, } +use onchain_reference/tests.{recipients_to_input, recipients_to_output} type ReduceRedeemer { policy_id: PolicyId, } validator { - fn spend(_d: Data, redeemer: ReduceRedeemer, context: ScriptContext) -> Bool { + fn reduce(_d: Data, redeemer: ReduceRedeemer, context: ScriptContext) -> Bool { let tx = context.transaction // find output datum @@ -23,7 +24,7 @@ validator { }, ) - expect out_royalty_info: RoyaltyInfo = get_data(tx, royalty_output.datum) + expect out_royalty_info: RoyaltyDatum = get_data(tx, royalty_output.datum) // find input datum expect Some(royalty_input) = @@ -34,7 +35,7 @@ validator { }, ) - expect in_royalty_info: RoyaltyInfo = + expect in_royalty_info: RoyaltyDatum = get_data(tx, royalty_input.output.datum) // check that for each in recipient, there is a corresponding out recipient @@ -52,10 +53,10 @@ validator { fn(out_recipient) { // find the corresponding out recipient if in_recipient.address == out_recipient.address { + // the recipient has signed the transaction AND // the fee has not changed OR - in_recipient.fee == out_recipient.fee || // the fee has decreased (encoding reverses this) AND - in_recipient.fee < out_recipient.fee && // the recipient has signed the transaction - list.has(tx.extra_signatories, sig) + // the fee has decreased (encoding reverses this) + list.has(tx.extra_signatories, sig) && in_recipient.fee > out_recipient.fee || (in_recipient.fee == out_recipient.fee)? } else { False } @@ -73,41 +74,81 @@ const mock_owner = "some_fake_address_hash" const mock_policy_id = "some_fake_policy_id" test should_reduce_recipient() { + trace @"do traces work?" let redeemer = ReduceRedeemer { policy_id: mock_policy_id } - let in_royalty_info = - RoyaltyInfo { - recipients: [ - RoyaltyRecipient { - address: Address { - payment_credential: VerificationKeyCredential(mock_owner), - stake_credential: None, - }, - fee: 125, - min_fee: None, - max_fee: None, - }, - ], - version: 1, - extra: None, + let in_recipient = + RoyaltyRecipient { + address: Address { + payment_credential: VerificationKeyCredential(mock_owner), + stake_credential: None, + }, + fee: 625, + min_fee: None, + max_fee: None, + } + + let input = recipients_to_input([in_recipient], mock_policy_id) + + let outputs = + [ + recipients_to_output( + [RoyaltyRecipient { ..in_recipient, fee: 125 }], + mock_policy_id, + ), + ] + + let ctx = + ScriptContext { + transaction: get_base_transaction([input], outputs), + purpose: Spend(input.output_reference), + } + + reduce([], redeemer, ctx) +} + +test should_fail_increase_recipient() fail { + trace @"do traces work?" + let redeemer = ReduceRedeemer { policy_id: mock_policy_id } + + let in_recipient = + RoyaltyRecipient { + address: Address { + payment_credential: VerificationKeyCredential(mock_owner), + stake_credential: None, + }, + fee: 125, + min_fee: None, + max_fee: None, } - True + let input = recipients_to_input([in_recipient], mock_policy_id) + + let outputs = + [ + recipients_to_output( + [RoyaltyRecipient { ..in_recipient, fee: 625 }], + mock_policy_id, + ), + ] + + let ctx = + ScriptContext { + transaction: get_base_transaction([input], outputs), + purpose: Spend(input.output_reference), + } + + reduce([], redeemer, ctx) +} + +fn get_base_transaction( + recipients_in: List, + recipients_out: List, +) -> Transaction { + Transaction { + ..transaction.placeholder(), + extra_signatories: [mock_owner], + inputs: recipients_in, + outputs: recipients_out, + } } -// fn get_base_transaction(recipients_in: List, recipients_out: List) -> Transaction { -// Transaction { -// ..transaction.placeholder() -// extra_signatories: [], -// inputs: [ -// Output { -// value: value.new( -// value.new_map([ -// (mock_policy_id, value.new_integer(1)) -// ]), -// royalty_tn -// ), -// datum: 0 -// } -// ] -// } -// } From a4d30b5d90a840652ddeec544c8fd6815a9b466e Mon Sep 17 00:00:00 2001 From: SamDelaney Date: Tue, 21 May 2024 15:32:16 -0700 Subject: [PATCH 17/24] add token test & remove traces --- .../validators/reducible_royalty.ak | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak b/CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak index 218eeedb6..645af02cf 100644 --- a/CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak +++ b/CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak @@ -20,7 +20,7 @@ validator { list.find( tx.outputs, fn(output) { - value.quantity_of(output.value, redeemer.policy_id, royalty_tn) >= 1 + value.quantity_of(output.value, redeemer.policy_id, royalty_tn) == 1 }, ) @@ -31,7 +31,7 @@ validator { list.find( tx.inputs, fn(input) { - value.quantity_of(input.output.value, redeemer.policy_id, royalty_tn) >= 1 + value.quantity_of(input.output.value, redeemer.policy_id, royalty_tn) == 1 }, ) @@ -56,7 +56,7 @@ validator { // the recipient has signed the transaction AND // the fee has not changed OR // the fee has decreased (encoding reverses this) - list.has(tx.extra_signatories, sig) && in_recipient.fee > out_recipient.fee || (in_recipient.fee == out_recipient.fee)? + list.has(tx.extra_signatories, sig) && in_recipient.fee > out_recipient.fee || in_recipient.fee == out_recipient.fee } else { False } @@ -74,7 +74,6 @@ const mock_owner = "some_fake_address_hash" const mock_policy_id = "some_fake_policy_id" test should_reduce_recipient() { - trace @"do traces work?" let redeemer = ReduceRedeemer { policy_id: mock_policy_id } let in_recipient = @@ -108,7 +107,6 @@ test should_reduce_recipient() { } test should_fail_increase_recipient() fail { - trace @"do traces work?" let redeemer = ReduceRedeemer { policy_id: mock_policy_id } let in_recipient = @@ -141,6 +139,39 @@ test should_fail_increase_recipient() fail { reduce([], redeemer, ctx) } +test should_fail_no_token() fail { + let redeemer = ReduceRedeemer { policy_id: mock_policy_id } + + let in_recipient = + RoyaltyRecipient { + address: Address { + payment_credential: VerificationKeyCredential(mock_owner), + stake_credential: None, + }, + fee: 125, + min_fee: None, + max_fee: None, + } + + let input = recipients_to_input([in_recipient], mock_policy_id) + let recipient_output = + recipients_to_output( + [RoyaltyRecipient { ..in_recipient, fee: 625 }], + mock_policy_id, + ) + + let outputs = + [Output { ..recipient_output, value: value.zero() }] + + let ctx = + ScriptContext { + transaction: get_base_transaction([input], outputs), + purpose: Spend(input.output_reference), + } + + reduce([], redeemer, ctx) +} + fn get_base_transaction( recipients_in: List, recipients_out: List, From 80a79d0f4b6cfdcc8500d34e17a49b4231d62dec Mon Sep 17 00:00:00 2001 From: SamDelaney Date: Wed, 5 Jun 2024 21:38:16 -0700 Subject: [PATCH 18/24] WIP: building out the listing contract --- .../lib/onchain-reference/common.ak | 116 ++++- .../{tests.ak => test_utils.ak} | 11 +- .../onchain-reference/validators/minting.ak | 5 +- .../validators/reducible_royalty.ak | 15 +- .../validators/simple_listing.ak | 405 ++++++++++++++++++ 5 files changed, 531 insertions(+), 21 deletions(-) rename CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/{tests.ak => test_utils.ak} (88%) create mode 100644 CIP-0102/ref_impl/onchain-reference/validators/simple_listing.ak diff --git a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak index 33bbb8548..10e26ea78 100644 --- a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak +++ b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak @@ -1,15 +1,36 @@ -use aiken/dict -use aiken/transaction.{Datum, DatumHash, InlineDatum, NoDatum, Transaction} -use aiken/transaction/credential.{Address} +use aiken/bytearray +use aiken/dict.{Dict} +use aiken/hash.{Blake2b_224, Blake2b_256, Hash} +use aiken/list +use aiken/option +use aiken/transaction.{ + Datum, DatumHash, InlineDatum, Input, NoDatum +} +use aiken/transaction/credential.{Address, VerificationKey} +use aiken/transaction/value.{AssetName, PolicyId} pub const royalty_tn: ByteArray = "001f4d70526f79616c7479" +// taken from the CIP 68 standard +pub const user_tn_prefix: ByteArray = "000de140" + +pub const ref_tn_prefix: ByteArray = "000643b0" + pub type RoyaltyDatum { recipients: List, version: Int, extra: Data, } +pub type Metadata = + Dict + +pub type RefDatumMetadata { + metadata: Metadata, + version: Int, + extra: Data, +} + pub type RoyaltyRecipient { address: Address, // percentage (fraction) @@ -20,13 +41,98 @@ pub type RoyaltyRecipient { max_fee: Option, } -pub fn get_data(tx: Transaction, datum: Datum) -> Data { +pub type Asset { + policy_id: PolicyId, + asset_name: AssetName, + quantity: Int, +} + +pub type VerificationKeyHash = + Hash + +// converts between the encoded onchain fee & per mille to avoid dealing with decimals +// since integer division handles the truncation for us, it's as simple as dividing by 10000 +// recall that n / (n / m) == m, so the same function handles both directions of conversion +pub fn to_from_per_mille_fee(in_fee: Int) -> Int { + 10000 / in_fee +} + +// take a CIP 68 user token asset name, and output the corresponding reference token asset name +pub fn get_ref_tn(asset_name: ByteArray) -> ByteArray { + bytearray.concat(ref_tn_prefix, bytearray.drop(asset_name, 8)) +} + +// take a CIP 68 reference token asset name, and output the corresponding user token asset name +pub fn get_user_tn(asset_name: ByteArray) -> ByteArray { + bytearray.concat(user_tn_prefix, bytearray.drop(asset_name, 8)) +} + +pub fn get_data( + tx_datums: Dict, Data>, + datum: Datum, +) -> Data { when datum is { NoDatum -> fail DatumHash(h) -> { - expect Some(d) = dict.get(tx.datums, h) + expect Some(d) = dict.get(tx_datums, h) d } InlineDatum(d) -> d } } + +pub fn get_recipients( + asset: Asset, + reference_inputs: List, + datums: Dict, Data>, +) -> List { + // Get royalty input + let royalty_input = + list.find( + reference_inputs, + fn(input) { + value.quantity_of(input.output.value, asset.policy_id, royalty_tn) == 1 + }, + ) + + if option.is_some(royalty_input) { + // Get Royalty Datum + expect Some(found_royalty_input) = royalty_input + expect royalty_info: RoyaltyDatum = + get_data(datums, found_royalty_input.output.datum) + // If found, get recipients + royalty_info.recipients + } else { + // If not found, check if the asset is a CIP-68 user token + if bytearray.take(asset.asset_name, 8) == user_tn_prefix { + // if so, calculate reference token asset name + let ref_tn = get_ref_tn(asset.asset_name) + + // look for an input with a reference token + expect Some(ref_token_input) = + list.find( + reference_inputs, + fn(input) { + value.quantity_of(input.output.value, asset.policy_id, ref_tn) == 1 + }, + ) + // parse the reference datum + expect _reference_datum: RefDatumMetadata = + get_data(datums, ref_token_input.output.datum) + + + // // Check for royalty flag + // if reference_datum.extra { + // TODO: Figure out how to pass on an `expect` failure + // // throw error if it exists and is > 0 + // } else { + // return empty list if it exists and is 0 + // return empty list if it doesn't exist + [] + } else { + // } + // if the asset is not a CIP 68 NFT, return an empty list + [] + } + } +} diff --git a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/tests.ak b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/test_utils.ak similarity index 88% rename from CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/tests.ak rename to CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/test_utils.ak index 30a0b1f51..cfe3dce64 100644 --- a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/tests.ak +++ b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/test_utils.ak @@ -3,16 +3,15 @@ use aiken/transaction/credential.{Address, VerificationKeyCredential} use aiken/transaction/value.{PolicyId} use onchain_reference/common.{RoyaltyDatum, RoyaltyRecipient, royalty_tn} +pub const placeholder_oref = OutputReference { + transaction_id: transaction.placeholder().id, + output_index: 0, +} + pub fn recipients_to_input( recipients: List, policy_id: PolicyId, ) -> Input { - let mock_outref = - OutputReference { - transaction_id: transaction.placeholder().id, - output_index: 0, - } - Input { output_reference: mock_outref, output: recipients_to_output(recipients, policy_id), diff --git a/CIP-0102/ref_impl/onchain-reference/validators/minting.ak b/CIP-0102/ref_impl/onchain-reference/validators/minting.ak index 526a005a9..a6736a730 100644 --- a/CIP-0102/ref_impl/onchain-reference/validators/minting.ak +++ b/CIP-0102/ref_impl/onchain-reference/validators/minting.ak @@ -1,17 +1,16 @@ -use aiken/hash.{Blake2b_224, Hash} use aiken/interval.{Finite, Interval, IntervalBound} use aiken/list use aiken/time.{PosixTime} use aiken/transaction.{Mint, ScriptContext, Transaction} -use aiken/transaction/credential.{VerificationKey} use aiken/transaction/value +use onchain_reference/common.{VerificationKeyHash} type Redeemer { MintNft BurnNft } -validator(lock_time: PosixTime, owner: Hash) { +validator(lock_time: PosixTime, owner: VerificationKeyHash) { fn minting_validator(redeemer: Redeemer, ctx: ScriptContext) -> Bool { expect is_not_expired(ctx.transaction, lock_time)? && list.has( diff --git a/CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak b/CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak index 645af02cf..862b17b9f 100644 --- a/CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak +++ b/CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak @@ -5,7 +5,7 @@ use aiken/transaction/value.{PolicyId} use onchain_reference/common.{ RoyaltyDatum, RoyaltyRecipient, get_data, royalty_tn, } -use onchain_reference/tests.{recipients_to_input, recipients_to_output} +use onchain_reference/test_utils.{recipients_to_input, recipients_to_output} type ReduceRedeemer { policy_id: PolicyId, @@ -24,7 +24,8 @@ validator { }, ) - expect out_royalty_info: RoyaltyDatum = get_data(tx, royalty_output.datum) + expect out_royalty_info: RoyaltyDatum = + get_data(tx.datums, royalty_output.datum) // find input datum expect Some(royalty_input) = @@ -36,7 +37,7 @@ validator { ) expect in_royalty_info: RoyaltyDatum = - get_data(tx, royalty_input.output.datum) + get_data(tx.datums, royalty_input.output.datum) // check that for each in recipient, there is a corresponding out recipient expect @@ -82,7 +83,7 @@ test should_reduce_recipient() { payment_credential: VerificationKeyCredential(mock_owner), stake_credential: None, }, - fee: 625, + fee: 625, // 1.6% min_fee: None, max_fee: None, } @@ -92,7 +93,7 @@ test should_reduce_recipient() { let outputs = [ recipients_to_output( - [RoyaltyRecipient { ..in_recipient, fee: 125 }], + [RoyaltyRecipient { ..in_recipient, fee: 125 }], // 8% mock_policy_id, ), ] @@ -115,7 +116,7 @@ test should_fail_increase_recipient() fail { payment_credential: VerificationKeyCredential(mock_owner), stake_credential: None, }, - fee: 125, + fee: 125, // 8% min_fee: None, max_fee: None, } @@ -125,7 +126,7 @@ test should_fail_increase_recipient() fail { let outputs = [ recipients_to_output( - [RoyaltyRecipient { ..in_recipient, fee: 625 }], + [RoyaltyRecipient { ..in_recipient, fee: 625 }], // 1.6% mock_policy_id, ), ] diff --git a/CIP-0102/ref_impl/onchain-reference/validators/simple_listing.ak b/CIP-0102/ref_impl/onchain-reference/validators/simple_listing.ak new file mode 100644 index 000000000..7ef9c5adb --- /dev/null +++ b/CIP-0102/ref_impl/onchain-reference/validators/simple_listing.ak @@ -0,0 +1,405 @@ +use aiken/builtin +use aiken/dict +use aiken/list +use aiken/math +use aiken/option +use aiken/transaction.{ + Datum, InlineDatum, NoDatum, Output, OutputReference, ScriptContext, Spend, + Transaction, TransactionId, +} +use aiken/transaction/credential.{Address, VerificationKeyCredential} +use aiken/transaction/value.{to_dict} +use onchain_reference/common.{ + Asset, RoyaltyRecipient, VerificationKeyHash, get_recipients, + to_from_per_mille_fee, +} +use onchain_reference/test_utils.{placeholder_oref} + +type ListingDatum { + // to further prevent circumvention, lot could be detected from consumed the Listing output + lot: Asset, + seller: VerificationKeyHash, + // in lovelace + price: Int, +} + +// functionally identical to ListingDatum, but used in a different capacity. +type Payout { + recipient: Address, + // in lovelace + amount: Int, +} + +/// A user can either buy a token +/// or cancel/update the listing. +type Redeemer { + /// Make a purchase. + Buy + /// Cancel or update a listing. + WithdrawOrUpdate +} + +validator { + /// Validate that the signer is the owner or that payouts + /// are present as outputs and that the tag is correct. + fn spend(datum: ListingDatum, redeemer: Redeemer, ctx: ScriptContext) -> Bool { + let ScriptContext { transaction, purpose } = ctx + + let Transaction { outputs, extra_signatories, reference_inputs, datums, .. } = + transaction + + // Match on the action. + when redeemer is { + Buy -> { + expect Spend(_out_ref) = purpose + + let royalty_recipients: List = + get_recipients(datum.lot, reference_inputs, datums) + + // ensure royalty amount is paid to the royalty address + let payouts: List = + get_payouts(datum.seller, royalty_recipients, datum.price) + + // check that the payouts are represented in the outputs and that enough are + datum.price <= check_payouts(outputs, payouts, NoDatum) + } + + // There's not much to do here. An asset + // owner can cancel or update their listing + // at any time. + WithdrawOrUpdate -> list.has(extra_signatories, datum.seller) + } + } +} + +fn get_payouts( + seller: VerificationKeyHash, + royalty_recipients: List, + price: Int, +) -> List { + // for each recipient + // DO THIS WORK? TEST IT + let already_paid_out = 0 + + let payouts = + list.map( + royalty_recipients, + fn(recipient: RoyaltyRecipient) -> Payout { + // get the estimated amount based on the price + let amount = to_from_per_mille_fee(recipient.fee) * price / 1000 + + // adjust based on min fee + let amount = + if option.is_some(recipient.min_fee) { + expect Some(min) = recipient.min_fee + math.max(amount, math.min(price - already_paid_out, min)) + } else { + amount + } + + // adjust based on max fee + let amount = + if option.is_some(recipient.max_fee) { + expect Some(max) = recipient.max_fee + math.min(amount, max) + } else { + amount + } + let already_paid_out = already_paid_out + amount + // output payout + Payout { recipient: recipient.address, amount } + }, + ) + + let seller_payout = + Payout { + recipient: Address { + payment_credential: VerificationKeyCredential(seller), + stake_credential: None, + }, + amount: price - already_paid_out, + } + + // output seller payout with remainder of price + list.push(payouts, seller_payout) +} + +/// Credit to JPG.Store's v3 open source contracts. Thank you for helping push the ecosystem forward! +/// Check that payouts and payout outputs +/// are correct. Payouts are stored in the datum +/// when assets are listed. On buy a transaction +/// with matching payout outputs needs to be constructed. +/// We also require that outputs are in the same order as the +/// payouts in the datum. Returns the sum of the payout amounts. +fn check_payouts( + outputs: List, + payouts: List, + datum_tag: Datum, +) -> Int { + expect [first_output, ..rest_outputs] = outputs + + let Output { address: output_address, value, datum, .. } = first_output + + expect datum == datum_tag + expect [payout, ..rest_payouts] = payouts + + let Payout { recipient: payout_address, amount: amount_lovelace } = payout + + // The `Output` address must match + // the address specified in the corresponding + // payout from the datum. + expect payout_address == output_address + expect [(policy, tokens)] = + value + |> to_dict + |> dict.to_list + + expect [(_, quantity)] = dict.to_list(tokens) + + expect policy == "" + // The quantity in the output must equal + // the amount specified in the corresponding + // payout from the datum. + expect quantity >= amount_lovelace && amount_lovelace > 0 + let rest_payouts_amount = + when rest_payouts is { + // the base case + [] -> + // if rest is empty we are done + 0 + _ -> + // continue with remaining outputs and payouts + check_payouts_aux(rest_outputs, rest_payouts) + } + + amount_lovelace + rest_payouts_amount +} + +fn check_payouts_aux(outputs: List, payouts: List) -> Int { + expect [first_output, ..rest_outputs] = outputs + + let Output { address: output_address, value, datum, .. } = first_output + + expect datum == NoDatum + expect [payout, ..rest_payouts] = payouts + + let Payout { recipient: payout_address, amount: amount_lovelace } = payout + + // The `Output` address must match + // the address specified in the corresponding + // payout from the datum. + expect payout_address == output_address + expect [(policy, tokens)] = + value + |> to_dict + |> dict.to_list + + expect [(_, quantity)] = dict.to_list(tokens) + + expect policy == "" + // The quantity in the output must equal + // the amount specified in the corresponding + // payout from the datum. + expect quantity >= amount_lovelace && amount_lovelace > 0 + let rest_payouts_amount = + when rest_payouts is { + // the base case + [] -> + // if rest is empty we are done + 0 + _ -> + // continue with remaining outputs and payouts + check_payouts_aux(rest_outputs, rest_payouts) + } + + amount_lovelace + rest_payouts_amount +} + +// ############### ENDPOINT TESTS ############### +const mock_owner: VerificationKeyHash = "some_fake_address_hash" +const mock_policy_id = "fake_lot_policy_id" +const mock_buyer: VerificationKeyHash = "some_other_fake_address_hash" +const spent_oref = OutputReference { + ..placeholder_oref, + output_index: 1 // 0 is the royalty input +} + +test one_recipient_cip68_buy() { + let redeemer = Redeemer { Buy } + + let listing_datum = ListingDatum { + lot: Asset { + policy_id: mock_policy_id, + asset_name: user_tn_prefix + "fake_lot_tn", + 1 + }, + seller: mock_owner, + price: 1 000 000 000 // 1000 Ada + } + + let royalty_recipients = + [RoyaltyRecipient { + address: Address { + payment_credential: VerificationKeyCredential(mock_owner), + stake_credential: None, + }, + fee: 125, // 8% + min_fee: None, + max_fee: None, + }] + + let ctx = ScriptContext { + transaction: get_base_transaction(listing_datum, royalty_recipients ), + purpose: Spend(spent_oref) + } + + spend(listing_datum, redeemer, ctx) +} + +fn get_base_transaction(listing_datum: ListingDatum, royalty_recipients: List , outputs: List) -> Transaction { + let lot_input = lot_to_input(listing_datum) + let royalty_policy_ref_input = recipients_to_input([in_recipient], mock_policy_id) + + let buyer_address = Address { + payment_credential: VerificationKeyCredential(mock_buyer), + stake_credential: None + } + + let buyer_input = Input { + output: Output { + address: buyer_address, + value: value.from_lovelace(listing_datum.price + 1000000) // add 1 ada for fees,, + datum: None, + reference_script: None + }, + output_reference: OutputReference {..placeholder_oref, output_index: 2 } // 0 is royalty input, 1 is lot + } + + let buyer_output = Output { + address: buyer_address, + value: value.from_lovelace(listing_datum.price), + datum: None, + reference_script: None + } + + let amount_to_royalties = list.foldl(royalty_recipients, 0, fn(recipient, total) { + to_from_per_mille_fee(royalty.fee) * 10 * listing_datum.price + }) + + let royalty_outputs: List = list.map(royalty_recipients, fn(recipient) { + Output { + address: first_recipient.address, + value: value.from_lovelace(to_from_per_mille_fee(first_recipient.fee) * 10 * listing_datum.price), + datum: None, + reference_script: None + } + }) + + let seller_output = Output { + address: Address { + payment_credential: VerificationKeyCredential(mock_owner), + stake_credential: None + }, + value: value.from_lovelace(listing_datum.price - amount_to_royalties), + datum: None, + reference_script: None + } + + Transaction { + ..transaction.placeholder(), + extra_signatories: [mock_buyer], + inputs: [lot_input, buyer_input], + reference_inputs: [royalty_policy_ref_input], + outputs: [seller_output, buyer_output, royalty_outputs], + } +} + +fn lot_to_input(datum: ListingDatum) -> Input { + let lot = datum.lot + Input { + output: Output { + address: Address { + payment_credential: ScriptCredential(""), + stake_credential: None + }, + value: value.from_asset(lot.policy_id, lot.asset_name, lot.quantity), + datum: InlineDatum(datum), + reference_script: None + }, + output_reference: spent_oref + } +} + +// ############### FUNCTION TESTS ############### + +/// This test makes sure the `check_payouts` returns true +/// when give the correct inputs. It is safe to have trailing outputs +/// in the transaction as long as the payouts are correct. +test check_payouts_with_trailing_outputs() { + let test_royalty_addr = + Address { + payment_credential: VerificationKeyCredential( + #"80f60f3b5ea7153e0acc7a803e4401d44b8ed1bae1c7baaad1a62a81", + ), + stake_credential: None, + } + + let test_seller_addr = + Address { + payment_credential: VerificationKeyCredential( + #"90f60f3b5ea7153e0acc7a803e4401d44b8ed1bae1c7baaad1a62a81", + ), + stake_credential: None, + } + + let test_random_addr = + Address { + payment_credential: VerificationKeyCredential( + #"fff60f3b5ea7153e0acc7a803e4401d44b8ed1bae1c7baaad1a62a81", + ), + stake_credential: None, + } + + let test_royalty_payouts = + [ + Payout { recipient: test_royalty_addr, amount: 3000000 }, + Payout { recipient: test_seller_addr, amount: 95000000 }, + ] + + let datum_tag = + OutputReference { + transaction_id: TransactionId { hash: #"00" }, + output_index: 0, + } + |> builtin.serialise_data + |> builtin.blake2b_256 + |> InlineDatum + + let out_1 = + Output { + address: test_royalty_addr, + value: value.from_lovelace(3100000), + datum: datum_tag, + reference_script: None, + } + + let out_2 = + Output { + address: test_seller_addr, + value: value.from_lovelace(95000000), + datum: NoDatum, + reference_script: None, + } + + let out_random = + Output { + address: test_random_addr, + value: value.from_lovelace(1000000), + datum: datum_tag, + reference_script: None, + } + + let outputs = list.concat([out_1, out_2], list.repeat(out_random, 100)) + + 98000000 == check_payouts(outputs, test_royalty_payouts, datum_tag) +} \ No newline at end of file From 8b7d85a1f12b3a10dc328ebf89a75311aea57686 Mon Sep 17 00:00:00 2001 From: SamDelaney Date: Wed, 12 Jun 2024 13:00:59 -0700 Subject: [PATCH 19/24] fix compile errors for simple_listing --- .../ref_impl/onchain-reference/aiken.lock | 4 +- .../ref_impl/onchain-reference/aiken.toml | 2 +- .../lib/onchain-reference/common.ak | 63 +++-- .../lib/onchain-reference/test_utils.ak | 10 +- .../ref_impl/onchain-reference/plutus.json | 2 +- .../validators/simple_listing.ak | 237 ++++++++++-------- 6 files changed, 192 insertions(+), 126 deletions(-) diff --git a/CIP-0102/ref_impl/onchain-reference/aiken.lock b/CIP-0102/ref_impl/onchain-reference/aiken.lock index 0a72eb3c6..62f2265a8 100644 --- a/CIP-0102/ref_impl/onchain-reference/aiken.lock +++ b/CIP-0102/ref_impl/onchain-reference/aiken.lock @@ -3,12 +3,12 @@ [[requirements]] name = "aiken-lang/stdlib" -version = "1.7.0" +version = "1.9.0" source = "github" [[packages]] name = "aiken-lang/stdlib" -version = "1.7.0" +version = "1.9.0" requirements = [] source = "github" diff --git a/CIP-0102/ref_impl/onchain-reference/aiken.toml b/CIP-0102/ref_impl/onchain-reference/aiken.toml index 7a07c2757..54430e60f 100644 --- a/CIP-0102/ref_impl/onchain-reference/aiken.toml +++ b/CIP-0102/ref_impl/onchain-reference/aiken.toml @@ -10,5 +10,5 @@ platform = "github" [[dependencies]] name = "aiken-lang/stdlib" -version = "1.7.0" +version = "1.9.0" source = "github" diff --git a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak index 10e26ea78..273751b4a 100644 --- a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak +++ b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak @@ -1,11 +1,11 @@ +use aiken/builtin.{choose_data} use aiken/bytearray use aiken/dict.{Dict} use aiken/hash.{Blake2b_224, Blake2b_256, Hash} +use aiken/int use aiken/list use aiken/option -use aiken/transaction.{ - Datum, DatumHash, InlineDatum, Input, NoDatum -} +use aiken/transaction.{Datum, DatumHash, InlineDatum, Input, NoDatum} use aiken/transaction/credential.{Address, VerificationKey} use aiken/transaction/value.{AssetName, PolicyId} @@ -23,12 +23,20 @@ pub type RoyaltyDatum { } pub type Metadata = - Dict + Pairs + +pub type ChooseData { + DConstr + DMap + DList + DInt + DByteArray +} pub type RefDatumMetadata { metadata: Metadata, version: Int, - extra: Data, + extra: Metadata, } pub type RoyaltyRecipient { @@ -54,7 +62,7 @@ pub type VerificationKeyHash = // since integer division handles the truncation for us, it's as simple as dividing by 10000 // recall that n / (n / m) == m, so the same function handles both directions of conversion pub fn to_from_per_mille_fee(in_fee: Int) -> Int { - 10000 / in_fee + 10_000 / in_fee } // take a CIP 68 user token asset name, and output the corresponding reference token asset name @@ -117,20 +125,41 @@ pub fn get_recipients( }, ) // parse the reference datum - expect _reference_datum: RefDatumMetadata = + expect reference_datum: RefDatumMetadata = get_data(datums, ref_token_input.output.datum) - - // // Check for royalty flag - // if reference_datum.extra { - // TODO: Figure out how to pass on an `expect` failure - // // throw error if it exists and is > 0 - // } else { - // return empty list if it exists and is 0 - // return empty list if it doesn't exist - [] + // Check for royalty flag + let has_flag = list.find(reference_datum.extra, fn(item) -> Bool { and { + // maybe TODO: convert to hex + item.1st == "royalty_included", + // when constr, map, list, int, bytearray + when + choose_data(item.2nd, DConstr, DMap, DList, DInt, DByteArray) + is { + DInt -> { + expect parsed_int: Int = item.2nd + parsed_int >= 1 + } + DByteArray -> { + expect parsed_ba: ByteArray = item.2nd + when int.from_utf8(parsed_ba) is { + None -> False + Some(parsed_int) -> parsed_int >= 1 + } + } + _ -> False + }, + } }) + + when has_flag is { + Some(_) -> + // throw error if it exists and is > 0 + fail + // return empty list if it doesn't exist or is 0 + None -> + [] + } } else { - // } // if the asset is not a CIP 68 NFT, return an empty list [] } diff --git a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/test_utils.ak b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/test_utils.ak index cfe3dce64..285adb052 100644 --- a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/test_utils.ak +++ b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/test_utils.ak @@ -3,9 +3,11 @@ use aiken/transaction/credential.{Address, VerificationKeyCredential} use aiken/transaction/value.{PolicyId} use onchain_reference/common.{RoyaltyDatum, RoyaltyRecipient, royalty_tn} -pub const placeholder_oref = OutputReference { - transaction_id: transaction.placeholder().id, - output_index: 0, +pub fn placeholder_oref() { + OutputReference { + transaction_id: transaction.placeholder().id, + output_index: 0, + } } pub fn recipients_to_input( @@ -13,7 +15,7 @@ pub fn recipients_to_input( policy_id: PolicyId, ) -> Input { Input { - output_reference: mock_outref, + output_reference: placeholder_oref(), output: recipients_to_output(recipients, policy_id), } } diff --git a/CIP-0102/ref_impl/onchain-reference/plutus.json b/CIP-0102/ref_impl/onchain-reference/plutus.json index 46df487e3..553f6c2f4 100644 --- a/CIP-0102/ref_impl/onchain-reference/plutus.json +++ b/CIP-0102/ref_impl/onchain-reference/plutus.json @@ -6,7 +6,7 @@ "plutusVersion": "v2", "compiler": { "name": "Aiken", - "version": "v1.0.20-alpha+unknown" + "version": "v1.0.29-alpha+unknown" }, "license": "Apache-2.0" }, diff --git a/CIP-0102/ref_impl/onchain-reference/validators/simple_listing.ak b/CIP-0102/ref_impl/onchain-reference/validators/simple_listing.ak index 7ef9c5adb..81948d6cf 100644 --- a/CIP-0102/ref_impl/onchain-reference/validators/simple_listing.ak +++ b/CIP-0102/ref_impl/onchain-reference/validators/simple_listing.ak @@ -1,19 +1,21 @@ use aiken/builtin -use aiken/dict +use aiken/bytearray use aiken/list use aiken/math use aiken/option use aiken/transaction.{ - Datum, InlineDatum, NoDatum, Output, OutputReference, ScriptContext, Spend, - Transaction, TransactionId, + Datum, InlineDatum, Input, NoDatum, Output, OutputReference, ScriptContext, + Spend, Transaction, TransactionId, } -use aiken/transaction/credential.{Address, VerificationKeyCredential} -use aiken/transaction/value.{to_dict} +use aiken/transaction/credential.{ + Address, ScriptCredential, VerificationKeyCredential, +} +use aiken/transaction/value.{quantity_of} use onchain_reference/common.{ Asset, RoyaltyRecipient, VerificationKeyHash, get_recipients, - to_from_per_mille_fee, + to_from_per_mille_fee, user_tn_prefix, } -use onchain_reference/test_utils.{placeholder_oref} +use onchain_reference/test_utils.{placeholder_oref, recipients_to_input} type ListingDatum { // to further prevent circumvention, lot could be detected from consumed the Listing output @@ -32,7 +34,7 @@ type Payout { /// A user can either buy a token /// or cancel/update the listing. -type Redeemer { +type ListingRedeemer { /// Make a purchase. Buy /// Cancel or update a listing. @@ -42,7 +44,11 @@ type Redeemer { validator { /// Validate that the signer is the owner or that payouts /// are present as outputs and that the tag is correct. - fn spend(datum: ListingDatum, redeemer: Redeemer, ctx: ScriptContext) -> Bool { + fn spend( + datum: ListingDatum, + redeemer: ListingRedeemer, + ctx: ScriptContext, + ) -> Bool { let ScriptContext { transaction, purpose } = ctx let Transaction { outputs, extra_signatories, reference_inputs, datums, .. } = @@ -79,12 +85,15 @@ fn get_payouts( ) -> List { // for each recipient // DO THIS WORK? TEST IT - let already_paid_out = 0 - let payouts = - list.map( + let (payouts, paid_out) = + list.foldl( royalty_recipients, - fn(recipient: RoyaltyRecipient) -> Payout { + ([], 0), + fn(recipient: RoyaltyRecipient, out: (List, Int)) -> ( + List, + Int, + ) { // get the estimated amount based on the price let amount = to_from_per_mille_fee(recipient.fee) * price / 1000 @@ -92,7 +101,7 @@ fn get_payouts( let amount = if option.is_some(recipient.min_fee) { expect Some(min) = recipient.min_fee - math.max(amount, math.min(price - already_paid_out, min)) + math.max(amount, math.min(price - out.2nd, min)) } else { amount } @@ -105,9 +114,15 @@ fn get_payouts( } else { amount } - let already_paid_out = already_paid_out + amount + let already_paid_out = out.2nd + amount // output payout - Payout { recipient: recipient.address, amount } + ( + list.concat( + out.1st, + [Payout { recipient: recipient.address, amount }], + ), + already_paid_out, + ) }, ) @@ -117,7 +132,7 @@ fn get_payouts( payment_credential: VerificationKeyCredential(seller), stake_credential: None, }, - amount: price - already_paid_out, + amount: price - paid_out, } // output seller payout with remainder of price @@ -149,17 +164,11 @@ fn check_payouts( // the address specified in the corresponding // payout from the datum. expect payout_address == output_address - expect [(policy, tokens)] = - value - |> to_dict - |> dict.to_list - expect [(_, quantity)] = dict.to_list(tokens) - - expect policy == "" // The quantity in the output must equal // the amount specified in the corresponding // payout from the datum. + let quantity = quantity_of(value, "", "") expect quantity >= amount_lovelace && amount_lovelace > 0 let rest_payouts_amount = when rest_payouts is { @@ -189,17 +198,11 @@ fn check_payouts_aux(outputs: List, payouts: List) -> Int { // the address specified in the corresponding // payout from the datum. expect payout_address == output_address - expect [(policy, tokens)] = - value - |> to_dict - |> dict.to_list - - expect [(_, quantity)] = dict.to_list(tokens) - expect policy == "" // The quantity in the output must equal // the amount specified in the corresponding // payout from the datum. + let quantity = quantity_of(value, "", "") expect quantity >= amount_lovelace && amount_lovelace > 0 let rest_payouts_amount = when rest_payouts is { @@ -217,100 +220,131 @@ fn check_payouts_aux(outputs: List, payouts: List) -> Int { // ############### ENDPOINT TESTS ############### const mock_owner: VerificationKeyHash = "some_fake_address_hash" + const mock_policy_id = "fake_lot_policy_id" + const mock_buyer: VerificationKeyHash = "some_other_fake_address_hash" -const spent_oref = OutputReference { - ..placeholder_oref, - output_index: 1 // 0 is the royalty input -} test one_recipient_cip68_buy() { - let redeemer = Redeemer { Buy } + let redeemer = Buy - let listing_datum = ListingDatum { - lot: Asset { + let lot = + Asset { policy_id: mock_policy_id, - asset_name: user_tn_prefix + "fake_lot_tn", - 1 - }, - seller: mock_owner, - price: 1 000 000 000 // 1000 Ada - } + asset_name: bytearray.concat(user_tn_prefix, "fake_lot_tn"), + quantity: 1, + } + + // Price: 1000 Ada + let listing_datum = + ListingDatum { lot, seller: mock_owner, price: 1_000_000_000 } let royalty_recipients = - [RoyaltyRecipient { - address: Address { - payment_credential: VerificationKeyCredential(mock_owner), - stake_credential: None, + [ + RoyaltyRecipient { + address: Address { + payment_credential: VerificationKeyCredential(mock_owner), + stake_credential: None, + }, + fee: 125, + // 8% + min_fee: None, + max_fee: None, }, - fee: 125, // 8% - min_fee: None, - max_fee: None, - }] - - let ctx = ScriptContext { - transaction: get_base_transaction(listing_datum, royalty_recipients ), - purpose: Spend(spent_oref) - } + ] + + let ctx = + ScriptContext { + transaction: get_base_transaction(listing_datum, royalty_recipients), + purpose: Spend(OutputReference { ..placeholder_oref(), output_index: 1 }), + } + // 0 is the royalty input spend(listing_datum, redeemer, ctx) } -fn get_base_transaction(listing_datum: ListingDatum, royalty_recipients: List , outputs: List) -> Transaction { +fn get_base_transaction( + listing_datum: ListingDatum, + royalty_recipients: List, +) -> Transaction { let lot_input = lot_to_input(listing_datum) - let royalty_policy_ref_input = recipients_to_input([in_recipient], mock_policy_id) + let royalty_policy_ref_input = + recipients_to_input(royalty_recipients, mock_policy_id) - let buyer_address = Address { - payment_credential: VerificationKeyCredential(mock_buyer), - stake_credential: None - } + let buyer_address = + Address { + payment_credential: VerificationKeyCredential(mock_buyer), + stake_credential: None, + } - let buyer_input = Input { - output: Output { + let buyer_output = + Output { address: buyer_address, - value: value.from_lovelace(listing_datum.price + 1000000) // add 1 ada for fees,, - datum: None, - reference_script: None - }, - output_reference: OutputReference {..placeholder_oref, output_index: 2 } // 0 is royalty input, 1 is lot - } - - let buyer_output = Output { - address: buyer_address, - value: value.from_lovelace(listing_datum.price), - datum: None, - reference_script: None - } + // add 1 ada for fees + value: value.from_lovelace(listing_datum.price + 1_000_000), + datum: NoDatum, + reference_script: None, + } - let amount_to_royalties = list.foldl(royalty_recipients, 0, fn(recipient, total) { - to_from_per_mille_fee(royalty.fee) * 10 * listing_datum.price - }) + let buyer_input = + Input { + output: buyer_output, + output_reference: OutputReference { + ..placeholder_oref(), + output_index: 2, + }, + } - let royalty_outputs: List = list.map(royalty_recipients, fn(recipient) { + // 0 is royalty input, 1 is lot + let buyer_output = Output { - address: first_recipient.address, - value: value.from_lovelace(to_from_per_mille_fee(first_recipient.fee) * 10 * listing_datum.price), - datum: None, - reference_script: None + address: buyer_address, + value: value.from_lovelace(listing_datum.price), + datum: NoDatum, + reference_script: None, } - }) - let seller_output = Output { - address: Address { - payment_credential: VerificationKeyCredential(mock_owner), - stake_credential: None - }, - value: value.from_lovelace(listing_datum.price - amount_to_royalties), - datum: None, - reference_script: None - } + let amount_to_royalties = + list.foldl( + royalty_recipients, + 0, + fn(recipient, total) { + total + to_from_per_mille_fee(recipient.fee) * 10 * listing_datum.price + }, + ) + + let royalty_outputs: List = + list.map( + royalty_recipients, + fn(recipient) { + Output { + address: recipient.address, + value: value.from_lovelace( + to_from_per_mille_fee(recipient.fee) * 10 * listing_datum.price, + ), + datum: NoDatum, + reference_script: None, + } + }, + ) + + let seller_output = + Output { + address: Address { + payment_credential: VerificationKeyCredential(mock_owner), + stake_credential: None, + }, + value: value.from_lovelace(listing_datum.price - amount_to_royalties), + datum: NoDatum, + reference_script: None, + } Transaction { ..transaction.placeholder(), extra_signatories: [mock_buyer], inputs: [lot_input, buyer_input], reference_inputs: [royalty_policy_ref_input], - outputs: [seller_output, buyer_output, royalty_outputs], + outputs: [seller_output, buyer_output, ..royalty_outputs], } } @@ -320,14 +354,15 @@ fn lot_to_input(datum: ListingDatum) -> Input { output: Output { address: Address { payment_credential: ScriptCredential(""), - stake_credential: None + stake_credential: None, }, value: value.from_asset(lot.policy_id, lot.asset_name, lot.quantity), datum: InlineDatum(datum), - reference_script: None + reference_script: None, }, - output_reference: spent_oref + output_reference: OutputReference { ..placeholder_oref(), output_index: 1 }, } + // 0 is the royalty input } // ############### FUNCTION TESTS ############### @@ -402,4 +437,4 @@ test check_payouts_with_trailing_outputs() { let outputs = list.concat([out_1, out_2], list.repeat(out_random, 100)) 98000000 == check_payouts(outputs, test_royalty_payouts, datum_tag) -} \ No newline at end of file +} From 75fe37b83838ea9fe76d55d475c0d2cf7c3c6fcf Mon Sep 17 00:00:00 2001 From: SamDelaney Date: Tue, 25 Jun 2024 17:43:51 -0700 Subject: [PATCH 20/24] WIP: bugfixes & optimizations --- .../lib/onchain-reference/common.ak | 7 + .../lib/onchain-reference/listing_utils.ak | 7 + .../lib/onchain-reference/test_utils.ak | 67 +++++++++- .../onchain-reference/validators/minting.ak | 2 +- .../validators/simple_listing.ak | 121 +++++++----------- 5 files changed, 125 insertions(+), 79 deletions(-) create mode 100644 CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/listing_utils.ak diff --git a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak index 273751b4a..70dadac53 100644 --- a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak +++ b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak @@ -165,3 +165,10 @@ pub fn get_recipients( } } } + +test test_to_per_mille() { + and { + to_from_per_mille_fee(125) == 80, + to_from_per_mille_fee(625) == 16, + } +} diff --git a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/listing_utils.ak b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/listing_utils.ak new file mode 100644 index 000000000..f2b778e8a --- /dev/null +++ b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/listing_utils.ak @@ -0,0 +1,7 @@ +use aiken/transaction/credential.{Address} + +pub type Payout { + recipient: Address, + // in lovelace + amount: Int, +} diff --git a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/test_utils.ak b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/test_utils.ak index 285adb052..91278c681 100644 --- a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/test_utils.ak +++ b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/test_utils.ak @@ -1,7 +1,11 @@ +use aiken/string use aiken/transaction.{InlineDatum, Input, Output, OutputReference} -use aiken/transaction/credential.{Address, VerificationKeyCredential} -use aiken/transaction/value.{PolicyId} +use aiken/transaction/credential.{ + Address, ScriptCredential, VerificationKeyCredential, +} +use aiken/transaction/value.{AssetName, PolicyId} use onchain_reference/common.{RoyaltyDatum, RoyaltyRecipient, royalty_tn} +use onchain_reference/listing_utils.{Payout} pub fn placeholder_oref() { OutputReference { @@ -36,3 +40,62 @@ pub fn recipients_to_output( reference_script: None, } } + +pub fn output_to_string(output: Output) -> String { + let value_string = + string.join( + value.flatten_with(output.value, asset_to_string), + @"\n", + ) + + let address_string = address_to_cred_hash(output.address) + + string.join( + [ + @"\nOutput:", + string.concat(@"Address: ", address_string), + string.concat(@"Value: ", value_string), + @"", + ], + @"\n", + ) +} + +fn asset_to_string( + policy_id: PolicyId, + asset_name: AssetName, + amt: Int, +) -> Option { + Some( + string.join( + [ + string.from_bytearray(policy_id), + string.from_bytearray(asset_name), + string.from_int(amt), + ], + @" ", + ), + ) +} + +pub fn payout_to_string(payout: Payout) -> String { + let address_string = address_to_cred_hash(payout.recipient) + + let amt_string = string.from_int(payout.amount) + string.join( + [ + @"\nPayout: ", + string.concat(@"Address: ", address_string), + string.concat(@"Amount: ", amt_string), + @"", + ], + @"\n", + ) +} + +pub fn address_to_cred_hash(address: Address) -> String { + when address.payment_credential is { + VerificationKeyCredential(hash) -> string.from_bytearray(hash) + ScriptCredential(hash) -> string.from_bytearray(hash) + } +} diff --git a/CIP-0102/ref_impl/onchain-reference/validators/minting.ak b/CIP-0102/ref_impl/onchain-reference/validators/minting.ak index a6736a730..5d7f9dc93 100644 --- a/CIP-0102/ref_impl/onchain-reference/validators/minting.ak +++ b/CIP-0102/ref_impl/onchain-reference/validators/minting.ak @@ -60,7 +60,7 @@ test should_mint_nft() { minting_validator(mock_timestamp, mock_owner, redeemer, ctx) == True } -test should_burn_nfg() { +test should_burn_nft() { // GIVEN let redeemer = BurnNft let ctx = diff --git a/CIP-0102/ref_impl/onchain-reference/validators/simple_listing.ak b/CIP-0102/ref_impl/onchain-reference/validators/simple_listing.ak index 81948d6cf..a06442639 100644 --- a/CIP-0102/ref_impl/onchain-reference/validators/simple_listing.ak +++ b/CIP-0102/ref_impl/onchain-reference/validators/simple_listing.ak @@ -1,11 +1,11 @@ -use aiken/builtin use aiken/bytearray use aiken/list use aiken/math use aiken/option +use aiken/string use aiken/transaction.{ - Datum, InlineDatum, Input, NoDatum, Output, OutputReference, ScriptContext, - Spend, Transaction, TransactionId, + InlineDatum, Input, NoDatum, Output, OutputReference, ScriptContext, Spend, + Transaction, } use aiken/transaction/credential.{ Address, ScriptCredential, VerificationKeyCredential, @@ -15,7 +15,10 @@ use onchain_reference/common.{ Asset, RoyaltyRecipient, VerificationKeyHash, get_recipients, to_from_per_mille_fee, user_tn_prefix, } -use onchain_reference/test_utils.{placeholder_oref, recipients_to_input} +use onchain_reference/listing_utils.{Payout} +use onchain_reference/test_utils.{ + output_to_string, payout_to_string, placeholder_oref, recipients_to_input, +} type ListingDatum { // to further prevent circumvention, lot could be detected from consumed the Listing output @@ -25,13 +28,6 @@ type ListingDatum { price: Int, } -// functionally identical to ListingDatum, but used in a different capacity. -type Payout { - recipient: Address, - // in lovelace - amount: Int, -} - /// A user can either buy a token /// or cancel/update the listing. type ListingRedeemer { @@ -66,8 +62,24 @@ validator { let payouts: List = get_payouts(datum.seller, royalty_recipients, datum.price) + trace list.foldl( + outputs, + @"", + fn(output, current_str) { + string.concat(current_str, output_to_string(output)) + }, + ) + + trace list.foldl( + payouts, + @"", + fn(payout, current_str) { + string.concat(current_str, payout_to_string(payout)) + }, + ) + // check that the payouts are represented in the outputs and that enough are - datum.price <= check_payouts(outputs, payouts, NoDatum) + datum.price <= check_payouts(list.reverse(outputs), payouts) } // There's not much to do here. An asset @@ -146,50 +158,10 @@ fn get_payouts( /// with matching payout outputs needs to be constructed. /// We also require that outputs are in the same order as the /// payouts in the datum. Returns the sum of the payout amounts. -fn check_payouts( - outputs: List, - payouts: List, - datum_tag: Datum, -) -> Int { - expect [first_output, ..rest_outputs] = outputs - - let Output { address: output_address, value, datum, .. } = first_output - - expect datum == datum_tag - expect [payout, ..rest_payouts] = payouts - - let Payout { recipient: payout_address, amount: amount_lovelace } = payout - - // The `Output` address must match - // the address specified in the corresponding - // payout from the datum. - expect payout_address == output_address - - // The quantity in the output must equal - // the amount specified in the corresponding - // payout from the datum. - let quantity = quantity_of(value, "", "") - expect quantity >= amount_lovelace && amount_lovelace > 0 - let rest_payouts_amount = - when rest_payouts is { - // the base case - [] -> - // if rest is empty we are done - 0 - _ -> - // continue with remaining outputs and payouts - check_payouts_aux(rest_outputs, rest_payouts) - } - - amount_lovelace + rest_payouts_amount -} - -fn check_payouts_aux(outputs: List, payouts: List) -> Int { +fn check_payouts(outputs: List, payouts: List) -> Int { expect [first_output, ..rest_outputs] = outputs - let Output { address: output_address, value, datum, .. } = first_output - - expect datum == NoDatum + let Output { address: output_address, value, .. } = first_output expect [payout, ..rest_payouts] = payouts let Payout { recipient: payout_address, amount: amount_lovelace } = payout @@ -212,7 +184,7 @@ fn check_payouts_aux(outputs: List, payouts: List) -> Int { 0 _ -> // continue with remaining outputs and payouts - check_payouts_aux(rest_outputs, rest_payouts) + check_payouts(rest_outputs, rest_payouts) } amount_lovelace + rest_payouts_amount @@ -277,7 +249,7 @@ fn get_base_transaction( stake_credential: None, } - let buyer_output = + let buyer_input_output = Output { address: buyer_address, // add 1 ada for fees @@ -288,7 +260,7 @@ fn get_base_transaction( let buyer_input = Input { - output: buyer_output, + output: buyer_input_output, output_reference: OutputReference { ..placeholder_oref(), output_index: 2, @@ -305,14 +277,20 @@ fn get_base_transaction( } let amount_to_royalties = - list.foldl( - royalty_recipients, + math.clamp( + list.foldl( + royalty_recipients, + 0, + fn(recipient, total) { + to_from_per_mille_fee(recipient.fee) * listing_datum.price / 1000 + total + }, + ), 0, - fn(recipient, total) { - total + to_from_per_mille_fee(recipient.fee) * 10 * listing_datum.price - }, + listing_datum.price, ) + trace string.from_int(amount_to_royalties) + let royalty_outputs: List = list.map( royalty_recipients, @@ -320,7 +298,7 @@ fn get_base_transaction( Output { address: recipient.address, value: value.from_lovelace( - to_from_per_mille_fee(recipient.fee) * 10 * listing_datum.price, + to_from_per_mille_fee(recipient.fee) * listing_datum.price / 1000, ), datum: NoDatum, reference_script: None, @@ -344,7 +322,7 @@ fn get_base_transaction( extra_signatories: [mock_buyer], inputs: [lot_input, buyer_input], reference_inputs: [royalty_policy_ref_input], - outputs: [seller_output, buyer_output, ..royalty_outputs], + outputs: list.concat([seller_output, ..royalty_outputs], [buyer_output]), } } @@ -401,20 +379,11 @@ test check_payouts_with_trailing_outputs() { Payout { recipient: test_seller_addr, amount: 95000000 }, ] - let datum_tag = - OutputReference { - transaction_id: TransactionId { hash: #"00" }, - output_index: 0, - } - |> builtin.serialise_data - |> builtin.blake2b_256 - |> InlineDatum - let out_1 = Output { address: test_royalty_addr, value: value.from_lovelace(3100000), - datum: datum_tag, + datum: NoDatum, reference_script: None, } @@ -430,11 +399,11 @@ test check_payouts_with_trailing_outputs() { Output { address: test_random_addr, value: value.from_lovelace(1000000), - datum: datum_tag, + datum: NoDatum, reference_script: None, } let outputs = list.concat([out_1, out_2], list.repeat(out_random, 100)) - 98000000 == check_payouts(outputs, test_royalty_payouts, datum_tag) + 98000000 == check_payouts(outputs, test_royalty_payouts) } From 665500fbb95f60b557ce8d8313dd17c4d14f696c Mon Sep 17 00:00:00 2001 From: SamDelaney Date: Wed, 26 Jun 2024 16:46:53 -0700 Subject: [PATCH 21/24] simple listing endpoint tests --- .../lib/onchain-reference/test_utils.ak | 22 ++++- .../validators/simple_listing.ak | 81 ++++++++++++++++--- 2 files changed, 90 insertions(+), 13 deletions(-) diff --git a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/test_utils.ak b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/test_utils.ak index 91278c681..e139a672a 100644 --- a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/test_utils.ak +++ b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/test_utils.ak @@ -4,7 +4,10 @@ use aiken/transaction/credential.{ Address, ScriptCredential, VerificationKeyCredential, } use aiken/transaction/value.{AssetName, PolicyId} -use onchain_reference/common.{RoyaltyDatum, RoyaltyRecipient, royalty_tn} +use onchain_reference/common.{ + RoyaltyDatum, RoyaltyRecipient, VerificationKeyHash, royalty_tn, + to_from_per_mille_fee, +} use onchain_reference/listing_utils.{Payout} pub fn placeholder_oref() { @@ -14,6 +17,22 @@ pub fn placeholder_oref() { } } +// takes per mille fee & recipient vkc and outputs recipient +pub fn placeholder_recipient( + fee_pml: Int, + address_vkh: VerificationKeyHash, +) -> RoyaltyRecipient { + RoyaltyRecipient { + address: Address { + payment_credential: VerificationKeyCredential(address_vkh), + stake_credential: None, + }, + fee: to_from_per_mille_fee(fee_pml), + min_fee: None, + max_fee: None, + } +} + pub fn recipients_to_input( recipients: List, policy_id: PolicyId, @@ -93,6 +112,7 @@ pub fn payout_to_string(payout: Payout) -> String { ) } +// UNSAFE - this appears to have some side affects pub fn address_to_cred_hash(address: Address) -> String { when address.payment_credential is { VerificationKeyCredential(hash) -> string.from_bytearray(hash) diff --git a/CIP-0102/ref_impl/onchain-reference/validators/simple_listing.ak b/CIP-0102/ref_impl/onchain-reference/validators/simple_listing.ak index a06442639..fadd72dac 100644 --- a/CIP-0102/ref_impl/onchain-reference/validators/simple_listing.ak +++ b/CIP-0102/ref_impl/onchain-reference/validators/simple_listing.ak @@ -17,7 +17,8 @@ use onchain_reference/common.{ } use onchain_reference/listing_utils.{Payout} use onchain_reference/test_utils.{ - output_to_string, payout_to_string, placeholder_oref, recipients_to_input, + output_to_string, payout_to_string, placeholder_oref, placeholder_recipient, + recipients_to_input, } type ListingDatum { @@ -79,7 +80,7 @@ validator { ) // check that the payouts are represented in the outputs and that enough are - datum.price <= check_payouts(list.reverse(outputs), payouts) + datum.price <= check_payouts(outputs, payouts) } // There's not much to do here. An asset @@ -200,6 +201,33 @@ const mock_buyer: VerificationKeyHash = "some_other_fake_address_hash" test one_recipient_cip68_buy() { let redeemer = Buy + let lot = + Asset { + policy_id: mock_policy_id, + asset_name: bytearray.concat(user_tn_prefix, "fake_lot_tn"), + quantity: 1, + } + + // Price: 1000 Ada + let listing_datum = + ListingDatum { lot, seller: mock_owner, price: 1_000_000_000 } + + let royalty_recipients = + [placeholder_recipient(80, mock_owner)] + + let ctx = + ScriptContext { + transaction: get_base_transaction(listing_datum, royalty_recipients), + purpose: Spend(OutputReference { ..placeholder_oref(), output_index: 1 }), + } + + // 0 is the royalty input + spend(listing_datum, redeemer, ctx) +} + +test multi_recipient_cip68_buy() { + let redeemer = Buy + let lot = Asset { policy_id: mock_policy_id, @@ -213,16 +241,11 @@ test one_recipient_cip68_buy() { let royalty_recipients = [ - RoyaltyRecipient { - address: Address { - payment_credential: VerificationKeyCredential(mock_owner), - stake_credential: None, - }, - fee: 125, - // 8% - min_fee: None, - max_fee: None, - }, + placeholder_recipient(80, mock_owner), + placeholder_recipient(15, "other_mock_owner"), + placeholder_recipient(50, "other_other_mock_owner"), + placeholder_recipient(20, "yet_another_mock_owner"), + placeholder_recipient(200, "so_many_mock_owners"), ] let ctx = @@ -235,6 +258,40 @@ test one_recipient_cip68_buy() { spend(listing_datum, redeemer, ctx) } +test no_royalty_paid() fail { + let redeemer = Buy + + let lot = + Asset { + policy_id: mock_policy_id, + asset_name: bytearray.concat(user_tn_prefix, "fake_lot_tn"), + quantity: 1, + } + + // Price: 1000 Ada + let listing_datum = + ListingDatum { lot, seller: mock_owner, price: 1_000_000_000 } + + let royalty_recipients = + [placeholder_recipient(80, mock_owner)] + + // constructing tx with no outputs to recipients + let tx = + Transaction { + ..get_base_transaction(listing_datum, royalty_recipients), + outputs: get_base_transaction(listing_datum, []).outputs, + } + + let ctx = + ScriptContext { + transaction: tx, + purpose: Spend(OutputReference { ..placeholder_oref(), output_index: 1 }), + } + + // 0 is the royalty input + spend(listing_datum, redeemer, ctx) +} + fn get_base_transaction( listing_datum: ListingDatum, royalty_recipients: List, From 4fa9091d3c4761b6bf1fc33b771cf9998053fb1b Mon Sep 17 00:00:00 2001 From: SamDelaney Date: Wed, 26 Jun 2024 16:53:57 -0700 Subject: [PATCH 22/24] update TODO.md --- CIP-0102/ref_impl/TODO.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CIP-0102/ref_impl/TODO.md b/CIP-0102/ref_impl/TODO.md index 774343bd8..d11a97ddf 100644 --- a/CIP-0102/ref_impl/TODO.md +++ b/CIP-0102/ref_impl/TODO.md @@ -5,4 +5,5 @@ - [x] Always-Fails Validator - [x] Reducible Validator - [x] Reading a CIP 102 NFT’s royalties off chain -- [ ] Reading and validating against CIP 102 NFT royalties on chain \ No newline at end of file +- [x] Reading and validating against CIP 102 NFT royalties on chain +- [ ] Final Cleanup \ No newline at end of file From 657a2bafae80374f13e4a4278173aa25a2f3881a Mon Sep 17 00:00:00 2001 From: samdelaney Date: Fri, 28 Jun 2024 01:44:45 -0700 Subject: [PATCH 23/24] move reference implementation to new repo --- CIP-0102/README.md | 10 +- CIP-0102/ref_impl/Spec.md | 59 --- CIP-0102/ref_impl/TODO.md | 9 - .../ref_impl/offchain-reference/.env.example | 5 - .../ref_impl/offchain-reference/.gitignore | 2 - CIP-0102/ref_impl/offchain-reference/README | 56 --- .../ref_impl/offchain-reference/conversion.ts | 11 - .../ref_impl/offchain-reference/deno.json | 8 - .../ref_impl/offchain-reference/deno.lock | 116 ----- CIP-0102/ref_impl/offchain-reference/index.ts | 3 - .../ref_impl/offchain-reference/lib/mint.ts | 207 -------- .../ref_impl/offchain-reference/lib/read.ts | 96 ---- .../offchain-reference/lib/types/chain.ts | 130 ----- .../offchain-reference/lib/types/index.ts | 3 - .../offchain-reference/lib/types/royalties.ts | 112 ----- .../offchain-reference/lib/types/utility.ts | 32 -- .../offchain-reference/scripts/env.ts | 22 - .../scripts/generate-wallet.js | 15 - .../scripts/mock-frontend.ts | 116 ----- .../offchain-reference/scripts/print-utxos.ts | 16 - .../offchain-reference/utils/query.ts | 63 --- .../.github/workflows/tests.yml | 20 - .../ref_impl/onchain-reference/.gitignore | 6 - CIP-0102/ref_impl/onchain-reference/README.md | 55 --- .../ref_impl/onchain-reference/aiken.lock | 15 - .../ref_impl/onchain-reference/aiken.toml | 14 - .../lib/onchain-reference/common.ak | 174 ------- .../lib/onchain-reference/listing_utils.ak | 7 - .../lib/onchain-reference/test_utils.ak | 121 ----- .../ref_impl/onchain-reference/plutus.json | 86 ---- .../validators/always_fails.ak | 5 - .../onchain-reference/validators/minting.ak | 125 ----- .../validators/reducible_royalty.ak | 186 ------- .../validators/simple_listing.ak | 466 ------------------ 34 files changed, 6 insertions(+), 2365 deletions(-) delete mode 100644 CIP-0102/ref_impl/Spec.md delete mode 100644 CIP-0102/ref_impl/TODO.md delete mode 100644 CIP-0102/ref_impl/offchain-reference/.env.example delete mode 100644 CIP-0102/ref_impl/offchain-reference/.gitignore delete mode 100644 CIP-0102/ref_impl/offchain-reference/README delete mode 100644 CIP-0102/ref_impl/offchain-reference/conversion.ts delete mode 100644 CIP-0102/ref_impl/offchain-reference/deno.json delete mode 100644 CIP-0102/ref_impl/offchain-reference/deno.lock delete mode 100644 CIP-0102/ref_impl/offchain-reference/index.ts delete mode 100644 CIP-0102/ref_impl/offchain-reference/lib/mint.ts delete mode 100644 CIP-0102/ref_impl/offchain-reference/lib/read.ts delete mode 100644 CIP-0102/ref_impl/offchain-reference/lib/types/chain.ts delete mode 100644 CIP-0102/ref_impl/offchain-reference/lib/types/index.ts delete mode 100644 CIP-0102/ref_impl/offchain-reference/lib/types/royalties.ts delete mode 100644 CIP-0102/ref_impl/offchain-reference/lib/types/utility.ts delete mode 100644 CIP-0102/ref_impl/offchain-reference/scripts/env.ts delete mode 100644 CIP-0102/ref_impl/offchain-reference/scripts/generate-wallet.js delete mode 100644 CIP-0102/ref_impl/offchain-reference/scripts/mock-frontend.ts delete mode 100644 CIP-0102/ref_impl/offchain-reference/scripts/print-utxos.ts delete mode 100644 CIP-0102/ref_impl/offchain-reference/utils/query.ts delete mode 100644 CIP-0102/ref_impl/onchain-reference/.github/workflows/tests.yml delete mode 100644 CIP-0102/ref_impl/onchain-reference/.gitignore delete mode 100644 CIP-0102/ref_impl/onchain-reference/README.md delete mode 100644 CIP-0102/ref_impl/onchain-reference/aiken.lock delete mode 100644 CIP-0102/ref_impl/onchain-reference/aiken.toml delete mode 100644 CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak delete mode 100644 CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/listing_utils.ak delete mode 100644 CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/test_utils.ak delete mode 100644 CIP-0102/ref_impl/onchain-reference/plutus.json delete mode 100644 CIP-0102/ref_impl/onchain-reference/validators/always_fails.ak delete mode 100644 CIP-0102/ref_impl/onchain-reference/validators/minting.ak delete mode 100644 CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak delete mode 100644 CIP-0102/ref_impl/onchain-reference/validators/simple_listing.ak diff --git a/CIP-0102/README.md b/CIP-0102/README.md index 5475373ae..5a2b85865 100644 --- a/CIP-0102/README.md +++ b/CIP-0102/README.md @@ -115,6 +115,8 @@ extra = ### Examples +In-code examples can be found in the [reference implementation](https://github.com/SamDelaney/CIP_102_Reference). + #### Retrieve metadata as 3rd party A third party has the following NFT `d5e6bf0500378d4f0da4e8dde6becec7621cd8cbf5cbb9b87013d4cc.(222)TestToken` and they want to look up the royalties. The steps are @@ -162,14 +164,14 @@ See the [CIP-0068 Extension Boilerplate](https://github.com/cardano-foundation/C ### Acceptance Criteria -- [ ] This CIP should receive feedback, criticism, and refinement from: CIP Editors and the community of people involved with NFT projects to review any weaknesses or areas of improvement. -- [ ] Guidelines and examples of publication of data as well as discovery and validation should be included as part of of criteria for acceptance. -- [ ] Minimal reference implementation making use of [Lucid](https://github.com/spacebudz/lucid) (off-chain), [PlutusTx](https://github.com/input-output-hk/plutus) (on-chain): [Reference Implementation](./ref_impl/). +- [x] This CIP should receive feedback, criticism, and refinement from: CIP Editors and the community of people involved with NFT projects to review any weaknesses or areas of improvement. +- [x] Guidelines and examples of publication of data as well as discovery and validation should be included as part of of criteria for acceptance. +- [x] Minimal reference implementation making use of [Lucid](https://github.com/spacebudz/lucid) (off-chain), [PlutusTx](https://github.com/input-output-hk/plutus) (on-chain): [Reference Implementation](https://github.com/SamDelaney/CIP_102_Reference). - [ ] Implementation and use demonstrated by the community: NFT Projects, Blockchain Explorers, Wallets, Marketplaces. ### Implementation Plan -- [ ] Publish open source reference implementation and instructions related to the creation, storage and reading of royalty utxos. +- [x] Publish open source reference implementation and instructions related to the creation, storage and reading of royalty utxos. - [ ] Implement in open source libraries and tooling such as [Lucid](https://github.com/spacebudz/lucid), [Blockfrost](https://github.com/blockfrost/blockfrost-backend-ryo), etc. - [ ] Achieve additional "buy in" from existing community actors and implementors such as: blockchain explorers, token marketplaces, minting platforms, wallets. diff --git a/CIP-0102/ref_impl/Spec.md b/CIP-0102/ref_impl/Spec.md deleted file mode 100644 index 1d0b8c812..000000000 --- a/CIP-0102/ref_impl/Spec.md +++ /dev/null @@ -1,59 +0,0 @@ -# Minting a CIP 102 compliant NFT with royalties - -## Timelocked Minting Policy -- Checks - - Owner has signed tx - - Tx validity window is before MP deadline - -## Validators - -### Always Fails -- Checks nothing -- Returns false or throws error - -### Reducible -- Checks - - tx is signed by recipient - - amount sent to recipient is < previous amount - - all other recipients remain the same - - amount - - address - -## Offchain -- Constructs well formed CIP-68 & CIP-102 Datums -- Sends reference NFT & datum to arbitrary address -- Sends royalty NFT & datum to - - arbitrary address - - always-fails script - - reducible validator - -# Reading a CIP 102 NFT’s royalties off chain - -- Querying the datum - - Check if datum is required - - Request datum - - Fail depending on whether datum is required -- Parsing the datum - - -# Reading and validating against CIP 102 NFT royalties on chain - -A simple listing contract which locks a CIP 102 NFT & a datum with a price & seller and must pay out royalties according to that price to be withdrawn. - -## Onchain - -- Royalty datum - - If empty, check if royalty is required - - Parse reference datum - - Fail if royalty field > 1 - - If not, check if royalties are being paid correctly - - Fail if not -- Listing datum - - Confirm the amount is getting paid to the seller, minus royalties - -## Offchain - -- Find & read the requested listing datum -- Find & read the associated royalty datum using the approach above -- Calculate payments -- Construct & Submit tx \ No newline at end of file diff --git a/CIP-0102/ref_impl/TODO.md b/CIP-0102/ref_impl/TODO.md deleted file mode 100644 index d11a97ddf..000000000 --- a/CIP-0102/ref_impl/TODO.md +++ /dev/null @@ -1,9 +0,0 @@ -- [x] Create Specification -- [x] Select Onchain Language -- [x] Minting a CIP 102 compliant NFT with royalties - - [x] Timelocked MP - - [x] Always-Fails Validator - - [x] Reducible Validator -- [x] Reading a CIP 102 NFT’s royalties off chain -- [x] Reading and validating against CIP 102 NFT royalties on chain -- [ ] Final Cleanup \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/.env.example b/CIP-0102/ref_impl/offchain-reference/.env.example deleted file mode 100644 index cbf186adc..000000000 --- a/CIP-0102/ref_impl/offchain-reference/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -PUBLIC_CARDANO_NETWORK=Preprod -BLOCKFROST_URL=https://cardano-preprod.blockfrost.io/api/v0 -BLOCKFROST_PROJECT_KEY=PLACEHOLDER -WALLET_ADDRESS=PLACEHOLDER -WALLET_PRIVATE_KEY=PLACEHOLDER \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/.gitignore b/CIP-0102/ref_impl/offchain-reference/.gitignore deleted file mode 100644 index c2111fc33..000000000 --- a/CIP-0102/ref_impl/offchain-reference/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.env -.vscode \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/README b/CIP-0102/ref_impl/offchain-reference/README deleted file mode 100644 index c05ea744f..000000000 --- a/CIP-0102/ref_impl/offchain-reference/README +++ /dev/null @@ -1,56 +0,0 @@ -# Using the CIP-102 Offchain Library - -## Importing -The contents of this library are available to be imported from `index.ts` and will eventually be published to a public package manager (or two). I will add instructions here when I do. - -In the meantime, you can clone or copy the contents of the `/lib/` folder wherever you need it. - -## Direct Use - -If you want to interact with the library directly without setting up your own project, you can run the code directly with the mock frontend defined in `scripts/mock-frontend.ts` - -### Setup - -- Make sure you have [Deno](https://deno.com/) installed. - -- Create a Blockfrost project if you don't already have one. These are free up to a certain number of queries. - -- Create a `.env` file in the parent directory with the structure defined in `.env.example`: - - Fill in the `PUBLIC_CARDANO_NETWORK` variable with "Preview", "Preprod" or "Mainnet" depending on which network you want to use. - - Fill in the corresponding `BLOCKFROST_URL`. - - Fill in the - -- Set up your wallet by filling in the `WALLET_ADDRESS` and `WALLET_PRIVATE_KEY` variables. - - I recommend using `deno task generate-wallet` to generate a new wallet for testing this specifically. Once generated you can send the minimal funds you need for testing to the wallet from your hot wallet or from a testnet faucet. - -To verify your setup worked, or if you want to check the contents of the wallet you have connected, I've included a handy `deno task print-utxos` script. - -You should see something like this: -``` -addr_test1vqhjcudw5m5pmehwtwduts2ayz2rlpm7vjq0ql6exsz6czq2gr7h8 -[ - { - txHash: "6b369bac955857261566812acde749t9d438032ac6633a004cceeb2c4dc5b287", - outputIndex: 7, - assets: { lovelace: 9980876490n }, - address: "addr_test1vqhjcudw5m5pmehwtwduts2ayz2rlpm7vjq0ql6exsz6czq2gr7h8", - datumHash: undefined, - datum: undefined, - scriptRef: undefined - } -] -``` - -### Reading Royalties -You can query the royalty information for a given collection with the following query: - -`deno task get-royalties [policyId]` - -This does not return CIP-27 royalties. - -### Minting CIP-102 Compliant NFTs -You can mint a collection with multiple CIP-68 nfts & a royalty policy with - -`deno task mint-collection` - -Instead of using the command line, this takes its parameters from the `mock_` prefixed consts at the start of the `testTimelockedMint` function in `mock-frontend.ts`. \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/conversion.ts b/CIP-0102/ref_impl/offchain-reference/conversion.ts deleted file mode 100644 index 4a46e86b5..000000000 --- a/CIP-0102/ref_impl/offchain-reference/conversion.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const CIP102_ROYALTY_TOKEN_NAME = '001f4d70526f79616c7479'; // (500)Royalty - -// convert from percentage to onchain royalty value -export function toOnchainRoyalty(percentage: number): number { - return Math.floor(1 / (percentage / 1000)); -} - -// convert from onchain royalty value to percentage -export function fromOnchainRoyalty(value: number): number { - return Math.trunc((10 / value) * 1000) / 10; -} \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/deno.json b/CIP-0102/ref_impl/offchain-reference/deno.json deleted file mode 100644 index a60f127fb..000000000 --- a/CIP-0102/ref_impl/offchain-reference/deno.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "tasks": { - "generate-wallet": "deno run --allow-net --allow-read --allow-env --allow-write scripts/generate-wallet.js", - "print-utxos": "deno run --allow-net --allow-read --allow-env --allow-write scripts/print-utxos.ts", - "mint-collection": "deno run --allow-net --allow-read --allow-env --allow-write scripts/mock-frontend.ts mint-collection", - "get-royalties": "deno run --allow-net --allow-read --allow-env --allow-write scripts/mock-frontend.ts get-royalties" - } -} diff --git a/CIP-0102/ref_impl/offchain-reference/deno.lock b/CIP-0102/ref_impl/offchain-reference/deno.lock deleted file mode 100644 index 0ac40fafb..000000000 --- a/CIP-0102/ref_impl/offchain-reference/deno.lock +++ /dev/null @@ -1,116 +0,0 @@ -{ - "version": "3", - "remote": { - "https://deno.land/std@0.100.0/encoding/hex.ts": "f952e0727bddb3b2fd2e6889d104eacbd62e92091f540ebd6459317a61932d9b", - "https://deno.land/std@0.110.0/_util/assert.ts": "2f868145a042a11d5ad0a3c748dcf580add8a0dbc0e876eaa0026303a5488f58", - "https://deno.land/std@0.110.0/_util/os.ts": "dfb186cc4e968c770ab6cc3288bd65f4871be03b93beecae57d657232ecffcac", - "https://deno.land/std@0.110.0/async/deadline.ts": "1d6ac7aeaee22f75eb86e4e105d6161118aad7b41ae2dd14f4cfd3bf97472b93", - "https://deno.land/std@0.110.0/async/debounce.ts": "b2f693e4baa16b62793fd618de6c003b63228db50ecfe3bd51fc5f6dc0bc264b", - "https://deno.land/std@0.110.0/async/deferred.ts": "ab60d46ba561abb3b13c0c8085d05797a384b9f182935f051dc67136817acdee", - "https://deno.land/std@0.110.0/async/delay.ts": "db68b7c22518ea9805be110cdc914017d741894d2bececf4d78607fd2f0548e7", - "https://deno.land/std@0.110.0/async/mod.ts": "78425176fabea7bd1046ce3819fd69ce40da85c83e0f174d17e8e224a91f7d10", - "https://deno.land/std@0.110.0/async/mux_async_iterator.ts": "62abff3af9ff619e8f2adc96fc70d4ca020fa48a50c23c13f12d02ed2b760dbe", - "https://deno.land/std@0.110.0/async/pool.ts": "353ce4f91865da203a097aa6f33de8966340c91b6f4a055611c8c5d534afd12f", - "https://deno.land/std@0.110.0/async/tee.ts": "63811ea47268825db2b15e973dc5c37bab37b749ffa00d2b7bbb6c6f568412cb", - "https://deno.land/std@0.110.0/bytes/mod.ts": "440684e07e8f57a19a43b34d57eb63af0b36fc92b6657b6dcdbf9d5612d62e29", - "https://deno.land/std@0.110.0/encoding/base64.ts": "eecae390f1f1d1cae6f6c6d732ede5276bf4b9cd29b1d281678c054dc5cc009e", - "https://deno.land/std@0.110.0/encoding/hex.ts": "5bc7df19af498c315cdaba69e2fce1b2aef5fc57344e8c21c08991aa8505a260", - "https://deno.land/std@0.110.0/fmt/colors.ts": "8368ddf2d48dfe413ffd04cdbb7ae6a1009cf0dccc9c7ff1d76259d9c61a0621", - "https://deno.land/std@0.110.0/fs/exists.ts": "b0d2e31654819cc2a8d37df45d6b14686c0cc1d802e9ff09e902a63e98b85a00", - "https://deno.land/std@0.110.0/io/buffer.ts": "3ead6bb11276ebcf093c403f74f67fd2205a515dbbb9061862c468ca56f37cd8", - "https://deno.land/std@0.110.0/io/util.ts": "85c33d61b20fd706acc094fe80d4c8ae618b04abcf3a96ca2b47071842c1c8ac", - "https://deno.land/std@0.110.0/node/_errors.ts": "74d1e7c7aad0f4a04df20be1f25f8a0a1d39483a75daabefa2cb285b0090e6e5", - "https://deno.land/std@0.110.0/node/_fs/_fs_access.ts": "7cbbfd1309e47983065dc7f186fd128eea0854c6693d58b39be2fd0f4de8d0c0", - "https://deno.land/std@0.110.0/node/_fs/_fs_appendFile.ts": "bd802989a76d2e87a92034cbbbf6ad3154bf925b68e1a0a43772b3f4fc2a3728", - "https://deno.land/std@0.110.0/node/_fs/_fs_chmod.ts": "101ea1cfc63b518f7434357e6e559c163b86c836826e87273ad4fbdd17153c6d", - "https://deno.land/std@0.110.0/node/_fs/_fs_chown.ts": "d314d0b3c33422c980e8b8d2abe95433d5d3ec6e0699b53de6e03c13bb37769e", - "https://deno.land/std@0.110.0/node/_fs/_fs_close.ts": "6a175fef187fa06ad7fd3c82a7a44410f947f5d20ec6673f871d4d327d98753e", - "https://deno.land/std@0.110.0/node/_fs/_fs_common.ts": "45f0b63e864d24c6c6c492a931e6b794f61ed125157e8f74282350557ec7e971", - "https://deno.land/std@0.110.0/node/_fs/_fs_constants.ts": "20e2d62e5c5bc1f94b68c14d686c0f61a683d405f31523456b863bb525c9cdee", - "https://deno.land/std@0.110.0/node/_fs/_fs_copy.ts": "ce349beecec9f833e8cbe02eaa6c79119cbdbe788093c2b9543ce4d865f2b941", - "https://deno.land/std@0.110.0/node/_fs/_fs_dir.ts": "b122324330d4d754a595618986039a2841f9551c16a20be21e93f371cc8e65ce", - "https://deno.land/std@0.110.0/node/_fs/_fs_dirent.ts": "c9f585db140749426bcbcd735801198e7c517d654cd72795a20dfa349e4bac05", - "https://deno.land/std@0.110.0/node/_fs/_fs_exists.ts": "ec185609b68149e259d4fb1d7872e70bb2f9aff135236a3c2e14109e579393cc", - "https://deno.land/std@0.110.0/node/_fs/_fs_fdatasync.ts": "a62e70004a0f9c2f49e369b44a9ff788bb13d7b8160c61cec942e5dc42da4181", - "https://deno.land/std@0.110.0/node/_fs/_fs_fstat.ts": "6e2280db419a521dd3ea4525982c02f84677493561d1bcbaa271abadc8e7f844", - "https://deno.land/std@0.110.0/node/_fs/_fs_fsync.ts": "c68c74966342d5a98437036a634359ac6feab85f114310892f14553c02fad5d4", - "https://deno.land/std@0.110.0/node/_fs/_fs_ftruncate.ts": "2964fe380c60db9e802a9ae65c72cbfee8d710abd0e078afbcfc2e0cc442599c", - "https://deno.land/std@0.110.0/node/_fs/_fs_futimes.ts": "88c1e0d7b4012bcee23f586b2b7282ac21725824f3961ffa235d96501afcb65f", - "https://deno.land/std@0.110.0/node/_fs/_fs_link.ts": "93c8997574a6f42aa2699012236b53f7ee2105400c207e0b8156dce04772ab81", - "https://deno.land/std@0.110.0/node/_fs/_fs_lstat.ts": "3d81117febd0abcab4e92ecbe70885179f62d10d09608d8ac6acfb7c2db34290", - "https://deno.land/std@0.110.0/node/_fs/_fs_mkdir.ts": "a98590e4a07416e4d7af1342d062b9c8e01997e6f047ed95411ef1215aa154b3", - "https://deno.land/std@0.110.0/node/_fs/_fs_mkdtemp.ts": "683c0e0fbe2583c318b2d67cd986cf55f8f7410199537edb76f3af0ec8b54a80", - "https://deno.land/std@0.110.0/node/_fs/_fs_open.ts": "6a08a3e5edf17f49589fa34223d2ea62193ea1e17392f4e84bfa49fc9d0ca532", - "https://deno.land/std@0.110.0/node/_fs/_fs_readFile.ts": "aa972cc3a6a13ddad8e7319c3908d089be56136ef11e2a8b8bfb48cfc368fde3", - "https://deno.land/std@0.110.0/node/_fs/_fs_readdir.ts": "d3b0c2b3d3b243e63f0c8df5294252edbb6a732c07f540044af3e89d6d75245f", - "https://deno.land/std@0.110.0/node/_fs/_fs_readlink.ts": "919df3481f3e339cfb153e899941fe93d4284287794fed3d92f071a055d473e9", - "https://deno.land/std@0.110.0/node/_fs/_fs_realpath.ts": "9dad0fe43e8c4cda6953bc297cd605c9647162e4a9d29cf91e435d96a741d8e1", - "https://deno.land/std@0.110.0/node/_fs/_fs_rename.ts": "98852b8f576e2bbdaede8b85abf8c459c3d0b50d8f6c8aff030a72f20ed26710", - "https://deno.land/std@0.110.0/node/_fs/_fs_rmdir.ts": "89458f749b97bf2e23e485cba09fbb18f57041141e1cbbcfa9e44bde75953e51", - "https://deno.land/std@0.110.0/node/_fs/_fs_stat.ts": "71c770228b0f6eb0bfa8b5db88e0313940f57fa3e5dccfbf46d8bd838d1a9497", - "https://deno.land/std@0.110.0/node/_fs/_fs_symlink.ts": "174e41dbb2dbd0af79ba6460f3ab849c48099b8e824beed7c7f7c5d6d616b36d", - "https://deno.land/std@0.110.0/node/_fs/_fs_truncate.ts": "fd016bc0b5ac236c4e88b326d0a9de0df6d56a512f20e927384fb4bd088d4e01", - "https://deno.land/std@0.110.0/node/_fs/_fs_unlink.ts": "95cb6d81c647ac46826a86b9ebc8ebd8008fd99080dd36ddb62c5ddc54339c5f", - "https://deno.land/std@0.110.0/node/_fs/_fs_utimes.ts": "5a87c6cb903f072a10a1917195e55f8673f4e1023964e16648e4146a778160e0", - "https://deno.land/std@0.110.0/node/_fs/_fs_watch.ts": "7f7757775b6cdeb2e66bf5dbb5e1eb15e105fce4429eecd1026e1301e696babe", - "https://deno.land/std@0.110.0/node/_fs/_fs_writeFile.ts": "6bf2646358e6e840353f1c1da184493121a1e57ab20e6ba6e8b1cf21574d4bf2", - "https://deno.land/std@0.110.0/node/_util/_util_callbackify.ts": "947aa66d148c10e484c5c5e65ca4041cdd65085d7045fb26d388269a63c4d079", - "https://deno.land/std@0.110.0/node/_util/_util_promisify.ts": "2ad6efe685f73443d5ed6ae009999789a8de4a0f01e6d2afdf242b4515477ee2", - "https://deno.land/std@0.110.0/node/_util/_util_types.ts": "ae3d21e07c975f06590ab80bbde8173670d70ff40546267c0c1df869fc2ff00c", - "https://deno.land/std@0.110.0/node/_utils.ts": "c32d3491e380488728d65ad471698ed0aadff7fe35bde0a26ba4dd8f434ed0e7", - "https://deno.land/std@0.110.0/node/buffer.ts": "29e7a8849479c9d325a1be899fd79d1289a858afe211ab5a0c78e57f3493dfea", - "https://deno.land/std@0.110.0/node/events.ts": "f92ba300cb0a6efa4ff4d8a267a507ee725858d6a250a2eeb829ec99b21f90b1", - "https://deno.land/std@0.110.0/node/fs.ts": "0f283d57d7d19b48845d80d9517c1a69c9b66cf29787ad1e182939e95c6a9c07", - "https://deno.land/std@0.110.0/node/fs/promises.ts": "88258c223ef5774e8467e1b7707be8ebf490f62b024cb56cf2b4aae29a81a6dd", - "https://deno.land/std@0.110.0/node/path.ts": "86b262d6957fba13d4f3d58a92ced49de4f40169d06542326b5547ff97257f0d", - "https://deno.land/std@0.110.0/node/util.ts": "23878bd3ee67a52e67cfe5acb78c7ccce9c54735c6d280b069577605e8679935", - "https://deno.land/std@0.110.0/path/_constants.ts": "1247fee4a79b70c89f23499691ef169b41b6ccf01887a0abd131009c5581b853", - "https://deno.land/std@0.110.0/path/_interface.ts": "1fa73b02aaa24867e481a48492b44f2598cd9dfa513c7b34001437007d3642e4", - "https://deno.land/std@0.110.0/path/_util.ts": "2e06a3b9e79beaf62687196bd4b60a4c391d862cfa007a20fc3a39f778ba073b", - "https://deno.land/std@0.110.0/path/common.ts": "f41a38a0719a1e85aa11c6ba3bea5e37c15dd009d705bd8873f94c833568cbc4", - "https://deno.land/std@0.110.0/path/glob.ts": "46708a3249cb5dc4a116cae3055114d6339bd5f0c1f412db6a4e0cb44c828a7d", - "https://deno.land/std@0.110.0/path/mod.ts": "4465dc494f271b02569edbb4a18d727063b5dbd6ed84283ff906260970a15d12", - "https://deno.land/std@0.110.0/path/posix.ts": "34349174b9cd121625a2810837a82dd8b986bbaaad5ade690d1de75bbb4555b2", - "https://deno.land/std@0.110.0/path/separator.ts": "8fdcf289b1b76fd726a508f57d3370ca029ae6976fcde5044007f062e643ff1c", - "https://deno.land/std@0.110.0/path/win32.ts": "2edb2f71f10578ee1168de01a8cbd3c65483e45a46bc2fa3156a0c6bfbd2720d", - "https://deno.land/std@0.110.0/testing/_diff.ts": "ccd6c3af6e44c74bf1591acb1361995f5f50df64323a6e7fb3f16c8ea792c940", - "https://deno.land/std@0.110.0/testing/asserts.ts": "6b0d6ba564bdff807bd0f0e93e02c48aa3177acf19416bf84a7f420191ef74cd", - "https://deno.land/std@0.148.0/bytes/equals.ts": "3c3558c3ae85526f84510aa2b48ab2ad7bdd899e2e0f5b7a8ffc85acb3a6043a", - "https://deno.land/std@0.148.0/bytes/mod.ts": "763f97d33051cc3f28af1a688dfe2830841192a9fea0cbaa55f927b49d49d0bf", - "https://deno.land/std@0.153.0/hash/sha256.ts": "aa9479c260f41b72c639f36c3e4bc9319940b5d2e52fe793ebe3dc646d12832f", - "https://deno.land/std@0.208.0/dotenv/mod.ts": "039468f5c87d39b69d7ca6c3d68ebca82f206ec0ff5e011d48205eea292ea5a6", - "https://deno.land/x/lucid@0.10.7/mod.ts": "9473507398048cb24dbb37b3a220777106c69c28d573898648deb2bb84a7e131", - "https://deno.land/x/lucid@0.10.7/package.json": "402ef25f1bcbf29c57b24f4737637db8470dc9197e80478455ec132a7a08a0cc", - "https://deno.land/x/lucid@0.10.7/src/core/core.ts": "5b2f6d4746933a425bfcb134a55b84c946bc2ddd98b795aa9189f345778b113c", - "https://deno.land/x/lucid@0.10.7/src/core/libs/cardano_message_signing/cardano_message_signing.generated.js": "b3cae6f286d855a1cf9c84987026bfa46f577e9edd6b269127e88c0f41d68bfa", - "https://deno.land/x/lucid@0.10.7/src/core/libs/cardano_multiplatform_lib/cardano_multiplatform_lib.generated.js": "286312109897e46032173ab3a0a233212ea0741051bcba936914ac7af8f22227", - "https://deno.land/x/lucid@0.10.7/src/core/mod.ts": "978b94101791fdad3113e5a9fa3813b1f48a8e505f68da14bcad72668b294926", - "https://deno.land/x/lucid@0.10.7/src/lucid/lucid.ts": "80f06b1965074a6c3504d509df676f2508d49cc0a8050dee1b1f8ea804fb565a", - "https://deno.land/x/lucid@0.10.7/src/lucid/message.ts": "b16bb7c06cbc81c777ac116d95db32959af42bc093ebe86b65f2dbc28b75f9b2", - "https://deno.land/x/lucid@0.10.7/src/lucid/mod.ts": "94bfe48ee683e932a67347786a1253a2e7277e53b1dc192d045d70bc5ce9bc10", - "https://deno.land/x/lucid@0.10.7/src/lucid/tx.ts": "5833f0cb10bcd83439e7caff03c20296b8e646ce1d3797e9db56f3fdcfe33e19", - "https://deno.land/x/lucid@0.10.7/src/lucid/tx_complete.ts": "a687e19df52072fe239af33081cfddf2fba40011324ff86f3352042940a65362", - "https://deno.land/x/lucid@0.10.7/src/lucid/tx_signed.ts": "58edce3295e5fa14977120d887b43796ebd40893caaf012d441a8b56909c8ae1", - "https://deno.land/x/lucid@0.10.7/src/misc/bip39.ts": "7dc0f49f96d43a254a048a956cac31ef8c8045f69f3b8dbc5350bc6c601441eb", - "https://deno.land/x/lucid@0.10.7/src/misc/crc8.ts": "c9abda52851d5c575f6a8714c3600d2e63bb8cfc942e035486b1ab03b780e2f9", - "https://deno.land/x/lucid@0.10.7/src/misc/sign_data.ts": "6f5066d19455ade77615b5cbbf5dd9947ea691a7b764f96c81ba911cc84183e5", - "https://deno.land/x/lucid@0.10.7/src/misc/wallet.ts": "7a8d63c234f0741dc49d3134554b7c455e24f4692ba42a3843aad7e37bcc7d2b", - "https://deno.land/x/lucid@0.10.7/src/mod.ts": "f4156883dc0dc394e9fd9e9e7222537a61ff8b2781a88761d7bcb65123a16bcf", - "https://deno.land/x/lucid@0.10.7/src/plutus/data.ts": "b6eb205124e6f340f47dd789d94d91146b2bf038c6a76b57ede12874b60d5920", - "https://deno.land/x/lucid@0.10.7/src/plutus/mod.ts": "3a7f784e950348d447cd6bcb27546b20902fae167e04fd42e67bfad57216e1a7", - "https://deno.land/x/lucid@0.10.7/src/plutus/time.ts": "f142f03f897a6e57625e8c0752bb73397818090871f3cff123ec64311590b6f9", - "https://deno.land/x/lucid@0.10.7/src/provider/blockfrost.ts": "7cd49ed72464f3faf3c4bc93b54a1c30dbe0f34035b3c5e844adfdeb3ca74e32", - "https://deno.land/x/lucid@0.10.7/src/provider/emulator.ts": "066892a5dfbe27bde86d449650913da18da8025bd618c7cfed0a6fc16e07bb29", - "https://deno.land/x/lucid@0.10.7/src/provider/kupmios.ts": "e15178e252d5a87b6c6123e51c3dc426ec7ebe02bac920dea26213f62eabcbd0", - "https://deno.land/x/lucid@0.10.7/src/provider/maestro.ts": "76c379f946bdc2198ccf6898ce1b09ed5a6ddb1d858f0abe2b39e5300aa7a6c1", - "https://deno.land/x/lucid@0.10.7/src/provider/mod.ts": "76b36094551556045e851fe0ba6012626e1a08c03dec62a21b69d22311266379", - "https://deno.land/x/lucid@0.10.7/src/types/global.ts": "3ea23ebcf9af819a01cbcf682856df2b178929ffec362113d24eb4a80a966ac3", - "https://deno.land/x/lucid@0.10.7/src/types/mod.ts": "2e6e4ffd7077025d1b45af726756455fc9c915666dd7f23a041be058039b49c6", - "https://deno.land/x/lucid@0.10.7/src/types/types.ts": "b637a944c04576c42c2bb5df98aa8128cd4677e04fa0ec911e042ce96e764fa2", - "https://deno.land/x/lucid@0.10.7/src/utils/cost_model.ts": "f04e4d393062b0057f703e37ddcb1aa9fe378b589134c679688f9786235a25ee", - "https://deno.land/x/lucid@0.10.7/src/utils/merkle_tree.ts": "af39a9167eb8b083a19a980916c95ab40332e959ef20bc43fdfef69eef08e594", - "https://deno.land/x/lucid@0.10.7/src/utils/mod.ts": "7e405bfa0db96d9e995e67fb77251e8465843addf141caf4ce64b3efa6077822", - "https://deno.land/x/lucid@0.10.7/src/utils/utils.ts": "177ec0578b788f947f6afb6cc911c4eae776aaf98b5249412a750b8f366b4d11", - "https://deno.land/x/typebox@0.25.13/src/typebox.ts": "9b20b62c0bf31f1a9128b6dc6dfd470a5d956a48f0b0ef0a1ccc4d794fb55dcc" - } -} diff --git a/CIP-0102/ref_impl/offchain-reference/index.ts b/CIP-0102/ref_impl/offchain-reference/index.ts deleted file mode 100644 index ce8cabfc8..000000000 --- a/CIP-0102/ref_impl/offchain-reference/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./lib/mint.ts" -export * from "./lib/read.ts" -export * from "./lib/types.ts" \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/lib/mint.ts b/CIP-0102/ref_impl/offchain-reference/lib/mint.ts deleted file mode 100644 index 50d7dc806..000000000 --- a/CIP-0102/ref_impl/offchain-reference/lib/mint.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { - Assets, - Constr, - Data, - Lucid, - Script, - Tx, - applyParamsToScript, - fromText, - toUnit -} from "https://deno.land/x/lucid@0.10.7/mod.ts"; - -import { - toCip102RoyaltyDatum, - Royalty, - RoyaltyFlag -} from './types/royalties.ts' - -import { - NFTMetadata, - NFTDatumMetadata -} from './types/chain.ts' - -import { - MediaAssets, - TxBuild -} from './types/utility.ts' - -/** - * Included in this file: - * - createRoyalty() - creates a royalty token with no other assets - * - mintNFTs() - mints CIP-68 nfts, may include a royalty token in the transaction as well - * - createTimelockedMP() - utility to create a parameterized minting policy - */ - -/** - * Create a royalty token with no other assets - * @param lucid a lucid instance with a wallet already selected - * @param policy the minting policy to mint off of - * @param validator the validator to lock reference & royalty datums ator - * @param royalty the royalty metadata - * @returns a TxComplete object, ready to sign & submit - */ -export async function createRoyalty(lucid: Lucid, policy: Script, validator: Script, royalty: Royalty) { - const royaltyDatum = toCip102RoyaltyDatum([royalty]) - - // generous tx validity window - const validTo = new Date(); - validTo.setHours(validTo.getHours() + 1); - - // user wallet info - const utxos = await lucid.wallet.getUtxos(); - const walletAddress = await lucid.wallet.address(); - - // the royalty token - const policyId = lucid.utils.mintingPolicyToId(policy); - const nftUnit = toUnit(policyId, fromText("Royalty"), 500); - - // the royalty validator address - const validatorAddress = lucid.utils.validatorToAddress(validator) - - const transaction = await lucid - .newTx() - .collectFrom(utxos) - .attachMintingPolicy(policy) - .mintAssets( - { [nftUnit]: BigInt(1) }, - Data.to(new Constr(0, [])) // Constr(1, []) to Burn - ) - .validTo(validTo.getTime()) - .addSigner(walletAddress) - .payToContract( - validatorAddress, - { inline: royaltyDatum }, - { [toUnit(policyId, fromText("Royalty"), 500)]: 1n } - ).complete() - - return transaction; - } - -/** - * Mints CIP-68 nfts as specified in the sanitizedMetadataAssets parameter. - * @param lucid a lucid instance with a wallet already selected - * @param policy the minting policy to mint off of - * @param validator the validator to lock reference & royalty datums at - * @param sanitizedMetadataAssets the asset specification to mint off of - * @param cip102 The cip102 parameter allows for 3 cases: - * - "NoRoyalty" - no royalty is attached to the collection - obviously not recommended here but hey, it's your collection - * - "Premade" - the royalty has already been minted to the policy - no need to mint new tokens, but include the flag to indicate a royalty token exists - * - Royalty - a new royalty token is minted to the policy in this transaction according to the information in the parameter - * @returns an object containing a TxComplete object & the policyId of the minted assets - */ - export async function mintNFTs( - lucid: Lucid, - policy: Script, - validator: Script, - sanitizedMetadataAssets: MediaAssets, - cip102?: "NoRoyalty" | "Premade" | Royalty -): Promise { - // extract royalty information or mark undefined - const royalty = cip102 === "NoRoyalty" || cip102 === "Premade" ? undefined : cip102; - - // get utxos from users wallet - const utxos = await lucid.wallet.getUtxos(); - if (!utxos || !utxos.length || !utxos[0]) { - return { error: 'empty-wallet' }; - } - const referenceUtxo = utxos[0]; - - // calculate the assets to mint - const tokenNames = Object.keys(sanitizedMetadataAssets); - console.log(policy) - const policyId = lucid.utils.mintingPolicyToId(policy); - const assets: Assets = {}; - if(royalty) - assets[toUnit(policyId, fromText("Royalty"), 500)] = 1n; - tokenNames.forEach((tokenName) => { - assets[toUnit(policyId, fromText(tokenName), 100)] = 1n; - assets[toUnit(policyId, fromText(tokenName), 222)] = 1n; - }); - - // addresses to send assets to - const validatorAddress = lucid.utils.validatorToAddress(validator) - const walletAddress = await lucid.wallet.address() - - // generous tx validity window - const validTo = new Date(); - validTo.setHours(validTo.getHours() + 1); - - let incompleteTx = await lucid - .newTx() - .collectFrom([...utxos, referenceUtxo]) - .attachMintingPolicy(policy) - .mintAssets(assets, Data.to(new Constr(0, []))) - .validTo(validTo.getTime()) - .addSigner(walletAddress) - - // send assets & datums to associated addresses - const attachDatums = (tx: Tx): Tx => { - if(royalty) { - // construct the royalty datum - const royaltyDatum = toCip102RoyaltyDatum([royalty]) - - // attach the royalty token & datum to the transaction - tx = tx.payToContract( - validatorAddress, - { inline: royaltyDatum }, - { [toUnit(policyId, fromText("Royalty"), 500)]: 1n } - ) - } - - // attach the royalty flag to the reference tokens if a royalty policy exists - const has102Royalty = cip102 === "Premade" || royalty !== undefined; - const extra = has102Royalty - ? Data.to({ royalty_included: BigInt(1) }, RoyaltyFlag) - : Data.from(Data.void()); - - for(const tokenName of tokenNames) { - // construct the reference datum - const metadataDatum = Data.to({ - metadata: Data.castFrom(Data.fromJson(sanitizedMetadataAssets[tokenName]), NFTMetadata), - version: BigInt(1), - extra - }, NFTDatumMetadata) - - // attach the reference token & datum to the transaction - tx = tx.payToContract( - validatorAddress, - { inline: metadataDatum }, - { [toUnit(policyId, fromText(tokenName), 100)]: BigInt(1) } - ) - } - - return tx; - } - - incompleteTx = await attachDatums(incompleteTx) - - const tx = await incompleteTx.complete() - - return { tx, policyId }; -} - -/** - * parameterize the timelocked minting policy with your wallet address & minting deadline - * @param lucid a lucid instance with a wallet already selected - * @param timestamp the minting deadline - * @param walletAddress the wallet address allowed to mint - * @returns the minting policy in a Script object - */ -export function createTimelockedMP( - lucid: Lucid, - mp: string, - timestamp: number, - walletAddress: string -) { - const paymentHash = lucid.utils.getAddressDetails(walletAddress).paymentCredential?.hash; - - if(!paymentHash) - throw new Error("Payment hash not found") - - const policy: Script = { - type: "PlutusV2", - script: applyParamsToScript(mp, [BigInt(timestamp), paymentHash]), - } - return policy; -} \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/lib/read.ts b/CIP-0102/ref_impl/offchain-reference/lib/read.ts deleted file mode 100644 index 5b0efa7fb..000000000 --- a/CIP-0102/ref_impl/offchain-reference/lib/read.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type { Royalty, RoyaltyConstr } from './types/royalties.ts'; -import type { output } from "./types/chain.ts"; -import { Data, toText } from 'https://deno.land/x/lucid@0.10.7/mod.ts'; -import { getAssetsTransactions, getScriptsDatumCbor, getTxsUtxos } from "../utils/query.ts"; -import { CIP102_ROYALTY_TOKEN_NAME, fromOnchainRoyalty } from '../conversion.ts'; - -/** - * Queries the blockchain for the royalty metadata of a CIP-102 collection - * @param policyId the policy id of the collection - * @returns the royalty metadata in the form of an array of royalty recipients - */ -export async function getRoyaltyPolicy( - policyId: string -): Promise { - const asset_unit = policyId + CIP102_ROYALTY_TOKEN_NAME; - try { - // get the last tx of the royalty token - const lastTx = await getAssetsTransactions( - asset_unit, - 1, - 1, - 'desc' - ); - const lastTxHash = lastTx?.at(0)?.tx_hash; - if (lastTxHash) { - // get the utxos of the last tx - const txUtxos = await getTxsUtxos(lastTxHash); - - // parse the utxos to find the royalty metadata - const royaltyData = await Promise.all( - txUtxos.outputs.map(async (output: output) => - await getRoyaltyMetadata(output) - )); - - // return the royalty metadata in the form of an array of royalty recipients with the fee converted to percentage - return royaltyData.flat().map((royaltyData) => { - return { ...royaltyData, fee: fromOnchainRoyalty(royaltyData.fee) } - }); - } - } catch (_error) { - // Best effort just silently fail - } - return []; -} - -/** - * Checks for royalty metadata in a given utxo - * @param out the utxo to examine - * @returns an array of royalty recipients if the utxo contains royalty metadata, [] if not - */ -async function getRoyaltyMetadata(out: output): Promise { - let datum; - // get the datum of the utxo - if(!out.inline_datum && out.data_hash) - datum = out.inline_datum ?? await getScriptsDatumCbor(out.data_hash).then(dat => dat.cbor) - else - datum = out.inline_datum - if (datum) { - try { - // try to parse the datum, return it as an array of royalty recipients if successful - const datumConstructor: RoyaltyConstr = Data.from(datum); - const datumData = datumConstructor.fields[0]; - if (Array.isArray(datumData)) { - const royalties = datumData - .map((royaltyMap) => decodeDatumMap(royaltyMap)) - .filter((royalty) => royalty); - return royalties as Royalty[]; - } - } catch (_error) { - // do nothing - } - } - return []; -} - -/** - * Parse a datum & check for royalty metadata - * @param data the datum to parse - * @returns a royalty recipient if the datum contains royalty metadata, undefined if not - */ -function decodeDatumMap( - data: Map -): Royalty | undefined { - const model: { [key: string]: string | number } = {}; - - // parse the datum to a javascript object - data.forEach((value, key) => { - model[toText(key)] = - typeof value === 'string' ? toText(value) : Number(value); - }); - - // check if the object contains the required fields. This could be more robust, but it's functional as is. - if ('address' in model && 'fee' in model) { - return model as unknown as Royalty; - } -} \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/lib/types/chain.ts b/CIP-0102/ref_impl/offchain-reference/lib/types/chain.ts deleted file mode 100644 index 1f4ad65d4..000000000 --- a/CIP-0102/ref_impl/offchain-reference/lib/types/chain.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { Data, Address, getAddressDetails } from "https://deno.land/x/lucid@0.10.7/mod.ts"; - -// NFT Metadata Schema - -export const NFTMetadataSchema = Data.Map(Data.Bytes(), Data.Any()); -export type NFTMetadata = Data.Static; -export const NFTMetadata = NFTMetadataSchema as unknown as NFTMetadata; - -export const NFTDatumMetadataSchema = Data.Object({ - metadata: NFTMetadataSchema, - version: Data.Integer({ minimum: 1, maximum: 1 }), - extra: Data.Any(), -}); -export type NFTDatumMetadata = Data.Static; -export const NFTDatumMetadata = NFTDatumMetadataSchema as unknown as NFTDatumMetadata - -// Address Schema - -export const ChainCredentialSchema = Data.Enum([ - Data.Object({ - VerificationKeyCredential: Data.Tuple([Data.Bytes({ minLength: 28, maxLength: 28 })]), - }), - Data.Object({ - ScriptCredential: Data.Tuple([Data.Bytes({ minLength: 28, maxLength: 28 })]), - }), -]); - -export const ChainAddressSchema = Data.Object({ - paymentCredential: ChainCredentialSchema, - stakeCredential: Data.Nullable( - Data.Enum([ - Data.Object({ Inline: Data.Tuple([ChainCredentialSchema]) }), - Data.Object({ - Pointer: Data.Object({ - slotNumber: Data.Integer(), - transactionIndex: Data.Integer(), - certificateIndex: Data.Integer(), - }), - }), - ]) - ), -}); - -export type ChainAddress = Data.Static; -export const ChainAddress = ChainAddressSchema as unknown as ChainAddress; - -/// Converts a Bech32 address to the aiken representation of a chain address -export function asChainAddress(address: Address): ChainAddress { - const { paymentCredential, stakeCredential } = getAddressDetails(address); - - if (!paymentCredential) throw new Error('Not a valid payment address.'); - - return { - paymentCredential: - paymentCredential?.type === 'Key' - ? { - VerificationKeyCredential: [paymentCredential.hash], - } - : { ScriptCredential: [paymentCredential.hash] }, - stakeCredential: stakeCredential - ? { - Inline: [ - stakeCredential.type === 'Key' - ? { - VerificationKeyCredential: [stakeCredential.hash], - } - : { ScriptCredential: [stakeCredential.hash] }, - ], - } - : null, - }; -} - -// based on Blockfrost's openAPI -export type asset_transactions = Array<{ - /** - * Hash of the transaction - */ - tx_hash: string; - /** - * Transaction index within the block - */ - tx_index: number; - /** - * Block height - */ - block_height: number; - /** - * Block creation time in UNIX time - */ - block_time: number; - }>; - - export type output = { - /** - * Output address - */ - address: string; - amount: Array<{ - /** - * The unit of the value - */ - unit: string; - /** - * The quantity of the unit - */ - quantity: string; - }>; - /** - * UTXO index in the transaction - */ - output_index: number; - /** - * The hash of the transaction output datum - */ - data_hash: string | null; - /** - * CBOR encoded inline datum - */ - inline_datum: string | null; - /** - * Whether the output is a collateral output - */ - collateral: boolean; - /** - * The hash of the reference script of the output - */ - reference_script_hash: string | null; - }; - \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/lib/types/index.ts b/CIP-0102/ref_impl/offchain-reference/lib/types/index.ts deleted file mode 100644 index 89dafceb2..000000000 --- a/CIP-0102/ref_impl/offchain-reference/lib/types/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './chain.ts' -export * from './royalties.ts' -export * from './utility.ts' \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/lib/types/royalties.ts b/CIP-0102/ref_impl/offchain-reference/lib/types/royalties.ts deleted file mode 100644 index 1954d2eb6..000000000 --- a/CIP-0102/ref_impl/offchain-reference/lib/types/royalties.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { fromText, toUnit, type Tx, Data, Address, Constr } from "https://deno.land/x/lucid@0.10.7/mod.ts"; - -import { asChainAddress, ChainAddressSchema } from './chain.ts'; - -export type Royalty = { - address: Address; - fee: number; // in percentage - maxFee?: number; - minFee?: number; -} - -export const ROYALTY_TOKEN_LABEL = 500; -export const ROYALTY_TOKEN_NAME = fromText('Royalty'); - -export const RoyaltyRecipientSchema = Data.Object({ - address: ChainAddressSchema, - fee: Data.Integer({ minimum: 1 }), - minFee: Data.Nullable(Data.Integer()), - maxFee: Data.Nullable(Data.Integer()), -}); - -export type RoyaltyRecipientType = Data.Static; -export const RoyaltyRecipientShape = RoyaltyRecipientSchema as unknown as RoyaltyRecipientType; - -export const RoyaltyInfoSchema = Data.Object({ - metadata: Data.Array(RoyaltyRecipientSchema), - version: Data.Integer({ minimum: 1, maximum: 1 }), - extra: Data.Any(), -}); -export type RoyaltyInfoType = Data.Static; -export const RoyaltyInfoShape = RoyaltyInfoSchema as unknown as RoyaltyInfoType; - -export function toRoyaltyUnit(policyId: string) { - return toUnit(policyId, ROYALTY_TOKEN_NAME, ROYALTY_TOKEN_LABEL); -} - -export type RoyaltyConstr = Constr< - Map | Map[] ->; - -// - -export const RoyaltyFlagSchema = Data.Object({ royalty_included: Data.Integer() }) -export type RoyaltyFlag = Data.Static; -export const RoyaltyFlag = RoyaltyFlagSchema as unknown as RoyaltyFlag; - -/// Converts a percentage between 0 and 100 inclusive to the CIP-102 fee format -export function asChainVariableFee(percent: number) { - if (percent < 0.1 || percent > 100) { - throw new Error('Royalty fee must be between 0.1 and 100 percent'); - } - - return BigInt(Math.floor(1 / (percent / 1000))); -} - -/// Converts from a on chain royalty to a percent between 0 and 100 -export function fromChainVariableFee(fee: bigint) { - return Math.ceil(Number(10000n / fee)) / 10; -} - -/// Confirms the fee is a positive integer and casts it to a bigint otherwise returns null -export function asChainFixedFee(fee?: number) { - if (fee) { - if (fee < 0 || !Number.isInteger(fee)) { - throw new Error('Fixed fee must be an positive integer or 0'); - } - - return BigInt(fee); - } else { - return null; - } -} - -// Converts the offchain representation of royaltyies into the format used for storing the royalties in datum on chain -export function toCip102RoyaltyDatum(royalties: Royalty[]) { - const metadata: RoyaltyRecipientType[] = royalties.map((royalty) => { - const address = asChainAddress(royalty.address); - const fee = asChainVariableFee(royalty.fee); - const minFee = asChainFixedFee(royalty.minFee); - const maxFee = asChainFixedFee(royalty.maxFee); - - return { - address, - fee, - minFee, - maxFee, - }; - }); - - const info: RoyaltyInfoType = { - metadata, - version: BigInt(1), - extra: '', - }; - - return Data.to(info, RoyaltyInfoShape); -} - -export function addCip102RoyaltyToTransaction( - tx: Tx, - policyId: string, - address: string, - royalties: Royalty[], - redeemer?: string -) { - const royaltyUnit = toRoyaltyUnit(policyId); - const royaltyAsset = { [royaltyUnit]: 1n }; - const royaltyDatum = toCip102RoyaltyDatum(royalties); - const royaltyOutputData = { inline: royaltyDatum }; - - tx.mintAssets(royaltyAsset, redeemer).payToAddressWithData(address, royaltyOutputData, royaltyAsset); -} \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/lib/types/utility.ts b/CIP-0102/ref_impl/offchain-reference/lib/types/utility.ts deleted file mode 100644 index 316e91f13..000000000 --- a/CIP-0102/ref_impl/offchain-reference/lib/types/utility.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { TxComplete } from 'https://deno.land/x/lucid@0.10.7/mod.ts' - -// Tx Builder Output -export type TxBuild = { - error: string; - tx?: undefined; - policyId?: undefined; -} | { - tx: TxComplete; - policyId: string; - error?: undefined; -} - -// NOTE: these are ripped from `lucid-cardano` and modified -export type MediaAsset = { - name: string - image: string | string[] - mediaType?: string - description?: string | string[] - files?: MediaAssetFile[] - [key: string]: unknown -} - -declare type MediaAssetFile = { - name?: string - mediaType: string - src: string | string[] -} - -export type MediaAssets = { - [key: string]: MediaAsset; -}; \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/scripts/env.ts b/CIP-0102/ref_impl/offchain-reference/scripts/env.ts deleted file mode 100644 index 7d08bc621..000000000 --- a/CIP-0102/ref_impl/offchain-reference/scripts/env.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { load } from "https://deno.land/std@0.208.0/dotenv/mod.ts"; -import { writeFileSync } from "https://deno.land/std@0.110.0/node/fs.ts"; - -const env = await load({ allowEmptyValues: true }); - -export function getEnv(variable: string): string { - const value = env[variable]; - if (!value) { - throw Error( - `Must set ${variable} is a required environment variable. Did you use 'pnpm run .env '?` - ); - } - - return value; -} - -export function updateEnv(config = {}, eol = '\n'){ - const envContents = Object.entries({...env, ...config}) - .map(([key,val]) => `${key}=${val}`) - .join(eol) - writeFileSync('.env', envContents); -} \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/scripts/generate-wallet.js b/CIP-0102/ref_impl/offchain-reference/scripts/generate-wallet.js deleted file mode 100644 index 0e1815fe1..000000000 --- a/CIP-0102/ref_impl/offchain-reference/scripts/generate-wallet.js +++ /dev/null @@ -1,15 +0,0 @@ -import { Lucid } from 'https://deno.land/x/lucid@0.10.7/mod.ts'; -import { getEnv, updateEnv } from './env.ts'; - -const network = getEnv('PUBLIC_CARDANO_NETWORK'); -const lucid = await Lucid.new(undefined, network); - -const privateKey = lucid.utils.generatePrivateKey(); -const address = await lucid - .selectWalletFromPrivateKey(privateKey) - .wallet.address(); -const envUpdate = { - WALLET_ADDRESS: address, - WALLET_PRIVATE_KEY: privateKey, -} -updateEnv(envUpdate); diff --git a/CIP-0102/ref_impl/offchain-reference/scripts/mock-frontend.ts b/CIP-0102/ref_impl/offchain-reference/scripts/mock-frontend.ts deleted file mode 100644 index 63bd138b6..000000000 --- a/CIP-0102/ref_impl/offchain-reference/scripts/mock-frontend.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { createTimelockedMP, mintNFTs } from "../lib/mint.ts"; -import { MediaAssets, Royalty, TxBuild } from "../lib/types/index.ts"; -import { getRoyaltyPolicy } from "../lib/read.ts"; -import { Lucid, Blockfrost, Network, Script, PlutusVersion } from "https://deno.land/x/lucid@0.10.7/mod.ts"; -import { getEnv } from "./env.ts"; -import contracts from "../../onchain-reference/plutus.json" with { type: "json" }; - -/** - * MAIN - * Sets up a mock frontend with a collection of required variables - * Then selects an action based on the parameter passed in by the user - */ - -const purpose = Deno.args[0]; - -// get environment variables -const blockfrostUrl = getEnv("BLOCKFROST_URL") -const projectId = getEnv("BLOCKFROST_PROJECT_KEY") -const cardanoNetwork = getEnv("PUBLIC_CARDANO_NETWORK") -const walletAddress = getEnv("WALLET_ADDRESS") -const privateKey = getEnv("WALLET_PRIVATE_KEY") - -// set up a blockfrost-connected lucid instance -const bf: Blockfrost = new Blockfrost(blockfrostUrl, projectId) -const lucid: Lucid = await Lucid.new(bf, cardanoNetwork as Network) -lucid.selectWalletFromPrivateKey(privateKey) - -// isolate the contracts' cbor -const type: PlutusVersion = "PlutusV2"; -const alwaysFails = { - type, - script: contracts.validators.find((v) => v.title === "always_fails.spend")?.compiledCode ?? "" -} - -const timelockedMP = { - type, - script: contracts.validators.find((v) => v.title === "minting.minting_validator")?.compiledCode ?? "" -} - -// interpret the user input and execute the requested action -selectAction().then(console.log) - -// Frontend Utilities - -/** - * Utility to select an action based on the purpose parameter passed in by the user - */ -async function selectAction() { - switch(purpose) { - case "mint-collection": - return await runTx(() => testTimelockedMint(lucid, timelockedMP, alwaysFails, walletAddress)) - case "get-royalties": - return await getRoyaltyPolicy(Deno.args[1]) - default: - return { error: "no transaction selected" } - } -} - -/** - * A simple function to attempt to build a tx, sign it, and submit it to the blockchain - * @param txBuilder the frontend transaction builder, often just a simple tunnel to the offchain library - */ -async function runTx(txBuilder: () => Promise) { - const txBuild = await txBuilder() - if(!txBuild.tx) { - return txBuild.error; - } - else { - //console.log(txBuild) - const signedTx = await txBuild.tx.sign().complete(); - const txHash = await signedTx.submit(); - return txHash; - } -} - -/** - * Examples of constructing transactions from the frontend using the endpoints defined in this library. - * - testTimelockedMint: a collection with CIP 68 NFTs and a CIP 102 royalty - * */ - -function testTimelockedMint(lucid: Lucid, mp: Script, validator: Script, walletAddress: string): Promise { - - // configuration - these would come from user input. Adjust these however you wish. - const mock_image = "ipfs://QmeTkA5bY4P3DUjhdtPc2MsT8G8keb7HAxjccKrLJN2xTz" - const mock_name = "test" - const mock_deadline = new Date("2024-12-31T23:59:59Z").getTime() - const mock_size = 5 - const mock_fee = 1.6 - - // parameterize minting policy - const parameterized_mp = createTimelockedMP(lucid, mp.script, mock_deadline, walletAddress); - - // define media assets for each nft - const assets: MediaAssets = {} - for(let i = 0; i < mock_size; i++) { - const tempdetails = { - name: mock_name + i, - image: mock_image - }; - assets[mock_name + i] = tempdetails - } - - // define royalty policy - const royalties: Royalty[] = [{ - address: walletAddress, - fee: mock_fee - }] - - return mintNFTs( - lucid, - parameterized_mp, - validator, - assets, - royalties[0] - ) -} \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/scripts/print-utxos.ts b/CIP-0102/ref_impl/offchain-reference/scripts/print-utxos.ts deleted file mode 100644 index 6edfd4497..000000000 --- a/CIP-0102/ref_impl/offchain-reference/scripts/print-utxos.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Lucid, Blockfrost, Network } from 'https://deno.land/x/lucid@0.10.7/mod.ts'; - -import { getEnv } from './env.ts'; - -const network = getEnv('PUBLIC_CARDANO_NETWORK'); -const address = getEnv('WALLET_ADDRESS'); -const blockfrostUrl = getEnv(`BLOCKFROST_URL`); -const blockfrostKey = getEnv(`BLOCKFROST_PROJECT_KEY`); - -console.log(address); - -const blockfrost = new Blockfrost(blockfrostUrl, blockfrostKey); -const lucid = await Lucid.new(blockfrost, network as Network); -const utxos = await lucid.utxosAt(address); - -console.log(utxos); \ No newline at end of file diff --git a/CIP-0102/ref_impl/offchain-reference/utils/query.ts b/CIP-0102/ref_impl/offchain-reference/utils/query.ts deleted file mode 100644 index 878d97f77..000000000 --- a/CIP-0102/ref_impl/offchain-reference/utils/query.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { asset_transactions, output } from "../lib/types/chain.ts"; -import { getEnv } from "../scripts/env.ts"; - -// Barebones Blockfrost query wrappers. Based on Blockfrost's openAPI. - -const blockfrost_url = getEnv("BLOCKFROST_URL"); -const headers = { - project_id: getEnv("BLOCKFROST_PROJECT_KEY"), lucid: "0.10.7" -} - -/** - * Get the transactions of an asset - * @param asset the asset to query for - * @param _count unused for now - * @param _page unused for now - * @param _order unused for now - * @returns - */ -export async function getAssetsTransactions( - asset: string, - _count: number = 100, - _page: number = 1, - _order: 'asc' | 'desc' = 'asc', - ): Promise -{ - const response = - await fetch( - blockfrost_url + "/assets/" + asset + "/transactions", - { headers }, - ).then((res) => res.json()); - - return response; -} - -/** - * Get the utxos of a transaction - * @param txHash the hash of the transaction - * @returns - */ -export async function getTxsUtxos(txHash: string): Promise<{ outputs: output[] }> { - const response = - await fetch( - blockfrost_url + "/txs/" + txHash + "/utxos", - { headers }, - ).then((res) => res.json()); - - return response; -} - -/** - * Get a cbor datum from its hash - * @param datumHash the hash of the datum - * @returns - */ -export async function getScriptsDatumCbor(datumHash: string): Promise<{ cbor: string }> { - const response = - await fetch( - blockfrost_url + "/scripts/datum/" + datumHash + "/cbor", - { headers }, - ).then((res) => res.json()); - - return response; -} \ No newline at end of file diff --git a/CIP-0102/ref_impl/onchain-reference/.github/workflows/tests.yml b/CIP-0102/ref_impl/onchain-reference/.github/workflows/tests.yml deleted file mode 100644 index 89136858e..000000000 --- a/CIP-0102/ref_impl/onchain-reference/.github/workflows/tests.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Tests - -on: - push: - branches: ["main"] - pull_request: - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - uses: aiken-lang/setup-aiken@v0.1.0 - with: - version: v1 - - - run: aiken fmt --check - - run: aiken check -D - - run: aiken build diff --git a/CIP-0102/ref_impl/onchain-reference/.gitignore b/CIP-0102/ref_impl/onchain-reference/.gitignore deleted file mode 100644 index ff7811b15..000000000 --- a/CIP-0102/ref_impl/onchain-reference/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -# Aiken compilation artifacts -artifacts/ -# Aiken's project working directory -build/ -# Aiken's default documentation export -docs/ diff --git a/CIP-0102/ref_impl/onchain-reference/README.md b/CIP-0102/ref_impl/onchain-reference/README.md deleted file mode 100644 index d70412214..000000000 --- a/CIP-0102/ref_impl/onchain-reference/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# onchain-reference - -Write validators in the `validators` folder, and supporting functions in the `lib` folder using `.ak` as a file extension. - -For example, as `validators/always_true.ak` - -```gleam -validator { - fn spend(_datum: Data, _redeemer: Data, _context: Data) -> Bool { - True - } -} -``` - -## Building - -```sh -aiken build -``` - -## Testing - -You can write tests in any module using the `test` keyword. For example: - -```gleam -test foo() { - 1 + 1 == 2 -} -``` - -To run all tests, simply do: - -```sh -aiken check -``` - -To run only tests matching the string `foo`, do: - -```sh -aiken check -m foo -``` - -## Documentation - -If you're writing a library, you might want to generate an HTML documentation for it. - -Use: - -```sh -aiken docs -``` - -## Resources - -Find more on the [Aiken's user manual](https://aiken-lang.org). diff --git a/CIP-0102/ref_impl/onchain-reference/aiken.lock b/CIP-0102/ref_impl/onchain-reference/aiken.lock deleted file mode 100644 index 62f2265a8..000000000 --- a/CIP-0102/ref_impl/onchain-reference/aiken.lock +++ /dev/null @@ -1,15 +0,0 @@ -# This file was generated by Aiken -# You typically do not need to edit this file - -[[requirements]] -name = "aiken-lang/stdlib" -version = "1.9.0" -source = "github" - -[[packages]] -name = "aiken-lang/stdlib" -version = "1.9.0" -requirements = [] -source = "github" - -[etags] diff --git a/CIP-0102/ref_impl/onchain-reference/aiken.toml b/CIP-0102/ref_impl/onchain-reference/aiken.toml deleted file mode 100644 index 54430e60f..000000000 --- a/CIP-0102/ref_impl/onchain-reference/aiken.toml +++ /dev/null @@ -1,14 +0,0 @@ -name = "0102/onchain-reference" -version = "0.0.0" -license = "Apache-2.0" -description = "Aiken contracts for project '0102/onchain-reference'" - -[repository] -user = "0102" -project = "onchain-reference" -platform = "github" - -[[dependencies]] -name = "aiken-lang/stdlib" -version = "1.9.0" -source = "github" diff --git a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak deleted file mode 100644 index 70dadac53..000000000 --- a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/common.ak +++ /dev/null @@ -1,174 +0,0 @@ -use aiken/builtin.{choose_data} -use aiken/bytearray -use aiken/dict.{Dict} -use aiken/hash.{Blake2b_224, Blake2b_256, Hash} -use aiken/int -use aiken/list -use aiken/option -use aiken/transaction.{Datum, DatumHash, InlineDatum, Input, NoDatum} -use aiken/transaction/credential.{Address, VerificationKey} -use aiken/transaction/value.{AssetName, PolicyId} - -pub const royalty_tn: ByteArray = "001f4d70526f79616c7479" - -// taken from the CIP 68 standard -pub const user_tn_prefix: ByteArray = "000de140" - -pub const ref_tn_prefix: ByteArray = "000643b0" - -pub type RoyaltyDatum { - recipients: List, - version: Int, - extra: Data, -} - -pub type Metadata = - Pairs - -pub type ChooseData { - DConstr - DMap - DList - DInt - DByteArray -} - -pub type RefDatumMetadata { - metadata: Metadata, - version: Int, - extra: Metadata, -} - -pub type RoyaltyRecipient { - address: Address, - // percentage (fraction) - fee: Int, - // fixed (absolute) - min_fee: Option, - // fixed (absolute) - max_fee: Option, -} - -pub type Asset { - policy_id: PolicyId, - asset_name: AssetName, - quantity: Int, -} - -pub type VerificationKeyHash = - Hash - -// converts between the encoded onchain fee & per mille to avoid dealing with decimals -// since integer division handles the truncation for us, it's as simple as dividing by 10000 -// recall that n / (n / m) == m, so the same function handles both directions of conversion -pub fn to_from_per_mille_fee(in_fee: Int) -> Int { - 10_000 / in_fee -} - -// take a CIP 68 user token asset name, and output the corresponding reference token asset name -pub fn get_ref_tn(asset_name: ByteArray) -> ByteArray { - bytearray.concat(ref_tn_prefix, bytearray.drop(asset_name, 8)) -} - -// take a CIP 68 reference token asset name, and output the corresponding user token asset name -pub fn get_user_tn(asset_name: ByteArray) -> ByteArray { - bytearray.concat(user_tn_prefix, bytearray.drop(asset_name, 8)) -} - -pub fn get_data( - tx_datums: Dict, Data>, - datum: Datum, -) -> Data { - when datum is { - NoDatum -> fail - DatumHash(h) -> { - expect Some(d) = dict.get(tx_datums, h) - d - } - InlineDatum(d) -> d - } -} - -pub fn get_recipients( - asset: Asset, - reference_inputs: List, - datums: Dict, Data>, -) -> List { - // Get royalty input - let royalty_input = - list.find( - reference_inputs, - fn(input) { - value.quantity_of(input.output.value, asset.policy_id, royalty_tn) == 1 - }, - ) - - if option.is_some(royalty_input) { - // Get Royalty Datum - expect Some(found_royalty_input) = royalty_input - expect royalty_info: RoyaltyDatum = - get_data(datums, found_royalty_input.output.datum) - // If found, get recipients - royalty_info.recipients - } else { - // If not found, check if the asset is a CIP-68 user token - if bytearray.take(asset.asset_name, 8) == user_tn_prefix { - // if so, calculate reference token asset name - let ref_tn = get_ref_tn(asset.asset_name) - - // look for an input with a reference token - expect Some(ref_token_input) = - list.find( - reference_inputs, - fn(input) { - value.quantity_of(input.output.value, asset.policy_id, ref_tn) == 1 - }, - ) - // parse the reference datum - expect reference_datum: RefDatumMetadata = - get_data(datums, ref_token_input.output.datum) - - // Check for royalty flag - let has_flag = list.find(reference_datum.extra, fn(item) -> Bool { and { - // maybe TODO: convert to hex - item.1st == "royalty_included", - // when constr, map, list, int, bytearray - when - choose_data(item.2nd, DConstr, DMap, DList, DInt, DByteArray) - is { - DInt -> { - expect parsed_int: Int = item.2nd - parsed_int >= 1 - } - DByteArray -> { - expect parsed_ba: ByteArray = item.2nd - when int.from_utf8(parsed_ba) is { - None -> False - Some(parsed_int) -> parsed_int >= 1 - } - } - _ -> False - }, - } }) - - when has_flag is { - Some(_) -> - // throw error if it exists and is > 0 - fail - // return empty list if it doesn't exist or is 0 - None -> - [] - } - } else { - // if the asset is not a CIP 68 NFT, return an empty list - [] - } - } -} - -test test_to_per_mille() { - and { - to_from_per_mille_fee(125) == 80, - to_from_per_mille_fee(625) == 16, - } -} diff --git a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/listing_utils.ak b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/listing_utils.ak deleted file mode 100644 index f2b778e8a..000000000 --- a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/listing_utils.ak +++ /dev/null @@ -1,7 +0,0 @@ -use aiken/transaction/credential.{Address} - -pub type Payout { - recipient: Address, - // in lovelace - amount: Int, -} diff --git a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/test_utils.ak b/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/test_utils.ak deleted file mode 100644 index e139a672a..000000000 --- a/CIP-0102/ref_impl/onchain-reference/lib/onchain-reference/test_utils.ak +++ /dev/null @@ -1,121 +0,0 @@ -use aiken/string -use aiken/transaction.{InlineDatum, Input, Output, OutputReference} -use aiken/transaction/credential.{ - Address, ScriptCredential, VerificationKeyCredential, -} -use aiken/transaction/value.{AssetName, PolicyId} -use onchain_reference/common.{ - RoyaltyDatum, RoyaltyRecipient, VerificationKeyHash, royalty_tn, - to_from_per_mille_fee, -} -use onchain_reference/listing_utils.{Payout} - -pub fn placeholder_oref() { - OutputReference { - transaction_id: transaction.placeholder().id, - output_index: 0, - } -} - -// takes per mille fee & recipient vkc and outputs recipient -pub fn placeholder_recipient( - fee_pml: Int, - address_vkh: VerificationKeyHash, -) -> RoyaltyRecipient { - RoyaltyRecipient { - address: Address { - payment_credential: VerificationKeyCredential(address_vkh), - stake_credential: None, - }, - fee: to_from_per_mille_fee(fee_pml), - min_fee: None, - max_fee: None, - } -} - -pub fn recipients_to_input( - recipients: List, - policy_id: PolicyId, -) -> Input { - Input { - output_reference: placeholder_oref(), - output: recipients_to_output(recipients, policy_id), - } -} - -pub fn recipients_to_output( - recipients: List, - policy_id: PolicyId, -) -> Output { - let out_royalty_info = RoyaltyDatum { recipients, version: 1, extra: None } - - Output { - address: Address { - payment_credential: VerificationKeyCredential(""), - stake_credential: None, - }, - value: value.from_asset(policy_id, royalty_tn, 1), - datum: InlineDatum(out_royalty_info), - reference_script: None, - } -} - -pub fn output_to_string(output: Output) -> String { - let value_string = - string.join( - value.flatten_with(output.value, asset_to_string), - @"\n", - ) - - let address_string = address_to_cred_hash(output.address) - - string.join( - [ - @"\nOutput:", - string.concat(@"Address: ", address_string), - string.concat(@"Value: ", value_string), - @"", - ], - @"\n", - ) -} - -fn asset_to_string( - policy_id: PolicyId, - asset_name: AssetName, - amt: Int, -) -> Option { - Some( - string.join( - [ - string.from_bytearray(policy_id), - string.from_bytearray(asset_name), - string.from_int(amt), - ], - @" ", - ), - ) -} - -pub fn payout_to_string(payout: Payout) -> String { - let address_string = address_to_cred_hash(payout.recipient) - - let amt_string = string.from_int(payout.amount) - string.join( - [ - @"\nPayout: ", - string.concat(@"Address: ", address_string), - string.concat(@"Amount: ", amt_string), - @"", - ], - @"\n", - ) -} - -// UNSAFE - this appears to have some side affects -pub fn address_to_cred_hash(address: Address) -> String { - when address.payment_credential is { - VerificationKeyCredential(hash) -> string.from_bytearray(hash) - ScriptCredential(hash) -> string.from_bytearray(hash) - } -} diff --git a/CIP-0102/ref_impl/onchain-reference/plutus.json b/CIP-0102/ref_impl/onchain-reference/plutus.json deleted file mode 100644 index 553f6c2f4..000000000 --- a/CIP-0102/ref_impl/onchain-reference/plutus.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "preamble": { - "title": "0102/onchain-reference", - "description": "Aiken contracts for project '0102/onchain-reference'", - "version": "0.0.0", - "plutusVersion": "v2", - "compiler": { - "name": "Aiken", - "version": "v1.0.29-alpha+unknown" - }, - "license": "Apache-2.0" - }, - "validators": [ - { - "title": "always_fails.spend", - "datum": { - "title": "_datum", - "schema": { - "$ref": "#/definitions/Data" - } - }, - "redeemer": { - "title": "_redeemer", - "schema": { - "$ref": "#/definitions/Data" - } - }, - "compiledCode": "510100003222253330044a029309b2b2b9a1", - "hash": "66a67769edd0c54d5f6ec8ba3925394bf0e4fc1f8bfbe3a131eb523c" - }, - { - "title": "minting.minting_validator", - "redeemer": { - "title": "redeemer", - "schema": { - "$ref": "#/definitions/minting~1Redeemer" - } - }, - "parameters": [ - { - "title": "lock_time", - "schema": { - "$ref": "#/definitions/Int" - } - }, - { - "title": "owner", - "schema": { - "$ref": "#/definitions/ByteArray" - } - } - ], - "compiledCode": "5902580100003232323232323232322322322232533300a32533300b533300b33223232533300f3370e9001000899b89375a6028601a0040062940c034004c00cc02cc048c04cc02cc048c04cc04cc04cc04cc04cc04cc04cc02c008c004c02400c01c4cc88c8cc00400400c894ccc04800452809919299980899b8f00200514a2266008008002602c0046eb8c050004dd61808180898089808980898089808980898089804980098048018028a50132533300c3370e900018058008991919191919299980919b87480000044cdc4a4004004266e240092001301000a375a602a002602a00260280026eb0c048004c02800458c8c8c8ccc8c0040048894ccc050008530103d87a80001323253330133370e0069000099ba548000cc05cdd380125eb804ccc014014004cdc0801a400460300066eb0c0580080052000323300100100222533301200114bd70099199911191980080080191299980c00088018991999111980e9ba73301d37520126603a6ea400ccc074dd400125eb80004dd7180b8009bad301800133003003301c002301a001375c60220026eacc048004cc00c00cc058008c050004c8cc004004008894ccc04400452f5bded8c0264646464a66602466e3d221000021003133016337606ea4008dd3000998030030019bab3013003375c6022004602a00460260026eacc040c044c044c044c044c024c004c02400c588c0400045261365632533300a3370e90000008a99980698040018a4c2c2a66601466e1d20020011533300d300800314985858c020008dd70009bad001230053754002460066ea80055cd2ab9d5573caae7d5d02ba157441", - "hash": "da3ed2bc3adda27c9012abaffa4d0dfb32d0181ad38caf0e12a8daca" - } - ], - "definitions": { - "ByteArray": { - "dataType": "bytes" - }, - "Data": { - "title": "Data", - "description": "Any Plutus data." - }, - "Int": { - "dataType": "integer" - }, - "minting/Redeemer": { - "title": "Redeemer", - "anyOf": [ - { - "title": "MintNft", - "dataType": "constructor", - "index": 0, - "fields": [] - }, - { - "title": "BurnNft", - "dataType": "constructor", - "index": 1, - "fields": [] - } - ] - } - } -} \ No newline at end of file diff --git a/CIP-0102/ref_impl/onchain-reference/validators/always_fails.ak b/CIP-0102/ref_impl/onchain-reference/validators/always_fails.ak deleted file mode 100644 index c62c006fa..000000000 --- a/CIP-0102/ref_impl/onchain-reference/validators/always_fails.ak +++ /dev/null @@ -1,5 +0,0 @@ -validator { - fn spend(_datum: Data, _redeemer: Data, _context: Data) -> Bool { - False - } -} diff --git a/CIP-0102/ref_impl/onchain-reference/validators/minting.ak b/CIP-0102/ref_impl/onchain-reference/validators/minting.ak deleted file mode 100644 index 5d7f9dc93..000000000 --- a/CIP-0102/ref_impl/onchain-reference/validators/minting.ak +++ /dev/null @@ -1,125 +0,0 @@ -use aiken/interval.{Finite, Interval, IntervalBound} -use aiken/list -use aiken/time.{PosixTime} -use aiken/transaction.{Mint, ScriptContext, Transaction} -use aiken/transaction/value -use onchain_reference/common.{VerificationKeyHash} - -type Redeemer { - MintNft - BurnNft -} - -validator(lock_time: PosixTime, owner: VerificationKeyHash) { - fn minting_validator(redeemer: Redeemer, ctx: ScriptContext) -> Bool { - expect - is_not_expired(ctx.transaction, lock_time)? && list.has( - ctx.transaction.extra_signatories, - owner, - )? - expect Some((_, _, quantity)) = - ctx.transaction.mint - |> value.from_minted_value - |> value.flatten - |> list.at(0) - - when redeemer is { - MintNft -> (quantity >= 1)? - BurnNft -> (quantity <= -1)? - } - } -} - -fn is_not_expired(tx: Transaction, lock_time: PosixTime) { - when tx.validity_range.upper_bound.bound_type is { - Finite(bound_time) -> bound_time <= lock_time - _ -> False - } -} - -// ############### TESTS ############### - -const mock_timestamp = 1704063599000 - -// 2023-12-31 23:59:59 -const mock_owner = "some_fake_address_hash" - -const mock_policy_id = "some_fake_policy_id" - -const mock_asset_name = "some_fake_asset_name" - -test should_mint_nft() { - // GIVEN - let redeemer = MintNft - let ctx = - ScriptContext { - purpose: Mint(mock_policy_id), - transaction: get_base_transaction(1604063500000, 1704063500000, 1), - } - // THEN - minting_validator(mock_timestamp, mock_owner, redeemer, ctx) == True -} - -test should_burn_nft() { - // GIVEN - let redeemer = BurnNft - let ctx = - ScriptContext { - purpose: Mint(mock_policy_id), - transaction: get_base_transaction(1604063500000, 1704063500000, -1), - } - // THEN - minting_validator(mock_timestamp, mock_owner, redeemer, ctx) == True -} - -test should_succeed_mint_multiple_asset_amount() { - // GIVEN - let redeemer = MintNft - let ctx = - ScriptContext { - purpose: Mint(mock_policy_id), - transaction: get_base_transaction(1604063500000, 1704063500000, 5), - } - // THEN - minting_validator(mock_timestamp, mock_owner, redeemer, ctx) == True -} - -test should_throw_error_by_exceeded_time_lock() fail { - // GIVEN - let redeemer = MintNft - let ctx = - ScriptContext { - purpose: Mint(mock_policy_id), - transaction: get_base_transaction(1604063500000, mock_timestamp, 1), - } - // THEN - minting_validator(1704063500000, mock_owner, redeemer, ctx) -} - -test should_throw_error_by_missing_signer() fail { - // GIVEN - let redeemer = MintNft - let ctx = - ScriptContext { - purpose: Mint(mock_policy_id), - transaction: get_base_transaction(1604063500000, 1704063500000, 1), - } - // THEN - minting_validator(mock_timestamp, "some_other_owner", redeemer, ctx) -} - -fn get_base_transaction(from: Int, to: Int, quantity: Int) { - Transaction { - ..transaction.placeholder(), - extra_signatories: [mock_owner], - mint: value.from_asset(mock_policy_id, mock_asset_name, quantity) - |> value.to_minted_value, - validity_range: Interval { - lower_bound: IntervalBound { - bound_type: Finite(from), - is_inclusive: True, - }, - upper_bound: IntervalBound { bound_type: Finite(to), is_inclusive: True }, - }, - } -} diff --git a/CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak b/CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak deleted file mode 100644 index 862b17b9f..000000000 --- a/CIP-0102/ref_impl/onchain-reference/validators/reducible_royalty.ak +++ /dev/null @@ -1,186 +0,0 @@ -use aiken/list -use aiken/transaction.{Input, Output, ScriptContext, Spend, Transaction} -use aiken/transaction/credential.{Address, VerificationKeyCredential} -use aiken/transaction/value.{PolicyId} -use onchain_reference/common.{ - RoyaltyDatum, RoyaltyRecipient, get_data, royalty_tn, -} -use onchain_reference/test_utils.{recipients_to_input, recipients_to_output} - -type ReduceRedeemer { - policy_id: PolicyId, -} - -validator { - fn reduce(_d: Data, redeemer: ReduceRedeemer, context: ScriptContext) -> Bool { - let tx = context.transaction - - // find output datum - expect Some(royalty_output) = - list.find( - tx.outputs, - fn(output) { - value.quantity_of(output.value, redeemer.policy_id, royalty_tn) == 1 - }, - ) - - expect out_royalty_info: RoyaltyDatum = - get_data(tx.datums, royalty_output.datum) - - // find input datum - expect Some(royalty_input) = - list.find( - tx.inputs, - fn(input) { - value.quantity_of(input.output.value, redeemer.policy_id, royalty_tn) == 1 - }, - ) - - expect in_royalty_info: RoyaltyDatum = - get_data(tx.datums, royalty_input.output.datum) - - // check that for each in recipient, there is a corresponding out recipient - expect - list.all( - // for each in recipient - in_royalty_info.recipients, - fn(in_recipient) { - // unwrap the vck - expect VerificationKeyCredential(sig) = - in_recipient.address.payment_credential - list.any( - // for each out recipient - out_royalty_info.recipients, - fn(out_recipient) { - // find the corresponding out recipient - if in_recipient.address == out_recipient.address { - // the recipient has signed the transaction AND - // the fee has not changed OR - // the fee has decreased (encoding reverses this) - list.has(tx.extra_signatories, sig) && in_recipient.fee > out_recipient.fee || in_recipient.fee == out_recipient.fee - } else { - False - } - }, - ) - }, - ) - True - } -} - -// ############### TESTS ############### -const mock_owner = "some_fake_address_hash" - -const mock_policy_id = "some_fake_policy_id" - -test should_reduce_recipient() { - let redeemer = ReduceRedeemer { policy_id: mock_policy_id } - - let in_recipient = - RoyaltyRecipient { - address: Address { - payment_credential: VerificationKeyCredential(mock_owner), - stake_credential: None, - }, - fee: 625, // 1.6% - min_fee: None, - max_fee: None, - } - - let input = recipients_to_input([in_recipient], mock_policy_id) - - let outputs = - [ - recipients_to_output( - [RoyaltyRecipient { ..in_recipient, fee: 125 }], // 8% - mock_policy_id, - ), - ] - - let ctx = - ScriptContext { - transaction: get_base_transaction([input], outputs), - purpose: Spend(input.output_reference), - } - - reduce([], redeemer, ctx) -} - -test should_fail_increase_recipient() fail { - let redeemer = ReduceRedeemer { policy_id: mock_policy_id } - - let in_recipient = - RoyaltyRecipient { - address: Address { - payment_credential: VerificationKeyCredential(mock_owner), - stake_credential: None, - }, - fee: 125, // 8% - min_fee: None, - max_fee: None, - } - - let input = recipients_to_input([in_recipient], mock_policy_id) - - let outputs = - [ - recipients_to_output( - [RoyaltyRecipient { ..in_recipient, fee: 625 }], // 1.6% - mock_policy_id, - ), - ] - - let ctx = - ScriptContext { - transaction: get_base_transaction([input], outputs), - purpose: Spend(input.output_reference), - } - - reduce([], redeemer, ctx) -} - -test should_fail_no_token() fail { - let redeemer = ReduceRedeemer { policy_id: mock_policy_id } - - let in_recipient = - RoyaltyRecipient { - address: Address { - payment_credential: VerificationKeyCredential(mock_owner), - stake_credential: None, - }, - fee: 125, - min_fee: None, - max_fee: None, - } - - let input = recipients_to_input([in_recipient], mock_policy_id) - let recipient_output = - recipients_to_output( - [RoyaltyRecipient { ..in_recipient, fee: 625 }], - mock_policy_id, - ) - - let outputs = - [Output { ..recipient_output, value: value.zero() }] - - let ctx = - ScriptContext { - transaction: get_base_transaction([input], outputs), - purpose: Spend(input.output_reference), - } - - reduce([], redeemer, ctx) -} - -fn get_base_transaction( - recipients_in: List, - recipients_out: List, -) -> Transaction { - Transaction { - ..transaction.placeholder(), - extra_signatories: [mock_owner], - inputs: recipients_in, - outputs: recipients_out, - } -} diff --git a/CIP-0102/ref_impl/onchain-reference/validators/simple_listing.ak b/CIP-0102/ref_impl/onchain-reference/validators/simple_listing.ak deleted file mode 100644 index fadd72dac..000000000 --- a/CIP-0102/ref_impl/onchain-reference/validators/simple_listing.ak +++ /dev/null @@ -1,466 +0,0 @@ -use aiken/bytearray -use aiken/list -use aiken/math -use aiken/option -use aiken/string -use aiken/transaction.{ - InlineDatum, Input, NoDatum, Output, OutputReference, ScriptContext, Spend, - Transaction, -} -use aiken/transaction/credential.{ - Address, ScriptCredential, VerificationKeyCredential, -} -use aiken/transaction/value.{quantity_of} -use onchain_reference/common.{ - Asset, RoyaltyRecipient, VerificationKeyHash, get_recipients, - to_from_per_mille_fee, user_tn_prefix, -} -use onchain_reference/listing_utils.{Payout} -use onchain_reference/test_utils.{ - output_to_string, payout_to_string, placeholder_oref, placeholder_recipient, - recipients_to_input, -} - -type ListingDatum { - // to further prevent circumvention, lot could be detected from consumed the Listing output - lot: Asset, - seller: VerificationKeyHash, - // in lovelace - price: Int, -} - -/// A user can either buy a token -/// or cancel/update the listing. -type ListingRedeemer { - /// Make a purchase. - Buy - /// Cancel or update a listing. - WithdrawOrUpdate -} - -validator { - /// Validate that the signer is the owner or that payouts - /// are present as outputs and that the tag is correct. - fn spend( - datum: ListingDatum, - redeemer: ListingRedeemer, - ctx: ScriptContext, - ) -> Bool { - let ScriptContext { transaction, purpose } = ctx - - let Transaction { outputs, extra_signatories, reference_inputs, datums, .. } = - transaction - - // Match on the action. - when redeemer is { - Buy -> { - expect Spend(_out_ref) = purpose - - let royalty_recipients: List = - get_recipients(datum.lot, reference_inputs, datums) - - // ensure royalty amount is paid to the royalty address - let payouts: List = - get_payouts(datum.seller, royalty_recipients, datum.price) - - trace list.foldl( - outputs, - @"", - fn(output, current_str) { - string.concat(current_str, output_to_string(output)) - }, - ) - - trace list.foldl( - payouts, - @"", - fn(payout, current_str) { - string.concat(current_str, payout_to_string(payout)) - }, - ) - - // check that the payouts are represented in the outputs and that enough are - datum.price <= check_payouts(outputs, payouts) - } - - // There's not much to do here. An asset - // owner can cancel or update their listing - // at any time. - WithdrawOrUpdate -> list.has(extra_signatories, datum.seller) - } - } -} - -fn get_payouts( - seller: VerificationKeyHash, - royalty_recipients: List, - price: Int, -) -> List { - // for each recipient - // DO THIS WORK? TEST IT - - let (payouts, paid_out) = - list.foldl( - royalty_recipients, - ([], 0), - fn(recipient: RoyaltyRecipient, out: (List, Int)) -> ( - List, - Int, - ) { - // get the estimated amount based on the price - let amount = to_from_per_mille_fee(recipient.fee) * price / 1000 - - // adjust based on min fee - let amount = - if option.is_some(recipient.min_fee) { - expect Some(min) = recipient.min_fee - math.max(amount, math.min(price - out.2nd, min)) - } else { - amount - } - - // adjust based on max fee - let amount = - if option.is_some(recipient.max_fee) { - expect Some(max) = recipient.max_fee - math.min(amount, max) - } else { - amount - } - let already_paid_out = out.2nd + amount - // output payout - ( - list.concat( - out.1st, - [Payout { recipient: recipient.address, amount }], - ), - already_paid_out, - ) - }, - ) - - let seller_payout = - Payout { - recipient: Address { - payment_credential: VerificationKeyCredential(seller), - stake_credential: None, - }, - amount: price - paid_out, - } - - // output seller payout with remainder of price - list.push(payouts, seller_payout) -} - -/// Credit to JPG.Store's v3 open source contracts. Thank you for helping push the ecosystem forward! -/// Check that payouts and payout outputs -/// are correct. Payouts are stored in the datum -/// when assets are listed. On buy a transaction -/// with matching payout outputs needs to be constructed. -/// We also require that outputs are in the same order as the -/// payouts in the datum. Returns the sum of the payout amounts. -fn check_payouts(outputs: List, payouts: List) -> Int { - expect [first_output, ..rest_outputs] = outputs - - let Output { address: output_address, value, .. } = first_output - expect [payout, ..rest_payouts] = payouts - - let Payout { recipient: payout_address, amount: amount_lovelace } = payout - - // The `Output` address must match - // the address specified in the corresponding - // payout from the datum. - expect payout_address == output_address - - // The quantity in the output must equal - // the amount specified in the corresponding - // payout from the datum. - let quantity = quantity_of(value, "", "") - expect quantity >= amount_lovelace && amount_lovelace > 0 - let rest_payouts_amount = - when rest_payouts is { - // the base case - [] -> - // if rest is empty we are done - 0 - _ -> - // continue with remaining outputs and payouts - check_payouts(rest_outputs, rest_payouts) - } - - amount_lovelace + rest_payouts_amount -} - -// ############### ENDPOINT TESTS ############### -const mock_owner: VerificationKeyHash = "some_fake_address_hash" - -const mock_policy_id = "fake_lot_policy_id" - -const mock_buyer: VerificationKeyHash = "some_other_fake_address_hash" - -test one_recipient_cip68_buy() { - let redeemer = Buy - - let lot = - Asset { - policy_id: mock_policy_id, - asset_name: bytearray.concat(user_tn_prefix, "fake_lot_tn"), - quantity: 1, - } - - // Price: 1000 Ada - let listing_datum = - ListingDatum { lot, seller: mock_owner, price: 1_000_000_000 } - - let royalty_recipients = - [placeholder_recipient(80, mock_owner)] - - let ctx = - ScriptContext { - transaction: get_base_transaction(listing_datum, royalty_recipients), - purpose: Spend(OutputReference { ..placeholder_oref(), output_index: 1 }), - } - - // 0 is the royalty input - spend(listing_datum, redeemer, ctx) -} - -test multi_recipient_cip68_buy() { - let redeemer = Buy - - let lot = - Asset { - policy_id: mock_policy_id, - asset_name: bytearray.concat(user_tn_prefix, "fake_lot_tn"), - quantity: 1, - } - - // Price: 1000 Ada - let listing_datum = - ListingDatum { lot, seller: mock_owner, price: 1_000_000_000 } - - let royalty_recipients = - [ - placeholder_recipient(80, mock_owner), - placeholder_recipient(15, "other_mock_owner"), - placeholder_recipient(50, "other_other_mock_owner"), - placeholder_recipient(20, "yet_another_mock_owner"), - placeholder_recipient(200, "so_many_mock_owners"), - ] - - let ctx = - ScriptContext { - transaction: get_base_transaction(listing_datum, royalty_recipients), - purpose: Spend(OutputReference { ..placeholder_oref(), output_index: 1 }), - } - - // 0 is the royalty input - spend(listing_datum, redeemer, ctx) -} - -test no_royalty_paid() fail { - let redeemer = Buy - - let lot = - Asset { - policy_id: mock_policy_id, - asset_name: bytearray.concat(user_tn_prefix, "fake_lot_tn"), - quantity: 1, - } - - // Price: 1000 Ada - let listing_datum = - ListingDatum { lot, seller: mock_owner, price: 1_000_000_000 } - - let royalty_recipients = - [placeholder_recipient(80, mock_owner)] - - // constructing tx with no outputs to recipients - let tx = - Transaction { - ..get_base_transaction(listing_datum, royalty_recipients), - outputs: get_base_transaction(listing_datum, []).outputs, - } - - let ctx = - ScriptContext { - transaction: tx, - purpose: Spend(OutputReference { ..placeholder_oref(), output_index: 1 }), - } - - // 0 is the royalty input - spend(listing_datum, redeemer, ctx) -} - -fn get_base_transaction( - listing_datum: ListingDatum, - royalty_recipients: List, -) -> Transaction { - let lot_input = lot_to_input(listing_datum) - let royalty_policy_ref_input = - recipients_to_input(royalty_recipients, mock_policy_id) - - let buyer_address = - Address { - payment_credential: VerificationKeyCredential(mock_buyer), - stake_credential: None, - } - - let buyer_input_output = - Output { - address: buyer_address, - // add 1 ada for fees - value: value.from_lovelace(listing_datum.price + 1_000_000), - datum: NoDatum, - reference_script: None, - } - - let buyer_input = - Input { - output: buyer_input_output, - output_reference: OutputReference { - ..placeholder_oref(), - output_index: 2, - }, - } - - // 0 is royalty input, 1 is lot - let buyer_output = - Output { - address: buyer_address, - value: value.from_lovelace(listing_datum.price), - datum: NoDatum, - reference_script: None, - } - - let amount_to_royalties = - math.clamp( - list.foldl( - royalty_recipients, - 0, - fn(recipient, total) { - to_from_per_mille_fee(recipient.fee) * listing_datum.price / 1000 + total - }, - ), - 0, - listing_datum.price, - ) - - trace string.from_int(amount_to_royalties) - - let royalty_outputs: List = - list.map( - royalty_recipients, - fn(recipient) { - Output { - address: recipient.address, - value: value.from_lovelace( - to_from_per_mille_fee(recipient.fee) * listing_datum.price / 1000, - ), - datum: NoDatum, - reference_script: None, - } - }, - ) - - let seller_output = - Output { - address: Address { - payment_credential: VerificationKeyCredential(mock_owner), - stake_credential: None, - }, - value: value.from_lovelace(listing_datum.price - amount_to_royalties), - datum: NoDatum, - reference_script: None, - } - - Transaction { - ..transaction.placeholder(), - extra_signatories: [mock_buyer], - inputs: [lot_input, buyer_input], - reference_inputs: [royalty_policy_ref_input], - outputs: list.concat([seller_output, ..royalty_outputs], [buyer_output]), - } -} - -fn lot_to_input(datum: ListingDatum) -> Input { - let lot = datum.lot - Input { - output: Output { - address: Address { - payment_credential: ScriptCredential(""), - stake_credential: None, - }, - value: value.from_asset(lot.policy_id, lot.asset_name, lot.quantity), - datum: InlineDatum(datum), - reference_script: None, - }, - output_reference: OutputReference { ..placeholder_oref(), output_index: 1 }, - } - // 0 is the royalty input -} - -// ############### FUNCTION TESTS ############### - -/// This test makes sure the `check_payouts` returns true -/// when give the correct inputs. It is safe to have trailing outputs -/// in the transaction as long as the payouts are correct. -test check_payouts_with_trailing_outputs() { - let test_royalty_addr = - Address { - payment_credential: VerificationKeyCredential( - #"80f60f3b5ea7153e0acc7a803e4401d44b8ed1bae1c7baaad1a62a81", - ), - stake_credential: None, - } - - let test_seller_addr = - Address { - payment_credential: VerificationKeyCredential( - #"90f60f3b5ea7153e0acc7a803e4401d44b8ed1bae1c7baaad1a62a81", - ), - stake_credential: None, - } - - let test_random_addr = - Address { - payment_credential: VerificationKeyCredential( - #"fff60f3b5ea7153e0acc7a803e4401d44b8ed1bae1c7baaad1a62a81", - ), - stake_credential: None, - } - - let test_royalty_payouts = - [ - Payout { recipient: test_royalty_addr, amount: 3000000 }, - Payout { recipient: test_seller_addr, amount: 95000000 }, - ] - - let out_1 = - Output { - address: test_royalty_addr, - value: value.from_lovelace(3100000), - datum: NoDatum, - reference_script: None, - } - - let out_2 = - Output { - address: test_seller_addr, - value: value.from_lovelace(95000000), - datum: NoDatum, - reference_script: None, - } - - let out_random = - Output { - address: test_random_addr, - value: value.from_lovelace(1000000), - datum: NoDatum, - reference_script: None, - } - - let outputs = list.concat([out_1, out_2], list.repeat(out_random, 100)) - - 98000000 == check_payouts(outputs, test_royalty_payouts) -} From c3370bc36e378f6dde471338a651d2e6a2245eff Mon Sep 17 00:00:00 2001 From: samdelaney Date: Fri, 28 Jun 2024 01:46:02 -0700 Subject: [PATCH 24/24] remove .vscode settings artifact --- CIP-0102/.vscode/settings.json | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 CIP-0102/.vscode/settings.json diff --git a/CIP-0102/.vscode/settings.json b/CIP-0102/.vscode/settings.json deleted file mode 100644 index 84bfcc7e5..000000000 --- a/CIP-0102/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "deno.cacheOnSave": true, - "deno.codeLens.test": false, - "deno.codeLens.referencesAllFunctions": false, - "deno.codeLens.references": false, - "deno.codeLens.implementations": false, - "deno.enable": false, - "deno.unstable": false -} \ No newline at end of file