feat: add shamir-coordinator-client for issuer wallet protection (#413)

* feat: add shamir-coordinator-client for issuer wallet protection
---------

Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com>
This commit is contained in:
Lorenz Herzberger 2024-06-26 14:02:28 +02:00 committed by GitHub
parent bd16b0a151
commit 7d87d662ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 284 additions and 60 deletions

View File

@ -0,0 +1,165 @@
package clients
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/url"
"strings"
"github.com/planetmint/planetmint-go/config"
)
// TODO: revert to actual rddl-claim-service client after CosmosSDK upgrade to v0.50.x
// see https://github.com/planetmint/planetmint-go/issues/384
var ShamirCoordinatorServiceClient IShamirCoordinatorClient
func lazyLoadShamirCoordinatorClient() IShamirCoordinatorClient {
if ShamirCoordinatorServiceClient != nil {
return ShamirCoordinatorServiceClient
}
cfg := config.GetConfig()
ShamirCoordinatorServiceClient = NewShamirCoordinatorClient(cfg.IssuerHost, &http.Client{})
return ShamirCoordinatorServiceClient
}
func SendTokens(ctx context.Context, recipient string, amount string, asset string) (txID string, err error) {
client := lazyLoadShamirCoordinatorClient()
res, err := client.SendTokens(ctx, recipient, amount, asset)
if err != nil {
return
}
return res.TxID, nil
}
func ReIssueAsset(ctx context.Context, asset string, amount string) (txID string, err error) {
client := lazyLoadShamirCoordinatorClient()
res, err := client.ReIssueAsset(ctx, asset, amount)
if err != nil {
return
}
return res.TxID, nil
}
type IShamirCoordinatorClient interface {
GetMnemonics(ctx context.Context) (res MnemonicsResponse, err error)
PostMnemonics(ctx context.Context, secret string) (err error)
SendTokens(ctx context.Context, recipient string, amount string, asset string) (res SendTokensResponse, err error)
ReIssueAsset(ctx context.Context, asset string, amount string) (res ReIssueResponse, err error)
}
type SendTokensRequest struct {
Recipient string `binding:"required" json:"recipient"`
Amount string `binding:"required" json:"amount"`
Asset string `binding:"required" json:"asset"`
}
type SendTokensResponse struct {
TxID string `binding:"required" json:"tx-id"`
}
type ReIssueRequest struct {
Asset string `binding:"required" json:"asset"`
Amount string `binding:"required" json:"amount"`
}
type ReIssueResponse struct {
TxID string `binding:"required" json:"tx-id"`
}
type MnemonicsResponse struct {
Mnemonics []string `binding:"required" json:"mnemonics"`
Seed string `binding:"required" json:"seed"`
}
type ShamirCoordinatorClient struct {
baseURL string
client *http.Client
}
func NewShamirCoordinatorClient(baseURL string, client *http.Client) *ShamirCoordinatorClient {
if client == nil {
client = &http.Client{}
}
return &ShamirCoordinatorClient{
baseURL: baseURL,
client: client,
}
}
func (scc *ShamirCoordinatorClient) GetMnemonics(ctx context.Context) (res MnemonicsResponse, err error) {
err = scc.doRequest(ctx, http.MethodGet, scc.baseURL+"/mnemonics", nil, &res)
return
}
func (scc *ShamirCoordinatorClient) PostMnemonics(ctx context.Context, secret string) (err error) {
err = scc.doRequest(ctx, http.MethodPost, scc.baseURL+"/mnemonics/"+url.PathEscape(secret), nil, nil)
return
}
func (scc *ShamirCoordinatorClient) SendTokens(ctx context.Context, recipient string, amount string, asset string) (res SendTokensResponse, err error) {
requestBody := SendTokensRequest{
Recipient: recipient,
Amount: amount,
Asset: asset,
}
err = scc.doRequest(ctx, http.MethodPost, scc.baseURL+"/send", &requestBody, &res)
return
}
func (scc *ShamirCoordinatorClient) ReIssueAsset(ctx context.Context, asset string, amount string) (res ReIssueResponse, err error) {
requestBody := ReIssueRequest{
Asset: asset,
Amount: amount,
}
err = scc.doRequest(ctx, http.MethodPost, scc.baseURL+"/reissue", &requestBody, &res)
return
}
func (scc *ShamirCoordinatorClient) doRequest(ctx context.Context, method, url string, body interface{}, response interface{}) (err error) {
var bodyReader io.Reader
if body != nil {
bodyBytes, err := json.Marshal(body)
if err != nil {
return err
}
bodyReader = bytes.NewBuffer(bodyBytes)
}
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
if err != nil {
return err
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := scc.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return &sccHTTPError{StatusCode: resp.StatusCode, Msg: strings.Join(resp.Header["Error"], "\n")}
}
if response != nil {
return json.NewDecoder(resp.Body).Decode(response)
}
return
}
type sccHTTPError struct {
StatusCode int
Msg string
}
func (e *sccHTTPError) Error() string {
return http.StatusText(e.StatusCode) + ": " + e.Msg
}

