implemented backend wallets/ledger accounts listing

This commit is contained in:
Stephan D
2025-11-25 23:38:10 +01:00
parent be913bf96c
commit 68f0a1048f
25 changed files with 882 additions and 131 deletions

View File

@@ -85,6 +85,12 @@ api:
chain: ARBITRUM_ONE
token_symbol: USDT
contract_address: ""
ledger:
address: sendico_ledger:50052
address_env: LEDGER_ADDRESS
dial_timeout_seconds: 5
call_timeout_seconds: 5
insecure: true
app:

View File

@@ -4,13 +4,15 @@ go 1.25.3
replace github.com/tech/sendico/pkg => ../pkg
replace github.com/tech/sendico/ledger => ../ledger
replace github.com/tech/sendico/chain/gateway => ../chain/gateway
require (
github.com/aws/aws-sdk-go-v2 v1.40.0
github.com/aws/aws-sdk-go-v2/config v1.32.1
github.com/aws/aws-sdk-go-v2/credentials v1.19.1
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.0
github.com/aws/aws-sdk-go-v2/config v1.32.2
github.com/aws/aws-sdk-go-v2/credentials v1.19.2
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1
github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/cors v1.2.2
github.com/go-chi/jwtauth/v5 v5.3.3
@@ -19,12 +21,14 @@ require (
github.com/mitchellh/mapstructure v1.5.0
github.com/stretchr/testify v1.11.1
github.com/tech/sendico/chain/gateway v0.1.0
github.com/tech/sendico/ledger v0.0.0-00010101000000-000000000000
github.com/tech/sendico/pkg v0.1.0
github.com/testcontainers/testcontainers-go v0.33.0
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0
go.mongodb.org/mongo-driver v1.17.6
go.uber.org/zap v1.27.1
golang.org/x/net v0.47.0
google.golang.org/protobuf v1.36.10
gopkg.in/yaml.v3 v3.0.1
moul.io/chizap v1.0.3
)
@@ -49,10 +53,10 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.9 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.1 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 // indirect
github.com/aws/smithy-go v1.23.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
@@ -133,5 +137,4 @@ require (
golang.org/x/text v0.31.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)

View File

@@ -10,10 +10,10 @@ github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrK
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y=
github.com/aws/aws-sdk-go-v2/config v1.32.1 h1:iODUDLgk3q8/flEC7ymhmxjfoAnBDwEEYEVyKZ9mzjU=
github.com/aws/aws-sdk-go-v2/config v1.32.1/go.mod h1:xoAgo17AGrPpJBSLg81W+ikM0cpOZG8ad04T2r+d5P0=
github.com/aws/aws-sdk-go-v2/credentials v1.19.1 h1:JeW+EwmtTE0yXFK8SmklrFh/cGTTXsQJumgMZNlbxfM=
github.com/aws/aws-sdk-go-v2/credentials v1.19.1/go.mod h1:BOoXiStwTF+fT2XufhO0Efssbi1CNIO/ZXpZu87N0pw=
github.com/aws/aws-sdk-go-v2/config v1.32.2 h1:4liUsdEpUUPZs5WVapsJLx5NPmQhQdez7nYFcovrytk=
github.com/aws/aws-sdk-go-v2/config v1.32.2/go.mod h1:l0hs06IFz1eCT+jTacU/qZtC33nvcnLADAPL/XyrkZI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.2 h1:qZry8VUyTK4VIo5aEdUcBjPZHL2v4FyQ3QEOaWcFLu4=
github.com/aws/aws-sdk-go-v2/credentials v1.19.2/go.mod h1:YUqm5a1/kBnoK+/NY5WEiMocZihKSo15/tJdmdXnM5g=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
@@ -32,16 +32,16 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 h1:FzQE21lNtUor0Fb7QNgnEyiRCBlolLTX/Z1j65S7teM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14/go.mod h1:s1ydyWG9pm3ZwmmYN21HKyG9WzAZhYVW85wMHs5FV6w=
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.0 h1:8FshVvnV2sr9kOSAbOnc/vwVmmAwMjOedKH6JW2ddPM=
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.0/go.mod h1:wYNqY3L02Z3IgRYxOBPH9I1zD9Cjh9hI5QOy/eOjQvw=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.1 h1:BDgIUYGEo5TkayOWv/oBLPphWwNm/A91AebUjAu5L5g=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.1/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.4 h1:U//SlnkE1wOQiIImxzdY5PXat4Wq+8rlfVEw4Y7J8as=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.4/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.9 h1:LU8S9W/mPDAU9q0FjCLi0TrCheLMGwzbRpvUMwYspcA=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.9/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.1 h1:GdGmKtG+/Krag7VfyOXV17xjTCz0i9NT+JnqLTOI5nA=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.1/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso=
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1 h1:OgQy/+0+Kc3khtqiEOk23xQAglXi3Tj0y5doOxbi5tg=
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1/go.mod h1:wYNqY3L02Z3IgRYxOBPH9I1zD9Cjh9hI5QOy/eOjQvw=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 h1:MxMBdKTYBjPQChlJhi4qlEueqB1p1KcbTEa7tD5aqPs=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 h1:ksUT5KtgpZd3SAiFJNJ0AFEJVva3gjBmN7eXUZjzUwQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 h1:GtsxyiF3Nd3JahRBJbxLCCdYW9ltGQYrFWg8XdkGDd8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 h1:a5UTtD4mHBU3t0o6aHQZFJTNKVfxFWfPX7J0Lr7G+uY=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso=
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=

View File

@@ -9,6 +9,7 @@ type Config struct {
Mw *mwa.Config `yaml:"middleware"`
Storage *fsc.Config `yaml:"storage"`
ChainGateway *ChainGatewayConfig `yaml:"chain_gateway"`
Ledger *LedgerConfig `yaml:"ledger"`
}
type ChainGatewayConfig struct {
@@ -25,3 +26,11 @@ type ChainGatewayAssetConfig struct {
TokenSymbol string `yaml:"token_symbol"`
ContractAddress string `yaml:"contract_address"`
}
type LedgerConfig struct {
Address string `yaml:"address"`
AddressEnv string `yaml:"address_env"`
DialTimeoutSeconds int `yaml:"dial_timeout_seconds"`
CallTimeoutSeconds int `yaml:"call_timeout_seconds"`
Insecure bool `yaml:"insecure"`
}

View File

@@ -0,0 +1,106 @@
package sresponse
import (
"net/http"
"time"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
)
type ledgerAccount struct {
LedgerAccountRef string `json:"ledgerAccountRef"`
OrganizationRef string `json:"organizationRef"`
AccountCode string `json:"accountCode"`
AccountType string `json:"accountType"`
Currency string `json:"currency"`
Status string `json:"status"`
AllowNegative bool `json:"allowNegative"`
IsSettlement bool `json:"isSettlement"`
Metadata map[string]string `json:"metadata,omitempty"`
CreatedAt time.Time `json:"createdAt,omitempty"`
UpdatedAt time.Time `json:"updatedAt,omitempty"`
}
type ledgerAccountsResponse struct {
authResponse `json:",inline"`
Accounts []ledgerAccount `json:"accounts"`
}
type ledgerMoney struct {
Amount string `json:"amount"`
Currency string `json:"currency"`
}
type ledgerBalance struct {
LedgerAccountRef string `json:"ledgerAccountRef"`
Balance *ledgerMoney `json:"balance,omitempty"`
Version int64 `json:"version"`
LastUpdated time.Time `json:"lastUpdated,omitempty"`
}
type ledgerBalanceResponse struct {
authResponse `json:",inline"`
Balance ledgerBalance `json:"balance"`
}
func LedgerAccounts(logger mlogger.Logger, accounts []*ledgerv1.LedgerAccount, accessToken *TokenData) http.HandlerFunc {
dto := make([]ledgerAccount, 0, len(accounts))
for _, acc := range accounts {
dto = append(dto, toLedgerAccount(acc))
}
return response.Ok(logger, ledgerAccountsResponse{
Accounts: dto,
authResponse: authResponse{AccessToken: *accessToken},
})
}
func LedgerBalance(logger mlogger.Logger, resp *ledgerv1.BalanceResponse, accessToken *TokenData) http.HandlerFunc {
return response.Ok(logger, ledgerBalanceResponse{
Balance: toLedgerBalance(resp),
authResponse: authResponse{AccessToken: *accessToken},
})
}
func toLedgerAccount(acc *ledgerv1.LedgerAccount) ledgerAccount {
if acc == nil {
return ledgerAccount{}
}
return ledgerAccount{
LedgerAccountRef: acc.GetLedgerAccountRef(),
OrganizationRef: acc.GetOrganizationRef(),
AccountCode: acc.GetAccountCode(),
AccountType: acc.GetAccountType().String(),
Currency: acc.GetCurrency(),
Status: acc.GetStatus().String(),
AllowNegative: acc.GetAllowNegative(),
IsSettlement: acc.GetIsSettlement(),
Metadata: acc.GetMetadata(),
CreatedAt: acc.GetCreatedAt().AsTime(),
UpdatedAt: acc.GetUpdatedAt().AsTime(),
}
}
func toLedgerBalance(resp *ledgerv1.BalanceResponse) ledgerBalance {
if resp == nil {
return ledgerBalance{}
}
return ledgerBalance{
LedgerAccountRef: resp.GetLedgerAccountRef(),
Balance: toLedgerMoney(resp.GetBalance()),
Version: resp.GetVersion(),
LastUpdated: resp.GetLastUpdated().AsTime(),
}
}
func toLedgerMoney(m *moneyv1.Money) *ledgerMoney {
if m == nil {
return nil
}
return &ledgerMoney{
Amount: m.GetAmount(),
Currency: m.GetCurrency(),
}
}

View File

@@ -0,0 +1,132 @@
package sresponse
import (
"net/http"
"time"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger"
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
"google.golang.org/protobuf/types/known/timestamppb"
)
type walletAsset struct {
Chain string `json:"chain"`
TokenSymbol string `json:"tokenSymbol"`
ContractAddress string `json:"contractAddress"`
}
type wallet struct {
WalletRef string `json:"walletRef"`
OrganizationRef string `json:"organizationRef"`
OwnerRef string `json:"ownerRef"`
Asset walletAsset `json:"asset"`
DepositAddress string `json:"depositAddress"`
Status string `json:"status"`
Metadata map[string]string `json:"metadata,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
UpdatedAt string `json:"updatedAt,omitempty"`
}
type walletsResponse struct {
authResponse `json:",inline"`
Wallets []wallet `json:"wallets"`
Page *paginationv1.CursorPageResponse `json:"page,omitempty"`
}
type walletMoney struct {
Amount string `json:"amount"`
Currency string `json:"currency"`
}
type walletBalance struct {
Available *walletMoney `json:"available,omitempty"`
PendingInbound *walletMoney `json:"pendingInbound,omitempty"`
PendingOutbound *walletMoney `json:"pendingOutbound,omitempty"`
CalculatedAt string `json:"calculatedAt,omitempty"`
}
type walletBalanceResponse struct {
authResponse `json:",inline"`
Balance walletBalance `json:"balance"`
}
func Wallets(logger mlogger.Logger, resp *gatewayv1.ListManagedWalletsResponse, accessToken *TokenData) http.HandlerFunc {
dto := walletsResponse{
Page: resp.GetPage(),
authResponse: authResponse{AccessToken: *accessToken},
}
dto.Wallets = make([]wallet, 0, len(resp.GetWallets()))
for _, w := range resp.GetWallets() {
dto.Wallets = append(dto.Wallets, toWallet(w))
}
return response.Ok(logger, dto)
}
func WalletBalance(logger mlogger.Logger, bal *gatewayv1.WalletBalance, accessToken *TokenData) http.HandlerFunc {
return response.Ok(logger, walletBalanceResponse{
Balance: toWalletBalance(bal),
authResponse: authResponse{AccessToken: *accessToken},
})
}
func toWallet(w *gatewayv1.ManagedWallet) wallet {
if w == nil {
return wallet{}
}
asset := w.GetAsset()
chain := ""
token := ""
contract := ""
if asset != nil {
chain = asset.GetChain().String()
token = asset.GetTokenSymbol()
contract = asset.GetContractAddress()
}
return wallet{
WalletRef: w.GetWalletRef(),
OrganizationRef: w.GetOrganizationRef(),
OwnerRef: w.GetOwnerRef(),
Asset: walletAsset{
Chain: chain,
TokenSymbol: token,
ContractAddress: contract,
},
DepositAddress: w.GetDepositAddress(),
Status: w.GetStatus().String(),
Metadata: w.GetMetadata(),
CreatedAt: tsToString(w.GetCreatedAt()),
UpdatedAt: tsToString(w.GetUpdatedAt()),
}
}
func toWalletBalance(b *gatewayv1.WalletBalance) walletBalance {
if b == nil {
return walletBalance{}
}
return walletBalance{
Available: toMoney(b.GetAvailable()),
PendingInbound: toMoney(b.GetPendingInbound()),
PendingOutbound: toMoney(b.GetPendingOutbound()),
CalculatedAt: tsToString(b.GetCalculatedAt()),
}
}
func toMoney(m *moneyv1.Money) *walletMoney {
if m == nil {
return nil
}
return &walletMoney{
Amount: m.GetAmount(),
Currency: m.GetCurrency(),
}
}
func tsToString(ts *timestamppb.Timestamp) string {
if ts == nil {
return ""
}
return ts.AsTime().UTC().Format(time.RFC3339)
}

View File

@@ -0,0 +1,11 @@
package ledger
import (
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api"
"github.com/tech/sendico/server/internal/server/ledgerapiimp"
)
func Create(a api.API) (mservice.MicroService, error) {
return ledgerapiimp.CreateAPI(a)
}

View File

@@ -0,0 +1,11 @@
package wallet
import (
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api"
"github.com/tech/sendico/server/internal/server/walletapiimp"
)
func Create(a api.API) (mservice.MicroService, error) {
return walletapiimp.CreateAPI(a)
}

View File

@@ -18,6 +18,8 @@ import (
"github.com/tech/sendico/server/interface/services/organization"
"github.com/tech/sendico/server/interface/services/permission"
"github.com/tech/sendico/server/interface/services/site"
"github.com/tech/sendico/server/interface/services/wallet"
"github.com/tech/sendico/server/interface/services/ledger"
"go.uber.org/zap"
)
@@ -83,6 +85,8 @@ func (a *APIImp) installServices() error {
srvf = append(srvf, logo.Create)
srvf = append(srvf, permission.Create)
srvf = append(srvf, site.Create)
srvf = append(srvf, wallet.Create)
srvf = append(srvf, ledger.Create)
for _, v := range srvf {
if err := a.addMicroservice(v); err != nil {

View File

@@ -0,0 +1,52 @@
package ledgerapiimp
import (
"errors"
"net/http"
"strings"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
"github.com/tech/sendico/server/interface/api/sresponse"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
func (a *LedgerAPI) getBalance(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
orgRef, err := a.oph.GetRef(r)
if err != nil {
a.logger.Warn("Failed to parse organization reference for ledger balance", zap.Error(err), zap.String(a.oph.Name(), a.oph.GetID(r)))
return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
}
accountRef := strings.TrimSpace(a.aph.GetID(r))
if accountRef == "" {
return response.BadReference(a.logger, a.Name(), a.aph.Name(), a.aph.GetID(r), errors.New("ledger account reference is required"))
}
ctx := r.Context()
res, err := a.enf.Enforce(ctx, a.balancePerm, account.ID, orgRef, primitive.NilObjectID, model.ActionRead)
if err != nil {
a.logger.Warn("Failed to check ledger balance access permissions", zap.Error(err), zap.String(a.oph.Name(), orgRef.Hex()), zap.String("ledger_account_ref", accountRef))
return response.Auto(a.logger, a.Name(), err)
}
if !res {
a.logger.Debug("Access denied when reading ledger balance", zap.String(a.oph.Name(), orgRef.Hex()), zap.String("ledger_account_ref", accountRef))
return response.AccessDenied(a.logger, a.Name(), "ledger balance read permission denied")
}
if a.client == nil {
return response.Internal(a.logger, mservice.Ledger, errors.New("ledger client is not configured"))
}
resp, err := a.client.GetBalance(ctx, &ledgerv1.GetBalanceRequest{
LedgerAccountRef: accountRef,
})
if err != nil {
a.logger.Warn("Failed to fetch ledger balance", zap.Error(err), zap.String("ledger_account_ref", accountRef))
return response.Auto(a.logger, mservice.Ledger, err)
}
return sresponse.LedgerBalance(a.logger, resp, token)
}

View File

@@ -0,0 +1,46 @@
package ledgerapiimp
import (
"errors"
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
"github.com/tech/sendico/server/interface/api/sresponse"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
func (a *LedgerAPI) listAccounts(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
orgRef, err := a.oph.GetRef(r)
if err != nil {
a.logger.Warn("Failed to parse organization reference for ledger account list", zap.Error(err), zap.String(a.oph.Name(), a.oph.GetID(r)))
return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
}
ctx := r.Context()
res, err := a.enf.Enforce(ctx, a.permissionRef, account.ID, orgRef, primitive.NilObjectID, model.ActionRead)
if err != nil {
a.logger.Warn("Failed to check ledger accounts access permissions", zap.Error(err), zap.String(a.oph.Name(), orgRef.Hex()))
return response.Auto(a.logger, a.Name(), err)
}
if !res {
a.logger.Debug("Access denied when listing ledger accounts", zap.String(a.oph.Name(), orgRef.Hex()))
return response.AccessDenied(a.logger, a.Name(), "ledger accounts read permission denied")
}
if a.client == nil {
return response.Internal(a.logger, mservice.Ledger, errors.New("ledger client is not configured"))
}
resp, err := a.client.ListAccounts(ctx, &ledgerv1.ListAccountsRequest{
OrganizationRef: orgRef.Hex(),
})
if err != nil {
a.logger.Warn("Failed to list ledger accounts", zap.Error(err), zap.String("organization_ref", orgRef.Hex()))
return response.Auto(a.logger, mservice.Ledger, err)
}
return sresponse.LedgerAccounts(a.logger, resp.GetAccounts(), token)
}

View File

@@ -0,0 +1,110 @@
package ledgerapiimp
import (
"context"
"fmt"
"os"
"strings"
"time"
ledgerclient "github.com/tech/sendico/ledger/client"
api "github.com/tech/sendico/pkg/api/http"
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
eapi "github.com/tech/sendico/server/interface/api"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
type ledgerClient interface {
ListAccounts(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error)
GetBalance(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, error)
Close() error
}
type LedgerAPI struct {
logger mlogger.Logger
client ledgerClient
enf auth.Enforcer
oph mutil.ParamHelper
aph mutil.ParamHelper
permissionRef primitive.ObjectID
balancePerm primitive.ObjectID
}
func (a *LedgerAPI) Name() mservice.Type { return mservice.LedgerAccounts }
func (a *LedgerAPI) Finish(ctx context.Context) error {
if a.client != nil {
if err := a.client.Close(); err != nil {
a.logger.Warn("Failed to close ledger client", zap.Error(err))
}
}
return nil
}
func CreateAPI(apiCtx eapi.API) (*LedgerAPI, error) {
p := &LedgerAPI{
logger: apiCtx.Logger().Named(mservice.LedgerAccounts),
enf: apiCtx.Permissions().Enforcer(),
oph: mutil.CreatePH(mservice.Organizations),
aph: mutil.CreatePH("ledger_account"),
}
desc, err := apiCtx.Permissions().GetPolicyDescription(context.Background(), mservice.LedgerAccounts)
if err != nil {
p.logger.Warn("Failed to fetch ledger accounts permission description", zap.Error(err))
return nil, err
}
p.permissionRef = desc.ID
bdesc, err := apiCtx.Permissions().GetPolicyDescription(context.Background(), mservice.LedgerBalances)
if err != nil {
p.logger.Warn("Failed to fetch ledger balances permission description", zap.Error(err))
return nil, err
}
p.balancePerm = bdesc.ID
if err := p.initLedgerClient(apiCtx.Config().Ledger); err != nil {
p.logger.Error("Failed to initialize ledger client", zap.Error(err))
return nil, err
}
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/"), api.Get, p.listAccounts)
apiCtx.Register().AccountHandler(p.Name(), p.aph.AddRef(p.oph.AddRef("/"))+"/balance", api.Get, p.getBalance)
return p, nil
}
func (a *LedgerAPI) initLedgerClient(cfg *eapi.LedgerConfig) error {
if cfg == nil {
return merrors.InvalidArgument("ledger configuration is not provided")
}
address := strings.TrimSpace(cfg.Address)
if address == "" {
address = strings.TrimSpace(os.Getenv(cfg.AddressEnv))
}
if address == "" {
return merrors.InvalidArgument(fmt.Sprintf("ledger address is not specified and address env %s is empty", cfg.AddressEnv))
}
clientCfg := ledgerclient.Config{
Address: address,
DialTimeout: time.Duration(cfg.DialTimeoutSeconds) * time.Second,
CallTimeout: time.Duration(cfg.CallTimeoutSeconds) * time.Second,
Insecure: cfg.Insecure,
}
client, err := ledgerclient.New(context.Background(), clientCfg)
if err != nil {
return err
}
a.client = client
return nil
}

View File

@@ -0,0 +1,55 @@
package walletapiimp
import (
"errors"
"net/http"
"strings"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
"github.com/tech/sendico/server/interface/api/sresponse"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
func (a *WalletAPI) getWalletBalance(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
orgRef, err := a.oph.GetRef(r)
if err != nil {
a.logger.Warn("Failed to parse organization reference for wallet balance", zap.Error(err), zap.String(a.oph.Name(), a.oph.GetID(r)))
return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
}
walletRef := strings.TrimSpace(a.wph.GetID(r))
if walletRef == "" {
return response.BadReference(a.logger, a.Name(), a.wph.Name(), a.wph.GetID(r), errors.New("wallet reference is required"))
}
ctx := r.Context()
res, err := a.enf.Enforce(ctx, a.balancesPermissionRef, account.ID, orgRef, primitive.NilObjectID, model.ActionRead)
if err != nil {
a.logger.Warn("Failed to check wallet balance permissions", zap.Error(err), zap.String(a.oph.Name(), orgRef.Hex()), zap.String("wallet_ref", walletRef))
return response.Auto(a.logger, a.Name(), err)
}
if !res {
a.logger.Debug("Access denied when reading wallet balance", zap.String(a.oph.Name(), orgRef.Hex()), zap.String("wallet_ref", walletRef))
return response.AccessDenied(a.logger, a.Name(), "wallet balance read permission denied")
}
if a.chainGateway == nil {
return response.Internal(a.logger, mservice.ChainGateway, errors.New("chain gateway client is not configured"))
}
resp, err := a.chainGateway.GetWalletBalance(ctx, &gatewayv1.GetWalletBalanceRequest{WalletRef: walletRef})
if err != nil {
a.logger.Warn("Failed to fetch wallet balance", zap.Error(err), zap.String("wallet_ref", walletRef))
return response.Auto(a.logger, mservice.ChainGateway, err)
}
bal := resp.GetBalance()
if bal == nil {
a.logger.Warn("Wallet balance missing in response", zap.String("wallet_ref", walletRef))
return response.Auto(a.logger, mservice.ChainGateway, errors.New("wallet balance not available"))
}
return sresponse.WalletBalance(a.logger, bal, token)
}

View File

@@ -0,0 +1,52 @@
package walletapiimp
import (
"errors"
"net/http"
"strings"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
"github.com/tech/sendico/server/interface/api/sresponse"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
func (a *WalletAPI) listWallets(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
orgRef, err := a.oph.GetRef(r)
if err != nil {
a.logger.Warn("Failed to parse organization reference for wallet list", zap.Error(err), zap.String(a.oph.Name(), a.oph.GetID(r)))
return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
}
ctx := r.Context()
res, err := a.enf.Enforce(ctx, a.walletsPermissionRef, account.ID, orgRef, primitive.NilObjectID, model.ActionRead)
if err != nil {
a.logger.Warn("Failed to check chain wallet access permissions", zap.Error(err), zap.String(a.oph.Name(), orgRef.Hex()))
return response.Auto(a.logger, a.Name(), err)
}
if !res {
a.logger.Debug("Access denied when listing organization wallets", zap.String(a.oph.Name(), orgRef.Hex()))
return response.AccessDenied(a.logger, a.Name(), "wallets read permission denied")
}
if a.chainGateway == nil {
return response.Internal(a.logger, mservice.ChainGateway, errors.New("chain gateway client is not configured"))
}
req := &gatewayv1.ListManagedWalletsRequest{
OrganizationRef: orgRef.Hex(),
}
if owner := strings.TrimSpace(r.URL.Query().Get("owner_ref")); owner != "" {
req.OwnerRef = owner
}
resp, err := a.chainGateway.ListManagedWallets(ctx, req)
if err != nil {
a.logger.Warn("Failed to list managed wallets", zap.Error(err), zap.String("organization_ref", orgRef.Hex()))
return response.Auto(a.logger, mservice.ChainGateway, err)
}
return sresponse.Wallets(a.logger, resp, token)
}

View File

@@ -0,0 +1,115 @@
package walletapiimp
import (
"context"
"fmt"
"os"
"strings"
"time"
chaingatewayclient "github.com/tech/sendico/chain/gateway/client"
api "github.com/tech/sendico/pkg/api/http"
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
eapi "github.com/tech/sendico/server/interface/api"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
type WalletAPI struct {
logger mlogger.Logger
chainGateway chainWalletClient
enf auth.Enforcer
oph mutil.ParamHelper
wph mutil.ParamHelper
walletsPermissionRef primitive.ObjectID
balancesPermissionRef primitive.ObjectID
}
type chainWalletClient interface {
ListManagedWallets(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) (*gatewayv1.ListManagedWalletsResponse, error)
GetWalletBalance(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) (*gatewayv1.GetWalletBalanceResponse, error)
Close() error
}
func (a *WalletAPI) Name() mservice.Type { return mservice.ChainWallets }
func (a *WalletAPI) Finish(ctx context.Context) error {
if a.chainGateway != nil {
if err := a.chainGateway.Close(); err != nil {
a.logger.Warn("Failed to close chain gateway client", zap.Error(err))
}
}
return nil
}
func CreateAPI(apiCtx eapi.API) (*WalletAPI, error) {
p := &WalletAPI{
logger: apiCtx.Logger().Named(mservice.Wallets),
enf: apiCtx.Permissions().Enforcer(),
oph: mutil.CreatePH(mservice.Organizations),
wph: mutil.CreatePH(mservice.Wallets),
}
walletsPolicy, err := apiCtx.Permissions().GetPolicyDescription(context.Background(), mservice.ChainWallets)
if err != nil {
p.logger.Warn("Failed to fetch chain wallets permission policy description", zap.Error(err))
return nil, err
}
p.walletsPermissionRef = walletsPolicy.ID
balancesPolicy, err := apiCtx.Permissions().GetPolicyDescription(context.Background(), mservice.ChainWalletBalances)
if err != nil {
p.logger.Warn("Failed to fetch chain wallet balances permission policy description", zap.Error(err))
return nil, err
}
p.balancesPermissionRef = balancesPolicy.ID
cfg := apiCtx.Config()
if cfg == nil {
p.logger.Error("Failed to fetch service configuration")
return nil, merrors.InvalidArgument("No configuration provided")
}
if err := p.initChainGateway(cfg.ChainGateway); err != nil {
p.logger.Error("Failed to initialize chain gateway client", zap.Error(err))
return nil, err
}
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/"), api.Get, p.listWallets)
apiCtx.Register().AccountHandler(p.Name(), p.wph.AddRef(p.oph.AddRef("/"))+"/balance", api.Get, p.getWalletBalance)
return p, nil
}
func (a *WalletAPI) initChainGateway(cfg *eapi.ChainGatewayConfig) error {
if cfg == nil {
return merrors.InvalidArgument("chain gateway configuration is not provided")
}
cfg.Address = strings.TrimSpace(cfg.Address)
if cfg.Address == "" {
cfg.Address = strings.TrimSpace(os.Getenv(cfg.AddressEnv))
}
if cfg.Address == "" {
return merrors.InvalidArgument(fmt.Sprintf("chain gateway address is not specified and address env %s is empty", cfg.AddressEnv))
}
clientCfg := chaingatewayclient.Config{
Address: cfg.Address,
DialTimeout: time.Duration(cfg.DialTimeoutSeconds) * time.Second,
CallTimeout: time.Duration(cfg.CallTimeoutSeconds) * time.Second,
Insecure: cfg.Insecure,
}
client, err := chaingatewayclient.New(context.Background(), clientCfg)
if err != nil {
return err
}
a.chainGateway = client
return nil
}