Compare commits
3 Commits
8e1d4bef59
...
48ccbb1c82
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48ccbb1c82 | ||
|
|
68f0a1048f | ||
|
|
be913bf96c |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@
|
||||
*.pb.gw.go
|
||||
pubspec.lock
|
||||
.DS_Store
|
||||
update_dep.sh
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
|
||||
// Client exposes typed helpers around the ledger gRPC API.
|
||||
type Client interface {
|
||||
CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error)
|
||||
ListAccounts(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error)
|
||||
PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error)
|
||||
PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error)
|
||||
TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error)
|
||||
@@ -29,6 +31,8 @@ type Client interface {
|
||||
}
|
||||
|
||||
type grpcLedgerClient interface {
|
||||
CreateAccount(ctx context.Context, in *ledgerv1.CreateAccountRequest, opts ...grpc.CallOption) (*ledgerv1.CreateAccountResponse, error)
|
||||
ListAccounts(ctx context.Context, in *ledgerv1.ListAccountsRequest, opts ...grpc.CallOption) (*ledgerv1.ListAccountsResponse, error)
|
||||
PostCreditWithCharges(ctx context.Context, in *ledgerv1.PostCreditRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
|
||||
PostDebitWithCharges(ctx context.Context, in *ledgerv1.PostDebitRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
|
||||
TransferInternal(ctx context.Context, in *ledgerv1.TransferRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
|
||||
@@ -91,6 +95,18 @@ func (c *ledgerClient) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ledgerClient) CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error) {
|
||||
ctx, cancel := c.callContext(ctx)
|
||||
defer cancel()
|
||||
return c.client.CreateAccount(ctx, req)
|
||||
}
|
||||
|
||||
func (c *ledgerClient) ListAccounts(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error) {
|
||||
ctx, cancel := c.callContext(ctx)
|
||||
defer cancel()
|
||||
return c.client.ListAccounts(ctx, req)
|
||||
}
|
||||
|
||||
func (c *ledgerClient) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
|
||||
ctx, cancel := c.callContext(ctx)
|
||||
defer cancel()
|
||||
|
||||
47
api/ledger/internal/service/ledger/list_accounts.go
Normal file
47
api/ledger/internal/service/ledger/list_accounts.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package ledger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (s *Service) listAccountsResponder(_ context.Context, req *ledgerv1.ListAccountsRequest) gsresponse.Responder[ledgerv1.ListAccountsResponse] {
|
||||
return func(ctx context.Context) (*ledgerv1.ListAccountsResponse, error) {
|
||||
if s.storage == nil {
|
||||
return nil, errStorageNotInitialized
|
||||
}
|
||||
if req == nil {
|
||||
return nil, merrors.InvalidArgument("request is required")
|
||||
}
|
||||
|
||||
orgRefStr := strings.TrimSpace(req.GetOrganizationRef())
|
||||
if orgRefStr == "" {
|
||||
return nil, merrors.InvalidArgument("organization_ref is required")
|
||||
}
|
||||
orgRef, err := parseObjectID(orgRefStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// No pagination requested; return all accounts for the organization.
|
||||
accounts, err := s.storage.Accounts().ListByOrganization(ctx, orgRef, 0, 0)
|
||||
if err != nil {
|
||||
s.logger.Warn("failed to list ledger accounts", zap.Error(err), zap.String("organizationRef", orgRef.Hex()))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := &ledgerv1.ListAccountsResponse{
|
||||
Accounts: make([]*ledgerv1.LedgerAccount, 0, len(accounts)),
|
||||
}
|
||||
for _, acc := range accounts {
|
||||
resp.Accounts = append(resp.Accounts, toProtoAccount(acc))
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,12 @@ func (s *Service) Register(router routers.GRPC) error {
|
||||
})
|
||||
}
|
||||
|
||||
// ListAccounts lists ledger accounts for an organization.
|
||||
func (s *Service) ListAccounts(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error) {
|
||||
responder := s.listAccountsResponder(ctx, req)
|
||||
return responder(ctx)
|
||||
}
|
||||
|
||||
// CreateAccount provisions a new ledger account scoped to an organization.
|
||||
func (s *Service) CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error) {
|
||||
responder := s.createAccountResponder(ctx, req)
|
||||
|
||||
@@ -40,6 +40,7 @@ const (
|
||||
Roles Type = "roles" // Represents roles in access control
|
||||
Storage Type = "storage" // Represents statuses of tasks or projects
|
||||
Tenants Type = "tenants" // Represents tenants managed in the system
|
||||
Wallets Type = "wallets" // Represents workflows for tasks or projects
|
||||
Workflows Type = "workflows" // Represents workflows for tasks or projects
|
||||
)
|
||||
|
||||
|
||||
@@ -77,6 +77,9 @@ service LedgerService {
|
||||
rpc GetBalance (GetBalanceRequest) returns (BalanceResponse);
|
||||
rpc GetJournalEntry (GetEntryRequest) returns (JournalEntryResponse);
|
||||
rpc GetStatement (GetStatementRequest) returns (StatementResponse);
|
||||
|
||||
// Lists ledger accounts for an organization.
|
||||
rpc ListAccounts (ListAccountsRequest) returns (ListAccountsResponse);
|
||||
}
|
||||
|
||||
message CreateAccountRequest {
|
||||
@@ -192,3 +195,11 @@ message StatementResponse {
|
||||
repeated JournalEntryResponse entries = 1;
|
||||
string next_cursor = 2;
|
||||
}
|
||||
|
||||
message ListAccountsRequest {
|
||||
string organization_ref = 1;
|
||||
}
|
||||
|
||||
message ListAccountsResponse {
|
||||
repeated LedgerAccount accounts = 1;
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
106
api/server/interface/api/sresponse/ledger.go
Normal file
106
api/server/interface/api/sresponse/ledger.go
Normal 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(),
|
||||
}
|
||||
}
|
||||
132
api/server/interface/api/sresponse/wallet.go
Normal file
132
api/server/interface/api/sresponse/wallet.go
Normal 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)
|
||||
}
|
||||
11
api/server/interface/services/ledger/ledger.go
Normal file
11
api/server/interface/services/ledger/ledger.go
Normal 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)
|
||||
}
|
||||
11
api/server/interface/services/wallet/wallet.go
Normal file
11
api/server/interface/services/wallet/wallet.go
Normal 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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
52
api/server/internal/server/ledgerapiimp/balance.go
Normal file
52
api/server/internal/server/ledgerapiimp/balance.go
Normal 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)
|
||||
}
|
||||
46
api/server/internal/server/ledgerapiimp/list.go
Normal file
46
api/server/internal/server/ledgerapiimp/list.go
Normal 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)
|
||||
}
|
||||
110
api/server/internal/server/ledgerapiimp/service.go
Normal file
110
api/server/internal/server/ledgerapiimp/service.go
Normal 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
|
||||
}
|
||||
55
api/server/internal/server/walletapiimp/balance.go
Normal file
55
api/server/internal/server/walletapiimp/balance.go
Normal 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)
|
||||
}
|
||||
52
api/server/internal/server/walletapiimp/list.go
Normal file
52
api/server/internal/server/walletapiimp/list.go
Normal 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)
|
||||
}
|
||||
115
api/server/internal/server/walletapiimp/service.go
Normal file
115
api/server/internal/server/walletapiimp/service.go
Normal 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
|
||||
}
|
||||
@@ -88,6 +88,7 @@ LEDGER_COMPOSE_PROJECT=sendico-ledger
|
||||
LEDGER_SERVICE_NAME=sendico_ledger
|
||||
LEDGER_GRPC_PORT=50052
|
||||
LEDGER_METRICS_PORT=9401
|
||||
LEDGER_ADDRESS=sendico_ledger:50520
|
||||
|
||||
# Ledger Mongo settings
|
||||
LEDGER_MONGO_HOST=sendico_db1
|
||||
|
||||
@@ -29,7 +29,8 @@ services:
|
||||
NATS_PORT: ${NATS_PORT}
|
||||
NATS_USER: ${NATS_USER}
|
||||
NATS_PASSWORD: ${NATS_PASSWORD}
|
||||
CHAIN_GATEWAY_ADDRESS: ${CHAIN_GATEWAY_ADDRESS}
|
||||
CHAIN_GATEWAY_ADDRESS: ${CHAIN_GATEWAY_SERVICE_NAME}:${CHAIN_GATEWAY_GRPC_PORT}
|
||||
LEDGER_ADDRESS: ${LEDGER_SERVICE_NAME}:${LEDGER_GRPC_PORT}
|
||||
MONGO_HOST: ${MONGO_HOST}
|
||||
MONGO_PORT: ${MONGO_PORT}
|
||||
MONGO_DATABASE: ${MONGO_DATABASE}
|
||||
|
||||
16
frontend/pshared/lib/api/responses/wallet_balance.dart
Normal file
16
frontend/pshared/lib/api/responses/wallet_balance.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'package:pshared/data/dto/wallet/balance.dart';
|
||||
|
||||
part 'wallet_balance.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class WalletBalanceResponse {
|
||||
final WalletBalanceDTO balance;
|
||||
|
||||
const WalletBalanceResponse({required this.balance});
|
||||
|
||||
factory WalletBalanceResponse.fromJson(Map<String, dynamic> json) => _$WalletBalanceResponseFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$WalletBalanceResponseToJson(this);
|
||||
}
|
||||
16
frontend/pshared/lib/api/responses/wallets.dart
Normal file
16
frontend/pshared/lib/api/responses/wallets.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'package:pshared/data/dto/wallet/wallet.dart';
|
||||
|
||||
part 'wallets.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class WalletsResponse {
|
||||
final List<WalletDTO> wallets;
|
||||
|
||||
const WalletsResponse({required this.wallets});
|
||||
|
||||
factory WalletsResponse.fromJson(Map<String, dynamic> json) => _$WalletsResponseFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$WalletsResponseToJson(this);
|
||||
}
|
||||
20
frontend/pshared/lib/data/dto/wallet/asset.dart
Normal file
20
frontend/pshared/lib/data/dto/wallet/asset.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'asset.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class WalletAssetDTO {
|
||||
final String chain;
|
||||
final String tokenSymbol;
|
||||
final String contractAddress;
|
||||
|
||||
const WalletAssetDTO({
|
||||
required this.chain,
|
||||
required this.tokenSymbol,
|
||||
required this.contractAddress,
|
||||
});
|
||||
|
||||
factory WalletAssetDTO.fromJson(Map<String, dynamic> json) => _$WalletAssetDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$WalletAssetDTOToJson(this);
|
||||
}
|
||||
24
frontend/pshared/lib/data/dto/wallet/balance.dart
Normal file
24
frontend/pshared/lib/data/dto/wallet/balance.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'package:pshared/data/dto/wallet/money.dart';
|
||||
|
||||
part 'balance.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class WalletBalanceDTO {
|
||||
final MoneyDTO? available;
|
||||
final MoneyDTO? pendingInbound;
|
||||
final MoneyDTO? pendingOutbound;
|
||||
final String? calculatedAt;
|
||||
|
||||
const WalletBalanceDTO({
|
||||
required this.available,
|
||||
required this.pendingInbound,
|
||||
required this.pendingOutbound,
|
||||
required this.calculatedAt,
|
||||
});
|
||||
|
||||
factory WalletBalanceDTO.fromJson(Map<String, dynamic> json) => _$WalletBalanceDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$WalletBalanceDTOToJson(this);
|
||||
}
|
||||
18
frontend/pshared/lib/data/dto/wallet/money.dart
Normal file
18
frontend/pshared/lib/data/dto/wallet/money.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'money.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class MoneyDTO {
|
||||
final String amount;
|
||||
final String currency;
|
||||
|
||||
const MoneyDTO({
|
||||
required this.amount,
|
||||
required this.currency,
|
||||
});
|
||||
|
||||
factory MoneyDTO.fromJson(Map<String, dynamic> json) => _$MoneyDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$MoneyDTOToJson(this);
|
||||
}
|
||||
34
frontend/pshared/lib/data/dto/wallet/wallet.dart
Normal file
34
frontend/pshared/lib/data/dto/wallet/wallet.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'package:pshared/data/dto/wallet/asset.dart';
|
||||
|
||||
part 'wallet.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class WalletDTO {
|
||||
final String walletRef;
|
||||
final String organizationRef;
|
||||
final String ownerRef;
|
||||
final WalletAssetDTO asset;
|
||||
final String depositAddress;
|
||||
final String status;
|
||||
final Map<String, String>? metadata;
|
||||
final String? createdAt;
|
||||
final String? updatedAt;
|
||||
|
||||
const WalletDTO({
|
||||
required this.walletRef,
|
||||
required this.organizationRef,
|
||||
required this.ownerRef,
|
||||
required this.asset,
|
||||
required this.depositAddress,
|
||||
required this.status,
|
||||
this.metadata,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
factory WalletDTO.fromJson(Map<String, dynamic> json) => _$WalletDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$WalletDTOToJson(this);
|
||||
}
|
||||
15
frontend/pshared/lib/data/mapper/wallet/balance.dart
Normal file
15
frontend/pshared/lib/data/mapper/wallet/balance.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:pshared/data/dto/wallet/balance.dart';
|
||||
import 'package:pshared/data/mapper/wallet/money.dart';
|
||||
import 'package:pshared/models/wallet/balance.dart';
|
||||
|
||||
|
||||
extension WalletBalanceDTOMapper on WalletBalanceDTO {
|
||||
WalletBalance toDomain() => WalletBalance(
|
||||
available: available?.toDomain(),
|
||||
pendingInbound: pendingInbound?.toDomain(),
|
||||
pendingOutbound: pendingOutbound?.toDomain(),
|
||||
calculatedAt: (calculatedAt == null || calculatedAt!.isEmpty)
|
||||
? null
|
||||
: DateTime.tryParse(calculatedAt!),
|
||||
);
|
||||
}
|
||||
10
frontend/pshared/lib/data/mapper/wallet/money.dart
Normal file
10
frontend/pshared/lib/data/mapper/wallet/money.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
import 'package:pshared/data/dto/wallet/money.dart';
|
||||
import 'package:pshared/models/wallet/money.dart';
|
||||
|
||||
|
||||
extension MoneyDTOMapper on MoneyDTO {
|
||||
WalletMoney toDomain() => WalletMoney(
|
||||
amount: amount,
|
||||
currency: currency,
|
||||
);
|
||||
}
|
||||
14
frontend/pshared/lib/data/mapper/wallet/response.dart
Normal file
14
frontend/pshared/lib/data/mapper/wallet/response.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:pshared/api/responses/wallet_balance.dart';
|
||||
import 'package:pshared/api/responses/wallets.dart';
|
||||
import 'package:pshared/data/mapper/wallet/balance.dart';
|
||||
import 'package:pshared/data/mapper/wallet/wallet.dart';
|
||||
import 'package:pshared/models/wallet/balance.dart';
|
||||
import 'package:pshared/models/wallet/wallet.dart';
|
||||
|
||||
extension WalletsResponseMapper on WalletsResponse {
|
||||
List<WalletModel> toDomain() => wallets.map((w) => w.toDomain()).toList();
|
||||
}
|
||||
|
||||
extension WalletBalanceResponseMapper on WalletBalanceResponse {
|
||||
WalletBalance toDomain() => balance.toDomain();
|
||||
}
|
||||
26
frontend/pshared/lib/data/mapper/wallet/wallet.dart
Normal file
26
frontend/pshared/lib/data/mapper/wallet/wallet.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:pshared/data/dto/wallet/balance.dart';
|
||||
import 'package:pshared/data/dto/wallet/wallet.dart';
|
||||
import 'package:pshared/data/mapper/wallet/balance.dart';
|
||||
import 'package:pshared/data/mapper/wallet/money.dart';
|
||||
import 'package:pshared/models/wallet/wallet.dart';
|
||||
|
||||
|
||||
extension WalletDTOMapper on WalletDTO {
|
||||
WalletModel toDomain({WalletBalanceDTO? balance}) => WalletModel(
|
||||
walletRef: walletRef,
|
||||
organizationRef: organizationRef,
|
||||
ownerRef: ownerRef,
|
||||
asset: WalletAsset(
|
||||
chain: asset.chain,
|
||||
tokenSymbol: asset.tokenSymbol,
|
||||
contractAddress: asset.contractAddress,
|
||||
),
|
||||
depositAddress: depositAddress,
|
||||
status: status,
|
||||
metadata: metadata,
|
||||
createdAt: (createdAt == null || createdAt!.isEmpty) ? null : DateTime.tryParse(createdAt!),
|
||||
updatedAt: (updatedAt == null || updatedAt!.isEmpty) ? null : DateTime.tryParse(updatedAt!),
|
||||
balance: balance?.toDomain(),
|
||||
availableMoney: balance?.available?.toDomain(),
|
||||
);
|
||||
}
|
||||
16
frontend/pshared/lib/models/wallet/balance.dart
Normal file
16
frontend/pshared/lib/models/wallet/balance.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:pshared/models/wallet/money.dart';
|
||||
|
||||
|
||||
class WalletBalance {
|
||||
final WalletMoney? available;
|
||||
final WalletMoney? pendingInbound;
|
||||
final WalletMoney? pendingOutbound;
|
||||
final DateTime? calculatedAt;
|
||||
|
||||
const WalletBalance({
|
||||
required this.available,
|
||||
required this.pendingInbound,
|
||||
required this.pendingOutbound,
|
||||
required this.calculatedAt,
|
||||
});
|
||||
}
|
||||
9
frontend/pshared/lib/models/wallet/money.dart
Normal file
9
frontend/pshared/lib/models/wallet/money.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
class WalletMoney {
|
||||
final String amount;
|
||||
final String currency;
|
||||
|
||||
const WalletMoney({
|
||||
required this.amount,
|
||||
required this.currency,
|
||||
});
|
||||
}
|
||||
62
frontend/pshared/lib/models/wallet/wallet.dart
Normal file
62
frontend/pshared/lib/models/wallet/wallet.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
import 'package:pshared/models/wallet/balance.dart';
|
||||
import 'package:pshared/models/wallet/money.dart';
|
||||
|
||||
|
||||
class WalletAsset {
|
||||
final String chain;
|
||||
final String tokenSymbol;
|
||||
final String contractAddress;
|
||||
|
||||
const WalletAsset({
|
||||
required this.chain,
|
||||
required this.tokenSymbol,
|
||||
required this.contractAddress,
|
||||
});
|
||||
}
|
||||
|
||||
class WalletModel {
|
||||
final String walletRef;
|
||||
final String organizationRef;
|
||||
final String ownerRef;
|
||||
final WalletAsset asset;
|
||||
final String depositAddress;
|
||||
final String status;
|
||||
final Map<String, String>? metadata;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final WalletBalance? balance;
|
||||
final WalletMoney? availableMoney;
|
||||
|
||||
const WalletModel({
|
||||
required this.walletRef,
|
||||
required this.organizationRef,
|
||||
required this.ownerRef,
|
||||
required this.asset,
|
||||
required this.depositAddress,
|
||||
required this.status,
|
||||
this.metadata,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.balance,
|
||||
this.availableMoney,
|
||||
});
|
||||
|
||||
WalletModel copyWith({
|
||||
WalletBalance? balance,
|
||||
WalletMoney? availableMoney,
|
||||
}) {
|
||||
return WalletModel(
|
||||
walletRef: walletRef,
|
||||
organizationRef: organizationRef,
|
||||
ownerRef: ownerRef,
|
||||
asset: asset,
|
||||
depositAddress: depositAddress,
|
||||
status: status,
|
||||
metadata: metadata,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
balance: balance ?? this.balance,
|
||||
availableMoney: availableMoney ?? this.availableMoney,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ class AccountService {
|
||||
|
||||
static Future<void> logout() async {
|
||||
_logger.fine('Logging out');
|
||||
await AuthorizationService.logout();
|
||||
return AuthorizationService.logout();
|
||||
}
|
||||
|
||||
static Future<Account> _getAccount(Future<Map<String, dynamic>> future) async {
|
||||
|
||||
@@ -7,6 +7,7 @@ class Services {
|
||||
static const String organization = 'organizations';
|
||||
static const String permission = 'permissions';
|
||||
static const String storage = 'storage';
|
||||
static const String chainWallets = 'chain_wallets';
|
||||
|
||||
static const String amplitude = 'amplitude';
|
||||
static const String clients = 'clients';
|
||||
|
||||
31
frontend/pshared/lib/service/wallet.dart
Normal file
31
frontend/pshared/lib/service/wallet.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:pshared/api/responses/wallet_balance.dart';
|
||||
import 'package:pshared/api/responses/wallets.dart';
|
||||
import 'package:pshared/data/mapper/wallet/response.dart';
|
||||
import 'package:pshared/models/wallet/balance.dart';
|
||||
import 'package:pshared/models/wallet/wallet.dart';
|
||||
import 'package:pshared/service/authorization/service.dart';
|
||||
import 'package:pshared/service/services.dart';
|
||||
|
||||
|
||||
class WalletService {
|
||||
static const String _objectType = Services.chainWallets;
|
||||
|
||||
static Future<List<WalletModel>> list(String organizationRef) async {
|
||||
final json = await AuthorizationService.getGETResponse(
|
||||
_objectType,
|
||||
'/$organizationRef',
|
||||
);
|
||||
return WalletsResponse.fromJson(json).toDomain();
|
||||
}
|
||||
|
||||
static Future<WalletBalance> getBalance({
|
||||
required String organizationRef,
|
||||
required String walletRef,
|
||||
}) async {
|
||||
final json = await AuthorizationService.getGETResponse(
|
||||
_objectType,
|
||||
'/$organizationRef/$walletRef/balance',
|
||||
);
|
||||
return WalletBalanceResponse.fromJson(json).toDomain();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:pshared/provider/organizations.dart';
|
||||
|
||||
import 'package:pweb/app/router/pages.dart';
|
||||
import 'package:pweb/app/router/page_params.dart';
|
||||
import 'package:pweb/pages/2fa/page.dart';
|
||||
@@ -32,7 +36,11 @@ GoRouter createRouter() => GoRouter(
|
||||
name: Pages.sfactor.name,
|
||||
path: routerPage(Pages.sfactor),
|
||||
builder: (context, _) => TwoFactorCodePage(
|
||||
onVerificationSuccess: () => context.goNamed(Pages.dashboard.name),
|
||||
onVerificationSuccess: () {
|
||||
// trigger organization load
|
||||
context.read<OrganizationsProvider>().load();
|
||||
context.goNamed(Pages.dashboard.name);
|
||||
},
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
|
||||
26
frontend/pweb/lib/data/mappers/wallet_ui.dart
Normal file
26
frontend/pweb/lib/data/mappers/wallet_ui.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:pshared/models/wallet/wallet.dart' as domain;
|
||||
|
||||
import 'package:pweb/models/currency.dart';
|
||||
import 'package:pweb/models/wallet.dart';
|
||||
|
||||
|
||||
extension WalletUiMapper on domain.WalletModel {
|
||||
Wallet toUi() {
|
||||
final amountStr = availableMoney?.amount ?? balance?.available?.amount ?? '0';
|
||||
final currencyStr = availableMoney?.currency ?? balance?.available?.currency ?? Currency.usd.toString().toUpperCase();
|
||||
final parsedAmount = double.tryParse(amountStr) ?? 0;
|
||||
final currency = Currency.values.firstWhere(
|
||||
(c) => c.name.toUpperCase() == currencyStr.toUpperCase(),
|
||||
orElse: () => Currency.usd,
|
||||
);
|
||||
return Wallet(
|
||||
id: walletRef,
|
||||
walletUserID: walletRef,
|
||||
name: metadata?['name'] ?? walletRef,
|
||||
balance: parsedAmount,
|
||||
currency: currency,
|
||||
isHidden: true,
|
||||
calculatedAt: balance?.calculatedAt ?? DateTime.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -72,8 +72,9 @@ void main() async {
|
||||
ChangeNotifierProvider(
|
||||
create: (_) => PaymentMethodsProvider(service: MockPaymentMethodsService())..loadMethods(),
|
||||
),
|
||||
ChangeNotifierProvider(
|
||||
create: (_) => WalletsProvider(MockWalletsService())..loadData(),
|
||||
ChangeNotifierProxyProvider<OrganizationsProvider, WalletsProvider>(
|
||||
create: (_) => WalletsProvider(ApiWalletsService()),
|
||||
update: (context, organizations, provider) => provider!..update(organizations),
|
||||
),
|
||||
ChangeNotifierProvider(
|
||||
create: (_) => WalletTransactionsProvider(MockWalletTransactionsService())..load(),
|
||||
|
||||
@@ -8,6 +8,7 @@ class Wallet {
|
||||
final double balance;
|
||||
final Currency currency;
|
||||
final bool isHidden;
|
||||
final DateTime calculatedAt;
|
||||
|
||||
Wallet({
|
||||
required this.id,
|
||||
@@ -15,6 +16,7 @@ class Wallet {
|
||||
required this.name,
|
||||
required this.balance,
|
||||
required this.currency,
|
||||
required this.calculatedAt,
|
||||
this.isHidden = true,
|
||||
});
|
||||
|
||||
@@ -25,14 +27,13 @@ class Wallet {
|
||||
Currency? currency,
|
||||
String? walletUserID,
|
||||
bool? isHidden,
|
||||
}) {
|
||||
return Wallet(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
balance: balance ?? this.balance,
|
||||
currency: currency ?? this.currency,
|
||||
walletUserID: walletUserID ?? this.walletUserID,
|
||||
isHidden: isHidden ?? this.isHidden,
|
||||
);
|
||||
}
|
||||
}) => Wallet(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
balance: balance ?? this.balance,
|
||||
currency: currency ?? this.currency,
|
||||
walletUserID: walletUserID ?? this.walletUserID,
|
||||
isHidden: isHidden ?? this.isHidden,
|
||||
calculatedAt: calculatedAt,
|
||||
);
|
||||
}
|
||||
@@ -44,6 +44,7 @@ class _LoginFormState extends State<LoginForm> {
|
||||
locale: context.read<LocaleProvider>().locale.languageCode,
|
||||
);
|
||||
if (outcome.isPending) {
|
||||
// TODO: fix context usage
|
||||
navigateAndReplace(context, Pages.sfactor);
|
||||
} else {
|
||||
onLogin();
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pweb/providers/wallets.dart';
|
||||
import 'package:pweb/widgets/error/snackbar.dart';
|
||||
|
||||
|
||||
class WalletEditHeader extends StatefulWidget {
|
||||
@@ -85,10 +86,11 @@ class _WalletEditHeaderState extends State<WalletEditHeader> {
|
||||
icon: const Icon(Icons.check),
|
||||
color: theme.colorScheme.primary,
|
||||
onPressed: () async {
|
||||
provider.updateName(wallet.id, _controller.text);
|
||||
await provider.updateWallet(wallet.copyWith(name: _controller.text));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Wallet name saved')),
|
||||
await executeActionWithNotification(
|
||||
context: context,
|
||||
action: () async => await provider.updateWallet(wallet.copyWith(name: _controller.text)),
|
||||
errorMessage: 'Failed to update wallet name',
|
||||
successMessage: 'Wallet name saved',
|
||||
);
|
||||
setState(() {
|
||||
_isEditing = false;
|
||||
|
||||
@@ -1,131 +1,103 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/provider/organizations.dart';
|
||||
import 'package:pshared/provider/resource.dart';
|
||||
import 'package:pshared/utils/exception.dart';
|
||||
|
||||
import 'package:pweb/models/wallet.dart';
|
||||
import 'package:pweb/services/wallets.dart';
|
||||
|
||||
|
||||
class WalletsProvider with ChangeNotifier {
|
||||
final WalletsService _service;
|
||||
late OrganizationsProvider _organizations;
|
||||
|
||||
WalletsProvider(this._service);
|
||||
|
||||
List<Wallet>? _wallets;
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
Wallet? _selectedWallet;
|
||||
final bool _isHidden = true;
|
||||
Resource<List<Wallet>> _resource = Resource(data: []);
|
||||
Resource<List<Wallet>> get resource => _resource;
|
||||
|
||||
List<Wallet>? get wallets => _wallets;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
List<Wallet> get wallets => _resource.data ?? [];
|
||||
bool get isLoading => _resource.isLoading;
|
||||
Exception? get error => _resource.error;
|
||||
|
||||
Wallet? _selectedWallet;
|
||||
Wallet? get selectedWallet => _selectedWallet;
|
||||
final bool _isHidden = true;
|
||||
bool get isHidden => _isHidden;
|
||||
|
||||
bool _isRefreshingBalances = false;
|
||||
bool get isRefreshingBalances => _isRefreshingBalances;
|
||||
|
||||
void update(OrganizationsProvider organizations) {
|
||||
_organizations = organizations;
|
||||
if (_organizations.isOrganizationSet) loadWalletsWithBalances();
|
||||
}
|
||||
|
||||
Future<Wallet> updateWallet(Wallet newWallet) {
|
||||
throw Exception('update wallet is not implemented');
|
||||
}
|
||||
|
||||
void selectWallet(Wallet wallet) {
|
||||
_selectedWallet = wallet;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> loadData() async {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
Future<void> loadWalletsWithBalances() async {
|
||||
_setResource(_resource.copyWith(isLoading: true, error: null));
|
||||
try {
|
||||
_wallets = await _service.getWallets();
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<Wallet?> getWalletById(String walletId) async {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final wallet = await _service.getWallet(walletId);
|
||||
return wallet;
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
return null;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void updateName(String walletRef, String newName) {
|
||||
final index = _wallets?.indexWhere((w) => w.id == walletRef);
|
||||
if (index != null && index >= 0) {
|
||||
_wallets![index] = _wallets![index].copyWith(name: newName);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void updateBalance(String walletRef, double newBalance) {
|
||||
final index = _wallets?.indexWhere((w) => w.id == walletRef);
|
||||
if (index != null && index >= 0) {
|
||||
_wallets![index] = _wallets![index].copyWith(balance: newBalance);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateWallet(Wallet wallet) async {
|
||||
try {
|
||||
await _service.updateWallet();
|
||||
final index = _wallets?.indexWhere((w) => w.id == wallet.id);
|
||||
if (index != null && index >= 0) {
|
||||
_wallets![index] = wallet;
|
||||
notifyListeners();
|
||||
final base = await _service.getWallets(_organizations.current.id);
|
||||
final withBalances = <Wallet>[];
|
||||
for (final wallet in base) {
|
||||
try {
|
||||
final balance = await _service.getBalance(_organizations.current.id, wallet.id);
|
||||
withBalances.add(wallet.copyWith(balance: balance));
|
||||
} catch (e) {
|
||||
_setResource(_resource.copyWith(error: toException(e)));
|
||||
withBalances.add(wallet);
|
||||
}
|
||||
}
|
||||
_setResource(Resource(data: withBalances, isLoading: false, error: _resource.error));
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refreshBalances() async {
|
||||
if (wallets.isEmpty) return;
|
||||
_isRefreshingBalances = true;
|
||||
notifyListeners();
|
||||
|
||||
Future<void> addWallet(Wallet wallet) async {
|
||||
try {
|
||||
final newWallet = await _service.createWallet(); // Pass the wallet parameter
|
||||
_wallets = [...?_wallets, ]; // Add the new wallet
|
||||
notifyListeners();
|
||||
final updated = <Wallet>[];
|
||||
for (final wallet in wallets) {
|
||||
final balance = await _service.getBalance(_organizations.current.id, wallet.id);
|
||||
updated.add(wallet.copyWith(balance: balance));
|
||||
}
|
||||
_setResource(_resource.copyWith(data: updated));
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteWallet(String walletId) async {
|
||||
try {
|
||||
await _service.deleteWallet(); // Pass the walletId parameter
|
||||
_wallets?.removeWhere((w) => w.id == walletId);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
_setResource(_resource.copyWith(error: toException(e)));
|
||||
} finally {
|
||||
_isRefreshingBalances = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void toggleVisibility(String walletId) {
|
||||
final index = _wallets?.indexWhere((w) => w.id == walletId);
|
||||
if (index != null && index >= 0) {
|
||||
final wallet = _wallets![index];
|
||||
_wallets![index] = wallet.copyWith(isHidden: !wallet.isHidden);
|
||||
|
||||
if (_selectedWallet?.id == walletId) {
|
||||
_selectedWallet = _wallets![index];
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
final index = wallets.indexWhere((w) => w.id == walletId);
|
||||
if (index < 0) return;
|
||||
final wallet = wallets[index];
|
||||
final updated = wallet.copyWith(isHidden: !wallet.isHidden);
|
||||
final next = List<Wallet>.from(wallets);
|
||||
next[index] = updated;
|
||||
_setResource(_resource.copyWith(data: next));
|
||||
if (_selectedWallet?.id == walletId) {
|
||||
_selectedWallet = updated;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _setResource(Resource<List<Wallet>> newResource) {
|
||||
_resource = newResource;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,50 @@
|
||||
import 'package:pshared/service/wallet.dart' as shared_wallet_service;
|
||||
|
||||
import 'package:pweb/models/currency.dart';
|
||||
import 'package:pweb/models/wallet.dart';
|
||||
import 'package:pweb/data/mappers/wallet_ui.dart';
|
||||
|
||||
|
||||
abstract class WalletsService {
|
||||
Future<List<Wallet>> getWallets();
|
||||
Future<List<Wallet>> updateWallet();
|
||||
Future<List<Wallet>> createWallet();
|
||||
Future<List<Wallet>> deleteWallet();
|
||||
|
||||
Future<Wallet> getWallet(String walletRef);
|
||||
Future<List<Wallet>> getWallets(String organizationRef);
|
||||
Future<double> getBalance(String organizationRef, String walletRef);
|
||||
}
|
||||
|
||||
class MockWalletsService implements WalletsService {
|
||||
final List<Wallet> _wallets = [
|
||||
Wallet(id: '1124', walletUserID: 'WA-12345667', name: 'Main Wallet', balance: 10000000.0, currency: Currency.rub),
|
||||
Wallet(id: '2124', walletUserID: 'WA-76654321', name: 'Savings', balance: 2500.5, currency: Currency.usd),
|
||||
Wallet(id: '1124', walletUserID: 'WA-12345667', name: 'Main Wallet', balance: 10000000.0, currency: Currency.rub, calculatedAt: DateTime.now()),
|
||||
Wallet(id: '2124', walletUserID: 'WA-76654321', name: 'Savings', balance: 2500.5, currency: Currency.usd, calculatedAt: DateTime.now()),
|
||||
];
|
||||
|
||||
@override
|
||||
Future<List<Wallet>> getWallets() async {
|
||||
Future<List<Wallet>> getWallets(String _) async {
|
||||
return _wallets;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Wallet> getWallet(String walletId) async {
|
||||
return _wallets.firstWhere(
|
||||
(wallet) => wallet.id == walletId,
|
||||
Future<double> getBalance(String _, String walletRef) async {
|
||||
final wallet = _wallets.firstWhere(
|
||||
(w) => w.id == walletRef,
|
||||
orElse: () => throw Exception('Wallet not found'),
|
||||
);
|
||||
return wallet.balance;
|
||||
}
|
||||
}
|
||||
|
||||
class ApiWalletsService implements WalletsService {
|
||||
@override
|
||||
Future<List<Wallet>> getWallets(String organizationRef) async {
|
||||
final models = await shared_wallet_service.WalletService.list(organizationRef);
|
||||
return models.map((m) => m.toUi()).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Wallet>> updateWallet() async => [];
|
||||
|
||||
@override
|
||||
Future<List<Wallet>> createWallet() async => [];
|
||||
|
||||
@override
|
||||
Future<List<Wallet>> deleteWallet() async => [];
|
||||
}
|
||||
Future<double> getBalance(String organizationRef, String walletRef) async {
|
||||
final balance = await shared_wallet_service.WalletService.getBalance(
|
||||
organizationRef: organizationRef,
|
||||
walletRef: walletRef,
|
||||
);
|
||||
final amount = balance.available?.amount;
|
||||
return amount == null ? 0 : double.tryParse(amount) ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/utils/error_handler.dart';
|
||||
import 'package:pweb/utils/snackbar.dart';
|
||||
import 'package:pweb/widgets/error/content.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
@@ -52,13 +53,18 @@ Future<T?> executeActionWithNotification<T>({
|
||||
required BuildContext context,
|
||||
required Future<T> Function() action,
|
||||
required String errorMessage,
|
||||
String? successMessage,
|
||||
int delaySeconds = 3,
|
||||
}) async {
|
||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||
final localizations = AppLocalizations.of(context)!;
|
||||
|
||||
try {
|
||||
return await action();
|
||||
final res = await action();
|
||||
if (successMessage != null) {
|
||||
notifyUserX(scaffoldMessenger, successMessage, delaySeconds: delaySeconds);
|
||||
}
|
||||
return res;
|
||||
} catch (e) {
|
||||
// Report the error using your existing notifier.
|
||||
notifyUserOfErrorX(
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/provider/account.dart';
|
||||
|
||||
import 'package:pweb/pages/address_book/form/page.dart';
|
||||
import 'package:pweb/pages/address_book/page/page.dart';
|
||||
import 'package:pweb/pages/payment_methods/page.dart';
|
||||
@@ -9,9 +11,9 @@ import 'package:pweb/pages/payout_page/page.dart';
|
||||
import 'package:pweb/pages/payout_page/wallet/edit/page.dart';
|
||||
import 'package:pweb/pages/report/page.dart';
|
||||
import 'package:pweb/pages/settings/profile/page.dart';
|
||||
import 'package:pweb/pages/dashboard/dashboard.dart';
|
||||
import 'package:pweb/providers/page_selector.dart';
|
||||
import 'package:pweb/widgets/appbar/app_bar.dart';
|
||||
import 'package:pweb/pages/dashboard/dashboard.dart';
|
||||
import 'package:pweb/widgets/sidebar/destinations.dart';
|
||||
import 'package:pweb/widgets/sidebar/sidebar.dart';
|
||||
|
||||
@@ -19,9 +21,10 @@ import 'package:pweb/widgets/sidebar/sidebar.dart';
|
||||
class PageSelector extends StatelessWidget {
|
||||
const PageSelector({super.key});
|
||||
|
||||
void _logout(BuildContext context) => context.read<AccountProvider>().logout();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<PageSelectorProvider>();
|
||||
Widget build(BuildContext context) => Consumer<PageSelectorProvider>(builder:(context, provider, _) {
|
||||
|
||||
Widget content;
|
||||
switch (provider.selected) {
|
||||
@@ -87,7 +90,7 @@ class PageSelector extends StatelessWidget {
|
||||
appBar: PayoutAppBar(
|
||||
title: Text(provider.selected.localizedLabel(context)),
|
||||
onAddFundsPressed: () {},
|
||||
onLogout: () => debugPrint('Logout clicked'),
|
||||
onLogout: () => _logout(context),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.only(left: 200, top: 40, right: 200),
|
||||
@@ -98,12 +101,12 @@ class PageSelector extends StatelessWidget {
|
||||
PayoutSidebar(
|
||||
selected: provider.selected,
|
||||
onSelected: provider.selectPage,
|
||||
onLogout: () => debugPrint('Logout clicked'),
|
||||
onLogout: () => _logout(context),
|
||||
),
|
||||
Expanded(child: content),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import Foundation
|
||||
import amplitude_flutter
|
||||
import file_selector_macos
|
||||
import flutter_timezone
|
||||
import path_provider_foundation
|
||||
import share_plus
|
||||
import shared_preferences_foundation
|
||||
import sqflite_darwin
|
||||
@@ -17,6 +18,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AmplitudeFlutterPlugin.register(with: registry.registrar(forPlugin: "AmplitudeFlutterPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
|
||||
Reference in New Issue
Block a user