Skip to content

Commit

Permalink
Merge branch 'dev' into 20240911_2
Browse files Browse the repository at this point in the history
  • Loading branch information
meetrick authored Sep 26, 2024
2 parents bb1f812 + 6e47286 commit 48dea45
Show file tree
Hide file tree
Showing 20 changed files with 1,562 additions and 8 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ jobs:
with:
go-version-file: "go.mod"
check-latest: true
- name: Install pass helper
run: sudo apt-get update && sudo apt-get install -y pass
- name: Generate GPG key
run: "
echo \"%no-protection\nKey-Type: 1\nKey-Length: 4096\nSubkey-Type: 1\nSubkey-Length: 4096\nName-Comment: keyring_test\nExpire-Date: 0\" > genkey && gpg --gen-key --batch genkey"
- name: Setup OS keystore
run: pass init keyring_test
- name: Run test and calculate coverage
run: make coverage
- name: Upload coverage to Codecov
Expand Down
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ exclude: |
(?x)^(
chain/.*|
exchange/.*|
proto/.*
proto/.*|
client/keyring/testdata/.*
)$
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
Expand Down
26 changes: 21 additions & 5 deletions client/chain/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,8 @@ type chainClient struct {

sessionEnabled bool

ofacChecker *OfacChecker

authQueryClient authtypes.QueryClient
authzQueryClient authztypes.QueryClient
bankQueryClient banktypes.QueryClient
Expand Down Expand Up @@ -440,22 +442,27 @@ func NewChainClient(
subaccountToNonce: make(map[ethcommon.Hash]uint32),
}

cc.ofacChecker, err = NewOfacChecker()
if err != nil {
return nil, errors.Wrap(err, "Error creating OFAC checker")
}
if cc.canSign {
var err error

cc.accNum, cc.accSeq, err = cc.txFactory.AccountRetriever().GetAccountNumberSequence(ctx, ctx.GetFromAddress())
account, err := cc.txFactory.AccountRetriever().GetAccount(ctx, ctx.GetFromAddress())
if err != nil {
err = errors.Wrap(err, "failed to get initial account num and seq")
err = errors.Wrapf(err, "failed to get account")
return nil, err
}

if cc.ofacChecker.IsBlacklisted(account.GetAddress().String()) {
return nil, errors.Errorf("Address %s is in the OFAC list", account.GetAddress())
}
cc.accNum, cc.accSeq = account.GetAccountNumber(), account.GetSequence()
go cc.runBatchBroadcast()
go cc.syncTimeoutHeight()
}

return cc, nil
}

