826 lines
30 KiB
Go
826 lines
30 KiB
Go
package client
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"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"
|
|
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
)
|
|
|
|
const chainConnectorID = "chain"
|
|
|
|
// Client exposes typed helpers around the chain gateway gRPC API.
|
|
type Client interface {
|
|
CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error)
|
|
GetManagedWallet(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error)
|
|
ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error)
|
|
GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error)
|
|
SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error)
|
|
GetTransfer(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error)
|
|
ListTransfers(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error)
|
|
EstimateTransferFee(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error)
|
|
ComputeGasTopUp(ctx context.Context, req *chainv1.ComputeGasTopUpRequest) (*chainv1.ComputeGasTopUpResponse, error)
|
|
EnsureGasTopUp(ctx context.Context, req *chainv1.EnsureGasTopUpRequest) (*chainv1.EnsureGasTopUpResponse, error)
|
|
Close() error
|
|
}
|
|
|
|
type grpcConnectorClient interface {
|
|
GetCapabilities(ctx context.Context, in *connectorv1.GetCapabilitiesRequest, opts ...grpc.CallOption) (*connectorv1.GetCapabilitiesResponse, error)
|
|
OpenAccount(ctx context.Context, in *connectorv1.OpenAccountRequest, opts ...grpc.CallOption) (*connectorv1.OpenAccountResponse, error)
|
|
GetAccount(ctx context.Context, in *connectorv1.GetAccountRequest, opts ...grpc.CallOption) (*connectorv1.GetAccountResponse, error)
|
|
ListAccounts(ctx context.Context, in *connectorv1.ListAccountsRequest, opts ...grpc.CallOption) (*connectorv1.ListAccountsResponse, error)
|
|
GetBalance(ctx context.Context, in *connectorv1.GetBalanceRequest, opts ...grpc.CallOption) (*connectorv1.GetBalanceResponse, error)
|
|
SubmitOperation(ctx context.Context, in *connectorv1.SubmitOperationRequest, opts ...grpc.CallOption) (*connectorv1.SubmitOperationResponse, error)
|
|
GetOperation(ctx context.Context, in *connectorv1.GetOperationRequest, opts ...grpc.CallOption) (*connectorv1.GetOperationResponse, error)
|
|
ListOperations(ctx context.Context, in *connectorv1.ListOperationsRequest, opts ...grpc.CallOption) (*connectorv1.ListOperationsResponse, error)
|
|
}
|
|
|
|
type chainGatewayClient struct {
|
|
cfg Config
|
|
conn *grpc.ClientConn
|
|
client grpcConnectorClient
|
|
}
|
|
|
|
// New dials the chain gateway endpoint and returns a ready client.
|
|
func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, error) {
|
|
cfg.setDefaults()
|
|
if strings.TrimSpace(cfg.Address) == "" {
|
|
return nil, merrors.InvalidArgument("chain-gateway: address is required")
|
|
}
|
|
|
|
dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout)
|
|
defer cancel()
|
|
|
|
dialOpts := make([]grpc.DialOption, 0, len(opts)+1)
|
|
dialOpts = append(dialOpts, opts...)
|
|
|
|
if cfg.Insecure {
|
|
dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
|
} else {
|
|
dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
|
|
}
|
|
|
|
conn, err := grpc.DialContext(dialCtx, cfg.Address, dialOpts...)
|
|
if err != nil {
|
|
return nil, merrors.Internal(fmt.Sprintf("chain-gateway: dial %s: %s", cfg.Address, err.Error()))
|
|
}
|
|
|
|
return &chainGatewayClient{
|
|
cfg: cfg,
|
|
conn: conn,
|
|
client: connectorv1.NewConnectorServiceClient(conn),
|
|
}, nil
|
|
}
|
|
|
|
// NewWithClient injects a pre-built gateway client (useful for tests).
|
|
func NewWithClient(cfg Config, gc grpcConnectorClient) Client {
|
|
cfg.setDefaults()
|
|
return &chainGatewayClient{
|
|
cfg: cfg,
|
|
client: gc,
|
|
}
|
|
}
|
|
|
|
func (c *chainGatewayClient) Close() error {
|
|
if c.conn != nil {
|
|
return c.conn.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *chainGatewayClient) CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error) {
|
|
ctx, cancel := c.callContext(ctx)
|
|
defer cancel()
|
|
|
|
params, err := walletParamsFromRequest(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
label := ""
|
|
if desc := req.GetDescribable(); desc != nil {
|
|
label = strings.TrimSpace(desc.GetName())
|
|
}
|
|
resp, err := c.client.OpenAccount(ctx, &connectorv1.OpenAccountRequest{
|
|
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
|
Kind: connectorv1.AccountKind_CHAIN_MANAGED_WALLET,
|
|
Asset: assetStringFromChainAsset(req.GetAsset()),
|
|
OwnerRef: strings.TrimSpace(req.GetOwnerRef()),
|
|
Label: label,
|
|
Params: params,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.GetError() != nil {
|
|
return nil, connectorError(resp.GetError())
|
|
}
|
|
wallet := managedWalletFromAccount(resp.GetAccount())
|
|
return &chainv1.CreateManagedWalletResponse{Wallet: wallet}, nil
|
|
}
|
|
|
|
func (c *chainGatewayClient) GetManagedWallet(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error) {
|
|
ctx, cancel := c.callContext(ctx)
|
|
defer cancel()
|
|
if req == nil || strings.TrimSpace(req.GetWalletRef()) == "" {
|
|
return nil, merrors.InvalidArgument("chain-gateway: wallet_ref is required")
|
|
}
|
|
resp, err := c.client.GetAccount(ctx, &connectorv1.GetAccountRequest{AccountRef: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(req.GetWalletRef())}})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &chainv1.GetManagedWalletResponse{Wallet: managedWalletFromAccount(resp.GetAccount())}, nil
|
|
}
|
|
|
|
func (c *chainGatewayClient) ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error) {
|
|
ctx, cancel := c.callContext(ctx)
|
|
defer cancel()
|
|
assetString := ""
|
|
ownerRef := ""
|
|
var page *paginationv1.CursorPageRequest
|
|
if req != nil {
|
|
assetString = assetStringFromChainAsset(req.GetAsset())
|
|
ownerRef = strings.TrimSpace(req.GetOwnerRef())
|
|
page = req.GetPage()
|
|
}
|
|
resp, err := c.client.ListAccounts(ctx, &connectorv1.ListAccountsRequest{
|
|
OwnerRef: ownerRef,
|
|
Asset: assetString,
|
|
Page: page,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
wallets := make([]*chainv1.ManagedWallet, 0, len(resp.GetAccounts()))
|
|
for _, account := range resp.GetAccounts() {
|
|
wallets = append(wallets, managedWalletFromAccount(account))
|
|
}
|
|
return &chainv1.ListManagedWalletsResponse{Wallets: wallets, Page: resp.GetPage()}, nil
|
|
}
|
|
|
|
func (c *chainGatewayClient) GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error) {
|
|
ctx, cancel := c.callContext(ctx)
|
|
defer cancel()
|
|
if req == nil || strings.TrimSpace(req.GetWalletRef()) == "" {
|
|
return nil, merrors.InvalidArgument("chain-gateway: wallet_ref is required")
|
|
}
|
|
resp, err := c.client.GetBalance(ctx, &connectorv1.GetBalanceRequest{AccountRef: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(req.GetWalletRef())}})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
balance := resp.GetBalance()
|
|
if balance == nil {
|
|
return nil, merrors.Internal("chain-gateway: balance response missing")
|
|
}
|
|
return &chainv1.GetWalletBalanceResponse{Balance: &chainv1.WalletBalance{
|
|
Available: balance.GetAvailable(),
|
|
PendingInbound: balance.GetPendingInbound(),
|
|
PendingOutbound: balance.GetPendingOutbound(),
|
|
CalculatedAt: balance.GetCalculatedAt(),
|
|
}}, nil
|
|
}
|
|
|
|
func (c *chainGatewayClient) SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
|
|
ctx, cancel := c.callContext(ctx)
|
|
defer cancel()
|
|
if req == nil {
|
|
return nil, merrors.InvalidArgument("chain-gateway: request is required")
|
|
}
|
|
operation, err := operationFromTransfer(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := c.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: operation})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
|
|
return nil, connectorError(resp.GetReceipt().GetError())
|
|
}
|
|
transfer := transferFromReceipt(req, resp.GetReceipt())
|
|
return &chainv1.SubmitTransferResponse{Transfer: transfer}, nil
|
|
}
|
|
|
|
func (c *chainGatewayClient) GetTransfer(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error) {
|
|
ctx, cancel := c.callContext(ctx)
|
|
defer cancel()
|
|
if req == nil || strings.TrimSpace(req.GetTransferRef()) == "" {
|
|
return nil, merrors.InvalidArgument("chain-gateway: transfer_ref is required")
|
|
}
|
|
resp, err := c.client.GetOperation(ctx, &connectorv1.GetOperationRequest{OperationId: strings.TrimSpace(req.GetTransferRef())})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &chainv1.GetTransferResponse{Transfer: transferFromOperation(resp.GetOperation())}, nil
|
|
}
|
|
|
|
func (c *chainGatewayClient) ListTransfers(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error) {
|
|
ctx, cancel := c.callContext(ctx)
|
|
defer cancel()
|
|
source := ""
|
|
status := chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
|
|
var page *paginationv1.CursorPageRequest
|
|
if req != nil {
|
|
source = strings.TrimSpace(req.GetSourceWalletRef())
|
|
status = req.GetStatus()
|
|
page = req.GetPage()
|
|
}
|
|
resp, err := c.client.ListOperations(ctx, &connectorv1.ListOperationsRequest{
|
|
AccountRef: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: source},
|
|
Status: operationStatusFromTransfer(status),
|
|
Page: page,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
transfers := make([]*chainv1.Transfer, 0, len(resp.GetOperations()))
|
|
for _, op := range resp.GetOperations() {
|
|
transfers = append(transfers, transferFromOperation(op))
|
|
}
|
|
return &chainv1.ListTransfersResponse{Transfers: transfers, Page: resp.GetPage()}, nil
|
|
}
|
|
|
|
func (c *chainGatewayClient) EstimateTransferFee(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error) {
|
|
ctx, cancel := c.callContext(ctx)
|
|
defer cancel()
|
|
if req == nil {
|
|
return nil, merrors.InvalidArgument("chain-gateway: request is required")
|
|
}
|
|
operation, err := feeEstimateOperation(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := c.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: operation})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
|
|
return nil, connectorError(resp.GetReceipt().GetError())
|
|
}
|
|
return estimateFromReceipt(resp.GetReceipt()), nil
|
|
}
|
|
|
|
func (c *chainGatewayClient) ComputeGasTopUp(ctx context.Context, req *chainv1.ComputeGasTopUpRequest) (*chainv1.ComputeGasTopUpResponse, error) {
|
|
ctx, cancel := c.callContext(ctx)
|
|
defer cancel()
|
|
if req == nil || strings.TrimSpace(req.GetWalletRef()) == "" {
|
|
return nil, merrors.InvalidArgument("chain-gateway: wallet_ref is required")
|
|
}
|
|
operation, err := gasTopUpComputeOperation(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := c.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: operation})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
|
|
return nil, connectorError(resp.GetReceipt().GetError())
|
|
}
|
|
return computeGasTopUpFromReceipt(resp.GetReceipt()), nil
|
|
}
|
|
|
|
func (c *chainGatewayClient) EnsureGasTopUp(ctx context.Context, req *chainv1.EnsureGasTopUpRequest) (*chainv1.EnsureGasTopUpResponse, error) {
|
|
ctx, cancel := c.callContext(ctx)
|
|
defer cancel()
|
|
if req == nil {
|
|
return nil, merrors.InvalidArgument("chain-gateway: request is required")
|
|
}
|
|
operation, err := gasTopUpEnsureOperation(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := c.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: operation})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
|
|
return nil, connectorError(resp.GetReceipt().GetError())
|
|
}
|
|
return ensureGasTopUpFromReceipt(resp.GetReceipt()), nil
|
|
}
|
|
|
|
func (c *chainGatewayClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) {
|
|
timeout := c.cfg.CallTimeout
|
|
if timeout <= 0 {
|
|
timeout = 3 * time.Second
|
|
}
|
|
return context.WithTimeout(ctx, timeout)
|
|
}
|
|
|
|
func walletParamsFromRequest(req *chainv1.CreateManagedWalletRequest) (*structpb.Struct, error) {
|
|
if req == nil {
|
|
return nil, nil
|
|
}
|
|
params := map[string]interface{}{
|
|
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
|
|
}
|
|
if asset := req.GetAsset(); asset != nil {
|
|
params["network"] = asset.GetChain().String()
|
|
params["token_symbol"] = strings.TrimSpace(asset.GetTokenSymbol())
|
|
params["contract_address"] = strings.TrimSpace(asset.GetContractAddress())
|
|
}
|
|
desc := ""
|
|
if describable := req.GetDescribable(); describable != nil {
|
|
desc = strings.TrimSpace(describable.GetDescription())
|
|
}
|
|
if desc != "" {
|
|
params["description"] = desc
|
|
}
|
|
if len(req.GetMetadata()) > 0 {
|
|
params["metadata"] = mapStringToInterface(req.GetMetadata())
|
|
}
|
|
return structpb.NewStruct(params)
|
|
}
|
|
|
|
func managedWalletFromAccount(account *connectorv1.Account) *chainv1.ManagedWallet {
|
|
if account == nil {
|
|
return nil
|
|
}
|
|
details := map[string]interface{}{}
|
|
if account.GetProviderDetails() != nil {
|
|
details = account.GetProviderDetails().AsMap()
|
|
}
|
|
walletRef := ""
|
|
if ref := account.GetRef(); ref != nil {
|
|
walletRef = strings.TrimSpace(ref.GetAccountId())
|
|
}
|
|
if v := stringFromDetails(details, "wallet_ref"); v != "" {
|
|
walletRef = v
|
|
}
|
|
organizationRef := stringFromDetails(details, "organization_ref")
|
|
ownerRef := stringFromDetails(details, "owner_ref")
|
|
if ownerRef == "" {
|
|
ownerRef = strings.TrimSpace(account.GetOwnerRef())
|
|
}
|
|
asset := &chainv1.Asset{
|
|
Chain: chainNetworkFromString(stringFromDetails(details, "network")),
|
|
TokenSymbol: strings.TrimSpace(stringFromDetails(details, "token_symbol")),
|
|
ContractAddress: strings.TrimSpace(stringFromDetails(details, "contract_address")),
|
|
}
|
|
if asset.GetTokenSymbol() == "" {
|
|
asset.TokenSymbol = strings.TrimSpace(tokenFromAssetString(account.GetAsset()))
|
|
}
|
|
describable := account.GetDescribable()
|
|
label := strings.TrimSpace(account.GetLabel())
|
|
if describable == nil {
|
|
if label != "" {
|
|
describable = &describablev1.Describable{Name: label}
|
|
}
|
|
} else if strings.TrimSpace(describable.GetName()) == "" && label != "" {
|
|
desc := strings.TrimSpace(describable.GetDescription())
|
|
if desc == "" {
|
|
describable = &describablev1.Describable{Name: label}
|
|
} else {
|
|
describable = &describablev1.Describable{Name: label, Description: &desc}
|
|
}
|
|
}
|
|
return &chainv1.ManagedWallet{
|
|
WalletRef: walletRef,
|
|
OrganizationRef: organizationRef,
|
|
OwnerRef: ownerRef,
|
|
Asset: asset,
|
|
DepositAddress: stringFromDetails(details, "deposit_address"),
|
|
Status: managedWalletStatusFromAccount(account.GetState()),
|
|
CreatedAt: account.GetCreatedAt(),
|
|
UpdatedAt: account.GetUpdatedAt(),
|
|
Describable: describable,
|
|
}
|
|
}
|
|
|
|
func operationFromTransfer(req *chainv1.SubmitTransferRequest) (*connectorv1.Operation, error) {
|
|
if req == nil {
|
|
return nil, merrors.InvalidArgument("chain-gateway: request is required")
|
|
}
|
|
if strings.TrimSpace(req.GetIdempotencyKey()) == "" {
|
|
return nil, merrors.InvalidArgument("chain-gateway: idempotency_key is required")
|
|
}
|
|
if strings.TrimSpace(req.GetSourceWalletRef()) == "" {
|
|
return nil, merrors.InvalidArgument("chain-gateway: source_wallet_ref is required")
|
|
}
|
|
if req.GetDestination() == nil {
|
|
return nil, merrors.InvalidArgument("chain-gateway: destination is required")
|
|
}
|
|
if req.GetAmount() == nil {
|
|
return nil, merrors.InvalidArgument("chain-gateway: amount is required")
|
|
}
|
|
|
|
params := map[string]interface{}{
|
|
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
|
|
"client_reference": strings.TrimSpace(req.GetClientReference()),
|
|
}
|
|
if memo := strings.TrimSpace(req.GetDestination().GetMemo()); memo != "" {
|
|
params["destination_memo"] = memo
|
|
}
|
|
if len(req.GetMetadata()) > 0 {
|
|
params["metadata"] = mapStringToInterface(req.GetMetadata())
|
|
}
|
|
if len(req.GetFees()) > 0 {
|
|
params["fees"] = feesToInterface(req.GetFees())
|
|
}
|
|
|
|
op := &connectorv1.Operation{
|
|
Type: connectorv1.OperationType_TRANSFER,
|
|
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
|
From: &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(req.GetSourceWalletRef())}}},
|
|
Money: req.GetAmount(),
|
|
Params: structFromMap(params),
|
|
}
|
|
to, err := destinationToParty(req.GetDestination())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
op.To = to
|
|
return op, nil
|
|
}
|
|
|
|
func destinationToParty(dest *chainv1.TransferDestination) (*connectorv1.OperationParty, error) {
|
|
if dest == nil {
|
|
return nil, merrors.InvalidArgument("chain-gateway: destination is required")
|
|
}
|
|
switch d := dest.GetDestination().(type) {
|
|
case *chainv1.TransferDestination_ManagedWalletRef:
|
|
return &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(d.ManagedWalletRef)}}}, nil
|
|
case *chainv1.TransferDestination_ExternalAddress:
|
|
return &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_External{External: &connectorv1.ExternalRef{ExternalRef: strings.TrimSpace(d.ExternalAddress)}}}, nil
|
|
default:
|
|
return nil, merrors.InvalidArgument("chain-gateway: destination is required")
|
|
}
|
|
}
|
|
|
|
func transferFromReceipt(req *chainv1.SubmitTransferRequest, receipt *connectorv1.OperationReceipt) *chainv1.Transfer {
|
|
transfer := &chainv1.Transfer{}
|
|
if req != nil {
|
|
transfer.IdempotencyKey = strings.TrimSpace(req.GetIdempotencyKey())
|
|
transfer.OrganizationRef = strings.TrimSpace(req.GetOrganizationRef())
|
|
transfer.SourceWalletRef = strings.TrimSpace(req.GetSourceWalletRef())
|
|
transfer.Destination = req.GetDestination()
|
|
transfer.RequestedAmount = req.GetAmount()
|
|
transfer.NetAmount = req.GetAmount()
|
|
}
|
|
if receipt != nil {
|
|
transfer.TransferRef = strings.TrimSpace(receipt.GetOperationId())
|
|
transfer.Status = transferStatusFromOperation(receipt.GetStatus())
|
|
transfer.TransactionHash = strings.TrimSpace(receipt.GetProviderRef())
|
|
}
|
|
return transfer
|
|
}
|
|
|
|
func transferFromOperation(op *connectorv1.Operation) *chainv1.Transfer {
|
|
if op == nil {
|
|
return nil
|
|
}
|
|
transfer := &chainv1.Transfer{
|
|
TransferRef: strings.TrimSpace(op.GetOperationId()),
|
|
IdempotencyKey: strings.TrimSpace(op.GetOperationId()),
|
|
RequestedAmount: op.GetMoney(),
|
|
NetAmount: op.GetMoney(),
|
|
Status: transferStatusFromOperation(op.GetStatus()),
|
|
TransactionHash: strings.TrimSpace(op.GetProviderRef()),
|
|
CreatedAt: op.GetCreatedAt(),
|
|
UpdatedAt: op.GetUpdatedAt(),
|
|
}
|
|
if from := op.GetFrom(); from != nil && from.GetAccount() != nil {
|
|
transfer.SourceWalletRef = strings.TrimSpace(from.GetAccount().GetAccountId())
|
|
}
|
|
if to := op.GetTo(); to != nil {
|
|
if account := to.GetAccount(); account != nil {
|
|
transfer.Destination = &chainv1.TransferDestination{Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(account.GetAccountId())}}
|
|
}
|
|
if external := to.GetExternal(); external != nil {
|
|
transfer.Destination = &chainv1.TransferDestination{Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(external.GetExternalRef())}}
|
|
}
|
|
}
|
|
return transfer
|
|
}
|
|
|
|
func feeEstimateOperation(req *chainv1.EstimateTransferFeeRequest) (*connectorv1.Operation, error) {
|
|
if req == nil {
|
|
return nil, merrors.InvalidArgument("chain-gateway: request is required")
|
|
}
|
|
if strings.TrimSpace(req.GetSourceWalletRef()) == "" {
|
|
return nil, merrors.InvalidArgument("chain-gateway: source_wallet_ref is required")
|
|
}
|
|
if req.GetDestination() == nil {
|
|
return nil, merrors.InvalidArgument("chain-gateway: destination is required")
|
|
}
|
|
if req.GetAmount() == nil {
|
|
return nil, merrors.InvalidArgument("chain-gateway: amount is required")
|
|
}
|
|
params := map[string]interface{}{}
|
|
op := &connectorv1.Operation{
|
|
Type: connectorv1.OperationType_FEE_ESTIMATE,
|
|
IdempotencyKey: feeEstimateKey(req),
|
|
From: &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(req.GetSourceWalletRef())}}},
|
|
Money: req.GetAmount(),
|
|
Params: structFromMap(params),
|
|
}
|
|
to, err := destinationToParty(req.GetDestination())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
op.To = to
|
|
return op, nil
|
|
}
|
|
|
|
func estimateFromReceipt(receipt *connectorv1.OperationReceipt) *chainv1.EstimateTransferFeeResponse {
|
|
resp := &chainv1.EstimateTransferFeeResponse{}
|
|
if receipt == nil || receipt.GetResult() == nil {
|
|
return resp
|
|
}
|
|
data := receipt.GetResult().AsMap()
|
|
if networkFee, ok := data["network_fee"].(map[string]interface{}); ok {
|
|
amount := strings.TrimSpace(fmt.Sprint(networkFee["amount"]))
|
|
currency := strings.TrimSpace(fmt.Sprint(networkFee["currency"]))
|
|
if amount != "" && currency != "" {
|
|
resp.NetworkFee = &moneyv1.Money{Amount: amount, Currency: currency}
|
|
}
|
|
}
|
|
if ctx, ok := data["estimation_context"].(string); ok {
|
|
resp.EstimationContext = strings.TrimSpace(ctx)
|
|
}
|
|
return resp
|
|
}
|
|
|
|
func gasTopUpComputeOperation(req *chainv1.ComputeGasTopUpRequest) (*connectorv1.Operation, error) {
|
|
if req == nil || strings.TrimSpace(req.GetWalletRef()) == "" {
|
|
return nil, merrors.InvalidArgument("chain-gateway: wallet_ref is required")
|
|
}
|
|
fee := req.GetEstimatedTotalFee()
|
|
if fee == nil {
|
|
return nil, merrors.InvalidArgument("chain-gateway: estimated_total_fee is required")
|
|
}
|
|
params := map[string]interface{}{
|
|
"mode": "compute",
|
|
"estimated_total_fee": map[string]interface{}{"amount": fee.GetAmount(), "currency": fee.GetCurrency()},
|
|
}
|
|
return &connectorv1.Operation{
|
|
Type: connectorv1.OperationType_GAS_TOPUP,
|
|
IdempotencyKey: fmt.Sprintf("gas_topup_compute:%s:%s", strings.TrimSpace(req.GetWalletRef()), strings.TrimSpace(fee.GetAmount())),
|
|
From: &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(req.GetWalletRef())}}},
|
|
Params: structFromMap(params),
|
|
}, nil
|
|
}
|
|
|
|
func gasTopUpEnsureOperation(req *chainv1.EnsureGasTopUpRequest) (*connectorv1.Operation, error) {
|
|
if req == nil {
|
|
return nil, merrors.InvalidArgument("chain-gateway: request is required")
|
|
}
|
|
if strings.TrimSpace(req.GetIdempotencyKey()) == "" {
|
|
return nil, merrors.InvalidArgument("chain-gateway: idempotency_key is required")
|
|
}
|
|
if strings.TrimSpace(req.GetSourceWalletRef()) == "" {
|
|
return nil, merrors.InvalidArgument("chain-gateway: source_wallet_ref is required")
|
|
}
|
|
if strings.TrimSpace(req.GetTargetWalletRef()) == "" {
|
|
return nil, merrors.InvalidArgument("chain-gateway: target_wallet_ref is required")
|
|
}
|
|
fee := req.GetEstimatedTotalFee()
|
|
if fee == nil {
|
|
return nil, merrors.InvalidArgument("chain-gateway: estimated_total_fee is required")
|
|
}
|
|
params := map[string]interface{}{
|
|
"mode": "ensure",
|
|
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
|
|
"target_wallet_ref": strings.TrimSpace(req.GetTargetWalletRef()),
|
|
"client_reference": strings.TrimSpace(req.GetClientReference()),
|
|
"estimated_total_fee": map[string]interface{}{"amount": fee.GetAmount(), "currency": fee.GetCurrency()},
|
|
}
|
|
if len(req.GetMetadata()) > 0 {
|
|
params["metadata"] = mapStringToInterface(req.GetMetadata())
|
|
}
|
|
return &connectorv1.Operation{
|
|
Type: connectorv1.OperationType_GAS_TOPUP,
|
|
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
|
From: &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(req.GetSourceWalletRef())}}},
|
|
Params: structFromMap(params),
|
|
}, nil
|
|
}
|
|
|
|
func computeGasTopUpFromReceipt(receipt *connectorv1.OperationReceipt) *chainv1.ComputeGasTopUpResponse {
|
|
resp := &chainv1.ComputeGasTopUpResponse{}
|
|
if receipt == nil || receipt.GetResult() == nil {
|
|
return resp
|
|
}
|
|
data := receipt.GetResult().AsMap()
|
|
if amount, ok := data["topup_amount"].(map[string]interface{}); ok {
|
|
resp.TopupAmount = &moneyv1.Money{
|
|
Amount: strings.TrimSpace(fmt.Sprint(amount["amount"])),
|
|
Currency: strings.TrimSpace(fmt.Sprint(amount["currency"])),
|
|
}
|
|
}
|
|
if capHit, ok := data["cap_hit"].(bool); ok {
|
|
resp.CapHit = capHit
|
|
}
|
|
return resp
|
|
}
|
|
|
|
func ensureGasTopUpFromReceipt(receipt *connectorv1.OperationReceipt) *chainv1.EnsureGasTopUpResponse {
|
|
resp := &chainv1.EnsureGasTopUpResponse{}
|
|
if receipt == nil || receipt.GetResult() == nil {
|
|
return resp
|
|
}
|
|
data := receipt.GetResult().AsMap()
|
|
if amount, ok := data["topup_amount"].(map[string]interface{}); ok {
|
|
resp.TopupAmount = &moneyv1.Money{
|
|
Amount: strings.TrimSpace(fmt.Sprint(amount["amount"])),
|
|
Currency: strings.TrimSpace(fmt.Sprint(amount["currency"])),
|
|
}
|
|
}
|
|
if capHit, ok := data["cap_hit"].(bool); ok {
|
|
resp.CapHit = capHit
|
|
}
|
|
if transferRef, ok := data["transfer_ref"].(string); ok {
|
|
resp.Transfer = &chainv1.Transfer{TransferRef: strings.TrimSpace(transferRef)}
|
|
}
|
|
return resp
|
|
}
|
|
|
|
func feeEstimateKey(req *chainv1.EstimateTransferFeeRequest) string {
|
|
if req == nil || req.GetAmount() == nil {
|
|
return "fee_estimate"
|
|
}
|
|
return fmt.Sprintf("fee_estimate:%s:%s:%s", strings.TrimSpace(req.GetSourceWalletRef()), strings.TrimSpace(req.GetAmount().GetCurrency()), strings.TrimSpace(req.GetAmount().GetAmount()))
|
|
}
|
|
|
|
func connectorError(err *connectorv1.ConnectorError) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
msg := strings.TrimSpace(err.GetMessage())
|
|
switch err.GetCode() {
|
|
case connectorv1.ErrorCode_INVALID_PARAMS:
|
|
return merrors.InvalidArgument(msg)
|
|
case connectorv1.ErrorCode_NOT_FOUND:
|
|
return merrors.NoData(msg)
|
|
case connectorv1.ErrorCode_UNSUPPORTED_OPERATION, connectorv1.ErrorCode_UNSUPPORTED_ACCOUNT_KIND:
|
|
return merrors.NotImplemented(msg)
|
|
case connectorv1.ErrorCode_RATE_LIMITED, connectorv1.ErrorCode_TEMPORARY_UNAVAILABLE:
|
|
return merrors.Internal(msg)
|
|
default:
|
|
return merrors.Internal(msg)
|
|
}
|
|
}
|
|
|
|
func structFromMap(data map[string]interface{}) *structpb.Struct {
|
|
if len(data) == 0 {
|
|
return nil
|
|
}
|
|
result, err := structpb.NewStruct(data)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
|
|
func mapStringToInterface(input map[string]string) map[string]interface{} {
|
|
if len(input) == 0 {
|
|
return nil
|
|
}
|
|
out := make(map[string]interface{}, len(input))
|
|
for k, v := range input {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
func feesToInterface(fees []*chainv1.ServiceFeeBreakdown) []interface{} {
|
|
if len(fees) == 0 {
|
|
return nil
|
|
}
|
|
result := make([]interface{}, 0, len(fees))
|
|
for _, fee := range fees {
|
|
if fee == nil || fee.GetAmount() == nil {
|
|
continue
|
|
}
|
|
result = append(result, map[string]interface{}{
|
|
"fee_code": strings.TrimSpace(fee.GetFeeCode()),
|
|
"description": strings.TrimSpace(fee.GetDescription()),
|
|
"amount": strings.TrimSpace(fee.GetAmount().GetAmount()),
|
|
"currency": strings.TrimSpace(fee.GetAmount().GetCurrency()),
|
|
})
|
|
}
|
|
if len(result) == 0 {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
|
|
func stringFromDetails(details map[string]interface{}, key string) string {
|
|
if details == nil {
|
|
return ""
|
|
}
|
|
if value, ok := details[key]; ok {
|
|
return strings.TrimSpace(fmt.Sprint(value))
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func managedWalletStatusFromAccount(state connectorv1.AccountState) chainv1.ManagedWalletStatus {
|
|
switch state {
|
|
case connectorv1.AccountState_ACCOUNT_ACTIVE:
|
|
return chainv1.ManagedWalletStatus_MANAGED_WALLET_ACTIVE
|
|
case connectorv1.AccountState_ACCOUNT_SUSPENDED:
|
|
return chainv1.ManagedWalletStatus_MANAGED_WALLET_SUSPENDED
|
|
case connectorv1.AccountState_ACCOUNT_CLOSED:
|
|
return chainv1.ManagedWalletStatus_MANAGED_WALLET_CLOSED
|
|
default:
|
|
return chainv1.ManagedWalletStatus_MANAGED_WALLET_STATUS_UNSPECIFIED
|
|
}
|
|
}
|
|
|
|
func transferStatusFromOperation(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_PENDING
|
|
}
|
|
}
|
|
|
|
func operationStatusFromTransfer(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_OPERATION_STATUS_UNSPECIFIED
|
|
}
|
|
}
|
|
|
|
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 tokenFromAssetString(asset string) string {
|
|
if asset == "" {
|
|
return ""
|
|
}
|
|
if idx := strings.Index(asset, "-"); idx > 0 {
|
|
return asset[:idx]
|
|
}
|
|
return asset
|
|
}
|
|
|
|
func chainNetworkFromString(value string) chainv1.ChainNetwork {
|
|
value = strings.ToUpper(strings.TrimSpace(value))
|
|
if value == "" {
|
|
return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
|
|
}
|
|
if val, ok := chainv1.ChainNetwork_value[value]; ok {
|
|
return chainv1.ChainNetwork(val)
|
|
}
|
|
if !strings.HasPrefix(value, "CHAIN_NETWORK_") {
|
|
value = "CHAIN_NETWORK_" + value
|
|
}
|
|
if val, ok := chainv1.ChainNetwork_value[value]; ok {
|
|
return chainv1.ChainNetwork(val)
|
|
}
|
|
return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
|
|
}
|