From bc46eccbe00eb7d9d459f84b72262de726fb156a Mon Sep 17 00:00:00 2001 From: Stephan D Date: Fri, 30 Jan 2026 15:42:50 +0100 Subject: [PATCH] tron driver removed --- api/fx/storage/go.mod | 2 +- api/gateway/chain/.air.toml | 64 +++-- api/gateway/chain/.gitignore | 1 + api/gateway/chain/client/client.go | 25 +- api/gateway/chain/client/client_test.go | 5 +- api/gateway/chain/client/rail_gateway.go | 23 +- api/gateway/chain/config.dev.yml | 75 ++++++ api/gateway/chain/config.yml | 22 +- api/gateway/chain/go.mod | 6 +- api/gateway/chain/go.sum | 8 +- .../internal/server/internal/serverimp.go | 12 +- .../gateway/commands/transfer/gas_topup.go | 52 ++-- .../service/gateway/commands/wallet/get.go | 6 +- .../service/gateway/commands/wallet/list.go | 5 +- .../internal/service/gateway/connector.go | 2 +- .../service/gateway/driver/evm/evm.go | 41 +-- .../service/gateway/driver/tron/address.go | 193 -------------- .../service/gateway/driver/tron/driver.go | 245 ------------------ .../gateway/driver/tron/driver_test.go | 33 --- .../service/gateway/driver/tron/gas_topup.go | 143 ---------- .../gateway/driver/tron/gas_topup_test.go | 147 ----------- .../service/gateway/drivers/registry.go | 3 +- .../internal/service/gateway/service_test.go | 7 +- api/gateway/chain/storage/model/wallet.go | 14 +- .../chain/storage/mongo/store/wallets.go | 7 +- 25 files changed, 247 insertions(+), 894 deletions(-) create mode 100644 api/gateway/chain/config.dev.yml delete mode 100644 api/gateway/chain/internal/service/gateway/driver/tron/address.go delete mode 100644 api/gateway/chain/internal/service/gateway/driver/tron/driver.go delete mode 100644 api/gateway/chain/internal/service/gateway/driver/tron/driver_test.go delete mode 100644 api/gateway/chain/internal/service/gateway/driver/tron/gas_topup.go delete mode 100644 api/gateway/chain/internal/service/gateway/driver/tron/gas_topup_test.go diff --git a/api/fx/storage/go.mod b/api/fx/storage/go.mod index f35987f6..aeb21b40 100644 --- a/api/fx/storage/go.mod +++ b/api/fx/storage/go.mod @@ -1,6 +1,6 @@ module github.com/tech/sendico/fx/storage -go 1.25.3 +go 1.25.6 replace github.com/tech/sendico/pkg => ../../pkg diff --git a/api/gateway/chain/.air.toml b/api/gateway/chain/.air.toml index 5a417ef6..16f8c34b 100644 --- a/api/gateway/chain/.air.toml +++ b/api/gateway/chain/.air.toml @@ -1,32 +1,46 @@ -# Config file for Air in TOML format - -root = "./../.." +root = "." +testdata_dir = "testdata" tmp_dir = "tmp" [build] -cmd = "go build -o app -ldflags \"-X 'github.com/tech/sendico/gateway/chain/internal/appversion.BuildUser=$(whoami)' -X 'github.com/tech/sendico/gateway/chain/internal/appversion.Version=$APP_V' -X 'github.com/tech/sendico/gateway/chain/internal/appversion.Branch=$BUILD_BRANCH' -X 'github.com/tech/sendico/gateway/chain/internal/appversion.Revision=$GIT_REV' -X 'github.com/tech/sendico/gateway/chain/internal/appversion.BuildDate=$(date)'\"" -bin = "./app" -full_bin = "./app --debug --config.file=config.yml" -include_ext = ["go", "yaml", "yml"] -exclude_dir = ["gateway/chain/tmp", "pkg/.git", "gateway/chain/env"] -exclude_regex = ["_test\\.go"] -exclude_unchanged = true -follow_symlink = true -log = "air.log" -delay = 0 -stop_on_error = true -send_interrupt = true -kill_delay = 500 -args_bin = [] - -[log] -time = false + args_bin = [] + entrypoint = "./tmp/main" + cmd = "go build -o ./tmp/main ." + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = [] + exclude_regex = ["_test.go", "_templ.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false [color] -main = "magenta" -watcher = "cyan" -build = "yellow" -runner = "green" + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false [misc] -clean_on_exit = true + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/api/gateway/chain/.gitignore b/api/gateway/chain/.gitignore index c62beb6b..436d3e5e 100644 --- a/api/gateway/chain/.gitignore +++ b/api/gateway/chain/.gitignore @@ -1,3 +1,4 @@ internal/generated .gocache app +tmp diff --git a/api/gateway/chain/client/client.go b/api/gateway/chain/client/client.go index 5d23b85f..94d8507f 100644 --- a/api/gateway/chain/client/client.go +++ b/api/gateway/chain/client/client.go @@ -9,6 +9,7 @@ import ( chainasset "github.com/tech/sendico/pkg/chain" "github.com/tech/sendico/pkg/merrors" + pmodel "github.com/tech/sendico/pkg/model" describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1" @@ -18,6 +19,7 @@ import ( "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/wrapperspb" ) const chainConnectorID = "chain" @@ -149,17 +151,17 @@ func (c *chainGatewayClient) ListManagedWallets(ctx context.Context, req *chainv ctx, cancel := c.callContext(ctx) defer cancel() assetString := "" - ownerRef := "" orgRef := "" var page *paginationv1.CursorPageRequest + var ownerRefFilter *wrapperspb.StringValue if req != nil { assetString = chainasset.AssetString(req.GetAsset()) - ownerRef = strings.TrimSpace(req.GetOwnerRef()) orgRef = strings.TrimSpace(req.GetOrganizationRef()) + ownerRefFilter = req.GetOwnerRefFilter() page = req.GetPage() } resp, err := c.client.ListAccounts(ctx, &connectorv1.ListAccountsRequest{ - OwnerRef: ownerRef, + OwnerRefFilter: ownerRefFilter, OrganizationRef: orgRef, Kind: connectorv1.AccountKind_CHAIN_MANAGED_WALLET, Asset: assetString, @@ -448,6 +450,7 @@ func operationFromTransfer(req *chainv1.SubmitTransferRequest) (*connectorv1.Ope return nil, err } op.To = to + setOperationRolesFromMetadata(op, req.GetMetadata()) return op, nil } @@ -465,6 +468,22 @@ func destinationToParty(dest *chainv1.TransferDestination) (*connectorv1.Operati } } +func setOperationRolesFromMetadata(op *connectorv1.Operation, metadata map[string]string) { + if op == nil || len(metadata) == 0 { + return + } + if raw := strings.TrimSpace(metadata[pmodel.MetadataKeyFromRole]); raw != "" { + if role, ok := pmodel.Parse(raw); ok && role != "" { + op.FromRole = pmodel.ToProto(role) + } + } + if raw := strings.TrimSpace(metadata[pmodel.MetadataKeyToRole]); raw != "" { + if role, ok := pmodel.Parse(raw); ok && role != "" { + op.ToRole = pmodel.ToProto(role) + } + } +} + func transferFromReceipt(req *chainv1.SubmitTransferRequest, receipt *connectorv1.OperationReceipt) *chainv1.Transfer { transfer := &chainv1.Transfer{} if req != nil { diff --git a/api/gateway/chain/client/client_test.go b/api/gateway/chain/client/client_test.go index f86634df..f32d0d95 100644 --- a/api/gateway/chain/client/client_test.go +++ b/api/gateway/chain/client/client_test.go @@ -8,6 +8,7 @@ import ( connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1" chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/wrapperspb" ) type stubConnectorClient struct { @@ -53,7 +54,7 @@ func TestListManagedWallets_ForwardsOrganizationRef(t *testing.T) { _, err := client.ListManagedWallets(context.Background(), &chainv1.ListManagedWalletsRequest{ OrganizationRef: "org-1", - OwnerRef: "owner-1", + OwnerRefFilter: wrapperspb.String("owner-1"), Asset: &chainv1.Asset{ Chain: chainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET, TokenSymbol: "USDC", @@ -62,6 +63,6 @@ func TestListManagedWallets_ForwardsOrganizationRef(t *testing.T) { require.NoError(t, err) require.NotNil(t, stub.listReq) require.Equal(t, "org-1", stub.listReq.GetOrganizationRef()) - require.Equal(t, "owner-1", stub.listReq.GetOwnerRef()) + require.Equal(t, "owner-1", stub.listReq.GetOwnerRefFilter().GetValue()) require.Equal(t, connectorv1.AccountKind_CHAIN_MANAGED_WALLET, stub.listReq.GetKind()) } diff --git a/api/gateway/chain/client/rail_gateway.go b/api/gateway/chain/client/rail_gateway.go index a3ead4fe..3f128c83 100644 --- a/api/gateway/chain/client/rail_gateway.go +++ b/api/gateway/chain/client/rail_gateway.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/tech/sendico/pkg/merrors" + pmodel "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/payments/rail" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" @@ -106,7 +107,7 @@ func (g *chainRailGateway) Send(ctx context.Context, req rail.TransferRequest) ( Amount: amountValue, }, Fees: fees, - Metadata: cloneMetadata(req.Metadata), + Metadata: transferMetadataWithRoles(req.Metadata, req.FromRole, req.ToRole), ClientReference: strings.TrimSpace(req.ClientReference), }) if err != nil { @@ -254,6 +255,26 @@ func railMoneyFromProto(m *moneyv1.Money) *rail.Money { } } +func transferMetadataWithRoles(metadata map[string]string, fromRole, toRole pmodel.AccountRole) map[string]string { + result := cloneMetadata(metadata) + if strings.TrimSpace(string(fromRole)) != "" { + if result == nil { + result = map[string]string{} + } + result[pmodel.MetadataKeyFromRole] = strings.TrimSpace(string(fromRole)) + } + if strings.TrimSpace(string(toRole)) != "" { + if result == nil { + result = map[string]string{} + } + result[pmodel.MetadataKeyToRole] = strings.TrimSpace(string(toRole)) + } + if len(result) == 0 { + return nil + } + return result +} + func cloneMetadata(input map[string]string) map[string]string { if len(input) == 0 { return nil diff --git a/api/gateway/chain/config.dev.yml b/api/gateway/chain/config.dev.yml new file mode 100644 index 00000000..7034f80f --- /dev/null +++ b/api/gateway/chain/config.dev.yml @@ -0,0 +1,75 @@ +runtime: + shutdown_timeout_seconds: 15 + +grpc: + network: tcp + address: ":50070" + advertise_host: "dev-chain-gateway" + enable_reflection: true + enable_health: true + +metrics: + address: ":9406" + +database: + driver: mongodb + settings: + host_env: CHAIN_GATEWAY_MONGO_HOST + port_env: CHAIN_GATEWAY_MONGO_PORT + database_env: CHAIN_GATEWAY_MONGO_DATABASE + user_env: CHAIN_GATEWAY_MONGO_USER + password_env: CHAIN_GATEWAY_MONGO_PASSWORD + auth_source_env: CHAIN_GATEWAY_MONGO_AUTH_SOURCE + replica_set_env: CHAIN_GATEWAY_MONGO_REPLICA_SET + +messaging: + driver: NATS + settings: + url_env: NATS_URL + host_env: NATS_HOST + port_env: NATS_PORT + username_env: NATS_USER + password_env: NATS_PASSWORD + broker_name: Chain Gateway Service + max_reconnects: 10 + reconnect_wait: 5 + buffer_size: 1024 + +chains: + - name: arbitrum_sepolia + chain_id: 421614 + native_token: ETH + rpc_url_env: CHAIN_GATEWAY_RPC_URL + + gas_topup_policy: + buffer_percent: 0.20 + min_native_balance: 0.002 + rounding_unit: 0.001 + max_topup: 0.02 + + tokens: + # Test USDT (official test deployment) + - symbol: USDT + contract: "0x5c6b6d1f2f2f6d7b8a9c3e4f5a6b7c8d9e0f1234" + + # Test USDC (Circle test deployment) + - symbol: USDC + contract: "0x75faf114eafb1bdbe2f0316df893fd58ce46aa4d" + +service_wallet: + chain: arbitrum_sepolia + address_env: CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS + private_key_env: CHAIN_GATEWAY_SERVICE_WALLET_KEY + +key_management: + driver: vault + settings: + address: "http://dev-vault:8200" + token_env: VAULT_TOKEN + namespace: "" + mount_path: kv + key_prefix: gateway/chain/wallets + +cache: + wallet_balance_ttl_seconds: 120 + rpc_request_timeout_seconds: 15 diff --git a/api/gateway/chain/config.yml b/api/gateway/chain/config.yml index 578cfcc6..ead5a0da 100644 --- a/api/gateway/chain/config.yml +++ b/api/gateway/chain/config.yml @@ -36,23 +36,25 @@ messaging: buffer_size: 1024 chains: - - name: tron_mainnet - chain_id: 728126428 # 0x2b6653dc - native_token: TRX + - name: arbitrum_one + chain_id: 42161 + native_token: ETH rpc_url_env: CHAIN_GATEWAY_RPC_URL + gas_topup_policy: - buffer_percent: 0.10 - min_native_balance_trx: 10 - rounding_unit_trx: 1 - max_topup_trx: 100 + buffer_percent: 0.15 + min_native_balance: 0.005 + rounding_unit: 0.001 + max_topup: 0.05 + tokens: - symbol: USDT - contract: "0xa614f803b6fd780986a42c78ec9c7f77e6ded13c" + contract: "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9" - symbol: USDC - contract: "0x3487b63d30b5b2c87fb7ffa8bcfade38eaac1abe" + contract: "0xaf88d065e77c8cc2239327c5edb3a432268e5831" service_wallet: - chain: tron_mainnet + chain: arbitrum_one address_env: CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS private_key_env: CHAIN_GATEWAY_SERVICE_WALLET_KEY diff --git a/api/gateway/chain/go.mod b/api/gateway/chain/go.mod index 7fef9092..fc9fad6f 100644 --- a/api/gateway/chain/go.mod +++ b/api/gateway/chain/go.mod @@ -1,6 +1,6 @@ module github.com/tech/sendico/gateway/chain -go 1.25.3 +go 1.25.6 replace github.com/tech/sendico/pkg => ../../pkg @@ -22,7 +22,7 @@ require ( require ( github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260124092617-829590d2c921 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260128015922-c6a88330dfcd // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect @@ -86,5 +86,5 @@ require ( golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect ) diff --git a/api/gateway/chain/go.sum b/api/gateway/chain/go.sum index ab4a63af..0a374196 100644 --- a/api/gateway/chain/go.sum +++ b/api/gateway/chain/go.sum @@ -6,8 +6,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260124092617-829590d2c921 h1:NxRnjiL8BBFLCnsDv18a20vb1d34TUiiZtdJGqpj3xs= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260124092617-829590d2c921/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260128015922-c6a88330dfcd h1:Q1TFWVLXoK7DoPIIBE7K0lDScjlxcRI0IUxjrm3yZ8A= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260128015922-c6a88330dfcd/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -362,8 +362,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d h1:xXzuihhT3gL/ntduUZwHECzAn57E8dA6l8SOtYWdD8Q= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/gateway/chain/internal/server/internal/serverimp.go b/api/gateway/chain/internal/server/internal/serverimp.go index 28f997db..0496613a 100644 --- a/api/gateway/chain/internal/server/internal/serverimp.go +++ b/api/gateway/chain/internal/server/internal/serverimp.go @@ -77,9 +77,9 @@ type gasTopUpPolicyConfig struct { type gasTopUpRuleConfig struct { BufferPercent float64 `yaml:"buffer_percent"` - MinNativeBalanceTRX float64 `yaml:"min_native_balance_trx"` - RoundingUnitTRX float64 `yaml:"rounding_unit_trx"` - MaxTopUpTRX float64 `yaml:"max_topup_trx"` + MinNativeBalanceTRX float64 `yaml:"min_native_balance"` + RoundingUnitTRX float64 `yaml:"rounding_unit"` + MaxTopUpTRX float64 `yaml:"max_topup"` } // Create initialises the chain gateway server implementation. @@ -308,13 +308,13 @@ func parseGasTopUpRule(chainName, label string, cfg gasTopUpRuleConfig) (gateway return gatewayshared.GasTopUpRule{}, true, merrors.InvalidArgument(fmt.Sprintf("chain %s gas_topup_policy %s buffer_percent must be >= 0", chainName, label)) } if cfg.MinNativeBalanceTRX < 0 { - return gatewayshared.GasTopUpRule{}, true, merrors.InvalidArgument(fmt.Sprintf("chain %s gas_topup_policy %s min_native_balance_trx must be >= 0", chainName, label)) + return gatewayshared.GasTopUpRule{}, true, merrors.InvalidArgument(fmt.Sprintf("chain %s gas_topup_policy %s min_native_balance must be >= 0", chainName, label)) } if cfg.RoundingUnitTRX <= 0 { - return gatewayshared.GasTopUpRule{}, true, merrors.InvalidArgument(fmt.Sprintf("chain %s gas_topup_policy %s rounding_unit_trx must be > 0", chainName, label)) + return gatewayshared.GasTopUpRule{}, true, merrors.InvalidArgument(fmt.Sprintf("chain %s gas_topup_policy %s rounding_unit must be > 0", chainName, label)) } if cfg.MaxTopUpTRX <= 0 { - return gatewayshared.GasTopUpRule{}, true, merrors.InvalidArgument(fmt.Sprintf("chain %s gas_topup_policy %s max_topup_trx must be > 0", chainName, label)) + return gatewayshared.GasTopUpRule{}, true, merrors.InvalidArgument(fmt.Sprintf("chain %s gas_topup_policy %s max_topup must be > 0", chainName, label)) } return gatewayshared.GasTopUpRule{ BufferPercent: decimal.NewFromFloat(cfg.BufferPercent), diff --git a/api/gateway/chain/internal/service/gateway/commands/transfer/gas_topup.go b/api/gateway/chain/internal/service/gateway/commands/transfer/gas_topup.go index 767854f1..8faa9a68 100644 --- a/api/gateway/chain/internal/service/gateway/commands/transfer/gas_topup.go +++ b/api/gateway/chain/internal/service/gateway/commands/transfer/gas_topup.go @@ -7,7 +7,6 @@ import ( "github.com/shopspring/decimal" "github.com/tech/sendico/gateway/chain/internal/service/gateway/commands/wallet" "github.com/tech/sendico/gateway/chain/internal/service/gateway/driver/evm" - "github.com/tech/sendico/gateway/chain/internal/service/gateway/driver/tron" "github.com/tech/sendico/gateway/chain/internal/service/gateway/shared" "github.com/tech/sendico/gateway/chain/storage/model" "github.com/tech/sendico/pkg/api/routers/gsresponse" @@ -48,12 +47,12 @@ func (c *computeGasTopUpCommand) Execute(ctx context.Context, req *chainv1.Compu return gsresponse.InvalidArgument[chainv1.ComputeGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("estimated_total_fee is required")) } - topUp, capHit, decision, nativeBalance, walletModel, err := computeGasTopUp(ctx, c.deps, walletRef, estimatedFee) + topUp, capHit, nativeBalance, walletModel, err := computeGasTopUp(ctx, c.deps, walletRef, estimatedFee) if err != nil { return gsresponse.Auto[chainv1.ComputeGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, err) } - logDecision(c.deps.Logger, walletRef, estimatedFee, nativeBalance, topUp, capHit, decision, walletModel) + logDecision(c.deps.Logger, walletRef, estimatedFee, nativeBalance, topUp, capHit, walletModel) return gsresponse.Success(&chainv1.ComputeGasTopUpResponse{ TopupAmount: topUp, @@ -105,12 +104,12 @@ func (c *ensureGasTopUpCommand) Execute(ctx context.Context, req *chainv1.Ensure return gsresponse.InvalidArgument[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("estimated_total_fee is required")) } - topUp, capHit, decision, nativeBalance, walletModel, err := computeGasTopUp(ctx, c.deps, targetWalletRef, estimatedFee) + topUp, capHit, nativeBalance, walletModel, err := computeGasTopUp(ctx, c.deps, targetWalletRef, estimatedFee) if err != nil { return gsresponse.Auto[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, err) } - logDecision(c.deps.Logger, targetWalletRef, estimatedFee, nativeBalance, topUp, capHit, decision, walletModel) + logDecision(c.deps.Logger, targetWalletRef, estimatedFee, nativeBalance, topUp, capHit, walletModel) if topUp == nil || strings.TrimSpace(topUp.GetAmount()) == "" { return gsresponse.Success(&chainv1.EnsureGasTopUpResponse{ @@ -145,46 +144,42 @@ func (c *ensureGasTopUpCommand) Execute(ctx context.Context, req *chainv1.Ensure } } -func computeGasTopUp(ctx context.Context, deps Deps, walletRef string, estimatedFee *moneyv1.Money) (*moneyv1.Money, bool, *tron.GasTopUpDecision, *moneyv1.Money, *model.ManagedWallet, error) { +func computeGasTopUp(ctx context.Context, deps Deps, walletRef string, estimatedFee *moneyv1.Money) (*moneyv1.Money, bool, *moneyv1.Money, *model.ManagedWallet, error) { walletRef = strings.TrimSpace(walletRef) estimatedFee = shared.CloneMoney(estimatedFee) walletModel, err := deps.Storage.Wallets().Get(ctx, walletRef) if err != nil { - return nil, false, nil, nil, nil, err + return nil, false, nil, nil, err } networkKey := strings.ToLower(strings.TrimSpace(walletModel.Network)) + if strings.HasPrefix(networkKey, "tron") { + return nil, false, nil, nil, merrors.InvalidArgument("tron networks must use the tron gateway") + } + networkCfg, ok := deps.Networks.Network(networkKey) if !ok { - return nil, false, nil, nil, nil, merrors.InvalidArgument("unsupported chain for wallet") + return nil, false, nil, nil, merrors.InvalidArgument("unsupported chain for wallet") } nativeBalance, err := nativeBalanceForWallet(ctx, deps, walletModel) if err != nil { - return nil, false, nil, nil, nil, err - } - - if strings.HasPrefix(networkKey, "tron") { - topUp, decision, err := tron.ComputeGasTopUp(networkCfg, walletModel, estimatedFee, nativeBalance) - if err != nil { - return nil, false, nil, nil, nil, err - } - return topUp, decision.CapHit, &decision, nativeBalance, walletModel, nil + return nil, false, nil, nil, err } if networkCfg.GasTopUpPolicy != nil { topUp, capHit, err := evm.ComputeGasTopUp(networkCfg, walletModel, estimatedFee, nativeBalance) if err != nil { - return nil, false, nil, nil, nil, err + return nil, false, nil, nil, err } - return topUp, capHit, nil, nativeBalance, walletModel, nil + return topUp, capHit, nativeBalance, walletModel, nil } topUp, err := defaultGasTopUp(estimatedFee, nativeBalance) if err != nil { - return nil, false, nil, nil, nil, err + return nil, false, nil, nil, err } - return topUp, false, nil, nativeBalance, walletModel, nil + return topUp, false, nativeBalance, walletModel, nil } func nativeBalanceForWallet(ctx context.Context, deps Deps, walletModel *model.ManagedWallet) (*moneyv1.Money, error) { @@ -241,7 +236,7 @@ func defaultGasTopUp(estimatedFee *moneyv1.Money, currentBalance *moneyv1.Money) }, nil } -func logDecision(logger mlogger.Logger, walletRef string, estimatedFee *moneyv1.Money, nativeBalance *moneyv1.Money, topUp *moneyv1.Money, capHit bool, decision *tron.GasTopUpDecision, walletModel *model.ManagedWallet) { +func logDecision(logger mlogger.Logger, walletRef string, estimatedFee *moneyv1.Money, nativeBalance *moneyv1.Money, topUp *moneyv1.Money, capHit bool, walletModel *model.ManagedWallet) { if logger == nil { return } @@ -255,19 +250,6 @@ func logDecision(logger mlogger.Logger, walletRef string, estimatedFee *moneyv1. if walletModel != nil { fields = append(fields, zap.String("network", strings.TrimSpace(walletModel.Network))) } - if decision != nil { - fields = append(fields, - zap.String("estimated_total_fee_trx", decision.EstimatedFeeTRX.String()), - zap.String("current_native_balance_trx", decision.CurrentBalanceTRX.String()), - zap.String("required_trx", decision.RequiredTRX.String()), - zap.String("buffered_required_trx", decision.BufferedRequiredTRX.String()), - zap.String("min_balance_topup_trx", decision.MinBalanceTopUpTRX.String()), - zap.String("raw_topup_trx", decision.RawTopUpTRX.String()), - zap.String("rounded_topup_trx", decision.RoundedTopUpTRX.String()), - zap.String("topup_trx", decision.TopUpTRX.String()), - zap.String("operation_type", decision.OperationType), - ) - } logger.Info("Gas top-up decision", fields...) } diff --git a/api/gateway/chain/internal/service/gateway/commands/wallet/get.go b/api/gateway/chain/internal/service/gateway/commands/wallet/get.go index 11ab552f..0b7b68e3 100644 --- a/api/gateway/chain/internal/service/gateway/commands/wallet/get.go +++ b/api/gateway/chain/internal/service/gateway/commands/wallet/get.go @@ -37,8 +37,10 @@ func (c *getManagedWalletCommand) Execute(ctx context.Context, req *chainv1.GetM wallet, err := c.deps.Storage.Wallets().Get(ctx, walletRef) if err != nil { if errors.Is(err, merrors.ErrNoData) { - c.deps.Logger.Warn("Not found", zap.String("wallet_ref", walletRef)) - return gsresponse.NotFound[chainv1.GetManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err) + // Missing wallet is normal when checking external addresses; avoid warning noise. + c.deps.Logger.Debug("Not found", zap.String("wallet_ref", walletRef)) + quietLogger := c.deps.Logger.WithOptions(zap.IncreaseLevel(zap.ErrorLevel)) + return gsresponse.NotFound[chainv1.GetManagedWalletResponse](quietLogger, mservice.ChainGateway, err) } c.deps.Logger.Warn("Storage get failed", zap.Error(err), zap.String("wallet_ref", walletRef)) return gsresponse.Auto[chainv1.GetManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err) diff --git a/api/gateway/chain/internal/service/gateway/commands/wallet/list.go b/api/gateway/chain/internal/service/gateway/commands/wallet/list.go index 9ddbee15..5db440d3 100644 --- a/api/gateway/chain/internal/service/gateway/commands/wallet/list.go +++ b/api/gateway/chain/internal/service/gateway/commands/wallet/list.go @@ -29,7 +29,10 @@ func (c *listManagedWalletsCommand) Execute(ctx context.Context, req *chainv1.Li filter := model.ManagedWalletFilter{} if req != nil { filter.OrganizationRef = strings.TrimSpace(req.GetOrganizationRef()) - filter.OwnerRef = strings.TrimSpace(req.GetOwnerRef()) + if req.GetOwnerRefFilter() != nil { + ownerRef := strings.TrimSpace(req.GetOwnerRefFilter().GetValue()) + filter.OwnerRefFilter = &ownerRef + } if asset := req.GetAsset(); asset != nil { filter.Network, _ = shared.ChainKeyFromEnum(asset.GetChain()) filter.TokenSymbol = strings.TrimSpace(asset.GetTokenSymbol()) diff --git a/api/gateway/chain/internal/service/gateway/connector.go b/api/gateway/chain/internal/service/gateway/connector.go index 0c514f7c..0fa3af65 100644 --- a/api/gateway/chain/internal/service/gateway/connector.go +++ b/api/gateway/chain/internal/service/gateway/connector.go @@ -95,7 +95,7 @@ func (s *Service) ListAccounts(ctx context.Context, req *connectorv1.ListAccount } resp, err := s.ListManagedWallets(ctx, &chainv1.ListManagedWalletsRequest{ OrganizationRef: strings.TrimSpace(req.GetOrganizationRef()), - OwnerRef: strings.TrimSpace(req.GetOwnerRef()), + OwnerRefFilter: req.GetOwnerRefFilter(), Asset: asset, Page: req.GetPage(), }) diff --git a/api/gateway/chain/internal/service/gateway/driver/evm/evm.go b/api/gateway/chain/internal/service/gateway/driver/evm/evm.go index 94016c2d..00d1a260 100644 --- a/api/gateway/chain/internal/service/gateway/driver/evm/evm.go +++ b/api/gateway/chain/internal/service/gateway/driver/evm/evm.go @@ -221,7 +221,7 @@ func NativeBalance(ctx context.Context, deps driver.Deps, network shared.Network return nil, err } - logger.Info("On-chain native balance fetched", + logger.Debug("On-chain native balance fetched", append(logFields, zap.String("balance_raw", bal.String()), )..., @@ -421,8 +421,7 @@ func SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Networ nonce, err := client.PendingNonceAt(ctx, sourceAddress) if err != nil { logger.Warn("Failed to fetch nonce", zap.Error(err), - zap.String("transfer_ref", transfer.TransferRef), - zap.String("wallet_ref", source.WalletRef), + zap.String("transfer_ref", transfer.TransferRef), zap.String("wallet_ref", source.WalletRef), ) return "", executorInternal("failed to fetch nonce", err) } @@ -430,8 +429,7 @@ func SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Networ gasPrice, err := client.SuggestGasPrice(ctx) if err != nil { logger.Warn("Failed to suggest gas price", zap.Error(err), - zap.String("transfer_ref", transfer.TransferRef), - zap.String("network", network.Name), + zap.String("transfer_ref", transfer.TransferRef), zap.String("network", network.Name), ) return "", executorInternal("failed to suggest gas price", err) } @@ -479,8 +477,7 @@ func SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Networ decimals, err := erc20Decimals(ctx, rpcClient, tokenAddress) if err != nil { logger.Warn("Failed to read token decimals", zap.Error(err), - zap.String("transfer_ref", transfer.TransferRef), - zap.String("contract", contract), + zap.String("transfer_ref", transfer.TransferRef), zap.String("contract", contract), ) return "", err } @@ -488,8 +485,7 @@ func SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Networ amountInt, err := toBaseUnits(amount.Amount, decimals) if err != nil { logger.Warn("Failed to convert amount to base units", zap.Error(err), - zap.String("transfer_ref", transfer.TransferRef), - zap.String("amount", amount.Amount), + zap.String("transfer_ref", transfer.TransferRef), zap.String("amount", amount.Amount), ) return "", err } @@ -522,8 +518,7 @@ func SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Networ signedTx, err := deps.KeyManager.SignTransaction(ctx, source.KeyReference, tx, chainID) if err != nil { logger.Warn("Failed to sign transaction", zap.Error(err), - zap.String("transfer_ref", transfer.TransferRef), - zap.String("wallet_ref", source.WalletRef), + zap.String("transfer_ref", transfer.TransferRef), zap.String("wallet_ref", source.WalletRef), ) return "", err } @@ -536,10 +531,8 @@ func SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Networ } txHash := signedTx.Hash().Hex() - logger.Info("Transaction submitted", - zap.String("transfer_ref", transfer.TransferRef), - zap.String("tx_hash", txHash), - zap.String("network", network.Name), + logger.Info("Transaction submitted", zap.String("transfer_ref", transfer.TransferRef), + zap.String("tx_hash", txHash), zap.String("network", network.Name), ) return txHash, nil @@ -578,31 +571,25 @@ func AwaitConfirmation(ctx context.Context, deps driver.Deps, network shared.Net if errors.Is(err, ethereum.NotFound) { select { case <-ticker.C: - logger.Debug("Transaction not yet mined", - zap.String("tx_hash", txHash), + logger.Debug("Transaction not yet mined", zap.String("tx_hash", txHash), zap.String("network", network.Name), ) continue case <-ctx.Done(): - logger.Warn("Context cancelled while awaiting confirmation", - zap.String("tx_hash", txHash), + logger.Warn("Context cancelled while awaiting confirmation", zap.String("tx_hash", txHash), zap.String("network", network.Name), ) return nil, ctx.Err() } } - logger.Warn("Failed to fetch transaction receipt", - zap.String("tx_hash", txHash), - zap.String("network", network.Name), - zap.Error(err), + logger.Warn("Failed to fetch transaction receipt", zap.Error(err), + zap.String("tx_hash", txHash), zap.String("network", network.Name), ) return nil, executorInternal("failed to fetch transaction receipt", err) } - logger.Info("Transaction confirmed", - zap.String("tx_hash", txHash), - zap.String("network", network.Name), + logger.Info("Transaction confirmed", zap.String("tx_hash", txHash), + zap.String("network", network.Name), zap.Uint64("status", receipt.Status), zap.Uint64("block_number", receipt.BlockNumber.Uint64()), - zap.Uint64("status", receipt.Status), ) return receipt, nil } diff --git a/api/gateway/chain/internal/service/gateway/driver/tron/address.go b/api/gateway/chain/internal/service/gateway/driver/tron/address.go deleted file mode 100644 index ff6048a6..00000000 --- a/api/gateway/chain/internal/service/gateway/driver/tron/address.go +++ /dev/null @@ -1,193 +0,0 @@ -package tron - -import ( - "bytes" - "crypto/sha256" - "encoding/hex" - "fmt" - "math/big" - "strings" - - "github.com/tech/sendico/pkg/merrors" -) - -const tronHexPrefix = "0x" - -var base58Alphabet = []byte("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz") - -func normalizeAddress(address string) (string, error) { - trimmed := strings.TrimSpace(address) - if trimmed == "" { - return "", merrors.InvalidArgument("address is required") - } - if strings.HasPrefix(trimmed, tronHexPrefix) || isHexString(trimmed) { - return hexToBase58(trimmed) - } - decoded, err := base58Decode(trimmed) - if err != nil { - return "", err - } - if err := validateChecksum(decoded); err != nil { - return "", err - } - return base58Encode(decoded), nil -} - -func rpcAddress(address string) (string, error) { - trimmed := strings.TrimSpace(address) - if trimmed == "" { - return "", merrors.InvalidArgument("address is required") - } - if strings.HasPrefix(trimmed, tronHexPrefix) || isHexString(trimmed) { - return normalizeHexRPC(trimmed) - } - return base58ToHex(trimmed) -} - -func hexToBase58(address string) (string, error) { - bytesAddr, err := parseHexAddress(address) - if err != nil { - return "", err - } - payload := append(bytesAddr, checksum(bytesAddr)...) - return base58Encode(payload), nil -} - -func base58ToHex(address string) (string, error) { - decoded, err := base58Decode(address) - if err != nil { - return "", err - } - if err := validateChecksum(decoded); err != nil { - return "", err - } - return tronHexPrefix + hex.EncodeToString(decoded[1:21]), nil -} - -func parseHexAddress(address string) ([]byte, error) { - trimmed := strings.TrimPrefix(strings.TrimSpace(address), tronHexPrefix) - if trimmed == "" { - return nil, merrors.InvalidArgument("address is required") - } - if len(trimmed)%2 == 1 { - trimmed = "0" + trimmed - } - decoded, err := hex.DecodeString(trimmed) - if err != nil { - return nil, merrors.InvalidArgument("invalid hex address") - } - switch len(decoded) { - case 20: - prefixed := make([]byte, 21) - prefixed[0] = 0x41 - copy(prefixed[1:], decoded) - return prefixed, nil - case 21: - if decoded[0] != 0x41 { - return nil, merrors.InvalidArgument("invalid tron address prefix") - } - return decoded, nil - default: - return nil, merrors.InvalidArgument(fmt.Sprintf("invalid tron address length %d", len(decoded))) - } -} - -func normalizeHexRPC(address string) (string, error) { - decoded, err := parseHexAddress(address) - if err != nil { - return "", err - } - return tronHexPrefix + hex.EncodeToString(decoded[1:21]), nil -} - -func validateChecksum(decoded []byte) error { - if len(decoded) != 25 { - return merrors.InvalidArgument("invalid tron address length") - } - payload := decoded[:21] - expected := checksum(payload) - if !bytes.Equal(expected, decoded[21:]) { - return merrors.InvalidArgument("invalid tron address checksum") - } - if payload[0] != 0x41 { - return merrors.InvalidArgument("invalid tron address prefix") - } - return nil -} - -func checksum(payload []byte) []byte { - first := sha256.Sum256(payload) - second := sha256.Sum256(first[:]) - return second[:4] -} - -func base58Encode(input []byte) string { - if len(input) == 0 { - return "" - } - x := new(big.Int).SetBytes(input) - base := big.NewInt(58) - zero := big.NewInt(0) - mod := new(big.Int) - - encoded := make([]byte, 0, len(input)) - for x.Cmp(zero) > 0 { - x.DivMod(x, base, mod) - encoded = append(encoded, base58Alphabet[mod.Int64()]) - } - for _, b := range input { - if b != 0 { - break - } - encoded = append(encoded, base58Alphabet[0]) - } - reverse(encoded) - return string(encoded) -} - -func base58Decode(input string) ([]byte, error) { - result := big.NewInt(0) - base := big.NewInt(58) - - for i := 0; i < len(input); i++ { - idx := bytes.IndexByte(base58Alphabet, input[i]) - if idx < 0 { - return nil, merrors.InvalidArgument("invalid base58 address") - } - result.Mul(result, base) - result.Add(result, big.NewInt(int64(idx))) - } - - decoded := result.Bytes() - zeroCount := 0 - for zeroCount < len(input) && input[zeroCount] == base58Alphabet[0] { - zeroCount++ - } - if zeroCount > 0 { - decoded = append(make([]byte, zeroCount), decoded...) - } - return decoded, nil -} - -func reverse(data []byte) { - for i, j := 0, len(data)-1; i < j; i, j = i+1, j-1 { - data[i], data[j] = data[j], data[i] - } -} - -func isHexString(value string) bool { - trimmed := strings.TrimPrefix(strings.TrimSpace(value), tronHexPrefix) - if trimmed == "" { - return false - } - for _, r := range trimmed { - switch { - case r >= '0' && r <= '9': - case r >= 'a' && r <= 'f': - case r >= 'A' && r <= 'F': - default: - return false - } - } - return true -} diff --git a/api/gateway/chain/internal/service/gateway/driver/tron/driver.go b/api/gateway/chain/internal/service/gateway/driver/tron/driver.go deleted file mode 100644 index 470e7a8d..00000000 --- a/api/gateway/chain/internal/service/gateway/driver/tron/driver.go +++ /dev/null @@ -1,245 +0,0 @@ -package tron - -import ( - "context" - "strings" - - "github.com/ethereum/go-ethereum/core/types" - "github.com/tech/sendico/gateway/chain/internal/service/gateway/driver" - "github.com/tech/sendico/gateway/chain/internal/service/gateway/driver/evm" - "github.com/tech/sendico/gateway/chain/internal/service/gateway/shared" - "github.com/tech/sendico/gateway/chain/storage/model" - "github.com/tech/sendico/pkg/merrors" - "github.com/tech/sendico/pkg/mlogger" - moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" - "go.uber.org/zap" -) - -// Driver implements Tron-specific behavior, including address conversion. -type Driver struct { - logger mlogger.Logger -} - -func New(logger mlogger.Logger) *Driver { - return &Driver{logger: logger.Named("tron")} -} - -func (d *Driver) Name() string { - return "tron" -} - -func (d *Driver) FormatAddress(address string) (string, error) { - d.logger.Debug("Format address", zap.String("address", address)) - normalized, err := normalizeAddress(address) - if err != nil { - d.logger.Warn("Format address failed", zap.String("address", address), zap.Error(err)) - } - return normalized, err -} - -func (d *Driver) NormalizeAddress(address string) (string, error) { - d.logger.Debug("Normalize address", zap.String("address", address)) - normalized, err := normalizeAddress(address) - if err != nil { - d.logger.Warn("Normalize address failed", zap.String("address", address), zap.Error(err)) - } - return normalized, err -} - -func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet) (*moneyv1.Money, error) { - if wallet == nil { - return nil, merrors.InvalidArgument("wallet is required") - } - d.logger.Debug("Balance request", zap.String("wallet_ref", wallet.WalletRef), zap.String("network", network.Name)) - rpcAddr, err := rpcAddress(wallet.DepositAddress) - if err != nil { - d.logger.Warn("Balance address conversion failed", zap.Error(err), - zap.String("wallet_ref", wallet.WalletRef), - zap.String("address", wallet.DepositAddress), - ) - return nil, err - } - driverDeps := deps - driverDeps.Logger = d.logger - result, err := evm.Balance(ctx, driverDeps, network, wallet, rpcAddr) - if err != nil { - d.logger.Warn("Balance failed", zap.Error(err), - zap.String("wallet_ref", wallet.WalletRef), - zap.String("network", network.Name), - ) - } else if result != nil { - d.logger.Debug("Balance result", - zap.String("wallet_ref", wallet.WalletRef), - zap.String("network", network.Name), - zap.String("amount", result.Amount), - zap.String("currency", result.Currency), - ) - } - return result, err -} - -func (d *Driver) NativeBalance(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet) (*moneyv1.Money, error) { - if wallet == nil { - return nil, merrors.InvalidArgument("wallet is required") - } - d.logger.Debug("Native balance request", zap.String("wallet_ref", wallet.WalletRef), zap.String("network", network.Name)) - rpcAddr, err := rpcAddress(wallet.DepositAddress) - if err != nil { - d.logger.Warn("Native balance address conversion failed", zap.Error(err), - zap.String("wallet_ref", wallet.WalletRef), - zap.String("address", wallet.DepositAddress), - ) - return nil, err - } - driverDeps := deps - driverDeps.Logger = d.logger - result, err := evm.NativeBalance(ctx, driverDeps, network, wallet, rpcAddr) - if err != nil { - d.logger.Warn("Native balance failed", zap.Error(err), - zap.String("wallet_ref", wallet.WalletRef), - zap.String("network", network.Name), - ) - } else if result != nil { - d.logger.Debug("Native balance result", - zap.String("wallet_ref", wallet.WalletRef), - zap.String("network", network.Name), - zap.String("amount", result.Amount), - zap.String("currency", result.Currency), - ) - } - return result, err -} - -func (d *Driver) EstimateFee(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet, destination string, amount *moneyv1.Money) (*moneyv1.Money, error) { - if wallet == nil { - return nil, merrors.InvalidArgument("wallet is required") - } - if amount == nil { - return nil, merrors.InvalidArgument("amount is required") - } - d.logger.Debug("Estimate fee request", - zap.String("wallet_ref", wallet.WalletRef), - zap.String("network", network.Name), - zap.String("destination", destination), - ) - rpcFrom, err := rpcAddress(wallet.DepositAddress) - if err != nil { - d.logger.Warn("Estimate fee address conversion failed", zap.Error(err), - zap.String("wallet_ref", wallet.WalletRef), - zap.String("address", wallet.DepositAddress), - ) - return nil, err - } - rpcTo, err := rpcAddress(destination) - if err != nil { - d.logger.Warn("Estimate fee destination conversion failed", zap.Error(err), - zap.String("wallet_ref", wallet.WalletRef), - zap.String("destination", destination), - ) - return nil, err - } - if rpcFrom == rpcTo { - return &moneyv1.Money{ - Currency: nativeCurrency(network), - Amount: "0", - }, nil - } - driverDeps := deps - driverDeps.Logger = d.logger - result, err := evm.EstimateFee(ctx, driverDeps, network, wallet, rpcFrom, rpcTo, amount) - if err != nil { - d.logger.Warn("Estimate fee failed", zap.Error(err), - zap.String("wallet_ref", wallet.WalletRef), - zap.String("network", network.Name), - zap.String("from_address", wallet.DepositAddress), - zap.String("from_rpc", rpcFrom), - zap.String("to_address", destination), - zap.String("to_rpc", rpcTo), - ) - } else if result != nil { - d.logger.Debug("Estimate fee result", - zap.String("wallet_ref", wallet.WalletRef), - zap.String("network", network.Name), - zap.String("amount", result.Amount), - zap.String("currency", result.Currency), - ) - } - return result, err -} - -func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Network, transfer *model.Transfer, source *model.ManagedWallet, destination string) (string, error) { - if source == nil { - return "", merrors.InvalidArgument("source wallet is required") - } - d.logger.Debug("Submit transfer request", - zap.String("transfer_ref", transfer.TransferRef), - zap.String("network", network.Name), - zap.String("destination", destination), - ) - rpcFrom, err := rpcAddress(source.DepositAddress) - if err != nil { - d.logger.Warn("Submit transfer address conversion failed", zap.Error(err), - zap.String("wallet_ref", source.WalletRef), - zap.String("address", source.DepositAddress), - ) - return "", err - } - rpcTo, err := rpcAddress(destination) - if err != nil { - d.logger.Warn("Submit transfer destination conversion failed", zap.Error(err), - zap.String("transfer_ref", transfer.TransferRef), - zap.String("destination", destination), - ) - return "", err - } - driverDeps := deps - driverDeps.Logger = d.logger - txHash, err := evm.SubmitTransfer(ctx, driverDeps, network, transfer, source, rpcFrom, rpcTo) - if err != nil { - d.logger.Warn("Submit transfer failed", zap.Error(err), - zap.String("transfer_ref", transfer.TransferRef), - zap.String("network", network.Name), - ) - } else { - d.logger.Debug("Submit transfer result", - zap.String("transfer_ref", transfer.TransferRef), - zap.String("network", network.Name), - zap.String("tx_hash", txHash), - ) - } - return txHash, err -} - -func (d *Driver) AwaitConfirmation(ctx context.Context, deps driver.Deps, network shared.Network, txHash string) (*types.Receipt, error) { - d.logger.Debug("Awaiting confirmation", - zap.String("tx_hash", txHash), - zap.String("network", network.Name), - ) - driverDeps := deps - driverDeps.Logger = d.logger - receipt, err := evm.AwaitConfirmation(ctx, driverDeps, network, txHash) - if err != nil { - d.logger.Warn("Awaiting of confirmation failed", zap.Error(err), - zap.String("tx_hash", txHash), - zap.String("network", network.Name), - ) - } else if receipt != nil { - d.logger.Debug("Await confirmation result", - zap.String("tx_hash", txHash), - zap.String("network", network.Name), - zap.Uint64("block_number", receipt.BlockNumber.Uint64()), - zap.Uint64("status", receipt.Status), - ) - } - return receipt, err -} - -func nativeCurrency(network shared.Network) string { - currency := strings.ToUpper(strings.TrimSpace(network.NativeToken)) - if currency == "" { - currency = strings.ToUpper(strings.TrimSpace(network.Name)) - } - return currency -} - -var _ driver.Driver = (*Driver)(nil) diff --git a/api/gateway/chain/internal/service/gateway/driver/tron/driver_test.go b/api/gateway/chain/internal/service/gateway/driver/tron/driver_test.go deleted file mode 100644 index 12456eb8..00000000 --- a/api/gateway/chain/internal/service/gateway/driver/tron/driver_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package tron - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - "github.com/tech/sendico/gateway/chain/internal/service/gateway/driver" - "github.com/tech/sendico/gateway/chain/internal/service/gateway/shared" - "github.com/tech/sendico/gateway/chain/storage/model" - moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" - "go.uber.org/zap" -) - -func TestEstimateFeeSelfTransferReturnsZero(t *testing.T) { - logger := zap.NewNop() - d := New(logger) - wallet := &model.ManagedWallet{ - WalletRef: "wallet_ref", - DepositAddress: "TGBDXEg9rxSqGFJDcb889zqTjDwx1bmLRF", - } - network := shared.Network{ - Name: "tron_mainnet", - NativeToken: "TRX", - } - amount := &moneyv1.Money{Currency: "TRX", Amount: "1000000"} - - fee, err := d.EstimateFee(context.Background(), driver.Deps{}, network, wallet, wallet.DepositAddress, amount) - require.NoError(t, err) - require.NotNil(t, fee) - require.Equal(t, "TRX", fee.GetCurrency()) - require.Equal(t, "0", fee.GetAmount()) -} diff --git a/api/gateway/chain/internal/service/gateway/driver/tron/gas_topup.go b/api/gateway/chain/internal/service/gateway/driver/tron/gas_topup.go deleted file mode 100644 index 28159ae5..00000000 --- a/api/gateway/chain/internal/service/gateway/driver/tron/gas_topup.go +++ /dev/null @@ -1,143 +0,0 @@ -package tron - -import ( - "fmt" - "strings" - - "github.com/shopspring/decimal" - "github.com/tech/sendico/gateway/chain/internal/service/gateway/shared" - "github.com/tech/sendico/gateway/chain/storage/model" - "github.com/tech/sendico/pkg/merrors" - moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" -) - -var tronBaseUnitFactor = decimal.NewFromInt(1_000_000) - -// GasTopUpDecision captures the applied policy inputs and outputs (in TRX units). -type GasTopUpDecision struct { - CurrentBalanceTRX decimal.Decimal - EstimatedFeeTRX decimal.Decimal - RequiredTRX decimal.Decimal - BufferedRequiredTRX decimal.Decimal - MinBalanceTopUpTRX decimal.Decimal - RawTopUpTRX decimal.Decimal - RoundedTopUpTRX decimal.Decimal - TopUpTRX decimal.Decimal - CapHit bool - OperationType string -} - -// ComputeGasTopUp applies the network policy to decide a TRX top-up amount. -func ComputeGasTopUp(network shared.Network, wallet *model.ManagedWallet, estimatedFee *moneyv1.Money, currentBalance *moneyv1.Money) (*moneyv1.Money, GasTopUpDecision, error) { - decision := GasTopUpDecision{} - if wallet == nil { - return nil, decision, merrors.InvalidArgument("wallet is required") - } - if estimatedFee == nil || strings.TrimSpace(estimatedFee.GetAmount()) == "" || strings.TrimSpace(estimatedFee.GetCurrency()) == "" { - return nil, decision, merrors.InvalidArgument("estimated fee is required") - } - if currentBalance == nil || strings.TrimSpace(currentBalance.GetAmount()) == "" || strings.TrimSpace(currentBalance.GetCurrency()) == "" { - return nil, decision, merrors.InvalidArgument("current native balance is required") - } - if network.GasTopUpPolicy == nil { - return nil, decision, merrors.InvalidArgument("gas top-up policy is not configured") - } - - nativeCurrency := strings.TrimSpace(network.NativeToken) - if nativeCurrency == "" { - nativeCurrency = strings.ToUpper(strings.TrimSpace(network.Name)) - } - if !strings.EqualFold(nativeCurrency, estimatedFee.GetCurrency()) { - return nil, decision, merrors.InvalidArgument(fmt.Sprintf("estimated fee currency mismatch (expected %s)", nativeCurrency)) - } - if !strings.EqualFold(nativeCurrency, currentBalance.GetCurrency()) { - return nil, decision, merrors.InvalidArgument(fmt.Sprintf("native balance currency mismatch (expected %s)", nativeCurrency)) - } - - estimatedTRX, err := tronToTRX(estimatedFee) - if err != nil { - return nil, decision, err - } - currentTRX, err := tronToTRX(currentBalance) - if err != nil { - return nil, decision, err - } - - isContract := strings.TrimSpace(wallet.ContractAddress) != "" - rule, ok := network.GasTopUpPolicy.Rule(isContract) - if !ok { - return nil, decision, merrors.InvalidArgument("gas top-up policy is not configured") - } - if rule.RoundingUnit.LessThanOrEqual(decimal.Zero) { - return nil, decision, merrors.InvalidArgument("gas top-up rounding unit must be > 0") - } - if rule.MaxTopUp.LessThanOrEqual(decimal.Zero) { - return nil, decision, merrors.InvalidArgument("gas top-up max top-up must be > 0") - } - - required := estimatedTRX.Sub(currentTRX) - if required.IsNegative() { - required = decimal.Zero - } - bufferedRequired := required.Mul(decimal.NewFromInt(1).Add(rule.BufferPercent)) - - minBalanceTopUp := rule.MinNativeBalance.Sub(currentTRX) - if minBalanceTopUp.IsNegative() { - minBalanceTopUp = decimal.Zero - } - - rawTopUp := bufferedRequired - if minBalanceTopUp.GreaterThan(rawTopUp) { - rawTopUp = minBalanceTopUp - } - - roundedTopUp := decimal.Zero - if rawTopUp.IsPositive() { - roundedTopUp = rawTopUp.Div(rule.RoundingUnit).Ceil().Mul(rule.RoundingUnit) - } - - topUp := roundedTopUp - capHit := false - if topUp.GreaterThan(rule.MaxTopUp) { - topUp = rule.MaxTopUp - capHit = true - } - - decision = GasTopUpDecision{ - CurrentBalanceTRX: currentTRX, - EstimatedFeeTRX: estimatedTRX, - RequiredTRX: required, - BufferedRequiredTRX: bufferedRequired, - MinBalanceTopUpTRX: minBalanceTopUp, - RawTopUpTRX: rawTopUp, - RoundedTopUpTRX: roundedTopUp, - TopUpTRX: topUp, - CapHit: capHit, - OperationType: operationType(isContract), - } - - if !topUp.IsPositive() { - return nil, decision, nil - } - - baseUnits := topUp.Mul(tronBaseUnitFactor).Ceil().Truncate(0) - return &moneyv1.Money{ - Currency: strings.ToUpper(nativeCurrency), - Amount: baseUnits.StringFixed(0), - }, decision, nil -} - -func tronToTRX(amount *moneyv1.Money) (decimal.Decimal, error) { - value, err := decimal.NewFromString(strings.TrimSpace(amount.GetAmount())) - if err != nil { - return decimal.Zero, err - } - return value.Div(tronBaseUnitFactor), nil -} - -func operationType(contract bool) string { - if contract { - return "trc20" - } - return "native" -} diff --git a/api/gateway/chain/internal/service/gateway/driver/tron/gas_topup_test.go b/api/gateway/chain/internal/service/gateway/driver/tron/gas_topup_test.go deleted file mode 100644 index 0cc7b199..00000000 --- a/api/gateway/chain/internal/service/gateway/driver/tron/gas_topup_test.go +++ /dev/null @@ -1,147 +0,0 @@ -package tron - -import ( - "testing" - - "github.com/shopspring/decimal" - "github.com/stretchr/testify/require" - "github.com/tech/sendico/gateway/chain/internal/service/gateway/shared" - "github.com/tech/sendico/gateway/chain/storage/model" - moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" -) - -func TestComputeGasTopUp_BalanceSufficient(t *testing.T) { - network := tronNetwork(defaultPolicy()) - wallet := &model.ManagedWallet{} - - topUp, decision, err := ComputeGasTopUp(network, wallet, tronMoney("5"), tronMoney("30")) - require.NoError(t, err) - require.Nil(t, topUp) - require.True(t, decision.TopUpTRX.IsZero()) -} - -func TestComputeGasTopUp_BufferedRequired(t *testing.T) { - network := tronNetwork(defaultPolicy()) - wallet := &model.ManagedWallet{} - - topUp, decision, err := ComputeGasTopUp(network, wallet, tronMoney("50"), tronMoney("10")) - require.NoError(t, err) - require.NotNil(t, topUp) - require.Equal(t, "46000000", topUp.GetAmount()) - require.Equal(t, "TRX", topUp.GetCurrency()) - require.Equal(t, "46", decision.TopUpTRX.String()) -} - -func TestComputeGasTopUp_MinBalanceBinding(t *testing.T) { - network := tronNetwork(defaultPolicy()) - wallet := &model.ManagedWallet{} - - topUp, decision, err := ComputeGasTopUp(network, wallet, tronMoney("5"), tronMoney("1")) - require.NoError(t, err) - require.NotNil(t, topUp) - require.Equal(t, "19000000", topUp.GetAmount()) - require.Equal(t, "19", decision.TopUpTRX.String()) -} - -func TestComputeGasTopUp_RoundsUp(t *testing.T) { - policy := shared.GasTopUpPolicy{ - Default: shared.GasTopUpRule{ - BufferPercent: decimal.NewFromFloat(0), - MinNativeBalance: decimal.NewFromFloat(0), - RoundingUnit: decimal.NewFromFloat(1), - MaxTopUp: decimal.NewFromFloat(100), - }, - } - network := tronNetwork(&policy) - wallet := &model.ManagedWallet{} - - topUp, decision, err := ComputeGasTopUp(network, wallet, tronMoney("1.1"), tronMoney("0")) - require.NoError(t, err) - require.NotNil(t, topUp) - require.Equal(t, "2000000", topUp.GetAmount()) - require.Equal(t, "2", decision.TopUpTRX.String()) -} - -func TestComputeGasTopUp_CapHit(t *testing.T) { - policy := shared.GasTopUpPolicy{ - Default: shared.GasTopUpRule{ - BufferPercent: decimal.NewFromFloat(0), - MinNativeBalance: decimal.NewFromFloat(0), - RoundingUnit: decimal.NewFromFloat(1), - MaxTopUp: decimal.NewFromFloat(10), - }, - } - network := tronNetwork(&policy) - wallet := &model.ManagedWallet{} - - topUp, decision, err := ComputeGasTopUp(network, wallet, tronMoney("100"), tronMoney("0")) - require.NoError(t, err) - require.NotNil(t, topUp) - require.Equal(t, "10000000", topUp.GetAmount()) - require.True(t, decision.CapHit) -} - -func TestComputeGasTopUp_MinBalanceWhenRequiredZero(t *testing.T) { - network := tronNetwork(defaultPolicy()) - wallet := &model.ManagedWallet{} - - topUp, decision, err := ComputeGasTopUp(network, wallet, tronMoney("0"), tronMoney("5")) - require.NoError(t, err) - require.NotNil(t, topUp) - require.Equal(t, "15000000", topUp.GetAmount()) - require.Equal(t, "15", decision.TopUpTRX.String()) -} - -func TestComputeGasTopUp_ContractPolicyOverride(t *testing.T) { - policy := shared.GasTopUpPolicy{ - Default: shared.GasTopUpRule{ - BufferPercent: decimal.NewFromFloat(0.1), - MinNativeBalance: decimal.NewFromFloat(10), - RoundingUnit: decimal.NewFromFloat(1), - MaxTopUp: decimal.NewFromFloat(100), - }, - Contract: &shared.GasTopUpRule{ - BufferPercent: decimal.NewFromFloat(0.5), - MinNativeBalance: decimal.NewFromFloat(5), - RoundingUnit: decimal.NewFromFloat(1), - MaxTopUp: decimal.NewFromFloat(100), - }, - } - network := tronNetwork(&policy) - wallet := &model.ManagedWallet{ContractAddress: "0xcontract"} - - topUp, decision, err := ComputeGasTopUp(network, wallet, tronMoney("10"), tronMoney("0")) - require.NoError(t, err) - require.NotNil(t, topUp) - require.Equal(t, "15000000", topUp.GetAmount()) - require.Equal(t, "15", decision.TopUpTRX.String()) - require.Equal(t, "trc20", decision.OperationType) -} - -func defaultPolicy() *shared.GasTopUpPolicy { - return &shared.GasTopUpPolicy{ - Default: shared.GasTopUpRule{ - BufferPercent: decimal.NewFromFloat(0.15), - MinNativeBalance: decimal.NewFromFloat(20), - RoundingUnit: decimal.NewFromFloat(1), - MaxTopUp: decimal.NewFromFloat(500), - }, - } -} - -func tronNetwork(policy *shared.GasTopUpPolicy) shared.Network { - return shared.Network{ - Name: "tron_mainnet", - NativeToken: "TRX", - GasTopUpPolicy: policy, - } -} - -func tronMoney(trx string) *moneyv1.Money { - value, _ := decimal.NewFromString(trx) - baseUnits := value.Mul(tronBaseUnitFactor).Truncate(0) - return &moneyv1.Money{ - Currency: "TRX", - Amount: baseUnits.StringFixed(0), - } -} diff --git a/api/gateway/chain/internal/service/gateway/drivers/registry.go b/api/gateway/chain/internal/service/gateway/drivers/registry.go index 858ad849..662a70ce 100644 --- a/api/gateway/chain/internal/service/gateway/drivers/registry.go +++ b/api/gateway/chain/internal/service/gateway/drivers/registry.go @@ -7,7 +7,6 @@ import ( "github.com/tech/sendico/gateway/chain/internal/service/gateway/driver" "github.com/tech/sendico/gateway/chain/internal/service/gateway/driver/arbitrum" "github.com/tech/sendico/gateway/chain/internal/service/gateway/driver/ethereum" - "github.com/tech/sendico/gateway/chain/internal/service/gateway/driver/tron" "github.com/tech/sendico/gateway/chain/internal/service/gateway/shared" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/mlogger" @@ -63,7 +62,7 @@ func (r *Registry) Driver(network string) (driver.Driver, error) { func resolveDriver(logger mlogger.Logger, network string) (driver.Driver, error) { switch { case strings.HasPrefix(network, "tron"): - return tron.New(logger), nil + return nil, merrors.InvalidArgument("tron networks must use the tron gateway, not chain gateway") case strings.HasPrefix(network, "arbitrum"): return arbitrum.New(logger), nil case strings.HasPrefix(network, "ethereum"): diff --git a/api/gateway/chain/internal/service/gateway/service_test.go b/api/gateway/chain/internal/service/gateway/service_test.go index 270a060b..89f3c7ce 100644 --- a/api/gateway/chain/internal/service/gateway/service_test.go +++ b/api/gateway/chain/internal/service/gateway/service_test.go @@ -329,8 +329,11 @@ func (w *inMemoryWallets) List(ctx context.Context, filter model.ManagedWalletFi if filter.OrganizationRef != "" && !strings.EqualFold(wallet.OrganizationRef, filter.OrganizationRef) { continue } - if filter.OwnerRef != "" && !strings.EqualFold(wallet.OwnerRef, filter.OwnerRef) { - continue + if filter.OwnerRefFilter != nil { + ownerRef := strings.TrimSpace(*filter.OwnerRefFilter) + if !strings.EqualFold(wallet.OwnerRef, ownerRef) { + continue + } } if filter.Network != "" && !strings.EqualFold(wallet.Network, filter.Network) { continue diff --git a/api/gateway/chain/storage/model/wallet.go b/api/gateway/chain/storage/model/wallet.go index 77fc8690..5e6d7347 100644 --- a/api/gateway/chain/storage/model/wallet.go +++ b/api/gateway/chain/storage/model/wallet.go @@ -61,11 +61,15 @@ func (*WalletBalance) Collection() string { // ManagedWalletFilter describes list filters. type ManagedWalletFilter struct { OrganizationRef string - OwnerRef string - Network string - TokenSymbol string - Cursor string - Limit int32 + // OwnerRefFilter is a 3-state filter: + // - nil: no filter on owner_ref (return all) + // - pointer to empty string: filter for wallets where owner_ref is empty + // - pointer to a value: filter for wallets where owner_ref matches + OwnerRefFilter *string + Network string + TokenSymbol string + Cursor string + Limit int32 } // ManagedWalletList contains paginated wallet results. diff --git a/api/gateway/chain/storage/mongo/store/wallets.go b/api/gateway/chain/storage/mongo/store/wallets.go index 3c4bc0ff..bd902b9a 100644 --- a/api/gateway/chain/storage/mongo/store/wallets.go +++ b/api/gateway/chain/storage/mongo/store/wallets.go @@ -156,9 +156,10 @@ func (w *Wallets) List(ctx context.Context, filter model.ManagedWalletFilter) (* query = query.Filter(repository.Field("organizationRef"), org) fields = append(fields, zap.String("organization_ref", org)) } - if owner := strings.TrimSpace(filter.OwnerRef); owner != "" { - query = query.Filter(repository.Field("ownerRef"), owner) - fields = append(fields, zap.String("owner_ref", owner)) + if filter.OwnerRefFilter != nil { + ownerRef := strings.TrimSpace(*filter.OwnerRefFilter) + query = query.Filter(repository.Field("ownerRef"), ownerRef) + fields = append(fields, zap.String("owner_ref_filter", ownerRef)) } if network := strings.TrimSpace(filter.Network); network != "" { normalized := strings.ToLower(network)