Browse Source

Aztec testnet integration (#4511)

* Aztec-Wormhole / Core Contract & Guardian (#1)

* aztec initial commit

* changes

* Initial structs commit

* changes

* changes

* changes

* expire guardian set

* publishMessage Implementation

* publishMessage changes

* initial watcher implementation

* aztec config

* preliminary work

* watcher changes

* watcher updates

* PR review

* PR review

* PR Review Changes

* Changes

* Changes

* PR Review Changes

* Gitignore

* Revert .env.blast.testnet

* Delete aztec/contracts/codegenCache.json

* Delete aztec/codegenCache.json

* Remove .DS_Store file

* Fix watcher & PR review comment

* Update sdk/vaa/structs.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix bug in node.go (#2)

* Remove extraneous diff

* Add aztec to Tilt

* Relayer Implementation & Verify VAA (#4)

* aztec initial commit

* changes

* Initial structs commit

* changes

* changes

* changes

* expire guardian set

* publishMessage Implementation

* publishMessage changes

* initial watcher implementation

* aztec config

* preliminary work

* watcher changes

* watcher updates

* PR review

* PR review

* PR Review Changes

* Changes

* Changes

* PR Review Changes

* Gitignore

* Revert .env.blast.testnet

* Delete aztec/contracts/codegenCache.json

* Delete aztec/codegenCache.json

* Remove .DS_Store file

* Fix watcher & PR review comment

* Update sdk/vaa/structs.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Relayer Implementation & Verify VAA

* Relayer

* Relayer

* Delete relayer compiled file

* Relayer dummy implementation for receiving in aztec

* Aztec core contracts

* PR Review changes to go.mod

* PR Review changes

* Changes

* Fixes

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Consistency Levels Implementation (#11)

* Consistency Levels Implementation

* Add Filter Based on contract Address for Logs

* Mutex Lock fix (#12)

* Refactoring to add defer

* Watcher & Functionallity Tests (#13)

Watcher & functionallity tests

* Minor fixes (#14)

* Minor fixes

* Update to v0.85.0

* Relayer fixes (#15)

* added wormhole core on aztec without token functionality (#17)

* added wormhole core on aztec without token functionality

* fixed pxe url & added consistency level & increased size of payload

* Integration Fixes (#18)

* Integration Fixes

* Removing unused simulation function

* Change default contract in relayer for consistency

* Format changes & Improvements (#20)

* Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Aztec RPC changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Upgrade to 0.87.2

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Upgrade to 0.87.2

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Upgrade to 0.87.2

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Integration fixes working

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* txHash fix

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Integration fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Improvements

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Improvements

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Improvements & unit tests fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Relayer Improvements

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* split payment into public & private

* Private flow changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

---------

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>
Co-authored-by: MBelegris <mikeybel98@gmail.com>

* Testnet fixes (#22)

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testnet Changes (#23)

* Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* updated hardcoded addresses

---------

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>
Co-authored-by: MBelegris <mikeybel98@gmail.com>

* Wormhole Contracts Aztec (#25)

* Initial work for parse and verify

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Initial work for parse and verify

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Initial work for parse and verify

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Contract Fixes & Guardian public keys

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Parse & Verify function & Tests

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Tests Improvements

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Finalized Contracts & JS service for relaying and script for extracting guardian public keys

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Finalized Contracts & JS service for relaying and script for extracting guardian public keys

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Improvements

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

---------

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Aztec Branch Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Aztec Branch Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Aztec Implementation (#4436)

* Aztec-Wormhole / Core Contract & Guardian (#1)

* aztec initial commit

* changes

* Initial structs commit

* changes

* changes

* changes

* expire guardian set

* publishMessage Implementation

* publishMessage changes

* initial watcher implementation

* aztec config

* preliminary work

* watcher changes

* watcher updates

* PR review

* PR review

* PR Review Changes

* Changes

* Changes

* PR Review Changes

* Gitignore

* Revert .env.blast.testnet

* Delete aztec/contracts/codegenCache.json

* Delete aztec/codegenCache.json

* Remove .DS_Store file

* Fix watcher & PR review comment

* Update sdk/vaa/structs.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix bug in node.go (#2)

* Remove extraneous diff

* Add aztec to Tilt

* Relayer Implementation & Verify VAA (#4)

* aztec initial commit

* changes

* Initial structs commit

* changes

* changes

* changes

* expire guardian set

* publishMessage Implementation

* publishMessage changes

* initial watcher implementation

* aztec config

* preliminary work

* watcher changes

* watcher updates

* PR review

* PR review

* PR Review Changes

* Changes

* Changes

* PR Review Changes

* Gitignore

* Revert .env.blast.testnet

* Delete aztec/contracts/codegenCache.json

* Delete aztec/codegenCache.json

* Remove .DS_Store file

* Fix watcher & PR review comment

* Update sdk/vaa/structs.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Relayer Implementation & Verify VAA

* Relayer

* Relayer

* Delete relayer compiled file

* Relayer dummy implementation for receiving in aztec

* Aztec core contracts

* PR Review changes to go.mod

* PR Review changes

* Changes

* Fixes

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Consistency Levels Implementation (#11)

* Consistency Levels Implementation

* Add Filter Based on contract Address for Logs

* Mutex Lock fix (#12)

* Refactoring to add defer

* Watcher & Functionallity Tests (#13)

Watcher & functionallity tests

* Minor fixes (#14)

* Minor fixes

* Update to v0.85.0

* Relayer fixes (#15)

* added wormhole core on aztec without token functionality (#17)

* added wormhole core on aztec without token functionality

* fixed pxe url & added consistency level & increased size of payload

* Integration Fixes (#18)

* Integration Fixes

* Removing unused simulation function

* Change default contract in relayer for consistency

* Format changes & Improvements (#20)

* Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Aztec RPC changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Upgrade to 0.87.2

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Upgrade to 0.87.2

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Upgrade to 0.87.2

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Integration fixes working

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* txHash fix

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Integration fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Improvements

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Improvements

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Improvements & unit tests fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Relayer Improvements

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* split payment into public & private

* Private flow changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

---------

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>
Co-authored-by: MBelegris <mikeybel98@gmail.com>

* Testnet fixes (#22)

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testnet Changes (#23)

* Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* updated hardcoded addresses

---------

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>
Co-authored-by: MBelegris <mikeybel98@gmail.com>

* Wormhole Contracts Aztec (#25)

* Initial work for parse and verify

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Initial work for parse and verify

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Initial work for parse and verify

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Contract Fixes & Guardian public keys

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Parse & Verify function & Tests

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Tests Improvements

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Finalized Contracts & JS service for relaying and script for extracting guardian public keys

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Finalized Contracts & JS service for relaying and script for extracting guardian public keys

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Improvements

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

---------

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Aztec Branch Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Aztec Branch Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

---------

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Josh Klopfenstein <git@joshklop.com>
Co-authored-by: MBelegris <35403627+MBelegris@users.noreply.github.com>
Co-authored-by: MBelegris <mikeybel98@gmail.com>

* build fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* build fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* build fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* build fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Aztec Build Fixes (#4437)

* Aztec-Wormhole / Core Contract & Guardian (#1)

* aztec initial commit

* changes

* Initial structs commit

* changes

* changes

* changes

* expire guardian set

* publishMessage Implementation

* publishMessage changes

* initial watcher implementation

* aztec config

* preliminary work

* watcher changes

* watcher updates

* PR review

* PR review

* PR Review Changes

* Changes

* Changes

* PR Review Changes

* Gitignore

* Revert .env.blast.testnet

* Delete aztec/contracts/codegenCache.json

* Delete aztec/codegenCache.json

* Remove .DS_Store file

* Fix watcher & PR review comment

* Update sdk/vaa/structs.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix bug in node.go (#2)

* Remove extraneous diff

* Add aztec to Tilt

* Relayer Implementation & Verify VAA (#4)

* aztec initial commit

* changes

* Initial structs commit

* changes

* changes

* changes

* expire guardian set

* publishMessage Implementation

* publishMessage changes

* initial watcher implementation

* aztec config

* preliminary work

* watcher changes

* watcher updates

* PR review

* PR review

* PR Review Changes

* Changes

* Changes

* PR Review Changes

* Gitignore

* Revert .env.blast.testnet

* Delete aztec/contracts/codegenCache.json

* Delete aztec/codegenCache.json

* Remove .DS_Store file

* Fix watcher & PR review comment

* Update sdk/vaa/structs.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Relayer Implementation & Verify VAA

* Relayer

* Relayer

* Delete relayer compiled file

* Relayer dummy implementation for receiving in aztec

* Aztec core contracts

* PR Review changes to go.mod

* PR Review changes

* Changes

* Fixes

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Consistency Levels Implementation (#11)

* Consistency Levels Implementation

* Add Filter Based on contract Address for Logs

* Mutex Lock fix (#12)

* Refactoring to add defer

* Watcher & Functionallity Tests (#13)

Watcher & functionallity tests

* Minor fixes (#14)

* Minor fixes

* Update to v0.85.0

* Relayer fixes (#15)

* added wormhole core on aztec without token functionality (#17)

* added wormhole core on aztec without token functionality

* fixed pxe url & added consistency level & increased size of payload

* Integration Fixes (#18)

* Integration Fixes

* Removing unused simulation function

* Change default contract in relayer for consistency

* Format changes & Improvements (#20)

* Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Aztec RPC changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Upgrade to 0.87.2

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Upgrade to 0.87.2

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Upgrade to 0.87.2

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Integration fixes working

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* txHash fix

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Integration fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Improvements

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Improvements

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Improvements & unit tests fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Relayer Improvements

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* split payment into public & private

* Private flow changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

---------

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>
Co-authored-by: MBelegris <mikeybel98@gmail.com>

* Testnet fixes (#22)

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testnet Changes (#23)

* Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* updated hardcoded addresses

---------

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>
Co-authored-by: MBelegris <mikeybel98@gmail.com>

* Wormhole Contracts Aztec (#25)

* Initial work for parse and verify

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Initial work for parse and verify

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Initial work for parse and verify

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Contract Fixes & Guardian public keys

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Parse & Verify function & Tests

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Tests Improvements

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Finalized Contracts & JS service for relaying and script for extracting guardian public keys

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Finalized Contracts & JS service for relaying and script for extracting guardian public keys

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Improvements

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

---------

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Aztec Branch Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Aztec Branch Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* build fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* build fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* build fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* build fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

---------

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Josh Klopfenstein <git@joshklop.com>
Co-authored-by: MBelegris <35403627+MBelegris@users.noreply.github.com>
Co-authored-by: MBelegris <mikeybel98@gmail.com>

* 1.0.0-staging.6 update

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Aztec Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Aztec Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Deployed Contract

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Deployed Contract

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testnet Changes & Unit Testing - Broken

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testing

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* tests

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* fix VAA input in test; delete unused function.

* Testnet Changes

* corrected versions, in-script PXE, redeployed wormhole

* capture rig

* remove loop in signature verification, update contract address

* update contract address

* WIP: deployment script

* fix: correct funding logic in service script

* WIP: deployment script

* add readme

* update contract address in json

* remove extra file

* update contract address

* fixes

* fixes

* fixes

* updates based on wh changes

* wh changes

* nolint for reobserver, not implemented in aztec

* nolint

* generate publicrpc.pb.go

* update deployment documentation

* add aztec-specific terms to cspell

* fix cspell-custom-words order

* fix cspell-custom-words order

* update Aztec core contract address

* Aztec Testnet (#4439)

* Aztec-Wormhole / Core Contract & Guardian (#1)

* aztec initial commit

* changes

* Initial structs commit

* changes

* changes

* changes

* expire guardian set

* publishMessage Implementation

* publishMessage changes

* initial watcher implementation

* aztec config

* preliminary work

* watcher changes

* watcher updates

* PR review

* PR review

* PR Review Changes

* Changes

* Changes

* PR Review Changes

* Gitignore

* Revert .env.blast.testnet

* Delete aztec/contracts/codegenCache.json

* Delete aztec/codegenCache.json

* Remove .DS_Store file

* Fix watcher & PR review comment

* Update sdk/vaa/structs.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix bug in node.go (#2)

* Remove extraneous diff

* Add aztec to Tilt

* Relayer Implementation & Verify VAA (#4)

* aztec initial commit

* changes

* Initial structs commit

* changes

* changes

* changes

* expire guardian set

* publishMessage Implementation

* publishMessage changes

* initial watcher implementation

* aztec config

* preliminary work

* watcher changes

* watcher updates

* PR review

* PR review

* PR Review Changes

* Changes

* Changes

* PR Review Changes

* Gitignore

* Revert .env.blast.testnet

* Delete aztec/contracts/codegenCache.json

* Delete aztec/codegenCache.json

* Remove .DS_Store file

* Fix watcher & PR review comment

* Update sdk/vaa/structs.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Relayer Implementation & Verify VAA

* Relayer

* Relayer

* Delete relayer compiled file

* Relayer dummy implementation for receiving in aztec

* Aztec core contracts

* PR Review changes to go.mod

* PR Review changes

* Changes

* Fixes

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Consistency Levels Implementation (#11)

* Consistency Levels Implementation

* Add Filter Based on contract Address for Logs

* Mutex Lock fix (#12)

* Refactoring to add defer

* Watcher & Functionallity Tests (#13)

Watcher & functionallity tests

* Minor fixes (#14)

* Minor fixes

* Update to v0.85.0

* Relayer fixes (#15)

* added wormhole core on aztec without token functionality (#17)

* added wormhole core on aztec without token functionality

* fixed pxe url & added consistency level & increased size of payload

* Integration Fixes (#18)

* Integration Fixes

* Removing unused simulation function

* Change default contract in relayer for consistency

* Format changes & Improvements (#20)

* Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Aztec RPC changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Upgrade to 0.87.2

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Upgrade to 0.87.2

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Upgrade to 0.87.2

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Integration fixes working

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* txHash fix

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Integration fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Improvements

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Improvements

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Improvements & unit tests fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Relayer Improvements

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* split payment into public & private

* Private flow changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

---------

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>
Co-authored-by: MBelegris <mikeybel98@gmail.com>

* Testnet fixes (#22)

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testnet Changes (#23)

* Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* updated hardcoded addresses

---------

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>
Co-authored-by: MBelegris <mikeybel98@gmail.com>

* Wormhole Contracts Aztec (#25)

* Initial work for parse and verify

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Initial work for parse and verify

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Initial work for parse and verify

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Contract Fixes & Guardian public keys

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Parse & Verify function & Tests

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Tests Improvements

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Finalized Contracts & JS service for relaying and script for extracting guardian public keys

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Finalized Contracts & JS service for relaying and script for extracting guardian public keys

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Improvements

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

---------

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Aztec Branch Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Aztec Branch Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Node: Remove obsolete L1Finalizer (#4434)

* build fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* build fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* build fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* build fixes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* 1.0.0-staging.6 update

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Aztec Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Aztec Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Reserving chanID for Aztec (#4440)

* Reserving chanID for Aztec

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Reserving chanID for Aztec

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Reserving chanID for Aztec

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

---------

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* node: Add emitters for mUSD deployment to NTT Accountant (#4435)

* node: Add emitters for mUSD deployment to NTT Accountant

* Fixed chain IDs for mUSD Wormhole Transceivers

* Deployed Contract

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Deployed Contract

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* docs: Add NTT-related information to Accountant whitepaper (#4422)

* docs: Add NTT-related information to Accountant whitepaper

* Doc: Update guardian doc (#4427)

* Doc: Update guardian doc

* Doc: Code review rework

* Testnet Changes

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* node: Fix marshaling for MessagePublications (#4428)

- Correct marshaling for MessagePublication to include the newer fields
  added to this struct (Unreliable, VerificationState)
- Implement marshal/unmarshal in new methods on MessagePublication that
  conform to the standard Go marshaling interfaces
- Upgrades the Governor's database methods to use the new format
- Add instructions on how to do a Governor database migration

Previously, these fields were not marshaled but this had little effect
as the MessagePublication was only unmarshaled by the Governor which did
not makes use of the new fields.
However, the Verification State will be required by the Transfer
Verifier and Notary, so it's now necessary to fix marshaling.

Chainlock
- Remove deprecated UnmarshalOldMessagePublicationWithTxHash function
- Replace basic TestUnmarshalError with comprehensive table-driven TestMessagePublicationUnmarshalBinaryErrors
- Add test cases covering all error conditions for new Unmarshal
  function
- Add deprecation comments to legacy test functions

Governor
- Update PendingTransfer to use new MarshalBinary/UnmarshalBinary methods
- Increment version prefixes: GOV:XFER3→XFER4, GOV:PENDING4→PENDING5
- Remove unmarshalOldTransfer function, consolidate to UnmarshalTransfer
- Add comprehensive documentation for Transfer and PendingTransfer types
- Update test expectations for new version prefixes

* Node: Remove unnecessary chain ID tests (#4449)

* sui: update wormhole upgrade script

* sui: update token bridge upgrade script

* sui: update testnet published-at addresses

* EVM/Node: Custom consistency level (#4406)

* eth: Custom consistency Level

* eth: Update TestCustomConsistencyLevel

* Node: Custom consistency level

* Add integration tests

* Move testing stuff

* Node: Code review rework

* Test: Simplify tests

* Doc: Update white paper

* Test: Use a different private key

* Node: Code review rework

* node: Update Governor token list

* Github: Remove Bruce as code owner (#4453)

* Node: Remove deprecated chains (#4446)

* Node: Remove deprecated chains

This chain removes references to Terra, Terra2, Oasis, Aurora, Karura, Acala, XLPA, Snaxchain and Blast from the guardian.

This includes removing them from the go SDK and the protobuf. This involves leaving comments indicating that they are obsolete so no one reuses the chain IDs.

It turns out that Tilt does not pass when Terra and Terra2 are removed (wormchain-deploy fails on the ibc-relayer). Therefore I left those two chains in the guardian, but only allow them to run in UnsafeDevNet (Tilt) mode.

The ts sdk in the monorepo has been deprecated and is no longer maintained. (If you dig deeper, you'll see it does not support many of the newer chains.) At some point, someone should probably rip that out all together, but we can't right now because lots of tests and tools still use it.

I created an issue for the Core Protocol Team to fix the tilt issue and finish removing the Terras.

* docs: Clarify tokenfactory restrictions (#4403)

- Add information about tokenfactory restrictions. These can be
  confirmed by reading through 5255e933d68629f0643207b0f9d3fa797af5cbf7
  where the module was added originally, as well as reviewing the Token
  Factory's capabilities configuration within Wormchain's app.go file.
- Remove documentation for unsupported message.
- Add link to upstream tokenfactory documentation.

* Testnet Changes & Unit Testing - Broken

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* Testing

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* tests

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>

* ci: Fix lint issues raised by new golangci-lint and clippy versions (#4460)

* ci: Upgrade golangci-lint version
* Add nolint/TODOs for new noctx violations (All of these warnings should be fixed. However, this is currently
  blocking CI and resolving that is the immediate issue to solve.)
* add TODO for exhaustruct
* fix clippy warnings in terra, cosmwasm

* ci: Specify permissions on job

* update nodejs (#4409) (#4410)

* noot

* oh they havent finished publishing the images yet lol

* oops

* adjust the parse tests

* noot

* noot

* Add guardian set prototxt files and short readme (#4466)

* Update CODEOWNERS (#4465)

Replace @nik-suri with @mdulin2 as a CODEOWNER for `cosmwasm`

* fix VAA input in test; delete unused function.

* Testnet Changes

* corrected versions, in-script PXE, redeployed wormhole

* Add support for XRPL-EVM (#4459)

Deploy core and tokenbridge contracts to xrplevm

Testnet deployment

```
-- Wormhole Core Addresses --------------------------------------------------
| Setup address                | 0xe74F20a5A07921f63F2d55B8aE6d14f4AD490938 |
| Implementation address       | 0x32b3b68e9f053E724Da0A9e57F062BFaE6695350 |
| Wormhole address             | 0xaBf89de706B583424328B54dD05a8fC986750Da8 |
-----------------------------------------------------------------------------

 -- TokenBridge Addresses ----------------------------------------------------
| Token Implementation address | 0x97791aB7E653c1E6D87Bf421B3B71e0154Dfb225 |
| BridgeSetup address          | 0x27ab99256eCbE78876Abb3671262c61F937D6eC2 |
| BridgeImplementation address | 0xC699482c17d43b7D5349F2D3f58d61fEFA972B8c |
| TokenBridge address          | 0x7d8eBc211C4221eA18E511E4f0fD50c5A539f275 |
-----------------------------------------------------------------------------

./verify -r https://rpc.testnet.xrplevm.org/ -c XLRPEVM build-forge/Implementation.sol/Implementation.json 0x32b3b68e9f053E724Da0A9e57F062BFaE6695350
Deployed bytecode of 0x32b3b68e9f053E724Da0A9e57F062BFaE6695350 on XLRPEVM
matches build-forge/Implementation.sol/Implementation.json
```

Mainnet deployment

```
-- Wormhole Core Addresses --------------------------------------------------
| Setup address                | 0xe74F20a5A07921f63F2d55B8aE6d14f4AD490938 |
| Implementation address       | 0x32b3b68e9f053E724Da0A9e57F062BFaE6695350 |
| Wormhole address             | 0xaBf89de706B583424328B54dD05a8fC986750Da8 |
-----------------------------------------------------------------------------

-- TokenBridge Addresses ----------------------------------------------------
| Token Implementation address | 0x3Fbd222f7ef286E2366cB3669640Cf67d48b3cef |
| BridgeSetup address          | 0x02dC9B094A3Bc68B2116F1b914fB0B5e523e5781 |
| BridgeImplementation address | 0x4eba0c3A3B6705D50Aa5417494125bD745ADe257 |
| TokenBridge address          | 0x47F5195163270345fb4d7B9319Eda8C64C75E278 |
-----------------------------------------------------------------------------

./verify -r https://rpc.xrplevm.org/ -c XLRPEVM build-forge/Implementation.sol/Implementation.json
0x32b3b68e9f053E724Da0A9e57F062BFaE6695350
Deployed bytecode of 0x32b3b68e9f053E724Da0A9e57F062BFaE6695350 on XLRPEVM
matches build-forge/Implementation.sol/Implementation.json
```

Update watcher with XRPL-EVM mainnet and testnet values

* capture rig

* Testnet XRPLEVM SR constants (#4467)

* fix(flags): update xrplEvmContract flag (#4477)

* add XRPLEVM tokenbridge and SR addresses to sdk constants (#4479)

* add XRPLEVM tokenbridge addresses to sdk constants

set mainnet tokenbridge address to 00000000000000000000000047F5195163270345fb4d7B9319Eda8C64C75E278

set testnet tokenbridge address to 0000000000000000000000007d8eBc211C4221eA18E511E4f0fD50c5A539f275

* add XRPL mainnet standard relayer address

add XRPL to mainnet governor

* omit xrpl from governor mainnet token test

* remove loop in signature verification, update contract address

* update contract address

* WIP: deployment script

* node: governor token list update (#4457)

Co-authored-by: djb15 <djb15@users.noreply.github.com>

* feat (watcher): add linea mainnet (#4464)

* feat (watcher): add linea mainnet

* chore: lint

* add linea mainnet tokenbridge address

* add linea to governor mainnet chains

* fix: correct funding logic in service script

* WIP: deployment script

* add readme

* update contract address in json

* remove extra file

* update contract address

* node(transfer verifier): Update TransferIsValid method to return results per Message rather than per Receipt (#4451)

* txverifier: independently process multiple messages in one receipt
- Update TransferIsValid method to support per-message validation
- Add msgID parameter (equivalent to VAA ID) to TransferIsValid method
  signature to enable validation of specific messages within transaction
  receipts. Update caching mechanism to store per-message validation
  results instead of per-transaction results. Modify receipt processing
  to track message-specific evaluations and validate transfer amounts on
  a per-message basis.

Key changes:
- Add msgID parameter to TransferIsValid method for targeted message validation
- Replace single Result field in evaluation struct with Results map keyed by message ID
- Update receipt summary to track transfers per message ID rather than aggregated amounts
- Modify invariant checking to validate each message's transfers individually
- Add MsgID method to generate unique message identifiers from LogMessagePublished events
- Add sequence field to LogMessagePublished struct for message identification

* fix(transfer verifier): Use Uint64() instead of parseUint for EVM chain ID conversion

* Add CODEOWNERS for Guardian Dependency Upgrades

Allows the security team to review dependencies for major components before being upgraded.

* node: change default ethereum testnet to sepolia from holesky (#4484)

* Update vaa.ChainIDEthereum testnet to use Sepolia instead of Goerli

* holesky, not goerli

* don't disable vaa.ChainIDSepolia

* Update chain_config.go

* Update tests

* fixes

* fixes

* fixes

* updates based on wh changes

* wh changes

* nolint for reobserver, not implemented in aztec

* nolint

* generate publicrpc.pb.go

* update deployment documentation

* add aztec-specific terms to cspell

* fix cspell-custom-words order

* fix cspell-custom-words order

* update Aztec core contract address

---------

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Josh Klopfenstein <git@joshklop.com>
Co-authored-by: MBelegris <35403627+MBelegris@users.noreply.github.com>
Co-authored-by: MBelegris <mikeybel98@gmail.com>
Co-authored-by: bruce-riley <96066700+bruce-riley@users.noreply.github.com>
Co-authored-by: Nikhil Suri <nikhilsuri@comcast.net>
Co-authored-by: John Saigle <4022790+johnsaigle@users.noreply.github.com>
Co-authored-by: Csongor Kiss <kiss.csongor.kiss@gmail.com>
Co-authored-by: Dirk Brink <hello@dirk.tech>
Co-authored-by: a <edward9.lee@gmail.com>
Co-authored-by: Adam <20446095+aadam-10@users.noreply.github.com>
Co-authored-by: jorem321 <jorgearce321@gmail.com>
Co-authored-by: thunkar <gregojquiros@gmail.com>
Co-authored-by: Aaron Clark <51387046+aapclark@users.noreply.github.com>
Co-authored-by: Hernán Di Pietro <hernan.di.pietro@gmail.com>
Co-authored-by: André Claro <andre.claro@gmail.com>
Co-authored-by: djb15 <djb15@users.noreply.github.com>
Co-authored-by: Edgar Barrantes <edgar@barrantes.dev>
Co-authored-by: Jason Matthyser <jasonmatthyser@gmail.com>
Co-authored-by: Maxwell "ꓘ" Dulin <mdulin2@zagmail.gonzaga.edu>

* Aztec fixes

* comments deletion

* revert l1_verifier

* comments deletion

* comments deletion

* update version

* Small fixes

* ci: lint: Empty body in an if or else branch (#4496)

* node: Add Notary package for verifying MessagePublications (#4315)

This PR adds a "Notary" package that can evaluates the verification state of a Message Publication. It provides a Verdict on what should occur, e.g. it reports that an `Anomalous` message should be delayed.

It stores messages that should be Delayed or Blackholed in a database (BadgerDB)

## Changes to the Processor
The processor now inspects the Notary's queue of message to delay and skips publishing them until their ReleaseTime has passed. Once they are ready to release, it passes them to the Governor and Accountant as normal (assuming Guardians have them enabled).

# Design decisions + supporting tasks

## Database

The package uses the BadgerDB key-value store to delay and persist messages, similar to the Governor. This allows the Notary's decisions to persist across restarts.

An implementation is possible that would use only in-memory representations, but this has some drawbacks:
* We would lose messages that are marked as delayed on a node restart
* Messages with a `Rejected` status should never be processed until the end of time. We need a way to track this.

Rather than use the existing `Database` struct (like Governor and Accountant), a new `NotaryDB` struct was added that is a wrapper for the BadgerDB connection. The goal here is to isolate the Notary-related API from other modules.

## Creation of min-heap data structure PendingMessageQueue

Conceptually, it makes sense to store delayed messages according to their release time. If the collection is always sorted, we can access elements from one side or the other until one message is too recent to process. In that case, all of the other messages will also be too new, and we can exit early.

This also allows us to be flexible with our delay time, compared with storing the time _at which_ the message is stored rather than when it should be delayed. We can delay a message for one minute, a day, or a year. As long as the sorting holds, we don't need to iterate over many items.

In order to achieve this, I've added a min-heap data structure with a safe API that allows for storing messages this way.

# Future Work

## Add drop/release/reset admin commands

This package should allow a super-minority of Guardians to forcibly drop, release, or reset the delay time of delayed or blackholed messages. This can help when the Notary has delayed an Anomalous message that is in fact fraudulent - in this case, the message can be moved to the 'blackhole' deny-list. Alternatively, if the Transfer Verifier or Notary has a bug, this allows the Guardians to release a harmless message.

## Add Guardian p2p feature flags
Similar to: https://github.com/wormhole-foundation/wormhole/pull/4317

This change will be done in a follow-up PR.

* add monad mainnet to watcher (#4508)

* migrate noir contracts to v2.0.3, make messaging fee optional

* contract cleanup

* contract cleanup

* remove hardcoded addresses; set access control and address change functions

* chore: update deployment addresses and instructions

* chore: update Wormhole contract addresses

* chore: remove old private keys from verification service, use .env instead

* fix: spellcheck lint error

* feat: migrate vaa-verification-service to Aztec v2.0.3

- Update dependencies to v2.0.3
- Fix import: getContractInstanceFromDeployParams → getContractInstanceFromInstantiationParams
- Recompile contracts with aztec-nargo and aztec-postprocess-contract
- Add mandatory 'from' parameter to contract method calls
- Remove problematic profiling call that caused errors
- Service now successfully connects to testnet and processes VAA verification requests

* fix: deployment instructions should target new testnet

* chore: update deployment contract addresses

* chore: update deployment addresses

* fix(watcher): replace io.ReadAll with common.SafeRead to prevent DoS

---------

Signed-off-by: stavrosvl7 <stavrosvl7@gmail.com>
Co-authored-by: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Josh Klopfenstein <git@joshklop.com>
Co-authored-by: MBelegris <35403627+MBelegris@users.noreply.github.com>
Co-authored-by: MBelegris <mikeybel98@gmail.com>
Co-authored-by: stavrosvl7 <stavrosvl7@gmail.com>
Co-authored-by: thunkar <gregojquiros@gmail.com>
Co-authored-by: Edgar Barrantes <edgar@barrantes.dev>
Co-authored-by: bruce-riley <96066700+bruce-riley@users.noreply.github.com>
Co-authored-by: Nikhil Suri <nikhilsuri@comcast.net>
Co-authored-by: John Saigle <4022790+johnsaigle@users.noreply.github.com>
Co-authored-by: Csongor Kiss <kiss.csongor.kiss@gmail.com>
Co-authored-by: Dirk Brink <hello@dirk.tech>
Co-authored-by: a <edward9.lee@gmail.com>
Co-authored-by: Adam <20446095+aadam-10@users.noreply.github.com>
Co-authored-by: Aaron Clark <51387046+aapclark@users.noreply.github.com>
Co-authored-by: Hernán Di Pietro <hernan.di.pietro@gmail.com>
Co-authored-by: André Claro <andre.claro@gmail.com>
Co-authored-by: djb15 <djb15@users.noreply.github.com>
Co-authored-by: Jason Matthyser <jasonmatthyser@gmail.com>
Co-authored-by: Maxwell "ꓘ" Dulin <mdulin2@zagmail.gonzaga.edu>
Jorge Arce-Garro 1 month ago
parent
commit
9f640ad865
66 changed files with 15216 additions and 44 deletions
  1. 5 3
      .github/CODEOWNERS
  2. 1 1
      .golangci.yml
  3. 4 1
      Dockerfile.cli
  4. 27 4
      Tiltfile
  5. 5 0
      aztec/.gitignore
  6. 3 0
      aztec/addresses.json
  7. 335 0
      aztec/contracts/DEPLOYMENT.md
  8. 8 0
      aztec/contracts/Nargo.toml
  9. 5 0
      aztec/contracts/addresses.json
  10. 1017 0
      aztec/contracts/deploy.sh
  11. 214 0
      aztec/contracts/src/artifacts/Wormhole.ts
  12. 632 0
      aztec/contracts/src/main.nr
  13. 537 0
      aztec/contracts/src/structs.nr
  14. 5669 0
      aztec/package-lock.json
  15. 16 0
      aztec/package.json
  16. 4 0
      aztec/packages/deploy/src/addresses.json
  17. 199 0
      aztec/packages/deploy/src/deploy.mjs
  18. 93 0
      aztec/packages/deploy/src/deploy_token.mjs
  19. 172 0
      aztec/packages/deploy/src/derive_guardians.js
  20. 3 0
      aztec/packages/deploy/src/nonce.json
  21. 131 0
      aztec/packages/deploy/src/send-private-message.mjs
  22. 137 0
      aztec/packages/deploy/src/send-public-message.mjs
  23. 3 0
      aztec/packages/deploy/src/token_address.json
  24. 87 0
      aztec/readme.md
  25. 48 0
      aztec/register-contract.mjs
  26. 222 0
      aztec/utils.mjs
  27. 358 0
      aztec/vaa-verification-service.mjs
  28. 3 0
      cspell-custom-words.txt
  29. 78 0
      devnet/aztec-devnet.yaml
  30. 2 1
      devnet/node.yaml
  31. 5 1
      ethereum/Dockerfile
  32. 7 0
      ethereum/contracts/cache/fuzz/failures
  33. 1 0
      ethereum/contracts/cache/test-failures
  34. 28 1
      node/cmd/guardiand/node.go
  35. 4 4
      node/go.mod
  36. 6 5
      node/go.sum
  37. 29 11
      node/pkg/common/chainlock.go
  38. 239 0
      node/pkg/common/pendingmessage.go
  39. 396 0
      node/pkg/common/pendingmessage_test.go
  40. 5 0
      node/pkg/db/db.go
  41. 231 0
      node/pkg/db/notary.go
  42. 105 0
      node/pkg/db/notary_test.go
  43. 2 2
      node/pkg/governor/governor.go
  44. 9 0
      node/pkg/node/node.go
  45. 1 0
      node/pkg/node/node_test.go
  46. 20 2
      node/pkg/node/options.go
  47. 521 0
      node/pkg/notary/notary.go
  48. 489 0
      node/pkg/notary/notary_test.go
  49. 109 6
      node/pkg/processor/processor.go
  50. 135 0
      node/pkg/watchers/aztec/block_fetcher.go
  51. 111 0
      node/pkg/watchers/aztec/block_processor.go
  52. 93 0
      node/pkg/watchers/aztec/config.go
  53. 86 0
      node/pkg/watchers/aztec/factory.go
  54. 121 0
      node/pkg/watchers/aztec/http_client.go
  55. 151 0
      node/pkg/watchers/aztec/l1_verifier.go
  56. 89 0
      node/pkg/watchers/aztec/message_publisher.go
  57. 362 0
      node/pkg/watchers/aztec/observation_manager.go
  58. 216 0
      node/pkg/watchers/aztec/types.go
  59. 50 0
      node/pkg/watchers/aztec/utils.go
  60. 85 0
      node/pkg/watchers/aztec/watcher.go
  61. 459 0
      node/pkg/watchers/aztec/watcher_test.go
  62. 2 2
      node/pkg/watchers/evm/chain_config.go
  63. 43 0
      relayer/aztec/go.mod
  64. 97 0
      relayer/aztec/go.sum
  65. 890 0
      relayer/aztec/relayer.go
  66. 1 0
      sdk/vaa/structs.go

+ 5 - 3
.github/CODEOWNERS

@@ -52,9 +52,6 @@
 
 
 /node/cmd/ @panoel @evan-gray
 /node/cmd/ @panoel @evan-gray
 
 
-## Transfer Verifier standalone tool
-
-/node/cmd/txverifier/ @djb15 @johnsaigle @mdulin2 @pleasew8t
 
 
 ## DB
 ## DB
 
 
@@ -91,6 +88,7 @@
 ## Transfer Verifier
 ## Transfer Verifier
 
 
 /node/pkg/txverifier/ @djb15 @johnsaigle @mdulin2 @pleasew8t
 /node/pkg/txverifier/ @djb15 @johnsaigle @mdulin2 @pleasew8t
+/node/cmd/txverifier/ @djb15 @johnsaigle @mdulin2 @pleasew8t
 
 
 ## Watchers
 ## Watchers
 
 
@@ -100,6 +98,10 @@
 /node/go.mod @bemic @djb15 @johnsaigle @mdulin2 @pleasew8t
 /node/go.mod @bemic @djb15 @johnsaigle @mdulin2 @pleasew8t
 /sdk/go.mod @bemic @djb15 @johnsaigle @mdulin2 @pleasew8t
 /sdk/go.mod @bemic @djb15 @johnsaigle @mdulin2 @pleasew8t
 
 
+## Notary
+
+/node/pkg/notary/ @djb15 @johnsaigle @mdulin2 @pleasew8t
+
 ## Hacks / Tools
 ## Hacks / Tools
 
 
 /node/hack/ @panoel @evan-gray
 /node/hack/ @panoel @evan-gray

+ 1 - 1
.golangci.yml

@@ -177,7 +177,7 @@ linters:
         # All of these lints should eventually be added.
         # All of these lints should eventually be added.
         # They occurred during the migration to v2 and were disabled to make the upgrade easier.
         # They occurred during the migration to v2 and were disabled to make the upgrade easier.
         # https://golangci-lint.run/usage/linters/#staticcheck
         # https://golangci-lint.run/usage/linters/#staticcheck
-        ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022", "-SA9003", "-QF1003", "-QF1006", "-QF1008", "-QF1011", "-S1009", "-ST1017", "-ST1018", "-ST1019", "-ST1023"]
+        ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022", "-QF1003", "-QF1006", "-QF1008", "-QF1011", "-S1009", "-ST1017", "-ST1018", "-ST1019", "-ST1023"]
     unparam:
     unparam:
           # Inspect exported functions.
           # Inspect exported functions.
           #
           #

+ 4 - 1
Dockerfile.cli

@@ -1,6 +1,9 @@
 # syntax=docker.io/docker/dockerfile:1.3@sha256:42399d4635eddd7a9b8a24be879d2f9a930d0ed040a61324cfdf59ef1357b3b2
 # syntax=docker.io/docker/dockerfile:1.3@sha256:42399d4635eddd7a9b8a24be879d2f9a930d0ed040a61324cfdf59ef1357b3b2
 FROM node:22.16-alpine3.22@sha256:41e4389f3d988d2ed55392df4db1420ad048ae53324a8e2b7c6d19508288107e as cli-build
 FROM node:22.16-alpine3.22@sha256:41e4389f3d988d2ed55392df4db1420ad048ae53324a8e2b7c6d19508288107e as cli-build
 
 
+# Install Python and build tools for native modules
+RUN apk add --no-cache python3 make g++
+
 # Copy package.json & package-lock.json by themselves to create a cache layer
 # Copy package.json & package-lock.json by themselves to create a cache layer
 COPY clients/js/package.json clients/js/package-lock.json /clients/js/
 COPY clients/js/package.json clients/js/package-lock.json /clients/js/
 
 
@@ -17,4 +20,4 @@ RUN npm run build
 FROM scratch AS cli-export
 FROM scratch AS cli-export
 
 
 COPY --from=cli-build clients/js/build/main.js clients/js/build/main.js
 COPY --from=cli-build clients/js/build/main.js clients/js/build/main.js
-COPY --from=cli-build clients/js/package.json clients/js/package.json
+COPY --from=cli-build clients/js/package.json clients/js/package.json

+ 27 - 4
Tiltfile

@@ -59,6 +59,7 @@ config.define_bool("near", False, "Enable Near component")
 config.define_bool("sui", False, "Enable Sui component")
 config.define_bool("sui", False, "Enable Sui component")
 config.define_bool("btc", False, "Enable BTC component")
 config.define_bool("btc", False, "Enable BTC component")
 config.define_bool("aptos", False, "Enable Aptos component")
 config.define_bool("aptos", False, "Enable Aptos component")
+config.define_bool("aztec", False, "Enable Aztec component")
 config.define_bool("algorand", False, "Enable Algorand component")
 config.define_bool("algorand", False, "Enable Algorand component")
 config.define_bool("evm2", False, "Enable second Eth component")
 config.define_bool("evm2", False, "Enable second Eth component")
 config.define_bool("solana", False, "Enable Solana component")
 config.define_bool("solana", False, "Enable Solana component")
@@ -85,6 +86,7 @@ ci = cfg.get("ci", False)
 algorand = cfg.get("algorand", ci)
 algorand = cfg.get("algorand", ci)
 near = cfg.get("near", ci)
 near = cfg.get("near", ci)
 aptos = cfg.get("aptos", ci)
 aptos = cfg.get("aptos", ci)
+aztec = cfg.get("aztec", ci)
 sui = cfg.get("sui", ci)
 sui = cfg.get("sui", ci)
 evm2 = cfg.get("evm2", ci)
 evm2 = cfg.get("evm2", ci)
 solana = cfg.get("solana", ci)
 solana = cfg.get("solana", ci)
@@ -159,7 +161,7 @@ def command_with_dlv(argv):
     ] + argv[1:]
     ] + argv[1:]
 
 
 def generate_bootstrap_peers(num_guardians, port_num):
 def generate_bootstrap_peers(num_guardians, port_num):
-    # Improve the chances of the guardians discovering each other in tilt by making them all bootstrap peers. 
+    # Improve the chances of the guardians discovering each other in tilt by making them all bootstrap peers.
     # The devnet guardian uses deterministic P2P peer IDs based on the guardian index. The peer IDs here
     # The devnet guardian uses deterministic P2P peer IDs based on the guardian index. The peer IDs here
     # were generated using `DeterministicP2PPrivKeyByIndex` in `node/pkg/devnet/deterministic_p2p_key.go`.
     # were generated using `DeterministicP2PPrivKeyByIndex` in `node/pkg/devnet/deterministic_p2p_key.go`.
     peer_ids = [
     peer_ids = [
@@ -181,7 +183,7 @@ def generate_bootstrap_peers(num_guardians, port_num):
         "12D3KooW9yvKfP5HgVaLnNaxWywo3pLAEypk7wjUcpgKwLznk5gQ",
         "12D3KooW9yvKfP5HgVaLnNaxWywo3pLAEypk7wjUcpgKwLznk5gQ",
         "12D3KooWRuYVGEsecrJJhZsSoKf1UNdBVYKFCmFLNj9ucZiSQCYj",
         "12D3KooWRuYVGEsecrJJhZsSoKf1UNdBVYKFCmFLNj9ucZiSQCYj",
         "12D3KooWGEcD5sW5osB6LajkHGqiGc3W8eKfYwnJVVqfujkpLWX2",
         "12D3KooWGEcD5sW5osB6LajkHGqiGc3W8eKfYwnJVVqfujkpLWX2",
-        "12D3KooWQYz2inBsgiBoqNtmEn1qeRBr9B8cdishFuBgiARcfMcY" 
+        "12D3KooWQYz2inBsgiBoqNtmEn1qeRBr9B8cdishFuBgiARcfMcY"
     ]
     ]
     bootstrap = ""
     bootstrap = ""
     for idx in range(num_guardians):
     for idx in range(num_guardians):
@@ -216,7 +218,7 @@ def build_node_yaml():
                     bootstrapPeers,
                     bootstrapPeers,
                     "--ccqP2pBootstrap",
                     "--ccqP2pBootstrap",
                     ccqBootstrapPeers,
                     ccqBootstrapPeers,
-                ]            
+                ]
 
 
             if aptos:
             if aptos:
                 container["command"] += [
                 container["command"] += [
@@ -228,6 +230,14 @@ def build_node_yaml():
                     "0xde0036a9600559e295d5f6802ef6f3f802f510366e0c23912b0655d972166017::state::WormholeMessageHandle",
                     "0xde0036a9600559e295d5f6802ef6f3f802f510366e0c23912b0655d972166017::state::WormholeMessageHandle",
                 ]
                 ]
 
 
+            if aztec:
+                container["command"] += [
+                    "--aztecRPC",
+                    "http://aztec-sandbox:8090",
+                    "--aztecContract",
+                    "0x0e61ae3f9f51ae20042f48674e2bf1c19cde5c916ae3a5ed114d84c873cc9a8f",
+                ]
+
             if sui:
             if sui:
                 container["command"] += [
                 container["command"] += [
                     "--suiRPC",
                     "--suiRPC",
@@ -381,6 +391,8 @@ if algorand:
     guardian_resource_deps = guardian_resource_deps + ["algorand"]
     guardian_resource_deps = guardian_resource_deps + ["algorand"]
 if aptos:
 if aptos:
     guardian_resource_deps = guardian_resource_deps + ["aptos"]
     guardian_resource_deps = guardian_resource_deps + ["aptos"]
+if aztec:
+    guardian_resource_deps = guardian_resource_deps + ["aztec-sandbox"]
 if wormchain:
 if wormchain:
     guardian_resource_deps = guardian_resource_deps + ["wormchain", "wormchain-deploy"]
     guardian_resource_deps = guardian_resource_deps + ["wormchain", "wormchain-deploy"]
 if sui:
 if sui:
@@ -658,7 +670,7 @@ if ci_tests:
                     "BOOTSTRAP_PEERS", str(ccqBootstrapPeers)),
                     "BOOTSTRAP_PEERS", str(ccqBootstrapPeers)),
                     "MAX_WORKERS", max_workers))
                     "MAX_WORKERS", max_workers))
     )
     )
-    
+
     # separate resources to parallelize docker builds
     # separate resources to parallelize docker builds
     k8s_resource(
     k8s_resource(
         "sdk-ci-tests",
         "sdk-ci-tests",
@@ -989,6 +1001,17 @@ if aptos:
         trigger_mode = trigger_mode,
         trigger_mode = trigger_mode,
     )
     )
 
 
+if aztec:
+    k8s_yaml_with_ns("devnet/aztec-devnet.yaml")
+    k8s_resource(
+        "aztec-sandbox",
+        port_forwards = [
+            port_forward(8090, name = "RPC [:8090]", host = webHost)
+        ],
+        labels = ["aztec-sandbox"],
+        trigger_mode = trigger_mode,
+    )
+
 def build_query_server_yaml():
 def build_query_server_yaml():
     qs_yaml = read_yaml_stream("devnet/query-server.yaml")
     qs_yaml = read_yaml_stream("devnet/query-server.yaml")
 
 

+ 5 - 0
aztec/.gitignore

@@ -0,0 +1,5 @@
+codegenCache.json
+contracts/codegenCache.json
+.DS_Store
+store
+data

+ 3 - 0
aztec/addresses.json

@@ -0,0 +1,3 @@
+{
+  "token": "0x08239f11b65e1c3034c344d4d6d83b9329bc0e6c72ccc43e8beb2566a84c27a4"
+}

+ 335 - 0
aztec/contracts/DEPLOYMENT.md

@@ -0,0 +1,335 @@
+# Aztec Testnet Deployment Guide
+
+This guide walks through deploying the Wormhole contract and Token contract on the Aztec testnet.
+
+## Prerequisites
+
+- Aztec CLI tools installed
+- Access to Aztec testnet
+- Basic understanding of Aztec wallet operations
+
+## Alternative: Automated Deployment Script
+
+For a fully automated deployment experience, you can use the `deploy.sh` script instead of following this manual guide:
+
+```bash
+# Make the script executable
+chmod +x deploy.sh
+
+# Run the automated deployment wizard
+./deploy.sh
+```
+
+The `deploy.sh` script provides:
+- Interactive configuration wizard
+- Automatic error handling and retry logic
+- Built-in dependency checking
+- Automatic contract address extraction and updates
+- Comprehensive logging and status reporting
+
+## Manual Deployment Steps
+
+If you prefer to deploy manually or need more control over the process, follow the steps below:
+
+## Environment Setup
+
+### 1. Set Environment Variables
+
+```bash
+# Testnet configuration
+export NODE_URL=https://aztec-testnet-fullnode.zkv.xyz
+export SPONSORED_FPC_ADDRESS=0x299f255076aa461e4e94a843f0275303470a6b8ebe7cb44a471c66711151e529
+# FPC address valid as of v2.0.3, get the latest address via % aztec get-canonical-sponsored-fpc-address
+
+# Owner private key (32-byte)
+export OWNER_SK=<contract_owner_private_key>
+```
+
+## Wallet Creation
+
+### 2. Create Wallets
+
+#### 2a. Create Owner Wallet
+
+```bash
+aztec-wallet create-account \
+    -sk $OWNER_SK \
+    --register-only \
+    --node-url $NODE_URL \
+    --alias owner-wallet
+```
+
+**Note**: The owner wallet will be used to deploy the contract. You may want to record this address for your own reference, though it's not required for the deployment process.
+
+#### 2b. Create Receiver Wallet
+
+```bash
+aztec-wallet create-account \
+    --register-only \
+    --node-url $NODE_URL \
+    --alias receiver-wallet
+```
+
+**Important**: The receiver wallet is used as a **fee collector** for Wormhole messages. This wallet will receive all fees paid when publishing cross-chain messages.
+
+#### 2c. Set Receiver Address Variable and Update addresses.json
+
+```bash
+# Extract the receiver address from the wallet creation logs
+export RECEIVER_ADDRESS=<receiver_address_from_step_2b>
+
+# Update the addresses.json file with the receiver address
+jq --arg receiver "$RECEIVER_ADDRESS" '.receiver = $receiver' addresses.json > addresses.json.tmp && mv addresses.json.tmp addresses.json
+```
+
+## FPC Registration
+
+### 3. Register Wallets with FPC
+
+#### 3a. Register Owner Wallet
+
+```bash
+aztec-wallet register-contract \
+    --node-url $NODE_URL \
+    --from owner-wallet \
+    --alias sponsoredfpc \
+    $SPONSORED_FPC_ADDRESS SponsoredFPC \
+    --salt 0
+```
+
+#### 3b. Register Receiver Wallet
+
+```bash
+aztec-wallet register-contract \
+    --node-url $NODE_URL \
+    --from receiver-wallet \
+    --alias sponsoredfpc \
+    $SPONSORED_FPC_ADDRESS SponsoredFPC \
+    --salt 0
+```
+
+## Account Deployment
+
+### 4. Deploy Accounts
+
+> **Note**: You may encounter `Timeout awaiting isMined` errors, but this is normal. Continue with the next step.
+
+#### 4a. Deploy Owner Wallet
+
+```bash
+aztec-wallet deploy-account \
+    --node-url $NODE_URL \
+    --from owner-wallet \
+    --payment method=fpc-sponsored,fpc=contracts:sponsoredfpc
+```
+
+#### 4b. Deploy Receiver Wallet
+
+```bash
+aztec-wallet deploy-account \
+    --node-url $NODE_URL \
+    --from receiver-wallet \
+    --payment method=fpc-sponsored,fpc=contracts:sponsoredfpc
+```
+
+## Contract Deployment
+
+### 5. Deploy Token Contract
+
+```bash
+aztec-wallet deploy \
+    --node-url $NODE_URL \
+    --from accounts:owner-wallet \
+    --payment method=fpc-sponsored,fpc=contracts:sponsoredfpc \
+    --alias token \
+    TokenContract \
+    --args accounts:owner-wallet WormToken WORM 18 --no-wait
+```
+
+**Important**: Wait for the transaction to be mined and capture the token contract address from the logs. You can check the transaction status at [AztecScan](http://aztecscan.xyz/).
+
+### 6. Set Token Contract Address and Update addresses.json
+
+#### 6a. Set Token Contract Address
+
+```bash
+# Set the token contract address (extract from step 5 deployment logs)
+export TOKEN_CONTRACT_ADDRESS=<token_contract_address_from_step_5>
+```
+
+#### 6b. Update addresses.json
+
+```bash
+# Update the addresses.json file with the new token address
+jq --arg token "$TOKEN_CONTRACT_ADDRESS" '.token = $token' addresses.json > addresses.json.tmp && mv addresses.json.tmp addresses.json
+```
+
+### 7. Mint Initial Tokens
+
+#### 7a. Mint Private Tokens
+
+```bash
+aztec-wallet send mint_to_private \
+    --node-url $NODE_URL \
+    --from accounts:owner-wallet \
+    --payment method=fpc-sponsored,fpc=contracts:sponsoredfpc \
+    --contract-address $TOKEN_CONTRACT_ADDRESS \
+    --args accounts:owner-wallet 10000
+```
+
+#### 7b. Mint Public Tokens
+
+```bash
+aztec-wallet send mint_to_public \
+    --node-url $NODE_URL \
+    --from accounts:owner-wallet \
+    --payment method=fpc-sponsored,fpc=contracts:sponsoredfpc \
+    --contract-address $TOKEN_CONTRACT_ADDRESS \
+    --args accounts:owner-wallet 10000
+```
+
+### 8. Compile the Wormhole Contract
+
+**Important**: The Wormhole contract must be compiled before deployment. If you've made any changes to the contract source code, compile it first:
+
+**Note**: The contract now uses `DelayedPublicMutable` storage for addresses, eliminating the need for hardcoded addresses in private functions. The addresses are now properly managed through the contract's storage system with appropriate access controls and delays.
+
+**Important**: The contract includes owner-based access control:
+- Only the contract owner can change addresses
+- Address changes have a 87,000 second (~1 day) delay
+- Ownership can be transferred by the current owner
+
+```bash
+# Compile the contract (v2.0.2+ requires two steps)
+aztec-nargo compile
+aztec-postprocess-contract
+```
+
+### 9. Deploy Wormhole Contract
+
+```bash
+aztec-wallet deploy \
+    --node-url $NODE_URL \
+    --from accounts:owner-wallet \
+    --payment method=fpc-sponsored,fpc=contracts:sponsoredfpc \
+    --alias wormhole \
+    target/wormhole_contracts-Wormhole.json \
+    --args 56 56 accounts:owner-wallet $RECEIVER_ADDRESS $TOKEN_CONTRACT_ADDRESS --no-wait --init init
+```
+
+**Note**: The `init()` function parameters are:
+- `chain_id` (56): Aztec testnet chain ID
+- `evm_chain_id` (56): EVM chain ID mapping  
+- `owner` (accounts:owner-wallet): Contract owner address (has admin privileges)
+- `receiver_address` ($RECEIVER_ADDRESS): Address that receives message fees
+- `token_address` ($TOKEN_CONTRACT_ADDRESS): Token contract for fee payments
+
+**Important**: The owner address is the wallet that deployed the contract and has exclusive rights to:
+- Change the receiver address (with 87,000 second delay)
+- Change the token address (with 87,000 second delay)  
+- Transfer ownership to another address
+
+The receiver address is the fee collector address, not the Wormhole contract address itself.
+
+### 10. Capture Wormhole Contract address and update addresses.json
+
+#### 10a. Set Wormhole Contract Address
+
+```bash
+# Set the wormhole contract address (extract from step 9 deployment logs)
+export WORMHOLE_CONTRACT_ADDRESS=<wormhole_contract_address_from_step_9>
+```
+
+#### 10b. Update addresses.json
+
+```bash
+# Update the addresses.json file with the new wormhole address
+jq --arg wormhole "$WORMHOLE_CONTRACT_ADDRESS" '.wormhole = $wormhole' addresses.json > addresses.json.tmp && mv addresses.json.tmp addresses.json
+```
+
+**Important**: Wait for the transaction to be mined. You can check the transaction status at [AztecScan](http://aztecscan.xyz/).
+
+## Post-Deployment Management
+
+### 11. Owner Functions
+
+After deployment, the following functions can be used:
+
+#### 11a. Check Current Owner
+```bash
+aztec-wallet call get_owner \
+    --node-url $NODE_URL \
+    --from accounts:owner-wallet \
+    --payment method=fpc-sponsored,fpc=contracts:sponsoredfpc \
+    --contract-address $WORMHOLE_CONTRACT_ADDRESS
+```
+
+#### 11b. Check Current Addresses
+```bash
+# Check receiver address
+aztec-wallet call get_receiver_address \
+    --node-url $NODE_URL \
+    --from accounts:owner-wallet \
+    --payment method=fpc-sponsored,fpc=contracts:sponsoredfpc \
+    --contract-address $WORMHOLE_CONTRACT_ADDRESS
+
+# Check token address
+aztec-wallet call get_token_address \
+    --node-url $NODE_URL \
+    --from accounts:owner-wallet \
+    --payment method=fpc-sponsored,fpc=contracts:sponsoredfpc \
+    --contract-address $WORMHOLE_CONTRACT_ADDRESS
+```
+
+#### 11c. Change Addresses (Owner Only)
+```bash
+# Change receiver address (takes effect after 87,000 seconds)
+aztec-wallet send set_receiver_address \
+    --node-url $NODE_URL \
+    --from accounts:owner-wallet \
+    --payment method=fpc-sponsored,fpc=contracts:sponsoredfpc \
+    --contract-address $WORMHOLE_CONTRACT_ADDRESS \
+    --args <new_receiver_address>
+
+# Change token address (takes effect after 87,000 seconds)
+aztec-wallet send set_token_address \
+    --node-url $NODE_URL \
+    --from accounts:owner-wallet \
+    --payment method=fpc-sponsored,fpc=contracts:sponsoredfpc \
+    --contract-address $WORMHOLE_CONTRACT_ADDRESS \
+    --args <new_token_address>
+```
+
+#### 11d. Transfer Ownership
+```bash
+# Transfer ownership to another address
+aztec-wallet send transfer_ownership \
+    --node-url $NODE_URL \
+    --from accounts:owner-wallet \
+    --payment method=fpc-sponsored,fpc=contracts:sponsoredfpc \
+    --contract-address $WORMHOLE_CONTRACT_ADDRESS \
+    --args <new_owner_address>
+```
+
+#### 11e. Check Scheduled Changes
+```bash
+# Check scheduled receiver address change
+aztec-wallet call get_scheduled_receiver_address \
+    --node-url $NODE_URL \
+    --from accounts:owner-wallet \
+    --payment method=fpc-sponsored,fpc=contracts:sponsoredfpc \
+    --contract-address $WORMHOLE_CONTRACT_ADDRESS
+
+# Check scheduled token address change
+aztec-wallet call get_scheduled_token_address \
+    --node-url $NODE_URL \
+    --from accounts:owner-wallet \
+    --payment method=fpc-sponsored,fpc=contracts:sponsoredfpc \
+    --contract-address $WORMHOLE_CONTRACT_ADDRESS
+```
+
+## Troubleshooting
+
+- **Timeout errors**: These are common on testnet and usually resolve themselves
+- **Transaction failures**: Check AztecScan for detailed error messages
+- **Network issues**: Ensure you're connected to the correct testnet node

+ 8 - 0
aztec/contracts/Nargo.toml

@@ -0,0 +1,8 @@
+[package]
+name = "wormhole_contracts"
+type = "contract"
+
+[dependencies]
+aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="v2.0.3", directory="noir-projects/aztec-nr/aztec" }
+keccak256 = { tag = "v0.1.0", git = "https://github.com/noir-lang/keccak256" }
+token = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v2.0.3", directory = "noir-projects/noir-contracts/contracts/app/token_contract"}

+ 5 - 0
aztec/contracts/addresses.json

@@ -0,0 +1,5 @@
+{
+  "receiver": "0x08239f11b65e1c3034c344d4d6d83b9329bc0e6c72ccc43e8beb2566a84c27a4",
+  "token": "0x04cdd48c7e047101c6e23d0284cd1bea6862be369e531098529848678079e191",
+  "wormhole": "0x0e61ae3f9f51ae20042f48674e2bf1c19cde5c916ae3a5ed114d84c873cc9a8f"
+}

+ 1017 - 0
aztec/contracts/deploy.sh

@@ -0,0 +1,1017 @@
+#!/bin/bash
+
+# Aztec Deployment Wizard Script
+# Interactive deployment script for Token and Wormhole contracts on Aztec testnet
+
+set -e  # Exit on any error
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+CYAN='\033[0;36m'
+MAGENTA='\033[0;35m'
+NC='\033[0m' # No Color
+
+# Configuration variables with defaults
+DEFAULT_NODE_URL="https://aztec-testnet-fullnode.zkv.xyz"
+DEFAULT_SPONSORED_FPC_ADDRESS="0x19b5539ca1b104d4c3705de94e4555c9630def411f025e023a13189d0c56f8f22"
+DEFAULT_OWNER_SK="0x0ff5c4c050588f4614255a5a4f800215b473e442ae9984347b3a727c3bb7ca55"
+
+# Actual configuration (will be set by wizard)
+NODE_URL=""
+SPONSORED_FPC_ADDRESS=""
+OWNER_SK=""
+
+# Contract addresses (captured during deployment)
+OWNER_ADDRESS=""
+RECEIVER_ADDRESS=""
+TOKEN_CONTRACT_ADDRESS=""
+WORMHOLE_CONTRACT_ADDRESS=""
+
+# Contract file paths
+WORMHOLE_CONTRACT_SRC="src/main.nr"
+WORMHOLE_CONTRACT_BACKUP="src/main.nr.backup"
+
+# Logging functions
+log() {
+    echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
+}
+
+error() {
+    echo -e "${RED}[ERROR]${NC} $1" >&2
+}
+
+success() {
+    echo -e "${GREEN}[SUCCESS]${NC} $1"
+}
+
+warning() {
+    echo -e "${YELLOW}[WARNING]${NC} $1"
+}
+
+info() {
+    echo -e "${CYAN}[INFO]${NC} $1"
+}
+
+wizard_header() {
+    echo -e "${MAGENTA}"
+    echo "╔══════════════════════════════════════════════════════════╗"
+    echo "║                  AZTEC DEPLOYMENT WIZARD                ║"
+    echo "║              Token & Wormhole Contract Deployer         ║"
+    echo "╚══════════════════════════════════════════════════════════╝"
+    echo -e "${NC}"
+}
+
+# Wizard configuration function
+setup_wizard() {
+    wizard_header
+    
+    echo -e "\n${CYAN}Welcome to the Aztec Deployment Wizard!${NC}\n"
+    
+    echo "This wizard will help you deploy Token and Wormhole contracts on Aztec testnet."
+    echo "You can use default values or customize the configuration."
+    echo ""
+    
+    read -p "Do you want to use default configuration values? (y/n): " use_defaults
+    
+    if [[ $use_defaults =~ ^[Yy]$ ]]; then
+        NODE_URL="$DEFAULT_NODE_URL"
+        SPONSORED_FPC_ADDRESS="$DEFAULT_SPONSORED_FPC_ADDRESS"
+        OWNER_SK="$DEFAULT_OWNER_SK"
+        
+        success "Using default configuration"
+    else
+        echo -e "\n${CYAN}Let's configure your deployment settings:${NC}\n"
+        
+        # Node URL configuration
+        echo -e "${YELLOW}Node URL Configuration:${NC}"
+        echo "Default: $DEFAULT_NODE_URL"
+        read -p "Enter Node URL (press Enter for default): " input_node_url
+        NODE_URL="${input_node_url:-$DEFAULT_NODE_URL}"
+        
+        # Sponsored FPC Address configuration
+        echo -e "\n${YELLOW}Sponsored FPC Address Configuration:${NC}"
+        echo "Default: $DEFAULT_SPONSORED_FPC_ADDRESS"
+        read -p "Enter Sponsored FPC Address (press Enter for default): " input_fpc_address
+        SPONSORED_FPC_ADDRESS="${input_fpc_address:-$DEFAULT_SPONSORED_FPC_ADDRESS}"
+        
+        # Owner Private Key configuration
+        echo -e "\n${YELLOW}Owner Private Key Configuration:${NC}"
+        echo "Default: $DEFAULT_OWNER_SK"
+        read -p "Enter Owner Private Key (press Enter for default): " input_owner_sk
+        OWNER_SK="${input_owner_sk:-$DEFAULT_OWNER_SK}"
+        
+        success "Custom configuration set"
+    fi
+    
+    echo -e "\n${CYAN}Configuration Summary:${NC}"
+    echo "Node URL: $NODE_URL"
+    echo "FPC Address: $SPONSORED_FPC_ADDRESS"
+    echo "Owner SK: ${OWNER_SK:0:10}..."
+    echo ""
+    
+    warning "Important: Default Private Key Usage"
+    if [[ "$OWNER_SK" == "$DEFAULT_OWNER_SK" ]]; then
+        echo -e "${YELLOW}You are using the default owner private key.${NC}"
+        echo ""
+        echo -e "${CYAN}What this means:${NC}"
+        echo "• This private key may have been used before on this network"
+        echo "• If the account is already deployed, you'll see 'Existing nullifier' error"
+        echo "• This is NORMAL and means your account is already ready to use"
+        echo "• The script will detect this and continue successfully"
+        echo ""
+        echo -e "${CYAN}Why this happens:${NC}"
+        echo "• Aztec uses nullifiers to prevent double-spending"
+        echo "• Each account deployment creates a unique nullifier"
+        echo "• Trying to deploy the same account twice triggers this protection"
+        echo "• The error actually confirms your account exists and is secure"
+        echo ""
+    else
+        echo -e "${GREEN}You are using a custom private key.${NC}"
+        echo "• If this is a new key, the account will be deployed fresh"
+        echo "• If you've used this key before, you may see 'Existing nullifier' error"
+        echo "• Either way, the script will handle it correctly"
+        echo ""
+    fi
+    
+    read -p "Press Enter to continue with deployment..."
+}
+
+# Check dependencies
+check_dependencies() {
+    log "Checking dependencies..."
+    
+    local missing_deps=()
+    
+    if ! command -v aztec-wallet &> /dev/null; then
+        missing_deps+=("aztec-wallet")
+    fi
+    
+    if ! command -v aztec-nargo &> /dev/null; then
+        missing_deps+=("aztec-nargo")
+    fi
+    
+    if ! command -v aztec &> /dev/null; then
+        missing_deps+=("aztec")
+    fi
+    
+    if [ ${#missing_deps[@]} -ne 0 ]; then
+        error "Missing dependencies: ${missing_deps[*]}"
+        error "Please install Aztec CLI tools before continuing."
+        info "Installation instructions:"
+        info "1. Install Aztec CLI: https://docs.aztec.network/getting_started"
+        info "2. Install Aztec Nargo: https://docs.aztec.network/getting_started"
+        exit 1
+    fi
+    
+    success "All dependencies are installed"
+    
+    # Display versions for debugging
+    info "Dependency versions:"
+    info "- aztec-wallet: $(aztec-wallet --version 2>/dev/null || echo 'version unknown')"
+    info "- aztec-nargo: $(aztec-nargo --version 2>/dev/null || echo 'version unknown')"
+    info "- aztec: $(aztec --version 2>/dev/null || echo 'version unknown')"
+}
+
+# Extract transaction ID from output
+extract_transaction_id() {
+    local output="$1"
+    echo "$output" | grep "Transaction hash:" | head -1 | sed 's/Transaction hash: //' | tr -d ' '
+}
+
+# Extract address using a flexible pattern
+extract_address() {
+    local output="$1"
+    local pattern="${2:-0x[a-fA-F0-9]{64}}"
+    echo "$output" | grep -o "$pattern" | head -1
+}
+
+# Check and handle stale transactions
+check_and_handle_stale_transaction() {
+    local tx_id="$1"
+    local description="$2"
+    
+    warning "Transaction may be stale: $tx_id"
+    info "You can check transaction status at: http://aztecscan.xyz/tx/$tx_id"
+    
+    echo ""
+    echo "What would you like to do?"
+    echo "1. Retry deployment (recommended)"
+    echo "2. Wait and continue (if you think transaction will complete)"
+    echo "3. Skip this step (not recommended)"
+    
+    local choice
+    read -p "Choose option (1/2/3) [default: 1]: " choice
+    choice=${choice:-1}
+    
+    case $choice in
+        1)
+            info "Will retry deployment"
+            return 0  # Retry
+            ;;
+        2)
+            info "Continuing with potentially stale transaction"
+            return 1  # Continue
+            ;;
+        3)
+            warning "Skipping deployment step"
+            return 2  # Skip
+            ;;
+        *)
+            info "Invalid choice, defaulting to retry"
+            return 0  # Retry
+            ;;
+    esac
+}
+
+# Wait for transaction to be mined with retry logic
+wait_for_transaction() {
+    local description="$1"
+    
+    info "Transaction submitted for $description"
+    info "Aztec transactions are processed automatically - continuing with deployment"
+    info "You can monitor all transactions at: http://aztecscan.xyz/"
+    
+    # Short pause to allow transaction to propagate
+    sleep 5
+}
+
+# Execute command with retry logic and specific error handling
+execute_with_retry() {
+    local description="$1"
+    shift
+    local max_retries=3
+    local retry=0
+    
+    while [ $retry -lt $max_retries ]; do
+        if [ $retry -gt 0 ]; then
+            warning "Retrying $description (attempt $((retry + 1))/$max_retries)..."
+            wait_for_transaction "$description"
+        fi
+        
+        log "Executing: $description"
+        
+        # Capture both stdout and stderr
+        local output
+        local exit_code=0
+        output=$("$@" 2>&1) || exit_code=$?
+        
+        if [ $exit_code -eq 0 ]; then
+            # Check if output indicates we need to wait for mining
+            if echo "$output" | grep -q "Waiting for account contract deployment"; then
+                success "$description submitted successfully"
+                info "Transaction is being mined - this may take several minutes"
+                wait_for_transaction "$description"
+            else
+                success "$description completed successfully"
+            fi
+            echo "$output"
+            return 0
+        else
+            # Check for specific error patterns
+            if echo "$output" | grep -q "Existing nullifier"; then
+                warning "Account already deployed (existing nullifier error)"
+                success "$description completed (account already exists)"
+                echo "$output"
+                return 0
+                
+            elif echo "$output" | grep -q "Timeout awaiting isMined"; then
+                local tx_id
+                tx_id=$(extract_transaction_id "$output")
+                
+                warning "Transaction timed out waiting for mining"
+                
+                local retry_decision
+                retry_decision=$(check_and_handle_stale_transaction "$tx_id" "$description")
+                
+                case $? in
+                    0)  # Retry
+                        warning "Retrying deployment..."
+                        retry=$((retry + 1))
+                        continue
+                        ;;
+                    1)  # Wait and continue
+                        info "Continuing with timed-out transaction"
+                        echo "$output"
+                        return 0
+                        ;;
+                    2)  # Skip step
+                        warning "Skipping $description"
+                        return 0
+                        ;;
+                esac
+            fi
+            
+            error "$description failed"
+            retry=$((retry + 1))
+            
+            if [ $retry -lt $max_retries ]; then
+                warning "Command failed, waiting before retry..."
+                sleep 30
+            else
+                error "Output from failed command:"
+                echo "$output"
+            fi
+        fi
+    done
+    
+    error "$description failed after $max_retries attempts"
+    return 1
+}
+
+# Extract address from command output
+extract_address_from_output() {
+    local output="$1"
+    # Extract the address from "Address: 0x..." line
+    echo "$output" | grep "^Address:" | sed 's/Address:[[:space:]]*//' | tr -d ' '
+}
+
+# Execute command that depends on previous deployments being mined
+execute_with_dependency_retry() {
+    local description="$1"
+    shift
+    local max_retries=5  # More retries for dependency-related commands
+    local retry=0
+    
+    while [ $retry -lt $max_retries ]; do
+        if [ $retry -gt 0 ]; then
+            warning "Retrying $description - waiting for dependencies (attempt $((retry + 1))/$max_retries)..."
+            info "Previous deployments may still be mining or transactions may be stale"
+            
+            # Longer wait for dependency issues, increasing with each retry
+            local wait_time=$((60 + (retry * 30)))
+            info "Waiting $wait_time seconds for blockchain state to sync..."
+            sleep $wait_time
+        fi
+        
+        log "Executing: $description"
+        
+        # Capture both stdout and stderr
+        local output
+        local exit_code=0
+        output=$("$@" 2>&1) || exit_code=$?
+        
+        if [ $exit_code -eq 0 ]; then
+            # Extract contract address if this is a deployment
+            if [[ "$description" == *"deployment"* ]]; then
+                local contract_address
+                contract_address=$(echo "$output" | grep "Contract deployed at" | sed 's/Contract deployed at //' | tr -d ' ')
+                
+                if [ -n "$contract_address" ]; then
+                    if [[ "$description" == *"Token"* ]]; then
+                        TOKEN_CONTRACT_ADDRESS="$contract_address"
+                        success "Token contract deployed at: $TOKEN_CONTRACT_ADDRESS"
+                    elif [[ "$description" == *"Wormhole"* ]]; then
+                        WORMHOLE_CONTRACT_ADDRESS="$contract_address"
+                        success "Wormhole contract deployed at: $WORMHOLE_CONTRACT_ADDRESS"
+                    fi
+                fi
+            fi
+            
+            # Check if we got a transaction hash and need to wait for mining
+            local tx_hash
+            tx_hash=$(echo "$output" | grep "Deploy tx hash:" | head -1 | sed 's/Deploy tx hash:[[:space:]]*//' | tr -d ' ')
+            
+            if [ -n "$tx_hash" ]; then
+                info "Transaction submitted: $tx_hash"
+                info "Check status at: http://aztecscan.xyz/tx/$tx_hash"
+                
+                # Check if transaction was already mined in the output
+                if echo "$output" | grep -q "Transaction has been mined"; then
+                    if echo "$output" | grep -q "Status: success"; then
+                        success "$description completed successfully"
+                    else
+                        warning "$description transaction mined but check status"
+                    fi
+                else
+                    info "Transaction deployment initiated - continuing with next step"
+                fi
+            else
+                success "$description completed successfully"
+            fi
+            
+            echo "$output"
+            return 0
+        else
+            # Check for errors that indicate we need to wait for previous deployments
+            if echo "$output" | grep -qi "contract.*not.*found\|contract.*not.*deployed\|account.*not.*found"; then
+                warning "Dependency not ready - previous deployment may still be mining"
+                retry=$((retry + 1))
+                continue
+            elif echo "$output" | grep -qi "Cannot find the leaf for nullifier\|nullifier.*not.*found"; then
+                warning "Nullifier/state synchronization issue - blockchain state may not be ready"
+                info "This often happens when previous transactions are stale or still processing"
+                
+                if [ $retry -ge 2 ]; then
+                    warning "Multiple failures suggest previous transactions may be stale"
+                    info "Consider checking transaction status at http://aztecscan.xyz/"
+                    echo ""
+                    echo "Options:"
+                    echo "1. Continue retrying (may work if transactions eventually mine)"
+                    echo "2. Exit and manually check/retry stale transactions"
+                    echo "3. Skip this step (not recommended)"
+                    
+                    local choice
+                    read -p "Choose option (1/2/3) [default: 1]: " choice
+                    choice=${choice:-1}
+                    
+                    case $choice in
+                        2)
+                            info "Exiting for manual intervention"
+                            exit 1
+                            ;;
+                        3)
+                            warning "Skipping $description"
+                            return 0
+                            ;;
+                    esac
+                fi
+                
+                retry=$((retry + 1))
+                continue
+            elif echo "$output" | grep -qi "simulation.*failed\|transaction.*simulation.*error"; then
+                warning "Transaction simulation failed - dependencies may not be ready"
+                retry=$((retry + 1))
+                continue
+            fi
+            
+            error "$description failed"
+            echo "$output"
+            retry=$((retry + 1))
+            
+            if [ $retry -lt $max_retries ]; then
+                warning "Command failed, waiting before retry..."
+                sleep 30
+            fi
+        fi
+    done
+    
+    error "$description failed after $max_retries attempts"
+    error "This may indicate that previous deployments are stale or not properly synced"
+    info "Check http://aztecscan.xyz/ for deployment status"
+    info "You may need to restart deployment with fresh transactions"
+    return 1
+}
+
+# Set environment variables
+setup_environment() {
+    log "Setting up environment variables..."
+    export NODE_URL="$NODE_URL"
+    export SPONSORED_FPC_ADDRESS="$SPONSORED_FPC_ADDRESS"
+    export OWNER_SK="$OWNER_SK"
+    success "Environment variables set"
+}
+
+# Step 2: Create wallets
+create_wallets() {
+    log "Creating wallets..."
+    
+    log "Creating owner wallet..."
+    local owner_output
+    owner_output=$(aztec-wallet create-account \
+        -sk "$OWNER_SK" \
+        --register-only \
+        --node-url "$NODE_URL" \
+        --alias owner-wallet 2>&1)
+    
+    # Extract owner address from output
+    OWNER_ADDRESS=$(extract_address_from_output "$owner_output")
+    
+    if [ -n "$OWNER_ADDRESS" ]; then
+        success "Owner wallet created. Address: $OWNER_ADDRESS"
+    else
+        error "Could not extract owner address from output"
+        echo "Owner wallet creation output:"
+        echo "$owner_output"
+        read -p "Please enter the owner address: " OWNER_ADDRESS
+    fi
+    
+    log "Creating receiver wallet..."
+    local receiver_output
+    receiver_output=$(aztec-wallet create-account \
+        --register-only \
+        --node-url "$NODE_URL" \
+        --alias receiver-wallet 2>&1)
+    
+    # Extract receiver address from output  
+    local temp_receiver_address
+    temp_receiver_address=$(extract_address_from_output "$receiver_output")
+    
+    if [ -n "$temp_receiver_address" ]; then
+        RECEIVER_ADDRESS="$temp_receiver_address"
+        success "Receiver wallet created. Address: $RECEIVER_ADDRESS"
+    else
+        error "Could not extract receiver address from output"
+        echo "Receiver wallet creation output:"
+        echo "$receiver_output"
+        read -p "Please enter the receiver address: " RECEIVER_ADDRESS
+    fi
+}
+
+# Step 3: Register accounts with FPC
+register_with_fpc() {
+    log "Registering wallets with FPC..."
+    
+    execute_with_retry "owner wallet FPC registration" \
+        aztec-wallet register-contract \
+        --node-url "$NODE_URL" \
+        --from owner-wallet \
+        --alias sponsoredfpc \
+        "$SPONSORED_FPC_ADDRESS" SponsoredFPC \
+        --salt 0
+    
+    execute_with_retry "receiver wallet FPC registration" \
+        aztec-wallet register-contract \
+        --node-url "$NODE_URL" \
+        --from receiver-wallet \
+        --alias sponsoredfpc \
+        "$SPONSORED_FPC_ADDRESS" SponsoredFPC \
+        --salt 0
+}
+
+# Step 4: Deploy accounts
+deploy_accounts() {
+    log "Deploying accounts..."
+    warning "Note: 'Existing nullifier' errors indicate accounts are already deployed"
+    info "This is expected when reusing the same private keys and will be handled automatically"
+    info "Account deployment now uses default payment method to avoid array size constraints"
+    
+    log "Deploying owner account..."
+    # Try deployment without FPC first to avoid array size issues
+    execute_with_retry "owner wallet deployment" \
+        aztec-wallet deploy-account \
+        --node-url "$NODE_URL" \
+        --from owner-wallet
+    
+    log "Deploying receiver account..."
+    # Try deployment without FPC first to avoid array size issues
+    execute_with_retry "receiver wallet deployment" \
+        aztec-wallet deploy-account \
+        --node-url "$NODE_URL" \
+        --from receiver-wallet
+    
+    success "Account deployment process completed"
+    info "Owner Address: $OWNER_ADDRESS"
+    info "Receiver Address: $RECEIVER_ADDRESS"
+    info "Both accounts are now ready for contract deployments"
+}
+
+# Step 5: Deploy Token contract
+deploy_token_contract() {
+    log "Deploying Token contract..."
+    
+    execute_with_dependency_retry "Token contract deployment" \
+        aztec-wallet deploy \
+        --node-url "$NODE_URL" \
+        --from accounts:owner-wallet \
+        --payment method=fpc-sponsored,fpc=contracts:sponsoredfpc \
+        --alias token \
+        TokenContract \
+        --args accounts:owner-wallet WormToken WORM 18 --no-wait
+}
+
+# Step 6: Mint tokens
+mint_tokens() {
+    log "Minting tokens..."
+    
+    info "Note: Minting may fail initially if previous deployments are still being processed"
+    info "The script will automatically retry if needed"
+    
+    execute_with_dependency_retry "private token minting" \
+        aztec-wallet send mint_to_private \
+        --node-url "$NODE_URL" \
+        --from accounts:owner-wallet \
+        --payment method=fpc-sponsored,fpc=contracts:sponsoredfpc \
+        --contract-address "$TOKEN_CONTRACT_ADDRESS" \
+        --args accounts:owner-wallet accounts:owner-wallet 10000
+    
+    execute_with_dependency_retry "public token minting" \
+        aztec-wallet send mint_to_public \
+        --node-url "$NODE_URL" \
+        --from accounts:owner-wallet \
+        --payment method=fpc-sponsored,fpc=contracts:sponsoredfpc \
+        --contract-address "$TOKEN_CONTRACT_ADDRESS" \
+        --args accounts:owner-wallet 10000
+}
+
+# Backup original contract file
+backup_contract() {
+    if [ ! -f "$WORMHOLE_CONTRACT_BACKUP" ] && [ -f "$WORMHOLE_CONTRACT_SRC" ]; then
+        log "Creating backup of original contract..."
+        cp "$WORMHOLE_CONTRACT_SRC" "$WORMHOLE_CONTRACT_BACKUP"
+        success "Contract backed up to $WORMHOLE_CONTRACT_BACKUP"
+    fi
+}
+
+# Restore original contract from backup
+restore_contract() {
+    if [ -f "$WORMHOLE_CONTRACT_BACKUP" ]; then
+        log "Restoring original contract from backup..."
+        cp "$WORMHOLE_CONTRACT_BACKUP" "$WORMHOLE_CONTRACT_SRC"
+        success "Original contract restored"
+    fi
+}
+
+# Modify Wormhole contract with captured addresses
+modify_wormhole_contract() {
+    log "Modifying Wormhole contract with deployment addresses..."
+    
+    if [ ! -f "$WORMHOLE_CONTRACT_SRC" ]; then
+        error "Wormhole contract source file not found: $WORMHOLE_CONTRACT_SRC"
+        info "Expected file structure:"
+        info "  src/"
+        info "  └── main.nr (Wormhole contract)"
+        exit 1
+    fi
+    
+    if [ -z "$RECEIVER_ADDRESS" ] || [ -z "$TOKEN_CONTRACT_ADDRESS" ]; then
+        error "Missing required addresses for contract modification"
+        error "Receiver Address: $RECEIVER_ADDRESS"
+        error "Token Contract Address: $TOKEN_CONTRACT_ADDRESS"
+        exit 1
+    fi
+    
+    # Create backup first
+    backup_contract
+    
+    info "Updating hardcoded addresses in contract..."
+    info "Receiver Address: $RECEIVER_ADDRESS"
+    info "Token Contract Address: $TOKEN_CONTRACT_ADDRESS"
+    
+    # Find and replace the hardcoded addresses in the publish_message_in_private function
+    # The current hardcoded addresses that need replacement:
+    # receiver_address: 0x2f73c9b19222c2a7931c6cba01eedbbabb51e01b405fe4e0cabe0de91c275d0e
+    # token_address: 0x13babb369e8c237a78ed507fe7cc44336a5178ffd02312a979c1fa0921f02a06
+    
+    local temp_file=$(mktemp)
+    
+    # Use sed to replace the hardcoded addresses
+    sed "s/inner: 0x2f73c9b19222c2a7931c6cba01eedbbabb51e01b405fe4e0cabe0de91c275d0e/inner: $RECEIVER_ADDRESS/g" "$WORMHOLE_CONTRACT_SRC" > "$temp_file" && \
+    sed "s/inner: 0x13babb369e8c237a78ed507fe7cc44336a5178ffd02312a979c1fa0921f02a06/inner: $TOKEN_CONTRACT_ADDRESS/g" "$temp_file" > "$WORMHOLE_CONTRACT_SRC"
+    
+    rm -f "$temp_file"
+    
+    # Verify the changes were made
+    if grep -q "$RECEIVER_ADDRESS" "$WORMHOLE_CONTRACT_SRC" && grep -q "$TOKEN_CONTRACT_ADDRESS" "$WORMHOLE_CONTRACT_SRC"; then
+        success "Contract addresses updated successfully"
+        info "Receiver address updated in contract"
+        info "Token contract address updated in contract"
+    else
+        error "Failed to update contract addresses"
+        warning "Restoring original contract..."
+        restore_contract
+        exit 1
+    fi
+}
+
+# Prepare Wormhole contract
+prepare_wormhole_contract() {
+    log "Preparing Wormhole contract..."
+    
+    # Modify the contract with the captured addresses
+    modify_wormhole_contract
+    
+    # Compile the contract with retry logic
+    compile_contract_with_retry
+    
+    # Run tests with retry logic
+    test_contract_with_retry
+    
+    # Verify the compiled contract exists
+    if [ ! -f "target/wormhole_contracts-Wormhole.json" ]; then
+        error "Compiled Wormhole contract not found at target/wormhole_contracts-Wormhole.json"
+        info "Expected compilation output location: target/wormhole_contracts-Wormhole.json"
+        warning "Restoring original contract..."
+        restore_contract
+        exit 1
+    fi
+    
+    success "Wormhole contract prepared successfully"
+}
+
+# Compile contract with retry logic
+compile_contract_with_retry() {
+    local max_attempts=3
+    local attempt=1
+    
+    while [ $attempt -le $max_attempts ]; do
+        log "Compiling Wormhole contract (attempt $attempt/$max_attempts)..."
+        
+        local compile_output
+        local compile_exit_code=0
+        compile_output=$(aztec-nargo compile 2>&1) || compile_exit_code=$?
+        
+        if [ $compile_exit_code -eq 0 ]; then
+            # v2.0.2+ requires postprocessing step
+            compile_output=$(aztec-postprocess-contract 2>&1) || compile_exit_code=$?
+        fi
+        
+        if [ $compile_exit_code -eq 0 ]; then
+            success "Contract compilation completed successfully"
+            echo "$compile_output"
+            return 0
+        else
+            error "Contract compilation failed (attempt $attempt/$max_attempts)"
+            echo "Compilation output:"
+            echo "$compile_output"
+            echo ""
+            
+            if [ $attempt -lt $max_attempts ]; then
+                warning "Compilation failed - this might happen if:"
+                echo "• The contract file is being modified while the script runs"
+                echo "• There are temporary file system issues"
+                echo "• The contract has syntax errors that were just introduced"
+                echo ""
+                
+                echo "Options:"
+                echo "1. Retry compilation (recommended if file was being edited)"
+                echo "2. Exit and fix compilation errors manually" 
+                echo "3. Skip compilation and try with existing artifacts (risky)"
+                
+                local choice
+                read -p "Choose option (1/2/3) [default: 1]: " choice
+                choice=${choice:-1}
+                
+                case $choice in
+                    1)
+                        info "Retrying compilation..."
+                        if [ $attempt -eq 1 ]; then
+                            info "Waiting 10 seconds in case files are still being modified..."
+                            sleep 10
+                        else
+                            info "Waiting 5 seconds before retry..."
+                            sleep 5
+                        fi
+                        attempt=$((attempt + 1))
+                        continue
+                        ;;
+                    2)
+                        info "Exiting for manual compilation fix"
+                        warning "Restoring original contract..."
+                        restore_contract
+                        exit 1
+                        ;;
+                    3)
+                        warning "Skipping compilation - using existing artifacts"
+                        warning "This may cause deployment failures if artifacts are outdated"
+                        return 0
+                        ;;
+                    *)
+                        info "Invalid choice, defaulting to retry"
+                        attempt=$((attempt + 1))
+                        continue
+                        ;;
+                esac
+            else
+                error "Compilation failed after $max_attempts attempts"
+                warning "Restoring original contract..."
+                restore_contract
+                
+                echo ""
+                echo "Compilation has failed multiple times. This usually means:"
+                echo "• There are syntax errors in the contract"
+                echo "• Missing dependencies or incorrect paths"
+                echo "• The contract modifications introduced errors"
+                echo ""
+                echo "Please check the compilation errors above and fix them manually."
+                echo "You can then run the script again or compile manually with:"
+                echo "  aztec-nargo compile"
+                echo "  aztec-postprocess-contract"
+                exit 1
+            fi
+        fi
+    done
+}
+
+# Test contract with retry logic  
+test_contract_with_retry() {
+    local max_attempts=2
+    local attempt=1
+    
+    while [ $attempt -le $max_attempts ]; do
+        log "Running contract tests (attempt $attempt/$max_attempts)..."
+        
+        local test_output
+        local test_exit_code=0
+        test_output=$(aztec test --silence-warnings 2>&1) || test_exit_code=$?
+        
+        if [ $test_exit_code -eq 0 ]; then
+            success "Contract tests passed"
+            echo "$test_output"
+            return 0
+        else
+            warning "Contract tests failed (attempt $attempt/$max_attempts)"
+            echo "Test output:"
+            echo "$test_output"
+            echo ""
+            
+            if [ $attempt -lt $max_attempts ]; then
+                warning "Tests failed - this might happen if:"
+                echo "• Contract was recently compiled and test cache is stale"
+                echo "• Temporary testing environment issues"
+                echo "• Tests depend on external state that's not ready"
+                echo ""
+                
+                echo "Options:"
+                echo "1. Retry tests (recommended for transient issues)"
+                echo "2. Continue with deployment despite test failures (risky)"
+                echo "3. Exit and fix test failures manually"
+                
+                local choice
+                read -p "Choose option (1/2/3) [default: 1]: " choice
+                choice=${choice:-1}
+                
+                case $choice in
+                    1)
+                        info "Retrying tests..."
+                        info "Waiting 5 seconds for test environment to stabilize..."
+                        sleep 5
+                        attempt=$((attempt + 1))
+                        continue
+                        ;;
+                    2)
+                        warning "Continuing with deployment despite test failures"
+                        warning "Deployment may fail if tests revealed actual issues"
+                        return 0
+                        ;;
+                    3)
+                        info "Exiting for manual test fix"
+                        warning "Restoring original contract..."
+                        restore_contract
+                        exit 1
+                        ;;
+                    *)
+                        info "Invalid choice, defaulting to retry"
+                        attempt=$((attempt + 1))
+                        continue
+                        ;;
+                esac
+            else
+                warning "Tests failed after $max_attempts attempts"
+                
+                echo ""
+                echo "Do you want to continue with deployment despite test failures?"
+                echo "This is risky but sometimes tests fail due to environment issues"
+                echo "while the actual contract functionality is correct."
+                
+                local continue_deploy
+                read -p "Continue with deployment? (y/n) [default: n]: " continue_deploy
+                continue_deploy=${continue_deploy:-n}
+                
+                if [[ $continue_deploy =~ ^[Yy]$ ]]; then
+                    warning "Continuing with deployment despite test failures"
+                    warning "Monitor deployment carefully for any issues"
+                    return 0
+                else
+                    info "Deployment cancelled by user"
+                    warning "Restoring original contract..."
+                    restore_contract
+                    exit 1
+                fi
+            fi
+        fi
+    done
+}
+
+# Step 7: Deploy Wormhole contract
+deploy_wormhole_contract() {
+    log "Deploying Wormhole contract..."
+    
+    if [ -z "$RECEIVER_ADDRESS" ] || [ -z "$TOKEN_CONTRACT_ADDRESS" ]; then
+        error "Missing required addresses for Wormhole deployment"
+        error "Receiver Address: $RECEIVER_ADDRESS"
+        error "Token Contract Address: $TOKEN_CONTRACT_ADDRESS"
+        exit 1
+    fi
+    
+    execute_with_dependency_retry "Wormhole contract deployment" \
+        aztec-wallet deploy \
+        --node-url "$NODE_URL" \
+        --from accounts:owner-wallet \
+        --payment method=fpc-sponsored,fpc=contracts:sponsoredfpc \
+        --alias wormhole \
+        target/wormhole_contracts-Wormhole.json \
+        --args 56 56 "$RECEIVER_ADDRESS" "$TOKEN_CONTRACT_ADDRESS" --no-wait --init init
+    
+    # Restore original contract after deployment attempt
+    log "Restoring original contract file..."
+    restore_contract
+}
+
+# Create environment file for verification service
+create_env_file() {
+    local env_file=".env"
+    
+    log "Creating environment file for verification service..."
+    
+    # Create or overwrite .env file
+    cat > "$env_file" << EOF
+# Aztec Deployment Configuration
+# Generated on $(date)
+
+# Network Configuration
+NODE_URL=$NODE_URL
+PRIVATE_KEY=$OWNER_SK
+SALT=0x0000000000000000000000000000000000000000000000000000000000000000
+
+# Contract Addresses
+OWNER_ADDRESS=$OWNER_ADDRESS
+RECEIVER_ADDRESS=$RECEIVER_ADDRESS
+TOKEN_CONTRACT_ADDRESS=$TOKEN_CONTRACT_ADDRESS
+WORMHOLE_CONTRACT_ADDRESS=$WORMHOLE_CONTRACT_ADDRESS
+
+# Service Configuration
+PORT=3000
+NETWORK=testnet
+EOF
+    
+    success "Environment file created: $env_file"
+    info "The verification service will now use these deployed contract addresses"
+}
+
+# Export environment variables for immediate use
+export_environment_variables() {
+    log "Exporting environment variables for verification service..."
+    
+    export NODE_URL="$NODE_URL"
+    export PRIVATE_KEY="$OWNER_SK"
+    export CONTRACT_ADDRESS="$WORMHOLE_CONTRACT_ADDRESS"
+    export SALT="0x0000000000000000000000000000000000000000000000000000000000000000"
+    export OWNER_ADDRESS="$OWNER_ADDRESS"
+    export RECEIVER_ADDRESS="$RECEIVER_ADDRESS"
+    export TOKEN_CONTRACT_ADDRESS="$TOKEN_CONTRACT_ADDRESS"
+    export WORMHOLE_CONTRACT_ADDRESS="$WORMHOLE_CONTRACT_ADDRESS"
+    export PORT="3000"
+    export NETWORK="testnet"
+    
+    success "Environment variables exported for current session"
+}
+
+# Cleanup function
+cleanup_on_exit() {
+    warning "Script interrupted - cleaning up..."
+    if [ -f "$WORMHOLE_CONTRACT_BACKUP" ]; then
+        log "Restoring original contract..."
+        restore_contract
+        rm -f "$WORMHOLE_CONTRACT_BACKUP"
+    fi
+    exit 1
+}
+
+# Main execution function
+main() {
+    setup_wizard
+    check_dependencies
+    setup_environment
+    
+    log "Starting deployment process..."
+    
+    create_wallets
+    register_with_fpc
+    deploy_accounts
+    deploy_token_contract
+    
+    # Add a pause to let the token contract deploy before minting
+    info "Waiting for token contract to be ready before minting..."
+    sleep 30
+    
+    mint_tokens
+    prepare_wormhole_contract
+    deploy_wormhole_contract
+    
+    success "🎉 Deployment completed successfully!"
+    echo -e "\n${CYAN}Final Deployment Summary:${NC}"
+    echo "├─ Node URL: $NODE_URL"
+    echo "├─ Owner Wallet: $OWNER_ADDRESS"
+    echo "├─ Receiver Wallet: $RECEIVER_ADDRESS"
+    echo "├─ Token Contract: $TOKEN_CONTRACT_ADDRESS"
+    
+    if [ -n "$WORMHOLE_CONTRACT_ADDRESS" ]; then
+        echo "├─ Wormhole Contract: $WORMHOLE_CONTRACT_ADDRESS"
+    else
+        echo "├─ Wormhole Contract: Deployed (check aztecscan for address)"
+    fi
+    
+    echo "└─ Transaction Explorer: http://aztecscan.xyz/"
+    echo ""
+    success "All contracts deployed and ready for use!"
+    info "Note: Contract addresses may take a few minutes to be fully propagated"
+    
+    # Create environment file and export variables
+    create_env_file
+    export_environment_variables
+    
+    # Clean up backup file
+    if [ -f "$WORMHOLE_CONTRACT_BACKUP" ]; then
+        rm -f "$WORMHOLE_CONTRACT_BACKUP"
+        info "Cleanup completed"
+    fi
+}
+
+# Handle script interruption and cleanup
+trap cleanup_on_exit SIGINT SIGTERM
+
+# Run the script
+main "$@"

File diff suppressed because it is too large
+ 214 - 0
aztec/contracts/src/artifacts/Wormhole.ts


+ 632 - 0
aztec/contracts/src/main.nr

@@ -0,0 +1,632 @@
+use dep::aztec::macros::aztec;
+mod structs;
+
+/*
+Aztec Wormhole contract for the Wormhole bridge on Aztec testnet.
+
+This contract is responsible for:
+- Parsing and verifying Wormhole VAA messages
+- Managing guardian sets and their expiration
+- Publishing messages to the Wormhole network
+- Receiving messages from the Wormhole network
+
+To deploy this contract, follow the instructions in the deploy.md file, or use the deploy.sh script.
+
+TODO: Fix hardcoded addresses in private functions.
+*/
+
+#[aztec]
+pub contract Wormhole {
+    use crate::structs::{Guardian, Provider, Signature, WormholeStorage};
+    use dep::keccak256::keccak256;
+
+    use dep::aztec::{
+        macros::{functions::{initializer, internal, private, public, utility}, storage::storage},
+        state_vars::{map::Map, public_mutable::PublicMutable, delayed_public_mutable::DelayedPublicMutable},
+        protocol_types::{
+            address::AztecAddress,
+            traits::ToField,
+        },
+    };
+
+    use dep::token::Token;
+
+    // ============================================================================
+    // STORAGE STRUCTURE
+    // ============================================================================
+
+    #[storage]
+    struct Storage<Context> {
+        state: PublicMutable<WormholeStorage, Context>,
+        sequences: Map<AztecAddress, PublicMutable<u64, Context>, Context>,
+        owner: PublicMutable<AztecAddress, Context>,
+        receiver_address: DelayedPublicMutable<AztecAddress, 87000, Context>, // Delay slightly over 1 day.
+        token_address: DelayedPublicMutable<AztecAddress, 87000, Context>, // Delay slightly over 1 day.
+        guardian_1: Map<u32, PublicMutable<Guardian, Context>, Context>,
+        guardian_2: Map<u32, PublicMutable<Guardian, Context>, Context>,
+        guardian_3: Map<u32, PublicMutable<Guardian, Context>, Context>,
+        guardian_4: Map<u32, PublicMutable<Guardian, Context>, Context>,
+        guardian_5: Map<u32, PublicMutable<Guardian, Context>, Context>,
+        guardian_6: Map<u32, PublicMutable<Guardian, Context>, Context>,
+        guardian_7: Map<u32, PublicMutable<Guardian, Context>, Context>,
+        guardian_8: Map<u32, PublicMutable<Guardian, Context>, Context>,
+        guardian_9: Map<u32, PublicMutable<Guardian, Context>, Context>,
+        guardian_10: Map<u32, PublicMutable<Guardian, Context>, Context>,
+        guardian_11: Map<u32, PublicMutable<Guardian, Context>, Context>,
+        guardian_12: Map<u32, PublicMutable<Guardian, Context>, Context>,
+        guardian_13: Map<u32, PublicMutable<Guardian, Context>, Context>,
+        guardian_14: Map<u32, PublicMutable<Guardian, Context>, Context>,
+        guardian_15: Map<u32, PublicMutable<Guardian, Context>, Context>,
+        guardian_16: Map<u32, PublicMutable<Guardian, Context>, Context>,
+        guardian_17: Map<u32, PublicMutable<Guardian, Context>, Context>,
+        guardian_18: Map<u32, PublicMutable<Guardian, Context>, Context>,
+        guardian_19: Map<u32, PublicMutable<Guardian, Context>, Context>,
+        current_guardian_set_index: Map<u32, PublicMutable<u64, Context>, Context>,
+    }
+
+    // ============================================================================
+    // CONTRACT INITIALIZATION
+    // ============================================================================
+
+    #[public]
+    #[initializer]
+    fn init(
+        chain_id: u16,
+        evm_chain_id: u16,
+        owner: AztecAddress,
+        receiver_address: AztecAddress,
+        token_address: AztecAddress,
+    ) {
+        let provider: Provider = Provider { chain_id, evm_chain_id };
+        storage.state.write(WormholeStorage::init(provider));
+        storage.owner.write(owner);
+        storage.receiver_address.schedule_value_change(receiver_address);
+        storage.token_address.schedule_value_change(token_address);
+        storage.current_guardian_set_index.at(0).write(18_446_744_073_709_551_615);
+    }
+
+    // ============================================================================
+    // CORE FUNCTIONALITY - MESSAGE PUBLISHING
+    // ============================================================================
+
+    #[public]
+    fn publish_message_in_public(
+        nonce: u64,
+        payloads: [[u8; 31]; 8],
+        message_fee: u128,
+        consistency: u8,
+        from: AztecAddress,
+        token_nonce: Field,
+    ) -> u64 {
+        assert(storage.state.read().message_fee <= message_fee, "insufficient fee");
+
+        let receiver_address = storage.receiver_address.get_current_value();
+        let token_address = storage.token_address.get_current_value();
+
+        // Only transfer tokens if fee is greater than 0
+        if message_fee > 0 {
+            let _ = Token::at(token_address)
+                .transfer_in_public(from, receiver_address, message_fee, token_nonce)
+                .call(&mut context);
+        }
+
+        let sequence = storage.sequences.at(context.msg_sender()).read();
+        storage.sequences.at(context.msg_sender()).write(sequence + 1);
+
+        let msg: [Field; 13] = [
+            context.msg_sender().to_field(),
+            sequence as Field,
+            nonce as Field,
+            consistency as Field,
+            context.timestamp() as Field,
+            Field::from_le_bytes(payloads[0]),
+            Field::from_le_bytes(payloads[1]),
+            Field::from_le_bytes(payloads[2]),
+            Field::from_le_bytes(payloads[3]),
+            Field::from_le_bytes(payloads[4]),
+            Field::from_le_bytes(payloads[5]),
+            Field::from_le_bytes(payloads[6]),
+            Field::from_le_bytes(payloads[7]),
+        ];
+
+        context.emit_public_log(msg);
+        sequence
+    }
+
+    #[private]
+    fn publish_message_in_private(
+        nonce: u64,
+        payloads: [[u8; 31]; 8],
+        message_fee: u128,
+        consistency: u8,
+        from: AztecAddress,
+        token_nonce: Field,
+    ) {
+        assert(0 <= message_fee, "insufficient fee");
+
+        let receiver_address = storage.receiver_address.get_current_value();
+        let token_address = storage.token_address.get_current_value();
+
+        // Only transfer tokens if fee is greater than 0
+        if message_fee > 0 {
+            let _ = Token::at(token_address)
+                .transfer_in_private(from, receiver_address, message_fee, token_nonce)
+                .call(&mut context);
+        }
+
+        Wormhole::at(context.this_address())._publish_message(nonce, payloads, consistency).enqueue(
+            &mut context,
+        );
+    }
+
+    #[public]
+    #[internal]
+    fn _publish_message(nonce: u64, payloads: [[u8; 31]; 8], consistency: u8) -> u64 {
+        let sequence = storage.sequences.at(context.msg_sender()).read();
+        storage.sequences.at(context.msg_sender()).write(sequence + 1);
+
+        let msg: [Field; 13] = [
+            context.msg_sender().to_field(),
+            sequence as Field,
+            nonce as Field,
+            consistency as Field,
+            context.timestamp() as Field,
+            Field::from_le_bytes(payloads[0]),
+            Field::from_le_bytes(payloads[1]),
+            Field::from_le_bytes(payloads[2]),
+            Field::from_le_bytes(payloads[3]),
+            Field::from_le_bytes(payloads[4]),
+            Field::from_le_bytes(payloads[5]),
+            Field::from_le_bytes(payloads[6]),
+            Field::from_le_bytes(payloads[7]),
+        ];
+
+        context.emit_public_log(msg);
+        sequence
+    }
+
+    // ============================================================================
+    // CORE FUNCTIONALITY - VAA VERIFICATION
+    // ============================================================================
+    
+    #[utility]
+    unconstrained fn parse_vaa_unconstrained(
+        bytes: [u8; 2000],
+        actual_length: u32,
+    ) -> (u32, u8, [[u8; 66]; 13], [u8; 1860], u32) {
+        let guardian_set_index: u32 = (bytes[1] as u32) << 24
+            | (bytes[2] as u32) << 16
+            | (bytes[3] as u32) << 8
+            | (bytes[4] as u32);
+
+        let signatures_len = bytes[5];
+
+        // Parse signatures
+        let mut signatures = [[0; 66]; 13];
+        for i in 0..13 {
+            if i < signatures_len {
+                let sig_start: u32 = 6 + (i as u32 * 66);
+                for j in 0..66 {
+                    let byte_index: u32 = sig_start + j as u32;
+                    if byte_index < actual_length {
+                        signatures[i as u32][j] = bytes[byte_index];
+                    }
+                }
+            }
+        }
+
+        // Calculate where the body starts
+        let body_start: u32 = 6 + (signatures_len as u32 * 66);
+
+        // Calculate actual body length
+        let body_length: u32 = actual_length - body_start;
+
+        let mut body_bytes = [0; 1860];
+        for i in 0..1860 {
+            if i < body_length {
+                body_bytes[i] = bytes[body_start + i];
+            }
+        }
+
+        (guardian_set_index, signatures_len, signatures, body_bytes, body_length)
+    }
+
+    #[utility]
+    unconstrained fn compute_vaa_hash_unconstrained(
+        body_bytes: [u8; 1860],
+        body_length: u32,
+    ) -> [u8; 32] {
+        // Extract only the actual body content
+        let mut actual_body = [0; 1860];
+        for i in 0..body_length {
+            actual_body[i] = body_bytes[i];
+        }
+
+        // Double hash as per Wormhole spec
+        let hash: [u8; 32] = keccak256(actual_body, body_length);
+        let double_hash: [u8; 32] = keccak256(hash, 32);
+
+        double_hash
+    }
+
+    #[contract_library_method]
+    pub unconstrained fn verify_vaa_unconstrained(
+        bytes: [u8; 2000],
+        actual_length: u32,
+    ) -> ([[u8; 32]; 13], [[u8; 32]; 13], [[u8; 64]; 13], [u8; 32], u8) {
+        let version = bytes[0];
+        assert(version == 1, "VM version incompatible");
+
+        let (_, signatures_len, signatures, body_bytes, body_length) =
+            parse_vaa_unconstrained(bytes, actual_length);
+
+        assert(signatures_len >= 1, "Need at least 1 signature");
+        assert(signatures_len <= 13, "Too many signatures");
+
+        // Compute the VAA hash
+        let hash = compute_vaa_hash_unconstrained(body_bytes, body_length);
+
+        let guardian = crate::structs::Guardian::new(
+            [
+                0x13, 0x94, 0x7b, 0xd4, 0x8b, 0x18, 0xe5, 0x3f, 0xda, 0xee, 0xe7, 0x7f, 0x34, 0x73,
+                0x39, 0x1a, 0xc7, 0x27, 0xc6, 0x38,
+            ],
+            [
+                0xfa, 0x9d, 0x6b, 0x47, 0x04, 0x3b, 0x15, 0xb4, 0xb3, 0x3c, 0xf0, 0x5b, 0xb1, 0xde,
+                0x0f, 0x13, 0xda, 0x70, 0x31, 0x36, 0xb2, 0xcd, 0x15, 0x73, 0x24, 0xeb, 0x61, 0x5e,
+                0xc9, 0xee, 0x95, 0x12,
+            ],
+            [
+                0x38, 0xc6, 0x7b, 0x54, 0xd0, 0x3e, 0x3c, 0x18, 0x2e, 0x0e, 0x77, 0xb8, 0x5f, 0x88,
+                0x3c, 0xcd, 0x74, 0xdd, 0xdf, 0xc5, 0x90, 0x10, 0xfb, 0xf4, 0x75, 0xa0, 0xbe, 0xe6,
+                0x59, 0x3c, 0xe2, 0xbd,
+            ],
+        );
+
+        // Prepare arrays for constrained verification
+        let mut pub_keys_x = [[0; 32]; 13];
+        let mut pub_keys_y = [[0; 32]; 13];
+        let mut signature_bytes_array = [[0; 64]; 13];
+
+        for i in 0..13 {
+            if i < signatures_len {
+                let sig = Signature::from_bytes(signatures[i as u32]);
+                let guardian_idx = sig.guardian_index;
+
+                assert(guardian_idx < 19, "guardian index out of bounds");
+
+                // Get public key components as byte arrays
+                let pub_key_x = guardian.get_pub_key_x();
+                let pub_key_y = guardian.get_pub_key_y();
+
+                pub_keys_x[i as u32] = pub_key_x;
+                pub_keys_y[i as u32] = pub_key_y;
+
+                // Prepare signature bytes
+                let mut signature_bytes = [0; 64];
+                for j in 0..32 {
+                    signature_bytes[j] = sig.r[j];
+                    signature_bytes[32 + j] = sig.s[j];
+                }
+                signature_bytes_array[i as u32] = signature_bytes;
+            }
+        }
+        
+        (pub_keys_x, pub_keys_y, signature_bytes_array, hash, signatures_len)
+    }
+
+    #[private]
+    fn verify_vaa(bytes: [u8; 2000], actual_length: u32) {
+        // Safety: verify_vaa_unconstrained performs parsing and validation that doesn't need constraints
+        let (pub_keys_x, pub_keys_y, signature_bytes_array, hash, _signatures_len) =
+            unsafe { verify_vaa_unconstrained(bytes, actual_length) };
+
+        let required_signatures = 1;
+        let mut verified_count = 0;
+
+        // Testnet VAA verification only utilizes a single guardian.
+
+        // TODO: Uncomment this when ECDSA verification in loop is implemented.
+        // See issue: https://github.com/noir-lang/noir/pull/8993
+        
+        // for i in 0..13 {
+        //     if i < signatures_len {
+        //        let is_valid = std::ecdsa_secp256k1::verify_signature(
+        //            pub_keys_x[i as u32],
+        //            pub_keys_y[i as u32],
+        //            signature_bytes_array[i as u32],
+        //            hash,
+        //        );
+
+        //        if is_valid {
+        //            verified_count += 1;
+        //        }
+        //    }
+        //}
+
+        let is_valid = std::ecdsa_secp256k1::verify_signature(
+            pub_keys_x[0 as u32],
+            pub_keys_y[0 as u32],
+            signature_bytes_array[0 as u32],
+            hash,
+        );
+
+        if is_valid {
+            verified_count += 1;
+        }
+
+        assert(verified_count >= required_signatures, "Insufficient valid signatures");
+    }
+
+    // ============================================================================
+    // GUARDIAN MANAGEMENT
+    // ============================================================================
+    
+    // Individual guardian setters
+    #[public]
+    fn set_guardian1(guardian: Guardian, index: u32) {
+        storage.guardian_1.at(index).write(guardian);
+    }
+
+    #[public]
+    fn set_guardian2(guardian: Guardian, index: u32) {
+        storage.guardian_2.at(index).write(guardian);
+    }
+
+    #[public]
+    fn set_guardian3(guardian: Guardian, index: u32) {
+        storage.guardian_3.at(index).write(guardian);
+    }
+
+    #[public]
+    fn set_guardian4(guardian: Guardian, index: u32) {
+        storage.guardian_4.at(index).write(guardian);
+    }
+
+    #[public]
+    fn set_guardian5(guardian: Guardian, index: u32) {
+        storage.guardian_5.at(index).write(guardian);
+    }
+
+    #[public]
+    fn set_guardian6(guardian: Guardian, index: u32) {
+        storage.guardian_6.at(index).write(guardian);
+    }
+
+    #[public]
+    fn set_guardian7(guardian: Guardian, index: u32) {
+        storage.guardian_7.at(index).write(guardian);
+    }
+
+    #[public]
+    fn set_guardian8(guardian: Guardian, index: u32) {
+        storage.guardian_8.at(index).write(guardian);
+    }
+
+    #[public]
+    fn set_guardian9(guardian: Guardian, index: u32) {
+        storage.guardian_9.at(index).write(guardian);
+    }
+
+    #[public]
+    fn set_guardian10(guardian: Guardian, index: u32) {
+        storage.guardian_10.at(index).write(guardian);
+    }
+
+    #[public]
+    fn set_guardian11(guardian: Guardian, index: u32) {
+        storage.guardian_11.at(index).write(guardian);
+    }
+
+    #[public]
+    fn set_guardian12(guardian: Guardian, index: u32) {
+        storage.guardian_12.at(index).write(guardian);
+    }
+
+    #[public]
+    fn set_guardian13(guardian: Guardian, index: u32) {
+        storage.guardian_13.at(index).write(guardian);
+    }
+
+    #[public]
+    fn set_guardian14(guardian: Guardian, index: u32) {
+        storage.guardian_14.at(index).write(guardian);
+    }
+
+    #[public]
+    fn set_guardian15(guardian: Guardian, index: u32) {
+        storage.guardian_15.at(index).write(guardian);
+    }
+
+    #[public]
+    fn set_guardian16(guardian: Guardian, index: u32) {
+        storage.guardian_16.at(index).write(guardian);
+    }
+
+    #[public]
+    fn set_guardian17(guardian: Guardian, index: u32) {
+        storage.guardian_17.at(index).write(guardian);
+    }
+
+    #[public]
+    fn set_guardian18(guardian: Guardian, index: u32) {
+        storage.guardian_18.at(index).write(guardian);
+    }
+
+    #[public]
+    fn set_guardian19(guardian: Guardian, index: u32) {
+        storage.guardian_19.at(index).write(guardian);
+    }
+
+    // ============================================================================
+    // GOVERNANCE FUNCTIONS
+    // ============================================================================
+
+    #[public]
+    fn expire_guardian_set(index: u32) {
+        storage.current_guardian_set_index.at(index).write(context.timestamp() + 86400);
+    }
+
+    #[public]
+    fn guardian_set_expired(index: u32) -> bool {
+        let timestamp = storage.current_guardian_set_index.at(index).read();
+        context.timestamp() > timestamp
+    }
+
+    #[public]
+    fn set_provider(provider: Provider) {
+        let updated_storage = WormholeStorage::set_provider(storage.state.read(), provider);
+        storage.state.write(updated_storage);
+    }
+
+    #[public]
+    fn get_provider() -> Provider {
+        WormholeStorage::get_provider(storage.state.read())
+    }
+
+    // ============================================================================
+    // ACCESS CONTROL
+    // ============================================================================
+    
+    #[public]
+    #[internal]
+    fn _assert_is_owner() {
+        assert(storage.owner.read().eq(context.msg_sender()), "caller is not owner");
+    }
+
+    // ============================================================================
+    // ADDRESS MANAGEMENT FUNCTIONS
+    // ============================================================================
+    
+    #[public]
+    fn set_receiver_address(new_receiver_address: AztecAddress) {
+        _assert_is_owner();
+        storage.receiver_address.schedule_value_change(new_receiver_address);
+    }
+
+    #[public]
+    fn set_token_address(new_token_address: AztecAddress) {
+        _assert_is_owner();
+        storage.token_address.schedule_value_change(new_token_address);
+    }
+
+    #[public]
+    fn get_receiver_address() -> AztecAddress {
+        storage.receiver_address.get_current_value()
+    }
+
+    #[public]
+    fn get_token_address() -> AztecAddress {
+        storage.token_address.get_current_value()
+    }
+
+    #[public]
+    fn get_scheduled_receiver_address() -> (AztecAddress, u64) {
+        storage.receiver_address.get_scheduled_value()
+    }
+
+    #[public]
+    fn get_scheduled_token_address() -> (AztecAddress, u64) {
+        storage.token_address.get_scheduled_value()
+    }
+
+    #[public]
+    fn get_owner() -> AztecAddress {
+        storage.owner.read()
+    }
+
+    #[public]
+    fn transfer_ownership(new_owner: AztecAddress) {
+        _assert_is_owner();
+        storage.owner.write(new_owner);
+    }
+
+}
+
+mod tests {
+    use crate::{structs::Signature, Wormhole};
+    use dep::aztec::protocol_types::{
+        address::AztecAddress,
+        traits::FromField,
+    };
+    use dep::aztec::test::helpers::test_environment::TestEnvironment;
+
+    #[test]
+    pub unconstrained fn test_signature_extraction() {
+        let signature_bytes: [u8; 66] = [
+            0x00, // Guardian index
+            // R component (32 bytes)
+            0x46, 0x82, 0xbc, 0x4d, 0x5f, 0xf2, 0xe5, 0x4d, 0xc2, 0xee, 0x5e, 0x0e, 0xb6, 0x4f,
+            0x5c, 0x6c, 0x07, 0xaa, 0x44, 0x9a, 0xc5, 0x39, 0xab, 0xc6, 0x3c, 0x2b, 0xe5, 0xc3,
+            0x06, 0xa4, 0x8f, 0x23,
+            // S component (32 bytes)
+            0x3e, 0x93, 0x00, 0x17, 0x0a, 0x82, 0xad, 0xf3, 0xc3, 0xb7, 0xf4, 0x3f, 0x23, 0x17,
+            0x6f, 0xb0, 0x79, 0x17, 0x4a, 0x58, 0xd6, 0x7d, 0x14, 0x24, 0x77, 0xf6, 0x46, 0x67,
+            0x5d, 0x86, 0xeb, 0x63,
+            // Recovery ID
+            0x01,
+        ];
+
+        let sig = Signature::from_bytes(signature_bytes);
+
+        assert(sig.guardian_index == 0, "Guardian index should be 0");
+        assert(sig.r[0] == 0x46, "R component should start with 0x46");
+        assert(sig.s[0] == 0x3e, "S component should start with 0x3e");
+    }
+
+    #[test]
+    pub unconstrained fn test_verify_vaa_logic() {
+        // Setup env, generate keys
+        let mut env = TestEnvironment::new();
+
+        // Deploy contract - need to specify all 3 parameters for deploy
+        let dummy_owner = AztecAddress::from_field(1);
+        let receiver_address = AztecAddress::from_field(
+            0x0d071eec273fa0c82825d9c5d2096965a40bcc33ae942714cf6c683af9632504,
+        );
+        let token_address = AztecAddress::from_field(
+            0x037e5d19d6d27e2fb7c947cfe7c36459e27d35e46dd59f5f47373a64ff491d2c,
+        );
+
+        let contract_address = env
+            .deploy("Wormhole")
+            .with_public_initializer(
+                dummy_owner,
+                Wormhole::interface().init(56, 56, dummy_owner, receiver_address, token_address),
+            );
+
+        let mut vaa_bytes: [u8; 2000] = [0; 2000];
+        let actual_length = 221;
+
+        // NOTE: This is a real Wormhole VAA from Arbitrum Sepolia testnet.
+        // VAA bytes, fetched from https://wormholescan.io/#/tx/0xf93fd41efeb09ff28174824d4abf6dbc06ac408953a9975aa4a403d434051efc
+        // This VAA contains the message "Hello Wormhole!" and is used to test VAA verification logic.
+        let wh_vaa: [u8; 221] = [
+            0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x46, 0x82, 0xbc, 0x4d, 0x5f, 0xf2, 0xe5,
+            0x4d, 0xc2, 0xee, 0x5e, 0x0e, 0xb6, 0x4f, 0x5c, 0x6c, 0x07, 0xaa, 0x44, 0x9a, 0xc5,
+            0x39, 0xab, 0xc6, 0x3c, 0x2b, 0xe5, 0xc3, 0x06, 0xa4, 0x8f, 0x23, 0x3e, 0x93, 0x00,
+            0x17, 0x0a, 0x82, 0xad, 0xf3, 0xc3, 0xb7, 0xf4, 0x3f, 0x23, 0x17, 0x6f, 0xb0, 0x79,
+            0x17, 0x4a, 0x58, 0xd6, 0x7d, 0x14, 0x24, 0x77, 0xf6, 0x46, 0x67, 0x5d, 0x86, 0xeb,
+            0x63, 0x01, 0x68, 0x4b, 0xfa, 0xd4, 0x49, 0x96, 0x02, 0xd2, 0x27, 0x13, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x69, 0x7f, 0x31, 0xe0,
+            0x74, 0xbf, 0x2c, 0x81, 0x93, 0x91, 0xd5, 0x27, 0x29, 0xf9, 0x55, 0x06, 0xe0, 0xa7,
+            0x2f, 0xfb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f,
+            0x72, 0x6d, 0x68, 0x6f, 0x6c, 0x65, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        ];
+
+        for i in 0..221 {
+            vaa_bytes[i] = wh_vaa[i];
+        }
+
+        env.call_private(
+            dummy_owner,
+            Wormhole::at(contract_address).verify_vaa(vaa_bytes, actual_length),
+        );
+    }
+}

+ 537 - 0
aztec/contracts/src/structs.nr

@@ -0,0 +1,537 @@
+use dep::aztec::{
+    protocol_types::{
+        address::AztecAddress,
+        traits::{Deserialize, Packable, Serialize},
+    },
+};
+use std::meta::derive;
+
+#[derive(Deserialize, Packable, Serialize)]
+pub struct Provider {
+    pub chain_id: u16,
+    pub evm_chain_id: u16,
+}
+
+// Keep your Guardian_PK EXACTLY as is - don't touch it
+#[derive(Deserialize, Packable, Serialize)]
+pub struct Guardian_PK {
+    // Address (20 bytes)
+    pub value0: u8, pub value1: u8, pub value2: u8, pub value3: u8, pub value4: u8,
+    pub value5: u8, pub value6: u8, pub value7: u8, pub value8: u8, pub value9: u8,
+    pub value10: u8, pub value11: u8, pub value12: u8, pub value13: u8, pub value14: u8,
+    pub value15: u8, pub value16: u8, pub value17: u8, pub value18: u8, pub value19: u8,
+    
+    // Public Key X (32 bytes)
+    pub pub_key_x0: u8, pub pub_key_x1: u8, pub pub_key_x2: u8, pub pub_key_x3: u8,
+    pub pub_key_x4: u8, pub pub_key_x5: u8, pub pub_key_x6: u8, pub pub_key_x7: u8,
+    pub pub_key_x8: u8, pub pub_key_x9: u8, pub pub_key_x10: u8, pub pub_key_x11: u8,
+    pub pub_key_x12: u8, pub pub_key_x13: u8, pub pub_key_x14: u8, pub pub_key_x15: u8,
+    pub pub_key_x16: u8, pub pub_key_x17: u8, pub pub_key_x18: u8, pub pub_key_x19: u8,
+    pub pub_key_x20: u8, pub pub_key_x21: u8, pub pub_key_x22: u8, pub pub_key_x23: u8,
+    pub pub_key_x24: u8, pub pub_key_x25: u8, pub pub_key_x26: u8, pub pub_key_x27: u8,
+    pub pub_key_x28: u8, pub pub_key_x29: u8, pub pub_key_x30: u8, pub pub_key_x31: u8,
+    
+    // Public Key Y (32 bytes)
+    pub pub_key_y0: u8, pub pub_key_y1: u8, pub pub_key_y2: u8, pub pub_key_y3: u8,
+    pub pub_key_y4: u8, pub pub_key_y5: u8, pub pub_key_y6: u8, pub pub_key_y7: u8,
+    pub pub_key_y8: u8, pub pub_key_y9: u8, pub pub_key_y10: u8, pub pub_key_y11: u8,
+    pub pub_key_y12: u8, pub pub_key_y13: u8, pub pub_key_y14: u8, pub pub_key_y15: u8,
+    pub pub_key_y16: u8, pub pub_key_y17: u8, pub pub_key_y18: u8, pub pub_key_y19: u8,
+    pub pub_key_y20: u8, pub pub_key_y21: u8, pub pub_key_y22: u8, pub pub_key_y23: u8,
+    pub pub_key_y24: u8, pub pub_key_y25: u8, pub pub_key_y26: u8, pub pub_key_y27: u8,
+    pub pub_key_y28: u8, pub pub_key_y29: u8, pub pub_key_y30: u8, pub pub_key_y31: u8,
+}
+
+impl Guardian_PK {
+    pub fn new(address: [u8; 20], pub_key_x: [u8; 32], pub_key_y: [u8; 32]) -> Self {
+        Guardian_PK { 
+            // Address
+            value0: address[0], value1: address[1], value2: address[2], value3: address[3], value4: address[4],
+            value5: address[5], value6: address[6], value7: address[7], value8: address[8], value9: address[9],
+            value10: address[10], value11: address[11], value12: address[12], value13: address[13], value14: address[14],
+            value15: address[15], value16: address[16], value17: address[17], value18: address[18], value19: address[19],
+            
+            // Public Key X
+            pub_key_x0: pub_key_x[0], pub_key_x1: pub_key_x[1], pub_key_x2: pub_key_x[2], pub_key_x3: pub_key_x[3],
+            pub_key_x4: pub_key_x[4], pub_key_x5: pub_key_x[5], pub_key_x6: pub_key_x[6], pub_key_x7: pub_key_x[7],
+            pub_key_x8: pub_key_x[8], pub_key_x9: pub_key_x[9], pub_key_x10: pub_key_x[10], pub_key_x11: pub_key_x[11],
+            pub_key_x12: pub_key_x[12], pub_key_x13: pub_key_x[13], pub_key_x14: pub_key_x[14], pub_key_x15: pub_key_x[15],
+            pub_key_x16: pub_key_x[16], pub_key_x17: pub_key_x[17], pub_key_x18: pub_key_x[18], pub_key_x19: pub_key_x[19],
+            pub_key_x20: pub_key_x[20], pub_key_x21: pub_key_x[21], pub_key_x22: pub_key_x[22], pub_key_x23: pub_key_x[23],
+            pub_key_x24: pub_key_x[24], pub_key_x25: pub_key_x[25], pub_key_x26: pub_key_x[26], pub_key_x27: pub_key_x[27],
+            pub_key_x28: pub_key_x[28], pub_key_x29: pub_key_x[29], pub_key_x30: pub_key_x[30], pub_key_x31: pub_key_x[31],
+            
+            // Public Key Y
+            pub_key_y0: pub_key_y[0], pub_key_y1: pub_key_y[1], pub_key_y2: pub_key_y[2], pub_key_y3: pub_key_y[3],
+            pub_key_y4: pub_key_y[4], pub_key_y5: pub_key_y[5], pub_key_y6: pub_key_y[6], pub_key_y7: pub_key_y[7],
+            pub_key_y8: pub_key_y[8], pub_key_y9: pub_key_y[9], pub_key_y10: pub_key_y[10], pub_key_y11: pub_key_y[11],
+            pub_key_y12: pub_key_y[12], pub_key_y13: pub_key_y[13], pub_key_y14: pub_key_y[14], pub_key_y15: pub_key_y[15],
+            pub_key_y16: pub_key_y[16], pub_key_y17: pub_key_y[17], pub_key_y18: pub_key_y[18], pub_key_y19: pub_key_y[19],
+            pub_key_y20: pub_key_y[20], pub_key_y21: pub_key_y[21], pub_key_y22: pub_key_y[22], pub_key_y23: pub_key_y[23],
+            pub_key_y24: pub_key_y[24], pub_key_y25: pub_key_y[25], pub_key_y26: pub_key_y[26], pub_key_y27: pub_key_y[27],
+            pub_key_y28: pub_key_y[28], pub_key_y29: pub_key_y[29], pub_key_y30: pub_key_y[30], pub_key_y31: pub_key_y[31],
+        }
+    }
+
+    pub fn default() -> Self {
+        Guardian_PK::new([0; 20], [0; 32], [0; 32])
+    }
+
+    pub fn as_array(Guardian_PK: Guardian_PK) -> [u8; 20] {
+        [Guardian_PK.value0, Guardian_PK.value1, Guardian_PK.value2, Guardian_PK.value3, Guardian_PK.value4, 
+         Guardian_PK.value5, Guardian_PK.value6, Guardian_PK.value7, Guardian_PK.value8, Guardian_PK.value9,
+         Guardian_PK.value10, Guardian_PK.value11, Guardian_PK.value12, Guardian_PK.value13, Guardian_PK.value14,
+         Guardian_PK.value15, Guardian_PK.value16, Guardian_PK.value17, Guardian_PK.value18, Guardian_PK.value19]
+    }
+
+    pub fn get_pub_key_x(self) -> [u8; 32] {
+        [self.pub_key_x0, self.pub_key_x1, self.pub_key_x2, self.pub_key_x3,
+         self.pub_key_x4, self.pub_key_x5, self.pub_key_x6, self.pub_key_x7,
+         self.pub_key_x8, self.pub_key_x9, self.pub_key_x10, self.pub_key_x11,
+         self.pub_key_x12, self.pub_key_x13, self.pub_key_x14, self.pub_key_x15,
+         self.pub_key_x16, self.pub_key_x17, self.pub_key_x18, self.pub_key_x19,
+         self.pub_key_x20, self.pub_key_x21, self.pub_key_x22, self.pub_key_x23,
+         self.pub_key_x24, self.pub_key_x25, self.pub_key_x26, self.pub_key_x27,
+         self.pub_key_x28, self.pub_key_x29, self.pub_key_x30, self.pub_key_x31]
+    }
+
+    pub fn get_pub_key_y(self) -> [u8; 32] {
+        [self.pub_key_y0, self.pub_key_y1, self.pub_key_y2, self.pub_key_y3,
+         self.pub_key_y4, self.pub_key_y5, self.pub_key_y6, self.pub_key_y7,
+         self.pub_key_y8, self.pub_key_y9, self.pub_key_y10, self.pub_key_y11,
+         self.pub_key_y12, self.pub_key_y13, self.pub_key_y14, self.pub_key_y15,
+         self.pub_key_y16, self.pub_key_y17, self.pub_key_y18, self.pub_key_y19,
+         self.pub_key_y20, self.pub_key_y21, self.pub_key_y22, self.pub_key_y23,
+         self.pub_key_y24, self.pub_key_y25, self.pub_key_y26, self.pub_key_y27,
+         self.pub_key_y28, self.pub_key_y29, self.pub_key_y30, self.pub_key_y31]
+    }
+
+    // Add ONE optimization - return Fields for ECDSA verification without breaking existing code
+    pub fn get_pub_key_x_field(self) -> Field {
+        let bytes = self.get_pub_key_x();
+        Field::from_le_bytes(bytes)
+    }
+
+    pub fn get_pub_key_y_field(self) -> Field {
+        let bytes = self.get_pub_key_y();
+        Field::from_le_bytes(bytes)
+    }
+}
+
+#[derive(Deserialize, Packable, Serialize)]
+pub struct Guardian {
+    pub address: Guardian_PK,
+}
+
+impl Guardian {
+    pub fn new(address: [u8; 20], pub_key_x: [u8; 32], pub_key_y: [u8; 32]) -> Self {
+        Guardian { address: Guardian_PK::new(address, pub_key_x, pub_key_y) }
+    }
+    
+    pub fn default() -> Self {
+        Guardian { address: Guardian_PK::default() }
+    }
+
+    pub fn get_address(g: Self) -> [u8; 20] {
+        Guardian_PK::as_array(g.address)
+    }
+
+    pub fn get_pub_key_x(self) -> [u8; 32] {
+        self.address.get_pub_key_x()
+    }
+
+    pub fn get_pub_key_y(self) -> [u8; 32] {
+        self.address.get_pub_key_y()
+    }
+
+    // Add the same optimization here
+    pub fn get_pub_key_x_field(self) -> Field {
+        self.address.get_pub_key_x_field()
+    }
+
+    pub fn get_pub_key_y_field(self) -> Field {
+        self.address.get_pub_key_y_field()
+    }
+}
+
+#[derive(Deserialize, Serialize)]
+pub struct Signature {
+    pub r: [u8; 32],
+    pub s: [u8; 32],
+    pub v: u8,
+    pub guardian_index: u8
+}
+
+impl Signature {
+    pub fn new(r: [u8; 32], s: [u8; 32], v: u8, guardian_index: u8) -> Self {
+        Signature { r, s, v, guardian_index }
+    }
+
+    pub fn default() -> Self {
+        Signature { r: [0; 32], s: [0; 32], v: 0, guardian_index: 0 }
+    }
+
+    pub fn from_bytes(bytes: [u8; 66]) -> Self {
+        // Wormhole VAA signature format:
+        // byte 0: guardian index
+        // bytes 1-32: r (32 bytes) - BIG ENDIAN
+        // bytes 33-64: s (32 bytes) - BIG ENDIAN  
+        // byte 65: recovery id (v)
+        
+        let guardian_index = bytes[0];
+        
+        // Extract r (bytes 1-32) - keep as big endian
+        let r = [bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8],
+                 bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], bytes[16],
+                 bytes[17], bytes[18], bytes[19], bytes[20], bytes[21], bytes[22], bytes[23], bytes[24],
+                 bytes[25], bytes[26], bytes[27], bytes[28], bytes[29], bytes[30], bytes[31], bytes[32]];
+        
+        // Extract s (bytes 33-64) - keep as big endian
+        let s = [bytes[33], bytes[34], bytes[35], bytes[36], bytes[37], bytes[38], bytes[39], bytes[40],
+                 bytes[41], bytes[42], bytes[43], bytes[44], bytes[45], bytes[46], bytes[47], bytes[48],
+                 bytes[49], bytes[50], bytes[51], bytes[52], bytes[53], bytes[54], bytes[55], bytes[56],
+                 bytes[57], bytes[58], bytes[59], bytes[60], bytes[61], bytes[62], bytes[63], bytes[64]];
+        
+        let v = bytes[65];
+        
+        Signature { r, s, v, guardian_index }
+    }
+}
+
+#[derive(Deserialize, Serialize)]
+pub struct Body {
+    pub timestamp: u32,
+    pub nonce: u32,
+    pub emitter_chain_id: u16,
+    pub emitter_address: AztecAddress,
+    pub sequence: u64,
+    pub consistency_level: u8,
+    pub payload: [u8; 1024],
+}
+
+impl Body {
+    pub fn new(
+        timestamp: u32,
+        nonce: u32,
+        emitter_chain_id: u16,
+        emitter_address: AztecAddress,
+        sequence: u64,
+        consistency_level: u8,
+        payload: [u8; 1024],
+    ) -> Self {
+        Body {
+            timestamp,
+            nonce,
+            emitter_chain_id,
+            emitter_address,
+            sequence,
+            consistency_level,
+            payload
+        }
+    }
+    
+    pub fn default() -> Self {
+        Body {
+            timestamp: 0,
+            nonce: 0,
+            emitter_chain_id: 0,
+            emitter_address: AztecAddress::zero(),
+            sequence: 0,
+            consistency_level: 0,
+            payload: [0; 1024],
+        }
+    }
+
+    pub fn from_bytes(bytes: [u8; 1060]) -> Body {
+        let timestamp: u32 = u32_from_u8s_le([bytes[0], bytes[1], bytes[2], bytes[3]]);
+        let nonce: u32 = u32_from_u8s_le([bytes[4], bytes[5], bytes[6], bytes[7]]);
+        let emitter_chain_id: u16 = u16_from_u8s_le([bytes[8], bytes[9]]);
+
+        // Parse emitter bytes into aztec address
+        let emitter_address_bytes: [u8; 32] = [
+            bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], bytes[16], bytes[17],
+            bytes[18], bytes[19], bytes[20], bytes[21], bytes[22], bytes[23], bytes[24], bytes[25],
+            bytes[26], bytes[27], bytes[28], bytes[29], bytes[30], bytes[31], bytes[32], bytes[33],
+            bytes[34], bytes[35], bytes[36], bytes[37], bytes[38], bytes[39], bytes[40], bytes[41],
+        ];
+
+        let emitter_address_field: Field = Field::from_le_bytes(emitter_address_bytes);
+        let emitter_address: AztecAddress = AztecAddress {
+            inner: emitter_address_field,
+        };
+        
+        let sequence: u64 = u64_from_u8s_le([bytes[42], bytes[43], bytes[44], bytes[45], bytes[46], bytes[47], bytes[48], bytes[49]]);
+        let consistency_level: u8 = bytes[50];
+        
+        // Extract payload (1024 bytes starting from index 51)
+        let mut payload = [0; 1024];
+        for i in 0..1024 {
+            payload[i] = bytes[51 + i];
+        }
+        
+        Body {
+            timestamp,
+            nonce,
+            emitter_chain_id,
+            emitter_address,
+            sequence,
+            consistency_level,
+            payload
+        }
+    }
+}
+
+#[derive(Deserialize, Serialize)]
+pub struct VAA {
+    pub version: u8,
+    pub timestamp: u32,
+    pub nonce: u32,
+    pub emitter_chain_id: u16,
+    pub emitter_address: AztecAddress,
+    pub sequence: u64,
+    pub consistency_level: u8,
+    pub payload: [u8; 1024],
+    pub guardian_set_index: u32,
+    pub signatures: [Signature; 13],
+    pub hash: [u8; 32], 
+}
+
+impl VAA {
+    pub fn default() -> Self {
+        VAA {
+            version: 0,
+            timestamp: 0,
+            nonce: 0,
+            emitter_chain_id: 0,
+            emitter_address: AztecAddress::zero(),
+            sequence: 0,
+            consistency_level: 0,
+            payload: [0; 1024],
+            guardian_set_index: 0,
+            signatures: [Signature::new([0;32], [0;32], 0, 0); 13],
+            hash: [0; 32]
+        }
+    }
+
+    pub fn new(
+        version: u8,
+        timestamp: u32,
+        nonce: u32,
+        emitter_chain_id: u16,
+        emitter_address: AztecAddress,
+        sequence: u64,
+        consistency_level: u8,
+        payload: [u8; 1024],
+        guardian_set_index: u32,
+        signatures: [Signature; 13],
+        hash: [u8; 32]
+    ) -> Self {
+        VAA {
+            version,
+            timestamp,
+            nonce,
+            emitter_chain_id,
+            emitter_address,
+            sequence,
+            consistency_level,
+            payload,
+            guardian_set_index,
+            signatures,
+            hash
+        }
+    }
+
+    pub fn get_guardian_set_index(vaa: Self) -> u32{
+        vaa.guardian_set_index
+    }
+
+    pub fn get_timestamp(vaa: Self) -> u32 {
+        vaa.timestamp
+    }
+
+    pub fn get_payload(vaa: Self) -> [u8; 1024] {
+        vaa.payload
+    }
+
+    pub fn get_hash(vaa: Self) -> [u8; 32] {
+        vaa.hash
+    }
+
+    pub fn get_emitter_chain_id(vaa: Self) -> u16 {
+        vaa.emitter_chain_id
+    }
+
+    pub fn get_sequence(vaa: Self) -> u64 {
+        vaa.sequence
+    }
+
+    pub fn get_consistency_level(vaa: Self) -> u8 {
+        vaa.consistency_level
+    }
+}
+
+// Helper functions
+pub fn u32_from_u8s_le(bytes: [u8; 4]) -> u32 {
+    let mut result = 0;
+    for i in 0..4 {
+        result |= (bytes[i as u32] as u32) << (i * 8);
+    }
+    result
+}
+
+pub fn u16_from_u8s_le(bytes: [u8; 2]) -> u16 {
+    let mut result = 0;
+    for i in 0..2 {
+        result |= (bytes[i as u32] as u16) << (i * 8);
+    }
+    result
+}
+
+pub fn u64_from_u8s_le(bytes: [u8; 8]) -> u64 {
+    let mut result = 0;
+    for i in 0..8 {
+        result |= (bytes[i as u32] as u64) << (i * 8);
+    }
+    result
+}
+
+// Governance structures
+pub struct ContractUpgrade {
+    pub module: [u8; 32],
+    pub action: u8,
+    pub chain: u16,
+    pub new_contract: Field
+}
+
+pub struct GuardianSetUpgrade {
+    pub module: [u8; 32],
+    pub action: u8,
+    pub chain: u16,
+    pub new_guardian_set_index: u32
+}
+
+pub struct SetMessageFee {
+    pub module: [u8; 32],
+    pub action: u8,
+    pub chain: u16,
+    pub message_fee: Field
+}
+
+pub struct TransferFees {
+    pub module: [u8; 32],
+    pub action: u8,
+    pub chain: u16,
+    pub amount: Field,
+    pub recipient: [u8; 32]
+}
+
+pub struct RecoverChainId {
+    pub module: [u8; 32],
+    pub action: u8,
+    pub evm_chain_id: Field,
+    pub new_chain_id: u16
+}
+
+#[derive(Deserialize, Packable, Serialize)]
+pub struct EmitterRegistry {
+    pub next_id: u64,
+}
+
+pub struct EmitterCapability {
+    pub emitter: u64,
+    pub sequence: u64,
+}
+
+impl EmitterRegistry {
+    pub fn new() -> Self {
+        EmitterRegistry { next_id: 0 }
+    }
+
+    pub fn get_next_id(&mut self) -> u64 {
+        let id = self.next_id;
+        self.next_id += 1;
+        id
+    }
+}
+
+impl EmitterCapability {
+    pub fn new(registry: &mut EmitterRegistry) -> Self {
+        let emitter = registry.get_next_id();
+        registry.next_id += 1;
+        EmitterCapability { emitter, sequence: 0 }
+    }
+
+    pub fn use_sequence(&mut self) -> u64 {
+        let sequence = self.sequence;
+        self.sequence += 1;
+        sequence
+    }
+}
+
+#[derive(Deserialize, Packable, Serialize)]
+pub struct WormholeStorage {
+    pub provider: Provider,
+    pub guardian_set_index: u64,
+    pub guardian_set_expiry: u64,
+    pub message_fee: u128,
+    pub emitter_registry: EmitterRegistry,
+}
+
+impl WormholeStorage {
+    pub fn init(provider: Provider) -> Self {
+        WormholeStorage {
+            provider: provider,
+            guardian_set_index: 0,
+            guardian_set_expiry: 86400,
+            message_fee: 0,
+            emitter_registry: EmitterRegistry::new(),
+        }
+    }
+
+    // Getters
+    pub fn get_provider(state: Self) -> Provider {
+        state.provider
+    }
+
+    pub fn get_message_fee(state: Self) -> u128 {
+        state.message_fee
+    }
+
+    pub fn get_chain_id(state: Self) -> u16 {
+        state.provider.chain_id
+    }
+
+    pub fn get_guardian_set_index(state: Self) -> u64 {
+        state.guardian_set_index
+    }
+
+    pub fn get_guardian_set_expiry(state: Self) -> u64 {
+        state.guardian_set_expiry
+    }
+
+    // Setters
+    pub fn set_guardian_set_expiry(mut state: Self, guardian_set_expiry: u64) -> Self {
+        state.guardian_set_expiry = guardian_set_expiry;
+        state
+    }
+
+    pub fn set_guardian_set_index(mut state: Self, guardian_set_index: u64) -> Self {
+        state.guardian_set_index = guardian_set_index;
+        state
+    }
+
+    pub fn set_chain_id(mut state: Self, chain_id: u16) -> Self {
+        state.provider.chain_id = chain_id;
+        state
+    }
+
+    pub fn set_provider(mut state: Self, provider: Provider) -> Self {
+        state.provider = provider;
+        state
+    }
+
+    pub fn set_message_fee(mut state: Self, message_fee: u128) -> Self {
+        state.message_fee = message_fee;
+        state
+    }
+}

+ 5669 - 0
aztec/package-lock.json

@@ -0,0 +1,5669 @@
+{
+  "name": "aztec",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "dependencies": {
+        "@aztec/accounts": "1.2.1",
+        "@aztec/noir-contracts.js": "1.2.1",
+        "@aztec/pxe": "1.2.1",
+        "@aztec/stdlib": "1.2.1",
+        "express": "^4.18.2",
+        "js-sha3": "^0.9.3",
+        "secp256k1": "^5.0.1"
+      }
+    },
+    "node_modules/@adraffy/ens-normalize": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz",
+      "integrity": "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==",
+      "license": "MIT"
+    },
+    "node_modules/@aztec/accounts": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/accounts/-/accounts-1.2.1.tgz",
+      "integrity": "sha512-OZht8Zs2yibtVgUji4ekpETC6KVzn9GSD2GlVvenWbEygLzuKesvjHpMVuwP9LpFscBKHDhbXzDosnJXPVPZmQ==",
+      "dependencies": {
+        "@aztec/aztec.js": "1.2.1",
+        "@aztec/entrypoints": "1.2.1",
+        "@aztec/ethereum": "1.2.1",
+        "@aztec/foundation": "1.2.1",
+        "@aztec/stdlib": "1.2.1",
+        "tslib": "^2.4.0"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@aztec/aztec.js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/aztec.js/-/aztec.js-1.2.1.tgz",
+      "integrity": "sha512-PSwEQzjGcyvKSWRy8U1LjmAu3kTUu2B7flUe+GTiTH+I5VHaotJ+wpbaS0L8bnv2XCvhsU91QT5++/9BtYtzQA==",
+      "dependencies": {
+        "@aztec/constants": "1.2.1",
+        "@aztec/entrypoints": "1.2.1",
+        "@aztec/ethereum": "1.2.1",
+        "@aztec/foundation": "1.2.1",
+        "@aztec/l1-artifacts": "1.2.1",
+        "@aztec/protocol-contracts": "1.2.1",
+        "@aztec/stdlib": "1.2.1",
+        "axios": "^1.8.2",
+        "tslib": "^2.4.0",
+        "viem": "2.23.7"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@aztec/bb-prover": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/bb-prover/-/bb-prover-1.2.1.tgz",
+      "integrity": "sha512-Tlo61f0zOQ9lQP8jJjqLiXHOZSUXm2CZX+QiXRQW1Jo8T38v8q62FVbm7sE4Quf4uCEUAU2KKhCwxoCxkU25Tg==",
+      "dependencies": {
+        "@aztec/bb.js": "1.2.1",
+        "@aztec/constants": "1.2.1",
+        "@aztec/foundation": "1.2.1",
+        "@aztec/noir-noirc_abi": "1.2.1",
+        "@aztec/noir-protocol-circuits-types": "1.2.1",
+        "@aztec/noir-types": "1.2.1",
+        "@aztec/simulator": "1.2.1",
+        "@aztec/stdlib": "1.2.1",
+        "@aztec/telemetry-client": "1.2.1",
+        "@aztec/world-state": "1.2.1",
+        "commander": "^12.1.0",
+        "pako": "^2.1.0",
+        "source-map-support": "^0.5.21",
+        "tslib": "^2.4.0"
+      },
+      "bin": {
+        "bb-cli": "dest/bb/index.js"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@aztec/bb.js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/bb.js/-/bb.js-1.2.1.tgz",
+      "integrity": "sha512-q1AuP74adCUdqhDM3nXpqX8rVz5iPSPb0t/bnqIUGp97Ta/YTJP/zDYv7Z3c3Eiqq/bBpCIAfK9D5z96LKMfTQ==",
+      "license": "MIT",
+      "dependencies": {
+        "comlink": "^4.4.1",
+        "commander": "^12.1.0",
+        "idb-keyval": "^6.2.1",
+        "msgpackr": "^1.11.2",
+        "pako": "^2.1.0",
+        "pino": "^9.5.0",
+        "tslib": "^2.4.0"
+      },
+      "bin": {
+        "bb.js": "dest/node/main.js"
+      }
+    },
+    "node_modules/@aztec/blob-lib": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/blob-lib/-/blob-lib-1.2.1.tgz",
+      "integrity": "sha512-QXJZM2VLz5Xz4RmsJ1iKrW0BvkfjlCC3VH1Pipcwq5oow93MTLQQQbzavUvp8n40ZgHUUanfQaVmpDcNn4iOGQ==",
+      "dependencies": {
+        "@aztec/constants": "1.2.1",
+        "@aztec/foundation": "1.2.1",
+        "c-kzg": "4.0.0-alpha.1",
+        "tslib": "^2.4.0"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@aztec/builder": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/builder/-/builder-1.2.1.tgz",
+      "integrity": "sha512-1FsuE7Q0GkJ2RpV9GHMyGt2tUKYfKdh47iAhceNoCg6DMJ6LfVzJ2VV94q2OEO9YLgrcAWU8tF+kfiT1J9HA6g==",
+      "dependencies": {
+        "@aztec/foundation": "1.2.1",
+        "@aztec/stdlib": "1.2.1",
+        "commander": "^12.1.0"
+      },
+      "bin": {
+        "aztec-builder": "dest/bin/cli.js"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@aztec/constants": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/constants/-/constants-1.2.1.tgz",
+      "integrity": "sha512-TtLjBb4ZFt/cW5HgsDSYoWI/d7ygCCgIW6ToKxcz7Gw/+ZzAwjmilQWwT+CfAHCSqgLATSjFjfyjsiBM2tezRA==",
+      "dependencies": {
+        "tslib": "^2.4.0"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@aztec/entrypoints": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/entrypoints/-/entrypoints-1.2.1.tgz",
+      "integrity": "sha512-vo6Emp1i8dQqS7pwYOWc8MFFggxetR1rx2UjXQHKJ68n+RYEqM9ZRun6Ueb55Ypu8L7270L2wSHFK2TdEK5O8Q==",
+      "dependencies": {
+        "@aztec/constants": "1.2.1",
+        "@aztec/foundation": "1.2.1",
+        "@aztec/protocol-contracts": "1.2.1",
+        "@aztec/stdlib": "1.2.1",
+        "tslib": "^2.4.0"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@aztec/ethereum": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/ethereum/-/ethereum-1.2.1.tgz",
+      "integrity": "sha512-uq82qosYMa/UE+4jUPBtDsr7skYE8hIIKimfNApKcn87Y6ZuaISAbNqH6R2G9LwlQvMVwMm4jrI6MNTPbUhq+w==",
+      "dependencies": {
+        "@aztec/blob-lib": "1.2.1",
+        "@aztec/foundation": "1.2.1",
+        "@aztec/l1-artifacts": "1.2.1",
+        "@viem/anvil": "^0.0.10",
+        "dotenv": "^16.0.3",
+        "lodash.pickby": "^4.5.0",
+        "tslib": "^2.4.0",
+        "viem": "2.23.7",
+        "zod": "^3.23.8"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@aztec/foundation": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/foundation/-/foundation-1.2.1.tgz",
+      "integrity": "sha512-2yIqG1N5Iol94tasawwM7tAx3Q3fyhVCwlekJJZ8ePVT8S9BVwngcdXgP0b54rXeowsgnRXLOJyKEopRJELY8g==",
+      "dependencies": {
+        "@aztec/bb.js": "1.2.1",
+        "@koa/cors": "^5.0.0",
+        "@noble/curves": "^1.2.0",
+        "bn.js": "^5.2.1",
+        "colorette": "^2.0.20",
+        "detect-node": "^2.1.0",
+        "hash.js": "^1.1.7",
+        "koa": "^2.16.1",
+        "koa-bodyparser": "^4.4.0",
+        "koa-compress": "^5.1.0",
+        "koa-router": "^12.0.0",
+        "leveldown": "^6.1.1",
+        "lodash.chunk": "^4.2.0",
+        "lodash.clonedeepwith": "^4.5.0",
+        "pako": "^2.1.0",
+        "pino": "^9.5.0",
+        "pino-pretty": "^13.0.0",
+        "sha3": "^2.1.4",
+        "undici": "^5.28.5",
+        "zod": "^3.23.8"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@aztec/key-store": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/key-store/-/key-store-1.2.1.tgz",
+      "integrity": "sha512-boj3kkQhgF8qy58QVribaZ5QV32vizlskMm+PpFi+7I0CbslKKdEW2NfeP0syx+LaRh42Guh8sNJvpuJPs0Vpw==",
+      "dependencies": {
+        "@aztec/constants": "1.2.1",
+        "@aztec/foundation": "1.2.1",
+        "@aztec/kv-store": "1.2.1",
+        "@aztec/stdlib": "1.2.1",
+        "tslib": "^2.4.0"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@aztec/kv-store": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/kv-store/-/kv-store-1.2.1.tgz",
+      "integrity": "sha512-AJr4DvkgDMsis0FCIJpLaOlx32yXvymOFYklout+YpPKGqKJyLLqX15AczC2h2V6ltoxpt58pfHpdti0R4elHw==",
+      "dependencies": {
+        "@aztec/ethereum": "1.2.1",
+        "@aztec/foundation": "1.2.1",
+        "@aztec/native": "1.2.1",
+        "@aztec/stdlib": "1.2.1",
+        "idb": "^8.0.0",
+        "lmdb": "^3.2.0",
+        "msgpackr": "^1.11.2",
+        "ohash": "^2.0.11",
+        "ordered-binary": "^1.5.3"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@aztec/l1-artifacts": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/l1-artifacts/-/l1-artifacts-1.2.1.tgz",
+      "integrity": "sha512-A9RTYHVetgn1LzDDPrn4D5B6RhZTCWqrAv2qFfL7FEY15ye9iY3NXXsUJsp8Fx03LJhrjnvqQ2dA1hdPOw5NaQ==",
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@aztec/merkle-tree": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/merkle-tree/-/merkle-tree-1.2.1.tgz",
+      "integrity": "sha512-I1x7ntPX7yoPos13DjZjSK5A8rukn2IFMUj0dTVNfY5TjZaAmBI6bCRjNKxgx7f553Ez/42HbOkk6hkX3Wcq5w==",
+      "dependencies": {
+        "@aztec/foundation": "1.2.1",
+        "@aztec/kv-store": "1.2.1",
+        "@aztec/stdlib": "1.2.1",
+        "sha256": "^0.2.0",
+        "tslib": "^2.4.0"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@aztec/native": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/native/-/native-1.2.1.tgz",
+      "integrity": "sha512-JbE9VPzizjk0m26F/TcGAYkxak/CPEez+xV5w3G/sWeiXITpKaTHld4L0KWNh1qrIpTcHDQ1gjiTZPVeYLCS1Q==",
+      "dependencies": {
+        "@aztec/foundation": "1.2.1",
+        "bindings": "^1.5.0",
+        "msgpackr": "^1.11.2"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@aztec/noir-acvm_js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/noir-acvm_js/-/noir-acvm_js-1.2.1.tgz",
+      "integrity": "sha512-Ikz9u5+vqn0uLTh91g0QVZlyMDDm5TkDKkE6qeZSMFaZUE8kgcBvt7IJBoNs5xx9LDaMp/FiErw0vHa4QvifOQ==",
+      "license": "MIT"
+    },
+    "node_modules/@aztec/noir-contracts.js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/noir-contracts.js/-/noir-contracts.js-1.2.1.tgz",
+      "integrity": "sha512-TSTByrde0LyFPWrk5cPSOQHJELI8wzoGbD4Obkpq8hSRFkCEXdAmuZ0A+79MUt1cfuV8F+HSvNFjoC3r3WQM6A==",
+      "dependencies": {
+        "@aztec/aztec.js": "1.2.1",
+        "tslib": "^2.4.0"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@aztec/noir-noir_codegen": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/noir-noir_codegen/-/noir-noir_codegen-1.2.1.tgz",
+      "integrity": "sha512-5LW+4tEdqe3J2wqyjGq6D84xEEgP993AusUGReI7hSLv//P4QMyG35oHR1mKuoTr2OXdG0k0QJBfHWxorTgIJg==",
+      "license": "(MIT OR Apache-2.0)",
+      "dependencies": {
+        "@aztec/noir-types": "1.2.1",
+        "glob": "^11.0.2",
+        "ts-command-line-args": "^2.5.1"
+      },
+      "bin": {
+        "noir-codegen": "lib/main.js"
+      }
+    },
+    "node_modules/@aztec/noir-noirc_abi": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/noir-noirc_abi/-/noir-noirc_abi-1.2.1.tgz",
+      "integrity": "sha512-4VxHncrUt8BH5n9KQYZwvHHuSPgz7FVkiT9FOaNf36xOw6kWS3bwsq1oLLQ0Q2MCxaSA+nNSbGxamS3mxW1Ysg==",
+      "license": "(MIT OR Apache-2.0)",
+      "dependencies": {
+        "@aztec/noir-types": "1.2.1"
+      }
+    },
+    "node_modules/@aztec/noir-protocol-circuits-types": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/noir-protocol-circuits-types/-/noir-protocol-circuits-types-1.2.1.tgz",
+      "integrity": "sha512-JAc3YTaXoox6vNGWlVHKJlSaZD5MJMujyv0KuZicQytclEwwAw+kxGNsiBtW0jlRMmzY/gYxY91CVuFIaLITaQ==",
+      "dependencies": {
+        "@aztec/blob-lib": "1.2.1",
+        "@aztec/constants": "1.2.1",
+        "@aztec/foundation": "1.2.1",
+        "@aztec/noir-acvm_js": "1.2.1",
+        "@aztec/noir-noir_codegen": "1.2.1",
+        "@aztec/noir-noirc_abi": "1.2.1",
+        "@aztec/noir-types": "1.2.1",
+        "@aztec/stdlib": "1.2.1",
+        "change-case": "^5.4.4",
+        "tslib": "^2.4.0"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@aztec/noir-types": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/noir-types/-/noir-types-1.2.1.tgz",
+      "integrity": "sha512-urMTE9hmFDKUEU+bn1m9GtynLoVL7ZtkXiTrRQBhwC2aKQUm+gbtwZ+zWS/lChq572pb6hQ/X0tlTZcjDUsA7g==",
+      "license": "(MIT OR Apache-2.0)"
+    },
+    "node_modules/@aztec/protocol-contracts": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/protocol-contracts/-/protocol-contracts-1.2.1.tgz",
+      "integrity": "sha512-QUTHJJ5s8Sj9/RYSRR5Nj3v/GLT27JdkQxBvZ2Hr+c+EYYvSHbtT2dtrSzR25+AOXUv8Hl/3WXgkh8aHGcIXwg==",
+      "dependencies": {
+        "@aztec/constants": "1.2.1",
+        "@aztec/foundation": "1.2.1",
+        "@aztec/stdlib": "1.2.1",
+        "lodash.chunk": "^4.2.0",
+        "lodash.omit": "^4.5.0",
+        "tslib": "^2.4.0"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@aztec/pxe": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/pxe/-/pxe-1.2.1.tgz",
+      "integrity": "sha512-rg/zGe/Rs8jxHl5nJlvf+kinxfQAPUQCzlA4czMc8HI7E8gKQ9is0GWvF09LYW9IgrJFrlDHu84qqc6xPMhhOw==",
+      "dependencies": {
+        "@aztec/bb-prover": "1.2.1",
+        "@aztec/bb.js": "1.2.1",
+        "@aztec/builder": "1.2.1",
+        "@aztec/constants": "1.2.1",
+        "@aztec/ethereum": "1.2.1",
+        "@aztec/foundation": "1.2.1",
+        "@aztec/key-store": "1.2.1",
+        "@aztec/kv-store": "1.2.1",
+        "@aztec/noir-protocol-circuits-types": "1.2.1",
+        "@aztec/noir-types": "1.2.1",
+        "@aztec/protocol-contracts": "1.2.1",
+        "@aztec/simulator": "1.2.1",
+        "@aztec/stdlib": "1.2.1",
+        "koa": "^2.16.1",
+        "koa-router": "^12.0.0",
+        "lodash.omit": "^4.5.0",
+        "sha3": "^2.1.4",
+        "tslib": "^2.4.0",
+        "viem": "2.23.7"
+      },
+      "bin": {
+        "pxe": "dest/bin/index.js"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@aztec/simulator": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/simulator/-/simulator-1.2.1.tgz",
+      "integrity": "sha512-s2XrBGnjH+ItHqdMCEq04YUTo+WOq3GuVU93WiFHI2GqgYteC5XmIVaInKz+aUaFQ/fVdXTQ6wkvGWH73P+/jA==",
+      "dependencies": {
+        "@aztec/constants": "1.2.1",
+        "@aztec/foundation": "1.2.1",
+        "@aztec/noir-acvm_js": "1.2.1",
+        "@aztec/noir-noirc_abi": "1.2.1",
+        "@aztec/noir-protocol-circuits-types": "1.2.1",
+        "@aztec/noir-types": "1.2.1",
+        "@aztec/protocol-contracts": "1.2.1",
+        "@aztec/stdlib": "1.2.1",
+        "@aztec/telemetry-client": "1.2.1",
+        "@aztec/world-state": "1.2.1",
+        "lodash.clonedeep": "^4.5.0",
+        "lodash.merge": "^4.6.2",
+        "tslib": "^2.4.0"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@aztec/stdlib": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/stdlib/-/stdlib-1.2.1.tgz",
+      "integrity": "sha512-A1/vvhIrccX941EJUDbojzmJ4FBwqDijIOJz6MQ5H0QtOSEOQImc+1NXwv8roC16mj5J5BiG1CAgjJmo4Hzivg==",
+      "dependencies": {
+        "@aztec/bb.js": "1.2.1",
+        "@aztec/blob-lib": "1.2.1",
+        "@aztec/constants": "1.2.1",
+        "@aztec/ethereum": "1.2.1",
+        "@aztec/foundation": "1.2.1",
+        "@aztec/noir-noirc_abi": "1.2.1",
+        "@google-cloud/storage": "^7.15.0",
+        "axios": "^1.9.0",
+        "json-stringify-deterministic": "1.0.12",
+        "lodash.chunk": "^4.2.0",
+        "lodash.isequal": "^4.5.0",
+        "lodash.omit": "^4.5.0",
+        "lodash.times": "^4.3.2",
+        "msgpackr": "^1.11.2",
+        "pako": "^2.1.0",
+        "tslib": "^2.4.0",
+        "viem": "2.23.7",
+        "zod": "^3.23.8"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@aztec/telemetry-client": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/telemetry-client/-/telemetry-client-1.2.1.tgz",
+      "integrity": "sha512-r7IuHVHWNHmoX7GOYW8DkruABSU+uEmT/aTaM/Hod34h5/lt+B09CYxf7HL5gnXWrOfq5eePoNIf0QsLu4jZWg==",
+      "dependencies": {
+        "@aztec/foundation": "1.2.1",
+        "@aztec/stdlib": "1.2.1",
+        "@opentelemetry/api": "^1.9.0",
+        "@opentelemetry/api-logs": "^0.55.0",
+        "@opentelemetry/core": "^1.28.0",
+        "@opentelemetry/exporter-logs-otlp-http": "^0.55.0",
+        "@opentelemetry/exporter-metrics-otlp-http": "^0.55.0",
+        "@opentelemetry/exporter-trace-otlp-http": "^0.55.0",
+        "@opentelemetry/host-metrics": "^0.35.4",
+        "@opentelemetry/otlp-exporter-base": "^0.55.0",
+        "@opentelemetry/resource-detector-gcp": "^0.32.0",
+        "@opentelemetry/resources": "^1.28.0",
+        "@opentelemetry/sdk-logs": "^0.55.0",
+        "@opentelemetry/sdk-metrics": "^1.28.0",
+        "@opentelemetry/sdk-trace-node": "^1.28.0",
+        "@opentelemetry/semantic-conventions": "^1.28.0",
+        "prom-client": "^15.1.3",
+        "viem": "2.23.7"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@aztec/world-state": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@aztec/world-state/-/world-state-1.2.1.tgz",
+      "integrity": "sha512-p8/7tFdf5gU58Zgpp8q0gXjDpz4+omlANint0udxPUB2Ugl3847C58PjQGs/UW1Ic5m/L4kDoM5NnsSuHGCDiw==",
+      "dependencies": {
+        "@aztec/constants": "1.2.1",
+        "@aztec/foundation": "1.2.1",
+        "@aztec/kv-store": "1.2.1",
+        "@aztec/merkle-tree": "1.2.1",
+        "@aztec/native": "1.2.1",
+        "@aztec/protocol-contracts": "1.2.1",
+        "@aztec/stdlib": "1.2.1",
+        "@aztec/telemetry-client": "1.2.1",
+        "tslib": "^2.4.0",
+        "zod": "^3.23.8"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@fastify/busboy": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
+      "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@google-cloud/paginator": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz",
+      "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "arrify": "^2.0.0",
+        "extend": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@google-cloud/projectify": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz",
+      "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@google-cloud/promisify": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz",
+      "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@google-cloud/storage": {
+      "version": "7.17.0",
+      "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.17.0.tgz",
+      "integrity": "sha512-5m9GoZqKh52a1UqkxDBu/+WVFDALNtHg5up5gNmNbXQWBcV813tzJKsyDtKjOPrlR1em1TxtD7NSPCrObH7koQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@google-cloud/paginator": "^5.0.0",
+        "@google-cloud/projectify": "^4.0.0",
+        "@google-cloud/promisify": "<4.1.0",
+        "abort-controller": "^3.0.0",
+        "async-retry": "^1.3.3",
+        "duplexify": "^4.1.3",
+        "fast-xml-parser": "^4.4.1",
+        "gaxios": "^6.0.2",
+        "google-auth-library": "^9.6.3",
+        "html-entities": "^2.5.2",
+        "mime": "^3.0.0",
+        "p-limit": "^3.0.1",
+        "retry-request": "^7.0.0",
+        "teeny-request": "^9.0.0",
+        "uuid": "^8.0.0"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@hapi/bourne": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz",
+      "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@isaacs/balanced-match": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
+      "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
+      "license": "MIT",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/@isaacs/brace-expansion": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
+      "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
+      "license": "MIT",
+      "dependencies": {
+        "@isaacs/balanced-match": "^4.0.1"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+      "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@koa/cors": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/@koa/cors/-/cors-5.0.0.tgz",
+      "integrity": "sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==",
+      "license": "MIT",
+      "dependencies": {
+        "vary": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 14.0.0"
+      }
+    },
+    "node_modules/@lmdb/lmdb-darwin-arm64": {
+      "version": "3.4.2",
+      "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.4.2.tgz",
+      "integrity": "sha512-NK80WwDoODyPaSazKbzd3NEJ3ygePrkERilZshxBViBARNz21rmediktGHExoj9n5t9+ChlgLlxecdFKLCuCKg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@lmdb/lmdb-darwin-x64": {
+      "version": "3.4.2",
+      "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.4.2.tgz",
+      "integrity": "sha512-zevaowQNmrp3U7Fz1s9pls5aIgpKRsKb3dZWDINtLiozh3jZI9fBrI19lYYBxqdyiIyNdlyiidPnwPShj4aK+w==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@lmdb/lmdb-linux-arm": {
+      "version": "3.4.2",
+      "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.4.2.tgz",
+      "integrity": "sha512-OmHCULY17rkx/RoCoXlzU7LyR8xqrksgdYWwtYa14l/sseezZ8seKWXcogHcjulBddER5NnEFV4L/Jtr2nyxeg==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@lmdb/lmdb-linux-arm64": {
+      "version": "3.4.2",
+      "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.4.2.tgz",
+      "integrity": "sha512-ZBEfbNZdkneebvZs98Lq30jMY8V9IJzckVeigGivV7nTHJc+89Ctomp1kAIWKlwIG0ovCDrFI448GzFPORANYg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@lmdb/lmdb-linux-x64": {
+      "version": "3.4.2",
+      "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.4.2.tgz",
+      "integrity": "sha512-vL9nM17C77lohPYE4YaAQvfZCSVJSryE4fXdi8M7uWPBnU+9DJabgKVAeyDb84ZM2vcFseoBE4/AagVtJeRE7g==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@lmdb/lmdb-win32-arm64": {
+      "version": "3.4.2",
+      "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.4.2.tgz",
+      "integrity": "sha512-SXWjdBfNDze4ZPeLtYIzsIeDJDJ/SdsA0pEXcUBayUIMO0FQBHfVZZyHXQjjHr4cvOAzANBgIiqaXRwfMhzmLw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@lmdb/lmdb-win32-x64": {
+      "version": "3.4.2",
+      "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.4.2.tgz",
+      "integrity": "sha512-IY+r3bxKW6Q6sIPiMC0L533DEfRJSXibjSI3Ft/w9Q8KQBNqEIvUFXt+09wV8S5BRk0a8uSF19YWxuRwEfI90g==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
+      "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
+      "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
+      "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
+      "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
+      "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
+      "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@noble/curves": {
+      "version": "1.9.7",
+      "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
+      "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==",
+      "license": "MIT",
+      "dependencies": {
+        "@noble/hashes": "1.8.0"
+      },
+      "engines": {
+        "node": "^14.21.3 || >=16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@noble/hashes": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+      "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+      "license": "MIT",
+      "engines": {
+        "node": "^14.21.3 || >=16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@opentelemetry/api": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
+      "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/@opentelemetry/api-logs": {
+      "version": "0.55.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.55.0.tgz",
+      "integrity": "sha512-3cpa+qI45VHYcA5c0bHM6VHo9gicv3p5mlLHNG3rLyjQU8b7e0st1rWtrUn3JbZ3DwwCfhKop4eQ9UuYlC6Pkg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@opentelemetry/context-async-hooks": {
+      "version": "1.30.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz",
+      "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/core": {
+      "version": "1.30.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz",
+      "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/semantic-conventions": "1.28.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/core/node_modules/@opentelemetry/semantic-conventions": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
+      "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@opentelemetry/exporter-logs-otlp-http": {
+      "version": "0.55.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.55.0.tgz",
+      "integrity": "sha512-fpFObWWq+DoLVrBU2dyMEaVkibByEkmKQZIUIjW/4j7lwIsTgW7aJCoD9RYFVB/tButcqov5Es2C0J2wTjM2tg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/api-logs": "0.55.0",
+        "@opentelemetry/core": "1.28.0",
+        "@opentelemetry/otlp-exporter-base": "0.55.0",
+        "@opentelemetry/otlp-transformer": "0.55.0",
+        "@opentelemetry/sdk-logs": "0.55.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/core": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.28.0.tgz",
+      "integrity": "sha512-ZLwRMV+fNDpVmF2WYUdBHlq0eOWtEaUJSusrzjGnBt7iSRvfjFE3RXYUZJrqou/wIDWV0DwQ5KIfYe9WXg9Xqw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/semantic-conventions": "1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/semantic-conventions": {
+      "version": "1.27.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
+      "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@opentelemetry/exporter-metrics-otlp-http": {
+      "version": "0.55.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.55.0.tgz",
+      "integrity": "sha512-3MqDNZzgXmLaiVo9gs9kCw/zPEaZYKIT0+jeMWscWHL/jrA9BNArTOYWUHEPabAQmWQ2BbvgNC7yzlqjoynQwA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.28.0",
+        "@opentelemetry/otlp-exporter-base": "0.55.0",
+        "@opentelemetry/otlp-transformer": "0.55.0",
+        "@opentelemetry/resources": "1.28.0",
+        "@opentelemetry/sdk-metrics": "1.28.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/core": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.28.0.tgz",
+      "integrity": "sha512-ZLwRMV+fNDpVmF2WYUdBHlq0eOWtEaUJSusrzjGnBt7iSRvfjFE3RXYUZJrqou/wIDWV0DwQ5KIfYe9WXg9Xqw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/semantic-conventions": "1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.28.0.tgz",
+      "integrity": "sha512-cIyXSVJjGeTICENN40YSvLDAq4Y2502hGK3iN7tfdynQLKWb3XWZQEkPc+eSx47kiy11YeFAlYkEfXwR1w8kfw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.28.0",
+        "@opentelemetry/semantic-conventions": "1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-metrics": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.28.0.tgz",
+      "integrity": "sha512-43tqMK/0BcKTyOvm15/WQ3HLr0Vu/ucAl/D84NO7iSlv6O4eOprxSHa3sUtmYkaZWHqdDJV0AHVz/R6u4JALVQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.28.0",
+        "@opentelemetry/resources": "1.28.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.3.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/semantic-conventions": {
+      "version": "1.27.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
+      "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@opentelemetry/exporter-trace-otlp-http": {
+      "version": "0.55.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.55.0.tgz",
+      "integrity": "sha512-lMiNic63EVHpW+eChmLD2CieDmwQBFi72+LFbh8+5hY0ShrDGrsGP/zuT5MRh7M/vM/UZYO/2A/FYd7CMQGR7A==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.28.0",
+        "@opentelemetry/otlp-exporter-base": "0.55.0",
+        "@opentelemetry/otlp-transformer": "0.55.0",
+        "@opentelemetry/resources": "1.28.0",
+        "@opentelemetry/sdk-trace-base": "1.28.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/core": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.28.0.tgz",
+      "integrity": "sha512-ZLwRMV+fNDpVmF2WYUdBHlq0eOWtEaUJSusrzjGnBt7iSRvfjFE3RXYUZJrqou/wIDWV0DwQ5KIfYe9WXg9Xqw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/semantic-conventions": "1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/resources": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.28.0.tgz",
+      "integrity": "sha512-cIyXSVJjGeTICENN40YSvLDAq4Y2502hGK3iN7tfdynQLKWb3XWZQEkPc+eSx47kiy11YeFAlYkEfXwR1w8kfw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.28.0",
+        "@opentelemetry/semantic-conventions": "1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/semantic-conventions": {
+      "version": "1.27.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
+      "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@opentelemetry/host-metrics": {
+      "version": "0.35.5",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/host-metrics/-/host-metrics-0.35.5.tgz",
+      "integrity": "sha512-Zf9Cjl7H6JalspnK5KD1+LLKSVecSinouVctNmUxRy+WP+20KwHq+qg4hADllkEmJ99MZByLLmEmzrr7s92V6g==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "systeminformation": "5.23.8"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/otlp-exporter-base": {
+      "version": "0.55.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.55.0.tgz",
+      "integrity": "sha512-iHQI0Zzq3h1T6xUJTVFwmFl5Dt5y1es+fl4kM+k5T/3YvmVyeYkSiF+wHCg6oKrlUAJfk+t55kaAu3sYmt7ZYA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.28.0",
+        "@opentelemetry/otlp-transformer": "0.55.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/core": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.28.0.tgz",
+      "integrity": "sha512-ZLwRMV+fNDpVmF2WYUdBHlq0eOWtEaUJSusrzjGnBt7iSRvfjFE3RXYUZJrqou/wIDWV0DwQ5KIfYe9WXg9Xqw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/semantic-conventions": "1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/semantic-conventions": {
+      "version": "1.27.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
+      "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@opentelemetry/otlp-transformer": {
+      "version": "0.55.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.55.0.tgz",
+      "integrity": "sha512-kVqEfxtp6mSN2Dhpy0REo1ghP4PYhC1kMHQJ2qVlO99Pc+aigELjZDfg7/YKmL71gR6wVGIeJfiql/eXL7sQPA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/api-logs": "0.55.0",
+        "@opentelemetry/core": "1.28.0",
+        "@opentelemetry/resources": "1.28.0",
+        "@opentelemetry/sdk-logs": "0.55.0",
+        "@opentelemetry/sdk-metrics": "1.28.0",
+        "@opentelemetry/sdk-trace-base": "1.28.0",
+        "protobufjs": "^7.3.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.28.0.tgz",
+      "integrity": "sha512-ZLwRMV+fNDpVmF2WYUdBHlq0eOWtEaUJSusrzjGnBt7iSRvfjFE3RXYUZJrqou/wIDWV0DwQ5KIfYe9WXg9Xqw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/semantic-conventions": "1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.28.0.tgz",
+      "integrity": "sha512-cIyXSVJjGeTICENN40YSvLDAq4Y2502hGK3iN7tfdynQLKWb3XWZQEkPc+eSx47kiy11YeFAlYkEfXwR1w8kfw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.28.0",
+        "@opentelemetry/semantic-conventions": "1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-metrics": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.28.0.tgz",
+      "integrity": "sha512-43tqMK/0BcKTyOvm15/WQ3HLr0Vu/ucAl/D84NO7iSlv6O4eOprxSHa3sUtmYkaZWHqdDJV0AHVz/R6u4JALVQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.28.0",
+        "@opentelemetry/resources": "1.28.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.3.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/semantic-conventions": {
+      "version": "1.27.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
+      "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@opentelemetry/propagator-b3": {
+      "version": "1.30.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.30.1.tgz",
+      "integrity": "sha512-oATwWWDIJzybAZ4pO76ATN5N6FFbOA1otibAVlS8v90B4S1wClnhRUk7K+2CHAwN1JKYuj4jh/lpCEG5BAqFuQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.30.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/propagator-jaeger": {
+      "version": "1.30.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.30.1.tgz",
+      "integrity": "sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.30.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/resource-detector-gcp": {
+      "version": "0.32.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.32.0.tgz",
+      "integrity": "sha512-+WdWSG4sZAfsk5DvRj/OUmatsHc+7Rdz8xdmxQdr1jpfUWjcKwOkGA4rondIf2ou/qPLOeYCs6hLLexsRdZaUw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.0.0",
+        "@opentelemetry/resources": "^1.10.0",
+        "@opentelemetry/semantic-conventions": "^1.27.0",
+        "gcp-metadata": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.0.0"
+      }
+    },
+    "node_modules/@opentelemetry/resources": {
+      "version": "1.30.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz",
+      "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.30.1",
+        "@opentelemetry/semantic-conventions": "1.28.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/semantic-conventions": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
+      "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@opentelemetry/sdk-logs": {
+      "version": "0.55.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.55.0.tgz",
+      "integrity": "sha512-TSx+Yg/d48uWW6HtjS1AD5x6WPfLhDWLl/WxC7I2fMevaiBuKCuraxTB8MDXieCNnBI24bw9ytyXrDCswFfWgA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/api-logs": "0.55.0",
+        "@opentelemetry/core": "1.28.0",
+        "@opentelemetry/resources": "1.28.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.4.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.28.0.tgz",
+      "integrity": "sha512-ZLwRMV+fNDpVmF2WYUdBHlq0eOWtEaUJSusrzjGnBt7iSRvfjFE3RXYUZJrqou/wIDWV0DwQ5KIfYe9WXg9Xqw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/semantic-conventions": "1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.28.0.tgz",
+      "integrity": "sha512-cIyXSVJjGeTICENN40YSvLDAq4Y2502hGK3iN7tfdynQLKWb3XWZQEkPc+eSx47kiy11YeFAlYkEfXwR1w8kfw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.28.0",
+        "@opentelemetry/semantic-conventions": "1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/semantic-conventions": {
+      "version": "1.27.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
+      "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@opentelemetry/sdk-metrics": {
+      "version": "1.30.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.30.1.tgz",
+      "integrity": "sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.30.1",
+        "@opentelemetry/resources": "1.30.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.3.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/sdk-trace-base": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.28.0.tgz",
+      "integrity": "sha512-ceUVWuCpIao7Y5xE02Xs3nQi0tOGmMea17ecBdwtCvdo9ekmO+ijc9RFDgfifMl7XCBf41zne/1POM3LqSTZDA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.28.0",
+        "@opentelemetry/resources": "1.28.0",
+        "@opentelemetry/semantic-conventions": "1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/core": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.28.0.tgz",
+      "integrity": "sha512-ZLwRMV+fNDpVmF2WYUdBHlq0eOWtEaUJSusrzjGnBt7iSRvfjFE3RXYUZJrqou/wIDWV0DwQ5KIfYe9WXg9Xqw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/semantic-conventions": "1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.28.0.tgz",
+      "integrity": "sha512-cIyXSVJjGeTICENN40YSvLDAq4Y2502hGK3iN7tfdynQLKWb3XWZQEkPc+eSx47kiy11YeFAlYkEfXwR1w8kfw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.28.0",
+        "@opentelemetry/semantic-conventions": "1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/semantic-conventions": {
+      "version": "1.27.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
+      "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@opentelemetry/sdk-trace-node": {
+      "version": "1.30.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.30.1.tgz",
+      "integrity": "sha512-cBjYOINt1JxXdpw1e5MlHmFRc5fgj4GW/86vsKFxJCJ8AL4PdVtYH41gWwl4qd4uQjqEL1oJVrXkSy5cnduAnQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/context-async-hooks": "1.30.1",
+        "@opentelemetry/core": "1.30.1",
+        "@opentelemetry/propagator-b3": "1.30.1",
+        "@opentelemetry/propagator-jaeger": "1.30.1",
+        "@opentelemetry/sdk-trace-base": "1.30.1",
+        "semver": "^7.5.2"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/sdk-trace-base": {
+      "version": "1.30.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz",
+      "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.30.1",
+        "@opentelemetry/resources": "1.30.1",
+        "@opentelemetry/semantic-conventions": "1.28.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/semantic-conventions": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
+      "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@opentelemetry/semantic-conventions": {
+      "version": "1.37.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.37.0.tgz",
+      "integrity": "sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@protobufjs/aspromise": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+      "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/base64": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+      "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/codegen": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+      "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/eventemitter": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+      "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/fetch": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+      "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@protobufjs/aspromise": "^1.1.1",
+        "@protobufjs/inquire": "^1.1.0"
+      }
+    },
+    "node_modules/@protobufjs/float": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+      "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/inquire": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+      "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/path": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+      "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/pool": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+      "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/utf8": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+      "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@scure/base": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
+      "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@scure/bip32": {
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz",
+      "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==",
+      "license": "MIT",
+      "dependencies": {
+        "@noble/curves": "~1.8.1",
+        "@noble/hashes": "~1.7.1",
+        "@scure/base": "~1.2.2"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@scure/bip32/node_modules/@noble/curves": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.2.tgz",
+      "integrity": "sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g==",
+      "license": "MIT",
+      "dependencies": {
+        "@noble/hashes": "1.7.2"
+      },
+      "engines": {
+        "node": "^14.21.3 || >=16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@scure/bip32/node_modules/@noble/hashes": {
+      "version": "1.7.2",
+      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.2.tgz",
+      "integrity": "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==",
+      "license": "MIT",
+      "engines": {
+        "node": "^14.21.3 || >=16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@scure/bip39": {
+      "version": "1.5.4",
+      "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz",
+      "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==",
+      "license": "MIT",
+      "dependencies": {
+        "@noble/hashes": "~1.7.1",
+        "@scure/base": "~1.2.4"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@scure/bip39/node_modules/@noble/hashes": {
+      "version": "1.7.2",
+      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.2.tgz",
+      "integrity": "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==",
+      "license": "MIT",
+      "engines": {
+        "node": "^14.21.3 || >=16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@tootallnate/once": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+      "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@types/caseless": {
+      "version": "0.12.5",
+      "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz",
+      "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/node": {
+      "version": "24.3.1",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz",
+      "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==",
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~7.10.0"
+      }
+    },
+    "node_modules/@types/request": {
+      "version": "2.48.13",
+      "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz",
+      "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/caseless": "*",
+        "@types/node": "*",
+        "@types/tough-cookie": "*",
+        "form-data": "^2.5.5"
+      }
+    },
+    "node_modules/@types/request/node_modules/form-data": {
+      "version": "2.5.5",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz",
+      "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.35",
+        "safe-buffer": "^5.2.1"
+      },
+      "engines": {
+        "node": ">= 0.12"
+      }
+    },
+    "node_modules/@types/tough-cookie": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
+      "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
+      "license": "MIT"
+    },
+    "node_modules/@viem/anvil": {
+      "version": "0.0.10",
+      "resolved": "https://registry.npmjs.org/@viem/anvil/-/anvil-0.0.10.tgz",
+      "integrity": "sha512-9PzYXBRikfSUhhm8Bd0avv07agwcbMJ5FaSu2D2vbE0cxkvXGtolL3fW5nz2yefMqOqVQL4XzfM5nwY81x3ytw==",
+      "license": "MIT",
+      "dependencies": {
+        "execa": "^7.1.1",
+        "get-port": "^6.1.2",
+        "http-proxy": "^1.18.1",
+        "ws": "^8.13.0"
+      }
+    },
+    "node_modules/abitype": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz",
+      "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/wevm"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.4",
+        "zod": "^3 >=3.22.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        },
+        "zod": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/abort-controller": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+      "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+      "license": "MIT",
+      "dependencies": {
+        "event-target-shim": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=6.5"
+      }
+    },
+    "node_modules/abstract-leveldown": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-7.2.0.tgz",
+      "integrity": "sha512-DnhQwcFEaYsvYDnACLZhMmCWd3rkOeEvglpa4q5i/5Jlm3UIsWaxVzuXvDLFCSCWRO3yy2/+V/G7FusFgejnfQ==",
+      "deprecated": "Superseded by abstract-level (https://github.com/Level/community#faq)",
+      "license": "MIT",
+      "dependencies": {
+        "buffer": "^6.0.3",
+        "catering": "^2.0.0",
+        "is-buffer": "^2.0.5",
+        "level-concat-iterator": "^3.0.0",
+        "level-supports": "^2.0.1",
+        "queue-microtask": "^1.2.3"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/accepts": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-types": "~2.1.34",
+        "negotiator": "0.6.3"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
+      "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/array-back": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz",
+      "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/array-flatten": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+      "license": "MIT"
+    },
+    "node_modules/arrify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
+      "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/async-retry": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz",
+      "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==",
+      "license": "MIT",
+      "dependencies": {
+        "retry": "0.13.1"
+      }
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/atomic-sleep": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
+      "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/axios": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
+      "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.4",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/base64-js": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/bignumber.js": {
+      "version": "9.3.1",
+      "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
+      "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/bindings": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+      "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+      "license": "MIT",
+      "dependencies": {
+        "file-uri-to-path": "1.0.0"
+      }
+    },
+    "node_modules/bintrees": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz",
+      "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==",
+      "license": "MIT"
+    },
+    "node_modules/bn.js": {
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz",
+      "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==",
+      "license": "MIT"
+    },
+    "node_modules/body-parser": {
+      "version": "1.20.3",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+      "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "3.1.2",
+        "content-type": "~1.0.5",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "1.2.0",
+        "http-errors": "2.0.0",
+        "iconv-lite": "0.4.24",
+        "on-finished": "2.4.1",
+        "qs": "6.13.0",
+        "raw-body": "2.5.2",
+        "type-is": "~1.6.18",
+        "unpipe": "1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/brorand": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+      "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==",
+      "license": "MIT"
+    },
+    "node_modules/buffer": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+      "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.2.1"
+      }
+    },
+    "node_modules/buffer-equal-constant-time": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+      "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/buffer-from": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+      "license": "MIT"
+    },
+    "node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/c-kzg": {
+      "version": "4.0.0-alpha.1",
+      "resolved": "https://registry.npmjs.org/c-kzg/-/c-kzg-4.0.0-alpha.1.tgz",
+      "integrity": "sha512-I8S9+c6OEaF6mD5OQJ/PylPk8C3TENQqvMomzV4u+NyOTdVOwF/VFj/z2o5OOPt930qkms0AbzXZ+Qu4qQCYxg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "dependencies": {
+        "bindings": "^1.5.0",
+        "node-addon-api": "^5.0.0"
+      }
+    },
+    "node_modules/cache-content-type": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz",
+      "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-types": "^2.1.18",
+        "ylru": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/catering": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/catering/-/catering-2.1.1.tgz",
+      "integrity": "sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/change-case": {
+      "version": "5.4.4",
+      "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz",
+      "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
+      "license": "MIT"
+    },
+    "node_modules/co": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+      "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
+      "license": "MIT",
+      "engines": {
+        "iojs": ">= 1.0.0",
+        "node": ">= 0.12.0"
+      }
+    },
+    "node_modules/co-body": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/co-body/-/co-body-6.2.0.tgz",
+      "integrity": "sha512-Kbpv2Yd1NdL1V/V4cwLVxraHDV6K8ayohr2rmH0J87Er8+zJjcTa6dAn9QMPC9CRgU8+aNajKbSf1TzDB1yKPA==",
+      "license": "MIT",
+      "dependencies": {
+        "@hapi/bourne": "^3.0.0",
+        "inflation": "^2.0.0",
+        "qs": "^6.5.2",
+        "raw-body": "^2.3.3",
+        "type-is": "^1.6.16"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "license": "MIT"
+    },
+    "node_modules/colorette": {
+      "version": "2.0.20",
+      "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+      "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+      "license": "MIT"
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/comlink": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz",
+      "integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/command-line-args": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz",
+      "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==",
+      "license": "MIT",
+      "dependencies": {
+        "array-back": "^3.1.0",
+        "find-replace": "^3.0.0",
+        "lodash.camelcase": "^4.3.0",
+        "typical": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "node_modules/command-line-usage": {
+      "version": "6.1.3",
+      "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz",
+      "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==",
+      "license": "MIT",
+      "dependencies": {
+        "array-back": "^4.0.2",
+        "chalk": "^2.4.2",
+        "table-layout": "^1.0.2",
+        "typical": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/command-line-usage/node_modules/ansi-styles": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^1.9.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/command-line-usage/node_modules/array-back": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz",
+      "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/command-line-usage/node_modules/chalk": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^3.2.1",
+        "escape-string-regexp": "^1.0.5",
+        "supports-color": "^5.3.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/command-line-usage/node_modules/color-convert": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "1.1.3"
+      }
+    },
+    "node_modules/command-line-usage/node_modules/color-name": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+      "license": "MIT"
+    },
+    "node_modules/command-line-usage/node_modules/has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/command-line-usage/node_modules/supports-color": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/command-line-usage/node_modules/typical": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz",
+      "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/commander": {
+      "version": "12.1.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
+      "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/compressible": {
+      "version": "2.0.18",
+      "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+      "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": ">= 1.43.0 < 2"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/content-disposition": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+      "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "5.2.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/convert-hex": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/convert-hex/-/convert-hex-0.1.0.tgz",
+      "integrity": "sha512-w20BOb1PiR/sEJdS6wNrUjF5CSfscZFUp7R9NSlXH8h2wynzXVEPFPJECAnkNylZ+cvf3p7TyRUHggDmrwXT9A=="
+    },
+    "node_modules/convert-string": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/convert-string/-/convert-string-0.1.0.tgz",
+      "integrity": "sha512-1KX9ESmtl8xpT2LN2tFnKSbV4NiarbVi8DVb39ZriijvtTklyrT+4dT1wsGMHKD3CJUjXgvJzstm9qL9ICojGA=="
+    },
+    "node_modules/cookie": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+      "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+      "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+      "license": "MIT"
+    },
+    "node_modules/cookies": {
+      "version": "0.9.1",
+      "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz",
+      "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "~2.0.0",
+        "keygrip": "~1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/copy-to": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz",
+      "integrity": "sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==",
+      "license": "MIT"
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/dateformat": {
+      "version": "4.6.3",
+      "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
+      "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/deep-equal": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
+      "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==",
+      "license": "MIT"
+    },
+    "node_modules/deep-extend": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+      "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/delegates": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+      "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
+      "license": "MIT"
+    },
+    "node_modules/depd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/destroy": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+      "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
+      "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/detect-node": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
+      "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
+      "license": "MIT"
+    },
+    "node_modules/dotenv": {
+      "version": "16.6.1",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+      "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/duplexify": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz",
+      "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==",
+      "license": "MIT",
+      "dependencies": {
+        "end-of-stream": "^1.4.1",
+        "inherits": "^2.0.3",
+        "readable-stream": "^3.1.1",
+        "stream-shift": "^1.0.2"
+      }
+    },
+    "node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+      "license": "MIT"
+    },
+    "node_modules/ecdsa-sig-formatter": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+      "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+      "license": "MIT"
+    },
+    "node_modules/elliptic": {
+      "version": "6.6.1",
+      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz",
+      "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==",
+      "license": "MIT",
+      "dependencies": {
+        "bn.js": "^4.11.9",
+        "brorand": "^1.1.0",
+        "hash.js": "^1.0.0",
+        "hmac-drbg": "^1.0.1",
+        "inherits": "^2.0.4",
+        "minimalistic-assert": "^1.0.1",
+        "minimalistic-crypto-utils": "^1.0.1"
+      }
+    },
+    "node_modules/elliptic/node_modules/bn.js": {
+      "version": "4.12.2",
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
+      "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
+      "license": "MIT"
+    },
+    "node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+      "license": "MIT"
+    },
+    "node_modules/encodeurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/end-of-stream": {
+      "version": "1.4.5",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+      "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+      "license": "MIT",
+      "dependencies": {
+        "once": "^1.4.0"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "license": "MIT"
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/event-target-shim": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+      "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/eventemitter3": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+      "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+      "license": "MIT"
+    },
+    "node_modules/execa": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz",
+      "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==",
+      "license": "MIT",
+      "dependencies": {
+        "cross-spawn": "^7.0.3",
+        "get-stream": "^6.0.1",
+        "human-signals": "^4.3.0",
+        "is-stream": "^3.0.0",
+        "merge-stream": "^2.0.0",
+        "npm-run-path": "^5.1.0",
+        "onetime": "^6.0.0",
+        "signal-exit": "^3.0.7",
+        "strip-final-newline": "^3.0.0"
+      },
+      "engines": {
+        "node": "^14.18.0 || ^16.14.0 || >=18.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/execa?sponsor=1"
+      }
+    },
+    "node_modules/express": {
+      "version": "4.21.2",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+      "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "~1.3.8",
+        "array-flatten": "1.1.1",
+        "body-parser": "1.20.3",
+        "content-disposition": "0.5.4",
+        "content-type": "~1.0.4",
+        "cookie": "0.7.1",
+        "cookie-signature": "1.0.6",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "1.3.1",
+        "fresh": "0.5.2",
+        "http-errors": "2.0.0",
+        "merge-descriptors": "1.0.3",
+        "methods": "~1.1.2",
+        "on-finished": "2.4.1",
+        "parseurl": "~1.3.3",
+        "path-to-regexp": "0.1.12",
+        "proxy-addr": "~2.0.7",
+        "qs": "6.13.0",
+        "range-parser": "~1.2.1",
+        "safe-buffer": "5.2.1",
+        "send": "0.19.0",
+        "serve-static": "1.16.2",
+        "setprototypeof": "1.2.0",
+        "statuses": "2.0.1",
+        "type-is": "~1.6.18",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+      "license": "MIT"
+    },
+    "node_modules/fast-copy": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz",
+      "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==",
+      "license": "MIT"
+    },
+    "node_modules/fast-redact": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz",
+      "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/fast-safe-stringify": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
+      "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
+      "license": "MIT"
+    },
+    "node_modules/fast-xml-parser": {
+      "version": "4.5.3",
+      "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz",
+      "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/NaturalIntelligence"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "strnum": "^1.1.1"
+      },
+      "bin": {
+        "fxparser": "src/cli/cli.js"
+      }
+    },
+    "node_modules/file-uri-to-path": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+      "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+      "license": "MIT"
+    },
+    "node_modules/finalhandler": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+      "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "2.6.9",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "on-finished": "2.4.1",
+        "parseurl": "~1.3.3",
+        "statuses": "2.0.1",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/find-replace": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz",
+      "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==",
+      "license": "MIT",
+      "dependencies": {
+        "array-back": "^3.0.1"
+      },
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.11",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/foreground-child": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+      "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+      "license": "ISC",
+      "dependencies": {
+        "cross-spawn": "^7.0.6",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/foreground-child/node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+      "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gaxios": {
+      "version": "6.7.1",
+      "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
+      "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "extend": "^3.0.2",
+        "https-proxy-agent": "^7.0.1",
+        "is-stream": "^2.0.0",
+        "node-fetch": "^2.6.9",
+        "uuid": "^9.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/gaxios/node_modules/is-stream": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+      "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/gaxios/node_modules/uuid": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+      "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
+    "node_modules/gcp-metadata": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
+      "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "gaxios": "^6.1.1",
+        "google-logging-utils": "^0.0.2",
+        "json-bigint": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-port": {
+      "version": "6.1.2",
+      "resolved": "https://registry.npmjs.org/get-port/-/get-port-6.1.2.tgz",
+      "integrity": "sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw==",
+      "license": "MIT",
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/get-stream": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+      "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/glob": {
+      "version": "11.0.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
+      "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
+      "license": "ISC",
+      "dependencies": {
+        "foreground-child": "^3.3.1",
+        "jackspeak": "^4.1.1",
+        "minimatch": "^10.0.3",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^2.0.0"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/google-auth-library": {
+      "version": "9.15.1",
+      "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
+      "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "base64-js": "^1.3.0",
+        "ecdsa-sig-formatter": "^1.0.11",
+        "gaxios": "^6.1.1",
+        "gcp-metadata": "^6.1.0",
+        "gtoken": "^7.0.0",
+        "jws": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/google-logging-utils": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
+      "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gtoken": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
+      "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
+      "license": "MIT",
+      "dependencies": {
+        "gaxios": "^6.0.0",
+        "jws": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hash.js": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
+      "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "minimalistic-assert": "^1.0.1"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/help-me": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
+      "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
+      "license": "MIT"
+    },
+    "node_modules/hmac-drbg": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+      "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
+      "license": "MIT",
+      "dependencies": {
+        "hash.js": "^1.0.3",
+        "minimalistic-assert": "^1.0.0",
+        "minimalistic-crypto-utils": "^1.0.1"
+      }
+    },
+    "node_modules/html-entities": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
+      "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/mdevils"
+        },
+        {
+          "type": "patreon",
+          "url": "https://patreon.com/mdevils"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/http-assert": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz",
+      "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==",
+      "license": "MIT",
+      "dependencies": {
+        "deep-equal": "~1.0.1",
+        "http-errors": "~1.8.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/http-assert/node_modules/depd": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+      "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/http-assert/node_modules/http-errors": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
+      "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "~1.1.2",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": ">= 1.5.0 < 2",
+        "toidentifier": "1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/http-assert/node_modules/statuses": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+      "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/http-errors": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+      "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "2.0.0",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": "2.0.1",
+        "toidentifier": "1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/http-proxy": {
+      "version": "1.18.1",
+      "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
+      "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
+      "license": "MIT",
+      "dependencies": {
+        "eventemitter3": "^4.0.0",
+        "follow-redirects": "^1.0.0",
+        "requires-port": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/http-proxy-agent": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+      "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+      "license": "MIT",
+      "dependencies": {
+        "@tootallnate/once": "2",
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/http-proxy-agent/node_modules/agent-base": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/http-proxy-agent/node_modules/debug": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+      "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/http-proxy-agent/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/https-proxy-agent/node_modules/debug": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+      "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/https-proxy-agent/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/human-signals": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz",
+      "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14.18.0"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/idb": {
+      "version": "8.0.3",
+      "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz",
+      "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==",
+      "license": "ISC"
+    },
+    "node_modules/idb-keyval": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz",
+      "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/ieee754": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/inflation": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.1.0.tgz",
+      "integrity": "sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/is-buffer": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
+      "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-generator-function": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
+      "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "get-proto": "^1.0.0",
+        "has-tostringtag": "^1.0.2",
+        "safe-regex-test": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-regex": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+      "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "gopd": "^1.2.0",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-stream": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+      "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+      "license": "MIT",
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "license": "ISC"
+    },
+    "node_modules/isows": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz",
+      "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/wevm"
+        }
+      ],
+      "license": "MIT",
+      "peerDependencies": {
+        "ws": "*"
+      }
+    },
+    "node_modules/jackspeak": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
+      "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/joycon": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
+      "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/js-sha3": {
+      "version": "0.9.3",
+      "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.9.3.tgz",
+      "integrity": "sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==",
+      "license": "MIT"
+    },
+    "node_modules/json-bigint": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
+      "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "bignumber.js": "^9.0.0"
+      }
+    },
+    "node_modules/json-stringify-deterministic": {
+      "version": "1.0.12",
+      "resolved": "https://registry.npmjs.org/json-stringify-deterministic/-/json-stringify-deterministic-1.0.12.tgz",
+      "integrity": "sha512-q3PN0lbUdv0pmurkBNdJH3pfFvOTL/Zp0lquqpvcjfKzt6Y0j49EPHAmVHCAS4Ceq/Y+PejWTzyiVpoY71+D6g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/jwa": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+      "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+      "license": "MIT",
+      "dependencies": {
+        "buffer-equal-constant-time": "^1.0.1",
+        "ecdsa-sig-formatter": "1.0.11",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/jws": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
+      "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
+      "license": "MIT",
+      "dependencies": {
+        "jwa": "^2.0.0",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/keygrip": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz",
+      "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==",
+      "license": "MIT",
+      "dependencies": {
+        "tsscmp": "1.0.6"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/koa": {
+      "version": "2.16.2",
+      "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.2.tgz",
+      "integrity": "sha512-+CCssgnrWKx9aI3OeZwroa/ckG4JICxvIFnSiOUyl2Uv+UTI+xIw0FfFrWS7cQFpoePpr9o8csss7KzsTzNL8Q==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "^1.3.5",
+        "cache-content-type": "^1.0.0",
+        "content-disposition": "~0.5.2",
+        "content-type": "^1.0.4",
+        "cookies": "~0.9.0",
+        "debug": "^4.3.2",
+        "delegates": "^1.0.0",
+        "depd": "^2.0.0",
+        "destroy": "^1.0.4",
+        "encodeurl": "^1.0.2",
+        "escape-html": "^1.0.3",
+        "fresh": "~0.5.2",
+        "http-assert": "^1.3.0",
+        "http-errors": "^1.6.3",
+        "is-generator-function": "^1.0.7",
+        "koa-compose": "^4.1.0",
+        "koa-convert": "^2.0.0",
+        "on-finished": "^2.3.0",
+        "only": "~0.0.2",
+        "parseurl": "^1.3.2",
+        "statuses": "^1.5.0",
+        "type-is": "^1.6.16",
+        "vary": "^1.1.2"
+      },
+      "engines": {
+        "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4"
+      }
+    },
+    "node_modules/koa-bodyparser": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/koa-bodyparser/-/koa-bodyparser-4.4.1.tgz",
+      "integrity": "sha512-kBH3IYPMb+iAXnrxIhXnW+gXV8OTzCu8VPDqvcDHW9SQrbkHmqPQtiZwrltNmSq6/lpipHnT7k7PsjlVD7kK0w==",
+      "license": "MIT",
+      "dependencies": {
+        "co-body": "^6.0.0",
+        "copy-to": "^2.0.1",
+        "type-is": "^1.6.18"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/koa-compose": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz",
+      "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==",
+      "license": "MIT"
+    },
+    "node_modules/koa-compress": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/koa-compress/-/koa-compress-5.1.1.tgz",
+      "integrity": "sha512-UgMIN7ZoEP2DuoSQmD6CYvFSLt0NReGlc2qSY4bO4Oq0L56OiD9pDG41Kj/zFmVY/A3Wvmn4BqKcfq5H30LGIg==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "^3.1.2",
+        "compressible": "^2.0.18",
+        "http-errors": "^1.8.1",
+        "koa-is-json": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/koa-compress/node_modules/depd": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+      "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/koa-compress/node_modules/http-errors": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
+      "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "~1.1.2",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": ">= 1.5.0 < 2",
+        "toidentifier": "1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/koa-compress/node_modules/statuses": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+      "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/koa-convert": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz",
+      "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==",
+      "license": "MIT",
+      "dependencies": {
+        "co": "^4.6.0",
+        "koa-compose": "^4.1.0"
+      },
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/koa-is-json": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/koa-is-json/-/koa-is-json-1.0.0.tgz",
+      "integrity": "sha512-+97CtHAlWDx0ndt0J8y3P12EWLwTLMXIfMnYDev3wOTwH/RpBGMlfn4bDXlMEg1u73K6XRE9BbUp+5ZAYoRYWw==",
+      "license": "MIT"
+    },
+    "node_modules/koa-router": {
+      "version": "12.0.1",
+      "resolved": "https://registry.npmjs.org/koa-router/-/koa-router-12.0.1.tgz",
+      "integrity": "sha512-gaDdj3GtzoLoeosacd50kBBTnnh3B9AYxDThQUo4sfUyXdOhY6ku1qyZKW88tQCRgc3Sw6ChXYXWZwwgjOxE0w==",
+      "deprecated": "Please use @koa/router instead, starting from v9! ",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.3.4",
+        "http-errors": "^2.0.0",
+        "koa-compose": "^4.1.0",
+        "methods": "^1.1.2",
+        "path-to-regexp": "^6.2.1"
+      },
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/koa-router/node_modules/debug": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+      "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/koa-router/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/koa-router/node_modules/path-to-regexp": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
+      "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
+      "license": "MIT"
+    },
+    "node_modules/koa/node_modules/debug": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+      "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/koa/node_modules/encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/koa/node_modules/http-errors": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
+      "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "~1.1.2",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": ">= 1.5.0 < 2",
+        "toidentifier": "1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/koa/node_modules/http-errors/node_modules/depd": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+      "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/koa/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/koa/node_modules/statuses": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+      "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/level-concat-iterator": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/level-concat-iterator/-/level-concat-iterator-3.1.0.tgz",
+      "integrity": "sha512-BWRCMHBxbIqPxJ8vHOvKUsaO0v1sLYZtjN3K2iZJsRBYtp+ONsY6Jfi6hy9K3+zolgQRryhIn2NRZjZnWJ9NmQ==",
+      "deprecated": "Superseded by abstract-level (https://github.com/Level/community#faq)",
+      "license": "MIT",
+      "dependencies": {
+        "catering": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/level-supports": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-2.1.0.tgz",
+      "integrity": "sha512-E486g1NCjW5cF78KGPrMDRBYzPuueMZ6VBXHT6gC7A8UYWGiM14fGgp+s/L1oFfDWSPV/+SFkYCmZ0SiESkRKA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/leveldown": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/leveldown/-/leveldown-6.1.1.tgz",
+      "integrity": "sha512-88c+E+Eizn4CkQOBHwqlCJaTNEjGpaEIikn1S+cINc5E9HEvJ77bqY4JY/HxT5u0caWqsc3P3DcFIKBI1vHt+A==",
+      "deprecated": "Superseded by classic-level (https://github.com/Level/community#faq)",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "dependencies": {
+        "abstract-leveldown": "^7.2.0",
+        "napi-macros": "~2.0.0",
+        "node-gyp-build": "^4.3.0"
+      },
+      "engines": {
+        "node": ">=10.12.0"
+      }
+    },
+    "node_modules/lmdb": {
+      "version": "3.4.2",
+      "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.4.2.tgz",
+      "integrity": "sha512-nwVGUfTBUwJKXd6lRV8pFNfnrCC1+l49ESJRM19t/tFb/97QfJEixe5DYRvug5JO7DSFKoKaVy7oGMt5rVqZvg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "dependencies": {
+        "msgpackr": "^1.11.2",
+        "node-addon-api": "^6.1.0",
+        "node-gyp-build-optional-packages": "5.2.2",
+        "ordered-binary": "^1.5.3",
+        "weak-lru-cache": "^1.2.2"
+      },
+      "bin": {
+        "download-lmdb-prebuilds": "bin/download-prebuilds.js"
+      },
+      "optionalDependencies": {
+        "@lmdb/lmdb-darwin-arm64": "3.4.2",
+        "@lmdb/lmdb-darwin-x64": "3.4.2",
+        "@lmdb/lmdb-linux-arm": "3.4.2",
+        "@lmdb/lmdb-linux-arm64": "3.4.2",
+        "@lmdb/lmdb-linux-x64": "3.4.2",
+        "@lmdb/lmdb-win32-arm64": "3.4.2",
+        "@lmdb/lmdb-win32-x64": "3.4.2"
+      }
+    },
+    "node_modules/lmdb/node_modules/node-addon-api": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
+      "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.camelcase": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+      "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.chunk": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/lodash.chunk/-/lodash.chunk-4.2.0.tgz",
+      "integrity": "sha512-ZzydJKfUHJwHa+hF5X66zLFCBrWn5GeF28OHEr4WVWtNDXlQ/IjWKPBiikqKo2ne0+v6JgCgJ0GzJp8k8bHC7w==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.clonedeep": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+      "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.clonedeepwith": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.clonedeepwith/-/lodash.clonedeepwith-4.5.0.tgz",
+      "integrity": "sha512-QRBRSxhbtsX1nc0baxSkkK5WlVTTm/s48DSukcGcWZwIyI8Zz+lB+kFiELJXtzfH4Aj6kMWQ1VWW4U5uUDgZMA==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isequal": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+      "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
+      "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
+      "license": "MIT"
+    },
+    "node_modules/lodash.merge": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.omit": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz",
+      "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==",
+      "deprecated": "This package is deprecated. Use destructuring assignment syntax instead.",
+      "license": "MIT"
+    },
+    "node_modules/lodash.pickby": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz",
+      "integrity": "sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.times": {
+      "version": "4.3.2",
+      "resolved": "https://registry.npmjs.org/lodash.times/-/lodash.times-4.3.2.tgz",
+      "integrity": "sha512-FfaJzl0SA35CRPDh5SWe2BTght6y5KSK7yJv166qIp/8q7qOwBDCvuDZE2RUSMRpBkLF6rZKbLEUoTmaP3qg6A==",
+      "license": "MIT"
+    },
+    "node_modules/long": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+      "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/lru-cache": {
+      "version": "11.2.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz",
+      "integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==",
+      "license": "ISC",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+      "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/merge-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+      "license": "MIT"
+    },
+    "node_modules/methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
+      "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
+      "license": "MIT",
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.54.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+      "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types/node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mimic-fn": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
+      "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/minimalistic-assert": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+      "license": "ISC"
+    },
+    "node_modules/minimalistic-crypto-utils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+      "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==",
+      "license": "MIT"
+    },
+    "node_modules/minimatch": {
+      "version": "10.0.3",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
+      "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
+      "license": "ISC",
+      "dependencies": {
+        "@isaacs/brace-expansion": "^5.0.0"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "license": "MIT"
+    },
+    "node_modules/msgpackr": {
+      "version": "1.11.5",
+      "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz",
+      "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==",
+      "license": "MIT",
+      "optionalDependencies": {
+        "msgpackr-extract": "^3.0.2"
+      }
+    },
+    "node_modules/msgpackr-extract": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
+      "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "node-gyp-build-optional-packages": "5.2.2"
+      },
+      "bin": {
+        "download-msgpackr-prebuilds": "bin/download-prebuilds.js"
+      },
+      "optionalDependencies": {
+        "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
+        "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
+        "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
+        "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
+        "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
+        "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
+      }
+    },
+    "node_modules/napi-macros": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz",
+      "integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==",
+      "license": "MIT"
+    },
+    "node_modules/negotiator": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/node-addon-api": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
+      "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
+      "license": "MIT"
+    },
+    "node_modules/node-fetch": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+      "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+      "license": "MIT",
+      "dependencies": {
+        "whatwg-url": "^5.0.0"
+      },
+      "engines": {
+        "node": "4.x || >=6.0.0"
+      },
+      "peerDependencies": {
+        "encoding": "^0.1.0"
+      },
+      "peerDependenciesMeta": {
+        "encoding": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/node-gyp-build": {
+      "version": "4.8.4",
+      "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
+      "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
+      "license": "MIT",
+      "bin": {
+        "node-gyp-build": "bin.js",
+        "node-gyp-build-optional": "optional.js",
+        "node-gyp-build-test": "build-test.js"
+      }
+    },
+    "node_modules/node-gyp-build-optional-packages": {
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
+      "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
+      "license": "MIT",
+      "dependencies": {
+        "detect-libc": "^2.0.1"
+      },
+      "bin": {
+        "node-gyp-build-optional-packages": "bin.js",
+        "node-gyp-build-optional-packages-optional": "optional.js",
+        "node-gyp-build-optional-packages-test": "build-test.js"
+      }
+    },
+    "node_modules/npm-run-path": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
+      "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^4.0.0"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/npm-run-path/node_modules/path-key": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+      "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/ohash": {
+      "version": "2.0.11",
+      "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
+      "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
+      "license": "MIT"
+    },
+    "node_modules/on-exit-leak-free": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
+      "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "license": "MIT",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/onetime": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
+      "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+      "license": "MIT",
+      "dependencies": {
+        "mimic-fn": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/only": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz",
+      "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ=="
+    },
+    "node_modules/ordered-binary": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.0.tgz",
+      "integrity": "sha512-IQh2aMfMIDbPjI/8a3Edr+PiOpcsB7yo8NdW7aHWVaoR/pcDldunMvnnwbk/auPGqmKeAdxtZl7MHX/QmPwhvQ==",
+      "license": "MIT"
+    },
+    "node_modules/ox": {
+      "version": "0.6.7",
+      "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.7.tgz",
+      "integrity": "sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/wevm"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@adraffy/ens-normalize": "^1.10.1",
+        "@noble/curves": "^1.6.0",
+        "@noble/hashes": "^1.5.0",
+        "@scure/bip32": "^1.5.0",
+        "@scure/bip39": "^1.4.0",
+        "abitype": "^1.0.6",
+        "eventemitter3": "5.0.1"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/ox/node_modules/eventemitter3": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+      "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+      "license": "MIT"
+    },
+    "node_modules/p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "license": "MIT",
+      "dependencies": {
+        "yocto-queue": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/package-json-from-dist": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+      "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+      "license": "BlueOak-1.0.0"
+    },
+    "node_modules/pako": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
+      "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
+      "license": "(MIT AND Zlib)"
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-scurry": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
+      "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^11.0.0",
+        "minipass": "^7.1.2"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/path-to-regexp": {
+      "version": "0.1.12",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+      "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+      "license": "MIT"
+    },
+    "node_modules/pino": {
+      "version": "9.9.4",
+      "resolved": "https://registry.npmjs.org/pino/-/pino-9.9.4.tgz",
+      "integrity": "sha512-d1XorUQ7sSKqVcYdXuEYs2h1LKxejSorMEJ76XoZ0pPDf8VzJMe7GlPXpMBZeQ9gE4ZPIp5uGD+5Nw7scxiigg==",
+      "license": "MIT",
+      "dependencies": {
+        "atomic-sleep": "^1.0.0",
+        "fast-redact": "^3.1.1",
+        "on-exit-leak-free": "^2.1.0",
+        "pino-abstract-transport": "^2.0.0",
+        "pino-std-serializers": "^7.0.0",
+        "process-warning": "^5.0.0",
+        "quick-format-unescaped": "^4.0.3",
+        "real-require": "^0.2.0",
+        "safe-stable-stringify": "^2.3.1",
+        "sonic-boom": "^4.0.1",
+        "thread-stream": "^3.0.0"
+      },
+      "bin": {
+        "pino": "bin.js"
+      }
+    },
+    "node_modules/pino-abstract-transport": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
+      "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
+      "license": "MIT",
+      "dependencies": {
+        "split2": "^4.0.0"
+      }
+    },
+    "node_modules/pino-pretty": {
+      "version": "13.1.1",
+      "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.1.tgz",
+      "integrity": "sha512-TNNEOg0eA0u+/WuqH0MH0Xui7uqVk9D74ESOpjtebSQYbNWJk/dIxCXIxFsNfeN53JmtWqYHP2OrIZjT/CBEnA==",
+      "license": "MIT",
+      "dependencies": {
+        "colorette": "^2.0.7",
+        "dateformat": "^4.6.3",
+        "fast-copy": "^3.0.2",
+        "fast-safe-stringify": "^2.1.1",
+        "help-me": "^5.0.0",
+        "joycon": "^3.1.1",
+        "minimist": "^1.2.6",
+        "on-exit-leak-free": "^2.1.0",
+        "pino-abstract-transport": "^2.0.0",
+        "pump": "^3.0.0",
+        "secure-json-parse": "^4.0.0",
+        "sonic-boom": "^4.0.1",
+        "strip-json-comments": "^5.0.2"
+      },
+      "bin": {
+        "pino-pretty": "bin.js"
+      }
+    },
+    "node_modules/pino-std-serializers": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz",
+      "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==",
+      "license": "MIT"
+    },
+    "node_modules/process-warning": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
+      "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/prom-client": {
+      "version": "15.1.3",
+      "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz",
+      "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/api": "^1.4.0",
+        "tdigest": "^0.1.1"
+      },
+      "engines": {
+        "node": "^16 || ^18 || >=20"
+      }
+    },
+    "node_modules/protobufjs": {
+      "version": "7.5.4",
+      "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
+      "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
+      "hasInstallScript": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@protobufjs/aspromise": "^1.1.2",
+        "@protobufjs/base64": "^1.1.2",
+        "@protobufjs/codegen": "^2.0.4",
+        "@protobufjs/eventemitter": "^1.1.0",
+        "@protobufjs/fetch": "^1.1.0",
+        "@protobufjs/float": "^1.0.2",
+        "@protobufjs/inquire": "^1.1.0",
+        "@protobufjs/path": "^1.1.2",
+        "@protobufjs/pool": "^1.1.0",
+        "@protobufjs/utf8": "^1.1.0",
+        "@types/node": ">=13.7.0",
+        "long": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "license": "MIT",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+      "license": "MIT"
+    },
+    "node_modules/pump": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+      "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+      "license": "MIT",
+      "dependencies": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.13.0",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+      "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "side-channel": "^1.0.6"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/quick-format-unescaped": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
+      "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
+      "license": "MIT"
+    },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+      "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "3.1.2",
+        "http-errors": "2.0.0",
+        "iconv-lite": "0.4.24",
+        "unpipe": "1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/real-require": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
+      "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 12.13.0"
+      }
+    },
+    "node_modules/reduce-flatten": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz",
+      "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/requires-port": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+      "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
+      "license": "MIT"
+    },
+    "node_modules/retry": {
+      "version": "0.13.1",
+      "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+      "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/retry-request": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz",
+      "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/request": "^2.48.8",
+        "extend": "^3.0.2",
+        "teeny-request": "^9.0.0"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/safe-regex-test": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+      "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "is-regex": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/safe-stable-stringify": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
+      "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "license": "MIT"
+    },
+    "node_modules/secp256k1": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-5.0.1.tgz",
+      "integrity": "sha512-lDFs9AAIaWP9UCdtWrotXWWF9t8PWgQDcxqgAnpM9rMqxb3Oaq2J0thzPVSxBwdJgyQtkU/sYtFtbM1RSt/iYA==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "dependencies": {
+        "elliptic": "^6.5.7",
+        "node-addon-api": "^5.0.0",
+        "node-gyp-build": "^4.2.0"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/secure-json-parse": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.0.0.tgz",
+      "integrity": "sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/semver": {
+      "version": "7.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+      "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/send": {
+      "version": "0.19.0",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+      "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "1.2.0",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "fresh": "0.5.2",
+        "http-errors": "2.0.0",
+        "mime": "1.6.0",
+        "ms": "2.1.3",
+        "on-finished": "2.4.1",
+        "range-parser": "~1.2.1",
+        "statuses": "2.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/send/node_modules/encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/send/node_modules/mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+      "license": "MIT",
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/send/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/serve-static": {
+      "version": "1.16.2",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+      "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+      "license": "MIT",
+      "dependencies": {
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.3",
+        "send": "0.19.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+      "license": "ISC"
+    },
+    "node_modules/sha256": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/sha256/-/sha256-0.2.0.tgz",
+      "integrity": "sha512-kTWMJUaez5iiT9CcMv8jSq6kMhw3ST0uRdcIWl3D77s6AsLXNXRp3heeqqfu5+Dyfu4hwpQnMzhqHh8iNQxw0w==",
+      "dependencies": {
+        "convert-hex": "~0.1.0",
+        "convert-string": "~0.1.0"
+      }
+    },
+    "node_modules/sha3": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/sha3/-/sha3-2.1.4.tgz",
+      "integrity": "sha512-S8cNxbyb0UGUM2VhRD4Poe5N58gJnJsLJ5vC7FYWGUmGhcsj4++WaIOBFVDxlG0W3To6xBuiRh+i0Qp2oNCOtg==",
+      "license": "MIT",
+      "dependencies": {
+        "buffer": "6.0.3"
+      }
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+      "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/signal-exit": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+      "license": "ISC"
+    },
+    "node_modules/sonic-boom": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
+      "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==",
+      "license": "MIT",
+      "dependencies": {
+        "atomic-sleep": "^1.0.0"
+      }
+    },
+    "node_modules/source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/source-map-support": {
+      "version": "0.5.21",
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+      "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+      "license": "MIT",
+      "dependencies": {
+        "buffer-from": "^1.0.0",
+        "source-map": "^0.6.0"
+      }
+    },
+    "node_modules/split2": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+      "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">= 10.x"
+      }
+    },
+    "node_modules/statuses": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+      "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/stream-events": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
+      "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==",
+      "license": "MIT",
+      "dependencies": {
+        "stubs": "^3.0.0"
+      }
+    },
+    "node_modules/stream-shift": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
+      "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==",
+      "license": "MIT"
+    },
+    "node_modules/string_decoder": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "~5.2.0"
+      }
+    },
+    "node_modules/string-format": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz",
+      "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==",
+      "license": "WTFPL OR MIT"
+    },
+    "node_modules/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "license": "MIT",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
+    "node_modules/string-width-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+      "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-final-newline": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+      "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/strip-json-comments": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz",
+      "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/strnum": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
+      "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/NaturalIntelligence"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/stubs": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
+      "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==",
+      "license": "MIT"
+    },
+    "node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/systeminformation": {
+      "version": "5.23.8",
+      "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.23.8.tgz",
+      "integrity": "sha512-Osd24mNKe6jr/YoXLLK3k8TMdzaxDffhpCxgkfgBHcapykIkd50HXThM3TCEuHO2pPuCsSx2ms/SunqhU5MmsQ==",
+      "license": "MIT",
+      "os": [
+        "darwin",
+        "linux",
+        "win32",
+        "freebsd",
+        "openbsd",
+        "netbsd",
+        "sunos",
+        "android"
+      ],
+      "bin": {
+        "systeminformation": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      },
+      "funding": {
+        "type": "Buy me a coffee",
+        "url": "https://www.buymeacoffee.com/systeminfo"
+      }
+    },
+    "node_modules/table-layout": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz",
+      "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==",
+      "license": "MIT",
+      "dependencies": {
+        "array-back": "^4.0.1",
+        "deep-extend": "~0.6.0",
+        "typical": "^5.2.0",
+        "wordwrapjs": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/table-layout/node_modules/array-back": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz",
+      "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/table-layout/node_modules/typical": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz",
+      "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/tdigest": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz",
+      "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==",
+      "license": "MIT",
+      "dependencies": {
+        "bintrees": "1.0.2"
+      }
+    },
+    "node_modules/teeny-request": {
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz",
+      "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "http-proxy-agent": "^5.0.0",
+        "https-proxy-agent": "^5.0.0",
+        "node-fetch": "^2.6.9",
+        "stream-events": "^1.0.5",
+        "uuid": "^9.0.0"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/teeny-request/node_modules/agent-base": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/teeny-request/node_modules/debug": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+      "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/teeny-request/node_modules/https-proxy-agent": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/teeny-request/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/teeny-request/node_modules/uuid": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+      "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
+    "node_modules/thread-stream": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
+      "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
+      "license": "MIT",
+      "dependencies": {
+        "real-require": "^0.2.0"
+      }
+    },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/tr46": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+      "license": "MIT"
+    },
+    "node_modules/ts-command-line-args": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/ts-command-line-args/-/ts-command-line-args-2.5.1.tgz",
+      "integrity": "sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==",
+      "license": "ISC",
+      "dependencies": {
+        "chalk": "^4.1.0",
+        "command-line-args": "^5.1.1",
+        "command-line-usage": "^6.1.0",
+        "string-format": "^2.0.0"
+      },
+      "bin": {
+        "write-markdown": "dist/write-markdown.js"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "license": "0BSD"
+    },
+    "node_modules/tsscmp": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
+      "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6.x"
+      }
+    },
+    "node_modules/type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "license": "MIT",
+      "dependencies": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/typical": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz",
+      "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/undici": {
+      "version": "5.29.0",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
+      "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
+      "license": "MIT",
+      "dependencies": {
+        "@fastify/busboy": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=14.0"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "7.10.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
+      "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
+      "license": "MIT"
+    },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "license": "MIT"
+    },
+    "node_modules/utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "node_modules/uuid": {
+      "version": "8.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/viem": {
+      "version": "2.23.7",
+      "resolved": "https://registry.npmjs.org/viem/-/viem-2.23.7.tgz",
+      "integrity": "sha512-Gbyz0uE3biWDPxECrEyzILWPsnIgDREgfRMuLSWHSSnM6ktefSC/lqQNImnxESdDEixa8/6EWXjmf2H6L9VV0A==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/wevm"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@noble/curves": "1.8.1",
+        "@noble/hashes": "1.7.1",
+        "@scure/bip32": "1.6.2",
+        "@scure/bip39": "1.5.4",
+        "abitype": "1.0.8",
+        "isows": "1.0.6",
+        "ox": "0.6.7",
+        "ws": "8.18.0"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.4"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/viem/node_modules/@noble/curves": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz",
+      "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@noble/hashes": "1.7.1"
+      },
+      "engines": {
+        "node": "^14.21.3 || >=16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/viem/node_modules/@noble/hashes": {
+      "version": "1.7.1",
+      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz",
+      "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==",
+      "license": "MIT",
+      "engines": {
+        "node": "^14.21.3 || >=16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/viem/node_modules/ws": {
+      "version": "8.18.0",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+      "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
+      "license": "MIT",
+      "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
+        }
+      }
+    },
+    "node_modules/weak-lru-cache": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz",
+      "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==",
+      "license": "MIT"
+    },
+    "node_modules/webidl-conversions": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/whatwg-url": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+      "license": "MIT",
+      "dependencies": {
+        "tr46": "~0.0.3",
+        "webidl-conversions": "^3.0.0"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/wordwrapjs": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz",
+      "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==",
+      "license": "MIT",
+      "dependencies": {
+        "reduce-flatten": "^2.0.0",
+        "typical": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/wordwrapjs/node_modules/typical": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz",
+      "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+      "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/ansi-styles": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+      "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "license": "ISC"
+    },
+    "node_modules/ws": {
+      "version": "8.18.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+      "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+      "license": "MIT",
+      "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
+        }
+      }
+    },
+    "node_modules/ylru": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.4.0.tgz",
+      "integrity": "sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4.0.0"
+      }
+    },
+    "node_modules/yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/zod": {
+      "version": "3.25.76",
+      "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+      "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/colinhacks"
+      }
+    }
+  }
+}

+ 16 - 0
aztec/package.json

@@ -0,0 +1,16 @@
+{
+  "dependencies": {
+    "@aztec/accounts": "^2.0.3",
+    "@aztec/noir-contracts.js": "^2.0.3",
+    "@aztec/pxe": "^2.0.3",
+    "@aztec/stdlib": "^2.0.3",
+    "dotenv": "^17.2.3",
+    "express": "^4.18.2",
+    "js-sha3": "^0.9.3",
+    "secp256k1": "^5.0.1"
+  },
+  "type": "module",
+  "scripts": {
+    "start-verification": "node vaa-verification-service.mjs"
+  }
+}

+ 4 - 0
aztec/packages/deploy/src/addresses.json

@@ -0,0 +1,4 @@
+{
+  "wormhole": "0x0e61ae3f9f51ae20042f48674e2bf1c19cde5c916ae3a5ed114d84c873cc9a8f",
+  "token": "0x08239f11b65e1c3034c344d4d6d83b9329bc0e6c72ccc43e8beb2566a84c27a4"
+}

+ 199 - 0
aztec/packages/deploy/src/deploy.mjs

@@ -0,0 +1,199 @@
+// src/deploy.mjs
+import { getInitialTestAccountsWallets } from '@aztec/accounts/testing';
+import { Contract, createPXEClient, loadContractArtifact, waitForPXE } from '@aztec/aztec.js';
+import WormholeJson from "../../../contracts/target/wormhole_contracts-Wormhole.json" assert { type: "json" };
+import { TokenContract } from '@aztec/noir-contracts.js/Token'; 
+import { writeFileSync, readFileSync } from 'fs';
+
+const WormholeJsonContractArtifact = loadContractArtifact(WormholeJson);
+
+const { PXE_URL = 'http://localhost:8090' } = process.env;
+
+// Call `aztec-nargo compile` to compile the contract
+// Call `aztec codegen ./src -o src/artifacts/` to generate the contract artifacts
+
+// Run first ``` aztec start --sandbox ```
+// then deploy the token with `node deploy_token.mjs`
+// copy the token address and receiver address into `main.nr::publish_message_in_private`
+// compile contract with `aztec nargo compile`
+// then run this script with ``` node deploy.mjs ```
+
+async function mintTokensToPublic(
+  token, // TokenContract
+  minterWallet, 
+  recipient,
+  amount
+) {
+  const tokenAsMinter = await TokenContract.at(token.address, minterWallet);
+  await tokenAsMinter.methods
+    .mint_to_public(recipient, amount)
+    .send()
+    .wait();
+}
+
+async function mintTokensToPrivate(
+  token, // TokenContract
+  minterWallet, 
+  recipient,
+  amount
+) {
+  const tokenAsMinter = await TokenContract.at(token.address, minterWallet);
+  await tokenAsMinter.methods
+    .mint_to_private(minterWallet.getAddress(), recipient, amount)
+    .send()
+    .wait();
+}
+
+
+async function main() {
+  const pxe = createPXEClient(PXE_URL);
+  await waitForPXE(pxe);
+
+  console.log(`Connected to PXE at ${PXE_URL}`);
+
+  const [ownerWallet, receiverWallet] = await getInitialTestAccountsWallets(pxe);
+  const ownerAddress = ownerWallet.getAddress();
+
+  console.log(`Owner address: ${ownerAddress}`);
+  console.log(`Receiver address: ${receiverWallet.getAddress()}`);
+
+  let token_address = JSON.parse(readFileSync("token_address.json", 'utf8'));
+
+  const token = await TokenContract.at(token_address.token_address, ownerWallet);
+
+  // Test parameters 
+  let wormhole_init_params = [
+    // Provider
+    1,1,
+    // wormhole owner account
+    receiverWallet.getAddress(),
+    // token address
+    token.address,
+  ];
+
+  const wormhole = await Contract.deploy(ownerWallet, WormholeJsonContractArtifact, wormhole_init_params, "init")
+    .send()
+    .deployed();
+
+  console.log(`Wormhole deployed at ${wormhole.address.toString()}`);
+
+  const addresses = { wormhole: wormhole.address.toString(), token: token.address.toString() };
+  writeFileSync('addresses.json', JSON.stringify(addresses, null, 2));
+
+  const contract = await Contract.at(wormhole.address, WormholeJsonContractArtifact, ownerWallet);
+
+  // The message to convert
+  let message = "Hello I am stavros vlach";
+
+  // Using TextEncoder (modern approach)
+  let encoder = new TextEncoder();
+  let bytes = encoder.encode(message);
+
+  // Create a padded array (try different sizes - this one is 31 bytes)
+  const PAYLOAD_SIZE = 31;
+  let paddedBytes = new Array(PAYLOAD_SIZE).fill(0);
+  
+  // Copy the message bytes into the padded array
+  for (let i = 0; i < bytes.length && i < PAYLOAD_SIZE; i++) {
+    paddedBytes[i] = bytes[i];
+  }
+  
+  let payload = [];
+  for (let i = 0; i < 8; i++) {
+    payload.push(paddedBytes);
+  }
+
+  console.log(`Calling publish_message with message "${message}" on wormhole contract...`);
+  console.log(`Payload: ${payload}`);
+
+  console.log(`Minting tokens to public...`);
+  await mintTokensToPublic(
+    token,
+    ownerWallet,
+    ownerAddress,
+    10000n
+  );
+
+  const msg_fee = 3n;
+  const nonce = 0n
+  const publish_private = true; // Set to true to publish in private, false for public
+
+  if (publish_private) {
+    console.log(`Performing payment in private...`);
+
+    console.log(`Minting tokens to private for owner...`);
+    await mintTokensToPrivate(
+      token,
+      ownerWallet,
+      ownerAddress,
+      10000n
+    );
+
+    const privateAction = token.methods.transfer_in_private(
+      ownerAddress,
+      receiverWallet.getAddress(),
+      msg_fee,
+      nonce
+    );
+    console.log(`${ownerAddress} is transferring ${msg_fee} tokens to ${receiverWallet.getAddress()} in private`);
+
+    const initWitness = await ownerWallet.createAuthWit({ 
+      caller: wormhole.address, 
+      action: privateAction 
+    });
+
+    console.log(`Generated private authwit`);
+
+    const _tx = await contract.methods.publish_message_in_private(100, payload, msg_fee, 2, ownerAddress, nonce).send({ authWitnesses: [initWitness] }).wait();
+    console.log(_tx);
+  } else {
+    console.log(`Publishing message in public...`);
+
+    // action to be taken using authwit
+    const tokenTransferAction = token.methods.transfer_in_public(
+      ownerAddress,
+      receiverWallet.getAddress(),
+      msg_fee,
+      nonce
+    );  
+    // generate authwit to allow for wormhole to send funds to itself on behalf of owner
+    const validateActionInteraction = await ownerWallet.setPublicAuthWit(
+      {
+        caller: wormhole.address,
+        action: tokenTransferAction
+      },
+      true
+    );
+
+    await validateActionInteraction.send().wait();
+
+    console.log(`Generated public authwit`);
+    
+    const _tx = await contract.methods.publish_message_in_public(100, payload, msg_fee,2, ownerAddress, nonce).send().wait();
+    console.log(_tx);
+  }
+
+  const sampleLogFilter = {
+    fromBlock: 0,
+    toBlock: 190,
+    contractAddress: '0x081a143b80470311c64f8fd1b67a074e2aa312bf5e22e6ebe0b17c5b3b44470b'
+  };
+
+  const logs = await pxe.getPublicLogs(sampleLogFilter);
+
+  console.log(logs.logs[0]);
+
+  const fromBlock = await pxe.getBlockNumber();
+  const logFilter = {
+    fromBlock,
+    toBlock: fromBlock + 1,
+  };
+  const publicLogs = (await pxe.getPublicLogs(logFilter)).logs;
+
+  console.log(publicLogs);
+}
+
+main().catch((err) => {
+  console.error(`Error in deployment script: ${err}`);
+  process.exit(1);
+});

+ 93 - 0
aztec/packages/deploy/src/deploy_token.mjs

@@ -0,0 +1,93 @@
+import { getInitialTestAccountsWallets } from '@aztec/accounts/testing';
+import { createPXEClient, waitForPXE } from '@aztec/aztec.js';
+import { TokenContract } from '@aztec/noir-contracts.js/Token'; 
+
+import { writeFileSync } from 'fs';
+
+const { PXE_URL = 'http://localhost:8090' } = process.env;
+
+// Call `aztec-nargo compile` to compile the contract
+// Call `aztec codegen ./src -o src/artifacts/` to generate the contract artifacts
+
+// Run first ``` aztec start --sandbox ```
+// then run this script with ``` node deploy.mjs ```
+
+
+// Following: https://docs.aztec.network/developers/tutorials/codealong/js_tutorials/aztecjs-getting-started#set-up-the-project
+async function deployToken(
+  adminWallet,
+  initialAdminBalance,
+) {
+  const contract = await TokenContract.deploy(
+    adminWallet,
+    adminWallet.getAddress(),
+    "ProverToken",
+    "PTZK",
+    18
+  )
+    .send()
+    .deployed();
+
+  if (initialAdminBalance > 0n) {
+    // Minter is minting to herself so contract as minter is the same as contract as recipient
+    await mintTokensToPublic(
+      contract,
+      adminWallet,
+      adminWallet.getAddress(),
+      initialAdminBalance
+    );
+  }
+
+  return contract;
+}
+
+async function mintTokensToPublic(
+  token, // TokenContract
+  minterWallet, 
+  recipient,
+  amount
+) {
+  const tokenAsMinter = await TokenContract.at(token.address, minterWallet);
+  await tokenAsMinter.methods
+    .mint_to_public(recipient, amount)
+    .send()
+    .wait();
+}
+
+async function mintTokensToPrivate(
+  token, // TokenContract
+  minterWallet, 
+  recipient,
+  amount
+) {
+  const tokenAsMinter = await TokenContract.at(token.address, minterWallet);
+  await tokenAsMinter.methods
+    .mint_to_private(minterWallet.getAddress(), recipient, amount)
+    .send()
+    .wait();
+}
+
+
+async function main() {
+  const pxe = createPXEClient(PXE_URL);
+  await waitForPXE(pxe);
+
+  console.log(`Connected to PXE at ${PXE_URL}`);
+
+  const [ownerWallet, receiverWallet] = await getInitialTestAccountsWallets(pxe);
+  const ownerAddress = ownerWallet.getAddress();
+
+  console.log(`Owner address: ${ownerAddress}`);
+  console.log(`Receiver address: ${receiverWallet.getAddress()}`);
+
+  let token = await deployToken(ownerWallet, 5000n);
+  console.log(`Deployed token contract at ${token.address}`)
+  
+  const address = { token_address: token.address.toString() };
+  writeFileSync('token_address.json', JSON.stringify(address, null, 2));
+}
+
+main().catch((err) => {
+  console.error(`Error in deployment script: ${err}`);
+  process.exit(1);
+});

+ 172 - 0
aztec/packages/deploy/src/derive_guardians.js

@@ -0,0 +1,172 @@
+// derive_guardians_fixed.js
+import secp256k1 from 'secp256k1';
+import pkg from 'js-sha3';
+const { keccak256 } = pkg;
+
+const guardianData = [
+  {
+    "name": "guardian-0",
+    "public": "0xbeFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe",
+    "private": "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0"
+  },
+  {
+    "name": "guardian-1",
+    "public": "0x88D7D8B32a9105d228100E72dFFe2Fae0705D31c",
+    "private": "c3b2e45c422a1602333a64078aeb42637370b0f48fe385f9cfa6ad54a8e0c47e"
+  },
+  {
+    "name": "guardian-2",
+    "public": "0x58076F561CC62A47087B567C86f986426dFCD000",
+    "private": "9f790d3f08bc4b5cd910d4278f3deb406e57bb5e924906ccd52052bb078ccd47"
+  },
+  {
+    "name": "guardian-3",
+    "public": "0xBd6e9833490F8fA87c733A183CD076a6cBD29074",
+    "private": "b20cc49d6f2c82a5e6519015fc18aa3e562867f85f872c58f1277cfbd2a0c8e4"
+  },
+  {
+    "name": "guardian-4",
+    "public": "0xb853FCF0a5C78C1b56D15fCE7a154e6ebe9ED7a2",
+    "private": "eded5a2fdcb5bbbfa5b07f2a91393813420e7ac30a72fc935b6df36f8294b855"
+  },
+  {
+    "name": "guardian-5",
+    "public": "0xAF3503dBD2E37518ab04D7CE78b630F98b15b78a",
+    "private": "00d39587c3556f289677a837c7f3c0817cb7541ce6e38a243a4bdc761d534c5e"
+  },
+  {
+    "name": "guardian-6",
+    "public": "0x785632deA5609064803B1c8EA8bB2c77a6004Bd1",
+    "private": "da534d61a8da77b232f3a2cee55c0125e2b3e33a5cd8247f3fe9e72379445c3b"
+  },
+  {
+    "name": "guardian-7",
+    "public": "0x09a281a698C0F5BA31f158585B41F4f33659e54D",
+    "private": "cdbabfc2118eb00bc62c88845f3bbd03cb67a9e18a055101588ca9b36387006c"
+  },
+  {
+    "name": "guardian-8",
+    "public": "0x3178443AB76a60E21690DBfB17f7F59F09Ae3Ea1",
+    "private": "c83d36423820e7350428dc4abe645cb2904459b7d7128adefe16472fdac397ba"
+  },
+  {
+    "name": "guardian-9",
+    "public": "0x647ec26ae49b14060660504f4DA1c2059E1C5Ab6",
+    "private": "1cbf4e1388b81c9020500fefc83a7a81f707091bb899074db1bfce4537428112"
+  },
+  {
+    "name": "guardian-10",
+    "public": "0x810AC3D8E1258Bd2F004a94Ca0cd4c68Fc1C0611",
+    "private": "17646a6ba14a541957fc7112cc973c0b3f04fce59484a92c09bb45a0b57eb740"
+  },
+  {
+    "name": "guardian-11",
+    "public": "0x80610e96d645b12f47ae5cf4546b18538739e90F",
+    "private": "eb94ff04accbfc8195d44b45e7c7da4c6993b2fbbfc4ef166a7675a905df9891"
+  },
+  {
+    "name": "guardian-12",
+    "public": "0x2edb0D8530E31A218E72B9480202AcBaeB06178d",
+    "private": "053a6527124b309d914a47f5257a995e9b0ad17f14659f90ed42af5e6e262b6a"
+  },
+  {
+    "name": "guardian-13",
+    "public": "0xa78858e5e5c4705CdD4B668FFe3Be5bae4867c9D",
+    "private": "3fbf1e46f6da69e62aed5670f279e818889aa7d8f1beb7fd730770fd4f8ea3d7"
+  },
+  {
+    "name": "guardian-14",
+    "public": "0x5Efe3A05Efc62D60e1D19fAeB56A80223CDd3472",
+    "private": "53b05697596ba04067e40be8100c9194cbae59c90e7870997de57337497172e9"
+  },
+  {
+    "name": "guardian-15",
+    "public": "0xD791b7D32C05aBB1cc00b6381FA0c4928f0c56fC",
+    "private": "4e95cb2ff3f7d5e963631ad85c28b1b79cb370f21c67cbdd4c2ffb0bf664aa06"
+  },
+  {
+    "name": "guardian-16",
+    "public": "0x14Bc029B8809069093D712A3fd4DfAb31963597e",
+    "private": "01b8c448ce2c1d43cfc5938d3a57086f88e3dc43bb8b08028ecb7a7924f4676f"
+  },
+  {
+    "name": "guardian-17",
+    "public": "0x246Ab29FC6EBeDf2D392a51ab2Dc5C59d0902A03",
+    "private": "1db31a6ba3bcd54d2e8a64f8a2415064265d291593450c6eb7e9a6a986bd9400"
+  },
+  {
+    "name": "guardian-18",
+    "public": "0x132A84dFD920b35a3D0BA5f7A0635dF298F9033e",
+    "private": "70d8f1c9534a0ab61a020366b831a494057a289441c07be67e4288c44bc6cd5d"
+  }
+];
+
+function derivedAddressFromPubKey(pubKey) {
+  // Remove 0x04 prefix, hash with keccak256, take last 20 bytes  
+  const publicKeyBytes = pubKey.slice(1); // Remove 0x04 prefix
+  const hash = keccak256(publicKeyBytes);
+  const hashBuffer = Buffer.from(hash, 'hex');
+  return hashBuffer.slice(-20); // Last 20 bytes for Ethereum address
+}
+
+console.log('// Generated Guardian Public Keys with Verification\n');
+console.log('[');
+
+let allValid = true;
+
+guardianData.forEach((guardian, index) => {
+  try {
+    const privKeyBuffer = Buffer.from(guardian.private, 'hex');
+    
+    // Validate private key length
+    if (privKeyBuffer.length !== 32) {
+      throw new Error(`Invalid private key length: ${privKeyBuffer.length}`);
+    }
+    
+    const pubKey = secp256k1.publicKeyCreate(privKeyBuffer, false); // uncompressed format
+    
+    // Verify the derived address matches the expected address
+    const derivedAddr = derivedAddressFromPubKey(pubKey);
+    const expectedAddr = Buffer.from(guardian.public.slice(2), 'hex');
+    
+    if (!derivedAddr.equals(expectedAddr)) {
+      console.error(`❌ Guardian ${index}: Address mismatch!`);
+      console.error(`   Expected: ${guardian.public}`);
+      console.error(`   Derived:  0x${derivedAddr.toString('hex')}`);
+      allValid = false;
+    } else {
+      console.log(`        // ✅ Guardian ${index}: ${guardian.public} (verified)`);
+    }
+    
+    // Remove the first byte (0x04) and split into X and Y coordinates
+    const x = pubKey.slice(1, 33); // X coordinate (32 bytes)
+    const y = pubKey.slice(33, 65); // Y coordinate (32 bytes)
+    
+    // Convert address to byte array
+    const addressHex = guardian.public.slice(2); // Remove '0x'
+    const addressBytes = Buffer.from(addressHex, 'hex');
+    
+    console.log(`        Guardian::new(`);
+    console.log(`            [${Array.from(addressBytes).map(b => `0x${b.toString(16).padStart(2, '0')}`).join(', ')}],`);
+    console.log(`            // Public key X`);
+    console.log(`            [${Array.from(x).map(b => `0x${b.toString(16).padStart(2, '0')}`).join(', ')}],`);
+    console.log(`            // Public key Y`);
+    console.log(`            [${Array.from(y).map(b => `0x${b.toString(16).padStart(2, '0')}`).join(', ')}]`);
+    console.log(`        )${index < guardianData.length - 1 ? ',' : ''}`);
+    
+  } catch (error) {
+    console.error(`❌ Error processing guardian ${index}:`, error);
+    allValid = false;
+  }
+});
+
+console.log('    ]');
+console.log('\n// Verification Summary:');
+console.log(`// Total guardians: ${guardianData.length}`);
+console.log(`// All addresses verified: ${allValid ? '✅ YES' : '❌ NO'}`);
+
+if (allValid) {
+  console.log('\n// 🎉 All guardian keys are valid! Copy the array above into your Rust code.');
+} else {
+  console.log('\n// ⚠️  Some guardian keys failed verification. Check the errors above.');
+}

+ 3 - 0
aztec/packages/deploy/src/nonce.json

@@ -0,0 +1,3 @@
+{
+  "token_nonce": "21"
+}

+ 131 - 0
aztec/packages/deploy/src/send-private-message.mjs

@@ -0,0 +1,131 @@
+// src/send-message.mjs
+import { getInitialTestAccountsWallets } from '@aztec/accounts/testing';
+import { Contract, createPXEClient, loadContractArtifact, waitForPXE } from '@aztec/aztec.js';
+import { readFileSync, writeFileSync } from 'fs';
+import WormholeJson from "../../../contracts/target/wormhole_contracts-Wormhole.json" assert { type: "json" };
+import { TokenContract } from '@aztec/noir-contracts.js/Token'; 
+
+const WormholeJsonContractArtifact = loadContractArtifact(WormholeJson);
+
+const { PXE_URL = 'http://localhost:8090' } = process.env;
+
+async function main() {
+    const pxe = createPXEClient(PXE_URL);
+    await waitForPXE(pxe);
+
+    console.log(`Connected to PXE at ${PXE_URL}`);
+
+    // Read the deployed contract address from addresses.json
+    let addresses;
+    try {
+        addresses = JSON.parse(readFileSync('addresses.json', 'utf8'));
+    } catch (error) {
+        console.error("Error reading addresses.json file:", error);
+        process.exit(1);
+    }
+    
+    if (!addresses.wormhole ||  !addresses.token) {
+        console.error("Wormhole or token contract address not found in addresses.json");
+        process.exit(1);
+    }
+
+    console.log("Addresses from addresses.json:", addresses);
+
+    const [ownerWallet, receiverWallet] = await getInitialTestAccountsWallets(pxe);
+    const ownerAddress = ownerWallet.getAddress();
+
+    // Connect to the already deployed contract
+    const contract = await Contract.at(addresses.wormhole, WormholeJsonContractArtifact, ownerWallet);
+    console.log(`Connected to Wormhole contract at ${addresses.wormhole}`);
+    
+    const token = await TokenContract.at(addresses.token, ownerWallet);
+    console.log(`Connected to Token contract at ${addresses.token}`);
+    
+    // The message to send
+    let message = "Hello World";
+
+    // Convert message to bytes
+    let encoder = new TextEncoder();
+    let messageBytes = encoder.encode(message);
+    
+    // Create a padded array (try different sizes - this one is 31 bytes)
+    const PAYLOAD_SIZE = 31;
+    let paddedBytes = new Array(PAYLOAD_SIZE).fill(0);
+    
+    // Copy the message bytes into the padded array
+    for (let i = 0; i < messageBytes.length && i < PAYLOAD_SIZE; i++) {
+        paddedBytes[i] = messageBytes[i];
+    }
+
+    let payloads = [];
+    for (let i = 0; i < 8; i++) {
+        payloads.push(paddedBytes);
+    }
+    
+    console.log(`Sending message: "${messageBytes} 8 times"`);
+    console.log(`Padded payload (${paddedBytes.length} bytes):`, payloads);
+    
+    console.log("Sending transaction...");
+
+    const msg_fee = 3n;
+    // get nonce and increment it
+    const nonce_file_data = JSON.parse(readFileSync('nonce.json', 'utf8'));
+
+    // Safe BigInt handling
+    const current_nonce = nonce_file_data.token_nonce
+        ? BigInt(nonce_file_data.token_nonce)
+        : 0n;
+
+    const token_nonce = current_nonce + 1n;
+
+    const new_nonce_data = { token_nonce: token_nonce.toString() };
+    
+    writeFileSync('nonce.json', JSON.stringify(new_nonce_data, null, 2));  
+    console.log(`Using token nonce: ${token_nonce}`);
+
+    console.log(`Taking payment in private...`);
+    const privateAction = token.methods.transfer_in_private(
+        ownerAddress,
+        receiverWallet.getAddress(),
+        msg_fee,
+        token_nonce
+    );
+    console.log(`${ownerAddress} is transferring ${msg_fee} tokens to ${receiverWallet.getAddress()} in private`);
+
+    const initWitness = await ownerWallet.createAuthWit({ 
+        caller: contract.address, 
+        action: privateAction 
+    });
+
+    console.log(`Generated private authwit`);
+
+    const tx = contract.methods.publish_message_in_private(100, payloads, msg_fee, 2, ownerAddress, token_nonce).send({ authWitnesses: [initWitness] });
+    
+    // Wait for the transaction to be mined
+    const receipt = await tx.wait();
+    console.log(`Transaction sent! Hash: ${receipt.txHash}`);
+    
+    const sampleLogFilter = {
+        fromBlock: 0,
+        toBlock: 190,
+        contractAddress: '0x081a143b80470311c64f8fd1b67a074e2aa312bf5e22e6ebe0b17c5b3b44470b'
+    };
+
+    const logs = await pxe.getPublicLogs(sampleLogFilter);
+
+    console.log(logs.logs[0]);
+
+    const fromBlock = await pxe.getBlockNumber();
+    const logFilter = {
+        fromBlock,
+        toBlock: fromBlock + 1,
+    };
+    const publicLogs = (await pxe.getPublicLogs(logFilter)).logs;
+
+    console.log(publicLogs);
+}
+
+main().catch((err) => {
+  console.error(`Error in message sending script: ${err}`);
+  process.exit(1);
+});

+ 137 - 0
aztec/packages/deploy/src/send-public-message.mjs

@@ -0,0 +1,137 @@
+// src/send-message.mjs
+import { getInitialTestAccountsWallets } from '@aztec/accounts/testing';
+import { Contract, createPXEClient, loadContractArtifact, waitForPXE } from '@aztec/aztec.js';
+import { readFileSync, writeFileSync } from 'fs';
+import WormholeJson from "../../../contracts/target/wormhole_contracts-Wormhole.json" assert { type: "json" };
+import { TokenContract } from '@aztec/noir-contracts.js/Token'; 
+
+const WormholeJsonContractArtifact = loadContractArtifact(WormholeJson);
+
+const { PXE_URL = 'http://localhost:8090' } = process.env;
+
+async function main() {
+  const pxe = createPXEClient(PXE_URL);
+  await waitForPXE(pxe);
+
+  console.log(`Connected to PXE at ${PXE_URL}`);
+
+  // Read the deployed contract address from addresses.json
+  let addresses;
+  try {
+    addresses = JSON.parse(readFileSync('addresses.json', 'utf8'));
+  } catch (error) {
+    console.error("Error reading addresses.json file:", error);
+    process.exit(1);
+  }
+  
+  if (!addresses.wormhole ||  !addresses.token) {
+    console.error("Wormhole or token contract address not found in addresses.json");
+    process.exit(1);
+  }
+
+  console.log("Addresses from addresses.json:", addresses);
+
+  const [ownerWallet, receiverWallet] = await getInitialTestAccountsWallets(pxe);
+
+  // Connect to the already deployed contract
+  const contract = await Contract.at(addresses.wormhole, WormholeJsonContractArtifact, ownerWallet);
+  console.log(`Connected to Wormhole contract at ${addresses.wormhole}`);
+  
+  const token = await TokenContract.at(addresses.token, ownerWallet);
+  console.log(`Connected to Token contract at ${addresses.token}`);
+  
+  // The message to send
+  let message = "Hello World";
+
+  // Convert message to bytes
+  let encoder = new TextEncoder();
+  let messageBytes = encoder.encode(message);
+  
+  // Create a padded array (try different sizes - this one is 31 bytes)
+  const PAYLOAD_SIZE = 31;
+  let paddedBytes = new Array(PAYLOAD_SIZE).fill(0);
+  
+  // Copy the message bytes into the padded array
+  for (let i = 0; i < messageBytes.length && i < PAYLOAD_SIZE; i++) {
+    paddedBytes[i] = messageBytes[i];
+  }
+
+  let payloads = [];
+  for (let i = 0; i < 8; i++) {
+    payloads.push(paddedBytes);
+  }
+  
+  console.log(`Sending message: "${messageBytes} 8 times"`);
+  console.log(`Padded payload (${paddedBytes.length} bytes):`, payloads);
+  
+  // Send the message with nonce 100 and consistency level 2
+  console.log("Sending transaction...");
+
+  const msg_fee = 3n;
+  // get nonce and increment it
+  const nonce_file_data = JSON.parse(readFileSync('nonce.json', 'utf8'));
+
+  // Safe BigInt handling
+  const current_nonce = nonce_file_data.token_nonce
+    ? BigInt(nonce_file_data.token_nonce)
+    : 0n;
+
+  const token_nonce = current_nonce + 1n;
+
+  const new_nonce_data = { token_nonce: token_nonce.toString() };
+  
+  writeFileSync('nonce.json', JSON.stringify(new_nonce_data, null, 2));  
+  console.log(`Using token nonce: ${token_nonce}`);
+
+  console.log(`Publishing message in public...`);
+
+  // action to be taken using authwit
+  const tokenTransferAction = token.methods.transfer_in_public(
+    ownerWallet.getAddress(),
+    receiverWallet.getAddress(),
+    msg_fee,
+    token_nonce
+  );  
+  // generate authwit to allow for wormhole to send funds to itself on behalf of owner
+  const validateActionInteraction = await ownerWallet.setPublicAuthWit(
+    {
+      caller: contract.address,
+      action: tokenTransferAction
+    },
+    true
+  );
+
+  await validateActionInteraction.send().wait();
+
+  console.log(`Generated public authwit`);
+  
+  const tx = contract.methods.publish_message_in_public(token_nonce, payloads, msg_fee,2, ownerWallet.getAddress(), token_nonce).send();
+  
+  // Wait for the transaction to be mined
+  const receipt = await tx.wait();
+  console.log(`Transaction sent! Hash: ${receipt.txHash}`);
+  
+  const sampleLogFilter = {
+    fromBlock: 0,
+    toBlock: 190,
+    contractAddress: '0x081a143b80470311c64f8fd1b67a074e2aa312bf5e22e6ebe0b17c5b3b44470b'
+  };
+
+  const logs = await pxe.getPublicLogs(sampleLogFilter);
+
+  console.log(logs.logs[0]);
+
+  const fromBlock = await pxe.getBlockNumber();
+  const logFilter = {
+    fromBlock,
+    toBlock: fromBlock + 1,
+  };
+  const publicLogs = (await pxe.getPublicLogs(logFilter)).logs;
+
+  console.log(publicLogs);
+}
+
+main().catch((err) => {
+  console.error(`Error in message sending script: ${err}`);
+  process.exit(1);
+});

+ 3 - 0
aztec/packages/deploy/src/token_address.json

@@ -0,0 +1,3 @@
+{
+  "token_address": "0x08239f11b65e1c3034c344d4d6d83b9329bc0e6c72ccc43e8beb2566a84c27a4"
+}

+ 87 - 0
aztec/readme.md

@@ -0,0 +1,87 @@
+# Wormhole Integration for Aztec Network
+
+This implementation demonstrates that **Aztec Network can be integrated into the Wormhole ecosystem**, enabling cross-chain messaging and access to Wormhole's multi-chain infrastructure.
+
+## What This Demonstrates
+
+This MVP showcases:
+- **VAA Verification on Aztec**: Complete Wormhole VAA parsing and signature verification in Noir
+- **Cross-Chain Message Reception**: Aztec can receive and verify messages from any Wormhole-supported chain
+- **Cross-Chain Message Sending**: Aztec can publish messages to the Wormhole network for cross-chain delivery
+
+**Bidirectional Integration**: This implementation provides full bidirectional messaging capabilities, with both private and public message publishing functions. This establishes the foundation for token bridges, NFT transfers, and arbitrary cross-chain data sharing.
+
+## Architecture
+
+- **Smart Contract** ([`contracts/src/main.nr`](./contracts/src/main.nr)): Noir implementation of VAA verification
+- **Verification Service** ([`vaa-verification-service.mjs`](./vaa-verification-service.mjs)): REST API server for testing
+- **Deployment Script** ([`contracts/deploy.sh`](./contracts/deploy.sh)): Automated testnet deployment
+
+## Key Technical Features
+
+- **VAA Parsing**: Extracts guardian signatures and message payload from VAA bytes
+- **ECDSA Verification**: secp256k1 signature validation using Aztec's cryptographic primitives  
+- **Guardian Management**: Configurable guardian set with signature verification
+- **Message Publishing**: Both private and public cross-chain message publishing to Wormhole network
+- **Wormhole Compatibility**: Full compliance with Wormhole message format and verification standards
+
+# Quick Start Guide
+
+## Prerequisites
+- Node.js v20+
+- Docker
+- [Aztec CLI tools](https://docs.aztec.network/developers/getting_started):
+  ```bash
+  bash -i <(curl -s https://install.aztec.network)
+  ```
+
+## 1. Deploy Contracts to Testnet
+
+You have two options for deployment:
+
+### Option A: Automated Deployment (Recommended)
+```bash
+cd contracts
+./deploy.sh
+```
+The deployment script will:
+- Set up wallets and accounts on Aztec testnet
+- Deploy a test token contract
+- Deploy the Wormhole VAA verification contract
+- Configure everything for testing
+
+### Option B: Manual Deployment
+For step-by-step manual deployment with full control, see [`contracts/DEPLOYMENT.md`](./contracts/DEPLOYMENT.md).
+
+## 2. Start the Verification Service
+```bash
+npm install
+npm run start-verification
+```
+
+## 3. Test VAA Verification
+Test with a real Wormhole VAA from Arbitrum Sepolia:
+```bash
+curl -X POST http://localhost:3000/test
+```
+
+Or verify a custom VAA:
+```bash
+curl -X POST http://localhost:3000/verify \
+  -H "Content-Type: application/json" \
+  -d '{"vaaBytes": "YOUR_VAA_HEX_HERE"}'
+```
+
+## 4. Monitor Results
+- Check service health: `GET http://localhost:3000/health`
+- View transactions: [Aztec Explorer](http://aztecscan.xyz/)
+
+# Current Implementation
+
+This MVP demonstrates **bidirectional Wormhole integration** on Aztec testnet with single guardian VAA verification, proving the technical feasibility of full cross-chain messaging. The implementation handles:
+
+- Complete VAA parsing, signature verification, and message extraction in Aztec's zero-knowledge environment
+- Cross-chain message publishing through both private and public functions
+- Integration with Wormhole's standard message format and verification protocols
+
+**Expandability**: This foundation supports scaling to full multi-guardian verification (13/19 consensus) and enhanced cross-chain functionality as Aztec moves toward mainnet.

+ 48 - 0
aztec/register-contract.mjs

@@ -0,0 +1,48 @@
+// register-contract.mjs - Register your deployed testnet contract with PXE
+import { createPXEClient, waitForPXE, loadContractArtifact, AztecAddress, Fr, Point } from '@aztec/aztec.js';
+import WormholeJson from "./contracts/target/wormhole_contracts-Wormhole.json" with { type: "json" };
+
+const PXE_URL = process.env.PXE_URL || 'http://localhost:8080';
+const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS || '0x0e61ae3f9f51ae20042f48674e2bf1c19cde5c916ae3a5ed114d84c873cc9a8f';
+
+async function registerDeployedContract() {
+  console.log('🔗 Connecting to PXE...');
+  const pxe = createPXEClient(PXE_URL);
+  await waitForPXE(pxe);
+  
+  console.log('📦 Loading contract artifact...');
+  const contractArtifact = loadContractArtifact(WormholeJson);
+  const contractAddress = AztecAddress.fromString(CONTRACT_ADDRESS);
+  
+  console.log('📡 Registering contract class...');
+  await pxe.registerContractClass(contractArtifact);
+  
+  console.log('🔍 Adding deployed contract instance...');
+  // For testnet contracts, we can add them with minimal instance data
+  await pxe.addContracts([
+    {
+      artifact: contractArtifact,
+      completeAddress: {
+        address: contractAddress,
+        publicKeysHash: Fr.ZERO,
+        partialAddress: Fr.ZERO
+      }
+    }
+  ]);
+  
+  console.log(`✅ Successfully registered deployed contract: ${CONTRACT_ADDRESS}`);
+  
+  // Verify it's registered
+  console.log('🔍 Verifying registration...');
+  const contracts = await pxe.getContracts();
+  const found = contracts.find(c => c.address.equals(contractAddress));
+  
+  if (found) {
+    console.log('✅ Contract found in PXE!');
+    console.log(`📍 Address: ${found.address}`);
+  } else {
+    console.log('❌ Contract not found in PXE after registration');
+  }
+}
+
+registerDeployedContract().catch(console.error);

+ 222 - 0
aztec/utils.mjs

@@ -0,0 +1,222 @@
+import { createLogger } from '@aztec/foundation/log';
+import { serializePrivateExecutionSteps } from '@aztec/stdlib/kernel';
+  
+import assert from 'node:assert';
+import { mkdir, writeFile } from 'node:fs/promises';
+import { join } from 'node:path';
+  
+  
+  const logger = createLogger('bench:profile_capture');
+  
+  const logLevel = ['silent', 'fatal', 'error', 'warn', 'info', 'verbose', 'debug', 'trace'];
+  
+  
+  const GATE_TYPES = [
+    'ecc_op',
+    'busread',
+    'lookup',
+    'pub_inputs',
+    'arithmetic',
+    'delta_range',
+    'elliptic',
+    'aux',
+    'poseidon2_external',
+    'poseidon2_internal',
+    'overflow',
+  ];
+  
+  
+  export class ProxyLogger {
+    static instance;
+    logs = [];
+  
+    constructor() {}
+  
+    static create() {
+      ProxyLogger.instance = new ProxyLogger();
+    }
+  
+    static getInstance() {
+      return ProxyLogger.instance;
+    }
+  
+    createLogger(prefix) {
+      return new Proxy(createLogger(prefix), {
+        get: (target, prop) => {
+          if (logLevel.includes(prop)) {
+            return function (...data) {
+              const loggingFn = prop;
+              const args = [loggingFn, prefix, ...data];
+              ProxyLogger.getInstance().handleLog(...args);
+              target[loggingFn].call(this, ...[data[0], data[1]]);
+            };
+          } else {
+            return target[prop];
+          }
+        },
+      });
+    }
+  
+    handleLog(type, prefix, message, data) {
+      this.logs.unshift({ type, prefix, message, data, timestamp: Date.now() });
+    }
+  
+    flushLogs() {
+      this.logs = [];
+    }
+  
+    getLogs() {
+      return this.logs;
+    }
+  }
+  
+
+  
+  function getMinimumTrace(logs) {
+    const minimumMessage = 'Minimum required block sizes for structured trace';
+    const minimumMessageIndex = logs.findIndex(log => log.message.includes(minimumMessage));
+    const candidateLogs = logs.slice(minimumMessageIndex - GATE_TYPES.length, minimumMessageIndex + 5);
+  
+    const traceLogs = candidateLogs
+      .filter(log => GATE_TYPES.some(type => log.message.includes(type)))
+      .map(log => log.message.split(/\t|\n/))
+      .flat()
+      .map(log => log.replace(/\(mem: .*\)/, '').trim())
+      .filter(Boolean);
+  
+    const traceSizes = traceLogs.map(log => {
+      const [gateType, gateSizeStr] = log
+        .replace(/\n.*\)$/, '')
+        .replace(/bb - /, '')
+        .split(':')
+        .map(s => s.trim());
+      const gateSize = parseInt(gateSizeStr);
+      assert(GATE_TYPES.includes(gateType), `Gate type ${gateType} is not recognized`);
+      return { [gateType]: gateSize };
+    });
+  
+    assert(traceSizes.length === GATE_TYPES.length, 'Decoded trace sizes do not match expected amount of gate types');
+    return traceSizes.reduce((acc, curr) => ({ ...acc, ...curr }), {});
+  }
+  
+  function getMaxMemory(logs) {
+    const candidateLogs = logs.slice(0, 100).filter(log => /\(mem: .*MiB\)/.test(log.message));
+    const usage = candidateLogs.map(log => {
+      const memStr = log ? log.message.slice(log.message.indexOf('(mem: ') + 6, log.message.indexOf('MiB') - 3) : '';
+      return memStr ? parseInt(memStr) : 0;
+    });
+    return Math.max(...usage);
+  }
+  
+  export function generateBenchmark(
+    flow,
+    logs,
+    stats,
+    privateExecutionSteps,
+    proverType,
+    error,
+  ) {
+    let maxMemory = 0;
+    let minimumTrace;
+    try {
+      minimumTrace = getMinimumTrace(logs);
+      maxMemory = getMaxMemory(logs);
+    } catch {
+      logger.warn(`Failed obtain minimum trace and max memory for ${flow}. Did you run with REAL_PROOFS=1?`);
+    }
+  
+    const steps = privateExecutionSteps.reduce((acc, step, i) => {
+      const previousAccGateCount = i === 0 ? 0 : acc[i - 1].accGateCount;
+      return [
+        ...acc,
+        {
+          functionName: step.functionName,
+          gateCount: step.gateCount,
+          accGateCount: previousAccGateCount + step.gateCount,
+          time: step.timings.witgen,
+          oracles: Object.entries(step.timings.oracles ?? {}).reduce(
+            (acc, [oracleName, oracleData]) => {
+              const total = oracleData.times.reduce((sum, time) => sum + time, 0);
+              const calls = oracleData.times.length;
+              acc[oracleName] = {
+                calls,
+                max: Math.max(...oracleData.times),
+                min: Math.min(...oracleData.times),
+                total,
+                avg: total / calls,
+              };
+              return acc;
+            },
+            {},
+          ),
+        },
+      ];
+    }, []);
+    const timings = stats.timings;
+    const totalGateCount = steps[steps.length - 1].accGateCount;
+    return {
+      name: flow,
+      timings: {
+        total: timings.total,
+        sync: timings.sync,
+        proving: timings.proving,
+        unaccounted: timings.unaccounted,
+        witgen: timings.perFunction.reduce((acc, fn) => acc + fn.time, 0),
+      },
+      rpc: Object.entries(stats.nodeRPCCalls ?? {}).reduce(
+        (acc, [RPCName, RPCCalls]) => {
+          const total = RPCCalls.times.reduce((sum, time) => sum + time, 0);
+          const calls = RPCCalls.times.length;
+          acc[RPCName] = {
+            calls,
+            max: Math.max(...RPCCalls.times),
+            min: Math.min(...RPCCalls.times),
+            total,
+            avg: total / calls,
+          };
+          return acc;
+        },
+        {},
+      ),
+      maxMemory,
+      proverType,
+      minimumTrace: minimumTrace,
+      totalGateCount: totalGateCount,
+      steps,
+      error,
+    };
+  }
+
+  export async function captureProfile(
+    label,
+    interaction,
+    opts,
+    expectedSteps,
+  ) {
+    // Make sure the proxy logger starts from a clean slate
+    ProxyLogger.getInstance().flushLogs();
+    const result = await interaction.profile({ ...opts, profileMode: 'full', skipProofGeneration: true });
+    const logs = ProxyLogger.getInstance().getLogs();
+    if (expectedSteps !== undefined && result.executionSteps.length !== expectedSteps) {
+      throw new Error(`Expected ${expectedSteps} execution steps, got ${result.executionSteps.length}`);
+    }
+    const benchmark = generateBenchmark(label, logs, result.stats, result.executionSteps, 'wasm', undefined);
+  
+    const ivcFolder = process.env.CAPTURE_IVC_FOLDER;
+    if (ivcFolder) {
+      logger.info(`Capturing client ivc execution profile for ${label}`);
+  
+      const resultsDirectory = join(ivcFolder, label);
+      logger.info(`Writing private execution steps to ${resultsDirectory}`);
+      await mkdir(resultsDirectory, { recursive: true });
+      // Write the client IVC files read by the prover.
+      const ivcInputsPath = join(resultsDirectory, 'ivc-inputs.msgpack');
+      await writeFile(ivcInputsPath, serializePrivateExecutionSteps(result.executionSteps));
+      await writeFile(join(resultsDirectory, 'logs.json'), JSON.stringify(logs, null, 2));
+      await writeFile(join(resultsDirectory, 'benchmark.json'), JSON.stringify(benchmark, null, 2));
+      logger.info(`Wrote private execution steps to ${resultsDirectory}`);
+    }
+  
+    return result;
+  }
+  

+ 358 - 0
aztec/vaa-verification-service.mjs

@@ -0,0 +1,358 @@
+// vaa-verification-service.mjs - TESTNET VERSION (FIXED)
+import express from 'express';
+import { SponsoredFeePaymentMethod, getContractInstanceFromInstantiationParams, Contract, loadContractArtifact, createAztecNodeClient, Fr, AztecAddress } from '@aztec/aztec.js';
+import { getSchnorrAccount } from '@aztec/accounts/schnorr';
+import { deriveSigningKey } from '@aztec/stdlib/keys';
+import { createPXEService, getPXEServiceConfig } from '@aztec/pxe/server';
+import { createStore } from "@aztec/kv-store/lmdb"
+import { SPONSORED_FPC_SALT } from '@aztec/constants';
+import { SponsoredFPCContract } from "@aztec/noir-contracts.js/SponsoredFPC";
+import WormholeJson from "./contracts/target/wormhole_contracts-Wormhole.json" with { type: "json" };
+import { ProxyLogger, captureProfile } from './utils.mjs';
+import dotenv from 'dotenv';
+
+// Load environment variables from .env file
+dotenv.config();
+
+const app = express();
+app.use(express.json());
+
+const PORT = process.env.PORT || 3000;
+
+// TESTNET CONFIGURATION
+const NODE_URL = process.env.NODE_URL || 'https://aztec-testnet-fullnode.zkv.xyz/';
+const PRIVATE_KEY = process.env.PRIVATE_KEY; // owner-wallet secret key from .env
+const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS || '0x0e61ae3f9f51ae20042f48674e2bf1c19cde5c916ae3a5ed114d84c873cc9a8f'; // Fresh Wormhole contract
+const SALT = process.env.SALT || '0x0000000000000000000000000000000000000000000000000000000000000000'; // Salt used in deployment
+
+let pxe, nodeClient, wormholeContract, paymentMethod, isReady = false;
+
+// Helper function to get the SponsoredFPC instance
+async function getSponsoredFPCInstance() {
+  return await getContractInstanceFromInstantiationParams(SponsoredFPCContract.artifact, {
+    salt: new Fr(SPONSORED_FPC_SALT),
+  });
+}
+
+// Initialize Aztec for Testnet
+async function init() {
+  console.log('🔄 Initializing Aztec TESTNET connection...');
+  
+  if (!PRIVATE_KEY) {
+    throw new Error('PRIVATE_KEY environment variable is required for testnet');
+  }
+  
+  if (!CONTRACT_ADDRESS) {
+    throw new Error('CONTRACT_ADDRESS environment variable is required for testnet');
+  }
+  
+  try {
+    // Create PXE and Node clients
+    nodeClient = createAztecNodeClient(NODE_URL);
+    const store = await createStore('pxe', {
+      dataDirectory: 'store',
+      dataStoreMapSizeKB: 1e6,
+    });
+    const config = getPXEServiceConfig();
+
+    const l1Contracts = await nodeClient.getL1ContractAddresses();
+    const configWithContracts = {
+      ...config,
+      l1Contracts,
+    };
+    ProxyLogger.create();
+    const proxyLogger = ProxyLogger.getInstance();
+    pxe = await createPXEService(nodeClient, configWithContracts, { 
+      store,  
+      loggers: {
+        prover: proxyLogger.createLogger('pxe:bb:wasm:bundle:proxied'),
+      } 
+    });
+    console.log('✅ Connected PXE to Aztec node and initialized');
+    
+    const sponsoredFPC = await getSponsoredFPCInstance();
+    await pxe.registerContract({
+      instance: sponsoredFPC,
+      artifact: SponsoredFPCContract.artifact,
+    });
+    paymentMethod = new SponsoredFeePaymentMethod(sponsoredFPC.address);
+
+    // Get contract instance from the node (Alex's simpler approach)
+    console.log('🔄 Fetching contract instance from node...');
+    const contractAddress = AztecAddress.fromString(CONTRACT_ADDRESS);
+    const contractInstance = await nodeClient.getContract(contractAddress);
+    
+    if (!contractInstance) {
+      throw new Error(`Contract instance not found at address ${CONTRACT_ADDRESS}`);
+    }
+    
+    console.log('✅ Contract instance retrieved from node');
+    console.log(`📍 Retrieved contract address: ${contractInstance.address}`);
+    console.log(`📍 Contract class ID: ${contractInstance.currentContractClassId}`);
+    
+    // Load contract artifact
+    const contractArtifact = loadContractArtifact(WormholeJson);
+    
+    // Register the contract with PXE (Alex's guidance)
+    console.log('🔄 Registering contract with PXE...');
+    await pxe.registerContract({
+      instance: contractInstance,
+      artifact: contractArtifact
+    });
+    
+    console.log('✅ Contract registered with PXE');
+    
+    // Create account using the deployed owner-wallet credentials
+    console.log('🔄 Setting up owner-wallet account...');
+    const secretKey = Fr.fromString(PRIVATE_KEY);
+    const salt = Fr.fromString(SALT);
+    const signingKey = deriveSigningKey(secretKey);
+    
+    console.log(`🔑 Using secret key: ${secretKey.toString()}`);
+    console.log(`🧂 Using salt: ${salt.toString()}`);
+    
+    // Create Schnorr account (this account is already deployed on testnet)
+    const schnorrAccount = await getSchnorrAccount(pxe, secretKey, signingKey, salt);
+    const accountAddress = schnorrAccount.getAddress();
+    console.log(`📍 Account address: ${accountAddress}`);
+    
+    // This account should already be registered with the PXE from the deployment
+    const registeredAccounts = await pxe.getRegisteredAccounts();
+    const isRegistered = registeredAccounts.some(acc => acc.address.equals(accountAddress));
+    
+    if (isRegistered) {
+      console.log('✅ Account found in PXE (from aztec-wallet deployment)');
+    } else {
+      console.log('⚠️  Account not in PXE, but it exists on testnet. Getting wallet anyway...');
+    }
+    
+    // Get wallet (this should work since the account exists on testnet)
+    const wallet = await schnorrAccount.register();
+    console.log(`✅ Using wallet: ${wallet.getAddress()}`);
+    // Now create the contract object
+    console.log(`🔄 Creating contract instance at ${contractAddress.toString()}...`);
+    console.log(`📍 Contract artifact name: ${contractArtifact.name}`);
+    
+    try {
+      wormholeContract = await Contract.at(contractAddress, contractArtifact, wallet);
+      console.log(`✅ Contract instance created successfully`);
+      console.log(`📍 Final contract address: ${wormholeContract.address.toString()}`);
+      
+    } catch (error) {
+      console.error('❌ Failed to create contract instance:', error);
+      throw error;
+    }
+    
+    isReady = true;
+    console.log(`✅ Connected to Wormhole contract on TESTNET: ${CONTRACT_ADDRESS}`);
+    console.log(`✅ Node URL: ${NODE_URL}`);
+    
+  } catch (error) {
+    console.error('❌ Initialization failed:', error);
+    throw error;
+  }
+}
+
+// Health check
+app.get('/health', (req, res) => {
+  res.json({ 
+    status: isReady ? 'healthy' : 'initializing',
+    network: 'testnet',
+    timestamp: new Date().toISOString(),
+    nodeUrl: NODE_URL,
+    contractAddress: CONTRACT_ADDRESS,
+    walletAddress: 'using PXE accounts'
+  });
+});
+
+// Verify VAA
+app.post('/verify', async (req, res) => {
+  if (!isReady) {
+    return res.status(503).json({ 
+      success: false, 
+      error: 'Service not ready - Aztec testnet connection still initializing' 
+    });
+  }
+
+  try {
+    const { vaaBytes } = req.body;
+    
+    if (!vaaBytes) {
+      return res.status(400).json({
+        success: false,
+        error: 'vaaBytes is required'
+      });
+    }
+    
+    // Convert hex to buffer
+    const hexString = vaaBytes.startsWith('0x') ? vaaBytes.slice(2) : vaaBytes;
+    const vaaBuffer = Buffer.from(hexString, 'hex');
+    
+    // Pad to 2000 bytes for contract but pass actual length
+    const paddedVAA = Buffer.alloc(2000);
+    vaaBuffer.copy(paddedVAA, 0, 0, Math.min(vaaBuffer.length, 2000));
+    
+    // Convert to array for Aztec contract
+    const vaaArray = Array.from(paddedVAA);
+    const actualLength = vaaBuffer.length;
+    
+    console.log(`🔍 Verifying VAA on TESTNET (${vaaBuffer.length} bytes actual, ${paddedVAA.length} bytes padded)`);
+    console.log(`📍 Contract: ${CONTRACT_ADDRESS}`);
+    console.log(`📍 Contract object address: ${wormholeContract.address.toString()}`);
+    console.log(`📍 Wallet address: ${wormholeContract.wallet.getAddress().toString()}`);
+    
+    // Call verify_vaa function with padded bytes and actual length
+    console.log('🔄 Calling contract method verify_vaa...');
+    const tx = await wormholeContract.methods
+      .verify_vaa(vaaArray, actualLength)
+      .send({ 
+        from: wormholeContract.wallet.getAddress(),
+        fee: { paymentMethod } 
+      })
+      .wait();
+    
+    console.log(`✅ VAA verified successfully on TESTNET: ${tx.txHash}`);
+    
+    res.json({
+      success: true,
+      network: 'testnet',
+      txHash: tx.txHash,
+      contractAddress: CONTRACT_ADDRESS,
+      message: 'VAA verified successfully on Aztec testnet',
+      processedAt: new Date().toISOString()
+    });
+    
+  } catch (error) {
+    console.error('❌ VAA verification failed on TESTNET:', error.message);
+    res.status(500).json({
+      success: false,
+      network: 'testnet',
+      error: error.message,
+      processedAt: new Date().toISOString()
+    });
+  }
+});
+
+// Test endpoint with Jorge's real Arbitrum Sepolia VAA
+app.post('/test', async (req, res) => {
+  // Jorge's real VAA from Arbitrum Sepolia that uses Guardian 0x13947Bd48b18E53fdAeEe77F3473391aC727C638
+  // This VAA contains "Hello Wormhole!" message and has been verified on Wormholescan
+  // Link: https://wormholescan.io/#/tx/0xf93fd41efeb09ff28174824d4abf6dbc06ac408953a9975aa4a403d434051efc?network=Testnet&view=advanced
+  const realVAA = "010000000001004682bc4d5ff2e54dc2ee5e0eb64f5c6c07aa449ac539abc63c2be5c306a48f233e9300170a82adf3c3b7f43f23176fb079174a58d67d142477f646675d86eb6301684bfad4499602d22713000000000000000000000000697f31e074bf2c819391d52729f95506e0a72ffb0000000000000000c8000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000e48656c6c6f20576f726d686f6c6521000000000000000000000000000000000000";
+  
+  console.log('🧪 Testing with real Arbitrum Sepolia VAA on TESTNET');
+  console.log('📍 Guardian: 0x13947Bd48b18E53fdAeEe77F3473391aC727C638');
+  console.log('📍 Signature: 0x4682bc4d5ff2e54dc2ee5e0eb64f5c6c07aa449ac539abc63c2be5c306a48f233e9300170a82adf3c3b7f43f23176fb079174a58d67d142477f646675d86eb6301');
+  console.log('📍 Expected message hash: 0xe64320fba193c98f2d0acf3a8c7479ec9b163192bfc19d4024497d4e4159758c');
+  console.log('📍 WormholeScan: https://wormholescan.io/#/tx/0xf93fd41efeb09ff28174824d4abf6dbc06ac408953a9975aa4a403d434051efc?network=Testnet&view=advanced');
+  
+  // Debug contract state before calling verify
+  console.log('🔍 Pre-verification debug:');
+  console.log(`   - Service ready: ${isReady}`);
+  console.log(`   - Contract object exists: ${!!wormholeContract}`);
+  if (wormholeContract) {
+    console.log(`   - Contract address: ${wormholeContract.address.toString()}`);
+    console.log(`   - Expected address: ${CONTRACT_ADDRESS}`);
+  }
+  
+  // Set up request body and call verify logic directly
+  const testReq = { 
+    body: { vaaBytes: realVAA },
+    // Add debug flag
+    isTest: true
+  };
+  
+  // Call verify logic directly instead of using the router
+  if (!isReady) {
+    return res.status(503).json({ 
+      success: false, 
+      error: 'Service not ready - Aztec testnet connection still initializing' 
+    });
+  }
+
+  try {
+  const { vaaBytes } = testReq.body;
+  
+  // Convert hex to buffer
+  const hexString = vaaBytes.startsWith('0x') ? vaaBytes.slice(2) : vaaBytes;
+  const vaaBuffer = Buffer.from(hexString, 'hex');
+  
+  // Debug the VAA data
+  console.log('🔍 VAA Debug Info:');
+  console.log(`   Raw hex length: ${hexString.length}`);
+  console.log(`   Buffer length: ${vaaBuffer.length}`);
+  console.log(`   First 20 bytes: ${vaaBuffer.slice(0, 20).toString('hex')}`);
+  console.log(`   Last 20 bytes: ${vaaBuffer.slice(-20).toString('hex')}`);
+  
+  // Back to padded version (contract expects fixed size)
+  const paddedVAA = Buffer.alloc(2000);
+  vaaBuffer.copy(paddedVAA, 0, 0, Math.min(vaaBuffer.length, 2000));
+  const vaaArray = Array.from(paddedVAA);
+  const actualLength = vaaBuffer.length;
+  
+  console.log('🔍 Using PADDED version (contract expects fixed size):');
+  console.log(`   Padded array length: ${vaaArray.length}`);
+  console.log(`   Actual VAA length param: ${actualLength}`);
+  console.log(`   First few padded elements: [${vaaArray.slice(0, 10).join(', ')}]`);
+  console.log(`   Elements around actual length: [${vaaArray.slice(actualLength-5, actualLength+10).join(', ')}]`);
+  
+  console.log(`🔍 Verifying VAA on TESTNET (${vaaBuffer.length} bytes actual, ${paddedVAA.length} bytes padded)`);
+  console.log(`📍 Contract: ${CONTRACT_ADDRESS}`);
+  console.log(`📍 Contract object address: ${wormholeContract.address.toString()}`);
+  console.log(`📍 Wallet address: ${wormholeContract.wallet.getAddress().toString()}`);
+  
+  // Call verify_vaa function with padded bytes and actual length
+  console.log('🔄 Calling contract method verify_vaa with PADDED data...');
+  const interaction = await wormholeContract.methods
+      .verify_vaa(vaaArray, actualLength);
+
+  //console.log('🔄 Capturing interaction profile...');
+  //await captureProfile('verify_vaa', interaction);
+
+  console.log('🔄 Sending transaction...');
+  const tx = await interaction.send({ 
+    from: wormholeContract.wallet.getAddress(),
+    fee: { paymentMethod } 
+  }).wait();
+  
+  console.log(`✅ VAA verified successfully on TESTNET: ${tx.txHash}`);
+  
+  res.json({
+    success: true,
+    network: 'testnet',
+    txHash: tx.txHash,
+    contractAddress: CONTRACT_ADDRESS,
+    message: 'VAA verified successfully on Aztec testnet (TEST ENDPOINT)',
+    processedAt: new Date().toISOString()
+  });
+  } catch (error) {
+    console.error('❌ VAA verification failed on TESTNET:', error.message);
+    console.error('❌ Full error:', error);
+    res.status(500).json({
+      success: false,
+      network: 'testnet',
+      error: error.message,
+      processedAt: new Date().toISOString()
+    });
+  }
+});
+
+// Start server
+init().then(() => {
+  app.listen(PORT, () => {
+    console.log(`🚀 VAA Verification Service running on port ${PORT}`);
+    console.log(`🌐 Network: TESTNET`);
+    console.log(`📡 Node: ${NODE_URL}`);
+    console.log(`📄 Contract: ${CONTRACT_ADDRESS}`);
+    console.log('Available endpoints:');
+    console.log('  GET  /health - Health check');
+    console.log('  POST /verify - Verify VAA on testnet');
+    console.log('  POST /test   - Test with Jorge\'s real Arbitrum Sepolia VAA');
+  });
+}).catch(error => {
+  console.error('❌ Failed to start testnet service:', error);
+  console.log('\n📝 Required environment variables:');
+  console.log('  PRIVATE_KEY=your_testnet_private_key');
+  console.log('  CONTRACT_ADDRESS=your_deployed_contract_address');
+  process.exit(1);
+});

+ 3 - 0
cspell-custom-words.txt

@@ -125,6 +125,7 @@ monad
 Monad
 Monad
 moonscan
 moonscan
 moretags
 moretags
+nargo
 Neodyme
 Neodyme
 nhooyr
 nhooyr
 obsv
 obsv
@@ -140,6 +141,7 @@ permissionlessly
 Polkachu
 Polkachu
 Polkadot
 Polkadot
 Positionals
 Positionals
+postprocess
 prefunded
 prefunded
 promauto
 promauto
 proto
 proto
@@ -179,6 +181,7 @@ SnaxChain
 solana
 solana
 Solana
 Solana
 Solana's
 Solana's
+sponsoredfpc
 spydk
 spydk
 Starport
 Starport
 statesync
 statesync

+ 78 - 0
devnet/aztec-devnet.yaml

@@ -0,0 +1,78 @@
+apiVersion: v1
+kind: Service
+metadata:
+  labels:
+    app: aztec-sandbox
+  name: aztec-sandbox
+spec:
+  ports:
+    - name: rpc
+      port: 8090
+      targetPort: 8090
+    - name: anvil        
+      port: 8549        
+      targetPort: 8545   
+  selector:
+    app: aztec-sandbox
+---
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+  labels:
+    app: aztec-sandbox
+  name: aztec-sandbox
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: aztec-sandbox
+  serviceName: aztec-sandbox
+  template:
+    metadata:
+      labels:
+        app: aztec-sandbox
+    spec:
+      containers:
+        - name: aztec-sandbox
+          image: aztecprotocol/aztec:1.2.1
+          command: ["bash"]
+          args:
+            - -c
+            - "anvil --host 0.0.0.0 --silent & node --no-warnings /usr/src/yarn-project/aztec/dest/bin/index.js start --sandbox"
+          env:
+            - name: LOG_LEVEL
+              value: "info"
+            - name: ETHEREUM_HOSTS
+              value: "http://0.0.0.0:8545"
+            - name: L1_CHAIN_ID
+              value: "31337"
+            - name: ARCHIVER_POLLING_INTERVAL_MS
+              value: "50"
+            - name: P2P_BLOCK_CHECK_INTERVAL_MS
+              value: "50"
+            - name: SEQ_TX_POLLING_INTERVAL_MS
+              value: "50"
+            - name: WS_BLOCK_CHECK_INTERVAL_MS
+              value: "50"
+            - name: ARCHIVER_VIEM_POLLING_INTERVAL_MS
+              value: "500"
+            - name: PXE_PORT
+              value: "8090"
+            - name: PORT
+              value: "8090"
+            - name: AZTEC_PORT
+              value: "8090"
+            - name: TEST_ACCOUNTS
+              value: "true"
+            - name: P2P_BOOTSTRAP
+              value: "1"
+          ports:
+            - containerPort: 8090
+              name: node
+              protocol: TCP
+            - containerPort: 8545
+              name: anvil
+              protocol: TCP
+          readinessProbe:
+            tcpSocket:
+              port: node

+ 2 - 1
devnet/node.yaml

@@ -149,7 +149,8 @@ spec:
             - --ccqAllowedRequesters=beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe,25021A4FCAf61F2EADC8202D3833Df48B2Fa0D54
             - --ccqAllowedRequesters=beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe,25021A4FCAf61F2EADC8202D3833Df48B2Fa0D54
             - --ccqAllowedPeers=12D3KooWSnju8zhywCYVi2JwTqky1sySPnmtYLsHHzc4WerMnDQH,12D3KooWM6WqedfR6ehtTd1y6rJu3ZUrEkTjcJJnJZYesjd89zj8
             - --ccqAllowedPeers=12D3KooWSnju8zhywCYVi2JwTqky1sySPnmtYLsHHzc4WerMnDQH,12D3KooWM6WqedfR6ehtTd1y6rJu3ZUrEkTjcJJnJZYesjd89zj8
             - --transferVerifierEnabledChainIDs=2
             - --transferVerifierEnabledChainIDs=2
-            # - --logLevel=debug
+            - --notaryEnabled=true
+            - --logLevel=warn
           securityContext:
           securityContext:
             capabilities:
             capabilities:
               add:
               add:

+ 5 - 1
ethereum/Dockerfile

@@ -1,7 +1,11 @@
 # syntax=docker.io/docker/dockerfile:1.3@sha256:42399d4635eddd7a9b8a24be879d2f9a930d0ed040a61324cfdf59ef1357b3b2
 # syntax=docker.io/docker/dockerfile:1.3@sha256:42399d4635eddd7a9b8a24be879d2f9a930d0ed040a61324cfdf59ef1357b3b2
 FROM const-gen AS const-export
 FROM const-gen AS const-export
 FROM --platform=linux/amd64 ghcr.io/foundry-rs/foundry:v1.0.0@sha256:d12a373ec950de170d5461014ef9320ba0fb6e0db6f87835999d0fcf3820370e as foundry
 FROM --platform=linux/amd64 ghcr.io/foundry-rs/foundry:v1.0.0@sha256:d12a373ec950de170d5461014ef9320ba0fb6e0db6f87835999d0fcf3820370e as foundry
-FROM node:22.16-bullseye-slim@sha256:550b434f7edc3a1875860657a3e306752358029c957280809ae6395ab296faeb
+FROM --platform=linux/amd64 node:22.16-bullseye-slim@sha256:550b434f7edc3a1875860657a3e306752358029c957280809ae6395ab296faeb
+
+RUN echo "Acquire::http::Pipeline-Depth 0;" >> /etc/apt/apt.conf.d/fixbadproxy
+RUN echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/fixbadproxy
+RUN echo "Acquire::BrokenProxy    true;" >> /etc/apt/apt.conf.d/fixbadproxy
 
 
 # npm wants to clone random Git repositories - lovely.
 # npm wants to clone random Git repositories - lovely.
 # RUN apk add git python make build-base
 # RUN apk add git python make build-base

+ 7 - 0
ethereum/contracts/cache/fuzz/failures

@@ -0,0 +1,7 @@
+# Seeds for failure cases proptest has generated in the past. It is
+# automatically read and these particular cases re-run before any
+# novel cases are generated.
+#
+# It is recommended to check this file in to source control so that
+# everyone who runs the test benefits from these saved cases.
+cc 119d48c0efbc444bd71a651077dbfae69252e7b715832343062b7004573ac476 # shrinks to 0xbc2e66d3000000000000000000000000000000000000000000000000000000000000ffffc6668601e5fa57a6f401bded162402724a614e95c883de8039eb3f6d495db412

+ 1 - 0
ethereum/contracts/cache/test-failures

@@ -0,0 +1 @@
+testChainId_KEVM|testEvmChainId_KEVM|testGetExpireGuardianSet_KEVM|testGetGovernanceActionConsumed_KEVM|testGetGovernanceContract_KEVM|testGetGuardianSetIndex_KEVM|testGetMessageFee_KEVM|testGovernanceChainId_KEVM|testIsInitialized_KEVM|testNextSequence_KEVM|testParseContractUpgradeWrongAction_KEVM|testParseContractUpgrade_KEVM|testParseRecoverChainIdWrongAction_KEVM|testParseRecoverChainId_KEVM|testParseSetMessageFeeWrongAction_KEVM|testParseSetMessageFee_KEVM|testParseTransferFeesWrongAction_KEVM|testParseTransferFees_KEVM|testExpireGuardianSet_KEVM|testSetChainId_KEVM|testSetEvmChainId_Revert_KEVM|testSetEvmChainId_Success_KEVM|testSetGovernanceActionConsumed_KEVM|testSetGovernanceChainId_KEVM|testSetGovernanceContract_KEVM|testSetInitialized_KEVM|testSetMessageFee_KEVM|testSetNextSequence_KEVM|testUpdateGuardianSetIndex_KEVM|testInitialize_after_setup_revert_KEVM|testSetup_after_setup_revert_KEVM

+ 28 - 1
node/cmd/guardiand/node.go

@@ -23,6 +23,7 @@ import (
 
 
 	"github.com/certusone/wormhole/node/pkg/watchers/algorand"
 	"github.com/certusone/wormhole/node/pkg/watchers/algorand"
 	"github.com/certusone/wormhole/node/pkg/watchers/aptos"
 	"github.com/certusone/wormhole/node/pkg/watchers/aptos"
+	"github.com/certusone/wormhole/node/pkg/watchers/aztec"
 	"github.com/certusone/wormhole/node/pkg/watchers/evm"
 	"github.com/certusone/wormhole/node/pkg/watchers/evm"
 	"github.com/certusone/wormhole/node/pkg/watchers/near"
 	"github.com/certusone/wormhole/node/pkg/watchers/near"
 	"github.com/certusone/wormhole/node/pkg/watchers/solana"
 	"github.com/certusone/wormhole/node/pkg/watchers/solana"
@@ -142,6 +143,9 @@ var (
 	aptosAccount *string
 	aptosAccount *string
 	aptosHandle  *string
 	aptosHandle  *string
 
 
+	aztecRPC      *string
+	aztecContract *string
+
 	movementRPC     *string
 	movementRPC     *string
 	movementAccount *string
 	movementAccount *string
 	movementHandle  *string
 	movementHandle  *string
@@ -294,7 +298,8 @@ var (
 	txVerifierChains []vaa.ChainID
 	txVerifierChains []vaa.ChainID
 
 
 	// featureFlags are additional static flags that should be published in P2P heartbeats.
 	// featureFlags are additional static flags that should be published in P2P heartbeats.
-	featureFlags []string
+	featureFlags  []string
+	notaryEnabled *bool
 )
 )
 
 
 func init() {
 func init() {
@@ -390,6 +395,9 @@ func init() {
 	aptosAccount = NodeCmd.Flags().String("aptosAccount", "", "aptos account")
 	aptosAccount = NodeCmd.Flags().String("aptosAccount", "", "aptos account")
 	aptosHandle = NodeCmd.Flags().String("aptosHandle", "", "aptos handle")
 	aptosHandle = NodeCmd.Flags().String("aptosHandle", "", "aptos handle")
 
 
+	aztecRPC = node.RegisterFlagWithValidationOrFail(NodeCmd, "aztecRPC", "Aztec RPC URL", "", []string{"http", "https"})
+	aztecContract = NodeCmd.Flags().String("aztecContract", "", "aztec contract")
+
 	movementRPC = node.RegisterFlagWithValidationOrFail(NodeCmd, "movementRPC", "Movement RPC URL", "", []string{"http", "https"})
 	movementRPC = node.RegisterFlagWithValidationOrFail(NodeCmd, "movementRPC", "Movement RPC URL", "", []string{"http", "https"})
 	movementAccount = NodeCmd.Flags().String("movementAccount", "", "movement account")
 	movementAccount = NodeCmd.Flags().String("movementAccount", "", "movement account")
 	movementHandle = NodeCmd.Flags().String("movementHandle", "", "movement handle")
 	movementHandle = NodeCmd.Flags().String("movementHandle", "", "movement handle")
@@ -526,6 +534,8 @@ func init() {
 	subscribeToVAAs = NodeCmd.Flags().Bool("subscribeToVAAs", false, "Guardiand should subscribe to incoming signed VAAs, set to true if running a public RPC node")
 	subscribeToVAAs = NodeCmd.Flags().Bool("subscribeToVAAs", false, "Guardiand should subscribe to incoming signed VAAs, set to true if running a public RPC node")
 
 
 	transferVerifierEnabledChainIDs = NodeCmd.Flags().UintSlice("transferVerifierEnabledChainIDs", make([]uint, 0), "Transfer Verifier will be enabled for these chain IDs (comma-separated)")
 	transferVerifierEnabledChainIDs = NodeCmd.Flags().UintSlice("transferVerifierEnabledChainIDs", make([]uint, 0), "Transfer Verifier will be enabled for these chain IDs (comma-separated)")
+
+	notaryEnabled = NodeCmd.Flags().Bool("notaryEnabled", false, "Run the notary")
 }
 }
 
 
 var (
 var (
@@ -640,6 +650,7 @@ func runNode(cmd *cobra.Command, args []string) {
 	}
 	}
 
 
 	// Override the default go-log config, which uses a magic environment variable.
 	// Override the default go-log config, which uses a magic environment variable.
+	logger.Info("setting level for all loggers", zap.String("level", logger.Level().String()))
 	ipfslog.SetAllLoggers(lvl)
 	ipfslog.SetAllLoggers(lvl)
 
 
 	if viper.ConfigFileUsed() != "" {
 	if viper.ConfigFileUsed() != "" {
@@ -945,6 +956,10 @@ func runNode(cmd *cobra.Command, args []string) {
 		logger.Fatal("If coinGeckoApiKey is set, then chainGovernorEnabled must be set")
 		logger.Fatal("If coinGeckoApiKey is set, then chainGovernorEnabled must be set")
 	}
 	}
 
 
+	if !argsConsistent([]string{*aztecRPC, *aztecContract}) {
+		logger.Fatal("Either --aztecRPC and --aztecContract must all be set or all unset")
+	}
+
 	// NOTE: If this flag isn't set, or the list is empty, Transfer Verifier should not be enabled.
 	// NOTE: If this flag isn't set, or the list is empty, Transfer Verifier should not be enabled.
 	if len(*transferVerifierEnabledChainIDs) != 0 {
 	if len(*transferVerifierEnabledChainIDs) != 0 {
 		var parseErr error
 		var parseErr error
@@ -1041,6 +1056,7 @@ func runNode(cmd *cobra.Command, args []string) {
 	rpcMap["monadRPC"] = *monadRPC
 	rpcMap["monadRPC"] = *monadRPC
 	rpcMap["movementRPC"] = *movementRPC
 	rpcMap["movementRPC"] = *movementRPC
 	rpcMap["mezoRPC"] = *mezoRPC
 	rpcMap["mezoRPC"] = *mezoRPC
+	rpcMap["aztecRPC"] = *aztecRPC
 	rpcMap["convergeRPC"] = *convergeRPC
 	rpcMap["convergeRPC"] = *convergeRPC
 	rpcMap["plumeRPC"] = *plumeRPC
 	rpcMap["plumeRPC"] = *plumeRPC
 	rpcMap["xrplevmRPC"] = *xrplEvmRPC
 	rpcMap["xrplevmRPC"] = *xrplEvmRPC
@@ -1668,6 +1684,16 @@ func runNode(cmd *cobra.Command, args []string) {
 		watcherConfigs = append(watcherConfigs, wc)
 		watcherConfigs = append(watcherConfigs, wc)
 	}
 	}
 
 
+	if shouldStart(aztecRPC) {
+		wc := &aztec.WatcherConfig{
+			NetworkID: "aztec",
+			ChainID:   vaa.ChainIDAztec,
+			Rpc:       *aztecRPC,
+			Contract:  *aztecContract,
+		}
+		watcherConfigs = append(watcherConfigs, wc)
+	}
+
 	if shouldStart(movementRPC) {
 	if shouldStart(movementRPC) {
 		wc := &aptos.WatcherConfig{
 		wc := &aptos.WatcherConfig{
 			NetworkID: "movement",
 			NetworkID: "movement",
@@ -1889,6 +1915,7 @@ func runNode(cmd *cobra.Command, args []string) {
 		node.GuardianOptionWatchers(watcherConfigs, ibcWatcherConfig),
 		node.GuardianOptionWatchers(watcherConfigs, ibcWatcherConfig),
 		node.GuardianOptionAccountant(*accountantWS, *accountantContract, *accountantCheckEnabled, accountantWormchainConn, *accountantNttContract, accountantNttWormchainConn),
 		node.GuardianOptionAccountant(*accountantWS, *accountantContract, *accountantCheckEnabled, accountantWormchainConn, *accountantNttContract, accountantNttWormchainConn),
 		node.GuardianOptionGovernor(*chainGovernorEnabled, *governorFlowCancelEnabled, *coinGeckoApiKey),
 		node.GuardianOptionGovernor(*chainGovernorEnabled, *governorFlowCancelEnabled, *coinGeckoApiKey),
+		node.GuardianOptionNotary(*notaryEnabled),
 		node.GuardianOptionGatewayRelayer(*gatewayRelayerContract, gatewayRelayerWormchainConn),
 		node.GuardianOptionGatewayRelayer(*gatewayRelayerContract, gatewayRelayerWormchainConn),
 		node.GuardianOptionQueryHandler(*ccqEnabled, *ccqAllowedRequesters),
 		node.GuardianOptionQueryHandler(*ccqEnabled, *ccqAllowedRequesters),
 		node.GuardianOptionAdminService(*adminSocketPath, ethRPC, ethContract, rpcMap),
 		node.GuardianOptionAdminService(*adminSocketPath, ethRPC, ethContract, rpcMap),

+ 4 - 4
node/go.mod

@@ -55,10 +55,12 @@ require (
 	github.com/google/uuid v1.6.0
 	github.com/google/uuid v1.6.0
 	github.com/grafana/dskit v0.0.0-20230201083518-528d8a7d52f2
 	github.com/grafana/dskit v0.0.0-20230201083518-528d8a7d52f2
 	github.com/grafana/loki v1.6.2-0.20230721141808-0d81144cfee8
 	github.com/grafana/loki v1.6.2-0.20230721141808-0d81144cfee8
+	github.com/hashicorp/go-retryablehttp v0.7.7
 	github.com/hashicorp/golang-lru v0.6.0
 	github.com/hashicorp/golang-lru v0.6.0
 	github.com/holiman/uint256 v1.2.1
 	github.com/holiman/uint256 v1.2.1
 	github.com/prometheus/client_model v0.6.1
 	github.com/prometheus/client_model v0.6.1
 	github.com/prometheus/common v0.60.0
 	github.com/prometheus/common v0.60.0
+	github.com/test-go/testify v1.1.4
 	github.com/wormhole-foundation/wormchain v0.0.0-00010101000000-000000000000
 	github.com/wormhole-foundation/wormchain v0.0.0-00010101000000-000000000000
 	github.com/wormhole-foundation/wormhole/sdk v0.0.0-20220926172624-4b38dc650bb0
 	github.com/wormhole-foundation/wormhole/sdk v0.0.0-20220926172624-4b38dc650bb0
 	google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e
 	google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e
@@ -130,7 +132,7 @@ require (
 	github.com/dvsekhvalnov/jose2go v1.5.0 // indirect
 	github.com/dvsekhvalnov/jose2go v1.5.0 // indirect
 	github.com/edsrzf/mmap-go v1.1.0 // indirect
 	github.com/edsrzf/mmap-go v1.1.0 // indirect
 	github.com/elastic/gosigar v0.14.3 // indirect
 	github.com/elastic/gosigar v0.14.3 // indirect
-	github.com/fatih/color v1.14.1 // indirect
+	github.com/fatih/color v1.16.0 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/flynn/noise v1.1.0 // indirect
 	github.com/flynn/noise v1.1.0 // indirect
 	github.com/francoispqt/gojay v1.2.13 // indirect
 	github.com/francoispqt/gojay v1.2.13 // indirect
@@ -180,7 +182,7 @@ require (
 	github.com/hashicorp/consul/api v1.18.0 // indirect
 	github.com/hashicorp/consul/api v1.18.0 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
-	github.com/hashicorp/go-hclog v1.2.0 // indirect
+	github.com/hashicorp/go-hclog v1.6.3 // indirect
 	github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
 	github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
 	github.com/hashicorp/go-msgpack v0.5.5 // indirect
 	github.com/hashicorp/go-msgpack v0.5.5 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
@@ -379,5 +381,3 @@ replace github.com/wormhole-foundation/wormchain => ../wormchain
 replace github.com/CosmWasm/wasmd v0.30.0 => github.com/wormhole-foundation/wasmd v0.30.0-wormchain-2
 replace github.com/CosmWasm/wasmd v0.30.0 => github.com/wormhole-foundation/wasmd v0.30.0-wormchain-2
 
 
 replace github.com/cosmos/cosmos-sdk => github.com/wormhole-foundation/cosmos-sdk v0.45.9-wormhole
 replace github.com/cosmos/cosmos-sdk => github.com/wormhole-foundation/cosmos-sdk v0.45.9-wormhole
-
-replace github.com/certusone/wormhole/node/pkg/txverifier => ./pkg/txverifier

+ 6 - 5
node/go.sum

@@ -1223,8 +1223,8 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL
 github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
 github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
 github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
 github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
 github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
 github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
-github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
-github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
+github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
+github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
 github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
 github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
 github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
@@ -1786,8 +1786,9 @@ github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrj
 github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
 github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
 github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
 github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
 github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
 github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
-github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
 github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
 github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
+github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
+github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
 github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
 github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
 github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
 github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
 github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
 github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
@@ -1803,8 +1804,8 @@ github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es
 github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
 github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
 github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
 github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
 github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
 github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
-github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0=
-github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
+github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
+github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
 github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
 github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
 github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
 github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
 github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
 github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=

+ 29 - 11
node/pkg/common/chainlock.go

@@ -55,7 +55,6 @@ const (
 )
 )
 
 
 var (
 var (
-	ErrBinaryWrite              = errors.New("failed to write binary data")
 	ErrInvalidBinaryBool        = errors.New("invalid binary bool (neither 0x00 nor 0x01)")
 	ErrInvalidBinaryBool        = errors.New("invalid binary bool (neither 0x00 nor 0x01)")
 	ErrInvalidVerificationState = errors.New("invalid verification state")
 	ErrInvalidVerificationState = errors.New("invalid verification state")
 )
 )
@@ -71,12 +70,12 @@ func (e ErrUnexpectedEndOfRead) Error() string {
 
 
 // ErrInputSize is returned when the input size is not the expected size during marshaling.
 // ErrInputSize is returned when the input size is not the expected size during marshaling.
 type ErrInputSize struct {
 type ErrInputSize struct {
-	msg string
-	got int
+	Msg string
+	Got int
 }
 }
 
 
 func (e ErrInputSize) Error() string {
 func (e ErrInputSize) Error() string {
-	return fmt.Sprintf("wrong size: %s. expected >= %d bytes, got %d", e.msg, marshaledMsgLenMin, e.got)
+	return fmt.Sprintf("wrong size: %s. expected >= %d bytes, got %d", e.Msg, marshaledMsgLenMin, e.Got)
 }
 }
 
 
 // MaxSafeInputSize defines the maximum safe size for untrusted input from `io` Readers.
 // MaxSafeInputSize defines the maximum safe size for untrusted input from `io` Readers.
@@ -86,6 +85,25 @@ const MaxSafeInputSize = 128 * 1024 * 1024 // 128MB (arbitrary)
 
 
 var ErrInputTooLarge = errors.New("input data exceeds maximum allowed size")
 var ErrInputTooLarge = errors.New("input data exceeds maximum allowed size")
 
 
+var (
+	ErrBinaryWrite         = errors.New("failed to write binary data")
+	ErrTxIDTooLong         = errors.New("field TxID too long")
+	ErrTxIDTooShort        = errors.New("field TxID too short")
+	ErrInvalidPayload      = errors.New("field payload too long")
+	ErrDataTooShort        = errors.New("data too short")
+	ErrTimestampTooShort   = errors.New("data too short for timestamp")
+	ErrNonceTooShort       = errors.New("data too short for nonce")
+	ErrSequenceTooShort    = errors.New("data too short for sequence")
+	ErrConsistencyTooShort = errors.New("data too short for consistency level")
+	ErrChainTooShort       = errors.New("data too short for emitter chain")
+	ErrAddressTooShort     = errors.New("data too short for emitter address")
+	ErrReobsTooShort       = errors.New("data too short for IsReobservation")
+	ErrUnreliableTooShort  = errors.New("data too short for Unreliable")
+	ErrVerStateTooShort    = errors.New("data too short for verification state")
+	ErrPayloadLenTooShort  = errors.New("data too short for payload length")
+	ErrPayloadTooShort     = errors.New("data too short for payload")
+)
+
 // The `VerificationState` is the result of applying transfer verification to the transaction associated with the `MessagePublication`.
 // The `VerificationState` is the result of applying transfer verification to the transaction associated with the `MessagePublication`.
 // While this could likely be extended to additional security controls in the future, it is only used for `txverifier` at present.
 // While this could likely be extended to additional security controls in the future, it is only used for `txverifier` at present.
 // Consequently, its status should be set to `NotVerified` or `NotApplicable` for all messages that aren't token transfers.
 // Consequently, its status should be set to `NotVerified` or `NotApplicable` for all messages that aren't token transfers.
@@ -246,11 +264,11 @@ func (msg *MessagePublication) MarshalBinary() ([]byte, error) {
 	// Check preconditions
 	// Check preconditions
 	txIDLen := len(msg.TxID)
 	txIDLen := len(msg.TxID)
 	if txIDLen > TxIDSizeMax {
 	if txIDLen > TxIDSizeMax {
-		return nil, ErrInputSize{msg: "TxID too long"}
+		return nil, ErrInputSize{Msg: "TxID too long"}
 	}
 	}
 
 
 	if txIDLen < TxIDLenMin {
 	if txIDLen < TxIDLenMin {
-		return nil, ErrInputSize{msg: "TxID too short"}
+		return nil, ErrInputSize{Msg: "TxID too short"}
 	}
 	}
 
 
 	payloadLen := len(msg.Payload)
 	payloadLen := len(msg.Payload)
@@ -407,7 +425,7 @@ func (m *MessagePublication) UnmarshalBinary(data []byte) error {
 	// Calculate minimum required length for the fixed portion
 	// Calculate minimum required length for the fixed portion
 	// (excluding variable-length fields: TxID and Payload)
 	// (excluding variable-length fields: TxID and Payload)
 	if len(data) < marshaledMsgLenMin {
 	if len(data) < marshaledMsgLenMin {
-		return ErrInputSize{msg: "data too short", got: len(data)}
+		return ErrInputSize{Msg: "data too short", Got: len(data)}
 	}
 	}
 
 
 	mp := &MessagePublication{}
 	mp := &MessagePublication{}
@@ -423,7 +441,7 @@ func (m *MessagePublication) UnmarshalBinary(data []byte) error {
 	// Bounds checks. TxID length should be at least TxIDLenMin, but not larger than the length of the data.
 	// Bounds checks. TxID length should be at least TxIDLenMin, but not larger than the length of the data.
 	// The second check is to avoid panics.
 	// The second check is to avoid panics.
 	if int(txIDLen) < TxIDLenMin || int(txIDLen) > len(data) {
 	if int(txIDLen) < TxIDLenMin || int(txIDLen) > len(data) {
-		return ErrInputSize{msg: "TxID length is invalid"}
+		return ErrInputSize{Msg: "TxID length is invalid"}
 	}
 	}
 
 
 	// Read TxID
 	// Read TxID
@@ -435,7 +453,7 @@ func (m *MessagePublication) UnmarshalBinary(data []byte) error {
 	// Concretely, we're checking that the data is at least long enough to contain information for all of
 	// Concretely, we're checking that the data is at least long enough to contain information for all of
 	// the fields except for the Payload itself.
 	// the fields except for the Payload itself.
 	if len(data)-pos < fixedFieldsLen {
 	if len(data)-pos < fixedFieldsLen {
-		return ErrInputSize{msg: "data too short after reading TxID", got: len(data)}
+		return ErrInputSize{Msg: "data too short after reading TxID", Got: len(data)}
 	}
 	}
 
 
 	// Timestamp
 	// Timestamp
@@ -497,13 +515,13 @@ func (m *MessagePublication) UnmarshalBinary(data []byte) error {
 	// exceed this limit and cause a runtime panic when passed to make([]byte, payloadLen).
 	// exceed this limit and cause a runtime panic when passed to make([]byte, payloadLen).
 	// This bounds check prevents such panics by rejecting oversized payload lengths early.
 	// This bounds check prevents such panics by rejecting oversized payload lengths early.
 	if payloadLen > PayloadLenMax {
 	if payloadLen > PayloadLenMax {
-		return ErrInputSize{msg: "payload length too large", got: len(data)}
+		return ErrInputSize{Msg: "payload length too large", Got: len(data)}
 	}
 	}
 
 
 	// Check if we have enough data for the payload
 	// Check if we have enough data for the payload
 	// #nosec G115 -- payloadLen is read from data, bounds checked above
 	// #nosec G115 -- payloadLen is read from data, bounds checked above
 	if len(data) < pos+int(payloadLen) {
 	if len(data) < pos+int(payloadLen) {
-		return ErrInputSize{msg: "invalid payload length"}
+		return ErrInputSize{Msg: "invalid payload length"}
 	}
 	}
 
 
 	// Read payload
 	// Read payload

+ 239 - 0
node/pkg/common/pendingmessage.go

@@ -0,0 +1,239 @@
+package common
+
+import (
+	"bytes"
+	"cmp"
+	"container/heap"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"slices"
+	"sync"
+	"time"
+
+	"github.com/wormhole-foundation/wormhole/sdk/vaa"
+)
+
+const (
+	// marshaledPendingMsgLenMin is the minimum length of a marshaled pending message.
+	// It includes 8 bytes for the timestamp.
+	marshaledPendingMsgLenMin = 8 + marshaledMsgLenMin
+)
+
+// PendingMessage is a wrapper type around a [MessagePublication] that includes the time for which it
+// should be released.
+type PendingMessage struct {
+	ReleaseTime time.Time
+	Msg         MessagePublication
+}
+
+func (p PendingMessage) Compare(other PendingMessage) int {
+	return cmp.Compare(p.ReleaseTime.Unix(), other.ReleaseTime.Unix())
+}
+
+// MarshalBinary implements BinaryMarshaler for [PendingMessage].
+func (p *PendingMessage) MarshalBinary() ([]byte, error) {
+	buf := new(bytes.Buffer)
+
+	// Compare with [PendingTransfer.Marshal].
+	// #nosec G115  -- int64 and uint64 have the same number of bytes, and Unix time won't be negative.
+	vaa.MustWrite(buf, binary.BigEndian, uint64(p.ReleaseTime.Unix()))
+
+	bz, err := p.Msg.MarshalBinary()
+	if err != nil {
+		return nil, fmt.Errorf("marshal pending message: %w", err)
+	}
+
+	vaa.MustWrite(buf, binary.BigEndian, bz)
+
+	return buf.Bytes(), nil
+}
+
+// UnmarshalBinary implements BinaryUnmarshaler for [PendingMessage].
+func (p *PendingMessage) UnmarshalBinary(data []byte) error {
+
+	if len(data) < marshaledPendingMsgLenMin {
+		return ErrInputSize{Msg: "pending message too short"}
+	}
+
+	// Compare with [UnmarshalPendingTransfer].
+	p.ReleaseTime = time.Unix(
+		// #nosec G115  -- int64 and uint64 have the same number of bytes, and Unix time won't be negative.
+		int64(binary.BigEndian.Uint64(data[0:8])),
+		0,
+	)
+
+	err := p.Msg.UnmarshalBinary(data[8:])
+
+	if err != nil {
+		return fmt.Errorf("unmarshal pending message: %w", err)
+	}
+
+	return nil
+}
+
+// A pendingMessageHeap is a min-heap of [PendingMessage] and uses the heap interface
+// by implementing the methods below.
+// As a result:
+// - The heap is always sorted by timestamp.
+// - the oldest (smallest) timestamp is always the last element.
+// This allows us to pop from the heap in order to get the oldest timestamp. If
+// that value greater than whatever time threshold we specify, we know that
+// there are no other messages that need to be released because their
+// timestamps must be greater. This should allow for constant-time lookups when
+// looking for messages to release.
+//
+// See: https://pkg.go.dev/container/heap#Interface
+type pendingMessageHeap []*PendingMessage
+
+func (h pendingMessageHeap) Len() int {
+	return len(h)
+}
+func (h pendingMessageHeap) Less(i, j int) bool {
+	return h[i].ReleaseTime.Before(h[j].ReleaseTime)
+}
+func (h pendingMessageHeap) Swap(i, j int) {
+	h[i], h[j] = h[j], h[i]
+}
+
+// Push dangerously pushes a value to the heap.
+func (h *pendingMessageHeap) Push(x any) {
+	// Push and Pop use pointer receivers because they modify the slice's length,
+	// not just its contents.
+	item, ok := x.(*PendingMessage)
+	if !ok {
+		panic("PendingMessageHeap: cannot push non-*PendingMessage")
+	}
+
+	// Null check
+	if item == nil {
+		panic("PendingMessageHeap: cannot push nil *PendingMessage")
+	}
+
+	*h = append(*h, item)
+}
+
+// Pops dangerously pops a value from the heap.
+func (h *pendingMessageHeap) Pop() any {
+	old := *h
+	n := len(old)
+	if n == 0 {
+		panic("PendingMessageHeap: cannot Pop from empty heap")
+	}
+	last := old[n-1]
+	*h = old[0 : n-1]
+	return last
+}
+
+// PendingMessageQueue is a thread-safe min-heap that sorts [PendingMessage] in descending order of Timestamp.
+// It also prevents duplicate [MessagePublication]s from being added to the queue.
+type PendingMessageQueue struct {
+	// pendingMessageHeap exposes dangerous APIs as a necessary consequence of implementing [heap.Interface].
+	// Wrap it and expose only a safe API.
+	heap pendingMessageHeap
+	mu   sync.RWMutex
+}
+
+func NewPendingMessageQueue() *PendingMessageQueue {
+	q := &PendingMessageQueue{heap: pendingMessageHeap{}}
+	heap.Init(&q.heap)
+	return q
+}
+
+// Push adds an element to the heap. If msg is nil, nothing is added.
+func (q *PendingMessageQueue) Push(pMsg *PendingMessage) {
+	// noop if the message is nil or already in the queue.
+	if pMsg == nil || q.ContainsMessagePublication(&pMsg.Msg) {
+		return
+	}
+
+	q.mu.Lock()
+	defer q.mu.Unlock()
+
+	heap.Push(&q.heap, pMsg)
+}
+
+// Pop removes the last element from the heap and returns its value.
+// Returns nil if the heap is empty or if the value is not a *[PendingMessage].
+func (q *PendingMessageQueue) Pop() *PendingMessage {
+	if q.heap.Len() == 0 {
+		return nil
+	}
+
+	q.mu.Lock()
+	defer q.mu.Unlock()
+
+	last, ok := heap.Pop(&q.heap).(*PendingMessage)
+	if !ok {
+		return nil
+	}
+
+	return last
+}
+
+func (q *PendingMessageQueue) Len() int {
+	return q.heap.Len()
+}
+
+// Peek returns the element at the top of the heap without removing it.
+func (q *PendingMessageQueue) Peek() *PendingMessage {
+	if q.heap.Len() == 0 {
+		return nil
+	}
+	// container/heap stores the "next" element at the first offset.
+	last := *q.heap[0]
+	return &last
+}
+
+// RemoveItem removes target MessagePublication from the heap. Returns the element that was removed or nil
+// if the item was not found. No error is returned if the item was not found.
+func (q *PendingMessageQueue) RemoveItem(target *MessagePublication) (*PendingMessage, error) {
+	if target == nil {
+		return nil, errors.New("pendingmessage: nil argument for RemoveItem")
+	}
+
+	q.mu.Lock()
+	defer q.mu.Unlock()
+
+	var removed *PendingMessage
+	for i, item := range q.heap {
+		// Assumption: MsgIDs are unique across MessagePublications.
+		if bytes.Equal(item.Msg.MessageID(), target.MessageID()) {
+			pMsg, ok := heap.Remove(&q.heap, i).(*PendingMessage)
+			if !ok {
+				return nil, errors.New("pendingmessage: item removed from heap is not PendingMessage")
+			}
+			removed = pMsg
+			break
+		}
+	}
+
+	return removed, nil
+}
+
+// Contains determines whether the queue contains a [PendingMessage].
+func (q *PendingMessageQueue) Contains(pMsg *PendingMessage) bool {
+	if pMsg == nil {
+		return false
+	}
+
+	q.mu.RLock()
+	defer q.mu.RUnlock()
+
+	return slices.Contains(q.heap, pMsg)
+}
+
+// ContainsMessagePublication determines whether the queue contains a [MessagePublication] (not a [PendingMessage]).
+func (q *PendingMessageQueue) ContainsMessagePublication(msgPub *MessagePublication) bool {
+	if msgPub == nil {
+		return false
+	}
+
+	q.mu.RLock()
+	defer q.mu.RUnlock()
+
+	// Relies on MessageIDString to be unique.
+	return slices.ContainsFunc(q.heap, func(pMsg *PendingMessage) bool {
+		return bytes.Equal(pMsg.Msg.MessageID(), msgPub.MessageID())
+	})
+}

+ 396 - 0
node/pkg/common/pendingmessage_test.go

@@ -0,0 +1,396 @@
+package common_test
+
+import (
+	"bytes"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"math"
+	"math/big"
+	"math/rand/v2"
+	"slices"
+	"testing"
+	"time"
+
+	"github.com/certusone/wormhole/node/pkg/common"
+	eth_common "github.com/ethereum/go-ethereum/common"
+	"github.com/stretchr/testify/require"
+	"github.com/wormhole-foundation/wormhole/sdk"
+	"github.com/wormhole-foundation/wormhole/sdk/vaa"
+)
+
+func TestPendingMessage_RoundTripMarshal(t *testing.T) {
+	orig := makeUniquePendingMessage(t)
+	var loaded common.PendingMessage
+
+	bz, writeErr := orig.MarshalBinary()
+	require.NoError(t, writeErr)
+
+	readErr := loaded.UnmarshalBinary(bz)
+	require.NoError(t, readErr)
+
+	require.Equal(t, *orig, loaded)
+}
+
+func TestPendingMessage_MarshalError(t *testing.T) {
+
+	type test struct {
+		label string
+		input common.MessagePublication
+		err   error
+	}
+
+	// Set up.
+	var (
+		longTxID = bytes.NewBuffer(make([]byte, math.MaxUint8+1))
+	)
+	emitter, err := vaa.StringToAddress("0x707f9118e33a9b8998bea41dd0d46f38bb963fc8")
+	require.NoError(t, err)
+
+	require.NoError(t, err)
+
+	tests := []test{
+		{
+			label: "txID too long",
+			input: common.MessagePublication{
+				TxID: longTxID.Bytes(),
+			},
+			err: common.ErrInputSize{Msg: "TxID too long"},
+		},
+		{
+			label: "txID too short",
+			input: common.MessagePublication{
+				TxID:             []byte{},
+				Timestamp:        time.Unix(int64(1654516425), 0),
+				Nonce:            123456,
+				Sequence:         789101112131415,
+				EmitterChain:     vaa.ChainIDEthereum,
+				EmitterAddress:   emitter,
+				Payload:          []byte{},
+				ConsistencyLevel: 32,
+				Unreliable:       true,
+				IsReobservation:  true,
+			},
+			err: common.ErrInputSize{Msg: "TxID too short"},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.label, func(t *testing.T) {
+			pMsg := &common.PendingMessage{
+				ReleaseTime: time.Now(),
+				Msg:         tc.input,
+			}
+
+			bz, writeErr := pMsg.MarshalBinary()
+			require.Error(t, writeErr)
+			require.True(t, errors.Is(writeErr, tc.err), fmt.Sprintf("got wrong error type: %v", writeErr))
+			require.Nil(t, bz)
+		})
+	}
+
+}
+
+func TestPendingMessageQueue_NoDuplicates(t *testing.T) {
+	q := common.NewPendingMessageQueue()
+
+	// Create two messages with the same sequence number.
+	msg1 := *makeUniquePendingMessage(t)
+	msg2 := *makeUniquePendingMessage(t)
+	msg2.Msg.Sequence = msg1.Msg.Sequence
+
+	q.Push(&msg1)
+	require.Equal(t, 1, q.Len())
+
+	msg2.ReleaseTime = msg1.ReleaseTime.Add(time.Hour)
+	require.True(t, msg1.ReleaseTime.Before(msg2.ReleaseTime))
+
+	// Pushing the same message twice should not add it to the queue.
+	q.Push(&msg2)
+	require.Equal(t, 1, q.Len())
+}
+
+func TestPendingMessage_HeapInvariants(t *testing.T) {
+
+	msg1 := *makeUniquePendingMessage(t)
+	msg2 := *makeUniquePendingMessage(t)
+	msg3 := *makeUniquePendingMessage(t)
+
+	// Modify release times, in ascending (past-to-future) order: msg1 < msg2 < msg3
+	msg2.ReleaseTime = msg1.ReleaseTime.Add(time.Hour)
+	msg3.ReleaseTime = msg1.ReleaseTime.Add(time.Hour * 2)
+
+	require.True(t, msg1.ReleaseTime.Before(msg2.ReleaseTime))
+	require.True(t, msg2.ReleaseTime.Before(msg3.ReleaseTime))
+
+	tests := []struct {
+		name string // description of this test case
+		// Named input parameters for target function.
+		order []*common.PendingMessage
+	}{
+		{
+			"ascending order",
+			[]*common.PendingMessage{&msg1, &msg2, &msg3},
+		},
+		{
+			"mixed order A",
+			[]*common.PendingMessage{&msg2, &msg3, &msg1},
+		},
+		{
+			"mixed order B",
+			[]*common.PendingMessage{&msg3, &msg1, &msg2},
+		},
+	}
+
+	// Try different variations of adding messages to the heap.
+	// After each variation, the first element returned should be equal
+	// to the smallest/oldest message publication, which is msg1.
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Set-up
+			q := common.NewPendingMessageQueue()
+			for pMsg := range slices.Values(tt.order) {
+				q.Push(pMsg)
+			}
+			require.Equal(t, len(tt.order), q.Len())
+
+			res := consumeHeapAndAssertOrdering(t, q)
+			require.Equal(t, 0, q.Len())
+			require.True(t, &msg1 == res[0])
+			assertSliceOrdering(t, res)
+		})
+	}
+
+	// Ensure that calling RemoveItem doesn't change the ordering.
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Set-up
+			q := common.NewPendingMessageQueue()
+			for pMsg := range slices.Values(tt.order) {
+				q.Push(pMsg)
+			}
+			require.Equal(t, len(tt.order), q.Len())
+
+			removed, err := q.RemoveItem(&msg2.Msg)
+			require.NoError(t, err)
+			require.NotNil(t, removed)
+			require.Equal(t, &msg2, removed, "removed message does not match expected message")
+
+			require.Equal(t, len(tt.order)-1, q.Len())
+
+			res := consumeHeapAndAssertOrdering(t, q)
+			require.Equal(t, 0, q.Len())
+			require.True(t, &msg1 == res[0])
+			assertSliceOrdering(t, res)
+		})
+	}
+
+}
+
+func TestPendingMessageQueue_Peek(t *testing.T) {
+	q := common.NewPendingMessageQueue()
+
+	msg1 := *makeUniquePendingMessage(t)
+	msg2 := *makeUniquePendingMessage(t)
+	msg3 := *makeUniquePendingMessage(t)
+
+	// Modify release times, in ascending (past-to-future) order: msg1 < msg2 < msg3
+	msg2.ReleaseTime = msg1.ReleaseTime.Add(time.Hour)
+	msg3.ReleaseTime = msg1.ReleaseTime.Add(time.Hour * 2)
+
+	require.True(t, msg1.ReleaseTime.Before(msg2.ReleaseTime))
+	require.True(t, msg2.ReleaseTime.Before(msg3.ReleaseTime))
+
+	// Push elements in an arbitrary order.
+	// Assert that Peek() returns msg1 because it is the smallest.
+	q.Push(&msg2)
+	q.Push(&msg3)
+	q.Push(&msg1)
+	require.Equal(t, 3, q.Len())
+	require.Equal(t, &msg1, q.Peek())
+
+}
+
+func TestPendingMessageQueue_RemoveItem(t *testing.T) {
+	msgInQueue := makeUniquePendingMessage(t).Msg
+	msgNotInQueue := makeUniquePendingMessage(t).Msg
+	msgNotInQueue.TxID = []byte{0xff}
+
+	tests := []struct {
+		name string // description of this test case
+		// Named input parameters for target function.
+		target *common.MessagePublication
+		want   *common.PendingMessage
+	}{
+		{
+			"successful removal",
+			&msgInQueue,
+			&common.PendingMessage{Msg: msgInQueue},
+		},
+		{
+			"remove an item that is not in the queue",
+			&msgNotInQueue,
+			nil,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+
+			q := common.NewPendingMessageQueue()
+
+			q.Push(&common.PendingMessage{Msg: msgInQueue})
+
+			got, gotErr := q.RemoveItem(tt.target)
+			require.NoError(t, gotErr)
+
+			if tt.want != nil {
+				require.NotNil(t, got)
+				require.Equal(t, 0, q.Len())
+				// The RemoveItem function relies on comparing TxIDs.
+				require.Equal(t, tt.want.Msg.TxID, got.Msg.TxID)
+			} else {
+				require.Nil(t, got)
+				require.Equal(t, 1, q.Len(), "item should not have been removed from queue")
+			}
+
+		})
+	}
+}
+
+// TestPendingMessageQueue_DangerousOperations ensures that dangerous operations
+// on the queue do not panic or cause unexpected behavior.
+func TestPendingMessageQueue_DangerousOperations(t *testing.T) {
+	q := common.NewPendingMessageQueue()
+
+	// Popping an empty queue should not panic or alter the queue.
+	element := q.Pop()
+	require.Nil(t, element)
+	require.Equal(t, 0, q.Len())
+
+	// Peeking an empty queue should not panic or alter the queue.
+	element = q.Peek()
+	require.Nil(t, element)
+	require.Equal(t, 0, q.Len())
+
+	// Build some state for the next test.
+	msg1 := *makeUniquePendingMessage(t)
+	msg2 := *makeUniquePendingMessage(t)
+	msg3 := *makeUniquePendingMessage(t)
+
+	q.Push(&msg1)
+	q.Push(&msg2)
+	q.Push(&msg3)
+	require.Equal(t, 3, q.Len())
+
+	// Add nil to the queue and ensure that it is ignored.
+	q.Push(nil)
+	require.Equal(t, 3, q.Len())
+}
+
+func assertSliceOrdering(t *testing.T, s []*common.PendingMessage) {
+	for i := range len(s) - 1 {
+		require.True(t, s[i].ReleaseTime.Before(s[i+1].ReleaseTime))
+	}
+}
+
+// consumeHeapAndAssertOrdering takes heap and pops every element, ensuring that
+// each popped element is smaller than the next one on the heap.
+// Returns the elements in order of when they were popped. This should result
+// in a slice of strictly ascending values.
+func consumeHeapAndAssertOrdering(t *testing.T, q *common.PendingMessageQueue) []*common.PendingMessage {
+	require.True(t, q.Len() > 0, "programming error: can't process empty queue")
+
+	res := make([]*common.PendingMessage, 0, q.Len())
+
+	// Pop all entries from the heap. Ensure that the element on top of the heap
+	// is always the earliest (smallest timestamp).
+	for q.Len() > 0 {
+		// length changes automatically after popping.
+		earliest := q.Pop()
+		res = append(res, earliest)
+
+		next := q.Peek()
+
+		// Expect next to not be nil unless we just popped the last element.
+		if q.Len() > 0 {
+			require.NotNil(t, next)
+		}
+
+		if next == nil {
+			continue
+		}
+
+		require.True(t, earliest.ReleaseTime.Before(q.Peek().ReleaseTime))
+	}
+	return res
+}
+
+func encodePayloadBytes(payload *vaa.TransferPayloadHdr) []byte {
+	bz := make([]byte, 101)
+	bz[0] = payload.Type
+
+	amtBytes := payload.Amount.Bytes()
+	if len(amtBytes) > 32 {
+		panic("amount will not fit in 32 bytes!")
+	}
+	copy(bz[33-len(amtBytes):33], amtBytes)
+
+	copy(bz[33:65], payload.OriginAddress.Bytes())
+	binary.BigEndian.PutUint16(bz[65:67], uint16(payload.OriginChain))
+	copy(bz[67:99], payload.TargetAddress.Bytes())
+	binary.BigEndian.PutUint16(bz[99:101], uint16(payload.TargetChain))
+	return bz
+}
+
+// Helper function that returns a valid PendingMessage. It creates identical messages publications
+// with different sequence numbers.
+func makeUniquePendingMessage(t *testing.T) *common.PendingMessage {
+	t.Helper()
+
+	originAddress, err := vaa.StringToAddress("0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E") //nolint:gosec
+	require.NoError(t, err)
+
+	targetAddress, err := vaa.StringToAddress("0x707f9118e33a9b8998bea41dd0d46f38bb963fc8")
+	require.NoError(t, err)
+
+	// Required as the Notary checks the emitter address.
+	tokenBridge := sdk.KnownTokenbridgeEmitters[vaa.ChainIDEthereum]
+	tokenBridgeAddress := vaa.Address(tokenBridge)
+	require.NoError(t, err)
+
+	payload := &vaa.TransferPayloadHdr{
+		Type:          0x01,
+		Amount:        big.NewInt(27000000000),
+		OriginAddress: originAddress,
+		OriginChain:   vaa.ChainIDEthereum,
+		TargetAddress: targetAddress,
+		TargetChain:   vaa.ChainIDPolygon,
+	}
+
+	payloadBytes := encodePayloadBytes(payload)
+
+	// Should be unique for each test with high probability.
+	// #nosec: G404 -- Cryptographically secure pseudo-random number generator not needed.
+	var sequence = rand.Uint64()
+	msgpub := &common.MessagePublication{
+		TxID:             eth_common.HexToHash("0x06f541f5ecfc43407c31587aa6ac3a689e8960f36dc23c332db5510dfc6a4063").Bytes(),
+		Timestamp:        time.Unix(int64(1654516425), 0),
+		Nonce:            123456,
+		Sequence:         sequence,
+		EmitterChain:     vaa.ChainIDEthereum,
+		EmitterAddress:   tokenBridgeAddress,
+		Payload:          payloadBytes,
+		ConsistencyLevel: 32,
+		Unreliable:       true,
+		IsReobservation:  true,
+	}
+	setErr := msgpub.SetVerificationState(common.Anomalous)
+	require.NoError(t, setErr)
+
+	// The nanoseconds are not important to us and are not serialized.
+	releaseTime := time.Unix(int64(1654516425), 0)
+	return &common.PendingMessage{
+		ReleaseTime: releaseTime,
+		Msg:         *msgpub,
+	}
+}

+ 5 - 0
node/pkg/db/db.go

@@ -236,3 +236,8 @@ func (d *Database) FindEmitterSequenceGap(prefix VAAID) (resp []uint64, firstSeq
 	}
 	}
 	return
 	return
 }
 }
+
+// Conn returns a pointer to the underlying database connection.
+func (d *Database) Conn() *badger.DB {
+	return d.db
+}

+ 231 - 0
node/pkg/db/notary.go

@@ -0,0 +1,231 @@
+// SECURITY: The calling code is responsible for handling mutex operations when
+// working with this package.
+package db
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	"github.com/certusone/wormhole/node/pkg/common"
+	"github.com/dgraph-io/badger/v3"
+	"go.uber.org/zap"
+)
+
+type NotaryDBInterface interface {
+	StoreBlackholed(m *common.MessagePublication) error
+	StoreDelayed(p *common.PendingMessage) error
+	DeleteBlackholed(m *common.MessagePublication) error
+	DeleteDelayed(p *common.PendingMessage) error
+	LoadAll(logger *zap.Logger) (*NotaryLoadResult, error)
+}
+
+// NotaryDB is a wrapper struct for a database connection.
+// Its main purpose is to provide some separation from the Notary's functionality
+// and the general functioning of db.Database
+type NotaryDB struct {
+	db *badger.DB
+}
+
+func NewNotaryDB(dbConn *badger.DB) *NotaryDB {
+	return &NotaryDB{
+		db: dbConn,
+	}
+}
+
+// Define prefixes used to isolate different message publications stored in the database.
+const (
+	delayedPrefix   = "NOTARY:DELAY:V1:"
+	blackholePrefix = "NOTARY:BLACKHOLE:V1:"
+)
+
+// The type of data stored in the Notary's database.
+type dataType string
+
+const (
+	Unknown    dataType = "unknown"
+	Delayed    dataType = "delayed"
+	Blackholed dataType = "blackholed"
+)
+
+var (
+	ErrMarshal   = errors.New("notary: marshal")
+	ErrUnmarshal = errors.New("notary: unmarshal")
+)
+
+// Operation represents a database operation type
+type Operation string
+
+const (
+	OpRead   Operation = "read"
+	OpUpdate Operation = "update"
+	OpDelete Operation = "delete"
+)
+
+type DBError struct {
+	Op  Operation
+	Key []byte
+	Err error
+}
+
+func (e *DBError) Unwrap() error {
+	return e.Err
+}
+
+func (e *DBError) Error() string {
+	return fmt.Sprintf("notary database: %s key: %x error: %v", e.Op, e.Key, e.Err)
+}
+
+func (d *NotaryDB) StoreDelayed(p *common.PendingMessage) error {
+	b, marshalErr := p.MarshalBinary()
+
+	if marshalErr != nil {
+		return errors.Join(ErrMarshal, marshalErr)
+	}
+
+	key := delayKey(p)
+	if updateErr := d.update(key, b); updateErr != nil {
+		return &DBError{Op: OpUpdate, Key: key, Err: updateErr}
+	}
+
+	return nil
+}
+
+func (d *NotaryDB) StoreBlackholed(m *common.MessagePublication) error {
+	b, marshalErr := m.MarshalBinary()
+
+	if marshalErr != nil {
+		return errors.Join(ErrMarshal, marshalErr)
+	}
+
+	key := blackholeKey(m)
+	if updateErr := d.update(key, b); updateErr != nil {
+		return &DBError{Op: OpUpdate, Key: key, Err: updateErr}
+	}
+	return nil
+}
+
+func (d *NotaryDB) DeleteDelayed(p *common.PendingMessage) error {
+	return d.deleteEntry(delayKey(p))
+}
+
+func (d *NotaryDB) DeleteBlackholed(m *common.MessagePublication) error {
+	return d.deleteEntry(blackholeKey(m))
+}
+
+type NotaryLoadResult struct {
+	Delayed    []*common.PendingMessage
+	Blackholed []*common.MessagePublication
+}
+
+// LoadAll retrieves all keys from the database.
+func (d *NotaryDB) LoadAll(logger *zap.Logger) (*NotaryLoadResult, error) {
+	result := NotaryLoadResult{
+		Delayed:    make([]*common.PendingMessage, 0),
+		Blackholed: make([]*common.MessagePublication, 0),
+	}
+	viewErr := d.db.View(func(txn *badger.Txn) error {
+		opts := badger.DefaultIteratorOptions
+		opts.PrefetchSize = 10
+		it := txn.NewIterator(opts)
+		defer it.Close()
+		for it.Rewind(); it.Valid(); it.Next() {
+			item := it.Item()
+			key := item.Key()
+			data, copyErr := item.ValueCopy(nil)
+			if copyErr != nil {
+				return copyErr
+			}
+
+			switch dbDataType(key) {
+			case Blackholed:
+				var msgPub common.MessagePublication
+				unmarshalErr := msgPub.UnmarshalBinary(data)
+				if unmarshalErr != nil {
+					return errors.Join(
+						ErrUnmarshal,
+						unmarshalErr,
+					)
+				}
+				result.Blackholed = append(result.Blackholed, &msgPub)
+			case Delayed:
+				var pMsg common.PendingMessage
+				unmarshalErr := pMsg.UnmarshalBinary(data)
+				if unmarshalErr != nil {
+					return errors.Join(
+						ErrUnmarshal,
+						unmarshalErr,
+					)
+				}
+				result.Delayed = append(result.Delayed, &pMsg)
+			case Unknown:
+				// The key-value store is shared across other modules and message types (e.g. Governor, Accountant).
+				// If another key is discovered, just ignore it.
+				logger.Debug("notary: load database ignoring unknown key type", zap.String("key", string(key)))
+				continue
+			}
+
+		}
+		return nil
+	})
+
+	if viewErr != nil {
+		// No key provided here since the View function is iterating over every entry.
+		return nil, &DBError{Op: OpRead, Err: viewErr}
+	}
+
+	return &result, nil
+}
+
+// dbDataType returns the data type for an entry in the database based on its key.
+func dbDataType(key []byte) dataType {
+	if strings.HasPrefix(string(key), blackholePrefix) {
+		return Blackholed
+	}
+	if strings.HasPrefix(string(key), delayedPrefix) {
+		return Delayed
+	}
+	return Unknown
+
+}
+
+func (d *NotaryDB) update(key []byte, data []byte) error {
+	updateErr := d.db.Update(func(txn *badger.Txn) error {
+		if setErr := txn.Set(key, data); setErr != nil {
+			return setErr
+		}
+		return nil
+	})
+
+	if updateErr != nil {
+		return &DBError{Op: OpUpdate, Key: key, Err: updateErr}
+	}
+
+	return nil
+}
+
+func (d *NotaryDB) deleteEntry(key []byte) error {
+	if updateErr := d.db.Update(func(txn *badger.Txn) error {
+		deleteErr := txn.Delete(key)
+		return deleteErr
+	}); updateErr != nil {
+		return &DBError{Op: OpDelete, Key: key, Err: updateErr}
+	}
+
+	return nil
+}
+
+// delayKey returns a unique prefix for pending messages to be stored in the Notary's database.
+func delayKey(p *common.PendingMessage) []byte {
+	return key(delayedPrefix, p.Msg.MessageIDString())
+}
+
+// blackholeKey returns a unique prefix for blackholed message publications to be stored in the Notary's database.
+func blackholeKey(m *common.MessagePublication) []byte {
+	return key(blackholePrefix, m.MessageIDString())
+}
+
+// key returns a unique prefix for different data types stored in the Notary's database.
+func key(prefix string, msgID string) (key []byte) {
+	return fmt.Appendf(key, "%v%v", prefix, msgID)
+}

+ 105 - 0
node/pkg/db/notary_test.go

@@ -0,0 +1,105 @@
+package db
+
+import (
+	"encoding/hex"
+	"fmt"
+	"os"
+	"testing"
+	"time"
+
+	"github.com/certusone/wormhole/node/pkg/common"
+	"github.com/stretchr/testify/require"
+	"github.com/wormhole-foundation/wormhole/sdk/vaa"
+	"go.uber.org/zap"
+)
+
+func TestStoreAndReloadData(t *testing.T) {
+	// Set-up.
+	dbPath := t.TempDir()
+	database := OpenDb(zap.NewNop(), &dbPath)
+	defer database.Close()
+	defer os.Remove(dbPath)
+	nDB := NotaryDB{db: database.db}
+
+	// Build messages.
+	msg1 := makeNewMsgPub(t)
+	msg2 := *msg1
+	pendingMsg := makeNewPendingMsg(t, msg1)
+
+	// Store messages.
+	delayErr := nDB.StoreDelayed(pendingMsg)
+	require.NoError(t, delayErr, fmt.Sprintf("failed to store delayed message: %v", delayErr))
+	blackholeErr := nDB.StoreBlackholed(&msg2)
+	require.NoError(t, blackholeErr, fmt.Sprintf("failed to store blackholed message: %v", blackholeErr))
+
+	// Retrieve both messages and ensure they're equal to what was stored.
+	res, loadErr := nDB.LoadAll(zap.NewNop())
+	require.NoError(t, loadErr)
+	require.Equal(t, 1, len(res.Delayed))
+	require.Equal(t, 1, len(res.Blackholed))
+	require.Equal(t, pendingMsg, res.Delayed[0])
+	require.Equal(t, &msg2, res.Blackholed[0])
+}
+
+func TestKeysForStoredMessagesV1(t *testing.T) {
+	msg1 := makeNewMsgPub(t)
+	pMsg := makeNewPendingMsg(t, msg1)
+
+	require.Equal(
+		t,
+		[]byte("NOTARY:DELAY:V1:2/0000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16/789101112131415"),
+		delayKey(pMsg),
+	)
+
+	require.Equal(
+		t,
+		[]byte("NOTARY:BLACKHOLE:V1:2/0000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16/789101112131415"),
+		blackholeKey(msg1),
+	)
+}
+
+// nowSeconds is a helper function that returns time.Now() with the nanoseconds truncated.
+// The nanoseconds are not important to us and are not serialized.
+func nowSeconds() time.Time {
+	return time.Unix(time.Now().Unix(), 0)
+}
+
+// makeNewMsgPub returns a MessagePublication that has a token transfer payload
+// but otherwise has default values.
+func makeNewMsgPub(t *testing.T) *common.MessagePublication {
+	t.Helper()
+
+	ethereumTokenBridgeAddr, err := vaa.StringToAddress("0x0290fb167208af455bb137780163b7b7a9a10c16")
+	require.NoError(t, err)
+
+	validTxID, err := hex.DecodeString("88029cf0e7432cec04c266a3e72903ee6650b4624c7f9c8e22b04d78e18e87f8")
+	require.NoError(t, err)
+
+	msg := &common.MessagePublication{
+		TxID:            validTxID,
+		Timestamp:       nowSeconds(),
+		Nonce:           1,
+		Sequence:        789101112131415,
+		EmitterChain:    vaa.ChainIDEthereum,
+		EmitterAddress:  ethereumTokenBridgeAddr,
+		Unreliable:      false,
+		IsReobservation: false,
+		Payload:         []byte{0x01},
+	}
+
+	err = msg.SetVerificationState(common.Anomalous)
+	require.NoError(t, err)
+
+	return msg
+}
+
+// makeNewPendingMsg wraps a message publication and adds a release time to create a PendingMessage
+func makeNewPendingMsg(t *testing.T, msg *common.MessagePublication) *common.PendingMessage {
+	t.Helper()
+
+	return &common.PendingMessage{
+		// The nanoseconds are not important to us and are not serialized.
+		ReleaseTime: nowSeconds().Add(24 * time.Hour),
+		Msg:         *msg,
+	}
+}

+ 2 - 2
node/pkg/governor/governor.go

@@ -744,8 +744,8 @@ func (gov *ChainGovernor) parseMsgAlreadyLocked(
 	return true, ce, token, payload, nil
 	return true, ce, token, payload, nil
 }
 }
 
 
-// CheckPending is a wrapper method for CheckPendingForTime. It is called by the processor with the purpose of releasing
-// queued transfers.
+// CheckPending is a wrapper method for CheckPendingForTime that uses time.Now as the release time.
+// Returns a slice of MessagePublications that are ready to be published.
 func (gov *ChainGovernor) CheckPending() ([]*common.MessagePublication, error) {
 func (gov *ChainGovernor) CheckPending() ([]*common.MessagePublication, error) {
 	return gov.CheckPendingForTime(time.Now())
 	return gov.CheckPendingForTime(time.Now())
 }
 }

+ 9 - 0
node/pkg/node/node.go

@@ -11,6 +11,7 @@ import (
 	"github.com/certusone/wormhole/node/pkg/governor"
 	"github.com/certusone/wormhole/node/pkg/governor"
 	"github.com/certusone/wormhole/node/pkg/guardiansigner"
 	"github.com/certusone/wormhole/node/pkg/guardiansigner"
 	"github.com/certusone/wormhole/node/pkg/gwrelayer"
 	"github.com/certusone/wormhole/node/pkg/gwrelayer"
+	"github.com/certusone/wormhole/node/pkg/notary"
 	gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1"
 	gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1"
 	"github.com/certusone/wormhole/node/pkg/query"
 	"github.com/certusone/wormhole/node/pkg/query"
 	"github.com/certusone/wormhole/node/pkg/supervisor"
 	"github.com/certusone/wormhole/node/pkg/supervisor"
@@ -90,6 +91,7 @@ type G struct {
 	gst                *common.GuardianSetState
 	gst                *common.GuardianSetState
 	acct               *accountant.Accountant
 	acct               *accountant.Accountant
 	gov                *governor.ChainGovernor
 	gov                *governor.ChainGovernor
+	notary             *notary.Notary
 	gatewayRelayer     *gwrelayer.GatewayRelayer
 	gatewayRelayer     *gwrelayer.GatewayRelayer
 	queryHandler       *query.QueryHandler
 	queryHandler       *query.QueryHandler
 	publicrpcServer    *grpc.Server
 	publicrpcServer    *grpc.Server
@@ -235,6 +237,13 @@ func (g *G) Run(rootCtxCancel context.CancelFunc, options ...*GuardianOption) su
 			}
 			}
 		}
 		}
 
 
+		if g.notary != nil {
+			logger.Info("starting notary")
+			if err := g.notary.Run(); err != nil {
+				logger.Fatal("failed to create notary", zap.Error(err))
+			}
+		}
+
 		if g.gatewayRelayer != nil {
 		if g.gatewayRelayer != nil {
 			logger.Info("Starting gateway relayer")
 			logger.Info("Starting gateway relayer")
 			if err := g.gatewayRelayer.Start(ctx); err != nil {
 			if err := g.gatewayRelayer.Start(ctx); err != nil {

+ 1 - 0
node/pkg/node/node_test.go

@@ -189,6 +189,7 @@ func mockGuardianRunnable(t testing.TB, gs []*mockGuardian, mockGuardianIndex ui
 			GuardianOptionWatchers(watcherConfigs, nil),
 			GuardianOptionWatchers(watcherConfigs, nil),
 			GuardianOptionNoAccountant(), // disable accountant
 			GuardianOptionNoAccountant(), // disable accountant
 			GuardianOptionGovernor(true, false, ""),
 			GuardianOptionGovernor(true, false, ""),
+			GuardianOptionNotary(true),
 			GuardianOptionGatewayRelayer("", nil), // disable gateway relayer
 			GuardianOptionGatewayRelayer("", nil), // disable gateway relayer
 			GuardianOptionQueryHandler(false, ""), // disable queries
 			GuardianOptionQueryHandler(false, ""), // disable queries
 			GuardianOptionPublicRpcSocket(cfg.publicSocket, publicRpcLogDetail),
 			GuardianOptionPublicRpcSocket(cfg.publicSocket, publicRpcLogDetail),

+ 20 - 2
node/pkg/node/options.go

@@ -13,6 +13,7 @@ import (
 	guardianDB "github.com/certusone/wormhole/node/pkg/db"
 	guardianDB "github.com/certusone/wormhole/node/pkg/db"
 	"github.com/certusone/wormhole/node/pkg/governor"
 	"github.com/certusone/wormhole/node/pkg/governor"
 	"github.com/certusone/wormhole/node/pkg/gwrelayer"
 	"github.com/certusone/wormhole/node/pkg/gwrelayer"
+	"github.com/certusone/wormhole/node/pkg/notary"
 	"github.com/certusone/wormhole/node/pkg/p2p"
 	"github.com/certusone/wormhole/node/pkg/p2p"
 	"github.com/certusone/wormhole/node/pkg/processor"
 	"github.com/certusone/wormhole/node/pkg/processor"
 	gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1"
 	gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1"
@@ -254,6 +255,22 @@ func GuardianOptionGovernor(governorEnabled bool, flowCancelEnabled bool, coinGe
 		}}
 		}}
 }
 }
 
 
+// GuardianOptionNotary enables or disables the Notary.
+// Dependencies: db
+func GuardianOptionNotary(notaryEnabled bool) *GuardianOption {
+	return &GuardianOption{
+		name:         "notary",
+		dependencies: []string{"db"},
+		f: func(ctx context.Context, logger *zap.Logger, g *G) error {
+			if notaryEnabled {
+				g.notary = notary.NewNotary(ctx, logger, g.db, g.env)
+			} else {
+				logger.Info("notary is disabled")
+			}
+			return nil
+		}}
+}
+
 // GuardianOptionGatewayRelayer configures the Gateway Relayer module. If the gateway relayer smart contract is configured, we will instantiate
 // GuardianOptionGatewayRelayer configures the Gateway Relayer module. If the gateway relayer smart contract is configured, we will instantiate
 // the GatewayRelayer and signed VAAs will be passed to it for processing when they are published. It will forward payload three transfers destined
 // the GatewayRelayer and signed VAAs will be passed to it for processing when they are published. It will forward payload three transfers destined
 // for the specified contract on wormchain to that contract.
 // for the specified contract on wormchain to that contract.
@@ -614,8 +631,8 @@ func GuardianOptionAlternatePublisher(guardianAddr []byte, configs []string) *Gu
 func GuardianOptionProcessor(networkId string) *GuardianOption {
 func GuardianOptionProcessor(networkId string) *GuardianOption {
 	return &GuardianOption{
 	return &GuardianOption{
 		name: "processor",
 		name: "processor",
-		// governor and accountant may be set to nil, but that choice needs to be made before the processor is configured
-		dependencies: []string{"accountant", "alternate-publisher", "db", "gateway-relayer", "governor"},
+		// governor, accountant, and notary may be set to nil, but that choice needs to be made before the processor is configured
+		dependencies: []string{"accountant", "alternate-publisher", "db", "gateway-relayer", "governor", "notary"},
 
 
 		f: func(ctx context.Context, logger *zap.Logger, g *G) error {
 		f: func(ctx context.Context, logger *zap.Logger, g *G) error {
 
 
@@ -633,6 +650,7 @@ func GuardianOptionProcessor(networkId string) *GuardianOption {
 				g.gov,
 				g.gov,
 				g.acct,
 				g.acct,
 				g.acctC.readC,
 				g.acctC.readC,
+				g.notary,
 				g.gatewayRelayer,
 				g.gatewayRelayer,
 				networkId,
 				networkId,
 				g.alternatePublisher,
 				g.alternatePublisher,

+ 521 - 0
node/pkg/notary/notary.go

@@ -0,0 +1,521 @@
+// Notary evaluates the status of [common.MessagePublication]s and makes decisions regarding
+// how they should be processed.
+//
+// Currently, it returns one of three possible verdicts:
+// 1. Approve
+//   - Messages should pass through normally.
+//   - This verdict is used for any message that has a non-error status.
+//
+// 2. Delay
+//   - Messages should be delayed.
+//   - This verdict is used for Anomalous messages.
+//
+// 3. Blackhole
+//   - Messages should be blocked from publication permanently, including for reobservation pathways.
+//   - This status is reserved for messages with a Rejected status.
+//
+// The Notary does not modify message publications nor does it stop them from
+// being processed. It only informs other code what to do. When a message is
+// Delayed or Rejected, the Notary will track it in a database.
+//
+// Delayed messages are stored with a timestamp indicating when they should be
+// released. After the timestamp expires, they can be removed from the
+// database.
+//
+// Because Blackholed messages are meant to be blocked permanently, they should
+// be stored in the database forever. In practice, messages will be marked as
+// Rejected only in very extreme circumstances, so the database should always
+// be small.
+package notary
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"slices"
+	"sync"
+	"time"
+
+	"github.com/certusone/wormhole/node/pkg/common"
+	"github.com/certusone/wormhole/node/pkg/db"
+	"github.com/wormhole-foundation/wormhole/sdk"
+	"github.com/wormhole-foundation/wormhole/sdk/vaa"
+
+	"go.uber.org/zap"
+)
+
+type (
+	// Verdict is an enum that reports what action the Notary has taken after processing a message.
+	Verdict uint8
+)
+
+const (
+	Unknown Verdict = iota
+	// Approve means a message should be processed normally. All messages that are not Token Transfers
+	// must always be Approve, as the Notary does not support other messasge types.
+	Approve
+	// Delay means a message should be temporarily delayed so that it can be manually inspected.
+	Delay
+	// Blackhole means a message should be permanently blocked from being processed.
+	Blackhole
+)
+
+func (v Verdict) String() string {
+	switch v {
+	case Approve:
+		return "Approve"
+	case Delay:
+		return "Delay"
+	case Blackhole:
+		return "Blackhole"
+	case Unknown:
+		return "Unknown"
+	default:
+		return "Unknown"
+	}
+}
+
+const (
+	// How long a message should be held in the pending list before being processed.
+	// The value should be long enough to allow for manual review and classification
+	// by the Guardians.
+	DelayFor = time.Hour * 24 * 4
+)
+
+var (
+	ErrAlreadyInitialized = errors.New("notary: message queues already initialized during database load")
+	ErrAlreadyBlackholed  = errors.New("notary: message is already blackholed")
+	ErrCannotRelease      = errors.New("notary: could not release message")
+	ErrInvalidMsg         = errors.New("notary: message is invalid")
+)
+
+type (
+	// A set corresponding to message publications. The elements of the set must be the results of
+	// the function [common.MessagePublication.VAAHashUnchecked].
+	msgPubSet struct {
+		elements map[string]struct{}
+	}
+
+	Notary struct {
+		ctx    context.Context
+		logger *zap.Logger
+		mutex  sync.RWMutex
+		// database persists information about delayed and black-holed messages.
+		// Must be guarded by a read-write mutex.
+		database db.NotaryDBInterface
+
+		// Min-heap queue of delayed messages (MessagePublication + Timestamp for release)
+		delayed *common.PendingMessageQueue
+
+		// All of the messages that have been black-holed due to being rejected by the Transfer Verifier.
+		// [msgPubSet] is not thread-safe so this field must be guarded by a read-write mutex.
+		blackholed *msgPubSet
+
+		// env reports whether the guardian is running in production or a test environment.
+		env common.Environment
+	}
+)
+
+func NewNotary(
+	ctx context.Context,
+	logger *zap.Logger,
+	guardianDB *db.Database,
+	env common.Environment,
+) *Notary {
+	return &Notary{
+		ctx:    ctx,
+		logger: logger,
+		mutex:  sync.RWMutex{},
+		// Get the underlying database connection from the Guardian.
+		database:   db.NewNotaryDB(guardianDB.Conn()),
+		delayed:    common.NewPendingMessageQueue(),
+		blackholed: nil,
+		env:        env,
+	}
+}
+
+func (n *Notary) Run() error {
+	if n.env != common.GoTest {
+		n.logger.Info("loading notary data from database")
+		if err := n.loadFromDB(n.logger); err != nil {
+			return err
+		}
+	}
+
+	n.logger.Info("notary ready")
+
+	return nil
+}
+
+func (n *Notary) ProcessMsg(msg *common.MessagePublication) (v Verdict, err error) {
+
+	n.logger.Debug("notary: processing message", msg.ZapFields()...)
+
+	// NOTE: Only token transfers originated on Ethereum are currently considered.
+	// For the initial implementation, the Notary only rules on messages based
+	// on the Transfer Verifier. However, there is no technical barrier to
+	// supporting other message types.
+	if msg.EmitterChain != vaa.ChainIDEthereum {
+		n.logger.Debug("notary: automatically approving message publication because it is not from Ethereum", msg.ZapFields()...)
+		return Approve, nil
+	}
+
+	if !vaa.IsTransfer(msg.Payload) {
+		n.logger.Debug("notary: automatically approving message publication because it is not a token transfer", msg.ZapFields()...)
+		return Approve, nil
+	}
+
+	if tokenBridge, ok := sdk.KnownTokenbridgeEmitters[msg.EmitterChain]; !ok {
+		// Return Unknown if the token bridge is not registered in the SDK.
+		n.logger.Error("notary: unknown token bridge emitter", msg.ZapFields()...)
+		return Unknown, errors.New("unknown token bridge emitter")
+	} else {
+		// Approve if the token transfer is not from the token bridge.
+		// For now, the notary only rules on token transfers from the token bridge.
+		if !bytes.Equal(msg.EmitterAddress.Bytes(), tokenBridge) {
+			n.logger.Debug("notary: automatically approving message publication because it is not from the token bridge", msg.ZapFields()...)
+			return Approve, nil
+		}
+	}
+
+	// Return early if the message has already been blackholed. This is important in case a message
+	// is reobserved or otherwise processed here more than once. An Anomalous message that becomes
+	// delayed and later blackholed should not be able to be re-added to the Delayed queue.
+	if n.IsBlackholed(msg) {
+		n.logger.Warn("notary: got message publication that is already blackholed",
+			msg.ZapFields(zap.String("verdict", Blackhole.String()))...,
+		)
+		return Blackhole, nil
+	}
+
+	switch msg.VerificationState() {
+	case common.Anomalous:
+		err = n.delay(msg, DelayFor)
+		v = Delay
+	case common.Rejected:
+		err = n.blackhole(msg)
+		v = Blackhole
+	case common.Valid:
+		v = Approve
+	case common.CouldNotVerify, common.NotVerified, common.NotApplicable:
+		// NOTE: All other statuses are simply approved for now. In the future, it may be
+		// desirable to log a warning if a [common.NotVerified] message is handled here, with
+		// the idea that messages handled by the Notary must already have a non-default
+		// status.
+		n.logger.Debug("notary: got unexpected verification status for token transfer", msg.ZapFields()...)
+		v = Approve
+	}
+
+	n.logger.Debug("notary result",
+		msg.ZapFields(zap.String("verdict", v.String()))...,
+	)
+	return
+}
+
+// ReleaseReadyMessages removes messages from the database and the delayed queue if they are ready to
+// be released. Returns the messages that are ready to be published.
+func (n *Notary) ReleaseReadyMessages() []*common.MessagePublication {
+	if n == nil || n.delayed == nil {
+		return nil
+	}
+
+	n.logger.Debug(
+		"notary: begin process ready message",
+		zap.Int("delayedCount", n.delayed.Len()),
+	)
+	var (
+		readyMsgs = make([]*common.MessagePublication, 0, n.delayed.Len())
+		now       = time.Now()
+	)
+
+	// Pop elements from the queue until the release time is after the current time.
+	// If errors occur, continue instead of returning early so that other messages
+	// can still be processed.
+	for n.delayed.Len() != 0 {
+		next := n.delayed.Peek()
+		if next == nil || next.ReleaseTime.After(now) {
+			break // No more messages to process or next message not ready
+		}
+
+		// Pop reduces the length of n.delayed
+		pMsg := n.delayed.Pop()
+		if pMsg == nil {
+			n.logger.Error("nil message after pop")
+			continue // Skip if Pop returns nil (shouldn't happen if Peek worked)
+		}
+
+		// Update database. Do this before adding the message to the ready list so that we don't
+		// accidentally add the same message twice if deleting the message from the database fails.
+		err := n.database.DeleteDelayed(pMsg)
+		if err != nil {
+			n.logger.Error("delete pending message from notary database", pMsg.Msg.ZapFields(zap.Error(err))...)
+			continue
+		}
+
+		// If the message is in the delayed queue, it should not be in the blackholed queue.
+		// This is a sanity check to ensure that the blackholed queue is not published,
+		// but it should never happen.
+		if n.IsBlackholed(&pMsg.Msg) {
+			n.logger.Error("notary: got blackholed message in delayed queue", pMsg.Msg.ZapFields()...)
+			continue
+		}
+
+		// Append return value.
+		readyMsgs = append(readyMsgs, &pMsg.Msg)
+
+	}
+
+	n.logger.Debug(
+		"notary: finish process ready message",
+		zap.Int("readyCount", len(readyMsgs)),
+		zap.Int("delayedCount", n.delayed.Len()),
+	)
+
+	return readyMsgs
+}
+
+// delay stores a MessagePublication in the database and populates its in-memory
+// representation in the Notary.
+// Acquires the mutex lock and unlocks when complete.
+func (n *Notary) delay(msg *common.MessagePublication, dur time.Duration) error {
+	if msg == nil {
+		return ErrInvalidMsg
+	}
+
+	n.mutex.Lock()
+	defer n.mutex.Unlock()
+
+	// Ensure that the message can't be added to the delayed list or database if it's already blackholed.
+	if n.blackholed.Contains(msg.VAAHash()) {
+		return ErrAlreadyBlackholed
+	}
+
+	// Remove nanoseconds from time.Now(). They are not serialized in the binary
+	// representation. If we don't truncate nanoseconds here, then testing
+	// message equality before and after loading to the database will fail.
+	release := time.Unix(time.Now().Unix(), 0)
+
+	pMsg := &common.PendingMessage{
+		Msg:         *msg,
+		ReleaseTime: release.Add(dur),
+	}
+
+	// Store in in-memory slice. This should happen even if a database error occurs.
+	n.delayed.Push(pMsg)
+
+	// Store in database.
+	dbErr := n.database.StoreDelayed(pMsg)
+	if dbErr != nil {
+		return dbErr
+	}
+
+	n.logger.Info("notary: delayed message", msg.ZapFields()...)
+
+	return dbErr
+}
+
+// blackhole adds a message publication to the blackholed in-memory set and stores it in the database.
+// It also removes the message from the delayed list and database, if present.
+// Acquires the mutex and unlocks when complete.
+func (n *Notary) blackhole(msg *common.MessagePublication) error {
+
+	if msg == nil {
+		return ErrInvalidMsg
+	}
+	n.mutex.Lock()
+
+	// Store in in-memory slice. This should happen even if a database error occurs.
+	n.blackholed.Add(msg.VAAHash())
+
+	// Store in database.
+	dbErr := n.database.StoreBlackholed(msg)
+	if dbErr != nil {
+		// Ensure the mutex is unlocked before returning.
+		// Not using defer for unlocking here because removeDelayed acquires the mutex.
+		n.mutex.Unlock()
+		return dbErr
+	}
+	// Unlock mutex before calling removeDelayed, which also acquires the mutex.
+	n.mutex.Unlock()
+
+	// When a message is blackholed, it should be removed from the delayed list and database.
+	err := n.removeDelayed(msg)
+	if err != nil {
+		return err
+	}
+
+	n.logger.Info("notary: blackholed message", msg.ZapFields()...)
+
+	return nil
+}
+
+// forget removes a message from the database and from the delayed and blackholed lists.
+func (n *Notary) forget(msg *common.MessagePublication) error {
+	if msg == nil {
+		return ErrInvalidMsg
+	}
+
+	// Both of the following methods lock and unlock the mutex.
+
+	err := n.removeDelayed(msg)
+	if err != nil {
+		return err
+	}
+
+	err = n.removeBlackholed(msg)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// IsBlackholed returns true if the message is in the blackholed list.
+func (n *Notary) IsBlackholed(msg *common.MessagePublication) bool {
+	n.mutex.RLock()
+	defer n.mutex.RUnlock()
+	return n.blackholed.Contains(msg.VAAHash())
+}
+
+// removeBlackholed removes a message from the blackholed list and database.
+// Acquires the mutex and unlocks when complete.
+func (n *Notary) removeBlackholed(msg *common.MessagePublication) error {
+	n.mutex.Lock()
+	defer n.mutex.Unlock()
+	if msg == nil {
+		return ErrInvalidMsg
+	}
+
+	n.blackholed.Remove(msg.VAAHash())
+
+	err := n.database.DeleteBlackholed(msg)
+	if err != nil {
+		return err
+	}
+
+	n.logger.Info("notary: removed blackholed message", msg.ZapFields()...)
+
+	return nil
+}
+
+func (n *Notary) IsDelayed(msg *common.MessagePublication) bool {
+	// The notary's mutex is not used here because the pending message queue
+	// uses its own read mutex for this method.
+	return n.delayed.ContainsMessagePublication(msg)
+}
+
+// removeDelayed removes a message from the delayed list and database.
+func (n *Notary) removeDelayed(msg *common.MessagePublication) error {
+	n.mutex.Lock()
+	defer n.mutex.Unlock()
+	if msg == nil {
+		return ErrInvalidMsg
+	}
+	removed, err := n.delayed.RemoveItem(msg)
+	if err != nil {
+		return err
+	}
+
+	if removed != nil {
+		err := n.database.DeleteDelayed(removed)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// loadFromDB reads all the database entries.
+func (n *Notary) loadFromDB(logger *zap.Logger) error {
+	n.mutex.Lock()
+	defer n.mutex.Unlock()
+
+	result, err := n.database.LoadAll(logger)
+	if err != nil {
+		n.logger.Error(
+			"notary: LoadAll call returned error",
+			zap.Error(err),
+		)
+		return err
+	}
+	if result == nil {
+		n.logger.Error(
+			"notary: LoadAll call produced nil result",
+		)
+		return errors.New("nil result from database")
+	}
+
+	n.logger.Info(
+		"loaded notary data from database",
+		zap.Int("delayedMsgs", len(result.Delayed)),
+		zap.Int("blackholedMsgs", len(result.Blackholed)),
+	)
+
+	// Avoid overwriting data by mistake.
+	if n.delayed != nil && n.delayed.Len() > 0 {
+		return ErrAlreadyInitialized
+	}
+
+	var (
+		delayed    = common.NewPendingMessageQueue()
+		blackholed = NewSet()
+	)
+
+	if len(result.Delayed) > 0 {
+		for entry := range slices.Values(result.Delayed) {
+			delayed.Push(entry)
+		}
+	}
+
+	if len(result.Blackholed) > 0 {
+		for result := range slices.Values(result.Blackholed) {
+			blackholed.Add(result.VAAHash())
+		}
+	}
+
+	n.blackholed = blackholed
+	n.delayed = delayed
+	n.logger.Info(
+		"initialized notary",
+		zap.Int("delayedMsgs", n.delayed.Len()),
+		zap.Int("blackholedMsgs", n.blackholed.Len()),
+	)
+
+	return nil
+}
+
+// NewSet creates and initializes a new Set
+func NewSet() *msgPubSet {
+	return &msgPubSet{
+		elements: make(map[string]struct{}),
+	}
+}
+
+func (s *msgPubSet) Len() int {
+	return len(s.elements)
+}
+
+// Add adds an element to the set
+func (s *msgPubSet) Add(element string) {
+	if s == nil {
+		return // Protect against nil receiver
+	}
+	s.elements[element] = struct{}{}
+}
+
+// Contains checks if an element is in the set
+func (s *msgPubSet) Contains(element string) bool {
+	if s == nil {
+		return false // Protect against nil receiver
+	}
+	_, exists := s.elements[element]
+	return exists
+}
+
+// Remove removes an element from the set
+func (s *msgPubSet) Remove(element string) {
+	if s == nil {
+		return // Protect against nil receiver
+	}
+	delete(s.elements, element)
+}

+ 489 - 0
node/pkg/notary/notary_test.go

@@ -0,0 +1,489 @@
+package notary
+
+import (
+	"context"
+	"encoding/binary"
+	"fmt"
+	"math/big"
+	"math/rand/v2"
+	"slices"
+	"sync"
+	"testing"
+	"time"
+
+	"github.com/certusone/wormhole/node/pkg/common"
+	"github.com/certusone/wormhole/node/pkg/db"
+	"github.com/stretchr/testify/require"
+	"github.com/wormhole-foundation/wormhole/sdk"
+	"github.com/wormhole-foundation/wormhole/sdk/vaa"
+	"go.uber.org/zap"
+
+	eth_common "github.com/ethereum/go-ethereum/common"
+)
+
+type MockNotaryDB struct{}
+
+func (md MockNotaryDB) StoreBlackholed(m *common.MessagePublication) error  { return nil }
+func (md MockNotaryDB) StoreDelayed(p *common.PendingMessage) error         { return nil }
+func (md MockNotaryDB) DeleteBlackholed(m *common.MessagePublication) error { return nil }
+func (md MockNotaryDB) DeleteDelayed(p *common.PendingMessage) error        { return nil }
+func (md MockNotaryDB) LoadAll(l *zap.Logger) (*db.NotaryLoadResult, error) { return nil, nil }
+
+func makeTestNotary(t *testing.T) *Notary {
+	t.Helper()
+
+	return &Notary{
+		ctx:        context.Background(),
+		logger:     zap.NewNop(),
+		mutex:      sync.RWMutex{},
+		database:   MockNotaryDB{},
+		delayed:    &common.PendingMessageQueue{},
+		blackholed: NewSet(),
+		env:        common.GoTest,
+	}
+}
+
+func TestNotary_ProcessMessageCorrectVerdict(t *testing.T) {
+
+	// NOTE: This test should be exhaustive over VerificationState variants.
+	tests := map[string]struct {
+		verificationState common.VerificationState
+		verdict           Verdict
+	}{
+		"approve N/A": {
+			common.NotApplicable,
+			Approve,
+		},
+		"approve not verified": {
+			common.NotVerified,
+			Approve,
+		},
+		"approve valid": {
+			common.Valid,
+			Approve,
+		},
+		"approve could not verify": {
+			common.CouldNotVerify,
+			Approve,
+		},
+		"blackhole rejected": {
+			common.Rejected,
+			Blackhole,
+		},
+		"delay anomalous": {
+			common.Anomalous,
+			Delay,
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			n := makeTestNotary(t)
+			msg := makeUniqueMessagePublication(t)
+
+			err := msg.SetVerificationState(test.verificationState)
+			if test.verificationState != common.NotVerified {
+				// SetVerificationState fails if the old status is equal to the new one.
+				require.NoError(t, err)
+			}
+
+			require.True(t, vaa.IsTransfer(msg.Payload))
+
+			verdict, err := n.ProcessMsg(msg)
+			require.NoError(t, err)
+			require.Equal(
+				t,
+				test.verdict,
+				verdict,
+				fmt.Sprintf("verificationState=%s verdict=%s", msg.VerificationState().String(), verdict.String()),
+			)
+		})
+	}
+}
+func TestNotary_ProcessMsgUpdatesCollections(t *testing.T) {
+
+	// NOTE: This test should be exhaustive over VerificationState variants.
+	type expectedSizes struct {
+		delayed    int
+		blackholed int
+	}
+	tests := map[string]struct {
+		verificationState common.VerificationState
+		expectedSizes
+	}{
+		"Valid has no effect": {
+			common.Valid,
+			expectedSizes{},
+		},
+		"NotVerified has no effect": {
+			common.NotVerified,
+			expectedSizes{},
+		},
+		"NotApplicable has no effect": {
+			common.NotApplicable,
+			expectedSizes{},
+		},
+		"CouldNotVerify has no effect": {
+			common.CouldNotVerify,
+			expectedSizes{},
+		},
+		"Anomalous gets delayed": {
+			common.Anomalous,
+			expectedSizes{
+				delayed:    1,
+				blackholed: 0,
+			},
+		},
+		"Rejected gets blackholed": {
+			common.Rejected,
+			expectedSizes{
+				delayed:    0,
+				blackholed: 1,
+			},
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			// Set-up
+			var (
+				n   = makeTestNotary(t)
+				msg = makeUniqueMessagePublication(t)
+				err = msg.SetVerificationState(test.verificationState)
+			)
+			if test.verificationState != common.NotVerified {
+				// SetVerificationState fails if the old status is equal to the new one.
+				require.NoError(t, err)
+			}
+			require.Equal(t, test.verificationState, msg.VerificationState())
+			require.True(t, vaa.IsTransfer(msg.Payload))
+
+			// Ensure that the collections are properly updated.
+			_, err = n.ProcessMsg(msg)
+			require.NoError(t, err)
+			require.Equal(
+				t,
+				test.expectedSizes.delayed,
+				n.delayed.Len(),
+				fmt.Sprintf("delayed count did not match. verificationState %s", msg.VerificationState().String()),
+			)
+			require.Equal(
+				t,
+				test.expectedSizes.blackholed,
+				n.blackholed.Len(),
+				fmt.Sprintf("blackholed count did not match. verificationState %s", msg.VerificationState().String()),
+			)
+
+		})
+	}
+}
+
+func TestNotary_ProcessMessageAlwaysApprovesNonTokenTransfers(t *testing.T) {
+	n := makeTestNotary(t)
+
+	// NOTE: This test should be exhaustive over VerificationState variants.
+	tests := map[string]struct {
+		verificationState common.VerificationState
+	}{
+		"approve non-token transfer: NotVerified": {
+			common.NotVerified,
+		},
+		"approve non-token transfer: CouldNotVerify": {
+			common.CouldNotVerify,
+		},
+		"approve non-token transfer: Anomalous": {
+			common.Anomalous,
+		},
+		"approve non-token transfer: Rejected": {
+			common.Rejected,
+		},
+		"approve non-token transfer: NotApplicable": {
+			common.NotApplicable,
+		},
+		"approve non-token transfer: Valid": {
+			common.Valid,
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			msg := makeUniqueMessagePublication(t)
+
+			// Change the payload to something other than a token transfer.
+			msg.Payload = []byte{0x02}
+			require.False(t, vaa.IsTransfer(msg.Payload))
+
+			if msg.VerificationState() != common.NotVerified {
+				// SetVerificationState fails if the old status is equal to the new one.
+				err := msg.SetVerificationState(test.verificationState)
+				require.NoError(t, err)
+			}
+
+			verdict, err := n.ProcessMsg(msg)
+			require.NoError(t, err)
+			require.Equal(t, Approve, verdict)
+		})
+	}
+}
+
+func TestNotary_ProcessReadyMessages(t *testing.T) {
+
+	tests := []struct {
+		name               string                   // description of this test case
+		delayed            []*common.PendingMessage // initial messages in delayed queue
+		expectedDelayCount int
+		expectedReadyCount int
+	}{
+		{
+			"no messages ready",
+			[]*common.PendingMessage{
+				{
+					ReleaseTime: time.Now().Add(time.Hour),
+					Msg:         *makeUniqueMessagePublication(t),
+				},
+			},
+			1,
+			0,
+		},
+		{
+			"some messages ready",
+			[]*common.PendingMessage{
+				{
+					ReleaseTime: time.Now().Add(-2 * time.Hour),
+					Msg:         *makeUniqueMessagePublication(t),
+				},
+				{
+					ReleaseTime: time.Now().Add(time.Hour),
+					Msg:         *makeUniqueMessagePublication(t),
+				},
+				{
+					ReleaseTime: time.Now().Add(-time.Hour),
+					Msg:         *makeUniqueMessagePublication(t),
+				},
+				{
+					ReleaseTime: time.Now().Add(2 * time.Hour),
+					Msg:         *makeUniqueMessagePublication(t),
+				},
+			},
+			2,
+			2,
+		},
+		{
+			"all messages ready",
+			[]*common.PendingMessage{
+				{
+					ReleaseTime: time.Now().Add(-2 * time.Hour),
+					Msg:         *makeUniqueMessagePublication(t),
+				},
+				{
+					ReleaseTime: time.Now().Add(-1 * time.Hour),
+					Msg:         *makeUniqueMessagePublication(t),
+				},
+			},
+			0,
+			2,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Set-up
+			n := makeTestNotary(t)
+			n.delayed = common.NewPendingMessageQueue()
+
+			currentLength := n.delayed.Len()
+			for pMsg := range slices.Values(tt.delayed) {
+				require.NotNil(t, pMsg)
+				n.delayed.Push(pMsg)
+				// Ensure that the queue grows after each push.
+				require.Greater(t, n.delayed.Len(), currentLength)
+				currentLength = n.delayed.Len()
+			}
+			require.Equal(t, len(tt.delayed), n.delayed.Len())
+
+			readyMsgs := n.ReleaseReadyMessages()
+			require.Equal(t, tt.expectedReadyCount, len(readyMsgs), "ready length does not match")
+			require.Equal(t, tt.expectedDelayCount, n.delayed.Len(), "delayed length does not match")
+		})
+	}
+}
+
+func TestNotary_Forget(t *testing.T) {
+	tests := []struct { // description of this test case
+		name               string
+		msg                *common.MessagePublication
+		expectedDelayCount int
+		expectedBlackholed int
+	}{
+		{
+			"remove from delayed list",
+			makeUniqueMessagePublication(t),
+			0,
+			0,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Set-up
+			n := makeTestNotary(t)
+			n.delayed = common.NewPendingMessageQueue()
+			n.blackholed = NewSet()
+
+			require.Equal(t, 0, n.delayed.Len())
+			require.Equal(t, 0, n.blackholed.Len())
+
+			err := n.delay(tt.msg, time.Hour)
+			require.NoError(t, err)
+
+			require.Equal(t, 1, n.delayed.Len())
+			require.Equal(t, 0, n.blackholed.Len())
+
+			// Modify the set manually because calling the blackhole function will remove the message from the delayed list.
+			n.blackholed.Add(tt.msg.VAAHash())
+
+			require.Equal(t, 1, n.delayed.Len())
+			require.Equal(t, 1, n.blackholed.Len())
+
+			err = n.forget(tt.msg)
+			require.NoError(t, err)
+
+			require.Equal(t, tt.expectedDelayCount, n.delayed.Len())
+			require.Equal(t, tt.expectedBlackholed, n.blackholed.Len())
+		})
+	}
+}
+
+func TestNotary_BlackholeRemovesFromDelayedList(t *testing.T) {
+	tests := []struct { // description of this test case
+		name               string
+		msg                *common.MessagePublication
+		expectedDelayCount int
+		expectedBlackholed int
+	}{
+		{
+			"remove from delayed list",
+			makeUniqueMessagePublication(t),
+			0,
+			1,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Set-up
+			n := makeTestNotary(t)
+			n.delayed = common.NewPendingMessageQueue()
+			n.blackholed = NewSet()
+
+			require.Equal(t, 0, n.delayed.Len())
+			require.Equal(t, 0, n.blackholed.Len())
+
+			err := n.delay(tt.msg, time.Hour)
+			require.NoError(t, err)
+
+			require.Equal(t, 1, n.delayed.Len())
+			require.Equal(t, 0, n.blackholed.Len())
+
+			err = n.blackhole(tt.msg)
+			require.NoError(t, err)
+
+			require.Equal(t, 0, n.delayed.Len())
+			require.Equal(t, 1, n.blackholed.Len())
+		})
+	}
+}
+
+func TestNotary_DelayFailsIfMessageAlreadyBlackholed(t *testing.T) {
+	tests := []struct { // description of this test case
+		name string
+		msg  *common.MessagePublication
+	}{
+		{
+			"delay fails if message is already blackholed",
+			makeUniqueMessagePublication(t),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Set-up
+			n := makeTestNotary(t)
+			n.delayed = common.NewPendingMessageQueue()
+			n.blackholed = NewSet()
+
+			require.Equal(t, 0, n.delayed.Len())
+			require.Equal(t, 0, n.blackholed.Len())
+
+			err := n.blackhole(tt.msg)
+			require.NoError(t, err)
+
+			require.Equal(t, 0, n.delayed.Len())
+			require.Equal(t, 1, n.blackholed.Len())
+
+			err = n.delay(tt.msg, time.Hour)
+			require.ErrorIs(t, err, ErrAlreadyBlackholed)
+
+			require.Equal(t, 0, n.delayed.Len())
+			require.Equal(t, 1, n.blackholed.Len())
+		})
+	}
+}
+
+// Helper function that returns a valid PendingMessage. It creates identical messages publications
+// with different sequence numbers.
+func makeUniqueMessagePublication(t *testing.T) *common.MessagePublication {
+	t.Helper()
+
+	originAddress, err := vaa.StringToAddress("0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E") //nolint:gosec
+	require.NoError(t, err)
+
+	targetAddress, err := vaa.StringToAddress("0x707f9118e33a9b8998bea41dd0d46f38bb963fc8")
+	require.NoError(t, err)
+
+	// Required as the Notary checks the emitter address.
+	tokenBridge := sdk.KnownTokenbridgeEmitters[vaa.ChainIDEthereum]
+	tokenBridgeAddress := vaa.Address(tokenBridge)
+	require.NoError(t, err)
+
+	payload := &vaa.TransferPayloadHdr{
+		Type:          0x01,
+		Amount:        big.NewInt(27000000000),
+		OriginAddress: originAddress,
+		OriginChain:   vaa.ChainIDEthereum,
+		TargetAddress: targetAddress,
+		TargetChain:   vaa.ChainIDPolygon,
+	}
+	payloadBytes := encodePayloadBytes(payload)
+
+	// #nosec: G404 -- Cryptographically secure pseudo-random number generator not needed.
+	var sequence = rand.Uint64()
+	msgpub := &common.MessagePublication{
+		TxID:             eth_common.HexToHash("0x06f541f5ecfc43407c31587aa6ac3a689e8960f36dc23c332db5510dfc6a4063").Bytes(),
+		Timestamp:        time.Unix(int64(1654516425), 0),
+		Nonce:            123456,
+		Sequence:         sequence,
+		EmitterChain:     vaa.ChainIDEthereum,
+		EmitterAddress:   tokenBridgeAddress,
+		Payload:          payloadBytes,
+		ConsistencyLevel: 32,
+		Unreliable:       true,
+		IsReobservation:  true,
+		// verificationState is set to NotVerified by default.
+	}
+
+	return msgpub
+}
+
+func encodePayloadBytes(payload *vaa.TransferPayloadHdr) []byte {
+	bz := make([]byte, 101)
+	bz[0] = payload.Type
+
+	amtBytes := payload.Amount.Bytes()
+	if len(amtBytes) > 32 {
+		panic("amount will not fit in 32 bytes!")
+	}
+	copy(bz[33-len(amtBytes):33], amtBytes)
+
+	copy(bz[33:65], payload.OriginAddress.Bytes())
+	binary.BigEndian.PutUint16(bz[65:67], uint16(payload.OriginChain))
+	copy(bz[67:99], payload.TargetAddress.Bytes())
+	binary.BigEndian.PutUint16(bz[99:101], uint16(payload.TargetChain))
+	return bz
+}

+ 109 - 6
node/pkg/processor/processor.go

@@ -11,6 +11,7 @@ import (
 	guardianDB "github.com/certusone/wormhole/node/pkg/db"
 	guardianDB "github.com/certusone/wormhole/node/pkg/db"
 	"github.com/certusone/wormhole/node/pkg/governor"
 	"github.com/certusone/wormhole/node/pkg/governor"
 	"github.com/certusone/wormhole/node/pkg/guardiansigner"
 	"github.com/certusone/wormhole/node/pkg/guardiansigner"
+	guardianNotary "github.com/certusone/wormhole/node/pkg/notary"
 	"github.com/certusone/wormhole/node/pkg/p2p"
 	"github.com/certusone/wormhole/node/pkg/p2p"
 
 
 	ethcommon "github.com/ethereum/go-ethereum/common"
 	ethcommon "github.com/ethereum/go-ethereum/common"
@@ -28,7 +29,7 @@ import (
 	"github.com/prometheus/client_golang/prometheus/promauto"
 	"github.com/prometheus/client_golang/prometheus/promauto"
 )
 )
 
 
-var GovInterval = time.Minute
+var PollInterval = time.Minute
 var CleanupInterval = time.Second * 30
 var CleanupInterval = time.Second * 30
 
 
 type (
 type (
@@ -149,6 +150,7 @@ type Processor struct {
 	governor       *governor.ChainGovernor
 	governor       *governor.ChainGovernor
 	acct           *accountant.Accountant
 	acct           *accountant.Accountant
 	acctReadC      <-chan *common.MessagePublication
 	acctReadC      <-chan *common.MessagePublication
+	notary         *guardianNotary.Notary
 	pythnetVaas    map[string]PythNetVaaEntry
 	pythnetVaas    map[string]PythNetVaaEntry
 	gatewayRelayer *gwrelayer.GatewayRelayer
 	gatewayRelayer *gwrelayer.GatewayRelayer
 	updateVAALock  sync.Mutex
 	updateVAALock  sync.Mutex
@@ -225,6 +227,7 @@ func NewProcessor(
 	g *governor.ChainGovernor,
 	g *governor.ChainGovernor,
 	acct *accountant.Accountant,
 	acct *accountant.Accountant,
 	acctReadC <-chan *common.MessagePublication,
 	acctReadC <-chan *common.MessagePublication,
+	notary *guardianNotary.Notary,
 	gatewayRelayer *gwrelayer.GatewayRelayer,
 	gatewayRelayer *gwrelayer.GatewayRelayer,
 	networkID string,
 	networkID string,
 	alternatePublisher *altpub.AlternatePublisher,
 	alternatePublisher *altpub.AlternatePublisher,
@@ -249,6 +252,7 @@ func NewProcessor(
 		governor:       g,
 		governor:       g,
 		acct:           acct,
 		acct:           acct,
 		acctReadC:      acctReadC,
 		acctReadC:      acctReadC,
+		notary:         notary,
 		pythnetVaas:    make(map[string]PythNetVaaEntry),
 		pythnetVaas:    make(map[string]PythNetVaaEntry),
 		gatewayRelayer: gatewayRelayer,
 		gatewayRelayer: gatewayRelayer,
 		batchObsvPubC:  make(chan *gossipv1.Observation, batchObsvPubChanSize),
 		batchObsvPubC:  make(chan *gossipv1.Observation, batchObsvPubChanSize),
@@ -269,7 +273,7 @@ func (p *Processor) Run(ctx context.Context) error {
 	cleanup := time.NewTicker(CleanupInterval)
 	cleanup := time.NewTicker(CleanupInterval)
 
 
 	// Always initialize the timer so don't have a nil pointer in the case below. It won't get rearmed after that.
 	// Always initialize the timer so don't have a nil pointer in the case below. It won't get rearmed after that.
-	govTimer := time.NewTimer(GovInterval)
+	pollTimer := time.NewTimer(PollInterval)
 
 
 	for {
 	for {
 		select {
 		select {
@@ -286,17 +290,73 @@ func (p *Processor) Run(ctx context.Context) error {
 			)
 			)
 			p.gst.Set(p.gs)
 			p.gst.Set(p.gs)
 		case k := <-p.msgC:
 		case k := <-p.msgC:
+			// This is the main message processing loop. It is responsible for handling messages that are
+			// received on the message channel. Depending on the configuration, a message may be processed
+			// by the Notary, the Governor, and/or the Accountant.
+			// This loop effectively causes each of these components to process messages in a modular
+			// manner. The Notary, Governor, and Accountant can be enabled or disabled independently.
+			// As a consequence of this loop, each of these components updates its internal state, tracking
+			// whether a message is ready to be processed from its perspective. This state is used by the
+			// processor to determine whether a message should be processed or not. This occurs elsewhere
+			// in the processor code.
+
+			p.logger.Debug("processor: received new message publication on message channel", k.ZapFields()...)
+
+			// Notary: check whether a message is well-formed.
+			// Send messages to the Notary first. If messages are not approved, they should not continue
+			// to the Governor or the Accountant.
+			if p.notary != nil {
+				p.logger.Debug("processor: sending message to notary for evaluation", k.ZapFields()...)
+
+				// NOTE: Always returns Approve for messages that are not token transfers.
+				verdict, err := p.notary.ProcessMsg(k)
+				if err != nil {
+					// TODO: The error is deliberately ignored so that the processor does not panic and restart.
+					// In contrast, the Accountant does not ignore the error and restarts the processor if it fails.
+					// The error-handling strategy can be revisited once the Notary is considered stable.
+					p.logger.Error("notary failed to process message", zap.Error(err), zap.String("messageID", k.MessageIDString()))
+					continue
+				}
+
+				// Based on the verdict, we can decide what to do with the message.
+				switch verdict {
+				case guardianNotary.Blackhole, guardianNotary.Delay:
+					p.logger.Error("notary evaluated message as threatening", k.ZapFields(zap.String("verdict", verdict.String()))...)
+					if verdict == guardianNotary.Blackhole {
+						// Black-holed messages should not be processed.
+						p.logger.Error("message will not be processed", k.ZapFields(zap.String("verdict", verdict.String()))...)
+					} else {
+						// Delayed messages are added to a separate queue and processed elsewhere.
+						p.logger.Error("message will be delayed", k.ZapFields(zap.String("verdict", verdict.String()))...)
+					}
+					// We're done processing the message.
+					continue
+				case guardianNotary.Unknown:
+					p.logger.Error("notary returned Unknown verdict", k.ZapFields(zap.String("verdict", verdict.String()))...)
+				case guardianNotary.Approve:
+					// no-op: process normally
+					p.logger.Debug("notary evaluated message as approved", k.ZapFields(zap.String("verdict", verdict.String()))...)
+				default:
+					p.logger.Error("notary returned unrecognized verdict", k.ZapFields(zap.String("verdict", verdict.String()))...)
+				}
+			}
+
+			// Governor: check if a message is ready to be published.
 			if p.governor != nil {
 			if p.governor != nil {
 				if !p.governor.ProcessMsg(k) {
 				if !p.governor.ProcessMsg(k) {
+					// We're done processing the message.
 					continue
 					continue
 				}
 				}
 			}
 			}
+
+			// Accountant: check if a message is ready to be published (i.e. if it has enough observations).
 			if p.acct != nil {
 			if p.acct != nil {
 				shouldPub, err := p.acct.SubmitObservation(k)
 				shouldPub, err := p.acct.SubmitObservation(k)
 				if err != nil {
 				if err != nil {
-					return fmt.Errorf("failed to process message `%s`: %w", k.MessageIDString(), err)
+					return fmt.Errorf("accountant: failed to process message `%s`: %w", k.MessageIDString(), err)
 				}
 				}
 				if !shouldPub {
 				if !shouldPub {
+					// We're done processing the message.
 					continue
 					continue
 				}
 				}
 			}
 			}
@@ -318,7 +378,49 @@ func (p *Processor) Run(ctx context.Context) error {
 			p.handleInboundSignedVAAWithQuorum(m)
 			p.handleInboundSignedVAAWithQuorum(m)
 		case <-cleanup.C:
 		case <-cleanup.C:
 			p.handleCleanup(ctx)
 			p.handleCleanup(ctx)
-		case <-govTimer.C:
+		case <-pollTimer.C:
+			// Poll the pending lists for messages that can be released. Both the Notary and the Governor
+			// can delay messages.
+			// As each of the Notary, Governor, and Accountant can be enabled separately, each must
+			// be processed in a modular way.
+			// When more than one of these features are enabled, messages should be processed
+			// serially in the order: Notary -> Governor -> Accountant.
+			// NOTE: The Accountant can signal to a channel that it is ready to publish a message via
+			// writing to acctReadC so it is not handled here.
+			if p.notary != nil {
+				readyMsgs := p.notary.ReleaseReadyMessages()
+
+				// Iterate over all ready messages. Hand-off to the Governor or the Accountant
+				// if they're enabled. If not, publish.
+				for _, msg := range readyMsgs {
+					// TODO: Much of this is duplicated from the msgC branch. It might be a good
+					// idea to refactor how we handle combinations of Notary, Governor, and Accountant being
+					// enabled.
+
+					// Hand-off to governor
+					if p.governor != nil {
+						if !p.governor.ProcessMsg(msg) {
+							continue
+						}
+					}
+
+					// Hand-off to accountant. If we get here, both the Notary and the Governor
+					// have signalled that the message is OK to publish.
+					if p.acct != nil {
+						shouldPub, err := p.acct.SubmitObservation(msg)
+						if err != nil {
+							return fmt.Errorf("accountant: failed to process message `%s`: %w", msg.MessageIDString(), err)
+						}
+						if !shouldPub {
+							continue
+						}
+					}
+
+					// Notary, Governor, and Accountant have all approved.
+					p.handleMessage(ctx, msg)
+				}
+			}
+
 			if p.governor != nil {
 			if p.governor != nil {
 				toBePublished, err := p.governor.CheckPending()
 				toBePublished, err := p.governor.CheckPending()
 				if err != nil {
 				if err != nil {
@@ -345,8 +447,9 @@ func (p *Processor) Run(ctx context.Context) error {
 					}
 					}
 				}
 				}
 			}
 			}
-			if (p.governor != nil) || (p.acct != nil) {
-				govTimer.Reset(GovInterval)
+
+			if (p.notary != nil) || (p.governor != nil) || (p.acct != nil) {
+				pollTimer.Reset(PollInterval)
 			}
 			}
 		}
 		}
 	}
 	}

+ 135 - 0
node/pkg/watchers/aztec/block_fetcher.go

@@ -0,0 +1,135 @@
+package aztec
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/ethereum/go-ethereum/rpc"
+	"go.uber.org/zap"
+)
+
+// BlockFetcher defines the interface for retrieving Aztec chain data
+type BlockFetcher interface {
+	FetchPublicLogs(ctx context.Context, fromBlock, toBlock int) ([]ExtendedPublicLog, error)
+	FetchBlock(ctx context.Context, blockNumber int) (BlockInfo, error)
+}
+
+// aztecBlockFetcher is the implementation of BlockFetcher
+type aztecBlockFetcher struct {
+	rpcClient *rpc.Client
+	logger    *zap.Logger
+}
+
+// NewAztecBlockFetcher creates a new block fetcher
+func NewAztecBlockFetcher(ctx context.Context, rpcURL string, logger *zap.Logger) (BlockFetcher, error) {
+	// Create a new RPC client
+	client, err := rpc.DialContext(ctx, rpcURL)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create RPC client: %v", err)
+	}
+
+	return &aztecBlockFetcher{
+		rpcClient: client,
+		logger:    logger,
+	}, nil
+}
+
+// Guardians should set ARCHIVER_MAX_LOGS to a high number in the Aztec node
+// since we don't support pagination in the client.
+// FetchPublicLogs gets logs for a specific block range
+func (f *aztecBlockFetcher) FetchPublicLogs(ctx context.Context, fromBlock, toBlock int) ([]ExtendedPublicLog, error) {
+	f.logger.Debug("Fetching logs",
+		zap.Int("fromBlock", fromBlock),
+		zap.Int("toBlock", toBlock))
+
+	// Prepare the filter arguments
+	logFilter := map[string]interface{}{
+		"fromBlock": fromBlock,
+		"toBlock":   toBlock,
+	}
+
+	// Create a variable to hold the result
+	var result struct {
+		Logs       []ExtendedPublicLog `json:"logs"`
+		MaxLogsHit bool                `json:"maxLogsHit"`
+	}
+
+	// Make the RPC call
+	err := f.rpcClient.CallContext(ctx, &result, "node_getPublicLogs", logFilter)
+	if err != nil {
+		return nil, fmt.Errorf("failed to fetch public logs: %v", err)
+	}
+
+	return result.Logs, nil
+}
+
+// FetchBlock gets info for a specific block
+func (f *aztecBlockFetcher) FetchBlock(ctx context.Context, blockNumber int) (BlockInfo, error) {
+	// Create variables to hold the result
+	var blockResult BlockResult
+
+	// Make the RPC call
+	err := f.rpcClient.CallContext(ctx, &blockResult, "node_getBlock", blockNumber)
+	if err != nil {
+		return BlockInfo{}, fmt.Errorf("failed to fetch block info: %v", err)
+	}
+
+	info := BlockInfo{}
+
+	// Set the block hash using the archive root
+	info.archiveRoot = blockResult.Archive.Root
+
+	// Set the parent hash using lastArchive.root
+	info.parentArchiveRoot = blockResult.Header.LastArchive.Root
+
+	// Get the timestamp from global variables (remove 0x prefix and convert from hex)
+	timestampHex := strings.TrimPrefix(blockResult.Header.GlobalVariables.Timestamp, "0x")
+	if timestampHex == "" {
+		// Handle empty timestamp (typically for genesis block)
+		if blockNumber == 0 {
+			// Use a default timestamp for genesis block
+			info.Timestamp = 0 // Or any appropriate value
+			f.logger.Debug("Genesis block has no timestamp, using default value")
+		} else {
+			// Use current time as fallback for non-genesis blocks
+			unixTime := time.Now().Unix()
+			if unixTime < 0 {
+				// Handle negative timestamp - this shouldn't happen in practice
+				// but gosec wants us to check for it
+				info.Timestamp = 0
+			} else {
+				info.Timestamp = uint64(unixTime)
+			}
+			f.logger.Warn("Block has empty timestamp, using current time",
+				zap.Int("blockNumber", blockNumber))
+		}
+	} else {
+		// Parse the timestamp normally
+		timestamp, err := strconv.ParseUint(timestampHex, 16, 64)
+		if err != nil {
+			return BlockInfo{}, fmt.Errorf("failed parsing timestamp: %v", err)
+		}
+		info.Timestamp = timestamp
+	}
+
+	// Default transaction hash
+	info.TxHash = "0x0"
+
+	// Store transaction hashes by index for log processing
+	info.TxHashesByIndex = make(map[int]string)
+	for i, txEffect := range blockResult.Body.TxEffects {
+		info.TxHashesByIndex[i] = txEffect.TxHash
+	}
+
+	// Log the block hash and parent hash for debugging
+	f.logger.Debug("Fetched block info",
+		zap.Int("blockNumber", blockNumber),
+		zap.String("archiveRoot", info.archiveRoot),
+		zap.String("parentArchiveRoot", info.parentArchiveRoot),
+		zap.Int("txCount", len(blockResult.Body.TxEffects)))
+
+	return info, nil
+}

+ 111 - 0
node/pkg/watchers/aztec/block_processor.go

@@ -0,0 +1,111 @@
+package aztec
+
+import (
+	"context"
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+// processBlocks processes new blocks since the last check
+func (w *Watcher) processBlocks(ctx context.Context) error {
+	// Get the latest finalized block from L2Tips
+	finalizedBlock, err := w.l1Verifier.GetFinalizedBlock(ctx)
+	if err != nil {
+		w.logger.Error("Failed to fetch latest finalized block", zap.Error(err))
+		return err
+	}
+
+	// Check if there are new blocks to process
+	if w.lastBlockNumber >= finalizedBlock.Number {
+		w.logger.Debug("No new finalized blocks to process",
+			zap.Int("latest_finalized", finalizedBlock.Number),
+			zap.Int("last_processed", w.lastBlockNumber))
+		return nil
+	}
+
+	w.logger.Info("Processing new finalized blocks",
+		zap.Int("from", w.lastBlockNumber+1),
+		zap.Int("to", finalizedBlock.Number))
+
+	// Process blocks from last+1 to finalized
+	for blockNumber := w.lastBlockNumber + 1; blockNumber <= finalizedBlock.Number; blockNumber++ {
+		// Fetch block info
+		blockInfo, err := w.blockFetcher.FetchBlock(ctx, blockNumber)
+		if err != nil {
+			w.logger.Error("Failed to fetch block info",
+				zap.Int("blockNumber", blockNumber),
+				zap.Error(err))
+			return err
+		}
+
+		// Process the block's logs
+		if err := w.processBlockLogs(ctx, blockNumber, blockInfo); err != nil {
+			w.logger.Error("Failed to process block logs",
+				zap.Int("blockNumber", blockNumber),
+				zap.Error(err))
+			return err
+		}
+
+		// Update the last processed block number
+		w.lastBlockNumber = blockNumber
+	}
+
+	return nil
+}
+
+// processBlockLogs processes the logs in a single block
+func (w *Watcher) processBlockLogs(ctx context.Context, blockNumber int, blockInfo BlockInfo) error {
+	logs, err := w.blockFetcher.FetchPublicLogs(ctx, blockNumber, blockNumber+1)
+	if err != nil {
+		return fmt.Errorf("failed to fetch public logs: %v", err)
+	}
+
+	// Only log if there are actually logs to process
+	if len(logs) > 0 {
+		w.logger.Info("Processing logs",
+			zap.Int("count", len(logs)),
+			zap.Int("blockNumber", blockNumber))
+	}
+
+	// Process each log
+	for _, log := range logs {
+		// Skip logs that don't match our contract address
+		if log.Log.ContractAddress != w.config.ContractAddress {
+			w.logger.Debug("Skipping log from different contract",
+				zap.String("expected", w.config.ContractAddress),
+				zap.String("actual", log.Log.ContractAddress))
+			continue
+		}
+
+		// Get the correct transaction hash for this log
+		txHash := "0x0" // Default if we can't find the right transaction
+
+		// Use the TxIndex from the log's ID to get the right transaction hash
+		if txIndex := log.ID.TxIndex; txIndex >= 0 {
+			if hash, exists := blockInfo.TxHashesByIndex[txIndex]; exists {
+				txHash = hash
+			} else {
+				w.logger.Error("Transaction index from log not found in block",
+					zap.Int("blockNumber", blockNumber),
+					zap.Int("txIndex", txIndex))
+			}
+		}
+
+		// Create a copy of blockInfo with the correct transaction hash for this log
+		logBlockInfo := blockInfo
+		logBlockInfo.TxHash = txHash
+
+		if err := w.processLog(ctx, log, logBlockInfo); err != nil {
+			w.logger.Error("Failed to process log",
+				zap.Int("block", log.ID.BlockNumber),
+				zap.Int("txIndex", log.ID.TxIndex),
+				zap.Int("logIndex", log.ID.LogIndex),
+				zap.Error(err))
+			// Continue with other logs
+			continue
+		}
+	}
+
+	return nil
+}

+ 93 - 0
node/pkg/watchers/aztec/config.go

@@ -0,0 +1,93 @@
+package aztec
+
+import (
+	"time"
+
+	"github.com/certusone/wormhole/node/pkg/common"
+	gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1"
+	"github.com/certusone/wormhole/node/pkg/query"
+	"github.com/certusone/wormhole/node/pkg/supervisor"
+	"github.com/certusone/wormhole/node/pkg/watchers"
+	"github.com/certusone/wormhole/node/pkg/watchers/interfaces"
+	"github.com/wormhole-foundation/wormhole/sdk/vaa"
+)
+
+// Config holds all configuration for the Aztec watcher
+type Config struct {
+	// Chain identification
+	ChainID   vaa.ChainID
+	NetworkID string
+
+	// Connection details
+	RpcURL          string
+	ContractAddress string
+
+	// Processing parameters
+	StartBlock        int
+	PayloadInitialCap int
+
+	// Timeouts and intervals
+	RPCTimeout            time.Duration
+	LogProcessingInterval time.Duration
+	RequestTimeout        time.Duration
+
+	// Retry configuration
+	MaxRetries        int
+	InitialBackoff    time.Duration
+	BackoffMultiplier float64
+}
+
+// DefaultConfig returns a default configuration
+func DefaultConfig(chainID vaa.ChainID, networkID string, rpcURL, contractAddress string) Config {
+	return Config{
+		// Chain identification
+		ChainID:   chainID,
+		NetworkID: networkID,
+
+		// Connection details
+		RpcURL:          rpcURL,
+		ContractAddress: contractAddress,
+
+		// Processing parameters
+		StartBlock:        1,
+		PayloadInitialCap: 13,
+
+		// Timeouts and intervals
+		RPCTimeout:            30 * time.Second,
+		LogProcessingInterval: 10 * time.Second,
+		RequestTimeout:        10 * time.Second,
+
+		// Retry configuration
+		MaxRetries:        3,
+		InitialBackoff:    500 * time.Millisecond,
+		BackoffMultiplier: 1.5,
+	}
+}
+
+// GetChainID implements the watchers.WatcherConfig interface
+func (c *WatcherConfig) GetChainID() vaa.ChainID {
+	return c.ChainID
+}
+
+// GetNetworkID implements the watchers.WatcherConfig interface
+func (c *WatcherConfig) GetNetworkID() watchers.NetworkID {
+	return c.NetworkID
+}
+
+// Create implements the watchers.WatcherConfig interface
+//
+//nolint:unparam // Aztec doesn't implement Reobserver, so we always return nil
+func (c *WatcherConfig) Create(
+	msgC chan<- *common.MessagePublication,
+	obsvReqC <-chan *gossipv1.ObservationRequest,
+	_ <-chan *query.PerChainQueryInternal,
+	_ chan<- *query.PerChainQueryResponseInternal,
+	_ chan<- *common.GuardianSet,
+	_ common.Environment,
+) (supervisor.Runnable, interfaces.Reobserver, error) {
+	// Create the runnable (L1Finalizer is handled internally by the watcher)
+	runnable := NewWatcherRunnable(c.ChainID, string(c.NetworkID), c.Rpc, c.Contract, msgC, obsvReqC)
+
+	// Aztec does not implement a Reobserver, so we return nil for that interface
+	return runnable, nil, nil
+}

+ 86 - 0
node/pkg/watchers/aztec/factory.go

@@ -0,0 +1,86 @@
+package aztec
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/certusone/wormhole/node/pkg/common"
+	gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1"
+	"github.com/certusone/wormhole/node/pkg/readiness"
+	"github.com/certusone/wormhole/node/pkg/supervisor"
+	"github.com/wormhole-foundation/wormhole/sdk/vaa"
+	"go.uber.org/zap"
+)
+
+// WatcherFactory creates and initializes a new Aztec watcher
+type WatcherFactory struct {
+	// Configuration values passed in from the main application
+	NetworkID string
+	ChainID   vaa.ChainID
+}
+
+// NewWatcherRunnable creates a new Aztec watcher runnable
+func NewWatcherRunnable(
+	chainID vaa.ChainID,
+	networkID string,
+	rpcURL string,
+	contractAddress string,
+	msgC chan<- *common.MessagePublication,
+	_ <-chan *gossipv1.ObservationRequest,
+) supervisor.Runnable {
+	// Create a runnable
+	runnable := supervisor.Runnable(func(ctx context.Context) error {
+		logger := supervisor.Logger(ctx)
+		logger.Info("Starting Aztec watcher",
+			zap.String("rpc", rpcURL),
+			zap.String("contract", contractAddress))
+
+		// Create the readiness component
+		readinessSync := common.MustConvertChainIdToReadinessSyncing(chainID)
+
+		// Create default config
+		config := DefaultConfig(chainID, networkID, rpcURL, contractAddress)
+
+		// Create the block fetcher
+		blockFetcher, err := NewAztecBlockFetcher(ctx, rpcURL, logger)
+		if err != nil {
+			return fmt.Errorf("failed to create block fetcher: %v", err)
+		}
+
+		// Create L1Verifier - this is used internally by the watcher
+		l1Verifier, err := NewAztecFinalityVerifier(ctx, rpcURL, logger.Named("aztec_finality"))
+		if err != nil {
+			return fmt.Errorf("failed to create L1Verifier: %v", err)
+		}
+
+		// Create the observation manager
+		observationManager := NewObservationManager(networkID, logger)
+
+		// Create the watcher
+		watcher := NewWatcher(
+			config,
+			blockFetcher,
+			l1Verifier,
+			observationManager,
+			msgC,
+			logger,
+		)
+
+		// Signal initialization complete
+		readiness.SetReady(readinessSync)
+		supervisor.Signal(ctx, supervisor.SignalHealthy)
+
+		// Run the watcher
+		return watcher.Run(ctx)
+	})
+
+	return runnable
+}
+
+// Create a factory instance that can be used in the main application
+func NewAztecWatcherFactory(networkID string, chainID vaa.ChainID) *WatcherFactory {
+	return &WatcherFactory{
+		NetworkID: networkID,
+		ChainID:   chainID,
+	}
+}

+ 121 - 0
node/pkg/watchers/aztec/http_client.go

@@ -0,0 +1,121 @@
+package aztec
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/certusone/wormhole/node/pkg/common"
+	"github.com/hashicorp/go-retryablehttp"
+	"go.uber.org/zap"
+)
+
+// HTTPClient provides a wrapper for HTTP requests with retries and timeouts
+type HTTPClient interface {
+	DoRequest(ctx context.Context, url string, payload map[string]any) ([]byte, error)
+}
+
+// retryableHTTPClient is the implementation of HTTPClient using retryablehttp
+type retryableHTTPClient struct {
+	client *http.Client
+	logger *zap.Logger
+}
+
+// NewHTTPClient creates a new HTTP client with built-in retry functionality
+func NewHTTPClient(timeout time.Duration, maxRetries int, initialBackoff time.Duration, backoffMultiplier float64, logger *zap.Logger) HTTPClient {
+	// Create a retryable HTTP client
+	retryClient := retryablehttp.NewClient()
+
+	// Configure the retry settings
+	retryClient.RetryMax = maxRetries
+	retryClient.RetryWaitMin = initialBackoff
+	retryClient.RetryWaitMax = time.Duration(float64(initialBackoff) * backoffMultiplier * float64(maxRetries))
+	retryClient.HTTPClient.Timeout = timeout
+
+	// Configure the logger
+	// Use a custom logger that wraps our zap logger
+	retryClient.Logger = newRetryableHTTPZapLogger(logger)
+
+	// Get the standard *http.Client from the retryable client
+	standardClient := retryClient.StandardClient()
+
+	return &retryableHTTPClient{
+		client: standardClient,
+		logger: logger,
+	}
+}
+
+// DoRequest sends an HTTP request with retries provided by retryablehttp
+func (c *retryableHTTPClient) DoRequest(ctx context.Context, url string, payload map[string]any) ([]byte, error) {
+	// Marshal the payload
+	jsonData, err := json.Marshal(payload)
+	if err != nil {
+		return nil, fmt.Errorf("error marshaling JSON: %v", err)
+	}
+
+	// Create the request
+	req, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(string(jsonData)))
+	if err != nil {
+		return nil, fmt.Errorf("error creating request: %v", err)
+	}
+	req.Header.Set("Content-Type", "application/json")
+
+	// Execute the request with automatic retries
+	resp, err := c.client.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("request error: %v", err)
+	}
+	defer resp.Body.Close()
+
+	// Check status code
+	if resp.StatusCode != http.StatusOK {
+		body, _ := common.SafeRead(resp.Body)
+		c.logger.Warn("Non-200 status code",
+			zap.String("url", url),
+			zap.Int("status", resp.StatusCode),
+			zap.String("response", string(body)))
+		return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+	}
+
+	// Read the response
+	body, err := common.SafeRead(resp.Body)
+	if err != nil {
+		return nil, fmt.Errorf("error reading response: %v", err)
+	}
+
+	// Check for JSON-RPC errors in the response
+	hasError, rpcError := GetJSONRPCError(body)
+	if hasError {
+		return nil, rpcError
+	}
+
+	return body, nil
+}
+
+// Adapter to make zap logger work with retryablehttp's logger interface
+type retryableHTTPZapLogger struct {
+	logger *zap.Logger
+}
+
+func newRetryableHTTPZapLogger(logger *zap.Logger) *retryableHTTPZapLogger {
+	return &retryableHTTPZapLogger{logger: logger}
+}
+
+func (l *retryableHTTPZapLogger) Error(msg string, _ ...interface{}) {
+	l.logger.Error(msg)
+}
+
+func (l *retryableHTTPZapLogger) Info(msg string, _ ...interface{}) {
+	l.logger.Info(msg)
+}
+
+func (l *retryableHTTPZapLogger) Debug(msg string, _ ...interface{}) {
+	l.logger.Debug(msg)
+}
+
+func (l *retryableHTTPZapLogger) Warn(msg string, _ ...interface{}) {
+	l.logger.Warn(msg)
+}

+ 151 - 0
node/pkg/watchers/aztec/l1_verifier.go

@@ -0,0 +1,151 @@
+package aztec
+
+import (
+	"context"
+	"fmt"
+	"sync"
+	"time"
+
+	"github.com/ethereum/go-ethereum/rpc"
+	"go.uber.org/zap"
+)
+
+type L1Verifier interface {
+	// Get the latest finalized block number
+	GetLatestFinalizedBlockNumber() uint64
+
+	// Get the latest finalized block from Aztec
+	GetFinalizedBlock(ctx context.Context) (*FinalizedBlock, error)
+
+	// Check if a block is finalized
+	IsBlockFinalized(ctx context.Context, blockNumber int) (bool, error)
+}
+
+// aztecFinalityVerifier is a simplified L1Verifier that queries Aztec directly
+type aztecFinalityVerifier struct {
+	rpcClient *rpc.Client
+	logger    *zap.Logger
+
+	// Cache for finalized blocks
+	finalizedBlockCache     *FinalizedBlock
+	finalizedBlockCacheTime time.Time
+	finalizedBlockCacheMu   sync.RWMutex
+	finalizedBlockCacheTTL  time.Duration
+}
+
+// NewAztecFinalityVerifier creates a new L1 verifier
+func NewAztecFinalityVerifier(
+	ctx context.Context,
+	rpcURL string,
+	logger *zap.Logger,
+) (L1Verifier, error) {
+	// Create a new RPC client - this needs to use context.Background() for the connection
+	client, err := rpc.DialContext(ctx, rpcURL)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create RPC client: %v", err)
+	}
+
+	return &aztecFinalityVerifier{
+		rpcClient:              client,
+		logger:                 logger,
+		finalizedBlockCacheTTL: 30 * time.Second,
+	}, nil
+}
+
+// GetLatestFinalizedBlockNumber implements the interfaces.L1Finalizer interface
+func (v *aztecFinalityVerifier) GetLatestFinalizedBlockNumber() uint64 {
+	// Check the cache first
+	if block, found := v.getFromCache(); found {
+		if block.Number < 0 {
+			v.logger.Warn("Negative block number detected, returning 0", zap.Int("blockNumber", block.Number))
+			return 0
+		}
+		return uint64(block.Number)
+	}
+
+	// If no cache, fetch the latest finalized block
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	block, err := v.GetFinalizedBlock(ctx)
+	if err != nil {
+		v.logger.Warn("Failed to get finalized block for L1Finalizer", zap.Error(err))
+		return 0
+	}
+
+	if block.Number < 0 {
+		v.logger.Warn("Failed to get finalized block for L1Finalizer", zap.Error(err))
+		return 0
+	}
+	return uint64(block.Number)
+}
+
+// Helper function to safely get from the finalized block cache
+func (v *aztecFinalityVerifier) getFromCache() (*FinalizedBlock, bool) {
+	v.finalizedBlockCacheMu.RLock()
+	defer v.finalizedBlockCacheMu.RUnlock()
+
+	if v.finalizedBlockCache != nil && time.Since(v.finalizedBlockCacheTime) < v.finalizedBlockCacheTTL {
+		return v.finalizedBlockCache, true
+	}
+
+	return nil, false // Cache miss or expired
+}
+
+// Helper function to safely update the finalized block cache
+func (v *aztecFinalityVerifier) updateCache(block *FinalizedBlock) {
+	v.finalizedBlockCacheMu.Lock()
+	defer v.finalizedBlockCacheMu.Unlock()
+
+	v.finalizedBlockCache = block
+	v.finalizedBlockCacheTime = time.Now()
+}
+
+// GetFinalizedBlock gets the latest finalized block from Aztec
+func (v *aztecFinalityVerifier) GetFinalizedBlock(ctx context.Context) (*FinalizedBlock, error) {
+	// Check cache first
+	if block, found := v.getFromCache(); found {
+		v.logger.Debug("Using cached finalized block",
+			zap.Int("number", block.Number),
+			zap.String("hash", block.Hash))
+		return block, nil
+	}
+
+	// Cache miss, fetch from network
+	v.logger.Debug("Fetching L2 tips")
+	var l2Tips L2Tips
+	err := v.rpcClient.CallContext(ctx, &l2Tips, "node_getL2Tips")
+	if err != nil {
+		return nil, fmt.Errorf("failed to fetch L2 tips: %v", err)
+	}
+
+	// Create finalized block info
+	block := &FinalizedBlock{
+		Number: l2Tips.Finalized.Number,
+		Hash:   l2Tips.Finalized.Hash,
+	}
+
+	// Update the cache
+	v.updateCache(block)
+
+	v.logger.Info("Updated finalized block",
+		zap.Int("number", block.Number))
+
+	return block, nil
+}
+
+// IsBlockFinalized checks if a specific block number is finalized
+func (v *aztecFinalityVerifier) IsBlockFinalized(ctx context.Context, blockNumber int) (bool, error) {
+	finalizedBlock, err := v.GetFinalizedBlock(ctx)
+	if err != nil {
+		return false, fmt.Errorf("failed to get finalized block: %v", err)
+	}
+
+	isFinalized := blockNumber <= finalizedBlock.Number
+	v.logger.Debug("Block finality check",
+		zap.Int("block", blockNumber),
+		zap.Int("finalized_block", finalizedBlock.Number),
+		zap.Bool("is_finalized", isFinalized))
+
+	return isFinalized, nil
+}

+ 89 - 0
node/pkg/watchers/aztec/message_publisher.go

@@ -0,0 +1,89 @@
+package aztec
+
+import (
+	"context"
+	"encoding/hex"
+	"fmt"
+	"math"
+	"strings"
+	"time"
+
+	"github.com/certusone/wormhole/node/pkg/common"
+	"go.uber.org/zap"
+)
+
+// publishObservation creates and publishes a message observation
+func (w *Watcher) publishObservation(ctx context.Context, params LogParameters, payload []byte, blockInfo BlockInfo, observationID string) error {
+	// Check for context cancellation
+	select {
+	case <-ctx.Done():
+		return ctx.Err()
+	default:
+		// Continue processing
+	}
+
+	// Log the extracted payload data
+	w.logger.Debug("Using extracted payload data",
+		zap.String("arbitrumAddress", fmt.Sprintf("0x%x", params.ArbitrumAddress)),
+		zap.Uint16("arbitrumChainID", params.ArbitrumChainID))
+
+	// Convert transaction hash to byte array for txID
+	txID, err := hex.DecodeString(strings.TrimPrefix(blockInfo.TxHash, "0x"))
+	if err != nil {
+		w.logger.Error("Failed to decode transaction hash", zap.Error(err))
+		// Fall back to default
+		txID = []byte{0x0}
+	}
+
+	// Check for context cancellation after potentially long operation
+	select {
+	case <-ctx.Done():
+		return ctx.Err()
+	default:
+		// Continue processing
+	}
+
+	// Create the observation
+	observation := &common.MessagePublication{
+		TxID:             txID,
+		Timestamp:        time.Unix(safeUint64ToInt64(blockInfo.Timestamp), 0),
+		Nonce:            params.Nonce,
+		Sequence:         params.Sequence,
+		EmitterChain:     w.config.ChainID,
+		EmitterAddress:   params.SenderAddress,
+		Payload:          payload,
+		ConsistencyLevel: params.ConsistencyLevel,
+		IsReobservation:  false,
+	}
+
+	// Increment metrics
+	w.observationManager.IncrementMessagesConfirmed()
+
+	// Log the observation
+	w.logger.Info("Message observed",
+		zap.String("id", observationID),
+		zap.String("txHash", observation.TxIDString()),
+		zap.Time("timestamp", observation.Timestamp),
+		zap.Uint64("sequence", observation.Sequence),
+		zap.Stringer("emitter_chain", observation.EmitterChain),
+		zap.Stringer("emitter_address", observation.EmitterAddress),
+		zap.String("arbitrumAddress", fmt.Sprintf("0x%x", params.ArbitrumAddress)),
+		zap.Uint16("arbitrumChainID", params.ArbitrumChainID))
+
+	// Send to the message channel
+	select {
+	case w.msgC <- observation:
+		// Message sent successfully
+	case <-ctx.Done():
+		return ctx.Err()
+	}
+
+	return nil
+}
+
+func safeUint64ToInt64(value uint64) int64 {
+	if value > math.MaxInt64 {
+		return math.MaxInt64 // Cap at maximum int64 value
+	}
+	return int64(value)
+}

+ 362 - 0
node/pkg/watchers/aztec/observation_manager.go

@@ -0,0 +1,362 @@
+package aztec
+
+import (
+	"context"
+	"encoding/hex"
+	"fmt"
+	"math"
+	"strconv"
+	"strings"
+	"sync"
+
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promauto"
+	"github.com/wormhole-foundation/wormhole/sdk/vaa"
+	"go.uber.org/zap"
+)
+
+// Global metrics variables
+var (
+	messagesConfirmedMetric *prometheus.CounterVec
+	metricsInitialized      sync.Once
+)
+
+// initMetrics initializes the metrics only once
+func initMetrics() {
+	metricsInitialized.Do(func() {
+		messagesConfirmedMetric = promauto.NewCounterVec(
+			prometheus.CounterOpts{
+				Name: "wormhole_aztec_observations_confirmed_total",
+				Help: "Total number of verified observations found for the chain",
+			}, []string{"chain_name"})
+	})
+}
+
+// ObservationManager handles storage and lifecycle of pending observations
+type ObservationManager interface {
+	IncrementMessagesConfirmed()
+}
+
+// observationManager is the implementation of ObservationManager
+type observationManager struct {
+	networkID string
+	logger    *zap.Logger
+	metrics   observationMetrics
+}
+
+// observationMetrics holds the Prometheus metrics for the observation manager
+type observationMetrics struct {
+	messagesConfirmed *prometheus.CounterVec
+}
+
+// NewObservationManager creates a new observation manager
+func NewObservationManager(networkID string, logger *zap.Logger) ObservationManager {
+	// Initialize metrics if not already done
+	initMetrics()
+
+	// Use the global metrics
+	metrics := observationMetrics{
+		messagesConfirmed: messagesConfirmedMetric,
+	}
+
+	return &observationManager{
+		networkID: networkID,
+		logger:    logger,
+		metrics:   metrics,
+	}
+}
+
+// IncrementMessagesConfirmed increases the counter for confirmed messages
+func (m *observationManager) IncrementMessagesConfirmed() {
+	m.metrics.messagesConfirmed.WithLabelValues(m.networkID).Inc()
+	m.logger.Debug("Incremented messages confirmed counter")
+}
+
+// processLog handles an individual log entry
+func (w *Watcher) processLog(ctx context.Context, extLog ExtendedPublicLog, blockInfo BlockInfo) error {
+	// Check for context cancellation
+	select {
+	case <-ctx.Done():
+		return ctx.Err()
+	default:
+		// Continue processing
+	}
+
+	// Skip empty logs
+	if len(extLog.Log.Fields) == 0 {
+		return nil
+	}
+
+	// Extract event parameters
+	params, err := w.parseLogParameters(extLog.Log.Fields)
+	if err != nil {
+		return fmt.Errorf("failed to parse log parameters: %v", err)
+	}
+
+	// Set the transaction ID from the block info
+	params.TxID = blockInfo.TxHash
+
+	// Create message payload (now including the txID)
+	rawPayload := w.createPayload(extLog.Log.Fields, params.TxID)
+
+	w.logger.Debug("Created payload",
+		zap.Int("payloadLength", len(rawPayload)),
+		zap.String("txID", params.TxID))
+
+	// Extract structured data from the payload (accounting for txID at the beginning)
+	arbitrumAddress, arbitrumChainID, amount, _, err := w.extractPayloadData(rawPayload)
+	if err != nil {
+		w.logger.Debug("Failed to extract payload data", zap.Error(err))
+		// Continue with empty values for these fields
+	} else {
+		// Add the extracted values to the parameters
+		params.ArbitrumAddress = arbitrumAddress
+		params.ArbitrumChainID = arbitrumChainID
+		params.Amount = amount
+	}
+
+	// Create a unique ID for this observation
+	observationID := CreateObservationID(params.SenderAddress.String(), params.Sequence, extLog.ID.BlockNumber)
+
+	// Log relevant information about the message
+	w.logger.Info("Processing message",
+		zap.Stringer("emitter", params.SenderAddress),
+		zap.Uint64("sequence", params.Sequence),
+		zap.String("arbitrumAddress", fmt.Sprintf("0x%x", params.ArbitrumAddress)),
+		zap.Uint16("arbitrumChainID", params.ArbitrumChainID),
+		zap.Uint64("amount", params.Amount))
+
+	// Check for context cancellation before proceeding
+	select {
+	case <-ctx.Done():
+		return ctx.Err()
+	default:
+		// Continue processing
+	}
+
+	// Since we're processing finalized blocks, we can publish immediately
+	// regardless of the original consistency level
+	if err := w.publishObservation(ctx, params, rawPayload, blockInfo, observationID); err != nil {
+		return fmt.Errorf("failed to publish observation: %v", err)
+	}
+
+	return nil
+}
+
+// extractPayloadData parses the structured payload to extract key information
+// Modified to account for txID at the beginning of the payload
+func (w *Watcher) extractPayloadData(payload []byte) ([]byte, uint16, uint64, []byte, error) {
+	// Skip past the txID (first 32 bytes)
+	txIDOffset := 32
+
+	if len(payload) < txIDOffset+93 { // Need txID + at least 3 full 31-byte arrays
+		return nil, 0, 0, nil, fmt.Errorf("payload too short, expected at least %d bytes, got %d", txIDOffset+93, len(payload))
+	}
+
+	// Extract the txID for debugging
+	txID := payload[:txIDOffset]
+	w.logger.Debug("Extracted txID from payload", zap.String("txID", fmt.Sprintf("0x%x", txID)))
+
+	// Each array is 31 bytes long for address and chain ID
+	const arraySize = 31
+
+	// Extract Arbitrum address (first 20 bytes after txID)
+	arbitrumAddress := make([]byte, 20)
+	copy(arbitrumAddress, payload[txIDOffset:txIDOffset+20])
+
+	// Extract Arbitrum chain ID (first 2 bytes of second array after txID)
+	chainIDLower := uint16(payload[txIDOffset+arraySize])
+	chainIDUpper := uint16(payload[txIDOffset+arraySize+1])
+	arbitrumChainID := (chainIDUpper << 8) | chainIDLower
+
+	// The amount is the first byte of the third array after txID
+	amount := uint64(0)
+	if len(payload) >= txIDOffset+2*arraySize+1 {
+		// Just read the first byte as the amount value
+		amount = uint64(payload[txIDOffset+2*arraySize])
+	}
+
+	// The verification data starts after the amount array
+	verificationDataStart := txIDOffset + 3*arraySize
+	verificationDataLength := len(payload) - verificationDataStart
+	verificationData := make([]byte, verificationDataLength)
+
+	if verificationDataLength > 0 {
+		copy(verificationData, payload[verificationDataStart:])
+	}
+
+	// Log what we've extracted at debug level
+	w.logger.Debug("Extracted payload data",
+		zap.String("arbitrumAddress", fmt.Sprintf("0x%x", arbitrumAddress)),
+		zap.Uint16("arbitrumChainID", arbitrumChainID),
+		zap.Uint64("amount", amount),
+		zap.Int("verificationDataLength", verificationDataLength))
+
+	return arbitrumAddress, arbitrumChainID, amount, verificationData, nil
+}
+
+// parseLogParameters extracts parameters from a log entry
+func (w *Watcher) parseLogParameters(logEntries []string) (LogParameters, error) {
+	if len(logEntries) < 4 {
+		return LogParameters{}, fmt.Errorf("log has insufficient entries: %d", len(logEntries))
+	}
+
+	// First value is the sender
+	senderHex := strings.TrimPrefix(logEntries[0], "0x")
+	senderBytes, err := hex.DecodeString(senderHex)
+	if err != nil {
+		return LogParameters{}, &ErrParsingFailed{
+			What: "sender address",
+			Err:  err,
+		}
+	}
+
+	var senderAddress vaa.Address
+	copy(senderAddress[:], senderBytes)
+
+	// Parse sequence
+	sequence, err := ParseHexUint64(logEntries[1])
+	if err != nil {
+		return LogParameters{}, fmt.Errorf("failed to parse sequence: %v", err)
+	}
+
+	// Parse nonce
+	nonce, err := ParseHexUint64(logEntries[2])
+	if err != nil {
+		return LogParameters{}, fmt.Errorf("failed to parse nonce: %v", err)
+	}
+
+	// Parse consistency level
+	consistencyLevel, err := ParseHexUint64(logEntries[3])
+	if err != nil {
+		return LogParameters{}, fmt.Errorf("failed to parse consistencyLevel: %v", err)
+	}
+
+	return LogParameters{
+		SenderAddress:    senderAddress,
+		Sequence:         sequence,
+		Nonce:            safeUint64ToUint32(nonce),
+		ConsistencyLevel: safeUint64ToUint8(consistencyLevel),
+		Amount:           0, // Initialize with 0, will be set later
+	}, nil
+}
+
+// createPayload processes log entries that contain field elements into a byte payload
+// Modified to include txID at the beginning of the payload
+func (w *Watcher) createPayload(logEntries []string, txID string) []byte {
+	// Start by adding the txID to the payload
+	txIDHex := strings.TrimPrefix(txID, "0x")
+	txIDBytes, err := hex.DecodeString(txIDHex)
+	if err != nil {
+		w.logger.Debug("Failed to decode txID hex, using empty txID", zap.Error(err))
+		txIDBytes = make([]byte, 0)
+	}
+
+	// Create a 32-byte array for txID
+	paddedTxID := make([]byte, 32)
+	// Copy txID bytes (this will handle padding correctly)
+	copy(paddedTxID, txIDBytes)
+
+	// Initialize payload with the txID
+	payload := paddedTxID
+
+	// Now continue with the rest of the payload
+	remainingPayload := make([]byte, 0, w.config.PayloadInitialCap)
+
+	// Skip the first 5 entries which are metadata (sender, sequence, nonce, consistency level, timestamp)
+	for i, entry := range logEntries[5:] {
+		// Clean up the entry - remove 0x
+		entry = strings.TrimPrefix(entry, "0x")
+
+		// Try to decode as hex
+		bytes, err := hex.DecodeString(entry)
+		if err != nil {
+			w.logger.Debug("Failed to decode hex entry", zap.Error(err), zap.Int("entryIndex", i+5))
+			continue
+		}
+
+		// Remove leading zeros
+		for len(bytes) > 0 && bytes[0] == 0 {
+			bytes = bytes[1:]
+		}
+
+		// Reverse the bytes to correct the order
+		for i, j := 0, len(bytes)-1; i < j; i, j = i+1, j-1 {
+			bytes[i], bytes[j] = bytes[j], bytes[i]
+		}
+
+		// Special handling for amount (the 3rd array, index 2)
+		if i == 2 {
+			// This is the amount field (third array)
+			// Ensure it's padded to 32 bytes
+			// First, add the current bytes
+			remainingPayload = append(remainingPayload, bytes...)
+
+			// Then add padding to make it 32 bytes total
+			// Don't include Jack after it - move Jack to the next 32-byte chunk
+			paddingNeeded := 32 - len(bytes)
+			padding := make([]byte, paddingNeeded)
+			remainingPayload = append(remainingPayload, padding...)
+
+			// Continue to next entry - skip the normal append
+			continue
+		}
+
+		// If this is the entry after the amount (Jack), ensure it starts on a new 32-byte boundary
+		if i == 3 {
+			// This is where Jack would start
+			// Calculate padding needed to align to next 32-byte boundary
+			currentLength := len(remainingPayload)
+			paddingNeeded := (32 - (currentLength % 32)) % 32
+			if paddingNeeded > 0 {
+				padding := make([]byte, paddingNeeded)
+				remainingPayload = append(remainingPayload, padding...)
+			}
+		}
+
+		// Add to payload
+		remainingPayload = append(remainingPayload, bytes...)
+	}
+
+	// Combine txID and remainingPayload
+	payload = append(payload, remainingPayload...)
+
+	// Log the final payload length at debug level
+	w.logger.Debug("Payload created",
+		zap.Int("length", len(payload)))
+
+	return payload
+}
+
+// ParseHexUint64 converts a hex string to uint64
+func ParseHexUint64(hexStr string) (uint64, error) {
+	// Remove "0x" prefix if present
+	hexStr = strings.TrimPrefix(hexStr, "0x")
+
+	// Parse the hex string to uint64
+	value, err := strconv.ParseUint(hexStr, 16, 64)
+	if err != nil {
+		return 0, &ErrParsingFailed{
+			What: "hex uint64",
+			Err:  err,
+		}
+	}
+
+	return value, nil
+}
+
+// safeUint64ToUint32 safely converts uint64 to uint32
+func safeUint64ToUint32(value uint64) uint32 {
+	if value > math.MaxUint32 {
+		return math.MaxUint32
+	}
+	return uint32(value)
+}
+
+// safeUint64ToUint8 safely converts uint64 to uint8
+func safeUint64ToUint8(value uint64) uint8 {
+	if value > math.MaxUint8 {
+		return math.MaxUint8
+	}
+	return uint8(value)
+}

+ 216 - 0
node/pkg/watchers/aztec/types.go

@@ -0,0 +1,216 @@
+package aztec
+
+import (
+	"github.com/certusone/wormhole/node/pkg/watchers"
+	"github.com/wormhole-foundation/wormhole/sdk/vaa"
+)
+
+// WatcherConfig is the configuration used by node.go
+type WatcherConfig struct {
+	NetworkID watchers.NetworkID
+	ChainID   vaa.ChainID
+	Rpc       string
+	Contract  string
+}
+
+// LogParameters encapsulates the core parameters from a log
+type LogParameters struct {
+	SenderAddress    vaa.Address
+	Sequence         uint64
+	Nonce            uint32
+	ConsistencyLevel uint8
+	ArbitrumAddress  []byte
+	ArbitrumChainID  uint16
+	Amount           uint64
+	TxID             string
+}
+
+// BlockInfo enhanced to include block hash and parent hash
+type BlockInfo struct {
+	TxHash            string
+	Timestamp         uint64
+	archiveRoot       string
+	parentArchiveRoot string
+	TxHashesByIndex   map[int]string
+}
+
+// FinalizedBlock represents a finalized block's information
+type FinalizedBlock struct {
+	Number int    `json:"number"`
+	Hash   string `json:"hash,omitempty"`
+}
+
+// L2Tips represents the response from the node_getL2Tips RPC method
+type L2Tips struct {
+	Latest struct {
+		Number int    `json:"number"`
+		Hash   string `json:"hash"`
+	} `json:"latest"`
+	Proven struct {
+		Number int    `json:"number"`
+		Hash   string `json:"hash"`
+	} `json:"proven"`
+	Finalized struct {
+		Number int    `json:"number"`
+		Hash   string `json:"hash,omitempty"`
+	} `json:"finalized"`
+}
+
+type L2TipsResponse struct {
+	JsonRPC string `json:"jsonrpc"`
+	ID      int    `json:"id"`
+	Result  L2Tips `json:"result"`
+}
+
+// JSON-RPC related structures
+type JsonRpcResponse struct {
+	JsonRPC string `json:"jsonrpc"`
+	ID      int    `json:"id"`
+	Result  struct {
+		Logs       []ExtendedPublicLog `json:"logs"`
+		MaxLogsHit bool                `json:"maxLogsHit"`
+	} `json:"result"`
+}
+
+type BlockResponse struct {
+	JsonRPC string      `json:"jsonrpc"`
+	ID      int         `json:"id"`
+	Result  BlockResult `json:"result"`
+}
+
+type BlockResult struct {
+	Archive BlockArchive `json:"archive"`
+	Header  BlockHeader  `json:"header"`
+	Body    BlockBody    `json:"body"`
+}
+
+type BlockArchive struct {
+	Root                   string `json:"root"`
+	NextAvailableLeafIndex int    `json:"nextAvailableLeafIndex"`
+}
+
+type BlockHeader struct {
+	LastArchive       BlockArchive      `json:"lastArchive"`
+	ContentCommitment ContentCommitment `json:"contentCommitment"`
+	State             State             `json:"state"`
+	GlobalVariables   GlobalVariables   `json:"globalVariables"`
+	TotalFees         string            `json:"totalFees"`
+	TotalManaUsed     string            `json:"totalManaUsed"`
+}
+
+type ContentCommitment struct {
+	BlobsHash string `json:"blobsHash"`
+	InHash    string `json:"inHash"`
+	OutHash   string `json:"outHash"`
+}
+
+type State struct {
+	L1ToL2MessageTree MerkleTree   `json:"l1ToL2MessageTree"`
+	Partial           PartialState `json:"partial"`
+}
+
+type PartialState struct {
+	NoteHashTree   MerkleTree `json:"noteHashTree"`
+	NullifierTree  MerkleTree `json:"nullifierTree"`
+	PublicDataTree MerkleTree `json:"publicDataTree"`
+}
+
+type MerkleTree struct {
+	Root                   string `json:"root"`
+	NextAvailableLeafIndex int    `json:"nextAvailableLeafIndex"`
+}
+
+type GlobalVariables struct {
+	ChainID      string  `json:"chainId"`
+	Version      string  `json:"version"`
+	BlockNumber  int     `json:"blockNumber"`
+	SlotNumber   string  `json:"slotNumber"`
+	Timestamp    string  `json:"timestamp"`
+	Coinbase     string  `json:"coinbase"`
+	FeeRecipient string  `json:"feeRecipient"`
+	GasFees      GasFees `json:"gasFees"`
+}
+
+type GasFees struct {
+	FeePerDaGas string `json:"feePerDaGas"`
+	FeePerL2Gas string `json:"feePerL2Gas"`
+}
+
+type BlockBody struct {
+	TxEffects []TxEffect `json:"txEffects"`
+}
+
+type TxEffect struct {
+	RevertCode        int                `json:"revertCode"`
+	TxHash            string             `json:"txHash"`
+	TransactionFee    string             `json:"transactionFee"`
+	NoteHashes        []string           `json:"noteHashes"`
+	Nullifiers        []string           `json:"nullifiers"`
+	L2ToL1Msgs        []string           `json:"l2ToL1Msgs"`
+	PublicDataWrites  []PublicDataWrite  `json:"publicDataWrites"`
+	PrivateLogs       []PrivateLog       `json:"privateLogs"`
+	PublicLogs        []PublicLog        `json:"publicLogs"`
+	ContractClassLogs []ContractClassLog `json:"contractClassLogs"`
+}
+
+type PublicDataWrite struct {
+	LeafSlot string `json:"leafSlot"`
+	Value    string `json:"value"`
+}
+
+type PrivateLog struct {
+	Fields        []string `json:"fields"`
+	EmittedLength int      `json:"emittedLength"`
+}
+
+type ContractClassLog struct {
+	ContractAddress string `json:"contractAddress"`
+	Fields          struct {
+		Fields []string `json:"fields"`
+	} `json:"fields"`
+	EmittedLength int `json:"emittedLength"`
+}
+
+type LogId struct {
+	BlockNumber int `json:"blockNumber"`
+	TxIndex     int `json:"txIndex"`
+	LogIndex    int `json:"logIndex"`
+}
+
+type PublicLog struct {
+	ContractAddress string   `json:"contractAddress"`
+	Fields          []string `json:"fields"`
+	EmittedLength   int      `json:"emittedLength"`
+}
+
+type ExtendedPublicLog struct {
+	ID  LogId     `json:"id"`
+	Log PublicLog `json:"log"`
+}
+
+type ErrRPCError struct {
+	Method string
+	Code   int
+	Msg    string
+}
+
+func (e ErrRPCError) Error() string {
+	return "RPC error calling " + e.Method + ": " + e.Msg
+}
+
+type ErrMaxRetriesExceeded struct {
+	Method string
+}
+
+func (e ErrMaxRetriesExceeded) Error() string {
+	return "max retries exceeded for " + e.Method
+}
+
+type ErrParsingFailed struct {
+	What string
+	Err  error
+}
+
+func (e ErrParsingFailed) Error() string {
+	return "failed parsing " + e.What + ": " + e.Err.Error()
+}

+ 50 - 0
node/pkg/watchers/aztec/utils.go

@@ -0,0 +1,50 @@
+package aztec
+
+import (
+	"encoding/json"
+	"fmt"
+	"strconv"
+)
+
+// Helper functions for common parsing tasks
+
+// ParseUint parses a string as an unsigned integer with proper error handling
+func ParseUint(s string, base int, bitSize int) (uint64, error) {
+	v, err := strconv.ParseUint(s, base, bitSize)
+	if err != nil {
+		return 0, &ErrParsingFailed{
+			What: fmt.Sprintf("unsigned integer with base %d", base),
+			Err:  err,
+		}
+	}
+	return v, nil
+}
+
+func (e *ErrParsingFailed) Unwrap() error {
+	return e.Err
+}
+
+// GetJSONRPCError extracts error information from a JSON-RPC response
+func GetJSONRPCError(body []byte) (bool, *ErrRPCError) {
+	var errorCheck struct {
+		Error *struct {
+			Code    int    `json:"code"`
+			Message string `json:"message"`
+		} `json:"error,omitempty"`
+	}
+
+	if err := json.Unmarshal(body, &errorCheck); err != nil || errorCheck.Error == nil {
+		return false, nil
+	}
+
+	return true, &ErrRPCError{
+		Method: "unknown", // Caller should update this
+		Code:   errorCheck.Error.Code,
+		Msg:    errorCheck.Error.Message,
+	}
+}
+
+// CreateObservationID creates a unique ID for tracking pending observations
+func CreateObservationID(senderAddress string, sequence uint64, blockNumber int) string {
+	return fmt.Sprintf("%s-%d-%d", senderAddress, sequence, blockNumber)
+}

+ 85 - 0
node/pkg/watchers/aztec/watcher.go

@@ -0,0 +1,85 @@
+package aztec
+
+import (
+	"context"
+	"time"
+
+	"github.com/certusone/wormhole/node/pkg/common"
+	"go.uber.org/zap"
+)
+
+// Watcher handles the processing of blocks and logs
+type Watcher struct {
+	config             Config
+	blockFetcher       BlockFetcher
+	l1Verifier         L1Verifier
+	observationManager ObservationManager
+	msgC               chan<- *common.MessagePublication
+	logger             *zap.Logger
+
+	// Simplified tracking - just track the last processed block number
+	lastBlockNumber int
+}
+
+// NewWatcher creates a new Watcher
+func NewWatcher(
+	config Config,
+	blockFetcher BlockFetcher,
+	l1Verifier L1Verifier,
+	observationManager ObservationManager,
+	msgC chan<- *common.MessagePublication,
+	logger *zap.Logger,
+) *Watcher {
+	return &Watcher{
+		config:             config,
+		blockFetcher:       blockFetcher,
+		l1Verifier:         l1Verifier,
+		observationManager: observationManager,
+		msgC:               msgC,
+		logger:             logger,
+		lastBlockNumber:    config.StartBlock - 1, // Start by processing the StartBlock
+	}
+}
+
+// Run starts the watcher with a single goroutine
+func (w *Watcher) Run(ctx context.Context) error {
+	w.logger.Info("Starting Aztec watcher",
+		zap.String("rpc", w.config.RpcURL),
+		zap.String("contract", w.config.ContractAddress))
+
+	// Create an error channel
+	errC := make(chan error)
+	defer close(errC)
+
+	// Start a single goroutine that handles all operations
+	common.RunWithScissors(ctx, errC, "aztec_processor", func(ctx context.Context) error {
+		return w.processor(ctx)
+	})
+
+	// Wait for context cancellation or error
+	select {
+	case <-ctx.Done():
+		return ctx.Err()
+	case err := <-errC:
+		return err
+	}
+}
+
+func (w *Watcher) processor(ctx context.Context) error {
+	ticker := time.NewTicker(w.config.LogProcessingInterval)
+	defer ticker.Stop()
+
+	w.logger.Info("Starting Aztec event processor using finalized blocks")
+
+	for {
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		case <-ticker.C:
+			// Process blocks
+			if err := w.processBlocks(ctx); err != nil {
+				w.logger.Error("Error processing blocks", zap.Error(err))
+			}
+		}
+	}
+}

+ 459 - 0
node/pkg/watchers/aztec/watcher_test.go

@@ -0,0 +1,459 @@
+package aztec
+
+import (
+	"context"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+	"time"
+
+	"github.com/certusone/wormhole/node/pkg/common"
+	"github.com/certusone/wormhole/node/pkg/watchers"
+	"github.com/ethereum/go-ethereum/rpc"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+	"github.com/wormhole-foundation/wormhole/sdk/vaa"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zaptest"
+)
+
+// MockBlockFetcher implements the BlockFetcher interface for testing
+type MockBlockFetcher struct {
+	mock.Mock
+}
+
+func (m *MockBlockFetcher) FetchPublicLogs(ctx context.Context, fromBlock, toBlock int) ([]ExtendedPublicLog, error) {
+	args := m.Called(ctx, fromBlock, toBlock)
+	result, ok := args.Get(0).([]ExtendedPublicLog)
+	if !ok {
+		return nil, args.Error(1)
+	}
+	return result, args.Error(1)
+}
+
+func (m *MockBlockFetcher) FetchBlock(ctx context.Context, blockNumber int) (BlockInfo, error) {
+	args := m.Called(ctx, blockNumber)
+	result, ok := args.Get(0).(BlockInfo)
+	if !ok {
+		return BlockInfo{}, args.Error(1)
+	}
+	return result, args.Error(1)
+}
+
+// MockL1Verifier implements the L1Verifier interface for testing
+type MockL1Verifier struct {
+	mock.Mock
+}
+
+func (m *MockL1Verifier) GetFinalizedBlock(ctx context.Context) (*FinalizedBlock, error) {
+	args := m.Called(ctx)
+	result, ok := args.Get(0).(*FinalizedBlock)
+	if !ok {
+		return nil, args.Error(1)
+	}
+	return result, args.Error(1)
+}
+
+func (m *MockL1Verifier) IsBlockFinalized(ctx context.Context, blockNumber int) (bool, error) {
+	args := m.Called(ctx, blockNumber)
+	return args.Bool(0), args.Error(1)
+}
+
+func (m *MockL1Verifier) GetLatestFinalizedBlockNumber() uint64 {
+	args := m.Called()
+	result, ok := args.Get(0).(uint64)
+	if !ok {
+		return 0
+	}
+	return result
+}
+
+// MockObservationManager implements the ObservationManager interface for testing
+type MockObservationManager struct {
+	mock.Mock
+}
+
+func (m *MockObservationManager) IncrementMessagesConfirmed() {
+	m.Called()
+}
+
+// setupMockAztecServer creates a mock HTTP server for testing
+func setupMockAztecServer(_ *testing.T) *httptest.Server {
+	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		var req struct {
+			Method string          `json:"method"`
+			Params json.RawMessage `json:"params"`
+		}
+
+		body, err := common.SafeRead(r.Body)
+		if err != nil {
+			http.Error(w, "Failed to read body", http.StatusBadRequest)
+			return
+		}
+
+		if err := json.Unmarshal(body, &req); err != nil {
+			http.Error(w, "Invalid JSON", http.StatusBadRequest)
+			return
+		}
+
+		switch req.Method {
+		case "node_getPublicLogs":
+			if _, err := w.Write([]byte(`{
+                "jsonrpc": "2.0",
+                "id": 1,
+                "result": {
+                    "logs": [
+                        {
+                            "id": {"blockNumber": 5, "txIndex": 0, "logIndex": 0},
+                            "log": {
+                                "contractAddress": "0xContract",
+                                "fields": ["0x123"]
+                            }
+                        }
+                    ],
+                    "maxLogsHit": false
+                }
+            }`)); err != nil {
+				http.Error(w, "Write error", http.StatusInternalServerError)
+			}
+
+		case "node_getBlock":
+			if _, err := w.Write([]byte(`{
+                "jsonrpc": "2.0",
+                "id": 1,
+                "result": {
+                    "archive": {"root": "0xarchive", "nextAvailableLeafIndex": 1},
+                    "header": {
+                        "lastArchive": {"root": "0xparent", "nextAvailableLeafIndex": 0},
+                        "globalVariables": {
+                            "blockNumber": "0x5",
+                            "timestamp": "0x61a91c40"
+                        }
+                    },
+                    "body": {
+                        "txEffects": [
+                            {"txHash": "0x0123456789abcdef", "revertCode": 0}
+                        ]
+                    }
+                }
+            }`)); err != nil {
+				http.Error(w, "Write error", http.StatusInternalServerError)
+			}
+
+		case "node_getL2Tips":
+			if _, err := w.Write([]byte(`{
+                "jsonrpc": "2.0",
+                "id": 1,
+                "result": {
+                    "latest": {"number": 10, "hash": "0x123"},
+                    "proven": {"number": 8, "hash": "0x456"},
+                    "finalized": {"number": 5, "hash": "0x789"}
+                }
+            }`)); err != nil {
+				http.Error(w, "Write error", http.StatusInternalServerError)
+			}
+
+		default:
+			w.WriteHeader(http.StatusBadRequest)
+			if _, err := w.Write([]byte(`{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found"}}`)); err != nil {
+				http.Error(w, "Write error", http.StatusInternalServerError)
+			}
+		}
+	}))
+}
+
+// TestCreateObservationID tests the observation ID creation
+func TestCreateObservationID(t *testing.T) {
+	id := CreateObservationID("0x123", 456, 789)
+	assert.Equal(t, "0x123-456-789", id)
+}
+
+// TestProcessLogParameters tests parameter parsing
+func TestProcessLogParameters(t *testing.T) {
+	logger := zaptest.NewLogger(t)
+	config := DefaultConfig(vaa.ChainID(1), "test", "http://localhost:8545", "0xContract")
+	watcher := &Watcher{
+		config: config,
+		logger: logger,
+	}
+
+	// Valid parameters
+	t.Run("valid parameters", func(t *testing.T) {
+		logEntries := []string{
+			"0000000000000000000000000290f41e61374c715c1127974bf08a3993c512fd", // Sender (32 bytes)
+			"0000000000000000000000000000000000000000000000000000000000000123", // Sequence (291 in hex)
+			"0000000000000000000000000000000000000000000000000000000000000001", // Nonce (1)
+			"0000000000000000000000000000000000000000000000000000000000000001", // Consistency level (1)
+		}
+
+		params, err := watcher.parseLogParameters(logEntries)
+		assert.NoError(t, err)
+		assert.Equal(t, uint64(291), params.Sequence)
+		assert.Equal(t, uint32(1), params.Nonce)
+		assert.Equal(t, uint8(1), params.ConsistencyLevel)
+	})
+
+	// Invalid parameters (too few entries)
+	t.Run("invalid parameters", func(t *testing.T) {
+		invalidEntries := []string{
+			"0000000000000000000000000290f41e61374c715c1127974bf08a3993c512fd", // Sender (32 bytes)
+			"0000000000000000000000000000000000000000000000000000000000000123", // Sequence (291)
+		}
+
+		_, err := watcher.parseLogParameters(invalidEntries)
+		assert.Error(t, err)
+	})
+}
+
+// TestProcessLog tests the log processing function
+func TestProcessLog(t *testing.T) {
+	logger := zaptest.NewLogger(t)
+	mockBlockFetcher := new(MockBlockFetcher)
+	mockL1Verifier := new(MockL1Verifier)
+	mockObservationManager := new(MockObservationManager)
+	msgC := make(chan *common.MessagePublication, 10)
+	config := DefaultConfig(vaa.ChainID(52), "test", "http://localhost:8545", "0xContract")
+	contractAddress := config.ContractAddress
+
+	watcher := &Watcher{
+		config:             config,
+		blockFetcher:       mockBlockFetcher,
+		l1Verifier:         mockL1Verifier,
+		observationManager: mockObservationManager,
+		msgC:               msgC,
+		logger:             logger,
+	}
+
+	// Test with valid log
+	log := ExtendedPublicLog{
+		ID: LogId{
+			BlockNumber: 100,
+			TxIndex:     0,
+			LogIndex:    0,
+		},
+		Log: PublicLog{
+			ContractAddress: contractAddress,
+			Fields: []string{
+				"0000000000000000000000000290f41e61374c715c1127974bf08a3993c512fd", // Sender (32 bytes)
+				"0000000000000000000000000000000000000000000000000000000000000123", // Sequence (291)
+				"0000000000000000000000000000000000000000000000000000000000000001", // Nonce (1)
+				"0000000000000000000000000000000000000000000000000000000000000001", // Consistency level (1)
+				"0000000000000000000000000000000000000000000000000000000061a91c40", // Timestamp
+				"000000000000000000000000742d35Cc6634C0532925a3b8D50C6d111111111",  // Arbitrum address
+				"00000000000000000000000000000000000000000000000000000000000a4b1",  // Arbitrum chain ID
+				"000000000000000000000000000000000000000000000000000000000f4240",   // Amount
+				"4a61636b000000000000000000000000000000000000000000000000000000",   // "Jack" padded
+			},
+		},
+	}
+
+	blockInfo := BlockInfo{
+		TxHash:    "0x0123456789abcdef",
+		Timestamp: safeUnixToUint64(time.Now().Unix()),
+	}
+
+	mockObservationManager.On("IncrementMessagesConfirmed").Return()
+
+	// Process the log
+	err := watcher.processLog(context.Background(), log, blockInfo)
+
+	// Verify expectations
+	assert.NoError(t, err)
+	mockObservationManager.AssertExpectations(t)
+	assert.Equal(t, 1, len(msgC), "Should have published 1 message")
+
+	// Check the message
+	msg := <-msgC
+	assert.Equal(t, uint64(291), msg.Sequence) // 0x123 = 291
+	assert.Equal(t, uint32(1), msg.Nonce)
+	assert.Equal(t, uint8(1), msg.ConsistencyLevel)
+	assert.True(t, len(msg.Payload) > 32) // Should have txID + payload data
+}
+
+// TestPublishObservation tests the observation publishing function
+func TestPublishObservation(t *testing.T) {
+	logger := zaptest.NewLogger(t)
+	mockObservationManager := new(MockObservationManager)
+	msgC := make(chan *common.MessagePublication, 10)
+	config := DefaultConfig(vaa.ChainID(52), "test", "http://localhost:8545", "0xContract")
+
+	watcher := &Watcher{
+		config:             config,
+		observationManager: mockObservationManager,
+		msgC:               msgC,
+		logger:             logger,
+	}
+
+	params := LogParameters{
+		SenderAddress:    vaa.Address{1, 2, 3, 4, 5},
+		Sequence:         123,
+		Nonce:            456,
+		ConsistencyLevel: 1,
+	}
+
+	payload := []byte{1, 2, 3, 4, 5}
+	blockInfo := BlockInfo{
+		TxHash:    "0x0123456789abcdef",
+		Timestamp: safeUnixToUint64(time.Now().Unix()),
+	}
+	observationID := "test-observation"
+
+	mockObservationManager.On("IncrementMessagesConfirmed").Return()
+	err := watcher.publishObservation(context.Background(), params, payload, blockInfo, observationID)
+
+	assert.NoError(t, err)
+	mockObservationManager.AssertExpectations(t)
+	assert.Equal(t, 1, len(msgC))
+
+	msg := <-msgC
+	assert.Equal(t, params.Sequence, msg.Sequence)
+	assert.Equal(t, params.Nonce, msg.Nonce)
+	assert.Equal(t, params.ConsistencyLevel, msg.ConsistencyLevel)
+	assert.Equal(t, payload, msg.Payload)
+
+	expectedTxID, _ := hex.DecodeString("0123456789abcdef")
+	assert.Equal(t, expectedTxID, msg.TxID)
+}
+
+// TestNewAztecFinalityVerifier tests the creation of the finality verifier
+func TestNewAztecFinalityVerifier(t *testing.T) {
+	// Create a verifier with a test URL
+	rpcURL := "http://localhost:9999"
+	logger := zap.NewNop()
+
+	verifier, err := NewAztecFinalityVerifier(context.Background(), rpcURL, logger)
+
+	// The function should return a valid verifier
+	assert.NoError(t, err)
+	assert.NotNil(t, verifier)
+
+	// Check the type of the returned verifier
+	aztecVerifier, ok := verifier.(*aztecFinalityVerifier)
+	assert.True(t, ok, "Should return an aztecFinalityVerifier instance")
+	assert.Equal(t, 30*time.Second, aztecVerifier.finalizedBlockCacheTTL)
+}
+
+// TestErrorTypes tests the error types
+func TestErrorTypes(t *testing.T) {
+	// Test RPC error
+	rpcErr := ErrRPCError{
+		Method: "test_method",
+		Code:   -32000,
+		Msg:    "test error",
+	}
+	assert.Equal(t, "RPC error calling test_method: test error", rpcErr.Error())
+
+	// Test parsing failed error
+	parsingErr := ErrParsingFailed{
+		What: "test data",
+		Err:  fmt.Errorf("test error"),
+	}
+	assert.Equal(t, "failed parsing test data: test error", parsingErr.Error())
+}
+
+// TestAztecFinalityVerifier_Implementation tests the finality verifier implementation
+func TestAztecFinalityVerifier_Implementation(t *testing.T) {
+	// Setup mock server
+	server := setupMockAztecServer(t)
+	defer server.Close()
+
+	// Create the verifier directly with the test server
+	client, err := rpc.DialContext(context.Background(), server.URL)
+	require.NoError(t, err)
+
+	// Create verifier manually
+	verifier := &aztecFinalityVerifier{
+		rpcClient:              client,
+		logger:                 zap.NewNop(),
+		finalizedBlockCacheTTL: time.Second,
+	}
+
+	// Test GetFinalizedBlock
+	t.Run("GetFinalizedBlock", func(t *testing.T) {
+		block, err := verifier.GetFinalizedBlock(context.Background())
+		require.NoError(t, err)
+		require.Equal(t, 5, block.Number)     // Should be finalized block number
+		require.Equal(t, "0x789", block.Hash) // Should be finalized hash
+	})
+
+	// Test IsBlockFinalized
+	t.Run("IsBlockFinalized", func(t *testing.T) {
+		finalized, err := verifier.IsBlockFinalized(context.Background(), 3)
+		require.NoError(t, err)
+		require.True(t, finalized)
+
+		finalized, err = verifier.IsBlockFinalized(context.Background(), 10)
+		require.NoError(t, err)
+		require.False(t, finalized)
+	})
+
+	// Test GetLatestFinalizedBlockNumber
+	t.Run("GetLatestFinalizedBlockNumber", func(t *testing.T) {
+		number := verifier.GetLatestFinalizedBlockNumber()
+		require.Equal(t, uint64(5), number) // Should match finalized block number
+	})
+}
+
+// TestHelperFunctions tests various helper functions
+func TestHelperFunctions(t *testing.T) {
+	// Test GetJSONRPCError
+	t.Run("GetJSONRPCError", func(t *testing.T) {
+		// Test with error response
+		jsonResp := []byte(`{"error":{"code":-32000,"message":"test error"}}`)
+		hasError, rpcErr := GetJSONRPCError(jsonResp)
+		require.True(t, hasError)
+		require.Equal(t, -32000, rpcErr.Code)
+		require.Equal(t, "test error", rpcErr.Msg)
+
+		// Test with success response
+		jsonResp = []byte(`{"result":"success"}`)
+		hasError, _ = GetJSONRPCError(jsonResp)
+		require.False(t, hasError)
+	})
+}
+
+// TestWatcherConfig tests the WatcherConfig implementation
+func TestWatcherConfig(t *testing.T) {
+	// Create config instance
+	config := &WatcherConfig{
+		NetworkID: "aztec-test",
+		ChainID:   vaa.ChainID(52),
+		Rpc:       "http://test-url",
+		Contract:  "0xContract",
+	}
+
+	// Test GetChainID
+	t.Run("GetChainID", func(t *testing.T) {
+		chainID := config.GetChainID()
+		require.Equal(t, vaa.ChainID(52), chainID)
+	})
+
+	// Test GetNetworkID
+	t.Run("GetNetworkID", func(t *testing.T) {
+		networkID := config.GetNetworkID()
+		require.Equal(t, watchers.NetworkID("aztec-test"), networkID)
+	})
+}
+
+// TestDefaultConfig tests the default configuration
+func TestDefaultConfig(t *testing.T) {
+	config := DefaultConfig(vaa.ChainID(52), "test", "http://localhost:8545", "0xContract")
+	require.Equal(t, vaa.ChainID(52), config.ChainID)
+	require.Equal(t, "test", config.NetworkID)
+	require.Equal(t, "http://localhost:8545", config.RpcURL)
+	require.Equal(t, "0xContract", config.ContractAddress)
+	require.Equal(t, 1, config.StartBlock)
+	require.Equal(t, 13, config.PayloadInitialCap)
+}
+
+// Helper function to safely convert Unix timestamp
+func safeUnixToUint64(unixTime int64) uint64 {
+	if unixTime < 0 {
+		return 0
+	}
+	return uint64(unixTime)
+}

+ 2 - 2
node/pkg/watchers/evm/chain_config.go

@@ -97,8 +97,8 @@ var (
 		vaa.ChainIDWorldchain: {Finalized: true, Safe: true, EvmChainID: 480, PublicRPC: "https://worldchain-mainnet.g.alchemy.com/public", ContractAddr: "0xcbcEe4e081464A15d8Ad5f58BB493954421eB506"},
 		vaa.ChainIDWorldchain: {Finalized: true, Safe: true, EvmChainID: 480, PublicRPC: "https://worldchain-mainnet.g.alchemy.com/public", ContractAddr: "0xcbcEe4e081464A15d8Ad5f58BB493954421eB506"},
 		vaa.ChainIDInk:        {Finalized: true, Safe: true, EvmChainID: 57073, PublicRPC: "https://rpc-qnd.inkonchain.com", ContractAddr: "0xCa1D5a146B03f6303baF59e5AD5615ae0b9d146D"},
 		vaa.ChainIDInk:        {Finalized: true, Safe: true, EvmChainID: 57073, PublicRPC: "https://rpc-qnd.inkonchain.com", ContractAddr: "0xCa1D5a146B03f6303baF59e5AD5615ae0b9d146D"},
 		vaa.ChainIDHyperEVM:   {Finalized: true, Safe: true, EvmChainID: 999, PublicRPC: "https://rpc.hyperliquid.xyz/evm", ContractAddr: "0x7C0faFc4384551f063e05aee704ab943b8B53aB3"},
 		vaa.ChainIDHyperEVM:   {Finalized: true, Safe: true, EvmChainID: 999, PublicRPC: "https://rpc.hyperliquid.xyz/evm", ContractAddr: "0x7C0faFc4384551f063e05aee704ab943b8B53aB3"},
-		// vaa.ChainIDMonad:      Not in Mainnet yet.
-		vaa.ChainIDMezo: {Finalized: true, Safe: true, EvmChainID: 31612, PublicRPC: "https://jsonrpc-mezo.boar.network/", ContractAddr: "0xaBf89de706B583424328B54dD05a8fC986750Da8"},
+		vaa.ChainIDMonad:      {Finalized: true, Safe: true, EvmChainID: 143, PublicRPC: "https://rpc.monad.xyz", ContractAddr: "0x194B123c5E96B9b2E49763619985790Dc241CAC0"},
+		vaa.ChainIDMezo:       {Finalized: true, Safe: true, EvmChainID: 31612, PublicRPC: "https://jsonrpc-mezo.boar.network/", ContractAddr: "0xaBf89de706B583424328B54dD05a8fC986750Da8"},
 		// vaa.ChainIDConverge: Not in Mainnet yet
 		// vaa.ChainIDConverge: Not in Mainnet yet
 		vaa.ChainIDPlume:   {Finalized: true, Safe: true, EvmChainID: 98866, PublicRPC: "https://rpc.plume.org", ContractAddr: "0xaBf89de706B583424328B54dD05a8fC986750Da8"},
 		vaa.ChainIDPlume:   {Finalized: true, Safe: true, EvmChainID: 98866, PublicRPC: "https://rpc.plume.org", ContractAddr: "0xaBf89de706B583424328B54dD05a8fC986750Da8"},
 		vaa.ChainIDXRPLEVM: {Finalized: true, Safe: true, EvmChainID: 1440000, PublicRPC: "https://rpc.xrplevm.org/", ContractAddr: "0xaBf89de706B583424328B54dD05a8fC986750Da8"},
 		vaa.ChainIDXRPLEVM: {Finalized: true, Safe: true, EvmChainID: 1440000, PublicRPC: "https://rpc.xrplevm.org/", ContractAddr: "0xaBf89de706B583424328B54dD05a8fC986750Da8"},

+ 43 - 0
relayer/aztec/go.mod

@@ -0,0 +1,43 @@
+module github.com/wormhole-foundation/wormhole/aztec/relayer
+
+go 1.24.1
+
+require (
+	github.com/Microsoft/go-winio v0.6.2 // indirect
+	github.com/StackExchange/wmi v1.2.1 // indirect
+	github.com/bits-and-blooms/bitset v1.17.0 // indirect
+	github.com/certusone/wormhole/node v0.0.0-20250411205235-4e03f24d0f79 // indirect
+	github.com/consensys/bavard v0.1.22 // indirect
+	github.com/consensys/gnark-crypto v0.14.0 // indirect
+	github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
+	github.com/crate-crypto/go-kzg-4844 v1.1.0 // indirect
+	github.com/deckarep/golang-set/v2 v2.6.0 // indirect
+	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
+	github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
+	github.com/ethereum/go-ethereum v1.15.8 // indirect
+	github.com/ethereum/go-verkle v0.2.2 // indirect
+	github.com/go-ole/go-ole v1.3.0 // indirect
+	github.com/golang/protobuf v1.5.4 // indirect
+	github.com/gorilla/websocket v1.5.3 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2 // indirect
+	github.com/holiman/uint256 v1.3.2 // indirect
+	github.com/mmcloughlin/addchain v0.4.0 // indirect
+	github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
+	github.com/supranational/blst v0.3.14 // indirect
+	github.com/tklauser/go-sysconf v0.3.12 // indirect
+	github.com/tklauser/numcpus v0.6.1 // indirect
+	github.com/wormhole-foundation/wormhole/sdk v0.0.0-20250411205235-4e03f24d0f79 // indirect
+	go.uber.org/multierr v1.11.0 // indirect
+	go.uber.org/zap v1.27.0 // indirect
+	golang.org/x/crypto v0.35.0 // indirect
+	golang.org/x/net v0.36.0 // indirect
+	golang.org/x/sync v0.11.0 // indirect
+	golang.org/x/sys v0.30.0 // indirect
+	golang.org/x/text v0.22.0 // indirect
+	google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
+	google.golang.org/grpc v1.71.1 // indirect
+	google.golang.org/protobuf v1.36.4 // indirect
+	rsc.io/tmplfunc v0.0.3 // indirect
+)

+ 97 - 0
relayer/aztec/go.sum

@@ -0,0 +1,97 @@
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
+github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
+github.com/bits-and-blooms/bitset v1.17.0 h1:1X2TS7aHz1ELcC0yU1y2stUs/0ig5oMU6STFZGrhvHI=
+github.com/bits-and-blooms/bitset v1.17.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
+github.com/certusone/wormhole/node v0.0.0-20250411205235-4e03f24d0f79 h1:fy1hcTlCeeFPzZ0TiPlGkFn19ZOuFlVwA2PLoOkl6v0=
+github.com/certusone/wormhole/node v0.0.0-20250411205235-4e03f24d0f79/go.mod h1:cDIImwaZSKl2sK+3uiRNn2EaHQeesftX7pcKTZX4p9w=
+github.com/consensys/bavard v0.1.22 h1:Uw2CGvbXSZWhqK59X0VG/zOjpTFuOMcPLStrp1ihI0A=
+github.com/consensys/bavard v0.1.22/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs=
+github.com/consensys/gnark-crypto v0.14.0 h1:DDBdl4HaBtdQsq/wfMwJvZNE80sHidrK3Nfrefatm0E=
+github.com/consensys/gnark-crypto v0.14.0/go.mod h1:CU4UijNPsHawiVGNxe9co07FkzCeWHHrb1li/n1XoU0=
+github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg=
+github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM=
+github.com/crate-crypto/go-kzg-4844 v1.1.0 h1:EN/u9k2TF6OWSHrCCDBBU6GLNMq88OspHHlMnHfoyU4=
+github.com/crate-crypto/go-kzg-4844 v1.1.0/go.mod h1:JolLjpSff1tCCJKaJx4psrlEdlXuJEC996PL3tTAFks=
+github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4=
+github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
+github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
+github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA=
+github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0=
+github.com/ethereum/go-ethereum v1.15.8 h1:H6NilvRXFVoHiXZ3zkuTqKW5XcxjLZniV5UjxJt1GJU=
+github.com/ethereum/go-ethereum v1.15.8/go.mod h1:+S9k+jFzlyVTNcYGvqFhzN/SFhI6vA+aOY4T5tLSPL0=
+github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
+github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
+github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
+github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2 h1:dygLcbEBA+t/P7ck6a8AkXv6juQ4cK0RHBoh32jxhHM=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2/go.mod h1:Ap9RLCIJVtgQg1/BBgVEfypOAySvvlcpcVQkSzJCH4Y=
+github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
+github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
+github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY=
+github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU=
+github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU=
+github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
+github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
+github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo=
+github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
+github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
+github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
+github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
+github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
+github.com/wormhole-foundation/wormhole/sdk v0.0.0-20250411205235-4e03f24d0f79 h1:Ch4eUT+Ti4rs3uZ2uDUmowz63McMZl9cnHbxJkKLXXg=
+github.com/wormhole-foundation/wormhole/sdk v0.0.0-20250411205235-4e03f24d0f79/go.mod h1:pE/jYet19kY4P3V6mE2+01zvEfxdyBqv6L6HsnSa5uc=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
+golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
+golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
+golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
+golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
+golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
+golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
+golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
+golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
+golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 h1:L6iMMGrtzgHsWofoFcihmDEMYeDR9KN/ThbPWGrh++g=
+google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8=
+google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e h1:z3vDksarJxsAKM5dmEGv0GHwE2hKJ096wZra71Vs4sw=
+google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ=
+google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
+google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577 h1:wukfNtZmZUurLN/atp2hiIeTKn7QJWIQdHzqmsOnAOk=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
+google.golang.org/grpc v1.57.1 h1:upNTNqv0ES+2ZOOqACwVtS3Il8M12/+Hz41RCPzAjQg=
+google.golang.org/grpc v1.57.1/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
+google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
+google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
+google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
+google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
+google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU=
+rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA=

+ 890 - 0
relayer/aztec/relayer.go

@@ -0,0 +1,890 @@
+package main
+
+import (
+	"bytes"
+	"context"
+	"crypto/ecdsa"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"io"
+	"math/big"
+	"net/http"
+	"os"
+	"os/signal"
+	"strconv"
+	"strings"
+	"sync"
+	"syscall"
+	"time"
+
+	spyv1 "github.com/certusone/wormhole/node/pkg/proto/spy/v1"
+	"github.com/ethereum/go-ethereum/accounts/abi"
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/crypto"
+	"github.com/ethereum/go-ethereum/ethclient"
+	"github.com/ethereum/go-ethereum/rpc"
+	vaaLib "github.com/wormhole-foundation/wormhole/sdk/vaa"
+	"go.uber.org/zap"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials/insecure"
+)
+
+// Global logger for initial setup
+var logger *zap.Logger
+
+// Initialize global logger
+func initLogger() {
+	var err error
+	logger, err = zap.NewProduction()
+	if err != nil {
+		// Fallback to standard logger if zap fails
+		fmt.Printf("Failed to initialize zap logger: %v\n", err)
+		logger = zap.NewExample()
+	}
+}
+
+// ADD: HTTP verification service types
+type VerificationRequest struct {
+	VAABytes string `json:"vaaBytes"`
+}
+
+type VerificationResponse struct {
+	Success bool   `json:"success"`
+	TxHash  string `json:"txHash,omitempty"`
+	Error   string `json:"error,omitempty"`
+}
+
+// ADD: HTTP client for verification service
+type VerificationServiceClient struct {
+	baseURL    string
+	httpClient *http.Client
+	logger     *zap.Logger
+}
+
+// ADD: Create new verification service client
+func NewVerificationServiceClient(baseURL string) *VerificationServiceClient {
+	return &VerificationServiceClient{
+		baseURL: strings.TrimSuffix(baseURL, "/"),
+		httpClient: &http.Client{
+			Timeout: 60 * time.Second,
+		},
+		logger: logger.With(zap.String("component", "VerificationServiceClient")),
+	}
+}
+
+// ADD: Verify VAA via HTTP service
+func (c *VerificationServiceClient) VerifyVAA(ctx context.Context, vaaBytes []byte) (string, error) {
+	c.logger.Debug("Sending VAA to verification service", zap.Int("vaaLength", len(vaaBytes)))
+
+	// Prepare request
+	vaaHex := hex.EncodeToString(vaaBytes)
+	if !strings.HasPrefix(vaaHex, "0x") {
+		vaaHex = "0x" + vaaHex
+	}
+
+	request := VerificationRequest{
+		VAABytes: vaaHex,
+	}
+
+	jsonData, err := json.Marshal(request)
+	if err != nil {
+		return "", fmt.Errorf("failed to marshal verification request: %v", err)
+	}
+
+	// Create HTTP request
+	req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/verify", bytes.NewBuffer(jsonData))
+	if err != nil {
+		return "", fmt.Errorf("failed to create HTTP request: %v", err)
+	}
+
+	req.Header.Set("Content-Type", "application/json")
+
+	// Send request
+	resp, err := c.httpClient.Do(req)
+	if err != nil {
+		return "", fmt.Errorf("failed to send verification request: %v", err)
+	}
+	defer resp.Body.Close()
+
+	// Read response
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return "", fmt.Errorf("failed to read verification response: %v", err)
+	}
+
+	c.logger.Debug("Received response from verification service",
+		zap.Int("statusCode", resp.StatusCode))
+
+	// Parse response
+	var response VerificationResponse
+	if err := json.Unmarshal(body, &response); err != nil {
+		return "", fmt.Errorf("failed to unmarshal verification response: %v", err)
+	}
+
+	if !response.Success {
+		return "", fmt.Errorf("verification failed: %s", response.Error)
+	}
+
+	return response.TxHash, nil
+}
+
+// ADD: Check if verification service is healthy
+func (c *VerificationServiceClient) CheckHealth(ctx context.Context) error {
+	req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/health", nil)
+	if err != nil {
+		return fmt.Errorf("failed to create health check request: %v", err)
+	}
+
+	resp, err := c.httpClient.Do(req)
+	if err != nil {
+		return fmt.Errorf("health check failed: %v", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return fmt.Errorf("verification service unhealthy: status %d", resp.StatusCode)
+	}
+
+	return nil
+}
+
+// Config holds all configuration parameters for the relayer
+type Config struct {
+	SpyRPCHost             string                         // Wormhole spy service endpoint
+	SourceChainID          uint16                         // Aztec chain ID
+	DestChainID            uint16                         // Arbitrum chain ID
+	AztecPXEURL            string                         // PXE URL for Aztec
+	AztecWalletAddress     string                         // Aztec wallet address to use
+	ArbitrumRPCURL         string                         // RPC URL for Arbitrum
+	PrivateKey             string                         // Private key for Arbitrum
+	WormholeContract       string                         // Wormhole core contract address
+	AztecTargetContract    string                         // Target contract on Aztec
+	ArbitrumTargetContract string                         // Target contract on Arbitrum
+	EmitterAddress         string                         // Emitter address to monitor
+	VerificationServiceURL string                         // ADD: Verification service URL
+	vaaProcessor           func(*Relayer, *VAAData) error // Custom VAA processor function
+}
+
+// NewConfigFromEnv creates a Config from environment variables
+func NewConfigFromEnv() Config {
+	return Config{
+		SpyRPCHost:             getEnvOrDefault("SPY_RPC_HOST", "localhost:7072"),
+		SourceChainID:          uint16(getEnvIntOrDefault("SOURCE_CHAIN_ID", 56)), // Aztec
+		DestChainID:            uint16(getEnvIntOrDefault("DEST_CHAIN_ID", 2)),    // Arbitrum
+		AztecPXEURL:            getEnvOrDefault("AZTEC_PXE_URL", "http://localhost:8090"),
+		AztecWalletAddress:     getEnvOrDefault("AZTEC_WALLET_ADDRESS", "0x1f3933ca4d66e948ace5f8339e5da687993b76ee57bcf65e82596e0fc10a8859"),
+		ArbitrumRPCURL:         getEnvOrDefault("ARBITRUM_RPC_URL", "http://localhost:8545"),
+		PrivateKey:             getEnvOrDefault("PRIVATE_KEY", "0x0ff5c4c050588f4614255a5a4f800215b473e442ae9984347b3a727c3bb7ca55"),
+		WormholeContract:       getEnvOrDefault("WORMHOLE_CONTRACT", "0x1b35884f8ba9371419d00ae228da9ff839edfe8fe6a804fdfcd430e0dc7e40db"),
+		AztecTargetContract:    getEnvOrDefault("AZTEC_TARGET_CONTRACT", "0x0e61ae3f9f51ae20042f48674e2bf1c19cde5c916ae3a5ed114d84c873cc9a8f"),
+		ArbitrumTargetContract: getEnvOrDefault("ARBITRUM_TARGET_CONTRACT", "0x009cbB8f91d392856Cb880d67c806Aa731E3d686"),
+		EmitterAddress:         getEnvOrDefault("EMITTER_ADDRESS", "0d6fe810321185c97a0e94200f998bcae787aaddf953a03b14ec5da3b6838bad"),
+		VerificationServiceURL: getEnvOrDefault("VERIFICATION_SERVICE_URL", "http://localhost:8080"), // ADD
+	}
+}
+
+// VAAData encapsulates a VAA and its metadata
+type VAAData struct {
+	VAA        *vaaLib.VAA // The parsed VAA
+	RawBytes   []byte      // Raw VAA bytes
+	ChainID    uint16      // Source chain ID
+	EmitterHex string      // Hex-encoded emitter address
+	Sequence   uint64      // VAA sequence number
+	TxID       string      // Source transaction ID
+}
+
+// SpyClient handles connections to the Wormhole spy service
+type SpyClient struct {
+	conn   *grpc.ClientConn
+	client spyv1.SpyRPCServiceClient
+	logger *zap.Logger
+}
+
+// NewSpyClient creates a new client for the Wormhole spy service
+func NewSpyClient(endpoint string) (*SpyClient, error) {
+	client := &SpyClient{
+		logger: logger.With(zap.String("component", "SpyClient")),
+	}
+
+	client.logger.Info("Connecting to spy service", zap.String("endpoint", endpoint))
+	conn, err := grpc.Dial(endpoint, grpc.WithTransportCredentials(insecure.NewCredentials()))
+	if err != nil {
+		return nil, fmt.Errorf("failed to connect to spy: %v", err)
+	}
+
+	client.conn = conn
+	client.client = spyv1.NewSpyRPCServiceClient(conn)
+	return client, nil
+}
+
+// Close closes the connection to the spy service
+func (c *SpyClient) Close() {
+	if c.conn != nil {
+		c.conn.Close()
+	}
+}
+
+// SubscribeSignedVAA subscribes to all signed VAAs with retry logic
+func (c *SpyClient) SubscribeSignedVAA(ctx context.Context) (spyv1.SpyRPCService_SubscribeSignedVAAClient, error) {
+	const maxRetries = 5
+	const retryDelay = 2 * time.Second
+
+	c.logger.Debug("Subscribing to signed VAAs")
+
+	var stream spyv1.SpyRPCService_SubscribeSignedVAAClient
+	var err error
+
+	for attempt := 1; attempt <= maxRetries; attempt++ {
+		stream, err = c.client.SubscribeSignedVAA(ctx, &spyv1.SubscribeSignedVAARequest{})
+		if err == nil {
+			return stream, nil
+		}
+
+		if attempt < maxRetries {
+			c.logger.Warn("Subscribe attempt failed",
+				zap.Int("attempt", attempt),
+				zap.Error(err),
+				zap.Duration("retryIn", retryDelay))
+
+			select {
+			case <-time.After(retryDelay):
+				// Continue to next retry
+			case <-ctx.Done():
+				return nil, fmt.Errorf("subscribe to signed VAAs: %v", ctx.Err())
+			}
+		}
+	}
+
+	return nil, fmt.Errorf("subscribe to signed VAAs after %d attempts: %v", maxRetries, err)
+}
+
+// AztecPXEClient handles interactions with Aztec blockchain via PXE
+type AztecPXEClient struct {
+	rpcClient     *rpc.Client
+	walletAddress string
+	logger        *zap.Logger
+}
+
+// NewAztecPXEClient creates a new client for Aztec blockchain via PXE
+func NewAztecPXEClient(pxeURL, walletAddress string) (*AztecPXEClient, error) {
+	client := &AztecPXEClient{
+		walletAddress: walletAddress,
+		logger:        logger.With(zap.String("component", "AztecPXEClient")),
+	}
+
+	client.logger.Info("Connecting to Aztec PXE",
+		zap.String("pxeURL", pxeURL),
+		zap.String("walletAddress", walletAddress))
+
+	// Create RPC client using the same pattern as your working code
+	rpcClient, err := rpc.Dial(pxeURL)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create RPC client: %v", err)
+	}
+
+	client.rpcClient = rpcClient
+
+	// Test connection using the working node_getBlock method
+	err = client.testConnection()
+	if err != nil {
+		return nil, fmt.Errorf("failed to connect to Aztec PXE: %v", err)
+	}
+
+	return client, nil
+}
+
+// testConnection tests the connection to Aztec PXE using working methods
+func (c *AztecPXEClient) testConnection() error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	// Test with node_getBlock method (we know this works)
+	var blockResult interface{}
+	err := c.rpcClient.CallContext(ctx, &blockResult, "node_getBlock", 1)
+	if err != nil {
+		c.logger.Debug("node_getBlock test failed", zap.Error(err))
+		// This is okay - block 1 might not exist, connection is still working
+	}
+
+	c.logger.Info("Aztec PXE connection successful")
+	return nil
+}
+
+// SendVerifyTransaction sends a transaction to verify and store a VAA on Aztec via PXE
+func (c *AztecPXEClient) SendVerifyTransaction(ctx context.Context, targetContract string, vaaBytes []byte) (string, error) {
+	c.logger.Debug("Sending verify_vaa transaction to Aztec via PXE", zap.Int("vaaLength", len(vaaBytes)))
+
+	// Pad to 2000 bytes for contract but pass actual length
+	paddedVAABytes := make([]byte, 2000)
+	copy(paddedVAABytes, vaaBytes)
+
+	// Convert the padded bytes to array format for Aztec
+	vaaArray := make([]interface{}, 2000)
+	for i, b := range paddedVAABytes {
+		vaaArray[i] = int(b)
+	}
+
+	actualLength := len(vaaBytes)
+
+	c.logger.Debug("Calling verify_vaa function",
+		zap.String("contract", targetContract),
+		zap.Int("actualLength", actualLength),
+		zap.Int("paddedLength", len(paddedVAABytes)))
+
+	// Use the RPC client pattern from your working code
+	// First, let's try to simulate the call to see if the contract/function exists
+	var result interface{}
+	err := c.rpcClient.CallContext(ctx, &result, "pxe_simulateTransaction", map[string]interface{}{
+		"contractAddress": targetContract,
+		"functionName":    "verify_vaa",
+		"args":            []interface{}{vaaArray, actualLength},
+		"origin":          c.walletAddress,
+	})
+
+	if err != nil {
+		c.logger.Warn("Transaction simulation failed", zap.Error(err))
+		// Continue anyway - simulation might not be available
+	} else {
+		c.logger.Debug("Transaction simulation successful", zap.Any("result", result))
+	}
+
+	// Now try to send the actual transaction
+	// This method name needs to be confirmed with actual PXE API
+	var txResult interface{}
+	err = c.rpcClient.CallContext(ctx, &txResult, "pxe_sendTransaction", map[string]interface{}{
+		"contractAddress": targetContract,
+		"functionName":    "verify_vaa",
+		"args":            []interface{}{vaaArray, actualLength},
+		"origin":          c.walletAddress,
+	})
+
+	if err != nil {
+		return "", fmt.Errorf("failed to send verify_vaa transaction: %v", err)
+	}
+
+	// Extract transaction hash from result
+	if txMap, ok := txResult.(map[string]interface{}); ok {
+		if txHash, exists := txMap["txHash"]; exists {
+			if txHashStr, ok := txHash.(string); ok {
+				return txHashStr, nil
+			}
+		}
+		if txHash, exists := txMap["hash"]; exists {
+			if txHashStr, ok := txHash.(string); ok {
+				return txHashStr, nil
+			}
+		}
+	}
+
+	if txHashStr, ok := txResult.(string); ok {
+		return txHashStr, nil
+	}
+
+	c.logger.Debug("PXE transaction result", zap.Any("result", txResult))
+	return fmt.Sprintf("tx_submitted_%d", time.Now().Unix()), nil
+}
+
+// GetWalletAddress returns the wallet address being used
+func (c *AztecPXEClient) GetWalletAddress() string {
+	return c.walletAddress
+}
+
+// EVMClient handles interactions with EVM-compatible blockchains (Arbitrum)
+type EVMClient struct {
+	client     *ethclient.Client
+	privateKey *ecdsa.PrivateKey
+	address    common.Address
+	logger     *zap.Logger
+}
+
+// NewEVMClient creates a new client for EVM-compatible blockchains
+func NewEVMClient(rpcURL, privateKeyHex string) (*EVMClient, error) {
+	client := &EVMClient{
+		logger: logger.With(zap.String("component", "EVMClient")),
+	}
+
+	client.logger.Info("Connecting to EVM chain", zap.String("rpcURL", rpcURL))
+	ethClient, err := ethclient.Dial(rpcURL)
+	if err != nil {
+		return nil, fmt.Errorf("failed to connect to EVM node: %v", err)
+	}
+
+	// Parse private key
+	privateKey, err := crypto.HexToECDSA(strings.TrimPrefix(privateKeyHex, "0x"))
+	if err != nil {
+		return nil, fmt.Errorf("invalid private key: %v", err)
+	}
+
+	// Derive public address
+	publicKey := privateKey.Public()
+	publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
+	if !ok {
+		return nil, fmt.Errorf("error casting public key to ECDSA")
+	}
+	address := crypto.PubkeyToAddress(*publicKeyECDSA)
+
+	client.client = ethClient
+	client.privateKey = privateKey
+	client.address = address
+
+	return client, nil
+}
+
+// GetAddress returns the public address for this client
+func (c *EVMClient) GetAddress() common.Address {
+	return c.address
+}
+
+// SendVerifyTransaction sends a transaction to the verify function to process and store a VAA
+func (c *EVMClient) SendVerifyTransaction(ctx context.Context, targetContract string, vaaBytes []byte) (string, error) {
+	c.logger.Debug("Sending verify transaction to EVM", zap.Int("vaaLength", len(vaaBytes)))
+
+	// Contract ABI for the verify function
+	const abiJSON = `[{
+        "inputs": [
+            {"internalType": "bytes", "name": "encodedVm", "type": "bytes"}
+        ],
+        "name": "verify",
+        "outputs": [],
+        "stateMutability": "nonpayable",
+        "type": "function"
+    }]`
+
+	parsedABI, err := abi.JSON(strings.NewReader(abiJSON))
+	if err != nil {
+		return "", fmt.Errorf("ABI parse error: %v", err)
+	}
+
+	// Pack the function call data
+	data, err := parsedABI.Pack("verify", vaaBytes)
+	if err != nil {
+		return "", fmt.Errorf("ABI pack error: %v", err)
+	}
+
+	// Get the latest nonce for our account
+	nonce, err := c.client.PendingNonceAt(ctx, c.address)
+	if err != nil {
+		return "", fmt.Errorf("failed to get nonce: %v", err)
+	}
+
+	// Get the current gas price
+	gasPrice, err := c.client.SuggestGasPrice(ctx)
+	if err != nil {
+		return "", fmt.Errorf("failed to get gas price: %v", err)
+	}
+
+	// Create the transaction
+	targetAddr := common.HexToAddress(targetContract)
+	tx := types.NewTransaction(
+		nonce,
+		targetAddr,
+		big.NewInt(0), // No ETH being sent
+		3000000,       // Gas limit - adjust as needed
+		gasPrice,
+		data,
+	)
+
+	// Get the chain ID
+	chainID, err := c.client.NetworkID(ctx)
+	if err != nil {
+		return "", fmt.Errorf("failed to get chain ID: %v", err)
+	}
+
+	// Sign the transaction
+	signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), c.privateKey)
+	if err != nil {
+		return "", fmt.Errorf("failed to sign transaction: %v", err)
+	}
+
+	// Send the transaction
+	err = c.client.SendTransaction(ctx, signedTx)
+	if err != nil {
+		return "", fmt.Errorf("failed to send transaction: %v", err)
+	}
+
+	return signedTx.Hash().Hex(), nil
+}
+
+// Relayer coordinates processing VAAs from the spy service
+type Relayer struct {
+	spyClient          *SpyClient
+	aztecClient        *AztecPXEClient
+	evmClient          *EVMClient
+	verificationClient *VerificationServiceClient // ADD: HTTP verification client
+	config             Config
+	vaaProcessor       func(*Relayer, *VAAData) error
+	logger             *zap.Logger
+}
+
+// NewRelayer creates a new relayer instance
+func NewRelayer(config Config) (*Relayer, error) {
+	relayer := &Relayer{
+		config: config,
+		logger: logger.With(zap.String("component", "Relayer")),
+	}
+
+	// Connect to the spy service
+	spyClient, err := NewSpyClient(config.SpyRPCHost)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create spy client: %v", err)
+	}
+
+	// Connect to Aztec via PXE
+	aztecClient, err := NewAztecPXEClient(config.AztecPXEURL, config.AztecWalletAddress)
+	if err != nil {
+		spyClient.Close()
+		return nil, fmt.Errorf("failed to create Aztec PXE client: %v", err)
+	}
+
+	// Connect to Arbitrum (EVM)
+	evmClient, err := NewEVMClient(config.ArbitrumRPCURL, config.PrivateKey)
+	if err != nil {
+		spyClient.Close()
+		return nil, fmt.Errorf("failed to create EVM client: %v", err)
+	}
+
+	// ADD: Create verification service client
+	verificationClient := NewVerificationServiceClient(config.VerificationServiceURL)
+
+	// ADD: Test connection to verification service
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+	if err := verificationClient.CheckHealth(ctx); err != nil {
+		spyClient.Close()
+		relayer.logger.Warn("Verification service not available", zap.Error(err))
+		// Don't fail - we can still relay Aztec->Arbitrum
+	} else {
+		relayer.logger.Info("Connected to verification service", zap.String("url", config.VerificationServiceURL))
+	}
+
+	relayer.spyClient = spyClient
+	relayer.aztecClient = aztecClient
+	relayer.evmClient = evmClient
+	relayer.verificationClient = verificationClient // ADD
+
+	// Set default VAA processor
+	if config.vaaProcessor == nil {
+		relayer.vaaProcessor = defaultVAAProcessor
+	} else {
+		relayer.vaaProcessor = config.vaaProcessor
+	}
+
+	return relayer, nil
+}
+
+// Close cleans up resources used by the relayer
+func (r *Relayer) Close() {
+	if r.spyClient != nil {
+		r.spyClient.Close()
+	}
+}
+
+// Start begins listening for VAAs and processing them
+func (r *Relayer) Start(ctx context.Context) error {
+	r.logger.Info("Starting bidirectional Aztec-Arbitrum relayer",
+		zap.String("aztecWallet", r.aztecClient.GetWalletAddress()),
+		zap.String("arbitrumAddress", r.evmClient.GetAddress().Hex()),
+		zap.Uint16("aztecChain", r.config.SourceChainID),
+		zap.Uint16("arbitrumChain", r.config.DestChainID),
+		zap.String("verificationServiceURL", r.config.VerificationServiceURL)) // ADD
+
+	// Create a wait group to track goroutines
+	var wg sync.WaitGroup
+
+	// Subscribe to VAAs
+	stream, err := r.spyClient.SubscribeSignedVAA(ctx)
+	if err != nil {
+		return fmt.Errorf("subscribe to VAA stream: %v", err)
+	}
+
+	r.logger.Info("Listening for VAAs")
+
+	// Create a separate context for graceful shutdown
+	processingCtx, cancelProcessing := context.WithCancel(context.Background())
+	defer cancelProcessing()
+
+	for {
+		select {
+		case <-ctx.Done():
+			r.logger.Info("Shutting down relayer")
+			// Cancel all processing
+			cancelProcessing()
+			// Wait for all processing goroutines to complete
+			r.logger.Info("Waiting for all VAA processing to complete")
+			wg.Wait()
+			r.logger.Info("Shutdown complete")
+			return nil
+		default:
+			// Receive the next VAA
+			resp, err := stream.Recv()
+			if err != nil {
+				r.logger.Warn("Stream error, retrying in 5s", zap.Error(err))
+				time.Sleep(5 * time.Second)
+				stream, err = r.spyClient.SubscribeSignedVAA(ctx)
+				if err != nil {
+					// Cancel all processing before returning
+					cancelProcessing()
+					// Wait for all processing goroutines to complete
+					wg.Wait()
+					return fmt.Errorf("subscribe to VAA stream after retry: %v", err)
+				}
+				continue
+			}
+
+			// Process the VAA in a goroutine, but track it with the WaitGroupp
+			wg.Add(1)
+			go func(vaaBytes []byte) {
+				defer wg.Done()
+				r.processVAA(processingCtx, vaaBytes)
+			}(resp.VaaBytes)
+		}
+	}
+}
+
+func (r *Relayer) processVAA(ctx context.Context, vaaBytes []byte) {
+	// Check for context cancellation first
+	select {
+	case <-ctx.Done():
+		r.logger.Debug("Processing cancelled for VAA")
+		return
+	default:
+		// Continue processing
+	}
+
+	// Parse the VAA
+	wormholeVAA, err := vaaLib.Unmarshal(vaaBytes)
+	if err != nil {
+		r.logger.Error("Failed to parse VAA", zap.Error(err))
+		return
+	}
+
+	// Extract the txID from the payload (first 32 bytes)
+	txID := ""
+	if len(wormholeVAA.Payload) >= 32 {
+		txIDBytes := wormholeVAA.Payload[:32]
+		txID = fmt.Sprintf("0x%x", txIDBytes)
+		r.logger.Debug("Extracted txID from payload", zap.String("txID", txID))
+	} else {
+		r.logger.Debug("Payload too short to contain txID", zap.Int("payload_length", len(wormholeVAA.Payload)))
+	}
+
+	// Create VAA data with essential information
+	vaaData := &VAAData{
+		VAA:        wormholeVAA,
+		RawBytes:   vaaBytes,
+		ChainID:    uint16(wormholeVAA.EmitterChain),
+		EmitterHex: fmt.Sprintf("%064x", wormholeVAA.EmitterAddress),
+		Sequence:   wormholeVAA.Sequence,
+		TxID:       txID,
+	}
+
+	r.logger.Info("Processing VAA",
+		zap.Uint16("chain", vaaData.ChainID),
+		zap.Uint64("sequence", vaaData.Sequence),
+		zap.String("emitter", vaaData.EmitterHex),
+		zap.String("sourceTxID", vaaData.TxID))
+
+	// Use the passed context when calling the processor
+	if err := r.vaaProcessor(r, vaaData); err != nil {
+		r.logger.Error("Error processing VAA", zap.Error(err))
+	}
+}
+
+// MODIFY: defaultVAAProcessor to use verification service for Arbitrum->Aztec
+func defaultVAAProcessor(r *Relayer, vaaData *VAAData) error {
+	// Create a context with timeout for processing operations
+	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) // Increased timeout for HTTP calls
+	defer cancel()
+
+	// Log essential VAA information
+	r.logger.Info("VAA Details",
+		zap.Uint16("emitterChain", vaaData.ChainID),
+		zap.String("emitterAddress", vaaData.EmitterHex),
+		zap.Uint64("sequence", vaaData.Sequence),
+		zap.Time("timestamp", vaaData.VAA.Timestamp),
+		zap.Int("payloadLength", len(vaaData.VAA.Payload)),
+		zap.String("sourceTxID", vaaData.TxID))
+
+	// Extract and log key payload information at debug level
+	r.logger.Debug("VAA Payload", zap.String("payloadHex", fmt.Sprintf("%x", vaaData.VAA.Payload)))
+
+	// Parse payload structure at debug level
+	if len(vaaData.VAA.Payload) >= 32 {
+		r.parseAndLogPayload(vaaData.VAA.Payload)
+	}
+
+	var txHash string
+	var err error
+	var direction string
+
+	// Check if this is a VAA from Aztec (source chain) -> send to Arbitrum
+	if vaaData.ChainID == r.config.SourceChainID {
+		direction = "Aztec->Arbitrum"
+
+		r.logger.Info("Processing VAA from Aztec to Arbitrum",
+			zap.Uint64("sequence", vaaData.Sequence),
+			zap.String("sourceTxID", vaaData.TxID))
+
+		// Send to Arbitrum using EVM client
+		txHash, err = r.evmClient.SendVerifyTransaction(ctx, r.config.ArbitrumTargetContract, vaaData.RawBytes)
+
+		// Check if this is a VAA from Arbitrum (dest chain) -> send to Aztec
+	} else if vaaData.ChainID == r.config.DestChainID {
+		direction = "Arbitrum->Aztec"
+
+		r.logger.Info("Processing VAA from Arbitrum to Aztec",
+			zap.Uint64("sequence", vaaData.Sequence),
+			zap.String("sourceTxID", vaaData.TxID))
+
+		// MODIFY: Try verification service first, fallback to direct PXE
+		txHash, err = r.verificationClient.VerifyVAA(ctx, vaaData.RawBytes)
+		if err != nil {
+			r.logger.Warn("Verification service failed, trying direct PXE", zap.Error(err))
+			// Fallback to direct PXE call
+			txHash, err = r.aztecClient.SendVerifyTransaction(ctx, r.config.AztecTargetContract, vaaData.RawBytes)
+		} else {
+			r.logger.Debug("Used verification service successfully")
+		}
+
+	} else {
+		// Skip VAAs not from our configured chains
+		r.logger.Debug("Skipping VAA (not from configured chains)",
+			zap.Uint64("sequence", vaaData.Sequence),
+			zap.Uint16("chain", vaaData.ChainID))
+		return nil
+	}
+
+	if err != nil {
+		// Check if the context was cancelled or timed out
+		if ctx.Err() != nil {
+			r.logger.Warn("Transaction sending cancelled or timed out", zap.Error(ctx.Err()))
+			return fmt.Errorf("transaction interrupted: %v", ctx.Err())
+		}
+
+		r.logger.Error("Failed to send verify transaction",
+			zap.String("direction", direction),
+			zap.Uint64("sequence", vaaData.Sequence),
+			zap.String("sourceTxID", vaaData.TxID),
+			zap.Error(err))
+		return fmt.Errorf("transaction failed: %v", err)
+	}
+
+	r.logger.Info("VAA verification completed",
+		zap.String("direction", direction),
+		zap.Uint64("sequence", vaaData.Sequence),
+		zap.String("txHash", txHash),
+		zap.String("sourceTxID", vaaData.TxID))
+
+	return nil
+}
+
+// parseAndLogPayload parses and logs payload structure at debug level
+func (r *Relayer) parseAndLogPayload(payload []byte) {
+	const txIDOffset = 32
+	const arraySize = 31
+
+	// Log the transaction ID from the first 32 bytes
+	if len(payload) >= 32 {
+		txIDBytes := payload[:32]
+		r.logger.Debug("Source Transaction ID", zap.String("txID", fmt.Sprintf("0x%x", txIDBytes)))
+	}
+
+	// Parse payload arrays (skip the txID)
+	for i := txIDOffset; i < len(payload); i += arraySize {
+		end := i + arraySize
+		if end > len(payload) {
+			end = len(payload)
+		}
+
+		arrayIndex := (i - txIDOffset) / arraySize
+		r.logger.Debug(fmt.Sprintf("Payload array %d", arrayIndex),
+			zap.String("hex", fmt.Sprintf("0x%x", payload[i:end])))
+
+		// Parse specific fields at debug level
+		switch arrayIndex {
+		case 0:
+			if i+20 <= end {
+				r.logger.Debug("Address", zap.String("address", fmt.Sprintf("0x%x", payload[i:i+20])))
+			}
+		case 1:
+			if i+2 <= end {
+				chainIDLower := uint16(payload[i])
+				chainIDUpper := uint16(payload[i+1])
+				chainID := (chainIDUpper << 8) | chainIDLower
+				r.logger.Debug("Chain ID", zap.Uint16("chainID", chainID))
+			}
+		case 2:
+			if i < end {
+				amount := uint64(payload[i])
+				r.logger.Debug("Amount", zap.Uint64("amount", amount))
+			}
+		}
+	}
+}
+
+// Environment variable helpers
+func getEnvOrDefault(key, defaultValue string) string {
+	val, exists := os.LookupEnv(key)
+	if !exists {
+		return defaultValue
+	}
+	return val
+}
+
+func getEnvIntOrDefault(key string, defaultValue int) int {
+	val, exists := os.LookupEnv(key)
+	if !exists {
+		return defaultValue
+	}
+
+	result, err := strconv.Atoi(val)
+	if err != nil {
+		logger.Warn("Invalid environment variable value, using default",
+			zap.String("key", key),
+			zap.Int("default", defaultValue))
+		return defaultValue
+	}
+	return result
+}
+
+func main() {
+	// Initialize the logger first
+	initLogger()
+	defer logger.Sync()
+
+	logger.Info("Starting bidirectional Aztec-Arbitrum Wormhole relayer")
+
+	// Load configuration from environment
+	config := NewConfigFromEnv()
+
+	logger.Info("DEBUG: Config loaded",
+		zap.Uint16("sourceChainID", config.SourceChainID),
+		zap.Uint16("destChainID", config.DestChainID))
+
+	// Create relayer
+	relayer, err := NewRelayer(config)
+	if err != nil {
+		logger.Fatal("Failed to initialize relayer", zap.Error(err))
+	}
+	defer relayer.Close()
+
+	// Setup context with cancellation
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	// Handle graceful shutdown
+	c := make(chan os.Signal, 1)
+	signal.Notify(c, os.Interrupt, syscall.SIGTERM)
+	go func() {
+		<-c
+		logger.Info("Received shutdown signal")
+		cancel()
+	}()
+
+	// Start the relayer
+	if err := relayer.Start(ctx); err != nil {
+		logger.Fatal("Relayer stopped with error", zap.Error(err))
+	}
+}

+ 1 - 0
sdk/vaa/structs.go

@@ -299,6 +299,7 @@ const (
 	ChainIDMezo ChainID = 50
 	ChainIDMezo ChainID = 50
 	// ChainIDFogo is the ChainID of Fogo
 	// ChainIDFogo is the ChainID of Fogo
 	ChainIDFogo ChainID = 51
 	ChainIDFogo ChainID = 51
+	// ChainIDWormchain is the ChainID of Wormchain and it is in it's own range.
 	// ChainIDSonic is the ChainID of Sonic
 	// ChainIDSonic is the ChainID of Sonic
 	ChainIDSonic ChainID = 52
 	ChainIDSonic ChainID = 52
 	// ChainIDConverge is the ChainID of Converge
 	// ChainIDConverge is the ChainID of Converge

Some files were not shown because too many files changed in this diff