func (c *chainClient) syncNonce() {
num, seq, err := c.txFactory.AccountRetriever().GetAccountNumberSequence(c.ctx, c.ctx.GetFromAddress())
if err != nil {
Expand Down Expand Up @@ -1153,6 +1160,9 @@ func (c *chainClient) GetAuthzGrants(ctx context.Context, req authztypes.QueryGr
}

func (c *chainClient) BuildGenericAuthz(granter, grantee, msgtype string, expireIn time.Time) *authztypes.MsgGrant {
if c.ofacChecker.IsBlacklisted(granter) {
panic("Address is in the OFAC list") // panics should generally be avoided, but otherwise function signature should be changed
}
authz := authztypes.NewGenericAuthorization(msgtype)
authzAny := codectypes.UnsafePackAny(authz)
return &authztypes.MsgGrant{
Expand Down Expand Up @@ -1184,6 +1194,9 @@ var (
)

func (c *chainClient) BuildExchangeAuthz(granter, grantee string, authzType ExchangeAuthz, subaccountId string, markets []string, expireIn time.Time) *authztypes.MsgGrant {
if c.ofacChecker.IsBlacklisted(granter) {
panic("Address is in the OFAC list") // panics should generally be avoided, but otherwise function signature should be changed
}
var typedAuthzAny codectypes.Any
var typedAuthzBytes []byte
switch authzType {
Expand Down Expand Up @@ -1279,6 +1292,9 @@ func (c *chainClient) BuildExchangeBatchUpdateOrdersAuthz(
derivativeMarkets []string,
expireIn time.Time,
) *authztypes.MsgGrant {
if c.ofacChecker.IsBlacklisted(granter) {
panic("Address is in the OFAC list") // panics should generally be avoided, but otherwise function signature should be changed
}
typedAuthz := &exchangetypes.BatchUpdateOrdersAuthz{
SubaccountId: subaccountId,
SpotMarkets: spotMarkets,
Expand Down
71 changes: 70 additions & 1 deletion client/chain/chain_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package chain

import (
"encoding/json"
"io"
"os"
"testing"

"github.com/stretchr/testify/suite"

"github.com/InjectiveLabs/sdk-go/client"
"github.com/InjectiveLabs/sdk-go/client/common"
rpchttp "github.com/cometbft/cometbft/rpc/client/http"
Expand Down Expand Up @@ -51,6 +55,72 @@ func createClient(senderAddress cosmtypes.AccAddress, cosmosKeyring keyring.Keyr
return chainClient, err
}

type OfacTestSuite struct {
suite.Suite
network common.Network
tmClient *rpchttp.HTTP
senderAddress cosmtypes.AccAddress
cosmosKeyring keyring.Keyring
}

func (suite *OfacTestSuite) SetupTest() {
var err error
suite.network = common.LoadNetwork("testnet", "lb")
suite.tmClient, err = rpchttp.New(suite.network.TmEndpoint, "/websocket")
suite.NoError(err)

suite.senderAddress, suite.cosmosKeyring, err = accountForTests()
suite.NoError(err)

// Prepare OFAC list file
testList := []string{
suite.senderAddress.String(),
}
jsonData, err := json.Marshal(testList)
suite.NoError(err)

ofacListFilename = "ofac_test.json"
file, err := os.Create(getOfacListPath())
suite.NoError(err)

_, err = io.WriteString(file, string(jsonData))
suite.NoError(err)

err = file.Close()
suite.NoError(err)
}

func (suite *OfacTestSuite) TearDownTest() {
err := os.Remove(getOfacListPath())
suite.NoError(err)
ofacListFilename = defaultofacListFilename
}

func (suite *OfacTestSuite) TestOfacList() {
clientCtx, err := NewClientContext(
suite.network.ChainId,
suite.senderAddress.String(),
suite.cosmosKeyring,
)
suite.NoError(err)

clientCtx = clientCtx.WithNodeURI(suite.network.TmEndpoint).WithClient(suite.tmClient)
testChecker, err := NewOfacChecker()
suite.NoError(err)
suite.Equal(1, len(testChecker.ofacList))

_, err = NewChainClient(
clientCtx,
suite.network,
common.OptionGasPrices(client.DefaultGasPriceWithDenom),
)
suite.Error(err)
}

func TestOfacTestSuite(t *testing.T) {
suite.Run(t, new(OfacTestSuite))
}

func TestDefaultSubaccount(t *testing.T) {
network := common.LoadNetwork("devnet", "lb")
senderAddress, cosmosKeyring, err := accountForTests()
Expand Down Expand Up @@ -103,5 +173,4 @@ func TestGetSubaccountWithIndex(t *testing.T) {
if subaccountThirty != expectedSubaccountThirtyIdHash {
t.Error("The subaccount with index 30 was calculated incorrectly")
}

}
92 changes: 92 additions & 0 deletions client/chain/ofac.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package chain

import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path"
)

const (
defaultOfacListURL = "https://raw.githubusercontent.com/InjectiveLabs/injective-lists/master/wallets/ofac.json"
defaultofacListFilename = "ofac.json"
)

var (
ofacListFilename = defaultofacListFilename
)

type OfacChecker struct {
ofacListPath string
ofacList map[string]bool
}

func NewOfacChecker() (*OfacChecker, error) {
checker := &OfacChecker{
ofacListPath: getOfacListPath(),
}
if _, err := os.Stat(checker.ofacListPath); os.IsNotExist(err) {
if err := DownloadOfacList(); err != nil {
return nil, err
}
}
if err := checker.loadOfacList(); err != nil {
return nil, err
}
return checker, nil
}

func getOfacListPath() string {
return getFileAbsPath(path.Join("..", "metadata", ofacListFilename))
}

func DownloadOfacList() error {
resp, err := http.Get(defaultOfacListURL)
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("failed to download OFAC list, status code: %d", resp.StatusCode)
}

outFile, err := os.Create(getOfacListPath())
if err != nil {
return err
}
defer outFile.Close()

_, err = io.Copy(outFile, resp.Body)
if err != nil {
return err
}
_, err = outFile.WriteString("\n")
if err != nil {
return err
}
return nil
}

func (oc *OfacChecker) loadOfacList() error {
file, err := os.ReadFile(oc.ofacListPath)
if err != nil {
return err
}
var list []string
err = json.Unmarshal(file, &list)
if err != nil {
return err
}
oc.ofacList = make(map[string]bool)
for _, item := range list {
oc.ofacList[item] = true
}
return nil
}

func (oc *OfacChecker) IsBlacklisted(address string) bool {
return oc.ofacList[address]
}
101 changes: 101 additions & 0 deletions client/keyring/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Injective Chain Keyring Helper

Creates a new keyring from a variety of options. See `ConfigOpt` and related options. This keyring helper allows initializing the Cosmos SDK keyring used for signing transactions.

It allows flexibly defining a static configuration of keys, supports multiple pre-defined keys in the same keyring, and allows loading keys from a file, deriving from mnemonic, or reading plain private key bytes from a HEX string. Extremely useful for testing and local development, but also robust for production use cases.

## Usage

```go
NewCosmosKeyring(cdc codec.Codec, opts ...ConfigOpt) (sdk.AccAddress, cosmkeyring.Keyring, error)
```

**ConfigOpts:**

These options are global on the keyring level.

* `WithKeyringDir` option sets keyring path in the filesystem, useful when keyring backend is `file`.
* `WithKeyringAppName` option sets keyring application name (defaults to `injectived`)
* `WithKeyringBackend` sets the keyring backend. Expected values: `test`, `file`, `os`.
* `WithUseLedger` sets the option to use hardware wallet, if available on the system.

These options allow adding keys to the keyring during initialization.

* `WithKey` adds a single key to the keyring, without having alias name.
* `WithNamedKey` addes a single key to the keyring, with a name.
* `WithDefaultKey` sets a default key reference to use for signing (by name).

**KeyConfigOpts:**

These options are set per key.

* `WithKeyFrom` sets the key name to use for signing. Must exist in the provided keyring.
* `WithKeyPassphrase` sets the passphrase for keyring files. The package will fallback to `os.Stdin` if this option was not provided, but passphrase is required.
* `WithPrivKeyHex` allows specifying a private key as plain-text hex. Insecure option, use for testing only. The package will create a virtual keyring holding that key, to meet all the interfaces.
* `WithMnemonic` allows specifying a mnemonic phrase as plain-text hex. Insecure option, use for testing only. The package will create a virtual keyring to derive the keys and meet all the interfaces.

## Examples

Initialize an in-memory keyring with a private key hex:

```go
NewCosmosKeyring(
cdc,
WithKey(
WithPrivKeyHex("e6888cb164d52e4880e08a8a5dbe69cd62f67fde3d5906f2c5c951be553b2267"),
WithKeyFrom("sender"),
),
)
```

Initialize an in-memory keyring with a mnemonic phrase:

```go
NewCosmosKeyring(
s.cdc,
WithKey(
WithMnemonic("real simple naive ....... love"),
WithKeyFrom("sender"),
),
)
```

Real world use case of keyring initialization from CLI flags, with a single named key set as default:

```go
NewCosmosKeyring(
cdc,
WithKeyringDir(*keyringDir),
WithKeyringAppName(*keyringAppName),
WithKeyringBackend(Backend(*keyringBackend)),
WithNamedKey(
"dispatcher",
WithKeyFrom(*dispatcherKeyFrom),
WithKeyPassphrase(*dispatcherKeyPassphrase),
WithPrivKeyHex(*dispatcherKeyPrivateHex),
WithMnemonic(*dispatcherKeyMnemonic),
),
WithDefaultKey(
"dispatcher",
),
)
```

## Testing

```bash
go test -v -cover

PASS
coverage: 83.1% of statements
```

## Generating a Test Fixture

```bash
> cd testdata

> injectived keys --keyring-dir `pwd` --keyring-backend file add test
```

Passphrase should be `test12345678` for this fixture to work.
Loading

0 comments on commit 48dea45

Please sign in to comment.