mirror of
https://github.com/planetmint/planetmint-go.git
synced 2025-06-06 14:16:39 +00:00
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:
parent
bd16b0a151
commit
7d87d662ea
165
clients/shamir_coordinator_client.go
Normal file
165
clients/shamir_coordinator_client.go
Normal 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
|
||||
}
|
@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
|
95
testutil/mocks/shamir_coordinator_client_mock.go
Normal file
95
testutil/mocks/shamir_coordinator_client_mock.go
Normal 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)
|
||||
}
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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{}
|
||||
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user