interface refactoring

This commit is contained in:
Stephan D
2026-01-05 01:22:47 +01:00
parent fcd831902a
commit 7424ef751c
16 changed files with 3623 additions and 133 deletions

View File

@@ -0,0 +1,691 @@
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"
"github.com/tech/sendico/pkg/connector/params"
"github.com/tech/sendico/pkg/merrors"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"google.golang.org/protobuf/types/known/structpb"
)
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 {
return nil, merrors.InvalidArgument(err.Error())
}
asset = parsed
}
resp, err := s.ListManagedWallets(ctx, &chainv1.ListManagedWalletsRequest{
OwnerRef: strings.TrimSpace(req.GetOwnerRef()),
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,
Destination: dest,
Amount: amount,
Fees: parseChainFees(reader),
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
ClientReference: strings.TrimSpace(reader.String("client_reference")),
})
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_CONFIRMED,
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_CONFIRMED,
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()),
OrganizationRef: orgRef,
SourceWalletRef: source,
TargetWalletRef: target,
EstimatedTotalFee: fee,
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
ClientReference: strings.TrimSpace(reader.String("client_reference")),
})
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: connectorv1.OperationStatus_CONFIRMED,
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")
}
resp, err := s.GetTransfer(ctx, &chainv1.GetTransferRequest{TransferRef: strings.TrimSpace(req.GetOperationId())})
if err != nil {
return nil, err
}
return &connectorv1.GetOperationResponse{Operation: chainTransferToOperation(resp.GetTransfer())}, 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: assetStringFromChainAsset(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(),
}
}
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, fmt.Errorf("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, fmt.Errorf("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, fmt.Errorf("money is required")
}
amount := strings.TrimSpace(fmt.Sprint(raw["amount"]))
currency := strings.TrimSpace(fmt.Sprint(raw["currency"]))
if amount == "" || currency == "" {
return nil, fmt.Errorf("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 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()),
CreatedAt: transfer.GetCreatedAt(),
UpdatedAt: transfer.GetUpdatedAt(),
From: &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{
ConnectorId: chainConnectorID,
AccountId: strings.TrimSpace(transfer.GetSourceWalletRef()),
}}},
}
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_CONFIRMED:
return connectorv1.OperationStatus_CONFIRMED
case chainv1.TransferStatus_TRANSFER_FAILED:
return connectorv1.OperationStatus_FAILED
case chainv1.TransferStatus_TRANSFER_CANCELLED:
return connectorv1.OperationStatus_CANCELED
default:
return connectorv1.OperationStatus_PENDING
}
}
func chainStatusFromOperation(status connectorv1.OperationStatus) chainv1.TransferStatus {
switch status {
case connectorv1.OperationStatus_CONFIRMED:
return chainv1.TransferStatus_TRANSFER_CONFIRMED
case connectorv1.OperationStatus_FAILED:
return chainv1.TransferStatus_TRANSFER_FAILED
case connectorv1.OperationStatus_CANCELED:
return chainv1.TransferStatus_TRANSFER_CANCELLED
default:
return chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
}
}
func parseChainAsset(assetString string, reader params.Reader) (*chainv1.Asset, error) {
network := strings.TrimSpace(reader.String("network"))
token := strings.TrimSpace(reader.String("token_symbol"))
contract := strings.TrimSpace(reader.String("contract_address"))
if token == "" {
token = tokenFromAssetString(assetString)
}
if network == "" {
network = networkFromAssetString(assetString)
}
if token == "" {
return nil, fmt.Errorf("asset: token_symbol is required")
}
chain := shared.ChainEnumFromName(network)
if chain == chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED {
return nil, fmt.Errorf("asset: network is required")
}
return &chainv1.Asset{
Chain: chain,
TokenSymbol: strings.ToUpper(token),
ContractAddress: strings.ToLower(contract),
}, nil
}
func tokenFromAssetString(asset string) string {
if asset == "" {
return ""
}
if idx := strings.Index(asset, "-"); idx > 0 {
return asset[:idx]
}
return asset
}
func networkFromAssetString(asset string) string {
if asset == "" {
return ""
}
idx := strings.Index(asset, "-")
if idx < 0 {
return ""
}
return strings.TrimSpace(asset[idx+1:])
}
func assetStringFromChainAsset(asset *chainv1.Asset) string {
if asset == nil {
return ""
}
symbol := strings.ToUpper(strings.TrimSpace(asset.GetTokenSymbol()))
if symbol == "" {
return ""
}
suffix := chainAssetSuffix(asset.GetChain())
if suffix == "" {
return symbol
}
return symbol + "-" + suffix
}
func chainAssetSuffix(chain chainv1.ChainNetwork) string {
switch chain {
case chainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET:
return "ETH"
case chainv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_ONE:
return "ARB"
case chainv1.ChainNetwork_CHAIN_NETWORK_TRON_MAINNET:
return "TRC20"
case chainv1.ChainNetwork_CHAIN_NETWORK_TRON_NILE:
return "TRC20"
default:
return ""
}
}
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 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
}
}

View File

@@ -19,8 +19,8 @@ import (
msg "github.com/tech/sendico/pkg/messaging"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
unifiedv1 "github.com/tech/sendico/pkg/proto/gateway/unified/v1"
"google.golang.org/grpc"
)
@@ -34,7 +34,7 @@ var (
errStorageUnavailable = serviceError("chain_gateway: storage not initialised")
)
// Service implements the UnifiedGatewayService RPC contract for chain operations.
// Service implements the ConnectorService RPC contract for chain operations.
type Service struct {
logger mlogger.Logger
storage storage.Repository
@@ -52,7 +52,7 @@ type Service struct {
commands commands.Registry
announcers []*discovery.Announcer
unifiedv1.UnimplementedUnifiedGatewayServiceServer
connectorv1.UnimplementedConnectorServiceServer
}
// NewService constructs the chain gateway service skeleton.
@@ -95,7 +95,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
// Register wires the service onto the provided gRPC router.
func (s *Service) Register(router routers.GRPC) error {
return router.Register(func(reg grpc.ServiceRegistrar) {
unifiedv1.RegisterUnifiedGatewayServiceServer(reg, s)
connectorv1.RegisterConnectorServiceServer(reg, s)
})
}