package main import "C" import ( "encoding/base64" "encoding/json" "fmt" "math/big" "os" "sync" "time" abci "github.com/cometbft/cometbft/abci/types" codectypes "github.com/cosmos/cosmos-sdk/codec/types" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" sdk "github.com/cosmos/cosmos-sdk/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/cosmos/gogoproto/proto" "github.com/margined-protocol/test-tube/neutron-test-tube/result" "github.com/margined-protocol/test-tube/neutron-test-tube/testenv" "github.com/pkg/errors" sdkmath "cosmossdk.io/math" wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" ) var ( envCounter uint64 = 0 envRegister = sync.Map{} mu sync.Mutex ) type Price struct { Base string `json:"base"` Quote string `json:"quote"` Price int `json:"price"` } //export InitTestEnv func InitTestEnv() uint64 { // Temp fix for concurrency issue mu.Lock() defer mu.Unlock() // temp: suppress noise from stdout os.Stdout = nil envCounter += 1 id := envCounter nodeHome, err := os.MkdirTemp("", ".neutron-test-tube-temp-") if err != nil { panic(err) } // set up the validator env := new(testenv.TestEnv) env.App = testenv.NewNeutronApp(nodeHome) env.NodeHome = nodeHome env.ParamTypesRegistry = *testenv.NewParamTypeRegistry() ctx, valPriv := testenv.InitChain(env.App) env.Ctx = ctx env.ValPrivs = secp256k1.PrivKey{Key: valPriv.Bytes()} env.Validator = valPriv.Bytes() env.SetupParamTypes() // Allow testing unoptimized contract wasmtypes.MaxWasmSize = 1024 * 1024 * 1024 * 1024 * 1024 newBlockTime := env.Ctx.BlockTime().Add(time.Duration(3) * time.Second) newCtx := env.Ctx.WithBlockTime(newBlockTime).WithBlockHeight(env.Ctx.BlockHeight() + 1) env.Ctx = newCtx reqFinalizeBlock := abci.RequestFinalizeBlock{Height: env.Ctx.BlockHeight(), Txs: [][]byte{}, Time: newBlockTime} env.App.FinalizeBlock(&reqFinalizeBlock) env.App.Commit() envRegister.Store(id, *env) return id } //export CleanUp func CleanUp(envId uint64) { env := loadEnv(envId) err := os.RemoveAll(env.NodeHome) if err != nil { panic(err) } envRegister.Delete(envId) } //export InitAccount func InitAccount(envId uint64, coinsJson string) *C.char { env := loadEnv(envId) var coins sdk.Coins if err := json.Unmarshal([]byte(coinsJson), &coins); err != nil { panic(err) } priv := secp256k1.GenPrivKey() accAddr := sdk.AccAddress(priv.PubKey().Address()) for _, coin := range coins { // create denom if not exist _, hasDenomMetaData := env.App.BankKeeper.GetDenomMetaData(env.Ctx, coin.Denom) if !hasDenomMetaData { denomMetaData := banktypes.Metadata{ DenomUnits: []*banktypes.DenomUnit{{ Denom: coin.Denom, Exponent: 0, }}, Base: coin.Denom, } env.App.BankKeeper.SetDenomMetaData(env.Ctx, denomMetaData) } } err := env.FundAccount(env.Ctx, env.App.BankKeeper, accAddr, coins) if err != nil { panic(errors.Wrapf(err, "Failed to fund account")) } base64Priv := base64.StdEncoding.EncodeToString(priv.Bytes()) envRegister.Store(envId, env) return C.CString(base64Priv) } // Core function to adjust block time and finalize func finalizeWithTime(envId uint64, txBytes [][]byte, seconds uint64) *C.char { env := loadEnv(envId) mu.Lock() defer mu.Unlock() // Update context with new block time and height newBlockTime := env.Ctx.BlockTime().Add(time.Duration(seconds) * time.Second) newCtx := env.Ctx.WithBlockTime(newBlockTime).WithBlockHeight(env.Ctx.BlockHeight() + 1) env.Ctx = newCtx reqFinalizeBlock := &abci.RequestFinalizeBlock{ Height: env.Ctx.BlockHeight(), Txs: txBytes, Time: newBlockTime, } // Finalize the block res, err := env.App.FinalizeBlock(reqFinalizeBlock) if err != nil { panic(err) } _, err = env.App.Commit() if err != nil { panic(err) } // Marshal result and update environment registry bz, err := proto.Marshal(res) if err != nil { panic(err) } envRegister.Store(envId, env) return encodeBytesResultBytes(bz) } // Helper function to create transaction bytes based on block height func getTxBytes(env *testenv.TestEnv, reqDeliverTxBytes []byte, additionalBytes []byte) [][]byte { if env.Ctx.BlockHeight() < 2 { return [][]byte{reqDeliverTxBytes} } return [][]byte{additionalBytes, reqDeliverTxBytes} } //export IncreaseTime func IncreaseTime(envId uint64, seconds uint64) { env := loadEnv(envId) finalizeWithTime(envId, getTxBytes(&env, nil, nil), seconds) } //export FinalizeBlock func FinalizeBlock(envId uint64, base64ReqDeliverTx string) *C.char { env := loadEnv(envId) reqDeliverTxBytes, err := base64.StdEncoding.DecodeString(base64ReqDeliverTx) if err != nil { panic(err) } return finalizeWithTime(envId, getTxBytes(&env, reqDeliverTxBytes, nil), 3) } //export SetSlinkyPrices func SetSlinkyPrices(envId uint64, pricesJson string) { env := loadEnv(envId) prices := parsePrices(pricesJson) slinkyPrices := calculateSlinkyPrices(&env, prices) extCommitInfoBz := testenv.CreateExtendedVoteInfo(env.ValPrivs, slinkyPrices) finalizeWithTime(envId, getTxBytes(&env, nil, extCommitInfoBz), 3) } // Helper to parse JSON prices into Price struct array func parsePrices(pricesJson string) []Price { var prices []Price if err := json.Unmarshal([]byte(pricesJson), &prices); err != nil { panic(err) } return prices } // Helper to calculate slinky prices func calculateSlinkyPrices(env *testenv.TestEnv, prices []Price) map[uint64][]byte { slinkyPrices := map[uint64][]byte{} for _, price := range prices { currentPrice, idx, err := testenv.GetCurrentPriceAndPairMapping(env.Ctx, *env.App.OracleKeeper, price.Base, price.Quote) if err != nil { panic(err) } newPrice := sdkmath.NewIntFromBigInt(big.NewInt(int64(price.Price))) delta := newPrice.Sub(currentPrice) encodedDelta, err := big.NewInt(delta.Int64()).GobEncode() if err != nil { panic(err) } slinkyPrices[idx] = encodedDelta } return slinkyPrices } //export WasmSudo func WasmSudo(envId uint64, bech32Address, msgJson string) *C.char { env := loadEnv(envId) // Temp fix for concurrency issue mu.Lock() defer mu.Unlock() accAddr, err := sdk.AccAddressFromBech32(bech32Address) if err != nil { panic(err) } msgBytes := []byte(msgJson) res, err := env.App.WasmKeeper.Sudo(env.Ctx, accAddr, msgBytes) if err != nil { return encodeErrToResultBytes(result.ExecuteError, err) } envRegister.Store(envId, env) return encodeBytesResultBytes(res) } //export Query func Query(envId uint64, path, base64QueryMsgBytes string) *C.char { env := loadEnv(envId) queryMsgBytes, err := base64.StdEncoding.DecodeString(base64QueryMsgBytes) if err != nil { panic(err) } req := abci.RequestQuery{} req.Data = queryMsgBytes route := env.App.GRPCQueryRouter().Route(path) if route == nil { err := errors.New("No route found for `" + path + "`") return encodeErrToResultBytes(result.QueryError, err) } res, err := route(env.Ctx, &req) if err != nil { return encodeErrToResultBytes(result.QueryError, err) } return encodeBytesResultBytes(res.Value) } //export GetBlockTime func GetBlockTime(envId uint64) int64 { env := loadEnv(envId) return env.Ctx.BlockTime().UnixNano() } //export GetBlockHeight func GetBlockHeight(envId uint64) int64 { env := loadEnv(envId) return env.Ctx.BlockHeight() } //export AccountSequence func AccountSequence(envId uint64, bech32Address string) uint64 { env := loadEnv(envId) addr, err := sdk.AccAddressFromBech32(bech32Address) if err != nil { panic(err) } seq, err := env.App.AccountKeeper.GetSequence(env.Ctx, addr) if err != nil { panic(err) } return seq } //export AccountNumber func AccountNumber(envId uint64, bech32Address string) uint64 { env := loadEnv(envId) addr, err := sdk.AccAddressFromBech32(bech32Address) if err != nil { panic(err) } acc := env.App.AccountKeeper.GetAccount(env.Ctx, addr) return acc.GetAccountNumber() } //export Simulate func Simulate(envId uint64, base64TxBytes string) *C.char { // => base64GasInfo env := loadEnv(envId) // Temp fix for concurrency issue mu.Lock() defer mu.Unlock() txBytes, err := base64.StdEncoding.DecodeString(base64TxBytes) if err != nil { panic(err) } gasInfo, _, err := env.App.Simulate(txBytes) if err != nil { return encodeErrToResultBytes(result.ExecuteError, err) } bz, err := proto.Marshal(&gasInfo) if err != nil { panic(err) } return encodeBytesResultBytes(bz) } //export SetParamSet func SetParamSet(envId uint64, subspaceName, base64ParamSetBytes string) *C.char { env := loadEnv(envId) // Temp fix for concurrency issue mu.Lock() defer mu.Unlock() paramSetBytes, err := base64.StdEncoding.DecodeString(base64ParamSetBytes) if err != nil { panic(err) } subspace, ok := env.App.ParamsKeeper.GetSubspace(subspaceName) if !ok { err := errors.New("No subspace found for `" + subspaceName + "`") return encodeErrToResultBytes(result.ExecuteError, err) } pReg := env.ParamTypesRegistry any := codectypes.Any{} err = proto.Unmarshal(paramSetBytes, &any) if err != nil { return encodeErrToResultBytes(result.ExecuteError, err) } pset, err := pReg.UnpackAny(&any) if err != nil { return encodeErrToResultBytes(result.ExecuteError, err) } subspace.SetParamSet(env.Ctx, pset) // return empty bytes if no error return encodeBytesResultBytes([]byte{}) } //export GetParamSet func GetParamSet(envId uint64, subspaceName, typeUrl string) *C.char { env := loadEnv(envId) subspace, ok := env.App.ParamsKeeper.GetSubspace(subspaceName) if !ok { err := errors.New("No subspace found for `" + subspaceName + "`") return encodeErrToResultBytes(result.ExecuteError, err) } pReg := env.ParamTypesRegistry pset, ok := pReg.GetEmptyParamsSet(typeUrl) if !ok { err := errors.New("No param set found for `" + typeUrl + "`") return encodeErrToResultBytes(result.ExecuteError, err) } subspace.GetParamSet(env.Ctx, pset) bz, err := proto.Marshal(pset) if err != nil { panic(err) } return encodeBytesResultBytes(bz) } //export GetValidatorAddress func GetValidatorAddress(envId uint64, n int32) *C.char { // env := loadEnv(envId) // return C.CString(env.GetValidatorAddresses()[n]) return C.CString("") } //export GetValidatorPrivateKey func GetValidatorPrivateKey(envId uint64) *C.char { env := loadEnv(envId) priv := env.GetValidatorPrivateKey() base64Priv := base64.StdEncoding.EncodeToString(priv) return C.CString(base64Priv) } // ========= utils ========= func loadEnv(envId uint64) testenv.TestEnv { item, ok := envRegister.Load(envId) env := testenv.TestEnv(item.(testenv.TestEnv)) if !ok { panic(fmt.Sprintf("env not found: %d", envId)) } return env } func encodeErrToResultBytes(code byte, err error) *C.char { return C.CString(result.EncodeResultFromError(code, err)) } func encodeBytesResultBytes(bytes []byte) *C.char { return C.CString(result.EncodeResultFromOk(bytes)) } // must define main for ffi build func main() {}