tron driver removed
This commit is contained in:
@@ -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
|
||||
|
||||
1
api/gateway/chain/.gitignore
vendored
1
api/gateway/chain/.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
internal/generated
|
||||
.gocache
|
||||
app
|
||||
tmp
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
75
api/gateway/chain/config.dev.yml
Normal file
75
api/gateway/chain/config.dev.yml
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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"):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user