614 lines
18 KiB
Go
614 lines
18 KiB
Go
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/big"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
ichainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
"go.uber.org/zap"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
|
|
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/drivers"
|
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
|
"github.com/tech/sendico/gateway/chain/storage"
|
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
|
"github.com/tech/sendico/pkg/merrors"
|
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
|
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
|
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
)
|
|
|
|
const (
|
|
walletDefaultLimit int64 = 50
|
|
walletMaxLimit int64 = 200
|
|
transferDefaultLimit int64 = 50
|
|
transferMaxLimit int64 = 200
|
|
depositDefaultLimit int64 = 100
|
|
depositMaxLimit int64 = 500
|
|
)
|
|
|
|
func TestCreateManagedWallet_Idempotent(t *testing.T) {
|
|
svc, repo := newTestService(t)
|
|
|
|
ctx := context.Background()
|
|
req := &ichainv1.CreateManagedWalletRequest{
|
|
IdempotencyKey: "idem-1",
|
|
OrganizationRef: "org-1",
|
|
OwnerRef: "owner-1",
|
|
Asset: &ichainv1.Asset{
|
|
Chain: ichainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET,
|
|
TokenSymbol: "USDC",
|
|
},
|
|
}
|
|
|
|
resp, err := svc.CreateManagedWallet(ctx, req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp.GetWallet())
|
|
firstRef := resp.GetWallet().GetWalletRef()
|
|
require.NotEmpty(t, firstRef)
|
|
|
|
resp2, err := svc.CreateManagedWallet(ctx, req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, firstRef, resp2.GetWallet().GetWalletRef())
|
|
|
|
// ensure stored only once
|
|
require.Equal(t, 1, repo.wallets.count())
|
|
}
|
|
|
|
func TestCreateManagedWallet_NativeTokenWithoutContract(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
ctx := context.Background()
|
|
|
|
resp, err := svc.CreateManagedWallet(ctx, &ichainv1.CreateManagedWalletRequest{
|
|
IdempotencyKey: "idem-native",
|
|
OrganizationRef: "org-1",
|
|
OwnerRef: "owner-1",
|
|
Asset: &ichainv1.Asset{
|
|
Chain: ichainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET,
|
|
TokenSymbol: "ETH",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp.GetWallet())
|
|
require.Equal(t, "ETH", resp.GetWallet().GetAsset().GetTokenSymbol())
|
|
require.Empty(t, resp.GetWallet().GetAsset().GetContractAddress())
|
|
}
|
|
|
|
func TestSubmitTransfer_ManagedDestination(t *testing.T) {
|
|
svc, repo := newTestService(t)
|
|
ctx := context.Background()
|
|
|
|
// create source wallet
|
|
srcResp, err := svc.CreateManagedWallet(ctx, &ichainv1.CreateManagedWalletRequest{
|
|
IdempotencyKey: "idem-src",
|
|
OrganizationRef: "org-1",
|
|
OwnerRef: "owner-1",
|
|
Asset: &ichainv1.Asset{
|
|
Chain: ichainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET,
|
|
TokenSymbol: "USDC",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
srcRef := srcResp.GetWallet().GetWalletRef()
|
|
|
|
// destination wallet
|
|
dstResp, err := svc.CreateManagedWallet(ctx, &ichainv1.CreateManagedWalletRequest{
|
|
IdempotencyKey: "idem-dst",
|
|
OrganizationRef: "org-1",
|
|
OwnerRef: "owner-2",
|
|
Asset: &ichainv1.Asset{
|
|
Chain: ichainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET,
|
|
TokenSymbol: "USDC",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
dstRef := dstResp.GetWallet().GetWalletRef()
|
|
|
|
transferResp, err := svc.SubmitTransfer(ctx, &ichainv1.SubmitTransferRequest{
|
|
IdempotencyKey: "transfer-1",
|
|
OrganizationRef: "org-1",
|
|
SourceWalletRef: srcRef,
|
|
Destination: &ichainv1.TransferDestination{
|
|
Destination: &ichainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: dstRef},
|
|
},
|
|
Amount: &moneyv1.Money{Currency: "USDC", Amount: "100"},
|
|
Fees: []*ichainv1.ServiceFeeBreakdown{
|
|
{
|
|
FeeCode: "service",
|
|
Amount: &moneyv1.Money{Currency: "USDC", Amount: "5"},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, transferResp.GetTransfer())
|
|
require.Equal(t, "95", transferResp.GetTransfer().GetNetAmount().GetAmount())
|
|
|
|
stored := repo.transfers.get(transferResp.GetTransfer().GetTransferRef())
|
|
require.NotNil(t, stored)
|
|
require.Equal(t, model.TransferStatusPending, stored.Status)
|
|
|
|
// GetTransfer
|
|
getResp, err := svc.GetTransfer(ctx, &ichainv1.GetTransferRequest{TransferRef: stored.TransferRef})
|
|
require.NoError(t, err)
|
|
require.Equal(t, stored.TransferRef, getResp.GetTransfer().GetTransferRef())
|
|
|
|
// ListTransfers
|
|
listResp, err := svc.ListTransfers(ctx, &ichainv1.ListTransfersRequest{
|
|
SourceWalletRef: srcRef,
|
|
Page: &paginationv1.CursorPageRequest{Limit: 10},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, listResp.GetTransfers(), 1)
|
|
require.Equal(t, stored.TransferRef, listResp.GetTransfers()[0].GetTransferRef())
|
|
}
|
|
|
|
func TestGetWalletBalance_NotFound(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
ctx := context.Background()
|
|
|
|
_, err := svc.GetWalletBalance(ctx, &ichainv1.GetWalletBalanceRequest{WalletRef: "missing"})
|
|
require.Error(t, err)
|
|
st, _ := status.FromError(err)
|
|
require.Equal(t, codes.NotFound, st.Code())
|
|
}
|
|
|
|
func TestGetWalletBalance_ReturnsCachedNativeAvailable(t *testing.T) {
|
|
svc, repo := newTestService(t)
|
|
ctx := context.Background()
|
|
|
|
createResp, err := svc.CreateManagedWallet(ctx, &ichainv1.CreateManagedWalletRequest{
|
|
IdempotencyKey: "idem-balance",
|
|
OrganizationRef: "org-1",
|
|
OwnerRef: "owner-1",
|
|
Asset: &ichainv1.Asset{
|
|
Chain: ichainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET,
|
|
TokenSymbol: "USDC",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
walletRef := createResp.GetWallet().GetWalletRef()
|
|
|
|
err = repo.wallets.SaveBalance(ctx, &model.WalletBalance{
|
|
WalletRef: walletRef,
|
|
Available: &moneyv1.Money{Currency: "USDC", Amount: "25"},
|
|
NativeAvailable: &moneyv1.Money{Currency: "ETH", Amount: "0.5"},
|
|
CalculatedAt: time.Now().UTC(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
resp, err := svc.GetWalletBalance(ctx, &ichainv1.GetWalletBalanceRequest{WalletRef: walletRef})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp.GetBalance())
|
|
require.Equal(t, "0.5", resp.GetBalance().GetNativeAvailable().GetAmount())
|
|
require.Equal(t, "ETH", resp.GetBalance().GetNativeAvailable().GetCurrency())
|
|
}
|
|
|
|
// ---- in-memory storage implementation ----
|
|
|
|
type inMemoryRepository struct {
|
|
wallets *inMemoryWallets
|
|
transfers *inMemoryTransfers
|
|
deposits *inMemoryDeposits
|
|
}
|
|
|
|
func newInMemoryRepository() *inMemoryRepository {
|
|
return &inMemoryRepository{
|
|
wallets: newInMemoryWallets(),
|
|
transfers: newInMemoryTransfers(),
|
|
deposits: newInMemoryDeposits(),
|
|
}
|
|
}
|
|
|
|
func (r *inMemoryRepository) Ping(context.Context) error { return nil }
|
|
func (r *inMemoryRepository) Wallets() storage.WalletsStore { return r.wallets }
|
|
func (r *inMemoryRepository) Transfers() storage.TransfersStore { return r.transfers }
|
|
func (r *inMemoryRepository) Deposits() storage.DepositsStore { return r.deposits }
|
|
|
|
// Wallets store
|
|
|
|
type inMemoryWallets struct {
|
|
mu sync.Mutex
|
|
wallets map[string]*model.ManagedWallet
|
|
byIdemp map[string]string
|
|
balances map[string]*model.WalletBalance
|
|
}
|
|
|
|
func newInMemoryWallets() *inMemoryWallets {
|
|
return &inMemoryWallets{
|
|
wallets: make(map[string]*model.ManagedWallet),
|
|
byIdemp: make(map[string]string),
|
|
balances: make(map[string]*model.WalletBalance),
|
|
}
|
|
}
|
|
|
|
func (w *inMemoryWallets) count() int {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
return len(w.wallets)
|
|
}
|
|
|
|
func (w *inMemoryWallets) Create(ctx context.Context, wallet *model.ManagedWallet) (*model.ManagedWallet, error) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
if wallet == nil {
|
|
return nil, merrors.InvalidArgument("walletsStore: nil wallet")
|
|
}
|
|
wallet.Normalize()
|
|
if wallet.IdempotencyKey == "" {
|
|
return nil, merrors.InvalidArgument("walletsStore: empty idempotencyKey")
|
|
}
|
|
|
|
if existingRef, ok := w.byIdemp[wallet.IdempotencyKey]; ok {
|
|
existing := w.wallets[existingRef]
|
|
return existing, merrors.ErrDataConflict
|
|
}
|
|
|
|
if wallet.WalletRef == "" {
|
|
wallet.WalletRef = primitive.NewObjectID().Hex()
|
|
}
|
|
if wallet.GetID() == nil || wallet.GetID().IsZero() {
|
|
wallet.SetID(primitive.NewObjectID())
|
|
} else {
|
|
wallet.Update()
|
|
}
|
|
|
|
w.wallets[wallet.WalletRef] = wallet
|
|
w.byIdemp[wallet.IdempotencyKey] = wallet.WalletRef
|
|
return wallet, nil
|
|
}
|
|
|
|
func (w *inMemoryWallets) Get(ctx context.Context, walletRef string) (*model.ManagedWallet, error) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
wallet, ok := w.wallets[strings.TrimSpace(walletRef)]
|
|
if !ok {
|
|
return nil, merrors.NoData("wallet not found")
|
|
}
|
|
return wallet, nil
|
|
}
|
|
|
|
func (w *inMemoryWallets) List(ctx context.Context, filter model.ManagedWalletFilter) (*model.ManagedWalletList, error) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
items := make([]*model.ManagedWallet, 0, len(w.wallets))
|
|
for _, wallet := range w.wallets {
|
|
if filter.OrganizationRef != "" && !strings.EqualFold(wallet.OrganizationRef, filter.OrganizationRef) {
|
|
continue
|
|
}
|
|
if filter.OwnerRef != "" && !strings.EqualFold(wallet.OwnerRef, filter.OwnerRef) {
|
|
continue
|
|
}
|
|
if filter.Network != "" && !strings.EqualFold(wallet.Network, filter.Network) {
|
|
continue
|
|
}
|
|
if filter.TokenSymbol != "" && !strings.EqualFold(wallet.TokenSymbol, filter.TokenSymbol) {
|
|
continue
|
|
}
|
|
items = append(items, wallet)
|
|
}
|
|
|
|
sort.Slice(items, func(i, j int) bool {
|
|
return items[i].ID.Timestamp().Before(items[j].ID.Timestamp())
|
|
})
|
|
|
|
startIndex := 0
|
|
if cursor := strings.TrimSpace(filter.Cursor); cursor != "" {
|
|
if oid, err := primitive.ObjectIDFromHex(cursor); err == nil {
|
|
for idx, item := range items {
|
|
if item.ID.Timestamp().After(oid.Timestamp()) {
|
|
startIndex = idx
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
limit := int(sanitizeLimit(filter.Limit, walletDefaultLimit, walletMaxLimit))
|
|
end := startIndex + limit
|
|
hasMore := false
|
|
if end < len(items) {
|
|
hasMore = true
|
|
items = items[startIndex:end]
|
|
} else {
|
|
items = items[startIndex:]
|
|
}
|
|
|
|
nextCursor := ""
|
|
if hasMore && len(items) > 0 {
|
|
nextCursor = items[len(items)-1].ID.Hex()
|
|
}
|
|
|
|
return &model.ManagedWalletList{Items: items, NextCursor: nextCursor}, nil
|
|
}
|
|
|
|
func (w *inMemoryWallets) SaveBalance(ctx context.Context, balance *model.WalletBalance) error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
if balance == nil {
|
|
return merrors.InvalidArgument("walletsStore: nil balance")
|
|
}
|
|
balance.Normalize()
|
|
if balance.WalletRef == "" {
|
|
return merrors.InvalidArgument("walletsStore: empty walletRef for balance")
|
|
}
|
|
if balance.CalculatedAt.IsZero() {
|
|
balance.CalculatedAt = time.Now().UTC()
|
|
}
|
|
existing, ok := w.balances[balance.WalletRef]
|
|
if !ok {
|
|
if balance.GetID() == nil || balance.GetID().IsZero() {
|
|
balance.SetID(primitive.NewObjectID())
|
|
}
|
|
w.balances[balance.WalletRef] = balance
|
|
return nil
|
|
}
|
|
existing.Available = balance.Available
|
|
existing.PendingInbound = balance.PendingInbound
|
|
existing.PendingOutbound = balance.PendingOutbound
|
|
existing.CalculatedAt = balance.CalculatedAt
|
|
existing.Update()
|
|
return nil
|
|
}
|
|
|
|
func (w *inMemoryWallets) GetBalance(ctx context.Context, walletRef string) (*model.WalletBalance, error) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
balance, ok := w.balances[strings.TrimSpace(walletRef)]
|
|
if !ok {
|
|
return nil, merrors.NoData("wallet balance not found")
|
|
}
|
|
return balance, nil
|
|
}
|
|
|
|
// Transfers store
|
|
|
|
type inMemoryTransfers struct {
|
|
mu sync.Mutex
|
|
items map[string]*model.Transfer
|
|
byIdemp map[string]string
|
|
}
|
|
|
|
func newInMemoryTransfers() *inMemoryTransfers {
|
|
return &inMemoryTransfers{
|
|
items: make(map[string]*model.Transfer),
|
|
byIdemp: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
func (t *inMemoryTransfers) Create(ctx context.Context, transfer *model.Transfer) (*model.Transfer, error) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if transfer == nil {
|
|
return nil, merrors.InvalidArgument("transfersStore: nil transfer")
|
|
}
|
|
transfer.Normalize()
|
|
if transfer.IdempotencyKey == "" {
|
|
return nil, merrors.InvalidArgument("transfersStore: empty idempotencyKey")
|
|
}
|
|
if ref, ok := t.byIdemp[transfer.IdempotencyKey]; ok {
|
|
return t.items[ref], merrors.ErrDataConflict
|
|
}
|
|
if transfer.TransferRef == "" {
|
|
transfer.TransferRef = primitive.NewObjectID().Hex()
|
|
}
|
|
if transfer.GetID() == nil || transfer.GetID().IsZero() {
|
|
transfer.SetID(primitive.NewObjectID())
|
|
} else {
|
|
transfer.Update()
|
|
}
|
|
t.items[transfer.TransferRef] = transfer
|
|
t.byIdemp[transfer.IdempotencyKey] = transfer.TransferRef
|
|
return transfer, nil
|
|
}
|
|
|
|
func (t *inMemoryTransfers) Get(ctx context.Context, transferRef string) (*model.Transfer, error) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
transfer, ok := t.items[strings.TrimSpace(transferRef)]
|
|
if !ok {
|
|
return nil, merrors.NoData("transfer not found")
|
|
}
|
|
return transfer, nil
|
|
}
|
|
|
|
func (t *inMemoryTransfers) List(ctx context.Context, filter model.TransferFilter) (*model.TransferList, error) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
items := make([]*model.Transfer, 0, len(t.items))
|
|
for _, transfer := range t.items {
|
|
if filter.SourceWalletRef != "" && !strings.EqualFold(transfer.SourceWalletRef, filter.SourceWalletRef) {
|
|
continue
|
|
}
|
|
if filter.DestinationWalletRef != "" && !strings.EqualFold(transfer.Destination.ManagedWalletRef, filter.DestinationWalletRef) {
|
|
continue
|
|
}
|
|
if filter.Status != "" && transfer.Status != filter.Status {
|
|
continue
|
|
}
|
|
items = append(items, transfer)
|
|
}
|
|
|
|
sort.Slice(items, func(i, j int) bool {
|
|
return items[i].ID.Timestamp().Before(items[j].ID.Timestamp())
|
|
})
|
|
|
|
start := 0
|
|
if cursor := strings.TrimSpace(filter.Cursor); cursor != "" {
|
|
if oid, err := primitive.ObjectIDFromHex(cursor); err == nil {
|
|
for idx, item := range items {
|
|
if item.ID.Timestamp().After(oid.Timestamp()) {
|
|
start = idx
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
limit := int(sanitizeLimit(filter.Limit, transferDefaultLimit, transferMaxLimit))
|
|
end := start + limit
|
|
hasMore := false
|
|
if end < len(items) {
|
|
hasMore = true
|
|
items = items[start:end]
|
|
} else {
|
|
items = items[start:]
|
|
}
|
|
|
|
nextCursor := ""
|
|
if hasMore && len(items) > 0 {
|
|
nextCursor = items[len(items)-1].ID.Hex()
|
|
}
|
|
|
|
return &model.TransferList{Items: items, NextCursor: nextCursor}, nil
|
|
}
|
|
|
|
func (t *inMemoryTransfers) UpdateStatus(ctx context.Context, transferRef string, status model.TransferStatus, failureReason string, txHash string) (*model.Transfer, error) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
transfer, ok := t.items[strings.TrimSpace(transferRef)]
|
|
if !ok {
|
|
return nil, merrors.NoData("transfer not found")
|
|
}
|
|
transfer.Status = status
|
|
if status == model.TransferStatusFailed {
|
|
transfer.FailureReason = strings.TrimSpace(failureReason)
|
|
} else {
|
|
transfer.FailureReason = ""
|
|
}
|
|
transfer.TxHash = strings.TrimSpace(txHash)
|
|
transfer.LastStatusAt = time.Now().UTC()
|
|
transfer.Update()
|
|
return transfer, nil
|
|
}
|
|
|
|
// helper for tests
|
|
func (t *inMemoryTransfers) get(ref string) *model.Transfer {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.items[ref]
|
|
}
|
|
|
|
// Deposits store (minimal for tests)
|
|
|
|
type inMemoryDeposits struct {
|
|
mu sync.Mutex
|
|
items map[string]*model.Deposit
|
|
}
|
|
|
|
func newInMemoryDeposits() *inMemoryDeposits {
|
|
return &inMemoryDeposits{items: make(map[string]*model.Deposit)}
|
|
}
|
|
|
|
func (d *inMemoryDeposits) Record(ctx context.Context, deposit *model.Deposit) error {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
if deposit == nil {
|
|
return merrors.InvalidArgument("depositsStore: nil deposit")
|
|
}
|
|
deposit.Normalize()
|
|
if deposit.DepositRef == "" {
|
|
return merrors.InvalidArgument("depositsStore: empty depositRef")
|
|
}
|
|
if existing, ok := d.items[deposit.DepositRef]; ok {
|
|
existing.Status = deposit.Status
|
|
existing.LastStatusAt = time.Now().UTC()
|
|
existing.Update()
|
|
return nil
|
|
}
|
|
if deposit.GetID() == nil || deposit.GetID().IsZero() {
|
|
deposit.SetID(primitive.NewObjectID())
|
|
}
|
|
if deposit.ObservedAt.IsZero() {
|
|
deposit.ObservedAt = time.Now().UTC()
|
|
}
|
|
if deposit.RecordedAt.IsZero() {
|
|
deposit.RecordedAt = time.Now().UTC()
|
|
}
|
|
deposit.LastStatusAt = time.Now().UTC()
|
|
d.items[deposit.DepositRef] = deposit
|
|
return nil
|
|
}
|
|
|
|
func (d *inMemoryDeposits) ListPending(ctx context.Context, network string, limit int32) ([]*model.Deposit, error) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
results := make([]*model.Deposit, 0)
|
|
for _, deposit := range d.items {
|
|
if deposit.Status != model.DepositStatusPending {
|
|
continue
|
|
}
|
|
if network != "" && !strings.EqualFold(deposit.Network, network) {
|
|
continue
|
|
}
|
|
results = append(results, deposit)
|
|
}
|
|
sort.Slice(results, func(i, j int) bool {
|
|
return results[i].ObservedAt.Before(results[j].ObservedAt)
|
|
})
|
|
limitVal := int(sanitizeLimit(limit, depositDefaultLimit, depositMaxLimit))
|
|
if len(results) > limitVal {
|
|
results = results[:limitVal]
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// shared helpers
|
|
|
|
func sanitizeLimit(requested int32, def, max int64) int64 {
|
|
if requested <= 0 {
|
|
return def
|
|
}
|
|
if requested > int32(max) {
|
|
return max
|
|
}
|
|
return int64(requested)
|
|
}
|
|
|
|
func newTestService(t *testing.T) (*Service, *inMemoryRepository) {
|
|
repo := newInMemoryRepository()
|
|
logger := zap.NewNop()
|
|
networks := []shared.Network{{
|
|
Name: "ethereum_mainnet",
|
|
NativeToken: "ETH",
|
|
TokenConfigs: []shared.TokenContract{
|
|
{Symbol: "USDC", ContractAddress: "0xusdc"},
|
|
},
|
|
}}
|
|
driverRegistry, err := drivers.NewRegistry(logger.Named("drivers"), networks)
|
|
require.NoError(t, err)
|
|
svc := NewService(logger, repo, nil,
|
|
WithKeyManager(&fakeKeyManager{}),
|
|
WithNetworks(networks),
|
|
WithServiceWallet(shared.ServiceWallet{Network: "ethereum_mainnet", Address: "0xservice"}),
|
|
WithDriverRegistry(driverRegistry),
|
|
)
|
|
return svc, repo
|
|
}
|
|
|
|
type fakeKeyManager struct{}
|
|
|
|
func (f *fakeKeyManager) CreateManagedWalletKey(ctx context.Context, walletRef string, network string) (*keymanager.ManagedWalletKey, error) {
|
|
return &keymanager.ManagedWalletKey{
|
|
KeyID: fmt.Sprintf("%s/%s", strings.ToLower(network), walletRef),
|
|
Address: "0x" + strings.Repeat("a", 40),
|
|
PublicKey: strings.Repeat("b", 128),
|
|
}, nil
|
|
}
|
|
|
|
func (f *fakeKeyManager) SignTransaction(ctx context.Context, keyID string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
|
|
return tx, nil
|
|
}
|