strangelove-ventures / lens

⚛️❤️👁 Brought to you by the LensCrafters
Apache License 2.0
168 stars 82 forks source link

Can't use SetSDKContext() more than once #222

Open freshfab opened 2 years ago

freshfab commented 2 years ago

I have a program that needs to interact with multiple chains. I've given each chain-specific sub-module its own ChainClient & ChainClientConfig to work with, but any time I want to interact with two chains or more (either via query or tx), the second one to get executed uses the Bech32 prefix of the module that was executed first.

The calls to the SDK config did not panic, so it can't be sealed. AFAICS the calls are made each time AccAddress.String() or ValAddress.String() are called, so I tried the following:

done := ChainClient.SetSDKContext()
// Get string representation of the account in the keyring and cache it.
accAddr := key.String()
done()

// Use the 'accAddr' variable from here on instead of calling the String() method each time.

But that didn't work either. I'm not sure how much further down this rabbit hole goes, so maybe someone can enlighten me here.

How To Reproduce

Create two accounts in your local keyring, e.g. one for Cosmos and one for Osmosis.

$ lens keys add cosmos_acc --chain cosmoshub
$ lens keys add osmo_acc --chain osmosis

Then, run this program.

package main

import (
    "fmt"
    "log"
    "os"
    "os/signal"
    "path"
    "sync"
    "syscall"
    "time"

    lens "github.com/strangelove-ventures/lens/client"
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

func main() {
    encConfig := zapcore.EncoderConfig{
        MessageKey: "msg",
        LevelKey:   "level",
        TimeKey:    "time",
        NameKey:    "logger",
        CallerKey:  "caller",
        FunctionKey:    "func",
        StacktraceKey:  "trace",
        LineEnding: zapcore.DefaultLineEnding,
        EncodeLevel:    zapcore.LowercaseLevelEncoder,
        EncodeTime: zapcore.ISO8601TimeEncoder,
        EncodeDuration: zapcore.SecondsDurationEncoder,
        EncodeCaller:   zapcore.ShortCallerEncoder,
    }
    logger := zap.New(
        zapcore.NewCore(zapcore.NewJSONEncoder(encConfig), zapcore.AddSync(os.Stdout), zapcore.InfoLevel),
        zap.AddStacktrace(zapcore.PanicLevel),
    )

    homeDir := path.Join(os.Getenv("HOME"), ".lens")
    keyDir := path.Join(homeDir, "keys")

    cosmosCCC := &lens.ChainClientConfig{
        Key:        "cosmos_acc",
        ChainID:    "cosmoshub-4",
        RPCAddr:    "https://cosmos-rpc.polkachu.com:443",
        AccountPrefix:  "cosmos",
        KeyringBackend: "test",
        GasAdjustment:  1.2,
        GasPrices:  "0.025uatom",
        KeyDirectory:   keyDir,
        Debug:      false,
        Timeout:    "20s",
        OutputFormat:   "plain",
        SignModeStr:    "direct",
        Modules:    lens.ModuleBasics,
    }
    cosmosClient, err := lens.NewChainClient(logger, cosmosCCC, homeDir, os.Stdin, os.Stdout)
    if err != nil {
        log.Fatal(err)
    }

    osmoCCC := &lens.ChainClientConfig{
        Key:        "osmo_acc",
        ChainID:    "osmosis-1",
        RPCAddr:    "https://osmosis-rpc.polkachu.com:443",
        AccountPrefix:  "osmo",
        KeyringBackend: "test",
        GasAdjustment:  1.2,
        GasPrices:  "0uosmo",
        KeyDirectory:   keyDir,
        Debug:      false,
        Timeout:    "20s",
        OutputFormat:   "plain",
        SignModeStr:    "direct",
        Modules:    lens.ModuleBasics,
    }
    osmoClient, err := lens.NewChainClient(logger, osmoCCC, homeDir, os.Stdin, os.Stdout)
    if err != nil {
        log.Fatal(err)
    }

    cosmosAddr, err := cosmosClient.GetKeyAddress()
    if err != nil {
        log.Fatal(err)
    }
    osmoAddr, err := osmoClient.GetKeyAddress()
    if err != nil {
        log.Fatal(err)
    }

    var wg sync.WaitGroup

    wg.Add(1)
    go func() {
        defer wg.Done()
        time.Sleep(time.Second)
        done := cosmosClient.SetSDKContext()
        fmt.Println("Cosmos: ", cosmosAddr.String())
        done()
    }()

    wg.Add(1)
    go func() {
        defer wg.Done()
        time.Sleep(time.Second)
        done := osmoClient.SetSDKContext()
        fmt.Println("Osmosis:", osmoAddr.String())
        done()
    }()

    wg.Wait()
}

This code example is intentionally racy as to show that the results are different each time you run the thing.

# Cosmos client runs first.
Cosmos:  cosmos13x77yexvf6qexfjg9czp6jhpv7vpjdwwnsrvej
Osmosis: cosmos13x77yexvf6qexfjg9czp6jhpv7vpjdwwnsrvej

# Osmosis client runs first.
Osmosis: osmo13x77yexvf6qexfjg9czp6jhpv7vpjdwwmtsu0q
Cosmos:  osmo13x77yexvf6qexfjg9czp6jhpv7vpjdwwmtsu0q
KyleMoser commented 1 year ago

Was just talking about this today with some folks and happened to see this issue.

This behavior is a known issue with the Cosmos SDK. When you call SetSDKContext, that setting is global within the SDK. I recommend that you avoid the AccAddress's .String() function and call EncodeBech32AccAddr in Lens client/address.go instead.

As a second option, you could call a function that holds the lock on the SDK context until its finished generating the address. For example, your function could be something like this:

sdkConfigMutex.Lock()
defer sdkConfigMutex.Unlock()
sdkConf := sdk.GetConfig()
sdkConf.SetBech32PrefixForAccount(cc.Config.AccountPrefix, cc.Config.AccountPrefix+"pub")
sdkConf.SetBech32PrefixForValidator(cc.Config.AccountPrefix+"valoper", cc.Config.AccountPrefix+"valoperpub")
sdkConf.SetBech32PrefixForConsensusNode(cc.Config.AccountPrefix+"valcons", cc.Config.AccountPrefix+"valconspub")
osmoAddr.String()
Caruso33 commented 1 year ago

Thanks @KyleMoser , the first solution seems to work 🙏🏼