753 lines
30 KiB
Go
753 lines
30 KiB
Go
package gateway
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/tech/sendico/gateway/chain/internal/appversion"
|
||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||
chainstoragemodel "github.com/tech/sendico/gateway/chain/storage/model"
|
||
chainasset "github.com/tech/sendico/pkg/chain"
|
||
"github.com/tech/sendico/pkg/connector/params"
|
||
"github.com/tech/sendico/pkg/merrors"
|
||
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||
"go.uber.org/zap"
|
||
"google.golang.org/protobuf/types/known/structpb"
|
||
"google.golang.org/protobuf/types/known/timestamppb"
|
||
)
|
||
|
||
const chainConnectorID = "chain"
|
||
|
||
func (s *Service) GetCapabilities(_ context.Context, _ *connectorv1.GetCapabilitiesRequest) (*connectorv1.GetCapabilitiesResponse, error) {
|
||
return &connectorv1.GetCapabilitiesResponse{
|
||
Capabilities: &connectorv1.ConnectorCapabilities{
|
||
ConnectorType: chainConnectorID,
|
||
Version: appversion.Create().Short(),
|
||
SupportedAccountKinds: []connectorv1.AccountKind{connectorv1.AccountKind_CHAIN_MANAGED_WALLET},
|
||
SupportedOperationTypes: []connectorv1.OperationType{
|
||
connectorv1.OperationType_TRANSFER,
|
||
connectorv1.OperationType_FEE_ESTIMATE,
|
||
connectorv1.OperationType_GAS_TOPUP,
|
||
},
|
||
OpenAccountParams: chainOpenAccountParams(),
|
||
OperationParams: chainOperationParams(),
|
||
},
|
||
}, nil
|
||
}
|
||
|
||
func (s *Service) OpenAccount(ctx context.Context, req *connectorv1.OpenAccountRequest) (*connectorv1.OpenAccountResponse, error) {
|
||
if req == nil {
|
||
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: request is required", nil, "")}, nil
|
||
}
|
||
if req.GetKind() != connectorv1.AccountKind_CHAIN_MANAGED_WALLET {
|
||
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_ACCOUNT_KIND, "open_account: unsupported account kind", nil, "")}, nil
|
||
}
|
||
reader := params.New(req.GetParams())
|
||
orgRef := strings.TrimSpace(reader.String("organization_ref"))
|
||
if orgRef == "" {
|
||
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: organization_ref is required", nil, "")}, nil
|
||
}
|
||
asset, err := parseChainAsset(strings.TrimSpace(req.GetAsset()), reader)
|
||
if err != nil {
|
||
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), nil, "")}, nil
|
||
}
|
||
|
||
resp, err := s.CreateManagedWallet(ctx, &chainv1.CreateManagedWalletRequest{
|
||
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
||
OrganizationRef: orgRef,
|
||
OwnerRef: strings.TrimSpace(req.GetOwnerRef()),
|
||
Asset: asset,
|
||
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
|
||
Describable: describableFromLabel(req.GetLabel(), reader.String("description")),
|
||
})
|
||
if err != nil {
|
||
return &connectorv1.OpenAccountResponse{Error: connectorError(mapErrorCode(err), err.Error(), nil, "")}, nil
|
||
}
|
||
return &connectorv1.OpenAccountResponse{Account: chainWalletToAccount(resp.GetWallet())}, nil
|
||
}
|
||
|
||
func (s *Service) GetAccount(ctx context.Context, req *connectorv1.GetAccountRequest) (*connectorv1.GetAccountResponse, error) {
|
||
if req == nil || req.GetAccountRef() == nil || strings.TrimSpace(req.GetAccountRef().GetAccountId()) == "" {
|
||
return nil, merrors.InvalidArgument("get_account: account_ref.account_id is required")
|
||
}
|
||
resp, err := s.GetManagedWallet(ctx, &chainv1.GetManagedWalletRequest{WalletRef: strings.TrimSpace(req.GetAccountRef().GetAccountId())})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &connectorv1.GetAccountResponse{Account: chainWalletToAccount(resp.GetWallet())}, nil
|
||
}
|
||
|
||
func (s *Service) ListAccounts(ctx context.Context, req *connectorv1.ListAccountsRequest) (*connectorv1.ListAccountsResponse, error) {
|
||
if req == nil {
|
||
return nil, merrors.InvalidArgument("list_accounts: request is required")
|
||
}
|
||
asset := (*chainv1.Asset)(nil)
|
||
if assetString := strings.TrimSpace(req.GetAsset()); assetString != "" {
|
||
parsed, err := parseChainAsset(assetString, params.New(nil))
|
||
if err != nil {
|
||
s.logger.Warn("Error listing accounts", zap.Error(err))
|
||
return nil, err
|
||
}
|
||
asset = parsed
|
||
}
|
||
resp, err := s.ListManagedWallets(ctx, &chainv1.ListManagedWalletsRequest{
|
||
OrganizationRef: strings.TrimSpace(req.GetOrganizationRef()),
|
||
OwnerRefFilter: req.GetOwnerRefFilter(),
|
||
Asset: asset,
|
||
Page: req.GetPage(),
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
accounts := make([]*connectorv1.Account, 0, len(resp.GetWallets()))
|
||
for _, wallet := range resp.GetWallets() {
|
||
accounts = append(accounts, chainWalletToAccount(wallet))
|
||
}
|
||
return &connectorv1.ListAccountsResponse{
|
||
Accounts: accounts,
|
||
Page: resp.GetPage(),
|
||
}, nil
|
||
}
|
||
|
||
func (s *Service) GetBalance(ctx context.Context, req *connectorv1.GetBalanceRequest) (*connectorv1.GetBalanceResponse, error) {
|
||
if req == nil || req.GetAccountRef() == nil || strings.TrimSpace(req.GetAccountRef().GetAccountId()) == "" {
|
||
return nil, merrors.InvalidArgument("get_balance: account_ref.account_id is required")
|
||
}
|
||
resp, err := s.GetWalletBalance(ctx, &chainv1.GetWalletBalanceRequest{WalletRef: strings.TrimSpace(req.GetAccountRef().GetAccountId())})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
bal := resp.GetBalance()
|
||
return &connectorv1.GetBalanceResponse{
|
||
Balance: &connectorv1.Balance{
|
||
AccountRef: req.GetAccountRef(),
|
||
Available: bal.GetAvailable(),
|
||
PendingInbound: bal.GetPendingInbound(),
|
||
PendingOutbound: bal.GetPendingOutbound(),
|
||
CalculatedAt: bal.GetCalculatedAt(),
|
||
},
|
||
}, nil
|
||
}
|
||
|
||
func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOperationRequest) (*connectorv1.SubmitOperationResponse, error) {
|
||
if req == nil || req.GetOperation() == nil {
|
||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: operation is required", nil, "")}}, nil
|
||
}
|
||
op := req.GetOperation()
|
||
if strings.TrimSpace(op.GetIdempotencyKey()) == "" {
|
||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: idempotency_key is required", op, "")}}, nil
|
||
}
|
||
reader := params.New(op.GetParams())
|
||
orgRef := strings.TrimSpace(reader.String("organization_ref"))
|
||
source := operationAccountID(op.GetFrom())
|
||
if source == "" {
|
||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "operation: from.account is required", op, "")}}, nil
|
||
}
|
||
|
||
switch op.GetType() {
|
||
case connectorv1.OperationType_TRANSFER:
|
||
dest, err := transferDestinationFromOperation(op)
|
||
if err != nil {
|
||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil
|
||
}
|
||
amount := op.GetMoney()
|
||
if amount == nil {
|
||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: money is required", op, "")}}, nil
|
||
}
|
||
amount = normalizeMoneyForChain(amount)
|
||
if orgRef == "" {
|
||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: organization_ref is required", op, "")}}, nil
|
||
}
|
||
resp, err := s.SubmitTransfer(ctx, &chainv1.SubmitTransferRequest{
|
||
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
||
OrganizationRef: orgRef,
|
||
SourceWalletRef: source,
|
||
IntentRef: strings.TrimSpace(op.GetIntentRef()),
|
||
OperationRef: op.GetOperationRef(),
|
||
Destination: dest,
|
||
Amount: amount,
|
||
Fees: parseChainFees(reader),
|
||
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
|
||
PaymentRef: strings.TrimSpace(reader.String("payment_ref")),
|
||
})
|
||
if err != nil {
|
||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||
}
|
||
transfer := resp.GetTransfer()
|
||
return &connectorv1.SubmitOperationResponse{
|
||
Receipt: &connectorv1.OperationReceipt{
|
||
OperationId: strings.TrimSpace(transfer.GetTransferRef()),
|
||
Status: chainTransferStatusToOperation(transfer.GetStatus()),
|
||
ProviderRef: strings.TrimSpace(transfer.GetTransactionHash()),
|
||
},
|
||
}, nil
|
||
case connectorv1.OperationType_FEE_ESTIMATE:
|
||
dest, err := transferDestinationFromOperation(op)
|
||
if err != nil {
|
||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil
|
||
}
|
||
amount := op.GetMoney()
|
||
if amount == nil {
|
||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "estimate: money is required", op, "")}}, nil
|
||
}
|
||
amount = normalizeMoneyForChain(amount)
|
||
opID := strings.TrimSpace(op.GetOperationId())
|
||
if opID == "" {
|
||
opID = strings.TrimSpace(op.GetIdempotencyKey())
|
||
}
|
||
resp, err := s.EstimateTransferFee(ctx, &chainv1.EstimateTransferFeeRequest{
|
||
SourceWalletRef: source,
|
||
Destination: dest,
|
||
Amount: amount,
|
||
})
|
||
if err != nil {
|
||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||
}
|
||
result := feeEstimateResult(resp)
|
||
return &connectorv1.SubmitOperationResponse{
|
||
Receipt: &connectorv1.OperationReceipt{
|
||
OperationId: opID,
|
||
Status: connectorv1.OperationStatus_OPERATION_SUCCESS,
|
||
Result: result,
|
||
},
|
||
}, nil
|
||
case connectorv1.OperationType_GAS_TOPUP:
|
||
fee, err := parseMoneyFromMap(reader.Map("estimated_total_fee"))
|
||
if err != nil {
|
||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil
|
||
}
|
||
fee = normalizeMoneyForChain(fee)
|
||
mode := strings.ToLower(strings.TrimSpace(reader.String("mode")))
|
||
if mode == "" {
|
||
mode = "compute"
|
||
}
|
||
switch mode {
|
||
case "compute":
|
||
opID := strings.TrimSpace(op.GetOperationId())
|
||
if opID == "" {
|
||
opID = strings.TrimSpace(op.GetIdempotencyKey())
|
||
}
|
||
resp, err := s.ComputeGasTopUp(ctx, &chainv1.ComputeGasTopUpRequest{
|
||
WalletRef: source,
|
||
EstimatedTotalFee: fee,
|
||
})
|
||
if err != nil {
|
||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||
}
|
||
return &connectorv1.SubmitOperationResponse{
|
||
Receipt: &connectorv1.OperationReceipt{
|
||
OperationId: opID,
|
||
Status: connectorv1.OperationStatus_OPERATION_SUCCESS,
|
||
Result: gasTopUpResult(resp.GetTopupAmount(), resp.GetCapHit(), ""),
|
||
},
|
||
}, nil
|
||
case "ensure":
|
||
opID := strings.TrimSpace(op.GetOperationId())
|
||
if opID == "" {
|
||
opID = strings.TrimSpace(op.GetIdempotencyKey())
|
||
}
|
||
if orgRef == "" {
|
||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "gas_topup: organization_ref is required", op, "")}}, nil
|
||
}
|
||
target := strings.TrimSpace(reader.String("target_wallet_ref"))
|
||
if target == "" {
|
||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "gas_topup: target_wallet_ref is required", op, "")}}, nil
|
||
}
|
||
resp, err := s.EnsureGasTopUp(ctx, &chainv1.EnsureGasTopUpRequest{
|
||
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
||
IntentRef: strings.TrimSpace(op.GetIntentRef()),
|
||
OperationRef: strings.TrimSpace(op.GetOperationRef()),
|
||
OrganizationRef: orgRef,
|
||
SourceWalletRef: source,
|
||
TargetWalletRef: target,
|
||
EstimatedTotalFee: fee,
|
||
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
|
||
PaymentRef: strings.TrimSpace(reader.String("payment_ref")),
|
||
})
|
||
if err != nil {
|
||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||
}
|
||
transferRef := ""
|
||
if transfer := resp.GetTransfer(); transfer != nil {
|
||
transferRef = strings.TrimSpace(transfer.GetTransferRef())
|
||
}
|
||
return &connectorv1.SubmitOperationResponse{
|
||
Receipt: &connectorv1.OperationReceipt{
|
||
OperationId: opID,
|
||
Status: shared.СhainTransferStatusToOperation(resp.GetTransfer().GetStatus()),
|
||
Result: gasTopUpResult(resp.GetTopupAmount(), resp.GetCapHit(), transferRef),
|
||
},
|
||
}, nil
|
||
default:
|
||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "gas_topup: invalid mode", op, "")}}, nil
|
||
}
|
||
default:
|
||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_OPERATION, "submit_operation: unsupported operation type", op, "")}}, nil
|
||
}
|
||
}
|
||
|
||
func (s *Service) GetOperation(ctx context.Context, req *connectorv1.GetOperationRequest) (*connectorv1.GetOperationResponse, error) {
|
||
if req == nil || strings.TrimSpace(req.GetOperationId()) == "" {
|
||
return nil, merrors.InvalidArgument("get_operation: operation_id is required")
|
||
}
|
||
|
||
operationRef := strings.TrimSpace(req.GetOperationId())
|
||
if s.storage == nil || s.storage.Transfers() == nil {
|
||
return nil, merrors.Internal("get_operation: storage is not configured")
|
||
}
|
||
|
||
transfer, err := s.storage.Transfers().FindByOperationRef(ctx, "", operationRef)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if transfer == nil {
|
||
return nil, merrors.NoData("transfer not found")
|
||
}
|
||
|
||
return &connectorv1.GetOperationResponse{Operation: chainTransferToOperation(storageTransferToProto(transfer))}, nil
|
||
}
|
||
|
||
func (s *Service) ListOperations(ctx context.Context, req *connectorv1.ListOperationsRequest) (*connectorv1.ListOperationsResponse, error) {
|
||
if req == nil {
|
||
return nil, merrors.InvalidArgument("list_operations: request is required")
|
||
}
|
||
source := ""
|
||
if req.GetAccountRef() != nil {
|
||
source = strings.TrimSpace(req.GetAccountRef().GetAccountId())
|
||
}
|
||
resp, err := s.ListTransfers(ctx, &chainv1.ListTransfersRequest{
|
||
SourceWalletRef: source,
|
||
Status: chainStatusFromOperation(req.GetStatus()),
|
||
Page: req.GetPage(),
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
ops := make([]*connectorv1.Operation, 0, len(resp.GetTransfers()))
|
||
for _, transfer := range resp.GetTransfers() {
|
||
ops = append(ops, chainTransferToOperation(transfer))
|
||
}
|
||
return &connectorv1.ListOperationsResponse{Operations: ops, Page: resp.GetPage()}, nil
|
||
}
|
||
|
||
func chainOpenAccountParams() []*connectorv1.ParamSpec {
|
||
return []*connectorv1.ParamSpec{
|
||
{Key: "organization_ref", Type: connectorv1.ParamType_STRING, Required: true, Description: "Organization reference for the wallet."},
|
||
{Key: "network", Type: connectorv1.ParamType_STRING, Required: true, Description: "Blockchain network name."},
|
||
{Key: "token_symbol", Type: connectorv1.ParamType_STRING, Required: true, Description: "Token symbol (e.g., USDT)."},
|
||
{Key: "contract_address", Type: connectorv1.ParamType_STRING, Required: false, Description: "Token contract address override."},
|
||
{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false, Description: "Additional metadata map."},
|
||
{Key: "description", Type: connectorv1.ParamType_STRING, Required: false, Description: "Wallet description."},
|
||
}
|
||
}
|
||
|
||
func chainOperationParams() []*connectorv1.OperationParamSpec {
|
||
return []*connectorv1.OperationParamSpec{
|
||
{OperationType: connectorv1.OperationType_TRANSFER, Params: []*connectorv1.ParamSpec{
|
||
{Key: "organization_ref", Type: connectorv1.ParamType_STRING, Required: true, Description: "Organization reference."},
|
||
{Key: "destination_memo", Type: connectorv1.ParamType_STRING, Required: false, Description: "Destination memo/tag."},
|
||
{Key: "client_reference", Type: connectorv1.ParamType_STRING, Required: false, Description: "Client reference id."},
|
||
{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false, Description: "Transfer metadata."},
|
||
{Key: "fees", Type: connectorv1.ParamType_JSON, Required: false, Description: "Service fee breakdowns."},
|
||
}},
|
||
{OperationType: connectorv1.OperationType_FEE_ESTIMATE, Params: []*connectorv1.ParamSpec{
|
||
{Key: "organization_ref", Type: connectorv1.ParamType_STRING, Required: false, Description: "Organization reference."},
|
||
{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false, Description: "Estimate metadata."},
|
||
}},
|
||
{OperationType: connectorv1.OperationType_GAS_TOPUP, Params: []*connectorv1.ParamSpec{
|
||
{Key: "mode", Type: connectorv1.ParamType_STRING, Required: false, Description: "compute | ensure."},
|
||
{Key: "organization_ref", Type: connectorv1.ParamType_STRING, Required: false, Description: "Organization reference (required for ensure)."},
|
||
{Key: "target_wallet_ref", Type: connectorv1.ParamType_STRING, Required: false, Description: "Target wallet ref (ensure)."},
|
||
{Key: "estimated_total_fee", Type: connectorv1.ParamType_JSON, Required: true, Description: "Estimated total fee {amount,currency}."},
|
||
{Key: "client_reference", Type: connectorv1.ParamType_STRING, Required: false, Description: "Client reference."},
|
||
{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false, Description: "Top-up metadata."},
|
||
}},
|
||
}
|
||
}
|
||
|
||
func chainWalletToAccount(wallet *chainv1.ManagedWallet) *connectorv1.Account {
|
||
if wallet == nil {
|
||
return nil
|
||
}
|
||
details, _ := structpb.NewStruct(map[string]interface{}{
|
||
"deposit_address": wallet.GetDepositAddress(),
|
||
"organization_ref": wallet.GetOrganizationRef(),
|
||
"owner_ref": wallet.GetOwnerRef(),
|
||
"network": wallet.GetAsset().GetChain().String(),
|
||
"token_symbol": wallet.GetAsset().GetTokenSymbol(),
|
||
"contract_address": wallet.GetAsset().GetContractAddress(),
|
||
"wallet_ref": wallet.GetWalletRef(),
|
||
})
|
||
return &connectorv1.Account{
|
||
Ref: &connectorv1.AccountRef{
|
||
ConnectorId: chainConnectorID,
|
||
AccountId: strings.TrimSpace(wallet.GetWalletRef()),
|
||
},
|
||
Kind: connectorv1.AccountKind_CHAIN_MANAGED_WALLET,
|
||
Asset: chainasset.AssetString(wallet.GetAsset()),
|
||
State: chainWalletState(wallet.GetStatus()),
|
||
Label: strings.TrimSpace(wallet.GetDescribable().GetName()),
|
||
OwnerRef: strings.TrimSpace(wallet.GetOwnerRef()),
|
||
ProviderDetails: details,
|
||
CreatedAt: wallet.GetCreatedAt(),
|
||
UpdatedAt: wallet.GetUpdatedAt(),
|
||
Describable: wallet.GetDescribable(),
|
||
}
|
||
}
|
||
|
||
func chainWalletState(status chainv1.ManagedWalletStatus) connectorv1.AccountState {
|
||
switch status {
|
||
case chainv1.ManagedWalletStatus_MANAGED_WALLET_ACTIVE:
|
||
return connectorv1.AccountState_ACCOUNT_ACTIVE
|
||
case chainv1.ManagedWalletStatus_MANAGED_WALLET_SUSPENDED:
|
||
return connectorv1.AccountState_ACCOUNT_SUSPENDED
|
||
case chainv1.ManagedWalletStatus_MANAGED_WALLET_CLOSED:
|
||
return connectorv1.AccountState_ACCOUNT_CLOSED
|
||
default:
|
||
return connectorv1.AccountState_ACCOUNT_STATE_UNSPECIFIED
|
||
}
|
||
}
|
||
|
||
func transferDestinationFromOperation(op *connectorv1.Operation) (*chainv1.TransferDestination, error) {
|
||
if op == nil {
|
||
return nil, merrors.InvalidArgument("transfer: operation is required")
|
||
}
|
||
if to := op.GetTo(); to != nil {
|
||
if account := to.GetAccount(); account != nil {
|
||
return &chainv1.TransferDestination{Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(account.GetAccountId())}}, nil
|
||
}
|
||
if ext := to.GetExternal(); ext != nil {
|
||
return &chainv1.TransferDestination{Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(ext.GetExternalRef())}}, nil
|
||
}
|
||
}
|
||
return nil, merrors.InvalidArgument("transfer: to.account or to.external is required")
|
||
}
|
||
|
||
func normalizeMoneyForChain(m *moneyv1.Money) *moneyv1.Money {
|
||
if m == nil {
|
||
return nil
|
||
}
|
||
currency := strings.TrimSpace(m.GetCurrency())
|
||
if idx := strings.Index(currency, "-"); idx > 0 {
|
||
currency = currency[:idx]
|
||
}
|
||
return &moneyv1.Money{
|
||
Amount: strings.TrimSpace(m.GetAmount()),
|
||
Currency: currency,
|
||
}
|
||
}
|
||
|
||
func parseChainFees(reader params.Reader) []*chainv1.ServiceFeeBreakdown {
|
||
rawFees := reader.List("fees")
|
||
if len(rawFees) == 0 {
|
||
return nil
|
||
}
|
||
result := make([]*chainv1.ServiceFeeBreakdown, 0, len(rawFees))
|
||
for _, item := range rawFees {
|
||
raw, ok := item.(map[string]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
amount := strings.TrimSpace(fmt.Sprint(raw["amount"]))
|
||
currency := strings.TrimSpace(fmt.Sprint(raw["currency"]))
|
||
if amount == "" || currency == "" {
|
||
continue
|
||
}
|
||
result = append(result, &chainv1.ServiceFeeBreakdown{
|
||
FeeCode: strings.TrimSpace(fmt.Sprint(raw["fee_code"])),
|
||
Description: strings.TrimSpace(fmt.Sprint(raw["description"])),
|
||
Amount: &moneyv1.Money{Amount: amount, Currency: currency},
|
||
})
|
||
}
|
||
if len(result) == 0 {
|
||
return nil
|
||
}
|
||
return result
|
||
}
|
||
|
||
func parseMoneyFromMap(raw map[string]interface{}) (*moneyv1.Money, error) {
|
||
if raw == nil {
|
||
return nil, merrors.InvalidArgument("money is required")
|
||
}
|
||
amount := strings.TrimSpace(fmt.Sprint(raw["amount"]))
|
||
currency := strings.TrimSpace(fmt.Sprint(raw["currency"]))
|
||
if amount == "" || currency == "" {
|
||
return nil, merrors.InvalidArgument("money is required")
|
||
}
|
||
return &moneyv1.Money{
|
||
Amount: amount,
|
||
Currency: currency,
|
||
}, nil
|
||
}
|
||
|
||
func feeEstimateResult(resp *chainv1.EstimateTransferFeeResponse) *structpb.Struct {
|
||
if resp == nil {
|
||
return nil
|
||
}
|
||
payload := map[string]interface{}{
|
||
"estimation_context": strings.TrimSpace(resp.GetEstimationContext()),
|
||
}
|
||
if fee := resp.GetNetworkFee(); fee != nil {
|
||
payload["network_fee"] = map[string]interface{}{
|
||
"amount": strings.TrimSpace(fee.GetAmount()),
|
||
"currency": strings.TrimSpace(fee.GetCurrency()),
|
||
}
|
||
}
|
||
result, err := structpb.NewStruct(payload)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
return result
|
||
}
|
||
|
||
func storageTransferToProto(transfer *chainstoragemodel.Transfer) *chainv1.Transfer {
|
||
if transfer == nil {
|
||
return nil
|
||
}
|
||
|
||
destination := &chainv1.TransferDestination{Memo: strings.TrimSpace(transfer.Destination.Memo)}
|
||
if managedWalletRef := strings.TrimSpace(transfer.Destination.ManagedWalletRef); managedWalletRef != "" {
|
||
destination.Destination = &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: managedWalletRef}
|
||
} else if externalAddress := strings.TrimSpace(transfer.Destination.ExternalAddress); externalAddress != "" {
|
||
destination.Destination = &chainv1.TransferDestination_ExternalAddress{ExternalAddress: externalAddress}
|
||
}
|
||
|
||
fees := make([]*chainv1.ServiceFeeBreakdown, 0, len(transfer.Fees))
|
||
for _, fee := range transfer.Fees {
|
||
fees = append(fees, &chainv1.ServiceFeeBreakdown{
|
||
FeeCode: strings.TrimSpace(fee.FeeCode),
|
||
Amount: fee.Amount,
|
||
Description: strings.TrimSpace(fee.Description),
|
||
})
|
||
}
|
||
|
||
asset := &chainv1.Asset{
|
||
Chain: shared.ChainEnumFromName(transfer.Network),
|
||
TokenSymbol: strings.TrimSpace(transfer.TokenSymbol),
|
||
ContractAddress: strings.TrimSpace(transfer.ContractAddress),
|
||
}
|
||
|
||
protoTransfer := &chainv1.Transfer{
|
||
TransferRef: strings.TrimSpace(transfer.TransferRef),
|
||
IdempotencyKey: strings.TrimSpace(transfer.IdempotencyKey),
|
||
IntentRef: strings.TrimSpace(transfer.IntentRef),
|
||
OperationRef: strings.TrimSpace(transfer.OperationRef),
|
||
OrganizationRef: strings.TrimSpace(transfer.OrganizationRef),
|
||
SourceWalletRef: strings.TrimSpace(transfer.SourceWalletRef),
|
||
Destination: destination,
|
||
Asset: asset,
|
||
RequestedAmount: shared.MonenyToProto(transfer.RequestedAmount),
|
||
NetAmount: shared.MonenyToProto(transfer.NetAmount),
|
||
Fees: fees,
|
||
Status: shared.TransferStatusToProto(transfer.Status),
|
||
TransactionHash: strings.TrimSpace(transfer.TxHash),
|
||
FailureReason: strings.TrimSpace(transfer.FailureReason),
|
||
PaymentRef: strings.TrimSpace(transfer.PaymentRef),
|
||
}
|
||
|
||
if !transfer.CreatedAt.IsZero() {
|
||
protoTransfer.CreatedAt = timestamppb.New(transfer.CreatedAt.UTC())
|
||
}
|
||
if !transfer.UpdatedAt.IsZero() {
|
||
protoTransfer.UpdatedAt = timestamppb.New(transfer.UpdatedAt.UTC())
|
||
}
|
||
|
||
return protoTransfer
|
||
}
|
||
|
||
func gasTopUpResult(amount *moneyv1.Money, capHit bool, transferRef string) *structpb.Struct {
|
||
payload := map[string]interface{}{
|
||
"cap_hit": capHit,
|
||
}
|
||
if amount != nil {
|
||
payload["topup_amount"] = map[string]interface{}{
|
||
"amount": strings.TrimSpace(amount.GetAmount()),
|
||
"currency": strings.TrimSpace(amount.GetCurrency()),
|
||
}
|
||
}
|
||
if strings.TrimSpace(transferRef) != "" {
|
||
payload["transfer_ref"] = strings.TrimSpace(transferRef)
|
||
}
|
||
result, err := structpb.NewStruct(payload)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
return result
|
||
}
|
||
|
||
func chainTransferToOperation(transfer *chainv1.Transfer) *connectorv1.Operation {
|
||
if transfer == nil {
|
||
return nil
|
||
}
|
||
op := &connectorv1.Operation{
|
||
OperationId: strings.TrimSpace(transfer.GetTransferRef()),
|
||
Type: connectorv1.OperationType_TRANSFER,
|
||
Status: chainTransferStatusToOperation(transfer.GetStatus()),
|
||
Money: transfer.GetRequestedAmount(),
|
||
ProviderRef: strings.TrimSpace(transfer.GetTransactionHash()),
|
||
IntentRef: strings.TrimSpace(transfer.GetIntentRef()),
|
||
OperationRef: strings.TrimSpace(transfer.GetOperationRef()),
|
||
CreatedAt: transfer.GetCreatedAt(),
|
||
UpdatedAt: transfer.GetUpdatedAt(),
|
||
From: &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{
|
||
ConnectorId: chainConnectorID,
|
||
AccountId: strings.TrimSpace(transfer.GetSourceWalletRef()),
|
||
}}},
|
||
}
|
||
params := map[string]interface{}{}
|
||
if paymentRef := strings.TrimSpace(transfer.GetPaymentRef()); paymentRef != "" {
|
||
params["payment_ref"] = paymentRef
|
||
}
|
||
if organizationRef := strings.TrimSpace(transfer.GetOrganizationRef()); organizationRef != "" {
|
||
params["organization_ref"] = organizationRef
|
||
}
|
||
if failureReason := strings.TrimSpace(transfer.GetFailureReason()); failureReason != "" {
|
||
params["failure_reason"] = failureReason
|
||
}
|
||
if len(params) > 0 {
|
||
op.Params = structFromMap(params)
|
||
}
|
||
if dest := transfer.GetDestination(); dest != nil {
|
||
switch d := dest.GetDestination().(type) {
|
||
case *chainv1.TransferDestination_ManagedWalletRef:
|
||
op.To = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{
|
||
ConnectorId: chainConnectorID,
|
||
AccountId: strings.TrimSpace(d.ManagedWalletRef),
|
||
}}}
|
||
case *chainv1.TransferDestination_ExternalAddress:
|
||
op.To = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_External{External: &connectorv1.ExternalRef{
|
||
ExternalRef: strings.TrimSpace(d.ExternalAddress),
|
||
}}}
|
||
}
|
||
}
|
||
return op
|
||
}
|
||
|
||
func chainTransferStatusToOperation(status chainv1.TransferStatus) connectorv1.OperationStatus {
|
||
switch status {
|
||
|
||
case chainv1.TransferStatus_TRANSFER_CREATED:
|
||
return connectorv1.OperationStatus_OPERATION_CREATED
|
||
|
||
case chainv1.TransferStatus_TRANSFER_PROCESSING:
|
||
return connectorv1.OperationStatus_OPERATION_PROCESSING
|
||
|
||
case chainv1.TransferStatus_TRANSFER_WAITING:
|
||
return connectorv1.OperationStatus_OPERATION_WAITING
|
||
|
||
case chainv1.TransferStatus_TRANSFER_SUCCESS:
|
||
return connectorv1.OperationStatus_OPERATION_SUCCESS
|
||
|
||
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||
return connectorv1.OperationStatus_OPERATION_FAILED
|
||
|
||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||
return connectorv1.OperationStatus_OPERATION_CANCELLED
|
||
|
||
default:
|
||
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
|
||
}
|
||
}
|
||
|
||
func chainStatusFromOperation(status connectorv1.OperationStatus) chainv1.TransferStatus {
|
||
switch status {
|
||
|
||
case connectorv1.OperationStatus_OPERATION_CREATED:
|
||
return chainv1.TransferStatus_TRANSFER_CREATED
|
||
|
||
case connectorv1.OperationStatus_OPERATION_PROCESSING:
|
||
return chainv1.TransferStatus_TRANSFER_PROCESSING
|
||
|
||
case connectorv1.OperationStatus_OPERATION_WAITING:
|
||
return chainv1.TransferStatus_TRANSFER_WAITING
|
||
|
||
case connectorv1.OperationStatus_OPERATION_SUCCESS:
|
||
return chainv1.TransferStatus_TRANSFER_SUCCESS
|
||
|
||
case connectorv1.OperationStatus_OPERATION_FAILED:
|
||
return chainv1.TransferStatus_TRANSFER_FAILED
|
||
|
||
case connectorv1.OperationStatus_OPERATION_CANCELLED:
|
||
return chainv1.TransferStatus_TRANSFER_CANCELLED
|
||
|
||
default:
|
||
return chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
|
||
}
|
||
}
|
||
|
||
func parseChainAsset(assetString string, reader params.Reader) (*chainv1.Asset, error) {
|
||
return chainasset.ParseAsset(
|
||
assetString,
|
||
reader.String("network"),
|
||
reader.String("token_symbol"),
|
||
reader.String("contract_address"),
|
||
)
|
||
}
|
||
|
||
func describableFromLabel(label, desc string) *describablev1.Describable {
|
||
label = strings.TrimSpace(label)
|
||
desc = strings.TrimSpace(desc)
|
||
if label == "" && desc == "" {
|
||
return nil
|
||
}
|
||
return &describablev1.Describable{
|
||
Name: label,
|
||
Description: &desc,
|
||
}
|
||
}
|
||
|
||
func operationAccountID(party *connectorv1.OperationParty) string {
|
||
if party == nil {
|
||
return ""
|
||
}
|
||
if account := party.GetAccount(); account != nil {
|
||
return strings.TrimSpace(account.GetAccountId())
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func structFromMap(values map[string]interface{}) *structpb.Struct {
|
||
if len(values) == 0 {
|
||
return nil
|
||
}
|
||
result, err := structpb.NewStruct(values)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
return result
|
||
}
|
||
|
||
func connectorError(code connectorv1.ErrorCode, message string, op *connectorv1.Operation, accountID string) *connectorv1.ConnectorError {
|
||
err := &connectorv1.ConnectorError{
|
||
Code: code,
|
||
Message: strings.TrimSpace(message),
|
||
AccountId: strings.TrimSpace(accountID),
|
||
}
|
||
if op != nil {
|
||
err.CorrelationId = strings.TrimSpace(op.GetCorrelationId())
|
||
err.ParentIntentId = strings.TrimSpace(op.GetParentIntentId())
|
||
err.OperationId = strings.TrimSpace(op.GetOperationId())
|
||
}
|
||
return err
|
||
}
|
||
|
||
func mapErrorCode(err error) connectorv1.ErrorCode {
|
||
switch {
|
||
case errors.Is(err, merrors.ErrInvalidArg):
|
||
return connectorv1.ErrorCode_INVALID_PARAMS
|
||
case errors.Is(err, merrors.ErrNoData):
|
||
return connectorv1.ErrorCode_NOT_FOUND
|
||
case errors.Is(err, merrors.ErrNotImplemented):
|
||
return connectorv1.ErrorCode_UNSUPPORTED_OPERATION
|
||
case errors.Is(err, merrors.ErrInternal):
|
||
return connectorv1.ErrorCode_TEMPORARY_UNAVAILABLE
|
||
default:
|
||
return connectorv1.ErrorCode_PROVIDER_ERROR
|
||
}
|
||
}
|