diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..39a35e3 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,10 @@ +[target.aarch64-apple-darwin] +rustflags = ["-C", "target-cpu=native"] + +[target.x86_64-unknown-linux-gnu] +rustflags = [ + "-C", + "target-cpu=native", + # (Nightly) Make the current crate share its generic instantiations + "-Zshare-generics=y", +] diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ad9775b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +[*.{js,jsx,ts,tsx,css,scss,html,json,md}] +charset = utf-8 +insert_final_newline = true +end_of_line = lf +indent_style = space +indent_size = 2 +max_line_length = 80 \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000..ebc5ed4 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +FOUNDRY_OUT="target/foundry" diff --git a/.gitignore b/.gitignore index f84ad73..3e0e929 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Project-specific ignores +/.db + # Generated by Cargo # will have compiled files and executables debug @@ -11,466 +14,33 @@ target # Generated by cargo mutants # Contains mutation testing data -**/mutants.out*/ - -# IntelliJ IDEA -.idea - -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates -*.env - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ - -[Dd]ebug/x64/ -[Dd]ebugPublic/x64/ -[Rr]elease/x64/ -[Rr]eleases/x64/ -bin/x64/ -obj/x64/ - -[Dd]ebug/x86/ -[Dd]ebugPublic/x86/ -[Rr]elease/x86/ -[Rr]eleases/x86/ -bin/x86/ -obj/x86/ - -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -[Aa][Rr][Mm]64[Ee][Cc]/ -bld/ -[Oo]bj/ -[Oo]ut/ -[Ll]og/ -[Ll]ogs/ - -# Build results on 'Bin' directories -**/[Bb]in/* -# Uncomment if you have tasks that rely on *.refresh files to move binaries -# (https://github.com/github/gitignore/pull/3736) -#!**/[Bb]in/*.refresh - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* -*.trx - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Approval Tests result files -*.received.* - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.idb -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -# but not Directory.Build.rsp, as it configures directory-level build defaults -!Directory.Build.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.tlog -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef +**/mutants.out/ -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio 6 workspace and project file (working project files containing files to include in project) -*.dsw -*.dsp - -# Visual Studio 6 technical files -*.ncb -*.aps - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -**/.paket/paket.exe -paket-files/ - -# FAKE - F# Make -**/.fake/ - -# CodeRush personal settings -**/.cr/personal - -# Python Tools for Visual Studio (PTVS) -**/__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -#tools/** -#!tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog -MSBuild_Logs/ - -# AWS SAM Build and Temporary Artifacts folder -.aws-sam - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -**/.mfractor/ - -# Local History for Visual Studio -**/.localhistory/ - -# Visual Studio History (VSHistory) files -.vshistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -**/.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd - -# VS Code files for those working on multiple tools -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -!.vscode/*.code-snippets - -# Local History for Visual Studio Code -.history/ - -# Built Visual Studio Code Extensions -*.vsix - -# Windows Installer files from build outputs -*.cab -*.msi -*.msix -*.msm -*.msp - -## MacOS related ignores -## -## Get latest from https://github.com/github/gitignore/blob/main/Global/macOS.gitignore -# General +# MacOS files .DS_Store -__MACOSX/ -.AppleDouble -.LSOverride -Icon[ -] - -# Thumbnails -._* -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent +# IntelliJ IDEA +.idea -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk \ No newline at end of file +# Hardhat/Foundry files +cache +cache-hardhat +artifacts +artifacts-hardhat +broadcast + +# Js +node_modules +dist +*.tsbuildinfo +.eslintcache +.rollup.cache + +# Project files +.db + +# WAL artifacts +.journal +*.wal +checkpoint.meta +checkpoint.meta.tmp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..23acfb1 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "lib/openzeppelin-foundry-upgrades"] + path = lib/openzeppelin-foundry-upgrades + url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades +[submodule "lib/openzeppelin-contracts-upgradeable"] + path = lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6761c56..04e49ab 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,4 +13,14 @@ repos: language: system types: [toml] pass_filenames: true - + - id: forge-fmt + name: forge fmt + entry: forge fmt + language: system + files: \.sol$ + - id: prettier + name: prettier + entry: npx prettier --write + language: system + files: \.(js|jsx|ts|tsx|json|css|scss|md|html|toml)$ + pass_filenames: true diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..0b79f98 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +pnpm-lock.yaml + +**/*.toml +**/*.sol + +crates/* +lib/* +target/* \ No newline at end of file diff --git a/.prettierrc.toml b/.prettierrc.toml new file mode 100644 index 0000000..3346a57 --- /dev/null +++ b/.prettierrc.toml @@ -0,0 +1,2 @@ +semi = false +singleQuote = true diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..91e9922 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "solidity.packageDefaultDependenciesContractsDirectory": "contracts", + "solidity.packageDefaultDependenciesDirectory": "lib", + "solidity.exclude": ["lib/**"] +} diff --git a/Cargo.lock b/Cargo.lock index 4ae7a74..2b3cacc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,51 +2,1130 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aead" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "allocator-api2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c583acf993cf4245c4acb0a2cc2ab1f9cc097de73411bb6d3647ff6af2b1013d" + +[[package]] +name = "allocator-api2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c880a97d28a3681c0267bd29cff89621202715b065127cd445fa0f0fe0aa2880" + +[[package]] +name = "alloy" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f609fb6392508278b276906d6247ea44f5777e448db95444fa39e89b7aee896a" +dependencies = [ + "alloy-consensus", + "alloy-contract", + "alloy-core", + "alloy-eips", + "alloy-genesis", + "alloy-network", + "alloy-node-bindings", + "alloy-provider", + "alloy-pubsub", + "alloy-rpc-client", + "alloy-rpc-types", + "alloy-serde", + "alloy-signer", + "alloy-signer-local", + "alloy-transport", + "alloy-transport-http", + "alloy-transport-ipc", + "alloy-transport-ws", + "alloy-trie", +] + +[[package]] +name = "alloy-chains" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b163ff4acf0eac29af05a911397cc418a76e153467b859398adc26cb9335a611" +dependencies = [ + "alloy-primitives", + "num_enum", + "serde", + "strum", +] + +[[package]] +name = "alloy-consensus" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c0dc44157867da82c469c13186015b86abef209bf0e41625e4b68bac61d728" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "alloy-trie", + "alloy-tx-macros", + "auto_impl", + "borsh", + "c-kzg", + "derive_more", + "either", + "k256", + "once_cell", + "rand 0.8.5", + "secp256k1", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-consensus-any" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4cdb42df3871cd6b346d6a938ec2ba69a9a0f49d1f82714bc5c48349268434" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-contract" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f01b6d8e5b4f3222aaf7f18613a7292e2fbc9163fe120649cd1b078ca534349" +dependencies = [ + "alloy-consensus", + "alloy-dyn-abi", + "alloy-json-abi", + "alloy-network", + "alloy-network-primitives", + "alloy-primitives", + "alloy-provider", + "alloy-pubsub", + "alloy-rpc-types-eth", + "alloy-sol-types", + "alloy-transport", + "futures", + "futures-util", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-core" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d4087016b0896051dd3d03e0bedda2f4d4d1689af8addc8450288c63a9e5f68" +dependencies = [ + "alloy-dyn-abi", + "alloy-json-abi", + "alloy-primitives", + "alloy-rlp", + "alloy-sol-types", +] + +[[package]] +name = "alloy-dyn-abi" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "369f5707b958927176265e8a58627fc6195e5dfa5c55689396e68b241b3a72e6" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "alloy-sol-type-parser", + "alloy-sol-types", + "itoa", + "serde", + "serde_json", + "winnow", +] + +[[package]] +name = "alloy-eip2124" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "741bdd7499908b3aa0b159bba11e71c8cddd009a2c2eb7a06e825f1ec87900a5" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "crc", + "serde", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-eip2930" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9441120fa82df73e8959ae0e4ab8ade03de2aaae61be313fbf5746277847ce25" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "serde", +] + +[[package]] +name = "alloy-eip7702" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2919c5a56a1007492da313e7a3b6d45ef5edc5d33416fdec63c0d7a2702a0d20" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "k256", + "serde", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-eip7928" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3231de68d5d6e75332b7489cfcc7f4dfabeba94d990a10e4b923af0e6623540" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "serde", +] + +[[package]] +name = "alloy-eips" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f7ef09f21bd1e9cb8a686f168cb4a206646804567f0889eadb8dcc4c9288c8" +dependencies = [ + "alloy-eip2124", + "alloy-eip2930", + "alloy-eip7702", + "alloy-eip7928", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "auto_impl", + "borsh", + "c-kzg", + "derive_more", + "either", + "serde", + "serde_with", + "sha2 0.10.9", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-genesis" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe3192fca2eb0b0c4b122b3c2d8254496b88a4e810558dddd3ea2f30ad9469df" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-serde", + "alloy-trie", + "borsh", + "serde", + "serde_with", +] + +[[package]] +name = "alloy-hardforks" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165210652f71dfc094b051602bafd691f506c54050a174b1cba18fb5ef706a3" +dependencies = [ + "alloy-chains", + "alloy-eip2124", + "alloy-primitives", + "auto_impl", + "dyn-clone", +] + +[[package]] +name = "alloy-json-abi" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84e3cf01219c966f95a460c95f1d4c30e12f6c18150c21a30b768af2a2a29142" +dependencies = [ + "alloy-primitives", + "alloy-sol-type-parser", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-json-rpc" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ab3330e491053e9608b2a315f147357bb8acb9377a988c1203f2e8e2b296c9" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "http", + "serde", + "serde_json", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "alloy-network" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e22ff194b1e34b4defd1e257e3fe4dce0eee37451c7757a1510d6b23e7379a" +dependencies = [ + "alloy-consensus", + "alloy-consensus-any", + "alloy-eips", + "alloy-json-rpc", + "alloy-network-primitives", + "alloy-primitives", + "alloy-rpc-types-any", + "alloy-rpc-types-eth", + "alloy-serde", + "alloy-signer", + "alloy-sol-types", + "async-trait", + "auto_impl", + "derive_more", + "futures-utils-wasm", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-network-primitives" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d6d15e069a8b11f56bef2eccbad2a873c6dd4d4c81d04dda29710f5ea52f04" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-node-bindings" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9e5dae7d2be44904dba55bb8b538e5de89fdb9e50b3f0f163277b729285011" +dependencies = [ + "alloy-genesis", + "alloy-hardforks", + "alloy-network", + "alloy-primitives", + "alloy-signer", + "alloy-signer-local", + "k256", + "rand 0.8.5", + "serde_json", + "tempfile", + "thiserror 2.0.17", + "tracing", + "url", +] + +[[package]] +name = "alloy-primitives" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6a0fb18dd5fb43ec5f0f6a20be1ce0287c79825827de5744afaa6c957737c33" +dependencies = [ + "alloy-rlp", + "bytes", + "cfg-if", + "const-hex", + "derive_more", + "foldhash 0.2.0", + "hashbrown 0.16.1", + "indexmap 2.12.1", + "itoa", + "k256", + "keccak-asm", + "paste", + "proptest", + "rand 0.9.2", + "rapidhash", + "ruint", + "rustc-hash", + "serde", + "sha3 0.10.8", + "tiny-keccak", +] + +[[package]] +name = "alloy-provider" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f5dde1abc3d582e53d139904fcdd8b2103f0bd03e8f2acb4292edbbaeaa7e6e" +dependencies = [ + "alloy-chains", + "alloy-consensus", + "alloy-eips", + "alloy-json-rpc", + "alloy-network", + "alloy-network-primitives", + "alloy-node-bindings", + "alloy-primitives", + "alloy-pubsub", + "alloy-rpc-client", + "alloy-rpc-types-anvil", + "alloy-rpc-types-debug", + "alloy-rpc-types-eth", + "alloy-rpc-types-trace", + "alloy-rpc-types-txpool", + "alloy-signer", + "alloy-sol-types", + "alloy-transport", + "alloy-transport-http", + "alloy-transport-ipc", + "alloy-transport-ws", + "async-stream", + "async-trait", + "auto_impl", + "dashmap 6.1.0", + "either", + "futures", + "futures-utils-wasm", + "lru", + "parking_lot", + "pin-project", + "reqwest 0.12.28", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tracing", + "url", + "wasmtimer", +] + +[[package]] +name = "alloy-pubsub" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbfe0a3c553a027f722185fb574124d205147fffb309cae52d0a2094f076887" +dependencies = [ + "alloy-json-rpc", + "alloy-primitives", + "alloy-transport", + "auto_impl", + "bimap", + "futures", + "parking_lot", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tower", + "tracing", + "wasmtimer", +] + +[[package]] +name = "alloy-rlp" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f70d83b765fdc080dbcd4f4db70d8d23fe4761f2f02ebfa9146b833900634b4" +dependencies = [ + "alloy-rlp-derive", + "arrayvec", + "bytes", +] + +[[package]] +name = "alloy-rlp-derive" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64b728d511962dda67c1bc7ea7c03736ec275ed2cf4c35d9585298ac9ccf3b73" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "alloy-rpc-client" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a94bdef2710322c6770be08689fee0878c2ad75615b8fc40e05d7f3c9618c0b" +dependencies = [ + "alloy-json-rpc", + "alloy-primitives", + "alloy-pubsub", + "alloy-transport", + "alloy-transport-http", + "alloy-transport-ipc", + "alloy-transport-ws", + "futures", + "pin-project", + "reqwest 0.12.28", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tower", + "tracing", + "url", + "wasmtimer", +] + +[[package]] +name = "alloy-rpc-types" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "811a573c8080e1b492d488e6a240ec5dd7677d7167e91ce9cb4d0ec1fcac8027" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types-anvil", + "alloy-rpc-types-debug", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth", + "alloy-rpc-types-trace", + "alloy-rpc-types-txpool", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-rpc-types-anvil" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "838ca94be532a929f27961851000ec8bbbaeb06e2a2bcca44fac7855a2fe0f6f" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types-eth", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-rpc-types-any" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df0b34551ca2eab8ec83b56cb709ee5da991737282180d354a659b907f00dc" +dependencies = [ + "alloy-consensus-any", + "alloy-rpc-types-eth", + "alloy-serde", +] + +[[package]] +name = "alloy-rpc-types-debug" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49a3a168a5bf18f1cf7ed5723a650aebe714edf7665b53dacf5707716733d0" +dependencies = [ + "alloy-primitives", + "derive_more", + "serde", + "serde_with", +] + +[[package]] +name = "alloy-rpc-types-engine" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe16cd1dea6089902ec609e04261a9ae6d11ec66005ba24c1f97f0eefbc0fa9" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "derive_more", + "rand 0.8.5", + "serde", + "strum", +] + +[[package]] +name = "alloy-rpc-types-eth" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2dc411f13092f237d2bf6918caf80977fc2f51485f9b90cb2a2f956912c8c9" +dependencies = [ + "alloy-consensus", + "alloy-consensus-any", + "alloy-eips", + "alloy-network-primitives", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "alloy-sol-types", + "itertools 0.14.0", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-rpc-types-trace" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cafe859944638c5d57d1a3a0034cdb5d07c98c37de8adce5508f28834acf958f" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types-eth", + "alloy-serde", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-rpc-types-txpool" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afaa06544e36f223b99b1415a12911230fd527994f020736c3c7950d5080208e" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types-eth", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-serde" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ce1e0dbf7720eee747700e300c99aac01b1a95bb93f493a01e78ee28bb1a37" +dependencies = [ + "alloy-primitives", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-signer" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acff6b251740ef473932386d3b71657d3825daebf2217fb41a7ef676229225d4" +dependencies = [ + "alloy-primitives", + "async-trait", + "auto_impl", + "either", + "elliptic-curve", + "k256", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-signer-local" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9129ef31975d987114c27c9930ee817cf3952355834d47f2fdf4596404507e8" +dependencies = [ + "alloy-consensus", + "alloy-network", + "alloy-primitives", + "alloy-signer", + "async-trait", + "coins-bip32", + "coins-bip39", + "k256", + "rand 0.8.5", + "thiserror 2.0.17", + "zeroize", +] + +[[package]] +name = "alloy-sol-macro" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09eb18ce0df92b4277291bbaa0ed70545d78b02948df756bbd3d6214bf39a218" +dependencies = [ + "alloy-sol-macro-expander", + "alloy-sol-macro-input", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "alloy-sol-macro-expander" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95d9fa2daf21f59aa546d549943f10b5cce1ae59986774019fbedae834ffe01b" +dependencies = [ + "alloy-json-abi", + "alloy-sol-macro-input", + "const-hex", + "heck", + "indexmap 2.12.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.111", + "syn-solidity", + "tiny-keccak", +] + +[[package]] +name = "alloy-sol-macro-input" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9396007fe69c26ee118a19f4dee1f5d1d6be186ea75b3881adf16d87f8444686" +dependencies = [ + "alloy-json-abi", + "const-hex", + "dunce", + "heck", + "macro-string", + "proc-macro2", + "quote", + "serde_json", + "syn 2.0.111", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-type-parser" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af67a0b0dcebe14244fc92002cd8d96ecbf65db4639d479f5fcd5805755a4c27" +dependencies = [ + "serde", + "winnow", +] + +[[package]] +name = "alloy-sol-types" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09aeea64f09a7483bdcd4193634c7e5cf9fd7775ee767585270cd8ce2d69dc95" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "alloy-sol-macro", + "serde", +] + +[[package]] +name = "alloy-transport" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec1fb08ee484e615f24867c0b154fff5722bb00176102a16868c6532b7c3623" +dependencies = [ + "alloy-json-rpc", + "auto_impl", + "base64 0.22.1", + "derive_more", + "futures", + "futures-utils-wasm", + "parking_lot", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tower", + "tracing", + "url", + "wasmtimer", +] + +[[package]] +name = "alloy-transport-http" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64b722073c76f2de7e118d546ee1921c50710f97feb32aed50db94cfa5b663e1" +dependencies = [ + "alloy-json-rpc", + "alloy-transport", + "reqwest 0.12.28", + "serde_json", + "tower", + "tracing", + "url", +] + +[[package]] +name = "alloy-transport-ipc" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdedcf401aab4b96d8b5e6638b79d04a6afb96c0bfcb50a2324fbadfe65c47b3" +dependencies = [ + "alloy-json-rpc", + "alloy-pubsub", + "alloy-transport", + "bytes", + "futures", + "interprocess", + "pin-project", + "serde", + "serde_json", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "alloy-transport-ws" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942210908f0c56941097f5653a5f334546940e6fd9073495b257e52216469feb" +dependencies = [ + "alloy-pubsub", + "alloy-transport", + "futures", + "http", + "serde_json", + "tokio", + "tokio-tungstenite", + "tracing", + "ws_stream_wasm", +] + +[[package]] +name = "alloy-trie" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b77b56af09ead281337d06b1d036c88e2dc8a2e45da512a532476dbee94912b" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "arrayvec", + "derive_more", + "nybbles", + "serde", + "smallvec", + "tracing", +] + +[[package]] +name = "alloy-tx-macros" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa0c53e8c1e1ef4d01066b01c737fb62fc9397ab52c6e7bb5669f97d281b9bc" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "ark-ff" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" dependencies = [ - "crypto-common 0.1.7", - "generic-array", + "ark-ff-asm 0.3.0", + "ark-ff-macros 0.3.0", + "ark-serialize 0.3.0", + "ark-std 0.3.0", + "derivative", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.3.3", + "zeroize", ] [[package]] -name = "aho-corasick" -version = "1.1.4" +name = "ark-ff" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" dependencies = [ - "memchr", + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.4.1", + "zeroize", ] [[package]] -name = "alloca" -version = "0.4.0" +name = "ark-ff" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" dependencies = [ - "cc", + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "educe", + "itertools 0.13.0", + "num-bigint", + "num-traits", + "paste", + "zeroize", ] [[package]] -name = "anes" -version = "0.1.6" +name = "ark-ff-asm" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" +dependencies = [ + "quote", + "syn 1.0.109", +] [[package]] -name = "anstyle" -version = "1.0.13" +name = "ark-ff-asm" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] [[package]] -name = "anyhow" -version = "1.0.100" +name = "ark-ff-asm" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.111", +] + +[[package]] +name = "ark-ff-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" +dependencies = [ + "num-bigint", + "num-traits", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "ark-serialize" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" +dependencies = [ + "ark-std 0.3.0", + "digest 0.9.0", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-std 0.4.0", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-std" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.5", +] [[package]] name = "arrayref" @@ -60,20 +1139,43 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" dependencies = [ + "serde", "zeroize", ] [[package]] name = "async-lock" -version = "3.4.1" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ "event-listener", "event-listener-strategy", "pin-project-lite", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -85,6 +1187,17 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version 0.4.1", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -108,13 +1221,36 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "axum" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ "axum-core", + "axum-macros", "bytes", "form_urlencoded", "futures-util", @@ -142,41 +1278,148 @@ dependencies = [ ] [[package]] -name = "axum-core" -version = "0.5.5" +name = "axum-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + +[[package]] +name = "bimap" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.111", +] + +[[package]] +name = "bit-set" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "sync_wrapper", - "tower-layer", - "tower-service", - "tracing", + "bit-vec", ] [[package]] -name = "base16ct" -version = "0.2.0" +name = "bit-vec" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] -name = "base64" -version = "0.22.1" +name = "bitcode" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +checksum = "0a6ed1b54d8dc333e7be604d00fa9262f4635485ffea923647b6521a5fff045d" +dependencies = [ + "arrayvec", + "bitcode_derive", + "bytemuck", + "glam", + "serde", +] [[package]] -name = "base64ct" -version = "1.8.1" +name = "bitcode_derive" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" +checksum = "238b90427dfad9da4a9abd60f3ec1cdee6b80454bde49ed37f1781dd8e9dc7f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "bitcoin-io" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" [[package]] name = "bitcoin-private" @@ -193,12 +1436,34 @@ dependencies = [ "bitcoin-private", ] +[[package]] +name = "bitcoin_hashes" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +dependencies = [ + "bitcoin-io", + "hex-conservative", +] + [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake3" version = "1.8.2" @@ -252,11 +1517,67 @@ dependencies = [ "zeroize", ] +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "sha2 0.10.9", + "tinyvec", +] + +[[package]] +name = "bump-scope" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a117a0e80c453511760a08c607060a778967a8dec5415c1d1a75e20630825824" +dependencies = [ + "allocator-api2 0.2.21", + "allocator-api2 0.3.1", + "allocator-api2 0.4.0", +] + [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "byte-slice-cast" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" [[package]] name = "byteorder" @@ -269,6 +1590,34 @@ name = "bytes" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +dependencies = [ + "serde", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "c-kzg" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e00bf4b112b07b505472dbefd19e37e53307e2bfed5a79e0cc161d58ccd0e687" +dependencies = [ + "blst", + "cc", + "glob", + "hex", + "libc", + "once_cell", + "serde", +] [[package]] name = "cast" @@ -278,9 +1627,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.49" +version = "1.2.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" dependencies = [ "find-msvc-tools", "jobserver", @@ -288,12 +1637,33 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chacha20" version = "0.9.1" @@ -302,7 +1672,18 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", ] [[package]] @@ -312,12 +1693,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", - "chacha20", + "chacha20 0.9.1", "cipher", "poly1305", "zeroize", ] +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -356,6 +1749,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.53" @@ -363,6 +1767,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -371,8 +1776,22 @@ version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] @@ -381,6 +1800,109 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[package]] +name = "coins-bip32" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2073678591747aed4000dd468b97b14d7007f7936851d3f2f01846899f5ebf08" +dependencies = [ + "bs58", + "coins-core", + "digest 0.10.7", + "hmac", + "k256", + "serde", + "sha2 0.10.9", + "thiserror 1.0.69", +] + +[[package]] +name = "coins-bip39" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74b169b26623ff17e9db37a539fe4f15342080df39f129ef7631df7683d6d9d4" +dependencies = [ + "bitvec", + "coins-bip32", + "hmac", + "once_cell", + "pbkdf2", + "rand 0.8.5", + "sha2 0.10.9", + "thiserror 1.0.69", +] + +[[package]] +name = "coins-core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b962ad8545e43a28e14e87377812ba9ae748dd4fd963f4c10e9fcc6d13475b" +dependencies = [ + "base64 0.21.7", + "bech32", + "bs58", + "const-hex", + "digest 0.10.7", + "generic-array", + "ripemd 0.1.3", + "serde", + "sha2 0.10.9", + "sha3 0.10.8", + "thiserror 1.0.69", +] + +[[package]] +name = "color-eyre" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "commonware-codec" version = "0.0.63" @@ -408,9 +1930,9 @@ dependencies = [ "ed25519-consensus", "getrandom 0.2.16", "p256", - "rand", - "rand_chacha", - "rand_core", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", "rayon", "sha2 0.10.9", "thiserror 2.0.17", @@ -454,7 +1976,7 @@ dependencies = [ "opentelemetry-otlp", "opentelemetry_sdk", "prometheus-client", - "rand", + "rand 0.8.5", "rayon", "sha2 0.10.9", "sysinfo", @@ -503,36 +2025,97 @@ dependencies = [ "num-integer", "num-rational", "num-traits", - "rand", + "rand 0.8.5", "thiserror 2.0.17", ] [[package]] -name = "concurrent-queue" -version = "2.5.0" +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-hex" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "proptest", + "serde_core", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dabb6555f92fb9ee4140454eb5dcd14c7960e1225c6d1a6cc361f032947713e" + +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "convert_case" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ - "crossbeam-utils", + "unicode-segmentation", ] [[package]] -name = "const-oid" -version = "0.9.6" +name = "core-foundation" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] [[package]] -name = "const-oid" +name = "core-foundation" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dabb6555f92fb9ee4140454eb5dcd14c7960e1225c6d1a6cc361f032947713e" - -[[package]] -name = "constant_time_eq" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] [[package]] name = "core-foundation-sys" @@ -549,6 +2132,30 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.5.0" @@ -668,7 +2275,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -680,15 +2287,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "typenum", ] [[package]] name = "crypto-common" -version = "0.2.0-rc.5" +version = "0.2.0-rc.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919bd05924682a5480aec713596b9e2aabed3a0a6022fab6847f85a99e5f190a" +checksum = "f7fa010a85c7440677a0f4c59cf7ebabef52d7d8b4f79051e5fa60d3f0dd87d0" dependencies = [ "hybrid-array", ] @@ -700,10 +2307,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "fiat-crypto", - "rustc_version", + "rustc_version 0.4.1", "subtle", "zeroize", ] @@ -727,11 +2334,47 @@ checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" dependencies = [ "byteorder", "digest 0.9.0", - "rand_core", + "rand_core 0.6.4", "subtle-ng", "zeroize", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "serde", + "strsim", + "syn 2.0.111", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.111", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -745,6 +2388,26 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + [[package]] name = "der" version = "0.7.10" @@ -755,6 +2418,50 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "syn 2.0.111", + "unicode-xid", +] + [[package]] name = "digest" version = "0.9.0" @@ -784,7 +2491,7 @@ checksum = "ea390c940e465846d64775e55e3115d5dc934acb953de6f6e6360bc232fe2bf7" dependencies = [ "block-buffer 0.11.0", "const-oid 0.10.1", - "crypto-common 0.2.0-rc.5", + "crypto-common 0.2.0-rc.6", ] [[package]] @@ -798,12 +2505,30 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + [[package]] name = "dtoa" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "ecdsa" version = "0.16.9" @@ -814,6 +2539,7 @@ dependencies = [ "digest 0.10.7", "elliptic-curve", "rfc6979", + "serdect", "signature", "spki", ] @@ -826,17 +2552,32 @@ checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" dependencies = [ "curve25519-dalek-ng", "hex", - "rand_core", + "rand_core 0.6.4", "sha2 0.9.9", "thiserror 1.0.69", "zeroize", ] +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] [[package]] name = "elliptic-curve" @@ -851,12 +2592,33 @@ dependencies = [ "generic-array", "group", "pkcs8", - "rand_core", + "rand_core 0.6.4", "sec1", + "serdect", "subtle", "zeroize", ] +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "env_logger" version = "0.10.2" @@ -876,6 +2638,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "event-listener" version = "5.4.1" @@ -897,13 +2669,51 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fastrlp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "fastrlp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + [[package]] name = "ff" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -919,6 +2729,51 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand 0.8.5", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -928,6 +2783,18 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.31" @@ -1023,6 +2890,12 @@ dependencies = [ "slab", ] +[[package]] +name = "futures-utils-wasm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9" + [[package]] name = "generic-array" version = "0.14.7" @@ -1052,13 +2925,41 @@ name = "getrandom" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" dependencies = [ "cfg-if", "libc", "r-efi", + "rand_core 0.10.0", "wasip2", + "wasip3", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "glam" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34627c5158214743a374170fed714833fdf4e4b0cbcc1ea98417866a4c5d4441" + [[package]] name = "glob" version = "0.3.3" @@ -1072,7 +2973,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b" dependencies = [ "cfg-if", - "dashmap", + "dashmap 5.5.3", "futures", "futures-timer", "no-std-compat", @@ -1080,7 +2981,7 @@ dependencies = [ "parking_lot", "portable-atomic", "quanta", - "rand", + "rand 0.8.5", "smallvec", "spinning_top", ] @@ -1092,10 +2993,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.12.1", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.7.1" @@ -1107,17 +3027,45 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2 0.2.21", + "equivalent", + "foldhash 0.1.5", +] + [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", + "serde", + "serde_core", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" @@ -1131,6 +3079,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + [[package]] name = "hmac" version = "0.12.1" @@ -1197,6 +3154,7 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f471e0a81b2f90ffc0cb2f951ae04da57de8baa46fa99112b062a5173a5088d0" dependencies = [ + "bytemuck", "typenum", ] @@ -1210,6 +3168,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -1222,13 +3181,46 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.4", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1246,6 +3238,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.1.1" @@ -1327,6 +3343,18 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1348,6 +3376,43 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.12.1" @@ -1356,6 +3421,8 @@ checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -1367,6 +3434,21 @@ dependencies = [ "generic-array", ] +[[package]] +name = "interprocess" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d941b405bd2322993887859a8ee6ac9134945a24ec5ec763a8a962fc64dfec2d" +dependencies = [ + "doctest-file", + "futures-core", + "libc", + "recvmsg", + "tokio", + "widestring", + "windows-sys 0.52.0", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1394,6 +3476,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.10.5" @@ -1412,11 +3500,83 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" + +[[package]] +name = "jiff" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" +dependencies = [ + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-sys 0.61.2", +] + +[[package]] +name = "jiff-static" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68971ebff725b9e2ca27a601c5eb38a4c5d64422c4cbab0c535f248087eda5c2" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" @@ -1438,13 +3598,47 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "serdect", + "sha2 0.10.9", + "signature", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures 0.2.17", +] + [[package]] name = "keccak" version = "0.2.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d546793a04a1d3049bd192856f804cfe96356e2cf36b54b4e575155babe9f41" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", +] + +[[package]] +name = "keccak-asm" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "505d1856a39b200489082f90d897c3f07c455563880bc5952e38eabf731c83b6" +dependencies = [ + "digest 0.10.7", + "sha3-asm", ] [[package]] @@ -1453,12 +3647,66 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "librocksdb-sys" +version = "0.17.3+10.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef2a00ee60fe526157c9023edab23943fae1ce2ab6f4abb2a807c1746835de9" +dependencies = [ + "bindgen", + "bzip2-sys", + "cc", + "libc", + "libz-sys", + "lz4-sys", + "zstd-sys", +] + +[[package]] +name = "libz-sys" +version = "1.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "litemap" version = "0.8.1" @@ -1480,6 +3728,42 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lz4-sys" +version = "1.11.1+lz4-1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "macro-string" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "matchers" version = "0.2.0" @@ -1507,6 +3791,21 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "1.1.1" @@ -1518,12 +3817,39 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe 0.1.6", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", +] + [[package]] name = "no-std-compat" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nonzero_ext" version = "0.3.0" @@ -1532,9 +3858,9 @@ checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" [[package]] name = "ntapi" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +checksum = "c70f219e21142367c70c0b30c6a9e3a14d55b4d12a204d897fbec83a0363f081" dependencies = [ "winapi", ] @@ -1558,6 +3884,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -1585,6 +3917,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1597,12 +3930,62 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "nybbles" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4b5ecbd0beec843101bffe848217f770e8b8da81d8355b7d6e226f2199b3dc" +dependencies = [ + "alloy-rlp", + "cfg-if", + "proptest", + "ruint", + "serde", + "smallvec", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "oorandom" version = "11.1.5" @@ -1615,6 +3998,66 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-src" +version = "300.5.4+3.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + [[package]] name = "opentelemetry" version = "0.28.0" @@ -1639,7 +4082,7 @@ dependencies = [ "bytes", "http", "opentelemetry", - "reqwest", + "reqwest 0.12.28", "tracing", ] @@ -1657,7 +4100,7 @@ dependencies = [ "opentelemetry-proto", "opentelemetry_sdk", "prost", - "reqwest", + "reqwest 0.12.28", "thiserror 2.0.17", "tracing", ] @@ -1687,7 +4130,7 @@ dependencies = [ "glob", "opentelemetry", "percent-encoding", - "rand", + "rand 0.8.5", "serde_json", "thiserror 2.0.17", "tokio", @@ -1700,11 +4143,17 @@ name = "opentimestamps" version = "0.2.0" source = "git+https://github.com/opentimestamps/rust-opentimestamps#c87e3e18284fd1bb9456e50721aa4e2796aed057" dependencies = [ - "bitcoin_hashes", + "bitcoin_hashes 0.12.0", "env_logger", "log", ] +[[package]] +name = "owo-colors" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" + [[package]] name = "p256" version = "0.13.2" @@ -1727,6 +4176,34 @@ dependencies = [ "winapi", ] +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "parking" version = "2.2.1" @@ -1762,12 +4239,42 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version 0.4.1", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -1850,16 +4357,25 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f59e70c4aef1e55797c2e8fd94a4f2a973fc972cfde0e0b05f683667b0cd39dd" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] [[package]] name = "potential_utf" @@ -1870,6 +4386,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1879,6 +4401,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.111", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -1888,6 +4420,17 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "primitive-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +dependencies = [ + "fixed-hash", + "impl-codec", + "uint", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -1897,6 +4440,28 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -1929,6 +4494,25 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "proptest" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "prost" version = "0.13.5" @@ -1946,7 +4530,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.13.0", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.111", @@ -1967,6 +4551,68 @@ dependencies = [ "winapi", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.42" @@ -1977,39 +4623,112 @@ dependencies = [ ] [[package]] -name = "r-efi" -version = "5.3.0" +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "serde", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", + "serde", +] + +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20 0.10.0", + "getrandom 0.4.1", + "rand_core 0.10.0", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] [[package]] -name = "rand" -version = "0.8.5" +name = "rand_core" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "libc", - "rand_chacha", - "rand_core", + "getrandom 0.3.4", + "serde", ] [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "rand_core" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "ppv-lite86", - "rand_core", + "rand_core 0.9.3", ] [[package]] -name = "rand_core" -version = "0.6.4" +name = "rapidhash" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "2988730ee014541157f48ce4dcc603940e00915edc3c7f9a8d78092256bb2493" dependencies = [ - "getrandom 0.2.16", + "rustversion", ] [[package]] @@ -2041,6 +4760,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -2050,6 +4775,26 @@ dependencies = [ "bitflags", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "regex" version = "1.12.2" @@ -2081,11 +4826,11 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.25" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -2094,16 +4839,64 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", + "hyper-tls", "hyper-util", "js-sys", "log", + "native-tls", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-native-tls", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.4", +] + +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", "tower", "tower-http", "tower-service", @@ -2123,6 +4916,29 @@ dependencies = [ "subtle", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "ripemd" version = "0.2.0-rc.3" @@ -2132,13 +4948,183 @@ dependencies = [ "digest 0.11.0-rc.4", ] +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rustc-hex", +] + +[[package]] +name = "rocksdb" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddb7af00d2b17dbd07d82c0063e25411959748ff03e8d4f96134c2ff41fce34f" +dependencies = [ + "libc", + "librocksdb-sys", +] + +[[package]] +name = "ruint" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68df0380e5c9d20ce49534f292a36a7514ae21350726efe1865bdb1fa91d278" +dependencies = [ + "alloy-rlp", + "ark-ff 0.3.0", + "ark-ff 0.4.2", + "ark-ff 0.5.0", + "bytes", + "fastrlp 0.3.1", + "fastrlp 0.4.0", + "num-bigint", + "num-integer", + "num-traits", + "parity-scale-codec", + "primitive-types", + "proptest", + "rand 0.8.5", + "rand 0.9.2", + "rlp", + "ruint-macro", + "serde_core", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver 0.11.0", +] + [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver", + "semver 1.0.27", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "aws-lc-rs", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe 0.2.1", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.5.1", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -2147,11 +5133,23 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "62049b2877bf12821e8f9ad256ee38fdc31db7387ec2d3b3f403024de2034aea" [[package]] name = "same-file" @@ -2162,6 +5160,39 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2178,16 +5209,98 @@ dependencies = [ "der", "generic-array", "pkcs8", + "serdect", "subtle", "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" +dependencies = [ + "bitcoin_hashes 0.14.1", + "rand 0.8.5", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + [[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +[[package]] +name = "semver-parser" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" +dependencies = [ + "pest", +] + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + [[package]] name = "serde" version = "1.0.228" @@ -2220,15 +5333,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "6af14725505314343e673e9ecb7cd7e8a36aa9791eb936235a3567cc31447ae4" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -2254,6 +5367,58 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.12.1", + "schemars 0.9.0", + "schemars 1.1.0", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + [[package]] name = "sha1" version = "0.11.0-rc.3" @@ -2261,7 +5426,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa1ae819b9870cadc959a052363de870944a1646932d274a4e270f64bf79e5ef" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.11.0-rc.4", ] @@ -2273,7 +5438,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.9.0", "opaque-debug", ] @@ -2285,7 +5450,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -2296,10 +5461,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d43dc0354d88b791216bb5c1bfbb60c0814460cc653ae0ebd71f286d0bd927" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.11.0-rc.4", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak 0.1.5", +] + [[package]] name = "sha3" version = "0.11.0-rc.3" @@ -2307,7 +5482,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2103ca0e6f4e9505eae906de5e5883e06fc3b2232fb5d6914890c7bbcb62f478" dependencies = [ "digest 0.11.0-rc.4", - "keccak", + "keccak 0.2.0-rc.0", +] + +[[package]] +name = "sha3-asm" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28efc5e327c837aa837c59eae585fc250715ef939ac32881bcc11677cd02d46" +dependencies = [ + "cc", + "cfg-if", ] [[package]] @@ -2341,7 +5526,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -2355,6 +5540,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -2391,6 +5579,39 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2425,6 +5646,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn-solidity" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f92d01b5de07eaf324f7fca61cc6bd3d82bbc1de5b6c963e6fe79e86f36580d" +dependencies = [ + "paste", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -2459,6 +5692,25 @@ dependencies = [ "windows", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -2526,6 +5778,46 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -2546,6 +5838,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.48.0" @@ -2574,6 +5881,26 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -2583,24 +5910,54 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.11", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", ] [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.9" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ - "indexmap", + "indexmap 2.12.1", "toml_datetime", "toml_parser", "winnow", @@ -2608,9 +5965,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow", ] @@ -2622,7 +5979,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" dependencies = [ "async-trait", - "base64", + "base64 0.22.1", "bytes", "http", "http-body", @@ -2684,9 +6041,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -2707,14 +6064,24 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", ] +[[package]] +name = "tracing-error" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" +dependencies = [ + "tracing", + "tracing-subscriber", +] + [[package]] name = "tracing-log" version = "0.2.0" @@ -2781,18 +6148,73 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.2", + "rustls", + "rustls-pki-types", + "sha1 0.10.6", + "thiserror 2.0.17", + "utf-8", +] + [[package]] name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "universal-hash" version = "0.5.1" @@ -2803,6 +6225,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.7" @@ -2815,6 +6243,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -2822,34 +6256,153 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] -name = "uts-bmt" +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uts-bmt" +version = "0.1.0" +dependencies = [ + "bytemuck", + "commonware-cryptography", + "commonware-storage", + "criterion 0.8.1", + "digest 0.11.0-rc.4", + "hybrid-array", + "sha2 0.11.0-rc.3", + "sha3 0.11.0-rc.3", +] + +[[package]] +name = "uts-calendar" +version = "0.1.0" +dependencies = [ + "alloy-primitives", + "alloy-provider", + "alloy-signer", + "alloy-signer-local", + "axum", + "bump-scope", + "bytemuck", + "bytes", + "criterion 0.8.1", + "digest 0.11.0-rc.4", + "eyre", + "hashbrown 0.15.5", + "itoa", + "rocksdb", + "sha3 0.11.0-rc.3", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", + "uts-bmt", + "uts-contracts", + "uts-core", + "uts-journal", + "uts-stamper", +] + +[[package]] +name = "uts-cli" +version = "0.1.0" +dependencies = [ + "alloy-provider", + "bytemuck", + "clap", + "color-eyre", + "digest 0.11.0-rc.4", + "eyre", + "futures", + "jiff", + "rand 0.10.0", + "reqwest 0.13.2", + "ripemd 0.2.0-rc.3", + "sha1 0.11.0-rc.3", + "sha2 0.11.0-rc.3", + "sha3 0.11.0-rc.3", + "tokio", + "tracing", + "url", + "uts-bmt", + "uts-core", +] + +[[package]] +name = "uts-contracts" version = "0.1.0" dependencies = [ - "commonware-cryptography", - "commonware-storage", - "criterion 0.8.1", - "digest 0.11.0-rc.4", - "sha2 0.11.0-rc.3", - "sha3", + "alloy", + "alloy-contract", + "alloy-primitives", + "alloy-sol-types", + "eyre", + "futures", + "tokio", ] [[package]] name = "uts-core" version = "0.1.0" dependencies = [ + "alloy-chains", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-types-eth", + "alloy-sol-types", "auto_impl", + "bytes", "criterion 0.5.1", "digest 0.11.0-rc.4", "hex", + "once_cell", "opentimestamps", "paste", - "ripemd", - "sha1", + "ripemd 0.2.0-rc.3", + "serde", + "serde_json", + "serde_with", + "sha1 0.11.0-rc.3", "sha2 0.11.0-rc.3", - "sha3", - "smallvec", + "sha3 0.11.0-rc.3", + "thiserror 2.0.17", + "tokio", + "tracing", + "uts-bmt", + "uts-contracts", +] + +[[package]] +name = "uts-journal" +version = "0.1.0" +dependencies = [ + "eyre", + "tempfile", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "uts-stamper" +version = "0.1.0" +dependencies = [ + "alloy-primitives", + "alloy-provider", + "bitcode", + "bytemuck", + "digest 0.11.0-rc.4", + "rocksdb", + "serde", "thiserror 2.0.17", + "tokio", "tracing", + "uts-bmt", + "uts-contracts", + "uts-core", + "uts-journal", ] [[package]] @@ -2858,12 +6411,27 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -2895,7 +6463,16 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] @@ -2956,6 +6533,54 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.12.1", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.12.1", + "semver 1.0.27", +] + +[[package]] +name = "wasmtimer" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" +dependencies = [ + "futures", + "js-sys", + "parking_lot", + "pin-utils", + "slab", + "wasm-bindgen", +] + [[package]] name = "web-sys" version = "0.3.83" @@ -2976,6 +6601,39 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.4", +] + +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" @@ -3013,7 +6671,7 @@ version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" dependencies = [ - "windows-core", + "windows-core 0.57.0", "windows-targets 0.52.6", ] @@ -3023,12 +6681,25 @@ version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" dependencies = [ - "windows-implement", - "windows-interface", - "windows-result", + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link", + "windows-result 0.4.1", + "windows-strings", +] + [[package]] name = "windows-implement" version = "0.57.0" @@ -3040,6 +6711,17 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "windows-interface" version = "0.57.0" @@ -3051,6 +6733,17 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -3066,6 +6759,42 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -3084,6 +6813,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -3117,6 +6861,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -3129,6 +6879,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -3141,6 +6897,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3165,6 +6927,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -3177,6 +6945,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -3189,6 +6963,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -3201,6 +6981,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -3228,12 +7014,128 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.12.1", + "prettyplease", + "syn 2.0.111", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.111", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap 2.12.1", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.12.1", + "log", + "semver 1.0.27", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "writeable" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "ws_stream_wasm" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "log", + "pharos", + "rustc_version 0.4.1", + "send_wrapper", + "thiserror 2.0.17", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x25519-dalek" version = "2.0.1" @@ -3241,7 +7143,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", - "rand_core", + "rand_core 0.6.4", "serde", "zeroize", ] @@ -3363,6 +7265,12 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "zmij" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dccf46b25b205e4bebe1d5258a991df1cc17801017a845cb5b3fe0269781aa" + [[package]] name = "zstd" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index 3c2fa86..5fd53e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +default-members = ["crates/cli"] members = ["crates/*"] resolver = "3" @@ -28,7 +29,21 @@ unnecessary-box-returns = "warn" unnecessary-debug-formatting = "warn" [workspace.dependencies] +alloy = "1" +alloy-chains = "0.2" +alloy-contract = "1.2" +alloy-primitives = "1.5" +alloy-provider = "1.2" +alloy-rpc-types-eth = "1.6" +alloy-signer = "1.1" +alloy-signer-local = "1.1" +alloy-sol-types = "1.5" auto_impl = "1.3" +axum = { version = "0.8", default-features = false } +axum-extra = "0.12" +bitcode = "0.6" +bump-scope = { version = "1.5", features = ["nightly"] } +bytemuck = "1" bytes = "1.11" cfg-if = "1.0" clap = { version = "4.5", features = ["derive"] } @@ -37,22 +52,57 @@ compact_str = "0.9" config = "0.15" const_format = "0.2" criterion = { version = "0.8", features = ["html_reports"] } +dyn-clone = "1.0" eyre = "0.6" +futures = "0.3" hex = "0.4" +itoa = "1.0" +jiff = "0.2.20" +once_cell = { version = "1.21", default-features = false } paste = "1.0" +rand = "0.10" regex = "1.12" +reqwest = { version = "0.13", default-features = false } +rocksdb = "0.24" serde = "1.0" +serde-wasm-bindgen = "0.6" +serde_json = "1.0" serde_with = "3.16" -smallvec = { version = "1.15", features = ["union", "const_generics", "const_new"] } strum = "0.27" thiserror = "2" tokio = { version = "1", features = ["rt"] } toml = "0.9" +tower-http = "0.6" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +url = "2.5" +wasm-bindgen = "0.2" +crypto-common = "0.2.0-rc.5" digest = "0.11.0-rc.4" +hybrid-array = "0.4.5" ripemd = "0.2.0-rc.3" sha1 = "0.11.0-rc.3" sha2 = "0.11.0-rc.3" sha3 = "0.11.0-rc.3" + +tempfile = "3" +uts-bmt = { path = "crates/bmt" } +uts-contracts = { path = "crates/contracts" } +uts-core = { path = "crates/core" } +uts-journal = { path = "crates/journal" } +uts-stamper = { path = "crates/stamper" } + +[profile.bench] +codegen-units = 1 +lto = "fat" + +[profile.release] +codegen-units = 1 +lto = "fat" + +[profile.dev] +opt-level = 1 + +[profile.dev.package."*"] +opt-level = 3 diff --git a/README.md b/README.md index 5eb0ce5..9bc639b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ # Universal Timestamps (UTS) Universal Timestamps is a super set of [opentimestamps](https://opentimestamps.org/). + +## Development + +### Pre-requisites + +- Rust >= 1.94.0-nightly (e7d44143a 2025-12-24) +- Cargo >= 1.94.0-nightly (3861f60f6 2025-12-19) +- pnpm >= 10.26.2 diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/web/.vscode/extensions.json b/apps/web/.vscode/extensions.json new file mode 100644 index 0000000..a7cea0b --- /dev/null +++ b/apps/web/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/apps/web/README.md b/apps/web/README.md new file mode 100644 index 0000000..33895ab --- /dev/null +++ b/apps/web/README.md @@ -0,0 +1,5 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/apps/web/lingui.config.ts b/apps/web/lingui.config.ts new file mode 100644 index 0000000..5771099 --- /dev/null +++ b/apps/web/lingui.config.ts @@ -0,0 +1,15 @@ +import type { LinguiConfig } from '@lingui/conf' + +const config: LinguiConfig = { + locales: ['en', 'zh'], + sourceLocale: 'en', + catalogs: [ + { + path: 'src/locales/{locale}', + include: ['src'], + }, + ], + format: 'po', +} + +export default config diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..48a2f2d --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,40 @@ +{ + "name": "web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview", + "lint": "eslint src --cache --fix", + "typecheck": "vue-tsc --noEmit" + }, + "dependencies": { + "@lingui/core": "^5.9.2", + "@lingui/message-utils": "^5.9.2", + "@noble/hashes": "^2.0.1", + "@uts/sdk": "workspace:*", + "@vueuse/core": "^14.2.1", + "date-fns": "^4.1.0", + "ethers": "^6.16.0", + "jszip": "^3.10.1", + "lucide-vue-next": "^0.575.0", + "pinia": "^3.0.4", + "vue": "^3.5.24" + }, + "devDependencies": { + "@lingui/cli": "^5.9.2", + "@lingui/format-po": "^5.9.2", + "@lingui/vite-plugin": "^5.9.2", + "@tailwindcss/vite": "^4.2.1", + "@types/jszip": "^3.4.1", + "@types/node": "^24.10.1", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/tsconfig": "^0.8.1", + "tailwindcss": "^4.2.1", + "typescript": "~5.9.3", + "vite": "^7.2.4", + "vue-tsc": "^3.1.4" + } +} diff --git a/apps/web/public/vite.svg b/apps/web/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/apps/web/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/App.vue b/apps/web/src/App.vue new file mode 100644 index 0000000..9239bff --- /dev/null +++ b/apps/web/src/App.vue @@ -0,0 +1,7 @@ + + + diff --git a/apps/web/src/assets/Scroll_Logomark.svg b/apps/web/src/assets/Scroll_Logomark.svg new file mode 100644 index 0000000..0e07355 --- /dev/null +++ b/apps/web/src/assets/Scroll_Logomark.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/assets/vue.svg b/apps/web/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/apps/web/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/components/base/BaseButton.vue b/apps/web/src/components/base/BaseButton.vue new file mode 100644 index 0000000..66e3a34 --- /dev/null +++ b/apps/web/src/components/base/BaseButton.vue @@ -0,0 +1,49 @@ + + + diff --git a/apps/web/src/components/base/GlassCard.vue b/apps/web/src/components/base/GlassCard.vue new file mode 100644 index 0000000..71d946d --- /dev/null +++ b/apps/web/src/components/base/GlassCard.vue @@ -0,0 +1,17 @@ + + + diff --git a/apps/web/src/components/base/StatusBadge.vue b/apps/web/src/components/base/StatusBadge.vue new file mode 100644 index 0000000..6be8731 --- /dev/null +++ b/apps/web/src/components/base/StatusBadge.vue @@ -0,0 +1,53 @@ + + + diff --git a/apps/web/src/components/feed/LiveFeed.vue b/apps/web/src/components/feed/LiveFeed.vue new file mode 100644 index 0000000..55946f7 --- /dev/null +++ b/apps/web/src/components/feed/LiveFeed.vue @@ -0,0 +1,261 @@ + + + diff --git a/apps/web/src/components/stamp/StampingWorkflow.vue b/apps/web/src/components/stamp/StampingWorkflow.vue new file mode 100644 index 0000000..476d581 --- /dev/null +++ b/apps/web/src/components/stamp/StampingWorkflow.vue @@ -0,0 +1,230 @@ + + + diff --git a/apps/web/src/components/terminal/HeroTerminal.vue b/apps/web/src/components/terminal/HeroTerminal.vue new file mode 100644 index 0000000..4f16e78 --- /dev/null +++ b/apps/web/src/components/terminal/HeroTerminal.vue @@ -0,0 +1,284 @@ + + + diff --git a/apps/web/src/components/upgrade/UpgradePanel.vue b/apps/web/src/components/upgrade/UpgradePanel.vue new file mode 100644 index 0000000..7dcb840 --- /dev/null +++ b/apps/web/src/components/upgrade/UpgradePanel.vue @@ -0,0 +1,205 @@ + + + diff --git a/apps/web/src/components/verify/AttestationDetail.vue b/apps/web/src/components/verify/AttestationDetail.vue new file mode 100644 index 0000000..4c85ad0 --- /dev/null +++ b/apps/web/src/components/verify/AttestationDetail.vue @@ -0,0 +1,414 @@ + + + diff --git a/apps/web/src/components/verify/MerkleTreeViz.vue b/apps/web/src/components/verify/MerkleTreeViz.vue new file mode 100644 index 0000000..25ca434 --- /dev/null +++ b/apps/web/src/components/verify/MerkleTreeViz.vue @@ -0,0 +1,393 @@ + + + + + diff --git a/apps/web/src/components/verify/VerificationResult.vue b/apps/web/src/components/verify/VerificationResult.vue new file mode 100644 index 0000000..de800f6 --- /dev/null +++ b/apps/web/src/components/verify/VerificationResult.vue @@ -0,0 +1,279 @@ + + + diff --git a/apps/web/src/composables/useFileDigest.ts b/apps/web/src/composables/useFileDigest.ts new file mode 100644 index 0000000..4bea4fc --- /dev/null +++ b/apps/web/src/composables/useFileDigest.ts @@ -0,0 +1,176 @@ +import { ref } from 'vue' +import { sha256 } from '@noble/hashes/sha2.js' +import { keccak_256 } from '@noble/hashes/sha3.js' +import { hexlify } from 'ethers/utils' +import type { SecureDigestOp, DigestHeader } from '@uts/sdk' + +export interface FileDigestResult { + fileName: string + fileSize: number + algorithm: SecureDigestOp + digest: string + header: DigestHeader +} + +// Estimated browser hashing throughput (~200 MB/s), used for fake progress +const ESTIMATED_THROUGHPUT = 200 * 1024 * 1024 +const MIN_DURATION_MS = 300 +const MAX_DURATION_MS = 30000 +const PROGRESS_CAP = 92 // fake progress caps at this before real completion + +export function useFileDigest() { + const isDigesting = ref(false) + const progress = ref(0) + const error = ref(null) + const result = ref(null) + + let progressTimer: ReturnType | null = null + + function startFakeProgress(totalBytes: number, fileCount: number) { + progress.value = 0 + const estimatedMs = Math.max( + MIN_DURATION_MS, + Math.min( + ((totalBytes * fileCount) / ESTIMATED_THROUGHPUT) * 1000, + MAX_DURATION_MS, + ), + ) + const intervalMs = 50 + const steps = estimatedMs / intervalMs + const increment = PROGRESS_CAP / steps + + progressTimer = setInterval(() => { + progress.value = Math.min(progress.value + increment, PROGRESS_CAP) + }, intervalMs) + } + + function stopFakeProgress() { + if (progressTimer) { + clearInterval(progressTimer) + progressTimer = null + } + progress.value = 100 + } + + async function digestFile( + file: File, + algorithm: SecureDigestOp = 'SHA256', + ): Promise { + isDigesting.value = true + progress.value = 0 + error.value = null + result.value = null + + startFakeProgress(file.size, 1) + + try { + const factory = algorithm === 'KECCAK256' ? keccak_256 : sha256 + const hasher = factory.create() + + const reader = file.stream().getReader() + while (true) { + const { done, value } = await reader.read() + if (done) break + hasher.update(value) + // Yield to main thread periodically for UI updates + await new Promise((resolve) => setTimeout(resolve, 0)) + } + + stopFakeProgress() + + const digestBytes = hasher.digest() + const digestHex = hexlify(digestBytes) + + const digestResult: FileDigestResult = { + fileName: file.webkitRelativePath || file.name, + fileSize: file.size, + algorithm, + digest: digestHex, + header: { + kind: algorithm, + digest: digestBytes, + }, + } + + result.value = digestResult + return digestResult + } catch (e) { + stopFakeProgress() + const msg = e instanceof Error ? e.message : 'Failed to digest file' + error.value = msg + throw new Error(msg) + } finally { + isDigesting.value = false + } + } + + async function digestFiles( + files: File[], + algorithm: SecureDigestOp = 'SHA256', + ): Promise { + isDigesting.value = true + progress.value = 0 + error.value = null + result.value = null + + const totalBytes = files.reduce((sum, f) => sum + f.size, 0) + startFakeProgress(totalBytes, files.length) + + try { + const results: FileDigestResult[] = [] + for (const file of files) { + const factory = algorithm === 'KECCAK256' ? keccak_256 : sha256 + const hasher = factory.create() + + const reader = file.stream().getReader() + while (true) { + const { done, value } = await reader.read() + if (done) break + hasher.update(value) + await new Promise((resolve) => setTimeout(resolve, 0)) + } + + const digestBytes = hasher.digest() + results.push({ + fileName: file.webkitRelativePath || file.name, + fileSize: file.size, + algorithm, + digest: hexlify(digestBytes), + header: { kind: algorithm, digest: digestBytes }, + }) + } + + stopFakeProgress() + if (results.length > 0) result.value = results[results.length - 1]! + return results + } catch (e) { + stopFakeProgress() + const msg = e instanceof Error ? e.message : 'Failed to digest files' + error.value = msg + throw new Error(msg) + } finally { + isDigesting.value = false + } + } + + function reset() { + if (progressTimer) { + clearInterval(progressTimer) + progressTimer = null + } + isDigesting.value = false + progress.value = 0 + error.value = null + result.value = null + } + + return { + isDigesting, + progress, + error, + result, + digestFile, + digestFiles, + reset, + } +} diff --git a/apps/web/src/composables/useLingui.ts b/apps/web/src/composables/useLingui.ts new file mode 100644 index 0000000..4c66010 --- /dev/null +++ b/apps/web/src/composables/useLingui.ts @@ -0,0 +1,40 @@ +import { ref } from 'vue' +import { i18n } from '@/i18n' +import { generateMessageId } from '@lingui/message-utils/generateMessageId' + +const _locale = ref(i18n.locale || 'en') + +// Keep _locale in sync when initLocale restores a saved locale +if (i18n.locale && i18n.locale !== _locale.value) { + _locale.value = i18n.locale +} + +export function useLingui() { + // Sync locale ref with i18n in case initLocale restored a saved locale + if (_locale.value !== i18n.locale) { + _locale.value = i18n.locale + } + + /** + * Translate a message. The message text is hashed to match + * Lingui's compiled catalog keys, and the reactive `_locale` ref + * ensures Vue re-renders when the active locale changes. + */ + function t(id: string, values?: Record): string { + // Reading _locale.value ensures Vue tracks this as a dependency + // so template expressions re-evaluate when locale changes. + void _locale.value + const hashedId = generateMessageId(id) + return i18n._({ id: hashedId, message: id }, values) + } + + async function activate(locale: string) { + const catalog = await import(`../locales/${locale}.po`) + i18n.load(locale, catalog.messages) + i18n.activate(locale) + _locale.value = locale + localStorage.setItem('uts-locale', locale) + } + + return { t, locale: _locale, activate, i18n } +} diff --git a/apps/web/src/composables/useTimestampSDK.ts b/apps/web/src/composables/useTimestampSDK.ts new file mode 100644 index 0000000..579e91c --- /dev/null +++ b/apps/web/src/composables/useTimestampSDK.ts @@ -0,0 +1,286 @@ +import { ref, shallowRef } from 'vue' +import { SDK, VerifyStatus, UpgradeStatus, Decoder, Encoder } from '@uts/sdk' +import type { + DetachedTimestamp, + AttestationStatus, + UpgradeResult, + DigestHeader, + StampEventCallback, +} from '@uts/sdk' +import type { Eip1193Provider } from 'ethers' +import JSZip from 'jszip' + +export type StampPhase = + | 'idle' + | 'generating-nonce' + | 'building-merkle-tree' + | 'broadcasting' + | 'waiting-attestation' + | 'building-proof' + | 'complete' + | 'upgrading' + | 'upgraded' + | 'error' + +let _sdkInstance: SDK | null = null + +export function getSDK(): SDK { + if (!_sdkInstance) { + _sdkInstance = new SDK({ timeout: 15000 }) + } + return _sdkInstance +} + +export function resetSDK(options?: { + calendars?: URL[] + web3Provider?: Eip1193Provider | null +}) { + _sdkInstance = new SDK({ + timeout: 15000, + calendars: options?.calendars, + web3Provider: options?.web3Provider, + }) +} + +export function setWeb3Provider(provider: Eip1193Provider | null) { + const sdk = getSDK() + sdk.web3Provider = provider +} + +function triggerDownload(blob: Blob, fileName: string) { + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = fileName + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) +} + +export function downloadOtsFile(stamp: DetachedTimestamp, fileName?: string) { + const encoded = Encoder.encodeDetachedTimestamp(stamp) + const blob = new Blob([encoded as BlobPart], { + type: 'application/octet-stream', + }) + triggerDownload(blob, fileName ? `${fileName}.ots` : 'timestamp.ots') +} + +async function downloadStampsAsZip( + stamps: DetachedTimestamp[], + names: string[], +) { + const zip = new JSZip() + for (let i = 0; i < stamps.length; i++) { + const fileName = names[i] ?? `file-${i}` + const encoded = Encoder.encodeDetachedTimestamp(stamps[i]!) + // Preserve directory structure in zip (fileName may contain path separators) + zip.file(`${fileName}.ots`, encoded) + } + const blob = await zip.generateAsync({ type: 'blob' }) + triggerDownload(blob, 'timestamps.zip') +} + +export function useTimestampSDK() { + const sdk = getSDK() + + const stampPhase = ref('idle') + const stampError = ref(null) + const stampResult = shallowRef(null) + const stampFileNames = ref([]) + const broadcastProgress = ref('') + const upgradeResults = shallowRef(null) + + const verifyStatus = ref(null) + const verifyAttestations = shallowRef([]) + const isVerifying = ref(false) + const verifyError = ref(null) + + let upgradeTimer: ReturnType | null = null + + async function stamp( + digests: DigestHeader[], + fileNames?: string[], + ): Promise { + stampPhase.value = 'generating-nonce' + stampError.value = null + stampResult.value = null + stampFileNames.value = fileNames ?? [] + broadcastProgress.value = '' + upgradeResults.value = null + + const onEvent: StampEventCallback = (event) => { + switch (event.phase) { + case 'generating-nonce': + stampPhase.value = 'generating-nonce' + break + case 'building-merkle-tree': + stampPhase.value = 'building-merkle-tree' + break + case 'broadcasting': + stampPhase.value = 'broadcasting' + broadcastProgress.value = `0/${event.totalCalendars}` + break + case 'calendar-response': + broadcastProgress.value = `${event.responsesReceived}/${event.totalCalendars}${event.success ? '' : ' (failed: ' + event.calendarUrl + ')'}` + break + case 'building-proof': + stampPhase.value = 'building-proof' + break + case 'complete': + stampPhase.value = 'complete' + break + } + } + + try { + const results = await sdk.stamp(digests, onEvent) + + stampPhase.value = 'complete' + stampResult.value = results + + // Start polling for upgrade (no auto-download — user uses the download button) + startUpgradePolling(results) + + return results + } catch (e) { + stampPhase.value = 'error' + stampError.value = e instanceof Error ? e.message : 'Stamping failed' + throw e + } + } + + async function downloadPendingStamp() { + if (!stampResult.value) return + const results = stampResult.value + const names = stampFileNames.value + + if (results.length === 1) { + downloadOtsFile(results[0]!, names[0]) + } else if (results.length > 1) { + await downloadStampsAsZip(results, names) + } + } + + function startUpgradePolling( + stamps: DetachedTimestamp[], + keepPending?: boolean, + ) { + stopUpgradePolling() + stampPhase.value = 'upgrading' + + let attempts = 0 + const maxAttempts = 40 // ~10 minutes at 15s intervals + + upgradeTimer = setInterval(async () => { + attempts++ + try { + const allResults: UpgradeResult[] = [] + for (const s of stamps) { + const results = await sdk.upgrade(s, keepPending ?? false) + allResults.push(...results) + } + upgradeResults.value = allResults + + const hasUpgraded = allResults.some( + (r) => r.status === UpgradeStatus.Upgraded, + ) + if (hasUpgraded) { + stampPhase.value = 'upgraded' + // Auto-download upgraded timestamps with correct per-file names + const names = stampFileNames.value + if (stamps.length === 1) { + downloadOtsFile(stamps[0]!, names[0]) + } else if (stamps.length > 1) { + await downloadStampsAsZip(stamps, names) + } + stopUpgradePolling() + } else if (attempts >= maxAttempts) { + stopUpgradePolling() + } + } catch { + // Silently retry on next interval + } + }, 15000) + } + + function stopUpgradePolling() { + if (upgradeTimer) { + clearInterval(upgradeTimer) + upgradeTimer = null + } + } + + async function verify( + stamp: DetachedTimestamp, + ): Promise<{ status: VerifyStatus; attestations: AttestationStatus[] }> { + isVerifying.value = true + verifyError.value = null + verifyStatus.value = null + verifyAttestations.value = [] + + try { + const attestations = await sdk.verify(stamp) + const status = sdk.transformResult(attestations) + + verifyStatus.value = status + verifyAttestations.value = attestations + return { status, attestations } + } catch (e) { + verifyError.value = e instanceof Error ? e.message : 'Verification failed' + throw e + } finally { + isVerifying.value = false + } + } + + async function upgrade( + detached: DetachedTimestamp, + keepPending?: boolean, + ): Promise { + return sdk.upgrade(detached, keepPending ?? false) + } + + function decodeOtsFile(data: Uint8Array): DetachedTimestamp { + const decoder = new Decoder(data) + return decoder.readDetachedTimestamp() + } + + function resetStamp() { + stampPhase.value = 'idle' + stampError.value = null + stampResult.value = null + stampFileNames.value = [] + broadcastProgress.value = '' + upgradeResults.value = null + stopUpgradePolling() + } + + function resetVerify() { + verifyStatus.value = null + verifyAttestations.value = [] + isVerifying.value = false + verifyError.value = null + } + + return { + stampPhase, + stampError, + stampResult, + stampFileNames, + broadcastProgress, + upgradeResults, + stamp, + downloadPendingStamp, + resetStamp, + + verifyStatus, + verifyAttestations, + isVerifying, + verifyError, + verify, + upgrade, + decodeOtsFile, + resetVerify, + } +} diff --git a/apps/web/src/composables/useWallet.ts b/apps/web/src/composables/useWallet.ts new file mode 100644 index 0000000..6c221c2 --- /dev/null +++ b/apps/web/src/composables/useWallet.ts @@ -0,0 +1,131 @@ +import { ref, computed } from 'vue' +import { BrowserProvider } from 'ethers' +import type { Eip1193Provider } from 'ethers' +import { WELL_KNOWN_CHAINS } from '@uts/sdk' + +const walletAddress = ref(null) +const walletChainId = ref(null) +const isConnecting = ref(false) +const walletError = ref(null) + +const isConnected = computed(() => walletAddress.value !== null) +const hasWallet = computed( + () => typeof window !== 'undefined' && !!(window as any).ethereum, +) + +const CHAIN_NAMES: Record = { + 1: 'Ethereum', + 17000: 'Holesky', + 11155111: 'Sepolia', + 534352: 'Scroll', + 534351: 'Scroll Sepolia', +} + +const walletChainName = computed(() => { + if (!walletChainId.value) return null + return CHAIN_NAMES[walletChainId.value] ?? `Chain ${walletChainId.value}` +}) + +function getEip1193Provider(): Eip1193Provider | null { + if (typeof window === 'undefined') return null + return (window as any).ethereum ?? null +} + +function truncateAddress(address: string): string { + return `${address.slice(0, 6)}...${address.slice(-4)}` +} + +export function useWallet() { + async function connect() { + const ethereum = getEip1193Provider() + if (!ethereum) { + walletError.value = + 'No wallet detected. Install MetaMask or another EIP-1193 wallet.' + return + } + + isConnecting.value = true + walletError.value = null + + try { + const provider = new BrowserProvider(ethereum) + const accounts = await provider.send('eth_requestAccounts', []) + if (accounts.length === 0) { + walletError.value = 'No accounts returned from wallet' + return + } + walletAddress.value = accounts[0] ?? null + + const network = await provider.getNetwork() + walletChainId.value = Number(network.chainId) + + // Listen for account/chain changes + ;(ethereum as any).on?.('accountsChanged', handleAccountsChanged) + ;(ethereum as any).on?.('chainChanged', handleChainChanged) + } catch (e) { + walletError.value = + e instanceof Error ? e.message : 'Failed to connect wallet' + } finally { + isConnecting.value = false + } + } + + function disconnect() { + const ethereum = getEip1193Provider() + if (ethereum) { + ;(ethereum as any).removeListener?.( + 'accountsChanged', + handleAccountsChanged, + ) + ;(ethereum as any).removeListener?.('chainChanged', handleChainChanged) + } + walletAddress.value = null + walletChainId.value = null + walletError.value = null + } + + async function switchChain(chainId: number) { + const ethereum = getEip1193Provider() + if (!ethereum) return + + const knownChain = WELL_KNOWN_CHAINS[chainId] + if (!knownChain) return + + try { + await ethereum.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: knownChain.chainId }], + }) + } catch { + // switch failed + } + } + + return { + walletAddress, + walletChainId, + walletChainName, + isConnected, + isConnecting, + hasWallet, + walletError, + connect, + disconnect, + switchChain, + getEip1193Provider, + truncateAddress, + } +} + +function handleAccountsChanged(accounts: string[]) { + if (accounts.length === 0) { + walletAddress.value = null + walletChainId.value = null + } else { + walletAddress.value = accounts[0] ?? null + } +} + +function handleChainChanged(chainIdHex: string) { + walletChainId.value = parseInt(chainIdHex, 16) +} diff --git a/apps/web/src/composables/useWebSocketFeed.ts b/apps/web/src/composables/useWebSocketFeed.ts new file mode 100644 index 0000000..555e8f3 --- /dev/null +++ b/apps/web/src/composables/useWebSocketFeed.ts @@ -0,0 +1,191 @@ +import { ref, onUnmounted } from 'vue' +import { BrowserProvider } from 'ethers' +import type { Eip1193Provider } from 'ethers' +import { SDK } from '@uts/sdk' +import { getSDK } from './useTimestampSDK' + +const CHAIN_NAMES: Record = { + 1: 'Ethereum', + 17000: 'Holesky', + 11155111: 'Sepolia', + 534352: 'Scroll', + 534351: 'Scroll Sepolia', +} + +export interface FeedEntry { + id: string + hash: string + type: 'ethereum' + chain: string + chainId: number + blockHeight: number + sender?: string + txHash?: string + timestamp: number +} + +export function useWebSocketFeed() { + const entries = ref([]) + const isConnected = ref(false) + const seenIds = new Set() + let intervalId: ReturnType | null = null + let lastBlockWeb3: Record = {} + let lastBlockRPC: Record = {} + + function addEntry(entry: FeedEntry) { + if (seenIds.has(entry.id)) return + seenIds.add(entry.id) + entries.value.unshift(entry) + if (entries.value.length > 50) { + const removed = entries.value.splice(50) + for (const r of removed) seenIds.delete(r.id) + } + } + + /** Poll web3Provider. Returns the wallet's chainId on success, or null. */ + async function fetchEventsFromWeb3( + web3Provider: Eip1193Provider, + ): Promise { + try { + const provider = new BrowserProvider(web3Provider) + const network = await provider.getNetwork() + const chainId = Number(network.chainId) + + const currentBlock = await provider.getBlockNumber() + const fromBlock = lastBlockWeb3[chainId] + ? lastBlockWeb3[chainId] + 1 + : currentBlock - 10 + + if (fromBlock > currentBlock) return chainId + + const logs = await provider.getLogs({ + fromBlock, + toBlock: currentBlock, + topics: [SDK.utsLogTopic], + }) + + for (const log of logs) { + const parsed = SDK.utsInterface.parseLog(log) + if (!parsed) continue + + addEntry({ + id: `${chainId}-${log.blockNumber}-${log.index}`, + hash: parsed.args[0], + type: 'ethereum', + chain: CHAIN_NAMES[chainId] ?? `Chain ${chainId}`, + chainId, + blockHeight: log.blockNumber, + sender: parsed.args[1], + txHash: log.transactionHash, + timestamp: Number(parsed.args[2] as bigint) * 1000, + }) + } + + lastBlockWeb3[chainId] = currentBlock + return chainId + } catch (e) { + console.warn('Feed: failed to poll web3Provider:', e) + return null + } + } + + /** Poll ethRPCs, skipping chains already covered by web3Provider. */ + async function fetchEventsFromRPCs( + sdk: SDK, + skipChainIds: Set = new Set(), + ) { + const chainIds = Object.keys(sdk.ethRPCs).map(Number) + + for (const chainId of chainIds) { + if (skipChainIds.has(chainId)) continue + + const provider = sdk.getEthProvider(chainId) + if (!provider) continue + + try { + const currentBlock = await provider.getBlockNumber() + const fromBlock = lastBlockRPC[chainId] + ? lastBlockRPC[chainId] + 1 + : currentBlock - 5 + + if (fromBlock > currentBlock) continue + + const logs = await provider.getLogs({ + fromBlock, + toBlock: currentBlock, + topics: [SDK.utsLogTopic], + }) + + for (const log of logs) { + const parsed = SDK.utsInterface.parseLog(log) + if (!parsed) continue + + addEntry({ + id: `${chainId}-${log.blockNumber}-${log.index}`, + hash: parsed.args[0], + type: 'ethereum', + chain: CHAIN_NAMES[chainId] ?? `Chain ${chainId}`, + chainId, + blockHeight: log.blockNumber, + sender: parsed.args[1], + txHash: log.transactionHash, + timestamp: Number(parsed.args[2] as bigint) * 1000, + }) + } + + lastBlockRPC[chainId] = currentBlock + } catch (e) { + console.warn(`Feed: failed to poll chain ${chainId}:`, e) + } + } + } + + /** Poll both web3Provider and ethRPCs, deduplicating by entry id. */ + async function pollAll() { + const sdk = getSDK() + const skipChainIds = new Set() + + // Poll web3Provider first (if available) + if (sdk.web3Provider) { + const web3ChainId = await fetchEventsFromWeb3(sdk.web3Provider) + if (web3ChainId !== null) { + // Skip this chain in ethRPCs since web3Provider already covers it + skipChainIds.add(web3ChainId) + } + } + + // Also poll ethRPCs for all remaining chains + await fetchEventsFromRPCs(sdk, skipChainIds) + } + + async function connect() { + isConnected.value = true + lastBlockWeb3 = {} + lastBlockRPC = {} + + await pollAll() + + intervalId = setInterval(() => { + pollAll() + }, 15000) + } + + function disconnect() { + isConnected.value = false + if (intervalId) { + clearInterval(intervalId) + intervalId = null + } + } + + onUnmounted(() => { + disconnect() + }) + + return { + entries, + isConnected, + connect, + disconnect, + } +} diff --git a/apps/web/src/env.d.ts b/apps/web/src/env.d.ts new file mode 100644 index 0000000..8e14b07 --- /dev/null +++ b/apps/web/src/env.d.ts @@ -0,0 +1,4 @@ +declare module '*.po' { + import type { Messages } from '@lingui/core' + export const messages: Messages +} diff --git a/apps/web/src/i18n.ts b/apps/web/src/i18n.ts new file mode 100644 index 0000000..b87aeb7 --- /dev/null +++ b/apps/web/src/i18n.ts @@ -0,0 +1,20 @@ +import { i18n } from '@lingui/core' +import { messages as enMessages } from './locales/en.po' + +i18n.load('en', enMessages) +i18n.activate('en') + +export async function initLocale() { + const saved = localStorage.getItem('uts-locale') + if (saved && saved !== 'en') { + try { + const catalog = await import(`./locales/${saved}.po`) + i18n.load(saved, catalog.messages) + i18n.activate(saved) + } catch { + // fallback to English + } + } +} + +export { i18n } diff --git a/apps/web/src/locales/en.po b/apps/web/src/locales/en.po new file mode 100644 index 0000000..bf89b41 --- /dev/null +++ b/apps/web/src/locales/en.po @@ -0,0 +1,358 @@ +msgid "" +msgstr "" +"POT-Creation-Date: 2025-01-01 00:00+0000\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" + +#. App title +msgid "UTS" +msgstr "" + +msgid "Universal Timestamps" +msgstr "" + +#. Header +msgid "{online}/{total} chains" +msgstr "" + +msgid "Ethereum Chains" +msgstr "" + +msgid "Refresh" +msgstr "" + +msgid "Chain ID: {chainId}" +msgstr "" + +msgid "Edit RPC endpoint" +msgstr "" + +msgid "Remove chain" +msgstr "" + +msgid "Save" +msgstr "" + +msgid "Add chain by ID" +msgstr "" + +msgid "Add" +msgstr "" + +msgid "Reset defaults" +msgstr "" + +msgid "Connecting..." +msgstr "" + +msgid "No Wallet" +msgstr "" + +msgid "Connect Wallet" +msgstr "" + +msgid "Calendar settings" +msgstr "" + +#. Settings panel +msgid "Calendar Nodes" +msgstr "" + +msgid "Reset to defaults" +msgstr "" + +msgid "Keep pending attestations after upgrade" +msgstr "" + +msgid "When enabled, the original pending attestation is preserved alongside the upgraded one" +msgstr "" + +msgid "Internal hash algorithm" +msgstr "" + +msgid "Used for Merkle tree construction" +msgstr "" + +#. Main content +msgid "Decentralized" +msgstr "" + +msgid "Timestamping" +msgstr "" + +msgid "Cryptographic proof of existence anchored to Ethereum" +msgstr "" + +msgid "Stamp" +msgstr "" + +msgid "Verify" +msgstr "" + +msgid "Upgrade" +msgstr "" + +msgid "UTS Protocol — Powered by Universal Timestamps" +msgstr "" + +#. HeroTerminal +msgid "Drop files here" +msgstr "" + +msgid "or" +msgstr "" + +msgid "Choose File" +msgstr "" + +msgid "Choose Directory" +msgstr "" + +msgid "{count, plural, one {# file} other {# files}} selected" +msgstr "" + +msgid "... and {count} more" +msgstr "" + +msgid "Or paste a hash directly:" +msgstr "" + +#. StampingWorkflow +msgid "Stamping Pipeline" +msgstr "" + +msgid "Generating Nonce" +msgstr "" + +msgid "Creating random nonce for privacy" +msgstr "" + +msgid "Building Merkle Tree" +msgstr "" + +msgid "Constructing proof tree from leaves" +msgstr "" + +msgid "Broadcasting" +msgstr "" + +msgid "Submitting to calendar nodes" +msgstr "" + +msgid "Building Proof" +msgstr "" + +msgid "Constructing Merkle proof paths" +msgstr "" + +msgid "Complete" +msgstr "" + +msgid "Timestamp recorded (pending attestation)" +msgstr "" + +msgid "Polling for Upgrade" +msgstr "" + +msgid "Waiting for on-chain attestation..." +msgstr "" + +msgid "Upgraded" +msgstr "" + +msgid "Attestation confirmed on-chain" +msgstr "" + +msgid "Calendar responses: {progress}" +msgstr "" + +msgid "Download pending .ots" +msgstr "" + +#. VerificationResult +msgid "Verification Dashboard" +msgstr "" + +msgid "Upload a .ots file to verify a timestamp proof" +msgstr "" + +msgid "Upload .ots" +msgstr "" + +msgid "Verifying proof chain..." +msgstr "" + +msgid "Original Digest ({algo})" +msgstr "" + +msgid "Verify Original File (Optional)" +msgstr "" + +msgid "Upload the original file to verify it matches the .ots header digest" +msgstr "" + +msgid "Digesting..." +msgstr "" + +msgid "Upload Original" +msgstr "" + +msgid "File digest matches — this is the original file" +msgstr "" + +msgid "File digest does NOT match the .ots header" +msgstr "" + +msgid "Attestations ({count})" +msgstr "" + +msgid "Show Proof Path" +msgstr "" + +msgid "Hide Proof Path" +msgstr "" + +msgid "Reset" +msgstr "" + +#. UpgradePanel +msgid "Manual Upgrade" +msgstr "" + +msgid "Upload a pending .ots file to upgrade it with on-chain attestations" +msgstr "" + +msgid "Digest ({algo})" +msgstr "" + +msgid "Current Attestations ({count})" +msgstr "" + +msgid "Upgrade Results" +msgstr "" + +msgid "Upgrading..." +msgstr "" + +msgid "Upgrade Now" +msgstr "" + +msgid "Download Upgraded .ots" +msgstr "" + +#. LiveFeed +msgid "Live Feed" +msgstr "" + +msgid "POLLING" +msgstr "" + +msgid "DISCONNECTED" +msgstr "" + +msgid "Root" +msgstr "" + +msgid "Chain" +msgstr "" + +msgid "Block" +msgstr "" + +msgid "Sender" +msgstr "" + +msgid "Tx Hash" +msgstr "" + +msgid "Testnet attestation — not suitable for production use" +msgstr "" + +msgid "Unknown network — cannot verify on-chain" +msgstr "" + +msgid "Polling for Attested events..." +msgstr "" + +msgid "Waiting for connection..." +msgstr "" + +msgid "Testnet" +msgstr "" + +msgid "Unknown network" +msgstr "" + +msgid "Testnet attestation" +msgstr "" + +#. StatusBadge +msgid "VERIFIED ON CHAIN" +msgstr "" + +msgid "INVALID" +msgstr "" + +msgid "PENDING" +msgstr "" + +msgid "PARTIALLY VERIFIED" +msgstr "" + +msgid "UNKNOWN" +msgstr "" + +#. AttestationDetail +msgid "Type" +msgstr "" + +msgid "Bitcoin" +msgstr "" + +msgid "Ethereum UTS" +msgstr "" + +msgid "Pending" +msgstr "" + +msgid "Block Height" +msgstr "" + +msgid "Merkle Root" +msgstr "" + +msgid "Timestamp" +msgstr "" + +msgid "Contract" +msgstr "" + +msgid "Calendar" +msgstr "" + +msgid "Unknown attestation" +msgstr "" + +msgid "Bitcoin block #{height}" +msgstr "" + +msgid "{chain} block #{height}" +msgstr "" + +msgid "Pending → {url}" +msgstr "" + +#. MerkleTreeViz +msgid "FORK ({count} branches)" +msgstr "" + +msgid "Bitcoin @ block {height}" +msgstr "" + +msgid "{chain} @ block {height}" +msgstr "" + +msgid "branch[{index}]" +msgstr "" diff --git a/apps/web/src/locales/zh.po b/apps/web/src/locales/zh.po new file mode 100644 index 0000000..4e3262d --- /dev/null +++ b/apps/web/src/locales/zh.po @@ -0,0 +1,358 @@ +msgid "" +msgstr "" +"POT-Creation-Date: 2025-01-01 00:00+0000\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh\n" + +#. App title +msgid "UTS" +msgstr "UTS" + +msgid "Universal Timestamps" +msgstr "通用时间戳" + +#. Header +msgid "{online}/{total} chains" +msgstr "{online}/{total} 条链" + +msgid "Ethereum Chains" +msgstr "以太坊链" + +msgid "Refresh" +msgstr "刷新" + +msgid "Chain ID: {chainId}" +msgstr "链 ID:{chainId}" + +msgid "Edit RPC endpoint" +msgstr "编辑 RPC 端点" + +msgid "Remove chain" +msgstr "移除链" + +msgid "Save" +msgstr "保存" + +msgid "Add chain by ID" +msgstr "通过 ID 添加链" + +msgid "Add" +msgstr "添加" + +msgid "Reset defaults" +msgstr "恢复默认" + +msgid "Connecting..." +msgstr "连接中..." + +msgid "No Wallet" +msgstr "无钱包" + +msgid "Connect Wallet" +msgstr "连接钱包" + +msgid "Calendar settings" +msgstr "日历设置" + +#. Settings panel +msgid "Calendar Nodes" +msgstr "日历节点" + +msgid "Reset to defaults" +msgstr "恢复默认设置" + +msgid "Keep pending attestations after upgrade" +msgstr "升级后保留待处理证明" + +msgid "When enabled, the original pending attestation is preserved alongside the upgraded one" +msgstr "启用后,原始待处理证明将与升级后的证明一起保留" + +msgid "Internal hash algorithm" +msgstr "内部哈希算法" + +msgid "Used for Merkle tree construction" +msgstr "用于 Merkle 树构建" + +#. Main content +msgid "Decentralized" +msgstr "去中心化" + +msgid "Timestamping" +msgstr "时间戳" + +msgid "Cryptographic proof of existence anchored to Ethereum" +msgstr "锚定到以太坊的加密存在证明" + +msgid "Stamp" +msgstr "盖章" + +msgid "Verify" +msgstr "验证" + +msgid "Upgrade" +msgstr "升级" + +msgid "UTS Protocol — Powered by Universal Timestamps" +msgstr "UTS 协议 ── 由通用时间戳驱动" + +#. HeroTerminal +msgid "Drop files here" +msgstr "拖放文件到此处" + +msgid "or" +msgstr "或" + +msgid "Choose File" +msgstr "选择文件" + +msgid "Choose Directory" +msgstr "选择目录" + +msgid "{count, plural, one {# file} other {# files}} selected" +msgstr "已选择 {count} 个文件" + +msgid "... and {count} more" +msgstr "... 以及其他 {count} 个" + +msgid "Or paste a hash directly:" +msgstr "或直接粘贴哈希值:" + +#. StampingWorkflow +msgid "Stamping Pipeline" +msgstr "盖章流水线" + +msgid "Generating Nonce" +msgstr "生成随机数" + +msgid "Creating random nonce for privacy" +msgstr "为隐私创建随机数" + +msgid "Building Merkle Tree" +msgstr "构建 Merkle 树" + +msgid "Constructing proof tree from leaves" +msgstr "从叶节点构建证明树" + +msgid "Broadcasting" +msgstr "广播中" + +msgid "Submitting to calendar nodes" +msgstr "提交到日历节点" + +msgid "Building Proof" +msgstr "构建证明" + +msgid "Constructing Merkle proof paths" +msgstr "构建 Merkle 证明路径" + +msgid "Complete" +msgstr "完成" + +msgid "Timestamp recorded (pending attestation)" +msgstr "时间戳已记录(等待证明)" + +msgid "Polling for Upgrade" +msgstr "轮询升级" + +msgid "Waiting for on-chain attestation..." +msgstr "等待链上证明..." + +msgid "Upgraded" +msgstr "已升级" + +msgid "Attestation confirmed on-chain" +msgstr "链上证明已确认" + +msgid "Calendar responses: {progress}" +msgstr "日历响应:{progress}" + +msgid "Download pending .ots" +msgstr "下载待处理 .ots" + +#. VerificationResult +msgid "Verification Dashboard" +msgstr "验证仪表板" + +msgid "Upload a .ots file to verify a timestamp proof" +msgstr "上传 .ots 文件以验证时间戳证明" + +msgid "Upload .ots" +msgstr "上传 .ots" + +msgid "Verifying proof chain..." +msgstr "验证证明链..." + +msgid "Original Digest ({algo})" +msgstr "原始摘要({algo})" + +msgid "Verify Original File (Optional)" +msgstr "验证原始文件(可选)" + +msgid "Upload the original file to verify it matches the .ots header digest" +msgstr "上传原始文件以验证其与 .ots 头部摘要是否匹配" + +msgid "Digesting..." +msgstr "计算摘要..." + +msgid "Upload Original" +msgstr "上传原始文件" + +msgid "File digest matches — this is the original file" +msgstr "文件摘要匹配 ── 这是原始文件" + +msgid "File digest does NOT match the .ots header" +msgstr "文件摘要与 .ots 头部不匹配" + +msgid "Attestations ({count})" +msgstr "证明({count})" + +msgid "Show Proof Path" +msgstr "显示证明路径" + +msgid "Hide Proof Path" +msgstr "隐藏证明路径" + +msgid "Reset" +msgstr "重置" + +#. UpgradePanel +msgid "Manual Upgrade" +msgstr "手动升级" + +msgid "Upload a pending .ots file to upgrade it with on-chain attestations" +msgstr "上传待处理 .ots 文件以通过链上证明进行升级" + +msgid "Digest ({algo})" +msgstr "摘要({algo})" + +msgid "Current Attestations ({count})" +msgstr "当前证明({count})" + +msgid "Upgrade Results" +msgstr "升级结果" + +msgid "Upgrading..." +msgstr "升级中..." + +msgid "Upgrade Now" +msgstr "立即升级" + +msgid "Download Upgraded .ots" +msgstr "下载已升级 .ots" + +#. LiveFeed +msgid "Live Feed" +msgstr "实时动态" + +msgid "POLLING" +msgstr "轮询中" + +msgid "DISCONNECTED" +msgstr "已断开" + +msgid "Root" +msgstr "根" + +msgid "Chain" +msgstr "链" + +msgid "Block" +msgstr "区块" + +msgid "Sender" +msgstr "发送者" + +msgid "Tx Hash" +msgstr "交易哈希" + +msgid "Testnet attestation — not suitable for production use" +msgstr "测试网证明 ── 不适用于生产环境" + +msgid "Unknown network — cannot verify on-chain" +msgstr "未知网络 ── 无法链上验证" + +msgid "Polling for Attested events..." +msgstr "轮询已证明事件..." + +msgid "Waiting for connection..." +msgstr "等待连接..." + +msgid "Testnet" +msgstr "测试网" + +msgid "Unknown network" +msgstr "未知网络" + +msgid "Testnet attestation" +msgstr "测试网证明" + +#. StatusBadge +msgid "VERIFIED ON CHAIN" +msgstr "链上已验证" + +msgid "INVALID" +msgstr "无效" + +msgid "PENDING" +msgstr "待处理" + +msgid "PARTIALLY VERIFIED" +msgstr "部分验证" + +msgid "UNKNOWN" +msgstr "未知" + +#. AttestationDetail +msgid "Type" +msgstr "类型" + +msgid "Bitcoin" +msgstr "比特币" + +msgid "Ethereum UTS" +msgstr "以太坊 UTS" + +msgid "Pending" +msgstr "待处理" + +msgid "Block Height" +msgstr "区块高度" + +msgid "Merkle Root" +msgstr "Merkle 根" + +msgid "Timestamp" +msgstr "时间戳" + +msgid "Contract" +msgstr "合约" + +msgid "Calendar" +msgstr "日历" + +msgid "Unknown attestation" +msgstr "未知证明" + +msgid "Bitcoin block #{height}" +msgstr "比特币区块 #{height}" + +msgid "{chain} block #{height}" +msgstr "{chain} 区块 #{height}" + +msgid "Pending → {url}" +msgstr "待处理 → {url}" + +#. MerkleTreeViz +msgid "FORK ({count} branches)" +msgstr "分叉({count} 个分支)" + +msgid "Bitcoin @ block {height}" +msgstr "比特币 @ 区块 {height}" + +msgid "{chain} @ block {height}" +msgstr "{chain} @ 区块 {height}" + +msgid "branch[{index}]" +msgstr "分支[{index}]" diff --git a/apps/web/src/main.ts b/apps/web/src/main.ts new file mode 100644 index 0000000..fddc919 --- /dev/null +++ b/apps/web/src/main.ts @@ -0,0 +1,14 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import './style.css' +import { initLocale } from './i18n' +import App from './App.vue' + +async function bootstrap() { + await initLocale() + const app = createApp(App) + app.use(createPinia()) + app.mount('#app') +} + +bootstrap() diff --git a/apps/web/src/stores/app.ts b/apps/web/src/stores/app.ts new file mode 100644 index 0000000..d876af3 --- /dev/null +++ b/apps/web/src/stores/app.ts @@ -0,0 +1,268 @@ +import { defineStore } from 'pinia' +import { ref, computed, watch } from 'vue' +import { DEFAULT_CALENDARS } from '@uts/sdk' +import type { DetachedTimestamp, SecureDigestOp } from '@uts/sdk' +import { getSDK, resetSDK } from '@/composables/useTimestampSDK' +import { JsonRpcProvider } from 'ethers' + +const CHAIN_NAMES: Record = { + 1: 'Ethereum', + 17000: 'Holesky', + 11155111: 'Sepolia', + 534352: 'Scroll', + 534351: 'Scroll Sepolia', +} + +export interface EthChainNode { + chainId: number + name: string + status: 'online' | 'offline' | 'checking' + latency?: number + rpcUrl?: string +} + +const STORAGE_KEY = 'uts-calendars' +const SETTINGS_KEY = 'uts-settings' +const CHAINS_KEY = 'uts-custom-chains' +const CHAIN_RPCS_KEY = 'uts-chain-rpcs' + +// Default chain IDs from SDK ethRPCs +function getDefaultChainIds(): number[] { + return Object.keys(getSDK().ethRPCs).map(Number) +} + +function loadCalendars(): string[] { + try { + const stored = localStorage.getItem(STORAGE_KEY) + if (stored) return JSON.parse(stored) + } catch { + /* ignore */ + } + return DEFAULT_CALENDARS.map((u) => u.toString()) +} + +function saveCalendars(urls: string[]) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(urls)) +} + +function loadSettings(): { + keepPending: boolean + internalHashAlgo: SecureDigestOp +} { + try { + const stored = localStorage.getItem(SETTINGS_KEY) + if (stored) { + const parsed = JSON.parse(stored) + return { + keepPending: parsed.keepPending ?? false, + internalHashAlgo: parsed.internalHashAlgo ?? 'KECCAK256', + } + } + } catch { + /* ignore */ + } + return { keepPending: false, internalHashAlgo: 'KECCAK256' } +} + +function saveSettings(settings: { + keepPending: boolean + internalHashAlgo: SecureDigestOp +}) { + localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)) +} + +function loadCustomChains(): number[] | null { + try { + const stored = localStorage.getItem(CHAINS_KEY) + if (stored) return JSON.parse(stored) + } catch { + /* ignore */ + } + return null +} + +function saveCustomChains(chainIds: number[]) { + localStorage.setItem(CHAINS_KEY, JSON.stringify(chainIds)) +} + +function loadChainRpcs(): Record { + try { + const stored = localStorage.getItem(CHAIN_RPCS_KEY) + if (stored) return JSON.parse(stored) + } catch { + /* ignore */ + } + return {} +} + +function saveChainRpcs(rpcs: Record) { + localStorage.setItem(CHAIN_RPCS_KEY, JSON.stringify(rpcs)) +} + +// Public RPC endpoints for common chains (used when user adds a chain not in SDK ethRPCs) +const PUBLIC_RPCS: Record = { + 1: 'https://eth.llamarpc.com', + 17000: 'https://rpc.holesky.ethpandaops.io', + 11155111: 'https://rpc.sepolia.org', + 534352: 'https://rpc.scroll.io', + 534351: 'https://sepolia-rpc.scroll.io', + 42161: 'https://arb1.arbitrum.io/rpc', + 10: 'https://mainnet.optimism.io', + 8453: 'https://mainnet.base.org', + 137: 'https://polygon-rpc.com', +} + +export const useAppStore = defineStore('app', () => { + const calendarUrls = ref(loadCalendars()) + const keepPending = ref(loadSettings().keepPending) + const internalHashAlgo = ref(loadSettings().internalHashAlgo) + const ethChains = ref([]) + const recentStamps = ref([]) + + // Chain IDs to monitor (persisted or default from SDK) + const customChainIds = ref( + loadCustomChains() ?? getDefaultChainIds(), + ) + + // Custom RPC endpoints per chain (persisted) + const customRpcs = ref>(loadChainRpcs()) + + const onlineCount = computed( + () => ethChains.value.filter((c) => c.status === 'online').length, + ) + + watch( + calendarUrls, + (urls) => { + saveCalendars(urls) + resetSDK({ calendars: urls.map((u) => new URL(u)) }) + }, + { deep: true }, + ) + + watch(keepPending, (val) => { + saveSettings({ keepPending: val, internalHashAlgo: internalHashAlgo.value }) + }) + + watch(internalHashAlgo, (val) => { + saveSettings({ keepPending: keepPending.value, internalHashAlgo: val }) + }) + + async function checkChains() { + const sdk = getSDK() + const chainIds = customChainIds.value + + ethChains.value = chainIds.map((chainId) => { + const rpc = customRpcs.value[chainId] ?? PUBLIC_RPCS[chainId] + return { + chainId, + name: CHAIN_NAMES[chainId] ?? `Chain ${chainId}`, + status: 'checking' as const, + rpcUrl: rpc, + } + }) + + for (const chain of ethChains.value) { + // Try custom RPC first, then SDK provider, then public RPC fallback + let provider: JsonRpcProvider | null = null + const rpc = customRpcs.value[chain.chainId] + if (rpc) { + try { + provider = new JsonRpcProvider(rpc) + } catch { + /* ignore */ + } + } + if (!provider) { + provider = sdk.getEthProvider(chain.chainId) + } + if (!provider && PUBLIC_RPCS[chain.chainId]) { + try { + provider = new JsonRpcProvider(PUBLIC_RPCS[chain.chainId]) + } catch { + /* ignore */ + } + } + + if (!provider) { + chain.status = 'offline' + continue + } + const start = performance.now() + try { + await provider.getBlockNumber() + chain.latency = Math.round(performance.now() - start) + chain.status = 'online' + } catch { + chain.status = 'offline' + chain.latency = undefined + } + } + } + + function addChain(chainId: number) { + if (customChainIds.value.includes(chainId)) return + customChainIds.value.push(chainId) + saveCustomChains(customChainIds.value) + } + + function removeChain(chainId: number) { + customChainIds.value = customChainIds.value.filter((id) => id !== chainId) + ethChains.value = ethChains.value.filter((c) => c.chainId !== chainId) + saveCustomChains(customChainIds.value) + } + + function resetChains() { + customChainIds.value = getDefaultChainIds() + customRpcs.value = {} + localStorage.removeItem(CHAINS_KEY) + localStorage.removeItem(CHAIN_RPCS_KEY) + } + + function setChainRpc(chainId: number, rpcUrl: string) { + const trimmed = rpcUrl.trim() + if (trimmed) { + customRpcs.value[chainId] = trimmed + } else { + delete customRpcs.value[chainId] + } + saveChainRpcs(customRpcs.value) + // Update the rpcUrl in the displayed chain list + const chain = ethChains.value.find((c) => c.chainId === chainId) + if (chain) { + chain.rpcUrl = trimmed || PUBLIC_RPCS[chainId] + } + } + + function addStamp(stamp: DetachedTimestamp) { + recentStamps.value.unshift(stamp) + if (recentStamps.value.length > 20) { + recentStamps.value.pop() + } + } + + function setCalendars(urls: string[]) { + calendarUrls.value = urls + } + + function resetCalendars() { + calendarUrls.value = DEFAULT_CALENDARS.map((u) => u.toString()) + } + + return { + calendarUrls, + keepPending, + internalHashAlgo, + ethChains, + recentStamps, + onlineCount, + checkChains, + addChain, + removeChain, + resetChains, + setChainRpc, + addStamp, + setCalendars, + resetCalendars, + } +}) diff --git a/apps/web/src/style.css b/apps/web/src/style.css new file mode 100644 index 0000000..8442c96 --- /dev/null +++ b/apps/web/src/style.css @@ -0,0 +1,168 @@ +@import 'tailwindcss'; + +@theme { + --color-deep-black: #050505; + --color-midnight: #0a0f14; + --color-neon-cyan: #00f3ff; + --color-neon-purple: #bc13fe; + --color-neon-orange: #ff9e00; + --color-glass: rgba(255, 255, 255, 0.04); + --color-glass-border: rgba(255, 255, 255, 0.08); + --color-surface: #0d1117; + --color-surface-light: #161b22; + --color-valid: #00ff88; + --color-invalid: #ff3366; + --color-pending: #ff9e00; + + --font-mono: 'JetBrains Mono', ui-monospace, monospace; + --font-heading: 'Space Grotesk', system-ui, sans-serif; + + --animate-glow-pulse: glow-pulse 2s ease-in-out infinite; + --animate-scan: scan 4s linear infinite; + --animate-fade-in: fade-in 0.5s ease-out; + --animate-slide-up: slide-up 0.4s ease-out; + --animate-typewriter: typewriter 0.05s steps(1) infinite; + + @keyframes glow-pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } + } + + @keyframes scan { + 0% { + transform: translateY(-100%); + } + 100% { + transform: translateY(100vh); + } + } + + @keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + @keyframes slide-up { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } + } +} + +/* Base styles */ +body { + margin: 0; + min-height: 100vh; + background: var(--color-deep-black); + overflow-x: hidden; +} + +#app { + width: 100%; + min-height: 100vh; +} + +/* Scanline overlay */ +.scanlines::after { + content: ''; + position: fixed; + inset: 0; + background: repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(0, 243, 255, 0.015) 2px, + rgba(0, 243, 255, 0.015) 4px + ); + pointer-events: none; + z-index: 9999; +} + +/* Glass card */ +.glass { + background: var(--color-glass); + border: 1px solid var(--color-glass-border); + backdrop-filter: blur(12px); +} + +/* Glow effects */ +.glow-cyan { + box-shadow: + 0 0 10px rgba(0, 243, 255, 0.15), + 0 0 40px rgba(0, 243, 255, 0.05); +} + +.glow-purple { + box-shadow: + 0 0 10px rgba(188, 19, 254, 0.15), + 0 0 40px rgba(188, 19, 254, 0.05); +} + +.glow-text-cyan { + text-shadow: 0 0 10px rgba(0, 243, 255, 0.5); +} + +.glow-text-valid { + text-shadow: 0 0 12px rgba(0, 255, 136, 0.6); +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(0, 243, 255, 0.2); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(0, 243, 255, 0.4); +} + +/* Transition classes */ +.fade-enter-active, +.fade-leave-active { + transition: + opacity 0.3s ease, + transform 0.3s ease; +} + +.fade-enter-from, +.fade-leave-to { + opacity: 0; + transform: translateY(8px); +} + +.list-enter-active, +.list-leave-active { + transition: all 0.4s ease; +} + +.list-enter-from { + opacity: 0; + transform: translateX(-20px); +} + +.list-leave-to { + opacity: 0; + transform: translateX(20px); +} diff --git a/apps/web/src/views/HomeView.vue b/apps/web/src/views/HomeView.vue new file mode 100644 index 0000000..296fda0 --- /dev/null +++ b/apps/web/src/views/HomeView.vue @@ -0,0 +1,611 @@ + + + diff --git a/apps/web/tsconfig.app.json b/apps/web/tsconfig.app.json new file mode 100644 index 0000000..5839f41 --- /dev/null +++ b/apps/web/tsconfig.app.json @@ -0,0 +1,20 @@ +{ + "extends": ["@vue/tsconfig/tsconfig.dom.json", "../../tsconfig.json"], + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "types": ["vite/client"], + "paths": { + "@/*": ["./src/*"] + }, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "../../packages/sdk/tsconfig.json" }] +} diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..0ab1bf5 --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ], + "compilerOptions": { + "composite": true + } +} diff --git a/apps/web/tsconfig.node.json b/apps/web/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/apps/web/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts new file mode 100644 index 0000000..510a53a --- /dev/null +++ b/apps/web/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import tailwindcss from '@tailwindcss/vite' +import { lingui } from '@lingui/vite-plugin' +import { fileURLToPath, URL } from 'node:url' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [vue(), tailwindcss(), lingui()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + conditions: ['uts-source', 'module', 'browser', 'development|production'], + }, +}) diff --git a/contract-tests/UniversalTimestamps.t.sol b/contract-tests/UniversalTimestamps.t.sol new file mode 100644 index 0000000..4c70b50 --- /dev/null +++ b/contract-tests/UniversalTimestamps.t.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Test, console} from "forge-std/Test.sol"; +import {UniversalTimestamps} from "../contracts/UniversalTimestamps.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +contract UniversalTimestampsV2 is UniversalTimestamps { + function version() public pure returns (string memory) { + return "V2"; + } +} + +contract UniversalTimestampsTest is Test { + UniversalTimestamps public proxy; + address owner = address(1); + + function setUp() public { + UniversalTimestamps implementation = new UniversalTimestamps(); + + bytes memory initData = abi.encodeWithSelector(UniversalTimestamps.initialize.selector, owner); + ERC1967Proxy proxyAddress = new ERC1967Proxy(address(implementation), initData); + + proxy = UniversalTimestamps(address(proxyAddress)); + } + + function test_StoragePersistenceAfterUpgrade() public { + bytes32 root = keccak256("test_data"); + + proxy.attest(root); + uint256 timeV1 = proxy.timestamp(root); + assertGt(timeV1, 0); + + vm.startPrank(owner); + + UniversalTimestampsV2 v2Impl = new UniversalTimestampsV2(); + + proxy.upgradeToAndCall(address(v2Impl), ""); + + vm.stopPrank(); + + UniversalTimestampsV2 proxyV2 = UniversalTimestampsV2(address(proxy)); + + assertEq(proxyV2.version(), "V2"); + + assertEq(proxyV2.timestamp(root), timeV1); + console.log("Storage persisted across upgrade at namespaced slot."); + } +} diff --git a/contracts/IUniversalTimestamps.sol b/contracts/IUniversalTimestamps.sol new file mode 100644 index 0000000..b5dfb0b --- /dev/null +++ b/contracts/IUniversalTimestamps.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +interface IUniversalTimestamps { + event Attested(bytes32 indexed root, address indexed sender, uint256 timestamp); + + function attest(bytes32 root) external; + + function timestamp(bytes32 root) external view returns (uint256); +} diff --git a/contracts/UniversalTimestamps.sol b/contracts/UniversalTimestamps.sol new file mode 100644 index 0000000..21969d1 --- /dev/null +++ b/contracts/UniversalTimestamps.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {IUniversalTimestamps} from "./IUniversalTimestamps.sol"; +import {SlotDerivation} from "@openzeppelin/contracts/utils/SlotDerivation.sol"; + +/** + * @title UniversalTimestamps + * @dev Records and exposes timestamps for attested Merkle roots using ERC-7201 + * namespaced storage (`uts.storage.UniversalTimestamps`) derived via + * {SlotDerivation}, and is implemented as a UUPS upgradeable contract via + * OpenZeppelin's Initializable, OwnableUpgradeable, and UUPSUpgradeable + * base contracts. Storage is kept in a dedicated namespaced struct to remain + * layout-compatible across upgrades, while upgrades are authorized by the + * contract owner through {_authorizeUpgrade}. + */ +contract UniversalTimestamps is Initializable, OwnableUpgradeable, UUPSUpgradeable, IUniversalTimestamps { + using SlotDerivation for string; + + string private constant _NAMESPACE = "uts.storage.UniversalTimestamps"; + + /// @custom:storage-location erc7201:uts.storage.UniversalTimestamps + struct UniversalTimestampsStorage { + mapping(bytes32 => uint256) timestamps; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(address initialOwner) public initializer { + __Ownable_init(initialOwner); + } + + function _getUniversalTimestampsStorage() private pure returns (UniversalTimestampsStorage storage $) { + bytes32 slot = _NAMESPACE.erc7201Slot(); + assembly ("memory-safe") { + $.slot := slot + } + } + + function timestamp(bytes32 root) external view returns (uint256) { + return _getUniversalTimestampsStorage().timestamps[root]; + } + + /** + * @notice Attest Merkle Root + * @param root The Merkle Root to be attested + */ + function attest(bytes32 root) external { + require(root != bytes32(0), "UTS: Root cannot be zero"); + + UniversalTimestampsStorage storage $ = _getUniversalTimestampsStorage(); + if ($.timestamps[root] == 0) { + $.timestamps[root] = block.timestamp; + emit Attested(root, msg.sender, block.timestamp); + } + } + + /** + * @dev Authorizes an upgrade to `newImplementation`. + * + * This function is restricted to the contract owner via the {onlyOwner} modifier, + * ensuring that only the owner can authorize upgrades to the implementation. + */ + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} +} diff --git a/crates/bmt/Cargo.toml b/crates/bmt/Cargo.toml index 6c6861f..565c54c 100644 --- a/crates/bmt/Cargo.toml +++ b/crates/bmt/Cargo.toml @@ -10,7 +10,9 @@ repository.workspace = true version.workspace = true [dependencies] +bytemuck = { workspace = true } digest.workspace = true +hybrid-array = { workspace = true, features = ["bytemuck"] } [dev-dependencies] commonware-cryptography = "0.0.63" # for benchmarks diff --git a/crates/bmt/benches/tree_construction.rs b/crates/bmt/benches/tree_construction.rs index caf9a11..dadc595 100644 --- a/crates/bmt/benches/tree_construction.rs +++ b/crates/bmt/benches/tree_construction.rs @@ -1,4 +1,5 @@ //! Benchmark for Merkle tree construction. +use bytemuck::Pod; use criterion::{ BatchSize, BenchmarkGroup, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main, measurement::WallTime, @@ -7,7 +8,7 @@ use digest::{Digest, FixedOutputReset, Output}; use sha2::Sha256; use sha3::Keccak256; use std::hint::black_box; -use uts_bmt::FlatMerkleTree; +use uts_bmt::UnorderedMerkleTree; const INPUT_SIZES: &[usize] = &[8, 1024, 65536, 1_048_576]; @@ -25,7 +26,7 @@ fn benchmark(c: &mut Criterion) { fn bench_digest(group: &mut BenchmarkGroup<'_, WallTime>, id: &str) where D: Digest + FixedOutputReset, - Output: Copy, + Output: Pod + Copy, { for &size in INPUT_SIZES { let leaves = generate_leaves::(size); @@ -33,7 +34,7 @@ where group.bench_function(BenchmarkId::new(id, size), move |b| { // Tree construction is the operation under test. b.iter(|| { - let tree = FlatMerkleTree::::new(black_box(leaves.as_slice())); + let tree = UnorderedMerkleTree::::new(black_box(leaves.as_slice())); black_box(tree); }); }); diff --git a/crates/bmt/src/lib.rs b/crates/bmt/src/lib.rs index eb70e96..e9e385c 100644 --- a/crates/bmt/src/lib.rs +++ b/crates/bmt/src/lib.rs @@ -1,36 +1,46 @@ -#![feature(maybe_uninit_slice)] #![feature(maybe_uninit_fill)] #![feature(likely_unlikely)] //! High performance binary Merkle tree implementation in Rust. +use bytemuck::Pod; use digest::{Digest, FixedOutputReset, Output}; use std::hint::unlikely; +/// Prefix byte to distinguish internal nodes from leaves when hashing. +pub const INNER_NODE_PREFIX: u8 = 0x01; + /// Flat, Fixed-Size, Read only Merkle Tree /// /// Expects the length of leaves to be equal or near(less) to a power of two. /// /// Leaves are **sorted** starting at index `len`. #[derive(Debug, Clone, Default)] -pub struct FlatMerkleTree { +pub struct UnorderedMerkleTree { /// Index 0 is not used, leaves start at index `len`. nodes: Box<[Output]>, len: usize, } -impl FlatMerkleTree +/// Merkle Tree without hashing the leaves +#[derive(Debug, Clone)] +pub struct UnhashedFlatMerkleTree { + buffer: Vec>, + len: usize, +} + +impl UnorderedMerkleTree where - Output: Copy, + Output: Pod + Copy, { /// Constructs a new Merkle tree from the given hash leaves. pub fn new(data: &[Output]) -> Self { + Self::new_unhashed(data).finalize() + } + + /// Constructs a new Merkle tree from the given hash leaves, without hashing internal nodes. + pub fn new_unhashed(data: &[Output]) -> UnhashedFlatMerkleTree { let raw_len = data.len(); - if unlikely(raw_len == 0) { - return Self { - nodes: Box::new([Output::::default(); 2]), - len: 1, - }; - } + assert_ne!(raw_len, 0, "Cannot create Merkle tree with zero leaves"); let len = raw_len.next_power_of_two(); let mut nodes = Vec::>::with_capacity(2 * len); @@ -62,29 +72,9 @@ where .get_unchecked_mut(len..) .assume_init_mut() .sort_unstable(); - - // Build the tree - let mut hasher = D::new(); - for i in (1..len).rev() { - // SAFETY: in bounds due to loop range and initialization above - let left = maybe_uninit.get_unchecked(2 * i).assume_init_ref(); - let right = maybe_uninit.get_unchecked(2 * i + 1).assume_init_ref(); - - Digest::update(&mut hasher, left); - Digest::update(&mut hasher, right); - let parent_hash = hasher.finalize_reset(); - - maybe_uninit.get_unchecked_mut(i).write(parent_hash); - } - - // SAFETY: initialized all elements. - nodes.set_len(2 * len); } - Self { - nodes: nodes.into_boxed_slice(), - len, - } + UnhashedFlatMerkleTree { buffer: nodes, len } } /// Returns the root hash of the Merkle tree @@ -114,6 +104,59 @@ where current: self.len + leaf_index_in_slice, }) } + + /// Returns the raw bytes of the Merkle tree nodes + #[inline] + pub fn as_raw_bytes(&self) -> &[u8] { + bytemuck::cast_slice(&self.nodes) + } + + /// From raw bytes, reconstruct the Merkle tree + #[inline] + pub unsafe fn from_raw_bytes(bytes: &[u8]) -> Self { + let nodes: &[Output] = bytemuck::cast_slice(bytes); + let len = nodes.len() / 2; + Self { + nodes: nodes.to_vec().into_boxed_slice(), + len, + } + } +} + +impl UnhashedFlatMerkleTree +where + Output: Pod + Copy, +{ + /// Finalizes the Merkle tree by hashing internal nodes + pub fn finalize(self) -> UnorderedMerkleTree { + let mut nodes = self.buffer; + let len = self.len; + unsafe { + let maybe_uninit = nodes.spare_capacity_mut(); + + // Build the tree + let mut hasher = D::new(); + for i in (1..len).rev() { + // SAFETY: in bounds due to loop range and initialization above + let left = maybe_uninit.get_unchecked(2 * i).assume_init_ref(); + let right = maybe_uninit.get_unchecked(2 * i + 1).assume_init_ref(); + + Digest::update(&mut hasher, [INNER_NODE_PREFIX]); + Digest::update(&mut hasher, left); + Digest::update(&mut hasher, right); + let parent_hash = hasher.finalize_reset(); + + maybe_uninit.get_unchecked_mut(i).write(parent_hash); + } + + // SAFETY: initialized all elements. + nodes.set_len(2 * len); + } + UnorderedMerkleTree { + nodes: nodes.into_boxed_slice(), + len, + } + } } /// Iterator over the sibling nodes of a leaf in the Merkle tree @@ -181,7 +224,7 @@ mod tests { fn test_merkle_tree() where - Output: Copy, + Output: Pod + Copy, { let mut leaves = vec![ D::digest(b"leaf1"), @@ -191,18 +234,21 @@ mod tests { ]; leaves.sort_unstable(); - let tree = FlatMerkleTree::::new(&leaves); + let tree = UnorderedMerkleTree::::new(&leaves); // Manually compute the expected root let mut hasher = D::new(); + Digest::update(&mut hasher, [INNER_NODE_PREFIX]); Digest::update(&mut hasher, &leaves[0]); Digest::update(&mut hasher, &leaves[1]); let left_hash = hasher.finalize_reset(); + Digest::update(&mut hasher, [INNER_NODE_PREFIX]); Digest::update(&mut hasher, &leaves[2]); Digest::update(&mut hasher, &leaves[3]); let right_hash = hasher.finalize_reset(); + Digest::update(&mut hasher, [INNER_NODE_PREFIX]); Digest::update(&mut hasher, &left_hash); Digest::update(&mut hasher, &right_hash); let expected_root = hasher.finalize(); @@ -212,7 +258,7 @@ mod tests { fn test_proof() where - Output: Copy, + Output: Pod + Copy, { let mut leaves = vec![ D::digest(b"apple"), @@ -222,7 +268,7 @@ mod tests { ]; leaves.sort_unstable(); - let tree = FlatMerkleTree::::new(&leaves); + let tree = UnorderedMerkleTree::::new(&leaves); for leaf in &leaves { let mut iter = tree @@ -234,10 +280,12 @@ mod tests { while let Some((side, sibling_hash)) = iter.next() { match side { NodePosition::Left => { + Digest::update(&mut hasher, [INNER_NODE_PREFIX]); Digest::update(&mut hasher, ¤t_hash); Digest::update(&mut hasher, sibling_hash); } NodePosition::Right => { + Digest::update(&mut hasher, [INNER_NODE_PREFIX]); Digest::update(&mut hasher, sibling_hash); Digest::update(&mut hasher, ¤t_hash); } diff --git a/crates/calendar/Cargo.toml b/crates/calendar/Cargo.toml new file mode 100644 index 0000000..9e74106 --- /dev/null +++ b/crates/calendar/Cargo.toml @@ -0,0 +1,52 @@ +[package] +authors.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +name = "uts-calendar" +repository.workspace = true +version.workspace = true + +[dependencies] +alloy-primitives = { workspace = true } +alloy-provider = { workspace = true } +alloy-signer = { workspace = true } +alloy-signer-local = { workspace = true, features = ["mnemonic"] } +axum = { workspace = true, default-features = false, features = [ + "macros", + "http2", + "tokio", +] } # http2 only +bump-scope.workspace = true +bytemuck = { workspace = true } +bytes = { workspace = true } +digest = { workspace = true } +eyre = { workspace = true } +hashbrown = { version = "0.15.5", features = ["nightly"] } # why? +itoa = { workspace = true } +rocksdb = { workspace = true } +sha3 = { workspace = true } +tokio = { workspace = true, features = ["full"] } +tower-http = { workspace = true, features = ["cors"] } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +uts-bmt = { workspace = true } +uts-contracts = { workspace = true } +uts-core = { workspace = true, features = ["bytes"] } +uts-journal = { workspace = true } +uts-stamper = { workspace = true } + +[dev-dependencies] +criterion.workspace = true + +[[bench]] +harness = false +name = "submit_digest" + +[lints] +workspace = true + +[features] +dev = ["axum/http1"] # for easier testing +performance = ["tracing/release_max_level_info"] diff --git a/crates/calendar/benches/submit_digest.rs b/crates/calendar/benches/submit_digest.rs new file mode 100644 index 0000000..760be3e --- /dev/null +++ b/crates/calendar/benches/submit_digest.rs @@ -0,0 +1,38 @@ +//! Benchmark for submitting digests of varying sizes. + +use alloy_primitives::b256; +use alloy_signer_local::LocalSigner; +use bytes::Bytes; +use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; +use std::hint::black_box; +use uts_calendar::routes::ots::submit_digest_inner; + +const DIGEST_SIZES: &[usize] = &[32, 64]; + +fn benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("submit_digest"); + + for &size in DIGEST_SIZES { + group.throughput(Throughput::Elements(1)); + group.bench_function(BenchmarkId::from_parameter(size), move |b| { + let signer = LocalSigner::from_bytes(&b256!( + "9ba9926331eb5f4995f1e358f57ba1faab8b005b51928d2fdaea16e69a6ad225" + )) + .unwrap(); + let input = Bytes::from(vec![0u8; size]); + + b.iter_batched( + || input.clone(), + |input| { + let out = submit_digest_inner(input, &signer); + black_box(out) + }, + BatchSize::SmallInput, + ); + }); + } + group.finish(); +} + +criterion_group!(benches, benchmark); +criterion_main!(benches); diff --git a/crates/calendar/src/lib.rs b/crates/calendar/src/lib.rs new file mode 100644 index 0000000..fd04d11 --- /dev/null +++ b/crates/calendar/src/lib.rs @@ -0,0 +1,58 @@ +#![feature(thread_sleep_until)] +#![feature(allocator_api)] + +//! Calendar server library. + +#[macro_use] +extern crate tracing; + +use alloy_signer::k256::ecdsa::SigningKey; +use alloy_signer_local::LocalSigner; +use digest::{OutputSizeUser, typenum::Unsigned}; +use rocksdb::DB; +use sha3::Keccak256; +use std::sync::Arc; +use uts_journal::Journal; + +/// Calendar server routes and handlers. +pub mod routes; +/// Time-related utilities and background tasks. +pub mod time; + +/// Application state shared across handlers. +#[derive(Debug)] +pub struct AppState { + /// Local signer for signing OTS timestamps. + pub signer: LocalSigner, + /// Journal + pub journal: Journal<{ ::OutputSize::USIZE }>, + /// RocksDB + pub db: Arc, +} + +/// Signal for graceful shutdown. +pub async fn shutdown_signal() { + use tokio::signal; + + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } +} diff --git a/crates/calendar/src/main.rs b/crates/calendar/src/main.rs new file mode 100644 index 0000000..6f8fe78 --- /dev/null +++ b/crates/calendar/src/main.rs @@ -0,0 +1,108 @@ +//! Calendar server + +use alloy_primitives::b256; +use alloy_provider::{Provider, ProviderBuilder, network::EthereumWallet}; +use alloy_signer_local::{LocalSigner, MnemonicBuilder}; +use axum::{ + Router, + extract::DefaultBodyLimit, + http::Method, + routing::{get, post}, +}; +use digest::{OutputSizeUser, typenum::Unsigned}; +use rocksdb::DB; +use sha3::Keccak256; +use std::{env, path::PathBuf, sync::Arc}; +use tower_http::{cors, cors::CorsLayer}; +use tracing::info; +use uts_calendar::{AppState, routes, shutdown_signal, time}; +use uts_contracts::uts::UniversalTimestamps; +use uts_journal::{Journal, JournalConfig, checkpoint::CheckpointConfig}; +use uts_stamper::{Stamper, StamperConfig}; + +const RING_BUFFER_CAPACITY: usize = 1 << 20; // 1 million entries + +#[tokio::main] +async fn main() -> eyre::Result<()> { + tracing_subscriber::fmt::init(); + + tokio::spawn(time::async_updater()); + + let signer = LocalSigner::from_bytes(&b256!( + "9ba9926331eb5f4995f1e358f57ba1faab8b005b51928d2fdaea16e69a6ad225" + ))?; + + // journal + let journal = Journal::with_capacity_and_config( + RING_BUFFER_CAPACITY, + JournalConfig { + consumer_checkpoint: CheckpointConfig { + path: PathBuf::from("./.journal/.checkpoint"), + ..Default::default() + }, + wal_dir: PathBuf::from("./.journal"), + }, + )?; + + let key = MnemonicBuilder::from_phrase(env::var("MNEMONIC")?.as_str()) + .index(0u32)? + .build()?; + info!("Using address: {:?}", key.address()); + let provider = ProviderBuilder::new() + .wallet(EthereumWallet::new(key)) + .connect("https://sepolia-rpc.scroll.io") + .await?; + provider.get_chain_id().await?; // sanity check + + let contract = UniversalTimestamps::new(uts_contracts::uts::DEFAULT_ADDRESS, provider.clone()); + + // stamper + let reader = journal.reader(); + let db = Arc::new(DB::open_default("./.db/tries")?); + let mut stamper = + Stamper::::OutputSize::USIZE }>::new( + reader, + db.clone(), + contract, + // TODO: tune configuration + StamperConfig { + max_interval_seconds: 10, + max_entries_per_timestamp: 1 << 10, // 1024 entries + min_leaves: 1 << 4, + max_cache_size: 256, + }, + ); + // TODO: graceful shutdown + tokio::spawn(async move { + stamper.run().await; + }); + + let app = Router::new() + .route( + "/digest", + post(routes::ots::submit_digest) + .layer(DefaultBodyLimit::max(routes::ots::MAX_DIGEST_SIZE)), + ) + .route("/timestamp/{commitment}", get(routes::ots::get_timestamp)) + .with_state(Arc::new(AppState { + signer, + journal: journal.clone(), + db, + })) + .layer( + CorsLayer::new() + .allow_methods([Method::GET, Method::POST]) + .allow_origin(cors::Any), + ); + + let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; + + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) + .await?; + + // this will join the journal's background task and ensure flush of all pending commits + journal.shutdown()?; + + Ok(()) +} diff --git a/crates/calendar/src/routes.rs b/crates/calendar/src/routes.rs new file mode 100644 index 0000000..ca72c74 --- /dev/null +++ b/crates/calendar/src/routes.rs @@ -0,0 +1,2 @@ +/// ots related routes +pub mod ots; diff --git a/crates/calendar/src/routes/ots.rs b/crates/calendar/src/routes/ots.rs new file mode 100644 index 0000000..4a2ae98 --- /dev/null +++ b/crates/calendar/src/routes/ots.rs @@ -0,0 +1,190 @@ +use crate::{AppState, time::current_time_sec}; +use alloy_primitives::B256; +use alloy_signer::SignerSync; +use axum::{ + body::Bytes, + extract::{Path, State}, + http::StatusCode, + response::{IntoResponse, Response}, +}; +use bump_scope::Bump; +use bytes::BytesMut; +use digest::Digest; +use sha3::Keccak256; +use std::{cell::RefCell, sync::Arc}; +use uts_bmt::UnorderedMerkleTree; +use uts_core::{ + codec::{ + Encode, + v1::{EthereumUTSAttestation, PendingAttestation, Timestamp}, + }, + utils::Hexed, +}; +use uts_stamper::DbExt; + +/// Maximum digest size accepted by the endpoint. +pub const MAX_DIGEST_SIZE: usize = 64; // e.g., SHA3-512 + +// Test this with official ots client: +// ots stamp -c "http://localhost:3000/" -m 1 +// cargo run --bin uts-info -- .ots +// Sample: +// ``` +// OTS Detached Timestamp found: +// Version 1 Proof digest of SHA256 877c470874fa92e5609a1396b1188ffa3e539d83ec2748a7cb6fb2d4430d45a2 +// execute APPEND ec04517482d3be52b6123ca37f683285 +// result 877c470874fa92e5609a1396b1188ffa3e539d83ec2748a7cb6fb2d4430d45a2ec04517482d3be52b6123ca37f683285 +// execute SHA256 +// result 2edc60a195a879bd446c5473921c46db14c4b1974516682ecae2b406121a5732 +// execute PREPEND 5137456900000000 +// result 51374569000000002edc60a195a879bd446c5473921c46db14c4b1974516682ecae2b406121a5732 +// execute APPEND 9f947a5cf576ba4f68593ac5e350204cc8b38bf0fd5f6f2d4436820d3164dfeaf7405188dfc4bad66e8f42e6fd0a6ffdcceebda548d01224113baab1a568a2b8 +// result 51374569000000002edc60a195a879bd446c5473921c46db14c4b1974516682ecae2b406121a57329f947a5cf576ba4f68593ac5e350204cc8b38bf0fd5f6f2d4436820d3164dfeaf7405188dfc4bad66e8f42e6fd0a6ffdcceebda548d01224113baab1a568a2b8 +// execute KECCAK256 +// result c15b4e8b93e9aaee5b8c736f5b73e5f313062e389925a0b1fc6495053f99d352 +// result attested by Pending: update URI https://localhost:3000 +// ``` +/// Submit digest to calendar server and get pending timestamp in response. +pub async fn submit_digest(State(state): State>, digest: Bytes) -> Response { + let (output, commitment) = submit_digest_inner(digest, &state.signer); + match state.journal.try_commit(&commitment) { + Err(_) => { + return (StatusCode::SERVICE_UNAVAILABLE, r#"{"err":"server busy"}"#).into_response(); + } // journal is full + Ok(fut) => fut.await, + } + output.into_response() +} + +// TODO: We need to benchmark this. +/// inner function to submit digest, returns (encoded timestamp, commitment) +pub fn submit_digest_inner(digest: Bytes, signer: impl SignerSync) -> (Bytes, [u8; 32]) { + const PRE_ALLOCATION_SIZE_HINT: usize = 4096; + thread_local! { + // We don't have `.await` in this function, so it's safe to borrow thread local. + static BUMP: RefCell = RefCell::new(Bump::with_size(PRE_ALLOCATION_SIZE_HINT)); + static HASHER: RefCell = RefCell::new(Keccak256::new()); + } + + // ots uses 32-bit unix time, but we use u64 here for future proofing, as it's not part of the ots spec. + let recv_timestamp = current_time_sec().to_le_bytes(); + + let undeniable_sig = { + // sign_message_sync invokes heap allocation, so manually hash it. + const EIP191_PREFIX: &str = "\x19Ethereum Signed Message:\n"; + let hash = HASHER.with(|hasher| { + let mut hasher = hasher.borrow_mut(); + hasher.update(EIP191_PREFIX.as_bytes()); + match digest.len() { + // 32 + 8 + 32 => hasher.update(b"40"), + // 64 + 8 + 64 => hasher.update(b"72"), + _ => { + let length = digest.len() + size_of::(); + let mut buffer = itoa::Buffer::new(); + let printed = buffer.format(length); + hasher.update(printed.as_bytes()); + } + } + hasher.update(recv_timestamp); + hasher.update(&digest); + hasher.finalize_reset() + }); + + let undeniable_sig = signer.sign_hash_sync(&hash.0.into()).unwrap(); + undeniable_sig.as_erc2098() + }; + + #[cfg(any(debug_assertions, not(feature = "performance")))] + trace!( + recv_timestamp = ?Hexed(&recv_timestamp), + digest = ?Hexed(&digest), + undeniable_sig = ?Hexed(&undeniable_sig), + ); + + BUMP.with(|bump| { + let mut bump = bump.borrow_mut(); + bump.reset(); + + let mut builder = Timestamp::builder_in(&*bump); + builder + .prepend(recv_timestamp.to_vec_in(&bump)) + .append(undeniable_sig.to_vec_in(&bump)) + .keccak256(); + + let mut commitment = [0u8; 32]; + commitment.copy_from_slice(&builder.commitment(&digest)); + + let timestamp = builder + .attest(PendingAttestation { + uri: "http://localhost:3000".into(), + }) + .unwrap(); + + // copy data out of bump + // TODO: eliminate this allocation by reusing from a pool + // TODO: wrap the buffer with a drop trait to return to pool + let mut buf = BytesMut::with_capacity(128); + timestamp.encode(&mut buf).unwrap(); + + #[cfg(any(debug_assertions, not(feature = "performance")))] + trace!(encoded_length = buf.len(), timestamp = ?timestamp); + + (buf.freeze(), commitment) + }) +} + +/// Get current timestamp from calendar server. +pub async fn get_timestamp( + State(state): State>, + Path(commitment): Path, +) -> Response { + const PRE_ALLOCATION_SIZE_HINT: usize = 4096; + thread_local! { + // We don't have `.await` in this function, so it's safe to borrow thread local. + static BUMP: RefCell = RefCell::new(Bump::with_size(PRE_ALLOCATION_SIZE_HINT)); + } + + let Some(root) = state.db.get_root_for_leaf(commitment).expect("DB error") else { + return (StatusCode::NOT_FOUND, r#"{"err":"timestamp not found"}"#).into_response(); + }; + let entry = state + .db + .load_entry(root) + .expect("DB error") + .expect("bug: entry not found"); + let trie: UnorderedMerkleTree = entry.trie(); + + let proof = trie + .get_proof_iter(bytemuck::cast_ref(&*commitment)) + .expect("bug: proof not found"); + let output = BUMP.with(|bump| { + let mut bump = bump.borrow_mut(); + bump.reset(); + + let mut builder = Timestamp::builder_in(&*bump); + builder.merkle_proof(proof); + + let timestamp = builder + .attest(EthereumUTSAttestation::new( + entry.chain_id, + entry.height, + Default::default(), + )) + .unwrap(); + + // copy data out of bump + // TODO: eliminate this allocation by reusing from a pool + // TODO: wrap the buffer with a drop trait to return to pool + let mut buf = BytesMut::with_capacity(128); + timestamp.encode(&mut buf).unwrap(); + + #[cfg(any(debug_assertions, not(feature = "performance")))] + trace!(encoded_length = buf.len(), timestamp = ?timestamp); + + buf.freeze() + }); + + output.into_response() +} diff --git a/crates/calendar/src/time.rs b/crates/calendar/src/time.rs new file mode 100644 index 0000000..86c4b52 --- /dev/null +++ b/crates/calendar/src/time.rs @@ -0,0 +1,55 @@ +//! A module that maintains a globally accessible current time in seconds since the Unix epoch. +//! +//! This is for performance optimization to avoid frequent syscalls for time retrieval. +use std::{ + sync::atomic::{AtomicU64, Ordering}, + time::{Duration, Instant, SystemTime, UNIX_EPOCH}, +}; +use tokio::time::MissedTickBehavior; + +static CURRENT_TIME_SEC: AtomicU64 = AtomicU64::new(0); +const UPDATE_PERIOD: Duration = Duration::from_secs(1); + +/// Returns the current time in seconds since the Unix epoch. +#[inline] +pub fn current_time_sec() -> u64 { + CURRENT_TIME_SEC.load(Ordering::Relaxed) +} + +/// An asynchronous task that updates the current time every second. +pub async fn async_updater() { + let mut interval = tokio::time::interval(UPDATE_PERIOD); + interval.set_missed_tick_behavior(MissedTickBehavior::Skip); + loop { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + CURRENT_TIME_SEC.store(now, Ordering::Relaxed); + interval.tick().await; + } +} + +/// A task that updates the current time every second. +pub fn updater() { + let mut next_tick = Instant::now(); + + loop { + next_tick += UPDATE_PERIOD; + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + CURRENT_TIME_SEC.store(now, Ordering::Relaxed); + + // Note: This behavior is different from the async version, which skips missed ticks. + let now_instant = Instant::now(); + if next_tick > now_instant { + std::thread::sleep_until(next_tick); + } else { + // If we've fallen behind, resynchronize to avoid accumulating drift. + next_tick = now_instant; + } + } +} diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml new file mode 100644 index 0000000..4710edf --- /dev/null +++ b/crates/cli/Cargo.toml @@ -0,0 +1,46 @@ +[package] +authors.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +name = "uts-cli" +repository.workspace = true +version.workspace = true + +[lints] +workspace = true + +[[bin]] +name = "uts" +path = "src/main.rs" + +[dependencies] +alloy-provider = { workspace = true } +bytemuck = { workspace = true } +clap = { workspace = true, features = ["derive"] } +color-eyre = { workspace = true } +digest = { workspace = true } +eyre = { workspace = true } +futures = { workspace = true } +jiff = { workspace = true } +rand = { workspace = true } +reqwest = { workspace = true, default-features = false, features = ["http2"] } +ripemd = { workspace = true } +sha1 = { workspace = true } +sha2 = { workspace = true } +sha3 = { workspace = true } +tokio = { workspace = true, features = ["full"] } +tracing = { workspace = true } +url = { workspace = true } +uts-bmt = { workspace = true } +uts-core = { workspace = true, features = ["std", "ethereum-uts-verifier", "io-utils"] } + +[features] +default = ["reqwest-default-tls"] +reqwest-default-tls = ["reqwest/default-tls"] +reqwest-native-tls = ["reqwest/native-tls"] +reqwest-native-tls-no-alpn = ["reqwest/native-tls-no-alpn"] +reqwest-native-tls-vendored = ["reqwest/native-tls-vendored"] +reqwest-native-tls-vendored-no-alpn = ["reqwest/native-tls-vendored-no-alpn"] +reqwest-rustls = ["reqwest/rustls"] diff --git a/crates/cli/README.md b/crates/cli/README.md new file mode 100644 index 0000000..a6097dd --- /dev/null +++ b/crates/cli/README.md @@ -0,0 +1,73 @@ +# UTS Cli Tool + +## Quick Start + +### 1. Create a Timestamp (`stamp`) + +Submit files to remote calendar servers for initial attestation: + +```bash +# Timestamp one or more files +uts stamp file1.txt file2.zip + +# Specify custom calendars and a quorum requirement +uts stamp -c https://calendar.example.com -m 1 document.pdf + +# Use a specific hashing algorithm +uts stamp --hasher sha256 photo.jpg + +``` + +This will create a corresponding `.ots` proof file (e.g., `document.pdf.ots`). + +### 2. Upgrade a Proof (`upgrade`) + +Initial proofs are often "pending." Once the calendar server commits the Merkle root to a blockchain, you must upgrade the proof to include the full path to the block: + +```bash +uts upgrade document.pdf.ots + +``` + +### 3. Verify a Proof (`verify`) + +Verify that a file matches its timestamp proof and check the attestation status on the calendar or blockchain: + +```bash +# Automatically finds the matching .ots file +uts verify document.pdf + +# Specify an Ethereum RPC provider for on-chain verification +uts verify document.pdf --eth-provider https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY + +``` + +### 4. Inspect a Proof (`inspect`) + +View the internal structure, opcodes, and attestation paths of an `.ots` file in a human-readable format: + +```bash +uts inspect document.pdf.ots + +``` + +## Command Reference + +### `stamp` + +| Argument | Description | Default | +| --- | --- | --- | +| `-c, --calendar` | Remote calendar URL (can be specified multiple times) | Built-in list | +| `-m` | Minimum quorum of calendars required | 1 | +| `-H, --hasher` | Hashing algorithm (`keccak256`, `sha256`, `sha1`, `ripemd160`) | `keccak256` | +| `--timeout` | Timeout in seconds for calendar responses | 5 | + +### `verify` + +| Argument | Description | +| --- | --- | +| `file` | The target file to verify | +| `stamp_file` | (Optional) Explicit path to the `.ots` file | +| `--eth-provider` | (Optional) Ethereum RPC URL for UTS contract verification | + + diff --git a/crates/cli/src/client.rs b/crates/cli/src/client.rs new file mode 100644 index 0000000..f0d0e53 --- /dev/null +++ b/crates/cli/src/client.rs @@ -0,0 +1,9 @@ +use reqwest::Client; +use std::sync::LazyLock; + +pub static CLIENT: LazyLock = LazyLock::new(|| { + Client::builder() + .user_agent(concat!("uts/", env!("CARGO_PKG_VERSION"))) + .build() + .expect("Failed to build HTTP client") +}); diff --git a/crates/cli/src/commands.rs b/crates/cli/src/commands.rs new file mode 100644 index 0000000..a4f8391 --- /dev/null +++ b/crates/cli/src/commands.rs @@ -0,0 +1,29 @@ +use clap::Subcommand; + +mod inspect; +mod stamp; +mod upgrade; +mod verify; + +#[derive(Debug, Subcommand)] +pub enum Commands { + /// Inspect an ots file + Inspect(inspect::Inspect), + /// Verify an ots file against a file + Verify(verify::Verify), + /// Create timestamp + Stamp(stamp::Stamp), + /// Upgrade timestamp + Upgrade(upgrade::Upgrade), +} + +impl Commands { + pub async fn run(self) -> eyre::Result<()> { + match self { + Commands::Inspect(cmd) => cmd.run(), + Commands::Verify(cmd) => cmd.run().await, + Commands::Stamp(cmd) => cmd.run().await, + Commands::Upgrade(cmd) => cmd.run().await, + } + } +} diff --git a/crates/cli/src/commands/inspect.rs b/crates/cli/src/commands/inspect.rs new file mode 100644 index 0000000..ff47f6a --- /dev/null +++ b/crates/cli/src/commands/inspect.rs @@ -0,0 +1,34 @@ +use clap::Args; +use std::{fs, io, io::Seek, path::PathBuf}; +use uts_core::codec::{ + Decode, Reader, VersionedProof, + v1::{DetachedTimestamp, Timestamp}, +}; + +#[derive(Debug, Args)] +pub struct Inspect { + /// Path to the OTS file to inspect + ots_file: PathBuf, +} + +impl Inspect { + pub fn run(self) -> eyre::Result<()> { + let mut fh = fs::File::open(&self.ots_file)?; + + match VersionedProof::::decode(&mut Reader(&mut fh)) { + Ok(ots) => { + eprintln!("OTS Detached Timestamp found:\n{ots}"); + return Ok(()); + } + Err(e) => { + eprintln!("Not a valid Detached Timestamp OTS file (trying raw timestamp): {e}\n"); + } + }; + + fh.seek(io::SeekFrom::Start(0))?; + + let raw = Timestamp::decode(&mut Reader(&mut fh))?; + eprintln!("Raw Timestamp found:\n{raw}"); + Ok(()) + } +} diff --git a/crates/cli/src/commands/stamp.rs b/crates/cli/src/commands/stamp.rs new file mode 100644 index 0000000..e747ced --- /dev/null +++ b/crates/cli/src/commands/stamp.rs @@ -0,0 +1,214 @@ +use crate::client::CLIENT; +use bytemuck::Pod; +use clap::{Args, ValueEnum}; +use digest::{Digest, FixedOutputReset, Output}; +use futures::TryFutureExt; +use std::{collections::HashMap, future::ready, io, path::PathBuf, sync::LazyLock, time::Duration}; +use tokio::{fs, io::AsyncWriteExt}; +use url::Url; +use uts_bmt::UnorderedMerkleTree; +use uts_core::{ + codec::{ + Decode, Encode, VersionedProof, + v1::{DetachedTimestamp, DigestHeader, Timestamp, TimestampBuilder, opcode::DigestOpExt}, + }, + utils::{HashAsyncFsExt, Hexed}, +}; + +static DEFAULT_CALENDARS: LazyLock> = LazyLock::new(|| { + vec![ + // Url::parse("https://a.pool.opentimestamps.org/").unwrap(), + // Url::parse("https://b.pool.opentimestamps.org/").unwrap(), + // Url::parse("https://a.pool.eternitywall.com/").unwrap(), + // Url::parse("https://ots.btc.catallaxy.com/").unwrap(), + Url::parse("http://127.0.0.1:3000/").unwrap(), + ] +}); + +#[derive(Debug, Args)] +pub struct Stamp { + /// Files to timestamp. May be specified multiple times. + #[arg(value_name = "FILE", num_args = 1..)] + files: Vec, + /// Create timestamp with the aid of a remote calendar. May be specified multiple times. + #[arg(short = 'c', long = "calendar", value_name = "URL", num_args = 0..)] + calendars: Vec, + /// Consider the timestamp complete if at least M calendars reply prior to the timeout + #[arg(short = 'm', default_value = "1")] + quorum: usize, + /// Hasher to use when digesting files. Default is Keccak256. + #[arg(short = 'H', long = "hasher", default_value = "keccak256")] + hasher: Hasher, + /// Timeout in seconds to wait for calendar responses. Default is 60 seconds. + #[arg(long = "timeout", default_value = "5")] + timeout: u64, +} + +#[derive(Default, Debug, Copy, Clone, ValueEnum)] +pub enum Hasher { + Sha1, + Ripemd160, + Sha256, + #[default] + Keccak256, +} + +impl Stamp { + pub async fn run(self) -> eyre::Result<()> { + match self.hasher { + Hasher::Sha1 => self.run_inner::().await, + Hasher::Ripemd160 => self.run_inner::().await, + Hasher::Sha256 => self.run_inner::().await, + Hasher::Keccak256 => self.run_inner::().await, + } + } + + async fn run_inner(self) -> eyre::Result<()> + where + D: Digest + FixedOutputReset + DigestOpExt + Send, + Output: Pod + Copy, + { + let digests = + futures::future::join_all(self.files.iter().map(|f| hash_file::(f.clone()))) + .await + .into_iter() + .collect::, _>>()?; + + for (header, path) in digests.iter().zip(self.files.iter()) { + eprintln!("File: {} {}", header, path.display()); + } + + let mut builders: HashMap = HashMap::from_iter( + self.files + .iter() + .map(|path| (path.clone(), Timestamp::builder())), + ); + + let nonced_digest = builders + .iter_mut() + .zip(digests.iter()) + .map(|((_, builder), digest)| { + let mut hasher = D::new(); + Digest::update(&mut hasher, digest.digest()); + let nonce: [u8; 32] = rand::random(); + Digest::update(&mut hasher, &nonce); + builder.append(nonce.to_vec()).digest::(); + hasher.finalize() + }) + .collect::>(); + + let internal_tire = UnorderedMerkleTree::::new(&nonced_digest); + let root = internal_tire.root(); + eprintln!("Internal Merkle root: {}", Hexed(root)); + + for ((_, builder), leaf) in builders.iter_mut().zip(nonced_digest) { + let proof = internal_tire.get_proof_iter(&leaf).expect("infallible"); + builder.merkle_proof(proof); + } + + let calendars = if self.calendars.is_empty() { + &*DEFAULT_CALENDARS + } else { + &*self.calendars + }; + + if self.quorum > calendars.len() { + eyre::bail!( + "Quorum of {} cannot be achieved with only {} calendars", + self.quorum, + self.calendars.len() + ); + } + + let stamps = futures::future::join_all( + calendars + .into_iter() + .map(|calendar| request_calendar(calendar.clone(), self.timeout, root)), + ) + .await + .into_iter() + .filter_map(|res| res.ok()) + .collect::>(); + if stamps.len() < self.quorum { + eyre::bail!( + "Only received {} valid responses from calendars, which does not meet the quorum of {}", + stamps.len(), + self.quorum + ); + } + let merged = if stamps.len() == 1 { + stamps.into_iter().next().unwrap() + } else { + Timestamp::merge(stamps) + }; + + let writes = + futures::future::join_all(builders.into_iter().zip(digests).map( + |((path, builder), header)| write_stamp(path, builder, merged.clone(), header), + )) + .await; + for (res, path) in writes.into_iter().zip(self.files.iter()) { + match res { + Ok(_) => eprintln!("Successfully wrote timestamp for {}", path.display()), + Err(e) => eprintln!("Failed to write timestamp for {}: {}", path.display(), e), + } + } + + Ok(()) + } +} + +async fn hash_file(path: PathBuf) -> io::Result { + let mut hasher = D::new(); + let file = fs::File::open(path).await?; + HashAsyncFsExt::update(&mut hasher, file).await?; + Ok(DigestHeader::new::(hasher.finalize())) +} + +async fn request_calendar(calendar: Url, timeout: u64, root: &[u8]) -> eyre::Result { + eprintln!("Submitting to remote calendar: {calendar}"); + let url = calendar.join(&"digest")?; + let response = CLIENT + .post(url) + .header("Accept", "application/vnd.opentimestamps.v1") + .body(root.to_vec()) + .timeout(Duration::from_secs(timeout)) + .send() + .and_then(|r| ready(r.error_for_status())) + .and_then(|r| r.bytes()) + .await + .inspect_err(|e| { + if e.is_status() { + eprintln!("Calendar {} responded with error: {}", calendar, e); + } else if e.is_timeout() { + eprintln!("Calendar {} timed out after {} seconds", calendar, timeout); + } else { + eprintln!("Failed to submit to calendar {}: {}", calendar, e); + } + })?; + + let ts = Timestamp::decode(&mut &*response).inspect_err(|e| { + eprintln!( + "Failed to decode response from calendar {}: {}", + calendar, e + ); + })?; + Ok(ts) +} + +async fn write_stamp( + mut path: PathBuf, + builder: TimestampBuilder, + merged: Timestamp, + header: DigestHeader, +) -> eyre::Result<()> { + let timestamp = builder.concat(merged.clone()); + let timestamp = DetachedTimestamp::from_parts(header, timestamp); + let timestamp = VersionedProof::::new(timestamp); + let mut buf = Vec::new(); + timestamp.encode(&mut buf)?; + path.add_extension("ots"); + let mut file = fs::File::create_new(path).await?; + file.write_all(&buf).await?; + Ok(()) +} diff --git a/crates/cli/src/commands/upgrade.rs b/crates/cli/src/commands/upgrade.rs new file mode 100644 index 0000000..cc939a4 --- /dev/null +++ b/crates/cli/src/commands/upgrade.rs @@ -0,0 +1,99 @@ +use crate::client::CLIENT; +use clap::Args; +use eyre::bail; +use futures::TryFutureExt; +use reqwest::StatusCode; +use std::{fs, future::ready, path::PathBuf, time::Duration}; +use url::Url; +use uts_core::{ + codec::{ + Decode, Encode, VersionedProof, + v1::{Attestation, DetachedTimestamp, PendingAttestation, Timestamp}, + }, + utils::Hexed, +}; + +#[derive(Debug, Args)] +pub struct Upgrade { + /// Files to timestamp. May be specified multiple times. + #[arg(value_name = "FILE", num_args = 1..)] + files: Vec, + /// Timeout in seconds to wait for calendar responses. Default is 5 seconds. + #[arg(long = "timeout", default_value = "5")] + timeout: u64, +} + +impl Upgrade { + pub async fn run(self) -> eyre::Result<()> { + // timestamp files are small, so we can read them all synchronously before upgrading. + let files = self + .files + .iter() + .map(|path| fs::read(path)) + .collect::, _>>()?; + + let results = futures::future::join_all( + self.files + .iter() + .cloned() + .zip(files) + .into_iter() + .map(|(path, file)| upgrade_one(path, file, self.timeout)), + ) + .await + .into_iter() + .collect::>(); + for (path, result) in self.files.iter().zip(results) { + match result { + Ok(_) => eprintln!("Upgraded: {}", path.display()), + Err(e) => eprintln!("Failed to upgrade {}: {e}", path.display()), + } + } + Ok(()) + } +} + +async fn upgrade_one(path: PathBuf, file: Vec, timeout: u64) -> eyre::Result<()> { + let mut proof = VersionedProof::::decode(&mut &*file)?; + + for step in proof.proof.pending_attestations_mut() { + let pending_uri = { + let Timestamp::Attestation(attestation) = step else { + unreachable!("bug: PendingAttestationIterMut should only yield Attestations"); + }; + let commitment = attestation.value().expect("finalized when decode"); + let pending_uri = PendingAttestation::from_raw(&*attestation)?.uri; + Url::parse(&pending_uri)?.join(&format!("timestamp/{}", Hexed(commitment)))? + }; + + let result = CLIENT + .get(pending_uri) + .header("Accept", "application/vnd.opentimestamps.v1") + .timeout(Duration::from_secs(timeout)) + .send() + .and_then(|r| ready(r.error_for_status())) + .and_then(|r| r.bytes()) + .await; + + match result { + Ok(response) => { + let attestation = Timestamp::decode(&mut &*response)?; + *step = Timestamp::merge(vec![attestation, step.clone()]) + } + Err(e) => { + if let Some(status) = e.status() + && status == StatusCode::NOT_FOUND + { + bail!("calendar not ready yet."); + } + return Err(e.into()); + } + } + } + + let mut buf = Vec::new(); + proof.encode(&mut buf)?; + tokio::fs::write(path, buf).await?; + + Ok(()) +} diff --git a/crates/cli/src/commands/verify.rs b/crates/cli/src/commands/verify.rs new file mode 100644 index 0000000..3f817a3 --- /dev/null +++ b/crates/cli/src/commands/verify.rs @@ -0,0 +1,146 @@ +use alloy_provider::ProviderBuilder; +use clap::Args; +use digest::{Digest, DynDigest}; +use eyre::bail; +use jiff::{Timestamp, tz::TimeZone}; +use std::{ + fs::File, + io::{BufReader, Read}, + path::PathBuf, + process, +}; +use uts_core::{ + codec::{ + Decode, Reader, VersionedProof, + v1::{ + Attestation, DetachedTimestamp, EthereumUTSAttestation, PendingAttestation, opcode::*, + }, + }, + utils::Hexed, + verifier::{AttestationVerifier, EthereumUTSVerifier}, +}; + +#[derive(Debug, Args)] +pub struct Verify { + file: PathBuf, + stamp_file: Option, + /// Optional Ethereum provider URL for verifying Ethereum UTS attestations. + /// If not provided, a default provider will be used based on the chain ID. + #[arg(long)] + eth_provider: Option, +} + +impl Verify { + pub async fn run(self) -> eyre::Result<()> { + let stamp_file = self.stamp_file.unwrap_or_else(|| { + let mut default = self.file.clone(); + default.add_extension("ots"); + default + }); + let timestamp = + VersionedProof::::decode(&mut Reader(File::open(stamp_file)?))? + .proof; + + let digest_header = timestamp.header(); + let mut hasher = match digest_header.kind().tag() { + SHA1 => Box::new(sha1::Sha1::new()) as Box, + RIPEMD160 => Box::new(ripemd::Ripemd160::new()) as Box, + SHA256 => Box::new(sha2::Sha256::new()) as Box, + KECCAK256 => Box::new(sha3::Keccak256::new()) as Box, + _ => bail!("Unsupported digest type: {}", digest_header.kind()), + }; + + let mut file = BufReader::new(File::open(self.file)?); + let mut buffer = [0u8; 64 * 1024]; // 64KB buffer + loop { + let bytes_read = file.read(&mut buffer)?; + if bytes_read == 0 { + break; + } + hasher.update(&buffer[..bytes_read]); + } + let expected = hasher.finalize(); + + if *expected != *digest_header.digest() { + eprintln!( + "Digest mismatch! Expected: {}, Found: {}", + Hexed(&expected), + Hexed(digest_header.digest()) + ); + process::exit(1); + } + eprintln!("Digest matches: {}", Hexed(&expected)); + + timestamp.try_finalize()?; + + for attestation in timestamp.attestations() { + if attestation.tag == PendingAttestation::TAG { + continue; // skip pending attestations + } + + if attestation.tag == EthereumUTSAttestation::TAG { + let eth_attestation = EthereumUTSAttestation::from_raw(&attestation)?; + eprintln!("Attested by {eth_attestation}"); + let provider_url = if let Some(url) = self.eth_provider.as_deref() { + url + } else { + match eth_attestation.chain.id() { + 1 => "https://0xrpc.io/eth", + 11155111 => "https://0xrpc.io/sep", + 534352 => "https://rpc.scroll.io", + 534351 => "https://sepolia-rpc.scroll.io", + _ => bail!("Unsupported chain: {}", eth_attestation.chain), + } + }; + let provider = ProviderBuilder::new().connect(provider_url).await?; + let verifier = EthereumUTSVerifier::new(provider).await?; + let result = verifier + .verify(ð_attestation, attestation.value().unwrap()) + .await?; + if let Some(block_number) = result.block_number { + if let Some(block_hash) = result.block_hash { + eprintln!("\tblock: #{block_number} {block_hash}"); + } else { + eprintln!("\tblock: {block_number}"); + } + } + if let Some(log_index) = result.log_index { + eprintln!("\tlog index: {log_index}"); + } + if let Some(transaction_hash) = result.transaction_hash { + if let Some((_, etherscan_url)) = eth_attestation.chain.etherscan_urls() { + eprintln!("\ttransaction: {etherscan_url}/tx/{transaction_hash}"); + } else { + eprintln!("\ttransaction hash: {transaction_hash}"); + } + } + + if let Some((_, etherscan_url)) = eth_attestation.chain.etherscan_urls() { + eprintln!( + "\tuts contract: {etherscan_url}/address/{}", + result.inner.address + ); + } else { + eprintln!("\tuts contract: {}", result.inner.address); + } + if let Some((_, etherscan_url)) = eth_attestation.chain.etherscan_urls() { + eprintln!( + "\ttx sender: {etherscan_url}/address/{}", + result.inner.sender + ); + } else { + eprintln!("\ttx sender: {}", result.inner.sender); + } + let ts = Timestamp::from_second(result.inner.timestamp.to())?; + let zdt = ts.to_zoned(TimeZone::system()); + eprintln!("\ttime attested: {zdt}"); + eprintln!("\tmerkle root: {}", result.inner.root); + continue; + } + + eprintln!("Unverifiable attestation: {attestation}"); + } + + Ok(()) + } +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs new file mode 100644 index 0000000..810c9a2 --- /dev/null +++ b/crates/cli/src/main.rs @@ -0,0 +1,19 @@ +//! UTS Cli +use crate::commands::Commands; +use clap::Parser; + +mod client; +mod commands; + +#[derive(Debug, Parser)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[tokio::main] +async fn main() -> eyre::Result<()> { + color_eyre::install()?; + + Cli::parse().command.run().await +} diff --git a/crates/contracts/Cargo.toml b/crates/contracts/Cargo.toml new file mode 100644 index 0000000..ac9d9ac --- /dev/null +++ b/crates/contracts/Cargo.toml @@ -0,0 +1,26 @@ +[package] +authors.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +name = "uts-contracts" +repository.workspace = true +version.workspace = true + +[dependencies] +alloy-contract = { workspace = true } +alloy-primitives = { workspace = true } +alloy-sol-types = { workspace = true, features = ["json"] } + +[dev-dependencies] +alloy = { workspace = true, features = ["full", "node-bindings", "signer-mnemonic"] } +eyre = { workspace = true } +futures = { workspace = true } +tokio = { workspace = true, features = ["full"] } + +[lints] +workspace = true + +[features] +erc1967 = [] diff --git a/crates/contracts/abi/ERC1967Proxy.json b/crates/contracts/abi/ERC1967Proxy.json new file mode 100644 index 0000000..0650ce4 --- /dev/null +++ b/crates/contracts/abi/ERC1967Proxy.json @@ -0,0 +1 @@ +{"abi":[{"type":"constructor","inputs":[{"name":"implementation","type":"address","internalType":"address"},{"name":"_data","type":"bytes","internalType":"bytes"}],"stateMutability":"payable"},{"type":"fallback","stateMutability":"payable"},{"type":"event","name":"Upgraded","inputs":[{"name":"implementation","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"error","name":"AddressEmptyCode","inputs":[{"name":"target","type":"address","internalType":"address"}]},{"type":"error","name":"ERC1967InvalidImplementation","inputs":[{"name":"implementation","type":"address","internalType":"address"}]},{"type":"error","name":"ERC1967NonPayable","inputs":[]},{"type":"error","name":"FailedCall","inputs":[]}],"bytecode":{"object":"0x608060405260405161037338038061037383398101604081905261002291610219565b61002c8282610033565b50506102fa565b61003c82610091565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561008557610080828261010c565b505050565b61008d6101ad565b5050565b806001600160a01b03163b5f036100cb57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc80546001600160a01b0319166001600160a01b0392909216919091179055565b60605f61011984846101ce565b905080801561013a57505f3d118061013a57505f846001600160a01b03163b115b1561014f576101476101e1565b9150506101a7565b801561017957604051639996b31560e01b81526001600160a01b03851660048201526024016100c2565b3d1561018c576101876101fa565b6101a5565b60405163d6bda27560e01b815260040160405180910390fd5b505b92915050565b34156101cc5760405163b398979f60e01b815260040160405180910390fd5b565b5f805f835160208501865af49392505050565b6040513d81523d5f602083013e3d602001810160405290565b6040513d5f823e3d81fd5b634e487b7160e01b5f52604160045260245ffd5b5f806040838503121561022a575f80fd5b82516001600160a01b0381168114610240575f80fd5b602084810151919350906001600160401b038082111561025e575f80fd5b818601915086601f830112610271575f80fd5b81518181111561028357610283610205565b604051601f8201601f19908116603f011681019083821181831017156102ab576102ab610205565b8160405282815289868487010111156102c2575f80fd5b5f93505b828410156102e357848401860151818501870152928501926102c6565b5f8684830101528096505050505050509250929050565b606d806103065f395ff3fe6080604052600a600c565b005b60186014601a565b6050565b565b5f604b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546001600160a01b031690565b905090565b365f80375f80365f845af43d5f803e8080156069573d5ff35b3d5ffd","sourceMap":"600:1117:28:-:0;;;1081:133;;;;;;;;;;;;;;;;;;:::i;:::-;1155:52;1185:14;1201:5;1155:29;:52::i;:::-;1081:133;;600:1117;;2264:344:29;2355:37;2374:17;2355:18;:37::i;:::-;2407:36;;-1:-1:-1;;;;;2407:36:29;;;;;;;;2458:11;;:15;2454:148;;2489:53;2518:17;2537:4;2489:28;:53::i;:::-;;2264:344;;:::o;2454:148::-;2573:18;:16;:18::i;:::-;2264:344;;:::o;1671:281::-;1748:17;-1:-1:-1;;;;;1748:29:29;;1781:1;1748:34;1744:119;;1805:47;;-1:-1:-1;;;1805:47:29;;-1:-1:-1;;;;;1523:32:39;;1805:47:29;;;1505:51:39;1478:18;;1805:47:29;;;;;;;;1744:119;811:66;1872:73;;-1:-1:-1;;;;;;1872:73:29;-1:-1:-1;;;;;1872:73:29;;;;;;;;;;1671:281::o;4691:549:34:-;4774:12;4798;4813:47;4847:6;4855:4;4813:33;:47::i;:::-;4798:62;;4874:7;:72;;;;-1:-1:-1;4918:1:34;4583:16:36;4886:33:34;:59;;;;4944:1;4923:6;-1:-1:-1;;;;;4923:18:34;;:22;4886:59;4870:364;;;4969:25;:23;:25::i;:::-;4962:32;;;;;4870:364;5015:7;5011:223;;;5045:24;;-1:-1:-1;;;5045:24:34;;-1:-1:-1;;;;;1523:32:39;;5045:24:34;;;1505:51:39;1478:18;;5045:24:34;1359:203:39;5011:223:34;4583:16:36;5090:33:34;5086:148;;5139:27;:25;:27::i;:::-;5086:148;;;5204:19;;-1:-1:-1;;;5204:19:34;;;;;;;;;;;5086:148;4788:452;4691:549;;;;;:::o;6113:122:29:-;6163:9;:13;6159:70;;6199:19;;-1:-1:-1;;;6199:19:29;;;;;;;;;;;6159:70;6113:122::o;3383:242:36:-;3466:12;3604:4;3598;3591;3585:11;3578:4;3572;3568:15;3560:6;3553:5;3540:69;3529:80;3383:242;-1:-1:-1;;;3383:242:36:o;4698:334::-;4829:4;4823:11;4862:16;4847:32;;4932:16;4926:4;4919;4907:17;;4892:57;4997:16;4991:4;4987:27;4979:6;4975:40;4969:4;4962:54;4698:334;:::o;5099:223::-;5203:4;5197:11;5247:16;5241:4;5236:3;5221:43;5289:16;5284:3;5277:29;14:127:39;75:10;70:3;66:20;63:1;56:31;106:4;103:1;96:15;130:4;127:1;120:15;146:1208;234:6;242;295:2;283:9;274:7;270:23;266:32;263:52;;;311:1;308;301:12;263:52;337:16;;-1:-1:-1;;;;;382:31:39;;372:42;;362:70;;428:1;425;418:12;362:70;475:2;506:18;;;500:25;451:5;;-1:-1:-1;475:2:39;-1:-1:-1;;;;;574:14:39;;;571:34;;;601:1;598;591:12;571:34;639:6;628:9;624:22;614:32;;684:7;677:4;673:2;669:13;665:27;655:55;;706:1;703;696:12;655:55;735:2;729:9;757:2;753;750:10;747:36;;;763:18;;:::i;:::-;838:2;832:9;806:2;892:13;;-1:-1:-1;;888:22:39;;;912:2;884:31;880:40;868:53;;;936:18;;;956:22;;;933:46;930:72;;;982:18;;:::i;:::-;1022:10;1018:2;1011:22;1057:2;1049:6;1042:18;1097:7;1092:2;1087;1083;1079:11;1075:20;1072:33;1069:53;;;1118:1;1115;1108:12;1069:53;1140:1;1131:10;;1150:129;1164:2;1161:1;1158:9;1150:129;;;1252:10;;;1248:19;;1242:26;1221:14;;;1217:23;;1210:59;1175:10;;;;1150:129;;;1321:1;1316:2;1311;1303:6;1299:15;1295:24;1288:35;1342:6;1332:16;;;;;;;;146:1208;;;;;:::o;1359:203::-;600:1117:28;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x6080604052600a600c565b005b60186014601a565b6050565b565b5f604b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546001600160a01b031690565b905090565b365f80375f80365f845af43d5f803e8080156069573d5ff35b3d5ffd","sourceMap":"600:1117:28:-:0;;;2676:11:30;:9;:11::i;:::-;600:1117:28;2350:83:30;2398:28;2408:17;:15;:17::i;:::-;2398:9;:28::i;:::-;2350:83::o;1583:132:28:-;1650:7;1676:32;811:66:29;1519:53;-1:-1:-1;;;;;1519:53:29;;1441:138;1676:32:28;1669:39;;1583:132;:::o;949:922:30:-;1293:14;1287:4;1281;1268:40;1513:4;1507;1491:14;1485:4;1469:14;1462:5;1449:69;1598:16;1592:4;1586;1571:44;1636:6;1703:69;;;;1824:16;1818:4;1811:30;1703:69;1741:16;1735:4;1728:30","linkReferences":{}},"methodIdentifiers":{},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.24+commit.e11b9ed9\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"implementation\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"_data\",\"type\":\"bytes\"}],\"stateMutability\":\"payable\",\"type\":\"constructor\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"}],\"name\":\"AddressEmptyCode\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"implementation\",\"type\":\"address\"}],\"name\":\"ERC1967InvalidImplementation\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ERC1967NonPayable\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"FailedCall\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"implementation\",\"type\":\"address\"}],\"name\":\"Upgraded\",\"type\":\"event\"},{\"stateMutability\":\"payable\",\"type\":\"fallback\"}],\"devdoc\":{\"details\":\"This contract implements an upgradeable proxy. It is upgradeable because calls are delegated to an implementation address that can be changed. This address is stored in storage in the location specified by https://eips.ethereum.org/EIPS/eip-1967[ERC-1967], so that it doesn't conflict with the storage layout of the implementation behind the proxy.\",\"errors\":{\"AddressEmptyCode(address)\":[{\"details\":\"There's no code at `target` (it is not a contract).\"}],\"ERC1967InvalidImplementation(address)\":[{\"details\":\"The `implementation` of the proxy is invalid.\"}],\"ERC1967NonPayable()\":[{\"details\":\"An upgrade function sees `msg.value > 0` that may be lost.\"}],\"FailedCall()\":[{\"details\":\"A call to an address target failed. The target may have reverted.\"}]},\"events\":{\"Upgraded(address)\":{\"details\":\"Emitted when the implementation is upgraded.\"}},\"kind\":\"dev\",\"methods\":{\"constructor\":{\"details\":\"Initializes the upgradeable proxy with an initial implementation specified by `implementation`. If `_data` is nonempty, it's used as data in a delegate call to `implementation`. This will typically be an encoded function call, and allows initializing the storage of the proxy like a Solidity constructor. Requirements: - If `data` is empty, `msg.value` must be zero.\"}},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol\":\"ERC1967Proxy\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"appendCBOR\":false,\"bytecodeHash\":\"none\"},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[\":@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/\",\":@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/\",\":erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/\",\":forge-std/=lib/forge-std/src/\",\":halmos-cheatcodes/=lib/openzeppelin-contracts-upgradeable/lib/halmos-cheatcodes/src/\",\":openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/\",\":openzeppelin-contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/\",\":openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/\"]},\"sources\":{\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/interfaces/IERC1967.sol\":{\"keccak256\":\"0xbf2aefe54b76d7f7bcd4f6da1080b7b1662611937d870b880db584d09cea56b5\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://f5e7e2f12e0feec75296e57f51f82fdaa8bd1551f4b8cc6560442c0bf60f818c\",\"dweb:/ipfs/QmcW9wDMaQ8RbQibMarfp17a3bABzY5KraWe2YDwuUrUoz\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol\":{\"keccak256\":\"0xa3066ff86b94128a9d3956a63a0511fa1aae41bd455772ab587b32ff322acb2e\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://bf7b192fd82acf6187970c80548f624b1b9c80425b62fa49e7fdb538a52de049\",\"dweb:/ipfs/QmWXG1YCde1tqDYTbNwjkZDWVgPEjzaQGSDqWkyKLzaNua\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol\":{\"keccak256\":\"0xa1ad192cd45317c788618bef5cb1fb3ca4ce8b230f6433ac68cc1d850fb81618\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://b43447bb85a53679d269a403c693b9d88d6c74177dfb35eddca63abaf7cf110a\",\"dweb:/ipfs/QmXSDmpd4bNZj1PDgegr6C4w1jDaWHXCconC3rYiw9TSkQ\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/Proxy.sol\":{\"keccak256\":\"0x80935e4fae2c414f4e7789e13a820d06901182a5733ab006f8d68b5b09db993f\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://752d991d6ca1087587b48103bc623f74888054f58581ff29166d90889c4765c5\",\"dweb:/ipfs/QmRBsa6K2ChKxVWYY54YiyYhDBPbmY5HyKCtij5LoWh56o\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol\":{\"keccak256\":\"0x20462ddb2665e9521372c76b001d0ce196e59dbbd989de9af5576cad0bd5628b\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://f417fd12aeec8fbfaceaa30e3a08a0724c0bc39de363e2acf6773c897abbaf6d\",\"dweb:/ipfs/QmU4Hko6sApdweVM92CsiuLKkCk8HfyBeutF89PCTz5Tye\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/Address.sol\":{\"keccak256\":\"0x0fa9e0d3a859900b5a46f70a03c73adf259603d5e05027a37fe0b45529d85346\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://c2add4da0240c9f2ce47649c8bb6b11b40e98cf6f88b8bdc76b2704e89391710\",\"dweb:/ipfs/QmNQTwF2uVzu4CRtNxr8bxyP9XuW6VsZuo2Nr4KR2bZr3d\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/Errors.sol\":{\"keccak256\":\"0x6afa713bfd42cf0f7656efa91201007ac465e42049d7de1d50753a373648c123\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://ba1d02f4847670a1b83dec9f7d37f0b0418d6043447b69f3a29a5f9efc547fcf\",\"dweb:/ipfs/QmQ7iH2keLNUKgq2xSWcRmuBE5eZ3F5whYAkAGzCNNoEWB\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/LowLevelCall.sol\":{\"keccak256\":\"0x5b4802a4352474792df3107e961d1cc593e47b820c14f69d3505cb28f5a6a583\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://a6f86fd01f829499fe0545ff5dda07d4521988e88bfe0bf801fc15650921ed56\",\"dweb:/ipfs/QmUUKu4ZDffHAmfkf3asuQfmLTyfpuy2Amdncc3SqfzKPG\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/StorageSlot.sol\":{\"keccak256\":\"0xcf74f855663ce2ae00ed8352666b7935f6cddea2932fdf2c3ecd30a9b1cd0e97\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://9f660b1f351b757dfe01438e59888f31f33ded3afcf5cb5b0d9bf9aa6f320a8b\",\"dweb:/ipfs/QmarDJ5hZEgBtCmmrVzEZWjub9769eD686jmzb2XpSU1cM\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.24+commit.e11b9ed9"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"address","name":"implementation","type":"address"},{"internalType":"bytes","name":"_data","type":"bytes"}],"stateMutability":"payable","type":"constructor"},{"inputs":[{"internalType":"address","name":"target","type":"address"}],"type":"error","name":"AddressEmptyCode"},{"inputs":[{"internalType":"address","name":"implementation","type":"address"}],"type":"error","name":"ERC1967InvalidImplementation"},{"inputs":[],"type":"error","name":"ERC1967NonPayable"},{"inputs":[],"type":"error","name":"FailedCall"},{"inputs":[{"internalType":"address","name":"implementation","type":"address","indexed":true}],"type":"event","name":"Upgraded","anonymous":false},{"inputs":[],"stateMutability":"payable","type":"fallback"}],"devdoc":{"kind":"dev","methods":{"constructor":{"details":"Initializes the upgradeable proxy with an initial implementation specified by `implementation`. If `_data` is nonempty, it's used as data in a delegate call to `implementation`. This will typically be an encoded function call, and allows initializing the storage of the proxy like a Solidity constructor. Requirements: - If `data` is empty, `msg.value` must be zero."}},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/","@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/","erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/","forge-std/=lib/forge-std/src/","halmos-cheatcodes/=lib/openzeppelin-contracts-upgradeable/lib/halmos-cheatcodes/src/","openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/","openzeppelin-contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/","openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/"],"optimizer":{"enabled":true,"runs":200},"metadata":{"bytecodeHash":"none","appendCBOR":false},"compilationTarget":{"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol":"ERC1967Proxy"},"evmVersion":"cancun","libraries":{}},"sources":{"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/interfaces/IERC1967.sol":{"keccak256":"0xbf2aefe54b76d7f7bcd4f6da1080b7b1662611937d870b880db584d09cea56b5","urls":["bzz-raw://f5e7e2f12e0feec75296e57f51f82fdaa8bd1551f4b8cc6560442c0bf60f818c","dweb:/ipfs/QmcW9wDMaQ8RbQibMarfp17a3bABzY5KraWe2YDwuUrUoz"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol":{"keccak256":"0xa3066ff86b94128a9d3956a63a0511fa1aae41bd455772ab587b32ff322acb2e","urls":["bzz-raw://bf7b192fd82acf6187970c80548f624b1b9c80425b62fa49e7fdb538a52de049","dweb:/ipfs/QmWXG1YCde1tqDYTbNwjkZDWVgPEjzaQGSDqWkyKLzaNua"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol":{"keccak256":"0xa1ad192cd45317c788618bef5cb1fb3ca4ce8b230f6433ac68cc1d850fb81618","urls":["bzz-raw://b43447bb85a53679d269a403c693b9d88d6c74177dfb35eddca63abaf7cf110a","dweb:/ipfs/QmXSDmpd4bNZj1PDgegr6C4w1jDaWHXCconC3rYiw9TSkQ"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/Proxy.sol":{"keccak256":"0x80935e4fae2c414f4e7789e13a820d06901182a5733ab006f8d68b5b09db993f","urls":["bzz-raw://752d991d6ca1087587b48103bc623f74888054f58581ff29166d90889c4765c5","dweb:/ipfs/QmRBsa6K2ChKxVWYY54YiyYhDBPbmY5HyKCtij5LoWh56o"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol":{"keccak256":"0x20462ddb2665e9521372c76b001d0ce196e59dbbd989de9af5576cad0bd5628b","urls":["bzz-raw://f417fd12aeec8fbfaceaa30e3a08a0724c0bc39de363e2acf6773c897abbaf6d","dweb:/ipfs/QmU4Hko6sApdweVM92CsiuLKkCk8HfyBeutF89PCTz5Tye"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/Address.sol":{"keccak256":"0x0fa9e0d3a859900b5a46f70a03c73adf259603d5e05027a37fe0b45529d85346","urls":["bzz-raw://c2add4da0240c9f2ce47649c8bb6b11b40e98cf6f88b8bdc76b2704e89391710","dweb:/ipfs/QmNQTwF2uVzu4CRtNxr8bxyP9XuW6VsZuo2Nr4KR2bZr3d"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/Errors.sol":{"keccak256":"0x6afa713bfd42cf0f7656efa91201007ac465e42049d7de1d50753a373648c123","urls":["bzz-raw://ba1d02f4847670a1b83dec9f7d37f0b0418d6043447b69f3a29a5f9efc547fcf","dweb:/ipfs/QmQ7iH2keLNUKgq2xSWcRmuBE5eZ3F5whYAkAGzCNNoEWB"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/LowLevelCall.sol":{"keccak256":"0x5b4802a4352474792df3107e961d1cc593e47b820c14f69d3505cb28f5a6a583","urls":["bzz-raw://a6f86fd01f829499fe0545ff5dda07d4521988e88bfe0bf801fc15650921ed56","dweb:/ipfs/QmUUKu4ZDffHAmfkf3asuQfmLTyfpuy2Amdncc3SqfzKPG"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/StorageSlot.sol":{"keccak256":"0xcf74f855663ce2ae00ed8352666b7935f6cddea2932fdf2c3ecd30a9b1cd0e97","urls":["bzz-raw://9f660b1f351b757dfe01438e59888f31f33ded3afcf5cb5b0d9bf9aa6f320a8b","dweb:/ipfs/QmarDJ5hZEgBtCmmrVzEZWjub9769eD686jmzb2XpSU1cM"],"license":"MIT"}},"version":1},"storageLayout":{"storage":[],"types":{}},"ast":{"absolutePath":"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol","id":40451,"exportedSymbols":{"ERC1967Proxy":[40450],"ERC1967Utils":[40744],"Proxy":[40780]},"nodeType":"SourceUnit","src":"114:1604:28","nodes":[{"id":40414,"nodeType":"PragmaDirective","src":"114:24:28","nodes":[],"literals":["solidity","^","0.8",".22"]},{"id":40416,"nodeType":"ImportDirective","src":"140:35:28","nodes":[],"absolutePath":"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/Proxy.sol","file":"../Proxy.sol","nameLocation":"-1:-1:-1","scope":40451,"sourceUnit":40781,"symbolAliases":[{"foreign":{"id":40415,"name":"Proxy","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":40780,"src":"148:5:28","typeDescriptions":{}},"nameLocation":"-1:-1:-1"}],"unitAlias":""},{"id":40418,"nodeType":"ImportDirective","src":"176:48:28","nodes":[],"absolutePath":"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol","file":"./ERC1967Utils.sol","nameLocation":"-1:-1:-1","scope":40451,"sourceUnit":40745,"symbolAliases":[{"foreign":{"id":40417,"name":"ERC1967Utils","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":40744,"src":"184:12:28","typeDescriptions":{}},"nameLocation":"-1:-1:-1"}],"unitAlias":""},{"id":40450,"nodeType":"ContractDefinition","src":"600:1117:28","nodes":[{"id":40437,"nodeType":"FunctionDefinition","src":"1081:133:28","nodes":[],"body":{"id":40436,"nodeType":"Block","src":"1145:69:28","nodes":[],"statements":[{"expression":{"arguments":[{"id":40432,"name":"implementation","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":40424,"src":"1185:14:28","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},{"id":40433,"name":"_data","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":40426,"src":"1201:5:28","typeDescriptions":{"typeIdentifier":"t_bytes_memory_ptr","typeString":"bytes memory"}}],"expression":{"argumentTypes":[{"typeIdentifier":"t_address","typeString":"address"},{"typeIdentifier":"t_bytes_memory_ptr","typeString":"bytes memory"}],"expression":{"id":40429,"name":"ERC1967Utils","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":40744,"src":"1155:12:28","typeDescriptions":{"typeIdentifier":"t_type$_t_contract$_ERC1967Utils_$40744_$","typeString":"type(library ERC1967Utils)"}},"id":40431,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"memberLocation":"1168:16:28","memberName":"upgradeToAndCall","nodeType":"MemberAccess","referencedDeclaration":40559,"src":"1155:29:28","typeDescriptions":{"typeIdentifier":"t_function_internal_nonpayable$_t_address_$_t_bytes_memory_ptr_$returns$__$","typeString":"function (address,bytes memory)"}},"id":40434,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"nameLocations":[],"names":[],"nodeType":"FunctionCall","src":"1155:52:28","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_tuple$__$","typeString":"tuple()"}},"id":40435,"nodeType":"ExpressionStatement","src":"1155:52:28"}]},"documentation":{"id":40422,"nodeType":"StructuredDocumentation","src":"637:439:28","text":" @dev Initializes the upgradeable proxy with an initial implementation specified by `implementation`.\n If `_data` is nonempty, it's used as data in a delegate call to `implementation`. This will typically be an\n encoded function call, and allows initializing the storage of the proxy like a Solidity constructor.\n Requirements:\n - If `data` is empty, `msg.value` must be zero."},"implemented":true,"kind":"constructor","modifiers":[],"name":"","nameLocation":"-1:-1:-1","parameters":{"id":40427,"nodeType":"ParameterList","parameters":[{"constant":false,"id":40424,"mutability":"mutable","name":"implementation","nameLocation":"1101:14:28","nodeType":"VariableDeclaration","scope":40437,"src":"1093:22:28","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":40423,"name":"address","nodeType":"ElementaryTypeName","src":"1093:7:28","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"internal"},{"constant":false,"id":40426,"mutability":"mutable","name":"_data","nameLocation":"1130:5:28","nodeType":"VariableDeclaration","scope":40437,"src":"1117:18:28","stateVariable":false,"storageLocation":"memory","typeDescriptions":{"typeIdentifier":"t_bytes_memory_ptr","typeString":"bytes"},"typeName":{"id":40425,"name":"bytes","nodeType":"ElementaryTypeName","src":"1117:5:28","typeDescriptions":{"typeIdentifier":"t_bytes_storage_ptr","typeString":"bytes"}},"visibility":"internal"}],"src":"1092:44:28"},"returnParameters":{"id":40428,"nodeType":"ParameterList","parameters":[],"src":"1145:0:28"},"scope":40450,"stateMutability":"payable","virtual":false,"visibility":"public"},{"id":40449,"nodeType":"FunctionDefinition","src":"1583:132:28","nodes":[],"body":{"id":40448,"nodeType":"Block","src":"1659:56:28","nodes":[],"statements":[{"expression":{"arguments":[],"expression":{"argumentTypes":[],"expression":{"id":40444,"name":"ERC1967Utils","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":40744,"src":"1676:12:28","typeDescriptions":{"typeIdentifier":"t_type$_t_contract$_ERC1967Utils_$40744_$","typeString":"type(library ERC1967Utils)"}},"id":40445,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"memberLocation":"1689:17:28","memberName":"getImplementation","nodeType":"MemberAccess","referencedDeclaration":40496,"src":"1676:30:28","typeDescriptions":{"typeIdentifier":"t_function_internal_view$__$returns$_t_address_$","typeString":"function () view returns (address)"}},"id":40446,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"nameLocations":[],"names":[],"nodeType":"FunctionCall","src":"1676:32:28","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"functionReturnParameters":40443,"id":40447,"nodeType":"Return","src":"1669:39:28"}]},"baseFunctions":[40761],"documentation":{"id":40438,"nodeType":"StructuredDocumentation","src":"1220:358:28","text":" @dev Returns the current implementation address.\n TIP: To get this value clients can read directly from the storage slot shown below (specified by ERC-1967) using\n the https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call.\n `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc`"},"implemented":true,"kind":"function","modifiers":[],"name":"_implementation","nameLocation":"1592:15:28","overrides":{"id":40440,"nodeType":"OverrideSpecifier","overrides":[],"src":"1632:8:28"},"parameters":{"id":40439,"nodeType":"ParameterList","parameters":[],"src":"1607:2:28"},"returnParameters":{"id":40443,"nodeType":"ParameterList","parameters":[{"constant":false,"id":40442,"mutability":"mutable","name":"","nameLocation":"-1:-1:-1","nodeType":"VariableDeclaration","scope":40449,"src":"1650:7:28","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":40441,"name":"address","nodeType":"ElementaryTypeName","src":"1650:7:28","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"internal"}],"src":"1649:9:28"},"scope":40450,"stateMutability":"view","virtual":true,"visibility":"internal"}],"abstract":false,"baseContracts":[{"baseName":{"id":40420,"name":"Proxy","nameLocations":["625:5:28"],"nodeType":"IdentifierPath","referencedDeclaration":40780,"src":"625:5:28"},"id":40421,"nodeType":"InheritanceSpecifier","src":"625:5:28"}],"canonicalName":"ERC1967Proxy","contractDependencies":[],"contractKind":"contract","documentation":{"id":40419,"nodeType":"StructuredDocumentation","src":"226:373:28","text":" @dev This contract implements an upgradeable proxy. It is upgradeable because calls are delegated to an\n implementation address that can be changed. This address is stored in storage in the location specified by\n https://eips.ethereum.org/EIPS/eip-1967[ERC-1967], so that it doesn't conflict with the storage layout of the\n implementation behind the proxy."},"fullyImplemented":true,"linearizedBaseContracts":[40450,40780],"name":"ERC1967Proxy","nameLocation":"609:12:28","scope":40451,"usedErrors":[40470,40483,41236,41627],"usedEvents":[40389]}],"license":"MIT"},"id":28} \ No newline at end of file diff --git a/crates/contracts/abi/IUniversalTimestamps.json b/crates/contracts/abi/IUniversalTimestamps.json new file mode 100644 index 0000000..9a80366 --- /dev/null +++ b/crates/contracts/abi/IUniversalTimestamps.json @@ -0,0 +1 @@ +{"abi":[{"type":"function","name":"attest","inputs":[{"name":"root","type":"bytes32","internalType":"bytes32"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"timestamp","inputs":[{"name":"root","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"event","name":"Attested","inputs":[{"name":"root","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"sender","type":"address","indexed":true,"internalType":"address"},{"name":"timestamp","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false}],"bytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"deployedBytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"methodIdentifiers":{"attest(bytes32)":"23c3617f","timestamp(bytes32)":"4d003070"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.24+commit.e11b9ed9\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"}],\"name\":\"Attested\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"}],\"name\":\"attest\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"}],\"name\":\"timestamp\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"contracts/IUniversalTimestamps.sol\":\"IUniversalTimestamps\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"appendCBOR\":false,\"bytecodeHash\":\"none\"},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[\":@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/\",\":@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/\",\":erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/\",\":forge-std/=lib/forge-std/src/\",\":halmos-cheatcodes/=lib/openzeppelin-contracts-upgradeable/lib/halmos-cheatcodes/src/\",\":openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/\",\":openzeppelin-contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/\",\":openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/\"]},\"sources\":{\"contracts/IUniversalTimestamps.sol\":{\"keccak256\":\"0xfa9490d2704cebe76fa78d15f51ed3bf13577ffaf782fe0337db402872571df0\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://c42b87e7040e6d72f5b49a2bff481191678e15df1c19793e9c55800e8f5276cf\",\"dweb:/ipfs/QmNr4ET95fihTnh3YF6WjjKUsCFU7KkoPkvKAizb83pyCC\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.24+commit.e11b9ed9"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"bytes32","name":"root","type":"bytes32","indexed":true},{"internalType":"address","name":"sender","type":"address","indexed":true},{"internalType":"uint256","name":"timestamp","type":"uint256","indexed":false}],"type":"event","name":"Attested","anonymous":false},{"inputs":[{"internalType":"bytes32","name":"root","type":"bytes32"}],"stateMutability":"nonpayable","type":"function","name":"attest"},{"inputs":[{"internalType":"bytes32","name":"root","type":"bytes32"}],"stateMutability":"view","type":"function","name":"timestamp","outputs":[{"internalType":"uint256","name":"","type":"uint256"}]}],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/","@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/","erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/","forge-std/=lib/forge-std/src/","halmos-cheatcodes/=lib/openzeppelin-contracts-upgradeable/lib/halmos-cheatcodes/src/","openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/","openzeppelin-contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/","openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/"],"optimizer":{"enabled":true,"runs":200},"metadata":{"bytecodeHash":"none","appendCBOR":false},"compilationTarget":{"contracts/IUniversalTimestamps.sol":"IUniversalTimestamps"},"evmVersion":"cancun","libraries":{}},"sources":{"contracts/IUniversalTimestamps.sol":{"keccak256":"0xfa9490d2704cebe76fa78d15f51ed3bf13577ffaf782fe0337db402872571df0","urls":["bzz-raw://c42b87e7040e6d72f5b49a2bff481191678e15df1c19793e9c55800e8f5276cf","dweb:/ipfs/QmNr4ET95fihTnh3YF6WjjKUsCFU7KkoPkvKAizb83pyCC"],"license":"MIT"}},"version":1},"storageLayout":{"storage":[],"types":{}},"ast":{"absolutePath":"contracts/IUniversalTimestamps.sol","id":187,"exportedSymbols":{"IUniversalTimestamps":[186]},"nodeType":"SourceUnit","src":"33:262:1","nodes":[{"id":165,"nodeType":"PragmaDirective","src":"33:24:1","nodes":[],"literals":["solidity","^","0.8",".24"]},{"id":186,"nodeType":"ContractDefinition","src":"59:235:1","nodes":[{"id":173,"nodeType":"EventDefinition","src":"96:80:1","nodes":[],"anonymous":false,"eventSelector":"61cae4201bb8c0117495b22a70f5202410666b349c27302dac280dc054b60f2a","name":"Attested","nameLocation":"102:8:1","parameters":{"id":172,"nodeType":"ParameterList","parameters":[{"constant":false,"id":167,"indexed":true,"mutability":"mutable","name":"root","nameLocation":"127:4:1","nodeType":"VariableDeclaration","scope":173,"src":"111:20:1","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"},"typeName":{"id":166,"name":"bytes32","nodeType":"ElementaryTypeName","src":"111:7:1","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"visibility":"internal"},{"constant":false,"id":169,"indexed":true,"mutability":"mutable","name":"sender","nameLocation":"149:6:1","nodeType":"VariableDeclaration","scope":173,"src":"133:22:1","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":168,"name":"address","nodeType":"ElementaryTypeName","src":"133:7:1","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"internal"},{"constant":false,"id":171,"indexed":false,"mutability":"mutable","name":"timestamp","nameLocation":"165:9:1","nodeType":"VariableDeclaration","scope":173,"src":"157:17:1","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"},"typeName":{"id":170,"name":"uint256","nodeType":"ElementaryTypeName","src":"157:7:1","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"visibility":"internal"}],"src":"110:65:1"}},{"id":178,"nodeType":"FunctionDefinition","src":"182:39:1","nodes":[],"functionSelector":"23c3617f","implemented":false,"kind":"function","modifiers":[],"name":"attest","nameLocation":"191:6:1","parameters":{"id":176,"nodeType":"ParameterList","parameters":[{"constant":false,"id":175,"mutability":"mutable","name":"root","nameLocation":"206:4:1","nodeType":"VariableDeclaration","scope":178,"src":"198:12:1","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"},"typeName":{"id":174,"name":"bytes32","nodeType":"ElementaryTypeName","src":"198:7:1","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"visibility":"internal"}],"src":"197:14:1"},"returnParameters":{"id":177,"nodeType":"ParameterList","parameters":[],"src":"220:0:1"},"scope":186,"stateMutability":"nonpayable","virtual":false,"visibility":"external"},{"id":185,"nodeType":"FunctionDefinition","src":"227:65:1","nodes":[],"functionSelector":"4d003070","implemented":false,"kind":"function","modifiers":[],"name":"timestamp","nameLocation":"236:9:1","parameters":{"id":181,"nodeType":"ParameterList","parameters":[{"constant":false,"id":180,"mutability":"mutable","name":"root","nameLocation":"254:4:1","nodeType":"VariableDeclaration","scope":185,"src":"246:12:1","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"},"typeName":{"id":179,"name":"bytes32","nodeType":"ElementaryTypeName","src":"246:7:1","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"visibility":"internal"}],"src":"245:14:1"},"returnParameters":{"id":184,"nodeType":"ParameterList","parameters":[{"constant":false,"id":183,"mutability":"mutable","name":"","nameLocation":"-1:-1:-1","nodeType":"VariableDeclaration","scope":185,"src":"283:7:1","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"},"typeName":{"id":182,"name":"uint256","nodeType":"ElementaryTypeName","src":"283:7:1","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"visibility":"internal"}],"src":"282:9:1"},"scope":186,"stateMutability":"view","virtual":false,"visibility":"external"}],"abstract":false,"baseContracts":[],"canonicalName":"IUniversalTimestamps","contractDependencies":[],"contractKind":"interface","fullyImplemented":false,"linearizedBaseContracts":[186],"name":"IUniversalTimestamps","nameLocation":"69:20:1","scope":187,"usedErrors":[],"usedEvents":[173]}],"license":"MIT"},"id":1} \ No newline at end of file diff --git a/crates/contracts/abi/UniversalTimestamps.json b/crates/contracts/abi/UniversalTimestamps.json new file mode 100644 index 0000000..1110a7d --- /dev/null +++ b/crates/contracts/abi/UniversalTimestamps.json @@ -0,0 +1 @@ +{"abi":[{"type":"constructor","inputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"UPGRADE_INTERFACE_VERSION","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"attest","inputs":[{"name":"root","type":"bytes32","internalType":"bytes32"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"initialize","inputs":[{"name":"initialOwner","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"owner","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"proxiableUUID","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"renounceOwnership","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"timestamp","inputs":[{"name":"root","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"transferOwnership","inputs":[{"name":"newOwner","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"upgradeToAndCall","inputs":[{"name":"newImplementation","type":"address","internalType":"address"},{"name":"data","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"payable"},{"type":"event","name":"Attested","inputs":[{"name":"root","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"sender","type":"address","indexed":true,"internalType":"address"},{"name":"timestamp","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"OwnershipTransferred","inputs":[{"name":"previousOwner","type":"address","indexed":true,"internalType":"address"},{"name":"newOwner","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Upgraded","inputs":[{"name":"implementation","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"error","name":"AddressEmptyCode","inputs":[{"name":"target","type":"address","internalType":"address"}]},{"type":"error","name":"ERC1967InvalidImplementation","inputs":[{"name":"implementation","type":"address","internalType":"address"}]},{"type":"error","name":"ERC1967NonPayable","inputs":[]},{"type":"error","name":"FailedCall","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"OwnableInvalidOwner","inputs":[{"name":"owner","type":"address","internalType":"address"}]},{"type":"error","name":"OwnableUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"}]},{"type":"error","name":"UUPSUnauthorizedCallContext","inputs":[]},{"type":"error","name":"UUPSUnsupportedProxiableUUID","inputs":[{"name":"slot","type":"bytes32","internalType":"bytes32"}]}],"bytecode":{"object":"0x60a060405230608052348015610013575f80fd5b5061001c610021565b6100d3565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00805468010000000000000000900460ff16156100715760405163f92ee8a960e01b815260040160405180910390fd5b80546001600160401b03908116146100d05780546001600160401b0319166001600160401b0390811782556040519081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50565b608051610b046100f95f395f81816104bb015281816104e401526106280152610b045ff3fe608060405260043610610084575f3560e01c8063715018a611610057578063715018a6146101025780638da5cb5b14610116578063ad3cb1cc1461015c578063c4d66de814610199578063f2fde38b146101b8575f80fd5b806323c3617f146100885780634d003070146100a95780634f1ef286146100db57806352d1902d146100ee575b5f80fd5b348015610093575f80fd5b506100a76100a236600461095e565b6101d7565b005b3480156100b4575f80fd5b506100c86100c336600461095e565b610295565b6040519081526020015b60405180910390f35b6100a76100e93660046109a4565b6102ae565b3480156100f9575f80fd5b506100c86102c9565b34801561010d575f80fd5b506100a76102e4565b348015610121575f80fd5b507f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546040516001600160a01b0390911681526020016100d2565b348015610167575f80fd5b5061018c604051806040016040528060058152602001640352e302e360dc1b81525081565b6040516100d29190610a60565b3480156101a4575f80fd5b506100a76101b3366004610aac565b6102f7565b3480156101c3575f80fd5b506100a76101d2366004610aac565b6103f0565b806102295760405162461bcd60e51b815260206004820152601860248201527f5554533a20526f6f742063616e6e6f74206265207a65726f000000000000000060448201526064015b60405180910390fd5b5f61023261042d565b5f8381526020829052604081205491925003610291575f828152602082815260409182902042908190559151918252339184917f61cae4201bb8c0117495b22a70f5202410666b349c27302dac280dc054b60f2a910160405180910390a35b5050565b5f61029e61042d565b5f92835260205250604090205490565b6102b66104b0565b6102bf82610554565b610291828261055c565b5f6102d261061d565b505f80516020610ae483398151915290565b6102ec610666565b6102f55f6106c1565b565b5f610300610731565b805490915060ff600160401b820416159067ffffffffffffffff165f811580156103275750825b90505f8267ffffffffffffffff1660011480156103435750303b155b905081158015610351575080155b1561036f5760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561039957845460ff60401b1916600160401b1785555b6103a286610759565b83156103e857845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b505050505050565b6103f8610666565b6001600160a01b03811661042157604051631e4fbdf760e01b81525f6004820152602401610220565b61042a816106c1565b50565b60408051808201909152601f81527f7574732e73746f726167652e556e6976657273616c54696d657374616d7073006020909101527f6191da0f5f254a176c2a5b8e81a37f62349600d58cdbf87518a33cdde24d517a5f908152807f500a69046951d8ea21a7dabf6fe6e1792e3ffa4dc61a276ae65b0e2b034681005b92915050565b306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016148061053657507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031661052a5f80516020610ae4833981519152546001600160a01b031690565b6001600160a01b031614155b156102f55760405163703e46dd60e11b815260040160405180910390fd5b61042a610666565b816001600160a01b03166352d1902d6040518163ffffffff1660e01b8152600401602060405180830381865afa9250505080156105b6575060408051601f3d908101601f191682019092526105b391810190610acc565b60015b6105de57604051634c9c8ce360e01b81526001600160a01b0383166004820152602401610220565b5f80516020610ae4833981519152811461060e57604051632a87526960e21b815260048101829052602401610220565b610618838361076a565b505050565b306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146102f55760405163703e46dd60e11b815260040160405180910390fd5b336106987f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b6001600160a01b0316146102f55760405163118cdaa760e01b8152336004820152602401610220565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b5f807ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a006104aa565b6107616107bf565b61042a816107e4565b610773826107ec565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a28051156107b757610618828261084f565b6102916108ef565b6107c761090e565b6102f557604051631afcd79f60e31b815260040160405180910390fd5b6103f86107bf565b806001600160a01b03163b5f0361082157604051634c9c8ce360e01b81526001600160a01b0382166004820152602401610220565b5f80516020610ae483398151915280546001600160a01b0319166001600160a01b0392909216919091179055565b60605f61085c8484610927565b905080801561087d57505f3d118061087d57505f846001600160a01b03163b115b156108925761088a61093a565b9150506104aa565b80156108bc57604051639996b31560e01b81526001600160a01b0385166004820152602401610220565b3d156108cf576108ca610953565b6108e8565b60405163d6bda27560e01b815260040160405180910390fd5b5092915050565b34156102f55760405163b398979f60e01b815260040160405180910390fd5b5f610917610731565b54600160401b900460ff16919050565b5f805f835160208501865af49392505050565b6040513d81523d5f602083013e3d602001810160405290565b6040513d5f823e3d81fd5b5f6020828403121561096e575f80fd5b5035919050565b80356001600160a01b038116811461098b575f80fd5b919050565b634e487b7160e01b5f52604160045260245ffd5b5f80604083850312156109b5575f80fd5b6109be83610975565b9150602083013567ffffffffffffffff808211156109da575f80fd5b818501915085601f8301126109ed575f80fd5b8135818111156109ff576109ff610990565b604051601f8201601f19908116603f01168101908382118183101715610a2757610a27610990565b81604052828152886020848701011115610a3f575f80fd5b826020860160208301375f6020848301015280955050505050509250929050565b5f602080835283518060208501525f5b81811015610a8c57858101830151858201604001528201610a70565b505f604082860101526040601f19601f8301168501019250505092915050565b5f60208284031215610abc575f80fd5b610ac582610975565b9392505050565b5f60208284031215610adc575f80fd5b505191905056fe360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc","sourceMap":"1042:1794:2:-:0;;;1084:4:33;1041:48;;1489:53:2;;;;;;;;;-1:-1:-1;1513:22:2;:20;:22::i;:::-;1042:1794;;7709:422:32;3147:66;7898:15;;;;;;;7894:76;;;7936:23;;-1:-1:-1;;;7936:23:32;;;;;;;;;;;7894:76;7983:14;;-1:-1:-1;;;;;7983:14:32;;;:34;7979:146;;8033:33;;-1:-1:-1;;;;;;8033:33:32;-1:-1:-1;;;;;8033:33:32;;;;;8085:29;;158:50:39;;;8085:29:32;;146:2:39;131:18;8085:29:32;;;;;;;7979:146;7758:373;7709:422::o;14:200:39:-;1042:1794:2;;;;;;;;;;;;;;;;;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x608060405260043610610084575f3560e01c8063715018a611610057578063715018a6146101025780638da5cb5b14610116578063ad3cb1cc1461015c578063c4d66de814610199578063f2fde38b146101b8575f80fd5b806323c3617f146100885780634d003070146100a95780634f1ef286146100db57806352d1902d146100ee575b5f80fd5b348015610093575f80fd5b506100a76100a236600461095e565b6101d7565b005b3480156100b4575f80fd5b506100c86100c336600461095e565b610295565b6040519081526020015b60405180910390f35b6100a76100e93660046109a4565b6102ae565b3480156100f9575f80fd5b506100c86102c9565b34801561010d575f80fd5b506100a76102e4565b348015610121575f80fd5b507f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546040516001600160a01b0390911681526020016100d2565b348015610167575f80fd5b5061018c604051806040016040528060058152602001640352e302e360dc1b81525081565b6040516100d29190610a60565b3480156101a4575f80fd5b506100a76101b3366004610aac565b6102f7565b3480156101c3575f80fd5b506100a76101d2366004610aac565b6103f0565b806102295760405162461bcd60e51b815260206004820152601860248201527f5554533a20526f6f742063616e6e6f74206265207a65726f000000000000000060448201526064015b60405180910390fd5b5f61023261042d565b5f8381526020829052604081205491925003610291575f828152602082815260409182902042908190559151918252339184917f61cae4201bb8c0117495b22a70f5202410666b349c27302dac280dc054b60f2a910160405180910390a35b5050565b5f61029e61042d565b5f92835260205250604090205490565b6102b66104b0565b6102bf82610554565b610291828261055c565b5f6102d261061d565b505f80516020610ae483398151915290565b6102ec610666565b6102f55f6106c1565b565b5f610300610731565b805490915060ff600160401b820416159067ffffffffffffffff165f811580156103275750825b90505f8267ffffffffffffffff1660011480156103435750303b155b905081158015610351575080155b1561036f5760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561039957845460ff60401b1916600160401b1785555b6103a286610759565b83156103e857845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b505050505050565b6103f8610666565b6001600160a01b03811661042157604051631e4fbdf760e01b81525f6004820152602401610220565b61042a816106c1565b50565b60408051808201909152601f81527f7574732e73746f726167652e556e6976657273616c54696d657374616d7073006020909101527f6191da0f5f254a176c2a5b8e81a37f62349600d58cdbf87518a33cdde24d517a5f908152807f500a69046951d8ea21a7dabf6fe6e1792e3ffa4dc61a276ae65b0e2b034681005b92915050565b306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016148061053657507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031661052a5f80516020610ae4833981519152546001600160a01b031690565b6001600160a01b031614155b156102f55760405163703e46dd60e11b815260040160405180910390fd5b61042a610666565b816001600160a01b03166352d1902d6040518163ffffffff1660e01b8152600401602060405180830381865afa9250505080156105b6575060408051601f3d908101601f191682019092526105b391810190610acc565b60015b6105de57604051634c9c8ce360e01b81526001600160a01b0383166004820152602401610220565b5f80516020610ae4833981519152811461060e57604051632a87526960e21b815260048101829052602401610220565b610618838361076a565b505050565b306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146102f55760405163703e46dd60e11b815260040160405180910390fd5b336106987f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b6001600160a01b0316146102f55760405163118cdaa760e01b8152336004820152602401610220565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b5f807ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a006104aa565b6107616107bf565b61042a816107e4565b610773826107ec565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a28051156107b757610618828261084f565b6102916108ef565b6107c761090e565b6102f557604051631afcd79f60e31b815260040160405180910390fd5b6103f86107bf565b806001600160a01b03163b5f0361082157604051634c9c8ce360e01b81526001600160a01b0382166004820152602401610220565b5f80516020610ae483398151915280546001600160a01b0319166001600160a01b0392909216919091179055565b60605f61085c8484610927565b905080801561087d57505f3d118061087d57505f846001600160a01b03163b115b156108925761088a61093a565b9150506104aa565b80156108bc57604051639996b31560e01b81526001600160a01b0385166004820152602401610220565b3d156108cf576108ca610953565b6108e8565b60405163d6bda27560e01b815260040160405180910390fd5b5092915050565b34156102f55760405163b398979f60e01b815260040160405180910390fd5b5f610917610731565b54600160401b900460ff16919050565b5f805f835160208501865af49392505050565b6040513d81523d5f602083013e3d602001810160405290565b6040513d5f823e3d81fd5b5f6020828403121561096e575f80fd5b5035919050565b80356001600160a01b038116811461098b575f80fd5b919050565b634e487b7160e01b5f52604160045260245ffd5b5f80604083850312156109b5575f80fd5b6109be83610975565b9150602083013567ffffffffffffffff808211156109da575f80fd5b818501915085601f8301126109ed575f80fd5b8135818111156109ff576109ff610990565b604051601f8201601f19908116603f01168101908382118183101715610a2757610a27610990565b81604052828152886020848701011115610a3f575f80fd5b826020860160208301375f6020848301015280955050505050509250929050565b5f602080835283518060208501525f5b81811015610a8c57858101830151858201604001528201610a70565b505f604082860101526040601f19601f8301168501019250505092915050565b5f60208284031215610abc575f80fd5b610ac582610975565b9392505050565b5f60208284031215610adc575f80fd5b505191905056fe360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc","sourceMap":"1042:1794:2:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2140:354;;;;;;;;;;-1:-1:-1;2140:354:2;;;;;:::i;:::-;;:::i;:::-;;1896:138;;;;;;;;;;-1:-1:-1;1896:138:2;;;;;:::i;:::-;;:::i;:::-;;;345:25:39;;;333:2;318:18;1896:138:2;;;;;;;;3911:214:33;;;;;;:::i;:::-;;:::i;3466:126::-;;;;;;;;;;;;;:::i;3176:101:22:-;;;;;;;;;;;;;:::i;2462:144::-;;;;;;;;;;-1:-1:-1;1334:22:22;2591:8;2462:144;;-1:-1:-1;;;;;2591:8:22;;;2019:51:39;;2007:2;1992:18;2462:144:22;1873:203:39;1732:58:33;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;;;1732:58:33;;;;;;;;;;;;:::i;1548:106:2:-;;;;;;;;;;-1:-1:-1;1548:106:2;;;;;:::i;:::-;;:::i;3426:215:22:-;;;;;;;;;;-1:-1:-1;3426:215:22;;;;;:::i;:::-;;:::i;2140:354:2:-;2197:4;2189:55;;;;-1:-1:-1;;;2189:55:2;;3027:2:39;2189:55:2;;;3009:21:39;3066:2;3046:18;;;3039:30;3105:26;3085:18;;;3078:54;3149:18;;2189:55:2;;;;;;;;;2255:36;2294:32;:30;:32::i;:::-;2340:12;:18;;;;;;;;;;;2255:71;;-1:-1:-1;2340:23:2;2336:152;;2379:12;:18;;;;;;;;;;;;2400:15;2379:36;;;;2434:43;;345:25:39;;;2449:10:2;;2379:18;;2434:43;;318:18:39;2434:43:2;;;;;;;2336:152;2179:315;2140:354;:::o;1896:138::-;1952:7;1978:32;:30;:32::i;:::-;:43;:49;;;;;-1:-1:-1;1978:49:2;;;;;1896:138::o;3911:214:33:-;2568:13;:11;:13::i;:::-;4026:36:::1;4044:17;4026;:36::i;:::-;4072:46;4094:17;4113:4;4072:21;:46::i;3466:126::-:0;3527:7;2839:20;:18;:20::i;:::-;-1:-1:-1;;;;;;;;;;;;3466:126:33;:::o;3176:101:22:-;2355:13;:11;:13::i;:::-;3240:30:::1;3267:1;3240:18;:30::i;:::-;3176:101::o:0;1548:106:2:-;4158:30:32;4191:26;:24;:26::i;:::-;4302:15;;4158:59;;-1:-1:-1;4302:15:32;-1:-1:-1;;;4302:15:32;;;4301:16;;4348:14;;4279:19;4724:16;;:34;;;;;4744:14;4724:34;4704:54;;4768:17;4788:11;:16;;4803:1;4788:16;:50;;;;-1:-1:-1;4816:4:32;4808:25;:30;4788:50;4768:70;;4854:12;4853:13;:30;;;;;4871:12;4870:13;4853:30;4849:91;;;4906:23;;-1:-1:-1;;;4906:23:32;;;;;;;;;;;4849:91;4949:18;;-1:-1:-1;;4949:18:32;4966:1;4949:18;;;4977:67;;;;5011:22;;-1:-1:-1;;;;5011:22:32;-1:-1:-1;;;5011:22:32;;;4977:67;1619:28:2::1;1634:12;1619:14;:28::i;:::-;5068:14:32::0;5064:101;;;5098:23;;-1:-1:-1;;;;5098:23:32;;;5140:14;;-1:-1:-1;3331:50:39;;5140:14:32;;3319:2:39;3304:18;5140:14:32;;;;;;;5064:101;4092:1079;;;;;1548:106:2;:::o;3426:215:22:-;2355:13;:11;:13::i;:::-;-1:-1:-1;;;;;3510:22:22;::::1;3506:91;;3555:31;::::0;-1:-1:-1;;;3555:31:22;;3583:1:::1;3555:31;::::0;::::1;2019:51:39::0;1992:18;;3555:31:22::1;1873:203:39::0;3506:91:22::1;3606:28;3625:8;3606:18;:28::i;:::-;3426:215:::0;:::o;1660:230:2:-;1787:10;;;;;;;;;;;;;;;;;;1846:57:37;1724:36:2;1833:71:37;;;1724:36:2;1925:37:37;1787:24:2;1772:39;1660:230;-1:-1:-1;;1660:230:2:o;4328:312:33:-;4408:4;-1:-1:-1;;;;;4417:6:33;4400:23;;;:120;;;4514:6;-1:-1:-1;;;;;4478:42:33;:32;-1:-1:-1;;;;;;;;;;;1519:53:29;-1:-1:-1;;;;;1519:53:29;;1441:138;4478:32:33;-1:-1:-1;;;;;4478:42:33;;;4400:120;4383:251;;;4594:29;;-1:-1:-1;;;4594:29:33;;;;;;;;;;;2750:84:2;2355:13:22;:11;:13::i;5782:538:33:-;5899:17;-1:-1:-1;;;;;5881:50:33;;:52;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;5881:52:33;;;;;;;;-1:-1:-1;;5881:52:33;;;;;;;;;;;;:::i;:::-;;;5877:437;;6243:60;;-1:-1:-1;;;6243:60:33;;-1:-1:-1;;;;;2037:32:39;;6243:60:33;;;2019:51:39;1992:18;;6243:60:33;1873:203:39;5877:437:33;-1:-1:-1;;;;;;;;;;;5975:40:33;;5971:120;;6042:34;;-1:-1:-1;;;6042:34:33;;;;;345:25:39;;;318:18;;6042:34:33;199:177:39;5971:120:33;6104:54;6134:17;6153:4;6104:29;:54::i;:::-;5934:235;5782:538;;:::o;4757:213::-;4831:4;-1:-1:-1;;;;;4840:6:33;4823:23;;4819:145;;4924:29;;-1:-1:-1;;;4924:29:33;;;;;;;;;;;2679:162:22;987:10:25;2738:7:22;1334:22;2591:8;-1:-1:-1;;;;;2591:8:22;;2462:144;2738:7;-1:-1:-1;;;;;2738:23:22;;2734:101;;2784:40;;-1:-1:-1;;;2784:40:22;;987:10:25;2784:40:22;;;2019:51:39;1992:18;;2784:40:22;1873:203:39;3795:248:22;1334:22;3944:8;;-1:-1:-1;;;;;;3962:19:22;;-1:-1:-1;;;;;3962:19:22;;;;;;;;3996:40;;3944:8;;;;;3996:40;;3868:24;;3996:40;3858:185;;3795:248;:::o;9071:205:32:-;9129:30;;3147:66;9186:27;8819:122;1868:127:22;6929:20:32;:18;:20::i;:::-;1950:38:22::1;1975:12;1950:24;:38::i;2264:344:29:-:0;2355:37;2374:17;2355:18;:37::i;:::-;2407:36;;-1:-1:-1;;;;;2407:36:29;;;;;;;;2458:11;;:15;2454:148;;2489:53;2518:17;2537:4;2489:28;:53::i;2454:148::-;2573:18;:16;:18::i;7082:141:32:-;7149:17;:15;:17::i;:::-;7144:73;;7189:17;;-1:-1:-1;;;7189:17:32;;;;;;;;;;;2001:235:22;6929:20:32;:18;:20::i;1671:281:29:-;1748:17;-1:-1:-1;;;;;1748:29:29;;1781:1;1748:34;1744:119;;1805:47;;-1:-1:-1;;;1805:47:29;;-1:-1:-1;;;;;2037:32:39;;1805:47:29;;;2019:51:39;1992:18;;1805:47:29;1873:203:39;1744:119:29;-1:-1:-1;;;;;;;;;;;1872:73:29;;-1:-1:-1;;;;;;1872:73:29;-1:-1:-1;;;;;1872:73:29;;;;;;;;;;1671:281::o;4691:549:34:-;4774:12;4798;4813:47;4847:6;4855:4;4813:33;:47::i;:::-;4798:62;;4874:7;:72;;;;-1:-1:-1;4918:1:34;4583:16:36;4886:33:34;:59;;;;4944:1;4923:6;-1:-1:-1;;;;;4923:18:34;;:22;4886:59;4870:364;;;4969:25;:23;:25::i;:::-;4962:32;;;;;4870:364;5015:7;5011:223;;;5045:24;;-1:-1:-1;;;5045:24:34;;-1:-1:-1;;;;;2037:32:39;;5045:24:34;;;2019:51:39;1992:18;;5045:24:34;1873:203:39;5011:223:34;4583:16:36;5090:33:34;5086:148;;5139:27;:25;:27::i;:::-;5086:148;;;5204:19;;-1:-1:-1;;;5204:19:34;;;;;;;;;;;5086:148;4788:452;4691:549;;;;:::o;6113:122:29:-;6163:9;:13;6159:70;;6199:19;;-1:-1:-1;;;6199:19:29;;;;;;;;;;;8485:120:32;8535:4;8558:26;:24;:26::i;:::-;:40;-1:-1:-1;;;8558:40:32;;;;;;-1:-1:-1;8485:120:32:o;3383:242:36:-;3466:12;3604:4;3598;3591;3585:11;3578:4;3572;3568:15;3560:6;3553:5;3540:69;3529:80;3383:242;-1:-1:-1;;;3383:242:36:o;4698:334::-;4829:4;4823:11;4862:16;4847:32;;4932:16;4926:4;4919;4907:17;;4892:57;4997:16;4991:4;4987:27;4979:6;4975:40;4969:4;4962:54;4698:334;:::o;5099:223::-;5203:4;5197:11;5247:16;5241:4;5236:3;5221:43;5289:16;5284:3;5277:29;14:180:39;73:6;126:2;114:9;105:7;101:23;97:32;94:52;;;142:1;139;132:12;94:52;-1:-1:-1;165:23:39;;14:180;-1:-1:-1;14:180:39:o;381:173::-;449:20;;-1:-1:-1;;;;;498:31:39;;488:42;;478:70;;544:1;541;534:12;478:70;381:173;;;:::o;559:127::-;620:10;615:3;611:20;608:1;601:31;651:4;648:1;641:15;675:4;672:1;665:15;691:995;768:6;776;829:2;817:9;808:7;804:23;800:32;797:52;;;845:1;842;835:12;797:52;868:29;887:9;868:29;:::i;:::-;858:39;;948:2;937:9;933:18;920:32;971:18;1012:2;1004:6;1001:14;998:34;;;1028:1;1025;1018:12;998:34;1066:6;1055:9;1051:22;1041:32;;1111:7;1104:4;1100:2;1096:13;1092:27;1082:55;;1133:1;1130;1123:12;1082:55;1169:2;1156:16;1191:2;1187;1184:10;1181:36;;;1197:18;;:::i;:::-;1272:2;1266:9;1240:2;1326:13;;-1:-1:-1;;1322:22:39;;;1346:2;1318:31;1314:40;1302:53;;;1370:18;;;1390:22;;;1367:46;1364:72;;;1416:18;;:::i;:::-;1456:10;1452:2;1445:22;1491:2;1483:6;1476:18;1531:7;1526:2;1521;1517;1513:11;1509:20;1506:33;1503:53;;;1552:1;1549;1542:12;1503:53;1608:2;1603;1599;1595:11;1590:2;1582:6;1578:15;1565:46;1653:1;1648:2;1643;1635:6;1631:15;1627:24;1620:35;1674:6;1664:16;;;;;;;691:995;;;;;:::o;2081:548::-;2193:4;2222:2;2251;2240:9;2233:21;2283:6;2277:13;2326:6;2321:2;2310:9;2306:18;2299:34;2351:1;2361:140;2375:6;2372:1;2369:13;2361:140;;;2470:14;;;2466:23;;2460:30;2436:17;;;2455:2;2432:26;2425:66;2390:10;;2361:140;;;2365:3;2550:1;2545:2;2536:6;2525:9;2521:22;2517:31;2510:42;2620:2;2613;2609:7;2604:2;2596:6;2592:15;2588:29;2577:9;2573:45;2569:54;2561:62;;;;2081:548;;;;:::o;2634:186::-;2693:6;2746:2;2734:9;2725:7;2721:23;2717:32;2714:52;;;2762:1;2759;2752:12;2714:52;2785:29;2804:9;2785:29;:::i;:::-;2775:39;2634:186;-1:-1:-1;;;2634:186:39:o;3392:184::-;3462:6;3515:2;3503:9;3494:7;3490:23;3486:32;3483:52;;;3531:1;3528;3521:12;3483:52;-1:-1:-1;3554:16:39;;3392:184;-1:-1:-1;3392:184:39:o","linkReferences":{},"immutableReferences":{"41074":[{"start":1211,"length":32},{"start":1252,"length":32},{"start":1576,"length":32}]}},"methodIdentifiers":{"UPGRADE_INTERFACE_VERSION()":"ad3cb1cc","attest(bytes32)":"23c3617f","initialize(address)":"c4d66de8","owner()":"8da5cb5b","proxiableUUID()":"52d1902d","renounceOwnership()":"715018a6","timestamp(bytes32)":"4d003070","transferOwnership(address)":"f2fde38b","upgradeToAndCall(address,bytes)":"4f1ef286"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.24+commit.e11b9ed9\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"}],\"name\":\"AddressEmptyCode\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"implementation\",\"type\":\"address\"}],\"name\":\"ERC1967InvalidImplementation\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ERC1967NonPayable\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"FailedCall\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidInitialization\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"NotInitializing\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"OwnableInvalidOwner\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"OwnableUnauthorizedAccount\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"UUPSUnauthorizedCallContext\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"slot\",\"type\":\"bytes32\"}],\"name\":\"UUPSUnsupportedProxiableUUID\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"}],\"name\":\"Attested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"version\",\"type\":\"uint64\"}],\"name\":\"Initialized\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"previousOwner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"implementation\",\"type\":\"address\"}],\"name\":\"Upgraded\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"UPGRADE_INTERFACE_VERSION\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"}],\"name\":\"attest\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"initialOwner\",\"type\":\"address\"}],\"name\":\"initialize\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"proxiableUUID\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"renounceOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"}],\"name\":\"timestamp\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newImplementation\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"upgradeToAndCall\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"}],\"devdoc\":{\"details\":\"Records and exposes timestamps for attested Merkle roots using ERC-7201 namespaced storage (`uts.storage.UniversalTimestamps`) derived via {SlotDerivation}, and is implemented as a UUPS upgradeable contract via OpenZeppelin's Initializable, OwnableUpgradeable, and UUPSUpgradeable base contracts. Storage is kept in a dedicated namespaced struct to remain layout-compatible across upgrades, while upgrades are authorized by the contract owner through {_authorizeUpgrade}.\",\"errors\":{\"AddressEmptyCode(address)\":[{\"details\":\"There's no code at `target` (it is not a contract).\"}],\"ERC1967InvalidImplementation(address)\":[{\"details\":\"The `implementation` of the proxy is invalid.\"}],\"ERC1967NonPayable()\":[{\"details\":\"An upgrade function sees `msg.value > 0` that may be lost.\"}],\"FailedCall()\":[{\"details\":\"A call to an address target failed. The target may have reverted.\"}],\"InvalidInitialization()\":[{\"details\":\"The contract is already initialized.\"}],\"NotInitializing()\":[{\"details\":\"The contract is not initializing.\"}],\"OwnableInvalidOwner(address)\":[{\"details\":\"The owner is not a valid owner account. (eg. `address(0)`)\"}],\"OwnableUnauthorizedAccount(address)\":[{\"details\":\"The caller account is not authorized to perform an operation.\"}],\"UUPSUnauthorizedCallContext()\":[{\"details\":\"The call is from an unauthorized context.\"}],\"UUPSUnsupportedProxiableUUID(bytes32)\":[{\"details\":\"The storage `slot` is unsupported as a UUID.\"}]},\"events\":{\"Initialized(uint64)\":{\"details\":\"Triggered when the contract has been initialized or reinitialized.\"},\"Upgraded(address)\":{\"details\":\"Emitted when the implementation is upgraded.\"}},\"kind\":\"dev\",\"methods\":{\"attest(bytes32)\":{\"params\":{\"root\":\"The Merkle Root to be attested\"}},\"constructor\":{\"custom:oz-upgrades-unsafe-allow\":\"constructor\"},\"owner()\":{\"details\":\"Returns the address of the current owner.\"},\"proxiableUUID()\":{\"details\":\"Implementation of the ERC-1822 {proxiableUUID} function. This returns the storage slot used by the implementation. It is used to validate the implementation's compatibility when performing an upgrade. IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this function revert if invoked through a proxy. This is guaranteed by the `notDelegated` modifier.\"},\"renounceOwnership()\":{\"details\":\"Leaves the contract without owner. It will not be possible to call `onlyOwner` functions. Can only be called by the current owner. NOTE: Renouncing ownership will leave the contract without an owner, thereby disabling any functionality that is only available to the owner.\"},\"transferOwnership(address)\":{\"details\":\"Transfers ownership of the contract to a new account (`newOwner`). Can only be called by the current owner.\"},\"upgradeToAndCall(address,bytes)\":{\"custom:oz-upgrades-unsafe-allow-reachable\":\"delegatecall\",\"details\":\"Upgrade the implementation of the proxy to `newImplementation`, and subsequently execute the function call encoded in `data`. Calls {_authorizeUpgrade}. Emits an {Upgraded} event.\"}},\"title\":\"UniversalTimestamps\",\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{\"attest(bytes32)\":{\"notice\":\"Attest Merkle Root\"}},\"version\":1}},\"settings\":{\"compilationTarget\":{\"contracts/UniversalTimestamps.sol\":\"UniversalTimestamps\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"appendCBOR\":false,\"bytecodeHash\":\"none\"},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[\":@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/\",\":@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/\",\":erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/\",\":forge-std/=lib/forge-std/src/\",\":halmos-cheatcodes/=lib/openzeppelin-contracts-upgradeable/lib/halmos-cheatcodes/src/\",\":openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/\",\":openzeppelin-contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/\",\":openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/\"]},\"sources\":{\"contracts/IUniversalTimestamps.sol\":{\"keccak256\":\"0xfa9490d2704cebe76fa78d15f51ed3bf13577ffaf782fe0337db402872571df0\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://c42b87e7040e6d72f5b49a2bff481191678e15df1c19793e9c55800e8f5276cf\",\"dweb:/ipfs/QmNr4ET95fihTnh3YF6WjjKUsCFU7KkoPkvKAizb83pyCC\"]},\"contracts/UniversalTimestamps.sol\":{\"keccak256\":\"0xb1596ca55406c833dc01c604dd4d6fb3791a9f9cc0f4cad6292757abc52acb4b\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://e8c53b928701b425781b8bce6e51fc3414d33d9d32e3b192777b6c5157396bfa\",\"dweb:/ipfs/QmQHq55s3BHW4YEhBe8bRGmxFvSc6xhWF4pUub8is9oSkf\"]},\"lib/openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol\":{\"keccak256\":\"0x85c3b9bac35a90dce9ed9b31532c3739cae432359d8d7ff59cb6712f21c7ed14\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://a084d32ad4ad5b1d4494124d7695334dbeff81c2d1846a01ef1215153dd38eed\",\"dweb:/ipfs/QmbzDrfeogDd3n65mADjLuy97oAMgh2CtiUxKKEpM3WB8b\"]},\"lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol\":{\"keccak256\":\"0x30d125b8417684dbfea3e8d57284b353a86b22077237b4aaf098c0b54b153e16\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://2813775a6326190e75dfa9005c1abbdb1e541c195c0bf5656dd4199e8c66fd8d\",\"dweb:/ipfs/QmYDKANBezQXNrEDyJ69RVXkgypW1hWj7MAvjfdNHTZY8L\"]},\"lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol\":{\"keccak256\":\"0x4918e374e9ce84e9b196486bafbd46851d5e72ab315e31f0b1d7c443dcfea5bf\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://2ced247afc54a93a13922ebbd63add61130abe483ab5b5b78e7e991d564d150e\",\"dweb:/ipfs/QmTfxjcTgfekiguegjvYMyfqhyRNffui17f8xi86BCZNVt\"]},\"lib/openzeppelin-contracts-upgradeable/contracts/utils/ContextUpgradeable.sol\":{\"keccak256\":\"0xad316bdc3ee64a0e29773256245045dc57b92660799ff14f668f7c0da9456a9d\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://66463434d266816fca2a3a2734ceee88544e61b7cc3899c50333b46e8e771455\",\"dweb:/ipfs/QmPYCzHjki1HQLvBub3uUqoUKGrwdgR3xP9Zpya14YTdXS\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/interfaces/IERC1967.sol\":{\"keccak256\":\"0xbf2aefe54b76d7f7bcd4f6da1080b7b1662611937d870b880db584d09cea56b5\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://f5e7e2f12e0feec75296e57f51f82fdaa8bd1551f4b8cc6560442c0bf60f818c\",\"dweb:/ipfs/QmcW9wDMaQ8RbQibMarfp17a3bABzY5KraWe2YDwuUrUoz\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/interfaces/draft-IERC1822.sol\":{\"keccak256\":\"0x82f757819bf2429a0d4db141b99a4bbe5039e4ef86dfb94e2e6d40577ed5b28b\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://37c30ed931e19fb71fdb806bb504cfdb9913b7127545001b64d4487783374422\",\"dweb:/ipfs/QmUBHpv4hm3ZmwJ4GH8BeVzK4mv41Q6vBbWXxn8HExPXza\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol\":{\"keccak256\":\"0xa1ad192cd45317c788618bef5cb1fb3ca4ce8b230f6433ac68cc1d850fb81618\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://b43447bb85a53679d269a403c693b9d88d6c74177dfb35eddca63abaf7cf110a\",\"dweb:/ipfs/QmXSDmpd4bNZj1PDgegr6C4w1jDaWHXCconC3rYiw9TSkQ\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol\":{\"keccak256\":\"0x20462ddb2665e9521372c76b001d0ce196e59dbbd989de9af5576cad0bd5628b\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://f417fd12aeec8fbfaceaa30e3a08a0724c0bc39de363e2acf6773c897abbaf6d\",\"dweb:/ipfs/QmU4Hko6sApdweVM92CsiuLKkCk8HfyBeutF89PCTz5Tye\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/utils/Initializable.sol\":{\"keccak256\":\"0xdb4d24ee2c087c391d587cd17adfe5b3f9d93b3110b1388c2ab6c7c0ad1dcd05\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://ab7b6d5b9e2b88176312967fe0f0e78f3d9a1422fa5e4b64e2440c35869b5d08\",\"dweb:/ipfs/QmXKYWWyzcLg1B2k7Sb1qkEXgLCYfXecR9wYW5obRzWP1Q\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/utils/UUPSUpgradeable.sol\":{\"keccak256\":\"0x1a26353563a2c63b4120ea0b94727253eeff84fe2241d42c1452308b9080e66a\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://49a95e36d267828b4357186a79917002d616d8634e25d1f9818e2354cd2e7d34\",\"dweb:/ipfs/QmWDkqE4KkyLAS2UkLsRgXE1FGB1qfEgBC3zMXBVsVWfdk\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/Address.sol\":{\"keccak256\":\"0x0fa9e0d3a859900b5a46f70a03c73adf259603d5e05027a37fe0b45529d85346\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://c2add4da0240c9f2ce47649c8bb6b11b40e98cf6f88b8bdc76b2704e89391710\",\"dweb:/ipfs/QmNQTwF2uVzu4CRtNxr8bxyP9XuW6VsZuo2Nr4KR2bZr3d\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/Errors.sol\":{\"keccak256\":\"0x6afa713bfd42cf0f7656efa91201007ac465e42049d7de1d50753a373648c123\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://ba1d02f4847670a1b83dec9f7d37f0b0418d6043447b69f3a29a5f9efc547fcf\",\"dweb:/ipfs/QmQ7iH2keLNUKgq2xSWcRmuBE5eZ3F5whYAkAGzCNNoEWB\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/LowLevelCall.sol\":{\"keccak256\":\"0x5b4802a4352474792df3107e961d1cc593e47b820c14f69d3505cb28f5a6a583\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://a6f86fd01f829499fe0545ff5dda07d4521988e88bfe0bf801fc15650921ed56\",\"dweb:/ipfs/QmUUKu4ZDffHAmfkf3asuQfmLTyfpuy2Amdncc3SqfzKPG\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/SlotDerivation.sol\":{\"keccak256\":\"0x94045fd4f268edf2b2d01ef119268548c320366d6f5294ad30c1b8f9d4f5225f\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://edfda81f426f8948b3834115c21e83c48180e6db0d2a8cd2debb2185ed349337\",\"dweb:/ipfs/QmdYZneFyDAux1BuWQxLAdqtABrGS2k9WYCa7C9dvpKkWv\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/StorageSlot.sol\":{\"keccak256\":\"0xcf74f855663ce2ae00ed8352666b7935f6cddea2932fdf2c3ecd30a9b1cd0e97\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://9f660b1f351b757dfe01438e59888f31f33ded3afcf5cb5b0d9bf9aa6f320a8b\",\"dweb:/ipfs/QmarDJ5hZEgBtCmmrVzEZWjub9769eD686jmzb2XpSU1cM\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.24+commit.e11b9ed9"},"language":"Solidity","output":{"abi":[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address","name":"target","type":"address"}],"type":"error","name":"AddressEmptyCode"},{"inputs":[{"internalType":"address","name":"implementation","type":"address"}],"type":"error","name":"ERC1967InvalidImplementation"},{"inputs":[],"type":"error","name":"ERC1967NonPayable"},{"inputs":[],"type":"error","name":"FailedCall"},{"inputs":[],"type":"error","name":"InvalidInitialization"},{"inputs":[],"type":"error","name":"NotInitializing"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"type":"error","name":"OwnableInvalidOwner"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"type":"error","name":"OwnableUnauthorizedAccount"},{"inputs":[],"type":"error","name":"UUPSUnauthorizedCallContext"},{"inputs":[{"internalType":"bytes32","name":"slot","type":"bytes32"}],"type":"error","name":"UUPSUnsupportedProxiableUUID"},{"inputs":[{"internalType":"bytes32","name":"root","type":"bytes32","indexed":true},{"internalType":"address","name":"sender","type":"address","indexed":true},{"internalType":"uint256","name":"timestamp","type":"uint256","indexed":false}],"type":"event","name":"Attested","anonymous":false},{"inputs":[{"internalType":"uint64","name":"version","type":"uint64","indexed":false}],"type":"event","name":"Initialized","anonymous":false},{"inputs":[{"internalType":"address","name":"previousOwner","type":"address","indexed":true},{"internalType":"address","name":"newOwner","type":"address","indexed":true}],"type":"event","name":"OwnershipTransferred","anonymous":false},{"inputs":[{"internalType":"address","name":"implementation","type":"address","indexed":true}],"type":"event","name":"Upgraded","anonymous":false},{"inputs":[],"stateMutability":"view","type":"function","name":"UPGRADE_INTERFACE_VERSION","outputs":[{"internalType":"string","name":"","type":"string"}]},{"inputs":[{"internalType":"bytes32","name":"root","type":"bytes32"}],"stateMutability":"nonpayable","type":"function","name":"attest"},{"inputs":[{"internalType":"address","name":"initialOwner","type":"address"}],"stateMutability":"nonpayable","type":"function","name":"initialize"},{"inputs":[],"stateMutability":"view","type":"function","name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}]},{"inputs":[],"stateMutability":"view","type":"function","name":"proxiableUUID","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}]},{"inputs":[],"stateMutability":"nonpayable","type":"function","name":"renounceOwnership"},{"inputs":[{"internalType":"bytes32","name":"root","type":"bytes32"}],"stateMutability":"view","type":"function","name":"timestamp","outputs":[{"internalType":"uint256","name":"","type":"uint256"}]},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"stateMutability":"nonpayable","type":"function","name":"transferOwnership"},{"inputs":[{"internalType":"address","name":"newImplementation","type":"address"},{"internalType":"bytes","name":"data","type":"bytes"}],"stateMutability":"payable","type":"function","name":"upgradeToAndCall"}],"devdoc":{"kind":"dev","methods":{"attest(bytes32)":{"params":{"root":"The Merkle Root to be attested"}},"constructor":{"custom:oz-upgrades-unsafe-allow":"constructor"},"owner()":{"details":"Returns the address of the current owner."},"proxiableUUID()":{"details":"Implementation of the ERC-1822 {proxiableUUID} function. This returns the storage slot used by the implementation. It is used to validate the implementation's compatibility when performing an upgrade. IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this function revert if invoked through a proxy. This is guaranteed by the `notDelegated` modifier."},"renounceOwnership()":{"details":"Leaves the contract without owner. It will not be possible to call `onlyOwner` functions. Can only be called by the current owner. NOTE: Renouncing ownership will leave the contract without an owner, thereby disabling any functionality that is only available to the owner."},"transferOwnership(address)":{"details":"Transfers ownership of the contract to a new account (`newOwner`). Can only be called by the current owner."},"upgradeToAndCall(address,bytes)":{"custom:oz-upgrades-unsafe-allow-reachable":"delegatecall","details":"Upgrade the implementation of the proxy to `newImplementation`, and subsequently execute the function call encoded in `data`. Calls {_authorizeUpgrade}. Emits an {Upgraded} event."}},"version":1},"userdoc":{"kind":"user","methods":{"attest(bytes32)":{"notice":"Attest Merkle Root"}},"version":1}},"settings":{"remappings":["@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/","@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/","erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/","forge-std/=lib/forge-std/src/","halmos-cheatcodes/=lib/openzeppelin-contracts-upgradeable/lib/halmos-cheatcodes/src/","openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/","openzeppelin-contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/","openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/"],"optimizer":{"enabled":true,"runs":200},"metadata":{"bytecodeHash":"none","appendCBOR":false},"compilationTarget":{"contracts/UniversalTimestamps.sol":"UniversalTimestamps"},"evmVersion":"cancun","libraries":{}},"sources":{"contracts/IUniversalTimestamps.sol":{"keccak256":"0xfa9490d2704cebe76fa78d15f51ed3bf13577ffaf782fe0337db402872571df0","urls":["bzz-raw://c42b87e7040e6d72f5b49a2bff481191678e15df1c19793e9c55800e8f5276cf","dweb:/ipfs/QmNr4ET95fihTnh3YF6WjjKUsCFU7KkoPkvKAizb83pyCC"],"license":"MIT"},"contracts/UniversalTimestamps.sol":{"keccak256":"0xb1596ca55406c833dc01c604dd4d6fb3791a9f9cc0f4cad6292757abc52acb4b","urls":["bzz-raw://e8c53b928701b425781b8bce6e51fc3414d33d9d32e3b192777b6c5157396bfa","dweb:/ipfs/QmQHq55s3BHW4YEhBe8bRGmxFvSc6xhWF4pUub8is9oSkf"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol":{"keccak256":"0x85c3b9bac35a90dce9ed9b31532c3739cae432359d8d7ff59cb6712f21c7ed14","urls":["bzz-raw://a084d32ad4ad5b1d4494124d7695334dbeff81c2d1846a01ef1215153dd38eed","dweb:/ipfs/QmbzDrfeogDd3n65mADjLuy97oAMgh2CtiUxKKEpM3WB8b"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol":{"keccak256":"0x30d125b8417684dbfea3e8d57284b353a86b22077237b4aaf098c0b54b153e16","urls":["bzz-raw://2813775a6326190e75dfa9005c1abbdb1e541c195c0bf5656dd4199e8c66fd8d","dweb:/ipfs/QmYDKANBezQXNrEDyJ69RVXkgypW1hWj7MAvjfdNHTZY8L"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol":{"keccak256":"0x4918e374e9ce84e9b196486bafbd46851d5e72ab315e31f0b1d7c443dcfea5bf","urls":["bzz-raw://2ced247afc54a93a13922ebbd63add61130abe483ab5b5b78e7e991d564d150e","dweb:/ipfs/QmTfxjcTgfekiguegjvYMyfqhyRNffui17f8xi86BCZNVt"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/contracts/utils/ContextUpgradeable.sol":{"keccak256":"0xad316bdc3ee64a0e29773256245045dc57b92660799ff14f668f7c0da9456a9d","urls":["bzz-raw://66463434d266816fca2a3a2734ceee88544e61b7cc3899c50333b46e8e771455","dweb:/ipfs/QmPYCzHjki1HQLvBub3uUqoUKGrwdgR3xP9Zpya14YTdXS"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/interfaces/IERC1967.sol":{"keccak256":"0xbf2aefe54b76d7f7bcd4f6da1080b7b1662611937d870b880db584d09cea56b5","urls":["bzz-raw://f5e7e2f12e0feec75296e57f51f82fdaa8bd1551f4b8cc6560442c0bf60f818c","dweb:/ipfs/QmcW9wDMaQ8RbQibMarfp17a3bABzY5KraWe2YDwuUrUoz"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/interfaces/draft-IERC1822.sol":{"keccak256":"0x82f757819bf2429a0d4db141b99a4bbe5039e4ef86dfb94e2e6d40577ed5b28b","urls":["bzz-raw://37c30ed931e19fb71fdb806bb504cfdb9913b7127545001b64d4487783374422","dweb:/ipfs/QmUBHpv4hm3ZmwJ4GH8BeVzK4mv41Q6vBbWXxn8HExPXza"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol":{"keccak256":"0xa1ad192cd45317c788618bef5cb1fb3ca4ce8b230f6433ac68cc1d850fb81618","urls":["bzz-raw://b43447bb85a53679d269a403c693b9d88d6c74177dfb35eddca63abaf7cf110a","dweb:/ipfs/QmXSDmpd4bNZj1PDgegr6C4w1jDaWHXCconC3rYiw9TSkQ"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol":{"keccak256":"0x20462ddb2665e9521372c76b001d0ce196e59dbbd989de9af5576cad0bd5628b","urls":["bzz-raw://f417fd12aeec8fbfaceaa30e3a08a0724c0bc39de363e2acf6773c897abbaf6d","dweb:/ipfs/QmU4Hko6sApdweVM92CsiuLKkCk8HfyBeutF89PCTz5Tye"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/utils/Initializable.sol":{"keccak256":"0xdb4d24ee2c087c391d587cd17adfe5b3f9d93b3110b1388c2ab6c7c0ad1dcd05","urls":["bzz-raw://ab7b6d5b9e2b88176312967fe0f0e78f3d9a1422fa5e4b64e2440c35869b5d08","dweb:/ipfs/QmXKYWWyzcLg1B2k7Sb1qkEXgLCYfXecR9wYW5obRzWP1Q"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/utils/UUPSUpgradeable.sol":{"keccak256":"0x1a26353563a2c63b4120ea0b94727253eeff84fe2241d42c1452308b9080e66a","urls":["bzz-raw://49a95e36d267828b4357186a79917002d616d8634e25d1f9818e2354cd2e7d34","dweb:/ipfs/QmWDkqE4KkyLAS2UkLsRgXE1FGB1qfEgBC3zMXBVsVWfdk"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/Address.sol":{"keccak256":"0x0fa9e0d3a859900b5a46f70a03c73adf259603d5e05027a37fe0b45529d85346","urls":["bzz-raw://c2add4da0240c9f2ce47649c8bb6b11b40e98cf6f88b8bdc76b2704e89391710","dweb:/ipfs/QmNQTwF2uVzu4CRtNxr8bxyP9XuW6VsZuo2Nr4KR2bZr3d"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/Errors.sol":{"keccak256":"0x6afa713bfd42cf0f7656efa91201007ac465e42049d7de1d50753a373648c123","urls":["bzz-raw://ba1d02f4847670a1b83dec9f7d37f0b0418d6043447b69f3a29a5f9efc547fcf","dweb:/ipfs/QmQ7iH2keLNUKgq2xSWcRmuBE5eZ3F5whYAkAGzCNNoEWB"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/LowLevelCall.sol":{"keccak256":"0x5b4802a4352474792df3107e961d1cc593e47b820c14f69d3505cb28f5a6a583","urls":["bzz-raw://a6f86fd01f829499fe0545ff5dda07d4521988e88bfe0bf801fc15650921ed56","dweb:/ipfs/QmUUKu4ZDffHAmfkf3asuQfmLTyfpuy2Amdncc3SqfzKPG"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/SlotDerivation.sol":{"keccak256":"0x94045fd4f268edf2b2d01ef119268548c320366d6f5294ad30c1b8f9d4f5225f","urls":["bzz-raw://edfda81f426f8948b3834115c21e83c48180e6db0d2a8cd2debb2185ed349337","dweb:/ipfs/QmdYZneFyDAux1BuWQxLAdqtABrGS2k9WYCa7C9dvpKkWv"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/StorageSlot.sol":{"keccak256":"0xcf74f855663ce2ae00ed8352666b7935f6cddea2932fdf2c3ecd30a9b1cd0e97","urls":["bzz-raw://9f660b1f351b757dfe01438e59888f31f33ded3afcf5cb5b0d9bf9aa6f320a8b","dweb:/ipfs/QmarDJ5hZEgBtCmmrVzEZWjub9769eD686jmzb2XpSU1cM"],"license":"MIT"}},"version":1},"storageLayout":{"storage":[],"types":{}},"ast":{"absolutePath":"contracts/UniversalTimestamps.sol","id":327,"exportedSymbols":{"IUniversalTimestamps":[186],"Initializable":[41058],"OwnableUpgradeable":[40327],"SlotDerivation":[41925],"UUPSUpgradeable":[41224],"UniversalTimestamps":[326]},"nodeType":"SourceUnit","src":"33:2804:2","nodes":[{"id":188,"nodeType":"PragmaDirective","src":"33:24:2","nodes":[],"literals":["solidity","^","0.8",".24"]},{"id":190,"nodeType":"ImportDirective","src":"59:96:2","nodes":[],"absolutePath":"lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol","file":"@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol","nameLocation":"-1:-1:-1","scope":327,"sourceUnit":40332,"symbolAliases":[{"foreign":{"id":189,"name":"Initializable","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":41058,"src":"67:13:2","typeDescriptions":{}},"nameLocation":"-1:-1:-1"}],"unitAlias":""},{"id":192,"nodeType":"ImportDirective","src":"156:101:2","nodes":[],"absolutePath":"lib/openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol","file":"@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol","nameLocation":"-1:-1:-1","scope":327,"sourceUnit":40328,"symbolAliases":[{"foreign":{"id":191,"name":"OwnableUpgradeable","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":40327,"src":"164:18:2","typeDescriptions":{}},"nameLocation":"-1:-1:-1"}],"unitAlias":""},{"id":194,"nodeType":"ImportDirective","src":"258:100:2","nodes":[],"absolutePath":"lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol","file":"@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol","nameLocation":"-1:-1:-1","scope":327,"sourceUnit":40336,"symbolAliases":[{"foreign":{"id":193,"name":"UUPSUpgradeable","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":41224,"src":"266:15:2","typeDescriptions":{}},"nameLocation":"-1:-1:-1"}],"unitAlias":""},{"id":196,"nodeType":"ImportDirective","src":"359:64:2","nodes":[],"absolutePath":"contracts/IUniversalTimestamps.sol","file":"./IUniversalTimestamps.sol","nameLocation":"-1:-1:-1","scope":327,"sourceUnit":187,"symbolAliases":[{"foreign":{"id":195,"name":"IUniversalTimestamps","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":186,"src":"367:20:2","typeDescriptions":{}},"nameLocation":"-1:-1:-1"}],"unitAlias":""},{"id":198,"nodeType":"ImportDirective","src":"424:80:2","nodes":[],"absolutePath":"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/SlotDerivation.sol","file":"@openzeppelin/contracts/utils/SlotDerivation.sol","nameLocation":"-1:-1:-1","scope":327,"sourceUnit":41926,"symbolAliases":[{"foreign":{"id":197,"name":"SlotDerivation","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":41925,"src":"432:14:2","typeDescriptions":{}},"nameLocation":"-1:-1:-1"}],"unitAlias":""},{"id":326,"nodeType":"ContractDefinition","src":"1042:1794:2","nodes":[{"id":210,"nodeType":"UsingForDirective","src":"1153:32:2","nodes":[],"global":false,"libraryName":{"id":208,"name":"SlotDerivation","nameLocations":["1159:14:2"],"nodeType":"IdentifierPath","referencedDeclaration":41925,"src":"1159:14:2"},"typeName":{"id":209,"name":"string","nodeType":"ElementaryTypeName","src":"1178:6:2","typeDescriptions":{"typeIdentifier":"t_string_storage_ptr","typeString":"string"}}},{"id":213,"nodeType":"VariableDeclaration","src":"1191:70:2","nodes":[],"constant":true,"mutability":"constant","name":"_NAMESPACE","nameLocation":"1215:10:2","scope":326,"stateVariable":true,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_string_memory_ptr","typeString":"string"},"typeName":{"id":211,"name":"string","nodeType":"ElementaryTypeName","src":"1191:6:2","typeDescriptions":{"typeIdentifier":"t_string_storage_ptr","typeString":"string"}},"value":{"hexValue":"7574732e73746f726167652e556e6976657273616c54696d657374616d7073","id":212,"isConstant":false,"isLValue":false,"isPure":true,"kind":"string","lValueRequested":false,"nodeType":"Literal","src":"1228:33:2","typeDescriptions":{"typeIdentifier":"t_stringliteral_6191da0f5f254a176c2a5b8e81a37f62349600d58cdbf87518a33cdde24d517b","typeString":"literal_string \"uts.storage.UniversalTimestamps\""},"value":"uts.storage.UniversalTimestamps"},"visibility":"private"},{"id":219,"nodeType":"StructDefinition","src":"1341:89:2","nodes":[],"canonicalName":"UniversalTimestamps.UniversalTimestampsStorage","documentation":{"id":214,"nodeType":"StructuredDocumentation","src":"1268:68:2","text":"@custom:storage-location erc7201:uts.storage.UniversalTimestamps"},"members":[{"constant":false,"id":218,"mutability":"mutable","name":"timestamps","nameLocation":"1413:10:2","nodeType":"VariableDeclaration","scope":219,"src":"1385:38:2","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_mapping$_t_bytes32_$_t_uint256_$","typeString":"mapping(bytes32 => uint256)"},"typeName":{"id":217,"keyName":"","keyNameLocation":"-1:-1:-1","keyType":{"id":215,"name":"bytes32","nodeType":"ElementaryTypeName","src":"1393:7:2","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"nodeType":"Mapping","src":"1385:27:2","typeDescriptions":{"typeIdentifier":"t_mapping$_t_bytes32_$_t_uint256_$","typeString":"mapping(bytes32 => uint256)"},"valueName":"","valueNameLocation":"-1:-1:-1","valueType":{"id":216,"name":"uint256","nodeType":"ElementaryTypeName","src":"1404:7:2","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}}},"visibility":"internal"}],"name":"UniversalTimestampsStorage","nameLocation":"1348:26:2","scope":326,"visibility":"public"},{"id":227,"nodeType":"FunctionDefinition","src":"1489:53:2","nodes":[],"body":{"id":226,"nodeType":"Block","src":"1503:39:2","nodes":[],"statements":[{"expression":{"arguments":[],"expression":{"argumentTypes":[],"id":223,"name":"_disableInitializers","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":41012,"src":"1513:20:2","typeDescriptions":{"typeIdentifier":"t_function_internal_nonpayable$__$returns$__$","typeString":"function ()"}},"id":224,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"nameLocations":[],"names":[],"nodeType":"FunctionCall","src":"1513:22:2","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_tuple$__$","typeString":"tuple()"}},"id":225,"nodeType":"ExpressionStatement","src":"1513:22:2"}]},"documentation":{"id":220,"nodeType":"StructuredDocumentation","src":"1436:48:2","text":"@custom:oz-upgrades-unsafe-allow constructor"},"implemented":true,"kind":"constructor","modifiers":[],"name":"","nameLocation":"-1:-1:-1","parameters":{"id":221,"nodeType":"ParameterList","parameters":[],"src":"1500:2:2"},"returnParameters":{"id":222,"nodeType":"ParameterList","parameters":[],"src":"1503:0:2"},"scope":326,"stateMutability":"nonpayable","virtual":false,"visibility":"public"},{"id":239,"nodeType":"FunctionDefinition","src":"1548:106:2","nodes":[],"body":{"id":238,"nodeType":"Block","src":"1609:45:2","nodes":[],"statements":[{"expression":{"arguments":[{"id":235,"name":"initialOwner","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":229,"src":"1634:12:2","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}}],"expression":{"argumentTypes":[{"typeIdentifier":"t_address","typeString":"address"}],"id":234,"name":"__Ownable_init","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":40187,"src":"1619:14:2","typeDescriptions":{"typeIdentifier":"t_function_internal_nonpayable$_t_address_$returns$__$","typeString":"function (address)"}},"id":236,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"nameLocations":[],"names":[],"nodeType":"FunctionCall","src":"1619:28:2","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_tuple$__$","typeString":"tuple()"}},"id":237,"nodeType":"ExpressionStatement","src":"1619:28:2"}]},"functionSelector":"c4d66de8","implemented":true,"kind":"function","modifiers":[{"id":232,"kind":"modifierInvocation","modifierName":{"id":231,"name":"initializer","nameLocations":["1597:11:2"],"nodeType":"IdentifierPath","referencedDeclaration":40898,"src":"1597:11:2"},"nodeType":"ModifierInvocation","src":"1597:11:2"}],"name":"initialize","nameLocation":"1557:10:2","parameters":{"id":230,"nodeType":"ParameterList","parameters":[{"constant":false,"id":229,"mutability":"mutable","name":"initialOwner","nameLocation":"1576:12:2","nodeType":"VariableDeclaration","scope":239,"src":"1568:20:2","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":228,"name":"address","nodeType":"ElementaryTypeName","src":"1568:7:2","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"internal"}],"src":"1567:22:2"},"returnParameters":{"id":233,"nodeType":"ParameterList","parameters":[],"src":"1609:0:2"},"scope":326,"stateMutability":"nonpayable","virtual":false,"visibility":"public"},{"id":253,"nodeType":"FunctionDefinition","src":"1660:230:2","nodes":[],"body":{"id":252,"nodeType":"Block","src":"1762:128:2","nodes":[],"statements":[{"assignments":[246],"declarations":[{"constant":false,"id":246,"mutability":"mutable","name":"slot","nameLocation":"1780:4:2","nodeType":"VariableDeclaration","scope":252,"src":"1772:12:2","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"},"typeName":{"id":245,"name":"bytes32","nodeType":"ElementaryTypeName","src":"1772:7:2","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"visibility":"internal"}],"id":250,"initialValue":{"arguments":[],"expression":{"argumentTypes":[],"expression":{"id":247,"name":"_NAMESPACE","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":213,"src":"1787:10:2","typeDescriptions":{"typeIdentifier":"t_string_memory_ptr","typeString":"string memory"}},"id":248,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"memberLocation":"1798:11:2","memberName":"erc7201Slot","nodeType":"MemberAccess","referencedDeclaration":41808,"src":"1787:22:2","typeDescriptions":{"typeIdentifier":"t_function_internal_pure$_t_string_memory_ptr_$returns$_t_bytes32_$attached_to$_t_string_memory_ptr_$","typeString":"function (string memory) pure returns (bytes32)"}},"id":249,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"nameLocations":[],"names":[],"nodeType":"FunctionCall","src":"1787:24:2","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"nodeType":"VariableDeclarationStatement","src":"1772:39:2"},{"AST":{"nativeSrc":"1846:38:2","nodeType":"YulBlock","src":"1846:38:2","statements":[{"nativeSrc":"1860:14:2","nodeType":"YulAssignment","src":"1860:14:2","value":{"name":"slot","nativeSrc":"1870:4:2","nodeType":"YulIdentifier","src":"1870:4:2"},"variableNames":[{"name":"$.slot","nativeSrc":"1860:6:2","nodeType":"YulIdentifier","src":"1860:6:2"}]}]},"evmVersion":"cancun","externalReferences":[{"declaration":243,"isOffset":false,"isSlot":true,"src":"1860:6:2","suffix":"slot","valueSize":1},{"declaration":246,"isOffset":false,"isSlot":false,"src":"1870:4:2","valueSize":1}],"flags":["memory-safe"],"id":251,"nodeType":"InlineAssembly","src":"1821:63:2"}]},"implemented":true,"kind":"function","modifiers":[],"name":"_getUniversalTimestampsStorage","nameLocation":"1669:30:2","parameters":{"id":240,"nodeType":"ParameterList","parameters":[],"src":"1699:2:2"},"returnParameters":{"id":244,"nodeType":"ParameterList","parameters":[{"constant":false,"id":243,"mutability":"mutable","name":"$","nameLocation":"1759:1:2","nodeType":"VariableDeclaration","scope":253,"src":"1724:36:2","stateVariable":false,"storageLocation":"storage","typeDescriptions":{"typeIdentifier":"t_struct$_UniversalTimestampsStorage_$219_storage_ptr","typeString":"struct UniversalTimestamps.UniversalTimestampsStorage"},"typeName":{"id":242,"nodeType":"UserDefinedTypeName","pathNode":{"id":241,"name":"UniversalTimestampsStorage","nameLocations":["1724:26:2"],"nodeType":"IdentifierPath","referencedDeclaration":219,"src":"1724:26:2"},"referencedDeclaration":219,"src":"1724:26:2","typeDescriptions":{"typeIdentifier":"t_struct$_UniversalTimestampsStorage_$219_storage_ptr","typeString":"struct UniversalTimestamps.UniversalTimestampsStorage"}},"visibility":"internal"}],"src":"1723:38:2"},"scope":326,"stateMutability":"pure","virtual":false,"visibility":"private"},{"id":267,"nodeType":"FunctionDefinition","src":"1896:138:2","nodes":[],"body":{"id":266,"nodeType":"Block","src":"1961:73:2","nodes":[],"statements":[{"expression":{"baseExpression":{"expression":{"arguments":[],"expression":{"argumentTypes":[],"id":260,"name":"_getUniversalTimestampsStorage","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":253,"src":"1978:30:2","typeDescriptions":{"typeIdentifier":"t_function_internal_pure$__$returns$_t_struct$_UniversalTimestampsStorage_$219_storage_ptr_$","typeString":"function () pure returns (struct UniversalTimestamps.UniversalTimestampsStorage storage pointer)"}},"id":261,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"nameLocations":[],"names":[],"nodeType":"FunctionCall","src":"1978:32:2","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_struct$_UniversalTimestampsStorage_$219_storage_ptr","typeString":"struct UniversalTimestamps.UniversalTimestampsStorage storage pointer"}},"id":262,"isConstant":false,"isLValue":true,"isPure":false,"lValueRequested":false,"memberLocation":"2011:10:2","memberName":"timestamps","nodeType":"MemberAccess","referencedDeclaration":218,"src":"1978:43:2","typeDescriptions":{"typeIdentifier":"t_mapping$_t_bytes32_$_t_uint256_$","typeString":"mapping(bytes32 => uint256)"}},"id":264,"indexExpression":{"id":263,"name":"root","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":255,"src":"2022:4:2","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"isConstant":false,"isLValue":true,"isPure":false,"lValueRequested":false,"nodeType":"IndexAccess","src":"1978:49:2","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"functionReturnParameters":259,"id":265,"nodeType":"Return","src":"1971:56:2"}]},"baseFunctions":[185],"functionSelector":"4d003070","implemented":true,"kind":"function","modifiers":[],"name":"timestamp","nameLocation":"1905:9:2","parameters":{"id":256,"nodeType":"ParameterList","parameters":[{"constant":false,"id":255,"mutability":"mutable","name":"root","nameLocation":"1923:4:2","nodeType":"VariableDeclaration","scope":267,"src":"1915:12:2","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"},"typeName":{"id":254,"name":"bytes32","nodeType":"ElementaryTypeName","src":"1915:7:2","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"visibility":"internal"}],"src":"1914:14:2"},"returnParameters":{"id":259,"nodeType":"ParameterList","parameters":[{"constant":false,"id":258,"mutability":"mutable","name":"","nameLocation":"-1:-1:-1","nodeType":"VariableDeclaration","scope":267,"src":"1952:7:2","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"},"typeName":{"id":257,"name":"uint256","nodeType":"ElementaryTypeName","src":"1952:7:2","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"visibility":"internal"}],"src":"1951:9:2"},"scope":326,"stateMutability":"view","virtual":false,"visibility":"external"},{"id":315,"nodeType":"FunctionDefinition","src":"2140:354:2","nodes":[],"body":{"id":314,"nodeType":"Block","src":"2179:315:2","nodes":[],"statements":[{"expression":{"arguments":[{"commonType":{"typeIdentifier":"t_bytes32","typeString":"bytes32"},"id":279,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"leftExpression":{"id":274,"name":"root","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":270,"src":"2197:4:2","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"nodeType":"BinaryOperation","operator":"!=","rightExpression":{"arguments":[{"hexValue":"30","id":277,"isConstant":false,"isLValue":false,"isPure":true,"kind":"number","lValueRequested":false,"nodeType":"Literal","src":"2213:1:2","typeDescriptions":{"typeIdentifier":"t_rational_0_by_1","typeString":"int_const 0"},"value":"0"}],"expression":{"argumentTypes":[{"typeIdentifier":"t_rational_0_by_1","typeString":"int_const 0"}],"id":276,"isConstant":false,"isLValue":false,"isPure":true,"lValueRequested":false,"nodeType":"ElementaryTypeNameExpression","src":"2205:7:2","typeDescriptions":{"typeIdentifier":"t_type$_t_bytes32_$","typeString":"type(bytes32)"},"typeName":{"id":275,"name":"bytes32","nodeType":"ElementaryTypeName","src":"2205:7:2","typeDescriptions":{}}},"id":278,"isConstant":false,"isLValue":false,"isPure":true,"kind":"typeConversion","lValueRequested":false,"nameLocations":[],"names":[],"nodeType":"FunctionCall","src":"2205:10:2","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"src":"2197:18:2","typeDescriptions":{"typeIdentifier":"t_bool","typeString":"bool"}},{"hexValue":"5554533a20526f6f742063616e6e6f74206265207a65726f","id":280,"isConstant":false,"isLValue":false,"isPure":true,"kind":"string","lValueRequested":false,"nodeType":"Literal","src":"2217:26:2","typeDescriptions":{"typeIdentifier":"t_stringliteral_2804b13209a936ca289456f44fff96ae78a8d5be97dfafdb6227532f3504fdd2","typeString":"literal_string \"UTS: Root cannot be zero\""},"value":"UTS: Root cannot be zero"}],"expression":{"argumentTypes":[{"typeIdentifier":"t_bool","typeString":"bool"},{"typeIdentifier":"t_stringliteral_2804b13209a936ca289456f44fff96ae78a8d5be97dfafdb6227532f3504fdd2","typeString":"literal_string \"UTS: Root cannot be zero\""}],"id":273,"name":"require","nodeType":"Identifier","overloadedDeclarations":[-18,-18],"referencedDeclaration":-18,"src":"2189:7:2","typeDescriptions":{"typeIdentifier":"t_function_require_pure$_t_bool_$_t_string_memory_ptr_$returns$__$","typeString":"function (bool,string memory) pure"}},"id":281,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"nameLocations":[],"names":[],"nodeType":"FunctionCall","src":"2189:55:2","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_tuple$__$","typeString":"tuple()"}},"id":282,"nodeType":"ExpressionStatement","src":"2189:55:2"},{"assignments":[285],"declarations":[{"constant":false,"id":285,"mutability":"mutable","name":"$","nameLocation":"2290:1:2","nodeType":"VariableDeclaration","scope":314,"src":"2255:36:2","stateVariable":false,"storageLocation":"storage","typeDescriptions":{"typeIdentifier":"t_struct$_UniversalTimestampsStorage_$219_storage_ptr","typeString":"struct UniversalTimestamps.UniversalTimestampsStorage"},"typeName":{"id":284,"nodeType":"UserDefinedTypeName","pathNode":{"id":283,"name":"UniversalTimestampsStorage","nameLocations":["2255:26:2"],"nodeType":"IdentifierPath","referencedDeclaration":219,"src":"2255:26:2"},"referencedDeclaration":219,"src":"2255:26:2","typeDescriptions":{"typeIdentifier":"t_struct$_UniversalTimestampsStorage_$219_storage_ptr","typeString":"struct UniversalTimestamps.UniversalTimestampsStorage"}},"visibility":"internal"}],"id":288,"initialValue":{"arguments":[],"expression":{"argumentTypes":[],"id":286,"name":"_getUniversalTimestampsStorage","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":253,"src":"2294:30:2","typeDescriptions":{"typeIdentifier":"t_function_internal_pure$__$returns$_t_struct$_UniversalTimestampsStorage_$219_storage_ptr_$","typeString":"function () pure returns (struct UniversalTimestamps.UniversalTimestampsStorage storage pointer)"}},"id":287,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"nameLocations":[],"names":[],"nodeType":"FunctionCall","src":"2294:32:2","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_struct$_UniversalTimestampsStorage_$219_storage_ptr","typeString":"struct UniversalTimestamps.UniversalTimestampsStorage storage pointer"}},"nodeType":"VariableDeclarationStatement","src":"2255:71:2"},{"condition":{"commonType":{"typeIdentifier":"t_uint256","typeString":"uint256"},"id":294,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"leftExpression":{"baseExpression":{"expression":{"id":289,"name":"$","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":285,"src":"2340:1:2","typeDescriptions":{"typeIdentifier":"t_struct$_UniversalTimestampsStorage_$219_storage_ptr","typeString":"struct UniversalTimestamps.UniversalTimestampsStorage storage pointer"}},"id":290,"isConstant":false,"isLValue":true,"isPure":false,"lValueRequested":false,"memberLocation":"2342:10:2","memberName":"timestamps","nodeType":"MemberAccess","referencedDeclaration":218,"src":"2340:12:2","typeDescriptions":{"typeIdentifier":"t_mapping$_t_bytes32_$_t_uint256_$","typeString":"mapping(bytes32 => uint256)"}},"id":292,"indexExpression":{"id":291,"name":"root","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":270,"src":"2353:4:2","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"isConstant":false,"isLValue":true,"isPure":false,"lValueRequested":false,"nodeType":"IndexAccess","src":"2340:18:2","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"nodeType":"BinaryOperation","operator":"==","rightExpression":{"hexValue":"30","id":293,"isConstant":false,"isLValue":false,"isPure":true,"kind":"number","lValueRequested":false,"nodeType":"Literal","src":"2362:1:2","typeDescriptions":{"typeIdentifier":"t_rational_0_by_1","typeString":"int_const 0"},"value":"0"},"src":"2340:23:2","typeDescriptions":{"typeIdentifier":"t_bool","typeString":"bool"}},"id":313,"nodeType":"IfStatement","src":"2336:152:2","trueBody":{"id":312,"nodeType":"Block","src":"2365:123:2","statements":[{"expression":{"id":302,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"leftHandSide":{"baseExpression":{"expression":{"id":295,"name":"$","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":285,"src":"2379:1:2","typeDescriptions":{"typeIdentifier":"t_struct$_UniversalTimestampsStorage_$219_storage_ptr","typeString":"struct UniversalTimestamps.UniversalTimestampsStorage storage pointer"}},"id":298,"isConstant":false,"isLValue":true,"isPure":false,"lValueRequested":false,"memberLocation":"2381:10:2","memberName":"timestamps","nodeType":"MemberAccess","referencedDeclaration":218,"src":"2379:12:2","typeDescriptions":{"typeIdentifier":"t_mapping$_t_bytes32_$_t_uint256_$","typeString":"mapping(bytes32 => uint256)"}},"id":299,"indexExpression":{"id":297,"name":"root","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":270,"src":"2392:4:2","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"isConstant":false,"isLValue":true,"isPure":false,"lValueRequested":true,"nodeType":"IndexAccess","src":"2379:18:2","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"nodeType":"Assignment","operator":"=","rightHandSide":{"expression":{"id":300,"name":"block","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":-4,"src":"2400:5:2","typeDescriptions":{"typeIdentifier":"t_magic_block","typeString":"block"}},"id":301,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"memberLocation":"2406:9:2","memberName":"timestamp","nodeType":"MemberAccess","src":"2400:15:2","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"src":"2379:36:2","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"id":303,"nodeType":"ExpressionStatement","src":"2379:36:2"},{"eventCall":{"arguments":[{"id":305,"name":"root","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":270,"src":"2443:4:2","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},{"expression":{"id":306,"name":"msg","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":-15,"src":"2449:3:2","typeDescriptions":{"typeIdentifier":"t_magic_message","typeString":"msg"}},"id":307,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"memberLocation":"2453:6:2","memberName":"sender","nodeType":"MemberAccess","src":"2449:10:2","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},{"expression":{"id":308,"name":"block","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":-4,"src":"2461:5:2","typeDescriptions":{"typeIdentifier":"t_magic_block","typeString":"block"}},"id":309,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"memberLocation":"2467:9:2","memberName":"timestamp","nodeType":"MemberAccess","src":"2461:15:2","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}}],"expression":{"argumentTypes":[{"typeIdentifier":"t_bytes32","typeString":"bytes32"},{"typeIdentifier":"t_address","typeString":"address"},{"typeIdentifier":"t_uint256","typeString":"uint256"}],"id":304,"name":"Attested","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":173,"src":"2434:8:2","typeDescriptions":{"typeIdentifier":"t_function_event_nonpayable$_t_bytes32_$_t_address_$_t_uint256_$returns$__$","typeString":"function (bytes32,address,uint256)"}},"id":310,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"nameLocations":[],"names":[],"nodeType":"FunctionCall","src":"2434:43:2","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_tuple$__$","typeString":"tuple()"}},"id":311,"nodeType":"EmitStatement","src":"2429:48:2"}]}}]},"baseFunctions":[178],"documentation":{"id":268,"nodeType":"StructuredDocumentation","src":"2040:95:2","text":" @notice Attest Merkle Root\n @param root The Merkle Root to be attested"},"functionSelector":"23c3617f","implemented":true,"kind":"function","modifiers":[],"name":"attest","nameLocation":"2149:6:2","parameters":{"id":271,"nodeType":"ParameterList","parameters":[{"constant":false,"id":270,"mutability":"mutable","name":"root","nameLocation":"2164:4:2","nodeType":"VariableDeclaration","scope":315,"src":"2156:12:2","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"},"typeName":{"id":269,"name":"bytes32","nodeType":"ElementaryTypeName","src":"2156:7:2","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"visibility":"internal"}],"src":"2155:14:2"},"returnParameters":{"id":272,"nodeType":"ParameterList","parameters":[],"src":"2179:0:2"},"scope":326,"stateMutability":"nonpayable","virtual":false,"visibility":"external"},{"id":325,"nodeType":"FunctionDefinition","src":"2750:84:2","nodes":[],"body":{"id":324,"nodeType":"Block","src":"2832:2:2","nodes":[],"statements":[]},"baseFunctions":[41178],"documentation":{"id":316,"nodeType":"StructuredDocumentation","src":"2500:245:2","text":" @dev Authorizes an upgrade to `newImplementation`.\n This function is restricted to the contract owner via the {onlyOwner} modifier,\n ensuring that only the owner can authorize upgrades to the implementation."},"implemented":true,"kind":"function","modifiers":[{"id":322,"kind":"modifierInvocation","modifierName":{"id":321,"name":"onlyOwner","nameLocations":["2822:9:2"],"nodeType":"IdentifierPath","referencedDeclaration":40222,"src":"2822:9:2"},"nodeType":"ModifierInvocation","src":"2822:9:2"}],"name":"_authorizeUpgrade","nameLocation":"2759:17:2","overrides":{"id":320,"nodeType":"OverrideSpecifier","overrides":[],"src":"2813:8:2"},"parameters":{"id":319,"nodeType":"ParameterList","parameters":[{"constant":false,"id":318,"mutability":"mutable","name":"newImplementation","nameLocation":"2785:17:2","nodeType":"VariableDeclaration","scope":325,"src":"2777:25:2","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":317,"name":"address","nodeType":"ElementaryTypeName","src":"2777:7:2","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"internal"}],"src":"2776:27:2"},"returnParameters":{"id":323,"nodeType":"ParameterList","parameters":[],"src":"2832:0:2"},"scope":326,"stateMutability":"nonpayable","virtual":false,"visibility":"internal"}],"abstract":false,"baseContracts":[{"baseName":{"id":200,"name":"Initializable","nameLocations":["1074:13:2"],"nodeType":"IdentifierPath","referencedDeclaration":41058,"src":"1074:13:2"},"id":201,"nodeType":"InheritanceSpecifier","src":"1074:13:2"},{"baseName":{"id":202,"name":"OwnableUpgradeable","nameLocations":["1089:18:2"],"nodeType":"IdentifierPath","referencedDeclaration":40327,"src":"1089:18:2"},"id":203,"nodeType":"InheritanceSpecifier","src":"1089:18:2"},{"baseName":{"id":204,"name":"UUPSUpgradeable","nameLocations":["1109:15:2"],"nodeType":"IdentifierPath","referencedDeclaration":41224,"src":"1109:15:2"},"id":205,"nodeType":"InheritanceSpecifier","src":"1109:15:2"},{"baseName":{"id":206,"name":"IUniversalTimestamps","nameLocations":["1126:20:2"],"nodeType":"IdentifierPath","referencedDeclaration":186,"src":"1126:20:2"},"id":207,"nodeType":"InheritanceSpecifier","src":"1126:20:2"}],"canonicalName":"UniversalTimestamps","contractDependencies":[],"contractKind":"contract","documentation":{"id":199,"nodeType":"StructuredDocumentation","src":"506:535:2","text":" @title UniversalTimestamps\n @dev Records and exposes timestamps for attested Merkle roots using ERC-7201\n namespaced storage (`uts.storage.UniversalTimestamps`) derived via\n {SlotDerivation}, and is implemented as a UUPS upgradeable contract via\n OpenZeppelin's Initializable, OwnableUpgradeable, and UUPSUpgradeable\n base contracts. Storage is kept in a dedicated namespaced struct to remain\n layout-compatible across upgrades, while upgrades are authorized by the\n contract owner through {_authorizeUpgrade}."},"fullyImplemented":true,"linearizedBaseContracts":[326,186,41224,40412,40327,40381,41058],"name":"UniversalTimestamps","nameLocation":"1051:19:2","scope":327,"usedErrors":[40163,40168,40470,40483,40807,40810,41081,41086,41236,41627],"usedEvents":[173,40174,40389,40815]}],"license":"MIT"},"id":2} \ No newline at end of file diff --git a/crates/contracts/src/lib.rs b/crates/contracts/src/lib.rs new file mode 100644 index 0000000..f955e44 --- /dev/null +++ b/crates/contracts/src/lib.rs @@ -0,0 +1,107 @@ +//! Solidity contracts for UTS + +/// UniversalTimestamps contract +pub mod uts { + use alloy_primitives::{Address, address}; + + #[doc(hidden)] + pub mod binding { + use alloy_sol_types::sol; + + sol!( + #[sol(rpc, all_derives)] + IUniversalTimestamps, + "abi/IUniversalTimestamps.json" + ); + sol!( + #[sol(rpc)] + UniversalTimestamps, + "abi/UniversalTimestamps.json" + ); + } + + pub use binding::IUniversalTimestamps::{ + Attested, IUniversalTimestampsInstance as UniversalTimestamps, + }; + + pub use binding::UniversalTimestamps::{BYTECODE, DEPLOYED_BYTECODE, deploy, deploy_builder}; + + /// Default address for the UniversalTimestamps contract. + pub const DEFAULT_ADDRESS: Address = address!("0xceB7a9E77bd00D0391349B9bC989167cAB5e35e7"); + + #[cfg(test)] + mod tests { + use super::*; + use crate::erc1967::ERC1967ProxyInstance; + use alloy::{ + network::EthereumWallet, + primitives::{B256, Bytes, U256, b256}, + providers::ProviderBuilder, + signers::local::MnemonicBuilder, + }; + use futures::StreamExt; + use std::env; + + const ROOT: B256 = + b256!("5cd5c6763b9f2b3fb1cd66a15fe92b7ac913eec295d9927886e175f144ce3308"); + + #[tokio::test] + async fn test() -> eyre::Result<()> { + let provider = ProviderBuilder::new().connect_anvil_with_wallet(); + let imp = deploy(&provider).await?; + let proxy = + ERC1967ProxyInstance::deploy(&provider, *imp.address(), Bytes::new()).await?; + let uts = UniversalTimestamps::new(*proxy.address(), &provider); + + let attested_log = uts.Attested_filter().watch().await?; + + let _ = uts.attest(ROOT).send().await?.watch().await?; + + let timestamp = uts.timestamp(ROOT).call().await?; + assert_ne!(timestamp, U256::ZERO); + + let (attested, _log) = attested_log.into_stream().next().await.unwrap()?; + assert_eq!(attested.root, ROOT); + + Ok(()) + } + + #[tokio::test] + #[ignore] + async fn deploy_to_sepolia() -> eyre::Result<()> { + let signer = MnemonicBuilder::from_phrase(env::var("MNEMONIC")?.as_str()) + .index(0u32)? + .build()?; + + let provider = ProviderBuilder::new() + .wallet(EthereumWallet::new(signer)) + .connect("https://0xrpc.io/sep") + .await?; + + let imp = deploy(&provider).await?; + println!("Implementation deployed at: {:?}", imp.address()); + + let proxy = + ERC1967ProxyInstance::deploy(&provider, *imp.address(), Bytes::new()).await?; + println!("Proxy deployed at: {:?}", proxy.address()); + + Ok(()) + } + } +} + +/// ERC-1967 Proxy contract +#[cfg(any(test, feature = "erc1967"))] +pub mod erc1967 { + mod binding { + use alloy_sol_types::sol; + + sol!( + #[sol(rpc)] + ERC1967Proxy, + "abi/ERC1967Proxy.json" + ); + } + + pub use binding::ERC1967Proxy::*; +} diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index c6fba23..1d3bb43 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -9,30 +9,41 @@ name = "uts-core" repository.workspace = true version.workspace = true -[[bin]] -name = "uts-info" -path = "src/bin/uts_info.rs" - [dependencies] +alloy-chains = { workspace = true } +alloy-primitives = { workspace = true } +alloy-provider = { workspace = true, optional = true } +alloy-rpc-types-eth = { workspace = true, optional = true } +alloy-sol-types = { workspace = true, optional = true } auto_impl.workspace = true +bytes = { workspace = true, optional = true } digest.workspace = true hex.workspace = true +once_cell = { workspace = true, features = ["alloc"] } paste.workspace = true ripemd.workspace = true +serde = { workspace = true, optional = true } +serde_with = { workspace = true, optional = true } sha1.workspace = true sha2.workspace = true sha3.workspace = true -smallvec = { workspace = true, features = ["write"] } thiserror.workspace = true +tokio = { workspace = true, optional = true } tracing = { workspace = true, optional = true } +uts-bmt = { workspace = true } +uts-contracts = { workspace = true, optional = true } [features] -nightly = [ - "smallvec/specialization", - "smallvec/may_dangle", -] +bytes = ["dep:bytes"] +default = ["std"] +ethereum-uts-verifier = ["verifier", "dep:alloy-provider", "dep:alloy-rpc-types-eth", "dep:alloy-sol-types", "dep:uts-contracts"] +io-utils = ["dep:tokio", "tokio/fs"] +serde = ["dep:serde", "dep:serde_with", "serde/derive", "serde_with/hex"] +std = [] tracing = ["dep:tracing"] +verifier = [] [dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } opentimestamps = { git = "https://github.com/opentimestamps/rust-opentimestamps" } +serde_json.workspace = true diff --git a/crates/core/src/bin/uts_info.rs b/crates/core/src/bin/uts_info.rs deleted file mode 100644 index 837c1a9..0000000 --- a/crates/core/src/bin/uts_info.rs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (C) The OpenTimestamps developers -// Copyright (C) The ots-rs developers -// SPDX-License-Identifier: MIT OR Apache-2.0 - -//! # OpenTimestamps Viewer -//! -//! Simple application to open an OTS info file and dump its contents -//! to stdout in a human-readable format - -use std::{ - env, fs, io, - io::{BufReader, Seek}, - process, -}; -use uts_core::codec::{ - Decode, VersionedProof, - v1::{DetachedTimestamp, Timestamp}, -}; - -fn main() { - let args: Vec = env::args().collect(); - if args.len() != 2 { - println!("Usage: {} ", args[0]); - process::exit(1); - } - - let mut fh = match fs::File::open(&args[1]) { - Ok(fh) => BufReader::new(fh), - Err(e) => { - println!("Failed to open {}: {}", args[1], e); - process::exit(1); - } - }; - - match VersionedProof::::decode(&mut fh) { - Ok(ots) => { - println!("OTS Detached Timestamp found:"); - println!("{ots}"); - } - Err(e) => { - println!( - "Not a valid Detached Timestamp OTS file (trying raw timestamp): {}\n", - e - ); - } - }; - - fh.seek(io::SeekFrom::Start(0)).unwrap(); - - match Timestamp::decode(fh) { - Ok(ots) => { - println!("Raw Timestamp found:"); - println!("{ots}"); - } - Err(e) => { - println!("Failed to parse {}: {}", args[1], e); - process::exit(1); - } - } -} diff --git a/crates/core/src/codec.rs b/crates/core/src/codec.rs index e51cc05..acc83ed 100644 --- a/crates/core/src/codec.rs +++ b/crates/core/src/codec.rs @@ -1,14 +1,14 @@ use crate::error::{DecodeError, EncodeError}; +use alloc::alloc::Global; use auto_impl::auto_impl; -use std::{ - io::{BufRead, Write}, - ops::RangeBounds, -}; +use core::{alloc::Allocator, ops::RangeBounds}; mod proof; pub use proof::{Proof, Version, VersionedProof}; -mod primitives; +mod imp; +#[cfg(feature = "std")] +pub use imp::{Reader, Writer}; /// Types and helpers for the version 1 serialization format. pub mod v1; @@ -17,15 +17,13 @@ pub mod v1; pub const MAGIC: &[u8; 31] = b"\x00OpenTimestamps\x00\x00Proof\x00\xbf\x89\xe2\xe8\x84\xe8\x92\x94"; /// Helper trait for writing OpenTimestamps primitives to a byte stream. -pub trait Encoder: Write { +pub trait Encoder: Sized { /// Encodes a single byte to the writer. - fn encode_byte(&mut self, byte: u8) -> Result<(), EncodeError> { - self.write_all(&[byte])?; - Ok(()) - } + fn encode_byte(&mut self, byte: u8) -> Result<(), EncodeError>; /// Encodes a byte slice prefixed with its length. - fn encode_bytes(&mut self, bytes: &[u8]) -> Result<(), EncodeError> { + fn encode_bytes(&mut self, bytes: impl AsRef<[u8]>) -> Result<(), EncodeError> { + let bytes = bytes.as_ref(); self.encode(bytes.len())?; self.write_all(bytes)?; Ok(()) @@ -33,27 +31,22 @@ pub trait Encoder: Write { /// Writes the OpenTimestamps magic sequence to the stream. fn encode_magic(&mut self) -> Result<(), EncodeError> { - self.write_all(MAGIC)?; - Ok(()) + self.write_all(&MAGIC[..]) } /// Encodes a value implementing the [`Encode`] trait. - #[inline] fn encode(&mut self, value: impl Encode) -> Result<(), EncodeError> { value.encode(self) } -} -impl Encoder for W {} + // --- no_std feature compatibility --- + fn write_all(&mut self, buf: impl AsRef<[u8]>) -> Result<(), EncodeError>; +} /// Helper trait for reading OpenTimestamps primitives from a byte stream. -pub trait Decoder: BufRead { +pub trait Decoder: Sized { /// Decodes a single byte from the reader. - fn decode_byte(&mut self) -> Result { - let mut byte = [0]; - self.read_exact(&mut byte)?; - Ok(byte[0]) - } + fn decode_byte(&mut self) -> Result; /// Decodes a value and ensures it falls within the supplied range. fn decode_ranged( @@ -80,26 +73,76 @@ pub trait Decoder: BufRead { } /// Decodes a value implementing the [`Decode`] trait. - #[inline] fn decode(&mut self) -> Result { T::decode(self) } -} -impl Decoder for R {} + /// Decodes a trailing optional value implementing the [`Decode`] trait. + /// + /// See [`Decode::decode_trailing`] for details and caveats. + fn decode_trailing(&mut self) -> Result, DecodeError> { + T::decode_trailing(self) + } -/// Marker trait for types supporting both [`Encode`] and [`Decode`]. -pub trait Codec: Encode + Decode {} + /// Decodes a value implementing the [`Decode`] trait. + fn decode_in, A: Allocator>(&mut self, alloc: A) -> Result { + T::decode_in(self, alloc) + } -impl Codec for T {} + // --- no_std feature compatibility --- + fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), DecodeError>; +} /// Serializes a value into an OpenTimestamps-compatible byte stream. #[auto_impl(&, &mut, Box, Rc, Arc)] pub trait Encode { - fn encode(&self, writer: impl Encoder) -> Result<(), EncodeError>; + fn encode(&self, writer: &mut impl Encoder) -> Result<(), EncodeError>; } /// Deserializes a value from an OpenTimestamps-compatible byte stream. pub trait Decode: Sized { - fn decode(reader: impl Decoder) -> Result; + fn decode(decoder: &mut impl Decoder) -> Result; + + /// Decodes a trailing optional value implementing the [`Decode`] trait. + /// + /// This treats any `UnexpectedEof` error as an indication that the value is absent, returning `Ok(None)`. + /// + /// If the implementor returns `UnexpectedEof` for any reason other than the absence of the value, + /// it should also override this method to avoid masking the error as `Ok(None)`. + fn decode_trailing(decoder: &mut impl Decoder) -> Result, DecodeError> { + match Self::decode(decoder) { + Ok(value) => Ok(Some(value)), + Err(DecodeError::UnexpectedEof) => Ok(None), + Err(e) => Err(e), + } + } +} + +/// Deserializes a value from an OpenTimestamps-compatible byte stream. +pub trait DecodeIn: Sized { + /// See [`Decode::decode`] for details. + fn decode_in(decoder: &mut impl Decoder, alloc: A) -> Result; + + /// See [`Decode::decode_trailing`] for details and caveats. + fn decode_trailing(decoder: &mut impl Decoder, alloc: A) -> Result, DecodeError> { + match Self::decode_in(decoder, alloc) { + Ok(value) => Ok(Some(value)), + Err(DecodeError::UnexpectedEof) => Ok(None), + Err(e) => Err(e), + } + } +} + +impl> Decode for T { + fn decode(decoder: &mut impl Decoder) -> Result { + T::decode_in(decoder, Global) + } + + fn decode_trailing(decoder: &mut impl Decoder) -> Result, DecodeError> { + match Self::decode_in(decoder, Global) { + Ok(value) => Ok(Some(value)), + Err(DecodeError::UnexpectedEof) => Ok(None), + Err(e) => Err(e), + } + } } diff --git a/crates/core/src/codec/imp.rs b/crates/core/src/codec/imp.rs new file mode 100644 index 0000000..d393fd9 --- /dev/null +++ b/crates/core/src/codec/imp.rs @@ -0,0 +1,44 @@ +use crate::codec::*; +use alloc::vec::Vec; + +mod alloy; +#[cfg(feature = "bytes")] +mod bytes; +mod primitives; +#[cfg(feature = "std")] +mod std_io; + +#[cfg(feature = "std")] +pub use std_io::{Reader, Writer}; + +impl Encoder for Vec { + fn encode_byte(&mut self, byte: u8) -> Result<(), EncodeError> { + self.push(byte); + Ok(()) + } + + fn write_all(&mut self, buf: impl AsRef<[u8]>) -> Result<(), EncodeError> { + self.extend_from_slice(buf.as_ref()); + Ok(()) + } +} + +impl Decoder for &[u8] { + fn decode_byte(&mut self) -> Result { + let Some((a, b)) = self.split_at_checked(1) else { + return Err(DecodeError::UnexpectedEof); + }; + *self = b; + Ok(a[0]) + } + + fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), DecodeError> { + let len = buf.len(); + let Some((a, b)) = self.split_at_checked(len) else { + return Err(DecodeError::UnexpectedEof); + }; + buf.copy_from_slice(a); + *self = b; + Ok(()) + } +} diff --git a/crates/core/src/codec/imp/alloy.rs b/crates/core/src/codec/imp/alloy.rs new file mode 100644 index 0000000..e91d6b5 --- /dev/null +++ b/crates/core/src/codec/imp/alloy.rs @@ -0,0 +1,43 @@ +use crate::codec::{Decode, DecodeError, Decoder, Encode, EncodeError, Encoder}; +use alloy_chains::Chain; +use alloy_primitives::{Address, ChainId, FixedBytes}; + +impl Encode for FixedBytes { + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { + encoder.write_all(self) + } +} + +impl Decode for FixedBytes { + fn decode(decoder: &mut impl Decoder) -> Result { + let mut buf = [0u8; N]; + decoder.read_exact(&mut buf)?; + Ok(Self::new(buf)) + } +} + +impl Encode for Address { + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { + encoder.write_all(self.0) + } +} + +impl Decode for Address { + fn decode(decoder: &mut impl Decoder) -> Result { + let inner: FixedBytes<20> = decoder.decode()?; + Ok(Self::from(inner)) + } +} + +impl Encode for Chain { + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { + self.id().encode(encoder) + } +} + +impl Decode for Chain { + fn decode(decoder: &mut impl Decoder) -> Result { + let id: ChainId = decoder.decode()?; + Ok(Chain::from_id(id)) + } +} diff --git a/crates/core/src/codec/imp/bytes.rs b/crates/core/src/codec/imp/bytes.rs new file mode 100644 index 0000000..f4d0158 --- /dev/null +++ b/crates/core/src/codec/imp/bytes.rs @@ -0,0 +1,14 @@ +use crate::codec::*; +use bytes::{BufMut, BytesMut}; + +impl Encoder for BytesMut { + fn encode_byte(&mut self, byte: u8) -> Result<(), EncodeError> { + self.put_u8(byte); + Ok(()) + } + + fn write_all(&mut self, buf: impl AsRef<[u8]>) -> Result<(), EncodeError> { + self.put_slice(buf.as_ref()); + Ok(()) + } +} diff --git a/crates/core/src/codec/primitives.rs b/crates/core/src/codec/imp/primitives.rs similarity index 69% rename from crates/core/src/codec/primitives.rs rename to crates/core/src/codec/imp/primitives.rs index 8655876..afd129d 100644 --- a/crates/core/src/codec/primitives.rs +++ b/crates/core/src/codec/imp/primitives.rs @@ -1,14 +1,11 @@ -use crate::{ - codec::{Decode, Encode}, - error::EncodeError, -}; +use crate::codec::*; macro_rules! leb128 { ($ty:ty) => { paste::paste! { - impl super::Encode for $ty { + impl crate::codec::Encode for $ty { #[inline] - fn encode(&self, mut encoder: impl crate::codec::Encoder) -> Result<(), $crate::error::EncodeError> { + fn encode(&self, encoder: &mut impl crate::codec::Encoder) -> Result<(), $crate::error::EncodeError> { let mut n = *self; let mut buf = [0u8; <$ty>::BITS.div_ceil(7) as usize]; let mut i = 0; @@ -34,9 +31,9 @@ macro_rules! leb128 { } } - impl super::Decode for $ty { + impl crate::codec::DecodeIn for $ty { #[inline] - fn decode(mut decoder: impl crate::codec::Decoder) -> Result { + fn decode_in(decoder: &mut impl crate::codec::Decoder, _alloc: A) -> Result { let mut ret: $ty = 0; let mut shift: u32 = 0; @@ -69,15 +66,15 @@ leb128!(u16, u32, u64, u128); impl Encode for u8 { #[inline] - fn encode(&self, mut encoder: impl crate::codec::Encoder) -> Result<(), EncodeError> { - encoder.write_all(&[*self])?; + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { + encoder.write_all([*self])?; Ok(()) } } -impl Decode for u8 { +impl DecodeIn for u8 { #[inline] - fn decode(mut decoder: impl crate::codec::Decoder) -> Result { + fn decode_in(decoder: &mut impl Decoder, _alloc: A) -> Result { let mut byte = [0u8; 1]; decoder.read_exact(&mut byte)?; Ok(byte[0]) @@ -86,17 +83,17 @@ impl Decode for u8 { impl Encode for usize { #[inline] - fn encode(&self, mut encoder: impl crate::codec::Encoder) -> Result<(), EncodeError> { + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { let val: u32 = (*self).try_into().map_err(|_| EncodeError::UsizeOverflow)?; - val.encode(&mut encoder)?; + val.encode(encoder)?; Ok(()) } } -impl Decode for usize { +impl DecodeIn for usize { #[inline] - fn decode(mut decoder: impl crate::codec::Decoder) -> Result { - let val = u32::decode(&mut decoder)?; + fn decode_in(decoder: &mut impl Decoder, _alloc: A) -> Result { + let val = u32::decode(decoder)?; Ok(val as usize) } } diff --git a/crates/core/src/codec/imp/std_io.rs b/crates/core/src/codec/imp/std_io.rs new file mode 100644 index 0000000..2c10093 --- /dev/null +++ b/crates/core/src/codec/imp/std_io.rs @@ -0,0 +1,35 @@ +use crate::codec::*; +use std::{ + io::{Read, Write}, + slice, +}; + +pub struct Writer(pub W); +pub struct Reader(pub R); + +impl Encoder for Writer { + fn encode_byte(&mut self, byte: u8) -> Result<(), EncodeError> { + self.write_all(slice::from_ref(&byte))?; + Ok(()) + } + + #[inline] + fn write_all(&mut self, buf: impl AsRef<[u8]>) -> Result<(), EncodeError> { + self.0.write_all(buf.as_ref())?; + Ok(()) + } +} + +impl Decoder for Reader { + fn decode_byte(&mut self) -> Result { + let mut byte = [0]; + self.read_exact(&mut byte)?; + Ok(byte[0]) + } + + #[inline] + fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), DecodeError> { + self.0.read_exact(buf)?; + Ok(()) + } +} diff --git a/crates/core/src/codec/proof.rs b/crates/core/src/codec/proof.rs index 179f2a6..ac0ec6a 100644 --- a/crates/core/src/codec/proof.rs +++ b/crates/core/src/codec/proof.rs @@ -1,44 +1,77 @@ use crate::{ - codec::{Codec, Decode, Decoder, Encode, Encoder}, + codec::{DecodeIn, Decoder, Encode, Encoder}, error::{DecodeError, EncodeError}, }; -use std::fmt; +use alloc::alloc::{Allocator, Global}; +use core::fmt; /// Version number of the serialization format. pub type Version = u32; /// Trait implemented by proof payloads for a specific serialization version. -pub trait Proof: Codec { +pub trait Proof: Encode + DecodeIn { /// Version identifier that must match the encoded proof. const VERSION: Version; } /// Wrapper that prefixes a proof with its version and magic bytes. #[derive(Clone, PartialEq, Eq, Debug)] -#[repr(transparent)] -pub struct VersionedProof(pub T); +pub struct VersionedProof, A: Allocator = Global> { + pub proof: T, + allocator: A, +} + +impl> VersionedProof { + /// Creates a new versioned proof with the global allocator. + pub fn new(proof: T) -> Self { + VersionedProof { + proof, + allocator: Global, + } + } +} + +impl, A: Allocator> VersionedProof { + /// Creates a new versioned proof with the specified allocator. + pub fn new_with_allocator(proof: T, allocator: A) -> Self { + VersionedProof { proof, allocator } + } + + /// Returns a reference to the proof payload. + pub fn proof(&self) -> &T { + &self.proof + } + + /// Returns a reference to the allocator used by this proof. + pub fn allocator(&self) -> &A { + &self.allocator + } +} -impl Decode for VersionedProof { - fn decode(mut reader: impl Decoder) -> Result { - reader.assert_magic()?; - let version: Version = reader.decode()?; +impl, A: Allocator + Clone> DecodeIn for VersionedProof { + fn decode_in(decoder: &mut impl Decoder, alloc: A) -> Result { + decoder.assert_magic()?; + let version: Version = decoder.decode()?; if version != T::VERSION { return Err(DecodeError::BadVersion); } - Ok(VersionedProof(T::decode(&mut reader)?)) + Ok(VersionedProof { + proof: T::decode_in(decoder, alloc.clone())?, + allocator: alloc, + }) } } -impl Encode for VersionedProof { - fn encode(&self, mut writer: impl Encoder) -> Result<(), EncodeError> { - writer.encode_magic()?; - writer.encode(T::VERSION)?; - self.0.encode(writer) +impl, A: Allocator> Encode for VersionedProof { + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { + encoder.encode_magic()?; + encoder.encode(T::VERSION)?; + self.proof.encode(encoder) } } -impl fmt::Display for VersionedProof { +impl + fmt::Display, A: Allocator> fmt::Display for VersionedProof { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Version {} Proof {}", T::VERSION, self.0) + write!(f, "Version {} Proof {}", T::VERSION, self.proof) } } diff --git a/crates/core/src/codec/v1.rs b/crates/core/src/codec/v1.rs index aa0b6b6..9091bee 100644 --- a/crates/core/src/codec/v1.rs +++ b/crates/core/src/codec/v1.rs @@ -6,7 +6,70 @@ mod digest; pub mod opcode; mod timestamp; -pub use attestation::{Attestation, AttestationTag}; +pub use attestation::{ + Attestation, AttestationTag, BitcoinAttestation, EthereumUTSAttestation, PendingAttestation, + RawAttestation, +}; pub use detached_timestamp::DetachedTimestamp; pub use digest::DigestHeader; -pub use timestamp::Timestamp; +pub use timestamp::{Step, Timestamp, builder::TimestampBuilder}; + +/// Error indicating that finalization of a timestamp failed due to conflicting inputs. +#[derive(Debug)] +pub struct FinalizationError; + +impl core::fmt::Display for FinalizationError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "failed to finalize timestamp due to conflicting inputs") + } +} + +#[cfg(feature = "std")] +impl std::error::Error for FinalizationError {} + +/// Trait for objects that may have input data. +pub trait MayHaveInput { + /// Returns the input data for this object, if finalized. + fn input(&self) -> Option<&[u8]>; +} + +trait ToInput { + fn to_input(&self) -> Option<&[u8]>; +} +impl ToInput for T { + fn to_input(&self) -> Option<&[u8]> { + self.input() + } +} +impl ToInput for [u8] { + fn to_input(&self) -> Option<&[u8]> { + Some(self) + } +} +impl ToInput for Vec { + fn to_input(&self) -> Option<&[u8]> { + Some(self) + } +} + +/// Trait for objects that can be checked for consistency with another object. +#[allow(private_bounds)] +pub trait ConsistentWith: MayHaveInput { + /// Checks if self is consistent with the given input. + /// + /// Note: Returns true if any of the inputs is not set. + fn is_consistent_with(&self, other: &T) -> bool { + self.input() + .zip(other.to_input()) + .map_or(true, |(a, b)| a == b) + } + + /// Checks if self is consistent with the given input. + /// + /// Note: Returns false if xor of the inputs is not set. + fn is_consistent_with_strict(&self, other: &T) -> bool { + self.input() == other.to_input() + } +} + +impl ConsistentWith for T {} diff --git a/crates/core/src/codec/v1/attestation.rs b/crates/core/src/codec/v1/attestation.rs index 85d313c..c1f336e 100644 --- a/crates/core/src/codec/v1/attestation.rs +++ b/crates/core/src/codec/v1/attestation.rs @@ -1,122 +1,387 @@ -// Copyright (C) The OpenTimestamps developers -// Copyright (C) The ots-rs developers -// SPDX-License-Identifier: MIT OR Apache-2.0 - //! # Attestations //! //! An attestation is a claim that some data existed at some time. It //! comes from some server or from a blockchain. use crate::{ - codec::{Decoder, Encoder}, + codec::{Decode, DecodeIn, Decoder, Encode, Encoder, v1::MayHaveInput}, error::{DecodeError, EncodeError}, - utils::Hexed, + utils::{Hexed, OnceLock}, }; -use smallvec::SmallVec; -use std::{ - fmt, - io::{BufRead, Write}, +use alloc::{ + alloc::{Allocator, Global}, + borrow::Cow, + vec::Vec, }; +use alloy_chains::Chain; +use alloy_primitives::{Address, BlockNumber, ChainId, TxHash}; +use core::fmt; /// Size in bytes of the tag identifying the attestation type. const TAG_SIZE: usize = 8; -/// Maximum length of a URI in a "pending" attestation. -const MAX_URI_LEN: usize = 1000; /// Tag indicating a Bitcoin attestation. const BITCOIN_TAG: &[u8; 8] = b"\x05\x88\x96\x0d\x73\xd7\x19\x01"; /// Tag indicating a pending attestation. const PENDING_TAG: &[u8; 8] = b"\x83\xdf\xe3\x0d\x2e\xf9\x0c\x8e"; +/// Tag indicating an Ethereum UTS contract attestation. +/// +/// TAG = keccak256("EthereumUTSAttestation")[:8] +const ETHEREUM_UTS_TAG: &[u8; 8] = b"\xea\xf2\xbc\x69\x3c\x93\x25\x1c"; /// Tag identifying the attestation kind. pub type AttestationTag = [u8; TAG_SIZE]; -/// Proof that some data existed at a given time. -#[derive(Clone, PartialEq, Eq, Debug)] -pub enum Attestation { - /// Attestation derived from a Bitcoin block header. - /// - /// This consists solely of a block height and asserts that the - /// current hash matches the Merkle root of the block at that height. - Bitcoin { height: u32 }, - /// Attestation delivered by an OpenTimestamps calendar server. - /// - /// Only a restricted URI is stored locally so that the server can be - /// queried later for the full proof material. - Pending { uri: String }, - /// Opaque attestation stored verbatim. - Unknown { tag: AttestationTag, data: Vec }, -} - -impl Attestation { - /// Decodes an attestation payload from the reader. - pub fn decode(mut reader: R) -> Result { +/// Raw Proof that some data existed at a given time. +#[derive(Clone)] +pub struct RawAttestation { + pub tag: AttestationTag, + pub data: Vec, + /// Cached value for verifying the attestation. + pub(crate) value: OnceLock>, +} + +impl fmt::Debug for RawAttestation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RawAttestation") + .field("tag", &Hexed(&self.tag)) + .field("data", &Hexed(&self.data)) + .finish() + } +} + +impl DecodeIn for RawAttestation { + fn decode_in(decoder: &mut impl Decoder, alloc: A) -> Result { let mut tag = [0u8; TAG_SIZE]; - reader.read_exact(&mut tag)?; - let len = reader.decode()?; - - if tag == *BITCOIN_TAG { - let height = reader.decode()?; - Ok(Attestation::Bitcoin { height }) - } else if tag == *PENDING_TAG { - // This validation logic copied from python-opentimestamps. Peter comments - // that he is deliberately avoiding ?, &, @, etc., to "keep us out of trouble" - let length = reader.decode_ranged(0..=MAX_URI_LEN)?; - let mut uri_bytes = Vec::with_capacity(len); - uri_bytes.resize(length, 0); - reader.read_exact(&mut uri_bytes)?; - let uri_string = - String::from_utf8(uri_bytes).map_err(|_| DecodeError::InvalidUriChar)?; - if !uri_string.chars().all( - |ch| matches!(ch, 'a'..='z' | 'A'..='Z' | '0'..='9' | '.' | '-' | '_' | '/' | ':'), - ) { - return Err(DecodeError::InvalidUriChar); + decoder.read_exact(&mut tag)?; + + let len = decoder.decode()?; + let mut data = Vec::with_capacity_in(len, alloc); + data.resize(len, 0); + decoder.read_exact(&mut data)?; + + Ok(RawAttestation { + tag, + data, + value: OnceLock::new(), + }) + } +} + +impl Encode for RawAttestation { + #[inline] + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { + encoder.write_all(self.tag)?; + encoder.encode_bytes(&self.data) + } +} + +impl PartialEq for RawAttestation { + fn eq(&self, other: &Self) -> bool { + self.tag == other.tag && self.data.as_slice() == other.data.as_slice() + } +} + +impl Eq for RawAttestation {} + +impl RawAttestation { + /// Returns the allocator used by this raw attestation. + #[inline] + pub fn allocator(&self) -> &A { + self.data.allocator() + } + + /// Returns the cached value for verifying the attestation, if it exists. + #[inline] + pub fn value(&self) -> Option<&[u8]> { + self.value.get().map(|v| v.as_slice()) + } +} + +pub trait Attestation<'a>: Sized { + const TAG: AttestationTag; + + fn from_raw(raw: &'a RawAttestation) -> Result { + if raw.tag != Self::TAG { + return Err(DecodeError::BadAttestationTag); + } + + Self::from_raw_data(&raw.data) + } + + fn to_raw(&self) -> Result { + self.to_raw_in(Global) + } + + fn to_raw_in(&self, alloc: A) -> Result, EncodeError> { + Ok(RawAttestation { + tag: Self::TAG, + data: self.to_raw_data_in(alloc)?, + value: OnceLock::new(), + }) + } + + fn from_raw_data(data: &'a [u8]) -> Result; + fn to_raw_data_in(&self, alloc: A) -> Result, EncodeError>; +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BitcoinAttestation { + pub height: u32, +} + +impl fmt::Display for BitcoinAttestation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Bitcoin at height {}", self.height) + } +} + +impl Attestation<'_> for BitcoinAttestation { + const TAG: AttestationTag = *BITCOIN_TAG; + + fn from_raw_data(data: &[u8]) -> Result { + let height = u32::decode(&mut &*data)?; + Ok(BitcoinAttestation { height }) + } + + fn to_raw_data_in(&self, alloc: A) -> Result, EncodeError> { + let mut buffer = Vec::with_capacity_in(u32::BITS.div_ceil(7) as usize, alloc); + buffer.encode(self.height)?; + Ok(buffer) + } +} + +/// Attestation by an Ethereum UTS contract. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EthereumUTSAttestation { + pub chain: Chain, + pub height: BlockNumber, + /// Optional extra metadata about the attestation, such as the contract address and transaction hash. + pub metadata: EthereumUTSAttestationExtraMetadata, +} + +/// Extra metadata for an Ethereum UTS attestation. +/// +/// The tx field is only present if the contract field is present, +/// and should be ignored if the contract field is None. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct EthereumUTSAttestationExtraMetadata { + contract: Option
, + tx: Option, +} + +impl EthereumUTSAttestation { + /// Creates a new Ethereum UTS attestation with the given chain id, block number, and extra metadata. + pub fn new( + chain_id: ChainId, + height: BlockNumber, + metadata: EthereumUTSAttestationExtraMetadata, + ) -> Self { + Self { + chain: Chain::from_id(chain_id), + height, + metadata, + } + } +} + +impl fmt::Display for EthereumUTSAttestation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "UTS on chain {} at block #{}({})", + self.chain, self.height, self.metadata + ) + } +} + +impl Attestation<'_> for EthereumUTSAttestation { + const TAG: AttestationTag = *ETHEREUM_UTS_TAG; + + fn from_raw_data(data: &[u8]) -> Result { + let data = &mut &data[..]; + let chain = Chain::decode(data)?; + let height = BlockNumber::decode(data)?; + let metadata = EthereumUTSAttestationExtraMetadata::decode(data)?; + Ok(EthereumUTSAttestation { + chain, + height, + metadata, + }) + } + + fn to_raw_data_in(&self, alloc: A) -> Result, EncodeError> { + // chain id + block number + optional address + optional tx hash + const SIZE: usize = size_of::() + size_of::() + 20 + 32; + let mut buffer = Vec::with_capacity_in(SIZE, alloc); + buffer.encode(self.chain)?; + buffer.encode(self.height)?; + buffer.encode(&self.metadata)?; + Ok(buffer) + } +} + +impl Encode for EthereumUTSAttestationExtraMetadata { + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { + if let Some(contract) = self.contract { + encoder.encode(&contract)?; + if let Some(tx) = self.tx { + encoder.encode(&tx)?; } + } + Ok(()) + } +} - Ok(Attestation::Pending { uri: uri_string }) +impl Decode for EthereumUTSAttestationExtraMetadata { + fn decode(decoder: &mut impl Decoder) -> Result { + let contract = Address::decode_trailing(decoder)?; + let tx = if contract.is_some() { + TxHash::decode_trailing(decoder)? } else { - let mut data = Vec::with_capacity(len); - data.resize(len, 0); - reader.read_exact(&mut data)?; + None + }; + Ok(Self { contract, tx }) + } +} + +impl fmt::Display for EthereumUTSAttestationExtraMetadata { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match (self.contract, self.tx) { + (Some(contract), Some(tx)) => write!(f, "{contract} by tx: {tx}"), + (Some(contract), None) => write!(f, "{contract}"), + (None, Some(_)) => unreachable!("Tx should not be present without contract"), + (None, None) => write!(f, "no extra metadata"), + } + } +} + +impl EthereumUTSAttestationExtraMetadata { + /// Creates new extra metadata with the given contract address and no transaction hash. + pub fn new(contract: Address) -> Self { + Self { + contract: Some(contract), + tx: None, + } + } + + /// Creates new extra metadata with the given contract address and transaction hash. + pub fn new_with_tx(contract: Address, tx: TxHash) -> Self { + Self { + contract: Some(contract), + tx: Some(tx), + } + } + + /// Returns the contract address if present, or None if not. + #[inline] + pub fn contract(&self) -> Option
{ + self.contract + } - Ok(Attestation::Unknown { tag, data }) + /// Returns the transaction hash if present, or None if not. + #[inline] + pub fn tx(&self) -> Option { + self.tx + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PendingAttestation<'a> { + pub uri: Cow<'a, str>, +} + +impl PendingAttestation<'_> { + /// Maximum length of a URI in a "pending" attestation. + pub const MAX_URI_LEN: usize = 1000; + + #[inline] + pub fn validate_uri(uri: &str) -> bool { + uri.chars() + .all(|ch| matches!(ch, 'a'..='z' | 'A'..='Z' | '0'..='9' | '.' | '-' | '_' | '/' | ':')) + } +} + +impl fmt::Display for PendingAttestation<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Pending at {}", self.uri) + } +} + +impl<'a> Attestation<'a> for PendingAttestation<'a> { + const TAG: AttestationTag = *PENDING_TAG; + + fn from_raw_data(data: &'a [u8]) -> Result { + let data = &mut &data[..]; + let length = u32::decode(data)? as usize; // length prefix + if length > Self::MAX_URI_LEN { + return Err(DecodeError::UriTooLong); } + if data.len() < length { + return Err(DecodeError::UnexpectedEof); + } + let uri = core::str::from_utf8(&data[..length]).map_err(|_| DecodeError::InvalidUriChar)?; + if !Self::validate_uri(uri) { + return Err(DecodeError::InvalidUriChar); + } + Ok(PendingAttestation { + uri: Cow::Borrowed(uri), + }) } - /// Encodes the attestation to the writer. - pub fn encode(&self, mut writer: W) -> Result<(), EncodeError> { - match *self { - Attestation::Bitcoin { height } => { - writer.write_all(BITCOIN_TAG)?; - let mut buffer = SmallVec::<[u8; u32::BITS.div_ceil(7) as usize]>::new(); - buffer.encode(height)?; - writer.encode_bytes(&buffer) + fn to_raw_data_in(&self, alloc: A) -> Result, EncodeError> { + if self.uri.len() > Self::MAX_URI_LEN { + return Err(EncodeError::UriTooLong); + } + if !Self::validate_uri(&self.uri) { + return Err(EncodeError::InvalidUriChar); + } + let mut buffer = + Vec::with_capacity_in(self.uri.len() + u32::BITS.div_ceil(7) as usize, alloc); + buffer.encode_bytes(self.uri.as_bytes())?; + Ok(buffer) + } +} + +impl fmt::Display for RawAttestation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.tag { + tag if *tag == *PENDING_TAG => { + let att = PendingAttestation::from_raw(self).expect("Valid Pending attestation"); + write!(f, "{}", att) } - Attestation::Pending { ref uri } => { - writer.write_all(PENDING_TAG)?; - let mut buffer = Vec::new(); - buffer.encode_bytes(uri.as_bytes())?; - writer.encode_bytes(&buffer) + tag if *tag == *BITCOIN_TAG => { + let att = BitcoinAttestation::from_raw(self).expect("Valid Bitcoin attestation"); + write!(f, "{}", att) } - Attestation::Unknown { ref tag, ref data } => { - writer.write_all(tag)?; - writer.encode_bytes(data) + tag if *tag == *ETHEREUM_UTS_TAG => { + let att = + EthereumUTSAttestation::from_raw(self).expect("Valid Ethereum UTS attestation"); + write!(f, "{}", att) } + _ => write!(f, "Unknown Attestation with tag {}", Hexed(&self.tag)), } } } -impl fmt::Display for Attestation { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Attestation::Bitcoin { height } => write!(f, "Bitcoin block {}", height), - Attestation::Pending { ref uri } => write!(f, "Pending: update URI {}", uri), - Attestation::Unknown { ref tag, ref data } => write!( - f, - "unknown attestation type {}: {}", - Hexed(tag), - Hexed(data) - ), - } +impl MayHaveInput for RawAttestation { + #[inline] + fn input(&self) -> Option<&[u8]> { + self.value.get().map(|v| v.as_slice()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ethereum_uts_tag() { + use sha3::{Digest, Keccak256}; + + let mut hasher = Keccak256::new(); + hasher.update(b"EthereumUTSAttestation"); + let result = hasher.finalize().to_vec(); + assert_eq!(&result[..8], ETHEREUM_UTS_TAG); } } diff --git a/crates/core/src/codec/v1/detached_timestamp.rs b/crates/core/src/codec/v1/detached_timestamp.rs index 627a1f7..353ac91 100644 --- a/crates/core/src/codec/v1/detached_timestamp.rs +++ b/crates/core/src/codec/v1/detached_timestamp.rs @@ -1,9 +1,10 @@ use crate::codec::{ - Decode, Encode, Proof, Version, - v1::{DigestHeader, Timestamp, timestamp}, + Decode, DecodeIn, Encode, Encoder, Proof, Version, + v1::{DigestHeader, FinalizationError, Timestamp}, }; -use smallvec::ToSmallVec; -use std::{fmt, fmt::Formatter}; +use alloc::alloc::{Allocator, Global}; +use core::{fmt, fmt::Formatter}; +use std::ops::{Deref, DerefMut}; /// A file containing a timestamp for another file /// Contains a timestamp, along with a header and the digest of the file. @@ -12,43 +13,118 @@ use std::{fmt, fmt::Formatter}; /// which don't encode/decode the magic and version. /// The Python version is equivalent to `VersionedProof`. #[derive(Clone, PartialEq, Eq, Debug)] -pub struct DetachedTimestamp { +pub struct DetachedTimestamp { header: DigestHeader, - timestamp: Timestamp, + timestamp: Timestamp, } -impl Proof for DetachedTimestamp { +impl Proof for DetachedTimestamp { const VERSION: Version = 1; } -impl Decode for DetachedTimestamp { - fn decode(mut reader: impl crate::codec::Decoder) -> Result { - let header = DigestHeader::decode(&mut reader)?; - let timestamp = Timestamp::decode(&mut reader)?; - Ok(DetachedTimestamp { header, timestamp }) +impl DecodeIn for DetachedTimestamp { + fn decode_in( + decoder: &mut impl crate::codec::Decoder, + alloc: A, + ) -> Result { + let header = DigestHeader::decode(decoder)?; + let timestamp = Timestamp::decode_in(decoder, alloc)?; + let detached = DetachedTimestamp { header, timestamp }; + detached.finalize(); + Ok(detached) } } -impl Encode for DetachedTimestamp { - fn encode( - &self, - mut writer: impl crate::codec::Encoder, - ) -> Result<(), crate::error::EncodeError> { - self.header.encode(&mut writer)?; - self.timestamp.encode(&mut writer)?; +impl Encode for DetachedTimestamp { + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), crate::error::EncodeError> { + self.header.encode(encoder)?; + self.timestamp.encode(encoder)?; Ok(()) } } -impl fmt::Display for DetachedTimestamp { +impl fmt::Display for DetachedTimestamp { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { writeln!(f, "digest of {}", self.header)?; - timestamp::fmt::fmt( - &self.timestamp, - Some(&self.header.digest().to_smallvec()), - f, - ) + self.timestamp.fmt(Some(self.header.digest()), f) + } +} + +impl DetachedTimestamp { + /// Returns the digest header. + pub fn header(&self) -> &DigestHeader { + &self.header + } + + /// Returns the timestamp. + pub fn timestamp(&self) -> &Timestamp { + &self.timestamp + } + + /// Returns the allocator used by this detached timestamp. + #[inline] + pub fn allocator(&self) -> &A { + self.timestamp.allocator() + } + + /// Consumes the detached timestamp and returns its parts. + pub fn into_parts(self) -> (DigestHeader, Timestamp) { + (self.header, self.timestamp) + } +} + +impl DetachedTimestamp { + /// Creates a new detached timestamp from the given header and timestamp. + /// + /// # Panics + /// + /// Panics if the timestamp cannot be finalized with the given header's digest. + pub fn from_parts(header: DigestHeader, timestamp: Timestamp) -> Self { + Self::try_from_parts(header, timestamp) + .expect("conflicting inputs when finalizing detached timestamp") + } + + /// Creates a new detached timestamp from the given header and timestamp. + /// + /// Returns an error if the timestamp cannot be finalized with the given header's digest. + pub fn try_from_parts( + header: DigestHeader, + timestamp: Timestamp, + ) -> Result { + timestamp.try_finalize(header.digest())?; + Ok(DetachedTimestamp { header, timestamp }) + } + + /// Finalize the detached timestamp's timestamp with the header's digest. + /// + /// # Panics + /// + /// Panics if the timestamp cannot be finalized. + pub fn finalize(&self) { + self.try_finalize() + .expect("conflicting inputs when finalizing detached timestamp"); + } + + /// Tries to finalize the detached timestamp's timestamp with the header's digest. + /// + /// Returns an error if the timestamp cannot be finalized. + pub fn try_finalize(&self) -> Result<(), FinalizationError> { + self.timestamp.try_finalize(self.header.digest()) + } +} + +impl Deref for DetachedTimestamp { + type Target = Timestamp; + + fn deref(&self) -> &Self::Target { + &self.timestamp + } +} + +impl DerefMut for DetachedTimestamp { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.timestamp } } @@ -56,7 +132,7 @@ impl fmt::Display for DetachedTimestamp { mod tests { use super::*; use crate::{ - codec::{Decode, Encode, proof::VersionedProof}, + codec::{Decode, Encoder, proof::VersionedProof}, fixtures, }; @@ -65,14 +141,18 @@ mod tests { let mut encoded_small = vec![]; let mut encoded_large = vec![]; - let ots = VersionedProof::::decode(fixtures::SMALL_DETACHED_TIMESTAMP); - assert!(ots.is_ok()); - assert!(ots.unwrap().encode(&mut encoded_small).is_ok()); + let ots = + VersionedProof::::decode(&mut &*fixtures::SMALL_DETACHED_TIMESTAMP) + .unwrap(); + println!("{:#?}", ots); + println!("{}", ots); + assert!(encoded_small.encode(&ots).is_ok()); assert_eq!(encoded_small, fixtures::SMALL_DETACHED_TIMESTAMP); - let ots = VersionedProof::::decode(fixtures::LARGE_DETACHED_TIMESTAMP); - assert!(ots.is_ok()); - assert!(ots.unwrap().encode(&mut encoded_large).is_ok()); + let ots = + VersionedProof::::decode(&mut &*fixtures::LARGE_DETACHED_TIMESTAMP) + .unwrap(); + assert!(encoded_large.encode(&ots).is_ok()); assert_eq!(encoded_large, fixtures::LARGE_DETACHED_TIMESTAMP); } } diff --git a/crates/core/src/codec/v1/digest.rs b/crates/core/src/codec/v1/digest.rs index 8ba71ff..93a6118 100644 --- a/crates/core/src/codec/v1/digest.rs +++ b/crates/core/src/codec/v1/digest.rs @@ -1,29 +1,54 @@ use crate::{ - codec::{Decode, Decoder, Encode, Encoder, v1::opcode::DigestOp}, + codec::{ + DecodeIn, Decoder, Encode, Encoder, + v1::opcode::{DigestOp, DigestOpExt}, + }, error::{DecodeError, EncodeError}, utils::Hexed, }; -use std::fmt; +use alloc::alloc::Allocator; +use core::fmt; +use digest::{Output, typenum::Unsigned}; /// Header describing the digest that anchors a timestamp. -#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + feature = "serde", + serde_with::serde_as, + derive(serde::Serialize, serde::Deserialize) +)] pub struct DigestHeader { - kind: DigestOp, - digest: [u8; 32], + pub(crate) kind: DigestOp, + #[cfg_attr(feature = "serde", serde_as(as = "serde_with::hex::Hex"))] + pub(crate) digest: [u8; 32], +} + +impl fmt::Debug for DigestHeader { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DigestHeader") + .field("kind", &self.kind) + .field("digest", &Hexed(self.digest())) + .finish() + } } impl fmt::Display for DigestHeader { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} {}", - self.kind, - Hexed(&self.digest[..self.kind.output_size()]) - ) + write!(f, "{} {}", self.kind, Hexed(self.digest())) } } impl DigestHeader { + /// Creates a new digest header from the given digest output. + pub fn new(digest: Output) -> Self { + let mut digest_bytes = [0u8; 32]; + digest_bytes[..D::OutputSize::USIZE].copy_from_slice(&digest); + DigestHeader { + kind: D::OPCODE, + digest: digest_bytes, + } + } + /// Returns the digest opcode recorded in the header. pub fn kind(&self) -> DigestOp { self.kind @@ -38,20 +63,20 @@ impl DigestHeader { impl Encode for DigestHeader { #[cfg_attr(feature = "tracing", tracing::instrument(skip(writer), err))] #[inline] - fn encode(&self, mut writer: impl Encoder) -> Result<(), EncodeError> { - writer.encode(&self.kind)?; - writer.write_all(&self.digest[..self.kind.output_size()])?; + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { + encoder.encode(self.kind)?; + encoder.write_all(&self.digest[..self.kind.output_size()])?; Ok(()) } } -impl Decode for DigestHeader { +impl DecodeIn for DigestHeader { #[cfg_attr(feature = "tracing", tracing::instrument(skip(reader), ret, err))] #[inline] - fn decode(mut reader: impl Decoder) -> Result { - let kind = reader.decode()?; + fn decode_in(decoder: &mut impl Decoder, _alloc: A) -> Result { + let kind = decoder.decode()?; let mut digest = [0u8; 32]; - reader.read_exact(&mut digest)?; + decoder.read_exact(&mut digest)?; Ok(DigestHeader { kind, digest }) } diff --git a/crates/core/src/codec/v1/opcode.rs b/crates/core/src/codec/v1/opcode.rs index ed8d2d5..237bd34 100644 --- a/crates/core/src/codec/v1/opcode.rs +++ b/crates/core/src/codec/v1/opcode.rs @@ -3,26 +3,34 @@ //! It contains opcode information and utilities to work with opcodes. use crate::{ - codec::{Decode, Decoder, Encode, Encoder}, + codec::{Decode, DecodeIn, Decoder, Encode, Encoder}, error::{DecodeError, EncodeError}, }; -use digest::{Digest, OutputSizeUser, typenum::Unsigned}; +use alloc::{alloc::Allocator, vec::Vec}; +use core::{fmt, hint::unreachable_unchecked}; +use digest::Digest; use ripemd::Ripemd160; use sha1::Sha1; use sha2::Sha256; use sha3::Keccak256; -use smallvec::ToSmallVec; -use std::{fmt, hint::unreachable_unchecked}; - -pub(crate) type OperationBuffer = smallvec::SmallVec<[u8; 64]>; /// An OpenTimestamps opcode. /// /// This is always a valid opcode. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + feature = "serde", + derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr) +)] #[repr(transparent)] pub struct OpCode(u8); +impl fmt::Debug for OpCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.name()) + } +} + impl fmt::Display for OpCode { /// Formats the opcode as a string. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -32,15 +40,15 @@ impl fmt::Display for OpCode { impl Encode for OpCode { #[inline] - fn encode(&self, mut writer: impl Encoder) -> Result<(), EncodeError> { - writer.encode_byte(self.tag()) + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { + encoder.encode_byte(self.tag()) } } impl Decode for OpCode { #[inline] - fn decode(mut reader: impl Decoder) -> Result { - let byte = reader.decode_byte()?; + fn decode(decoder: &mut impl Decoder) -> Result { + let byte = decoder.decode_byte()?; OpCode::new(byte).ok_or(DecodeError::BadOpCode(byte)) } } @@ -55,19 +63,13 @@ impl OpCode { /// Returns `true` when the opcode requires an immediate operand. #[inline] pub const fn has_immediate(&self) -> bool { - match *self { - Self::APPEND | Self::PREPEND => true, - _ => false, - } + matches!(*self, Self::APPEND | Self::PREPEND) } /// Returns `true` for control opcodes. #[inline] pub const fn is_control(&self) -> bool { - match *self { - Self::ATTESTATION | Self::FORK => true, - _ => false, - } + matches!(*self, Self::ATTESTATION | Self::FORK) } /// Returns `true` for digest opcodes. @@ -91,26 +93,45 @@ impl OpCode { /// /// Panics if the opcode is a control opcode. #[inline] - pub fn execute(&self, input: impl AsRef<[u8]>, immediate: impl AsRef<[u8]>) -> OperationBuffer { + pub fn execute(&self, input: impl AsRef<[u8]>, immediate: impl AsRef<[u8]>) -> Vec { + self.execute_in(input, immediate, alloc::alloc::Global) + } + + /// Executes the opcode on the given input data, with an optional immediate value. + /// + /// # Panics + /// + /// Panics if the opcode is a control opcode. + #[inline] + pub fn execute_in( + &self, + input: impl AsRef<[u8]>, + immediate: impl AsRef<[u8]>, + alloc: A, + ) -> Vec { if let Some(digest_op) = self.as_digest() { - return digest_op.execute(input); + return digest_op.execute_in(input, alloc); } let input = input.as_ref(); match *self { Self::APPEND => { - let mut out = OperationBuffer::from_slice(input); - out.extend_from_slice(immediate.as_ref()); + let immediate = immediate.as_ref(); + let mut out = Vec::with_capacity_in(input.len() + immediate.len(), alloc); + out.extend_from_slice(input); + out.extend_from_slice(immediate); out } Self::PREPEND => { - let mut out = OperationBuffer::from_slice(immediate.as_ref()); + let immediate = immediate.as_ref(); + let mut out = Vec::with_capacity_in(input.len() + immediate.len(), alloc); + out.extend_from_slice(immediate); out.extend_from_slice(input); out } Self::REVERSE => { let len = input.len(); - let mut out = OperationBuffer::with_capacity(len); + let mut out = Vec::::with_capacity_in(len, alloc); unsafe { // SAFETY: The vector capacity is set to len, so setting the length to len is valid. @@ -128,7 +149,7 @@ impl OpCode { } Self::HEXLIFY => { let hex_len = input.len() * 2; - let mut out = OperationBuffer::with_capacity(hex_len); + let mut out = Vec::::with_capacity_in(hex_len, alloc); // SAFETY: that the vector is actually the specified size. unsafe { out.set_len(hex_len); @@ -153,10 +174,17 @@ impl PartialEq for OpCode { /// An OpenTimestamps digest opcode. /// /// This is always a valid opcode. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(transparent)] pub struct DigestOp(OpCode); +impl fmt::Debug for DigestOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.0.name()) + } +} + impl fmt::Display for DigestOp { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) @@ -177,15 +205,15 @@ impl PartialEq for DigestOp { impl Encode for DigestOp { #[inline] - fn encode(&self, writer: impl Encoder) -> Result<(), EncodeError> { - self.0.encode(writer) + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { + self.0.encode(encoder) } } -impl Decode for DigestOp { +impl DecodeIn for DigestOp { #[inline] - fn decode(mut reader: impl Decoder) -> Result { - let opcode = OpCode::decode(&mut reader)?; + fn decode_in(decoder: &mut impl Decoder, _alloc: A) -> Result { + let opcode = OpCode::decode(decoder)?; opcode .as_digest() .ok_or(DecodeError::ExpectedDigestOp(opcode)) @@ -206,6 +234,13 @@ impl DigestOp { } } +/// Extension trait for `Digest` implementors to get the corresponding `DigestOp`. +pub trait DigestOpExt: Digest { + const OPCODE: DigestOp; + + fn opcode() -> DigestOp; +} + macro_rules! define_opcodes { ($($val:literal => $variant:ident),* $(,)?) => { $( @@ -238,6 +273,31 @@ macro_rules! define_opcodes { } } } + + /// Error returned when parsing an invalid opcode from a string. + #[derive(Debug)] + pub struct OpCodeFromStrError; + + impl core::fmt::Display for OpCodeFromStrError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "invalid opcode string") + } + } + + #[cfg(feature = "std")] + impl std::error::Error for OpCodeFromStrError {} + + impl core::str::FromStr for OpCode { + type Err = OpCodeFromStrError; + + #[inline] + fn from_str(s: &str) -> Result { + match s { + $( stringify!($variant) => Ok(Self::$variant), )* + _ => Err(OpCodeFromStrError), + } + } + } }; } @@ -254,9 +314,10 @@ macro_rules! define_digest_opcodes { /// Returns the output length of the digest in bytes. #[inline] pub const fn output_size(&self) -> usize { + use digest::typenum::Unsigned; paste::paste! { match *self { - $( Self::$variant => <[<$variant:camel>] as OutputSizeUser>::OutputSize::USIZE, )* + $( Self::$variant => <[<$variant:camel>] as ::digest::OutputSizeUser>::OutputSize::USIZE, )* // SAFETY: unreachable as all variants are covered. _ => unsafe { unreachable_unchecked() } } @@ -264,13 +325,18 @@ macro_rules! define_digest_opcodes { } /// Executes the digest operation on the input data. - pub fn execute(&self, input: impl AsRef<[u8]>) -> OperationBuffer { + pub fn execute(&self, input: impl AsRef<[u8]>) -> ::alloc::vec::Vec { + self.execute_in(input, ::alloc::alloc::Global) + } + + /// Executes the digest operation on the input data. + pub fn execute_in(&self, input: impl AsRef<[u8]>, alloc: A) -> ::alloc::vec::Vec { match *self { $( Self::$variant => { paste::paste! { let mut hasher = [<$variant:camel>]::new(); hasher.update(input); - hasher.finalize().to_smallvec() + hasher.finalize().to_vec_in(alloc) } }, )* // SAFETY: unreachable as all variants are covered. @@ -278,6 +344,50 @@ macro_rules! define_digest_opcodes { } } } + paste::paste! { + $( + impl DigestOpExt for [<$variant:camel>] { + const OPCODE: DigestOp = DigestOp::$variant; + + #[inline] + fn opcode() -> DigestOp { + DigestOp::$variant + } + } + )* + } + }; +} + +macro_rules! impl_simple_step { + ($variant:ident) => {paste::paste! { + impl $crate::codec::v1::timestamp::builder::TimestampBuilder { + #[doc = concat!("Push the `", stringify!($variant), "` opcode.")] + pub fn [< $variant:lower >](&mut self) -> &mut Self { + self.push_step(OpCode::[<$variant>]) + } + } + }}; + ($($variant:ident),* $(,)?) => { + $( + impl_simple_step! { $variant } + )* + }; +} + +macro_rules! impl_step_with_data { + ($variant:ident) => {paste::paste! { + impl $crate::codec::v1::timestamp::builder::TimestampBuilder { + #[doc = concat!("Push the `", stringify!($variant), "` opcode.")] + pub fn [< $variant:lower >](&mut self, data: ::alloc::vec::Vec) -> &mut Self { + self.push_immediate_step(OpCode::[<$variant>], data) + } + } + }}; + ($($variant:ident),* $(,)?) => { + $( + impl_step_with_data! { $variant } + )* }; } @@ -301,6 +411,20 @@ define_digest_opcodes! { 0x67 => KECCAK256, } +impl_simple_step! { + SHA1, + RIPEMD160, + SHA256, + KECCAK256, + REVERSE, + HEXLIFY, +} + +impl_step_with_data! { + APPEND, + PREPEND, +} + #[cfg(test)] mod tests { use super::*; @@ -312,4 +436,20 @@ mod tests { assert_eq!(DigestOp::SHA256.output_size(), 32); assert_eq!(DigestOp::KECCAK256.output_size(), 32); } + + #[cfg(feature = "serde")] + #[test] + fn serde_opcode() { + let opcode = OpCode::SHA256; + let serialized = serde_json::to_string(&opcode).unwrap(); + assert_eq!(serialized, "\"SHA256\""); + let deserialized: OpCode = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, opcode); + + let digest_op = DigestOp::SHA256; + let serialized = serde_json::to_string(&digest_op).unwrap(); + assert_eq!(serialized, "\"SHA256\""); + let deserialized: DigestOp = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, digest_op); + } } diff --git a/crates/core/src/codec/v1/timestamp.rs b/crates/core/src/codec/v1/timestamp.rs index d15dfcf..0ca4e2a 100644 --- a/crates/core/src/codec/v1/timestamp.rs +++ b/crates/core/src/codec/v1/timestamp.rs @@ -1,18 +1,19 @@ //! ** The implementation here is subject to change as this is a read-only version. ** -use crate::codec::{ - Proof, Version, - v1::{Attestation, opcode::OpCode}, -}; -use std::num::NonZeroU32; -type StepPtr = Option; +use crate::{ + codec::v1::{ + Attestation, FinalizationError, MayHaveInput, PendingAttestation, + attestation::RawAttestation, opcode::OpCode, + }, + utils::{Hexed, OnceLock}, +}; +use alloc::{alloc::Global, vec::Vec}; +use core::{alloc::Allocator, fmt::Debug}; +pub(crate) mod builder; mod decode; mod encode; -pub(crate) mod fmt; - -const RECURSION_LIMIT: usize = 256; -const MAX_OP_LENGTH: usize = 4096; +mod fmt; /// Proof that that one or more attestations commit to a message. /// @@ -31,125 +32,326 @@ const MAX_OP_LENGTH: usize = 4096; /// execute APPEND 0ef41e45bb5534b3 /// result attested by Pending: update URI https://alice.btc.calendar.opentimestamps.org /// ``` -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct Timestamp { - steps: Vec, - data: Vec, - attestations: Vec, +#[derive(Clone, Debug)] +pub enum Timestamp { + Step(Step), + Attestation(RawAttestation), } -/// An OpenTimestamps step. -#[derive(Clone, PartialEq, Eq, Debug)] -#[repr(C)] -struct Step { - opcode: OpCode, - _padding: u8, - data_len: u16, - data_offset: u32, - // LCRS tree structure - first_child: StepPtr, - next_sibling: StepPtr, +/// An execution Step. +#[derive(Clone)] +pub struct Step { + op: OpCode, + data: Vec, + input: OnceLock>, + next: Vec, A>, } -// cache line aligned -const _: () = assert!(size_of::() == 16); - -impl Default for Step { - fn default() -> Self { - Step { - opcode: OpCode::ATTESTATION, - _padding: 0, - data_len: 0, - data_offset: 0, - first_child: None, - next_sibling: None, + +impl PartialEq for Timestamp { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Timestamp::Step(s1), Timestamp::Step(s2)) => s1 == s2, + (Timestamp::Attestation(a1), Timestamp::Attestation(a2)) => a1 == a2, + _ => false, } } } +impl Eq for Timestamp {} + +impl PartialEq for Step { + fn eq(&self, other: &Self) -> bool { + self.op == other.op && self.data == other.data && self.next == other.next + } +} +impl Eq for Step {} -impl Proof for Timestamp { - const VERSION: Version = 1; +impl Debug for Step { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let mut f = f.debug_struct("Step"); + f.field("op", &self.op); + if self.op.has_immediate() { + f.field("data", &Hexed(&self.data)); + } + f.field("next", &self.next).finish() + } } impl Timestamp { - /// Returns the data slice associated with a step. + /// Creates a new timestamp builder. + pub fn builder() -> builder::TimestampBuilder { + builder::TimestampBuilder::new_in(Global) + } + + /// Merges multiple timestamps into a single FORK timestamp. /// - /// # Safety + /// # Panics /// - /// The caller must ensure that the step was constructed from this timestamp's data buffer. + /// This will panic if there are conflicting inputs when finalizing unfinalized timestamps. + pub fn merge(timestamps: Vec) -> Timestamp { + Self::merge_in(timestamps, Global) + } + + /// Try to merge multiple timestamps into a single FORK timestamp. + /// + /// Returns an error if there are conflicting inputs when finalizing unfinalized timestamps. + pub fn try_merge(timestamps: Vec) -> Result { + Self::try_merge_in(timestamps, Global) + } +} + +impl Timestamp { + /// Returns the opcode of this timestamp node. + pub fn op(&self) -> OpCode { + match self { + Timestamp::Step(step) => { + debug_assert_ne!( + step.op, + OpCode::ATTESTATION, + "sanity check failed: Step with ATTESTATION opcode" + ); + step.op + } + Timestamp::Attestation(_) => OpCode::ATTESTATION, + } + } + + /// Returns this timestamp as a step, if it is one. #[inline] - unsafe fn get_step_data(&self, step: &Step) -> &[u8] { - if step.data_len == 0 { - return &[]; + pub fn as_step(&self) -> Option<&Step> { + match self { + Timestamp::Step(step) => Some(step), + Timestamp::Attestation(_) => None, } - let start = step.data_offset as usize; - debug_assert!(start < self.data.len()); - let end = start + step.data_len as usize; - debug_assert!(end <= self.data.len()); - // SAFETY: bounds checked above - unsafe { self.data.get_unchecked(start..end) } } - /// Returns the attestation index encoded by an attestation step. - /// - /// # Safety - /// - /// The caller must ensure that the step is an attestation step and that the - /// safety requirements of [`Self::get_step_data`] also hold. + /// Returns this timestamp as an attestation, if it is one. #[inline] - unsafe fn get_attest_idx(&self, step: &Step) -> u32 { - debug_assert!(step.opcode == OpCode::ATTESTATION); - debug_assert_eq!(step.data_len as usize, size_of::()); - let data = unsafe { self.get_step_data(step) }; - u32::from_le_bytes(data.try_into().unwrap()) + pub fn as_attestation(&self) -> Option<&RawAttestation> { + match self { + Timestamp::Attestation(attestation) => Some(attestation), + Timestamp::Step(_) => None, + } } + /// Returns the allocator used by this timestamp node. #[inline] - fn push_to_heap(heap: &mut Vec, data: &[u8]) -> (u32, u16) { - if data.is_empty() { - return (0, 0); + pub fn allocator(&self) -> &A { + match self { + Self::Attestation(attestation) => attestation.allocator(), + Self::Step(step) => step.allocator(), } + } - let offset = heap.len(); - let len = data.len(); + /// Returns true if this timestamp is finalized. + #[inline] + pub fn is_finalized(&self) -> bool { + self.input().is_some() + } - assert!(offset <= u32::MAX as usize, "Data heap overflow (max 4GB)"); - assert!(len <= u16::MAX as usize, "Ref data too large (max 65KB)"); + /// Iterates over all attestations in this timestamp. + #[inline] + pub fn attestations(&self) -> AttestationIter<'_, A> { + AttestationIter { stack: vec![self] } + } - heap.extend_from_slice(data); + /// Iterates over all pending attestation steps in this timestamp. + /// + /// # Note + /// + /// This iterator will yield `Timestamp` instead of `RawAttestation`. + #[inline] + pub fn pending_attestations_mut(&mut self) -> PendingAttestationIterMut<'_, A> { + PendingAttestationIterMut { stack: vec![self] } + } +} - (offset as u32, len as u16) +impl Timestamp { + /// Creates a new timestamp builder with the given allocator. + pub fn builder_in(alloc: A) -> builder::TimestampBuilder { + builder::TimestampBuilder::new_in(alloc) } - /// Returns a mutable buffer from the heap. + /// Finalizes the timestamp with the given input data. /// - /// # Safety + /// # Panics /// - /// The caller must write exactly `len` bytes into the returned buffer. + /// Panics if the timestamp is already finalized with different input data. #[inline] - unsafe fn get_buffer_from_heap(heap: &mut Vec, len: usize) -> (u32, u16, &mut [u8]) { - let offset = heap.len(); - assert!(offset <= u32::MAX as usize, "Data heap overflow (max 4GB)"); - assert!(len <= u16::MAX as usize, "Ref data too large (max 65KB)"); + pub fn finalize(&self, input: &[u8]) { + self.try_finalize(input) + .expect("conflicting inputs when finalizing timestamp") + } - heap.reserve(len); + /// Try finalizes the timestamp with the given input data. + /// + /// Returns an error if the timestamp is already finalized with different input data. + pub fn try_finalize(&self, input: &[u8]) -> Result<(), FinalizationError> { + let init_fn = || input.to_vec_in(self.allocator().clone()); + match self { + Self::Attestation(attestation) => { + if let Some(already) = attestation.value.get() { + return if &input != already { + Err(FinalizationError) + } else { + Ok(()) + }; + } + let _ = attestation.value.get_or_init(init_fn); + } + Self::Step(step) => { + if let Some(already) = step.input.get() { + return if &input != already { + Err(FinalizationError) + } else { + Ok(()) + }; + } + let input = step.input.get_or_init(init_fn); - // SAFETY: we just reserved enough space - let buffer = unsafe { - heap.set_len(offset + len); - heap.get_unchecked_mut(offset..offset + len) - }; + match step.op { + OpCode::FORK => { + debug_assert!(step.next.len() >= 2, "FORK must have at least two children"); + for child in &step.next { + child.try_finalize(input)?; + } + } + OpCode::ATTESTATION => unreachable!("should not happen"), + op => { + let output = op.execute_in(input, &step.data, step.allocator().clone()); + debug_assert!(step.next.len() == 1, "non-FORK must have exactly one child"); + step.next[0].try_finalize(&output)?; + } + } + } + } + Ok(()) + } - (offset as u32, len as u16, buffer) + /// Merges multiple timestamps into a single FORK timestamp. + /// + /// # Panics + /// + /// This will panic if there are conflicting inputs when finalizing unfinalized timestamps. + pub fn merge_in(timestamps: Vec, A>, alloc: A) -> Timestamp { + Self::try_merge_in(timestamps, alloc).expect("conflicting inputs when merging timestamps") + } + + /// Merges multiple timestamps into a single FORK timestamp. + /// + /// This will attempt to finalize unfinalized timestamps if any of the input timestamps are finalized. + /// + /// Returns an error if there are conflicting inputs when finalizing unfinalized timestamps. + pub fn try_merge_in( + timestamps: Vec, A>, + alloc: A, + ) -> Result, FinalizationError> { + // if any timestamp is finalized, ensure they are with the same input, + // finalize unfinalized timestamps with that input + let finalized_input = timestamps.iter().find_map(|ts| ts.input()); + if let Some(ref input) = finalized_input { + for ts in timestamps.iter().filter(|ts| !ts.is_finalized()) { + ts.try_finalize(input)?; + } + } + + Ok(Timestamp::Step(Step { + op: OpCode::FORK, + data: Vec::new_in(alloc.clone()), + input: OnceLock::new(), + next: timestamps, + })) } } -#[inline] -fn make_ptr(idx: usize) -> StepPtr { - assert!(idx < u32::MAX as usize); - NonZeroU32::new((idx + 1) as u32) +impl MayHaveInput for Timestamp { + #[inline] + fn input(&self) -> Option<&[u8]> { + match self { + Timestamp::Step(step) => step.input(), + Timestamp::Attestation(attestation) => attestation.input(), + } + } } -#[inline] -fn resolve_ptr(ptr: StepPtr) -> Option { - ptr.map(|nz| (nz.get() - 1) as usize) +impl Step { + /// Returns the opcode of this step. + pub fn op(&self) -> OpCode { + self.op + } + + /// Returns the immediate data of this step. + pub fn data(&self) -> &[u8] { + self.data.as_slice() + } + + /// Returns the next timestamps of this step. + pub fn next(&self) -> &[Timestamp] { + self.next.as_slice() + } + + /// Returns the next timestamps of this step. + pub fn next_mut(&mut self) -> &mut [Timestamp] { + self.next.as_mut_slice() + } + + /// Returns the allocator used by this step. + pub fn allocator(&self) -> &A { + self.data.allocator() + } +} + +impl MayHaveInput for Step { + #[inline] + fn input(&self) -> Option<&[u8]> { + self.input.get().map(|v| v.as_slice()) + } +} + +#[must_use = "AttestationIter is an iterator, it does nothing unless consumed"] +pub struct AttestationIter<'a, A: Allocator> { + stack: Vec<&'a Timestamp>, +} + +impl<'a, A: Allocator> Iterator for AttestationIter<'a, A> { + type Item = &'a RawAttestation; + + fn next(&mut self) -> Option { + while let Some(ts) = self.stack.pop() { + match ts { + Timestamp::Step(step) => { + for next in step.next().iter().rev() { + self.stack.push(next); + } + } + Timestamp::Attestation(attestation) => return Some(attestation), + } + } + None + } +} + +pub struct PendingAttestationIterMut<'a, A: Allocator> { + stack: Vec<&'a mut Timestamp>, +} + +impl<'a, A: Allocator> Iterator for PendingAttestationIterMut<'a, A> { + type Item = &'a mut Timestamp; + + fn next(&mut self) -> Option { + while let Some(ts) = self.stack.pop() { + match ts { + Timestamp::Step(step) => { + for next in step.next_mut().iter_mut().rev() { + self.stack.push(next); + } + } + Timestamp::Attestation(attestation) => { + if attestation.tag == PendingAttestation::TAG { + return Some(ts); + } + } + } + } + None + } } diff --git a/crates/core/src/codec/v1/timestamp/builder.rs b/crates/core/src/codec/v1/timestamp/builder.rs new file mode 100644 index 0000000..9c36254 --- /dev/null +++ b/crates/core/src/codec/v1/timestamp/builder.rs @@ -0,0 +1,147 @@ +//! Timestamp Builder + +use crate::{ + codec::v1::{ + Attestation, Timestamp, + opcode::{DigestOpExt, OpCode}, + timestamp::Step, + }, + error::EncodeError, + utils::OnceLock, +}; +use alloc::alloc::{Allocator, Global}; +use uts_bmt::{NodePosition, SiblingIter}; + +#[derive(Debug, Clone)] +pub struct TimestampBuilder { + steps: Vec, A>, +} + +#[derive(Debug, Clone)] +struct LinearStep { + op: OpCode, + data: Vec, +} + +impl TimestampBuilder { + /// Creates a new `TimestampBuilder`. + pub fn new_in(alloc: A) -> TimestampBuilder { + TimestampBuilder { + steps: Vec::new_in(alloc), + } + } + + /// Pushes a new execution step with immediate data to the timestamp. + /// + /// # Panics + /// + /// Panics if the opcode is not an opcode with immediate data. + pub(crate) fn push_immediate_step(&mut self, op: OpCode, data: Vec) -> &mut Self { + assert!(op.has_immediate()); + self.steps.push(LinearStep { op, data }); + self + } + + /// Pushes a new execution step without immediate data to the timestamp. + /// + /// # Panics + /// + /// Panics if: + /// - the opcode is control opcode + /// - the opcode is an opcode with immediate data + pub fn push_step(&mut self, op: OpCode) -> &mut Self { + self.steps.push(LinearStep { + op, + data: Vec::new_in(self.allocator().clone()), + }); + self + } + + /// Pushes a new digest step to the timestamp. + pub fn digest(&mut self) -> &mut Self { + self.push_step(D::OPCODE.to_opcode()); + self + } + + /// Pushes the steps corresponding to the given Merkle proof to the timestamp. + pub fn merkle_proof(&mut self, mut proof: SiblingIter<'_, D>) -> &mut Self { + let alloc = self.allocator().clone(); + while let Some((side, sibling_hash)) = proof.next() { + match side { + NodePosition::Left => self + .prepend([uts_bmt::INNER_NODE_PREFIX].to_vec_in(alloc.clone())) + .append(sibling_hash.to_vec_in(alloc.clone())), + NodePosition::Right => self + .prepend(sibling_hash.to_vec_in(alloc.clone())) + .prepend([uts_bmt::INNER_NODE_PREFIX].to_vec_in(alloc.clone())), + } + .digest::(); + } + self + } + + /// Computes the commitment of the timestamp for the given input. + /// + /// In this context, the **commitment** is the deterministic result of + /// executing the timestamp's linear chain of operations over the input + /// bytes. It is computed by: + /// + /// 1. Taking the provided `input` bytes as the initial value. + /// 2. Iterating over all steps in the order they were added to the builder. + /// 3. For each step, applying its opcode to the current value together + /// with the step's immediate data via [`OpCode::execute_in`], and using + /// the result as the new current value. + /// + /// The final value after all steps have been applied is returned as the + /// commitment. + pub fn commitment(&self, input: impl AsRef<[u8]>) -> Vec { + let alloc = self.allocator().clone(); + let mut commitment = input.as_ref().to_vec_in(alloc.clone()); + for step in &self.steps { + commitment = step.op.execute_in(&commitment, &step.data, alloc.clone()); + } + commitment + } + + /// Finalizes the timestamp with the given attestation. + /// + /// # Notes + /// + /// The built timestamp does not include any input data. The input data must be + /// provided later using the `finalize` method on the `Timestamp` object. + pub fn attest<'a, T: Attestation<'a>>( + self, + attestation: T, + ) -> Result, EncodeError> { + let current = Timestamp::Attestation(attestation.to_raw_in(self.allocator().clone())?); + Ok(self.concat(current)) + } + + /// Append the given timestamp after the steps in the builder. + pub fn concat(self, timestamp: Timestamp) -> Timestamp { + let alloc = self.allocator().clone(); + + let mut current = timestamp; + + for step in self.steps.into_iter().rev() { + let step_node = Step { + op: step.op, + data: step.data, + input: OnceLock::new(), + next: { + let mut v = Vec::with_capacity_in(1, alloc.clone()); + v.push(current); + v + }, + }; + current = Timestamp::Step(step_node); + } + + current + } + + #[inline] + fn allocator(&self) -> &A { + self.steps.allocator() + } +} diff --git a/crates/core/src/codec/v1/timestamp/decode.rs b/crates/core/src/codec/v1/timestamp/decode.rs index 277814f..c03e018 100644 --- a/crates/core/src/codec/v1/timestamp/decode.rs +++ b/crates/core/src/codec/v1/timestamp/decode.rs @@ -1,152 +1,86 @@ use super::*; use crate::{ - codec::{Decode, Decoder}, + codec::{Decode, DecodeIn, Decoder}, error::DecodeError, }; -use std::io::BufRead; -impl Decode for Timestamp { - fn decode(mut reader: impl Decoder) -> Result { - let mut steps = Vec::new(); - let mut data = Vec::new(); - let mut attestations = Vec::new(); +const RECURSION_LIMIT: usize = 256; - Self::decode_step_recurse( - &mut reader, - &mut steps, - &mut data, - &mut attestations, - None, - RECURSION_LIMIT, - )?; - - Ok(Timestamp { - steps, - data, - attestations, - }) +impl DecodeIn for Timestamp { + fn decode_in(decoder: &mut impl Decoder, alloc: A) -> Result { + Self::decode_recursive(decoder, RECURSION_LIMIT, alloc) } } -impl Timestamp { - fn decode_step_recurse( - reader: &mut R, - steps: &mut Vec, - data: &mut Vec, - attestations: &mut Vec, - op: Option, +impl Timestamp { + fn decode_recursive( + decoder: &mut impl Decoder, recursion_limit: usize, - ) -> Result { + alloc: A, + ) -> Result, DecodeError> { if recursion_limit == 0 { return Err(DecodeError::RecursionLimit); } + let op = OpCode::decode(&mut *decoder)?; - let op = match op { - Some(op) => op, - None => reader.decode()?, - }; + Self::decode_from_op(op, decoder, recursion_limit, alloc) + } - let step = match op { + fn decode_from_op( + op: OpCode, + decoder: &mut impl Decoder, + limit: usize, + alloc: A, + ) -> Result, DecodeError> { + match op { OpCode::ATTESTATION => { - let attest = Attestation::decode(reader)?; - let attest_idx = attestations.len(); - attestations.push(attest); - let (data_offset, data_len) = - Self::push_to_heap(data, &(attest_idx as u32).to_le_bytes()); - Step { - opcode: op, - data_len, - data_offset, - ..Default::default() - } + let attestation = RawAttestation::decode_in(decoder, alloc)?; + Ok(Timestamp::Attestation(attestation)) } OpCode::FORK => { - let mut first_child: StepPtr = None; - let mut prev_sibling_idx: Option = None; - + let mut children = Vec::new_in(alloc.clone()); let mut next_op = OpCode::FORK; - while next_op == OpCode::FORK { - let child_ptr = Self::decode_step_recurse( - reader, - steps, - data, - attestations, - None, - recursion_limit - 1, - )?; - - // LCRS: - // if prev sibling exist, link its next_sibling to current child - // else it's first_child - if let Some(prev) = prev_sibling_idx { - steps[prev].next_sibling = child_ptr; - } else { - first_child = child_ptr; - } - - // update prev_sibling_idx to current child - prev_sibling_idx = resolve_ptr(child_ptr); - - next_op = reader.decode()?; - } - - let child_ptr = Self::decode_step_recurse( - reader, - steps, - data, - attestations, - Some(next_op), - recursion_limit - 1, - )?; - if let Some(prev) = prev_sibling_idx { - steps[prev].next_sibling = child_ptr; - } else { - first_child = child_ptr; - } - - Step { - opcode: op, - data_len: 0, - data_offset: 0, - first_child, - ..Default::default() + let child = Self::decode_recursive(&mut *decoder, limit - 1, alloc.clone())?; + children.push(child); + next_op = OpCode::decode(&mut *decoder)?; } + children.push(Self::decode_from_op( + next_op, + decoder, + limit - 1, + alloc.clone(), + )?); + Ok(Timestamp::Step(Step { + op: OpCode::FORK, + data: Vec::new_in(alloc), + input: OnceLock::new(), + next: children, + })) } _ => { - debug_assert!(!op.is_control()); - let (data_offset, data_len) = if op.has_immediate() { - let length = reader.decode_ranged(1..=MAX_OP_LENGTH)?; - // SAFETY: We will fill the buffer right after getting it. - let (data_offset, data_len, buffer) = - unsafe { Self::get_buffer_from_heap(data, length) }; - reader.read_exact(buffer)?; - (data_offset, data_len) + let data = if op.has_immediate() { + const MAX_OP_LENGTH: usize = 4096; + let length = decoder.decode_ranged(1..=MAX_OP_LENGTH)?; + let mut data = Vec::with_capacity_in(length, alloc.clone()); + data.resize(length, 0); + decoder.read_exact(&mut data)?; + + data } else { - (0, 0) + Vec::new_in(alloc.clone()) }; - let next = Self::decode_step_recurse( - reader, - steps, - data, - attestations, - None, - recursion_limit - 1, - )?; + let mut next = Vec::with_capacity_in(1, alloc.clone()); + next.push(Self::decode_recursive(decoder, limit - 1, alloc)?); - Step { - opcode: op, - data_len, - data_offset, - first_child: next, - ..Default::default() - } + Ok(Timestamp::Step(Step { + op, + data, + input: OnceLock::new(), + next, + })) } - }; - - let step_idx = steps.len(); - steps.push(step); - Ok(make_ptr(step_idx)) + } } } diff --git a/crates/core/src/codec/v1/timestamp/encode.rs b/crates/core/src/codec/v1/timestamp/encode.rs index 67b4f4a..e9e97e2 100644 --- a/crates/core/src/codec/v1/timestamp/encode.rs +++ b/crates/core/src/codec/v1/timestamp/encode.rs @@ -3,67 +3,34 @@ use crate::{ codec::{Encode, Encoder}, error::EncodeError, }; -use std::io::Write; -impl Encode for Timestamp { - fn encode(&self, mut writer: impl Encoder) -> Result<(), EncodeError> { - self.encode_step_recurse(&mut writer, &self.steps.last().unwrap()) - } -} - -impl Timestamp { - fn encode_step_recurse( - &self, - writer: &mut W, - step: &Step, - ) -> Result<(), EncodeError> { - // 1. Write OpCode - // Note: We need a way to serialize the OpCode (e.g., as u8) - writer.encode(step.opcode)?; - - // 2. Write data - match step.opcode { - OpCode::ATTESTATION => { - // SAFETY: caller ensures step is attestation step - let attest_idx = unsafe { self.get_attest_idx(step) }; - let attest = &self.attestations[attest_idx as usize]; - attest.encode(&mut *writer)?; +impl Encode for Timestamp { + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { + match self { + Self::Attestation(attestation) => { + encoder.encode(self.op())?; + attestation.encode(encoder)?; } - _ if step.data_len != 0 => { - // SAFETY: caller ensures step is valid - let step_data = unsafe { self.get_step_data(step) }; - writer.encode_bytes(step_data)?; + Self::Step(step) if step.op == OpCode::FORK => { + debug_assert!(step.next.len() >= 2, "FORK must have at least two children"); + for child in step.next.iter().take(step.next.len() - 1) { + encoder.encode(self.op())?; + child.encode(encoder)?; + } + // Encode the last child + step.next.last().expect("infallible").encode(encoder)?; } - _ => {} - } - - if let Some(first_child) = resolve_ptr(step.first_child) { - let mut current = &self.steps[first_child]; - if let OpCode::FORK = step.opcode { - // Encode the first child - self.encode_step_recurse(writer, current)?; - - // Logic: Child -> FORK -> Child -> ... -> LastChild - while let Some(next_sibling_idx) = resolve_ptr(current.next_sibling) { - let next = &self.steps[next_sibling_idx]; - // Encode current child - self.encode_step_recurse(writer, next)?; - // Write Separator FORK - let continues = resolve_ptr(next.next_sibling).is_some(); - if continues { - writer.encode(OpCode::FORK)?; - } - - // Move to next - current = next; + Self::Step(step) => { + encoder.encode(self.op())?; + if !step.data.is_empty() { + debug_assert!(step.op.has_immediate()); + encoder.encode_bytes(&step.data)?; } - Ok(()) - } else { - // FIXME: tailcall optimization - self.encode_step_recurse(writer, current) + debug_assert_eq!(step.next.len(), 1); + step.next[0].encode(encoder)?; } - } else { - Ok(()) } + + Ok(()) } } diff --git a/crates/core/src/codec/v1/timestamp/fmt.rs b/crates/core/src/codec/v1/timestamp/fmt.rs index 5ded172..17c6d6a 100644 --- a/crates/core/src/codec/v1/timestamp/fmt.rs +++ b/crates/core/src/codec/v1/timestamp/fmt.rs @@ -1,96 +1,84 @@ use super::*; -use crate::{ - codec::v1::opcode::{OpCode, OperationBuffer}, - utils::Hexed, -}; -use std::fmt; +use crate::utils::Hexed; +use core::fmt; -pub(crate) fn fmt( - timestamp: &Timestamp, - input: Option<&OperationBuffer>, - f: &mut fmt::Formatter, -) -> fmt::Result { - fmt_recurse( - ×tamp, - input, - ×tamp.steps.last().unwrap(), - f, - 0, - true, - ) +impl fmt::Display for Timestamp { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.fmt_recurse(None, f, 0, true) + } } -fn fmt_recurse( - timestamp: &Timestamp, - input: Option<&OperationBuffer>, - step: &Step, - f: &mut fmt::Formatter, - depth: usize, - first_line: bool, -) -> fmt::Result { - fn indent(f: &mut fmt::Formatter, depth: usize, first_line: bool) -> fmt::Result { - if depth == 0 { - return Ok(()); - } - - for _ in 0..depth - 1 { - f.write_str(" ")?; - } - if first_line { - f.write_str("--->")?; - } else { - f.write_str(" ")?; - } - Ok(()) +impl Timestamp { + pub(crate) fn fmt(&self, input: Option<&[u8]>, f: &mut fmt::Formatter) -> fmt::Result { + let input = match input { + Some(input) => Some(input), + None => match self { + Self::Step(step) => step.input.get().map(|v| v.as_slice()), + Self::Attestation(_) => None, + }, + }; + self.fmt_recurse(input, f, 0, true) } - indent(f, depth, first_line)?; - match step.opcode { - OpCode::FORK => { - writeln!(f, "fork")?; - let mut child_ptr = step.first_child; - while let Some(child_idx) = resolve_ptr(child_ptr) { - let child_step = ×tamp.steps[child_idx]; - fmt_recurse(timestamp, input, child_step, f, depth + 1, true)?; - child_ptr = child_step.next_sibling; + fn fmt_recurse( + &self, + input: Option<&[u8]>, + f: &mut fmt::Formatter, + depth: usize, + first_line: bool, + ) -> fmt::Result { + fn indent(f: &mut fmt::Formatter, depth: usize, first_line: bool) -> fmt::Result { + if depth == 0 { + return Ok(()); + } + + for _ in 0..depth - 1 { + f.write_str(" ")?; + } + if first_line { + f.write_str("--->")?; + } else { + f.write_str(" ")?; } Ok(()) } - OpCode::ATTESTATION => { - // SAFETY: caller ensures step is attestation step - let attest_idx = unsafe { timestamp.get_attest_idx(step) } as usize; - let attest = ×tamp.attestations[attest_idx]; - writeln!(f, "result attested by {attest}") - } - op @ _ => { - let data = unsafe { timestamp.get_step_data(step) }; - if op.has_immediate() { - writeln!(f, "execute {op} {}", Hexed(&data))?; - } else { - writeln!(f, "execute {op}")?; + indent(f, depth, first_line)?; + match self { + Self::Step(step) if step.op == OpCode::FORK => { + writeln!(f, "fork")?; + for child in &step.next { + child.fmt_recurse(input, f, depth + 1, true)?; + } + Ok(()) } + Self::Step(step) => { + let op = step.op; + if op.has_immediate() { + writeln!(f, "execute {op} {}", Hexed(&step.data))?; + } else { + writeln!(f, "execute {op}")?; + } - let result = if let Some(input) = input { - let result = op.execute(&input, &data); - indent(f, depth, false)?; - writeln!(f, " result {}", Hexed(&result))?; - Some(result) - } else { - None - }; + let result = if let Some(value) = step.next.first().and_then(|next| next.input()) { + Some(value.to_vec_in(step.allocator().clone())) + } else if let Some(input) = input { + let result = op.execute_in(input, &step.data, step.allocator().clone()); + indent(f, depth, false)?; + writeln!(f, " result {}", Hexed(&result))?; + Some(result) + } else { + None + }; - if let Some(step_idx) = resolve_ptr(step.first_child) { - let step = ×tamp.steps[step_idx]; - fmt_recurse(timestamp, result.as_ref(), step, f, depth, false)?; + for child in &step.next { + child.fmt_recurse(result.as_deref(), f, depth, false)?; + } + Ok(()) + } + Self::Attestation(attestation) => { + writeln!(f, "result attested by {attestation}") } - Ok(()) } } } - -impl fmt::Display for Timestamp { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt_recurse(self, None, &self.steps.last().unwrap(), f, 0, true) - } -} diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index 44c0362..e5a4946 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -9,6 +9,9 @@ pub enum DecodeError { /// File has a version we do not understand. #[error("bad version")] BadVersion, + /// Expected an attestation tag but decoded something else. + #[error("bad attestation tag")] + BadAttestationTag, /// Read an LEB128-encoded integer that overflowed the expected size. #[error("read a LEB128 value overflows {0} bits")] LEB128Overflow(u32), @@ -24,12 +27,19 @@ pub enum DecodeError { /// Encountered an invalid character in a URI. #[error("invalid character in URI")] InvalidUriChar, + /// URI is too long. + #[error("URI too long")] + UriTooLong, /// Recursed deeper than allowed while decoding the proof. #[error("recursion limit reached")] RecursionLimit, + /// Reached end-of-file unexpectedly. + #[error("unexpected end of file")] + UnexpectedEof, /// General I/O error + #[cfg(feature = "std")] #[error("I/O error: {0}")] - Io(#[from] std::io::Error), + Io(std::io::Error), } /// Errors returned while encoding proofs. @@ -38,7 +48,24 @@ pub enum EncodeError { /// Tried to encode a `usize` exceeding `u32::MAX`. #[error("tried to encode a usize exceeding u32::MAX")] UsizeOverflow, + /// Encountered an invalid character in a URI. + #[error("invalid character in URI")] + InvalidUriChar, + /// URI is too long. + #[error("URI too long")] + UriTooLong, /// General I/O error + #[cfg(feature = "std")] #[error("I/O error: {0}")] Io(#[from] std::io::Error), } + +#[cfg(feature = "std")] +impl From for DecodeError { + fn from(err: std::io::Error) -> Self { + match err.kind() { + std::io::ErrorKind::UnexpectedEof => Self::UnexpectedEof, + _ => Self::Io(err), + } + } +} diff --git a/crates/core/src/fixtures.rs b/crates/core/src/fixtures.rs index 7b9aa3f..2eb9b7c 100644 --- a/crates/core/src/fixtures.rs +++ b/crates/core/src/fixtures.rs @@ -4,7 +4,7 @@ //! Embedded test fixtures for detached timestamp files used in tests and benchmarks. -pub const SMALL_DETACHED_TIMESTAMP: &[u8] = b"\ +pub static SMALL_DETACHED_TIMESTAMP: &[u8] = b"\ \x00\x4f\x70\x65\x6e\x54\x69\x6d\x65\x73\x74\x61\x6d\x70\x73\x00\x00\x50\x72\x6f\x6f\x66\x00\xbf\x89\xe2\xe8\x84\xe8\x92\ \x94\x01\x08\xa7\x0d\xfe\x69\xc5\xa0\xd6\x28\x16\x78\x1a\xbb\x6e\x17\x77\x85\x47\x18\x62\x4a\x0d\x19\x42\x31\xad\xb1\x4c\ \x32\xee\x54\x38\xa4\xf0\x10\x7a\x46\x05\xde\x0a\x5b\x37\xcb\x21\x17\x59\xc6\x81\x2b\xfe\x2e\x08\xff\xf0\x10\x24\x4b\x79\ @@ -15,7 +15,7 @@ pub const SMALL_DETACHED_TIMESTAMP: &[u8] = b"\ \x83\xdf\xe3\x0d\x2e\xf9\x0c\x8e\x2e\x2d\x68\x74\x74\x70\x73\x3a\x2f\x2f\x61\x6c\x69\x63\x65\x2e\x62\x74\x63\x2e\x63\x61\ \x6c\x65\x6e\x64\x61\x72\x2e\x6f\x70\x65\x6e\x74\x69\x6d\x65\x73\x74\x61\x6d\x70\x73\x2e\x6f\x72\x67"; -pub const LARGE_DETACHED_TIMESTAMP: &[u8] = b"\ +pub static LARGE_DETACHED_TIMESTAMP: &[u8] = b"\ \x00\x4f\x70\x65\x6e\x54\x69\x6d\x65\x73\x74\x61\x6d\x70\x73\x00\x00\x50\x72\x6f\x6f\x66\x00\xbf\x89\xe2\xe8\x84\xe8\x92\ \x94\x01\x08\x6f\xd9\xc1\xc4\xf0\x96\xb7\x7e\x6d\x44\x57\xba\xc1\xc7\xf5\x10\x10\xd3\x18\xdb\x48\x3f\x28\x68\xd3\x79\x58\ \x43\xf0\x98\xd3\x78\xf0\x10\xe2\xe2\x24\x43\x9e\x7f\x0f\xdd\x8c\x1e\xea\xc7\x3e\xa7\x39\xdb\x08\xf1\x20\xa5\x74\x44\x4a\ diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index db5a5d1..a5fe5f4 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,6 +1,11 @@ #![feature(exact_bitshifts)] +#![feature(allocator_api)] +#![cfg_attr(not(feature = "std"), no_std)] //! # Universal Timestamps Core Library +extern crate alloc; +extern crate core; + mod tracing; #[cfg(test)] @@ -10,4 +15,6 @@ pub mod fixtures; pub mod codec; /// Error types raised by codec operations. pub mod error; -mod utils; +pub mod utils; +#[cfg(feature = "verifier")] +pub mod verifier; diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs index 56c4372..1b50d53 100644 --- a/crates/core/src/utils.rs +++ b/crates/core/src/utils.rs @@ -1,19 +1,10 @@ -use std::fmt; +mod hex; +pub use hex::Hexed; -/// Zero-allocation wrapper that displays byte slices as lowercase hex. -pub struct Hexed<'a, T: ?Sized>(pub &'a T); +mod sync; +pub use sync::OnceLock; -impl<'a, T: ?Sized + AsRef<[u8]>> fmt::Display for Hexed<'a, T> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for b in self.0.as_ref() { - write!(f, "{:02x}", b)?; - } - Ok(()) - } -} - -impl<'a, T: ?Sized + AsRef<[u8]>> fmt::Debug for Hexed<'a, T> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(self, f) - } -} +mod hash; +#[cfg(feature = "io-utils")] +pub use hash::HashAsyncFsExt; +pub use hash::HashFsExt; diff --git a/crates/core/src/utils/hash.rs b/crates/core/src/utils/hash.rs new file mode 100644 index 0000000..5fc5d7d --- /dev/null +++ b/crates/core/src/utils/hash.rs @@ -0,0 +1,48 @@ +use digest::Digest; +use std::io::{self, Read}; + +pub trait HashFsExt { + fn update(&mut self, reader: R) -> io::Result<()>; +} + +impl HashFsExt for D { + fn update(&mut self, mut reader: R) -> io::Result<()> { + let mut buffer = [0u8; 64 * 1024]; // 64KB buffer + loop { + let bytes_read = reader.read(&mut buffer)?; + if bytes_read == 0 { + break; + } + self.update(&buffer[..bytes_read]); + } + Ok(()) + } +} + +#[cfg(feature = "io-utils")] +pub trait HashAsyncFsExt { + fn update( + &mut self, + reader: R, + ) -> impl Future> + Send; +} + +#[cfg(feature = "io-utils")] +impl HashAsyncFsExt for D { + async fn update( + &mut self, + mut reader: R, + ) -> io::Result<()> { + use tokio::io::AsyncReadExt; + + let mut buffer = [0u8; 64 * 1024]; // 64KB buffer + loop { + let bytes_read = reader.read(&mut buffer).await?; + if bytes_read == 0 { + break; + } + self.update(&buffer[..bytes_read]); + } + Ok(()) + } +} diff --git a/crates/core/src/utils/hex.rs b/crates/core/src/utils/hex.rs new file mode 100644 index 0000000..8a1948b --- /dev/null +++ b/crates/core/src/utils/hex.rs @@ -0,0 +1,19 @@ +use core::fmt; + +/// Zero-allocation wrapper that displays byte slices as lowercase hex. +pub struct Hexed<'a, T: ?Sized>(pub &'a T); + +impl<'a, T: ?Sized + AsRef<[u8]>> fmt::Display for Hexed<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for b in self.0.as_ref() { + write!(f, "{:02x}", b)?; + } + Ok(()) + } +} + +impl<'a, T: ?Sized + AsRef<[u8]>> fmt::Debug for Hexed<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} diff --git a/crates/core/src/utils/sync.rs b/crates/core/src/utils/sync.rs new file mode 100644 index 0000000..660f4c9 --- /dev/null +++ b/crates/core/src/utils/sync.rs @@ -0,0 +1,9 @@ +#[cfg(feature = "std")] +mod std; +#[cfg(feature = "std")] +pub use self::std::OnceLock; + +#[cfg(not(feature = "std"))] +mod race; +#[cfg(not(feature = "std"))] +pub use self::race::OnceLock; diff --git a/crates/core/src/utils/sync/race.rs b/crates/core/src/utils/sync/race.rs new file mode 100644 index 0000000..1ee7567 --- /dev/null +++ b/crates/core/src/utils/sync/race.rs @@ -0,0 +1,20 @@ +#[derive(Default, Debug, Clone)] +#[repr(transparent)] +pub struct OnceLock(once_cell::race::OnceBox); + +impl OnceLock { + pub const fn new() -> OnceLock { + OnceLock(once_cell::race::OnceBox::new()) + } + + pub fn get(&self) -> Option<&T> { + self.0.get() + } + + pub fn get_or_init(&self, init: F) -> &T + where + F: FnOnce() -> T, + { + self.0.get_or_init(|| alloc::boxed::Box::new(init())) + } +} diff --git a/crates/core/src/utils/sync/std.rs b/crates/core/src/utils/sync/std.rs new file mode 100644 index 0000000..3078519 --- /dev/null +++ b/crates/core/src/utils/sync/std.rs @@ -0,0 +1,20 @@ +#[derive(Default, Debug, Clone)] +#[repr(transparent)] +pub struct OnceLock(std::sync::OnceLock); + +impl OnceLock { + pub const fn new() -> OnceLock { + OnceLock(std::sync::OnceLock::new()) + } + + pub fn get(&self) -> Option<&T> { + self.0.get() + } + + pub fn get_or_init(&self, init: F) -> &T + where + F: FnOnce() -> T, + { + self.0.get_or_init(init) + } +} diff --git a/crates/core/src/verifier.rs b/crates/core/src/verifier.rs new file mode 100644 index 0000000..500ad52 --- /dev/null +++ b/crates/core/src/verifier.rs @@ -0,0 +1,65 @@ +use crate::{ + codec::v1::{Attestation, PendingAttestation, RawAttestation}, + error::DecodeError, +}; + +#[cfg(feature = "ethereum-uts-verifier")] +mod ethereum_uts; +#[cfg(feature = "ethereum-uts-verifier")] +pub use ethereum_uts::EthereumUTSVerifier; + +#[derive(Debug, thiserror::Error)] +pub enum VerifyError { + /// The raw attestation lacks a value, so it cannot be verified. + #[error("raw attestation lacks a value")] + NoValue, + /// The attestation is still pending and cannot be verified yet. + #[error("attestation is still pending and cannot be verified yet")] + Pending, + /// The attestation is not the expected type + /// (e.g. a Bitcoin attestation was expected but an Ethereum attestation was found). + #[error("attestation is not the expected type")] + BadAttestationTag, + /// An error occurred while decoding the attestation. + #[error("error decoding attestation: {0}")] + Decode(DecodeError), + /// An error occurred while verifying the ethereum uts attestation. + #[cfg(feature = "ethereum-uts-verifier")] + #[error("error verifying ethereum uts attestation: {0}")] + EthereumUTS(#[from] ethereum_uts::EthereumUTSVerifierError), +} + +pub trait AttestationVerifier

+where + P: for<'a> Attestation<'a> + Send, + Self: Send + Sync, +{ + type Output; + + fn verify_raw( + &self, + raw: &RawAttestation, + ) -> impl Future> + Send { + async { + if raw.tag == PendingAttestation::TAG { + return Err(VerifyError::Pending); + } + + let Some(value) = raw.value.get() else { + return Err(VerifyError::NoValue); + }; + + match P::from_raw(raw) { + Ok(attestation) => self.verify(&attestation, value).await, + Err(DecodeError::BadAttestationTag) => Err(VerifyError::BadAttestationTag), + Err(e) => Err(VerifyError::Decode(e)), + } + } + } + + fn verify( + &self, + attestation: &P, + value: &[u8], + ) -> impl Future> + Send; +} diff --git a/crates/core/src/verifier/ethereum_uts.rs b/crates/core/src/verifier/ethereum_uts.rs new file mode 100644 index 0000000..ec0d6ca --- /dev/null +++ b/crates/core/src/verifier/ethereum_uts.rs @@ -0,0 +1,145 @@ +use super::{AttestationVerifier, VerifyError}; +use crate::codec::v1::EthereumUTSAttestation; +use alloy_primitives::{Address, ChainId, TxHash}; +use alloy_provider::{Provider, transport::TransportError}; +use alloy_rpc_types_eth::{Filter, Log}; +use alloy_sol_types::SolEvent; +use digest::OutputSizeUser; +use sha3::Keccak256; +use uts_contracts::uts::Attested; + +#[derive(Debug, Clone)] +pub struct EthereumUTSVerifier { + provider: P, + chain_id: ChainId, +} + +#[derive(Debug, thiserror::Error)] +pub enum EthereumUTSVerifierError { + #[error("invalid value length for Ethereum UTS attestation")] + InvalidLength, + #[error("chain ID mismatch")] + ChainIdMismatch, + #[error("root not found in attested logs")] + NotFound, + #[error("contract address mismatch, expected {expected}, found {found}")] + ContractMismatch { expected: Address, found: Address }, + #[error("transaction hash mismatch, expected {expected}, found {found}")] + TransactionMismatch { expected: TxHash, found: TxHash }, + #[error(transparent)] + Rpc(#[from] TransportError), +} + +impl EthereumUTSVerifier

{ + pub async fn new(provider: P) -> Result { + let chain_id = provider.get_chain_id().await?; + Ok(Self { provider, chain_id }) + } +} + +impl AttestationVerifier for EthereumUTSVerifier

{ + type Output = Log; + + async fn verify( + &self, + attestation: &EthereumUTSAttestation, + value: &[u8], + ) -> Result { + Ok(self.verify_attestation(attestation, value).await?) + } +} + +impl EthereumUTSVerifier

{ + async fn verify_attestation( + &self, + attestation: &EthereumUTSAttestation, + value: &[u8], + ) -> Result, EthereumUTSVerifierError> { + if value.len() != Keccak256::output_size() { + return Err(EthereumUTSVerifierError::InvalidLength); + } + if attestation.chain.id() != self.chain_id { + return Err(EthereumUTSVerifierError::ChainIdMismatch); + } + + let filter = Filter::new() + .from_block(attestation.height) + .to_block(attestation.height) + .event_signature(Attested::SIGNATURE_HASH); + let logs = self.provider.get_logs(&filter).await?; + + let Some(log) = logs + .into_iter() + .filter_map(|log| { + Attested::decode_log(&log.inner) + .map(|inner| Log { + inner, + block_hash: log.block_hash, + block_number: log.block_number, + block_timestamp: log.block_timestamp, + transaction_hash: log.transaction_hash, + transaction_index: log.transaction_index, + log_index: log.log_index, + removed: log.removed, + }) + .ok() + }) + .find(|log| log.inner.data.root == value) + else { + return Err(EthereumUTSVerifierError::NotFound); + }; + + // perform additional checks if available + if let Some(contract) = attestation.metadata.contract() { + if log.inner.address != contract { + return Err(EthereumUTSVerifierError::ContractMismatch { + expected: contract, + found: log.inner.address, + }); + } + if let Some(expect_tx) = attestation.metadata.tx() + && let Some(found_tx) = log.transaction_hash + && expect_tx != found_tx + { + return Err(EthereumUTSVerifierError::TransactionMismatch { + expected: expect_tx, + found: found_tx, + }); + } + } + Ok(log) + } +} + +impl EthereumUTSVerifierError { + /// The error indicates this attestation is invalid and cannot be verified. + #[inline] + pub fn is_fatal(&self) -> bool { + matches!( + self, + EthereumUTSVerifierError::InvalidLength | EthereumUTSVerifierError::NotFound + ) + } + + /// The error indicates this attestation is valid but not attested by the expected contract or transaction. + #[inline] + pub fn is_mismatch(&self) -> bool { + matches!( + self, + EthereumUTSVerifierError::ContractMismatch { .. } + | EthereumUTSVerifierError::TransactionMismatch { .. } + ) + } + + /// The error indicates this attestation may be valid but cannot be verified at the moment. + #[inline] + pub fn should_retry(&self) -> bool { + matches!(self, EthereumUTSVerifierError::Rpc(_)) + } + + /// The error indicates this attestation may be valid but the provider is not suitable for verifying it. + #[inline] + pub fn is_wrong_provider(&self) -> bool { + matches!(self, EthereumUTSVerifierError::ChainIdMismatch) + } +} diff --git a/crates/journal/Cargo.toml b/crates/journal/Cargo.toml new file mode 100644 index 0000000..11ae72b --- /dev/null +++ b/crates/journal/Cargo.toml @@ -0,0 +1,22 @@ +[package] +authors.workspace = true +description = "High performance append-only journal for UTS" +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +name = "uts-journal" +repository.workspace = true +version.workspace = true + +[dependencies] +thiserror = { workspace = true } +tracing = { workspace = true } + +[dev-dependencies] +eyre = { workspace = true } +tempfile = { workspace = true } +tokio = { workspace = true, features = ["full"] } + +[lints] +workspace = true diff --git a/crates/journal/README.md b/crates/journal/README.md new file mode 100644 index 0000000..39aecea --- /dev/null +++ b/crates/journal/README.md @@ -0,0 +1,33 @@ +# uts-journal + +**High-performance, append-only journal designed for the Universal Timestamps (UTS) protocol.** + +`uts-journal` is an embedded, lock-free, ring-buffer-based Write-Ahead Log (WAL) implemented in Rust. +It is designed to handle extremely high throughput (target: 1M+ TPS) with sub-millisecond durability guarantees, +specifically optimized for the low-latency requirements of timestamping services. + +## Architecture & Design Rationale + +### Why not Kafka? + +While distributed message queues are the standard for microservices decoupling, `uts-journal` was built to satisfy a +specific set of constraints where generic solutions fall short: + +| Feature | Distributed MQ (e.g., Kafka) | uts-journal | +|------------------------|--------------------------------|--------------------------------------| +| **Topology** | External Service (Networked) | Embedded Library (In-Process) | +| **Durability Latency** | Network RTT + Disk IO (~2-5ms) | Disk IO only (<1ms possible) | +| **Throughput Control** | Limited by Network Bandwidth | Limited by PCIe/Memory Bandwidth | +| **Consistency** | Eventual / ISync Replicas | Strong Local Consistency | +| **Resource Usage** | Heavy (JVM/Broker overhead) | Minimal (Zero-copy, Zero-allocation) | + +### The "Request-Persist-Return" Loop + +UTS requires a synchronous acknowledgement model: **HTTP POST → Sequence → Persist → Return**. +To achieve 1M TPS under this constraint: + +1. **Group Commit:** `uts-journal` allows thousands of concurrent write requests to queue in the ring buffer. +2. **Batched IO:** A dedicated WAL worker thread detects pending writes and flushes them to stable storage in minimal syscalls. +3. **Wake-on-Persist:** Once the persist boundary advances, the worker efficiently wakes only the relevant `Waker`s associated with the committed slots. + +This architecture converts random IOPS into sequential throughput, allowing the system to handle massive concurrency on a single node. diff --git a/crates/journal/src/checkpoint.rs b/crates/journal/src/checkpoint.rs new file mode 100644 index 0000000..b5108de --- /dev/null +++ b/crates/journal/src/checkpoint.rs @@ -0,0 +1,181 @@ +use std::{ + fs, + fs::File, + io, + io::Read, + path::{Path, PathBuf}, + sync::{Mutex, atomic::AtomicU64}, + time::{Duration, Instant}, +}; + +/// Configuration for checkpointing mechanism. +#[derive(Debug, Clone)] +pub struct CheckpointConfig { + /// Path to the checkpoint file where the last persisted index is stored. This file will be + /// created if it does not exist, and updated atomically when a new checkpoint is flushed to + /// disk. + pub path: PathBuf, + /// Flush interval for checkpointing, used to determine when to flush the persisted checkpoint + /// to disk. + pub min_interval: Duration, + /// Flush threshold for checkpointing, used to determine when to flush the persisted checkpoint + /// to disk based on the number of new entries since the last flush. + pub min_advance: u64, + /// Suffix for temporary checkpoint file when performing checkpointing. The checkpoint will be + /// atomically renamed to the final checkpoint file after flush. + pub temp_suffix: &'static str, +} + +impl Default for CheckpointConfig { + fn default() -> Self { + Self { + path: PathBuf::from("checkpoint.meta"), + min_interval: Duration::from_secs(1), + min_advance: 128, + temp_suffix: ".tmp", + } + } +} + +/// Checkpointing for tracking `consumed_index`. +#[derive(Debug)] +pub struct Checkpoint { + config: CheckpointConfig, + current: AtomicU64, + inner: Mutex, +} + +#[derive(Debug)] +struct CheckpointInner { + temp_path: PathBuf, + + /// The index of the last persisted checkpoint. This is updated when a new checkpoint is + /// flushed to disk. + persisted_index: u64, + last_flush_time: Instant, +} + +impl Checkpoint { + /// Creates a new checkpoint instance with the given configuration. This will attempt to recover + /// the last persisted checkpoint index from disk, and initialize the internal state accordingly. + #[instrument(skip_all, err)] + pub fn new(config: CheckpointConfig) -> io::Result { + let parent = config.path.parent().ok_or(io::Error::new( + io::ErrorKind::NotFound, + "parent directory does not exist", + ))?; + fs::create_dir_all(parent)?; + + let mut inner = CheckpointInner { + temp_path: config.path.with_added_extension(config.temp_suffix), + + persisted_index: 0, + last_flush_time: Instant::now(), + }; + let recovered = inner.recover(&config)?; + + Ok(Self { + config, + current: AtomicU64::new(recovered), + inner: Mutex::new(inner), + }) + } + + /// Returns the last persisted checkpoint index, which is updated when a new checkpoint is + /// flushed to disk. This requires acquiring the lock on the inner state, and may lag behind + /// the current index until the next flush to disk. + #[instrument(skip(self), ret)] + pub fn persisted_index(&self) -> u64 { + let inner = self.inner.lock().unwrap(); + inner.persisted_index + } + + /// Returns the current checkpoint index, which may be ahead of the last persisted index. + /// This is updated atomically when `update` is called, and can be read without acquiring the + /// lock on the inner state. + /// + /// The persisted index may lag behind the current index until the next flush to disk. + #[instrument(skip(self), ret)] + pub fn current_index(&self) -> u64 { + self.current.load(std::sync::atomic::Ordering::Acquire) + } + + /// Updates the current checkpoint index. This will trigger a flush to disk if: + /// - the new index has advanced by at least `min_advance` since the last persisted index + /// - or, the time since the last flush has exceeded `min_interval`. + #[instrument(skip(self), err)] + pub fn update(&self, new_index: u64) -> io::Result<()> { + let mut inner = self.inner.lock().unwrap(); + self.current + .store(new_index, std::sync::atomic::Ordering::Release); + inner.update(new_index, &self.config, false) + } + + /// Flush the current checkpoint to disk immediately, regardless of the configured flush + /// interval and flush threshold. + #[instrument(skip(self), err)] + pub fn flush(&self) -> io::Result<()> { + let mut inner = self.inner.lock().unwrap(); + let new_index = self.current.load(std::sync::atomic::Ordering::Acquire); + inner.update(new_index, &self.config, true) + } +} + +impl CheckpointInner { + #[instrument(skip(self), err)] + fn recover(&mut self, config: &CheckpointConfig) -> io::Result { + // Try to recover from the temp checkpoint file first + if let Ok(index) = recover_from_disk(&self.temp_path) { + self.persisted_index = index; + fs::rename(&self.temp_path, &config.path)?; + } else { + match recover_from_disk(&config.path) { + Ok(index) => self.persisted_index = index, + Err(e) if e.kind() == io::ErrorKind::NotFound => self.persisted_index = 0, + Err(e) => return Err(e), + } + } + Ok(self.persisted_index) + } + + fn update( + &mut self, + new_index: u64, + config: &CheckpointConfig, + forced: bool, + ) -> io::Result<()> { + if new_index <= self.persisted_index { + warn!( + "New checkpoint index {} is not greater than persisted index {}, skipping update", + new_index, self.persisted_index + ); + return Ok(()); + } + + let now = Instant::now(); + let should_flush = new_index - self.persisted_index >= config.min_advance; + let timeouts = now.duration_since(self.last_flush_time) >= config.min_interval; + if forced || should_flush || timeouts { + fs::write(&self.temp_path, &new_index.to_le_bytes())?; + fs::rename(&self.temp_path, &config.path)?; + self.persisted_index = new_index; + self.last_flush_time = now; + } + Ok(()) + } +} + +fn recover_from_disk(path: &Path) -> io::Result { + let mut file = File::open(&path)?; + let metadata = file.metadata()?; + if metadata.len() != 8 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "invalid checkpoint file", + )); + } + let mut buf = [0u8; 8]; + file.read_exact(&mut buf)?; + let index = u64::from_le_bytes(buf); + Ok(index) +} diff --git a/crates/journal/src/error.rs b/crates/journal/src/error.rs new file mode 100644 index 0000000..3355ad5 --- /dev/null +++ b/crates/journal/src/error.rs @@ -0,0 +1,11 @@ +/// Error indicating that the journal buffer is not available now. +#[derive(Debug, thiserror::Error)] +pub enum JournalUnavailable { + /// The journal is shutting down, no new entries can be accepted. + #[error("journal is shutting down")] + Shutdown, + /// The journal buffer is full, new entries cannot be accepted until some entries are consumed + /// and the buffer has space. + #[error("journal buffer is full")] + Full, +} diff --git a/crates/journal/src/lib.rs b/crates/journal/src/lib.rs new file mode 100644 index 0000000..34c28b3 --- /dev/null +++ b/crates/journal/src/lib.rs @@ -0,0 +1,502 @@ +//! Journal implementation for UTS +#[macro_use] +extern crate tracing; + +use crate::{ + checkpoint::{Checkpoint, CheckpointConfig}, + error::JournalUnavailable, + reader::JournalReader, + wal::Wal, +}; +use std::{ + cell::UnsafeCell, + fmt, io, + ops::Deref, + path::PathBuf, + pin::Pin, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, AtomicU64, Ordering}, + }, + task::{Poll, Waker}, +}; + +/// Checkpointing +pub mod checkpoint; +/// Error types. +pub mod error; +/// Journal reader. +pub mod reader; +/// Write-Ahead Log backend. +pub mod wal; + +/// Configuration for the journal. +#[derive(Debug, Clone)] +pub struct JournalConfig { + /// Configuration for the consumer checkpoint, which tracks the `consumed_index` of the journal. + pub consumer_checkpoint: CheckpointConfig, + /// Directory for the write-ahead log (WAL) backend, which persists committed entries to disk + /// for durability and crash recovery, allowing the journal to recover from crashes without data + /// loss. + pub wal_dir: PathBuf, +} + +impl Default for JournalConfig { + fn default() -> Self { + Self { + consumer_checkpoint: CheckpointConfig::default(), + wal_dir: PathBuf::from("wal"), + } + } +} + +/// An `At-Least-Once` journal for storing fixed-size entries in a ring buffer. +/// +/// All index here are monotonic u64, wrapping around on overflow. +/// +/// Following invariants are maintained: +/// `consumed_index` <= `persisted_index` <= `filled_index` <= `write_index`. +#[derive(Clone)] +pub struct Journal { + inner: Arc>, + /// Wal backend for recovery. + wal: Wal<{ ENTRY_SIZE }>, +} + +pub(crate) struct JournalInner { + /// The ring buffer storing the entries. + /// The capacity of the ring buffer, **MUST** be power of two. + buffer: Box<[UnsafeCell<[u8; ENTRY_SIZE]>]>, + /// The co-ring buffer storing the wakers. + /// The capacity of the ring buffer, **MUST** be power of two. + waker_buffer: Box<[WakerEntry]>, + /// Mask for indexing into the ring buffer. + index_mask: u64, + /// Next Write Position, aka: + /// - Total entries reserved count. + /// - Position to write the next entry to. + write_index: AtomicU64, + /// Filled Boundary, aka: + /// - Total entries that have been fully written to the ring buffer. + /// - Advanced in order after each writer finishes copying data into its reserved slot. + /// - The WAL worker uses this (not `write_index`) to determine how far it can safely read. + filled_index: AtomicU64, + /// WAL Committed Boundary, aka.: + /// - Total committed entries count. + /// - Position has not yet been persisted to durable storage. + persisted_index: AtomicU64, + /// Free Boundary, aka.: + /// - Total consumed entries count. + /// - Position that has not yet been consumed by readers. + consumed_checkpoint: Checkpoint, + /// Whether a reader has taken ownership of this journal. + reader_taken: AtomicBool, + /// Waker for the consumer to notify new persisted entries. + consumer_wait: Mutex>, + /// Shutdown flag + shutdown: AtomicBool, +} + +unsafe impl Sync for JournalInner {} +unsafe impl Send for JournalInner {} + +impl fmt::Debug for Journal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Journal").finish() + } +} + +impl Journal { + /// Create a new journal with the specified capacity. + /// + /// The capacity will be rounded up to the next power of two. + pub fn with_capacity(capacity: usize) -> io::Result { + Self::with_capacity_and_config(capacity, JournalConfig::default()) + } + + /// Create a new journal with the specified capacity. + /// + /// The capacity will be rounded up to the next power of two. + pub fn with_capacity_and_config(capacity: usize, config: JournalConfig) -> io::Result { + let capacity = capacity.next_power_of_two(); + let index_mask = capacity as u64 - 1; + + let mut buffer = Vec::with_capacity(capacity); + buffer.resize_with(capacity, || UnsafeCell::new([0u8; ENTRY_SIZE])); + let buffer = buffer.into_boxed_slice(); + + let mut waker_buffer = Vec::with_capacity(capacity); + waker_buffer.resize_with(capacity, Default::default); + let waker_buffer = waker_buffer.into_boxed_slice(); + + let inner = Arc::new(JournalInner { + buffer, + waker_buffer, + index_mask, + write_index: AtomicU64::new(0), + filled_index: AtomicU64::new(0), + persisted_index: AtomicU64::new(0), + consumed_checkpoint: Checkpoint::new(config.consumer_checkpoint)?, + reader_taken: AtomicBool::new(false), + consumer_wait: Mutex::new(None), + shutdown: AtomicBool::new(false), + }); + + let wal = Wal::new(config.wal_dir, inner.clone())?; + + Ok(Self { inner, wal }) + } + + /// Get the capacity of the journal. + #[inline] + fn capacity(&self) -> usize { + self.inner.capacity() + } + + /// Acquires a reader for this journal. + /// + /// # Panics + /// + /// Panics if a reader is already taken. + pub fn reader(&self) -> JournalReader { + self.try_reader().expect("Journal reader already taken") + } + + /// Try acquires a reader for this journal. + /// + /// If a reader is already taken, returns None. + pub fn try_reader(&self) -> Option> { + if self.inner.reader_taken.swap(true, Ordering::AcqRel) { + return None; + } + + Some(JournalReader::new(self.inner.clone())) + } + + /// Commit a new entry to the journal. + /// + /// # Panics + /// + /// Panics if: + /// - the journal is full. + /// - the journal is shut down. + pub fn commit(&self, data: &[u8; ENTRY_SIZE]) -> CommitFuture<'_, ENTRY_SIZE> { + self.try_commit(data).expect("Journal buffer is full") + } + + /// Try commit a new entry to the journal. + /// + /// Returns a future that resolves when the entry has been safely persisted. + /// Returns `BufferFull` error if the journal is full. + pub fn try_commit( + &self, + data: &[u8; ENTRY_SIZE], + ) -> Result, JournalUnavailable> { + if self.inner.shutdown.load(Ordering::Acquire) { + return Err(JournalUnavailable::Shutdown); + } + + let mut current_written = self.inner.write_index.load(Ordering::Relaxed); + loop { + // 1. Check if there is space in the buffer. + let consumed = self.inner.consumed_checkpoint.current_index(); + if current_written.wrapping_sub(consumed) >= self.capacity() as u64 { + return Err(JournalUnavailable::Full); + } + + // 2. Try to reserve a slot. + match self.inner.write_index.compare_exchange_weak( + current_written, + current_written.wrapping_add(1), + Ordering::AcqRel, + Ordering::Relaxed, + ) { + Ok(_) => break, + Err(actual) => current_written = actual, + } + } + + // 3. Write the data to the slot. + let slot = unsafe { &mut *self.data_slot_ptr(current_written) }; + slot.copy_from_slice(data); + + // 4. Publish the filled slot. + // Spin-wait until all prior slots are filled, then advance `filled_index`. + // The Release ordering ensures the slot write above is visible to the WAL worker + // before it reads `filled_index`. + while self + .inner + .filled_index + .compare_exchange_weak( + current_written, + current_written.wrapping_add(1), + Ordering::Release, + Ordering::Relaxed, + ) + .is_err() + { + std::hint::spin_loop(); + } + + // 5. Notify WAL worker if needed. + let committed = self.inner.persisted_index.load(Ordering::Relaxed); + // Explain: If there is no pending committed entry before ours, + // the WAL worker may be sleeping, so we need to wake it up. + if current_written == committed { + // Notify the WAL worker thread to persist new entries. + self.wal.unpark(); + } + + Ok(CommitFuture { + journal: self, + slot: current_written, + active_waker: None, + }) + } + + /// Shut down the journal, flushing all checkpoints and shutting down the WAL. + pub fn shutdown(&self) -> io::Result<()> { + self.inner.shutdown.store(true, Ordering::SeqCst); + + self.inner.consumed_checkpoint.flush()?; + self.wal.shutdown(); + Ok(()) + } + + /// Get a mut ptr to the slot at the given index. + #[inline] + fn data_slot_ptr(&self, index: u64) -> *mut [u8; ENTRY_SIZE] { + self.inner.data_slot_ptr(index) + } + + /// Get a ref to the waker entry at the given index. + #[inline] + fn waker_slot(&self, index: u64) -> &WakerEntry { + self.inner.waker_slot(index) + } +} + +impl JournalInner { + /// Get the capacity of the journal. + #[inline] + fn capacity(&self) -> usize { + self.buffer.len() + } + + /// Get a mut ptr to the slot at the given index. + #[inline] + const fn data_slot_ptr(&self, index: u64) -> *mut [u8; ENTRY_SIZE] { + let slot_idx = index & self.index_mask; + self.buffer[slot_idx as usize].get() + } + + /// Get a ref to the waker entry at the given index. + #[inline] + const fn waker_slot(&self, index: u64) -> &WakerEntry { + let slot_idx = index & self.index_mask; + &self.waker_buffer[slot_idx as usize] + } +} + +/// Future returned by `Journal::commit` representing the commit operation. +/// The future resolves when the entry has been safely persisted. +#[derive(Debug)] +pub struct CommitFuture<'a, const ENTRY_SIZE: usize> { + journal: &'a Journal, + slot: u64, + /// Whether the waker has been registered. + active_waker: Option, +} + +impl Future for CommitFuture<'_, ENTRY_SIZE> { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + if self.journal.inner.persisted_index.load(Ordering::Acquire) > self.slot { + return Poll::Ready(()); + } + + let should_register = match &self.active_waker { + None => true, + // waker changed, need to update, rare case + Some(w) => !w.will_wake(cx.waker()), + }; + + if should_register { + let entry = self.journal.waker_slot(self.slot); + let mut guard = entry.lock().expect("Mutex poisoned"); + + if self.journal.inner.persisted_index.load(Ordering::Acquire) > self.slot { + return Poll::Ready(()); + } + + *guard = Some(cx.waker().clone()); + self.active_waker = Some(cx.waker().clone()); + } + + if self.journal.inner.persisted_index.load(Ordering::Acquire) > self.slot { + return Poll::Ready(()); + } + + Poll::Pending + } +} + +/// A consumer wait entry. +struct ConsumerWait { + waker: Waker, + target_index: u64, +} + +/// A waker entry in the co-ring buffer. +/// +/// Aligned to cache line size to prevent false sharing. +#[derive(Default)] +#[repr(C, align(64))] +struct WakerEntry(Mutex>); + +impl Deref for WakerEntry { + type Target = Mutex>; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(test)] +pub(crate) mod tests { + use crate::checkpoint::CheckpointConfig; + + pub const ENTRY_SIZE: usize = 8; + pub const TEST_DATA: &[[u8; ENTRY_SIZE]] = &[ + [0u8; ENTRY_SIZE], + [1u8; ENTRY_SIZE], + [2u8; ENTRY_SIZE], + [3u8; ENTRY_SIZE], + [4u8; ENTRY_SIZE], + [5u8; ENTRY_SIZE], + [6u8; ENTRY_SIZE], + [7u8; ENTRY_SIZE], + [8u8; ENTRY_SIZE], + [9u8; ENTRY_SIZE], + ]; + pub type Journal = crate::Journal; + + /// Create a journal with an isolated temporary directory for WAL and checkpoint files. + /// Returns the journal and the temp dir guard (must be kept alive for the test duration). + pub fn test_journal(capacity: usize) -> (Journal, tempfile::TempDir) { + let tmp = tempfile::tempdir().expect("failed to create temp dir"); + let config = crate::JournalConfig { + consumer_checkpoint: CheckpointConfig { + path: tmp.path().join("checkpoint.meta"), + ..Default::default() + }, + wal_dir: tmp.path().join("wal"), + }; + let journal = + Journal::with_capacity_and_config(capacity, config).expect("failed to create journal"); + (journal, tmp) + } + + #[tokio::test(flavor = "current_thread")] + async fn try_reader_is_exclusive() -> eyre::Result<()> { + let (journal, _tmp) = test_journal(2); + + let reader = journal.try_reader().unwrap(); + + assert!( + journal.try_reader().is_none(), + "second reader acquisition should fail" + ); + + drop(reader); + assert!( + journal.try_reader().is_some(), + "reader acquisition should succeed after drop" + ); + + journal.shutdown()?; + Ok(()) + } + + #[tokio::test(flavor = "current_thread")] + async fn commit_and_read_round_trip() -> eyre::Result<()> { + let (journal, _tmp) = test_journal(4); + let mut reader = journal.reader(); + + journal.commit(&TEST_DATA[0]).await; + journal.commit(&TEST_DATA[1]).await; + + { + let entries = reader.read(2); + assert_eq!(entries.len(), 2); + assert_eq!(entries[0], TEST_DATA[0]); + assert_eq!(entries[1], TEST_DATA[1]); + } + + reader.commit()?; + assert_eq!(reader.available(), 0); + journal.shutdown()?; + Ok(()) + } + + #[tokio::test(flavor = "current_thread")] + async fn commit_returns_error_when_full() -> eyre::Result<()> { + let (journal, _tmp) = test_journal(2); + + journal.commit(&TEST_DATA[1]).await; + journal.commit(&TEST_DATA[2]).await; + + let err = journal + .try_commit(&TEST_DATA[3]) + .expect_err("buffer should report full on third commit"); + assert!( + matches!(err, crate::error::JournalUnavailable::Full), + "expected Full, got {err:?}" + ); + journal.shutdown()?; + Ok(()) + } + + #[tokio::test(flavor = "current_thread")] + async fn reader_handles_wrap_around_reads() -> eyre::Result<()> { + let (journal, _tmp) = test_journal(4); + let mut reader = journal.reader(); + + for entry in TEST_DATA.iter().take(4) { + journal.commit(entry).await; + } + + { + let entries = reader.read(2); + assert_eq!(entries.len(), 2); + assert_eq!(entries[0], TEST_DATA[0]); + assert_eq!(entries[1], TEST_DATA[1]); + } + reader.commit()?; + + for entry in TEST_DATA.iter().skip(4).take(2) { + journal.commit(entry).await; + } + + { + let entries = reader.read(4); + assert_eq!(entries.len(), 2); + assert_eq!(entries[0], TEST_DATA[2]); + assert_eq!(entries[1], TEST_DATA[3]); + } + + { + let entries = reader.read(4); + assert_eq!(entries.len(), 2); + assert_eq!(entries[0], TEST_DATA[4]); + assert_eq!(entries[1], TEST_DATA[5]); + } + + reader.commit()?; + assert_eq!(reader.available(), 0); + journal.shutdown()?; + Ok(()) + } +} diff --git a/crates/journal/src/reader.rs b/crates/journal/src/reader.rs new file mode 100644 index 0000000..9bf0702 --- /dev/null +++ b/crates/journal/src/reader.rs @@ -0,0 +1,260 @@ +use crate::{ConsumerWait, JournalInner}; +use std::{ + fmt, io, + pin::Pin, + sync::{Arc, atomic::Ordering}, + task::{Context, Poll}, +}; + +/// A reader for consuming settled entries from the journal. +/// +/// Reader **WON'T** advance the shared consumed boundary until `commit()` is called. +pub struct JournalReader { + journal: Arc>, + consumed: u64, +} + +impl fmt::Debug for JournalReader { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("JournalReader") + .field("consumed", &self.consumed) + .finish() + } +} + +impl Drop for JournalReader { + fn drop(&mut self) { + self.journal.reader_taken.store(false, Ordering::Release); + } +} + +impl JournalReader { + pub(super) fn new(journal: Arc>) -> Self { + let consumed = journal.consumed_checkpoint.current_index(); + Self { journal, consumed } + } + + /// Returns the number of available entries that are settled but not yet consumed by this reader. + #[inline] + pub fn available(&self) -> usize { + if self.journal.shutdown.load(Ordering::Acquire) { + return 0; + } + let persisted = self.journal.persisted_index.load(Ordering::Acquire); + persisted.wrapping_sub(self.consumed) as usize + } + + /// Wait until at least `min` entries are available. + pub async fn wait_at_least(&mut self, min: usize) { + if self.available() >= min { + return; + } + + let target_index = self.consumed.wrapping_add(min as u64); + { + // panics if the target_index exceeds buffer size, otherwise we might wait forever + // this happens if: + // - asks for more entries than the buffer can hold + // - didn't commit previously read entries, then asks for more than new entries than the buffer can hold + // this is considered a misuse of the API / design flaw in the caller, so we panics + let journal_buffer_size = self.journal.buffer.len() as u64; + let current_consumed = self.journal.consumed_checkpoint.current_index(); + let max_possible_target = current_consumed.wrapping_add(journal_buffer_size); + if target_index > max_possible_target { + panic!( + "requested ({target_index}) exceeds max possible ({max_possible_target}): journal.buffer.len()={journal_buffer_size}, journal.consumed_index={current_consumed}" + ); + } + } + + // Slow path + struct WaitForBatch<'a, const ENTRY_SIZE: usize> { + reader: &'a JournalReader, + target_index: u64, + } + + impl Future for WaitForBatch<'_, ENTRY_SIZE> { + type Output = (); + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { + if self.reader.journal.persisted_index.load(Ordering::Acquire) >= self.target_index + { + return Poll::Ready(()); + } + + let mut guard = self + .reader + .journal + .consumer_wait + .lock() + .expect("Mutex poisoned"); + if self.reader.journal.persisted_index.load(Ordering::Acquire) >= self.target_index + { + return Poll::Ready(()); + } + *guard = Some(ConsumerWait { + waker: cx.waker().clone(), + target_index: self.target_index, + }); + + Poll::Pending + } + } + + WaitForBatch { + reader: self, + target_index, + } + .await; + } + + /// Read available entries, up to `max`. + /// Bumps the internal consumed index by the number of entries yielded. + /// + /// Caller is responsible for calling `commit()` after processing the entries. + pub fn read(&mut self, max: usize) -> &[[u8; ENTRY_SIZE]] { + let available = self.available(); + if available == 0 { + return &[]; + } + + let count = available.min(max); + let start_idx = self.consumed; + + // handle wrap-around + let buffer_len = self.journal.buffer.len(); + let slot_idx = (start_idx & self.journal.index_mask) as usize; + let continuous_len = count.min(buffer_len - slot_idx); + + // push local consumed index + self.consumed += continuous_len as u64; + + // return slice + let ptr = self.journal.buffer[slot_idx].get(); + // SAFETY: bounds checked above + unsafe { std::slice::from_raw_parts(ptr, continuous_len) } + } + + /// Commit current consumed index. + pub fn commit(&mut self) -> io::Result<()> { + self.journal.consumed_checkpoint.update(self.consumed) + } +} + +#[cfg(test)] +mod tests { + use crate::tests::*; + use tokio::time::{Duration, sleep, timeout}; + + #[tokio::test(flavor = "current_thread")] + async fn available_tracks_persisted_entries() -> eyre::Result<()> { + let (journal, _tmp) = test_journal(4); + let mut reader = journal.reader(); + + assert_eq!(reader.available(), 0); + + journal.commit(&TEST_DATA[0]).await; + assert_eq!(reader.available(), 1); + + journal.commit(&TEST_DATA[1]).await; + assert_eq!(reader.available(), 2); + + let slice = reader.read(1); + assert_eq!(slice.len(), 1); + assert_eq!(reader.available(), 1); + journal.shutdown()?; + Ok(()) + } + + #[tokio::test(flavor = "current_thread")] + async fn commit_updates_shared_consumed_boundary() -> eyre::Result<()> { + let (journal, _tmp) = test_journal(4); + let mut reader = journal.reader(); + + for entry in TEST_DATA.iter().take(3) { + journal.commit(entry).await; + } + + let slice = reader.read(2); + assert_eq!(slice.len(), 2); + assert_eq!(reader.available(), 1); + assert_eq!( + reader.journal.consumed_checkpoint.current_index(), + 0, + "global consumed boundary should not advance before commit", + ); + + reader.commit()?; + assert_eq!(reader.journal.consumed_checkpoint.current_index(), 2); + journal.shutdown()?; + Ok(()) + } + + #[tokio::test(flavor = "current_thread")] + async fn wait_at_least_resumes_after_persistence() -> eyre::Result<()> { + let (journal, _tmp) = test_journal(4); + let mut reader = journal.reader(); + + let journal_clone = journal.clone(); + let task = tokio::spawn(async move { + sleep(Duration::from_millis(5)).await; + journal_clone.commit(&TEST_DATA[0]).await; + }); + + reader.wait_at_least(1).await; + assert_eq!(reader.available(), 1); + + task.await?; + journal.shutdown()?; + Ok(()) + } + + #[tokio::test(flavor = "current_thread")] + async fn wait_at_least_waits_for_correct_count() -> eyre::Result<()> { + let (journal, _tmp) = test_journal(4); + let mut reader = journal.reader(); + + let journal_clone = journal.clone(); + let task = tokio::spawn(async move { + for entry in TEST_DATA.iter().take(4) { + journal_clone.commit(entry).await; + sleep(Duration::from_millis(5)).await; + } + }); + + timeout(Duration::from_secs(10), reader.wait_at_least(3)).await?; + assert!(reader.available() >= 3); + + task.await?; + journal.shutdown()?; + Ok(()) + } + + #[tokio::test(flavor = "current_thread")] + #[should_panic( + expected = "requested (5) exceeds max possible (4): journal.buffer.len()=4, journal.consumed_index=0" + )] + async fn wait_at_least_exceeds_buffer_size() { + let (journal, _tmp) = test_journal(4); + let mut reader = journal.reader(); + + timeout(Duration::from_secs(1), reader.wait_at_least(5)) + .await + .unwrap(); + } + + #[tokio::test(flavor = "current_thread")] + #[should_panic( + expected = "requested (5) exceeds max possible (4): journal.buffer.len()=4, journal.consumed_index=0" + )] + async fn wait_at_least_dirty_read_exceeds_available() { + let (journal, _tmp) = test_journal(4); + journal.commit(&TEST_DATA[0]).await; + + let mut reader = journal.reader(); + reader.read(1); + + timeout(Duration::from_secs(1), reader.wait_at_least(4)) + .await + .unwrap(); + } +} diff --git a/crates/journal/src/wal.rs b/crates/journal/src/wal.rs new file mode 100644 index 0000000..97666b6 --- /dev/null +++ b/crates/journal/src/wal.rs @@ -0,0 +1,856 @@ +use crate::JournalInner; +use std::{ + fmt, + fmt::Formatter, + fs, + fs::{File, OpenOptions}, + io, + io::{BufWriter, Read, Seek, SeekFrom, Write}, + path::{Path, PathBuf}, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, + }, + thread, + thread::JoinHandle, +}; + +const MAX_SPIN: usize = 100; +const MAX_IO_BATCH: u64 = 128; + +/// Write-Ahead Log +/// +/// Busy-Wait + Parking when there's no work to do. +/// +/// Using segmented log files named as `{base_dir}/{segment_id}.wal`, where `segment_id` is a +/// monotonically increasing integer. +/// +/// Each segment file contains a fixed number of entries +/// (at least to be the size of the journal buffer) to simplify recovery and management. +#[derive(Clone)] +pub struct Wal { + inner: Arc>, +} + +struct WalInner { + worker: Mutex>>, + journal: Arc>, + shutdown_flag: Arc, +} + +impl Drop for WalInner { + fn drop(&mut self) { + // Signal the WAL worker to exit if it hasn't been shut down yet. + // This prevents orphaned worker threads from spinning after the journal is dropped. + if !self.shutdown_flag.swap(true, Ordering::AcqRel) { + if let Some(worker) = self.worker.lock().unwrap().as_ref() { + worker.thread().unpark(); + } + } + } +} + +impl fmt::Debug for Wal { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("Wal") + .field( + "write_index", + &self.inner.journal.write_index.load(Ordering::Acquire), + ) + .field( + "persisted_index", + &self.inner.journal.persisted_index.load(Ordering::Acquire), + ) + .finish() + } +} + +impl Wal { + /// Create a new WAL instance with the given base directory for storing log segments and + /// a reference to the journal. This will recover existing segments from the base directory, + /// and start a background worker thread to handle persistence of log entries. + pub(crate) fn new>( + base_dir: P, + journal: Arc>, + ) -> io::Result { + let base_dir = base_dir.as_ref(); + fs::create_dir_all(base_dir)?; + if !base_dir.is_dir() { + return Err(io::Error::new( + io::ErrorKind::Other, + "Base path is not a directory", + )); + } + + let shutdown_flag = Arc::new(AtomicBool::new(false)); + + let current_segment_id = recover(base_dir, &journal)?; + let mut current_file = open_segment_file(base_dir, current_segment_id)?; + current_file.seek(SeekFrom::End(0))?; + + let worker = WalWorker { + current_segment_id, + current_file: BufWriter::new(current_file), + persisted_index: journal.persisted_index.load(Ordering::Acquire), + + base_dir: base_dir.to_path_buf(), + journal: journal.clone(), + shutdown_flag: shutdown_flag.clone(), + }; + + let handle = thread::Builder::new() + .name("wal-worker".to_string()) + .spawn(move || { + let mut worker = worker; + worker.run() + }) + .expect("Failed to spawn WAL worker thread"); + + let inner = WalInner { + worker: Mutex::new(Some(handle)), + journal, + shutdown_flag, + }; + + Ok(Self { + inner: Arc::new(inner), + }) + } + + /// Unpark the WAL worker thread to wake it up if it's parked. + /// This should be called after new entries are written to the journal, to ensure that the + /// worker thread can persist them in a timely manner. + pub fn unpark(&self) { + if self.inner.shutdown_flag.load(Ordering::Acquire) { + return; + } + self.inner + .worker + .lock() + .unwrap() + .as_ref() + .expect("WAL worker thread should be running") + .thread() + .unpark(); + } + + /// Shut down the WAL worker thread gracefully. This will set the shutdown flag, unpark the + /// worker thread if it's parked, and wait for it to finish. + pub fn shutdown(&self) { + if self.inner.shutdown_flag.swap(true, Ordering::AcqRel) { + // already shutdown + return; + } + let worker = self + .inner + .worker + .lock() + .unwrap() + .take() + .expect("WAL worker thread should be running"); + worker.thread().unpark(); + worker.join().expect("Failed to join WAL worker thread"); + } +} + +struct WalWorker { + current_segment_id: u64, + current_file: BufWriter, + persisted_index: u64, + + base_dir: PathBuf, + journal: Arc>, + shutdown_flag: Arc, +} + +impl WalWorker { + fn run(&mut self) { + let mut spin_count = 0; + + while !self.shutdown_flag.load(Ordering::Acquire) { + let filled = self.journal.filled_index.load(Ordering::Acquire); + let available = filled.wrapping_sub(self.persisted_index); + + if available > 0 { + spin_count = 0; + + let new_persisted_index = self + .write(available.min(MAX_IO_BATCH)) + .expect("Failed to write WAL entries"); + + self.notify_writer(new_persisted_index); + self.update_index(new_persisted_index); + self.notify_consumer(); + + continue; + } + + // no new data to persist, spin for a while before parking + if spin_count <= MAX_SPIN { + spin_count += 1; + std::hint::spin_loop(); + continue; + } + + // park until unparked by a new commit + thread::park(); + } + + // cleanup before exiting: persist any remaining entries + let filled = self.journal.filled_index.load(Ordering::Acquire); + let available = filled.wrapping_sub(self.persisted_index); + let new_persisted_index = self + .write(available.min(MAX_IO_BATCH)) + .expect("Failed to write WAL entries"); + + self.notify_writer(new_persisted_index); + self.update_index(new_persisted_index); + self.notify_consumer(); + } + + /// update the persisted index + fn update_index(&mut self, new_persisted_index: u64) { + self.persisted_index = new_persisted_index; + self.journal + .persisted_index + .store(self.persisted_index, Ordering::Release); + } + + /// Write `n` entries from the journal to the current WAL segment file, rotating files as needed. + fn write(&mut self, n: u64) -> io::Result { + let segment_size: u64 = self.journal.capacity() as u64; + let new_persisted_index = self.persisted_index + n; + + // write ALL available entries to segment files, rotating files as needed + for i in self.persisted_index..new_persisted_index { + let seg_id = i / segment_size; + if seg_id != self.current_segment_id { + // rotate to new segment file + self.current_segment_id = seg_id; + let new_file = open_segment_file(&self.base_dir, seg_id)?; + self.current_file = BufWriter::new(new_file); + let base_dir = self.base_dir.clone(); + thread::spawn(move || truncate_old_segments(base_dir, seg_id)); + } + + self.current_file + .write_all(unsafe { &*self.journal.data_slot_ptr(i) })?; + } + + self.current_file.flush()?; + self.current_file.get_ref().sync_all()?; + Ok(new_persisted_index) + } + + fn notify_writer(&mut self, new_persisted_index: u64) { + // notify waiters only after data is persisted + for i in self.persisted_index..new_persisted_index { + let entry = self.journal.waker_slot(i); + if let Some(waker) = entry.lock().unwrap().take() { + waker.wake(); + } + } + } + + fn notify_consumer(&mut self) { + // notify consumer if needed + let mut guard = self.journal.consumer_wait.lock().unwrap(); + if let Some(wait) = guard.as_ref() { + // Only wake if the new_persisted_index is reached + if self.persisted_index >= wait.target_index { + guard.take().unwrap().waker.wake(); + } + } + } +} + +fn recover( + base_dir: &Path, + journal: &JournalInner, +) -> io::Result { + let mut segments = scan_segments(base_dir)?; + if segments.is_empty() { + info!("No WAL segments found, starting fresh"); + return Ok(0); + } + + // remove the last segment for recovery, as it may be incomplete + let last_segment_id = segments.pop().expect("segments is not empty"); + + let segment_size = journal.capacity() as u64; + let complete_segment_size = segment_size * ENTRY_SIZE as u64; + for segment_id in segments.iter().copied() { + let path = base_dir.join(format_segment_file_name(segment_id)); + let metadata = fs::metadata(&path)?; + let file_size = metadata.len(); + + if file_size != complete_segment_size { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Incomplete WAL segment: {}", path.display()), + )); + } + } + + // handle last segment, which may be partially written + let path = base_dir.join(format_segment_file_name(last_segment_id)); + let metadata = fs::metadata(&path)?; + let file_size = metadata.len(); + let valid_count = file_size / ENTRY_SIZE as u64; + if file_size % ENTRY_SIZE as u64 != 0 { + warn!("Detected partial write in last segment #{last_segment_id}. Truncating."); + let f = OpenOptions::new().write(true).open(&path)?; + f.set_len(valid_count * ENTRY_SIZE as u64)?; + f.sync_all()?; + } + + let write_index = if last_segment_id == 0 { + valid_count + } else { + last_segment_id * segment_size + valid_count + }; + // consumed_checkpoint just recovered. + let consumed_index = journal.consumed_checkpoint.persisted_index(); + // Data loss happens, don't continue to recover, as it may cause more damage. + // User intervention is needed to fix the issue. + if consumed_index > write_index { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "Consumed index {consumed_index} is greater than recovered write index {write_index}" + ), + )); + } + + journal.write_index.store(write_index, Ordering::Relaxed); + journal.filled_index.store(write_index, Ordering::Relaxed); + journal + .persisted_index + .store(write_index, Ordering::Relaxed); + info!("WAL Recovered: write_index={write_index}, consumed_index={consumed_index}"); + + // replay data to ring buffer + replay_data(&base_dir, journal)?; + Ok(last_segment_id) +} + +fn scan_segments(base_dir: &Path) -> io::Result> { + let mut segments: Vec = Vec::new(); + + let entries = fs::read_dir(&base_dir)?; + + for entry in entries { + let entry = entry?; + let file_type = entry.file_type()?; + if !file_type.is_file() { + continue; + } + + let file_name = entry.file_name(); + let Some(file_name) = file_name.to_str().and_then(|n| n.strip_suffix(".wal")) else { + continue; + }; + + let Ok(id) = file_name.parse::() else { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Invalid WAL file name: {}", entry.path().display()), + )); + }; + segments.push(id); + } + segments.sort_unstable(); + + Ok(segments) +} + +fn replay_data( + base_dir: &Path, + journal: &JournalInner<{ ENTRY_SIZE }>, +) -> io::Result<()> { + let segment_size = journal.capacity() as u64; + let entry_size = ENTRY_SIZE as u64; + + let write_index = journal.write_index.load(Ordering::Relaxed); + let consumed_index = journal.consumed_checkpoint.persisted_index(); + + let mut current_file: Option = None; + let mut current_seg_id: u64 = u64::MAX; + + for idx in consumed_index..write_index { + let seg_id = idx / segment_size; + + if seg_id != current_seg_id { + current_file = Some(open_segment_file(&base_dir, seg_id)?); + current_seg_id = seg_id; + } + + let offset = (idx % segment_size) * entry_size; + let slot_ptr = journal.data_slot_ptr(idx); + + if let Some(ref mut f) = current_file { + f.seek(SeekFrom::Start(offset))?; + let buffer = unsafe { &mut *slot_ptr }; + f.read_exact(buffer)?; + } + } + Ok(()) +} + +#[inline] +fn format_segment_file_name(segment_id: u64) -> String { + format!("{segment_id:012}.wal") +} + +fn open_segment_file(base_dir: &Path, segment_id: u64) -> io::Result { + let path = base_dir.join(format_segment_file_name(segment_id)); + OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(path) +} + +#[instrument(err)] +fn truncate_old_segments(base_dir: PathBuf, current_segment_id: u64) -> io::Result<()> { + let Some(to_delete) = current_segment_id.checked_sub(2) else { + // not segments to truncate + return Ok(()); + }; + let path = base_dir.join(format_segment_file_name(to_delete)); + if path.exists() { + fs::remove_file(path)?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + JournalConfig, + checkpoint::CheckpointConfig, + tests::{ENTRY_SIZE, TEST_DATA, test_journal}, + }; + use std::sync::atomic::Ordering; + + type Journal = crate::Journal; + + /// Helper: create a journal config pointing at the given temp directory. + fn test_config(tmp: &Path) -> JournalConfig { + JournalConfig { + consumer_checkpoint: CheckpointConfig { + path: tmp.join("checkpoint.meta"), + ..Default::default() + }, + wal_dir: tmp.join("wal"), + } + } + + // ── Segment file helpers ───────────────────────────────────────────── + + #[test] + fn format_segment_file_name_pads_to_twelve_digits() { + assert_eq!(format_segment_file_name(0), "000000000000.wal"); + assert_eq!(format_segment_file_name(1), "000000000001.wal"); + assert_eq!( + format_segment_file_name(999_999_999_999), + "999999999999.wal" + ); + } + + #[test] + fn open_segment_file_creates_file() -> io::Result<()> { + let tmp = tempfile::tempdir()?; + let f = open_segment_file(tmp.path(), 0)?; + assert!(tmp.path().join("000000000000.wal").exists()); + drop(f); + Ok(()) + } + + // ── scan_segments ──────────────────────────────────────────────────── + + #[test] + fn scan_segments_empty_directory() -> io::Result<()> { + let tmp = tempfile::tempdir()?; + let segments = scan_segments(tmp.path())?; + assert!(segments.is_empty()); + Ok(()) + } + + #[test] + fn scan_segments_returns_sorted_ids() -> io::Result<()> { + let tmp = tempfile::tempdir()?; + // Create files out of order. + File::create(tmp.path().join("000000000002.wal"))?; + File::create(tmp.path().join("000000000000.wal"))?; + File::create(tmp.path().join("000000000001.wal"))?; + + let segments = scan_segments(tmp.path())?; + assert_eq!(segments, vec![0, 1, 2]); + Ok(()) + } + + #[test] + fn scan_segments_skips_non_wal_files() -> io::Result<()> { + let tmp = tempfile::tempdir()?; + File::create(tmp.path().join("000000000000.wal"))?; + File::create(tmp.path().join("readme.txt"))?; + + let segments = scan_segments(tmp.path())?; + assert_eq!(segments, vec![0]); + Ok(()) + } + + #[test] + fn scan_segments_rejects_invalid_wal_names() -> io::Result<()> { + let tmp = tempfile::tempdir()?; + File::create(tmp.path().join("not_a_number.wal"))?; + + let err = scan_segments(tmp.path()).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidData); + Ok(()) + } + + // ── truncate_old_segments ──────────────────────────────────────────── + + #[test] + fn truncate_old_segments_removes_two_behind() -> io::Result<()> { + let tmp = tempfile::tempdir()?; + File::create(tmp.path().join(format_segment_file_name(0)))?; + File::create(tmp.path().join(format_segment_file_name(1)))?; + File::create(tmp.path().join(format_segment_file_name(2)))?; + + truncate_old_segments(tmp.path().to_path_buf(), 2)?; + assert!( + !tmp.path().join(format_segment_file_name(0)).exists(), + "segment 0 should be deleted when current is 2" + ); + assert!(tmp.path().join(format_segment_file_name(1)).exists()); + assert!(tmp.path().join(format_segment_file_name(2)).exists()); + Ok(()) + } + + #[test] + fn truncate_old_segments_noop_when_too_few() -> io::Result<()> { + let tmp = tempfile::tempdir()?; + File::create(tmp.path().join(format_segment_file_name(0)))?; + File::create(tmp.path().join(format_segment_file_name(1)))?; + + // current_segment_id = 1, checked_sub(2) underflows → noop + truncate_old_segments(tmp.path().to_path_buf(), 1)?; + assert!(tmp.path().join(format_segment_file_name(0)).exists()); + assert!(tmp.path().join(format_segment_file_name(1)).exists()); + Ok(()) + } + + // ── WAL persistence end-to-end ─────────────────────────────────────── + + #[tokio::test(flavor = "current_thread")] + async fn wal_persists_entries_to_segment_file() -> eyre::Result<()> { + let (journal, tmp) = test_journal(4); + + journal.commit(&TEST_DATA[0]).await; + journal.commit(&TEST_DATA[1]).await; + + // WAL file should exist and contain the two entries. + let wal_path = tmp.path().join("wal").join(format_segment_file_name(0)); + assert!(wal_path.exists(), "WAL segment file should exist"); + + let data = fs::read(&wal_path)?; + assert_eq!( + data.len(), + ENTRY_SIZE * 2, + "segment should contain exactly 2 entries" + ); + assert_eq!(&data[..ENTRY_SIZE], &TEST_DATA[0]); + assert_eq!(&data[ENTRY_SIZE..ENTRY_SIZE * 2], &TEST_DATA[1]); + Ok(()) + } + + #[tokio::test(flavor = "current_thread")] + async fn wal_segment_rotation() -> eyre::Result<()> { + // Capacity 4 → segment_size = 4 entries per segment. + let (journal, tmp) = test_journal(4); + let mut reader = journal.reader(); + + // Fill first segment (4 entries). + for entry in TEST_DATA.iter().take(4) { + journal.commit(entry).await; + } + + // Consume some entries to free ring buffer space. + reader.read(4); + reader.commit()?; + + // Write one more entry, causing rotation to segment 1. + journal.commit(&TEST_DATA[4]).await; + + let seg0 = tmp.path().join("wal").join(format_segment_file_name(0)); + let seg1 = tmp.path().join("wal").join(format_segment_file_name(1)); + assert!(seg0.exists(), "segment 0 should exist"); + assert!(seg1.exists(), "segment 1 should exist after rotation"); + + let seg1_data = fs::read(&seg1)?; + assert_eq!(seg1_data.len(), ENTRY_SIZE); + assert_eq!(&seg1_data[..ENTRY_SIZE], &TEST_DATA[4]); + Ok(()) + } + + // ── Recovery ───────────────────────────────────────────────────────── + + #[tokio::test(flavor = "current_thread")] + async fn recover_from_empty_dir() -> eyre::Result<()> { + let tmp = tempfile::tempdir()?; + let config = test_config(tmp.path()); + + // First journal writes nothing. + let journal = Journal::with_capacity_and_config(4, config.clone())?; + journal.shutdown()?; + drop(journal); + + // Second journal recovers with indices at 0. + let journal = Journal::with_capacity_and_config(4, config)?; + assert_eq!(journal.inner.write_index.load(Ordering::Acquire), 0); + assert_eq!(journal.inner.persisted_index.load(Ordering::Acquire), 0); + journal.shutdown()?; + Ok(()) + } + + #[tokio::test(flavor = "current_thread")] + async fn recover_replays_data_into_ring_buffer() -> eyre::Result<()> { + let tmp = tempfile::tempdir()?; + let config = test_config(tmp.path()); + + // Write entries and shut down. + { + let journal = Journal::with_capacity_and_config(4, config.clone())?; + journal.commit(&TEST_DATA[0]).await; + journal.commit(&TEST_DATA[1]).await; + journal.commit(&TEST_DATA[2]).await; + journal.shutdown()?; + } + + // Recover and verify data via reader. + { + let journal = Journal::with_capacity_and_config(4, config)?; + assert_eq!(journal.inner.write_index.load(Ordering::Acquire), 3); + assert_eq!(journal.inner.persisted_index.load(Ordering::Acquire), 3); + + let mut reader = journal.reader(); + let entries = reader.read(3); + assert_eq!(entries.len(), 3); + assert_eq!(entries[0], TEST_DATA[0]); + assert_eq!(entries[1], TEST_DATA[1]); + assert_eq!(entries[2], TEST_DATA[2]); + journal.shutdown()?; + } + Ok(()) + } + + #[tokio::test(flavor = "current_thread")] + async fn recover_respects_consumed_checkpoint() -> eyre::Result<()> { + let tmp = tempfile::tempdir()?; + let config = test_config(tmp.path()); + + // Write entries, consume some, then shut down. + { + let journal = Journal::with_capacity_and_config(4, config.clone())?; + journal.commit(&TEST_DATA[0]).await; + journal.commit(&TEST_DATA[1]).await; + journal.commit(&TEST_DATA[2]).await; + let mut reader = journal.reader(); + reader.read(2); + reader.commit()?; + journal.shutdown()?; + } + + // Recover - reader should only see unconsumed entries. + { + let journal = Journal::with_capacity_and_config(4, config)?; + assert_eq!(journal.inner.write_index.load(Ordering::Acquire), 3); + let mut reader = journal.reader(); + let entries = reader.read(10); + assert_eq!(entries.len(), 1, "only 1 unconsumed entry should remain"); + assert_eq!(entries[0], TEST_DATA[2]); + journal.shutdown()?; + } + Ok(()) + } + + #[test] + fn recover_truncates_partial_last_segment() -> io::Result<()> { + let tmp = tempfile::tempdir()?; + let wal_dir = tmp.path().join("wal"); + fs::create_dir_all(&wal_dir)?; + + // Write 2.5 entries worth of data (partial entry at end). + let mut data = Vec::new(); + data.extend_from_slice(&TEST_DATA[0]); + data.extend_from_slice(&TEST_DATA[1]); + data.extend_from_slice(&[0xFF; ENTRY_SIZE / 2]); // partial write + fs::write(wal_dir.join(format_segment_file_name(0)), &data)?; + + let config = test_config(tmp.path()); + let journal = Journal::with_capacity_and_config(4, config).unwrap(); + + // Should recover 2 valid entries (truncated partial write). + assert_eq!(journal.inner.write_index.load(Ordering::Acquire), 2); + assert_eq!(journal.inner.persisted_index.load(Ordering::Acquire), 2); + + // Verify truncated file size. + let file_size = fs::metadata(wal_dir.join(format_segment_file_name(0)))?.len(); + assert_eq!(file_size, (ENTRY_SIZE * 2) as u64); + journal.shutdown().unwrap(); + Ok(()) + } + + #[test] + fn recover_detects_incomplete_non_last_segment() { + let tmp = tempfile::tempdir().unwrap(); + let wal_dir = tmp.path().join("wal"); + fs::create_dir_all(&wal_dir).unwrap(); + + // Segment 0 is incomplete (not full), segment 1 exists. + // For capacity 4, a full segment should be 4 * ENTRY_SIZE bytes. + fs::write( + wal_dir.join(format_segment_file_name(0)), + &[0u8; ENTRY_SIZE * 2], + ) + .unwrap(); + fs::write(wal_dir.join(format_segment_file_name(1)), &[0u8; 0]).unwrap(); + + let config = test_config(tmp.path()); + let err = Journal::with_capacity_and_config(4, config).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidData); + } + + #[test] + fn recover_detects_consumed_ahead_of_written() { + let tmp = tempfile::tempdir().unwrap(); + let wal_dir = tmp.path().join("wal"); + fs::create_dir_all(&wal_dir).unwrap(); + + // Write a checkpoint claiming index 10 consumed... + let checkpoint_path = tmp.path().join("checkpoint.meta"); + fs::write(&checkpoint_path, &10u64.to_le_bytes()).unwrap(); + + // ...but WAL only has 2 entries. + let mut data = Vec::new(); + data.extend_from_slice(&TEST_DATA[0]); + data.extend_from_slice(&TEST_DATA[1]); + fs::write(wal_dir.join(format_segment_file_name(0)), &data).unwrap(); + + let config = JournalConfig { + consumer_checkpoint: CheckpointConfig { + path: checkpoint_path, + ..Default::default() + }, + wal_dir, + }; + let err = Journal::with_capacity_and_config(4, config).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidData); + } + + // ── Shutdown ───────────────────────────────────────────────────────── + + #[tokio::test(flavor = "current_thread")] + async fn shutdown_rejects_new_commits() -> eyre::Result<()> { + let (journal, _tmp) = test_journal(4); + + journal.commit(&TEST_DATA[0]).await; + journal.shutdown()?; + + let err = journal + .try_commit(&TEST_DATA[1]) + .expect_err("commit after shutdown should fail"); + assert!( + matches!(err, crate::error::JournalUnavailable::Shutdown), + "expected Shutdown, got {err:?}" + ); + Ok(()) + } + + #[tokio::test(flavor = "current_thread")] + async fn shutdown_is_idempotent() -> eyre::Result<()> { + let (journal, _tmp) = test_journal(4); + journal.commit(&TEST_DATA[0]).await; + journal.shutdown()?; + journal.shutdown()?; + Ok(()) + } + + // ── WAL worker wakes on commit ─────────────────────────────────────── + + #[tokio::test(flavor = "current_thread")] + async fn commit_future_resolves_after_wal_persistence() -> eyre::Result<()> { + let (journal, _tmp) = test_journal(4); + + // The commit future should only resolve after the WAL worker persists. + journal.commit(&TEST_DATA[0]).await; + + // If we got here, the persisted_index must have advanced. + assert!( + journal.inner.persisted_index.load(Ordering::Acquire) >= 1, + "persisted_index should be >= 1 after commit future resolves" + ); + Ok(()) + } + + #[tokio::test(flavor = "current_thread")] + async fn multiple_commits_advance_persisted_index() -> eyre::Result<()> { + let (journal, _tmp) = test_journal(4); + + for i in 0..4 { + journal.commit(&TEST_DATA[i]).await; + } + + assert_eq!(journal.inner.persisted_index.load(Ordering::Acquire), 4); + Ok(()) + } + + // ── Recovery across segments ───────────────────────────────────────── + + #[tokio::test(flavor = "current_thread")] + async fn recover_across_segment_rotation() -> eyre::Result<()> { + let tmp = tempfile::tempdir()?; + let config = test_config(tmp.path()); + + // Fill more than one segment. Capacity 4 → segment_size = 4. + { + let journal = Journal::with_capacity_and_config(4, config.clone())?; + let mut reader = journal.reader(); + + // Fill first segment. + for entry in TEST_DATA.iter().take(4) { + journal.commit(entry).await; + } + + // Consume to free ring buffer. + reader.read(4); + reader.commit()?; + + // Write into second segment. + journal.commit(&TEST_DATA[4]).await; + journal.commit(&TEST_DATA[5]).await; + + // Consume second batch. + reader.read(2); + reader.commit()?; + + journal.shutdown()?; + } + + // Recover and write more. + { + let journal = Journal::with_capacity_and_config(4, config)?; + assert_eq!(journal.inner.write_index.load(Ordering::Acquire), 6); + + // Should be able to write new entries. + journal.commit(&TEST_DATA[6]).await; + assert_eq!(journal.inner.persisted_index.load(Ordering::Acquire), 7); + journal.shutdown()?; + } + Ok(()) + } +} diff --git a/crates/stamper/Cargo.toml b/crates/stamper/Cargo.toml new file mode 100644 index 0000000..c408805 --- /dev/null +++ b/crates/stamper/Cargo.toml @@ -0,0 +1,28 @@ +[package] +authors.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +name = "uts-stamper" +repository.workspace = true +version.workspace = true + +[dependencies] +alloy-primitives = { workspace = true, features = ["serde"] } +alloy-provider = { workspace = true } +bitcode = { workspace = true, features = ["serde"] } +bytemuck = { workspace = true } +digest = { workspace = true } +rocksdb = { workspace = true } +serde = { workspace = true, features = ["derive"] } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["time", "macros"] } +tracing = { workspace = true } +uts-bmt = { workspace = true } +uts-contracts = { workspace = true } +uts-core = { workspace = true } +uts-journal = { workspace = true } + +[lints] +workspace = true diff --git a/crates/stamper/src/lib.rs b/crates/stamper/src/lib.rs new file mode 100644 index 0000000..ed087f3 --- /dev/null +++ b/crates/stamper/src/lib.rs @@ -0,0 +1,304 @@ +#![feature(generic_const_exprs)] +#![allow(incomplete_features)] + +//! Timestamping + +#[macro_use] +extern crate tracing; + +use alloy_primitives::{B256, BlockNumber, ChainId, TxHash}; +use alloy_provider::Provider; +use bytemuck::{NoUninit, Pod}; +use digest::{Digest, FixedOutputReset, Output, typenum::Unsigned}; +use rocksdb::{DB, WriteBatch}; +use serde::{Deserialize, Serialize}; +use std::{ + borrow::Cow, + collections::{HashMap, VecDeque}, + fmt, + sync::Arc, + time::Duration, +}; +use tokio::time::{Interval, MissedTickBehavior}; +use uts_bmt::UnorderedMerkleTree; +use uts_contracts::uts::UniversalTimestamps; +use uts_core::utils::Hexed; +use uts_journal::reader::JournalReader; + +/// Stamper for timestamping +/// +/// A stamper will wait for, either: +/// - Timeout: `max_interval_seconds` has passed since last timestamp +/// - Max Entries: `max_entries_per_timestamp` have been collected since last timestamp +/// +/// Then it will collect entries from the journal reader, with the size of: +/// - at most `max_entries_per_timestamp` +/// - if available entries size is not power of two, it will take: +/// - the largest power of two less than available entries, if that is >= `min_leaves` +/// - else, it will take all available entries +pub struct Stamper { + /// Journal reader to read entries from + reader: JournalReader, + /// Storage for merkle trees and leaf->root mappings + storage: Arc, + /// FIFO cache of recent merkle trees + cache: VecDeque>, + /// FIFO cache index of recent merkle trees + cache_index: HashMap, + /// The contract + contract: UniversalTimestamps

, + /// Stamper configuration + config: StamperConfig, +} + +/// Configuration for the Stamper +#[derive(Debug, Clone)] +pub struct StamperConfig { + /// The maximum interval (in seconds) between create new timestamps + pub max_interval_seconds: u64, + /// The maximum number of entries per timestamp. + /// It should be a power of two. + pub max_entries_per_timestamp: usize, + /// The minimum size of the Merkle tree leaves. + /// It should be a power of two. + pub min_leaves: usize, + /// The maximum number of recent Merkle trees to keep in cache. + pub max_cache_size: usize, +} + +/// Merkle entry stored in the database +#[derive(Debug, Serialize, Deserialize)] +pub struct MerkleEntry<'a> { + /// Chain ID of the timestamp transaction + pub chain_id: ChainId, + /// Transaction hash of the timestamp transaction + pub tx_hash: TxHash, + /// Block number of the timestamp transaction + pub height: BlockNumber, + + trie: Cow<'a, [u8]>, +} + +impl MerkleEntry<'_> { + /// Get the Merkle tree from the entry + pub fn trie(&self) -> UnorderedMerkleTree + where + D: Digest + FixedOutputReset, + Output: Pod + Copy, + { + // SAFETY: We trust that the data in the database is valid, and that the trie was serialized correctly. + unsafe { UnorderedMerkleTree::from_raw_bytes(&self.trie) } + } +} + +/// Errors that can occur during storage operations +#[derive(Debug, thiserror::Error)] +pub enum StorageError { + /// Errors from RocksDB + #[error(transparent)] + Rocks(#[from] rocksdb::Error), + /// Errors from bitcode serialization/deserialization + #[error(transparent)] + Bitcode(#[from] bitcode::Error), +} + +/// Extension trait for DB to load Merkle entries and leaf->root mappings +pub trait DbExt { + /// Load a Merkle entry from the database by root hash + fn load_entry(&self, root: B256) -> Result>, StorageError>; + + /// Get the root hash for a given leaf hash, if it exists + fn get_root_for_leaf(&self, leaf: B256) -> Result, StorageError>; +} + +impl DbExt for DB { + fn load_entry(&self, root: B256) -> Result>, StorageError> { + let Some(data) = self.get(root)? else { + return Ok(None); + }; + let entry: MerkleEntry<'static> = bitcode::deserialize(&data)?; + Ok(Some(entry)) + } + + fn get_root_for_leaf(&self, leaf: B256) -> Result, StorageError> { + let Some(root) = self.get(leaf)? else { + return Ok(None); + }; + if root.len() != 32 { + let _entry: MerkleEntry<'static> = bitcode::deserialize(&root)?; + return Ok(Some(leaf)); // it's a single-leaf tree + } + let hash: [u8; 32] = root.as_slice().try_into().expect("infallible"); + Ok(Some(B256::new(hash))) + } +} + +impl fmt::Debug for Stamper { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Stamper") + .field("cache_size", &self.cache.len()) + .field("config", &self.config) + .finish() + } +} + +impl Stamper +where + D: Digest + FixedOutputReset + 'static, + P: Provider, + Output: Pod + Copy, + [u8; ENTRY_SIZE]: NoUninit, +{ + const _SIZE_MATCHES: () = assert!(D::OutputSize::USIZE == ENTRY_SIZE); + + /// Create a new Stamper + pub fn new( + reader: JournalReader, + storage: Arc, + contract: UniversalTimestamps

, + config: StamperConfig, + ) -> Self { + Self { + reader, + storage, + cache: VecDeque::with_capacity(config.max_cache_size), + cache_index: HashMap::with_capacity(config.max_cache_size), + contract, + config, + } + } + + /// Work loop + pub async fn run(&mut self) { + let chain_id = self + .contract + .provider() + .get_chain_id() + .await + .expect("Failed to get chain ID"); + let mut ticker = + tokio::time::interval(Duration::from_secs(self.config.max_interval_seconds)); + ticker.set_missed_tick_behavior(MissedTickBehavior::Delay); + let mut leaves_buffer = Vec::with_capacity(self.config.max_entries_per_timestamp); + loop { + self.pack(chain_id, &mut ticker, &mut leaves_buffer).await; + } + } + + async fn pack( + &mut self, + chain_id: ChainId, + ticker: &mut Interval, + buffer: &mut Vec<[u8; ENTRY_SIZE]>, + ) { + let entries = self + .reader + .wait_at_least(self.config.max_entries_per_timestamp); + + let target_size = tokio::select! { + _ = ticker.tick() => { + // Timeout reached, create timestamp with available entries + let current_available = self.reader.available(); + if current_available == 0 { + debug!("No available entries, skipping this round..."); + return; + } + + debug!(current_available, "Timeout reached, creating timestamp"); + + // Determine the number of entries to take + let next_power_of_two = current_available.next_power_of_two(); + if next_power_of_two == current_available { + trace!("Current available is power of two, taking all"); + current_available + } else if next_power_of_two / 2 >= self.config.min_leaves { + let target = next_power_of_two / 2; + trace!(target, "Taking largest power of two less than available"); + target + } else { + trace!("Taking all available entries"); + current_available + } + } + _ = entries => { + // Max entries reached, create timestamp + debug!("Max entries reached, creating timestamp"); + self.config.max_entries_per_timestamp + } + }; + trace!(target_size); + + // Read entries, could need two reads if wrapping around + buffer.clear(); + buffer.extend_from_slice(self.reader.read(target_size)); + let remaining = target_size - buffer.len(); + if remaining > 0 { + buffer.extend_from_slice(self.reader.read(remaining)); + } + debug_assert_eq!(buffer.len(), target_size); + + let merkle_tree = UnorderedMerkleTree::::new_unhashed(bytemuck::cast_slice(&buffer)); + let storage = self.storage.clone(); + + let merkle_tree = tokio::task::spawn_blocking(move || { + let merkle_tree = merkle_tree.finalize(); // CPU intensive + let root = merkle_tree.root(); + info!(root = ?Hexed(root)); + merkle_tree + }) + .await + .expect("Failed to create Merkle tree"); // FIXME: handle error properly + + let root = B256::new(bytemuck::cast(*merkle_tree.root())); + + // commit to blockchain + let receipt = self + .contract + .attest(root) + .send() + .await + .expect("failed to build transaction") + .get_receipt() + .await + .expect("failed to send transaction"); // FIXME: handle error properly + let block_number = receipt.block_number.expect("Transaction not yet mined"); + info!(%block_number, %receipt.transaction_hash, %root,"Timestamp attested on-chain"); + + // write to storage + let mut batch = WriteBatch::default(); + // store leaf->root mappings for quick lookup + for leaf in merkle_tree.leaves() { + batch.put(leaf, root); + } + // store the Merkle tree + let entry = MerkleEntry { + chain_id, + tx_hash: receipt.transaction_hash, + height: block_number, + trie: Cow::Borrowed(merkle_tree.as_raw_bytes()), + }; + let serialized_entry = + bitcode::serialize(&entry).expect("Failed to serialize Merkle entry"); + bitcode::deserialize::(&serialized_entry) + .expect("Failed to deserialize Merkle entry"); // sanity check + // if it's a single-leaf tree, the root == the leaf, so we write mapping first. + batch.put(root, serialized_entry); + storage.write(batch).expect("Failed to write to storage"); // FIXME: handle error properly + + if self.cache.len() >= self.config.max_cache_size { + let evicted = self + .cache + .pop_front() + .expect("infallible due to check above"); + + let root = evicted.root(); + let removed = self.cache_index.remove(&B256::new(bytemuck::cast(*root))); + debug_assert_eq!(removed, Some(0)); + + self.cache_index.iter_mut().for_each(|(_, idx)| *idx -= 1); + } + self.cache.push_back(merkle_tree); + self.cache_index.insert(root, self.cache.len() - 1); + self.reader.commit().expect("Failed to commit read entries"); // FIXME: handle error properly + } +} diff --git a/dev-docs/e2e.md b/dev-docs/e2e.md index eccb514..1b49791 100644 --- a/dev-docs/e2e.md +++ b/dev-docs/e2e.md @@ -7,16 +7,18 @@ This is for referencing overall end2end procedures of OpenTimestamps protocol im > Good news is we can use ots servers in our clients. --- + ## Client Side 1. Calc initial digest from (data, file) and adding nonce (random generated) to it. - This allows to separate the sub-timestamp for individual file without leaking any information about the adjacent files. + This allows to separate the sub-timestamp for individual file without leaking any information about the adjacent files. 2. Construct Merkle tree. 3. Submit tree root digest to **aggregator** servers in parallel via [POST /digest](./ots-api.md#post-digest) endpoint. 4. Merge received pending attestations into a single attestation file. 5. Periodically check for completed attestations to **calendar** servers via [GET /attestation/{digest}](./ots-api.md#get-timestamphex_commitment) endpoint. --- + ## Aggregator Side 1. Receive submitted digest via [POST /digest](./ots-api.md#post-digest) endpoint. @@ -27,12 +29,14 @@ This is for referencing overall end2end procedures of OpenTimestamps protocol im The aggregator only has `/digest` endpoint, and do not persist data. Default ots aggregator servers: + - https://a.pool.opentimestamps.org - https://b.pool.opentimestamps.org - https://a.pool.eternitywall.com - https://ots.btc.catallaxy.com --- + ## Calendar Server Side **Note: the following procedure is not a part of OpenTimestamps protocol.** @@ -43,7 +47,7 @@ Default ots aggregator servers: 1. Receive submitted digest via [POST /digest](./ots-api.md#post-digest) endpoint. 2. Add `PREPEND` op: prepends current timestamp (u32) to the digest. 3. Add `APPEND` op: HMAC with server secret to the digest, truncated to 8 bytes, appended. - After this step, the digest becomes the attestation message (44 bytes). + After this step, the digest becomes the attestation message (44 bytes). 4. Add `ATTESTATION` op: uri with self. 5. Insert the step 3 **message** into journal as pending attestation. (just a file opened for append in python implementation) 6. **Return** pending attestation for the request. @@ -54,12 +58,15 @@ Default ots aggregator servers: 2. Search for the commitment in db. --- + Default calendar servers: -- https://*.calendar.opentimestamps.org', # Run by Peter Todd -- https://*.calendar.eternitywall.com', # Run by Riccardo Casatta -- https://*.calendar.catallaxy.com', # Run by Bull Bitcoin + +- https://\*.calendar.opentimestamps.org', # Run by Peter Todd +- https://\*.calendar.eternitywall.com', # Run by Riccardo Casatta +- https://\*.calendar.catallaxy.com', # Run by Bull Bitcoin --- + ## Stamper Side 1. Stream reading pending attestations from journal file. diff --git a/dev-docs/milestones/M1-mvp-server.md b/dev-docs/milestones/M1-mvp-server.md index 553310f..409ab8e 100644 --- a/dev-docs/milestones/M1-mvp-server.md +++ b/dev-docs/milestones/M1-mvp-server.md @@ -2,5 +2,5 @@ # Target -Implement a minimal OpenTimestamps server that is compatible with existing otsclient implementations. +Implement a minimal OpenTimestamps server that is compatible with existing otsclient implementations. The server should support the core functionalities required for timestamping and upgrading timestamps. diff --git a/dev-docs/ots-api.md b/dev-docs/ots-api.md index 6198979..b33827b 100644 --- a/dev-docs/ots-api.md +++ b/dev-docs/ots-api.md @@ -3,24 +3,29 @@ The API is summarized here for reference. - Core Endpoints - - Submit [POST /digest](#post-digest) - - Upgrade [GET /timestamp/{hex_commitment}](#get-timestamphex_commitment) +- Submit [POST /digest](#post-digest) +- Upgrade [GET /timestamp/{hex_commitment}](#get-timestamphex_commitment) - Other Endpoints - Fetch Tip [GET /tip](#get-tip) --- + ## POST /digest + Submit a commitment to the calendar aggregator for stamping. Implementation: https://github.com/opentimestamps/opentimestamps-server/blob/6309db6b2c9ac79f6f444d85c9dc96e39219eb63/otsserver/rpc.py#L48-L81 ### Request Body + Raw digest bytes (≤64 bytes); Content-Length header required. ### On Success + 200 application/octet-stream with serialized [Timestamp](../crates/core/src/codec/v1/timestamp.rs) tree. ### On Failure + 400 invalid or missing Content-Length; 400 digest too long. ### Example @@ -33,16 +38,20 @@ curl -X POST https://a.pool.opentimestamps.org/digest \ ``` --- + ## GET /timestamp/{hex_commitment} + Retrieve upgraded timestamp data for a commitment. Commitment must be lowercase/uppercase hex. Implementation: https://github.com/opentimestamps/opentimestamps-server/blob/6309db6b2c9ac79f6f444d85c9dc96e39219eb63/otsserver/rpc.py#L122-L186 ### On Success + 200 application/octet-stream with serialized [Timestamp](../crates/core/src/codec/v1/timestamp.rs) tree; Cache-Control max-age=31536000 once confirmed. ### On Failure + 400 non-hex input; 404 with text body (Pending… or Not found) and Cache-Control max-age=60. ### Example @@ -54,21 +63,25 @@ curl https://alice.btc.calendar.opentimestamps.org/timestamp/6938f93117b90a25a18 ``` --- + ## Get /tip + Fetch the most recent unconfirmed Merkle tree tip being prepared for anchoring. Implementation: https://github.com/opentimestamps/opentimestamps-server/blob/6309db6b2c9ac79f6f444d85c9dc96e39219eb63/otsserver/rpc.py#L83-L101 ### On Success + 200 application/octet-stream containing tip commitment; Cache-Control max-age=10. ### On Failure + 204 when tip exists but has no payload; 404 when no unconfirmed transactions. ### Example ```bash -$ curl https://alice.btc.calendar.opentimestamps.org/tip --output tip.bin +$ curl https://alice.btc.calendar.opentimestamps.org/tip --output tip.bin $ hexdump -C tip.bin 00000000 45 3f 4d 34 fd 72 c5 70 44 60 eb c4 b6 f5 09 87 |E?M4.r.pD`......| 00000010 72 7d da 8c a8 9e 00 1e cf c7 29 17 b4 c4 1f b0 |r}........).....| diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..117842e --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,409 @@ +// cSpell:disable +// @ts-check +import tseslint from 'typescript-eslint' +import UnicornPlugin from 'eslint-plugin-unicorn' +import UnusedImportsPlugin from 'eslint-plugin-unused-imports' +import ImportXPlugin from 'eslint-plugin-import-x' +import { defineConfig } from 'eslint/config' + +// Prefer rules from @typescript-eslint > unicorn > other plugins +// Level: if the rule is fixable and can be tolerate during dev, use 'warn' is better. +// if the fix needs big rewrite (e.g. XHR => fetch), use 'error' to notice the developer early. +// for RegEx rules, always uses 'error'. + +const avoidMistakeRules = { + // Code quality + 'no-invalid-regexp': 'error', // RegEx + 'unicorn/no-abusive-eslint-disable': 'error', // disable a rule requires a reason + '@typescript-eslint/ban-ts-comment': [ + 'error', + { + 'ts-expect-error': 'allow-with-description', + 'ts-ignore': true, + 'ts-nocheck': true, + 'ts-check': false, + minimumDescriptionLength: 5, + }, + ], // disable a rule requires a reason + /// TypeScript bad practice + '@typescript-eslint/no-empty-object-type': [ + 'error', + { allowInterfaces: 'with-single-extends' }, + ], + // '@typescript-eslint/no-invalid-void-type': 'warn', // Disallow void type outside of generic or return types + '@typescript-eslint/no-misused-new': 'error', // wrong 'new ()' or 'constructor()' signatures + '@typescript-eslint/no-unsafe-function-type': 'error', + // '@typescript-eslint/no-unsafe-type-assertion': 'error', // bans `expr as T` + '@typescript-eslint/no-wrapper-object-types': 'error', + /// Unicode support + 'no-misleading-character-class': 'error', // RegEx + 'unicorn/prefer-code-point': 'error', + /// type safety + // '@typescript-eslint/method-signature-style': 'warn', // method signature is bivariant + '@typescript-eslint/no-non-null-asserted-optional-chain': 'error', // bans foo?.bar! + // '@typescript-eslint/no-unsafe-argument': 'error', // bans call(any) + // '@typescript-eslint/no-unsafe-assignment': 'error', // bans a = any + // '@typescript-eslint/no-unsafe-call': 'error', // bans any() + // '@typescript-eslint/no-unsafe-member-access': 'error', // bans a = any.prop + // '@typescript-eslint/no-unsafe-return': 'error', // bans return any + '@typescript-eslint/prefer-return-this-type': 'error', // use `: this` properly + // '@typescript-eslint/restrict-plus-operands': 'error', // stronger `a + b` check + // '@typescript-eslint/restrict-template-expressions': 'error', // bans `${nonString}` + // '@typescript-eslint/strict-boolean-expressions': 'error', // stronger check for nullable string/number/boolean + // '@typescript-eslint/switch-exhaustiveness-check': 'error', // switch should be exhaustive + // '@typescript-eslint/unbound-method': 'error', // requires `this` to be set properly + + // Security + 'no-script-url': 'error', // javascript: + // 'unicorn/require-post-message-target-origin': 'warn', // postMessage(data, 'origin') + '@typescript-eslint/no-implied-eval': 'error', // setTimeout('code') + + // Confusing code + 'no-constant-binary-expression': 'error', // a + b ?? c + 'no-control-regex': 'error', // RegEx + 'no-div-regex': 'error', // RegEx + 'no-label-var': 'warn', // name collision + 'no-sequences': 'warn', // (a, b) + '@typescript-eslint/no-confusing-non-null-assertion': 'error', // a! == b + + // Problematic language features + /// API with trap + radix: 'warn', // parseInt('1', _required_) + 'unicorn/no-instanceof-builtins': 'warn', // bans `expr instanceof String` etc + 'unicorn/require-array-join-separator': 'warn', // Array.join(_required_) + // This rule breaks BigNumber class which has different .toFixed() default value. + // 'unicorn/require-number-to-fixed-digits-argument': 'warn', // Number#toFixed(_required_) + '@typescript-eslint/require-array-sort-compare': 'error', // Array#sort(_required_) + /// Footgun language features + 'no-compare-neg-zero': 'error', // x === -0 is wrong + 'no-new-wrappers': 'error', // wrapper objects are bad + 'no-unsafe-finally': 'error', // finally { return expr } + 'unicorn/no-thenable': 'error', // export function then() + 'no-loss-of-precision': 'error', // 5123000000000000000000000000001 is 5123000000000000000000000000000 actually + '@typescript-eslint/prefer-enum-initializers': 'warn', // add a new item in the middle is an API breaking change. + /// Little-known language features + 'no-constructor-return': 'error', // constructor() { return expr } + '@typescript-eslint/no-unsafe-declaration-merging': 'error', + '@typescript-eslint/no-mixed-enums': 'error', // enum { a = 1, b = "b" } + '@typescript-eslint/prefer-literal-enum-member': 'error', // enum { a = outsideVar } + + // Prevent bugs + // 'array-callback-return': 'error', // .map .some ... calls should have a return value + 'default-case-last': 'error', // default: should be the last + eqeqeq: 'error', // === + 'no-cond-assign': 'error', // if (a = b) + 'no-duplicate-case': 'error', // switch + 'no-empty-character-class': 'error', // RegEx /[]/ means a empty character class, not "[]" + 'no-global-assign': 'error', // onmessage = ... + 'no-self-assign': 'error', // a = a + 'no-self-compare': 'error', // a === a + 'no-sparse-arrays': 'error', // [,, 1] + 'no-unmodified-loop-condition': 'error', // loop bug + 'no-unreachable-loop': 'error', // loop bug + 'no-restricted-globals': [ + 'error', + // source of bug (those names are too common) + 'error', + 'event', + 'name', + 'length', + 'closed', + // no localStorage & sessionStorage in a web extension + { + name: 'localStorage', + message: + "If you're in the background script, localStorage is banned. It will cause Manifest V3 to crash. If you're in the chrome-extension:// pages, localStorage is discouraged. If you're in the content scripts, we can only use localStorage to read websites' data and MUST NOT store our own data.", + }, + { + name: 'sessionStorage', + message: + "If you're in the background script, sessionStorage is banned. It will cause Manifest V3 to crash. If you're in the chrome-extension:// pages, sessionStorage is discouraged. If you're in the content scripts, we can only use sessionStorage to read websites' data and MUST NOT store our own data.", + }, + ], + 'no-template-curly-in-string': 'error', // "${expr}" looks like a bug + // 'require-atomic-updates': 'error', // await/yield race condition + 'valid-typeof': 'error', // typeof expr === undefined + 'unicorn/no-invalid-remove-event-listener': 'error', // removeEventListener('click', f.bind(...)) + 'unicorn/no-negation-in-equality-check': 'error', // !foo === bar + '@typescript-eslint/no-base-to-string': 'error', // prevent buggy .toString() call + '@typescript-eslint/no-loop-func': 'warn', // capture a loop variable might be a bug + '@typescript-eslint/no-duplicate-enum-values': 'error', // enum { a = 1, b = 1 } + + // Performance + 'unicorn/consistent-function-scoping': 'warn', // hoist unnecessary higher order functions +} +const codeStyleRules = { + // Deprecated + 'no-alert': 'warn', // alert() + 'no-proto': 'error', // __proto__ accessor + 'no-prototype-builtins': 'error', // bans `obj.hasOwnProperty()` etc + 'no-var': 'error', // var x + 'unicorn/no-new-buffer': 'error', // NodeJS + // '@typescript-eslint/no-namespace': 'error', // namespace T {}, they won't support type only namespace + '@typescript-eslint/prefer-namespace-keyword': 'error', // but if you really need to, don't use `module T {}` + + // Useless code + 'no-constant-condition': 'warn', // if (false) + 'no-debugger': 'warn', + 'no-extra-bind': 'warn', // unused bind on a function that does not uses this + 'no-extra-boolean-cast': 'warn', // if (!!expr) + 'no-empty-pattern': 'warn', // const { a: {} } = expr + 'no-extra-label': 'warn', // break/continue is ok without label + 'no-unneeded-ternary': 'warn', // expr ? true : false + 'no-useless-backreference': 'error', // RegEx + 'no-useless-call': 'warn', // expr.call(undefined, ...) + 'no-useless-catch': 'warn', // catch (e) { throw e } + 'no-useless-concat': 'warn', // "a" + "b" + 'no-useless-escape': 'warn', // "hol\a" + // 'no-lone-blocks': 'warn', // no block that not introducing a new scope + 'unicorn/no-console-spaces': 'warn', // console.log('id: ', id) + 'unicorn/no-empty-file': 'warn', + 'unicorn/no-useless-fallback-in-spread': 'warn', // {...(foo || {})} + 'unicorn/no-useless-length-check': 'warn', // array.length === 0 || array.every(...) + 'unicorn/no-useless-promise-resolve-reject': 'warn', // return Promise.resolve(value) in async function + // 'unicorn/no-useless-spread': 'warn', // new Set([...iterable]) + 'unicorn/no-zero-fractions': 'warn', // 1.0 + 'unicorn/prefer-export-from': 'warn', // prefer export { } from than import-and-export + 'unicorn/prefer-native-coercion-functions': 'warn', // no coercion wrapper v => Boolean(v) + '@typescript-eslint/await-thenable': 'warn', // await 1 + // '@typescript-eslint/no-empty-interface': 'warn', // interface T extends Q {} + '@typescript-eslint/no-extra-non-null-assertion': 'warn', // foo!!!.bar + // '@typescript-eslint/no-inferrable-types': 'warn', // let x: number = 1 + '@typescript-eslint/no-meaningless-void-operator': 'warn', // void a_void_call() + '@typescript-eslint/no-non-null-asserted-nullish-coalescing': 'warn', // foo! ?? bar + // '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'warn', // no if (nullable_bool === true) + // '@typescript-eslint/no-unnecessary-condition': 'warn', // no if (some_object) + '@typescript-eslint/no-unnecessary-qualifier': 'warn', // no extra qualifier in enum/namespace + '@typescript-eslint/no-unnecessary-type-arguments': 'warn', // provided type argument equals the default + // Note: this rule seems like does not have the correct type checking behavior. before typescript-eslint has project reference support, don't use it. + // '@typescript-eslint/no-unnecessary-type-assertion': 'warn', // non_nullable! + '@typescript-eslint/no-unnecessary-type-constraint': 'warn', // T extends any + // '@typescript-eslint/no-useless-constructor': 'warn', // empty constructor + // '@typescript-eslint/no-useless-empty-export': 'warn', // export {} + // '@typescript-eslint/no-redundant-type-constituents': 'warn', // type Q = any | T + + // Prefer modern things + 'prefer-const': 'warn', + // 'prefer-exponentiation-operator': 'warn', // ** + // 'prefer-named-capture-group': 'warn', // RegEx + 'prefer-object-has-own': 'warn', + // 'prefer-object-spread': 'warn', // { ... } than Object.assign + // 'prefer-rest-params': 'warn', + + 'unicorn/no-document-cookie': 'error', // even if you have to do so, use CookieJar + 'unicorn/prefer-keyboard-event-key': 'warn', + 'unicorn/prefer-add-event-listener': 'warn', + // 'unicorn/prefer-array-find': 'warn', + // 'unicorn/prefer-array-flat': 'warn', + // 'unicorn/prefer-array-flat-map': 'warn', + 'unicorn/prefer-array-index-of': 'warn', + // 'unicorn/prefer-array-some': 'warn', + 'unicorn/prefer-at': 'warn', + 'unicorn/prefer-blob-reading-methods': 'warn', + 'unicorn/prefer-date-now': 'warn', + // 'unicorn/prefer-dom-node-append': 'warn', + 'unicorn/prefer-dom-node-dataset': 'warn', + // 'unicorn/prefer-dom-node-remove': 'warn', + // 'unicorn/prefer-dom-node-text-content': 'warn', + 'unicorn/prefer-event-target': 'warn', // prevent use of Node's EventEmitter + 'unicorn/prefer-math-min-max': 'warn', // Math.min/max than x < y ? x : y + 'unicorn/prefer-math-trunc': 'warn', + 'unicorn/prefer-modern-dom-apis': 'warn', + 'unicorn/prefer-modern-math-apis': 'warn', + // 'unicorn/prefer-object-from-entries': 'warn', + // 'unicorn/prefer-query-selector': 'warn', + 'unicorn/prefer-number-properties': 'warn', + 'unicorn/prefer-reflect-apply': 'warn', + // 'unicorn/prefer-set-has': 'warn', + 'unicorn/prefer-set-size': 'warn', + // 'unicorn/prefer-spread': 'warn', // prefer [...] than Array.from + 'unicorn/prefer-string-replace-all': 'warn', // str.replaceAll(...) + 'unicorn/prefer-string-slice': 'warn', + 'unicorn/prefer-string-trim-start-end': 'warn', // str.trimStart(...) + '@typescript-eslint/no-this-alias': 'warn', + '@typescript-eslint/prefer-string-starts-ends-with': 'warn', + '@typescript-eslint/prefer-for-of': 'warn', + '@typescript-eslint/prefer-includes': 'warn', + '@typescript-eslint/no-for-in-array': 'warn', + // '@typescript-eslint/prefer-nullish-coalescing': 'warn', + '@typescript-eslint/prefer-optional-chain': 'warn', + + // Better debug + // 'prefer-promise-reject-errors': 'warn', // Promise.reject(need_error) + 'symbol-description': 'warn', // Symbol(desc) + 'unicorn/catch-error-name': ['warn', { ignore: ['^err$'] }], // catch (err) + // 'unicorn/custom-error-definition': 'warn', // correctly extends the native error + // 'unicorn/error-message': 'warn', // error must have a message + // 'unicorn/prefer-type-error': 'warn', // prefer TypeError + // '@typescript-eslint/only-throw-error': 'warn', // no throw 'string' + + // API design + // '@typescript-eslint/no-extraneous-class': 'error', // no class with only static members + // '@typescript-eslint/prefer-readonly': 'error', + // '@typescript-eslint/prefer-readonly-parameter-types': 'error', + + // More readable code + // 'max-lines': ['warn', { max: 400 }], + // 'no-dupe-else-if': 'warn', // different condition with same if body + // 'no-else-return': 'warn', + 'no-regex-spaces': 'error', // RegEx + 'object-shorthand': 'warn', + 'prefer-numeric-literals': 'warn', // 0b111110111 === 503 + 'prefer-regex-literals': 'warn', // RegEx + 'spaced-comment': ['warn', 'always', { line: { markers: ['/'] } }], + // 'unicorn/no-array-reduce': 'warn', + // 'unicorn/no-lonely-if': 'warn', // else if (a) { if (b) expr } + // 'unicorn/no-negated-condition': 'warn', // if (!a) else + // 'unicorn/no-nested-ternary': 'warn', // a ? b : c ? d : e + // 'unicorn/no-typeof-undefined': 'warn', // typeof expr !== 'undefined' + // 'unicorn/no-unreadable-array-destructuring': 'warn', // [,, foo] = parts + 'unicorn/no-unreadable-iife': 'warn', // (bar => (bar ? bar.baz : baz))(getBar()) + 'unicorn/prefer-import-meta-properties': 'warn', + // 'unicorn/prefer-negative-index': 'warn', + 'unicorn/prefer-single-call': 'warn', + 'unicorn/throw-new-error': 'warn', + // 'unicorn/prefer-logical-operator-over-ternary': 'warn', // prefer ?? and || + // 'unicorn/prefer-optional-catch-binding': 'warn', // prefer to omit catch binding + '@typescript-eslint/prefer-as-const': 'warn', + // '@typescript-eslint/no-unnecessary-type-conversion': 'warn', // for code like str.toString() + + // Consistency + 'no-irregular-whitespace': 'warn', // unusual but safe + yoda: 'warn', + 'unicorn/better-regex': 'error', // RegEx + 'unicorn/consistent-existence-index-check': 'warn', // index === -1 + 'unicorn/escape-case': 'warn', // correct casing of escape '\xA9' + 'unicorn/no-hex-escape': 'warn', // correct casing of escape '\u001B' + // 'unicorn/numeric-separators-style': 'warn', // correct using of 1_234_567 + 'unicorn/prefer-prototype-methods': 'warn', // prefer Array.prototype.slice than [].slice + 'unicorn/relative-url-style': ['warn', 'always'], // prefer relative url starts with ./ + // 'unicorn/text-encoding-identifier-case': 'warn', // prefer 'utf-8' than 'UTF-8' + '@typescript-eslint/array-type': ['warn', { default: 'array-simple' }], // prefer T[] than Array + // '@typescript-eslint/consistent-generic-constructors': 'warn', // prefer const map = new Map() than generics on the left + '@typescript-eslint/consistent-type-assertions': [ + 'warn', + { assertionStyle: 'as' /* objectLiteralTypeAssertions: 'never' */ }, + ], // prefer a as T than a, and bans it on object literal + // '@typescript-eslint/consistent-type-definitions': 'warn', // prefer interface, also has better performance when type checking + '@typescript-eslint/dot-notation': 'warn', // prefer a.b than a['b'] + '@typescript-eslint/no-array-constructor': 'warn', + // '@typescript-eslint/non-nullable-type-assertion-style': 'warn', // prefer a! than a as T + // '@typescript-eslint/prefer-function-type': 'warn', + '@typescript-eslint/prefer-reduce-type-parameter': 'warn', + // '@typescript-eslint/sort-type-constituents': 'warn', + // '@typescript-eslint/triple-slash-reference': ['error', { lib: 'never', path: 'never', types: 'always' }], + // '@typescript-eslint/unified-signatures': 'warn', // prefer merging overload + + // Naming convention + // 'func-name-matching': 'warn', + // 'new-cap': 'warn', + + // Bad practice + 'no-ex-assign': 'warn', // reassign err in catch + 'no-multi-assign': 'warn', // a = b = c + // 'no-param-reassign': 'warn', + 'no-return-assign': 'warn', // return x = expr + 'unicorn/no-object-as-default-parameter': 'warn', + '@typescript-eslint/default-param-last': 'warn', // (a, b = 1, c) + '@typescript-eslint/no-dynamic-delete': 'error', // this usually means you should use Map/Set + /// Async functions / Promise bad practice + 'no-async-promise-executor': 'error', // new Promise(async (resolve) => ) + 'no-promise-executor-return': 'error', // new Promise(() => result) + // '@typescript-eslint/no-floating-promises': 'warn', // unhandled promises + // '@typescript-eslint/promise-function-async': 'warn', // avoid Zalgo + '@typescript-eslint/return-await': 'warn', // return await expr + + // No unused + 'no-unused-labels': 'warn', + // 'unicorn/no-unused-properties': 'warn', + // '@typescript-eslint/no-unused-expressions': 'warn', + // '@typescript-eslint/no-unused-vars': 'warn', +} +const moduleSystemRules = { + // Style + 'unused-imports/no-unused-imports': 'warn', + 'unicorn/prefer-node-protocol': 'warn', + '@typescript-eslint/consistent-type-exports': [ + 'warn', + { fixMixedExportsWithInlineTypeSpecifier: true }, + ], + '@typescript-eslint/consistent-type-imports': [ + 'warn', + { + prefer: 'type-imports', + disallowTypeAnnotations: false, + fixStyle: 'inline-type-imports', + }, + ], + 'no-useless-rename': 'error', + + // import-x: Prevent issues with misspelling of file paths and import names + 'import-x/no-unresolved': 'error', + 'import-x/named': 'error', + 'import-x/export': 'error', + 'import-x/no-duplicates': 'warn', + 'import-x/no-self-import': 'error', + 'import-x/no-useless-path-segments': 'warn', + 'import-x/extensions': ['error', 'always', { ignorePackages: true }], +} + +/** @type {any} */ +const plugins = { + unicorn: UnicornPlugin, + '@typescript-eslint': tseslint.plugin, + 'unused-imports': UnusedImportsPlugin, + 'import-x': ImportXPlugin, + // @ts-ignore +} +export default defineConfig( + { + ignores: ['**/dist/**', '**/.rollup.cache/**', '**/node_modules/**'], + }, + { + files: [ + 'packages/**/*.ts', + 'packages/**/*.tsx', + 'apps/**/*.ts', + 'apps/**/*.tsx', + ], + languageOptions: { + parser: tseslint.parser, + parserOptions: { + ecmaVersion: 'latest', + projectService: true, + // @ts-expect-error + tsconfigRootDir: import.meta.dirname, + warnOnUnsupportedTypeScriptVersion: false, + allowAutomaticSingleRunInference: true, + }, + }, + plugins, + settings: { + 'import-x/resolver': { + typescript: { + alwaysTryTypes: true, + project: ['./packages/*/tsconfig.json', './apps/*/tsconfig.json'], + }, + }, + }, + linterOptions: { + reportUnusedDisableDirectives: true, + }, + rules: /** @type {any} */ ({ + ...avoidMistakeRules, + ...codeStyleRules, + ...moduleSystemRules, + }), + }, + { + files: [ + 'packages/**/tests/**/*.ts', + 'packages/**/test/**/*.ts', + 'apps/**/tests/**/*.ts', + 'apps/**/test/**/*.ts', + ], + rules: { + 'unicorn/consistent-function-scoping': 'off', + }, + }, +) diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 0000000..1f56371 --- /dev/null +++ b/foundry.lock @@ -0,0 +1,20 @@ +{ + "lib/forge-std": { + "tag": { + "name": "v1.12.0", + "rev": "7117c90c8cf6c68e5acce4f09a6b24715cea4de6" + } + }, + "lib/openzeppelin-contracts-upgradeable": { + "tag": { + "name": "v5.5.0", + "rev": "aa677e9d28ed78fc427ec47ba2baef2030c58e7c" + } + }, + "lib/openzeppelin-foundry-upgrades": { + "tag": { + "name": "v0.4.0", + "rev": "cbce1e00305e943aa1661d43f41e5ac72c662b07" + } + } +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..eed2c8e --- /dev/null +++ b/foundry.toml @@ -0,0 +1,26 @@ +[profile.default] +ast = true +build_info = true +extra_output = ["storageLayout"] +ffi = true +fs_permissions = [{ access = "read", path = "target/foundry" }] +ignored_error_codes = [ + 2018, # Function state mutability can be restricted to pure +] +libs = ["lib"] +out = "target/foundry" +script = "contract-scripts" +solc_version = "0.8.24" +src = "contracts" +test = "contract-tests" + +# Set EVM version explicitly (must match across environments) +evm_version = "cancun" + +# Disable metadata hash for deterministic bytecode +bytecode_hash = "none" +cbor_metadata = false + +# If using optimizer, keep settings consistent +optimizer = true +optimizer_runs = 200 diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..7117c90 --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 7117c90c8cf6c68e5acce4f09a6b24715cea4de6 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..aa677e9 --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit aa677e9d28ed78fc427ec47ba2baef2030c58e7c diff --git a/lib/openzeppelin-foundry-upgrades b/lib/openzeppelin-foundry-upgrades new file mode 160000 index 0000000..cbce1e0 --- /dev/null +++ b/lib/openzeppelin-foundry-upgrades @@ -0,0 +1 @@ +Subproject commit cbce1e00305e943aa1661d43f41e5ac72c662b07 diff --git a/package.json b/package.json new file mode 100644 index 0000000..f3e3c8d --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "uts-monorepo", + "private": true, + "type": "module", + "scripts": { + "build": "pnpm -r run build", + "test": "pnpm -r run test:run", + "lint": "eslint packages apps --cache --fix", + "typecheck": "pnpm -r run typecheck", + "format": "prettier --write .", + "format:check": "prettier --check ." + }, + "devDependencies": { + "eslint": "^9.38.0", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import-x": "^4.16.1", + "eslint-plugin-unicorn": "^63.0.0", + "eslint-plugin-unused-imports": "^4.4.1", + "pnpm": "^10.26.2", + "prettier": "^3.8.1", + "typescript-eslint": "^8.56.1" + } +} diff --git a/packages/sdk/fixtures/test.ots b/packages/sdk/fixtures/test.ots new file mode 100644 index 0000000..0e706a0 Binary files /dev/null and b/packages/sdk/fixtures/test.ots differ diff --git a/packages/sdk/package.json b/packages/sdk/package.json new file mode 100644 index 0000000..0c459f6 --- /dev/null +++ b/packages/sdk/package.json @@ -0,0 +1,46 @@ +{ + "name": "@uts/sdk", + "version": "0.1.0", + "description": "", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "uts-source": "./src/index.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "files": [ + "dist", + "!dist/.tsbuildinfo", + "!dist/test", + "src/**/*.ts" + ], + "scripts": { + "test": "vitest", + "test:run": "vitest run", + "build": "rimraf dist && tsc && rollup -c", + "dev": "rollup -c --watch", + "lint": "eslint src --cache --fix", + "typecheck": "tsc --noEmit" + }, + "keywords": [], + "author": "Akase Haruka ", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "@noble/hashes": "^2.0.1", + "ethers": "^6.16.0" + }, + "devDependencies": { + "@rollup/plugin-node-resolve": "^16.0.3", + "@rollup/plugin-typescript": "^12.3.0", + "rimraf": "^6.1.3", + "rollup": "^4.59.0", + "typescript": "^5.9.3", + "vitest": "^4.0.18" + } +} diff --git a/packages/sdk/rollup.config.js b/packages/sdk/rollup.config.js new file mode 100644 index 0000000..dcff556 --- /dev/null +++ b/packages/sdk/rollup.config.js @@ -0,0 +1,25 @@ +import resolve from '@rollup/plugin-node-resolve' +import typescript from '@rollup/plugin-typescript' + +/** @type {import('rollup').RollupOptions[]} */ +export default [ + { + input: 'src/index.ts', + output: [ + { + file: 'dist/index.js', + format: 'es', + sourcemap: true, + }, + ], + external: [/node_modules/, '@noble/hashes', 'ethers'], + plugins: [ + resolve(), + typescript({ + tsconfig: './tsconfig.json', + declaration: false, + declarationMap: false, + }), + ], + }, +] diff --git a/packages/sdk/src/bmt.ts b/packages/sdk/src/bmt.ts new file mode 100644 index 0000000..c9e50ad --- /dev/null +++ b/packages/sdk/src/bmt.ts @@ -0,0 +1,276 @@ +import { type CHash } from '@noble/hashes/utils.js' + +export const INNER_NODE_PREFIX: number = 0x01 +const prefixBuffer = new Uint8Array([INNER_NODE_PREFIX]) + +export enum NodePosition { + /** The sibling is a right child, `APPEND` its hash when computing the parent */ + Left = 'LEFT', + /** The sibling is a left child, `PREPEND` its hash when computing the parent */ + Right = 'RIGHT', +} + +export class UnhashedFlatMerkleTree { + constructor( + public readonly buffer: Uint8Array[], + public readonly len: number, + ) {} + + /** + * Finalizes the Merkle tree by hashing internal nodes + */ + public finalize(factory: T): UnorderedMerkleTree { + const nodes = this.buffer + const len = this.len + + // Build the tree (from bottom to top) + for (let i = len - 1; i >= 1; i--) { + const left = nodes[2 * i] + const right = nodes[2 * i + 1] + + const hasher = factory.create() + hasher.update(prefixBuffer) + hasher.update(left) + hasher.update(right) + nodes[i] = hasher.digest() + } + + return new UnorderedMerkleTree(nodes, len, factory) + } +} + +/** + * Flat, Fixed-Size, Read only Merkle Tree + * * Expects the length of leaves to be equal or near(less) to a power of two. + * Leaves are **sorted** starting at index `len`. + */ +export class UnorderedMerkleTree { + /** Index 0 is not used, leaves start at index `len` */ + protected nodes: Uint8Array[] + protected len: number + + private readonly factory: T + + constructor(nodes: Uint8Array[], len: number, factory: T) { + this.nodes = nodes + this.len = len + this.factory = factory + } + + /** + * Constructs a new Merkle tree from the given hash leaves. + */ + public static new( + data: Uint8Array[], + factory: T, + ): UnorderedMerkleTree { + return UnorderedMerkleTree.newUnhashed(data, factory).finalize(factory) + } + + /** + * Constructs a new Merkle tree from the given hash leaves, without hashing internal nodes. + */ + public static newUnhashed( + prehashedLeaves: Uint8Array[], + factory: T, + ): UnhashedFlatMerkleTree { + const rawLen = prehashedLeaves.length + if (rawLen === 0) { + throw new Error('Cannot create Merkle tree with zero leaves') + } + + const len = nextPowerOfTwo(rawLen) + const nodes = new Array(2 * len) + + // index 0, we will never use it + nodes[0] = new Uint8Array(factory.outputLen) + + // Prepare leaves block + const leavesBlock = new Array(len) + for (let i = 0; i < len; i++) { + if (i < rawLen) { + if (prehashedLeaves[i].length !== factory.outputLen) { + throw new Error( + `Invalid leaf at index ${i}: expected length ${factory.outputLen}, got ${prehashedLeaves[i].length}`, + ) + } + leavesBlock[i] = prehashedLeaves[i] + } else { + // Pad with default (zeroed) hash + leavesBlock[i] = new Uint8Array(factory.outputLen) + } + } + + // Sort leaves unstable (lexicographical) + leavesBlock.sort(compareBytes) + + // Copy back to tree nodes + for (let i = 0; i < len; i++) { + nodes[len + i] = leavesBlock[i] + } + + return new UnhashedFlatMerkleTree(nodes, len) + } + + /** + * Returns the root hash of the Merkle tree + */ + public root(): Uint8Array { + return this.nodes[1] + } + + /** + * Returns the leaves of the Merkle tree + */ + public leaves(): Uint8Array[] { + return this.nodes.slice(this.len, this.len * 2) + } + + /** + * Checks if the given leaf is contained in the Merkle tree + */ + public contains(leaf: Uint8Array): boolean { + return binarySearch(this.leaves(), leaf) !== -1 + } + + /** + * Get proof for a given leaf + */ + public getProofIter(leaf: Uint8Array): SiblingIter | null { + const leafIndexInSlice = binarySearch(this.leaves(), leaf) + if (leafIndexInSlice === -1) { + return null + } + + return new SiblingIter(this.nodes, this.len + leafIndexInSlice) + } + + /** + * Returns the raw bytes of the Merkle tree nodes (mimicking bytemuck::cast_slice) + */ + public asRawBytes(): Uint8Array { + const totalSize = this.nodes.length * this.factory.outputLen + const bytes = new Uint8Array(totalSize) + + for (let i = 0; i < this.nodes.length; i++) { + bytes.set(this.nodes[i], i * this.factory.outputLen) + } + + return bytes + } + + /** + * From raw bytes, reconstruct the Merkle tree + */ + public static fromRawBytes( + bytes: Uint8Array, + factory: T, + ): UnorderedMerkleTree { + if (bytes.length % factory.outputLen !== 0) { + throw new Error('Bytes length must be a multiple of hashLength') + } + const totalNodes = bytes.length / factory.outputLen + if (totalNodes % 2 !== 0) { + throw new Error('Invalid tree structure: node count is not even') + } + + const len = totalNodes / 2 + const nodes = new Array(totalNodes) + + for (let i = 0; i < totalNodes; i++) { + nodes[i] = bytes.slice(i * factory.outputLen, (i + 1) * factory.outputLen) + } + + return new UnorderedMerkleTree(nodes, len, factory) + } +} + +/** + * Iterator over the sibling nodes of a leaf in the Merkle tree + */ +export class SiblingIter implements IterableIterator<{ + position: NodePosition + sibling: Uint8Array +}> { + private readonly nodes: Uint8Array[] + private current: number + + constructor(nodes: Uint8Array[], current: number) { + this.nodes = nodes + this.current = current + } + + public next(): IteratorResult<{ + position: NodePosition + sibling: Uint8Array + }> { + if (this.current <= 1) { + return { done: true, value: undefined } + } + + const isLeft = (this.current & 1) === 0 + const position = isLeft ? NodePosition.Left : NodePosition.Right + + const siblingIndex = this.current ^ 1 + const sibling = this.nodes[siblingIndex] + + this.current >>= 1 + + return { + done: false, + value: { position, sibling }, + } + } + + public [Symbol.iterator](): IterableIterator<{ + position: NodePosition + sibling: Uint8Array + }> { + return this + } + + /** Returns the exact remaining size of the iterator */ + public get length(): number { + if (this.current <= 1) return 0 + return 31 - Math.clz32(this.current) + } +} + +// --- Helper Functions --- + +function nextPowerOfTwo(n: number): number { + if (n <= 1) return 1 + let p = 1 + while (p < n) p *= 2 + return p +} + +function compareBytes(a: Uint8Array, b: Uint8Array): number { + const len = Math.min(a.length, b.length) + for (let i = 0; i < len; i++) { + if (a[i] !== b[i]) { + return a[i] - b[i] + } + } + return a.length - b.length +} + +function binarySearch(arr: Uint8Array[], target: Uint8Array): number { + let left = 0 + let right = arr.length - 1 + + while (left <= right) { + const mid = (left + right) >>> 1 + const cmp = compareBytes(arr[mid], target) + + if (cmp === 0) { + return mid + } else if (cmp < 0) { + left = mid + 1 + } else { + right = mid - 1 + } + } + + return -1 +} diff --git a/packages/sdk/src/codec/constants.ts b/packages/sdk/src/codec/constants.ts new file mode 100644 index 0000000..9d2a6d2 --- /dev/null +++ b/packages/sdk/src/codec/constants.ts @@ -0,0 +1,46 @@ +import { type DigestOp, type Op } from '../types.ts' + +export const OP_CODE_MAP: Record = { + SHA1: 0x02, + RIPEMD160: 0x03, + SHA256: 0x08, + KECCAK256: 0x67, + APPEND: 0xf0, + PREPEND: 0xf1, + REVERSE: 0xf2, + HEXLIFY: 0xf3, + ATTESTATION: 0x00, + FORK: 0xff, +} + +export const DIGEST_LENGTHS: Record = { + SHA1: 20, + RIPEMD160: 20, + SHA256: 32, + KECCAK256: 32, +} + +export const getOpName = (code: number): Op => { + return Object.entries(OP_CODE_MAP).find(([_, v]) => v === code)?.[0] as Op +} + +// b"\x00OpenTimestamps\x00\x00Proof\x00\xbf\x89\xe2\xe8\x84\xe8\x92\x94"; +export const MAGIC_BYTES = new Uint8Array([ + 0x0, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x73, 0x0, 0x0, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x0, 0xbf, 0x89, 0xe2, + 0xe8, 0x84, 0xe8, 0x92, 0x94, +]) + +export const ATTESTATION_TAG_LENGTH = 8 +export const BITCOIN_ATTESTATION_TAG = new Uint8Array([ + 0x05, 0x88, 0x96, 0x0d, 0x73, 0xd7, 0x19, 0x01, +]) +export const PENDING_ATTESTATION_TAG = new Uint8Array([ + 0x83, 0xdf, 0xe3, 0x0d, 0x2e, 0xf9, 0x0c, 0x8e, +]) +export const ETHEREUM_UTS_ATTESTATION_TAG = new Uint8Array([ + 0xea, 0xf2, 0xbc, 0x69, 0x3c, 0x93, 0x25, 0x1c, +]) + +export const MAX_URI_LEN = 1000 +export const SAFE_URL_REGEX = /^http(s)?:\/\/[\s\w./:-]+$/ diff --git a/packages/sdk/src/codec/decode.ts b/packages/sdk/src/codec/decode.ts new file mode 100644 index 0000000..147e679 --- /dev/null +++ b/packages/sdk/src/codec/decode.ts @@ -0,0 +1,384 @@ +import { hexlify } from 'ethers/utils' +import { DecodeError, ErrorCode } from '../errors.ts' +import { + DIGEST_OPS, + type AttestationStep, + type BitcoinAttestation, + type DetachedTimestamp, + type DigestHeader, + type DigestOp, + type EthereumUTSAttestation, + type ExecutionStep, + type ForkStep, + type Op, + type PendingAttestation, + type Step, + type Timestamp, +} from '../types.ts' +import { + ATTESTATION_TAG_LENGTH, + BITCOIN_ATTESTATION_TAG, + DIGEST_LENGTHS, + ETHEREUM_UTS_ATTESTATION_TAG, + getOpName, + MAGIC_BYTES, + MAX_URI_LEN, + PENDING_ATTESTATION_TAG, + SAFE_URL_REGEX, +} from './constants.ts' + +const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER) +export default class Decoder { + private readonly view: DataView + private offset: number = 0 + private readonly length: number + + private static textDecoder = new TextDecoder() + + constructor(buffer: Uint8Array) { + this.view = new DataView( + buffer.buffer, + buffer.byteOffset, + buffer.byteLength, + ) + this.length = buffer.byteLength + this.offset = 0 + } + + get remaining(): number { + return this.length - this.offset + } + + private checkBounds(required: number): void { + if (this.offset + required > this.length) { + throw new DecodeError( + ErrorCode.UNEXPECTED_EOF, + `Unexpected end of stream: needed ${required} bytes but only ${this.remaining} available`, + { offset: this.offset }, + ) + } + } + + checkEOF(): void { + if (this.remaining > 0) { + throw new DecodeError( + ErrorCode.INVALID_STRUCTURE, + `Expected end of stream but ${this.remaining} bytes remain`, + { offset: this.offset }, + ) + } + } + + readByte(): number { + this.checkBounds(1) + return this.view.getUint8(this.offset++) + } + + readBytes(length: number): Uint8Array { + this.checkBounds(length) + const slice = new Uint8Array( + this.view.buffer, + this.view.byteOffset + this.offset, + length, + ) + this.offset += length + return slice + } + + readNumber(): number { + const result = this.readBigUint() + if (result > MAX_SAFE_INTEGER) { + throw new DecodeError( + ErrorCode.OVERFLOW, + `Decoded number exceeds MAX_SAFE_INTEGER: ${result}`, + { offset: this.offset }, + ) + } + return Number(result) + } + + readBigUint(): bigint { + let result = 0n + let shift = 0n + + while (true) { + const byte = this.readByte() + result |= BigInt(byte & 0x7f) << shift + + if ((byte & 0x80) === 0) break + + shift += 7n + } + + return result + } + + readLengthPrefixedBytes(): Uint8Array { + const len = this.readNumber() + return this.readBytes(len) + } + + peekOp(): Op | null { + if (this.remaining === 0) return null + const code = this.view.getUint8(this.offset) + return getOpName(code) + } + + readOp(): Op { + const code = this.readByte() + const op = getOpName(code) + if (!op) { + throw new DecodeError( + ErrorCode.UNKNOWN_OP, + `Unknown opcode: 0x${code.toString(16).padStart(2, '0')}`, + { offset: this.offset - 1, context: { code } }, + ) + } + return op + } + + readVersionedMagic(): number { + const magic = this.readBytes(MAGIC_BYTES.length) + if (!magic.every((val, idx) => val === MAGIC_BYTES[idx])) { + throw new DecodeError(ErrorCode.BAD_MAGIC, 'Invalid magic bytes', { + offset: this.offset - MAGIC_BYTES.length, + context: { found: hexlify(magic) }, + }) + } + return this.readByte() + } + + readHeader(): DigestHeader { + const op = this.readOp() + + if (!(DIGEST_OPS as readonly string[]).includes(op)) { + throw new DecodeError( + ErrorCode.INVALID_STRUCTURE, + `Expected digest op in header, got: ${op}`, + { offset: this.offset - 1, context: { op } }, + ) + } + + const kind = op as DigestOp + const len = DIGEST_LENGTHS[kind] + const digest = this.readBytes(len) + + return { kind, digest } + } + + readExecutionStep(): ExecutionStep { + const op = this.readOp() + switch (op) { + case 'APPEND': + case 'PREPEND': + const data = this.readLengthPrefixedBytes() + return { op, data } + case 'FORK': + case 'ATTESTATION': + throw new DecodeError( + ErrorCode.INVALID_STRUCTURE, + `Unexpected ${op} step in execution steps, should be handled separately`, + { offset: this.offset - 1, context: { op } }, + ) + default: + return { op } + } + } + + readForkStep(): ForkStep { + const steps: Timestamp[] = [] + if (this.peekOp() !== 'FORK') { + throw new DecodeError( + ErrorCode.INVALID_STRUCTURE, + `Expected FORK op at the beginning of fork step, got: ${this.peekOp()}`, + { offset: this.offset, context: { op: this.peekOp() } }, + ) + } + + while (true) { + const op = this.peekOp() + if (op === 'FORK') { + // not the last branch, consume the FORK op and continue + this.readOp() + steps.push(this.readTimestamp()) + } else { + // last branch, read it and break + steps.push(this.readTimestamp()) + break + } + } + + return { op: 'FORK', steps } + } + + readPendingAttestation(): PendingAttestation { + const urlBytes = this.readLengthPrefixedBytes() + const urlStr = Decoder.textDecoder.decode(urlBytes) + + if (urlStr.length > MAX_URI_LEN) { + throw new DecodeError( + ErrorCode.INVALID_URI, + `Attestation URL exceeds maximum length of ${MAX_URI_LEN} characters`, + { offset: this.offset - urlBytes.length, context: { url: urlStr } }, + ) + } + + if (!SAFE_URL_REGEX.test(urlStr)) { + throw new DecodeError( + ErrorCode.INVALID_URI, + `Invalid URL in pending attestation: ${urlStr}`, + { offset: this.offset, context: { url: urlStr } }, + ) + } + + try { + const url = new URL(urlStr) + return { kind: 'pending', url } + } catch (error) { + throw new DecodeError( + ErrorCode.INVALID_URI, + `Malformed URL in pending attestation: ${urlStr}`, + { + offset: this.offset, + context: { + url: urlStr, + error: error instanceof Error ? error.message : String(error), + }, + }, + ) + } + } + + readBitcoinAttestation(): BitcoinAttestation { + return { + kind: 'bitcoin', + height: this.readNumber(), + } + } + + readEthereumUTSAttestation(): EthereumUTSAttestation { + const chain = this.readNumber() + const height = this.readNumber() + + if (this.remaining === 0) { + return { + kind: 'ethereum-uts', + chain, + height, + } + } + + if (this.remaining > 0 && this.remaining < 20) { + throw new DecodeError( + ErrorCode.INVALID_STRUCTURE, + `Invalid extra metadata length for Ethereum UTS attestation: expected 0 or at least 20 bytes, got ${this.remaining}`, + { offset: this.offset, context: { remaining: this.remaining } }, + ) + } + // Extra metadata is optional, only read if there's remaining data + const contract = this.readBytes(20) + if (this.remaining === 0) { + return { + kind: 'ethereum-uts', + chain, + height, + metadata: { contract }, + } + } + + if (this.remaining > 0 && this.remaining < 32) { + throw new DecodeError( + ErrorCode.INVALID_STRUCTURE, + `Invalid extra metadata length for Ethereum UTS attestation with contract: expected 0 or 32 bytes, got ${this.remaining}`, + { offset: this.offset, context: { remaining: this.remaining } }, + ) + } + + const txHash = this.readBytes(32) + return { + kind: 'ethereum-uts', + chain, + height, + metadata: { contract, txHash }, + } + } + + readAttestationStep(strict?: boolean): AttestationStep { + const op = this.readOp() + if (op !== 'ATTESTATION') { + throw new DecodeError( + ErrorCode.INVALID_STRUCTURE, + `Expected ATTESTATION op, got: ${op}`, + { offset: this.offset - 1, context: { op } }, + ) + } + + const tag = this.readBytes(ATTESTATION_TAG_LENGTH) + const data = this.readLengthPrefixedBytes() + const decoder = new Decoder(data) + if (tag.every((byte, idx) => byte === BITCOIN_ATTESTATION_TAG[idx])) { + const attestation = decoder.readBitcoinAttestation() + if (strict) decoder.checkEOF() + return { op: 'ATTESTATION', attestation } + } else if ( + tag.every((byte, idx) => byte === PENDING_ATTESTATION_TAG[idx]) + ) { + const attestation = decoder.readPendingAttestation() + if (strict) decoder.checkEOF() + return { op: 'ATTESTATION', attestation } + } else if ( + tag.every((byte, idx) => byte === ETHEREUM_UTS_ATTESTATION_TAG[idx]) + ) { + const attestation = decoder.readEthereumUTSAttestation() + if (strict) decoder.checkEOF() + return { op: 'ATTESTATION', attestation } + } else { + return { + op: 'ATTESTATION', + attestation: { + kind: 'unknown', + tag, + data, + }, + } + } + } + + readStep(strict?: boolean): Step { + const op = this.peekOp() + switch (op) { + case 'FORK': + return this.readForkStep() + case 'ATTESTATION': + return this.readAttestationStep(strict) + default: + return this.readExecutionStep() + } + } + + readTimestamp(strict?: boolean): Timestamp { + const steps: Step[] = [] + while (this.remaining > 0) { + const step = this.readStep(strict) + steps.push(step) + if (step.op === 'FORK' || step.op === 'ATTESTATION') break + } + return steps + } + + readDetachedTimestamp(strict?: boolean): DetachedTimestamp { + const version = this.readVersionedMagic() + if (version !== 0x01) { + throw new DecodeError( + ErrorCode.INVALID_STRUCTURE, + `Unsupported detached timestamp version: 0x${version.toString(16)}`, + { offset: this.offset - 1, context: { version } }, + ) + } + const header = this.readHeader() + const timestamp = this.readTimestamp(strict) + const detached = { header, timestamp } + if (strict) this.checkEOF() + return detached + } +} diff --git a/packages/sdk/src/codec/encode.ts b/packages/sdk/src/codec/encode.ts new file mode 100644 index 0000000..d157987 --- /dev/null +++ b/packages/sdk/src/codec/encode.ts @@ -0,0 +1,315 @@ +import { + OP_CODE_MAP, + MAGIC_BYTES, + PENDING_ATTESTATION_TAG, + BITCOIN_ATTESTATION_TAG, + ETHEREUM_UTS_ATTESTATION_TAG, + MAX_URI_LEN, + SAFE_URL_REGEX, + DIGEST_LENGTHS, +} from './constants.ts' +import type { + DetachedTimestamp, + Step, + ForkStep, + AttestationStep, + ExecutionStep, + DigestHeader, + Timestamp, + Op, + PendingAttestation, + BitcoinAttestation, + EthereumUTSAttestation, +} from '../types.ts' +import { getBytes, hexlify } from 'ethers/utils' +import { EncodeError, ErrorCode } from '../errors.ts' + +export default class Encoder { + private buffer: Uint8Array + private offset: number = 0 + + private static textEncoder = new TextEncoder() + + constructor(initialSize: number = 1024) { + this.buffer = new Uint8Array(initialSize) + } + + toUint8Array(): Uint8Array { + return this.buffer.slice(0, this.offset) + } + + private ensureCapacity(required: number) { + if (this.offset + required > this.buffer.length) { + const newLen = Math.max(this.buffer.length * 2, this.offset + required) + const newBuffer = new Uint8Array(newLen) + newBuffer.set(this.buffer) + this.buffer = newBuffer + } + } + + writeByte(byte: number): this { + this.ensureCapacity(1) + this.buffer[this.offset++] = byte + return this + } + + writeBytes(data: Uint8Array): this { + this.ensureCapacity(data.length) + this.buffer.set(data, this.offset) + this.offset += data.length + return this + } + + writeU32(value: number): this { + if (value < 0 || !Number.isInteger(value)) { + throw new EncodeError( + ErrorCode.NEGATIVE_LEB128_INPUT, + `LEB128 only supports non-negative integers, got ${value}`, + { offset: this.offset }, + ) + } + if (value > 0xffffffff) { + throw new EncodeError( + ErrorCode.OVERFLOW, + `Value exceeds maximum for u32: ${value}, use writeBigUint instead`, + { offset: this.offset }, + ) + } + + let n = value + do { + // Get bottom 7 bits + let byte = n & 0x7f + n >>>= 7 // Unsigned right shift + + // If there are more bits to come, set the continuation bit (0x80) + if (n !== 0) { + byte |= 0x80 + } + + this.writeByte(byte) + } while (n !== 0) + + return this + } + + writeBigUint(value: bigint): this { + if (value < 0n) { + throw new EncodeError( + ErrorCode.NEGATIVE_LEB128_INPUT, + `LEB128 only supports non-negative integers, got ${value}`, + { offset: this.offset }, + ) + } + + let n = value + do { + // Get bottom 7 bits + let byte = Number(n & 0x7fn) + n >>= 7n // Right shift by 7 bits + + // If there are more bits to come, set the continuation bit (0x80) + if (n !== 0n) { + byte |= 0x80 + } + + this.writeByte(byte) + } while (n !== 0n) + + return this + } + + writeLengthPrefixedBytes(data: Uint8Array): this { + const len = data.length + this.writeU32(len) + this.writeBytes(data) + return this + } + + writeOp(op: Op): this { + const opCode = OP_CODE_MAP[op] + if (opCode === undefined) { + throw new EncodeError(ErrorCode.UNKNOWN_OP, `Unknown operation: ${op}`, { + offset: this.offset, + context: { op }, + }) + } + return this.writeByte(opCode) + } + + writeVersionedMagic(version: number): this { + this.writeBytes(MAGIC_BYTES) + this.writeByte(version) + return this + } + + writeHeader(header: DigestHeader): this { + this.writeOp(header.kind) + const digestBytes = getBytes(header.digest) + if (digestBytes.length !== DIGEST_LENGTHS[header.kind]) { + throw new EncodeError( + ErrorCode.LENGTH_MISMATCH, + `Digest length mismatch for ${header.kind}: expected ${DIGEST_LENGTHS[header.kind]}, got ${digestBytes.length}`, + { offset: this.offset, context: { header } }, + ) + } + this.writeBytes(digestBytes) + return this + } + + writeExecutionStep(step: ExecutionStep): this { + this.writeOp(step.op) + switch (step.op) { + case 'APPEND': + case 'PREPEND': + this.writeLengthPrefixedBytes(getBytes(step.data)) + break + } + return this + } + + writeForkStep(step: ForkStep): this { + if (step.steps.length < 2) { + throw new EncodeError( + ErrorCode.INVALID_STRUCTURE, + 'FORK step must have at least 2 branches', + { offset: this.offset, context: { step } }, + ) + } + for (const branch of step.steps.slice(0, step.steps.length - 1)) { + this.writeOp(step.op) + this.writeTimestamp(branch) + } + this.writeTimestamp(step.steps.at(-1)!) + return this + } + + writePendingAttestation(attestation: PendingAttestation): this { + let urlStr = attestation.url.toString() + // trim url ends with slash + if (urlStr.endsWith('/')) { + urlStr = urlStr.slice(0, -1) + } + + if (urlStr.length > MAX_URI_LEN) { + throw new EncodeError( + ErrorCode.INVALID_URI, + `URL in pending attestation exceeds maximum length of ${MAX_URI_LEN}: ${urlStr}`, + { offset: this.offset, context: { url: urlStr } }, + ) + } + if (!SAFE_URL_REGEX.test(urlStr)) { + throw new EncodeError( + ErrorCode.INVALID_URI, + `Invalid URL in pending attestation: ${urlStr}`, + { offset: this.offset, context: { url: urlStr } }, + ) + } + // Encode URL as UTF-8 bytes + const urlBytes = Encoder.textEncoder.encode(urlStr) + this.writeLengthPrefixedBytes(urlBytes) + return this + } + + writeBitcoinAttestation(attestation: BitcoinAttestation): this { + this.writeU32(attestation.height) + return this + } + + writeEthereumUTSAttestation(attestation: EthereumUTSAttestation): this { + this.writeU32(attestation.chain) + this.writeU32(attestation.height) + if (attestation.metadata) { + // trailing optional + if (attestation.metadata.contract) { + const contractBytes = getBytes(attestation.metadata.contract) + if (contractBytes.length !== 20) { + throw new EncodeError( + ErrorCode.LENGTH_MISMATCH, + `Invalid contract address in Ethereum UTS attestation: ${attestation.metadata.contract}`, + { + offset: this.offset, + context: { contract: hexlify(attestation.metadata.contract) }, + }, + ) + } + this.writeBytes(contractBytes) + + if (attestation.metadata.txHash) { + const txHashBytes = getBytes(attestation.metadata.txHash) + if (txHashBytes.length !== 32) { + throw new EncodeError( + ErrorCode.LENGTH_MISMATCH, + `Invalid transaction hash in Ethereum UTS attestation: ${attestation.metadata.txHash}`, + { + offset: this.offset, + context: { txHash: hexlify(attestation.metadata.txHash) }, + }, + ) + } + this.writeBytes(txHashBytes) + } + } + } + return this + } + + writeAttestationStep(step: AttestationStep): this { + this.writeOp('ATTESTATION') + const encoder = new Encoder() + switch (step.attestation.kind) { + case 'pending': + this.writeBytes(PENDING_ATTESTATION_TAG) + encoder.writePendingAttestation(step.attestation) + this.writeLengthPrefixedBytes(encoder.toUint8Array()) + break + case 'bitcoin': + this.writeBytes(BITCOIN_ATTESTATION_TAG) + encoder.writeBitcoinAttestation(step.attestation) + this.writeLengthPrefixedBytes(encoder.toUint8Array()) + break + case 'ethereum-uts': + this.writeBytes(ETHEREUM_UTS_ATTESTATION_TAG) + encoder.writeEthereumUTSAttestation(step.attestation) + this.writeLengthPrefixedBytes(encoder.toUint8Array()) + break + case 'unknown': + this.writeBytes(getBytes(step.attestation.tag)) + this.writeLengthPrefixedBytes(getBytes(step.attestation.data)) + break + default: + throw new EncodeError( + ErrorCode.GENERAL_ERROR, + `Unsupported attestation: ${step.attestation}`, + { offset: this.offset, context: { attestation: step.attestation } }, + ) + } + return this + } + + writeStep(step: Step): this { + switch (step.op) { + case 'FORK': + return this.writeForkStep(step as ForkStep) + case 'ATTESTATION': + return this.writeAttestationStep(step as AttestationStep) + default: + return this.writeExecutionStep(step as ExecutionStep) + } + } + + writeTimestamp(timestamp: Timestamp): this { + for (const step of timestamp) { + this.writeStep(step) + } + return this + } + + static encodeDetachedTimestamp(ots: DetachedTimestamp): Uint8Array { + return new Encoder() + .writeVersionedMagic(0x01) + .writeHeader(ots.header) + .writeTimestamp(ots.timestamp) + .toUint8Array() + } +} diff --git a/packages/sdk/src/errors.ts b/packages/sdk/src/errors.ts new file mode 100644 index 0000000..b202cb4 --- /dev/null +++ b/packages/sdk/src/errors.ts @@ -0,0 +1,87 @@ +export enum ErrorCode { + GENERAL_ERROR = 'GENERAL_ERROR', + + BAD_MAGIC = 'BAD_MAGIC', + + UNKNOWN_OP = 'UNKNOWN_OP', + INVALID_STRUCTURE = 'INVALID_STRUCTURE', + + NEGATIVE_LEB128_INPUT = 'NEGATIVE_LEB128_INPUT', + OVERFLOW = 'OVERFLOW', + + INVALID_URI = 'INVALID_URI', + + LENGTH_MISMATCH = 'LENGTH_MISMATCH', + + UNEXPECTED_EOF = 'UNEXPECTED_EOF', + + REMOTE_ERROR = 'REMOTE_ERROR', + + UNSUPPORTED_ATTESTATION = 'UNSUPPORTED_ATTESTATION', + ATTESTATION_MISMATCH = 'ATTESTATION_MISMATCH', +} + +export class UTSError extends Error { + public readonly code: ErrorCode + + // Optional: offset in the input data where the error occurred + public readonly offset?: number + // Optional fields for additional context + public readonly context?: Record + + constructor( + code: ErrorCode, + message: string, + options?: { offset?: number; context?: Record }, + ) { + super(message) + this.name = 'UTSError' + this.code = code + this.offset = options?.offset + this.context = options?.context + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, UTSError) + } + } +} + +export class EncodeError extends UTSError { + constructor( + code: ErrorCode, + message: string, + options?: { offset?: number; context?: Record }, + ) { + super(code, `[Encode] ${message}`, options) + this.name = 'EncodeError' + } +} + +export class DecodeError extends UTSError { + constructor( + code: ErrorCode, + message: string, + options?: { offset?: number; context?: Record }, + ) { + super(code, `[Decode] ${message}`, options) + this.name = 'DecodeError' + } +} + +export class RemoteError extends UTSError { + constructor(message: string, options?: { context?: Record }) { + super(ErrorCode.REMOTE_ERROR, `[Remote] ${message}`, options) + this.name = 'RemoteError' + } +} + +export class VerifyError extends UTSError { + constructor( + code: ErrorCode, + message: string, + options?: { context?: Record }, + ) { + super(code, `[Verify] ${message}`, options) + this.name = 'VerifyError' + } +} diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts new file mode 100644 index 0000000..c909047 --- /dev/null +++ b/packages/sdk/src/index.ts @@ -0,0 +1,59 @@ +import { hexlify as h } from 'ethers' + +export type * from './types.ts' + +export type { + Attestation, + PendingAttestation, + BitcoinAttestation, + EthereumUTSAttestation, + EthereumUTSAttestationExtraMetadata, +} from './types.ts' + +export { + DIGEST_OPS, + UpgradeStatus, + AttestationStatusKind, + VerifyStatus, +} from './types.ts' + +export { default as Encoder } from './codec/encode.ts' +export { default as Decoder } from './codec/decode.ts' + +export * from './errors.ts' + +export * from './codec/constants.ts' + +export * from './bmt.ts' + +export { default as BitcoinRPC } from './rpc/btc.ts' + +export { + default as SDK, + DEFAULT_CALENDARS, + UTS_ABI, + WELL_KNOWN_CHAINS, +} from './sdk.ts' +export type { SDKOptions, StampEvent, StampEventCallback } from './sdk.ts' + +export const hexlify = (obj: any): any => { + if (obj instanceof URL) { + return obj + } + if (obj instanceof Uint8Array) { + return h(obj) + } + if (Array.isArray(obj)) { + return obj.map((item) => hexlify(item)) + } + if (typeof obj === 'object' && obj !== null) { + const result: any = {} + for (const key in obj) { + if (Object.hasOwn(obj, key)) { + result[key] = hexlify(obj[key]) + } + } + return result + } + return obj +} diff --git a/packages/sdk/src/rpc/btc.ts b/packages/sdk/src/rpc/btc.ts new file mode 100644 index 0000000..5da2f80 --- /dev/null +++ b/packages/sdk/src/rpc/btc.ts @@ -0,0 +1,127 @@ +import { RemoteError } from '../errors.ts' + +export interface BitcoinRPCResponse { + jsonrpc: string + id: number + result?: any + error?: { + code: number + message: string + data?: any + } +} + +export interface BitcoinBlockHeader { + hash: string + confirmations: number + height: number + version: number + versionHex: string + merkleroot: string + time: number + mediantime: number + nonce: number + bits: string + target: string + difficulty: number + chainwork: string + nTx: number + previousblockhash?: string + nextblockhash?: string +} + +export default class BitcoinRPC { + readonly url: URL = new URL('https://bitcoin-rpc.publicnode.com') + + constructor(url?: URL) { + if (url) { + this.url = url + } + } + + async call(method: string, params: any[] = []): Promise { + let response: Response + try { + response = await fetch(this.url.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + jsonrpc: '1.0', + method, + params, + id: 1, + }), + }) + } catch (err) { + throw new RemoteError('Bitcoin RPC network error', { + context: { + url: this.url.toString(), + error: err instanceof Error ? err.message : String(err), + }, + }) + } + + let rawBody: string + try { + rawBody = await response.text() + } catch (err) { + throw new RemoteError('Bitcoin RPC error reading response body', { + context: { + status: response.status, + statusText: response.statusText, + error: err instanceof Error ? err.message : String(err), + }, + }) + } + + if (!response.ok) { + throw new RemoteError( + `Bitcoin RPC HTTP error: ${response.status} ${response.statusText}`, + { + context: { + status: response.status, + statusText: response.statusText, + response: rawBody, + }, + }, + ) + } + + let data: BitcoinRPCResponse + try { + data = JSON.parse(rawBody) as BitcoinRPCResponse + } catch (err) { + throw new RemoteError('Bitcoin RPC invalid JSON response', { + context: { + status: response.status, + statusText: response.statusText, + body: rawBody, + error: err instanceof Error ? err.message : String(err), + }, + }) + } + + if (data.result !== undefined) { + return data.result + } + if (data.error !== undefined && data.error !== null) { + throw new RemoteError(`Bitcoin RPC error: ${data.error.message}`, { + context: { + code: data.error.code, + data: data.error.data, + }, + }) + } + return data.result + } + + getBlockHash(height: number): Promise { + return this.call('getblockhash', [height]) + } + + getBlockHeader(hash: string): Promise { + return this.call('getblockheader', [hash]) + } +} diff --git a/packages/sdk/src/sdk.ts b/packages/sdk/src/sdk.ts new file mode 100644 index 0000000..9cfed58 --- /dev/null +++ b/packages/sdk/src/sdk.ts @@ -0,0 +1,845 @@ +import { + type AbstractProvider, + type Eip1193Provider, + BrowserProvider, + getBytes, + hexlify, + id, + Interface, + JsonRpcProvider, +} from 'ethers' +import { + AttestationStatusKind, + UpgradeStatus, + VerifyStatus, + type Attestation, + type AttestationStatus, + type BitcoinAttestation, + type DetachedTimestamp, + type DigestHeader, + type EthereumUTSAttestation, + type ExecutionStep, + type ForkStep, + type PendingAttestation, + type SecureDigestOp, + type Timestamp, + type UpgradeResult, +} from './types.ts' +import type { CHash } from '@noble/hashes/utils.js' +import { sha256 } from '@noble/hashes/sha2.js' +import { keccak_256 } from '@noble/hashes/sha3.js' +import { INNER_NODE_PREFIX, NodePosition, UnorderedMerkleTree } from './bmt.ts' +import Decoder from './codec/decode.ts' +import { EncodeError, ErrorCode, RemoteError, VerifyError } from './errors.ts' +import { ripemd160, sha1 } from '@noble/hashes/legacy.js' +import BitcoinRPC from './rpc/btc.ts' + +export type StampEvent = + | { phase: 'generating-nonce' } + | { phase: 'building-merkle-tree' } + | { phase: 'broadcasting'; totalCalendars: number } + | { + phase: 'calendar-response' + calendarUrl: string + success: boolean + responsesReceived: number + totalCalendars: number + } + | { phase: 'building-proof' } + | { phase: 'complete' } + +export type StampEventCallback = (event: StampEvent) => void + +export interface SDKOptions { + calendars?: URL[] + btcRPC?: BitcoinRPC + ethRPCs?: Record + web3Provider?: Eip1193Provider | null + timeout?: number + quorum?: number + nonceSize?: number + hashAlgorithm?: SecureDigestOp +} + +/** + * Well-known EVM chain IDs to hex for wallet_switchEthereumChain. + */ +export const WELL_KNOWN_CHAINS: Record< + number, + { chainId: string; chainName: string } +> = { + 1: { chainId: '0x1', chainName: 'Ethereum Mainnet' }, + 17000: { chainId: '0x4268', chainName: 'Holesky' }, + 11155111: { chainId: '0xaa36a7', chainName: 'Sepolia' }, + 534352: { chainId: '0x82750', chainName: 'Scroll' }, + 534351: { chainId: '0x8274f', chainName: 'Scroll Sepolia' }, +} + +export const DEFAULT_CALENDARS = [ + // new URL('https://a.pool.opentimestamps.org/'), + // new URL('https://b.pool.opentimestamps.org/'), + // new URL('https://a.pool.eternitywall.com/'), + // new URL('https://ots.btc.catallaxy.com/'), + new URL('http://127.0.0.1:3000/'), +] + +export const UTS_ABI = [ + 'event Attested(bytes32 indexed root, address indexed sender, uint256 timestamp)', + 'function attest(bytes32 root) external', + 'function timestamp(bytes32 root) external view returns (uint256)', +] + +export default class SDK { + readonly calendars: URL[] + btcRPC: BitcoinRPC + ethRPCs: Record + web3Provider: Eip1193Provider | null = null + + /** + * Maximum time to wait for calendar responses in milliseconds. + * + * Calendars that do not respond within this time will be ignored, + * and the timestamp will be generated from the successful responses received prior to the timeout. + */ + timeout: number = 10000 + + /** + * Consider the timestamp complete if at least M calendars reply prior to the timeout + */ + quorum: number + + /** + * Number of random bytes to append to each digest before stamping, which are required to generate the internal Merkle proof. + * + * This is needed to prevent leaking information about the original digest to the calendar servers, + * which could be used to censor or preimage attack the digest. + * + * The nonce is included in the timestamp and can be safely revealed without compromising the security of the original digest. + */ + nonceSize: number + + private hashAlg: SecureDigestOp = 'KECCAK256' + private hasher: CHash = keccak_256 + + private static encoder = new TextEncoder() + + // 0x61cae4201bb8c0117495b22a70f5202410666b349c27302dac280dc054b60f2a + static readonly utsLogTopic = id('Attested(bytes32,address,uint256)') + static readonly utsInterface = new Interface(UTS_ABI) + + constructor(options: SDKOptions = {}) { + const { + calendars = DEFAULT_CALENDARS, + btcRPC = new BitcoinRPC(), + ethRPCs = { + 534352: new JsonRpcProvider('https://rpc.scroll.io'), + 534351: new JsonRpcProvider('https://sepolia-rpc.scroll.io'), + }, + web3Provider = null, + timeout = 10000, + nonceSize = 32, + hashAlgorithm = 'KECCAK256', + quorum, + } = options + + this.calendars = calendars + this.btcRPC = btcRPC + this.ethRPCs = ethRPCs + this.web3Provider = web3Provider + + this.timeout = timeout + this.nonceSize = nonceSize + + this.quorum = quorum ?? Math.ceil(this.calendars.length * 0.66) + this.hashAlgorithm = hashAlgorithm + } + + get hashAlgorithm(): SecureDigestOp { + return this.hashAlg + } + + /** + * Set the hash algorithm to be used during stamping. + * + * This will affect the internal Merkle tree construction and the proof generation. + * + * Supported algorithms are 'SHA256' and 'KECCAK256'. + * @param alg + */ + set hashAlgorithm(alg: SecureDigestOp) { + this.hashAlg = alg + switch (alg) { + case 'SHA256': + this.hasher = sha256 + break + case 'KECCAK256': + this.hasher = keccak_256 + break + default: + throw new Error(`Unsupported hash algorithm: ${alg}`) + } + } + + getEthProvider(chainId: number): AbstractProvider | null { + if (Object.hasOwn(this.ethRPCs, chainId)) { + return this.ethRPCs[chainId]! + } + return null + } + + /** + * Try to get a provider for the given chain from the web3 wallet. + * If the wallet is on a different chain, attempts to switch to the target chain if it's well-known. + * Returns null if no web3Provider or if switching fails. + */ + async getWeb3ProviderForChain( + chainId: number, + ): Promise { + if (!this.web3Provider) return null + + try { + const browser = new BrowserProvider(this.web3Provider) + const network = await browser.getNetwork() + if (Number(network.chainId) === chainId) { + return browser + } + + // Try switching to the target chain if it's well-known + const knownChain = WELL_KNOWN_CHAINS[chainId] + if (knownChain) { + try { + await this.web3Provider.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: knownChain.chainId }], + }) + return new BrowserProvider(this.web3Provider) + } catch { + // Switch failed, fall through + } + } + } catch { + // web3Provider not usable + } + return null + } + + /** + * Stamp the provided digests by submitting them to the configured calendars. + * + * @param digests The digests to be stamped, each with its associated header information. Input digests can use different hash algorithms, but the internal Merkle tree will be constructed using the SDK's configured hash algorithm (default KECCAK256). + */ + async stamp( + digests: DigestHeader[], + onEvent?: StampEventCallback, + ): Promise { + const nonces: Uint8Array[] = [] + const nonceDigests: Uint8Array[] = [] + + onEvent?.({ phase: 'generating-nonce' }) + + for (const digest of digests) { + const hasher = this.hasher.create() + hasher.update(getBytes(digest.digest)) + const nonce = crypto.getRandomValues(new Uint8Array(this.nonceSize)) + hasher.update(nonce) + const nonceDigest = new Uint8Array(hasher.digest()) + nonces.push(nonce) + nonceDigests.push(nonceDigest) + } + + onEvent?.({ phase: 'building-merkle-tree' }) + + const internalMerkleTree = UnorderedMerkleTree.new( + nonceDigests, + this.hasher, + ) + const root = internalMerkleTree.root() + console.debug(`Internal Merkle root: ${hexlify(root)}`) + + onEvent?.({ phase: 'broadcasting', totalCalendars: this.calendars.length }) + + let responsesReceived = 0 + const calendarResponses = await Promise.allSettled( + this.calendars.map(async (calendar) => { + try { + const result = await this.requestAttest(calendar, root) + responsesReceived++ + onEvent?.({ + phase: 'calendar-response', + calendarUrl: calendar.toString(), + success: true, + responsesReceived, + totalCalendars: this.calendars.length, + }) + return result + } catch (error) { + responsesReceived++ + onEvent?.({ + phase: 'calendar-response', + calendarUrl: calendar.toString(), + success: false, + responsesReceived, + totalCalendars: this.calendars.length, + }) + throw error + } + }), + ) + + const successfulResponses = calendarResponses.filter( + (res) => res.status === 'fulfilled', + ) as Array> + if (successfulResponses.length < this.quorum) { + throw new RemoteError( + `Only received ${successfulResponses.length} valid responses from calendars, which does not meet the quorum of ${this.quorum}`, + ) + } + + const mergedTimestamp = + successfulResponses.length === 1 + ? successfulResponses[0].value + : [ + { + op: 'FORK', + steps: successfulResponses.map((res) => res.value), + } as ForkStep, + ] + + onEvent?.({ phase: 'building-proof' }) + + const results = digests.map((digest, i) => { + const timestamp: Timestamp = [ + { op: 'APPEND', data: nonces[i] }, + { op: this.hashAlg }, + ] + + const proofIter = internalMerkleTree.getProofIter(nonceDigests[i]) + if (proofIter === null) { + throw new EncodeError( + ErrorCode.INVALID_STRUCTURE, + `Failed to generate proof for digest ${hexlify(digest.digest)}`, + ) + } + + for (const step of proofIter) { + if (step.position === NodePosition.Left) { + timestamp.push( + { + op: 'PREPEND', + data: new Uint8Array([INNER_NODE_PREFIX]), + }, + { op: 'APPEND', data: step.sibling }, + { op: this.hashAlg }, + ) + } else { + timestamp.push( + { op: 'PREPEND', data: step.sibling }, + { + op: 'PREPEND', + data: new Uint8Array([INNER_NODE_PREFIX]), + }, + { op: this.hashAlg }, + ) + } + } + timestamp.push(...mergedTimestamp) + + return { + header: digest, + timestamp, + } + }) + + onEvent?.({ phase: 'complete' }) + + return results + } + + /** + * Submit the root digest to the calendar and receive the timestamp steps in response. + * + * @param calendar The URL of the calendar to submit the root digest to. + * @param root The root digest to be submitted to the calendar. + * @returns The timestamp steps received from the calendar. + */ + async requestAttest(calendar: URL, root: Uint8Array): Promise { + console.debug(`Submitting to remote calendar: ${calendar}`) + const url = new URL('/digest', calendar) + + let response: Response + try { + response = await fetch(url.toString(), { + body: root as BodyInit, + method: 'POST', + headers: { Accept: 'application/vnd.opentimestamps.v1' }, + signal: AbortSignal.timeout(this.timeout), + }) + } catch (error) { + throw new RemoteError(`Failed to submit to calendar ${calendar}`, { + context: { source: error }, + }) + } + + if (!response.ok) { + throw new RemoteError( + `Calendar ${calendar} responded with status ${response.status}`, + { + context: { status: response.status }, + }, + ) + } + const responseData = await response.arrayBuffer() + const decoder = new Decoder(new Uint8Array(responseData)) + return decoder.readTimestamp() + } + + /** + * Perform in-place upgrade of the provided detached timestamp by replacing any pending attestations with their upgraded timestamp steps, if they have become available. + * @param detached The detached timestamp to be upgraded. + * @param keepPending Whether to keep the original pending attestation alongside the upgraded one. Default is false (purge pending on success). + * @returns The result of the upgrade operation, including the original and upgraded timestamps if applicable. + */ + async upgrade( + detached: DetachedTimestamp, + keepPending: boolean = false, + ): Promise { + return this.upgradeTimestamp( + getBytes(detached.header.digest), + detached.timestamp, + keepPending, + ) + } + + /** + * Upgrade the provided timestamp steps by replacing any pending attestations with their upgraded timestamp steps, if they have become available. + * This function will recursively traverse the timestamp steps and perform in-place upgrades of any pending attestations encountered. + * @param input The original digest input associated with the timestamp, which is needed to verify and upgrade the pending attestations. + * @param timestamp The timestamp steps to be upgraded, which may contain pending attestations that need to be replaced with their upgraded timestamp steps if they have become available. + * @param keepPending Whether to keep the original pending attestation alongside the upgraded one. Default is false (purge pending on success). + * @returns The result of the upgrade operation, including the original and upgraded timestamps if applicable. + */ + async upgradeTimestamp( + input: Uint8Array, + timestamp: Timestamp, + keepPending: boolean = false, + ): Promise { + let current = input + + const result: UpgradeResult[] = [] + + for (let i = 0; i < timestamp.length; i++) { + const step = timestamp[i] + switch (step.op) { + case 'APPEND': + case 'PREPEND': + case 'REVERSE': + case 'HEXLIFY': + case 'SHA1': + case 'RIPEMD160': + case 'SHA256': + case 'KECCAK256': + current = this.executeStep(current, step) + break + + case 'FORK': + // upgrade sub stamps + const results = ( + await Promise.all( + step.steps.map((branch) => + this.upgradeTimestamp(input, branch, keepPending), + ), + ) + ).flat() + result.push(...results) + break + case 'ATTESTATION': + if (step.attestation.kind !== 'pending') { + continue + } + try { + const upgraded = await this.upgradeAttestation( + current, + step.attestation, + ) + if (upgraded === null) { + result.push({ + status: UpgradeStatus.Pending, + original: step.attestation, + }) + continue + } + if (keepPending) { + // preserve the original attestation in the upgraded timestamp for transparency + timestamp[i] = { + op: 'FORK', + steps: [[step], upgraded], + } + } else { + // replace the pending attestation with the upgraded one + timestamp.splice(i, 1, ...upgraded) + } + result.push({ + status: UpgradeStatus.Upgraded, + original: step.attestation, + upgraded, + }) + } catch (error) { + console.error(`Error upgrading attestation: ${error}`) + result.push({ + status: UpgradeStatus.Failed, + original: step.attestation, + error: error instanceof Error ? error : new Error(String(error)), + }) + } + break + } + } + return result + } + + /** + * Upgrade the provided pending attestation by fetching its upgraded timestamp steps from the remote calendar. + * @param commitment The original digest input associated with the attestation, which is needed to verify and upgrade the pending attestation. + * @param attestation The pending attestation to be upgraded. + * @returns The upgraded timestamp steps if available, or null if the attestation is still pending. + */ + async upgradeAttestation( + commitment: Uint8Array, + attestation: PendingAttestation, + ): Promise { + const url = new URL(`timestamp/${hexlify(commitment)}`, attestation.url) + + let response: Response + try { + response = await fetch(url.toString(), { + method: 'GET', + headers: { Accept: 'application/vnd.opentimestamps.v1' }, + signal: AbortSignal.timeout(this.timeout), + }) + } catch (error) { + throw new RemoteError( + `Failed to fetch from calendar ${attestation.url}`, + { + context: { source: error }, + }, + ) + } + + if (response.status === 404) { + console.debug(`Attestation at ${attestation.url} is still pending`) + return null + } + + if (!response.ok) { + throw new RemoteError( + `Calendar ${attestation.url} responded with status ${response.status}`, + { + context: { status: response.status }, + }, + ) + } + const responseData = await response.arrayBuffer() + const decoder = new Decoder(new Uint8Array(responseData)) + return decoder.readTimestamp() + } + + /** + * Verify the provided detached timestamp by replaying the timestamp steps and validating the attestations. + * + * @param stamp Detached timestamp to verify, which includes the original digest header and the associated timestamp steps. + * @returns An array of attestation statuses resulting from the verification process, which can be used to determine the overall validity of the timestamp. + */ + async verify(stamp: DetachedTimestamp): Promise { + const input = getBytes(stamp.header.digest) + + return this.verifyTimestamp(input, stamp.timestamp) + } + + /** + * Verify the provided timestamp steps against the input digest by replaying the operations and validating any encountered attestations. + * + * @param input The original digest input to be verified against the timestamp steps. This should match the digest in the timestamp header. + * @param timestamp The timestamp steps to verify against the input digest. + * @returns An array of attestation statuses resulting from the verification process. + */ + async verifyTimestamp( + input: Uint8Array, + timestamp: Timestamp, + ): Promise { + const attestations: AttestationStatus[] = [] + + let current = input + for (const step of timestamp) { + switch (step.op) { + case 'APPEND': + case 'PREPEND': + case 'REVERSE': + case 'HEXLIFY': + case 'SHA1': + case 'RIPEMD160': + case 'SHA256': + case 'KECCAK256': + current = this.executeStep(current, step) + break + + case 'FORK': + // verify sub stamps + for (const branch of step.steps) { + const result = await this.verifyTimestamp(current, branch) + attestations.push(...result) + } + break + case 'ATTESTATION': + const status = await this.verifyAttestation(current, step.attestation) + attestations.push(status) + break + default: + throw new VerifyError( + ErrorCode.INVALID_STRUCTURE, + `Unsupported step ${step} in timestamp`, + ) + } + } + + return attestations + } + + async verifyAttestation( + input: Uint8Array, + attestation: Attestation, + ): Promise { + switch (attestation.kind) { + case 'pending': + return { + attestation, + status: AttestationStatusKind.PENDING, + } + case 'bitcoin': + return this.verifyBitcoinAttestation(input, attestation) + case 'ethereum-uts': + return this.verifyEthereumUTSAttestation(input, attestation) + case 'unknown': + return { + attestation, + status: AttestationStatusKind.UNKNOWN, + error: new VerifyError( + ErrorCode.UNSUPPORTED_ATTESTATION, + `Unknown attestation with tag ${hexlify(attestation.tag)} cannot be verified`, + ), + } + } + } + + async verifyBitcoinAttestation( + input: Uint8Array, + attestation: BitcoinAttestation, + ): Promise { + try { + const header = await this.btcRPC + .getBlockHash(attestation.height) + .then((hash) => this.btcRPC.getBlockHeader(hash)) + // sha256d reverse the displayed hash, so we need to reverse it back to compare with the input + const merkleRoot = getBytes(`0x${header.merkleroot}`).reverse() + if ( + merkleRoot.length !== input.length || + !merkleRoot.every((byte, i) => byte === input[i]) + ) { + return { + attestation, + status: AttestationStatusKind.INVALID, + error: new VerifyError( + ErrorCode.ATTESTATION_MISMATCH, + `Bitcoin attestation does not match the expected merkle root at height ${attestation.height}`, + ), + } + } + return { + attestation, + status: AttestationStatusKind.VALID, + additionalInfo: { header }, + } + } catch (error) { + console.error(`Error verifying Bitcoin attestation: ${error}`) + return { + attestation, + status: AttestationStatusKind.UNKNOWN, + error: new VerifyError( + ErrorCode.REMOTE_ERROR, + `Failed to verify Bitcoin attestation for height ${attestation.height}`, + { context: { source: error } }, + ), + } + } + } + + async verifyEthereumUTSAttestation( + input: Uint8Array, + attestation: EthereumUTSAttestation, + ): Promise { + // Try web3Provider first (works in browser without CORS issues) + let provider: AbstractProvider | null = await this.getWeb3ProviderForChain( + attestation.chain, + ) + + // Fallback to ethRPCs + if (!provider) { + if (!Object.hasOwn(this.ethRPCs, attestation.chain)) { + return { + attestation, + status: AttestationStatusKind.UNKNOWN, + error: new VerifyError( + ErrorCode.UNSUPPORTED_ATTESTATION, + `No RPC provider configured for Ethereum chain ${attestation.chain}`, + ), + } + } + provider = this.ethRPCs[attestation.chain]! + } + + try { + const logs = await provider.getLogs({ + fromBlock: attestation.height, + toBlock: attestation.height, + topics: [ + SDK.utsLogTopic, // Topic 0: Attested(bytes32,address,uint256) + hexlify(input), // Topic 1: digest + ], + }) + + if (logs.length === 0) { + return { + attestation, + status: AttestationStatusKind.INVALID, + error: new VerifyError( + ErrorCode.ATTESTATION_MISMATCH, + `No attestation log found for block ${attestation.height} on chain ${attestation.chain}`, + ), + } + } + + const log = SDK.utsInterface.parseLog(logs[0])! + const root = log.args[0] // root + const sender = log.args[1] // sender + const timestamp = log.args[2] // timestamp + + // sanity check to ensure the root matches + const rootBytes = getBytes(root) + if ( + rootBytes.length !== input.length || + !rootBytes.every((byte: number, i: number) => byte === input[i]) + ) { + return { + attestation, + status: AttestationStatusKind.INVALID, + error: new VerifyError( + ErrorCode.ATTESTATION_MISMATCH, + `Attestation log found but root does not match expected digest for block ${attestation.height} on chain ${attestation.chain}`, + ), + } + } + + return { + attestation, + status: AttestationStatusKind.VALID, + additionalInfo: { root, sender, timestamp }, + } + } catch (error) { + console.error(`Error verifying Ethereum attestation: ${error}`) + return { + attestation, + status: AttestationStatusKind.UNKNOWN, + error: new VerifyError( + ErrorCode.REMOTE_ERROR, + `Failed to verify Ethereum attestation for block ${attestation.height} on chain ${attestation.chain}`, + { context: { source: error } }, + ), + } + } + } + + /** + * Transform the individual attestation statuses into an overall verification status for the timestamp. + * + * The logic is as follows: + * - If there is at least one VALID attestation: + * - If there are also INVALID or UNKNOWN attestations, the overall status is PARTIAL_VALID + * - Otherwise, the overall status is VALID + * - If there are no VALID attestations, but at least one PENDING attestation, the overall status is PENDING + * - If there are no VALID or PENDING attestations, the overall status is INVALID + * @param attestations + */ + transformResult(attestations: AttestationStatus[]): VerifyStatus { + const counts = { + [AttestationStatusKind.VALID]: 0, + [AttestationStatusKind.INVALID]: 0, + [AttestationStatusKind.PENDING]: 0, + [AttestationStatusKind.UNKNOWN]: 0, + } + for (const attestation of attestations) { + counts[attestation.status]++ + } + + let status: VerifyStatus = VerifyStatus.INVALID + + if (counts[AttestationStatusKind.VALID] > 0) { + if ( + counts[AttestationStatusKind.INVALID] > 0 || + counts[AttestationStatusKind.UNKNOWN] > 0 + ) { + status = VerifyStatus.PARTIAL_VALID + } else { + status = VerifyStatus.VALID + } + } else if (counts[AttestationStatusKind.PENDING] > 0) { + status = VerifyStatus.PENDING + } + return status + } + + executeStep(input: Uint8Array, step: ExecutionStep): Uint8Array { + switch (step.op) { + case 'APPEND': + if (!step.data) { + throw new VerifyError( + ErrorCode.INVALID_STRUCTURE, + `Missing data for APPEND operation`, + ) + } + return new Uint8Array([...input, ...getBytes(step.data)]) + case 'PREPEND': + if (!step.data) { + throw new VerifyError( + ErrorCode.INVALID_STRUCTURE, + `Missing data for PREPEND operation`, + ) + } + return new Uint8Array([...getBytes(step.data), ...input]) + case 'REVERSE': + return new Uint8Array(input).reverse() + case 'HEXLIFY': + const str = hexlify(input).slice(2) // remove 0x prefix + return SDK.encoder.encode(str) + + case 'SHA1': + console.warn( + 'SHA1 encountered during verification, which is considered weak. Consider re-stamping with a stronger hash algorithm.', + ) + return sha1(input) + case 'RIPEMD160': + console.warn( + 'RIPEMD160 encountered during verification, which is not encouraged. Consider re-stamping with a stronger hash algorithm.', + ) + return ripemd160(input) + case 'SHA256': + return sha256(input) + case 'KECCAK256': + return keccak_256(input) + + default: + throw new VerifyError( + ErrorCode.INVALID_STRUCTURE, + `Unsupported step ${step} in timestamp`, + ) + } + } +} diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts new file mode 100644 index 0000000..30a9a6f --- /dev/null +++ b/packages/sdk/src/types.ts @@ -0,0 +1,120 @@ +import { type BytesLike } from 'ethers/utils' + +export const DIGEST_OPS = ['SHA1', 'SHA256', 'RIPEMD160', 'KECCAK256'] as const + +export type DigestOp = (typeof DIGEST_OPS)[number] +export type SecureDigestOp = Exclude +export type Op = + | DigestOp + | 'APPEND' + | 'PREPEND' + | 'REVERSE' + | 'HEXLIFY' + | 'ATTESTATION' + | 'FORK' + +export interface DigestHeader { + kind: DigestOp + digest: BytesLike +} + +export interface StepLike { + op: Op +} + +export interface BaseExecutionStep extends StepLike { + op: Exclude + input?: BytesLike + output?: BytesLike +} + +export interface DataExecutionStep extends BaseExecutionStep { + op: 'APPEND' | 'PREPEND' + data: BytesLike +} + +export interface UnaryExecutionStep extends BaseExecutionStep { + op: DigestOp | 'REVERSE' | 'HEXLIFY' +} + +export type AttestationKind = 'pending' | 'bitcoin' | 'ethereum-uts' | 'unknown' + +export type PendingAttestation = { kind: 'pending'; url: URL } +export type BitcoinAttestation = { kind: 'bitcoin'; height: number } +export type EthereumUTSAttestation = { + kind: 'ethereum-uts' + chain: number + height: number + metadata?: EthereumUTSAttestationExtraMetadata +} +export type UnknownAttestation = { + kind: 'unknown' + tag: BytesLike + data: BytesLike +} + +export type Attestation = + | PendingAttestation + | BitcoinAttestation + | EthereumUTSAttestation + | UnknownAttestation + +export type AttestationStep = { op: 'ATTESTATION'; attestation: Attestation } + +export type ForkStep = { op: 'FORK'; steps: Timestamp[] } + +export interface EthereumUTSAttestationExtraMetadata { + contract?: BytesLike + txHash?: BytesLike +} + +export type ExecutionStep = DataExecutionStep | UnaryExecutionStep + +export type Step = ExecutionStep | AttestationStep | ForkStep + +export type Timestamp = Step[] + +export interface DetachedTimestamp { + header: DigestHeader + timestamp: Timestamp +} + +export enum UpgradeStatus { + Upgraded = 'UPGRADED', + Pending = 'PENDING', + Failed = 'FAILED', +} +export type UpgradeResult = + | { + status: UpgradeStatus.Upgraded + original: PendingAttestation + upgraded: Timestamp + } + | { status: UpgradeStatus.Pending; original: PendingAttestation } + | { status: UpgradeStatus.Failed; original: PendingAttestation; error: Error } + +export enum AttestationStatusKind { + VALID = 'VALID', + INVALID = 'INVALID', + PENDING = 'PENDING', + UNKNOWN = 'UNKNOWN', +} + +export type AttestationStatus = + | { + attestation: Attestation + status: 'VALID' | 'PENDING' + additionalInfo?: Record + } + | { + attestation: Attestation + status: 'INVALID' | 'UNKNOWN' + error: Error + } + +export enum VerifyStatus { + VALID = 'VALID', + PARTIAL_VALID = 'PARTIAL_VALID', + INVALID = 'INVALID', + PENDING = 'PENDING', +} diff --git a/packages/sdk/test/btc.test.ts b/packages/sdk/test/btc.test.ts new file mode 100644 index 0000000..a64a2cd --- /dev/null +++ b/packages/sdk/test/btc.test.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from 'vitest' +import BitcoinRPC from '../src/rpc/btc.ts' + +describe('btc', () => { + it('get block', async () => { + const rpc = new BitcoinRPC() + const blockHash = await rpc.getBlockHash(938073) + const blockHeader = await rpc.getBlockHeader(blockHash) + console.debug('Block hash:', blockHash) + console.debug('Block header:', JSON.stringify(blockHeader, null, 2)) + + expect(blockHash).toBe( + '000000000000000000014a0305931a1a12218059a0dd8a79633c27b3e9153172', + ) + expect(blockHeader.hash).toBe( + '000000000000000000014a0305931a1a12218059a0dd8a79633c27b3e9153172', + ) + expect(blockHeader.height).toBe(938073) + expect(blockHeader.merkleroot).toBe( + '015b0dc9e6c4d514f09f0b6bc67ec0382e4cacdfc18f7e069c74e14145447df3', + ) + }) +}) diff --git a/packages/sdk/test/codec.test.ts b/packages/sdk/test/codec.test.ts new file mode 100644 index 0000000..3b6c409 --- /dev/null +++ b/packages/sdk/test/codec.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect, beforeAll } from 'vitest' +import fs from 'node:fs' +import path from 'node:path' +import Encoder from '../src/codec/encode.ts' +import Decoder from '../src/codec/decode.ts' +import type { DetachedTimestamp } from '../src/types.ts' + +const __filename = import.meta.filename +const __dirname = import.meta.dirname + +const OTS_FILE_PATH = path.join(__dirname, '../fixtures/test.ots') + +describe('Codec', () => { + let fileBuffer: Uint8Array + + beforeAll(() => { + if (!fs.existsSync(OTS_FILE_PATH)) { + throw new Error( + `Test file not found: ${OTS_FILE_PATH}. Please put a valid .ots file there.`, + ) + } + fileBuffer = new Uint8Array(fs.readFileSync(OTS_FILE_PATH)) + }) + + it('should decode a valid .ots file successfully', () => { + const decoder = new Decoder(fileBuffer) + const result: DetachedTimestamp = decoder.readDetachedTimestamp() + + expect(result).toBeDefined() + expect(result.header.kind).toMatch( + /^(SHA1|SHA256|SHA512|RIPEMD160|KECCAK256)$/, + ) + expect(result.header.digest).toBeInstanceOf(Uint8Array) + expect(result.timestamp).toBeInstanceOf(Array) + expect(result.timestamp.length).toBeGreaterThan(0) + }) + + it('should match the original data after encoding and decoding', () => { + const decoder = new Decoder(fileBuffer) + const original = decoder.readDetachedTimestamp() + + const encodedBuffer = Encoder.encodeDetachedTimestamp(original) + + const reDecoder = new Decoder(encodedBuffer) + const decoded = reDecoder.readDetachedTimestamp() + expect(decoded).toEqual(original) + + expect(encodedBuffer).toEqual(fileBuffer) + }) +}) diff --git a/packages/sdk/test/merkle.test.ts b/packages/sdk/test/merkle.test.ts new file mode 100644 index 0000000..25a87bd --- /dev/null +++ b/packages/sdk/test/merkle.test.ts @@ -0,0 +1,68 @@ +import { describe, it, expect } from 'vitest' +import { UnorderedMerkleTree } from '../src/bmt.ts' +import { sha256 } from '@noble/hashes/sha2.js' + +describe('UnorderedMerkleTree', () => { + it('should create a tree and compute root', () => { + const leaves = [ + new Uint8Array(Array(32).fill(1)), + new Uint8Array(Array(32).fill(2)), + new Uint8Array(Array(32).fill(3)), + ] + + const tree = UnorderedMerkleTree.new(leaves, sha256) + const root = tree.root() + + expect(root).toBeDefined() + expect(root.length).toBe(32) // SHA256 output size + }) + + it('should verify leaf existence', () => { + const leaves = [ + new Uint8Array(Array(32).fill(1)), + new Uint8Array(Array(32).fill(2)), + ] + + const tree = UnorderedMerkleTree.new(leaves, sha256) + + expect(tree.contains(new Uint8Array(Array(32).fill(1)))).toBe(true) + expect(tree.contains(new Uint8Array(Array(32).fill(9)))).toBe(false) + }) + + it('should generate valid proof iteration', () => { + const leaves = [ + new Uint8Array(Array(32).fill(1)), + new Uint8Array(Array(32).fill(2)), + new Uint8Array(Array(32).fill(3)), + new Uint8Array(Array(32).fill(4)), + ] + + const tree = UnorderedMerkleTree.new(leaves, sha256) + const targetLeaf = new Uint8Array(Array(32).fill(4)) + + const proofIter = tree.getProofIter(targetLeaf) + expect(proofIter).not.toBeNull() + + const steps = Array.from(proofIter!) + expect(steps.length).toBe(2) + + steps.forEach((step) => { + expect(step.position).toBeDefined() + expect(step.sibling).toBeInstanceOf(Uint8Array) + }) + }) + + it('should serialize and deserialize correctly', () => { + const leaves = [ + new Uint8Array(Array(32).fill(1)), + new Uint8Array(Array(32).fill(2)), + new Uint8Array(Array(32).fill(3)), + ] + + const tree = UnorderedMerkleTree.new(leaves, sha256) + const rawBytes = tree.asRawBytes() + + const deserializedTree = UnorderedMerkleTree.fromRawBytes(rawBytes, sha256) + expect(deserializedTree.root()).toEqual(tree.root()) + }) +}) diff --git a/packages/sdk/test/sdk.test.ts b/packages/sdk/test/sdk.test.ts new file mode 100644 index 0000000..a6dc36b --- /dev/null +++ b/packages/sdk/test/sdk.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect, beforeAll } from 'vitest' +import fs from 'node:fs' +import path from 'node:path' +import Decoder from '../src/codec/decode.ts' +import type { DetachedTimestamp } from '../src/types.ts' +import SDK from '../src/sdk.ts' +import { getBytes } from 'ethers' + +const __filename = import.meta.filename +const __dirname = import.meta.dirname + +const OTS_FILE_PATH = path.join(__dirname, '../fixtures/test.ots') + +describe.skip('Stamp', () => { + it('should stamp', async () => { + const sdk = new SDK() + + const testDigest = '0x' + 'aa'.repeat(32) + const results = await sdk.stamp([ + { + kind: 'SHA256', + digest: testDigest, + }, + ]) + + expect(results).toHaveLength(1) + const result = results[0] + + expect(result.header.digest).toEqual(getBytes(testDigest)) + expect(result.timestamp).toBeDefined() + console.debug('Timestamp:', JSON.stringify(result, null, 2)) + }) +}) + +describe('Verify', () => { + let fileBuffer: Uint8Array + + beforeAll(() => { + if (!fs.existsSync(OTS_FILE_PATH)) { + throw new Error( + `Test file not found: ${OTS_FILE_PATH}. Please put a valid .ots file there.`, + ) + } + fileBuffer = new Uint8Array(fs.readFileSync(OTS_FILE_PATH)) + }) + + it('should verify ethereum attestation', async () => { + const sdk = new SDK() + + const verified = await sdk.verifyAttestation( + getBytes( + '0x7eb06fdbe20e402a8125775968899b4ab87b9af1c20a81d4af8d5bb0c96d7c64', + ), + { + kind: 'ethereum-uts', + chain: 54351, + height: 16862454, + }, + ) + + expect(verified.status).toBe('VALID') + }) + + it('should verify fixture timestamp', async () => { + const sdk = new SDK() + + const decoder = new Decoder(fileBuffer) + const detachedTimestamp: DetachedTimestamp = decoder.readDetachedTimestamp() + + const verified = await sdk.verify(detachedTimestamp) + expect(verified).toBeDefined() + console.debug('Verification details:', JSON.stringify(verified, null, 2)) + + const result = sdk.transformResult(verified) + expect(result).toBeDefined() + console.debug('Verification result:', JSON.stringify(result, null, 2)) + }) +}) diff --git a/packages/sdk/test/tsconfig.json b/packages/sdk/test/tsconfig.json new file mode 100644 index 0000000..3f44a63 --- /dev/null +++ b/packages/sdk/test/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "references": [{ "path": "../tsconfig.json" }], + "compilerOptions": { + "tsBuildInfoFile": "../dist/test/.tsbuildinfo", + "rewriteRelativeImportExtensions": false, + "outDir": "../dist/test/", + "rootDir": "." + }, + "include": ["*.ts"] +} diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json new file mode 100644 index 0000000..823c461 --- /dev/null +++ b/packages/sdk/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "tsBuildInfoFile": "./dist/.tsbuildinfo", + "emitDeclarationOnly": true, + "types": [] + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/sdk/vitest.config.ts b/packages/sdk/vitest.config.ts new file mode 100644 index 0000000..04b71ae --- /dev/null +++ b/packages/sdk/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'node', + + include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + + testTimeout: 10000, + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..4c06003 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,5027 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + eslint: + specifier: ^9.38.0 + version: 9.39.3(jiti@2.6.1) + eslint-import-resolver-typescript: + specifier: ^4.4.4 + version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-import-x: + specifier: ^4.16.1 + version: 4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-unicorn: + specifier: ^63.0.0 + version: 63.0.0(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-unused-imports: + specifier: ^4.4.1 + version: 4.4.1(@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)) + pnpm: + specifier: ^10.26.2 + version: 10.26.2 + prettier: + specifier: ^3.8.1 + version: 3.8.1 + typescript-eslint: + specifier: ^8.56.1 + version: 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + + apps/web: + dependencies: + '@lingui/core': + specifier: ^5.9.2 + version: 5.9.2(@lingui/babel-plugin-lingui-macro@5.9.2(typescript@5.9.3)) + '@lingui/message-utils': + specifier: ^5.9.2 + version: 5.9.2 + '@noble/hashes': + specifier: ^2.0.1 + version: 2.0.1 + '@uts/sdk': + specifier: workspace:* + version: link:../../packages/sdk + '@vueuse/core': + specifier: ^14.2.1 + version: 14.2.1(vue@3.5.26(typescript@5.9.3)) + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + ethers: + specifier: ^6.16.0 + version: 6.16.0 + jszip: + specifier: ^3.10.1 + version: 3.10.1 + lucide-vue-next: + specifier: ^0.575.0 + version: 0.575.0(vue@3.5.26(typescript@5.9.3)) + pinia: + specifier: ^3.0.4 + version: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)) + vue: + specifier: ^3.5.24 + version: 3.5.26(typescript@5.9.3) + devDependencies: + '@lingui/cli': + specifier: ^5.9.2 + version: 5.9.2(typescript@5.9.3) + '@lingui/format-po': + specifier: ^5.9.2 + version: 5.9.2(typescript@5.9.3) + '@lingui/vite-plugin': + specifier: ^5.9.2 + version: 5.9.2(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)) + '@tailwindcss/vite': + specifier: ^4.2.1 + version: 4.2.1(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)) + '@types/jszip': + specifier: ^3.4.1 + version: 3.4.1 + '@types/node': + specifier: ^24.10.1 + version: 24.10.4 + '@vitejs/plugin-vue': + specifier: ^6.0.1 + version: 6.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))(vue@3.5.26(typescript@5.9.3)) + '@vue/tsconfig': + specifier: ^0.8.1 + version: 0.8.1(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)) + tailwindcss: + specifier: ^4.2.1 + version: 4.2.1 + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vite: + specifier: ^7.2.4 + version: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1) + vue-tsc: + specifier: ^3.1.4 + version: 3.2.1(typescript@5.9.3) + + packages/sdk: + dependencies: + '@noble/hashes': + specifier: ^2.0.1 + version: 2.0.1 + ethers: + specifier: ^6.16.0 + version: 6.16.0 + devDependencies: + '@rollup/plugin-node-resolve': + specifier: ^16.0.3 + version: 16.0.3(rollup@4.59.0) + '@rollup/plugin-typescript': + specifier: ^12.3.0 + version: 12.3.0(rollup@4.59.0)(tslib@2.7.0)(typescript@5.9.3) + rimraf: + specifier: ^6.1.3 + version: 6.1.3 + rollup: + specifier: ^4.59.0 + version: 4.59.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: ^4.0.18 + version: 4.0.18(@types/node@24.10.4)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(lightningcss@1.31.1) + +packages: + + '@adraffy/ens-normalize@1.10.1': + resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@emnapi/core@1.8.1': + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.4': + resolution: {integrity: sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.3': + resolution: {integrity: sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@lingui/babel-plugin-extract-messages@5.9.2': + resolution: {integrity: sha512-x9HxUUZgnzF5nTsRwCyJxL/csll/psQCeiTiUPMqE1qC9Mq3ZFPygHDUgijIwOCSDnILHM581Fjar/Njvpnnow==} + engines: {node: '>=20.0.0'} + + '@lingui/babel-plugin-lingui-macro@5.9.2': + resolution: {integrity: sha512-xy5MNUvtoAlZwQtfby3B9Dnjmnf3w4Xe+jeLEQvB1wOxz/pYMNGxCWgekUQyihlxvFFRKmMyA88EByqAyvGEug==} + engines: {node: '>=20.0.0'} + peerDependencies: + babel-plugin-macros: 2 || 3 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + '@lingui/cli@5.9.2': + resolution: {integrity: sha512-jGmIB/hKparFvuyspcn3UTjnQEdOrlnBwkQH3xip0LRCyQNjd1sALi27HDyYQAqVINTfk3sDmLbKixYvJhPl1Q==} + engines: {node: '>=20.0.0'} + hasBin: true + + '@lingui/conf@5.9.2': + resolution: {integrity: sha512-tkobrx4HIJDWgYO/qUOJgwOVB5QX+FdQHmk9wetzlhgf0BSc52OCGYeG//oKHteJB0dnr2XYicnTuUtoVwr3+g==} + engines: {node: '>=20.0.0'} + + '@lingui/core@5.9.2': + resolution: {integrity: sha512-2K2lIEiUJ9VNTZU0igiRUubIUvcHu8TEuS4uRrrA5f2DGgCtHD5o7rw6OO9cM1RxZFCC5rpRwIMDeXHrc44W3g==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@lingui/babel-plugin-lingui-macro': 5.9.2 + babel-plugin-macros: 2 || 3 + peerDependenciesMeta: + '@lingui/babel-plugin-lingui-macro': + optional: true + babel-plugin-macros: + optional: true + + '@lingui/format-po@5.9.2': + resolution: {integrity: sha512-PMlJERgG0/rCYqMuBDQdsAuEEUxbqe1apwwDJxIniN9vZpwl+4jDZhDQ6J9CwjlaUPe7rCfOm/B79N88ELwTQQ==} + engines: {node: '>=20.0.0'} + + '@lingui/message-utils@5.9.2': + resolution: {integrity: sha512-PQA+bP4TBX7F5nmrEm9eDk+xddepjO7okI6bsOQ+6LIw1eJMvh9w/Xt3AoKTgzBCwd1ZwyqzBOelrX6sa6y+fg==} + engines: {node: '>=20.0.0'} + + '@lingui/vite-plugin@5.9.2': + resolution: {integrity: sha512-cKfVCoOJsHssx7OazCb8P/KHn1UqCiod+OGIpGqchtla09xPNSZmfyEp/Of0+S6G5REkOMV/ZGgl+awtGkJ0fA==} + engines: {node: '>=20.0.0'} + peerDependencies: + vite: ^3 || ^4 || ^5.0.9 || ^6 || ^7 + + '@messageformat/parser@5.1.1': + resolution: {integrity: sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg==} + + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + + '@noble/curves@1.2.0': + resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} + + '@noble/hashes@1.3.2': + resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} + engines: {node: '>= 16'} + + '@noble/hashes@2.0.1': + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@rolldown/pluginutils@1.0.0-beta.53': + resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} + + '@rollup/plugin-node-resolve@16.0.3': + resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-typescript@12.3.0': + resolution: {integrity: sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.14.0||^3.0.0||^4.0.0 + tslib: '*' + typescript: '>=3.7.0' + peerDependenciesMeta: + rollup: + optional: true + tslib: + optional: true + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.54.0': + resolution: {integrity: sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.54.0': + resolution: {integrity: sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.54.0': + resolution: {integrity: sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.54.0': + resolution: {integrity: sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.54.0': + resolution: {integrity: sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.54.0': + resolution: {integrity: sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.54.0': + resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.54.0': + resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.54.0': + resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.54.0': + resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.54.0': + resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.54.0': + resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.54.0': + resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.54.0': + resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.54.0': + resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.54.0': + resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.54.0': + resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.54.0': + resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.54.0': + resolution: {integrity: sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.54.0': + resolution: {integrity: sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.54.0': + resolution: {integrity: sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.54.0': + resolution: {integrity: sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@sinclair/typebox@0.27.10': + resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tailwindcss/node@4.2.1': + resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} + + '@tailwindcss/oxide-android-arm64@4.2.1': + resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.1': + resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + resolution: {integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.1': + resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.2.1': + resolution: {integrity: sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/jszip@3.4.1': + resolution: {integrity: sha512-TezXjmf3lj+zQ651r6hPqvSScqBLvyPI9FxdXBqpEwBijNGQ2NXpaFW/7joGzveYkKQUil7iiDHLo6LV71Pc0A==} + deprecated: This is a stub types definition. jszip provides its own type definitions, so you do not need this installed. + + '@types/node@22.7.5': + resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + + '@types/node@24.10.4': + resolution: {integrity: sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==} + + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + + '@typescript-eslint/eslint-plugin@8.56.1': + resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.56.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.56.1': + resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.56.1': + resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.56.1': + resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.56.1': + resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.56.1': + resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.56.1': + resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.56.1': + resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.56.1': + resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.56.1': + resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + + '@vitejs/plugin-vue@6.0.3': + resolution: {integrity: sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + vue: ^3.2.25 + + '@vitest/browser-playwright@4.0.18': + resolution: {integrity: sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==} + peerDependencies: + playwright: '*' + vitest: 4.0.18 + + '@vitest/browser@4.0.18': + resolution: {integrity: sha512-gVQqh7paBz3gC+ZdcCmNSWJMk70IUjDeVqi+5m5vYpEHsIwRgw3Y545jljtajhkekIpIp5Gg8oK7bctgY0E2Ng==} + peerDependencies: + vitest: 4.0.18 + + '@vitest/expect@4.0.18': + resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + + '@vitest/mocker@4.0.18': + resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.0.18': + resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + + '@vitest/runner@4.0.18': + resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + + '@vitest/snapshot@4.0.18': + resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + + '@vitest/spy@4.0.18': + resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + + '@vitest/utils@4.0.18': + resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + + '@volar/language-core@2.4.27': + resolution: {integrity: sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ==} + + '@volar/source-map@2.4.27': + resolution: {integrity: sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg==} + + '@volar/typescript@2.4.27': + resolution: {integrity: sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg==} + + '@vue/compiler-core@3.5.26': + resolution: {integrity: sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==} + + '@vue/compiler-dom@3.5.26': + resolution: {integrity: sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==} + + '@vue/compiler-sfc@3.5.26': + resolution: {integrity: sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==} + + '@vue/compiler-ssr@3.5.26': + resolution: {integrity: sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==} + + '@vue/devtools-api@7.7.9': + resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} + + '@vue/devtools-kit@7.7.9': + resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} + + '@vue/devtools-shared@7.7.9': + resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} + + '@vue/language-core@3.2.1': + resolution: {integrity: sha512-g6oSenpnGMtpxHGAwKuu7HJJkNZpemK/zg3vZzZbJ6cnnXq1ssxuNrXSsAHYM3NvH8p4IkTw+NLmuxyeYz4r8A==} + + '@vue/reactivity@3.5.26': + resolution: {integrity: sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==} + + '@vue/runtime-core@3.5.26': + resolution: {integrity: sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==} + + '@vue/runtime-dom@3.5.26': + resolution: {integrity: sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==} + + '@vue/server-renderer@3.5.26': + resolution: {integrity: sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==} + peerDependencies: + vue: 3.5.26 + + '@vue/shared@3.5.26': + resolution: {integrity: sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==} + + '@vue/tsconfig@0.8.1': + resolution: {integrity: sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==} + peerDependencies: + typescript: 5.x + vue: ^3.4.0 + peerDependenciesMeta: + typescript: + optional: true + vue: + optional: true + + '@vueuse/core@14.2.1': + resolution: {integrity: sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/metadata@14.2.1': + resolution: {integrity: sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==} + + '@vueuse/shared@14.2.1': + resolution: {integrity: sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==} + peerDependencies: + vue: ^3.5.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + aes-js@4.0.0-beta.5: + resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==} + + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + + alien-signals@3.1.2: + resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.10.0: + resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + engines: {node: '>=6.0.0'} + hasBin: true + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@5.0.3: + resolution: {integrity: sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + builtin-modules@5.0.0: + resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} + engines: {node: '>=18.20'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001774: + resolution: {integrity: sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + + chokidar@3.5.1: + resolution: {integrity: sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==} + engines: {node: '>= 8.10.0'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + clean-regexp@1.0.0: + resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} + engines: {node: '>=4'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-table@0.3.11: + resolution: {integrity: sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==} + engines: {node: '>= 0.2.0'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colors@1.0.3: + resolution: {integrity: sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==} + engines: {node: '>=0.1.90'} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + comment-parser@1.4.5: + resolution: {integrity: sha512-aRDkn3uyIlCFfk5NUA+VdwMmMsh8JGhc4hapfV4yxymHGQ3BVskMQfoXGpCo5IoBuQ9tS5iiVKhCpTcB4pW4qw==} + engines: {node: '>= 12.0.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + + core-js-compat@3.48.0: + resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + date-fns@3.6.0: + resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + electron-to-chromium@1.5.302: + resolution: {integrity: sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==} + + enhanced-resolve@5.19.0: + resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} + engines: {node: '>=10.13.0'} + + entities@7.0.0: + resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==} + engines: {node: '>=0.12'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-import-context@0.1.9: + resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + peerDependencies: + unrs-resolver: ^1.0.0 + peerDependenciesMeta: + unrs-resolver: + optional: true + + eslint-import-resolver-typescript@4.4.4: + resolution: {integrity: sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==} + engines: {node: ^16.17.0 || >=18.6.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-plugin-import-x@4.16.1: + resolution: {integrity: sha512-vPZZsiOKaBAIATpFE2uMI4w5IRwdv/FpQ+qZZMR4E+PeOcM4OeoEbqxRMnywdxP19TyB/3h6QBB0EWon7letSQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/utils': ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 + eslint-import-resolver-node: '*' + peerDependenciesMeta: + '@typescript-eslint/utils': + optional: true + eslint-import-resolver-node: + optional: true + + eslint-plugin-unicorn@63.0.0: + resolution: {integrity: sha512-Iqecl9118uQEXYh7adylgEmGfkn5es3/mlQTLLkd4pXkIk9CTGrAbeUux+YljSa2ohXCBmQQ0+Ej1kZaFgcfkA==} + engines: {node: ^20.10.0 || >=21.0.0} + peerDependencies: + eslint: '>=9.38.0' + + eslint-plugin-unused-imports@4.4.1: + resolution: {integrity: sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ==} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0 + eslint: ^10.0.0 || ^9.0.0 || ^8.0.0 + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.3: + resolution: {integrity: sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + esm@3.2.25: + resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} + engines: {node: '>=6'} + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + ethers@6.16.0: + resolution: {integrity: sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==} + engines: {node: '>=14.0.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up-simple@1.0.1: + resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} + engines: {node: '>=18'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@11.1.0: + resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} + engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-builtin-module@5.0.0: + resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} + engines: {node: '>=18.20'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-observable@2.1.0: + resolution: {integrity: sha512-DailKdLb0WU+xX8K5w7VsJhapwHLZ9jjmazqCJq4X12CTgqq73TKnbRcnSLuXYPOoLQgV5IrD7ePiX/h1vnkBw==} + engines: {node: '>=8'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-sha256@0.10.1: + resolution: {integrity: sha512-5obBtsz9301ULlsgggLg542s/jqtddfOpV5KJc4hajc9JV8GeY2gZHSVpYBn4nWqAUTJ9v+xwtbJ1mIBgIH5Vw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + + lightningcss-android-arm64@1.31.1: + resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.31.1: + resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.31.1: + resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.31.1: + resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.31.1: + resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.31.1: + resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.31.1: + resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.31.1: + resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.31.1: + resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.31.1: + resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.31.1: + resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.31.1: + resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + lru-cache@11.2.6: + resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-vue-next@0.575.0: + resolution: {integrity: sha512-UHzA3cYMCgBLyGay5R9IQaidwV0NLocx7cIBnFt8vJ9Xhl6IM/oKD0fUhoCUuouFta15SX1rLXVoko9s3TzWMA==} + peerDependencies: + vue: '>=3.0.1' + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@10.2.2: + resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.4: + resolution: {integrity: sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + moo@0.5.2: + resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + observable-fns@0.6.1: + resolution: {integrity: sha512-9gRK4+sRWzeN6AOewNBTLXir7Zl/i3GB6Yl26gK4flxz8BXVpD3kt8amREmWNb0mxYOGDotvE5a4N+PtGGKdkg==} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pinia@3.0.4: + resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==} + peerDependencies: + typescript: '>=4.5.0' + vue: ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + pixelmatch@7.1.0: + resolution: {integrity: sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==} + hasBin: true + + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + engines: {node: '>=18'} + hasBin: true + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + + pnpm@10.26.2: + resolution: {integrity: sha512-DjCP8gBfx0EDZvFU9iX2YxqysWsdLnAjhETdaunWMKhILZKkURRN68SSQWiW7Rb3sRSobsaLhASyRDhp5o/9pg==} + engines: {node: '>=18.12'} + hasBin: true + + pofile@1.1.4: + resolution: {integrity: sha512-r6Q21sKsY1AjTVVjOuU02VYKVNQGJNQHjTIvs4dEbeuuYfxgYk/DGD2mqqq4RDaVkwdSq0VEtmQUOPe/wH8X3g==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + pseudolocale@2.2.0: + resolution: {integrity: sha512-O+D2eU7fO9wVLqrohvt9V/9fwMadnJQ4jxwiK+LeNEqhMx8JYx4xQHkArDCJFAdPPOp/pQq6z5L37eBvAoc8jw==} + engines: {node: '>=16.0.0'} + hasBin: true + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.5.0: + resolution: {integrity: sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==} + engines: {node: '>=8.10.0'} + + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + + regjsparser@0.13.0: + resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} + hasBin: true + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@6.1.3: + resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} + engines: {node: 20 || >=22} + hasBin: true + + rollup@4.54.0: + resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + stable-hash-x@0.2.0: + resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} + engines: {node: '>=12.0.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-indent@4.1.1: + resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwindcss@4.2.1: + resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + threads@1.7.0: + resolution: {integrity: sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==} + + tiny-worker@2.3.0: + resolution: {integrity: sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.56.1: + resolution: {integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@7.3.0: + resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.0.18: + resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.18 + '@vitest/browser-preview': 4.0.18 + '@vitest/browser-webdriverio': 4.0.18 + '@vitest/ui': 4.0.18 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-tsc@3.2.1: + resolution: {integrity: sha512-I23Rk8dkQfmcSbxDO0dmg9ioMLjKA1pjlU3Lz6Jfk2pMGu3Uryu9810XkcZH24IzPbhzPCnkKo2rEMRX0skSrw==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.26: + resolution: {integrity: sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@adraffy/ens-normalize@1.10.1': {} + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/runtime@7.28.6': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@emnapi/core@1.8.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.7.0 + optional: true + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.7.0 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.7.0 + optional: true + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.2': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.2': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.2': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.27.2': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.27.2': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.27.2': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.2': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.27.2': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.27.2': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.3(jiti@2.6.1))': + dependencies: + eslint: 9.39.3(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.4 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.4': + dependencies: + ajv: 6.14.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.4 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.3': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@isaacs/cliui@9.0.0': {} + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.10 + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 24.10.4 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@lingui/babel-plugin-extract-messages@5.9.2': {} + + '@lingui/babel-plugin-lingui-macro@5.9.2(typescript@5.9.3)': + dependencies: + '@babel/core': 7.29.0 + '@babel/runtime': 7.28.6 + '@babel/types': 7.28.5 + '@lingui/conf': 5.9.2(typescript@5.9.3) + '@lingui/core': 5.9.2(@lingui/babel-plugin-lingui-macro@5.9.2(typescript@5.9.3)) + '@lingui/message-utils': 5.9.2 + transitivePeerDependencies: + - supports-color + - typescript + + '@lingui/cli@5.9.2(typescript@5.9.3)': + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/parser': 7.28.5 + '@babel/runtime': 7.28.6 + '@babel/types': 7.28.5 + '@lingui/babel-plugin-extract-messages': 5.9.2 + '@lingui/babel-plugin-lingui-macro': 5.9.2(typescript@5.9.3) + '@lingui/conf': 5.9.2(typescript@5.9.3) + '@lingui/core': 5.9.2(@lingui/babel-plugin-lingui-macro@5.9.2(typescript@5.9.3)) + '@lingui/format-po': 5.9.2(typescript@5.9.3) + '@lingui/message-utils': 5.9.2 + chokidar: 3.5.1 + cli-table: 0.3.11 + commander: 10.0.1 + convert-source-map: 2.0.0 + date-fns: 3.6.0 + esbuild: 0.25.12 + glob: 11.1.0 + micromatch: 4.0.8 + ms: 2.1.3 + normalize-path: 3.0.0 + ora: 5.4.1 + picocolors: 1.1.1 + pofile: 1.1.4 + pseudolocale: 2.2.0 + source-map: 0.7.6 + threads: 1.7.0 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - typescript + + '@lingui/conf@5.9.2(typescript@5.9.3)': + dependencies: + '@babel/runtime': 7.28.6 + cosmiconfig: 8.3.6(typescript@5.9.3) + jest-validate: 29.7.0 + jiti: 2.6.1 + picocolors: 1.1.1 + transitivePeerDependencies: + - typescript + + '@lingui/core@5.9.2(@lingui/babel-plugin-lingui-macro@5.9.2(typescript@5.9.3))': + dependencies: + '@babel/runtime': 7.28.6 + '@lingui/message-utils': 5.9.2 + optionalDependencies: + '@lingui/babel-plugin-lingui-macro': 5.9.2(typescript@5.9.3) + + '@lingui/format-po@5.9.2(typescript@5.9.3)': + dependencies: + '@lingui/conf': 5.9.2(typescript@5.9.3) + '@lingui/message-utils': 5.9.2 + date-fns: 3.6.0 + pofile: 1.1.4 + transitivePeerDependencies: + - typescript + + '@lingui/message-utils@5.9.2': + dependencies: + '@messageformat/parser': 5.1.1 + js-sha256: 0.10.1 + + '@lingui/vite-plugin@5.9.2(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))': + dependencies: + '@lingui/cli': 5.9.2(typescript@5.9.3) + '@lingui/conf': 5.9.2(typescript@5.9.3) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - typescript + + '@messageformat/parser@5.1.1': + dependencies: + moo: 0.5.2 + + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@noble/curves@1.2.0': + dependencies: + '@noble/hashes': 1.3.2 + + '@noble/hashes@1.3.2': {} + + '@noble/hashes@2.0.1': {} + + '@polka/url@1.0.0-next.29': + optional: true + + '@rolldown/pluginutils@1.0.0-beta.53': {} + + '@rollup/plugin-node-resolve@16.0.3(rollup@4.59.0)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.11 + optionalDependencies: + rollup: 4.59.0 + + '@rollup/plugin-typescript@12.3.0(rollup@4.59.0)(tslib@2.7.0)(typescript@5.9.3)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) + resolve: 1.22.11 + typescript: 5.9.3 + optionalDependencies: + rollup: 4.59.0 + tslib: 2.7.0 + + '@rollup/pluginutils@5.3.0(rollup@4.59.0)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.59.0 + + '@rollup/rollup-android-arm-eabi@4.54.0': + optional: true + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.54.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.54.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.54.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.54.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.54.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.54.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.54.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.54.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.54.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.54.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.54.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.54.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.54.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.54.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.54.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@sinclair/typebox@0.27.10': {} + + '@standard-schema/spec@1.1.0': {} + + '@tailwindcss/node@4.2.1': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.19.0 + jiti: 2.6.1 + lightningcss: 1.31.1 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.1 + + '@tailwindcss/oxide-android-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide@4.2.1': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-x64': 4.2.1 + '@tailwindcss/oxide-freebsd-x64': 4.2.1 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.1 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.1 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-x64-musl': 4.2.1 + '@tailwindcss/oxide-wasm32-wasi': 4.2.1 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 + + '@tailwindcss/vite@4.2.1(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))': + dependencies: + '@tailwindcss/node': 4.2.1 + '@tailwindcss/oxide': 4.2.1 + tailwindcss: 4.2.1 + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1) + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.7.0 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/json-schema@7.0.15': {} + + '@types/jszip@3.4.1': + dependencies: + jszip: 3.10.1 + + '@types/node@22.7.5': + dependencies: + undici-types: 6.19.8 + + '@types/node@24.10.4': + dependencies: + undici-types: 7.16.0 + + '@types/resolve@1.20.2': {} + + '@types/web-bluetooth@0.0.21': {} + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.35': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/type-utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.1 + eslint: 9.39.3(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3 + eslint: 9.39.3(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.56.1': + dependencies: + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + + '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.3(jiti@2.6.1) + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.56.1': {} + + '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3 + minimatch: 10.2.2 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + eslint: 9.39.3(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.56.1': + dependencies: + '@typescript-eslint/types': 8.56.1 + eslint-visitor-keys: 5.0.1 + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + + '@vitejs/plugin-vue@6.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))(vue@3.5.26(typescript@5.9.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.53 + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1) + vue: 3.5.26(typescript@5.9.3) + + '@vitest/browser-playwright@4.0.18(playwright@1.58.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))(vitest@4.0.18)': + dependencies: + '@vitest/browser': 4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))(vitest@4.0.18) + '@vitest/mocker': 4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)) + playwright: 1.58.2 + tinyrainbow: 3.0.3 + vitest: 4.0.18(@types/node@24.10.4)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(lightningcss@1.31.1) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + + '@vitest/browser@4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))(vitest@4.0.18)': + dependencies: + '@vitest/mocker': 4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)) + '@vitest/utils': 4.0.18 + magic-string: 0.30.21 + pixelmatch: 7.1.0 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.0.3 + vitest: 4.0.18(@types/node@24.10.4)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(lightningcss@1.31.1) + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + + '@vitest/expect@4.0.18': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + chai: 6.2.2 + tinyrainbow: 3.0.3 + + '@vitest/mocker@4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))': + dependencies: + '@vitest/spy': 4.0.18 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1) + + '@vitest/pretty-format@4.0.18': + dependencies: + tinyrainbow: 3.0.3 + + '@vitest/runner@4.0.18': + dependencies: + '@vitest/utils': 4.0.18 + pathe: 2.0.3 + + '@vitest/snapshot@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.0.18': {} + + '@vitest/utils@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + tinyrainbow: 3.0.3 + + '@volar/language-core@2.4.27': + dependencies: + '@volar/source-map': 2.4.27 + + '@volar/source-map@2.4.27': {} + + '@volar/typescript@2.4.27': + dependencies: + '@volar/language-core': 2.4.27 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/compiler-core@3.5.26': + dependencies: + '@babel/parser': 7.28.5 + '@vue/shared': 3.5.26 + entities: 7.0.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.26': + dependencies: + '@vue/compiler-core': 3.5.26 + '@vue/shared': 3.5.26 + + '@vue/compiler-sfc@3.5.26': + dependencies: + '@babel/parser': 7.28.5 + '@vue/compiler-core': 3.5.26 + '@vue/compiler-dom': 3.5.26 + '@vue/compiler-ssr': 3.5.26 + '@vue/shared': 3.5.26 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.26': + dependencies: + '@vue/compiler-dom': 3.5.26 + '@vue/shared': 3.5.26 + + '@vue/devtools-api@7.7.9': + dependencies: + '@vue/devtools-kit': 7.7.9 + + '@vue/devtools-kit@7.7.9': + dependencies: + '@vue/devtools-shared': 7.7.9 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + '@vue/devtools-shared@7.7.9': + dependencies: + rfdc: 1.4.1 + + '@vue/language-core@3.2.1': + dependencies: + '@volar/language-core': 2.4.27 + '@vue/compiler-dom': 3.5.26 + '@vue/shared': 3.5.26 + alien-signals: 3.1.2 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.3 + + '@vue/reactivity@3.5.26': + dependencies: + '@vue/shared': 3.5.26 + + '@vue/runtime-core@3.5.26': + dependencies: + '@vue/reactivity': 3.5.26 + '@vue/shared': 3.5.26 + + '@vue/runtime-dom@3.5.26': + dependencies: + '@vue/reactivity': 3.5.26 + '@vue/runtime-core': 3.5.26 + '@vue/shared': 3.5.26 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.26(vue@3.5.26(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.26 + '@vue/shared': 3.5.26 + vue: 3.5.26(typescript@5.9.3) + + '@vue/shared@3.5.26': {} + + '@vue/tsconfig@0.8.1(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))': + optionalDependencies: + typescript: 5.9.3 + vue: 3.5.26(typescript@5.9.3) + + '@vueuse/core@14.2.1(vue@3.5.26(typescript@5.9.3))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 14.2.1 + '@vueuse/shared': 14.2.1(vue@3.5.26(typescript@5.9.3)) + vue: 3.5.26(typescript@5.9.3) + + '@vueuse/metadata@14.2.1': {} + + '@vueuse/shared@14.2.1(vue@3.5.26(typescript@5.9.3))': + dependencies: + vue: 3.5.26(typescript@5.9.3) + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + aes-js@4.0.0-beta.5: {} + + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + alien-signals@3.1.2: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@2.0.1: {} + + assertion-error@2.0.1: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.10.0: {} + + binary-extensions@2.3.0: {} + + birpc@2.9.0: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.3: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.0 + caniuse-lite: 1.0.30001774 + electron-to-chromium: 1.5.302 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + builtin-modules@5.0.0: {} + + callsites@3.1.0: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001774: {} + + chai@6.2.2: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + change-case@5.4.4: {} + + chokidar@3.5.1: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.5.0 + optionalDependencies: + fsevents: 2.3.3 + + ci-info@4.4.0: {} + + clean-regexp@1.0.0: + dependencies: + escape-string-regexp: 1.0.5 + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-spinners@2.9.2: {} + + cli-table@0.3.11: + dependencies: + colors: 1.0.3 + + clone@1.0.4: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colors@1.0.3: {} + + commander@10.0.1: {} + + comment-parser@1.4.5: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + + core-js-compat@3.48.0: + dependencies: + browserslist: 4.28.1 + + core-util-is@1.0.3: {} + + cosmiconfig@8.3.6(typescript@5.9.3): + dependencies: + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 5.9.3 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + date-fns@3.6.0: {} + + date-fns@4.1.0: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + detect-libc@2.1.2: {} + + electron-to-chromium@1.5.302: {} + + enhanced-resolve@5.19.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + entities@7.0.0: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-module-lexer@1.7.0: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + + escalade@3.2.0: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + eslint-import-context@0.1.9(unrs-resolver@1.11.1): + dependencies: + get-tsconfig: 4.13.6 + stable-hash-x: 0.2.0 + optionalDependencies: + unrs-resolver: 1.11.1 + + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)): + dependencies: + debug: 4.4.3 + eslint: 9.39.3(jiti@2.6.1) + eslint-import-context: 0.1.9(unrs-resolver@1.11.1) + get-tsconfig: 4.13.6 + is-bun-module: 2.0.0 + stable-hash-x: 0.2.0 + tinyglobby: 0.2.15 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)): + dependencies: + '@typescript-eslint/types': 8.56.1 + comment-parser: 1.4.5 + debug: 4.4.3 + eslint: 9.39.3(jiti@2.6.1) + eslint-import-context: 0.1.9(unrs-resolver@1.11.1) + is-glob: 4.0.3 + minimatch: 10.2.2 + semver: 7.7.4 + stable-hash-x: 0.2.0 + unrs-resolver: 1.11.1 + optionalDependencies: + '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + transitivePeerDependencies: + - supports-color + + eslint-plugin-unicorn@63.0.0(eslint@9.39.3(jiti@2.6.1)): + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.6.1)) + change-case: 5.4.4 + ci-info: 4.4.0 + clean-regexp: 1.0.0 + core-js-compat: 3.48.0 + eslint: 9.39.3(jiti@2.6.1) + find-up-simple: 1.0.1 + globals: 16.5.0 + indent-string: 5.0.0 + is-builtin-module: 5.0.0 + jsesc: 3.1.0 + pluralize: 8.0.0 + regexp-tree: 0.1.27 + regjsparser: 0.13.0 + semver: 7.7.4 + strip-indent: 4.1.1 + + eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)): + dependencies: + eslint: 9.39.3(jiti@2.6.1) + optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.3(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.4 + '@eslint/js': 9.39.3 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.14.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.4 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + esm@3.2.25: + optional: true + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + ethers@6.16.0: + dependencies: + '@adraffy/ens-normalize': 1.10.1 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@types/node': 22.7.5 + aes-js: 4.0.0-beta.5 + tslib: 2.7.0 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + expect-type@1.3.0: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up-simple@1.0.1: {} + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@11.1.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.2.3 + minimatch: 10.2.2 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.2 + + glob@13.0.6: + dependencies: + minimatch: 10.2.2 + minipass: 7.1.3 + path-scurry: 2.0.2 + + globals@14.0.0: {} + + globals@16.5.0: {} + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hookable@5.5.3: {} + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + immediate@3.0.6: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + indent-string@5.0.0: {} + + inherits@2.0.4: {} + + is-arrayish@0.2.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-builtin-module@5.0.0: + dependencies: + builtin-modules: 5.0.0 + + is-bun-module@2.0.0: + dependencies: + semver: 7.7.4 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-interactive@1.0.0: {} + + is-module@1.0.0: {} + + is-number@7.0.0: {} + + is-observable@2.1.0: {} + + is-unicode-supported@0.1.0: {} + + is-what@5.5.0: {} + + isarray@1.0.0: {} + + isexe@2.0.0: {} + + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + + jest-get-type@29.6.3: {} + + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + jiti@2.6.1: {} + + js-sha256@0.10.1: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + leven@3.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + + lightningcss-android-arm64@1.31.1: + optional: true + + lightningcss-darwin-arm64@1.31.1: + optional: true + + lightningcss-darwin-x64@1.31.1: + optional: true + + lightningcss-freebsd-x64@1.31.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.31.1: + optional: true + + lightningcss-linux-arm64-gnu@1.31.1: + optional: true + + lightningcss-linux-arm64-musl@1.31.1: + optional: true + + lightningcss-linux-x64-gnu@1.31.1: + optional: true + + lightningcss-linux-x64-musl@1.31.1: + optional: true + + lightningcss-win32-arm64-msvc@1.31.1: + optional: true + + lightningcss-win32-x64-msvc@1.31.1: + optional: true + + lightningcss@1.31.1: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.31.1 + lightningcss-darwin-arm64: 1.31.1 + lightningcss-darwin-x64: 1.31.1 + lightningcss-freebsd-x64: 1.31.1 + lightningcss-linux-arm-gnueabihf: 1.31.1 + lightningcss-linux-arm64-gnu: 1.31.1 + lightningcss-linux-arm64-musl: 1.31.1 + lightningcss-linux-x64-gnu: 1.31.1 + lightningcss-linux-x64-musl: 1.31.1 + lightningcss-win32-arm64-msvc: 1.31.1 + lightningcss-win32-x64-msvc: 1.31.1 + + lines-and-columns@1.2.4: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + lru-cache@11.2.6: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-vue-next@0.575.0(vue@3.5.26(typescript@5.9.3)): + dependencies: + vue: 3.5.26(typescript@5.9.3) + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mimic-fn@2.1.0: {} + + minimatch@10.2.2: + dependencies: + brace-expansion: 5.0.3 + + minimatch@3.1.4: + dependencies: + brace-expansion: 1.1.12 + + minipass@7.1.3: {} + + mitt@3.0.1: {} + + moo@0.5.2: {} + + mrmime@2.0.1: + optional: true + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + nanoid@3.3.11: {} + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.27: {} + + normalize-path@3.0.0: {} + + observable-fns@0.6.1: {} + + obug@2.1.1: {} + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-json-from-dist@1.0.1: {} + + pako@1.0.11: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.2.6 + minipass: 7.1.3 + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 7.7.9 + vue: 3.5.26(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + + pixelmatch@7.1.0: + dependencies: + pngjs: 7.0.0 + optional: true + + playwright-core@1.58.2: + optional: true + + playwright@1.58.2: + dependencies: + playwright-core: 1.58.2 + optionalDependencies: + fsevents: 2.3.2 + optional: true + + pluralize@8.0.0: {} + + pngjs@7.0.0: + optional: true + + pnpm@10.26.2: {} + + pofile@1.1.4: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@3.8.1: {} + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + process-nextick-args@2.0.1: {} + + pseudolocale@2.2.0: + dependencies: + commander: 10.0.1 + + punycode@2.3.1: {} + + react-is@18.3.1: {} + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readdirp@3.5.0: + dependencies: + picomatch: 2.3.1 + + regexp-tree@0.1.27: {} + + regjsparser@0.13.0: + dependencies: + jsesc: 3.1.0 + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + rfdc@1.4.1: {} + + rimraf@6.1.3: + dependencies: + glob: 13.0.6 + package-json-from-dist: 1.0.1 + + rollup@4.54.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.54.0 + '@rollup/rollup-android-arm64': 4.54.0 + '@rollup/rollup-darwin-arm64': 4.54.0 + '@rollup/rollup-darwin-x64': 4.54.0 + '@rollup/rollup-freebsd-arm64': 4.54.0 + '@rollup/rollup-freebsd-x64': 4.54.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.54.0 + '@rollup/rollup-linux-arm-musleabihf': 4.54.0 + '@rollup/rollup-linux-arm64-gnu': 4.54.0 + '@rollup/rollup-linux-arm64-musl': 4.54.0 + '@rollup/rollup-linux-loong64-gnu': 4.54.0 + '@rollup/rollup-linux-ppc64-gnu': 4.54.0 + '@rollup/rollup-linux-riscv64-gnu': 4.54.0 + '@rollup/rollup-linux-riscv64-musl': 4.54.0 + '@rollup/rollup-linux-s390x-gnu': 4.54.0 + '@rollup/rollup-linux-x64-gnu': 4.54.0 + '@rollup/rollup-linux-x64-musl': 4.54.0 + '@rollup/rollup-openharmony-arm64': 4.54.0 + '@rollup/rollup-win32-arm64-msvc': 4.54.0 + '@rollup/rollup-win32-ia32-msvc': 4.54.0 + '@rollup/rollup-win32-x64-gnu': 4.54.0 + '@rollup/rollup-win32-x64-msvc': 4.54.0 + fsevents: 2.3.3 + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + safe-buffer@5.1.2: {} + + semver@6.3.1: {} + + semver@7.7.4: {} + + setimmediate@1.0.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + optional: true + + source-map-js@1.2.1: {} + + source-map@0.7.6: {} + + speakingurl@14.0.1: {} + + stable-hash-x@0.2.0: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-indent@4.1.1: {} + + strip-json-comments@3.1.1: {} + + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwindcss@4.2.1: {} + + tapable@2.3.0: {} + + threads@1.7.0: + dependencies: + callsites: 3.1.0 + debug: 4.4.3 + is-observable: 2.1.0 + observable-fns: 0.6.1 + optionalDependencies: + tiny-worker: 2.3.0 + transitivePeerDependencies: + - supports-color + + tiny-worker@2.3.0: + dependencies: + esm: 3.2.25 + optional: true + + tinybench@2.9.0: {} + + tinyexec@1.0.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinyrainbow@3.0.3: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + totalist@3.0.1: + optional: true + + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tslib@2.7.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.3(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + undici-types@6.19.8: {} + + undici-types@7.16.0: {} + + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1): + dependencies: + esbuild: 0.27.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.54.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.4 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.31.1 + + vitest@4.0.18(@types/node@24.10.4)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(lightningcss@1.31.1): + dependencies: + '@vitest/expect': 4.0.18 + '@vitest/mocker': 4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)) + '@vitest/pretty-format': 4.0.18 + '@vitest/runner': 4.0.18 + '@vitest/snapshot': 4.0.18 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + es-module-lexer: 1.7.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.10.4 + '@vitest/browser-playwright': 4.0.18(playwright@1.58.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))(vitest@4.0.18) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + + vscode-uri@3.1.0: {} + + vue-tsc@3.2.1(typescript@5.9.3): + dependencies: + '@volar/typescript': 2.4.27 + '@vue/language-core': 3.2.1 + typescript: 5.9.3 + + vue@3.5.26(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.26 + '@vue/compiler-sfc': 3.5.26 + '@vue/runtime-dom': 3.5.26 + '@vue/server-renderer': 3.5.26(vue@3.5.26(typescript@5.9.3)) + '@vue/shared': 3.5.26 + optionalDependencies: + typescript: 5.9.3 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + ws@8.17.1: {} + + ws@8.19.0: + optional: true + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..182def7 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,8 @@ +packages: + - apps/* + - packages/* + +onlyBuiltDependencies: + - esbuild + - unrs-resolver + - wasm-pack diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..9b42f71 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,8 @@ +@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ +@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/ +erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/ +forge-std/=lib/forge-std/src/ +halmos-cheatcodes/=lib/openzeppelin-contracts-upgradeable/lib/halmos-cheatcodes/src/ +openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/ +openzeppelin-contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/ +openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/ diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..5d56faf --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" diff --git a/script/DeployCreate2.s.sol b/script/DeployCreate2.s.sol new file mode 100644 index 0000000..07a73ab --- /dev/null +++ b/script/DeployCreate2.s.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Script, console} from "forge-std/Script.sol"; +import {UniversalTimestamps} from "../contracts/UniversalTimestamps.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +interface ICreateX { + function deployCreate2(bytes32 salt, bytes memory initCode) external payable returns (address); + + function computeCreate2Address(bytes32 salt, bytes32 initCodeHash) external view returns (address); +} + +contract DeployCreate2 is Script { + // CreateX is deployed at the same address on all supported chains + ICreateX constant CREATEX = ICreateX(0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed); + bytes32 constant SALT = keccak256("universal-timestamps"); + + function run() public { + address owner = vm.envAddress("OWNER_ADDRESS"); + + vm.startBroadcast(); + UniversalTimestamps implementation = new UniversalTimestamps{salt: SALT}(); + // Implementation deployed at: 0x2D806e4ae1c3FDCfecb019B192a53371CAC889A7 + console.log("Implementation deployed at:", address(implementation)); + + bytes memory initData = abi.encodeCall(UniversalTimestamps.initialize, (owner)); + + ERC1967Proxy proxy = new ERC1967Proxy{salt: SALT}(address(implementation), initData); + vm.stopBroadcast(); + + // Proxy deployed at: 0xceB7a9E77bd00D0391349B9bC989167cAB5e35e7 + console.log("Proxy deployed at:", address(proxy)); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9e7bf2a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,63 @@ +{ + "compilerOptions": { + "verbatimModuleSyntax": true, + // Classification follows https://www.typescriptlang.org/tsconfig + + // Type Checking + "strict": true, + // exactOptionalPropertyTypes: false, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noImplicitThis": true, + // noPropertyAccessFromIndexSignature: false, + // noUncheckedIndexedAccess: false, + "noUncheckedSideEffectImports": true, + // noUnusedLocals: false, + // noUnusedParameters: false, + "useUnknownInCatchVariables": true, + + // Modules + "module": "preserve", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, + + // Emit + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "stripInternal": true, // skip type emit for @internal types + + // JavaScript Support + "allowJs": false, + "checkJs": false, + + // Interop Constraints + "forceConsistentCasingInFileNames": true, + + // Language and Environment + "jsx": "react-jsx", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "target": "ES2023", + "useDefineForClassFields": true, + + // Projects + "composite": true, + "incremental": true, + + // Completeness + "skipLibCheck": true, // skip all type checks for .d.ts files + + "paths": { + "@uts/sdk": ["./packages/sdk/src/index.ts"] + }, + "customConditions": ["uts-source"] + }, + "references": [ + { "path": "./packages/sdk" }, + { "path": "./packages/sdk/test/" }, + { "path": "./apps/web" } + ] +}