View File

@ -25,6 +25,7 @@ mqtt-user = "{{ .PlmntConfig.MqttUser }}"
mqtt-password = "{{ .PlmntConfig.MqttPassword }}"
claim-host = "{{ .PlmntConfig.ClaimHost }}"
mqtt-tls = {{ .PlmntConfig.MqttTLS }}
issuer-host = "{{ .PlmntConfig.IssuerHost }}"
`
// Config defines Planetmint's top level configuration
@ -43,6 +44,7 @@ type Config struct {
MqttPassword string `json:"mqtt-password" mapstructure:"mqtt-password"`
ClaimHost string `json:"claim-host" mapstructure:"claim-host"`
MqttTLS bool `json:"mqtt-tls" mapstructure:"mqtt-tls"`
IssuerHost string `json:"issuer-host" mapstructure:"issuer-host"`
}
// cosmos-sdk wide global singleton
@ -68,6 +70,7 @@ func DefaultConfig() *Config {
MqttPassword: "password",
ClaimHost: "https://testnet-p2r.rddl.io",
MqttTLS: true,
IssuerHost: "https://testnet-issuer.rddl.io",
}
}

View File

@ -0,0 +1,95 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: ./clients/shamir_coordinator_client.go
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
clients "github.com/planetmint/planetmint-go/clients"
)
// MockIShamirCoordinatorClient is a mock of IShamirCoordinatorClient interface.
type MockIShamirCoordinatorClient struct {
ctrl *gomock.Controller
recorder *MockIShamirCoordinatorClientMockRecorder
}
// MockIShamirCoordinatorClientMockRecorder is the mock recorder for MockIShamirCoordinatorClient.
type MockIShamirCoordinatorClientMockRecorder struct {
mock *MockIShamirCoordinatorClient
}
// NewMockIShamirCoordinatorClient creates a new mock instance.
func NewMockIShamirCoordinatorClient(ctrl *gomock.Controller) *MockIShamirCoordinatorClient {
mock := &MockIShamirCoordinatorClient{ctrl: ctrl}
mock.recorder = &MockIShamirCoordinatorClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockIShamirCoordinatorClient) EXPECT() *MockIShamirCoordinatorClientMockRecorder {
return m.recorder
}
// GetMnemonics mocks base method.
func (m *MockIShamirCoordinatorClient) GetMnemonics(ctx context.Context) (clients.MnemonicsResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMnemonics", ctx)
ret0, _ := ret[0].(clients.MnemonicsResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetMnemonics indicates an expected call of GetMnemonics.
func (mr *MockIShamirCoordinatorClientMockRecorder) GetMnemonics(ctx interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMnemonics", reflect.TypeOf((*MockIShamirCoordinatorClient)(nil).GetMnemonics), ctx)
}
// PostMnemonics mocks base method.
func (m *MockIShamirCoordinatorClient) PostMnemonics(ctx context.Context, secret string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PostMnemonics", ctx, secret)
ret0, _ := ret[0].(error)
return ret0
}
// PostMnemonics indicates an expected call of PostMnemonics.
func (mr *MockIShamirCoordinatorClientMockRecorder) PostMnemonics(ctx, secret interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostMnemonics", reflect.TypeOf((*MockIShamirCoordinatorClient)(nil).PostMnemonics), ctx, secret)
}
// ReIssueAsset mocks base method.
func (m *MockIShamirCoordinatorClient) ReIssueAsset(ctx context.Context, asset, amount string) (clients.ReIssueResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReIssueAsset", ctx, asset, amount)
ret0, _ := ret[0].(clients.ReIssueResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReIssueAsset indicates an expected call of ReIssueAsset.
func (mr *MockIShamirCoordinatorClientMockRecorder) ReIssueAsset(ctx, asset, amount interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReIssueAsset", reflect.TypeOf((*MockIShamirCoordinatorClient)(nil).ReIssueAsset), ctx, asset, amount)
}
// SendTokens mocks base method.
func (m *MockIShamirCoordinatorClient) SendTokens(ctx context.Context, recipient, amount, asset string) (clients.SendTokensResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SendTokens", ctx, recipient, amount, asset)
ret0, _ := ret[0].(clients.SendTokensResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SendTokens indicates an expected call of SendTokens.
func (mr *MockIShamirCoordinatorClientMockRecorder) SendTokens(ctx, recipient, amount, asset interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendTokens", reflect.TypeOf((*MockIShamirCoordinatorClient)(nil).SendTokens), ctx, recipient, amount, asset)
}

View File

@ -19,7 +19,7 @@ import (
"github.com/planetmint/planetmint-go/clients"
"github.com/planetmint/planetmint-go/monitor"
monitormocks "github.com/planetmint/planetmint-go/monitor/mocks"
claimmocks "github.com/planetmint/planetmint-go/testutil/mocks"
clientmocks "github.com/planetmint/planetmint-go/testutil/mocks"
"github.com/planetmint/planetmint-go/testutil/sample"
"github.com/planetmint/planetmint-go/util"
"github.com/planetmint/planetmint-go/util/mocks"
@ -49,12 +49,21 @@ func Load(t *testing.T, configs ...Config) *Network {
elements.Client = &elementsmocks.MockClient{}
util.RegisterAssetServiceHTTPClient = &mocks.MockClient{}
ctrl := gomock.NewController(t)
claimMock := claimmocks.NewMockIRCClient(ctrl)
claimMock := clientmocks.NewMockIRCClient(ctrl)
claimMock.EXPECT().PostClaim(gomock.Any(), gomock.Any()).AnyTimes().Return(clients.PostClaimResponse{
TxID: "0000000000000000000000000000000000000000000000000000000000000000",
}, nil)
clients.ClaimServiceClient = claimMock
shamirMock := clientmocks.NewMockIShamirCoordinatorClient(ctrl)
shamirMock.EXPECT().SendTokens(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(clients.SendTokensResponse{
TxID: "7add40beb27df701e02ee85089c5bc0021bc813823fedb5f1dcb5debda7f3da9",
}, nil)
shamirMock.EXPECT().ReIssueAsset(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(clients.ReIssueResponse{
TxID: "7add40beb27df701e02ee85089c5bc0021bc813823fedb5f1dcb5debda7f3da9",
}, nil)
clients.ShamirCoordinatorServiceClient = shamirMock
// enable application logger in tests
appLogger := util.GetAppLogger()
appLogger.SetTestingLogger(t)

View File

@ -4,7 +4,6 @@ import (
"crypto/sha256"
"encoding/json"
"fmt"
"strings"
"sync"
"github.com/planetmint/planetmint-go/config"
@ -18,41 +17,6 @@ var (
elementsSyncAccess sync.Mutex
)
func ReissueAsset(reissueTx string) (txID string, err error) {
conf := config.GetConfig()
url := conf.GetRPCURL()
cmdArgs := strings.Split(reissueTx, " ")
elementsSyncAccess.Lock()
defer elementsSyncAccess.Unlock()
result, err := elements.ReissueAsset(url, []string{`"` + cmdArgs[1] + `"`, cmdArgs[2]})
if err != nil {
return
}
txID = result.TxID
return
}
func DistributeAsset(address string, amount string, reissuanceAsset string) (txID string, err error) {
conf := config.GetConfig()
url := conf.GetRPCURL()
elementsSyncAccess.Lock()
defer elementsSyncAccess.Unlock()
txID, err = elements.SendToAddress(url, []string{
`"` + address + `"`,
`"` + amount + `"`,
`""`,
`""`,
"false",
"true",
"null",
`"unset"`,
"false",
`"` + reissuanceAsset + `"`,
})
return
}
func IssueNFTAsset(name string, machineAddress string, domain string) (assetID string, contract string, hexTx string, err error) {
conf := config.GetConfig()
url := conf.GetRPCURL()

View File

@ -15,22 +15,6 @@ import (
"github.com/stretchr/testify/assert"
)
func TestReissueAsset(t *testing.T) {
elements.Client = &elementsmocks.MockClient{}
_, err := util.ReissueAsset("reissueasset 06c20c8de513527f1ae6c901f74a05126525ac2d7e89306f4a7fd5ec4e674403 900.000")
assert.NoError(t, err)
}
func TestDistributeAsset(t *testing.T) {
elements.Client = &elementsmocks.MockClient{}
_, err := util.DistributeAsset(
"tlq1qqt5078sef4aqls29c3j3pwfmukgjug70t37x26gwyhzpdxmtmjmphar88fwsl9qcm559jevve772prhtuyf9xkxdtrhvuce6a",
"20",
"06c20c8de513527f1ae6c901f74a05126525ac2d7e89306f4a7fd5ec4e674403")
assert.NoError(t, err)
}
func TestIssueNFTAsset(t *testing.T) {
elements.Client = &elementsmocks.MockClient{}

View File

@ -5,6 +5,7 @@ import (
errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/planetmint/planetmint-go/clients"
"github.com/planetmint/planetmint-go/errormsg"
"github.com/planetmint/planetmint-go/util"
"github.com/planetmint/planetmint-go/x/dao/types"
@ -42,23 +43,23 @@ func (k msgServer) DistributionRequest(goCtx context.Context, msg *types.MsgDist
reissuanceAsset := k.GetParams(ctx).ReissuanceAsset
util.GetAppLogger().Info(ctx, distributionRequestTag+"entering asset distribution mode")
// issue 5 distributions:
earlyInvestorTx, err := util.DistributeAsset(msg.Distribution.EarlyInvAddr, msg.Distribution.EarlyInvAmount, reissuanceAsset)
earlyInvestorTx, err := clients.SendTokens(goCtx, msg.Distribution.EarlyInvAddr, msg.Distribution.EarlyInvAmount, reissuanceAsset)
if err != nil {
util.GetAppLogger().Error(ctx, distributionRequestTag+"could not distribute asset to early investors: "+err.Error())
}
investorTx, err := util.DistributeAsset(msg.Distribution.InvestorAddr, msg.Distribution.InvestorAmount, reissuanceAsset)
investorTx, err := clients.SendTokens(goCtx, msg.Distribution.InvestorAddr, msg.Distribution.InvestorAmount, reissuanceAsset)
if err != nil {
util.GetAppLogger().Error(ctx, distributionRequestTag+"could not distribute asset to investors: "+err.Error())
}
strategicTx, err := util.DistributeAsset(msg.Distribution.StrategicAddr, msg.Distribution.StrategicAmount, reissuanceAsset)
strategicTx, err := clients.SendTokens(goCtx, msg.Distribution.StrategicAddr, msg.Distribution.StrategicAmount, reissuanceAsset)
if err != nil {
util.GetAppLogger().Error(ctx, distributionRequestTag+"could not distribute asset to strategic investments: "+err.Error())
}
popTx, err := util.DistributeAsset(msg.Distribution.PopAddr, msg.Distribution.PopAmount, reissuanceAsset)
popTx, err := clients.SendTokens(goCtx, msg.Distribution.PopAddr, msg.Distribution.PopAmount, reissuanceAsset)
if err != nil {
util.GetAppLogger().Error(ctx, distributionRequestTag+"could not distribute asset to PoP: "+err.Error())
}
daoTx, err := util.DistributeAsset(msg.Distribution.DaoAddr, msg.Distribution.DaoAmount, reissuanceAsset)
daoTx, err := clients.SendTokens(goCtx, msg.Distribution.DaoAddr, msg.Distribution.DaoAmount, reissuanceAsset)
if err != nil {
util.GetAppLogger().Error(ctx, distributionRequestTag+"could not distribute asset to DAO: "+err.Error())
}

View File

@ -2,9 +2,11 @@ package keeper
import (
"context"
"strings"
errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/planetmint/planetmint-go/clients"
"github.com/planetmint/planetmint-go/errormsg"
"github.com/planetmint/planetmint-go/util"
"github.com/planetmint/planetmint-go/x/dao/types"
@ -41,7 +43,8 @@ func (k msgServer) ReissueRDDLProposal(goCtx context.Context, msg *types.MsgReis
}
util.GetAppLogger().Info(ctx, reissueTag+"asset: "+msg.GetCommand())
txID, err := util.ReissueAsset(msg.Command)
cmdArgs := strings.Split(msg.Command, " ")
txID, err := clients.ReIssueAsset(goCtx, cmdArgs[1], cmdArgs[2])
if err != nil {
util.GetAppLogger().Error(ctx, reissueTag+"asset reissuance failed: "+err.Error())
}