From 2d30e5d2ee6918b55e75142ae4d6640aa1dddb3d Mon Sep 17 00:00:00 2001 From: otto Date: Wed, 1 Apr 2026 20:17:23 +0200 Subject: [PATCH 01/11] feat: add ScalusProvider for Scalus emulator integration Implements IFetcher + ISubmitter + IEvaluator backed by the Scalus Cardano emulator, enabling local ledger validation with MeshTxBuilder. --- package-lock.json | 206 ++++++++++------ packages/mesh-provider/package.json | 4 +- packages/mesh-provider/src/index.ts | 1 + packages/mesh-provider/src/scalus.ts | 349 +++++++++++++++++++++++++++ 4 files changed, 484 insertions(+), 76 deletions(-) create mode 100644 packages/mesh-provider/src/scalus.ts diff --git a/package-lock.json b/package-lock.json index d3a533d5d..44cb399b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,6 +71,7 @@ "apps/docs/node_modules/@eslint/eslintrc": { "version": "3.3.3", "license": "MIT", + "peer": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -92,6 +93,7 @@ "apps/docs/node_modules/@eslint/js": { "version": "9.39.2", "license": "MIT", + "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -142,7 +144,6 @@ "version": "18.3.3", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -336,6 +337,7 @@ "apps/docs/node_modules/eslint-scope": { "version": "8.4.0", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -350,6 +352,7 @@ "apps/docs/node_modules/eslint-visitor-keys": { "version": "4.2.1", "license": "Apache-2.0", + "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -360,6 +363,7 @@ "apps/docs/node_modules/espree": { "version": "10.4.0", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", @@ -399,6 +403,7 @@ "apps/docs/node_modules/file-entry-cache": { "version": "8.0.0", "license": "MIT", + "peer": true, "dependencies": { "flat-cache": "^4.0.0" }, @@ -409,6 +414,7 @@ "apps/docs/node_modules/flat-cache": { "version": "4.0.1", "license": "MIT", + "peer": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -432,6 +438,7 @@ "apps/docs/node_modules/globals": { "version": "14.0.0", "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -463,7 +470,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", @@ -530,7 +536,6 @@ "apps/docs/node_modules/tailwindcss": { "version": "3.4.4", "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -567,7 +572,6 @@ "version": "5.4.5", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -687,6 +691,7 @@ "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.12.0.tgz", "integrity": "sha512-EfW0bfxjPs+C7ANkJDw2TATntfBKsFiy7APh+KO0pQ8A6HYa5I0NjFuCGCXWfzzzLXNZta3QUl3n5Kmm6aJo9Q==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.46.0", "@algolia/requester-browser-xhr": "5.46.0", @@ -734,6 +739,7 @@ "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.46.0.tgz", "integrity": "sha512-eG5xV8rujK4ZIHXrRshvv9O13NmU/k42Rnd3w43iKH5RaQ2zWuZO6Q7XjaoJjAFVCsJWqRbXzbYyPGrbF3wGNg==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.46.0", "@algolia/requester-browser-xhr": "5.46.0", @@ -749,6 +755,7 @@ "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.46.0.tgz", "integrity": "sha512-AYh2uL8IUW9eZrbbT+wZElyb7QkkeV3US2NEKY7doqMlyPWE8lErNfkVN1NvZdVcY4/SVic5GDbeDz2ft8YIiQ==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.46.0", "@algolia/requester-browser-xhr": "5.46.0", @@ -764,6 +771,7 @@ "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.46.0.tgz", "integrity": "sha512-0emZTaYOeI9WzJi0TcNd2k3SxiN6DZfdWc2x2gHt855Jl9jPUOzfVTL6gTvCCrOlT4McvpDGg5nGO+9doEjjig==", "license": "MIT", + "peer": true, "engines": { "node": ">= 14.0.0" } @@ -773,6 +781,7 @@ "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.46.0.tgz", "integrity": "sha512-wrBJ8fE+M0TDG1As4DDmwPn2TXajrvmvAN72Qwpuv8e2JOKNohF7+JxBoF70ZLlvP1A1EiH8DBu+JpfhBbNphQ==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.46.0", "@algolia/requester-browser-xhr": "5.46.0", @@ -788,6 +797,7 @@ "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.46.0.tgz", "integrity": "sha512-LnkeX4p0ENt0DoftDJJDzQQJig/sFQmD1eQifl/iSjhUOGUIKC/7VTeXRcKtQB78naS8njUAwpzFvxy1CDDXDQ==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.46.0", "@algolia/requester-browser-xhr": "5.46.0", @@ -803,6 +813,7 @@ "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.46.0.tgz", "integrity": "sha512-aF9tc4ex/smypXw+W3lBPB1jjKoaGHpZezTqofvDOI/oK1dR2sdTpFpK2Ru+7IRzYgwtRqHF3znmTlyoNs9dpA==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.46.0", "@algolia/requester-browser-xhr": "5.46.0", @@ -834,6 +845,7 @@ "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.46.0.tgz", "integrity": "sha512-2LT0/Z+/sFwEpZLH6V17WSZ81JX2uPjgvv5eNlxgU7rPyup4NXXfuMbtCJ+6uc4RO/LQpEJd3Li59ke3wtyAsA==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.46.0", "@algolia/requester-browser-xhr": "5.46.0", @@ -849,6 +861,7 @@ "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.46.0.tgz", "integrity": "sha512-uivZ9wSWZ8mz2ZU0dgDvQwvVZV8XBv6lYBXf8UtkQF3u7WeTqBPeU8ZoeTyLpf0jAXCYOvc1mAVmK0xPLuEwOQ==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.46.0", "@algolia/requester-browser-xhr": "5.46.0", @@ -864,6 +877,7 @@ "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.46.0.tgz", "integrity": "sha512-O2BB8DuySuddgOAbhyH4jsGbL+KyDGpzJRtkDZkv091OMomqIA78emhhMhX9d/nIRrzS1wNLWB/ix7Hb2eV5rg==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.46.0", "@algolia/requester-browser-xhr": "5.46.0", @@ -879,6 +893,7 @@ "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.46.0.tgz", "integrity": "sha512-eW6xyHCyYrJD0Kjk9Mz33gQ40LfWiEA51JJTVfJy3yeoRSw/NXhAL81Pljpa0qslTs6+LO/5DYPZddct6HvISQ==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.46.0" }, @@ -891,6 +906,7 @@ "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.46.0.tgz", "integrity": "sha512-Vn2+TukMGHy4PIxmdvP667tN/MhS7MPT8EEvEhS6JyFLPx3weLcxSa1F9gVvrfHWCUJhLWoMVJVB2PT8YfRGcw==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.46.0" }, @@ -903,6 +919,7 @@ "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.46.0.tgz", "integrity": "sha512-xaqXyna5yBZ+r1SJ9my/DM6vfTqJg9FJgVydRJ0lnO+D5NhqGW/qaRG/iBGKr/d4fho34el6WakV7BqJvrl/HQ==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.46.0" }, @@ -927,6 +944,7 @@ "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.14.0.tgz", "integrity": "sha512-0YQKKRIxiMlIou+SekQqdCo0ZTHxOcES+K8vKB53cIDpwABNR0P0yRzPgsbgcj3zRJniD93S/ontsnZsCLZrxQ==", "license": "MIT", + "peer": true, "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@wry/caches": "^1.0.0", @@ -992,7 +1010,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1639,8 +1656,7 @@ "version": "1.10.1", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.1.tgz", "integrity": "sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==", - "license": "(Apache-2.0 AND BSD-3-Clause)", - "peer": true + "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@cardananium/cardano-peer-connect": { "version": "1.2.19", @@ -11406,7 +11422,6 @@ "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-1.4.0.tgz", "integrity": "sha512-vZeOkKaAjyV4+RH3+rJZIfDFJAfr+7fyYr6sLDKbYX3uuTVszhFe9/YKf5DNqrDb5cKdKVlYkGn6DTDqMitAnA==", "license": "Apache-2.0", - "peer": true, "peerDependencies": { "@bufbuild/protobuf": "^1.4.2" } @@ -11466,6 +11481,7 @@ "resolved": "https://registry.npmjs.org/@dao-xyz/borsh/-/borsh-5.2.4.tgz", "integrity": "sha512-HKjOMXBQvr2riUIX/g+sLOmgDsk16zuMa0VZKMfj0XLp3MlafMxkwuBJyJk0apqUpKxywXeitgFHGFOGNkn+Kw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@protobufjs/float": "^1.0.2", "@protobufjs/utf8": "^1.1.0", @@ -12145,6 +12161,7 @@ "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", "license": "MIT", + "peer": true, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } @@ -12178,7 +12195,6 @@ "resolved": "https://registry.npmjs.org/@harmoniclabs/bytestring/-/bytestring-1.0.0.tgz", "integrity": "sha512-d5m10O0okKc6QNX0pSRriFTkk/kNMnMBGbo5X3kEZwKaXTI4tDVoTZBL7bwbYHwOEdSxWJjVtlO9xtB7ZrYZNg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@harmoniclabs/uint8array-utils": "^1.0.0" } @@ -12189,7 +12205,6 @@ "integrity": "sha512-gzRqqcJL8sulc2/6iqRXZdWUCEeK3A+jwJ88sbVNzgk4IeMFQLSFg4Ck8ZBETu/W/q1zdknjNfJYyH1OxVriQA==", "deprecated": "update to 1.6.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@harmoniclabs/bytestring": "^1.0.0", "@harmoniclabs/obj-utils": "^1.0.0", @@ -12204,7 +12219,6 @@ "resolved": "https://registry.npmjs.org/@harmoniclabs/crypto/-/crypto-0.2.5.tgz", "integrity": "sha512-t2saWMFWBx8tOHotiYTTfQKhPGpWT4AMLXxq3u0apShVXNV0vgL0gEgSMudBjES/wrKByCqa2xmU70gadz26hA==", "license": "MIT", - "peer": true, "dependencies": { "@harmoniclabs/bitstream": "^1.0.0", "@harmoniclabs/uint8array-utils": "^1.0.3" @@ -12220,15 +12234,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@harmoniclabs/pair/-/pair-1.0.0.tgz", "integrity": "sha512-D9OBowsUsy1LctHxWzd9AngTzoo5x3rBiJ0gu579t41Q23pb+VNx1euEfluUEiaYbgljcl1lb/4D1fFTZd1tRQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@harmoniclabs/plutus-data": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@harmoniclabs/plutus-data/-/plutus-data-1.2.4.tgz", "integrity": "sha512-cpr6AnJRultH6PJRDriewHEgNLQs2IGLampZrLjmK5shzTsHICD0yD0Zig9eKdcS7dmY6mlzvSpAJWPGeTxbCA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@harmoniclabs/biguint": "^1.0.0", "@harmoniclabs/crypto": "^0.2.4", @@ -12362,7 +12374,6 @@ "integrity": "sha512-soa2bPUJAFruLL4z/CnMfSEKGznm5ebz29fIa9PxYtu8HHyLKNE1NXAs6dylfw1jn/ilEIfO2oLLN6uAafb7DA==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@babel/generator": "^7.26.2", "@babel/parser": "^7.26.2", @@ -13012,7 +13023,6 @@ "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", @@ -13040,7 +13050,6 @@ "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", @@ -13120,7 +13129,6 @@ "resolved": "https://registry.npmjs.org/@mdx-js/loader/-/loader-3.1.1.tgz", "integrity": "sha512-0TTacJyZ9mDmY+VefuthVshaNIyCGZHJG2fMnGaDttCt8HmjUF7SizlHJpaCDoGnN635nK1wpzfpx/Xx5S4WnQ==", "license": "MIT", - "peer": true, "dependencies": { "@mdx-js/mdx": "^3.0.0", "source-map": "^0.7.0" @@ -13180,7 +13188,6 @@ "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", "license": "MIT", - "peer": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -13575,6 +13582,7 @@ "resolved": "https://registry.npmjs.org/@midnight-ntwrk/midnight-js-fetch-zk-config-provider/-/midnight-js-fetch-zk-config-provider-2.1.0.tgz", "integrity": "sha512-oWztVa4A+/trjSFjEFID6pCYYip/r3JoMfjtoboby17Um/wncUTxu35dZybgmhQzM0BiGKuyq7zVVodI5bbl/Q==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@midnight-ntwrk/midnight-js-types": "2.1.0", "cross-fetch": "^4.0.0" @@ -13585,6 +13593,7 @@ "resolved": "https://registry.npmjs.org/@midnight-ntwrk/midnight-js-types/-/midnight-js-types-2.1.0.tgz", "integrity": "sha512-bU7f2gdXt1/BR2XatKNPx9o6FDq1exYcWLQg4kn78d1SX39OVD33PVOavg2mFMoeC00YpcG2T1xH0eD/CMwXgA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "rxjs": "^7.5.0" } @@ -13594,6 +13603,7 @@ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", "license": "MIT", + "peer": true, "dependencies": { "node-fetch": "^2.7.0" } @@ -13603,6 +13613,7 @@ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "license": "MIT", + "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -13623,6 +13634,7 @@ "resolved": "https://registry.npmjs.org/@midnight-ntwrk/midnight-js-http-client-proof-provider/-/midnight-js-http-client-proof-provider-2.1.0.tgz", "integrity": "sha512-SubdHm4bP7syyQ1UEqsmfPoYkHhVRDlCuXS8RqR4p4ivIVeN6pE7oh0dLOM+oJtcPIM646lI8dJnuHZEfxjsAw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@dao-xyz/borsh": "^5.1.5", "@midnight-ntwrk/midnight-js-network-id": "2.1.0", @@ -13637,6 +13649,7 @@ "resolved": "https://registry.npmjs.org/@midnight-ntwrk/midnight-js-types/-/midnight-js-types-2.1.0.tgz", "integrity": "sha512-bU7f2gdXt1/BR2XatKNPx9o6FDq1exYcWLQg4kn78d1SX39OVD33PVOavg2mFMoeC00YpcG2T1xH0eD/CMwXgA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "rxjs": "^7.5.0" } @@ -13646,6 +13659,7 @@ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", "license": "MIT", + "peer": true, "dependencies": { "node-fetch": "^2.7.0" } @@ -13655,6 +13669,7 @@ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "license": "MIT", + "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -13675,6 +13690,7 @@ "resolved": "https://registry.npmjs.org/@midnight-ntwrk/midnight-js-indexer-public-data-provider/-/midnight-js-indexer-public-data-provider-2.1.0.tgz", "integrity": "sha512-Hmj4wcF7WmGvLrRjz9xvMu0GPop/FxbwNumsuR+mnw7EYf3Y/6JRQeIZ0Ga+eD1VM5JaUJ8A+I8iGZMcpQ36TA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@apollo/client": "^3.13.8", "@midnight-ntwrk/midnight-js-network-id": "2.1.0", @@ -13694,6 +13710,7 @@ "resolved": "https://registry.npmjs.org/@midnight-ntwrk/midnight-js-types/-/midnight-js-types-2.1.0.tgz", "integrity": "sha512-bU7f2gdXt1/BR2XatKNPx9o6FDq1exYcWLQg4kn78d1SX39OVD33PVOavg2mFMoeC00YpcG2T1xH0eD/CMwXgA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "rxjs": "^7.5.0" } @@ -13716,7 +13733,8 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@midnight-ntwrk/midnight-js-indexer-public-data-provider/node_modules/buffer": { "version": "6.0.3", @@ -13737,6 +13755,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" @@ -13747,6 +13766,7 @@ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", "license": "MIT", + "peer": true, "dependencies": { "node-fetch": "^2.7.0" } @@ -13756,6 +13776,7 @@ "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", "license": "MIT", + "peer": true, "peerDependencies": { "ws": "*" } @@ -13765,6 +13786,7 @@ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "license": "MIT", + "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -13785,6 +13807,7 @@ "resolved": "https://registry.npmjs.org/@midnight-ntwrk/midnight-js-level-private-state-provider/-/midnight-js-level-private-state-provider-2.1.0.tgz", "integrity": "sha512-sTRbDu1LVoghoYMOfsKmwUMhKIO56lDj+N9YGUdLE0M4HgL5Q7xi0Md7JzZmeOCQEyStVNCae1gTdEFsUD4MCQ==", "license": "MIT", + "peer": true, "dependencies": { "@midnight-ntwrk/midnight-js-types": "2.1.0", "abstract-level": "^3.0.0", @@ -13801,6 +13824,7 @@ "resolved": "https://registry.npmjs.org/@midnight-ntwrk/midnight-js-types/-/midnight-js-types-2.1.0.tgz", "integrity": "sha512-bU7f2gdXt1/BR2XatKNPx9o6FDq1exYcWLQg4kn78d1SX39OVD33PVOavg2mFMoeC00YpcG2T1xH0eD/CMwXgA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "rxjs": "^7.5.0" } @@ -13823,7 +13847,8 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@midnight-ntwrk/midnight-js-level-private-state-provider/node_modules/buffer": { "version": "6.0.3", @@ -13844,6 +13869,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" @@ -13881,6 +13907,7 @@ "resolved": "https://registry.npmjs.org/@midnight-ntwrk/wallet-api/-/wallet-api-5.0.0.tgz", "integrity": "sha512-L5Z9+v+ouqTtPLoXpngtBVHZ0SmC3zLrCZbuYnd/ul6p9UbwUQ3AQeqMhclv2jhwFRNYsL0fBOqpD5dvs4SvLQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@midnight-ntwrk/zswap": "^4.0.0" }, @@ -13891,7 +13918,8 @@ "node_modules/@midnight-ntwrk/zswap": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@midnight-ntwrk/zswap/-/zswap-4.0.0.tgz", - "integrity": "sha512-yKfzp4M/wUkqxUJv1D2SizojYdWYnF3FDeKaxPehLPOFC4BrR9KnLZsNR2Y/occ8a4yFDtQXf7bN1QvK5k7Grw==" + "integrity": "sha512-yKfzp4M/wUkqxUJv1D2SizojYdWYnF3FDeKaxPehLPOFC4BrR9KnLZsNR2Y/occ8a4yFDtQXf7bN1QvK5k7Grw==", + "peer": true }, "node_modules/@multiformats/dns": { "version": "1.0.11", @@ -14347,6 +14375,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=14" } @@ -14462,31 +14491,36 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -14496,31 +14530,36 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@radix-ui/primitive": { "version": "1.1.3", @@ -16178,7 +16217,6 @@ "integrity": "sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", @@ -16291,7 +16329,6 @@ "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", @@ -16377,6 +16414,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=10" } @@ -16394,6 +16432,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=10" } @@ -16411,6 +16450,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -16428,6 +16468,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -16445,6 +16486,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -16462,6 +16504,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -16479,6 +16522,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -16496,6 +16540,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=10" } @@ -16513,6 +16558,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=10" } @@ -16530,6 +16576,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=10" } @@ -16752,7 +16799,8 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/benchmark/-/benchmark-2.1.5.tgz", "integrity": "sha512-cKio2eFB3v7qmKcvIHLUMw/dIx/8bhWPuzpzRT4unCPRTD8VdA9Zb0afxpcxOqR4PixRS7yT42FqGS8BYL8g1w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/blake2b": { "version": "2.1.3", @@ -16979,7 +17027,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.2.tgz", "integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -17025,7 +17072,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -17159,7 +17205,6 @@ "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "7.18.0", @@ -17194,7 +17239,6 @@ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", @@ -17824,6 +17868,7 @@ "resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz", "integrity": "sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -17836,6 +17881,7 @@ "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.4.tgz", "integrity": "sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -17848,6 +17894,7 @@ "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.7.tgz", "integrity": "sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -17860,6 +17907,7 @@ "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.5.0.tgz", "integrity": "sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -17897,6 +17945,7 @@ "resolved": "https://registry.npmjs.org/abstract-level/-/abstract-level-3.1.1.tgz", "integrity": "sha512-CW2gKbJFTuX1feMvOrvsVMmijAOgI9kg2Ie9Dq3gOcMt/dVVoVmqNlLcEUCT13NxHFMEajcUcVBIplbyDroDiw==", "license": "MIT", + "peer": true, "dependencies": { "buffer": "^6.0.3", "is-buffer": "^2.0.5", @@ -17927,7 +17976,8 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/abstract-level/node_modules/buffer": { "version": "6.0.3", @@ -17948,6 +17998,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" @@ -17958,7 +18009,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -18523,7 +18573,6 @@ "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", @@ -18684,7 +18733,6 @@ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", "license": "Apache-2.0", - "peer": true, "peerDependencies": { "bare-abort-controller": "*" }, @@ -19304,6 +19352,7 @@ "resolved": "https://registry.npmjs.org/browser-level/-/browser-level-3.0.0.tgz", "integrity": "sha512-kGXtLh29jMwqKaskz5xeDLtCtN1KBz/DbQSqmvH7QdJiyGRC7RAM8PPg6gvUiNMa+wVnaxS9eSmEtP/f5ajOVw==", "license": "MIT", + "peer": true, "dependencies": { "abstract-level": "^3.1.0" } @@ -19477,7 +19526,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -20183,6 +20231,7 @@ "integrity": "sha512-yGy8j8LjPbN0Bh3+ygmyYvrmskVita92pD/zCoalfcC9XxZj6iDtZTAnz+ot7GG8p9KLTG+MZ84tSA4AhkgVZQ==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "abstract-level": "^3.1.0", "module-error": "^1.0.1", @@ -20528,6 +20577,7 @@ "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", "license": "MIT", + "peer": true, "dependencies": { "is-what": "^5.2.0" }, @@ -20852,8 +20902,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -21857,7 +21906,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -21926,7 +21974,6 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -22097,7 +22144,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -23169,7 +23215,8 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-6.0.0.tgz", "integrity": "sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fflate": { "version": "0.7.4", @@ -24165,6 +24212,7 @@ "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.1.0" }, @@ -24545,6 +24593,7 @@ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "react-is": "^16.7.0" } @@ -24553,7 +24602,8 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/hosted-git-info": { "version": "2.8.9", @@ -24887,6 +24937,7 @@ "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.22.tgz", "integrity": "sha512-FHCCztTkHoV9mdBsHpocLpdTAfh956ZQcIkWQxxS0U5HT53vtrcuYdQneEJKH6xILaLNzXVl2Cvwtoy8XNN0AA==", "license": "MIT", + "peer": true, "peerDependencies": { "fp-ts": "^2.5.0" } @@ -25073,6 +25124,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=4" } @@ -25548,6 +25600,7 @@ "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -25718,7 +25771,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -26330,7 +26382,6 @@ "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -26414,7 +26465,6 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -26658,6 +26708,7 @@ "resolved": "https://registry.npmjs.org/level/-/level-10.0.0.tgz", "integrity": "sha512-aZJvdfRr/f0VBbSRF5C81FHON47ZsC2TkGxbBezXpGGXAUEL/s6+GP73nnhAYRSCIqUNsmJjfeOF4lzRDKbUig==", "license": "MIT", + "peer": true, "dependencies": { "abstract-level": "^3.1.0", "browser-level": "^3.0.0", @@ -26676,6 +26727,7 @@ "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-6.2.0.tgz", "integrity": "sha512-QNxVXP0IRnBmMsJIh+sb2kwNCYcKciQZJEt+L1hPCHrKNELllXhvrlClVHXBYZVT+a7aTSM6StgNXdAldoab3w==", "license": "MIT", + "peer": true, "engines": { "node": ">=16" } @@ -26685,6 +26737,7 @@ "resolved": "https://registry.npmjs.org/level-transcoder/-/level-transcoder-1.0.1.tgz", "integrity": "sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w==", "license": "MIT", + "peer": true, "dependencies": { "buffer": "^6.0.3", "module-error": "^1.0.1" @@ -26711,7 +26764,8 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/level-transcoder/node_modules/buffer": { "version": "6.0.3", @@ -26732,6 +26786,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" @@ -26950,7 +27005,8 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/longest-streak": { "version": "3.1.0", @@ -27168,6 +27224,7 @@ "resolved": "https://registry.npmjs.org/maybe-combine-errors/-/maybe-combine-errors-1.0.0.tgz", "integrity": "sha512-eefp6IduNPT6fVdwPp+1NgD0PML1NU5P6j1Mj5nz1nidX8/sWY7119WL8vTAHgqfsY74TzW0w1XPgdYEKkGZ5A==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" } @@ -28265,6 +28322,7 @@ "resolved": "https://registry.npmjs.org/module-error/-/module-error-1.0.2.tgz", "integrity": "sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" } @@ -28406,7 +28464,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "14.2.35", "@swc/helpers": "0.5.5", @@ -31045,7 +31102,6 @@ "version": "4.0.2", "inBundle": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -31467,6 +31523,7 @@ "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.18.1.tgz", "integrity": "sha512-mLXNwWPa9dgFyDqkNi54sjDyNJ9/fTI6WGBLgnXku1vdKY/jovHfZT5r+aiVeFFLOz+foPNOm5YJ4mqgld2GBQ==", "license": "MIT", + "peer": true, "dependencies": { "@wry/caches": "^1.0.0", "@wry/context": "^0.7.0", @@ -32148,7 +32205,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -32341,7 +32397,6 @@ "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -32547,6 +32602,7 @@ "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", "hasInstallScript": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -32952,7 +33008,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -32965,7 +33020,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -33504,6 +33558,7 @@ "resolved": "https://registry.npmjs.org/rehackt/-/rehackt-0.1.0.tgz", "integrity": "sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw==", "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "*", "react": "*" @@ -33801,7 +33856,6 @@ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -33907,7 +33961,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -34040,6 +34093,12 @@ "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", "license": "BlueOak-1.0.0" }, + "node_modules/scalus": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/scalus/-/scalus-0.15.0.tgz", + "integrity": "sha512-4yWMpHkGj89/VUUn18kXZHjUHdJDSw2Se9E1Aa3xmNWSe5YdWyGclpRlqfih5mTFuQik9/vt0fgrh3HaEUVqUw==", + "license": "Apache-2.0" + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -35110,6 +35169,7 @@ "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", "license": "MIT", + "peer": true, "dependencies": { "copy-anything": "^4" }, @@ -35146,7 +35206,6 @@ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.46.0.tgz", "integrity": "sha512-ZhLtvroYxUxr+HQJfMZEDRsGsmU46x12RvAv/zi9584f5KOX7bUrEbhPJ7cKFmUvZTJXi/CFZUYwDC6M1FigPw==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -35288,6 +35347,7 @@ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10" } @@ -35332,7 +35392,6 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -35462,7 +35521,6 @@ "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -35674,7 +35732,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -35866,6 +35923,7 @@ "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", "integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.1.0" }, @@ -35961,7 +36019,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -36490,7 +36547,6 @@ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -37118,7 +37174,6 @@ "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -37753,7 +37808,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -38278,7 +38332,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -38467,13 +38520,15 @@ "version": "0.8.15", "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/zen-observable-ts": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz", "integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==", "license": "MIT", + "peer": true, "dependencies": { "zen-observable": "0.8.15" } @@ -39910,7 +39965,6 @@ "resolved": "https://registry.npmjs.org/@harmoniclabs/cbor/-/cbor-1.6.0.tgz", "integrity": "sha512-KI25p8pHI1rmFZC9NYSxATwlCZ+KJdjydpptKebHcw03Iy7M+E8mF+hSnN5dTbS45xw5ZyKUgPLRgLo1sTuIoQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@harmoniclabs/bytestring": "^1.0.0", "@harmoniclabs/obj-utils": "^1.0.0", @@ -40029,10 +40083,12 @@ "@meshsdk/bitcoin": "1.9.0-beta.90", "@meshsdk/common": "1.9.0-beta.90", "@meshsdk/core-cst": "1.9.0-beta.90", + "@scure/base": "^1.2.6", "@utxorpc/sdk": "^0.6.7", "@utxorpc/spec": "^0.16.0", "axios": "^1.7.2", - "cbor": "^10.0.9" + "cbor": "^10.0.9", + "scalus": "^0.15.0" }, "devDependencies": { "@meshsdk/configs": "*", diff --git a/packages/mesh-provider/package.json b/packages/mesh-provider/package.json index 859b5b86e..deee6952a 100644 --- a/packages/mesh-provider/package.json +++ b/packages/mesh-provider/package.json @@ -40,8 +40,10 @@ "@meshsdk/core-cst": "1.9.0-beta.90", "@utxorpc/sdk": "^0.6.7", "@utxorpc/spec": "^0.16.0", + "@scure/base": "^1.2.6", "axios": "^1.7.2", - "cbor": "^10.0.9" + "cbor": "^10.0.9", + "scalus": "^0.15.0" }, "prettier": "@meshsdk/configs/prettier", "publishConfig": { diff --git a/packages/mesh-provider/src/index.ts b/packages/mesh-provider/src/index.ts index 3206e16ab..9faa7843c 100644 --- a/packages/mesh-provider/src/index.ts +++ b/packages/mesh-provider/src/index.ts @@ -8,3 +8,4 @@ export * from "./yaci"; export * from "./offline"; export * from "./kupo"; export * from "./multi-chain"; +export * from "./scalus"; diff --git a/packages/mesh-provider/src/scalus.ts b/packages/mesh-provider/src/scalus.ts new file mode 100644 index 000000000..35af88123 --- /dev/null +++ b/packages/mesh-provider/src/scalus.ts @@ -0,0 +1,349 @@ +import { + DEFAULT_PROTOCOL_PARAMETERS, + type AccountInfo, + type Action, + type Asset, + type AssetMetadata, + type BlockInfo, + type GovernanceProposalInfo, + type IEvaluator, + type IFetcher, + type IFetcherOptions, + type ISubmitter, + type Protocol, + type TransactionInfo, + type UTxO, +} from "@meshsdk/common"; +import cbor from "cbor"; +import { bech32 } from "@scure/base"; + +// Use `import type` for scalus types, `require()` at runtime since scalus is CJS +import type { Emulator, SlotConfig, SubmitResult } from "scalus"; + +// Scalus is CJS so we use dynamic import at construction time +let ScalusLib: typeof import("scalus") | undefined; + +/** + * Scalus Emulator provider for MeshJS. + * Implements IFetcher + ISubmitter + IEvaluator backed by a local Scalus Cardano emulator. + * + * Usage: + * ```ts + * import { ScalusProvider } from "@meshsdk/provider"; + * import { Emulator, SlotConfig } from "scalus"; + * + * const emulator = Emulator.withAddresses([aliceAddr], SlotConfig.preview); + * const provider = new ScalusProvider(emulator, SlotConfig.preview); + * const txBuilder = new MeshTxBuilder({ fetcher: provider, submitter: provider, evaluator: provider }); + * ``` + */ +export class ScalusProvider implements IFetcher, ISubmitter, IEvaluator { + private emulator: Emulator; + private slotConfig: SlotConfig; + private protocolParams: Protocol; + private costModels: number[][]; + + constructor( + emulator: Emulator, + slotConfig: SlotConfig, + options?: { + protocolParams?: Protocol; + costModels?: { PlutusV1?: number[]; PlutusV2?: number[]; PlutusV3?: number[] }; + }, + ) { + this.emulator = emulator; + this.slotConfig = slotConfig; + this.protocolParams = options?.protocolParams ?? DEFAULT_PROTOCOL_PARAMETERS; + this.costModels = [ + options?.costModels?.PlutusV1 ?? [], + options?.costModels?.PlutusV2 ?? [], + options?.costModels?.PlutusV3 ?? [], + ]; + + // Eagerly load the scalus module + if (!ScalusLib) { + // eslint-disable-next-line @typescript-eslint/no-require-imports + ScalusLib = require("scalus") as typeof import("scalus"); + } + } + + // --------------------------------------------------------------------------- + // IFetcher + // --------------------------------------------------------------------------- + + async fetchAddressUTxOs(address: string, asset?: string): Promise { + const entries = this.emulator.getUtxosForAddress(address); + const utxos = entries.map((e) => decodeUtxoEntry(e, address)); + if (asset) { + return utxos.filter((u) => + u.output.amount.some((a) => a.unit === asset), + ); + } + return utxos; + } + + async fetchUTxOs(hash: string, index?: number): Promise { + const allEntries = this.emulator.getAllUtxos(); + const utxos: UTxO[] = []; + for (const entry of allEntries) { + const utxo = decodeUtxoEntry(entry); + if (utxo.input.txHash === hash) { + if (index === undefined || utxo.input.outputIndex === index) { + utxos.push(utxo); + } + } + } + return utxos; + } + + async fetchProtocolParameters(_epoch: number): Promise { + return this.protocolParams; + } + + // --- Unsupported IFetcher methods (emulator doesn't track this data) --- + + async fetchAccountInfo(_address: string): Promise { + throw new Error("fetchAccountInfo not supported by ScalusProvider"); + } + + async fetchAddressTxs( + _address: string, + _options?: IFetcherOptions, + ): Promise { + throw new Error("fetchAddressTxs not supported by ScalusProvider"); + } + + async fetchAssetAddresses( + _asset: string, + ): Promise<{ address: string; quantity: string }[]> { + throw new Error("fetchAssetAddresses not supported by ScalusProvider"); + } + + async fetchAssetMetadata(_asset: string): Promise { + throw new Error("fetchAssetMetadata not supported by ScalusProvider"); + } + + async fetchBlockInfo(_hash: string): Promise { + throw new Error("fetchBlockInfo not supported by ScalusProvider"); + } + + async fetchCollectionAssets( + _policyId: string, + _cursor?: number | string, + ): Promise<{ assets: Asset[]; next?: string | number | null }> { + throw new Error("fetchCollectionAssets not supported by ScalusProvider"); + } + + async fetchTxInfo(_hash: string): Promise { + throw new Error("fetchTxInfo not supported by ScalusProvider"); + } + + async fetchGovernanceProposal( + _txHash: string, + _certIndex: number, + ): Promise { + throw new Error("fetchGovernanceProposal not supported by ScalusProvider"); + } + + async get(_url: string): Promise { + throw new Error("get not supported by ScalusProvider"); + } + + // --------------------------------------------------------------------------- + // ISubmitter + // --------------------------------------------------------------------------- + + async submitTx(tx: string): Promise { + const txBytes = hexToBytes(tx); + const result: SubmitResult = this.emulator.submitTx(txBytes); + if (!result.isSuccess) { + const logs = result.logs?.join("\n") ?? ""; + throw new Error( + `Transaction rejected: ${result.error}${logs ? `\nLogs:\n${logs}` : ""}`, + ); + } + return result.txHash!; + } + + // --------------------------------------------------------------------------- + // IEvaluator + // --------------------------------------------------------------------------- + + async evaluateTx( + tx: string, + additionalUtxos?: UTxO[], + ): Promise[]> { + const txBytes = hexToBytes(tx); + + const utxoMapBytes = this.emulator.getUtxosCbor(); + + const scalusSlotConfig = new ScalusLib!.SlotConfig( + this.slotConfig.slotToTime(0), + 0, + 1000, + ); + + const redeemers = ScalusLib!.Scalus.evalPlutusScripts( + txBytes, + utxoMapBytes, + scalusSlotConfig, + this.costModels, + ); + + const tagMap: Record = { + Spend: "SPEND", + Mint: "MINT", + Cert: "CERT", + Reward: "REWARD", + Voting: "VOTE", + Proposing: "PROPOSE", + }; + + return redeemers.map( + (r): Omit => ({ + tag: tagMap[r.tag] || "SPEND", + index: r.index, + budget: { + mem: Number(r.budget.memory), + steps: Number(r.budget.steps), + }, + }), + ); + } +} + +// --------------------------------------------------------------------------- +// CBOR Decoding Helpers +// --------------------------------------------------------------------------- + +function hexToBytes(hex: string): Uint8Array { + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); + } + return bytes; +} + +function bytesToHex(bytes: Uint8Array | Buffer): string { + return Buffer.from(bytes).toString("hex"); +} + +/** + * Convert raw Cardano address bytes to bech32 string. + * Header byte determines address type and network. + */ +function addressBytesToBech32(addrBytes: Uint8Array): string { + const header = addrBytes[0]!; + const networkId = header & 0x0f; + const prefix = + networkId === 1 + ? "addr" // mainnet + : "addr_test"; // testnet + + // bech32 encode the full address bytes (header + payload) + const words = bech32.toWords(addrBytes); + return bech32.encode(prefix, words, 1023); +} + +/** + * Decode a Cardano CBOR value field into Asset[]. + * Value is either: uint (lovelace only) or [uint, multiasset_map] + */ +function decodeValue(value: unknown): Asset[] { + if (typeof value === "number" || typeof value === "bigint") { + return [{ unit: "lovelace", quantity: String(value) }]; + } + if (Array.isArray(value)) { + const [lovelace, multiAsset] = value; + const assets: Asset[] = [ + { unit: "lovelace", quantity: String(lovelace) }, + ]; + if (multiAsset instanceof Map) { + for (const [policyId, assetMap] of multiAsset) { + const policyHex = bytesToHex(policyId as Uint8Array); + if (assetMap instanceof Map) { + for (const [assetName, quantity] of assetMap) { + const nameHex = bytesToHex(assetName as Uint8Array); + assets.push({ + unit: policyHex + nameHex, + quantity: String(quantity), + }); + } + } + } + } + return assets; + } + return [{ unit: "lovelace", quantity: "0" }]; +} + +/** + * Decode a single CBOR-encoded UTxO entry (Map with one key-value pair) + * from the Scalus emulator into a MeshJS UTxO. + * + * @param cborBytes - CBOR encoded Map[TransactionInput, TransactionOutput] + * @param knownAddress - If provided, skip address decoding (optimization for fetchAddressUTxOs) + */ +function decodeUtxoEntry(cborBytes: Uint8Array, knownAddress?: string): UTxO { + const decoded = cbor.decode(cborBytes) as Map; + const entry = Array.from(decoded.entries())[0]!; + const [txIn, txOut] = entry; + + // Decode TransactionInput: [hash_bytes, index] + const txInArr = txIn as [Uint8Array, number]; + const txHash = bytesToHex(txInArr[0]); + const outputIndex = txInArr[1]; + + // Decode TransactionOutput (Babbage era uses Map format) + let address: string; + let amount: Asset[]; + let dataHash: string | undefined; + let plutusData: string | undefined; + let scriptRef: string | undefined; + + if (txOut instanceof Map) { + // Babbage-era map format: {0: address, 1: value, ?2: datumOption, ?3: scriptRef} + const addrBytes = txOut.get(0) as Uint8Array; + address = knownAddress ?? addressBytesToBech32(addrBytes); + amount = decodeValue(txOut.get(1)); + + const datumOption = txOut.get(2); + if (datumOption != null && Array.isArray(datumOption)) { + const [tag, datum] = datumOption; + if (tag === 0) { + // DatumHash + dataHash = bytesToHex(datum as Uint8Array); + } else if (tag === 1) { + // Inline datum — encode back to CBOR hex + plutusData = bytesToHex(cbor.encode(datum)); + } + } + + const scriptRefVal = txOut.get(3); + if (scriptRefVal != null) { + // ScriptRef is CBOR-tagged, encode back to hex + scriptRef = bytesToHex(cbor.encode(scriptRefVal)); + } + } else if (Array.isArray(txOut)) { + // Shelley-era array format: [address, value, ?datumHash] + const addrBytes = txOut[0] as Uint8Array; + address = knownAddress ?? addressBytesToBech32(addrBytes); + amount = decodeValue(txOut[1]); + if (txOut[2]) { + dataHash = bytesToHex(txOut[2] as Uint8Array); + } + } else { + throw new Error("Unexpected TransactionOutput format"); + } + + return { + input: { txHash, outputIndex }, + output: { + address: address!, + amount, + dataHash, + plutusData, + scriptRef, + }, + }; +} From c8d915977c9cd403e5a219721e67ec5f0517bd01 Mon Sep 17 00:00:00 2001 From: twwu123 Date: Wed, 22 Apr 2026 10:33:11 +0800 Subject: [PATCH 02/11] add emulator to common package --- package-lock.json | 58 ++++++----- packages/configs/typescript/base.json | 3 +- packages/mesh-core-cst/src/utils/converter.ts | 38 ++++++++ packages/mesh-core/package.json | 3 +- packages/mesh-core/src/utils/emulator.ts | 96 +++++++++++++++++++ packages/mesh-core/src/utils/serializer.ts | 3 - 6 files changed, 174 insertions(+), 27 deletions(-) create mode 100644 packages/mesh-core/src/utils/emulator.ts diff --git a/package-lock.json b/package-lock.json index e9018f9a0..1f6da5791 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14943,13 +14943,14 @@ }, "packages/mesh-common": { "name": "@meshsdk/common", - "version": "1.9.0-beta.101", + "version": "1.9.0-beta.102", "license": "Apache-2.0", "dependencies": { "bech32": "^2.0.0", "bip39": "3.1.0", "blake2b": "^2.1.4", - "blakejs": "^1.2.1" + "blakejs": "^1.2.1", + "scalus": "^0.15.0" }, "devDependencies": { "@meshsdk/configs": "*", @@ -14959,13 +14960,19 @@ "typescript": "^5.3.3" } }, + "packages/mesh-common/node_modules/scalus": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/scalus/-/scalus-0.15.0.tgz", + "integrity": "sha512-4yWMpHkGj89/VUUn18kXZHjUHdJDSw2Se9E1Aa3xmNWSe5YdWyGclpRlqfih5mTFuQik9/vt0fgrh3HaEUVqUw==", + "license": "Apache-2.0" + }, "packages/mesh-contract": { "name": "@meshsdk/contract", - "version": "1.9.0-beta.101", + "version": "1.9.0-beta.102", "license": "Apache-2.0", "dependencies": { - "@meshsdk/common": "1.9.0-beta.101", - "@meshsdk/core": "1.9.0-beta.101" + "@meshsdk/common": "1.9.0-beta.102", + "@meshsdk/core": "1.9.0-beta.102" }, "devDependencies": { "@meshsdk/configs": "*", @@ -14976,14 +14983,15 @@ }, "packages/mesh-core": { "name": "@meshsdk/core", - "version": "1.9.0-beta.101", + "version": "1.9.0-beta.102", "license": "Apache-2.0", "dependencies": { - "@meshsdk/common": "1.9.0-beta.101", - "@meshsdk/core-cst": "1.9.0-beta.101", + "@meshsdk/common": "1.9.0-beta.102", + "@meshsdk/core-cst": "1.9.0-beta.102", "@meshsdk/provider": "1.9.0-beta.100", - "@meshsdk/transaction": "1.9.0-beta.101", - "@meshsdk/wallet": "1.9.0-beta.101" + "@meshsdk/transaction": "1.9.0-beta.102", + "@meshsdk/wallet": "1.9.0-beta.102", + "scalus": "^0.15.0" }, "devDependencies": { "@meshsdk/configs": "*", @@ -14994,10 +15002,10 @@ }, "packages/mesh-core-csl": { "name": "@meshsdk/core-csl", - "version": "1.9.0-beta.101", + "version": "1.9.0-beta.102", "license": "Apache-2.0", "dependencies": { - "@meshsdk/common": "1.9.0-beta.101", + "@meshsdk/common": "1.9.0-beta.102", "@sidan-lab/whisky-js-browser": "^1.0.11", "@sidan-lab/whisky-js-nodejs": "^1.0.11", "@types/base32-encoding": "^1.0.2", @@ -15016,7 +15024,7 @@ }, "packages/mesh-core-cst": { "name": "@meshsdk/core-cst", - "version": "1.9.0-beta.101", + "version": "1.9.0-beta.102", "license": "Apache-2.0", "dependencies": { "@cardano-sdk/core": "0.46.12", @@ -15027,7 +15035,7 @@ "@harmoniclabs/pair": "^1.0.0", "@harmoniclabs/plutus-data": "1.2.6", "@harmoniclabs/uplc": "1.4.1", - "@meshsdk/common": "1.9.0-beta.101", + "@meshsdk/common": "1.9.0-beta.102", "@types/base32-encoding": "^1.0.2", "base32-encoding": "^1.0.0", "bech32": "^2.0.0", @@ -15046,16 +15054,22 @@ "typescript": "^5.3.3" } }, + "packages/mesh-core/node_modules/scalus": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/scalus/-/scalus-0.15.0.tgz", + "integrity": "sha512-4yWMpHkGj89/VUUn18kXZHjUHdJDSw2Se9E1Aa3xmNWSe5YdWyGclpRlqfih5mTFuQik9/vt0fgrh3HaEUVqUw==", + "license": "Apache-2.0" + }, "packages/mesh-transaction": { "name": "@meshsdk/transaction", - "version": "1.9.0-beta.101", + "version": "1.9.0-beta.102", "license": "Apache-2.0", "dependencies": { "@cardano-sdk/core": "0.46.12", "@cardano-sdk/input-selection": "0.14.28", "@cardano-sdk/util": "0.17.1", - "@meshsdk/common": "1.9.0-beta.101", - "@meshsdk/core-cst": "1.9.0-beta.101", + "@meshsdk/common": "1.9.0-beta.102", + "@meshsdk/core-cst": "1.9.0-beta.102", "json-bigint": "^1.0.0" }, "devDependencies": { @@ -15068,12 +15082,12 @@ }, "packages/mesh-wallet": { "name": "@meshsdk/wallet", - "version": "1.9.0-beta.101", + "version": "1.9.0-beta.102", "license": "Apache-2.0", "dependencies": { - "@meshsdk/common": "1.9.0-beta.101", - "@meshsdk/core-cst": "1.9.0-beta.101", - "@meshsdk/transaction": "1.9.0-beta.101", + "@meshsdk/common": "1.9.0-beta.102", + "@meshsdk/core-cst": "1.9.0-beta.102", + "@meshsdk/transaction": "1.9.0-beta.102", "@simplewebauthn/browser": "^13.0.0" }, "devDependencies": { @@ -15086,7 +15100,7 @@ }, "scripts/mesh-cli": { "name": "meshjs", - "version": "1.9.0-beta.101", + "version": "1.9.0-beta.102", "license": "Apache-2.0", "dependencies": { "@sidan-lab/cardano-bar": "^0.0.7", diff --git a/packages/configs/typescript/base.json b/packages/configs/typescript/base.json index 1f621d565..bbf716503 100644 --- a/packages/configs/typescript/base.json +++ b/packages/configs/typescript/base.json @@ -23,6 +23,7 @@ "sourceMap": true, "allowJs": true, "allowSyntheticDefaultImports": true, - "noUnusedLocals": false + "noUnusedLocals": false, + "types": ["node", "jest"] } } \ No newline at end of file diff --git a/packages/mesh-core-cst/src/utils/converter.ts b/packages/mesh-core-cst/src/utils/converter.ts index 48f170d82..36449e2fb 100644 --- a/packages/mesh-core-cst/src/utils/converter.ts +++ b/packages/mesh-core-cst/src/utils/converter.ts @@ -466,3 +466,41 @@ export const toPlutusLanguageVersion = ( return PlutusLanguageVersion.V3; } }; + +export const utxosToCborMap = (utxos: UTxO[]): string => { + const cborWriter = new Serialization.CborWriter(); + cborWriter.writeStartMap(utxos.length); + for (const utxo of utxos) { + const cardanoUtxo = toTxUnspentOutput(utxo); + cborWriter.writeEncodedValue( + Buffer.from(cardanoUtxo.input().toCbor(), "hex"), + ); + cborWriter.writeEncodedValue( + Buffer.from(cardanoUtxo.output().toCbor(), "hex"), + ); + } + return cborWriter.encodeAsHex(); +}; + +export const cborMapToUtxos = (cborMaps: string[]): UTxO[] => { + const utxos: UTxO[] = []; + for (const cborMap of cborMaps) { + const cborReader = new Serialization.CborReader( + Buffer.from(cborMap, "hex"), + ); + const mapLength = cborReader.readStartMap(); + if (!mapLength) { + throw new Error("Invalid CBOR map: expected a map of UTxOs"); + } + for (let i = 0; i < mapLength; i++) { + const inputCbor = cborReader.readEncodedValue(); + const outputCbor = cborReader.readEncodedValue(); + const utxo = Serialization.TransactionUnspentOutput.fromCore([ + Serialization.TransactionInput.fromCbor(inputCbor).toCore(), + Serialization.TransactionOutput.fromCbor(outputCbor).toCore(), + ]); + utxos.push(fromTxUnspentOutput(utxo)); + } + } + return utxos; +}; diff --git a/packages/mesh-core/package.json b/packages/mesh-core/package.json index ac44f6983..366d370b5 100644 --- a/packages/mesh-core/package.json +++ b/packages/mesh-core/package.json @@ -37,7 +37,8 @@ "@meshsdk/core-cst": "1.9.0-beta.102", "@meshsdk/provider": "1.9.0-beta.100", "@meshsdk/transaction": "1.9.0-beta.102", - "@meshsdk/wallet": "1.9.0-beta.102" + "@meshsdk/wallet": "1.9.0-beta.102", + "scalus": "^0.15.0" }, "prettier": "@meshsdk/configs/prettier", "publishConfig": { diff --git a/packages/mesh-core/src/utils/emulator.ts b/packages/mesh-core/src/utils/emulator.ts new file mode 100644 index 000000000..c51418daf --- /dev/null +++ b/packages/mesh-core/src/utils/emulator.ts @@ -0,0 +1,96 @@ +import { Emulator, SlotConfig as ScalusSlotConfig, SubmitResult } from "scalus"; + +import { + AccountInfo, + Asset, + AssetMetadata, + BlockInfo, + GovernanceProposalInfo, + IFetcher, + IFetcherOptions, + Protocol, + SlotConfig, + TransactionInfo, + UTxO, +} from "@meshsdk/common"; +import { cborMapToUtxos, utxosToCborMap } from "@meshsdk/core-cst"; +import { OfflineFetcher } from "@meshsdk/provider"; + +export class MeshEmulator extends Emulator implements IFetcher { + fetcher: OfflineFetcher; + + constructor(initialUtxos: UTxO[], slotConfig: SlotConfig) { + super( + Buffer.from(utxosToCborMap(initialUtxos), "hex"), + new ScalusSlotConfig( + slotConfig.zeroTime, + slotConfig.zeroSlot, + slotConfig.slotLength, + ), + ); + this.fetcher = new OfflineFetcher(); + this.fetcher.addUTxOs(initialUtxos); + } + fetchAccountInfo(address: string): Promise { + return this.fetcher.fetchAccountInfo(address); + } + fetchAddressUTxOs(address: string, asset?: string): Promise { + return this.fetcher.fetchAddressUTxOs(address, asset); + } + fetchAddressTxs( + address: string, + options?: IFetcherOptions, + ): Promise { + return this.fetcher.fetchAddressTxs(address, options); + } + fetchAssetAddresses( + asset: string, + ): Promise<{ address: string; quantity: string }[]> { + return this.fetcher.fetchAssetAddresses(asset); + } + fetchAssetMetadata(asset: string): Promise { + return this.fetcher.fetchAssetMetadata(asset); + } + fetchBlockInfo(hash: string): Promise { + return this.fetcher.fetchBlockInfo(hash); + } + fetchCollectionAssets( + policyId: string, + cursor?: number | string, + ): Promise<{ assets: Asset[]; next?: string | number | null }> { + return this.fetcher.fetchCollectionAssets(policyId, cursor); + } + fetchProtocolParameters(epoch: number): Promise { + return this.fetcher.fetchProtocolParameters(epoch); + } + fetchTxInfo(hash: string): Promise { + return this.fetcher.fetchTxInfo(hash); + } + fetchUTxOs(hash: string, index?: number): Promise { + return this.fetcher.fetchUTxOs(hash); + } + fetchGovernanceProposal( + txHash: string, + certIndex: number, + ): Promise { + return this.fetcher.fetchGovernanceProposal(txHash, certIndex); + } + get(url: string): Promise { + return this.fetcher.get(url); + } + + submitTxHex(txHex: string): SubmitResult { + const result = this.submitTx(Buffer.from(txHex, "hex")); + if (result.isSuccess) { + this.fetcher = new OfflineFetcher(); + this.fetcher.addUTxOs(this.getAllUtxosMesh()); + } + return result; + } + + getAllUtxosMesh() { + const allUtxos = this.getAllUtxos(); + const utxoList = allUtxos.map((u) => Buffer.from(u).toString("hex")); + return cborMapToUtxos(utxoList); + } +} diff --git a/packages/mesh-core/src/utils/serializer.ts b/packages/mesh-core/src/utils/serializer.ts index a7ff97c6b..f1e74925e 100644 --- a/packages/mesh-core/src/utils/serializer.ts +++ b/packages/mesh-core/src/utils/serializer.ts @@ -1,8 +1,5 @@ -import JSONBig from "json-bigint"; - import { BuilderData, - Data, DeserializedAddress, NativeScript, PlutusDataType, From dd9ee2d01045e7779a69cf415eb5cb6fbc4285bf Mon Sep 17 00:00:00 2001 From: otto Date: Fri, 24 Apr 2026 16:09:07 +0200 Subject: [PATCH 03/11] e2e tests --- packages/mesh-provider/src/scalus.ts | 30 +- .../mesh-provider/test/scalus/e2e.test.ts | 647 ++++++++++++++++++ 2 files changed, 670 insertions(+), 7 deletions(-) create mode 100644 packages/mesh-provider/test/scalus/e2e.test.ts diff --git a/packages/mesh-provider/src/scalus.ts b/packages/mesh-provider/src/scalus.ts index 35af88123..ee169bf27 100644 --- a/packages/mesh-provider/src/scalus.ts +++ b/packages/mesh-provider/src/scalus.ts @@ -18,7 +18,7 @@ import cbor from "cbor"; import { bech32 } from "@scure/base"; // Use `import type` for scalus types, `require()` at runtime since scalus is CJS -import type { Emulator, SlotConfig, SubmitResult } from "scalus"; +import type { Emulator, Scalus, SlotConfig, SubmitResult } from "scalus"; // Scalus is CJS so we use dynamic import at construction time let ScalusLib: typeof import("scalus") | undefined; @@ -183,12 +183,28 @@ export class ScalusProvider implements IFetcher, ISubmitter, IEvaluator { 1000, ); - const redeemers = ScalusLib!.Scalus.evalPlutusScripts( - txBytes, - utxoMapBytes, - scalusSlotConfig, - this.costModels, - ); + let redeemers: Scalus.Redeemer[]; + try { + redeemers = ScalusLib!.Scalus.evalPlutusScripts( + txBytes, + utxoMapBytes, + scalusSlotConfig, + this.costModels, + ); + } catch (error) { + const msg = + error instanceof Error ? error.message : String(error); + const isScriptFailure = + msg.includes("script evaluation") || + msg.includes("PlutusScript") || + (error != null && + typeof error === "object" && + "logs" in error); + if (isScriptFailure) { + throw error; + } + return []; + } const tagMap: Record = { Spend: "SPEND", diff --git a/packages/mesh-provider/test/scalus/e2e.test.ts b/packages/mesh-provider/test/scalus/e2e.test.ts new file mode 100644 index 000000000..2a9b63b35 --- /dev/null +++ b/packages/mesh-provider/test/scalus/e2e.test.ts @@ -0,0 +1,647 @@ +import { + applyCborEncoding, + MeshTxBuilder, + resolveNativeScriptHash, + resolveNativeScriptHex, + resolvePaymentKeyHash, + resolveScriptHash, + NativeScript, +} from "@meshsdk/core"; +import { AppWallet } from "@meshsdk/wallet"; +import { ScalusProvider } from "@meshsdk/provider"; +import { Emulator, SlotConfig } from "scalus"; + +const TEST_MNEMONIC = [ + "solution", + "solution", + "solution", + "solution", + "solution", + "solution", + "solution", + "solution", + "solution", + "solution", + "solution", + "solution", + "solution", + "solution", + "solution", + "solution", + "solution", + "solution", + "solution", + "solution", + "solution", + "solution", + "solution", + "solution", +]; + +const alwaysSucceedCbor = applyCborEncoding( + "58340101002332259800a518a4d153300249011856616c696461746f722072657475726e65642066616c736500136564004ae715cd01", +); + +async function createTestSetup(lovelacePerAddress = 10_000_000_000n) { + const slotConfig = SlotConfig.preview; + const wallet = new AppWallet({ + networkId: 0, + key: { type: "mnemonic", words: TEST_MNEMONIC }, + }); + await wallet.init(); + const address = wallet.getPaymentAddress(); + + const emulator = Emulator.withAddresses( + [address], + slotConfig, + lovelacePerAddress, + ); + const currentSlot = slotConfig.timeToSlot(Date.now()); + emulator.setSlot(currentSlot); + + const provider = new ScalusProvider(emulator, slotConfig); + + const newTxBuilder = () => + new MeshTxBuilder({ + fetcher: provider, + submitter: provider, + evaluator: provider, + }); + + return { wallet, address, provider, emulator, slotConfig, newTxBuilder }; +} + +describe("ScalusProvider", () => { + describe("Basic payment lifecycle", () => { + it("should build, sign, submit and confirm a simple payment", async () => { + const { wallet, address, provider, newTxBuilder } = + await createTestSetup(); + + const utxos = await provider.fetchAddressUTxOs(address); + expect(utxos.length).toBeGreaterThan(0); + + const txHex = await newTxBuilder() + .txOut(address, [{ unit: "lovelace", quantity: "5000000" }]) + .changeAddress(address) + .selectUtxosFrom(utxos) + .complete(); + + const signedTx = await wallet.signTx(txHex); + const txHash = await provider.submitTx(signedTx); + expect(txHash).toBeDefined(); + expect(txHash.length).toBe(64); + }); + + it("should reflect UTxO changes after submission", async () => { + const { wallet, address, provider, newTxBuilder } = + await createTestSetup(); + + const utxosBefore = await provider.fetchAddressUTxOs(address); + const totalBefore = utxosBefore.reduce( + (sum, u) => + sum + + BigInt(u.output.amount.find((a) => a.unit === "lovelace")!.quantity), + 0n, + ); + + const txHex = await newTxBuilder() + .txOut(address, [{ unit: "lovelace", quantity: "3000000" }]) + .changeAddress(address) + .selectUtxosFrom(utxosBefore) + .complete(); + + const signedTx = await wallet.signTx(txHex); + await provider.submitTx(signedTx); + + const utxosAfter = await provider.fetchAddressUTxOs(address); + const totalAfter = utxosAfter.reduce( + (sum, u) => + sum + + BigInt(u.output.amount.find((a) => a.unit === "lovelace")!.quantity), + 0n, + ); + + expect(utxosAfter.length).toBeGreaterThan(0); + // Total should decrease by fees + expect(totalAfter).toBeLessThan(totalBefore); + expect(totalAfter).toBeGreaterThan(totalBefore - 1_000_000n); + }); + + it("should chain multiple transactions", async () => { + const { wallet, address, provider, newTxBuilder } = + await createTestSetup(); + + const utxos1 = await provider.fetchAddressUTxOs(address); + const txHex1 = await newTxBuilder() + .txOut(address, [{ unit: "lovelace", quantity: "2000000" }]) + .changeAddress(address) + .selectUtxosFrom(utxos1) + .complete(); + const signed1 = await wallet.signTx(txHex1); + const hash1 = await provider.submitTx(signed1); + + const utxos2 = await provider.fetchAddressUTxOs(address); + const txHex2 = await newTxBuilder() + .txOut(address, [{ unit: "lovelace", quantity: "2000000" }]) + .changeAddress(address) + .selectUtxosFrom(utxos2) + .complete(); + const signed2 = await wallet.signTx(txHex2); + const hash2 = await provider.submitTx(signed2); + + expect(hash1).not.toBe(hash2); + + const utxos3 = await provider.fetchAddressUTxOs(address); + expect(utxos3.length).toBeGreaterThan(0); + // Original UTxOs should be consumed + const utxo1Hashes = new Set(utxos1.map((u) => u.input.txHash)); + const utxo3Hashes = new Set(utxos3.map((u) => u.input.txHash)); + for (const h of utxo1Hashes) { + expect(utxo3Hashes.has(h)).toBe(false); + } + }); + }); + + describe("Native script minting", () => { + it("should mint tokens with a native script and verify UTxOs", async () => { + const { wallet, address, provider, newTxBuilder } = + await createTestSetup(); + + const keyHash = resolvePaymentKeyHash(address); + const nativeScript: NativeScript = { type: "sig", keyHash }; + const scriptCbor = resolveNativeScriptHex(nativeScript); + const policyId = resolveNativeScriptHash(nativeScript); + const tokenNameHex = Buffer.from("TestToken").toString("hex"); + const unit = policyId + tokenNameHex; + + const utxos = await provider.fetchAddressUTxOs(address); + + const txHex = await newTxBuilder() + .mint("1000", policyId, tokenNameHex) + .mintingScript(scriptCbor) + .txOut(address, [ + { unit: "lovelace", quantity: "2000000" }, + { unit, quantity: "1000" }, + ]) + .changeAddress(address) + .selectUtxosFrom(utxos) + .complete(); + + const signedTx = await wallet.signTx(txHex); + const txHash = await provider.submitTx(signedTx); + expect(txHash.length).toBe(64); + + const utxosAfter = await provider.fetchAddressUTxOs(address); + const tokenUtxo = utxosAfter.find((u) => + u.output.amount.some((a) => a.unit === unit), + ); + expect(tokenUtxo).toBeDefined(); + expect( + tokenUtxo!.output.amount.find((a) => a.unit === unit)!.quantity, + ).toBe("1000"); + }); + + it("should mint and then burn tokens", async () => { + const { wallet, address, provider, newTxBuilder } = + await createTestSetup(); + + const keyHash = resolvePaymentKeyHash(address); + const nativeScript: NativeScript = { type: "sig", keyHash }; + const scriptCbor = resolveNativeScriptHex(nativeScript); + const policyId = resolveNativeScriptHash(nativeScript); + const tokenNameHex = Buffer.from("BurnToken").toString("hex"); + const unit = policyId + tokenNameHex; + + // Step 1: Mint + const utxos = await provider.fetchAddressUTxOs(address); + const mintTxHex = await newTxBuilder() + .mint("1000", policyId, tokenNameHex) + .mintingScript(scriptCbor) + .txOut(address, [ + { unit: "lovelace", quantity: "2000000" }, + { unit, quantity: "1000" }, + ]) + .changeAddress(address) + .selectUtxosFrom(utxos) + .complete(); + + const signedMint = await wallet.signTx(mintTxHex); + await provider.submitTx(signedMint); + + // Step 2: Burn half + const utxosAfterMint = await provider.fetchAddressUTxOs(address); + const tokenUtxo = utxosAfterMint.find((u) => + u.output.amount.some((a) => a.unit === unit), + ); + expect(tokenUtxo).toBeDefined(); + + const burnTxHex = await newTxBuilder() + .mint("-500", policyId, tokenNameHex) + .mintingScript(scriptCbor) + .txIn( + tokenUtxo!.input.txHash, + tokenUtxo!.input.outputIndex, + tokenUtxo!.output.amount, + tokenUtxo!.output.address, + ) + .txOut(address, [ + { unit: "lovelace", quantity: "2000000" }, + { unit, quantity: "500" }, + ]) + .changeAddress(address) + .selectUtxosFrom( + utxosAfterMint.filter( + (u) => + u.input.txHash !== tokenUtxo!.input.txHash || + u.input.outputIndex !== tokenUtxo!.input.outputIndex, + ), + ) + .complete(); + + const signedBurn = await wallet.signTx(burnTxHex); + await provider.submitTx(signedBurn); + + // Step 3: Verify remaining + const utxosFinal = await provider.fetchAddressUTxOs(address); + let totalTokens = 0n; + for (const u of utxosFinal) { + const tokenAsset = u.output.amount.find((a) => a.unit === unit); + if (tokenAsset) totalTokens += BigInt(tokenAsset.quantity); + } + expect(totalTokens).toBe(500n); + }); + }); + + describe("Plutus script evaluation", () => { + it("should evaluate and submit a plutus minting transaction", async () => { + const { wallet, address, provider, newTxBuilder } = + await createTestSetup(); + + const policyId = resolveScriptHash(alwaysSucceedCbor, "V3"); + const tokenNameHex = Buffer.from("PlutusToken").toString("hex"); + const unit = policyId + tokenNameHex; + + const utxos = await provider.fetchAddressUTxOs(address); + + const txHex = await newTxBuilder() + .mintPlutusScriptV3() + .mint("100", policyId, tokenNameHex) + .mintRedeemerValue("") + .mintingScript(alwaysSucceedCbor) + .txInCollateral( + utxos[0]!.input.txHash, + utxos[0]!.input.outputIndex, + utxos[0]!.output.amount, + utxos[0]!.output.address, + ) + .txOut(address, [ + { unit: "lovelace", quantity: "2000000" }, + { unit, quantity: "100" }, + ]) + .changeAddress(address) + .selectUtxosFrom(utxos) + .complete(); + + const signedTx = await wallet.signTx(txHex); + const txHash = await provider.submitTx(signedTx); + expect(txHash.length).toBe(64); + + const utxosAfter = await provider.fetchAddressUTxOs(address); + const tokenUtxo = utxosAfter.find((u) => + u.output.amount.some((a) => a.unit === unit), + ); + expect(tokenUtxo).toBeDefined(); + expect( + tokenUtxo!.output.amount.find((a) => a.unit === unit)!.quantity, + ).toBe("100"); + }); + + it("should evaluate a plutus spending transaction", async () => { + const { wallet, address, provider, newTxBuilder, slotConfig } = + await createTestSetup(); + + const policyId = resolveScriptHash(alwaysSucceedCbor, "V3"); + const tokenNameHex = Buffer.from("SpendTest").toString("hex"); + const unit = policyId + tokenNameHex; + + const utxos = await provider.fetchAddressUTxOs(address); + + // Step 1: Mint tokens using plutus script + const mintTxHex = await newTxBuilder() + .mintPlutusScriptV3() + .mint("50", policyId, tokenNameHex) + .mintRedeemerValue("") + .mintingScript(alwaysSucceedCbor) + .txInCollateral( + utxos[0]!.input.txHash, + utxos[0]!.input.outputIndex, + utxos[0]!.output.amount, + utxos[0]!.output.address, + ) + .txOut(address, [ + { unit: "lovelace", quantity: "2000000" }, + { unit, quantity: "50" }, + ]) + .changeAddress(address) + .selectUtxosFrom(utxos) + .complete(); + + const signedMint = await wallet.signTx(mintTxHex); + const mintHash = await provider.submitTx(signedMint); + expect(mintHash.length).toBe(64); + + // Step 2: Verify the minted tokens via fetchUTxOs + const mintedUtxos = await provider.fetchUTxOs(mintHash); + expect(mintedUtxos.length).toBeGreaterThan(0); + const tokenOutput = mintedUtxos.find((u) => + u.output.amount.some((a) => a.unit === unit), + ); + expect(tokenOutput).toBeDefined(); + }); + }); + + describe("Validity intervals", () => { + it("should build and submit a transaction with TTL", async () => { + const { wallet, address, provider, newTxBuilder, slotConfig } = + await createTestSetup(); + + const currentSlot = slotConfig.timeToSlot(Date.now()); + const ttlSlot = currentSlot + 300; + + const utxos = await provider.fetchAddressUTxOs(address); + + const txHex = await newTxBuilder() + .txOut(address, [{ unit: "lovelace", quantity: "2000000" }]) + .invalidHereafter(ttlSlot) + .changeAddress(address) + .selectUtxosFrom(utxos) + .complete(); + + const signedTx = await wallet.signTx(txHex); + const txHash = await provider.submitTx(signedTx); + expect(txHash.length).toBe(64); + }); + + it("should build and submit with both validity bounds", async () => { + const { wallet, address, provider, newTxBuilder, slotConfig } = + await createTestSetup(); + + const currentSlot = slotConfig.timeToSlot(Date.now()); + const validFrom = currentSlot - 10; + const validTo = currentSlot + 300; + + const utxos = await provider.fetchAddressUTxOs(address); + + const txHex = await newTxBuilder() + .txOut(address, [{ unit: "lovelace", quantity: "2000000" }]) + .invalidBefore(validFrom) + .invalidHereafter(validTo) + .changeAddress(address) + .selectUtxosFrom(utxos) + .complete(); + + const signedTx = await wallet.signTx(txHex); + const txHash = await provider.submitTx(signedTx); + expect(txHash.length).toBe(64); + }); + + it("should reject an expired transaction", async () => { + const { wallet, address, provider, newTxBuilder, slotConfig } = + await createTestSetup(); + + const currentSlot = slotConfig.timeToSlot(Date.now()); + const expiredSlot = currentSlot - 100; + + const utxos = await provider.fetchAddressUTxOs(address); + + const txHex = await newTxBuilder() + .txOut(address, [{ unit: "lovelace", quantity: "2000000" }]) + .invalidHereafter(expiredSlot) + .changeAddress(address) + .selectUtxosFrom(utxos) + .complete(); + + const signedTx = await wallet.signTx(txHex); + await expect(provider.submitTx(signedTx)).rejects.toThrow(); + }); + + it("should reject a transaction before validity start", async () => { + const { wallet, address, provider, newTxBuilder, slotConfig } = + await createTestSetup(); + + const currentSlot = slotConfig.timeToSlot(Date.now()); + const futureStart = currentSlot + 300; + const futureTtl = currentSlot + 600; + + const utxos = await provider.fetchAddressUTxOs(address); + + const txHex = await newTxBuilder() + .txOut(address, [{ unit: "lovelace", quantity: "2000000" }]) + .invalidBefore(futureStart) + .invalidHereafter(futureTtl) + .changeAddress(address) + .selectUtxosFrom(utxos) + .complete(); + + const signedTx = await wallet.signTx(txHex); + await expect(provider.submitTx(signedTx)).rejects.toThrow(); + }); + }); + + describe("Transaction composition", () => { + it("should build a transaction with multiple outputs", async () => { + const { wallet, address, provider, newTxBuilder } = + await createTestSetup(); + + const utxos = await provider.fetchAddressUTxOs(address); + + const txHex = await newTxBuilder() + .txOut(address, [{ unit: "lovelace", quantity: "2000000" }]) + .txOut(address, [{ unit: "lovelace", quantity: "3000000" }]) + .txOut(address, [{ unit: "lovelace", quantity: "4000000" }]) + .changeAddress(address) + .selectUtxosFrom(utxos) + .complete(); + + const signedTx = await wallet.signTx(txHex); + const txHash = await provider.submitTx(signedTx); + expect(txHash.length).toBe(64); + + const utxosAfter = await provider.fetchAddressUTxOs(address); + const amounts = utxosAfter.map((u) => + BigInt(u.output.amount.find((a) => a.unit === "lovelace")!.quantity), + ); + expect(amounts.filter((a) => a === 2_000_000n).length).toBeGreaterThan(0); + expect(amounts.filter((a) => a === 3_000_000n).length).toBeGreaterThan(0); + expect(amounts.filter((a) => a === 4_000_000n).length).toBeGreaterThan(0); + }); + + it("should combine minting and payment in a single transaction", async () => { + const { wallet, address, provider, newTxBuilder } = + await createTestSetup(); + + const keyHash = resolvePaymentKeyHash(address); + const nativeScript: NativeScript = { type: "sig", keyHash }; + const scriptCbor = resolveNativeScriptHex(nativeScript); + const policyId = resolveNativeScriptHash(nativeScript); + const tokenNameHex = Buffer.from("ComboToken").toString("hex"); + const unit = policyId + tokenNameHex; + + const utxos = await provider.fetchAddressUTxOs(address); + + const txHex = await newTxBuilder() + .mint("777", policyId, tokenNameHex) + .mintingScript(scriptCbor) + .txOut(address, [ + { unit: "lovelace", quantity: "5000000" }, + { unit, quantity: "777" }, + ]) + .txOut(address, [{ unit: "lovelace", quantity: "3000000" }]) + .changeAddress(address) + .selectUtxosFrom(utxos) + .complete(); + + const signedTx = await wallet.signTx(txHex); + const txHash = await provider.submitTx(signedTx); + expect(txHash.length).toBe(64); + + const utxosAfter = await provider.fetchAddressUTxOs(address); + const tokenUtxo = utxosAfter.find((u) => + u.output.amount.some((a) => a.unit === unit), + ); + expect(tokenUtxo).toBeDefined(); + expect( + tokenUtxo!.output.amount.find((a) => a.unit === unit)!.quantity, + ).toBe("777"); + }); + + it("should build a transaction with metadata", async () => { + const { wallet, address, provider, newTxBuilder } = + await createTestSetup(); + + const utxos = await provider.fetchAddressUTxOs(address); + + const txHex = await newTxBuilder() + .txOut(address, [{ unit: "lovelace", quantity: "2000000" }]) + .metadataValue(674, "Hello from ScalusProvider test") + .changeAddress(address) + .selectUtxosFrom(utxos) + .complete(); + + const signedTx = await wallet.signTx(txHex); + const txHash = await provider.submitTx(signedTx); + expect(txHash.length).toBe(64); + }); + }); + + describe("Error handling", () => { + it("should reject a transaction with insufficient funds", async () => { + const { wallet, address, provider, newTxBuilder } = + await createTestSetup(5_000_000n); + + const utxos = await provider.fetchAddressUTxOs(address); + + await expect( + newTxBuilder() + .txOut(address, [{ unit: "lovelace", quantity: "100000000000" }]) + .changeAddress(address) + .selectUtxosFrom(utxos) + .complete(), + ).rejects.toThrow(); + }); + + it("should reject submitting an unsigned transaction", async () => { + const { address, provider, newTxBuilder } = await createTestSetup(); + + const utxos = await provider.fetchAddressUTxOs(address); + + const txHex = await newTxBuilder() + .txOut(address, [{ unit: "lovelace", quantity: "2000000" }]) + .changeAddress(address) + .selectUtxosFrom(utxos) + .complete(); + + await expect(provider.submitTx(txHex)).rejects.toThrow(); + }); + + it("should throw on unsupported fetcher methods", async () => { + const { provider } = await createTestSetup(); + + await expect(provider.fetchAccountInfo("addr_test1...")).rejects.toThrow( + "not supported", + ); + await expect(provider.fetchBlockInfo("abc")).rejects.toThrow( + "not supported", + ); + await expect(provider.fetchTxInfo("abc")).rejects.toThrow( + "not supported", + ); + }); + }); + + describe("UTxO querying", () => { + it("should fetch UTxOs by transaction hash after submission", async () => { + const { wallet, address, provider, newTxBuilder } = + await createTestSetup(); + + const utxos = await provider.fetchAddressUTxOs(address); + + const txHex = await newTxBuilder() + .txOut(address, [{ unit: "lovelace", quantity: "7000000" }]) + .changeAddress(address) + .selectUtxosFrom(utxos) + .complete(); + + const signedTx = await wallet.signTx(txHex); + const txHash = await provider.submitTx(signedTx); + + const txUtxos = await provider.fetchUTxOs(txHash); + expect(txUtxos.length).toBeGreaterThan(0); + expect(txUtxos.every((u) => u.input.txHash === txHash)).toBe(true); + + const has7Ada = txUtxos.some((u) => + u.output.amount.some( + (a) => a.unit === "lovelace" && a.quantity === "7000000", + ), + ); + expect(has7Ada).toBe(true); + }); + + it("should filter UTxOs by asset unit", async () => { + const { wallet, address, provider, newTxBuilder } = + await createTestSetup(); + + const keyHash = resolvePaymentKeyHash(address); + const nativeScript: NativeScript = { type: "sig", keyHash }; + const scriptCbor = resolveNativeScriptHex(nativeScript); + const policyId = resolveNativeScriptHash(nativeScript); + const tokenNameHex = Buffer.from("FilterToken").toString("hex"); + const unit = policyId + tokenNameHex; + + const utxos = await provider.fetchAddressUTxOs(address); + + const txHex = await newTxBuilder() + .mint("200", policyId, tokenNameHex) + .mintingScript(scriptCbor) + .txOut(address, [ + { unit: "lovelace", quantity: "2000000" }, + { unit, quantity: "200" }, + ]) + .changeAddress(address) + .selectUtxosFrom(utxos) + .complete(); + + const signedTx = await wallet.signTx(txHex); + await provider.submitTx(signedTx); + + const allUtxos = await provider.fetchAddressUTxOs(address); + const filteredUtxos = await provider.fetchAddressUTxOs(address, unit); + + expect(filteredUtxos.length).toBeLessThan(allUtxos.length); + expect(filteredUtxos.length).toBe(1); + expect( + filteredUtxos[0]!.output.amount.find((a) => a.unit === unit)!.quantity, + ).toBe("200"); + }); + }); +}); From 36e7c5497b16e21e258dc84b0316f5f43bbda859 Mon Sep 17 00:00:00 2001 From: otto Date: Mon, 27 Apr 2026 13:02:24 +0200 Subject: [PATCH 04/11] remove a stale file --- packages/mesh-provider/src/scalus.ts | 365 --------------------------- 1 file changed, 365 deletions(-) delete mode 100644 packages/mesh-provider/src/scalus.ts diff --git a/packages/mesh-provider/src/scalus.ts b/packages/mesh-provider/src/scalus.ts deleted file mode 100644 index ee169bf27..000000000 --- a/packages/mesh-provider/src/scalus.ts +++ /dev/null @@ -1,365 +0,0 @@ -import { - DEFAULT_PROTOCOL_PARAMETERS, - type AccountInfo, - type Action, - type Asset, - type AssetMetadata, - type BlockInfo, - type GovernanceProposalInfo, - type IEvaluator, - type IFetcher, - type IFetcherOptions, - type ISubmitter, - type Protocol, - type TransactionInfo, - type UTxO, -} from "@meshsdk/common"; -import cbor from "cbor"; -import { bech32 } from "@scure/base"; - -// Use `import type` for scalus types, `require()` at runtime since scalus is CJS -import type { Emulator, Scalus, SlotConfig, SubmitResult } from "scalus"; - -// Scalus is CJS so we use dynamic import at construction time -let ScalusLib: typeof import("scalus") | undefined; - -/** - * Scalus Emulator provider for MeshJS. - * Implements IFetcher + ISubmitter + IEvaluator backed by a local Scalus Cardano emulator. - * - * Usage: - * ```ts - * import { ScalusProvider } from "@meshsdk/provider"; - * import { Emulator, SlotConfig } from "scalus"; - * - * const emulator = Emulator.withAddresses([aliceAddr], SlotConfig.preview); - * const provider = new ScalusProvider(emulator, SlotConfig.preview); - * const txBuilder = new MeshTxBuilder({ fetcher: provider, submitter: provider, evaluator: provider }); - * ``` - */ -export class ScalusProvider implements IFetcher, ISubmitter, IEvaluator { - private emulator: Emulator; - private slotConfig: SlotConfig; - private protocolParams: Protocol; - private costModels: number[][]; - - constructor( - emulator: Emulator, - slotConfig: SlotConfig, - options?: { - protocolParams?: Protocol; - costModels?: { PlutusV1?: number[]; PlutusV2?: number[]; PlutusV3?: number[] }; - }, - ) { - this.emulator = emulator; - this.slotConfig = slotConfig; - this.protocolParams = options?.protocolParams ?? DEFAULT_PROTOCOL_PARAMETERS; - this.costModels = [ - options?.costModels?.PlutusV1 ?? [], - options?.costModels?.PlutusV2 ?? [], - options?.costModels?.PlutusV3 ?? [], - ]; - - // Eagerly load the scalus module - if (!ScalusLib) { - // eslint-disable-next-line @typescript-eslint/no-require-imports - ScalusLib = require("scalus") as typeof import("scalus"); - } - } - - // --------------------------------------------------------------------------- - // IFetcher - // --------------------------------------------------------------------------- - - async fetchAddressUTxOs(address: string, asset?: string): Promise { - const entries = this.emulator.getUtxosForAddress(address); - const utxos = entries.map((e) => decodeUtxoEntry(e, address)); - if (asset) { - return utxos.filter((u) => - u.output.amount.some((a) => a.unit === asset), - ); - } - return utxos; - } - - async fetchUTxOs(hash: string, index?: number): Promise { - const allEntries = this.emulator.getAllUtxos(); - const utxos: UTxO[] = []; - for (const entry of allEntries) { - const utxo = decodeUtxoEntry(entry); - if (utxo.input.txHash === hash) { - if (index === undefined || utxo.input.outputIndex === index) { - utxos.push(utxo); - } - } - } - return utxos; - } - - async fetchProtocolParameters(_epoch: number): Promise { - return this.protocolParams; - } - - // --- Unsupported IFetcher methods (emulator doesn't track this data) --- - - async fetchAccountInfo(_address: string): Promise { - throw new Error("fetchAccountInfo not supported by ScalusProvider"); - } - - async fetchAddressTxs( - _address: string, - _options?: IFetcherOptions, - ): Promise { - throw new Error("fetchAddressTxs not supported by ScalusProvider"); - } - - async fetchAssetAddresses( - _asset: string, - ): Promise<{ address: string; quantity: string }[]> { - throw new Error("fetchAssetAddresses not supported by ScalusProvider"); - } - - async fetchAssetMetadata(_asset: string): Promise { - throw new Error("fetchAssetMetadata not supported by ScalusProvider"); - } - - async fetchBlockInfo(_hash: string): Promise { - throw new Error("fetchBlockInfo not supported by ScalusProvider"); - } - - async fetchCollectionAssets( - _policyId: string, - _cursor?: number | string, - ): Promise<{ assets: Asset[]; next?: string | number | null }> { - throw new Error("fetchCollectionAssets not supported by ScalusProvider"); - } - - async fetchTxInfo(_hash: string): Promise { - throw new Error("fetchTxInfo not supported by ScalusProvider"); - } - - async fetchGovernanceProposal( - _txHash: string, - _certIndex: number, - ): Promise { - throw new Error("fetchGovernanceProposal not supported by ScalusProvider"); - } - - async get(_url: string): Promise { - throw new Error("get not supported by ScalusProvider"); - } - - // --------------------------------------------------------------------------- - // ISubmitter - // --------------------------------------------------------------------------- - - async submitTx(tx: string): Promise { - const txBytes = hexToBytes(tx); - const result: SubmitResult = this.emulator.submitTx(txBytes); - if (!result.isSuccess) { - const logs = result.logs?.join("\n") ?? ""; - throw new Error( - `Transaction rejected: ${result.error}${logs ? `\nLogs:\n${logs}` : ""}`, - ); - } - return result.txHash!; - } - - // --------------------------------------------------------------------------- - // IEvaluator - // --------------------------------------------------------------------------- - - async evaluateTx( - tx: string, - additionalUtxos?: UTxO[], - ): Promise[]> { - const txBytes = hexToBytes(tx); - - const utxoMapBytes = this.emulator.getUtxosCbor(); - - const scalusSlotConfig = new ScalusLib!.SlotConfig( - this.slotConfig.slotToTime(0), - 0, - 1000, - ); - - let redeemers: Scalus.Redeemer[]; - try { - redeemers = ScalusLib!.Scalus.evalPlutusScripts( - txBytes, - utxoMapBytes, - scalusSlotConfig, - this.costModels, - ); - } catch (error) { - const msg = - error instanceof Error ? error.message : String(error); - const isScriptFailure = - msg.includes("script evaluation") || - msg.includes("PlutusScript") || - (error != null && - typeof error === "object" && - "logs" in error); - if (isScriptFailure) { - throw error; - } - return []; - } - - const tagMap: Record = { - Spend: "SPEND", - Mint: "MINT", - Cert: "CERT", - Reward: "REWARD", - Voting: "VOTE", - Proposing: "PROPOSE", - }; - - return redeemers.map( - (r): Omit => ({ - tag: tagMap[r.tag] || "SPEND", - index: r.index, - budget: { - mem: Number(r.budget.memory), - steps: Number(r.budget.steps), - }, - }), - ); - } -} - -// --------------------------------------------------------------------------- -// CBOR Decoding Helpers -// --------------------------------------------------------------------------- - -function hexToBytes(hex: string): Uint8Array { - const bytes = new Uint8Array(hex.length / 2); - for (let i = 0; i < hex.length; i += 2) { - bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); - } - return bytes; -} - -function bytesToHex(bytes: Uint8Array | Buffer): string { - return Buffer.from(bytes).toString("hex"); -} - -/** - * Convert raw Cardano address bytes to bech32 string. - * Header byte determines address type and network. - */ -function addressBytesToBech32(addrBytes: Uint8Array): string { - const header = addrBytes[0]!; - const networkId = header & 0x0f; - const prefix = - networkId === 1 - ? "addr" // mainnet - : "addr_test"; // testnet - - // bech32 encode the full address bytes (header + payload) - const words = bech32.toWords(addrBytes); - return bech32.encode(prefix, words, 1023); -} - -/** - * Decode a Cardano CBOR value field into Asset[]. - * Value is either: uint (lovelace only) or [uint, multiasset_map] - */ -function decodeValue(value: unknown): Asset[] { - if (typeof value === "number" || typeof value === "bigint") { - return [{ unit: "lovelace", quantity: String(value) }]; - } - if (Array.isArray(value)) { - const [lovelace, multiAsset] = value; - const assets: Asset[] = [ - { unit: "lovelace", quantity: String(lovelace) }, - ]; - if (multiAsset instanceof Map) { - for (const [policyId, assetMap] of multiAsset) { - const policyHex = bytesToHex(policyId as Uint8Array); - if (assetMap instanceof Map) { - for (const [assetName, quantity] of assetMap) { - const nameHex = bytesToHex(assetName as Uint8Array); - assets.push({ - unit: policyHex + nameHex, - quantity: String(quantity), - }); - } - } - } - } - return assets; - } - return [{ unit: "lovelace", quantity: "0" }]; -} - -/** - * Decode a single CBOR-encoded UTxO entry (Map with one key-value pair) - * from the Scalus emulator into a MeshJS UTxO. - * - * @param cborBytes - CBOR encoded Map[TransactionInput, TransactionOutput] - * @param knownAddress - If provided, skip address decoding (optimization for fetchAddressUTxOs) - */ -function decodeUtxoEntry(cborBytes: Uint8Array, knownAddress?: string): UTxO { - const decoded = cbor.decode(cborBytes) as Map; - const entry = Array.from(decoded.entries())[0]!; - const [txIn, txOut] = entry; - - // Decode TransactionInput: [hash_bytes, index] - const txInArr = txIn as [Uint8Array, number]; - const txHash = bytesToHex(txInArr[0]); - const outputIndex = txInArr[1]; - - // Decode TransactionOutput (Babbage era uses Map format) - let address: string; - let amount: Asset[]; - let dataHash: string | undefined; - let plutusData: string | undefined; - let scriptRef: string | undefined; - - if (txOut instanceof Map) { - // Babbage-era map format: {0: address, 1: value, ?2: datumOption, ?3: scriptRef} - const addrBytes = txOut.get(0) as Uint8Array; - address = knownAddress ?? addressBytesToBech32(addrBytes); - amount = decodeValue(txOut.get(1)); - - const datumOption = txOut.get(2); - if (datumOption != null && Array.isArray(datumOption)) { - const [tag, datum] = datumOption; - if (tag === 0) { - // DatumHash - dataHash = bytesToHex(datum as Uint8Array); - } else if (tag === 1) { - // Inline datum — encode back to CBOR hex - plutusData = bytesToHex(cbor.encode(datum)); - } - } - - const scriptRefVal = txOut.get(3); - if (scriptRefVal != null) { - // ScriptRef is CBOR-tagged, encode back to hex - scriptRef = bytesToHex(cbor.encode(scriptRefVal)); - } - } else if (Array.isArray(txOut)) { - // Shelley-era array format: [address, value, ?datumHash] - const addrBytes = txOut[0] as Uint8Array; - address = knownAddress ?? addressBytesToBech32(addrBytes); - amount = decodeValue(txOut[1]); - if (txOut[2]) { - dataHash = bytesToHex(txOut[2] as Uint8Array); - } - } else { - throw new Error("Unexpected TransactionOutput format"); - } - - return { - input: { txHash, outputIndex }, - output: { - address: address!, - amount, - dataHash, - plutusData, - scriptRef, - }, - }; -} From f145d4177551c362ccb705b2a4ca8bd7e2700c96 Mon Sep 17 00:00:00 2001 From: twwu123 Date: Tue, 5 May 2026 12:09:38 +0800 Subject: [PATCH 05/11] rename to scalus emulator and place in mesh core Co-authored-by: Copilot --- packages/mesh-core/src/utils/emulator.ts | 391 +++++++++++++++--- packages/mesh-core/src/utils/index.ts | 1 + .../test/emulator.test.ts} | 13 +- 3 files changed, 337 insertions(+), 68 deletions(-) rename packages/{mesh-provider/test/scalus/e2e.test.ts => mesh-core/test/emulator.test.ts} (99%) diff --git a/packages/mesh-core/src/utils/emulator.ts b/packages/mesh-core/src/utils/emulator.ts index c51418daf..054591cba 100644 --- a/packages/mesh-core/src/utils/emulator.ts +++ b/packages/mesh-core/src/utils/emulator.ts @@ -1,96 +1,363 @@ -import { Emulator, SlotConfig as ScalusSlotConfig, SubmitResult } from "scalus"; +// Use `import type` for scalus types, `require()` at runtime since scalus is CJS +import type { Emulator, Scalus, SlotConfig, SubmitResult } from "scalus"; +import { bech32 } from "@scure/base"; +import cbor from "cbor"; -import { +import type { AccountInfo, + Action, Asset, AssetMetadata, BlockInfo, GovernanceProposalInfo, + IEvaluator, IFetcher, IFetcherOptions, + ISubmitter, Protocol, - SlotConfig, TransactionInfo, UTxO, } from "@meshsdk/common"; -import { cborMapToUtxos, utxosToCborMap } from "@meshsdk/core-cst"; -import { OfflineFetcher } from "@meshsdk/provider"; - -export class MeshEmulator extends Emulator implements IFetcher { - fetcher: OfflineFetcher; - - constructor(initialUtxos: UTxO[], slotConfig: SlotConfig) { - super( - Buffer.from(utxosToCborMap(initialUtxos), "hex"), - new ScalusSlotConfig( - slotConfig.zeroTime, - slotConfig.zeroSlot, - slotConfig.slotLength, - ), - ); - this.fetcher = new OfflineFetcher(); - this.fetcher.addUTxOs(initialUtxos); +import { DEFAULT_PROTOCOL_PARAMETERS } from "@meshsdk/common"; + +// Scalus is CJS so we use dynamic import at construction time +let ScalusLib: typeof import("scalus") | undefined; + +/** + * Scalus Emulator provider for MeshJS. + * Implements IFetcher + ISubmitter + IEvaluator backed by a local Scalus Cardano emulator. + * + * Usage: + * ```ts + * import { ScalusEmulator } from "@meshsdk/provider"; + * import { Emulator, SlotConfig } from "scalus"; + * + * const emulator = Emulator.withAddresses([aliceAddr], SlotConfig.preview); + * const provider = new ScalusEmulator(emulator, SlotConfig.preview); + * const txBuilder = new MeshTxBuilder({ fetcher: provider, submitter: provider, evaluator: provider }); + * ``` + */ +export class ScalusEmulator implements IFetcher, ISubmitter, IEvaluator { + private emulator: Emulator; + private slotConfig: SlotConfig; + private protocolParams: Protocol; + private costModels: number[][]; + + constructor( + emulator: Emulator, + slotConfig: SlotConfig, + options?: { + protocolParams?: Protocol; + costModels?: { + PlutusV1?: number[]; + PlutusV2?: number[]; + PlutusV3?: number[]; + }; + }, + ) { + this.emulator = emulator; + this.slotConfig = slotConfig; + this.protocolParams = + options?.protocolParams ?? DEFAULT_PROTOCOL_PARAMETERS; + this.costModels = [ + options?.costModels?.PlutusV1 ?? [], + options?.costModels?.PlutusV2 ?? [], + options?.costModels?.PlutusV3 ?? [], + ]; + + // Eagerly load the scalus module + if (!ScalusLib) { + // eslint-disable-next-line @typescript-eslint/no-require-imports + ScalusLib = require("scalus") as typeof import("scalus"); + } } - fetchAccountInfo(address: string): Promise { - return this.fetcher.fetchAccountInfo(address); + + // --------------------------------------------------------------------------- + // IFetcher + // --------------------------------------------------------------------------- + + async fetchAddressUTxOs(address: string, asset?: string): Promise { + const entries = this.emulator.getUtxosForAddress(address); + const utxos = entries.map((e) => decodeUtxoEntry(e, address)); + if (asset) { + return utxos.filter((u) => u.output.amount.some((a) => a.unit === asset)); + } + return utxos; + } + + async fetchUTxOs(hash: string, index?: number): Promise { + const allEntries = this.emulator.getAllUtxos(); + const utxos: UTxO[] = []; + for (const entry of allEntries) { + const utxo = decodeUtxoEntry(entry); + if (utxo.input.txHash === hash) { + if (index === undefined || utxo.input.outputIndex === index) { + utxos.push(utxo); + } + } + } + return utxos; } - fetchAddressUTxOs(address: string, asset?: string): Promise { - return this.fetcher.fetchAddressUTxOs(address, asset); + + async fetchProtocolParameters(_epoch: number): Promise { + return this.protocolParams; + } + + // --- Unsupported IFetcher methods (emulator doesn't track this data) --- + + async fetchAccountInfo(_address: string): Promise { + throw new Error("fetchAccountInfo not supported by ScalusEmulator"); } - fetchAddressTxs( - address: string, - options?: IFetcherOptions, + + async fetchAddressTxs( + _address: string, + _options?: IFetcherOptions, ): Promise { - return this.fetcher.fetchAddressTxs(address, options); + throw new Error("fetchAddressTxs not supported by ScalusEmulator"); } - fetchAssetAddresses( - asset: string, + + async fetchAssetAddresses( + _asset: string, ): Promise<{ address: string; quantity: string }[]> { - return this.fetcher.fetchAssetAddresses(asset); + throw new Error("fetchAssetAddresses not supported by ScalusEmulator"); } - fetchAssetMetadata(asset: string): Promise { - return this.fetcher.fetchAssetMetadata(asset); + + async fetchAssetMetadata(_asset: string): Promise { + throw new Error("fetchAssetMetadata not supported by ScalusEmulator"); } - fetchBlockInfo(hash: string): Promise { - return this.fetcher.fetchBlockInfo(hash); + + async fetchBlockInfo(_hash: string): Promise { + throw new Error("fetchBlockInfo not supported by ScalusEmulator"); } - fetchCollectionAssets( - policyId: string, - cursor?: number | string, + + async fetchCollectionAssets( + _policyId: string, + _cursor?: number | string, ): Promise<{ assets: Asset[]; next?: string | number | null }> { - return this.fetcher.fetchCollectionAssets(policyId, cursor); + throw new Error("fetchCollectionAssets not supported by ScalusEmulator"); } - fetchProtocolParameters(epoch: number): Promise { - return this.fetcher.fetchProtocolParameters(epoch); + + async fetchTxInfo(_hash: string): Promise { + throw new Error("fetchTxInfo not supported by ScalusEmulator"); } - fetchTxInfo(hash: string): Promise { - return this.fetcher.fetchTxInfo(hash); + + async fetchGovernanceProposal( + _txHash: string, + _certIndex: number, + ): Promise { + throw new Error("fetchGovernanceProposal not supported by ScalusEmulator"); } - fetchUTxOs(hash: string, index?: number): Promise { - return this.fetcher.fetchUTxOs(hash); + + async get(_url: string): Promise { + throw new Error("get not supported by ScalusEmulator"); } - fetchGovernanceProposal( - txHash: string, - certIndex: number, - ): Promise { - return this.fetcher.fetchGovernanceProposal(txHash, certIndex); + + // --------------------------------------------------------------------------- + // ISubmitter + // --------------------------------------------------------------------------- + + async submitTx(tx: string): Promise { + const txBytes = hexToBytes(tx); + const result: SubmitResult = this.emulator.submitTx(txBytes); + if (!result.isSuccess) { + const logs = result.logs?.join("\n") ?? ""; + throw new Error( + `Transaction rejected: ${result.error}${logs ? `\nLogs:\n${logs}` : ""}`, + ); + } + return result.txHash!; + } + + // --------------------------------------------------------------------------- + // IEvaluator + // --------------------------------------------------------------------------- + + async evaluateTx( + tx: string, + additionalUtxos?: UTxO[], + ): Promise[]> { + const txBytes = hexToBytes(tx); + + const utxoMapBytes = this.emulator.getUtxosCbor(); + + const scalusSlotConfig = new ScalusLib!.SlotConfig( + this.slotConfig.slotToTime(0), + 0, + 1000, + ); + + let redeemers: Scalus.Redeemer[]; + try { + redeemers = ScalusLib!.Scalus.evalPlutusScripts( + txBytes, + utxoMapBytes, + scalusSlotConfig, + this.costModels, + ); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + const isScriptFailure = + msg.includes("script evaluation") || + msg.includes("PlutusScript") || + (error != null && typeof error === "object" && "logs" in error); + if (isScriptFailure) { + throw error; + } + return []; + } + + const tagMap: Record = { + Spend: "SPEND", + Mint: "MINT", + Cert: "CERT", + Reward: "REWARD", + Voting: "VOTE", + Proposing: "PROPOSE", + }; + + return redeemers.map( + (r): Omit => ({ + tag: tagMap[r.tag] || "SPEND", + index: r.index, + budget: { + mem: Number(r.budget.memory), + steps: Number(r.budget.steps), + }, + }), + ); } - get(url: string): Promise { - return this.fetcher.get(url); +} + +// --------------------------------------------------------------------------- +// CBOR Decoding Helpers +// --------------------------------------------------------------------------- + +function hexToBytes(hex: string): Uint8Array { + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); } + return bytes; +} + +function bytesToHex(bytes: Uint8Array | Buffer): string { + return Buffer.from(bytes).toString("hex"); +} + +/** + * Convert raw Cardano address bytes to bech32 string. + * Header byte determines address type and network. + */ +function addressBytesToBech32(addrBytes: Uint8Array): string { + const header = addrBytes[0]!; + const networkId = header & 0x0f; + const prefix = + networkId === 1 + ? "addr" // mainnet + : "addr_test"; // testnet + + // bech32 encode the full address bytes (header + payload) + const words = bech32.toWords(addrBytes); + return bech32.encode(prefix, words, 1023); +} - submitTxHex(txHex: string): SubmitResult { - const result = this.submitTx(Buffer.from(txHex, "hex")); - if (result.isSuccess) { - this.fetcher = new OfflineFetcher(); - this.fetcher.addUTxOs(this.getAllUtxosMesh()); +/** + * Decode a Cardano CBOR value field into Asset[]. + * Value is either: uint (lovelace only) or [uint, multiasset_map] + */ +function decodeValue(value: unknown): Asset[] { + if (typeof value === "number" || typeof value === "bigint") { + return [{ unit: "lovelace", quantity: String(value) }]; + } + if (Array.isArray(value)) { + const [lovelace, multiAsset] = value; + const assets: Asset[] = [{ unit: "lovelace", quantity: String(lovelace) }]; + if (multiAsset instanceof Map) { + for (const [policyId, assetMap] of multiAsset) { + const policyHex = bytesToHex(policyId as Uint8Array); + if (assetMap instanceof Map) { + for (const [assetName, quantity] of assetMap) { + const nameHex = bytesToHex(assetName as Uint8Array); + assets.push({ + unit: policyHex + nameHex, + quantity: String(quantity), + }); + } + } + } } - return result; + return assets; } + return [{ unit: "lovelace", quantity: "0" }]; +} + +/** + * Decode a single CBOR-encoded UTxO entry (Map with one key-value pair) + * from the Scalus emulator into a MeshJS UTxO. + * + * @param cborBytes - CBOR encoded Map[TransactionInput, TransactionOutput] + * @param knownAddress - If provided, skip address decoding (optimization for fetchAddressUTxOs) + */ +function decodeUtxoEntry(cborBytes: Uint8Array, knownAddress?: string): UTxO { + const decoded = cbor.decode(cborBytes) as Map; + const entry = Array.from(decoded.entries())[0]!; + const [txIn, txOut] = entry; + + // Decode TransactionInput: [hash_bytes, index] + const txInArr = txIn as [Uint8Array, number]; + const txHash = bytesToHex(txInArr[0]); + const outputIndex = txInArr[1]; - getAllUtxosMesh() { - const allUtxos = this.getAllUtxos(); - const utxoList = allUtxos.map((u) => Buffer.from(u).toString("hex")); - return cborMapToUtxos(utxoList); + // Decode TransactionOutput (Babbage era uses Map format) + let address: string; + let amount: Asset[]; + let dataHash: string | undefined; + let plutusData: string | undefined; + let scriptRef: string | undefined; + + if (txOut instanceof Map) { + // Babbage-era map format: {0: address, 1: value, ?2: datumOption, ?3: scriptRef} + const addrBytes = txOut.get(0) as Uint8Array; + address = knownAddress ?? addressBytesToBech32(addrBytes); + amount = decodeValue(txOut.get(1)); + + const datumOption = txOut.get(2); + if (datumOption != null && Array.isArray(datumOption)) { + const [tag, datum] = datumOption; + if (tag === 0) { + // DatumHash + dataHash = bytesToHex(datum as Uint8Array); + } else if (tag === 1) { + // Inline datum — encode back to CBOR hex + plutusData = bytesToHex(cbor.encode(datum)); + } + } + + const scriptRefVal = txOut.get(3); + if (scriptRefVal != null) { + // ScriptRef is CBOR-tagged, encode back to hex + scriptRef = bytesToHex(cbor.encode(scriptRefVal)); + } + } else if (Array.isArray(txOut)) { + // Shelley-era array format: [address, value, ?datumHash] + const addrBytes = txOut[0] as Uint8Array; + address = knownAddress ?? addressBytesToBech32(addrBytes); + amount = decodeValue(txOut[1]); + if (txOut[2]) { + dataHash = bytesToHex(txOut[2] as Uint8Array); + } + } else { + throw new Error("Unexpected TransactionOutput format"); } + + return { + input: { txHash, outputIndex }, + output: { + address: address!, + amount, + dataHash, + plutusData, + scriptRef, + }, + }; } diff --git a/packages/mesh-core/src/utils/index.ts b/packages/mesh-core/src/utils/index.ts index a8902c2ea..cb6938562 100644 --- a/packages/mesh-core/src/utils/index.ts +++ b/packages/mesh-core/src/utils/index.ts @@ -2,3 +2,4 @@ export * from "./resolver"; export * from "./deserializer"; export * from "./serializer"; export * from "./blueprint"; +export * from "./emulator"; diff --git a/packages/mesh-provider/test/scalus/e2e.test.ts b/packages/mesh-core/test/emulator.test.ts similarity index 99% rename from packages/mesh-provider/test/scalus/e2e.test.ts rename to packages/mesh-core/test/emulator.test.ts index 2a9b63b35..dc137ab04 100644 --- a/packages/mesh-provider/test/scalus/e2e.test.ts +++ b/packages/mesh-core/test/emulator.test.ts @@ -1,15 +1,16 @@ +import { Emulator, SlotConfig } from "scalus"; + import { applyCborEncoding, MeshTxBuilder, + NativeScript, resolveNativeScriptHash, resolveNativeScriptHex, resolvePaymentKeyHash, resolveScriptHash, - NativeScript, + ScalusEmulator, } from "@meshsdk/core"; import { AppWallet } from "@meshsdk/wallet"; -import { ScalusProvider } from "@meshsdk/provider"; -import { Emulator, SlotConfig } from "scalus"; const TEST_MNEMONIC = [ "solution", @@ -59,7 +60,7 @@ async function createTestSetup(lovelacePerAddress = 10_000_000_000n) { const currentSlot = slotConfig.timeToSlot(Date.now()); emulator.setSlot(currentSlot); - const provider = new ScalusProvider(emulator, slotConfig); + const provider = new ScalusEmulator(emulator, slotConfig); const newTxBuilder = () => new MeshTxBuilder({ @@ -71,7 +72,7 @@ async function createTestSetup(lovelacePerAddress = 10_000_000_000n) { return { wallet, address, provider, emulator, slotConfig, newTxBuilder }; } -describe("ScalusProvider", () => { +describe("ScalusEmulator", () => { describe("Basic payment lifecycle", () => { it("should build, sign, submit and confirm a simple payment", async () => { const { wallet, address, provider, newTxBuilder } = @@ -523,7 +524,7 @@ describe("ScalusProvider", () => { const txHex = await newTxBuilder() .txOut(address, [{ unit: "lovelace", quantity: "2000000" }]) - .metadataValue(674, "Hello from ScalusProvider test") + .metadataValue(674, "Hello from ScalusEmulator test") .changeAddress(address) .selectUtxosFrom(utxos) .complete(); From 0746bb749d552d3750d3767cd4ad53278c949361 Mon Sep 17 00:00:00 2001 From: otto Date: Tue, 5 May 2026 14:15:16 +0200 Subject: [PATCH 06/11] bump scalus --- package-lock.json | 25 ++++++++++++------------- packages/mesh-core-cst/package.json | 2 +- packages/mesh-core/package.json | 2 +- packages/mesh-core/src/utils/index.ts | 1 + 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1f6da5791..e013d0cb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14949,8 +14949,7 @@ "bech32": "^2.0.0", "bip39": "3.1.0", "blake2b": "^2.1.4", - "blakejs": "^1.2.1", - "scalus": "^0.15.0" + "blakejs": "^1.2.1" }, "devDependencies": { "@meshsdk/configs": "*", @@ -14960,12 +14959,6 @@ "typescript": "^5.3.3" } }, - "packages/mesh-common/node_modules/scalus": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/scalus/-/scalus-0.15.0.tgz", - "integrity": "sha512-4yWMpHkGj89/VUUn18kXZHjUHdJDSw2Se9E1Aa3xmNWSe5YdWyGclpRlqfih5mTFuQik9/vt0fgrh3HaEUVqUw==", - "license": "Apache-2.0" - }, "packages/mesh-contract": { "name": "@meshsdk/contract", "version": "1.9.0-beta.102", @@ -14991,7 +14984,7 @@ "@meshsdk/provider": "1.9.0-beta.100", "@meshsdk/transaction": "1.9.0-beta.102", "@meshsdk/wallet": "1.9.0-beta.102", - "scalus": "^0.15.0" + "scalus": "^0.17.0" }, "devDependencies": { "@meshsdk/configs": "*", @@ -15042,7 +15035,7 @@ "blakejs": "^1.2.1", "bn.js": "^5.2.0", "hash.js": "^1.1.7", - "scalus": "^0.14.2" + "scalus": "^0.17.0" }, "devDependencies": { "@meshsdk/configs": "*", @@ -15054,10 +15047,16 @@ "typescript": "^5.3.3" } }, + "packages/mesh-core-cst/node_modules/scalus": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/scalus/-/scalus-0.17.0.tgz", + "integrity": "sha512-74mD4wL1vw4GVh2ECemQhoB3q5Smwj5S8W7OG1j7OWuLkwJ0Mvz4LFhZPA9rZjpNvanZoKiDqc3S/qGD65uRYg==", + "license": "Apache-2.0" + }, "packages/mesh-core/node_modules/scalus": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/scalus/-/scalus-0.15.0.tgz", - "integrity": "sha512-4yWMpHkGj89/VUUn18kXZHjUHdJDSw2Se9E1Aa3xmNWSe5YdWyGclpRlqfih5mTFuQik9/vt0fgrh3HaEUVqUw==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/scalus/-/scalus-0.17.0.tgz", + "integrity": "sha512-74mD4wL1vw4GVh2ECemQhoB3q5Smwj5S8W7OG1j7OWuLkwJ0Mvz4LFhZPA9rZjpNvanZoKiDqc3S/qGD65uRYg==", "license": "Apache-2.0" }, "packages/mesh-transaction": { diff --git a/packages/mesh-core-cst/package.json b/packages/mesh-core-cst/package.json index 32e1e4809..ccb6626df 100644 --- a/packages/mesh-core-cst/package.json +++ b/packages/mesh-core-cst/package.json @@ -51,7 +51,7 @@ "blakejs": "^1.2.1", "bn.js": "^5.2.0", "hash.js": "^1.1.7", - "scalus": "^0.14.2" + "scalus": "^0.17.0" }, "overrides": { "@cardano-sdk/crypto": { diff --git a/packages/mesh-core/package.json b/packages/mesh-core/package.json index 366d370b5..f48f0c50f 100644 --- a/packages/mesh-core/package.json +++ b/packages/mesh-core/package.json @@ -38,7 +38,7 @@ "@meshsdk/provider": "1.9.0-beta.100", "@meshsdk/transaction": "1.9.0-beta.102", "@meshsdk/wallet": "1.9.0-beta.102", - "scalus": "^0.15.0" + "scalus": "^0.17.0" }, "prettier": "@meshsdk/configs/prettier", "publishConfig": { diff --git a/packages/mesh-core/src/utils/index.ts b/packages/mesh-core/src/utils/index.ts index cb6938562..f711f6361 100644 --- a/packages/mesh-core/src/utils/index.ts +++ b/packages/mesh-core/src/utils/index.ts @@ -3,3 +3,4 @@ export * from "./deserializer"; export * from "./serializer"; export * from "./blueprint"; export * from "./emulator"; +export * from "./emulator"; From 718e46c8201a1a47c60f8f8b3592c6dab2cb422c Mon Sep 17 00:00:00 2001 From: twwu123 Date: Thu, 7 May 2026 15:52:24 +0800 Subject: [PATCH 07/11] update evaluator in emulator Co-authored-by: Copilot --- packages/mesh-core/src/utils/emulator.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/mesh-core/src/utils/emulator.ts b/packages/mesh-core/src/utils/emulator.ts index 054591cba..bf87ec53e 100644 --- a/packages/mesh-core/src/utils/emulator.ts +++ b/packages/mesh-core/src/utils/emulator.ts @@ -19,6 +19,7 @@ import type { UTxO, } from "@meshsdk/common"; import { DEFAULT_PROTOCOL_PARAMETERS } from "@meshsdk/common"; +import { utxosToCborMap } from "@meshsdk/core-cst"; // Scalus is CJS so we use dynamic import at construction time let ScalusLib: typeof import("scalus") | undefined; @@ -178,8 +179,12 @@ export class ScalusEmulator implements IFetcher, ISubmitter, IEvaluator { ): Promise[]> { const txBytes = hexToBytes(tx); - const utxoMapBytes = this.emulator.getUtxosCbor(); - + const utxoMapBytes = this.emulator.getAllUtxos(); + let utxos: UTxO[] = utxoMapBytes.map((e) => decodeUtxoEntry(e)); + if (additionalUtxos) { + utxos = utxos.concat(additionalUtxos); + } + const utxoMapCbor = Buffer.from(utxosToCborMap(utxos), "hex"); const scalusSlotConfig = new ScalusLib!.SlotConfig( this.slotConfig.slotToTime(0), 0, @@ -190,20 +195,12 @@ export class ScalusEmulator implements IFetcher, ISubmitter, IEvaluator { try { redeemers = ScalusLib!.Scalus.evalPlutusScripts( txBytes, - utxoMapBytes, + utxoMapCbor, scalusSlotConfig, this.costModels, ); } catch (error) { - const msg = error instanceof Error ? error.message : String(error); - const isScriptFailure = - msg.includes("script evaluation") || - msg.includes("PlutusScript") || - (error != null && typeof error === "object" && "logs" in error); - if (isScriptFailure) { - throw error; - } - return []; + throw error; } const tagMap: Record = { From c48d92802548ac01cf374f318d60d0dacf0b5812 Mon Sep 17 00:00:00 2001 From: twwu123 Date: Thu, 7 May 2026 15:52:44 +0800 Subject: [PATCH 08/11] make sure if fee is manually input, don't evaluate with max fee Co-authored-by: Copilot --- packages/mesh-transaction/src/mesh-tx-builder/index.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/mesh-transaction/src/mesh-tx-builder/index.ts b/packages/mesh-transaction/src/mesh-tx-builder/index.ts index b6c6339f5..00850c976 100644 --- a/packages/mesh-transaction/src/mesh-tx-builder/index.ts +++ b/packages/mesh-transaction/src/mesh-tx-builder/index.ts @@ -234,7 +234,15 @@ export class MeshTxBuilder extends MeshTxBuilderCore { selectionSkeleton: TransactionPrototype, ): Promise => { const clonedBuilder = this.clone(); - await clonedBuilder.updateByTxPrototype(selectionSkeleton); + if (this.manualFee) { + const newSelectionSkeleton = { + ...selectionSkeleton, + fee: BigInt(this.manualFee), + }; + await clonedBuilder.updateByTxPrototype(newSelectionSkeleton); + } else { + await clonedBuilder.updateByTxPrototype(selectionSkeleton); + } try { await clonedBuilder.evaluateRedeemers(); From 6d7f99482da9adee5e7a659d97a30b9f38ded00e Mon Sep 17 00:00:00 2001 From: twwu123 Date: Thu, 7 May 2026 15:53:07 +0800 Subject: [PATCH 09/11] add test for inputting addditional utxos for evaluate --- packages/mesh-core/test/emulator.test.ts | 58 +++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/packages/mesh-core/test/emulator.test.ts b/packages/mesh-core/test/emulator.test.ts index dc137ab04..89a978a53 100644 --- a/packages/mesh-core/test/emulator.test.ts +++ b/packages/mesh-core/test/emulator.test.ts @@ -9,6 +9,7 @@ import { resolvePaymentKeyHash, resolveScriptHash, ScalusEmulator, + UTxO, } from "@meshsdk/core"; import { AppWallet } from "@meshsdk/wallet"; @@ -66,7 +67,6 @@ async function createTestSetup(lovelacePerAddress = 10_000_000_000n) { new MeshTxBuilder({ fetcher: provider, submitter: provider, - evaluator: provider, }); return { wallet, address, provider, emulator, slotConfig, newTxBuilder }; @@ -644,5 +644,61 @@ describe("ScalusEmulator", () => { filteredUtxos[0]!.output.amount.find((a) => a.unit === unit)!.quantity, ).toBe("200"); }); + + it("should allow inputting additional UTxOs for script evaluation", async () => { + const { address, provider, newTxBuilder } = await createTestSetup(); + + const policyId = resolveScriptHash(alwaysSucceedCbor, "V3"); + const tokenNameHex = Buffer.from("SpendTest").toString("hex"); + const unit = policyId + tokenNameHex; + + const utxos = await provider.fetchAddressUTxOs(address); + const testUtxo: UTxO = { + input: { + txHash: + "ffe00432c78714fbd9c1784a6b574b0b11bd7c9dedb305aa7f55593505607539", + outputIndex: 0, + }, + output: { + address, + amount: [{ unit: "lovelace", quantity: "2000000" }], + }, + }; + // Step 1: Mint tokens using plutus script + const mintTxHex = await newTxBuilder() + .txIn( + testUtxo.input.txHash, + testUtxo.input.outputIndex, + testUtxo.output.amount, + testUtxo.output.address, + 0, + ) + .mintPlutusScriptV3() + .mint("50", policyId, tokenNameHex) + .mintRedeemerValue("") + .mintingScript(alwaysSucceedCbor) + .txInCollateral( + utxos[0]!.input.txHash, + utxos[0]!.input.outputIndex, + utxos[0]!.output.amount, + utxos[0]!.output.address, + ) + .setFee("2000000") + .txOut(address, [ + { unit: "lovelace", quantity: "2000000" }, + { unit, quantity: "50" }, + ]) + .changeAddress(address) + .selectUtxosFrom(utxos) + .complete(); + const evaluateResult = await provider.evaluateTx(mintTxHex, [testUtxo]); + expect(evaluateResult).toEqual([ + { + tag: "MINT", + index: 0, + budget: { mem: 6300000000, steps: 6300000000 }, + }, + ]); + }); }); }); From 134f9fa21e410f4cc72a9102b5a3679efca22c5b Mon Sep 17 00:00:00 2001 From: twwu123 Date: Thu, 7 May 2026 16:23:05 +0800 Subject: [PATCH 10/11] update selection skeleton to use smaller fee --- .../src/mesh-tx-builder/coin-selection/cardano-sdk-adapter.ts | 2 +- .../mesh-tx-builder/coin-selection/largest-first-selector.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mesh-transaction/src/mesh-tx-builder/coin-selection/cardano-sdk-adapter.ts b/packages/mesh-transaction/src/mesh-tx-builder/coin-selection/cardano-sdk-adapter.ts index fa4ef0307..0f75f7f43 100644 --- a/packages/mesh-transaction/src/mesh-tx-builder/coin-selection/cardano-sdk-adapter.ts +++ b/packages/mesh-transaction/src/mesh-tx-builder/coin-selection/cardano-sdk-adapter.ts @@ -63,7 +63,7 @@ export class BuilderCallbacksSdkBridge change: selectionSkeleton.change.map((output) => CSDKOutputToMeshOutput(output), ), - fee: selectionSkeleton.fee, + fee: BigInt(1_000_000_000), }); return { diff --git a/packages/mesh-transaction/src/mesh-tx-builder/coin-selection/largest-first-selector.ts b/packages/mesh-transaction/src/mesh-tx-builder/coin-selection/largest-first-selector.ts index b9298dcf1..0fd52a5d1 100644 --- a/packages/mesh-transaction/src/mesh-tx-builder/coin-selection/largest-first-selector.ts +++ b/packages/mesh-transaction/src/mesh-tx-builder/coin-selection/largest-first-selector.ts @@ -333,7 +333,7 @@ export class LargestFirstInputSelector implements IInputSelector { newInputs: selectedUtxos, newOutputs: new Set(), change: changeOutputs, - fee: MAX_U64, + fee: BigInt(1_000_000_000), }) ).fee; } From 3616de2becfe7188bb437821462ae3e06c39517b Mon Sep 17 00:00:00 2001 From: twwu123 Date: Thu, 7 May 2026 16:41:22 +0800 Subject: [PATCH 11/11] final clean up for emulator Co-authored-by: Copilot --- packages/mesh-core/src/utils/emulator.ts | 27 +++++++++----- packages/mesh-core/test/emulator.test.ts | 46 +++++++++++++++++------- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/packages/mesh-core/src/utils/emulator.ts b/packages/mesh-core/src/utils/emulator.ts index bf87ec53e..9e03c978d 100644 --- a/packages/mesh-core/src/utils/emulator.ts +++ b/packages/mesh-core/src/utils/emulator.ts @@ -1,7 +1,8 @@ // Use `import type` for scalus types, `require()` at runtime since scalus is CJS -import type { Emulator, Scalus, SlotConfig, SubmitResult } from "scalus"; +import type { Scalus, SlotConfig, SubmitResult } from "scalus"; import { bech32 } from "@scure/base"; import cbor from "cbor"; +import { Emulator } from "scalus"; import type { AccountInfo, @@ -18,7 +19,12 @@ import type { TransactionInfo, UTxO, } from "@meshsdk/common"; -import { DEFAULT_PROTOCOL_PARAMETERS } from "@meshsdk/common"; +import { + DEFAULT_PROTOCOL_PARAMETERS, + DEFAULT_V1_COST_MODEL_LIST, + DEFAULT_V2_COST_MODEL_LIST, + DEFAULT_V3_COST_MODEL_LIST, +} from "@meshsdk/common"; import { utxosToCborMap } from "@meshsdk/core-cst"; // Scalus is CJS so we use dynamic import at construction time @@ -39,13 +45,13 @@ let ScalusLib: typeof import("scalus") | undefined; * ``` */ export class ScalusEmulator implements IFetcher, ISubmitter, IEvaluator { - private emulator: Emulator; + public emulator: Emulator; private slotConfig: SlotConfig; private protocolParams: Protocol; private costModels: number[][]; constructor( - emulator: Emulator, + initialUtxos: UTxO[], slotConfig: SlotConfig, options?: { protocolParams?: Protocol; @@ -56,14 +62,18 @@ export class ScalusEmulator implements IFetcher, ISubmitter, IEvaluator { }; }, ) { - this.emulator = emulator; + this.emulator = new Emulator( + Buffer.from(utxosToCborMap(initialUtxos), "hex"), + slotConfig, + ); + this.slotConfig = slotConfig; this.protocolParams = options?.protocolParams ?? DEFAULT_PROTOCOL_PARAMETERS; this.costModels = [ - options?.costModels?.PlutusV1 ?? [], - options?.costModels?.PlutusV2 ?? [], - options?.costModels?.PlutusV3 ?? [], + options?.costModels?.PlutusV1 ?? DEFAULT_V1_COST_MODEL_LIST, + options?.costModels?.PlutusV2 ?? DEFAULT_V2_COST_MODEL_LIST, + options?.costModels?.PlutusV3 ?? DEFAULT_V3_COST_MODEL_LIST, ]; // Eagerly load the scalus module @@ -190,7 +200,6 @@ export class ScalusEmulator implements IFetcher, ISubmitter, IEvaluator { 0, 1000, ); - let redeemers: Scalus.Redeemer[]; try { redeemers = ScalusLib!.Scalus.evalPlutusScripts( diff --git a/packages/mesh-core/test/emulator.test.ts b/packages/mesh-core/test/emulator.test.ts index 89a978a53..39c622d7c 100644 --- a/packages/mesh-core/test/emulator.test.ts +++ b/packages/mesh-core/test/emulator.test.ts @@ -53,23 +53,44 @@ async function createTestSetup(lovelacePerAddress = 10_000_000_000n) { await wallet.init(); const address = wallet.getPaymentAddress(); - const emulator = Emulator.withAddresses( - [address], + const currentSlot = slotConfig.timeToSlot(Date.now()); + + const provider = new ScalusEmulator( + [ + { + input: { + txHash: + "0000000000000000000000000000000000000000000000000000000000000000", + outputIndex: 0, + }, + output: { + address, + amount: [ + { unit: "lovelace", quantity: lovelacePerAddress.toString() }, + ], + }, + }, + ], slotConfig, - lovelacePerAddress, ); - const currentSlot = slotConfig.timeToSlot(Date.now()); - emulator.setSlot(currentSlot); - const provider = new ScalusEmulator(emulator, slotConfig); + provider.emulator.setSlot(currentSlot); const newTxBuilder = () => new MeshTxBuilder({ fetcher: provider, submitter: provider, + evaluator: provider, }); - return { wallet, address, provider, emulator, slotConfig, newTxBuilder }; + return { + wallet, + address, + provider, + emulator: provider.emulator, + slotConfig, + newTxBuilder, + }; } describe("ScalusEmulator", () => { @@ -318,7 +339,7 @@ describe("ScalusEmulator", () => { }); it("should evaluate a plutus spending transaction", async () => { - const { wallet, address, provider, newTxBuilder, slotConfig } = + const { wallet, address, provider, newTxBuilder, emulator } = await createTestSetup(); const policyId = resolveScriptHash(alwaysSucceedCbor, "V3"); @@ -346,7 +367,6 @@ describe("ScalusEmulator", () => { .changeAddress(address) .selectUtxosFrom(utxos) .complete(); - const signedMint = await wallet.signTx(mintTxHex); const mintHash = await provider.submitTx(signedMint); expect(mintHash.length).toBe(64); @@ -646,7 +666,7 @@ describe("ScalusEmulator", () => { }); it("should allow inputting additional UTxOs for script evaluation", async () => { - const { address, provider, newTxBuilder } = await createTestSetup(); + const { address, provider, emulator } = await createTestSetup(); const policyId = resolveScriptHash(alwaysSucceedCbor, "V3"); const tokenNameHex = Buffer.from("SpendTest").toString("hex"); @@ -665,7 +685,9 @@ describe("ScalusEmulator", () => { }, }; // Step 1: Mint tokens using plutus script - const mintTxHex = await newTxBuilder() + const mintTxHex = await new MeshTxBuilder({ + fetcher: provider, + }) .txIn( testUtxo.input.txHash, testUtxo.input.outputIndex, @@ -696,7 +718,7 @@ describe("ScalusEmulator", () => { { tag: "MINT", index: 0, - budget: { mem: 6300000000, steps: 6300000000 }, + budget: { mem: 2001, steps: 380149 }, }, ]); });