From ba7c9225a68236996271e5330e375ff6c2ca435c Mon Sep 17 00:00:00 2001 From: angelorc Date: Thu, 19 Feb 2026 15:14:00 +0100 Subject: [PATCH 1/5] feat(hyperlane): integrate hyperlane-cosmos v1.1.0 bridge modules - Add Hyperlane Core and Warp modules to enable cross-chain messaging and token bridging between BitSong and EVM chains - Register hyperlane and warp store keys, module accounts, and keepers - Wire WarpKeeper with CoreKeeper dependency for mailbox access - Add begin/end/init blocker ordering after IBC modules - Grant Minter+Burner permissions to warp module for synthetic tokens - Add hyperlane-cosmos v1.1.0 dependency --- .gitignore | 15 ++++++++++++++- app/keepers/keepers.go | 34 ++++++++++++++++++++++++++++++++++ app/keepers/keys.go | 4 ++++ app/modules.go | 16 ++++++++++++++-- go.mod | 7 +++++-- go.sum | 19 ++++++++++++++----- 6 files changed, 85 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 208c93b8..5dfd9f71 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,17 @@ state_export.json github.com* gogoproto* -target/ \ No newline at end of file +target/ + +# Local devnet data +.localbitsong-hyperlane/ + +# EVM deployment artifacts +evm/out/ +evm/cache/ +evm/broadcast/ +evm/node_modules/ + +# Secrets (never commit) +scripts/hyperlane/.env +infra/.env.secrets \ No newline at end of file diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index 219868f9..f1798372 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -44,6 +44,10 @@ import ( distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + hyperlanekeeper "github.com/bcp-innovations/hyperlane-cosmos/x/core/keeper" + hyperlanetypes "github.com/bcp-innovations/hyperlane-cosmos/x/core/types" + warpkeeper "github.com/bcp-innovations/hyperlane-cosmos/x/warp/keeper" + warptypes "github.com/bcp-innovations/hyperlane-cosmos/x/warp/types" cadencekeeper "github.com/bitsongofficial/go-bitsong/x/cadence/keeper" cadencetypes "github.com/bitsongofficial/go-bitsong/x/cadence/types" govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" @@ -116,6 +120,8 @@ var maccPerms = map[string][]string{ wasmtypes.ModuleName: {authtypes.Burner}, protocolpooltypes.ModuleName: nil, protocolpooltypes.ProtocolPoolEscrowAccount: nil, + hyperlanetypes.ModuleName: nil, // Core module account (IGP gas payments) + warptypes.ModuleName: {authtypes.Minter, authtypes.Burner}, // Warp needs mint/burn for synthetic tokens } type AppKeepers struct { @@ -160,6 +166,10 @@ type AppKeepers struct { AuthenticatorManager *authenticator.AuthenticatorManager ProtocolPoolKeeper protocolpoolkeeper.Keeper + // Hyperlane keepers + HyperlaneKeeper *hyperlanekeeper.Keeper + WarpKeeper warpkeeper.Keeper + // Middleware wrapper Ics20WasmHooks *ibchooks.WasmHooks HooksICS4Wrapper ibchooks.ICS4Middleware @@ -245,6 +255,30 @@ func NewAppKeepers( govModAddress, bApp.Logger(), ) + // Hyperlane Core Keeper + hyperlaneKeeper := hyperlanekeeper.NewKeeper( + appCodec, + appKeepers.AccountKeeper.AddressCodec(), + runtime.NewKVStoreService(keys[hyperlanetypes.ModuleName]), + govModAddress, + appKeepers.BankKeeper, + ) + appKeepers.HyperlaneKeeper = &hyperlaneKeeper + + // Hyperlane Warp Keeper + appKeepers.WarpKeeper = warpkeeper.NewKeeper( + appCodec, + appKeepers.AccountKeeper.AddressCodec(), + runtime.NewKVStoreService(keys[warptypes.ModuleName]), + govModAddress, + appKeepers.BankKeeper, + appKeepers.HyperlaneKeeper, // *Keeper satisfies types.CoreKeeper interface + []int32{ + int32(warptypes.HYP_TOKEN_TYPE_COLLATERAL), + int32(warptypes.HYP_TOKEN_TYPE_SYNTHETIC), + }, + ) + // Initialize authenticators appKeepers.AuthenticatorManager = authenticator.NewAuthenticatorManager() appKeepers.AuthenticatorManager.InitializeAuthenticators([]authenticator.Authenticator{ diff --git a/app/keepers/keys.go b/app/keepers/keys.go index 38128e93..11f653c0 100644 --- a/app/keepers/keys.go +++ b/app/keepers/keys.go @@ -5,6 +5,8 @@ import ( "cosmossdk.io/x/feegrant" wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + hyperlanetypes "github.com/bcp-innovations/hyperlane-cosmos/x/core/types" + warptypes "github.com/bcp-innovations/hyperlane-cosmos/x/warp/types" cadencetypes "github.com/bitsongofficial/go-bitsong/x/cadence/types" smartaccounttypes "github.com/bitsongofficial/go-bitsong/x/smart-account/types" authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" @@ -60,6 +62,8 @@ func (appKeepers *AppKeepers) GenerateKeys() { cadencetypes.StoreKey, smartaccounttypes.StoreKey, protocolpooltypes.StoreKey, + hyperlanetypes.ModuleName, // "hyperlane" — no StoreKey constant exported, ModuleName is the store key + warptypes.ModuleName, // "warp" — no StoreKey constant exported, ModuleName is the store key ) appKeepers.tkeys = storetypes.NewTransientStoreKeys(paramstypes.TStoreKey) diff --git a/app/modules.go b/app/modules.go index 9d368444..def03d2b 100644 --- a/app/modules.go +++ b/app/modules.go @@ -8,6 +8,10 @@ import ( "cosmossdk.io/x/upgrade" upgradetypes "cosmossdk.io/x/upgrade/types" "github.com/CosmWasm/wasmd/x/wasm" + hyperlane "github.com/bcp-innovations/hyperlane-cosmos/x/core" + hyperlanetypes "github.com/bcp-innovations/hyperlane-cosmos/x/core/types" + warp "github.com/bcp-innovations/hyperlane-cosmos/x/warp" + warptypes "github.com/bcp-innovations/hyperlane-cosmos/x/warp/types" encparams "github.com/bitsongofficial/go-bitsong/app/params" "github.com/bitsongofficial/go-bitsong/x/cadence" "github.com/cosmos/cosmos-sdk/client" @@ -109,6 +113,8 @@ var AppModuleBasics = module.NewBasicManager( ibcwasm.AppModuleBasic{}, smartaccount.AppModuleBasic{}, protocolpool.AppModule{}, + hyperlane.AppModule{}, + warp.AppModule{}, ) func appModules( @@ -149,6 +155,8 @@ func appModules( cadence.NewAppModule(appCodec, app.CadenceKeeper), protocolpool.NewAppModule(app.ProtocolPoolKeeper, app.AccountKeeper, app.BankKeeper), smartaccount.NewAppModule(appCodec, *app.SmartAccountKeeper), + hyperlane.NewAppModule(appCodec, app.HyperlaneKeeper), + warp.NewAppModule(appCodec, app.WarpKeeper), crisis.NewAppModule(app.CrisisKeeper, skipGenesisInvariants, app.GetSubspace(crisistypes.ModuleName)), // always be last to make sure that it checks for all invariants and not only part of them } } @@ -158,7 +166,8 @@ func orderBeginBlockers() []string { capabilitytypes.ModuleName, minttypes.ModuleName, authtypes.ModuleName, banktypes.ModuleName, distrtypes.ModuleName, protocolpooltypes.ModuleName, slashingtypes.ModuleName, govtypes.ModuleName, crisistypes.ModuleName, stakingtypes.ModuleName, ibctransfertypes.ModuleName, ibcexported.ModuleName, packetforwardtypes.ModuleName, - icqtypes.ModuleName, authz.ModuleName, genutiltypes.ModuleName, evidencetypes.ModuleName, wasmtypes.ModuleName, + icqtypes.ModuleName, hyperlanetypes.ModuleName, warptypes.ModuleName, + authz.ModuleName, genutiltypes.ModuleName, evidencetypes.ModuleName, wasmtypes.ModuleName, feegrant.ModuleName, paramstypes.ModuleName, vestingtypes.ModuleName, cadencetypes.ModuleName, ibchookstypes.ModuleName, ibcwasmtypes.ModuleName, fantokentypes.ModuleName, } @@ -167,7 +176,8 @@ func orderBeginBlockers() []string { func orderEndBlockers() []string { return []string{ crisistypes.ModuleName, govtypes.ModuleName, stakingtypes.ModuleName, ibctransfertypes.ModuleName, ibcexported.ModuleName, - packetforwardtypes.ModuleName, icqtypes.ModuleName, feegrant.ModuleName, authz.ModuleName, capabilitytypes.ModuleName, authtypes.ModuleName, + packetforwardtypes.ModuleName, icqtypes.ModuleName, hyperlanetypes.ModuleName, warptypes.ModuleName, + feegrant.ModuleName, authz.ModuleName, capabilitytypes.ModuleName, authtypes.ModuleName, protocolpooltypes.ModuleName, // must be before bank banktypes.ModuleName, distrtypes.ModuleName, slashingtypes.ModuleName, minttypes.ModuleName, genutiltypes.ModuleName, wasmtypes.ModuleName, evidencetypes.ModuleName, paramstypes.ModuleName, upgradetypes.ModuleName, vestingtypes.ModuleName, cadencetypes.ModuleName, @@ -200,6 +210,8 @@ func orderInitBlockers() []string { paramstypes.ModuleName, upgradetypes.ModuleName, vestingtypes.ModuleName, + hyperlanetypes.ModuleName, + warptypes.ModuleName, wasmtypes.ModuleName, ibcwasmtypes.ModuleName, ibchookstypes.ModuleName, diff --git a/go.mod b/go.mod index 1c57085d..1d961e24 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.23.2 require ( cosmossdk.io/api v0.9.2 - cosmossdk.io/client/v2 v2.0.0-beta.3 + cosmossdk.io/client/v2 v2.0.0-beta.8 cosmossdk.io/collections v1.2.0 cosmossdk.io/core v0.11.3 cosmossdk.io/errors v1.0.2 @@ -17,6 +17,7 @@ require ( cosmossdk.io/x/upgrade v0.1.4 github.com/CosmWasm/wasmd v0.53.3 github.com/CosmWasm/wasmvm/v2 v2.1.6 + github.com/bcp-innovations/hyperlane-cosmos v1.1.0 github.com/cometbft/cometbft v0.38.21 github.com/cosmos/cosmos-db v1.1.1 github.com/cosmos/cosmos-proto v1.0.0-beta.5 @@ -107,6 +108,7 @@ require ( github.com/emicklei/dot v1.6.4 // indirect github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect + github.com/ethereum/go-ethereum v1.14.12 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -121,7 +123,7 @@ require ( github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect - github.com/golang/snappy v0.0.4 // indirect + github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/google/btree v1.1.3 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/go-cmp v0.7.0 // indirect @@ -146,6 +148,7 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect + github.com/holiman/uint256 v1.3.1 // indirect github.com/huandu/skiplist v1.2.1 // indirect github.com/iancoleman/orderedmap v0.3.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect diff --git a/go.sum b/go.sum index a926c24b..8f413efa 100644 --- a/go.sum +++ b/go.sum @@ -616,8 +616,8 @@ cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= cosmossdk.io/api v0.9.2 h1:9i9ptOBdmoIEVEVWLtYYHjxZonlF/aOVODLFaxpmNtg= cosmossdk.io/api v0.9.2/go.mod h1:CWt31nVohvoPMTlPv+mMNCtC0a7BqRdESjCsstHcTkU= -cosmossdk.io/client/v2 v2.0.0-beta.3 h1:+TTuH0DwQYsUq2JFAl3fDZzKq5gQG7nt3dAattkjFDU= -cosmossdk.io/client/v2 v2.0.0-beta.3/go.mod h1:CZcL41HpJPOOayTCO28j8weNBQprG+SRiKX39votypo= +cosmossdk.io/client/v2 v2.0.0-beta.8 h1:RXMJdA4V9H1H3/3BfMD6dAW3lF8W9DpNPPYnKD+ArxY= +cosmossdk.io/client/v2 v2.0.0-beta.8/go.mod h1:x+E2eji+ToMtUIqKzoJ5mJIhat+Zak47xZ8jOYjJQBA= cosmossdk.io/collections v1.2.0 h1:IesfVG8G/+FYCMVMP01frS/Cw99Omk5vBh3cHbO01Gg= cosmossdk.io/collections v1.2.0/go.mod h1:4NkMoYw6qRA8fnSH/yn1D/MOutr8qyQnwsO50Mz9ItU= cosmossdk.io/core v0.11.3 h1:mei+MVDJOwIjIniaKelE3jPDqShCc/F4LkNNHh+4yfo= @@ -715,6 +715,8 @@ github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/bcp-innovations/hyperlane-cosmos v1.1.0 h1:WXt+WrKv2DG/xVIkLvggDRbi/2law104Vj6AWZGxHNw= +github.com/bcp-innovations/hyperlane-cosmos v1.1.0/go.mod h1:NP59yKAk2qFaT7+FSCh7kkoKKLlTxXNdIlxMstAJ5no= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -729,8 +731,8 @@ github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCk github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= -github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= +github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c= github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= @@ -917,6 +919,8 @@ github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0+ github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/ethereum/go-ethereum v1.14.12 h1:8hl57x77HSUo+cXExrURjU/w1VhL+ShCTJrTwcCQSe4= +github.com/ethereum/go-ethereum v1.14.12/go.mod h1:RAC2gVMWJ6FkxSPESfbshrcKpIokgQKsVKmAuqdekDY= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= @@ -1045,8 +1049,9 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= @@ -1204,6 +1209,8 @@ github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8 github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= +github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs= +github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c= github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= @@ -1367,6 +1374,8 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108 github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.7.0 h1:/XxtEV3I3Eif/HobnVx9YmJgk8ENdRsuUmM+fLCFNow= +github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= From 72315d0aa877fb09e67b2f53775aac74a0045a49 Mon Sep 17 00:00:00 2001 From: angelorc Date: Thu, 19 Feb 2026 15:16:46 +0100 Subject: [PATCH 2/5] refactor(keepers): clean up module account permissions and formatting --- .gitignore | 3 --- app/keepers/keepers.go | 6 +++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 5dfd9f71..585c0f7d 100644 --- a/.gitignore +++ b/.gitignore @@ -54,15 +54,12 @@ github.com* gogoproto* target/ -# Local devnet data .localbitsong-hyperlane/ -# EVM deployment artifacts evm/out/ evm/cache/ evm/broadcast/ evm/node_modules/ -# Secrets (never commit) scripts/hyperlane/.env infra/.env.secrets \ No newline at end of file diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index f1798372..eea32cc9 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -120,8 +120,8 @@ var maccPerms = map[string][]string{ wasmtypes.ModuleName: {authtypes.Burner}, protocolpooltypes.ModuleName: nil, protocolpooltypes.ProtocolPoolEscrowAccount: nil, - hyperlanetypes.ModuleName: nil, // Core module account (IGP gas payments) - warptypes.ModuleName: {authtypes.Minter, authtypes.Burner}, // Warp needs mint/burn for synthetic tokens + hyperlanetypes.ModuleName: nil, + warptypes.ModuleName: {authtypes.Minter, authtypes.Burner}, } type AppKeepers struct { @@ -272,7 +272,7 @@ func NewAppKeepers( runtime.NewKVStoreService(keys[warptypes.ModuleName]), govModAddress, appKeepers.BankKeeper, - appKeepers.HyperlaneKeeper, // *Keeper satisfies types.CoreKeeper interface + appKeepers.HyperlaneKeeper, []int32{ int32(warptypes.HYP_TOKEN_TYPE_COLLATERAL), int32(warptypes.HYP_TOKEN_TYPE_SYNTHETIC), From 9b246ba9bd59721d8c14aa9462b8dba24a339b3e Mon Sep 17 00:00:00 2001 From: angelorc Date: Thu, 19 Feb 2026 15:43:59 +0100 Subject: [PATCH 3/5] feat(hyperlane): add hyperlane scripts --- scripts/hyperlane/.env.example | 15 + scripts/hyperlane/01-chain.sh | 168 ++++++++++ scripts/hyperlane/02-hyperlane.sh | 224 ++++++++++++++ scripts/hyperlane/03-evm-deploy.sh | 270 ++++++++++++++++ scripts/hyperlane/04-agents.sh | 474 +++++++++++++++++++++++++++++ scripts/hyperlane/05-test.sh | 349 +++++++++++++++++++++ scripts/hyperlane/lib.sh | 279 +++++++++++++++++ scripts/hyperlane/run-all.sh | 65 ++++ scripts/hyperlane/status.sh | 383 +++++++++++++++++++++++ scripts/hyperlane/stop.sh | 76 +++++ 10 files changed, 2303 insertions(+) create mode 100644 scripts/hyperlane/.env.example create mode 100755 scripts/hyperlane/01-chain.sh create mode 100755 scripts/hyperlane/02-hyperlane.sh create mode 100755 scripts/hyperlane/03-evm-deploy.sh create mode 100755 scripts/hyperlane/04-agents.sh create mode 100755 scripts/hyperlane/05-test.sh create mode 100755 scripts/hyperlane/lib.sh create mode 100755 scripts/hyperlane/run-all.sh create mode 100755 scripts/hyperlane/status.sh create mode 100755 scripts/hyperlane/stop.sh diff --git a/scripts/hyperlane/.env.example b/scripts/hyperlane/.env.example new file mode 100644 index 00000000..46b824b1 --- /dev/null +++ b/scripts/hyperlane/.env.example @@ -0,0 +1,15 @@ +# ============================================================================= +# Copy to .env and fill in real values: +# cp .env.example .env +# +# .env is gitignored +# ============================================================================= + +# BitSong validator mnemonic (coin-type 639 — derives bitsong1... address) +VAL_MNEMONIC="your 24-word mnemonic here" + +# EVM / Hyperlane agent keys (hex, 0x-prefixed) +HYP_KEY=0x... +VALIDATOR_KEY=0x... +COSMOS_SIGNER_KEY=$VALIDATOR_KEY +EVM_RELAYER_KEY=$HYP_KEY diff --git a/scripts/hyperlane/01-chain.sh b/scripts/hyperlane/01-chain.sh new file mode 100755 index 00000000..8e8e6bc9 --- /dev/null +++ b/scripts/hyperlane/01-chain.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# ============================================================================= +# 01-chain.sh — Initialize and start a single-validator BitSong localnet +# +# Usage: +# bash 01-chain.sh # Init (if needed) + start +# bash 01-chain.sh --clean # Wipe state and re-init +# ============================================================================= + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib.sh" + +# Test accounts (from tests/localbitsong/scripts/setup.sh) +TEST_ADDRESSES=( + "bitsong1regz7kj3ylg2dn9rl8vwrhclkgz528mf0tfsck" + "bitsong1hvrhhex6wfxh7r7nnc3y39p0qlmff6v9t5rc25" + "bitsong175vgzztymvvcxvqun54nlu9dq6856thgvyl5sa" + "bitsong1t8nznzj4sd6zzutwdmslgy4dcxyd2jafz7822x" + "bitsong14vdrvstsffj8mq5e4fhm6y2hpfxtedajczsj5d" + "bitsong1vwe5hay74v0vhuzdhadteyqfasu5d7tdf83pyy" + "bitsong16866dezn6ez2qpmpcrrv9cyud8v8c7ufnzwhhh" + "bitsong1tlwh75lvu35nw9vcg557mxhspz5s88t6vzscd8" + "bitsong16z9wj8n5f3zgzwspw0r9sj9v7k7hdasqj95us9" + "bitsong1gulaxnca7rped0grw0lz4h4zy0xn3ttvmlad8x" +) +GENESIS_AMOUNT="10000000000000${DENOM}" +STAKE_AMOUNT="5000000000000${DENOM}" + +# ─── Init ──────────────────────────────────────────────────────────────────── + +init_chain() { + log_step "Initializing Chain" + + log "Initializing chain (chain-id=$CHAIN_ID)..." + echo "$VAL_MNEMONIC" | "$BINARY" init "$MONIKER" \ + --chain-id="$CHAIN_ID" --home="$BITSONG_HOME" --recover -o > /dev/null 2>&1 + + # Genesis modifications + log "Modifying genesis.json..." + local genesis="$BITSONG_HOME/config/genesis.json" + local tmp="${genesis}.tmp" + jq ' + .app_state.staking.params.bond_denom = "ubtsg" | + .app_state.staking.params.unbonding_time = "240s" | + .app_state.crisis.constant_fee.denom = "ubtsg" | + .app_state.gov.params.voting_period = "60s" | + .app_state.gov.params.expedited_voting_period = "30s" | + .app_state.gov.params.min_deposit[0].denom = "ubtsg" | + .app_state.gov.params.expedited_min_deposit[0].denom = "ubtsg" | + .app_state.mint.params.mint_denom = "ubtsg" | + .app_state.bank.denom_metadata = [{ + "description": "Registered denom ubtsg for localbitsong-hyperlane testing", + "denom_units": [{"denom": "ubtsg", "exponent": 0}], + "base": "ubtsg", "display": "ubtsg", "name": "ubtsg", "symbol": "ubtsg" + }] + ' "$genesis" > "$tmp" && mv "$tmp" "$genesis" + log_ok "Genesis modified" + + # Keys + accounts + log "Adding validator key (coin-type 639)..." + echo "$VAL_MNEMONIC" | "$BINARY" keys add "$KEY_NAME" \ + --recover --coin-type 639 --keyring-backend="$KEYRING_BACKEND" --home="$BITSONG_HOME" > /dev/null 2>&1 + + # Derive VAL_ADDRESS from the just-created key + VAL_ADDRESS=$("$BINARY" keys show "$KEY_NAME" \ + --keyring-backend "$KEYRING_BACKEND" --home "$BITSONG_HOME" -a 2>/dev/null) + [[ -n "$VAL_ADDRESS" ]] || { log_err "Failed to derive VAL_ADDRESS from keyring"; return 1; } + log_ok "Validator address: $VAL_ADDRESS" + + log "Adding genesis accounts..." + "$BINARY" genesis add-genesis-account "$VAL_ADDRESS" "$GENESIS_AMOUNT" \ + --home="$BITSONG_HOME" > /dev/null 2>&1 + for addr in "${TEST_ADDRESSES[@]}"; do + "$BINARY" genesis add-genesis-account "$addr" "$GENESIS_AMOUNT" \ + --home="$BITSONG_HOME" > /dev/null 2>&1 + done + log_ok "Added $(( ${#TEST_ADDRESSES[@]} + 1 )) genesis accounts" + + # Gentx + log "Creating gentx..." + "$BINARY" genesis gentx "$KEY_NAME" "$STAKE_AMOUNT" \ + --keyring-backend="$KEYRING_BACKEND" --chain-id="$CHAIN_ID" \ + --home="$BITSONG_HOME" > /dev/null 2>&1 + "$BINARY" genesis collect-gentxs --home="$BITSONG_HOME" > /dev/null 2>&1 + "$BINARY" genesis validate-genesis --home="$BITSONG_HOME" > /dev/null 2>&1 + log_ok "Genesis validated" + + # Config tweaks + log "Modifying config.toml + app.toml..." + local config="$BITSONG_HOME/config/config.toml" + local app_toml="$BITSONG_HOME/config/app.toml" + sed -i 's/seeds = ".*"/seeds = ""/' "$config" + sed -i 's|laddr = "tcp://127.0.0.1:26657"|laddr = "tcp://0.0.0.0:26657"|' "$config" + sed -i 's/cors_allowed_origins = \[\]/cors_allowed_origins = ["*"]/' "$config" + sed -i '/^\[api\]/,/^\[/{s/^enable = false/enable = true/}' "$app_toml" + sed -i 's/^swagger = false/swagger = true/' "$app_toml" + sed -i 's/^enabled-unsafe-cors = false/enabled-unsafe-cors = true/' "$app_toml" + sed -i 's/^minimum-gas-prices = ".*"/minimum-gas-prices = "0ubtsg"/' "$app_toml" + log_ok "Config files modified" +} + +# ─── Start ─────────────────────────────────────────────────────────────────── + +start_chain() { + log_step "Starting Chain" + + if [[ -f "$BITSONG_HOME/chain.pid" ]]; then + local pid; pid=$(cat "$BITSONG_HOME/chain.pid") + if kill -0 "$pid" 2>/dev/null; then + log_ok "Chain already running (PID=$pid)" + wait_for_block; return 0 + fi + log_warn "Stale PID file, removing" + rm -f "$BITSONG_HOME/chain.pid" + fi + + log "Starting bitsongd (logs: $BITSONG_HOME/chain.log)..." + "$BINARY" start --home "$BITSONG_HOME" > "$BITSONG_HOME/chain.log" 2>&1 & + local chain_pid=$!; disown "$chain_pid" + echo "$chain_pid" > "$BITSONG_HOME/chain.pid" + + sleep 3 + if ! kill -0 "$chain_pid" 2>/dev/null; then + log_err "Chain died immediately. Last 20 lines:" + tail -20 "$BITSONG_HOME/chain.log" || true + return 1 + fi + + log_ok "Chain started (PID=$chain_pid)" + wait_for_block +} + +# ─── Main ──────────────────────────────────────────────────────────────────── + +CLEAN=false +while [[ $# -gt 0 ]]; do + case "$1" in + --clean) CLEAN=true; shift ;; + -h|--help) + echo "Usage: $0 [--clean]" + echo " --clean Remove existing chain data before starting" + exit 0 ;; + *) echo "Unknown flag: $1"; exit 1 ;; + esac +done + +require_binary +require_jq +[[ -n "$VAL_MNEMONIC" ]] || { log_err "Set VAL_MNEMONIC in scripts/hyperlane/.env"; exit 1; } + +banner "BitSong Localnet (Hyperlane)" "Chain: $CHAIN_ID" + +if [[ "$CLEAN" == "true" ]]; then + log "Cleaning previous state at $BITSONG_HOME..." + if [[ -f "$BITSONG_HOME/chain.pid" ]]; then + kill "$(cat "$BITSONG_HOME/chain.pid")" 2>/dev/null || true; sleep 2 + fi + rm -rf "$BITSONG_HOME" + log_ok "Clean complete" +fi + +if [[ ! -d "$BITSONG_HOME/config" ]]; then + init_chain +else + log_ok "Chain already initialized at $BITSONG_HOME" +fi + +start_chain +log_ok "Chain ready!" diff --git a/scripts/hyperlane/02-hyperlane.sh b/scripts/hyperlane/02-hyperlane.sh new file mode 100755 index 00000000..00bc7664 --- /dev/null +++ b/scripts/hyperlane/02-hyperlane.sh @@ -0,0 +1,224 @@ +#!/usr/bin/env bash +# ============================================================================= +# 02-hyperlane.sh — Configure Hyperlane bridge on the local chain (9 txs) +# +# Usage: +# bash 02-hyperlane.sh # Full 9-step setup +# bash 02-hyperlane.sh --update-enrollment 0xABC...DEF # Re-enroll with real EVM addr +# ============================================================================= + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib.sh" + +# EVM_RECEIVER_CONTRACT is only used if set externally (not enrolled with zero placeholder) +EVM_RECEIVER_CONTRACT="${EVM_RECEIVER_CONTRACT:-}" + +# ─── 9-Step Hyperlane Setup ────────────────────────────────────────────────── + +setup_hyperlane() { + log_step "Configuring Hyperlane Bridge (9 Steps)" + + local multisig_ism_id routing_ism_id mailbox_id merkle_hook_id igp_id token_id + + # Step 1: MultisigISM + multisig_ism_id=$(load_state "multisig_ism_id") + if [[ -n "$multisig_ism_id" ]]; then + log_ok "1/9: MultisigISM exists ($multisig_ism_id)" + else + submit_tx "1/9: Create MessageIdMultisigISM" \ + "$BINARY" tx hyperlane ism create-message-id-multisig \ + "$VALIDATOR_ADDRESSES" "$VALIDATOR_THRESHOLD" + multisig_ism_id=$(extract_id) + [[ -n "$multisig_ism_id" ]] || { log_err "Failed to extract MultisigISM ID"; return 1; } + save_state "multisig_ism_id" "$multisig_ism_id" + log_ok "1/9: MultisigISM = $multisig_ism_id" + fi + + # Step 2: RoutingISM + routing_ism_id=$(load_state "routing_ism_id") + if [[ -n "$routing_ism_id" ]]; then + log_ok "2/9: RoutingISM exists ($routing_ism_id)" + else + submit_tx "2/9: Create RoutingISM" \ + "$BINARY" tx hyperlane ism create-routing \ + --routes="[{\"domain\":${REMOTE_DOMAIN},\"ism\":\"${multisig_ism_id}\"}]" + routing_ism_id=$(extract_id) + [[ -n "$routing_ism_id" ]] || { log_err "Failed to extract RoutingISM ID"; return 1; } + save_state "routing_ism_id" "$routing_ism_id" + log_ok "2/9: RoutingISM = $routing_ism_id" + fi + + # Step 3: Mailbox + mailbox_id=$(load_state "mailbox_id") + if [[ -n "$mailbox_id" ]]; then + log_ok "3/9: Mailbox exists ($mailbox_id)" + else + submit_tx "3/9: Create Mailbox (domain=$DOMAIN_ID)" \ + "$BINARY" tx hyperlane mailbox create "$routing_ism_id" "$DOMAIN_ID" + mailbox_id=$(extract_id) + [[ -n "$mailbox_id" ]] || { log_err "Failed to extract Mailbox ID"; return 1; } + save_state "mailbox_id" "$mailbox_id" + log_ok "3/9: Mailbox = $mailbox_id" + fi + + # Step 4: MerkleTreeHook + merkle_hook_id=$(load_state "merkle_hook_id") + if [[ -n "$merkle_hook_id" ]]; then + log_ok "4/9: MerkleTreeHook exists ($merkle_hook_id)" + else + submit_tx "4/9: Create MerkleTreeHook" \ + "$BINARY" tx hyperlane hooks merkle create "$mailbox_id" + merkle_hook_id=$(extract_id) + [[ -n "$merkle_hook_id" ]] || { log_err "Failed to extract MerkleTreeHook ID"; return 1; } + save_state "merkle_hook_id" "$merkle_hook_id" + log_ok "4/9: MerkleTreeHook = $merkle_hook_id" + fi + + # Step 5: IGP + igp_id=$(load_state "igp_id") + if [[ -n "$igp_id" ]]; then + log_ok "5/9: IGP exists ($igp_id)" + else + submit_tx "5/9: Create IGP (denom=$DENOM)" \ + "$BINARY" tx hyperlane hooks igp create "$DENOM" + igp_id=$(extract_id) + [[ -n "$igp_id" ]] || { log_err "Failed to extract IGP ID"; return 1; } + save_state "igp_id" "$igp_id" + log_ok "5/9: IGP = $igp_id" + fi + + # Step 6: IGP Gas Oracle + if [[ "$(load_state "igp_configured")" == "true" ]]; then + log_ok "6/9: IGP gas config already set" + else + submit_tx "6/9: Set IGP destination gas config (remote=$REMOTE_DOMAIN)" \ + "$BINARY" tx hyperlane hooks igp set-destination-gas-config \ + "$igp_id" "$REMOTE_DOMAIN" "$TOKEN_EXCHANGE_RATE" "$GAS_PRICE" "$GAS_OVERHEAD" + save_state "igp_configured" "true" + log_ok "6/9: IGP gas config set for domain $REMOTE_DOMAIN" + fi + + # Step 7: Set Mailbox Hooks + if [[ "$(load_state "mailbox_updated")" == "true" ]]; then + log_ok "7/9: Mailbox hooks already set" + else + submit_tx "7/9: Set Mailbox hooks (default=IGP, required=MerkleTree)" \ + "$BINARY" tx hyperlane mailbox set "$mailbox_id" \ + --default-hook="$igp_id" --required-hook="$merkle_hook_id" + save_state "mailbox_updated" "true" + log_ok "7/9: Mailbox updated with hooks" + fi + + # Step 8: Collateral Token + token_id=$(load_state "token_id") + if [[ -n "$token_id" ]]; then + log_ok "8/9: Collateral token exists ($token_id)" + else + submit_tx "8/9: Create collateral token ($DENOM)" \ + "$BINARY" tx warp create-collateral-token "$mailbox_id" "$DENOM" + token_id=$(extract_id) + [[ -n "$token_id" ]] || { log_err "Failed to extract Token ID"; return 1; } + save_state "token_id" "$token_id" + log_ok "8/9: Token = $token_id" + fi + + # Step 9: Enroll Remote Router (only if real EVM address is available) + if [[ "$(load_state "router_enrolled")" == "true" ]]; then + log_ok "9/9: Remote router already enrolled" + elif [[ -n "$EVM_RECEIVER_CONTRACT" ]]; then + submit_tx "9/9: Enroll remote router (domain=$REMOTE_DOMAIN)" \ + "$BINARY" tx warp enroll-remote-router \ + "$token_id" "$REMOTE_DOMAIN" "$EVM_RECEIVER_CONTRACT" "$ENROLL_GAS" + save_state "router_enrolled" "true" + log_ok "9/9: Remote router enrolled for domain $REMOTE_DOMAIN" + else + log_warn "9/9: Skipped — no EVM address yet. Run 03-evm-deploy.sh to deploy + enroll." + fi +} + +# ─── Verify ────────────────────────────────────────────────────────────────── + +verify_setup() { + log_step "Verifying Hyperlane Setup" + + local qf=(--output json --node "$NODE" --home "$BITSONG_HOME") + + log "Mailboxes:"; "$BINARY" query hyperlane mailboxes "${qf[@]}" | jq '.' 2>/dev/null || true; echo + log "ISMs:"; "$BINARY" query hyperlane ism isms "${qf[@]}" | jq '.' 2>/dev/null || true; echo + log "IGPs:"; "$BINARY" query hyperlane hooks igps "${qf[@]}" | jq '.' 2>/dev/null || true; echo + log "MerkleTree hooks:"; "$BINARY" query hyperlane hooks merkle-tree-hooks "${qf[@]}" | jq '.' 2>/dev/null || true; echo + log "Warp tokens:"; "$BINARY" query warp tokens "${qf[@]}" | jq '.' 2>/dev/null || true; echo + + local token_id; token_id=$(load_state "token_id") + if [[ -n "$token_id" ]]; then + log "Remote routers for $token_id:" + "$BINARY" query warp remote-routers "$token_id" "${qf[@]}" | jq '.' 2>/dev/null || true; echo + fi + + # Summary + log_step "Summary" + echo -e "${BOLD}Chain${NC}: $CHAIN_ID (RPC: $NODE)" + echo -e "${BOLD}Domain${NC}: $DOMAIN_ID (Remote: $REMOTE_DOMAIN)" + echo -e "${BOLD}Mailbox${NC}: $(load_state mailbox_id)" + echo -e "${BOLD}ISM${NC}: $(load_state routing_ism_id)" + echo -e "${BOLD}Hooks${NC}: merkle=$(load_state merkle_hook_id) igp=$(load_state igp_id)" + echo -e "${BOLD}Token${NC}: $(load_state token_id)" + echo -e "${BOLD}State${NC}: $STATE_FILE" +} + +# ─── Update Enrollment ─────────────────────────────────────────────────────── + +update_enrollment() { + local evm_addr="$1" + log_step "Updating Remote Router Enrollment" + + [[ "$evm_addr" =~ ^0x[0-9a-fA-F]{40}$ ]] || { log_err "Invalid EVM address: $evm_addr"; return 1; } + + local evm_bytes32; evm_bytes32=$(evm_to_bytes32 "$evm_addr") + local token_id; token_id=$(load_state "token_id") + [[ -n "$token_id" ]] || { log_err "No token_id in state. Run full setup first."; return 1; } + + log "EVM address: $evm_addr" + log "As bytes32: $evm_bytes32" + log "Token: $token_id" + + submit_tx "Update remote router (domain=$REMOTE_DOMAIN)" \ + "$BINARY" tx warp enroll-remote-router \ + "$token_id" "$REMOTE_DOMAIN" "$evm_bytes32" "$ENROLL_GAS" + + save_state "evm_hyp_erc20" "$evm_addr" + save_state "evm_hyp_erc20_bytes32" "$evm_bytes32" + save_state "evm_enrollment_updated" "true" + log_ok "Enrollment updated" + + "$BINARY" query warp remote-routers "$token_id" \ + --output json --node "$NODE" --home "$BITSONG_HOME" | jq '.' 2>/dev/null || true +} + +# ─── Main ──────────────────────────────────────────────────────────────────── + +UPDATE_EVM_ADDR="" +while [[ $# -gt 0 ]]; do + case "$1" in + --update-enrollment) UPDATE_EVM_ADDR="$2"; shift 2 ;; + -h|--help) + echo "Usage: $0 [--update-enrollment ]" + exit 0 ;; + *) echo "Unknown flag: $1"; exit 1 ;; + esac +done + +require_binary +require_jq +require_chain_running +log "Using binary: $BINARY ($("$BINARY" version 2>&1))" + +banner "Hyperlane Bridge Setup" "Domain: $DOMAIN_ID" + +setup_hyperlane +verify_setup + +if [[ -n "$UPDATE_EVM_ADDR" ]]; then + update_enrollment "$UPDATE_EVM_ADDR" +fi + +log_ok "Done!" diff --git a/scripts/hyperlane/03-evm-deploy.sh b/scripts/hyperlane/03-evm-deploy.sh new file mode 100755 index 00000000..43c7075c --- /dev/null +++ b/scripts/hyperlane/03-evm-deploy.sh @@ -0,0 +1,270 @@ +#!/usr/bin/env bash +# ============================================================================= +# 03-evm-deploy.sh — Deploy HypERC20 on Base Sepolia + bidirectional enrollment +# +# Deploys via Hyperlane CLI, then uses cast for EVM enrollment + verification. +# +# Usage: +# bash 03-evm-deploy.sh # Deploy + enroll +# bash 03-evm-deploy.sh --clean # Wipe Phase 4 state and redo +# +# Required environment: +# HYP_KEY (or EVM_PRIVATE_KEY) — EVM deployer private key (0x...) +# ============================================================================= + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib.sh" + +HYP_CHAINS_DIR="${HYP_CHAINS_DIR:-$HOME/.hyperlane/chains}" + +# ─── Step 1: Register Chain Metadata ───────────────────────────────────────── + +register_chain_metadata() { + log_step "Step 1: Register Chain Metadata" + + local mailbox_id; mailbox_id=$(require_state "mailbox_id" "mailbox_id") + + mkdir -p "$HYP_CHAINS_DIR/bitsong" + + cat > "$HYP_CHAINS_DIR/bitsong/metadata.yaml" << EOF +chainId: $CHAIN_ID +domainId: $DOMAIN_ID +name: bitsong +protocol: cosmos +bech32Prefix: bitsong +slip44: 639 +rpcUrls: + - http: http://localhost:26657 +restUrls: + - http: http://localhost:1317 +grpcUrls: + - http: http://localhost:9090 +nativeToken: + name: BitSong + symbol: BTSG + decimals: 6 + denom: $DENOM +blocks: + confirmations: 1 + estimateBlockTime: 6 +isTestnet: true +EOF + + cat > "$HYP_CHAINS_DIR/bitsong/addresses.yaml" << EOF +mailbox: "$mailbox_id" +EOF + + log_ok "Chain metadata: $HYP_CHAINS_DIR/bitsong/" +} + +# ─── Step 2: Deploy HypERC20 ──────────────────────────────────────────────── + +deploy_hyp_erc20() { + log_step "Step 2: Deploy HypERC20 on Base Sepolia" + + DEPLOYED_EVM=$(load_state "evm_hyp_erc20") + if [[ -n "$DEPLOYED_EVM" && "$DEPLOYED_EVM" =~ ^0x[0-9a-fA-F]{40}$ ]]; then + log_ok "HypERC20 already deployed: $DEPLOYED_EVM" + return 0 + fi + + # Generate single-chain warp config + local warp_config="$BITSONG_HOME/warp-route-basesepolia.yaml" + cat > "$warp_config" << EOF +basesepolia: + type: synthetic + name: "BitSong" + symbol: "BTSG" + decimals: 6 + mailbox: "$BASESEPOLIA_MAILBOX" + owner: "$DEPLOYER_ADDR" + interchainSecurityModule: "0x0000000000000000000000000000000000000000" +EOF + + log "Warp config:" + cat "$warp_config"; echo + + export HYP_KEY="$EVM_KEY" + log "Running: hyperlane warp deploy ..." + + # The CLI attempts Etherscan verification (requires API key we don't have). + hyperlane warp deploy --config "$warp_config" --yes + + # Extract deployed address from CLI artifacts + log "Looking for deployment artifacts..." + local latest_warp + latest_warp=$(find "$HOME/.hyperlane/deployments/warp_routes" -name "*basesepolia*config.yaml" 2>/dev/null \ + | sort -r | head -1) || true + + if [[ -n "$latest_warp" ]]; then + log "Artifact: $latest_warp" + DEPLOYED_EVM=$(grep -oP '(?<=addressOrDenom: ")[^"]+' "$latest_warp" 2>/dev/null | head -1) || true + fi + + if [[ -z "$DEPLOYED_EVM" || ! "$DEPLOYED_EVM" =~ ^0x[0-9a-fA-F]{40}$ ]]; then + log_err "Could not extract HypERC20 address from CLI artifacts." + log_err "Set manually: jq '.evm_hyp_erc20=\"0xADDR\"' $STATE_FILE > /tmp/s && mv /tmp/s $STATE_FILE" + exit 1 + fi + + save_state "evm_hyp_erc20" "$DEPLOYED_EVM" + log_ok "HypERC20: $DEPLOYED_EVM" + + # Compute bytes32 + local evm_bytes32; evm_bytes32=$(evm_to_bytes32 "$DEPLOYED_EVM") + save_state "evm_hyp_erc20_bytes32" "$evm_bytes32" + log " bytes32: $evm_bytes32" +} + +# ─── Step 3: Create Cosmos Collateral Token ────────────────────────────────── + +create_cosmos_token() { + log_step "Step 3: Create Cosmos Collateral Token" + + TOKEN_ID=$(load_state "token_id") + if [[ -n "$TOKEN_ID" ]]; then + log_ok "Cosmos token already exists: $TOKEN_ID" + return 0 + fi + + local mailbox_id; mailbox_id=$(require_state "mailbox_id" "mailbox_id") + + submit_tx "MsgCreateCollateralToken (denom=$DENOM)" \ + "$BINARY" tx warp create-collateral-token "$mailbox_id" "$DENOM" + + TOKEN_ID=$(extract_id) + [[ -n "$TOKEN_ID" ]] || { log_err "Could not extract token_id"; exit 1; } + save_state "token_id" "$TOKEN_ID" + log_ok "Cosmos token: $TOKEN_ID" +} + +# ─── Step 4: Enroll EVM Router on Cosmos ───────────────────────────────────── + +enroll_cosmos_side() { + log_step "Step 4: Enroll EVM Router on Cosmos" + + local evm_bytes32; evm_bytes32=$(require_state "evm_hyp_erc20_bytes32" "evm_hyp_erc20_bytes32") + + submit_tx "MsgEnrollRemoteRouter (domain=$REMOTE_DOMAIN)" \ + "$BINARY" tx warp enroll-remote-router \ + "$TOKEN_ID" "$REMOTE_DOMAIN" "$evm_bytes32" "$ENROLL_GAS" + + save_state "router_enrolled" "true" + log_ok "Cosmos side enrolled: domain $REMOTE_DOMAIN -> $evm_bytes32" +} + +# ─── Step 5: Enroll Cosmos Router on EVM ───────────────────────────────────── + +enroll_evm_side() { + log_step "Step 5: Enroll Cosmos Router on EVM" + + log "enrollRemoteRouter($DOMAIN_ID, $TOKEN_ID) on $DEPLOYED_EVM ..." + local tx; tx=$(cast send "$DEPLOYED_EVM" \ + "enrollRemoteRouter(uint32,bytes32)" "$DOMAIN_ID" "$TOKEN_ID" \ + --private-key "$EVM_KEY" --rpc-url "$EVM_RPC" --json 2>&1) || true + + local status; status=$(echo "$tx" | jq -r '.status // empty' 2>/dev/null) || true + local hash; hash=$(echo "$tx" | jq -r '.transactionHash // empty' 2>/dev/null) || true + + if [[ "$status" == "0x1" || "$status" == "1" ]]; then + save_state "evm_router_enrolled" "true" + log_ok "EVM enrolled: domain $DOMAIN_ID -> $TOKEN_ID (tx: $hash)" + else + log_err "EVM enrollment failed (status: $status)" + echo "$tx" | jq -c '.' 2>/dev/null || echo "$tx" + exit 1 + fi +} + +# ─── Step 6: Verify ───────────────────────────────────────────────────────── + +verify_deployment() { + log_step "Step 6: Verify" + + log "--- EVM HypERC20 ($DEPLOYED_EVM) ---" + local decimals mailbox supply router_on_evm + decimals=$(cast call "$DEPLOYED_EVM" "decimals()(uint8)" --rpc-url "$EVM_RPC" 2>/dev/null || echo "?") + mailbox=$(cast call "$DEPLOYED_EVM" "mailbox()(address)" --rpc-url "$EVM_RPC" 2>/dev/null || echo "?") + supply=$(cast call "$DEPLOYED_EVM" "totalSupply()(uint256)" --rpc-url "$EVM_RPC" 2>/dev/null || echo "?") + router_on_evm=$(cast call "$DEPLOYED_EVM" "routers(uint32)(bytes32)" "$DOMAIN_ID" \ + --rpc-url "$EVM_RPC" 2>/dev/null || echo "?") + + echo " decimals: $decimals (expected 6)" + echo " mailbox: $mailbox" + echo " totalSupply: $supply (expected 0)" + echo " router($DOMAIN_ID): $router_on_evm" + echo + + log "--- Cosmos warp token ($TOKEN_ID) ---" + "$BINARY" query warp remote-routers "$TOKEN_ID" \ + --output json --node "$NODE" --home "$BITSONG_HOME" 2>/dev/null | jq '.' || true +} + +# ─── Summary ───────────────────────────────────────────────────────────────── + +print_summary() { + log_step "Summary" + echo -e "${BOLD}Warp Route${NC}" + echo " Cosmos token: $(load_state token_id)" + echo " EVM HypERC20: $(load_state evm_hyp_erc20)" + echo " EVM (bytes32): $(load_state evm_hyp_erc20_bytes32)" + echo " State file: $STATE_FILE" + echo + echo -e "${BOLD}BaseScan${NC}: https://sepolia.basescan.org/address/$(load_state evm_hyp_erc20)" + echo + echo -e "${BOLD}Next${NC}: Run 04-agents.sh to start validators + relayer" +} + +# ─── Main ──────────────────────────────────────────────────────────────────── + +CLEAN=false +while [[ $# -gt 0 ]]; do + case "$1" in + --clean) CLEAN=true; shift ;; + -h|--help) + echo "Usage: $0 [--clean]" + echo " --clean Wipe Phase 4 state keys and redo" + echo "" + echo "Required: HYP_KEY or EVM_PRIVATE_KEY (deployer private key)" + exit 0 ;; + *) echo "Unknown flag: $1"; exit 1 ;; + esac +done + +# Preflight +require_jq +command -v hyperlane >/dev/null 2>&1 || { log_err "Hyperlane CLI missing: npm install -g @hyperlane-xyz/cli"; exit 1; } +command -v cast >/dev/null 2>&1 || { log_err "cast missing: foundryup"; exit 1; } + +[[ -f "$STATE_FILE" ]] || { log_err "Phase 3 state not found: $STATE_FILE. Run 02-hyperlane.sh first."; exit 1; } + +EVM_KEY="${HYP_KEY:-${EVM_PRIVATE_KEY:-}}" +[[ -n "$EVM_KEY" ]] || { log_err "Set HYP_KEY=0x"; exit 1; } + +DEPLOYER_ADDR=$(cast wallet address --private-key "$EVM_KEY" 2>/dev/null) \ + || { log_err "Cannot derive EVM address from key"; exit 1; } + +require_binary +require_chain_running + +log_ok "Hyperlane CLI: $(hyperlane --version 2>&1 | head -1)" +log_ok "cast: $(cast --version 2>&1 | head -1)" +log_ok "EVM deployer: $DEPLOYER_ADDR" +log_ok "Phase 3 mailbox: $(load_state mailbox_id)" + +if [[ "$CLEAN" == "true" ]]; then + jq 'del(.token_id, .router_enrolled, .evm_hyp_erc20, .evm_hyp_erc20_bytes32, .evm_router_enrolled)' \ + "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE" + log_ok "Phase 4 state cleared" +fi + +banner "Phase 4: Deploy Warp Route" "bitsong <-> Base Sepolia" + +register_chain_metadata +deploy_hyp_erc20 +create_cosmos_token +enroll_cosmos_side +enroll_evm_side +verify_deployment +print_summary + +log_ok "Phase 4 complete!" diff --git a/scripts/hyperlane/04-agents.sh b/scripts/hyperlane/04-agents.sh new file mode 100755 index 00000000..5ad9b224 --- /dev/null +++ b/scripts/hyperlane/04-agents.sh @@ -0,0 +1,474 @@ +#!/usr/bin/env bash +# ============================================================================= +# 04-agents.sh — Replace NoopISMs with real MultisigISMs, start validators +# and relayer via Docker +# +# Usage: +# bash 04-agents.sh # Full setup +# bash 04-agents.sh --validator-only # Start validators only +# bash 04-agents.sh --relayer-only # Start relayer only +# bash 04-agents.sh --clean # Stop containers + wipe Phase 5 state +# bash 04-agents.sh --stop # Stop all Phase 5 containers +# +# Required environment: +# VALIDATOR_KEY — EVM hex private key for validator signing +# COSMOS_SIGNER_KEY — hex key for Cosmos announcement txs +# EVM_RELAYER_KEY — hex key for Base Sepolia relayer (HYP_KEY alias) +# ============================================================================= + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib.sh" + +HYP_KEY="${HYP_KEY:-${EVM_RELAYER_KEY:-}}" +EVM_RELAYER_KEY="${EVM_RELAYER_KEY:-${HYP_KEY:-}}" + +# ─── Step 0: Prerequisites ────────────────────────────────────────────────── + +check_prerequisites() { + log_step "Step 0: Prerequisites" + local ok=true + + for cmd in docker cast jq; do + command -v "$cmd" >/dev/null 2>&1 && log_ok "$cmd: $(command -v "$cmd")" \ + || { log_err "$cmd not found"; ok=false; } + done + + require_binary + log_ok "bitsongd: $BINARY ($("$BINARY" version 2>&1 || echo 'unknown'))" + + [[ -n "${VALIDATOR_KEY:-}" ]] || { log_err "VALIDATOR_KEY not set"; ok=false; } + [[ -n "${COSMOS_SIGNER_KEY:-}" ]] || { log_err "COSMOS_SIGNER_KEY not set"; ok=false; } + [[ -n "${EVM_RELAYER_KEY:-}" ]] || { log_err "EVM_RELAYER_KEY (or HYP_KEY) not set"; ok=false; } + + [[ -f "$STATE_FILE" ]] || { log_err "State file not found: $STATE_FILE"; ok=false; } + if [[ -f "$STATE_FILE" ]]; then + require_state "mailbox_id" "mailbox_id" > /dev/null + require_state "token_id" "token_id" > /dev/null + log_ok "Phase 3/4 state verified" + fi + + [[ "$ok" == "true" ]] || { log_err "Prerequisites failed"; exit 1; } + log_ok "All prerequisites satisfied" +} + +# ─── Step 1: Derive Validator Address ──────────────────────────────────────── + +derive_validator_addr() { + log_step "Step 1: Derive Validator Address" + VALIDATOR_ADDR=$(load_state "validator_addr") + if [[ -n "$VALIDATOR_ADDR" ]]; then + log_ok "Validator (cached): $VALIDATOR_ADDR" + else + VALIDATOR_ADDR=$(cast wallet address --private-key "$VALIDATOR_KEY" 2>&1) + [[ "$VALIDATOR_ADDR" =~ ^0x[0-9a-fA-F]{40}$ ]] || { log_err "Failed to derive validator address"; return 1; } + save_state "validator_addr" "$VALIDATOR_ADDR" + log_ok "Validator: $VALIDATOR_ADDR" + fi +} + +# ─── Step 1b: Fund Cosmos Signer ──────────────────────────────────────────── + +fund_cosmos_signer() { + log_step "Step 1b: Fund Cosmos Signer" + + local cosmos_signer_addr + cosmos_signer_addr=$(python3 -c " +from ecdsa import SigningKey, SECP256k1 +import hashlib +def to_bech32(hex_key, prefix='bitsong'): + pk = bytes.fromhex(hex_key.replace('0x','')) + sk = SigningKey.from_string(pk, curve=SECP256k1) + vk = sk.get_verifying_key() + x, y = vk.to_string()[:32], vk.to_string()[32:] + compressed = (b'\x02' if y[-1] % 2 == 0 else b'\x03') + x + ripe = hashlib.new('ripemd160', hashlib.sha256(compressed).digest()).digest() + CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l' + def polymod(values): + GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + chk = 1 + for v in values: + b = (chk >> 25); chk = (chk & 0x1ffffff) << 5 ^ v + for i in range(5): chk ^= GEN[i] if ((b >> i) & 1) else 0 + return chk + hrp_exp = [ord(x) >> 5 for x in prefix] + [0] + [ord(x) & 31 for x in prefix] + def conv(data): + acc, bits, ret = 0, 0, [] + for value in data: + acc = (acc << 8) | value; bits += 8 + while bits >= 5: bits -= 5; ret.append((acc >> bits) & 31) + if bits: ret.append((acc << (5 - bits)) & 31) + return ret + data = conv(list(ripe)) + ck = [(polymod(hrp_exp + data + [0]*6) ^ 1) >> 5*(5-i) & 31 for i in range(6)] + return prefix + '1' + ''.join([CHARSET[d] for d in data + ck]) +print(to_bech32('$COSMOS_SIGNER_KEY')) +" 2>/dev/null) + + if [[ -z "$cosmos_signer_addr" || ! "$cosmos_signer_addr" =~ ^bitsong1 ]]; then + log_err "Failed to derive Cosmos signer address. Ensure: pip3 install ecdsa" + return 1 + fi + + log "Cosmos signer: $cosmos_signer_addr" + + local balance + balance=$("$BINARY" query bank balances "$cosmos_signer_addr" --home "$BITSONG_HOME" --output json 2>/dev/null \ + | jq -r '.balances[] | select(.denom=="ubtsg") | .amount // "0"') || balance="0" + if [[ -n "$balance" && "$balance" != "0" && "$balance" != "null" ]]; then + log_ok "Already funded: ${balance}ubtsg" + return 0 + fi + + log "Funding with 10000000ubtsg..." + submit_tx "Fund cosmos signer" "$BINARY" tx bank send val "$cosmos_signer_addr" 10000000ubtsg + log_ok "Cosmos signer funded" +} + +# ─── Step 2: Update ISM — Cosmos Side ─────────────────────────────────────── + +update_ism_cosmos_side() { + log_step "Step 2: Update ISM — Cosmos Side (EVM->Cosmos)" + + [[ "$(load_state "ism_updated_cosmos_side")" == "true" ]] && { log_ok "Already done"; return 0; } + + local routing_ism_id; routing_ism_id=$(require_state "routing_ism_id" "routing_ism_id") + + # Create MultisigISM with real validator (1-of-1) + local bitsong_multisig_ism_id + bitsong_multisig_ism_id=$(load_state "bitsong_multisig_ism_id") + if [[ -z "$bitsong_multisig_ism_id" ]]; then + submit_tx "Create MultisigISM (validator=$VALIDATOR_ADDR, threshold=1)" \ + "$BINARY" tx hyperlane ism create-message-id-multisig "$VALIDATOR_ADDR" "1" + bitsong_multisig_ism_id=$(extract_id) + [[ -n "$bitsong_multisig_ism_id" ]] || { log_err "Failed to extract ISM ID"; return 1; } + save_state "bitsong_multisig_ism_id" "$bitsong_multisig_ism_id" + log_ok "New MultisigISM: $bitsong_multisig_ism_id" + else + log_ok "MultisigISM exists: $bitsong_multisig_ism_id" + fi + + # Remove-then-add pattern (avoids Go range-copy bug in SetDomain) + submit_tx "Remove domain $REMOTE_DOMAIN from RoutingISM" \ + "$BINARY" tx hyperlane ism remove-routing-ism-domain "$routing_ism_id" "$REMOTE_DOMAIN" + submit_tx "Set domain $REMOTE_DOMAIN to new MultisigISM" \ + "$BINARY" tx hyperlane ism set-routing-ism-domain "$routing_ism_id" "$REMOTE_DOMAIN" "$bitsong_multisig_ism_id" + + save_state "ism_updated_cosmos_side" "true" + log_ok "Cosmos ISM: routing[$REMOTE_DOMAIN] -> $bitsong_multisig_ism_id" +} + +# ─── Step 3: Update ISM — EVM Side ────────────────────────────────────────── + +update_ism_evm_side() { + log_step "Step 3: Update ISM — EVM Side (Cosmos->EVM)" + + [[ "$(load_state "ism_updated_evm_side")" == "true" ]] && { log_ok "Already done"; return 0; } + + local evm_hyp_erc20; evm_hyp_erc20=$(require_state "evm_hyp_erc20" "evm_hyp_erc20") + + # Deploy ISM via factory (CREATE2 — deterministic) + local basesepolia_multisig_ism + basesepolia_multisig_ism=$(load_state "basesepolia_multisig_ism") + if [[ -z "$basesepolia_multisig_ism" ]]; then + log "Deploying staticMessageIdMultisigISM on Base Sepolia..." + + basesepolia_multisig_ism=$(cast call "$BASESEPOLIA_ISM_FACTORY" \ + "deploy(address[],uint8)(address)" "[$VALIDATOR_ADDR]" 1 \ + --rpc-url "$EVM_RPC" 2>/dev/null) || true + [[ "$basesepolia_multisig_ism" =~ ^0x[0-9a-fA-F]{40}$ ]] || { log_err "Failed to predict ISM address"; return 1; } + log " Predicted: $basesepolia_multisig_ism" + + local tx_out; tx_out=$(cast send "$BASESEPOLIA_ISM_FACTORY" \ + "deploy(address[],uint8)" "[$VALIDATOR_ADDR]" 1 \ + --private-key "$HYP_KEY" --rpc-url "$EVM_RPC" --json 2>&1) || true + local tx_hash; tx_hash=$(echo "$tx_out" | jq -r '.transactionHash // empty' 2>/dev/null) || true + [[ -n "$tx_hash" ]] || { log_err "ISM deploy failed"; echo "$tx_out"; return 1; } + + save_state "basesepolia_multisig_ism" "$basesepolia_multisig_ism" + log_ok "ISM deployed: $basesepolia_multisig_ism (tx: $tx_hash)" + else + log_ok "ISM exists: $basesepolia_multisig_ism" + fi + + # Wait for ISM tx confirmation, then update HypERC20 + sleep 5 + log "Setting HypERC20 ISM to $basesepolia_multisig_ism..." + local update_out; update_out=$(cast send "$evm_hyp_erc20" \ + "setInterchainSecurityModule(address)" "$basesepolia_multisig_ism" \ + --private-key "$HYP_KEY" --rpc-url "$EVM_RPC" --json 2>&1) || true + local update_status; update_status=$(echo "$update_out" | jq -r '.status // empty' 2>/dev/null) || true + [[ "$update_status" == "0x1" || "$update_status" == "1" ]] \ + || { log_err "setInterchainSecurityModule failed (status: $update_status)"; echo "$update_out"; return 1; } + + save_state "ism_updated_evm_side" "true" + log_ok "HypERC20 ISM updated to real MultisigISM" +} + +# ─── Agent Config ──────────────────────────────────────────────────────────── + +write_agent_config() { + local mailbox_id merkle_hook_id igp_id + mailbox_id=$(load_state "mailbox_id") + merkle_hook_id=$(load_state "merkle_hook_id") + igp_id=$(load_state "igp_id") + + # CRITICAL: index.from for basesepolia MUST be the mailbox deployment block (~13,850,000). + # The relayer's msg::db_loader scans by mailbox nonce starting from 0 sequentially. + # If index.from is too recent, only high nonces are indexed and db_loader is stuck at 0. + local basesep_index_from=13850000 + + cat > "$BITSONG_HOME/agent-config.json" << EOF +{ + "chains": { + "bitsong": { + "name": "bitsong", + "chainId": "${CHAIN_ID}", + "domainId": $DOMAIN_ID, + "protocol": "cosmosNative", + "bech32Prefix": "bitsong", + "slip44": 639, + "contractAddressBytes": 32, + "canonicalAsset": "ubtsg", + "rpcUrls": [{"http": "http://localhost:26657"}], + "grpcUrls": [{"http": "http://localhost:9090"}], + "nativeToken": { "name": "BitSong", "symbol": "BTSG", "decimals": 6, "denom": "ubtsg" }, + "gasPrice": { "amount": "0.025", "denom": "ubtsg" }, + "gasMultiplier": "2.0", + "blocks": { "confirmations": 1, "estimateBlockTime": 6, "reorgPeriod": 1 }, + "index": { "from": 1, "chunk": 50 }, + "mailbox": "${mailbox_id}", + "validatorAnnounce": "${mailbox_id}", + "merkleTreeHook": "${merkle_hook_id}", + "interchainGasPaymaster": "${igp_id}" + }, + "basesepolia": { + "name": "basesepolia", + "chainId": 84532, + "domainId": 84532, + "protocol": "ethereum", + "rpcUrls": [{"http": "${EVM_RPC}"}], + "nativeToken": { "name": "Ether", "symbol": "ETH", "decimals": 18 }, + "blocks": { "confirmations": 1, "estimateBlockTime": 2, "reorgPeriod": 1 }, + "index": { "from": ${basesep_index_from}, "chunk": 9999 }, + "mailbox": "0x6966b0E55883d49BFB24539356a2f8A673E02039", + "validatorAnnounce": "${VALIDATOR_ANNOUNCE_CONTRACT}", + "merkleTreeHook": "0x86fb9F1c124fB20ff130C41a79a432F770f67AFD", + "interchainGasPaymaster": "0x28B02B97a850872C4D33C3E024fab6499ad96564", + "interchainSecurityModule": "0xBB276c7419155980558BFf56E22AfF83023a2dB2", + "proxyAdmin": "0x44b764045BfDC68517e10e783E69B376cef196B2" + } + } +} +EOF + log_ok "Agent config: $BITSONG_HOME/agent-config.json (basesepolia index.from=$basesep_index_from)" +} + +# ─── Step 4: Start Validator — BitSong ─────────────────────────────────────── + +start_validator_bitsong() { + log_step "Step 4: Start Validator — BitSong" + + if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^hyperlane-validator-bitsong$"; then + log_ok "Already running"; return 0 + fi + docker rm -f hyperlane-validator-bitsong 2>/dev/null || true + + mkdir -p "$BITSONG_HOME/validator-bitsong-db" "$BITSONG_HOME/checkpoints-bitsong" + write_agent_config + + log "Starting validator-bitsong (domain=$DOMAIN_ID)..." + docker run -d --name hyperlane-validator-bitsong --network host \ + -e CONFIG_FILES=/config/agent-config.json \ + -v "$BITSONG_HOME/agent-config.json:/config/agent-config.json:ro" \ + -v "$BITSONG_HOME/validator-bitsong-db:/hyperlane_db" \ + -v "$BITSONG_HOME/checkpoints-bitsong:/checkpoints-bitsong" \ + "$DOCKER_IMAGE" ./validator \ + --db /hyperlane_db --originChainName bitsong \ + --reorgPeriod 1 --interval 5 \ + --validator.type hexKey --validator.key "$VALIDATOR_KEY" \ + --chains.bitsong.signer.type cosmosKey \ + --chains.bitsong.signer.key "$COSMOS_SIGNER_KEY" \ + --chains.bitsong.signer.prefix bitsong \ + --checkpointSyncer.type localStorage \ + --checkpointSyncer.path /checkpoints-bitsong + + log_ok "validator-bitsong started" +} + +# ─── Step 5: Start Validator — Base Sepolia ────────────────────────────────── + +start_validator_basesepolia() { + log_step "Step 5: Start Validator — Base Sepolia" + + if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^hyperlane-validator-basesepolia$"; then + log_ok "Already running"; return 0 + fi + docker rm -f hyperlane-validator-basesepolia 2>/dev/null || true + + mkdir -p "$BITSONG_HOME/validator-basesepolia-db" "$BITSONG_HOME/checkpoints-basesepolia" + + log "Starting validator-basesepolia (domain=$REMOTE_DOMAIN)..." + docker run -d --name hyperlane-validator-basesepolia --network host \ + -e CONFIG_FILES=/config/agent-config.json \ + -v "$BITSONG_HOME/agent-config.json:/config/agent-config.json:ro" \ + -v "$BITSONG_HOME/validator-basesepolia-db:/hyperlane_db" \ + -v "$BITSONG_HOME/checkpoints-basesepolia:/checkpoints-basesepolia" \ + "$DOCKER_IMAGE" ./validator \ + --db /hyperlane_db --originChainName basesepolia \ + --reorgPeriod 1 --interval 5 \ + --validator.type hexKey --validator.key "$VALIDATOR_KEY" \ + --chains.basesepolia.signer.type hexKey \ + --chains.basesepolia.signer.key "$EVM_RELAYER_KEY" \ + --checkpointSyncer.type localStorage \ + --checkpointSyncer.path /checkpoints-basesepolia + + log_ok "validator-basesepolia started" +} + +# ─── Step 6: Wait for Announcements ───────────────────────────────────────── + +wait_for_announcements() { + log_step "Step 6: Wait for Validator Announcements" + + local bitsong_ok basesepolia_ok + bitsong_ok=$(load_state "validator_bitsong_announced") + basesepolia_ok=$(load_state "validator_basesepolia_announced") + [[ "$bitsong_ok" == "true" && "$basesepolia_ok" == "true" ]] && { log_ok "Both already announced"; return 0; } + + log "Waiting up to 120s..." + local validator_addr_lower; validator_addr_lower=$(echo "$VALIDATOR_ADDR" | tr '[:upper:]' '[:lower:]') + + for i in $(seq 1 24); do + sleep 5 + + if [[ "$bitsong_ok" != "true" ]]; then + local mailbox_id locations + mailbox_id=$(load_state "mailbox_id") + locations=$("$BINARY" query hyperlane ism announced-storage-locations \ + "$mailbox_id" "$validator_addr_lower" \ + --output json --node "$NODE" --home "$BITSONG_HOME" 2>/dev/null | \ + jq -r '.storage_locations // [] | length' 2>/dev/null) || locations="0" + if [[ "$locations" -gt 0 ]]; then + save_state "validator_bitsong_announced" "true"; bitsong_ok="true" + log_ok "validator-bitsong announced" + fi + fi + + if [[ "$basesepolia_ok" != "true" ]]; then + local evm_announced + evm_announced=$(cast call "$VALIDATOR_ANNOUNCE_CONTRACT" \ + "getAnnouncedValidators()(address[])" --rpc-url "$EVM_RPC" 2>/dev/null | tr ',' '\n' | \ + grep -i "$(echo "${VALIDATOR_ADDR#0x}" | tr '[:upper:]' '[:lower:]')" || true) + if [[ -n "$evm_announced" ]]; then + save_state "validator_basesepolia_announced" "true"; basesepolia_ok="true" + log_ok "validator-basesepolia announced" + fi + fi + + [[ "$bitsong_ok" == "true" && "$basesepolia_ok" == "true" ]] && return 0 + log " [${i}/24] bitsong=${bitsong_ok:-pending}, basesepolia=${basesepolia_ok:-pending}" + done + + log_warn "Announcement timeout — validators may still be starting. Check: docker logs hyperlane-validator-bitsong" +} + +# ─── Step 7: Start Relayer ────────────────────────────────────────────────── + +start_relayer() { + log_step "Step 7: Start Relayer" + + if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^hyperlane-relayer$"; then + log_ok "Already running"; return 0 + fi + docker rm -f hyperlane-relayer 2>/dev/null || true + + mkdir -p "$BITSONG_HOME/relayer-db" + + # Whitelist: only relay our warp messages (avoids noise from other projects on shared testnet) + local token_id evm_hyp_erc20 evm_padded + token_id=$(load_state "token_id") + evm_hyp_erc20=$(load_state "evm_hyp_erc20") + evm_padded=$(evm_to_bytes32 "$evm_hyp_erc20") + + local whitelist="[{\"senderAddress\":\"${token_id}\",\"destinationDomain\":\"${REMOTE_DOMAIN}\"},{\"senderAddress\":\"${evm_padded}\",\"destinationDomain\":\"${DOMAIN_ID}\"}]" + + log "Starting relayer (bitsong <-> basesepolia)..." + docker run -d --name hyperlane-relayer --network host \ + -e CONFIG_FILES=/config/agent-config.json \ + -v "$BITSONG_HOME/agent-config.json:/config/agent-config.json:ro" \ + -v "$BITSONG_HOME/relayer-db:/hyperlane_db" \ + -v "$BITSONG_HOME/checkpoints-bitsong:/checkpoints-bitsong:ro" \ + -v "$BITSONG_HOME/checkpoints-basesepolia:/checkpoints-basesepolia:ro" \ + "$DOCKER_IMAGE" ./relayer \ + --db /hyperlane_db --relayChains bitsong,basesepolia \ + --allowLocalCheckpointSyncers true \ + --gaspaymentenforcement '[{"type": "none"}]' \ + --whitelist "$whitelist" \ + --chains.bitsong.signer.type cosmosKey \ + --chains.bitsong.signer.key "$COSMOS_SIGNER_KEY" \ + --chains.bitsong.signer.prefix bitsong \ + --chains.basesepolia.signer.type hexKey \ + --chains.basesepolia.signer.key "$EVM_RELAYER_KEY" \ + --metricsPort 9091 + + log_ok "Relayer started" +} + +# ─── Main ──────────────────────────────────────────────────────────────────── + +CLEAN=false +STOP=false +VALIDATOR_ONLY=false +RELAYER_ONLY=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --clean) CLEAN=true; shift ;; + --stop) STOP=true; shift ;; + --validator-only) VALIDATOR_ONLY=true; shift ;; + --relayer-only) RELAYER_ONLY=true; shift ;; + -h|--help) + echo "Usage: $0 [--clean|--stop|--validator-only|--relayer-only]" + echo "" + echo "Required env: VALIDATOR_KEY, COSMOS_SIGNER_KEY, EVM_RELAYER_KEY" + exit 0 ;; + *) log_err "Unknown flag: $1"; exit 1 ;; + esac +done + +if [[ "$STOP" == "true" ]]; then + log "Stopping Phase 5 containers..." + docker stop hyperlane-validator-bitsong hyperlane-validator-basesepolia hyperlane-relayer 2>/dev/null || true + log_ok "Stopped"; exit 0 +fi + +if [[ "$CLEAN" == "true" ]]; then + log "Cleaning Phase 5 state..." + docker rm -f hyperlane-validator-bitsong hyperlane-validator-basesepolia hyperlane-relayer 2>/dev/null || true + for key in validator_addr bitsong_multisig_ism_id basesepolia_multisig_ism \ + validator_bitsong_announced validator_basesepolia_announced \ + ism_updated_cosmos_side ism_updated_evm_side; do + [[ -f "$STATE_FILE" ]] && jq --arg k "$key" 'del(.[$k])' "$STATE_FILE" > "${STATE_FILE}.tmp" \ + && mv "${STATE_FILE}.tmp" "$STATE_FILE" + done + log_ok "Phase 5 state cleaned" +fi + +banner "Phase 5: Agents" "bitsong <-> basesepolia" + +check_prerequisites +derive_validator_addr +fund_cosmos_signer + +if [[ "$RELAYER_ONLY" != "true" ]]; then + update_ism_cosmos_side + update_ism_evm_side + start_validator_bitsong + start_validator_basesepolia + wait_for_announcements +fi + +if [[ "$VALIDATOR_ONLY" != "true" ]]; then + start_relayer +fi + +log_ok "Agents running! Next: bash 05-test.sh" diff --git a/scripts/hyperlane/05-test.sh b/scripts/hyperlane/05-test.sh new file mode 100755 index 00000000..8b73cdcc --- /dev/null +++ b/scripts/hyperlane/05-test.sh @@ -0,0 +1,349 @@ +#!/usr/bin/env bash +# ============================================================================= +# 05-test.sh — End-to-end transfer tests (Cosmos<->EVM) +# +# Usage: +# bash 05-test.sh # Run both tests +# bash 05-test.sh --cosmos-only # Cosmos->EVM only +# bash 05-test.sh --evm-only # EVM->Cosmos only +# +# Required environment: +# EVM_RELAYER_KEY (or HYP_KEY) — for EVM->Cosmos transfer +# ============================================================================= + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib.sh" + +EVM_RELAYER_KEY="${EVM_RELAYER_KEY:-${HYP_KEY:-}}" + +# ─── Readiness Check ──────────────────────────────────────────────────────── + +check_agents_ready() { + log_step "Checking Agent Readiness" + + local ok=true + for name in hyperlane-validator-bitsong hyperlane-validator-basesepolia hyperlane-relayer; do + if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${name}$"; then + log_ok "$name running" + else + log_err "$name NOT running — start with: bash 04-agents.sh" + ok=false + fi + done + [[ "$ok" == "true" ]] || { log_err "Agents not ready. Run 04-agents.sh first."; exit 1; } + + # Wait for bitsong validator to be connected and syncing with the chain. + # NOTE: Checkpoint index will be 0 until a message is dispatched (merkle tree is empty). + # So we check validator logs for signs it's watching the chain, not for checkpoint production. + log "Waiting for bitsong validator to connect to chain..." + + for i in $(seq 1 20); do + local val_logs + val_logs=$(docker logs hyperlane-validator-bitsong --tail 50 2>&1) || val_logs="" + + # Look for signs the validator is connected and watching the chain: + # - "Watching for new messages" / "fetching" / "cursor" = actively scanning + # - "Latest block" / "block_height" = synced with chain + # - Any log output at all after startup = healthy + if echo "$val_logs" | grep -qiE "watching|fetching|cursor|latest.block|block_height|merkle|tree_count|scanning"; then + log_ok "Validator connected and watching chain" + return 0 + fi + + # Also check if the validator has been running for >10s (it connects quickly) + local started_at + started_at=$(docker inspect hyperlane-validator-bitsong --format '{{.State.StartedAt}}' 2>/dev/null) || true + if [[ -n "$started_at" ]]; then + local start_epoch now_epoch + start_epoch=$(date -d "$started_at" +%s 2>/dev/null) || start_epoch=0 + now_epoch=$(date +%s) + local uptime=$(( now_epoch - start_epoch )) + if [[ "$uptime" -gt 15 ]]; then + log_ok "Validator running for ${uptime}s — proceeding" + return 0 + fi + fi + + if [[ $i -eq 1 ]]; then + log " Validator starting up — waiting for it to connect..." + fi + log " [${i}/20] waiting..." + sleep 3 + done + + log_warn "Could not confirm validator is syncing after 60s." + log_warn "Continuing anyway — check: docker logs hyperlane-validator-bitsong" +} + +# ─── Relay Diagnostics ────────────────────────────────────────────────────── + +# Print a snapshot of what the relayer is doing (called during wait loops) +show_relay_progress() { + local logs + logs=$(docker logs hyperlane-relayer --tail 3000 2>&1 \ + | sed 's/\x1b\[[0-9;]*m//g') || return + + # Hyperlane v2 relayer log format: + # "Found log(s) in index range, ...cursor: RateLimitedContractSyncCursor { tip: N, ... + # domain: HyperlaneDomain(bitsong (7171)) }" + # "status: Finalized" for delivered txs + # "pool_size: N" in finality_stage + + local bitsong_tip basesep_tip finalized_count + bitsong_tip=$(echo "$logs" | grep "HyperlaneDomain(bitsong" \ + | grep -oP 'tip: \K[0-9]+' | tail -1) || true + basesep_tip=$(echo "$logs" | grep "HyperlaneDomain(basesepolia" \ + | grep -oP 'tip: \K[0-9]+' | tail -1) || true + finalized_count=$(echo "$logs" | grep -ci "status: Finalized" 2>/dev/null) || finalized_count=0 + + echo -n " relayer: bitsong=${bitsong_tip:-?} basesep=${basesep_tip:-?} finalized=$finalized_count" + + # Check for errors (excluding CCIP noise) + local real_errors + real_errors=$(echo "$logs" | grep -i "error\|failed" \ + | grep -cv "0xa2827cb39\|CCIP\|verification" 2>/dev/null) || real_errors=0 + if [[ "$real_errors" -gt 0 ]]; then + echo -n " ${RED}errors=$real_errors${NC}" + fi + echo +} + +# ─── Cosmos → EVM ─────────────────────────────────────────────────────────── + +test_cosmos_to_evm() { + log_step "Test: Cosmos -> EVM" + + local token_id evm_hyp_erc20 merkle_hook_id + token_id=$(require_state "token_id" "token_id") + evm_hyp_erc20=$(require_state "evm_hyp_erc20" "evm_hyp_erc20") + merkle_hook_id=$(load_state "merkle_hook_id") + + # Send to EVM signer (so they have tokens for the EVM->Cosmos test) + local evm_signer_addr evm_signer_bytes32 + evm_signer_addr=$(cast wallet address --private-key "$EVM_RELAYER_KEY" 2>/dev/null) + [[ -n "$evm_signer_addr" ]] || { log_err "Cannot derive EVM signer address"; return 1; } + local hex="${evm_signer_addr#0x}"; hex=$(echo "$hex" | tr '[:upper:]' '[:lower:]') + evm_signer_bytes32=$(printf "0x%064s" "$hex" | tr ' ' '0') + + log "Sending 1000 ubtsg: BitSong -> Base Sepolia" + log " Token: $token_id" + log " Recipient: $evm_signer_addr" + + local initial_supply + initial_supply=$(cast call "$evm_hyp_erc20" "totalSupply()(uint256)" --rpc-url "$EVM_RPC" 2>/dev/null) || initial_supply="0" + log " Initial totalSupply: $initial_supply" + + # Use merkle hook to bypass IGP payment (devnet) + local tx_args=("$BINARY" tx warp transfer "$token_id" "$REMOTE_DOMAIN" "$evm_signer_bytes32" "1000" + --max-hyperlane-fee "0ubtsg") + [[ -n "$merkle_hook_id" ]] && tx_args+=(--custom-hook-id "$merkle_hook_id") + + submit_tx "Cosmos->EVM (1000 ubtsg)" "${tx_args[@]}" + + # Check if merkle tree count increased (confirms message was dispatched) + local merkle_count + merkle_count=$("$BINARY" query hyperlane hooks merkle-tree-hooks \ + --output json --node "$NODE" --home "$BITSONG_HOME" 2>/dev/null \ + | jq -r '.merkle_tree_hooks[0].merkle_tree.count // "?"' 2>/dev/null) || merkle_count="?" + log " Merkle tree count after dispatch: $merkle_count" + + # Wait for relayer to deliver — 300s timeout with diagnostics + log "Waiting for relayer to pick up, sign, and deliver (timeout: 300s)..." + for i in $(seq 1 60); do + sleep 5 + local current_supply + current_supply=$(cast call "$evm_hyp_erc20" "totalSupply()(uint256)" --rpc-url "$EVM_RPC" 2>/dev/null) || current_supply="0" + if [[ "$current_supply" != "$initial_supply" ]]; then + log_ok "Cosmos->EVM SUCCESS! totalSupply: $initial_supply -> $current_supply" + save_state "cosmos_to_evm_test_passed" "true" + return 0 + fi + + # Show diagnostics every 30s + if (( i % 6 == 0 )); then + echo -e " ${CYAN}[${i}/60] totalSupply=$current_supply — checking relayer...${NC}" + show_relay_progress + + # Also check validator checkpoint progress + local cp_file="$BITSONG_HOME/checkpoints-bitsong/index.json" + if [[ -f "$cp_file" ]]; then + local cp_idx + cp_idx=$(cat "$cp_file" 2>/dev/null) || cp_idx="?" + echo " validator-bitsong: checkpoint_index=$cp_idx" + fi + else + log " [${i}/60] totalSupply=$current_supply" + fi + done + + # Timed out — show diagnostics + log_err "Timed out after 300s!" + log_warn "Diagnostic info:" + log_warn " Last 10 relayer log lines (filtered):" + docker logs hyperlane-relayer --tail 20 2>&1 \ + | grep -v "0xa2827cb39\|CCIP Read" | tail -10 || true + echo + log_warn " Validator checkpoint:" + local cp_file="$BITSONG_HOME/checkpoints-bitsong/index.json" + [[ -f "$cp_file" ]] && cat "$cp_file" || echo " (no checkpoint file)" + echo + log_warn "Troubleshooting:" + log_warn " 1. Check validator: docker logs hyperlane-validator-bitsong --tail 50" + log_warn " 2. Check relayer: docker logs hyperlane-relayer --tail 50" + log_warn " 3. Run status: bash status.sh" + log_warn " 4. Retry test: bash 05-test.sh --cosmos-only" + return 1 +} + +# ─── EVM → Cosmos ─────────────────────────────────────────────────────────── + +test_evm_to_cosmos() { + log_step "Test: EVM -> Cosmos" + + local evm_hyp_erc20 token_id + evm_hyp_erc20=$(require_state "evm_hyp_erc20" "evm_hyp_erc20") + token_id=$(require_state "token_id" "token_id") + + local cosmos_recipient_bytes32 + cosmos_recipient_bytes32=$(bech32_to_bytes32 "$VAL_ADDRESS") + [[ -n "$cosmos_recipient_bytes32" ]] || { log_err "Failed to convert $VAL_ADDRESS to bytes32"; return 1; } + + # Check EVM balance first + local evm_balance + evm_balance=$(cast call "$evm_hyp_erc20" "totalSupply()(uint256)" --rpc-url "$EVM_RPC" 2>/dev/null) || evm_balance="0" + if [[ "$evm_balance" == "0" ]]; then + log_err "HypERC20 totalSupply is 0 — no tokens to send back." + log_err "Run Cosmos->EVM test first to mint tokens on EVM side." + return 1 + fi + + log "Sending 1000 ubtsg: Base Sepolia -> BitSong" + log " HypERC20: $evm_hyp_erc20 (balance: $evm_balance)" + log " Recipient: $VAL_ADDRESS" + + local initial_height + initial_height=$("$BINARY" status --node "$NODE" --home "$BITSONG_HOME" 2>/dev/null | \ + jq -r '.sync_info.latest_block_height // "0"' 2>/dev/null) || initial_height="0" + + local evm_tx + evm_tx=$(cast send "$evm_hyp_erc20" \ + "transferRemote(uint32,bytes32,uint256)" "$DOMAIN_ID" "$cosmos_recipient_bytes32" "1000" \ + --value 1 --private-key "$EVM_RELAYER_KEY" --rpc-url "$EVM_RPC" --json 2>&1) || true + + local evm_tx_hash + evm_tx_hash=$(echo "$evm_tx" | jq -r '.transactionHash // empty' 2>/dev/null) || true + [[ -n "$evm_tx_hash" ]] || { log_err "EVM tx failed"; echo "$evm_tx"; return 1; } + + log "EVM TX: $evm_tx_hash" + log "Waiting for relayer delivery (timeout: 1500s — first run scans ~24M blocks)..." + + for i in $(seq 1 100); do + sleep 15 + local events + events=$("$BINARY" query txs \ + --query "coin_received.receiver='$VAL_ADDRESS'" \ + --limit 5 --output json --node "$NODE" --home "$BITSONG_HOME" 2>/dev/null | \ + jq --arg h "$initial_height" '[.txs[]? | select((.height | tonumber) > ($h | tonumber))] | length' \ + 2>/dev/null) || events="0" + if [[ "${events:-0}" -gt 0 ]]; then + log_ok "EVM->Cosmos SUCCESS! coin_received by $VAL_ADDRESS" + save_state "evm_to_cosmos_test_passed" "true" + return 0 + fi + + local height + height=$("$BINARY" status --node "$NODE" --home "$BITSONG_HOME" 2>/dev/null | \ + jq -r '.sync_info.latest_block_height // "?"' 2>/dev/null) || height="?" + + # Show diagnostics every 60s + if (( i % 4 == 0 )); then + echo -e " ${CYAN}[${i}/100] height=$height — checking relayer...${NC}" + show_relay_progress + else + log " [${i}/100] height=$height" + fi + done + + log_err "Timed out after 1500s!" + log_warn "Last 10 relayer logs:" + docker logs hyperlane-relayer --tail 20 2>&1 \ + | grep -v "0xa2827cb39\|CCIP Read" | tail -10 || true + return 1 +} + +# ─── Summary ───────────────────────────────────────────────────────────────── + +print_summary() { + log_step "Full Summary" + + echo -e "${BOLD}Chain${NC}" + echo " ID: $CHAIN_ID" + echo " RPC: $NODE" + echo " Domain: $DOMAIN_ID" + echo + + echo -e "${BOLD}Hyperlane${NC}" + echo " Mailbox: $(load_state mailbox_id)" + echo " RoutingISM: $(load_state routing_ism_id)" + echo " MerkleHook: $(load_state merkle_hook_id)" + echo " IGP: $(load_state igp_id)" + echo " Token: $(load_state token_id)" + echo + + echo -e "${BOLD}EVM (Base Sepolia)${NC}" + echo " HypERC20: $(load_state evm_hyp_erc20)" + echo " MultisigISM: $(load_state basesepolia_multisig_ism)" + echo " Validator: $(load_state validator_addr)" + echo + + echo -e "${BOLD}Transfer Tests${NC}" + echo " Cosmos->EVM: $(load_state cosmos_to_evm_test_passed || echo 'not run')" + echo " EVM->Cosmos: $(load_state evm_to_cosmos_test_passed || echo 'not run')" + echo + + echo -e "${BOLD}Docker${NC}" + echo " docker logs hyperlane-validator-bitsong" + echo " docker logs hyperlane-validator-basesepolia" + echo " docker logs hyperlane-relayer" + echo + + echo -e "${BOLD}State${NC}: $STATE_FILE" +} + +# ─── Main ──────────────────────────────────────────────────────────────────── + +COSMOS_ONLY=false +EVM_ONLY=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --cosmos-only) COSMOS_ONLY=true; shift ;; + --evm-only) EVM_ONLY=true; shift ;; + -h|--help) + echo "Usage: $0 [--cosmos-only|--evm-only]" + echo "Required env: EVM_RELAYER_KEY (or HYP_KEY)" + exit 0 ;; + *) log_err "Unknown flag: $1"; exit 1 ;; + esac +done + +require_binary +require_jq +require_chain_running +command -v cast >/dev/null 2>&1 || { log_err "cast missing: foundryup"; exit 1; } +[[ -n "$EVM_RELAYER_KEY" ]] || { log_err "Set EVM_RELAYER_KEY or HYP_KEY"; exit 1; } + +banner "Transfer Tests" "bitsong <-> basesepolia" + +# Pre-flight: make sure agents are running + validator is caught up +check_agents_ready + +if [[ "$EVM_ONLY" != "true" ]]; then + test_cosmos_to_evm +fi + +if [[ "$COSMOS_ONLY" != "true" ]]; then + test_evm_to_cosmos +fi + +print_summary +log_ok "Tests complete!" diff --git a/scripts/hyperlane/lib.sh b/scripts/hyperlane/lib.sh new file mode 100755 index 00000000..0ecf5d19 --- /dev/null +++ b/scripts/hyperlane/lib.sh @@ -0,0 +1,279 @@ +#!/usr/bin/env bash +# ============================================================================= +# lib.sh — Shared library for BitSong Hyperlane scripts +# +# Source this file from any script: +# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib.sh" +# ============================================================================= + +[[ -n "${_HYPERLANE_LIB_LOADED:-}" ]] && return 0 +_HYPERLANE_LIB_LOADED=1 + +set -euo pipefail + +# ============================================================================= +# Paths +# ============================================================================= + +_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Walk up to project root (contains go.mod) +_PROJECT_DIR="$_LIB_DIR" +while [[ "$_PROJECT_DIR" != "/" && ! -f "$_PROJECT_DIR/go.mod" ]]; do + _PROJECT_DIR="$(dirname "$_PROJECT_DIR")" +done +if [[ ! -f "$_PROJECT_DIR/go.mod" ]]; then + echo "ERROR: Could not find project root (go.mod)." >&2 + exit 1 +fi + +# Auto-load .env if present (private keys, etc. — gitignored) +if [[ -f "$_LIB_DIR/.env" ]]; then + set -a; source "$_LIB_DIR/.env"; set +a +fi + +# ============================================================================= +# Colors & Logging +# ============================================================================= + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' +CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' + +log() { echo -e "${CYAN}[$(date '+%H:%M:%S')]${NC} $*"; } +log_ok() { echo -e "${GREEN}[$(date '+%H:%M:%S')] ✓${NC} $*"; } +log_warn() { echo -e "${YELLOW}[$(date '+%H:%M:%S')] !${NC} $*"; } +log_err() { echo -e "${RED}[$(date '+%H:%M:%S')] ✗${NC} $*"; } +log_step() { echo -e "\n${BOLD}${CYAN}═══ $* ═══${NC}\n"; } + +banner() { + echo -e "${BOLD}${CYAN}" + echo " ╔══════════════════════════════════════════════╗" + printf " ║ %-43s║\n" "$1" + printf " ║ %-43s║\n" "$2" + echo " ╚══════════════════════════════════════════════╝" + echo -e "${NC}" +} + +# ============================================================================= +# Chain Configuration (all overridable via environment) +# ============================================================================= + +CHAIN_ID="${CHAIN_ID:-localbitsong-hyperlane}" +BITSONG_HOME="${BITSONG_HOME:-$HOME/.localbitsong-hyperlane}" +MONIKER="${MONIKER:-val}" +KEY_NAME="${KEY_NAME:-val}" +KEYRING_BACKEND="${KEYRING_BACKEND:-test}" +DENOM="${DENOM:-ubtsg}" +NODE="${NODE:-tcp://localhost:26657}" + +# Auto-detect bitsongd +if [[ -z "${BINARY:-}" ]]; then + if [[ -x "$_PROJECT_DIR/build/bitsongd" ]]; then + BINARY="$_PROJECT_DIR/build/bitsongd" + elif [[ -x "./build/bitsongd" ]]; then + BINARY="./build/bitsongd" + else + BINARY="bitsongd" + fi +fi + +# Validator mnemonic — loaded from .env (never hardcoded) +# Required by 01-chain.sh; optional for other scripts. +VAL_MNEMONIC="${VAL_MNEMONIC:-}" + +# Derive VAL_ADDRESS from keyring (coin type 639, set by bitsongd) +VAL_ADDRESS="${VAL_ADDRESS:-}" +if [[ -z "$VAL_ADDRESS" && -d "$BITSONG_HOME/keyring-$KEYRING_BACKEND" ]]; then + VAL_ADDRESS=$("$BINARY" keys show "$KEY_NAME" \ + --keyring-backend "$KEYRING_BACKEND" --home "$BITSONG_HOME" -a 2>/dev/null) || true +fi + +# ============================================================================= +# Hyperlane / EVM Configuration +# ============================================================================= + +DOMAIN_ID="${DOMAIN_ID:-7171}" +REMOTE_DOMAIN="${REMOTE_DOMAIN:-84532}" +BASESEPOLIA_MAILBOX="${BASESEPOLIA_MAILBOX:-0x6966b0E55883d49BFB24539356a2f8A673E02039}" +EVM_RPC="${EVM_RPC:-https://sepolia.base.org}" + +VALIDATOR_ADDRESSES="${VALIDATOR_ADDRESSES:-0x1111111111111111111111111111111111111111,0x2222222222222222222222222222222222222222,0x3333333333333333333333333333333333333333}" +VALIDATOR_THRESHOLD="${VALIDATOR_THRESHOLD:-2}" +TOKEN_EXCHANGE_RATE="${TOKEN_EXCHANGE_RATE:-10000000000}" +GAS_PRICE="${GAS_PRICE:-1000000000}" +GAS_OVERHEAD="${GAS_OVERHEAD:-75000}" +ENROLL_GAS="${ENROLL_GAS:-300000}" + +# Docker agents +DOCKER_IMAGE="${DOCKER_IMAGE:-gcr.io/abacus-labs-dev/hyperlane-agent:agents-v2.0.0}" +BASESEPOLIA_ISM_FACTORY="${BASESEPOLIA_ISM_FACTORY:-0xfc6e546510dC9d76057F1f76633FCFfC188CB213}" +VALIDATOR_ANNOUNCE_CONTRACT="${VALIDATOR_ANNOUNCE_CONTRACT:-0x20c44b1E3BeaDA1e9826CFd48BeEDABeE9871cE9}" + +# State file (shared across all phases) +STATE_FILE="$BITSONG_HOME/hyperlane-state.json" + +# ============================================================================= +# State Management +# ============================================================================= + +save_state() { + local key="$1" value="$2" + [[ ! -f "$STATE_FILE" ]] && echo '{}' > "$STATE_FILE" + local tmp="${STATE_FILE}.tmp" + jq --arg k "$key" --arg v "$value" '.[$k] = $v' "$STATE_FILE" > "$tmp" && mv "$tmp" "$STATE_FILE" +} + +load_state() { + local key="$1" + [[ -f "$STATE_FILE" ]] && jq -r --arg k "$key" '.[$k] // empty' "$STATE_FILE" 2>/dev/null || true +} + +# ============================================================================= +# Chain Helpers +# ============================================================================= + +wait_for_block() { + log "Waiting for chain to produce blocks..." + for _ in $(seq 1 60); do + local height + height=$("$BINARY" status --node "$NODE" --home "$BITSONG_HOME" 2>&1 \ + | jq -r '.sync_info.latest_block_height // .SyncInfo.latest_block_height // "0"' 2>/dev/null) || height="0" + if [[ "$height" =~ ^[0-9]+$ && "$height" -gt 0 ]]; then + log_ok "Chain producing blocks (height=$height)" + return 0 + fi + sleep 2 + done + log_err "Chain not producing blocks after 120s — check: $BITSONG_HOME/chain.log" + return 1 +} + +# ============================================================================= +# Transaction Helpers +# ============================================================================= + +TX_RESULT="" + +# Submit a bitsongd TX and wait for inclusion (up to 60s). +# Usage: submit_tx "description" $BINARY tx hyperlane ... +submit_tx() { + local desc="$1"; shift + TX_RESULT="" + log "Submitting: $desc" + + local result + result=$("$@" \ + --from "$KEY_NAME" --keyring-backend "$KEYRING_BACKEND" \ + --chain-id "$CHAIN_ID" --node "$NODE" \ + --gas auto --gas-adjustment 1.5 --fees "10000${DENOM}" \ + --output json -y --home "$BITSONG_HOME" 2>&1) || true + + # --gas auto prints "gas estimate: N" before JSON + local json_result + json_result=$(echo "$result" | grep '^\{' | tail -1) || true + + local txhash + txhash=$(echo "$json_result" | jq -r '.txhash // empty' 2>/dev/null) || true + if [[ -z "$txhash" ]]; then + log_err "Failed to submit tx. Output:"; echo "$result"; return 1 + fi + + log " TX: $txhash — waiting for inclusion..." + for _ in $(seq 1 30); do + sleep 2 + TX_RESULT=$("$BINARY" query tx "$txhash" --output json --node "$NODE" --home "$BITSONG_HOME" 2>&1) || true + if echo "$TX_RESULT" | jq -e '.code != null' >/dev/null 2>&1; then break; fi + TX_RESULT="" + done + + [[ -n "$TX_RESULT" ]] || { log_err "TX not found after 60s"; return 1; } + + local code + code=$(echo "$TX_RESULT" | jq -r '.code') + if [[ "$code" != "0" ]]; then + log_err "TX failed (code=$code)" + echo "$TX_RESULT" | jq -r '.raw_log // "unknown"' 2>/dev/null || true + return 1 + fi + log_ok "TX succeeded (code=0)" +} + +# Extract hex ID (0x...) from the last TX_RESULT. +extract_id() { + local id="" + + # Strategy 1: msg_responses + id=$(echo "$TX_RESULT" | jq -r '.msg_responses[0].id // empty' 2>/dev/null) || true + [[ -n "$id" && "$id" != "null" ]] && { echo "$id"; return; } + + # Strategy 2: decode hex data field (protobuf response) + local data + data=$(echo "$TX_RESULT" | jq -r '.data // empty' 2>/dev/null) || true + if [[ -n "$data" ]]; then + id=$(echo "$data" | xxd -r -p | grep -aoP '0x[0-9a-f]{64}' | tail -1) || true + [[ -n "$id" ]] && { echo "$id"; return; } + fi + + # Strategy 3: Hyperlane/Warp events + id=$(echo "$TX_RESULT" | jq -r ' + [.events[]? | select(.type | test("hyperlane|warp")) | + select(.type | test("[Cc]reate")) | + .attributes[]? | select(.key | test("_id$|^id$")) | + select(.key != "msg_index") | .value + ] | first // empty' 2>/dev/null) || true + [[ -n "$id" && "$id" != "null" ]] && { echo "$id" | sed 's/^"//;s/"$//'; return; } + + echo "" +} + +# ============================================================================= +# Address Conversion +# ============================================================================= + +# 20-byte EVM address → 32-byte Hyperlane hex (left-padded) +evm_to_bytes32() { + local addr="${1#0x}" + addr=$(echo "$addr" | tr '[:upper:]' '[:lower:]') + printf "0x%064s" "$addr" | tr ' ' '0' +} + +# bech32 Cosmos address → 32-byte hex +bech32_to_bytes32() { + local bech32_addr="$1" hex="" + + hex=$("$BINARY" keys parse "$bech32_addr" --output json 2>/dev/null \ + | jq -r '.bytes // empty' 2>/dev/null) || true + + if [[ -z "$hex" ]] && command -v python3 >/dev/null 2>&1; then + hex=$(python3 -c " +CHARSET='qpzry9x8gf2tvdw0s3jn54khce6mua7l' +def decode(addr): + _, dp = addr.rsplit('1', 1) + v = [CHARSET.index(c) for c in dp][:-6] + acc, bits, out = 0, 0, [] + for d in v: + acc = (acc << 5) | d; bits += 5 + while bits >= 8: bits -= 8; out.append((acc >> bits) & 0xff) + return bytes(out).hex() +print(decode('$bech32_addr')) +" 2>/dev/null) || true + fi + + [[ -z "$hex" ]] && { log_err "Failed to decode bech32: $bech32_addr"; return 1; } + printf "0x%064s" "$hex" | tr ' ' '0' +} + +# ============================================================================= +# Preflight Helpers +# ============================================================================= + +require_binary() { [[ -x "$BINARY" ]] || command -v "$BINARY" >/dev/null 2>&1 || { log_err "bitsongd not found. Build: LEDGER_ENABLED=false make build"; exit 1; }; } +require_jq() { command -v jq >/dev/null 2>&1 || { log_err "jq missing: sudo apt install jq"; exit 1; }; } +require_chain_running() { "$BINARY" status --node "$NODE" --home "$BITSONG_HOME" >/dev/null 2>&1 || { log_err "Chain not running at $NODE"; exit 1; }; } + +require_state() { + local key="$1" label="${2:-$1}" val + val=$(load_state "$key") + [[ -n "$val" ]] || { log_err "$label not in state file. Run previous phase first."; exit 1; } + echo "$val" +} diff --git a/scripts/hyperlane/run-all.sh b/scripts/hyperlane/run-all.sh new file mode 100755 index 00000000..5840ea91 --- /dev/null +++ b/scripts/hyperlane/run-all.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# ============================================================================= +# run-all.sh — Run all Hyperlane phases in sequence +# +# Usage: +# bash run-all.sh # Run phases 1-5 +# bash run-all.sh --from 3 # Start from phase 3 +# bash run-all.sh --skip-test # Skip transfer tests +# bash run-all.sh --clean # Clean everything first +# ============================================================================= + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +FROM_PHASE=1 +SKIP_TEST=false +CLEAN=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --from) FROM_PHASE="$2"; shift 2 ;; + --skip-test) SKIP_TEST=true; shift ;; + --clean) CLEAN=true; shift ;; + -h|--help) + echo "Usage: $0 [--from N] [--skip-test] [--clean]" + echo "" + echo "Phases:" + echo " 1 Init + start local chain (01-chain.sh)" + echo " 2 Configure Hyperlane bridge (02-hyperlane.sh)" + echo " 3 Deploy HypERC20 + enrollment (03-evm-deploy.sh)" + echo " 4 ISM upgrade + validators/relayer (04-agents.sh)" + echo " 5 Transfer tests (05-test.sh)" + exit 0 ;; + *) echo "Unknown flag: $1"; exit 1 ;; + esac +done + +run_phase() { + local num="$1" script="$2" + shift 2 + if [[ "$FROM_PHASE" -le "$num" ]]; then + echo "" + echo "================================================================" + echo " Phase $num: $script" + echo "================================================================" + echo "" + bash "$SCRIPT_DIR/$script" "$@" + fi +} + +CLEAN_FLAG="" +[[ "$CLEAN" == "true" ]] && CLEAN_FLAG="--clean" + +run_phase 1 "01-chain.sh" $CLEAN_FLAG +run_phase 2 "02-hyperlane.sh" +run_phase 3 "03-evm-deploy.sh" $CLEAN_FLAG +run_phase 4 "04-agents.sh" $CLEAN_FLAG + +if [[ "$SKIP_TEST" != "true" ]]; then + run_phase 5 "05-test.sh" +fi + +echo "" +echo "All phases complete!" diff --git a/scripts/hyperlane/status.sh b/scripts/hyperlane/status.sh new file mode 100755 index 00000000..c6877aec --- /dev/null +++ b/scripts/hyperlane/status.sh @@ -0,0 +1,383 @@ +#!/usr/bin/env bash +# ============================================================================= +# status.sh — Dashboard for Hyperlane agent sync status +# +# Shows: container health, validator checkpoints, relayer block indexing, +# message processing, and warp token state. +# +# Usage: +# bash status.sh # One-shot status +# bash status.sh --watch # Refresh every 10s +# bash status.sh --watch 5 # Refresh every 5s +# bash status.sh --logs # Show recent relayer logs +# bash status.sh --logs 50 # Show last 50 lines +# ============================================================================= + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib.sh" + +# ─── Docker Status ─────────────────────────────────────────────────────────── + +print_containers() { + echo -e "${BOLD}Docker Containers${NC}" + echo -e " NAME STATUS UPTIME HEALTH" + echo -e " ─────────────────────────────────── ────────── ─────────────── ──────" + + for name in hyperlane-validator-bitsong hyperlane-validator-basesepolia hyperlane-relayer; do + local info + info=$(docker inspect "$name" --format '{{.State.Status}}|{{.State.StartedAt}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}n/a{{end}}' 2>/dev/null) || info="" + if [[ -z "$info" ]]; then + echo -e " ${name}$(printf '%*s' $((35 - ${#name})) '') ${RED}absent${NC} - -" + else + local status started health + IFS='|' read -r status started health <<< "$info" + local uptime="-" + if [[ "$status" == "running" && -n "$started" ]]; then + local start_epoch now_epoch + start_epoch=$(date -d "$started" +%s 2>/dev/null) || start_epoch=0 + now_epoch=$(date +%s) + local diff=$(( now_epoch - start_epoch )) + if [[ $diff -lt 60 ]]; then uptime="${diff}s" + elif [[ $diff -lt 3600 ]]; then uptime="$(( diff / 60 ))m$(( diff % 60 ))s" + else uptime="$(( diff / 3600 ))h$(( (diff % 3600) / 60 ))m" + fi + fi + local color="$GREEN" + [[ "$status" != "running" ]] && color="$RED" + echo -e " ${name}$(printf '%*s' $((35 - ${#name})) '') ${color}${status}${NC}$(printf '%*s' $((10 - ${#status})) '') ${uptime}$(printf '%*s' $((15 - ${#uptime})) '') ${health:-n/a}" + fi + done + echo +} + +# ─── Chain Heights ─────────────────────────────────────────────────────────── + +print_chain_heights() { + echo -e "${BOLD}Chain Heights${NC}" + + local bitsong_height="-" + if "$BINARY" status --node "$NODE" --home "$BITSONG_HOME" >/dev/null 2>&1; then + bitsong_height=$("$BINARY" status --node "$NODE" --home "$BITSONG_HOME" 2>/dev/null \ + | jq -r '.sync_info.latest_block_height // "?"' 2>/dev/null) || bitsong_height="?" + fi + echo -e " BitSong (local): $bitsong_height" + + if command -v cast >/dev/null 2>&1; then + local basesep_height + basesep_height=$(cast block-number --rpc-url "$EVM_RPC" 2>/dev/null) || basesep_height="?" + echo -e " Base Sepolia: $basesep_height" + fi + echo +} + +# ─── Validator Checkpoints ─────────────────────────────────────────────────── + +print_checkpoints() { + echo -e "${BOLD}Validator Checkpoints${NC}" + + for chain in bitsong basesepolia; do + local cp_dir="$BITSONG_HOME/checkpoints-${chain}" + if [[ ! -d "$cp_dir" ]]; then + echo -e " $chain: ${YELLOW}no checkpoint dir${NC}" + continue + fi + + local latest_idx="-" + local checkpoint_file="$cp_dir/index.json" + if [[ -f "$checkpoint_file" ]]; then + # index.json contains either a plain number or JSON with .latest_index/.index + local raw + raw=$(cat "$checkpoint_file" 2>/dev/null) || raw="" + if [[ "$raw" =~ ^[0-9]+$ ]]; then + latest_idx="$raw" + else + latest_idx=$(echo "$raw" | jq -r '.latest_index // .index // "?"' 2>/dev/null) || latest_idx="?" + fi + fi + + local sig_count + sig_count=$(find "$cp_dir" -name "*.json" -not -name "checkpoint_*" -not -name "announcement*" 2>/dev/null | wc -l) || sig_count=0 + + echo -e " $chain: latest_index=$latest_idx signatures=$sig_count" + done + echo +} + +# ─── Relayer Sync Progress ────────────────────────────────────────────────── + +print_relayer_sync() { + echo -e "${BOLD}Relayer Sync Progress${NC}" + + if ! docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^hyperlane-relayer$"; then + echo -e " ${YELLOW}Relayer not running${NC}" + echo + return + fi + + # Strip ANSI helper — Rust tracing embeds color codes around field names + local strip_ansi='s/\x1b\[[0-9;]*m//g' + + # Per-chain block indexing — single docker call, then split locally. + # Use --since 5m (not --tail) because basesepolia tx_id lookups are extremely + # verbose (thousands of lines between each ~30s cursor entry). + # + # Two cursor types exist: + # RateLimitedContractSyncCursor — has tip/next_block/start_block (gas_payments) + # ForwardBackwardSequenceAwareSyncCursor — has at_block/sequence (dispatched_messages) + # We prefer RateLimited (more useful block progress data), fall back to any. + local all_sync_logs + all_sync_logs=$(docker logs hyperlane-relayer --since 5m 2>&1 \ + | sed "$strip_ansi" \ + | grep "HyperlaneDomain(") || all_sync_logs="" + + for chain in bitsong basesepolia; do + # Prefer RateLimited cursor (has tip/next_block — best for block progress) + local chain_log + chain_log=$(echo "$all_sync_logs" \ + | grep "HyperlaneDomain(${chain}" \ + | grep "RateLimitedContractSyncCursor" \ + | tail -1) || true + # Fallback: any cursor type + if [[ -z "$chain_log" ]]; then + chain_log=$(echo "$all_sync_logs" \ + | grep "HyperlaneDomain(${chain}" \ + | tail -1) || true + fi + + if [[ -n "$chain_log" ]]; then + local sync_status + sync_status=$(echo "$chain_log" | grep -oP 'estimated_time_to_sync: "\K[^"]+') || sync_status="" + + # Try RateLimited fields: tip, next_block, start_block, chunk_size + local tip next_block start_block chunk_size + tip=$(echo "$chain_log" | grep -oP 'tip: \K[0-9]+' | head -1) || tip="" + next_block=$(echo "$chain_log" | grep -oP 'next_block: \K[0-9]+' | head -1) || next_block="" + start_block=$(echo "$chain_log" | grep -oP 'start_block: \K[0-9]+' | head -1) || start_block="" + chunk_size=$(echo "$chain_log" | grep -oP 'chunk_size: \K[0-9]+' | head -1) || chunk_size="" + + # Fallback for ForwardBackward cursor: use at_block from last_indexed_snapshot + if [[ -z "$tip" ]]; then + tip=$(echo "$chain_log" | grep -oP 'at_block: \K[0-9]+' | tail -1) || tip="" + # Use range field: "range: A..=B" + next_block=$(echo "$chain_log" | grep -oP 'range: \K[0-9]+') || next_block="" + fi + + if [[ -n "$tip" ]]; then + # Calculate progress percentage + local pct="-" + if [[ -n "$start_block" && -n "$next_block" ]]; then + local scanned=$(( next_block - start_block )) + local total=$(( tip - start_block + 1 )) + if [[ "$total" -gt 0 ]]; then + pct=$(( scanned * 100 / total )) + fi + fi + + if [[ "$sync_status" == "synced" ]]; then + echo -e " $chain: block ${tip} ${GREEN}synced${NC}" + else + # Not synced — show block progress, percentage, chunk size, ETA + local status_text="" + if [[ -n "$sync_status" ]]; then + status_text="ETA: $sync_status" + else + status_text="scanning" + fi + + local progress_detail="${next_block:-?}/${tip}" + if [[ "$pct" != "-" ]]; then + progress_detail="${progress_detail} (${pct}%)" + fi + echo -e " $chain: ${progress_detail} chunk=${chunk_size:-?} ${YELLOW}${status_text}${NC}" + fi + else + echo -e " $chain: ${YELLOW}syncing...${NC}" + fi + else + echo -e " $chain: ${YELLOW}no progress data yet${NC}" + fi + done + echo + + # Grab recent logs for stats (submitted/finalized/errors) + local stats_logs + stats_logs=$(docker logs hyperlane-relayer --since 10m 2>&1 \ + | sed "$strip_ansi") || stats_logs="" + + # Pending tx_id lookups (basesepolia generates many — shows indexer backlog) + local pending_ids + pending_ids=$(echo "$stats_logs" | grep -oP 'pending_ids: \K[0-9]+' | tail -1) || true + if [[ -n "$pending_ids" && "$pending_ids" != "0" ]]; then + echo -e " Pending tx_id lookups: $pending_ids" + fi + + # Finality pool size + local pool_size + pool_size=$(echo "$stats_logs" | grep -oP 'pool_size: \K[0-9]+' | tail -1) || true + if [[ -n "$pool_size" ]]; then + echo -e " Finality pool: $pool_size txs" + fi + + # Relay stats — match actual v2 log patterns + # Transaction lifecycle: submitting → PendingInclusion → Mempool → Included → Finalized + # "Message successfully processed" = confirmed delivery + local submitted_count finalized_count processed_count error_count real_errors + submitted_count=$(echo "$stats_logs" | grep -ci "submitting transaction" 2>/dev/null) || submitted_count=0 + finalized_count=$(echo "$stats_logs" | grep -ci "new_status: Finalized" 2>/dev/null) || finalized_count=0 + processed_count=$(echo "$stats_logs" | grep -ci "Message successfully processed" 2>/dev/null) || processed_count=0 + error_count=$(echo "$stats_logs" | grep -ci "error\|failed" 2>/dev/null) || error_count=0 + real_errors=$(echo "$stats_logs" | grep -i "error\|failed" | grep -cv "0xa2827cb39\|CCIP\|verification" 2>/dev/null) || real_errors=0 + + echo -e " Messages: ${GREEN}${processed_count} delivered${NC} ${submitted_count} submitted ${finalized_count} finalized" + if [[ "$real_errors" -gt 0 ]]; then + echo -e " Errors: ${RED}${real_errors} real${NC} (${error_count} total incl. CCIP noise)" + else + echo -e " Errors: 0 (${error_count} total incl. CCIP noise)" + fi + + # Show latest relay transaction status (most useful when waiting for a transfer) + local last_tx_status + last_tx_status=$(echo "$stats_logs" \ + | grep -oP 'Updating tx status.*?new_status: \K\w+' \ + | tail -1) || true + if [[ -n "$last_tx_status" ]]; then + local last_tx_fn + last_tx_fn=$(echo "$stats_logs" \ + | grep "Updating tx status" \ + | tail -1 \ + | grep -oP 'function\.name: "\K[^"]+') || last_tx_fn="" + local last_tx_to + last_tx_to=$(echo "$stats_logs" \ + | grep "Updating tx status" \ + | tail -1 \ + | grep -oP 'tx\.to: Some\(\K0x[0-9a-fA-F]+') || last_tx_to="" + local tx_color="$YELLOW" + [[ "$last_tx_status" == "Finalized" ]] && tx_color="$GREEN" + echo -e " Last tx: ${tx_color}${last_tx_status}${NC} ${last_tx_fn:+fn=$last_tx_fn} ${last_tx_to:+to=${last_tx_to:0:14}...}" + fi + echo +} + +# ─── Warp Token State ─────────────────────────────────────────────────────── + +print_warp_state() { + echo -e "${BOLD}Warp Token State${NC}" + + local evm_hyp_erc20 token_id + evm_hyp_erc20=$(load_state "evm_hyp_erc20") + token_id=$(load_state "token_id") + + if [[ -n "$evm_hyp_erc20" ]] && command -v cast >/dev/null 2>&1; then + local supply ism + supply=$(cast call "$evm_hyp_erc20" "totalSupply()(uint256)" --rpc-url "$EVM_RPC" 2>/dev/null) || supply="?" + ism=$(cast call "$evm_hyp_erc20" "interchainSecurityModule()(address)" --rpc-url "$EVM_RPC" 2>/dev/null) || ism="?" + echo -e " EVM HypERC20: $evm_hyp_erc20" + echo -e " EVM totalSupply: $supply" + echo -e " EVM ISM: $ism" + else + echo -e " EVM HypERC20: ${YELLOW}not deployed${NC}" + fi + + if [[ -n "$token_id" ]] && "$BINARY" status --node "$NODE" --home "$BITSONG_HOME" >/dev/null 2>&1; then + echo -e " Cosmos token: $token_id" + fi + echo +} + +# ─── Test Results ──────────────────────────────────────────────────────────── + +print_test_results() { + echo -e "${BOLD}Transfer Tests${NC}" + local c2e e2c + c2e=$(load_state "cosmos_to_evm_test_passed") + e2c=$(load_state "evm_to_cosmos_test_passed") + if [[ -n "$c2e" ]]; then + echo -e " Cosmos -> EVM: ${GREEN}$c2e${NC}" + else + echo -e " Cosmos -> EVM: ${YELLOW}not run${NC}" + fi + if [[ -n "$e2c" ]]; then + echo -e " EVM -> Cosmos: ${GREEN}$e2c${NC}" + else + echo -e " EVM -> Cosmos: ${YELLOW}not run${NC}" + fi + echo +} + +# ─── Show Logs ─────────────────────────────────────────────────────────────── + +show_logs() { + local lines="${1:-30}" + log_step "Relayer Logs (last $lines lines)" + + if ! docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^hyperlane-relayer$"; then + log_warn "Relayer not running" + return + fi + + docker logs hyperlane-relayer --tail "$lines" 2>&1 \ + | grep -v "0xa2827cb39\|CCIP Read" || true +} + +# ─── Full Dashboard ───────────────────────────────────────────────────────── + +print_dashboard() { + clear 2>/dev/null || true + echo -e "${BOLD}${CYAN}═══════════════════════════════════════════════${NC}" + echo -e "${BOLD}${CYAN} Hyperlane Status — $(date '+%H:%M:%S')${NC}" + echo -e "${BOLD}${CYAN}═══════════════════════════════════════════════${NC}" + echo + + print_containers + print_chain_heights + print_checkpoints + print_relayer_sync + print_warp_state + print_test_results +} + +# ─── Main ──────────────────────────────────────────────────────────────────── + +WATCH=false +WATCH_INTERVAL=10 +SHOW_LOGS=false +LOG_LINES=30 + +while [[ $# -gt 0 ]]; do + case "$1" in + --watch) + WATCH=true + if [[ -n "${2:-}" && "$2" =~ ^[0-9]+$ ]]; then + WATCH_INTERVAL="$2"; shift + fi + shift ;; + --logs) + SHOW_LOGS=true + if [[ -n "${2:-}" && "$2" =~ ^[0-9]+$ ]]; then + LOG_LINES="$2"; shift + fi + shift ;; + -h|--help) + echo "Usage: $0 [--watch [interval]] [--logs [lines]]" + echo "" + echo " --watch [N] Refresh every N seconds (default: 10)" + echo " --logs [N] Show last N lines of relayer logs (default: 30)" + exit 0 ;; + *) echo "Unknown flag: $1"; exit 1 ;; + esac +done + +if [[ "$SHOW_LOGS" == "true" ]]; then + show_logs "$LOG_LINES" + exit 0 +fi + +if [[ "$WATCH" == "true" ]]; then + trap 'echo; echo "Stopped."; exit 0' INT + while true; do + print_dashboard + echo -e "${CYAN}Refreshing in ${WATCH_INTERVAL}s... (Ctrl+C to stop)${NC}" + sleep "$WATCH_INTERVAL" + done +else + print_dashboard +fi diff --git a/scripts/hyperlane/stop.sh b/scripts/hyperlane/stop.sh new file mode 100755 index 00000000..b6c5f2ab --- /dev/null +++ b/scripts/hyperlane/stop.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# ============================================================================= +# stop.sh — Stop BitSong chain and/or Hyperlane Docker agents +# +# Usage: +# bash stop.sh # Stop everything +# bash stop.sh --chain-only # Stop chain only +# bash stop.sh --agents-only # Stop Docker agents only +# bash stop.sh --clean # Stop everything + wipe all data +# ============================================================================= + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib.sh" + +CLEAN=false +CHAIN_ONLY=false +AGENTS_ONLY=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --clean) CLEAN=true; shift ;; + --chain-only) CHAIN_ONLY=true; shift ;; + --agents-only) AGENTS_ONLY=true; shift ;; + -h|--help) + echo "Usage: $0 [--clean] [--chain-only] [--agents-only]" + exit 0 ;; + *) echo "Unknown flag: $1"; exit 1 ;; + esac +done + +# ─── Stop Docker Agents ────────────────────────────────────────────────────── + +if [[ "$CHAIN_ONLY" == "false" ]]; then + log "Stopping Hyperlane Docker agents..." + for name in hyperlane-validator-bitsong hyperlane-validator-basesepolia hyperlane-relayer; do + if docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${name}$"; then + docker stop "$name" 2>/dev/null || true + docker rm -f "$name" 2>/dev/null || true + log_ok "Stopped $name" + fi + done +fi + +# ─── Stop Chain ────────────────────────────────────────────────────────────── + +if [[ "$AGENTS_ONLY" == "false" ]]; then + local_pid_file="$BITSONG_HOME/chain.pid" + if [[ -f "$local_pid_file" ]]; then + PID=$(cat "$local_pid_file") + if kill -0 "$PID" 2>/dev/null; then + log "Stopping chain (PID=$PID)..." + kill "$PID" 2>/dev/null || true + for _ in $(seq 1 20); do + kill -0 "$PID" 2>/dev/null || break + sleep 0.5 + done + if kill -0 "$PID" 2>/dev/null; then + log_warn "Force killing chain..." + kill -9 "$PID" 2>/dev/null || true + fi + log_ok "Chain stopped" + else + log "Chain not running (stale PID=$PID)" + fi + rm -f "$local_pid_file" + else + log "No chain PID file found" + fi +fi + +# ─── Clean ─────────────────────────────────────────────────────────────────── + +if [[ "$CLEAN" == "true" ]]; then + log "Removing all data at $BITSONG_HOME..." + rm -rf "$BITSONG_HOME" + log_ok "Clean complete" +fi From bc637a7915692b7451e107632485d5447174b7e3 Mon Sep 17 00:00:00 2001 From: angelorc Date: Thu, 19 Feb 2026 16:50:18 +0100 Subject: [PATCH 4/5] add fantoken support --- scripts/hyperlane/01-chain.sh | 7 +- scripts/hyperlane/06-fantoken-route.sh | 638 +++++++++++++++++++++++++ scripts/hyperlane/07-fantoken-test.sh | 363 ++++++++++++++ scripts/hyperlane/run-all.sh | 15 + scripts/hyperlane/status.sh | 66 ++- 5 files changed, 1083 insertions(+), 6 deletions(-) create mode 100755 scripts/hyperlane/06-fantoken-route.sh create mode 100755 scripts/hyperlane/07-fantoken-test.sh diff --git a/scripts/hyperlane/01-chain.sh b/scripts/hyperlane/01-chain.sh index 8e8e6bc9..4ee98542 100755 --- a/scripts/hyperlane/01-chain.sh +++ b/scripts/hyperlane/01-chain.sh @@ -48,10 +48,13 @@ init_chain() { .app_state.gov.params.expedited_min_deposit[0].denom = "ubtsg" | .app_state.mint.params.mint_denom = "ubtsg" | .app_state.bank.denom_metadata = [{ - "description": "Registered denom ubtsg for localbitsong-hyperlane testing", + "description": "denom ubtsg for testing", "denom_units": [{"denom": "ubtsg", "exponent": 0}], "base": "ubtsg", "display": "ubtsg", "name": "ubtsg", "symbol": "ubtsg" - }] + }] | + .app_state.fantoken.params.issue_fee = {"denom": "ubtsg", "amount": "1000000000"} | + .app_state.fantoken.params.mint_fee = {"denom": "ubtsg", "amount": "0"} | + .app_state.fantoken.params.burn_fee = {"denom": "ubtsg", "amount": "0"} ' "$genesis" > "$tmp" && mv "$tmp" "$genesis" log_ok "Genesis modified" diff --git a/scripts/hyperlane/06-fantoken-route.sh b/scripts/hyperlane/06-fantoken-route.sh new file mode 100755 index 00000000..bf73487c --- /dev/null +++ b/scripts/hyperlane/06-fantoken-route.sh @@ -0,0 +1,638 @@ +#!/usr/bin/env bash +# ============================================================================= +# 06-fantoken-route.sh — Issue a fantoken and create its Hyperlane warp route +# +# Supports multiple fantoken warp routes, each stored independently in state +# using per-symbol prefixed keys (ft__*). +# +# Usage: +# bash 06-fantoken-route.sh --symbol clay --name "Clay Token" # Register new +# bash 06-fantoken-route.sh --symbol clay # Resume/show +# bash 06-fantoken-route.sh --list # List all routes +# bash 06-fantoken-route.sh --symbol clay --clean # Wipe & redo +# +# Required environment: +# HYP_KEY (or EVM_PRIVATE_KEY) — EVM deployer private key (0x...) +# EVM_RELAYER_KEY — for relayer restart +# VALIDATOR_KEY, COSMOS_SIGNER_KEY — for relayer restart +# ============================================================================= + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib.sh" + +HYP_KEY="${HYP_KEY:-${EVM_PRIVATE_KEY:-}}" +EVM_RELAYER_KEY="${EVM_RELAYER_KEY:-${HYP_KEY:-}}" + +FT_MAX_SUPPLY="${FT_MAX_SUPPLY:-1000000000000}" +FT_MINT_AMOUNT="${FT_MINT_AMOUNT:-1000000000}" + +HYP_CHAINS_DIR="${HYP_CHAINS_DIR:-$HOME/.hyperlane/chains}" + +# ============================================================================= +# EVM nonce helper — wait for pending TX to confirm +# ============================================================================= + +# Wait until EVM pending nonce equals confirmed nonce (no pending TXs). +# Prevents "replacement transaction underpriced" when sending back-to-back. +wait_evm_pending() { + local addr + addr=$(cast wallet address --private-key "$HYP_KEY" 2>/dev/null) || return 0 + log "Waiting for pending EVM TXs to confirm ($addr)..." + for _ in $(seq 1 30); do + local pending confirmed + pending=$(cast nonce "$addr" --rpc-url "$EVM_RPC" --pending 2>/dev/null) || break + confirmed=$(cast nonce "$addr" --rpc-url "$EVM_RPC" 2>/dev/null) || break + if [[ "$pending" == "$confirmed" ]]; then + log_ok "EVM nonce settled (nonce=$confirmed)" + return 0 + fi + sleep 3 + done + log_warn "Timed out waiting for EVM pending TXs (proceeding anyway)" +} + +# ============================================================================= +# Per-symbol state helpers +# ============================================================================= + +FT_KEY="" # lowercased symbol, set by CLI parsing + +ft_save() { save_state "ft_${FT_KEY}_$1" "$2"; } +ft_load() { load_state "ft_${FT_KEY}_$1"; } + +# Write flat aliases so test/status scripts work with the latest route +ft_set_current() { + save_state "ft_denom" "$(ft_load denom)" + save_state "ft_minted" "$(ft_load minted)" + save_state "ft_token_id" "$(ft_load token_id)" + save_state "ft_evm_hyp_erc20" "$(ft_load evm_hyp_erc20)" + save_state "ft_evm_hyp_erc20_bytes32" "$(ft_load evm_hyp_erc20_bytes32)" + save_state "ft_router_enrolled" "$(ft_load router_enrolled)" + save_state "ft_evm_router_enrolled" "$(ft_load evm_router_enrolled)" + save_state "ft_evm_ism_set" "$(ft_load evm_ism_set)" + save_state "ft_relayer_restarted" "$(ft_load relayer_restarted)" +} + +# Add symbol to ft_route_list if not already present +register_route() { + local list + list=$(load_state "ft_route_list") + if [[ -z "$list" ]]; then + save_state "ft_route_list" "$FT_KEY" + elif ! echo ",$list," | grep -q ",$FT_KEY,"; then + save_state "ft_route_list" "${list},${FT_KEY}" + fi +} + +# Remove symbol from ft_route_list +unregister_route() { + local list + list=$(load_state "ft_route_list") + [[ -z "$list" ]] && return 0 + # Remove the symbol and clean up commas + local new_list + new_list=$(echo "$list" | tr ',' '\n' | grep -v "^${FT_KEY}$" | paste -sd ',' -) || true + save_state "ft_route_list" "$new_list" +} + +# ============================================================================= +# Migration: detect old flat ft_* keys with no ft_route_list +# ============================================================================= + +migrate_legacy_state() { + local route_list + route_list=$(load_state "ft_route_list") + [[ -n "$route_list" ]] && return 0 # Already migrated + + local old_denom + old_denom=$(load_state "ft_denom") + [[ -z "$old_denom" ]] && return 0 # No legacy state + + log "Migrating legacy fantoken state..." + + # Try to derive symbol from chain query + local old_symbol="" + if "$BINARY" status --node "$NODE" --home "$BITSONG_HOME" >/dev/null 2>&1; then + old_symbol=$("$BINARY" query fantoken denom "$old_denom" \ + --output json --node "$NODE" --home "$BITSONG_HOME" 2>/dev/null | \ + jq -r '.fantoken.symbol // empty' 2>/dev/null) || true + fi + if [[ -z "$old_symbol" ]]; then + old_symbol="legacy" + log_warn "Could not query symbol for $old_denom, using 'legacy'" + fi + old_symbol=$(echo "$old_symbol" | tr '[:upper:]' '[:lower:]') + + # Copy flat keys to per-symbol keys + for suffix in denom minted token_id evm_hyp_erc20 evm_hyp_erc20_bytes32 \ + router_enrolled evm_router_enrolled evm_ism_set relayer_restarted; do + local val + val=$(load_state "ft_${suffix}") + [[ -n "$val" ]] && save_state "ft_${old_symbol}_${suffix}" "$val" + done + + save_state "ft_route_list" "$old_symbol" + log_ok "Migrated legacy state to symbol '$old_symbol'" +} + +# ============================================================================= +# --list: show all registered routes +# ============================================================================= + +list_routes() { + local route_list + route_list=$(load_state "ft_route_list") + if [[ -z "$route_list" ]]; then + echo "No fantoken routes registered." + return 0 + fi + + echo -e "${BOLD}Registered Fantoken Routes${NC}" + echo -e " SYMBOL DENOM EVM HYERC20 STATUS" + echo -e " ──────── ────────────────────────────────────────────── ────────────────────────────────────────────── ──────────" + + IFS=',' read -ra routes <<< "$route_list" + for sym in "${routes[@]}"; do + [[ -z "$sym" ]] && continue + local denom evm_hyp status_text + denom=$(load_state "ft_${sym}_denom") + evm_hyp=$(load_state "ft_${sym}_evm_hyp_erc20") + local ism_set + ism_set=$(load_state "ft_${sym}_evm_ism_set") + + if [[ -n "$evm_hyp" && "$ism_set" == "true" ]]; then + status_text="${GREEN}complete${NC}" + elif [[ -n "$evm_hyp" ]]; then + status_text="${YELLOW}partial: no ISM${NC}" + elif [[ -n "$denom" ]]; then + status_text="${YELLOW}partial: no EVM deploy${NC}" + else + status_text="${RED}empty${NC}" + fi + + printf " %-10s %-50s %-44s " "$sym" "${denom:-—}" "${evm_hyp:-—}" + echo -e "$status_text" + done + echo +} + +# ============================================================================= +# Build dynamic relayer whitelist from all routes +# ============================================================================= + +build_whitelist() { + local whitelist="[" + + # Always include ubtsg route + local token_id evm_hyp_erc20 + token_id=$(load_state "token_id") + evm_hyp_erc20=$(load_state "evm_hyp_erc20") + if [[ -n "$token_id" && -n "$evm_hyp_erc20" ]]; then + whitelist+="{\"senderAddress\":\"${token_id}\",\"destinationDomain\":\"${REMOTE_DOMAIN}\"}," + whitelist+="{\"senderAddress\":\"$(evm_to_bytes32 "$evm_hyp_erc20")\",\"destinationDomain\":\"${DOMAIN_ID}\"}," + fi + + # Add each fantoken route + local route_list + route_list=$(load_state "ft_route_list") + if [[ -n "$route_list" ]]; then + IFS=',' read -ra routes <<< "$route_list" + for sym in "${routes[@]}"; do + [[ -z "$sym" ]] && continue + local tid evm + tid=$(load_state "ft_${sym}_token_id") + evm=$(load_state "ft_${sym}_evm_hyp_erc20") + [[ -n "$tid" && -n "$evm" ]] || continue + whitelist+="{\"senderAddress\":\"${tid}\",\"destinationDomain\":\"${REMOTE_DOMAIN}\"}," + whitelist+="{\"senderAddress\":\"$(evm_to_bytes32 "$evm")\",\"destinationDomain\":\"${DOMAIN_ID}\"}," + done + fi + + whitelist="${whitelist%,}]" # Remove trailing comma + echo "$whitelist" +} + +# ============================================================================= +# Step 1: Issue Fantoken +# ============================================================================= + +issue_fantoken() { + log_step "Step 1: Issue Fantoken ($FT_SYMBOL)" + + FT_DENOM=$(ft_load "denom") + if [[ -n "$FT_DENOM" ]]; then + log_ok "Fantoken already issued: $FT_DENOM" + return 0 + fi + + [[ -n "$FT_NAME" ]] || { log_err "--name is required when issuing a new fantoken"; return 1; } + + submit_tx "Issue fantoken ($FT_SYMBOL)" \ + "$BINARY" tx fantoken issue \ + --symbol "$FT_SYMBOL" --name "$FT_NAME" --max-supply "$FT_MAX_SUPPLY" + + # Extract denom from protobuf data field (MsgIssueResponse contains the denom) + local data + data=$(echo "$TX_RESULT" | jq -r '.data // empty' 2>/dev/null) || true + if [[ -n "$data" ]]; then + FT_DENOM=$(echo "$data" | xxd -r -p | grep -aoP 'ft[0-9A-Fa-f]{40}' | head -1) || true + fi + + # Fallback: query fantokens by authority + if [[ -z "$FT_DENOM" ]]; then + log "Extracting denom from data failed, querying by authority..." + FT_DENOM=$("$BINARY" query fantoken authority "$VAL_ADDRESS" \ + --output json --node "$NODE" --home "$BITSONG_HOME" 2>/dev/null | \ + jq -r --arg sym "$FT_SYMBOL" ' + [.fantokens[]? | select(.symbol == $sym) | .denom] | first // empty + ' 2>/dev/null) || true + fi + + [[ -n "$FT_DENOM" ]] || { log_err "Could not extract fantoken denom"; return 1; } + ft_save "denom" "$FT_DENOM" + log_ok "Fantoken issued: $FT_DENOM" +} + +# ============================================================================= +# Step 2: Mint Fantoken +# ============================================================================= + +mint_fantoken() { + log_step "Step 2: Mint Fantoken to Validator ($FT_SYMBOL)" + + if [[ "$(ft_load "minted")" == "true" ]]; then + log_ok "Already minted" + return 0 + fi + + submit_tx "Mint ${FT_MINT_AMOUNT} ${FT_DENOM}" \ + "$BINARY" tx fantoken mint "${FT_MINT_AMOUNT}${FT_DENOM}" \ + --recipient "$VAL_ADDRESS" + + ft_save "minted" "true" + + local balance + balance=$("$BINARY" query bank balance "$VAL_ADDRESS" "$FT_DENOM" \ + --output json --node "$NODE" --home "$BITSONG_HOME" 2>/dev/null | \ + jq -r '.balance.amount // "?"' 2>/dev/null) || balance="?" + log_ok "Minted. Validator balance: ${balance} ${FT_DENOM}" +} + +# ============================================================================= +# Step 3: Create Collateral Token +# ============================================================================= + +create_ft_collateral() { + log_step "Step 3: Create Collateral Token ($FT_SYMBOL)" + + FT_TOKEN_ID=$(ft_load "token_id") + if [[ -n "$FT_TOKEN_ID" ]]; then + log_ok "Collateral token exists: $FT_TOKEN_ID" + return 0 + fi + + local mailbox_id; mailbox_id=$(require_state "mailbox_id" "mailbox_id") + + submit_tx "Create collateral token ($FT_DENOM)" \ + "$BINARY" tx warp create-collateral-token "$mailbox_id" "$FT_DENOM" + + FT_TOKEN_ID=$(extract_id) + [[ -n "$FT_TOKEN_ID" ]] || { log_err "Could not extract ft_token_id"; return 1; } + ft_save "token_id" "$FT_TOKEN_ID" + log_ok "Cosmos token: $FT_TOKEN_ID" +} + +# ============================================================================= +# Step 4: Deploy HypERC20 on Base Sepolia +# ============================================================================= + +deploy_ft_hyp_erc20() { + log_step "Step 4: Deploy HypERC20 for $FT_SYMBOL" + + FT_EVM_HYP_ERC20=$(ft_load "evm_hyp_erc20") + if [[ -n "$FT_EVM_HYP_ERC20" && "$FT_EVM_HYP_ERC20" =~ ^0x[0-9a-fA-F]{40}$ ]]; then + log_ok "HypERC20 already deployed: $FT_EVM_HYP_ERC20" + return 0 + fi + + local deployer_addr + deployer_addr=$(cast wallet address --private-key "$HYP_KEY" 2>/dev/null) \ + || { log_err "Cannot derive EVM address from HYP_KEY"; return 1; } + + # Generate single-chain warp config for fantoken + local warp_config="$BITSONG_HOME/warp-route-ft-${FT_KEY}-basesepolia.yaml" + cat > "$warp_config" << EOF +basesepolia: + type: synthetic + name: "$FT_NAME" + symbol: "$FT_SYMBOL" + decimals: 6 + mailbox: "$BASESEPOLIA_MAILBOX" + owner: "$deployer_addr" + interchainSecurityModule: "0x0000000000000000000000000000000000000000" +EOF + + log "Warp config:" + cat "$warp_config"; echo + + export HYP_KEY + log "Running: hyperlane warp deploy ..." + hyperlane warp deploy --config "$warp_config" --yes + + # Extract deployed address from CLI artifacts + log "Looking for deployment artifacts..." + local latest_warp + latest_warp=$(find "$HOME/.hyperlane/deployments/warp_routes" -name "*basesepolia*config.yaml" 2>/dev/null \ + | sort -r | head -1) || true + + if [[ -n "$latest_warp" ]]; then + log "Artifact: $latest_warp" + FT_EVM_HYP_ERC20=$(grep -oP '(?<=addressOrDenom: ")[^"]+' "$latest_warp" 2>/dev/null | head -1) || true + fi + + if [[ -z "$FT_EVM_HYP_ERC20" || ! "$FT_EVM_HYP_ERC20" =~ ^0x[0-9a-fA-F]{40}$ ]]; then + log_err "Could not extract HypERC20 address from CLI artifacts." + log_err "Set manually: jq '.ft_${FT_KEY}_evm_hyp_erc20=\"0xADDR\"' $STATE_FILE > /tmp/s && mv /tmp/s $STATE_FILE" + return 1 + fi + + ft_save "evm_hyp_erc20" "$FT_EVM_HYP_ERC20" + log_ok "FT HypERC20: $FT_EVM_HYP_ERC20" + + local evm_bytes32; evm_bytes32=$(evm_to_bytes32 "$FT_EVM_HYP_ERC20") + ft_save "evm_hyp_erc20_bytes32" "$evm_bytes32" + log " bytes32: $evm_bytes32" +} + +# ============================================================================= +# Step 5: Enroll Remote Routers (bidirectional) +# ============================================================================= + +enroll_ft_routers() { + log_step "Step 5: Enroll Remote Routers ($FT_SYMBOL)" + + # Cosmos side: enroll EVM HypERC20 as remote router + if [[ "$(ft_load "router_enrolled")" != "true" ]]; then + local evm_bytes32 + evm_bytes32=$(ft_load "evm_hyp_erc20_bytes32") + [[ -n "$evm_bytes32" ]] || { log_err "ft_${FT_KEY}_evm_hyp_erc20_bytes32 not in state"; return 1; } + + submit_tx "Enroll FT remote router on Cosmos (domain=$REMOTE_DOMAIN)" \ + "$BINARY" tx warp enroll-remote-router \ + "$FT_TOKEN_ID" "$REMOTE_DOMAIN" "$evm_bytes32" "$ENROLL_GAS" + + ft_save "router_enrolled" "true" + log_ok "Cosmos enrolled: domain $REMOTE_DOMAIN -> $evm_bytes32" + else + log_ok "Cosmos router already enrolled" + fi + + # EVM side: enroll Cosmos token as remote router + if [[ "$(ft_load "evm_router_enrolled")" != "true" ]]; then + log "enrollRemoteRouter($DOMAIN_ID, $FT_TOKEN_ID) on $FT_EVM_HYP_ERC20 ..." + local tx; tx=$(cast send "$FT_EVM_HYP_ERC20" \ + "enrollRemoteRouter(uint32,bytes32)" "$DOMAIN_ID" "$FT_TOKEN_ID" \ + --private-key "$HYP_KEY" --rpc-url "$EVM_RPC" --json 2>&1) || true + + local status; status=$(echo "$tx" | jq -r '.status // empty' 2>/dev/null) || true + local hash; hash=$(echo "$tx" | jq -r '.transactionHash // empty' 2>/dev/null) || true + + if [[ "$status" == "0x1" || "$status" == "1" ]]; then + ft_save "evm_router_enrolled" "true" + log_ok "EVM enrolled: domain $DOMAIN_ID -> $FT_TOKEN_ID (tx: $hash)" + else + log_err "EVM enrollment failed (status: $status)" + echo "$tx" | jq -c '.' 2>/dev/null || echo "$tx" + return 1 + fi + else + log_ok "EVM router already enrolled" + fi +} + +# ============================================================================= +# Step 6: Set ISM on EVM HypERC20 +# ============================================================================= + +set_ft_ism() { + log_step "Step 6: Set ISM on FT HypERC20 ($FT_SYMBOL)" + + if [[ "$(ft_load "evm_ism_set")" == "true" ]]; then + log_ok "ISM already set" + return 0 + fi + + local basesepolia_multisig_ism + basesepolia_multisig_ism=$(require_state "basesepolia_multisig_ism" "basesepolia_multisig_ism (run 04-agents.sh first)") + + log "Setting ISM to $basesepolia_multisig_ism on $FT_EVM_HYP_ERC20..." + local tx; tx=$(cast send "$FT_EVM_HYP_ERC20" \ + "setInterchainSecurityModule(address)" "$basesepolia_multisig_ism" \ + --private-key "$HYP_KEY" --rpc-url "$EVM_RPC" --json 2>&1) || true + + local status; status=$(echo "$tx" | jq -r '.status // empty' 2>/dev/null) || true + if [[ "$status" == "0x1" || "$status" == "1" ]]; then + ft_save "evm_ism_set" "true" + log_ok "ISM set to $basesepolia_multisig_ism" + else + log_err "setInterchainSecurityModule failed (status: $status)" + echo "$tx" | jq -c '.' 2>/dev/null || echo "$tx" + return 1 + fi +} + +# ============================================================================= +# Step 7: Restart Relayer with Dynamic Whitelist +# ============================================================================= + +restart_relayer() { + log_step "Step 7: Restart Relayer (dynamic whitelist)" + + # Always restart when a new route is added — the whitelist changes + local whitelist + whitelist=$(build_whitelist) + + # Count entries + local entry_count + entry_count=$(echo "$whitelist" | jq 'length' 2>/dev/null) || entry_count="?" + + log "Stopping existing relayer..." + docker stop hyperlane-relayer 2>/dev/null || true + docker rm -f hyperlane-relayer 2>/dev/null || true + sleep 2 + + log "Starting relayer with dynamic whitelist ($entry_count entries)..." + log " Whitelist: $whitelist" + docker run -d --name hyperlane-relayer --network host \ + -e CONFIG_FILES=/config/agent-config.json \ + -v "$BITSONG_HOME/agent-config.json:/config/agent-config.json:ro" \ + -v "$BITSONG_HOME/relayer-db:/hyperlane_db" \ + -v "$BITSONG_HOME/checkpoints-bitsong:/checkpoints-bitsong:ro" \ + -v "$BITSONG_HOME/checkpoints-basesepolia:/checkpoints-basesepolia:ro" \ + "$DOCKER_IMAGE" ./relayer \ + --db /hyperlane_db --relayChains bitsong,basesepolia \ + --allowLocalCheckpointSyncers true \ + --gaspaymentenforcement '[{"type": "none"}]' \ + --whitelist "$whitelist" \ + --chains.bitsong.signer.type cosmosKey \ + --chains.bitsong.signer.key "$COSMOS_SIGNER_KEY" \ + --chains.bitsong.signer.prefix bitsong \ + --chains.basesepolia.signer.type hexKey \ + --chains.basesepolia.signer.key "$EVM_RELAYER_KEY" \ + --metricsPort 9091 + + sleep 3 + if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^hyperlane-relayer$"; then + ft_save "relayer_restarted" "true" + log_ok "Relayer restarted with $entry_count whitelist entries" + else + log_err "Relayer failed to start — check: docker logs hyperlane-relayer" + return 1 + fi +} + +# ============================================================================= +# Verify +# ============================================================================= + +verify_ft_deployment() { + log_step "Verify Fantoken Warp Route ($FT_SYMBOL)" + + log "--- Cosmos Warp Token ($FT_TOKEN_ID) ---" + "$BINARY" query warp remote-routers "$FT_TOKEN_ID" \ + --output json --node "$NODE" --home "$BITSONG_HOME" 2>/dev/null | jq '.' || true + echo + + log "--- EVM HypERC20 ($FT_EVM_HYP_ERC20) ---" + local decimals symbol mailbox supply ism router_on_evm + symbol=$(cast call "$FT_EVM_HYP_ERC20" "symbol()(string)" --rpc-url "$EVM_RPC" 2>/dev/null || echo "?") + decimals=$(cast call "$FT_EVM_HYP_ERC20" "decimals()(uint8)" --rpc-url "$EVM_RPC" 2>/dev/null || echo "?") + mailbox=$(cast call "$FT_EVM_HYP_ERC20" "mailbox()(address)" --rpc-url "$EVM_RPC" 2>/dev/null || echo "?") + supply=$(cast call "$FT_EVM_HYP_ERC20" "totalSupply()(uint256)" --rpc-url "$EVM_RPC" 2>/dev/null || echo "?") + ism=$(cast call "$FT_EVM_HYP_ERC20" "interchainSecurityModule()(address)" --rpc-url "$EVM_RPC" 2>/dev/null || echo "?") + router_on_evm=$(cast call "$FT_EVM_HYP_ERC20" "routers(uint32)(bytes32)" "$DOMAIN_ID" \ + --rpc-url "$EVM_RPC" 2>/dev/null || echo "?") + + echo " symbol: $symbol (expected $FT_SYMBOL)" + echo " decimals: $decimals (expected 6)" + echo " mailbox: $mailbox" + echo " totalSupply: $supply (expected 0)" + echo " ISM: $ism" + echo " router($DOMAIN_ID): $router_on_evm" + echo +} + +# ============================================================================= +# Summary +# ============================================================================= + +print_summary() { + log_step "Summary" + echo -e "${BOLD}Fantoken Warp Route ($FT_SYMBOL)${NC}" + echo " Denom: $(ft_load denom)" + echo " Cosmos token: $(ft_load token_id)" + echo " EVM HypERC20: $(ft_load evm_hyp_erc20)" + echo " EVM (bytes32): $(ft_load evm_hyp_erc20_bytes32)" + echo " State prefix: ft_${FT_KEY}_*" + echo " State file: $STATE_FILE" + echo + echo -e "${BOLD}BaseScan${NC}: https://sepolia.basescan.org/address/$(ft_load evm_hyp_erc20)" + echo + echo -e "${BOLD}Next${NC}: Run 07-fantoken-test.sh --symbol $FT_SYMBOL to test transfers" +} + +# ============================================================================= +# Main +# ============================================================================= + +CLEAN=false +LIST_MODE=false +FT_SYMBOL="" +FT_NAME="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --symbol) + [[ -n "${2:-}" ]] || { log_err "--symbol requires a value"; exit 1; } + FT_SYMBOL="$2"; shift 2 ;; + --name) + [[ -n "${2:-}" ]] || { log_err "--name requires a value"; exit 1; } + FT_NAME="$2"; shift 2 ;; + --list) LIST_MODE=true; shift ;; + --clean) CLEAN=true; shift ;; + -h|--help) + echo "Usage: $0 --symbol [--name \"Name\"] [--clean]" + echo " $0 --list" + echo "" + echo "Options:" + echo " --symbol Fantoken symbol (required unless --list)" + echo " --name \"Name\" Fantoken name (required for new tokens)" + echo " --list List all registered fantoken routes" + echo " --clean Wipe this symbol's state keys and redo" + echo "" + echo "Required env: HYP_KEY, EVM_RELAYER_KEY, VALIDATOR_KEY, COSMOS_SIGNER_KEY" + exit 0 ;; + *) echo "Unknown flag: $1"; exit 1 ;; + esac +done + +# Handle --list early (only needs state file) +if [[ "$LIST_MODE" == "true" ]]; then + [[ -f "$STATE_FILE" ]] || { echo "No state file found."; exit 0; } + migrate_legacy_state + list_routes + exit 0 +fi + +# --symbol is required for everything else +[[ -n "$FT_SYMBOL" ]] || { log_err "--symbol is required (or use --list)"; exit 1; } +FT_KEY=$(echo "$FT_SYMBOL" | tr '[:upper:]' '[:lower:]') + +# Preflight +require_binary +require_jq +require_chain_running +command -v hyperlane >/dev/null 2>&1 || { log_err "Hyperlane CLI missing: npm install -g @hyperlane-xyz/cli"; exit 1; } +command -v cast >/dev/null 2>&1 || { log_err "cast missing: foundryup"; exit 1; } + +[[ -f "$STATE_FILE" ]] || { log_err "State not found: $STATE_FILE. Run earlier phases first."; exit 1; } +[[ -n "$HYP_KEY" ]] || { log_err "Set HYP_KEY=0x"; exit 1; } +[[ -n "${VALIDATOR_KEY:-}" ]] || { log_err "VALIDATOR_KEY not set (needed for relayer restart)"; exit 1; } +[[ -n "${COSMOS_SIGNER_KEY:-}" ]] || { log_err "COSMOS_SIGNER_KEY not set (needed for relayer restart)"; exit 1; } +[[ -n "${EVM_RELAYER_KEY:-}" ]] || { log_err "EVM_RELAYER_KEY not set (needed for relayer restart)"; exit 1; } +[[ -n "$VAL_ADDRESS" ]] || { log_err "VAL_ADDRESS not available. Is chain initialized?"; exit 1; } + +# Migrate legacy flat keys if needed +migrate_legacy_state + +# Handle --clean for this specific symbol +if [[ "$CLEAN" == "true" ]]; then + log "Cleaning state for symbol '$FT_KEY'..." + for suffix in denom minted token_id evm_hyp_erc20 evm_hyp_erc20_bytes32 \ + router_enrolled evm_router_enrolled evm_ism_set relayer_restarted; do + jq --arg k "ft_${FT_KEY}_${suffix}" 'del(.[$k])' "$STATE_FILE" > "${STATE_FILE}.tmp" \ + && mv "${STATE_FILE}.tmp" "$STATE_FILE" + done + unregister_route + log_ok "State cleaned for '$FT_KEY'" +fi + +banner "Phase 6: Fantoken Warp Route" "$FT_SYMBOL on bitsong <-> basesepolia" + +issue_fantoken +mint_fantoken +create_ft_collateral +deploy_ft_hyp_erc20 +wait_evm_pending # ensure HypERC20 deploy TX confirms before enrollment +enroll_ft_routers +wait_evm_pending # ensure EVM enrollment TX confirms before ISM TX +set_ft_ism + +# Register route BEFORE relayer restart so build_whitelist() includes it +register_route +ft_set_current + +restart_relayer +verify_ft_deployment +print_summary + +log_ok "Phase 6 complete for $FT_SYMBOL!" diff --git a/scripts/hyperlane/07-fantoken-test.sh b/scripts/hyperlane/07-fantoken-test.sh new file mode 100755 index 00000000..861da525 --- /dev/null +++ b/scripts/hyperlane/07-fantoken-test.sh @@ -0,0 +1,363 @@ +#!/usr/bin/env bash +# ============================================================================= +# 07-fantoken-test.sh — End-to-end fantoken transfer tests (Cosmos<->EVM) +# +# Supports per-symbol testing. If --symbol is provided, loads state from +# per-symbol keys (ft__*). Otherwise falls back to flat keys (ft_*). +# +# Usage: +# bash 07-fantoken-test.sh --symbol clay # Test clay route +# bash 07-fantoken-test.sh --symbol clay --cosmos-only # Cosmos->EVM only +# bash 07-fantoken-test.sh # Test latest route +# +# Required environment: +# EVM_RELAYER_KEY (or HYP_KEY) — for EVM->Cosmos transfer +# ============================================================================= + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib.sh" + +EVM_RELAYER_KEY="${EVM_RELAYER_KEY:-${HYP_KEY:-}}" + +# ============================================================================= +# Per-symbol state helpers (mirrors 06-fantoken-route.sh) +# ============================================================================= + +FT_KEY="" # lowercased symbol, empty = use flat keys + +ft_load_key() { + if [[ -n "$FT_KEY" ]]; then + load_state "ft_${FT_KEY}_$1" + else + load_state "ft_$1" + fi +} + +ft_save_key() { + if [[ -n "$FT_KEY" ]]; then + save_state "ft_${FT_KEY}_$1" "$2" + # Also update flat aliases + save_state "ft_$1" "$2" + else + save_state "ft_$1" "$2" + fi +} + +# ============================================================================= +# Readiness Check +# ============================================================================= + +check_agents_ready() { + log_step "Checking Agent Readiness" + + local ok=true + for name in hyperlane-validator-bitsong hyperlane-validator-basesepolia hyperlane-relayer; do + if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${name}$"; then + log_ok "$name running" + else + log_err "$name NOT running — start with: bash 04-agents.sh" + ok=false + fi + done + [[ "$ok" == "true" ]] || { log_err "Agents not ready. Run 04-agents.sh / 06-fantoken-route.sh first."; exit 1; } + + # Verify relayer has fantoken in whitelist + local ft_restarted + ft_restarted=$(ft_load_key "relayer_restarted") + if [[ "$ft_restarted" != "true" ]]; then + log_warn "Relayer may not have fantoken whitelist. Run 06-fantoken-route.sh first." + fi +} + +# ============================================================================= +# Relay Diagnostics +# ============================================================================= + +show_relay_progress() { + local logs + logs=$(docker logs hyperlane-relayer --tail 3000 2>&1 \ + | sed 's/\x1b\[[0-9;]*m//g') || return + + local bitsong_tip basesep_tip finalized_count + bitsong_tip=$(echo "$logs" | grep "HyperlaneDomain(bitsong" \ + | grep -oP 'tip: \K[0-9]+' | tail -1) || true + basesep_tip=$(echo "$logs" | grep "HyperlaneDomain(basesepolia" \ + | grep -oP 'tip: \K[0-9]+' | tail -1) || true + finalized_count=$(echo "$logs" | grep -ci "status: Finalized" 2>/dev/null) || finalized_count=0 + + echo -n " relayer: bitsong=${bitsong_tip:-?} basesep=${basesep_tip:-?} finalized=$finalized_count" + + local real_errors + real_errors=$(echo "$logs" | grep -i "error\|failed" \ + | grep -cv "0xa2827cb39\|CCIP\|verification" 2>/dev/null) || real_errors=0 + if [[ "$real_errors" -gt 0 ]]; then + echo -n " ${RED}errors=$real_errors${NC}" + fi + echo +} + +# ============================================================================= +# Cosmos -> EVM (fantoken) +# ============================================================================= + +test_ft_cosmos_to_evm() { + local symbol_label="${FT_KEY:-latest}" + log_step "Test: Cosmos -> EVM (Fantoken: $symbol_label)" + + local ft_token_id ft_evm_hyp_erc20 ft_denom merkle_hook_id + ft_token_id=$(ft_load_key "token_id") + ft_evm_hyp_erc20=$(ft_load_key "evm_hyp_erc20") + ft_denom=$(ft_load_key "denom") + merkle_hook_id=$(load_state "merkle_hook_id") + + [[ -n "$ft_token_id" ]] || { log_err "ft_token_id not in state"; return 1; } + [[ -n "$ft_evm_hyp_erc20" ]] || { log_err "ft_evm_hyp_erc20 not in state"; return 1; } + [[ -n "$ft_denom" ]] || { log_err "ft_denom not in state"; return 1; } + + # Send to EVM signer (so they have tokens for the EVM->Cosmos test) + local evm_signer_addr evm_signer_bytes32 + evm_signer_addr=$(cast wallet address --private-key "$EVM_RELAYER_KEY" 2>/dev/null) + [[ -n "$evm_signer_addr" ]] || { log_err "Cannot derive EVM signer address"; return 1; } + local hex="${evm_signer_addr#0x}"; hex=$(echo "$hex" | tr '[:upper:]' '[:lower:]') + evm_signer_bytes32=$(printf "0x%064s" "$hex" | tr ' ' '0') + + local transfer_amount=1000 + + log "Sending $transfer_amount $ft_denom: BitSong -> Base Sepolia" + log " Token: $ft_token_id" + log " Denom: $ft_denom" + log " Recipient: $evm_signer_addr" + + local initial_supply + initial_supply=$(cast call "$ft_evm_hyp_erc20" "totalSupply()(uint256)" --rpc-url "$EVM_RPC" 2>/dev/null) || initial_supply="0" + log " Initial totalSupply: $initial_supply" + + # Use merkle hook to bypass IGP payment (devnet) + local tx_args=("$BINARY" tx warp transfer "$ft_token_id" "$REMOTE_DOMAIN" "$evm_signer_bytes32" "$transfer_amount" + --max-hyperlane-fee "0ubtsg") + [[ -n "$merkle_hook_id" ]] && tx_args+=(--custom-hook-id "$merkle_hook_id") + + submit_tx "Cosmos->EVM ($transfer_amount $ft_denom)" "${tx_args[@]}" + + # Check if merkle tree count increased + local merkle_count + merkle_count=$("$BINARY" query hyperlane hooks merkle-tree-hooks \ + --output json --node "$NODE" --home "$BITSONG_HOME" 2>/dev/null \ + | jq -r '.merkle_tree_hooks[0].merkle_tree.count // "?"' 2>/dev/null) || merkle_count="?" + log " Merkle tree count after dispatch: $merkle_count" + + # Wait for relayer to deliver — 300s timeout with diagnostics + log "Waiting for relayer to pick up, sign, and deliver (timeout: 300s)..." + for i in $(seq 1 60); do + sleep 5 + local current_supply + current_supply=$(cast call "$ft_evm_hyp_erc20" "totalSupply()(uint256)" --rpc-url "$EVM_RPC" 2>/dev/null) || current_supply="0" + if [[ "$current_supply" != "$initial_supply" ]]; then + log_ok "Cosmos->EVM (FT) SUCCESS! totalSupply: $initial_supply -> $current_supply" + ft_save_key "cosmos_to_evm_test_passed" "true" + return 0 + fi + + if (( i % 6 == 0 )); then + echo -e " ${CYAN}[${i}/60] totalSupply=$current_supply — checking relayer...${NC}" + show_relay_progress + + local cp_file="$BITSONG_HOME/checkpoints-bitsong/index.json" + if [[ -f "$cp_file" ]]; then + local cp_idx + cp_idx=$(cat "$cp_file" 2>/dev/null) || cp_idx="?" + echo " validator-bitsong: checkpoint_index=$cp_idx" + fi + else + log " [${i}/60] totalSupply=$current_supply" + fi + done + + log_err "Timed out after 300s!" + log_warn "Diagnostic info:" + log_warn " Last 10 relayer log lines (filtered):" + docker logs hyperlane-relayer --tail 20 2>&1 \ + | grep -v "0xa2827cb39\|CCIP Read" | tail -10 || true + echo + log_warn " Validator checkpoint:" + local cp_file="$BITSONG_HOME/checkpoints-bitsong/index.json" + [[ -f "$cp_file" ]] && cat "$cp_file" || echo " (no checkpoint file)" + echo + log_warn "Troubleshooting:" + log_warn " 1. Check relayer whitelist includes ft_token_id" + log_warn " 2. docker logs hyperlane-relayer --tail 50" + log_warn " 3. bash status.sh" + log_warn " 4. Retry: bash 07-fantoken-test.sh${FT_KEY:+ --symbol $FT_KEY} --cosmos-only" + return 1 +} + +# ============================================================================= +# EVM -> Cosmos (fantoken) +# ============================================================================= + +test_ft_evm_to_cosmos() { + local symbol_label="${FT_KEY:-latest}" + log_step "Test: EVM -> Cosmos (Fantoken: $symbol_label)" + + local ft_evm_hyp_erc20 ft_token_id ft_denom + ft_evm_hyp_erc20=$(ft_load_key "evm_hyp_erc20") + ft_token_id=$(ft_load_key "token_id") + ft_denom=$(ft_load_key "denom") + + [[ -n "$ft_evm_hyp_erc20" ]] || { log_err "ft_evm_hyp_erc20 not in state"; return 1; } + [[ -n "$ft_token_id" ]] || { log_err "ft_token_id not in state"; return 1; } + [[ -n "$ft_denom" ]] || { log_err "ft_denom not in state"; return 1; } + + local cosmos_recipient_bytes32 + cosmos_recipient_bytes32=$(bech32_to_bytes32 "$VAL_ADDRESS") + [[ -n "$cosmos_recipient_bytes32" ]] || { log_err "Failed to convert $VAL_ADDRESS to bytes32"; return 1; } + + # Check EVM balance first + local evm_balance + evm_balance=$(cast call "$ft_evm_hyp_erc20" "totalSupply()(uint256)" --rpc-url "$EVM_RPC" 2>/dev/null) || evm_balance="0" + if [[ "$evm_balance" == "0" ]]; then + log_err "FT HypERC20 totalSupply is 0 — no tokens to send back." + log_err "Run Cosmos->EVM fantoken test first." + return 1 + fi + + local transfer_amount=1000 + + # Get initial Cosmos balance for the fantoken + local initial_balance + initial_balance=$("$BINARY" query bank balance "$VAL_ADDRESS" "$ft_denom" \ + --output json --node "$NODE" --home "$BITSONG_HOME" 2>/dev/null | \ + jq -r '.balance.amount // "0"' 2>/dev/null) || initial_balance="0" + + log "Sending $transfer_amount $ft_denom: Base Sepolia -> BitSong" + log " HypERC20: $ft_evm_hyp_erc20 (supply: $evm_balance)" + log " Recipient: $VAL_ADDRESS" + log " Initial Cosmos balance: $initial_balance $ft_denom" + + local initial_height + initial_height=$("$BINARY" status --node "$NODE" --home "$BITSONG_HOME" 2>/dev/null | \ + jq -r '.sync_info.latest_block_height // "0"' 2>/dev/null) || initial_height="0" + + local evm_tx + evm_tx=$(cast send "$ft_evm_hyp_erc20" \ + "transferRemote(uint32,bytes32,uint256)" "$DOMAIN_ID" "$cosmos_recipient_bytes32" "$transfer_amount" \ + --value 1 --private-key "$EVM_RELAYER_KEY" --rpc-url "$EVM_RPC" --json 2>&1) || true + + local evm_tx_hash + evm_tx_hash=$(echo "$evm_tx" | jq -r '.transactionHash // empty' 2>/dev/null) || true + [[ -n "$evm_tx_hash" ]] || { log_err "EVM tx failed"; echo "$evm_tx"; return 1; } + + log "EVM TX: $evm_tx_hash" + log "Waiting for relayer delivery (timeout: 1500s — first run scans ~24M blocks)..." + + for i in $(seq 1 100); do + sleep 15 + local current_balance + current_balance=$("$BINARY" query bank balance "$VAL_ADDRESS" "$ft_denom" \ + --output json --node "$NODE" --home "$BITSONG_HOME" 2>/dev/null | \ + jq -r '.balance.amount // "0"' 2>/dev/null) || current_balance="0" + + if [[ "$current_balance" != "$initial_balance" ]]; then + log_ok "EVM->Cosmos (FT) SUCCESS! Balance: $initial_balance -> $current_balance $ft_denom" + ft_save_key "evm_to_cosmos_test_passed" "true" + return 0 + fi + + local height + height=$("$BINARY" status --node "$NODE" --home "$BITSONG_HOME" 2>/dev/null | \ + jq -r '.sync_info.latest_block_height // "?"' 2>/dev/null) || height="?" + + if (( i % 4 == 0 )); then + echo -e " ${CYAN}[${i}/100] height=$height balance=$current_balance — checking relayer...${NC}" + show_relay_progress + else + log " [${i}/100] height=$height balance=$current_balance" + fi + done + + log_err "Timed out after 1500s!" + log_warn "Last 10 relayer logs:" + docker logs hyperlane-relayer --tail 20 2>&1 \ + | grep -v "0xa2827cb39\|CCIP Read" | tail -10 || true + return 1 +} + +# ============================================================================= +# Summary +# ============================================================================= + +print_summary() { + local symbol_label="${FT_KEY:-latest}" + log_step "Fantoken Test Summary ($symbol_label)" + + echo -e "${BOLD}Fantoken${NC}" + echo " Denom: $(ft_load_key denom)" + echo " Token ID: $(ft_load_key token_id)" + echo " HypERC20: $(ft_load_key evm_hyp_erc20)" + echo + + echo -e "${BOLD}Transfer Tests${NC}" + local c2e e2c + c2e=$(ft_load_key "cosmos_to_evm_test_passed") + e2c=$(ft_load_key "evm_to_cosmos_test_passed") + echo " Cosmos->EVM (FT): ${c2e:-not run}" + echo " EVM->Cosmos (FT): ${e2c:-not run}" + echo +} + +# ============================================================================= +# Main +# ============================================================================= + +COSMOS_ONLY=false +EVM_ONLY=false +FT_SYMBOL="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --symbol) + [[ -n "${2:-}" ]] || { log_err "--symbol requires a value"; exit 1; } + FT_SYMBOL="$2"; shift 2 ;; + --cosmos-only) COSMOS_ONLY=true; shift ;; + --evm-only) EVM_ONLY=true; shift ;; + -h|--help) + echo "Usage: $0 [--symbol ] [--cosmos-only|--evm-only]" + echo "" + echo "Options:" + echo " --symbol Test a specific fantoken route (default: latest)" + echo " --cosmos-only Cosmos->EVM only" + echo " --evm-only EVM->Cosmos only" + echo "" + echo "Required env: EVM_RELAYER_KEY (or HYP_KEY)" + exit 0 ;; + *) log_err "Unknown flag: $1"; exit 1 ;; + esac +done + +if [[ -n "$FT_SYMBOL" ]]; then + FT_KEY=$(echo "$FT_SYMBOL" | tr '[:upper:]' '[:lower:]') +fi + +require_binary +require_jq +require_chain_running +command -v cast >/dev/null 2>&1 || { log_err "cast missing: foundryup"; exit 1; } +[[ -n "$EVM_RELAYER_KEY" ]] || { log_err "Set EVM_RELAYER_KEY or HYP_KEY"; exit 1; } + +if [[ -n "$FT_KEY" ]]; then + banner "Fantoken Transfer Tests" "$FT_SYMBOL — bitsong <-> basesepolia" +else + banner "Fantoken Transfer Tests" "latest route — bitsong <-> basesepolia" +fi + +check_agents_ready + +if [[ "$EVM_ONLY" != "true" ]]; then + test_ft_cosmos_to_evm +fi + +if [[ "$COSMOS_ONLY" != "true" ]]; then + test_ft_evm_to_cosmos +fi + +print_summary +log_ok "Fantoken tests complete!" diff --git a/scripts/hyperlane/run-all.sh b/scripts/hyperlane/run-all.sh index 5840ea91..1183b0bd 100755 --- a/scripts/hyperlane/run-all.sh +++ b/scripts/hyperlane/run-all.sh @@ -31,6 +31,12 @@ while [[ $# -gt 0 ]]; do echo " 3 Deploy HypERC20 + enrollment (03-evm-deploy.sh)" echo " 4 ISM upgrade + validators/relayer (04-agents.sh)" echo " 5 Transfer tests (05-test.sh)" + echo " 6 Fantoken warp route (06-fantoken-route.sh)" + echo " 7 Fantoken transfer tests (07-fantoken-test.sh)" + echo "" + echo "Environment (Phase 6/7):" + echo " FT_SYMBOL Fantoken symbol (default: clay)" + echo " FT_NAME Fantoken name (default: Clay Token)" exit 0 ;; *) echo "Unknown flag: $1"; exit 1 ;; esac @@ -61,5 +67,14 @@ if [[ "$SKIP_TEST" != "true" ]]; then run_phase 5 "05-test.sh" fi +FT_SYMBOL="${FT_SYMBOL:-clay}" +FT_NAME="${FT_NAME:-Clay Token}" + +run_phase 6 "06-fantoken-route.sh" --symbol "$FT_SYMBOL" --name "$FT_NAME" $CLEAN_FLAG + +if [[ "$SKIP_TEST" != "true" ]]; then + run_phase 7 "07-fantoken-test.sh" --symbol "$FT_SYMBOL" +fi + echo "" echo "All phases complete!" diff --git a/scripts/hyperlane/status.sh b/scripts/hyperlane/status.sh index c6877aec..f2b1b248 100755 --- a/scripts/hyperlane/status.sh +++ b/scripts/hyperlane/status.sh @@ -281,6 +281,41 @@ print_warp_state() { echo -e " Cosmos token: $token_id" fi echo + + # Fantoken warp routes (iterate over ft_route_list) + local route_list + route_list=$(load_state "ft_route_list") + if [[ -n "$route_list" ]]; then + echo -e " ${BOLD}Fantoken Warp Routes${NC}" + IFS=',' read -ra routes <<< "$route_list" + for sym in "${routes[@]}"; do + [[ -z "$sym" ]] && continue + local ft_evm ft_tid ft_denom_val + ft_evm=$(load_state "ft_${sym}_evm_hyp_erc20") + ft_tid=$(load_state "ft_${sym}_token_id") + ft_denom_val=$(load_state "ft_${sym}_denom") + + echo -e " ── ${BOLD}${sym}${NC} ──" + echo -e " denom: ${ft_denom_val:-?}" + + if [[ -n "$ft_evm" ]] && command -v cast >/dev/null 2>&1; then + local ft_supply ft_symbol ft_ism + ft_supply=$(cast call "$ft_evm" "totalSupply()(uint256)" --rpc-url "$EVM_RPC" 2>/dev/null) || ft_supply="?" + ft_symbol=$(cast call "$ft_evm" "symbol()(string)" --rpc-url "$EVM_RPC" 2>/dev/null) || ft_symbol="?" + ft_ism=$(cast call "$ft_evm" "interchainSecurityModule()(address)" --rpc-url "$EVM_RPC" 2>/dev/null) || ft_ism="?" + echo -e " HypERC20: $ft_evm ($ft_symbol)" + echo -e " totalSupply: $ft_supply" + echo -e " ISM: $ft_ism" + else + echo -e " HypERC20: ${YELLOW}not deployed${NC}" + fi + + if [[ -n "$ft_tid" ]]; then + echo -e " Cosmos token: $ft_tid" + fi + done + echo + fi } # ─── Test Results ──────────────────────────────────────────────────────────── @@ -291,14 +326,37 @@ print_test_results() { c2e=$(load_state "cosmos_to_evm_test_passed") e2c=$(load_state "evm_to_cosmos_test_passed") if [[ -n "$c2e" ]]; then - echo -e " Cosmos -> EVM: ${GREEN}$c2e${NC}" + echo -e " Cosmos -> EVM (ubtsg): ${GREEN}$c2e${NC}" else - echo -e " Cosmos -> EVM: ${YELLOW}not run${NC}" + echo -e " Cosmos -> EVM (ubtsg): ${YELLOW}not run${NC}" fi if [[ -n "$e2c" ]]; then - echo -e " EVM -> Cosmos: ${GREEN}$e2c${NC}" + echo -e " EVM -> Cosmos (ubtsg): ${GREEN}$e2c${NC}" else - echo -e " EVM -> Cosmos: ${YELLOW}not run${NC}" + echo -e " EVM -> Cosmos (ubtsg): ${YELLOW}not run${NC}" + fi + + # Per-symbol fantoken test results + local route_list + route_list=$(load_state "ft_route_list") + if [[ -n "$route_list" ]]; then + IFS=',' read -ra routes <<< "$route_list" + for sym in "${routes[@]}"; do + [[ -z "$sym" ]] && continue + local ft_c2e ft_e2c + ft_c2e=$(load_state "ft_${sym}_cosmos_to_evm_test_passed") + ft_e2c=$(load_state "ft_${sym}_evm_to_cosmos_test_passed") + if [[ -n "$ft_c2e" ]]; then + echo -e " Cosmos->EVM ($sym): ${GREEN}$ft_c2e${NC}" + else + echo -e " Cosmos->EVM ($sym): ${YELLOW}not run${NC}" + fi + if [[ -n "$ft_e2c" ]]; then + echo -e " EVM->Cosmos ($sym): ${GREEN}$ft_e2c${NC}" + else + echo -e " EVM->Cosmos ($sym): ${YELLOW}not run${NC}" + fi + done fi echo } From 6ef6074e5d97f6e1056e7b070bb3dcc1af125a67 Mon Sep 17 00:00:00 2001 From: angelorc Date: Fri, 20 Feb 2026 17:16:43 +0100 Subject: [PATCH 5/5] feat(docker): update workflow to push images on branch pushes and refine tag handling --- .github/workflows/push-docker-image.yml | 69 ++++++++++++++----------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/.github/workflows/push-docker-image.yml b/.github/workflows/push-docker-image.yml index d2145583..7bf7157d 100644 --- a/.github/workflows/push-docker-image.yml +++ b/.github/workflows/push-docker-image.yml @@ -1,4 +1,4 @@ -# This workflow pushes new Bitsong docker images on every new tag. +# This workflow pushes new Bitsong docker images on every new tag or branch push. # # On every new `vX.Y.Z` tag the following images are pushed: # @@ -6,13 +6,11 @@ # bitsongofficial/go-bitsong:X.Y.Z # is pushed # bitsongofficial/go-bitsong:X.Y # is updated to X.Y.Z # bitsongofficial/go-bitsong:X # is updated to X.Y.Z -# bitsongofficial/go-bitsong:latest # is updated to X.Y.Z # -# bitsongofficial/go-bitsong-e2e:vX.Y.Z # is pushed -# bitsongofficial/go-bitsong-e2e:X.Y.Z # is pushed -# bitsongofficial/go-bitsong-e2e:X.Y # is updated to X.Y.Z -# bitsongofficial/go-bitsong-e2e:X # is updated to X.Y.Z -# bitsongofficial/go-bitsong-e2e:latest # is updated to X.Y.Z +# On branch pushes (e.g. feat-hyperlane) the following images are pushed: +# +# bitsongofficial/go-bitsong:feat-hyperlane # latest for that branch +# bitsongofficial/go-bitsong:feat-hyperlane-abc1234 # pinned to commit # # All the images above have support for linux/amd64 and linux/arm64. @@ -20,28 +18,27 @@ name: Push Docker Images env: DOCKER_REPOSITORY: bitsongofficial/go-bitsong - RUNNER_BASE_IMAGE_DISTROLESS: gcr.io/distroless/static-debian12 - RUNNER_BASE_IMAGE_NONROOT: gcr.io/distroless/static-debian12:nonroot - RUNNER_BASE_IMAGE_ALPINE: alpine:3.21 on: release: types: [published, created, edited] push: tags: - - 'v[0-9]+.[0-9]+.[0-9]+' # ignore rc + - 'v[0-9]+.[0-9]+.[0-9]+' # ignore rc + branches: + - 'feat-hyperlane' jobs: bitsong-images: runs-on: ubuntu-latest steps: - - + - name: Check out the repo uses: actions/checkout@v4 - - + - name: Set up QEMU uses: docker/setup-qemu-action@v3 - - + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry @@ -51,19 +48,33 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Parse tag - id: tag + name: Determine image tags + id: tags run: | - VERSION=$(echo ${{ github.ref_name }} | sed "s/v//") - MAJOR_VERSION=$(echo $VERSION | cut -d '.' -f 1) - MINOR_VERSION=$(echo $VERSION | cut -d '.' -f 2) - PATCH_VERSION=$(echo $VERSION | cut -d '.' -f 3) - echo "VERSION=$VERSION" >> $GITHUB_ENV - echo "MAJOR_VERSION=$MAJOR_VERSION" >> $GITHUB_ENV - echo "MINOR_VERSION=$MINOR_VERSION" >> $GITHUB_ENV - echo "PATCH_VERSION=$PATCH_VERSION" >> $GITHUB_ENV - - - name: Build and push + SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) + + if [[ "${{ github.ref_type }}" == "tag" ]]; then + # Tag push: produce semver tags (e.g. v0.21.0 -> 0, 0.21, 0.21.0, v0.21.0) + VERSION=$(echo "${{ github.ref_name }}" | sed "s/v//") + MAJOR_VERSION=$(echo "$VERSION" | cut -d '.' -f 1) + MINOR_VERSION=$(echo "$VERSION" | cut -d '.' -f 2) + PATCH_VERSION=$(echo "$VERSION" | cut -d '.' -f 3) + IMAGE_TAGS="ghcr.io/${{ env.DOCKER_REPOSITORY }}:${MAJOR_VERSION} + ghcr.io/${{ env.DOCKER_REPOSITORY }}:${MAJOR_VERSION}.${MINOR_VERSION} + ghcr.io/${{ env.DOCKER_REPOSITORY }}:${MAJOR_VERSION}.${MINOR_VERSION}.${PATCH_VERSION} + ghcr.io/${{ env.DOCKER_REPOSITORY }}:v${MAJOR_VERSION}.${MINOR_VERSION}.${PATCH_VERSION}" + else + # Branch push: produce branch + commit-pinned tags + BRANCH=$(echo "${{ github.ref_name }}" | sed 's/\//-/g') + IMAGE_TAGS="ghcr.io/${{ env.DOCKER_REPOSITORY }}:${BRANCH} + ghcr.io/${{ env.DOCKER_REPOSITORY }}:${BRANCH}-${SHORT_SHA}" + fi + + echo "IMAGE_TAGS<> $GITHUB_OUTPUT + echo "$IMAGE_TAGS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + - + name: Build and push id: build_push_image uses: docker/build-push-action@v5 with: @@ -71,8 +82,4 @@ jobs: context: . push: true platforms: linux/amd64,linux/arm64 - tags: | - ghcr.io/bitsongofficial/go-bitsong:${{ env.MAJOR_VERSION }} - ghcr.io/bitsongofficial/go-bitsong:${{ env.MAJOR_VERSION }}.${{ env.MINOR_VERSION }} - ghcr.io/bitsongofficial/go-bitsong:${{ env.MAJOR_VERSION }}.${{ env.MINOR_VERSION }}.${{ env.PATCH_VERSION }} - ghcr.io/bitsongofficial/go-bitsong:v${{ env.MAJOR_VERSION }}.${{ env.MINOR_VERSION }}.${{ env.PATCH_VERSION }} + tags: ${{ steps.tags.outputs.IMAGE_TAGS }}