43 Commits

Author SHA1 Message Date
Stephan D
bf85ca062c restucturization of recipients payment methods
All checks were successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
2025-12-04 14:42:25 +01:00
Arseni
3b04753f4e Revert "Merge branch 'devKA' into devka (resolve conflicts)"
Some checks are pending
ci/woodpecker/push/bump_version Pipeline is pending
ci/woodpecker/push/fx_oracle Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/frontend Pipeline is running
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline is running
This reverts commit 5f4184760d, reversing
changes made to 5e1da9617f.

Reverting changes on main
2025-12-04 15:38:01 +03:00
Arseni
5f4184760d Merge branch 'devKA' into devka (resolve conflicts)
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline failed
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version unknown status
2025-12-04 14:08:32 +03:00
Stephan D
5e1da9617f +address book service
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
2025-12-01 21:20:10 +01:00
Arseni
9c16e27645 Fixed issue with wallet form feild and made page selecor in dashboard into a router 2025-11-27 18:07:28 +03:00
Stephan D
c4d34c5663 added wallet management localizations
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
2025-11-26 23:25:31 +01:00
Stephan D
34420ca2fb +gas estimation
Some checks failed
ci/woodpecker/push/db Pipeline is pending
ci/woodpecker/push/frontend Pipeline is pending
ci/woodpecker/push/fx_ingestor Pipeline is pending
ci/woodpecker/push/fx_oracle Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline failed
ci/woodpecker/push/bump_version unknown status
ci/woodpecker/push/bff Pipeline failed
2025-11-26 23:19:29 +01:00
Stephan D
d16703197d onchain balance getter implementation
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
2025-11-26 22:25:15 +01:00
Stephan D
35897f9aa1 impreoved commands logging
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
2025-11-26 21:45:38 +01:00
Stephan D
f59ee55084 removed untranslated.txt from repo
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
2025-11-26 19:36:47 +01:00
Stephan D
8bf86c5c93 removed obsolete files
Some checks failed
ci/woodpecker/push/bump_version unknown status
ci/woodpecker/push/billing_fees Pipeline failed
ci/woodpecker/push/chain_gateway Pipeline is pending
ci/woodpecker/push/db Pipeline is pending
ci/woodpecker/push/frontend Pipeline is pending
ci/woodpecker/push/fx_ingestor Pipeline is pending
ci/woodpecker/push/fx_oracle Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/bff Pipeline failed
2025-11-26 19:35:35 +01:00
Stephan D
5e8ff2adb7 removed CORS restrictions
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/bump_version unknown status
ci/woodpecker/push/payments_orchestrator Pipeline failed
2025-11-26 19:02:30 +01:00
Stephan D
da57b1d2e0 fixed providers initialization
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
2025-11-26 18:17:30 +01:00
2ef9ac24a1 Merge pull request 'Added Localizations and ran small fixes' (#6) from devKA into main
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
Reviewed-on: #6
2025-11-26 14:00:56 +00:00
Stephan D
44446c6ad4 conflicts resolution 2025-11-26 15:00:21 +01:00
Arseni
357af99564 Added account permissions and ui for recipient 2025-11-26 13:03:52 +03:00
Stephan D
48ccbb1c82 implemented backend wallet service connection
Some checks failed
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
2025-11-26 00:48:00 +01:00
Stephan D
68f0a1048f implemented backend wallets/ledger accounts listing 2025-11-25 23:38:10 +01:00
Stephan D
be913bf96c + logout connected 2025-11-25 21:37:22 +01:00
Stephan D
8e1d4bef59 fixed account provider dependencies
Some checks failed
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/notification Pipeline failed
ci/woodpecker/push/payments_orchestrator Pipeline failed
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/bump_version unknown status
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
2025-11-25 19:25:07 +01:00
Stephan D
c6da138184 fixed account provider dependencies
Some checks failed
ci/woodpecker/push/fx_ingestor Pipeline failed
ci/woodpecker/push/bump_version unknown status
ci/woodpecker/push/fx_oracle Pipeline failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/frontend Pipeline failed
2025-11-25 19:04:39 +01:00
Stephan D
d78619bccf improved logging
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
2025-11-25 18:30:53 +01:00
Stephan D
85b780b57e check other approach
Some checks failed
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
2025-11-25 17:45:47 +01:00
Stephan D
1f31fedc3a excessive provider removed
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
2025-11-25 12:40:39 +01:00
Stephan D
bdf3a01f80 fixed paths
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
2025-11-25 11:57:27 +01:00
Stephan D
d126d5d5de fixed service name for verfication
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
2025-11-25 11:06:40 +01:00
Arseni
fcb5ab4f2c Added Localizations and ran small fixes 2025-11-25 08:20:09 +03:00
Stephan D
26a1e284b2 Rewired login confirmation
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
2025-11-25 00:46:11 +01:00
Stephan D
fc0600d6c4 fix
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
2025-11-24 23:16:13 +01:00
Stephan D
b855404999 new message type
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline failed
ci/woodpecker/push/frontend Pipeline failed
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version unknown status
2025-11-24 21:49:02 +01:00
Stephan D
e3a8fb4f2d mail templates
Some checks failed
ci/woodpecker/push/db Pipeline is pending
ci/woodpecker/push/frontend Pipeline is pending
ci/woodpecker/push/fx_ingestor Pipeline is pending
ci/woodpecker/push/fx_oracle Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline failed
ci/woodpecker/push/bump_version unknown status
ci/woodpecker/push/chain_gateway Pipeline failed
2025-11-24 21:42:06 +01:00
Stephan D
d65e442cb6 + token verification
Some checks failed
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline failed
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version unknown status
2025-11-24 20:53:42 +01:00
Stephan D
b4f6f63871 fixed mail server connection string
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
2025-11-24 19:44:55 +01:00
Stephan D
803683be7c fixed change order
Some checks failed
ci/woodpecker/push/fx_ingestor Pipeline is pending
ci/woodpecker/push/fx_oracle Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline failed
ci/woodpecker/push/bump_version unknown status
ci/woodpecker/push/frontend Pipeline failed
2025-11-24 19:32:04 +01:00
Stephan D
72271cfc9a migration to replicaset connection
Some checks failed
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline failed
ci/woodpecker/push/bump_version unknown status
ci/woodpecker/push/frontend Pipeline failed
2025-11-24 19:10:07 +01:00
Stephan D
cd79355e69 fixed org ref setting
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
2025-11-24 15:18:31 +01:00
Stephan D
3f84f8c609 fixed org creation org referencing
Some checks failed
ci/woodpecker/push/db Pipeline is pending
ci/woodpecker/push/frontend Pipeline is pending
ci/woodpecker/push/fx_ingestor Pipeline is pending
ci/woodpecker/push/fx_oracle Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline failed
ci/woodpecker/push/bump_version unknown status
ci/woodpecker/push/bff Pipeline failed
2025-11-24 15:11:47 +01:00
Stephan D
ae15e1887b better error checks
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline is pending
ci/woodpecker/push/fx_oracle Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/frontend Pipeline failed
ci/woodpecker/push/bump_version unknown status
ci/woodpecker/push/chain_gateway Pipeline failed
2025-11-24 15:03:10 +01:00
Stephan D
8a41785b1d better logging in the pkg
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
2025-11-24 14:15:37 +01:00
Stephan D
56abc10dce version bump + loggins
Some checks failed
ci/woodpecker/push/fx_oracle Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline failed
ci/woodpecker/push/bump_version unknown status
ci/woodpecker/push/frontend Pipeline failed
2025-11-24 13:57:50 +01:00
d8a3a5550d Merge pull request 'Multiple Wallet support, history of each wallet and updated payment page' (#5) from devKA into main
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
Reviewed-on: #5
2025-11-23 14:52:06 +00:00
Stephan D
72d8da1fe8 removed dev file 2025-11-23 15:50:46 +01:00
Stephan D
1fcd77cd95 fixes 2025-11-23 15:49:24 +01:00
293 changed files with 6653 additions and 2179 deletions

6
.gitignore vendored
View File

@@ -3,3 +3,9 @@
*.pb.gw.go *.pb.gw.go
pubspec.lock pubspec.lock
.DS_Store .DS_Store
analysis_options.yaml
devtools_options.yaml
untranslated.txt
generate_protos.sh
update_dep.sh
.vscode/

48
.vscode/launch.json vendored
View File

@@ -1,48 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "pshared",
"cwd": "frontend/pshared",
"request": "launch",
"type": "dart"
},
{
"name": "pshared (profile mode)",
"cwd": "frontend/pshared",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "pshared (release mode)",
"cwd": "frontend/pshared",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "pweb",
"cwd": "frontend/pweb",
"request": "launch",
"type": "dart"
},
{
"name": "pweb (profile mode)",
"cwd": "frontend/pweb",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "pweb (release mode)",
"cwd": "frontend/pweb",
"request": "launch",
"type": "dart",
"flutterMode": "release"
}
]
}

View File

@@ -1,9 +0,0 @@
{
"yaml.schemas": {
"https://raw.githubusercontent.com/woodpecker-ci/woodpecker/main/pipeline/frontend/yaml/linter/schema/schema.json": [
".woodpecker/*.yml",
".woodpecker.yml",
"woodpecker.yml"
]
}
}

View File

@@ -1,33 +0,0 @@
depends_on:
- bff
- billing_fees
- chain_gateway
- db
- frontend
- fx_ingestor
- fx_oracle
- ledger
- nats
- notification
- payments_orchestrator
when:
event: push
branch: main
steps:
- name: bump-version
image: alpine:latest
environment:
GIT_AUTHOR_NAME: woodpecker
GIT_AUTHOR_EMAIL: ci@sendico.io
GIT_COMMITTER_NAME: woodpecker
GIT_COMMITTER_EMAIL: ci@sendico.io
commands:
- set -euo pipefail
- apk add --no-cache git
# make sure git knows who commits
- git config user.name "$GIT_AUTHOR_NAME"
- git config user.email "$GIT_AUTHOR_EMAIL"
# run your script (must do commit + push)
- sh ci/scripts/common/bump_version.sh

View File

@@ -1,11 +1,11 @@
matrix: matrix:
include: include:
- CHAIN_GATEWAY_IMAGE_PATH: chain/gateway - CHAIN_GATEWAY_IMAGE_PATH: gateway/chain
CHAIN_GATEWAY_DOCKERFILE: ci/prod/compose/chain_gateway.dockerfile CHAIN_GATEWAY_DOCKERFILE: ci/prod/compose/chain_gateway.dockerfile
CHAIN_GATEWAY_MONGO_SECRET_PATH: sendico/db CHAIN_GATEWAY_MONGO_SECRET_PATH: sendico/db
CHAIN_GATEWAY_RPC_SECRET_PATH: sendico/chain/gateway CHAIN_GATEWAY_RPC_SECRET_PATH: sendico/gateway/chain
CHAIN_GATEWAY_WALLET_SECRET_PATH: sendico/chain/gateway/wallet CHAIN_GATEWAY_WALLET_SECRET_PATH: sendico/gateway/chain/wallet
CHAIN_GATEWAY_VAULT_SECRET_PATH: sendico/chain/gateway/vault CHAIN_GATEWAY_VAULT_SECRET_PATH: sendico/gateway/chain/vault
CHAIN_GATEWAY_ENV: prod CHAIN_GATEWAY_ENV: prod
when: when:

View File

@@ -25,21 +25,21 @@ require (
github.com/go-chi/chi/v5 v5.2.3 // indirect github.com/go-chi/chi/v5 v5.2.3 // indirect
github.com/golang/snappy v1.0.0 // indirect github.com/golang/snappy v1.0.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect github.com/klauspost/compress v1.18.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect github.com/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.47.0 // indirect github.com/nats-io/nats.go v1.47.0 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nkeys v0.4.12 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect github.com/prometheus/procfs v0.19.2 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
@@ -49,6 +49,6 @@ require (
golang.org/x/sync v0.18.0 // indirect golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect golang.org/x/text v0.31.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/protobuf v1.36.10 google.golang.org/protobuf v1.36.10
) )

View File

@@ -59,8 +59,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -97,8 +97,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM= github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -141,8 +141,8 @@ github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfj
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
@@ -212,8 +212,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= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=

View File

@@ -1,83 +0,0 @@
package client
import (
"context"
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
)
// Fake implements Client for tests.
type Fake struct {
CreateManagedWalletFn func(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error)
GetManagedWalletFn func(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) (*gatewayv1.GetManagedWalletResponse, error)
ListManagedWalletsFn func(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) (*gatewayv1.ListManagedWalletsResponse, error)
GetWalletBalanceFn func(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) (*gatewayv1.GetWalletBalanceResponse, error)
SubmitTransferFn func(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error)
GetTransferFn func(ctx context.Context, req *gatewayv1.GetTransferRequest) (*gatewayv1.GetTransferResponse, error)
ListTransfersFn func(ctx context.Context, req *gatewayv1.ListTransfersRequest) (*gatewayv1.ListTransfersResponse, error)
EstimateTransferFeeFn func(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) (*gatewayv1.EstimateTransferFeeResponse, error)
CloseFn func() error
}
func (f *Fake) CreateManagedWallet(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error) {
if f.CreateManagedWalletFn != nil {
return f.CreateManagedWalletFn(ctx, req)
}
return &gatewayv1.CreateManagedWalletResponse{}, nil
}
func (f *Fake) GetManagedWallet(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) (*gatewayv1.GetManagedWalletResponse, error) {
if f.GetManagedWalletFn != nil {
return f.GetManagedWalletFn(ctx, req)
}
return &gatewayv1.GetManagedWalletResponse{}, nil
}
func (f *Fake) ListManagedWallets(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) (*gatewayv1.ListManagedWalletsResponse, error) {
if f.ListManagedWalletsFn != nil {
return f.ListManagedWalletsFn(ctx, req)
}
return &gatewayv1.ListManagedWalletsResponse{}, nil
}
func (f *Fake) GetWalletBalance(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) (*gatewayv1.GetWalletBalanceResponse, error) {
if f.GetWalletBalanceFn != nil {
return f.GetWalletBalanceFn(ctx, req)
}
return &gatewayv1.GetWalletBalanceResponse{}, nil
}
func (f *Fake) SubmitTransfer(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error) {
if f.SubmitTransferFn != nil {
return f.SubmitTransferFn(ctx, req)
}
return &gatewayv1.SubmitTransferResponse{}, nil
}
func (f *Fake) GetTransfer(ctx context.Context, req *gatewayv1.GetTransferRequest) (*gatewayv1.GetTransferResponse, error) {
if f.GetTransferFn != nil {
return f.GetTransferFn(ctx, req)
}
return &gatewayv1.GetTransferResponse{}, nil
}
func (f *Fake) ListTransfers(ctx context.Context, req *gatewayv1.ListTransfersRequest) (*gatewayv1.ListTransfersResponse, error) {
if f.ListTransfersFn != nil {
return f.ListTransfersFn(ctx, req)
}
return &gatewayv1.ListTransfersResponse{}, nil
}
func (f *Fake) EstimateTransferFee(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) (*gatewayv1.EstimateTransferFeeResponse, error) {
if f.EstimateTransferFeeFn != nil {
return f.EstimateTransferFeeFn(ctx, req)
}
return &gatewayv1.EstimateTransferFeeResponse{}, nil
}
func (f *Fake) Close() error {
if f.CloseFn != nil {
return f.CloseFn()
}
return nil
}

View File

@@ -1,21 +0,0 @@
package gateway
import moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
func cloneMoney(m *moneyv1.Money) *moneyv1.Money {
if m == nil {
return nil
}
return &moneyv1.Money{Amount: m.GetAmount(), Currency: m.GetCurrency()}
}
func cloneMetadata(input map[string]string) map[string]string {
if len(input) == 0 {
return nil
}
clone := make(map[string]string, len(input))
for k, v := range input {
clone[k] = v
}
return clone
}

View File

@@ -1,214 +0,0 @@
package gateway
import (
"context"
"strings"
"github.com/tech/sendico/chain/gateway/internal/keymanager"
"github.com/tech/sendico/chain/gateway/storage"
"github.com/tech/sendico/chain/gateway/storage/model"
"github.com/tech/sendico/pkg/api/routers"
"github.com/tech/sendico/pkg/api/routers/gsresponse"
clockpkg "github.com/tech/sendico/pkg/clock"
msg "github.com/tech/sendico/pkg/messaging"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
"go.mongodb.org/mongo-driver/bson/primitive"
"google.golang.org/grpc"
)
type serviceError string
func (e serviceError) Error() string {
return string(e)
}
var (
errStorageUnavailable = serviceError("chain_gateway: storage not initialised")
)
// Service implements the ChainGatewayService RPC contract.
type Service struct {
logger mlogger.Logger
storage storage.Repository
producer msg.Producer
clock clockpkg.Clock
networks map[string]Network
serviceWallet ServiceWallet
keyManager keymanager.Manager
executor TransferExecutor
gatewayv1.UnimplementedChainGatewayServiceServer
}
// NewService constructs the chain gateway service skeleton.
func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Producer, opts ...Option) *Service {
svc := &Service{
logger: logger.Named("chain_gateway"),
storage: repo,
producer: producer,
clock: clockpkg.System{},
networks: map[string]Network{},
}
initMetrics()
for _, opt := range opts {
if opt != nil {
opt(svc)
}
}
if svc.clock == nil {
svc.clock = clockpkg.System{}
}
if svc.networks == nil {
svc.networks = map[string]Network{}
}
return svc
}
// Register wires the service onto the provided gRPC router.
func (s *Service) Register(router routers.GRPC) error {
return router.Register(func(reg grpc.ServiceRegistrar) {
gatewayv1.RegisterChainGatewayServiceServer(reg, s)
})
}
func (s *Service) CreateManagedWallet(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error) {
return executeUnary(ctx, s, "CreateManagedWallet", s.createManagedWalletHandler, req)
}
func (s *Service) GetManagedWallet(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) (*gatewayv1.GetManagedWalletResponse, error) {
return executeUnary(ctx, s, "GetManagedWallet", s.getManagedWalletHandler, req)
}
func (s *Service) ListManagedWallets(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) (*gatewayv1.ListManagedWalletsResponse, error) {
return executeUnary(ctx, s, "ListManagedWallets", s.listManagedWalletsHandler, req)
}
func (s *Service) GetWalletBalance(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) (*gatewayv1.GetWalletBalanceResponse, error) {
return executeUnary(ctx, s, "GetWalletBalance", s.getWalletBalanceHandler, req)
}
func (s *Service) SubmitTransfer(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error) {
return executeUnary(ctx, s, "SubmitTransfer", s.submitTransferHandler, req)
}
func (s *Service) GetTransfer(ctx context.Context, req *gatewayv1.GetTransferRequest) (*gatewayv1.GetTransferResponse, error) {
return executeUnary(ctx, s, "GetTransfer", s.getTransferHandler, req)
}
func (s *Service) ListTransfers(ctx context.Context, req *gatewayv1.ListTransfersRequest) (*gatewayv1.ListTransfersResponse, error) {
return executeUnary(ctx, s, "ListTransfers", s.listTransfersHandler, req)
}
func (s *Service) EstimateTransferFee(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) (*gatewayv1.EstimateTransferFeeResponse, error) {
return executeUnary(ctx, s, "EstimateTransferFee", s.estimateTransferFeeHandler, req)
}
func (s *Service) ensureRepository(ctx context.Context) error {
if s.storage == nil {
return errStorageUnavailable
}
return s.storage.Ping(ctx)
}
func executeUnary[TReq any, TResp any](ctx context.Context, svc *Service, method string, handler func(context.Context, *TReq) gsresponse.Responder[TResp], req *TReq) (*TResp, error) {
start := svc.clock.Now()
resp, err := gsresponse.Unary(svc.logger, mservice.ChainGateway, handler)(ctx, req)
observeRPC(method, err, svc.clock.Now().Sub(start))
return resp, err
}
func resolveContractAddress(tokens []TokenContract, symbol string) string {
upper := strings.ToUpper(symbol)
for _, token := range tokens {
if strings.EqualFold(token.Symbol, upper) && token.ContractAddress != "" {
return strings.ToLower(token.ContractAddress)
}
}
return ""
}
func generateWalletRef() string {
return primitive.NewObjectID().Hex()
}
func generateTransferRef() string {
return primitive.NewObjectID().Hex()
}
func chainKeyFromEnum(chain gatewayv1.ChainNetwork) (string, gatewayv1.ChainNetwork) {
if name, ok := gatewayv1.ChainNetwork_name[int32(chain)]; ok {
key := strings.ToLower(strings.TrimPrefix(name, "CHAIN_NETWORK_"))
return key, chain
}
return "", gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
}
func chainEnumFromName(name string) gatewayv1.ChainNetwork {
if name == "" {
return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
}
upper := strings.ToUpper(strings.ReplaceAll(strings.ReplaceAll(name, " ", "_"), "-", "_"))
key := "CHAIN_NETWORK_" + upper
if val, ok := gatewayv1.ChainNetwork_value[key]; ok {
return gatewayv1.ChainNetwork(val)
}
return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
}
func managedWalletStatusToProto(status model.ManagedWalletStatus) gatewayv1.ManagedWalletStatus {
switch status {
case model.ManagedWalletStatusActive:
return gatewayv1.ManagedWalletStatus_MANAGED_WALLET_ACTIVE
case model.ManagedWalletStatusSuspended:
return gatewayv1.ManagedWalletStatus_MANAGED_WALLET_SUSPENDED
case model.ManagedWalletStatusClosed:
return gatewayv1.ManagedWalletStatus_MANAGED_WALLET_CLOSED
default:
return gatewayv1.ManagedWalletStatus_MANAGED_WALLET_STATUS_UNSPECIFIED
}
}
func transferStatusToModel(status gatewayv1.TransferStatus) model.TransferStatus {
switch status {
case gatewayv1.TransferStatus_TRANSFER_PENDING:
return model.TransferStatusPending
case gatewayv1.TransferStatus_TRANSFER_SIGNING:
return model.TransferStatusSigning
case gatewayv1.TransferStatus_TRANSFER_SUBMITTED:
return model.TransferStatusSubmitted
case gatewayv1.TransferStatus_TRANSFER_CONFIRMED:
return model.TransferStatusConfirmed
case gatewayv1.TransferStatus_TRANSFER_FAILED:
return model.TransferStatusFailed
case gatewayv1.TransferStatus_TRANSFER_CANCELLED:
return model.TransferStatusCancelled
default:
return ""
}
}
func transferStatusToProto(status model.TransferStatus) gatewayv1.TransferStatus {
switch status {
case model.TransferStatusPending:
return gatewayv1.TransferStatus_TRANSFER_PENDING
case model.TransferStatusSigning:
return gatewayv1.TransferStatus_TRANSFER_SIGNING
case model.TransferStatusSubmitted:
return gatewayv1.TransferStatus_TRANSFER_SUBMITTED
case model.TransferStatusConfirmed:
return gatewayv1.TransferStatus_TRANSFER_CONFIRMED
case model.TransferStatusFailed:
return gatewayv1.TransferStatus_TRANSFER_FAILED
case model.TransferStatusCancelled:
return gatewayv1.TransferStatus_TRANSFER_CANCELLED
default:
return gatewayv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
}
}

View File

@@ -1,309 +0,0 @@
package gateway
import (
"context"
"errors"
"strings"
"github.com/shopspring/decimal"
"github.com/tech/sendico/chain/gateway/storage/model"
"github.com/tech/sendico/pkg/api/routers/gsresponse"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mservice"
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
"go.uber.org/zap"
"google.golang.org/protobuf/types/known/timestamppb"
)
func (s *Service) submitTransferHandler(ctx context.Context, req *gatewayv1.SubmitTransferRequest) gsresponse.Responder[gatewayv1.SubmitTransferResponse] {
if err := s.ensureRepository(ctx); err != nil {
return gsresponse.Unavailable[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
}
if req == nil {
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
}
idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey())
if idempotencyKey == "" {
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required"))
}
organizationRef := strings.TrimSpace(req.GetOrganizationRef())
if organizationRef == "" {
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref is required"))
}
sourceWalletRef := strings.TrimSpace(req.GetSourceWalletRef())
if sourceWalletRef == "" {
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("source_wallet_ref is required"))
}
amount := req.GetAmount()
if amount == nil {
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("amount is required"))
}
amountCurrency := strings.ToUpper(strings.TrimSpace(amount.GetCurrency()))
if amountCurrency == "" {
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("amount.currency is required"))
}
amountValue := strings.TrimSpace(amount.GetAmount())
if amountValue == "" {
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("amount.amount is required"))
}
sourceWallet, err := s.storage.Wallets().Get(ctx, sourceWalletRef)
if err != nil {
if errors.Is(err, merrors.ErrNoData) {
return gsresponse.NotFound[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
}
return gsresponse.Auto[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
}
if !strings.EqualFold(sourceWallet.OrganizationRef, organizationRef) {
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref mismatch with wallet"))
}
networkKey := strings.ToLower(strings.TrimSpace(sourceWallet.Network))
networkCfg, ok := s.networks[networkKey]
if !ok {
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet"))
}
destination, err := s.resolveDestination(ctx, req.GetDestination(), sourceWallet)
if err != nil {
if errors.Is(err, merrors.ErrNoData) {
return gsresponse.NotFound[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
}
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
}
fees, feeSum, err := convertFees(req.GetFees(), amountCurrency)
if err != nil {
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
}
amountDec, err := decimal.NewFromString(amountValue)
if err != nil {
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("invalid amount"))
}
netDec := amountDec.Sub(feeSum)
if netDec.IsNegative() {
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("fees exceed amount"))
}
netAmount := cloneMoney(amount)
netAmount.Amount = netDec.String()
transfer := &model.Transfer{
IdempotencyKey: idempotencyKey,
TransferRef: generateTransferRef(),
OrganizationRef: organizationRef,
SourceWalletRef: sourceWalletRef,
Destination: destination,
Network: sourceWallet.Network,
TokenSymbol: sourceWallet.TokenSymbol,
ContractAddress: sourceWallet.ContractAddress,
RequestedAmount: cloneMoney(amount),
NetAmount: netAmount,
Fees: fees,
Status: model.TransferStatusPending,
ClientReference: strings.TrimSpace(req.GetClientReference()),
LastStatusAt: s.clock.Now().UTC(),
}
saved, err := s.storage.Transfers().Create(ctx, transfer)
if err != nil {
if errors.Is(err, merrors.ErrDataConflict) {
s.logger.Debug("transfer already exists", zap.String("transfer_ref", transfer.TransferRef), zap.String("idempotency_key", idempotencyKey))
return gsresponse.Success(&gatewayv1.SubmitTransferResponse{Transfer: s.toProtoTransfer(saved)})
}
return gsresponse.Auto[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
}
if s.executor != nil {
s.launchTransferExecution(saved.TransferRef, sourceWalletRef, networkCfg)
}
return gsresponse.Success(&gatewayv1.SubmitTransferResponse{Transfer: s.toProtoTransfer(saved)})
}
func (s *Service) getTransferHandler(ctx context.Context, req *gatewayv1.GetTransferRequest) gsresponse.Responder[gatewayv1.GetTransferResponse] {
if err := s.ensureRepository(ctx); err != nil {
return gsresponse.Unavailable[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, err)
}
if req == nil {
return gsresponse.InvalidArgument[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
}
transferRef := strings.TrimSpace(req.GetTransferRef())
if transferRef == "" {
return gsresponse.InvalidArgument[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("transfer_ref is required"))
}
transfer, err := s.storage.Transfers().Get(ctx, transferRef)
if err != nil {
if errors.Is(err, merrors.ErrNoData) {
return gsresponse.NotFound[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, err)
}
return gsresponse.Auto[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, err)
}
return gsresponse.Success(&gatewayv1.GetTransferResponse{Transfer: s.toProtoTransfer(transfer)})
}
func (s *Service) listTransfersHandler(ctx context.Context, req *gatewayv1.ListTransfersRequest) gsresponse.Responder[gatewayv1.ListTransfersResponse] {
if err := s.ensureRepository(ctx); err != nil {
return gsresponse.Unavailable[gatewayv1.ListTransfersResponse](s.logger, mservice.ChainGateway, err)
}
filter := model.TransferFilter{}
if req != nil {
filter.SourceWalletRef = strings.TrimSpace(req.GetSourceWalletRef())
filter.DestinationWalletRef = strings.TrimSpace(req.GetDestinationWalletRef())
if status := transferStatusToModel(req.GetStatus()); status != "" {
filter.Status = status
}
if page := req.GetPage(); page != nil {
filter.Cursor = strings.TrimSpace(page.GetCursor())
filter.Limit = page.GetLimit()
}
}
result, err := s.storage.Transfers().List(ctx, filter)
if err != nil {
return gsresponse.Auto[gatewayv1.ListTransfersResponse](s.logger, mservice.ChainGateway, err)
}
protoTransfers := make([]*gatewayv1.Transfer, 0, len(result.Items))
for _, transfer := range result.Items {
protoTransfers = append(protoTransfers, s.toProtoTransfer(transfer))
}
resp := &gatewayv1.ListTransfersResponse{
Transfers: protoTransfers,
Page: &paginationv1.CursorPageResponse{NextCursor: result.NextCursor},
}
return gsresponse.Success(resp)
}
func (s *Service) estimateTransferFeeHandler(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) gsresponse.Responder[gatewayv1.EstimateTransferFeeResponse] {
if err := s.ensureRepository(ctx); err != nil {
return gsresponse.Unavailable[gatewayv1.EstimateTransferFeeResponse](s.logger, mservice.ChainGateway, err)
}
if req == nil || req.GetAmount() == nil {
return gsresponse.InvalidArgument[gatewayv1.EstimateTransferFeeResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("amount is required"))
}
currency := req.GetAmount().GetCurrency()
fee := &moneyv1.Money{
Currency: currency,
Amount: "0",
}
resp := &gatewayv1.EstimateTransferFeeResponse{
NetworkFee: fee,
EstimationContext: "not_implemented",
}
return gsresponse.Success(resp)
}
func (s *Service) toProtoTransfer(transfer *model.Transfer) *gatewayv1.Transfer {
if transfer == nil {
return nil
}
destination := &gatewayv1.TransferDestination{}
if transfer.Destination.ManagedWalletRef != "" {
destination.Destination = &gatewayv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: transfer.Destination.ManagedWalletRef}
} else if transfer.Destination.ExternalAddress != "" {
destination.Destination = &gatewayv1.TransferDestination_ExternalAddress{ExternalAddress: transfer.Destination.ExternalAddress}
}
destination.Memo = transfer.Destination.Memo
protoFees := make([]*gatewayv1.ServiceFeeBreakdown, 0, len(transfer.Fees))
for _, fee := range transfer.Fees {
protoFees = append(protoFees, &gatewayv1.ServiceFeeBreakdown{
FeeCode: fee.FeeCode,
Amount: cloneMoney(fee.Amount),
Description: fee.Description,
})
}
asset := &gatewayv1.Asset{
Chain: chainEnumFromName(transfer.Network),
TokenSymbol: transfer.TokenSymbol,
ContractAddress: transfer.ContractAddress,
}
return &gatewayv1.Transfer{
TransferRef: transfer.TransferRef,
IdempotencyKey: transfer.IdempotencyKey,
OrganizationRef: transfer.OrganizationRef,
SourceWalletRef: transfer.SourceWalletRef,
Destination: destination,
Asset: asset,
RequestedAmount: cloneMoney(transfer.RequestedAmount),
NetAmount: cloneMoney(transfer.NetAmount),
Fees: protoFees,
Status: transferStatusToProto(transfer.Status),
TransactionHash: transfer.TxHash,
FailureReason: transfer.FailureReason,
CreatedAt: timestamppb.New(transfer.CreatedAt.UTC()),
UpdatedAt: timestamppb.New(transfer.UpdatedAt.UTC()),
}
}
func (s *Service) resolveDestination(ctx context.Context, dest *gatewayv1.TransferDestination, source *model.ManagedWallet) (model.TransferDestination, error) {
if dest == nil {
return model.TransferDestination{}, merrors.InvalidArgument("destination is required")
}
managedRef := strings.TrimSpace(dest.GetManagedWalletRef())
external := strings.TrimSpace(dest.GetExternalAddress())
if managedRef != "" && external != "" {
return model.TransferDestination{}, merrors.InvalidArgument("destination must be managed_wallet_ref or external_address")
}
if managedRef != "" {
wallet, err := s.storage.Wallets().Get(ctx, managedRef)
if err != nil {
return model.TransferDestination{}, err
}
if !strings.EqualFold(wallet.Network, source.Network) {
return model.TransferDestination{}, merrors.InvalidArgument("destination wallet network mismatch")
}
if strings.TrimSpace(wallet.DepositAddress) == "" {
return model.TransferDestination{}, merrors.InvalidArgument("destination wallet missing deposit address")
}
return model.TransferDestination{
ManagedWalletRef: wallet.WalletRef,
Memo: strings.TrimSpace(dest.GetMemo()),
}, nil
}
if external == "" {
return model.TransferDestination{}, merrors.InvalidArgument("destination is required")
}
return model.TransferDestination{
ExternalAddress: strings.ToLower(external),
Memo: strings.TrimSpace(dest.GetMemo()),
}, nil
}
func convertFees(fees []*gatewayv1.ServiceFeeBreakdown, currency string) ([]model.ServiceFee, decimal.Decimal, error) {
result := make([]model.ServiceFee, 0, len(fees))
sum := decimal.NewFromInt(0)
for _, fee := range fees {
if fee == nil || fee.GetAmount() == nil {
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount is required")
}
amtCurrency := strings.ToUpper(strings.TrimSpace(fee.GetAmount().GetCurrency()))
if amtCurrency != strings.ToUpper(currency) {
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee currency mismatch")
}
amtValue := strings.TrimSpace(fee.GetAmount().GetAmount())
if amtValue == "" {
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount is required")
}
dec, err := decimal.NewFromString(amtValue)
if err != nil {
return nil, decimal.Decimal{}, merrors.InvalidArgument("invalid fee amount")
}
if dec.IsNegative() {
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount must be non-negative")
}
sum = sum.Add(dec)
result = append(result, model.ServiceFee{
FeeCode: strings.TrimSpace(fee.GetFeeCode()),
Amount: cloneMoney(fee.GetAmount()),
Description: strings.TrimSpace(fee.GetDescription()),
})
}
return result, sum, nil
}

View File

@@ -1,213 +0,0 @@
package gateway
import (
"context"
"errors"
"strings"
"github.com/tech/sendico/chain/gateway/storage/model"
"github.com/tech/sendico/pkg/api/routers/gsresponse"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mservice"
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
"go.uber.org/zap"
"google.golang.org/protobuf/types/known/timestamppb"
)
func (s *Service) createManagedWalletHandler(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) gsresponse.Responder[gatewayv1.CreateManagedWalletResponse] {
if err := s.ensureRepository(ctx); err != nil {
return gsresponse.Unavailable[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, err)
}
if req == nil {
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
}
idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey())
if idempotencyKey == "" {
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required"))
}
organizationRef := strings.TrimSpace(req.GetOrganizationRef())
if organizationRef == "" {
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref is required"))
}
ownerRef := strings.TrimSpace(req.GetOwnerRef())
if ownerRef == "" {
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("owner_ref is required"))
}
asset := req.GetAsset()
if asset == nil {
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("asset is required"))
}
chainKey, _ := chainKeyFromEnum(asset.GetChain())
if chainKey == "" {
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
}
networkCfg, ok := s.networks[chainKey]
if !ok {
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
}
tokenSymbol := strings.ToUpper(strings.TrimSpace(asset.GetTokenSymbol()))
if tokenSymbol == "" {
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("asset.token_symbol is required"))
}
contractAddress := strings.ToLower(strings.TrimSpace(asset.GetContractAddress()))
if contractAddress == "" {
contractAddress = resolveContractAddress(networkCfg.TokenConfigs, tokenSymbol)
if contractAddress == "" {
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported token for chain"))
}
}
walletRef := generateWalletRef()
if s.keyManager == nil {
return gsresponse.Internal[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.Internal("key manager not configured"))
}
keyInfo, err := s.keyManager.CreateManagedWalletKey(ctx, walletRef, chainKey)
if err != nil {
return gsresponse.Auto[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, err)
}
if keyInfo == nil || strings.TrimSpace(keyInfo.Address) == "" {
return gsresponse.Internal[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.Internal("key manager returned empty address"))
}
wallet := &model.ManagedWallet{
IdempotencyKey: idempotencyKey,
WalletRef: walletRef,
OrganizationRef: organizationRef,
OwnerRef: ownerRef,
Network: chainKey,
TokenSymbol: tokenSymbol,
ContractAddress: contractAddress,
DepositAddress: strings.ToLower(keyInfo.Address),
KeyReference: keyInfo.KeyID,
Status: model.ManagedWalletStatusActive,
Metadata: cloneMetadata(req.GetMetadata()),
}
created, err := s.storage.Wallets().Create(ctx, wallet)
if err != nil {
if errors.Is(err, merrors.ErrDataConflict) {
s.logger.Debug("wallet already exists", zap.String("wallet_ref", walletRef), zap.String("idempotency_key", idempotencyKey))
return gsresponse.Success(&gatewayv1.CreateManagedWalletResponse{Wallet: s.toProtoManagedWallet(created)})
}
return gsresponse.Auto[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, err)
}
return gsresponse.Success(&gatewayv1.CreateManagedWalletResponse{Wallet: s.toProtoManagedWallet(created)})
}
func (s *Service) getManagedWalletHandler(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) gsresponse.Responder[gatewayv1.GetManagedWalletResponse] {
if err := s.ensureRepository(ctx); err != nil {
return gsresponse.Unavailable[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, err)
}
if req == nil {
return gsresponse.InvalidArgument[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
}
walletRef := strings.TrimSpace(req.GetWalletRef())
if walletRef == "" {
return gsresponse.InvalidArgument[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required"))
}
wallet, err := s.storage.Wallets().Get(ctx, walletRef)
if err != nil {
if errors.Is(err, merrors.ErrNoData) {
return gsresponse.NotFound[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, err)
}
return gsresponse.Auto[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, err)
}
return gsresponse.Success(&gatewayv1.GetManagedWalletResponse{Wallet: s.toProtoManagedWallet(wallet)})
}
func (s *Service) listManagedWalletsHandler(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) gsresponse.Responder[gatewayv1.ListManagedWalletsResponse] {
if err := s.ensureRepository(ctx); err != nil {
return gsresponse.Unavailable[gatewayv1.ListManagedWalletsResponse](s.logger, mservice.ChainGateway, err)
}
filter := model.ManagedWalletFilter{}
if req != nil {
filter.OrganizationRef = strings.TrimSpace(req.GetOrganizationRef())
filter.OwnerRef = strings.TrimSpace(req.GetOwnerRef())
if asset := req.GetAsset(); asset != nil {
filter.Network, _ = chainKeyFromEnum(asset.GetChain())
filter.TokenSymbol = strings.TrimSpace(asset.GetTokenSymbol())
}
if page := req.GetPage(); page != nil {
filter.Cursor = strings.TrimSpace(page.GetCursor())
filter.Limit = page.GetLimit()
}
}
result, err := s.storage.Wallets().List(ctx, filter)
if err != nil {
return gsresponse.Auto[gatewayv1.ListManagedWalletsResponse](s.logger, mservice.ChainGateway, err)
}
protoWallets := make([]*gatewayv1.ManagedWallet, 0, len(result.Items))
for _, wallet := range result.Items {
protoWallets = append(protoWallets, s.toProtoManagedWallet(wallet))
}
resp := &gatewayv1.ListManagedWalletsResponse{
Wallets: protoWallets,
Page: &paginationv1.CursorPageResponse{NextCursor: result.NextCursor},
}
return gsresponse.Success(resp)
}
func (s *Service) getWalletBalanceHandler(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) gsresponse.Responder[gatewayv1.GetWalletBalanceResponse] {
if err := s.ensureRepository(ctx); err != nil {
return gsresponse.Unavailable[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, err)
}
if req == nil {
return gsresponse.InvalidArgument[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
}
walletRef := strings.TrimSpace(req.GetWalletRef())
if walletRef == "" {
return gsresponse.InvalidArgument[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required"))
}
balance, err := s.storage.Wallets().GetBalance(ctx, walletRef)
if err != nil {
if errors.Is(err, merrors.ErrNoData) {
return gsresponse.NotFound[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, err)
}
return gsresponse.Auto[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, err)
}
return gsresponse.Success(&gatewayv1.GetWalletBalanceResponse{Balance: toProtoWalletBalance(balance)})
}
func (s *Service) toProtoManagedWallet(wallet *model.ManagedWallet) *gatewayv1.ManagedWallet {
if wallet == nil {
return nil
}
asset := &gatewayv1.Asset{
Chain: chainEnumFromName(wallet.Network),
TokenSymbol: wallet.TokenSymbol,
ContractAddress: wallet.ContractAddress,
}
return &gatewayv1.ManagedWallet{
WalletRef: wallet.WalletRef,
OrganizationRef: wallet.OrganizationRef,
OwnerRef: wallet.OwnerRef,
Asset: asset,
DepositAddress: wallet.DepositAddress,
Status: managedWalletStatusToProto(wallet.Status),
Metadata: cloneMetadata(wallet.Metadata),
CreatedAt: timestamppb.New(wallet.CreatedAt.UTC()),
UpdatedAt: timestamppb.New(wallet.UpdatedAt.UTC()),
}
}
func toProtoWalletBalance(balance *model.WalletBalance) *gatewayv1.WalletBalance {
if balance == nil {
return nil
}
return &gatewayv1.WalletBalance{
Available: cloneMoney(balance.Available),
PendingInbound: cloneMoney(balance.PendingInbound),
PendingOutbound: cloneMoney(balance.PendingOutbound),
CalculatedAt: timestamppb.New(balance.CalculatedAt.UTC()),
}
}

View File

@@ -25,20 +25,20 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/golang/snappy v1.0.0 // indirect github.com/golang/snappy v1.0.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect github.com/klauspost/compress v1.18.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect github.com/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.47.0 // indirect github.com/nats-io/nats.go v1.47.0 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nkeys v0.4.12 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect github.com/prometheus/procfs v0.19.2 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.mongodb.org/mongo-driver v1.17.6 // indirect go.mongodb.org/mongo-driver v1.17.6 // indirect
@@ -49,7 +49,7 @@ require (
golang.org/x/sync v0.18.0 // indirect golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect golang.org/x/text v0.31.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.77.0 // indirect google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect google.golang.org/protobuf v1.36.10 // indirect
) )

View File

@@ -59,8 +59,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -97,8 +97,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM= github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -141,8 +141,8 @@ github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfj
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
@@ -212,8 +212,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= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=

View File

@@ -27,20 +27,20 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/go-chi/chi/v5 v5.2.3 // indirect github.com/go-chi/chi/v5 v5.2.3 // indirect
github.com/golang/snappy v1.0.0 // indirect github.com/golang/snappy v1.0.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect github.com/klauspost/compress v1.18.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect github.com/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.47.0 // indirect github.com/nats-io/nats.go v1.47.0 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nkeys v0.4.12 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect github.com/prometheus/procfs v0.19.2 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
@@ -50,5 +50,5 @@ require (
golang.org/x/sync v0.18.0 // indirect golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect golang.org/x/text v0.31.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
) )

View File

@@ -59,8 +59,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -97,8 +97,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM= github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -141,8 +141,8 @@ github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfj
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
@@ -212,8 +212,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= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=

View File

@@ -17,11 +17,11 @@ require (
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
github.com/golang/snappy v1.0.0 // indirect github.com/golang/snappy v1.0.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect github.com/klauspost/compress v1.18.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect github.com/montanaflynn/stats v0.7.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect

View File

@@ -51,8 +51,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg= github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
@@ -109,8 +109,8 @@ github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfj
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=

View File

@@ -4,11 +4,11 @@ root = "./../.."
tmp_dir = "tmp" tmp_dir = "tmp"
[build] [build]
cmd = "go build -o app -ldflags \"-X 'github.com/tech/sendico/chain/gateway/internal/appversion.BuildUser=$(whoami)' -X 'github.com/tech/sendico/chain/gateway/internal/appversion.Version=$APP_V' -X 'github.com/tech/sendico/chain/gateway/internal/appversion.Branch=$BUILD_BRANCH' -X 'github.com/tech/sendico/chain/gateway/internal/appversion.Revision=$GIT_REV' -X 'github.com/tech/sendico/chain/gateway/internal/appversion.BuildDate=$(date)'\"" 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" bin = "./app"
full_bin = "./app --debug --config.file=config.yml" full_bin = "./app --debug --config.file=config.yml"
include_ext = ["go", "yaml", "yml"] include_ext = ["go", "yaml", "yml"]
exclude_dir = ["chain/gateway/tmp", "pkg/.git", "chain/gateway/env"] exclude_dir = ["gateway/chain/tmp", "pkg/.git", "gateway/chain/env"]
exclude_regex = ["_test\\.go"] exclude_regex = ["_test\\.go"]
exclude_unchanged = true exclude_unchanged = true
follow_symlink = true follow_symlink = true

View File

@@ -8,7 +8,7 @@ import (
"time" "time"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1" chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/credentials/insecure"
@@ -16,26 +16,26 @@ import (
// Client exposes typed helpers around the chain gateway gRPC API. // Client exposes typed helpers around the chain gateway gRPC API.
type Client interface { type Client interface {
CreateManagedWallet(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error) CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error)
GetManagedWallet(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) (*gatewayv1.GetManagedWalletResponse, error) GetManagedWallet(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error)
ListManagedWallets(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) (*gatewayv1.ListManagedWalletsResponse, error) ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error)
GetWalletBalance(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) (*gatewayv1.GetWalletBalanceResponse, error) GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error)
SubmitTransfer(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error) SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error)
GetTransfer(ctx context.Context, req *gatewayv1.GetTransferRequest) (*gatewayv1.GetTransferResponse, error) GetTransfer(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error)
ListTransfers(ctx context.Context, req *gatewayv1.ListTransfersRequest) (*gatewayv1.ListTransfersResponse, error) ListTransfers(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error)
EstimateTransferFee(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) (*gatewayv1.EstimateTransferFeeResponse, error) EstimateTransferFee(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error)
Close() error Close() error
} }
type grpcGatewayClient interface { type grpcGatewayClient interface {
CreateManagedWallet(ctx context.Context, in *gatewayv1.CreateManagedWalletRequest, opts ...grpc.CallOption) (*gatewayv1.CreateManagedWalletResponse, error) CreateManagedWallet(ctx context.Context, in *chainv1.CreateManagedWalletRequest, opts ...grpc.CallOption) (*chainv1.CreateManagedWalletResponse, error)
GetManagedWallet(ctx context.Context, in *gatewayv1.GetManagedWalletRequest, opts ...grpc.CallOption) (*gatewayv1.GetManagedWalletResponse, error) GetManagedWallet(ctx context.Context, in *chainv1.GetManagedWalletRequest, opts ...grpc.CallOption) (*chainv1.GetManagedWalletResponse, error)
ListManagedWallets(ctx context.Context, in *gatewayv1.ListManagedWalletsRequest, opts ...grpc.CallOption) (*gatewayv1.ListManagedWalletsResponse, error) ListManagedWallets(ctx context.Context, in *chainv1.ListManagedWalletsRequest, opts ...grpc.CallOption) (*chainv1.ListManagedWalletsResponse, error)
GetWalletBalance(ctx context.Context, in *gatewayv1.GetWalletBalanceRequest, opts ...grpc.CallOption) (*gatewayv1.GetWalletBalanceResponse, error) GetWalletBalance(ctx context.Context, in *chainv1.GetWalletBalanceRequest, opts ...grpc.CallOption) (*chainv1.GetWalletBalanceResponse, error)
SubmitTransfer(ctx context.Context, in *gatewayv1.SubmitTransferRequest, opts ...grpc.CallOption) (*gatewayv1.SubmitTransferResponse, error) SubmitTransfer(ctx context.Context, in *chainv1.SubmitTransferRequest, opts ...grpc.CallOption) (*chainv1.SubmitTransferResponse, error)
GetTransfer(ctx context.Context, in *gatewayv1.GetTransferRequest, opts ...grpc.CallOption) (*gatewayv1.GetTransferResponse, error) GetTransfer(ctx context.Context, in *chainv1.GetTransferRequest, opts ...grpc.CallOption) (*chainv1.GetTransferResponse, error)
ListTransfers(ctx context.Context, in *gatewayv1.ListTransfersRequest, opts ...grpc.CallOption) (*gatewayv1.ListTransfersResponse, error) ListTransfers(ctx context.Context, in *chainv1.ListTransfersRequest, opts ...grpc.CallOption) (*chainv1.ListTransfersResponse, error)
EstimateTransferFee(ctx context.Context, in *gatewayv1.EstimateTransferFeeRequest, opts ...grpc.CallOption) (*gatewayv1.EstimateTransferFeeResponse, error) EstimateTransferFee(ctx context.Context, in *chainv1.EstimateTransferFeeRequest, opts ...grpc.CallOption) (*chainv1.EstimateTransferFeeResponse, error)
} }
type chainGatewayClient struct { type chainGatewayClient struct {
@@ -71,7 +71,7 @@ func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, erro
return &chainGatewayClient{ return &chainGatewayClient{
cfg: cfg, cfg: cfg,
conn: conn, conn: conn,
client: gatewayv1.NewChainGatewayServiceClient(conn), client: chainv1.NewChainGatewayServiceClient(conn),
}, nil }, nil
} }
@@ -91,49 +91,49 @@ func (c *chainGatewayClient) Close() error {
return nil return nil
} }
func (c *chainGatewayClient) CreateManagedWallet(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error) { func (c *chainGatewayClient) CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error) {
ctx, cancel := c.callContext(ctx) ctx, cancel := c.callContext(ctx)
defer cancel() defer cancel()
return c.client.CreateManagedWallet(ctx, req) return c.client.CreateManagedWallet(ctx, req)
} }
func (c *chainGatewayClient) GetManagedWallet(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) (*gatewayv1.GetManagedWalletResponse, error) { func (c *chainGatewayClient) GetManagedWallet(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error) {
ctx, cancel := c.callContext(ctx) ctx, cancel := c.callContext(ctx)
defer cancel() defer cancel()
return c.client.GetManagedWallet(ctx, req) return c.client.GetManagedWallet(ctx, req)
} }
func (c *chainGatewayClient) ListManagedWallets(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) (*gatewayv1.ListManagedWalletsResponse, error) { func (c *chainGatewayClient) ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error) {
ctx, cancel := c.callContext(ctx) ctx, cancel := c.callContext(ctx)
defer cancel() defer cancel()
return c.client.ListManagedWallets(ctx, req) return c.client.ListManagedWallets(ctx, req)
} }
func (c *chainGatewayClient) GetWalletBalance(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) (*gatewayv1.GetWalletBalanceResponse, error) { func (c *chainGatewayClient) GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error) {
ctx, cancel := c.callContext(ctx) ctx, cancel := c.callContext(ctx)
defer cancel() defer cancel()
return c.client.GetWalletBalance(ctx, req) return c.client.GetWalletBalance(ctx, req)
} }
func (c *chainGatewayClient) SubmitTransfer(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error) { func (c *chainGatewayClient) SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
ctx, cancel := c.callContext(ctx) ctx, cancel := c.callContext(ctx)
defer cancel() defer cancel()
return c.client.SubmitTransfer(ctx, req) return c.client.SubmitTransfer(ctx, req)
} }
func (c *chainGatewayClient) GetTransfer(ctx context.Context, req *gatewayv1.GetTransferRequest) (*gatewayv1.GetTransferResponse, error) { func (c *chainGatewayClient) GetTransfer(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error) {
ctx, cancel := c.callContext(ctx) ctx, cancel := c.callContext(ctx)
defer cancel() defer cancel()
return c.client.GetTransfer(ctx, req) return c.client.GetTransfer(ctx, req)
} }
func (c *chainGatewayClient) ListTransfers(ctx context.Context, req *gatewayv1.ListTransfersRequest) (*gatewayv1.ListTransfersResponse, error) { func (c *chainGatewayClient) ListTransfers(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error) {
ctx, cancel := c.callContext(ctx) ctx, cancel := c.callContext(ctx)
defer cancel() defer cancel()
return c.client.ListTransfers(ctx, req) return c.client.ListTransfers(ctx, req)
} }
func (c *chainGatewayClient) EstimateTransferFee(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) (*gatewayv1.EstimateTransferFeeResponse, error) { func (c *chainGatewayClient) EstimateTransferFee(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error) {
ctx, cancel := c.callContext(ctx) ctx, cancel := c.callContext(ctx)
defer cancel() defer cancel()
return c.client.EstimateTransferFee(ctx, req) return c.client.EstimateTransferFee(ctx, req)

View File

@@ -0,0 +1,83 @@
package client
import (
"context"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
)
// Fake implements Client for tests.
type Fake struct {
CreateManagedWalletFn func(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error)
GetManagedWalletFn func(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error)
ListManagedWalletsFn func(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error)
GetWalletBalanceFn func(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error)
SubmitTransferFn func(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error)
GetTransferFn func(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error)
ListTransfersFn func(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error)
EstimateTransferFeeFn func(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error)
CloseFn func() error
}
func (f *Fake) CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error) {
if f.CreateManagedWalletFn != nil {
return f.CreateManagedWalletFn(ctx, req)
}
return &chainv1.CreateManagedWalletResponse{}, nil
}
func (f *Fake) GetManagedWallet(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error) {
if f.GetManagedWalletFn != nil {
return f.GetManagedWalletFn(ctx, req)
}
return &chainv1.GetManagedWalletResponse{}, nil
}
func (f *Fake) ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error) {
if f.ListManagedWalletsFn != nil {
return f.ListManagedWalletsFn(ctx, req)
}
return &chainv1.ListManagedWalletsResponse{}, nil
}
func (f *Fake) GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error) {
if f.GetWalletBalanceFn != nil {
return f.GetWalletBalanceFn(ctx, req)
}
return &chainv1.GetWalletBalanceResponse{}, nil
}
func (f *Fake) SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
if f.SubmitTransferFn != nil {
return f.SubmitTransferFn(ctx, req)
}
return &chainv1.SubmitTransferResponse{}, nil
}
func (f *Fake) GetTransfer(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error) {
if f.GetTransferFn != nil {
return f.GetTransferFn(ctx, req)
}
return &chainv1.GetTransferResponse{}, nil
}
func (f *Fake) ListTransfers(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error) {
if f.ListTransfersFn != nil {
return f.ListTransfersFn(ctx, req)
}
return &chainv1.ListTransfersResponse{}, nil
}
func (f *Fake) EstimateTransferFee(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error) {
if f.EstimateTransferFeeFn != nil {
return f.EstimateTransferFeeFn(ctx, req)
}
return &chainv1.EstimateTransferFeeResponse{}, nil
}
func (f *Fake) Close() error {
if f.CloseFn != nil {
return f.CloseFn()
}
return nil
}

View File

@@ -54,4 +54,4 @@ key_management:
token_env: VAULT_TOKEN token_env: VAULT_TOKEN
namespace: "" namespace: ""
mount_path: kv mount_path: kv
key_prefix: chain/gateway/wallets key_prefix: gateway/chain/wallets

View File

@@ -1,4 +1,4 @@
module github.com/tech/sendico/chain/gateway module github.com/tech/sendico/gateway/chain
go 1.25.3 go 1.25.3
@@ -61,7 +61,7 @@ require (
github.com/montanaflynn/stats v0.7.1 // indirect github.com/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.47.0 // indirect github.com/nats-io/nats.go v1.47.0 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nkeys v0.4.12 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
@@ -73,7 +73,7 @@ require (
github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect github.com/tklauser/numcpus v0.11.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
@@ -86,5 +86,5 @@ require (
golang.org/x/sys v0.38.0 // indirect golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.14.0 // indirect golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
) )

View File

@@ -209,8 +209,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM= github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
@@ -283,8 +283,8 @@ github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
@@ -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= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=

View File

@@ -16,7 +16,7 @@ import (
"github.com/hashicorp/vault/api" "github.com/hashicorp/vault/api"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/tech/sendico/chain/gateway/internal/keymanager" "github.com/tech/sendico/gateway/chain/internal/keymanager"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
) )

View File

@@ -7,11 +7,12 @@ import (
"time" "time"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"github.com/tech/sendico/chain/gateway/internal/keymanager" "github.com/tech/sendico/gateway/chain/internal/keymanager"
vaultmanager "github.com/tech/sendico/chain/gateway/internal/keymanager/vault" vaultmanager "github.com/tech/sendico/gateway/chain/internal/keymanager/vault"
gatewayservice "github.com/tech/sendico/chain/gateway/internal/service/gateway" gatewayservice "github.com/tech/sendico/gateway/chain/internal/service/gateway"
"github.com/tech/sendico/chain/gateway/storage" gatewayshared "github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
gatewaymongo "github.com/tech/sendico/chain/gateway/storage/mongo" "github.com/tech/sendico/gateway/chain/storage"
gatewaymongo "github.com/tech/sendico/gateway/chain/storage/mongo"
"github.com/tech/sendico/pkg/api/routers" "github.com/tech/sendico/pkg/api/routers"
"github.com/tech/sendico/pkg/db" "github.com/tech/sendico/pkg/db"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
@@ -154,8 +155,8 @@ func (i *Imp) loadConfig() (*config, error) {
return cfg, nil return cfg, nil
} }
func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) []gatewayservice.Network { func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) []gatewayshared.Network {
result := make([]gatewayservice.Network, 0, len(chains)) result := make([]gatewayshared.Network, 0, len(chains))
for _, chain := range chains { for _, chain := range chains {
if strings.TrimSpace(chain.Name) == "" { if strings.TrimSpace(chain.Name) == "" {
logger.Warn("skipping unnamed chain configuration") logger.Warn("skipping unnamed chain configuration")
@@ -165,7 +166,7 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) []gatewa
if rpcURL == "" { if rpcURL == "" {
logger.Warn("chain RPC endpoint not configured", zap.String("chain", chain.Name), zap.String("env", chain.RPCURLEnv)) logger.Warn("chain RPC endpoint not configured", zap.String("chain", chain.Name), zap.String("env", chain.RPCURLEnv))
} }
contracts := make([]gatewayservice.TokenContract, 0, len(chain.Tokens)) contracts := make([]gatewayshared.TokenContract, 0, len(chain.Tokens))
for _, token := range chain.Tokens { for _, token := range chain.Tokens {
symbol := strings.TrimSpace(token.Symbol) symbol := strings.TrimSpace(token.Symbol)
if symbol == "" { if symbol == "" {
@@ -185,13 +186,13 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) []gatewa
} }
continue continue
} }
contracts = append(contracts, gatewayservice.TokenContract{ contracts = append(contracts, gatewayshared.TokenContract{
Symbol: symbol, Symbol: symbol,
ContractAddress: addr, ContractAddress: addr,
}) })
} }
result = append(result, gatewayservice.Network{ result = append(result, gatewayshared.Network{
Name: chain.Name, Name: chain.Name,
RPCURL: rpcURL, RPCURL: rpcURL,
ChainID: chain.ChainID, ChainID: chain.ChainID,
@@ -202,7 +203,7 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) []gatewa
return result return result
} }
func resolveServiceWallet(logger mlogger.Logger, cfg serviceWalletConfig) gatewayservice.ServiceWallet { func resolveServiceWallet(logger mlogger.Logger, cfg serviceWalletConfig) gatewayshared.ServiceWallet {
address := strings.TrimSpace(cfg.Address) address := strings.TrimSpace(cfg.Address)
if address == "" && cfg.AddressEnv != "" { if address == "" && cfg.AddressEnv != "" {
address = strings.TrimSpace(os.Getenv(cfg.AddressEnv)) address = strings.TrimSpace(os.Getenv(cfg.AddressEnv))
@@ -221,7 +222,7 @@ func resolveServiceWallet(logger mlogger.Logger, cfg serviceWalletConfig) gatewa
logger.Warn("service wallet private key not configured", zap.String("env", cfg.PrivateKeyEnv)) logger.Warn("service wallet private key not configured", zap.String("env", cfg.PrivateKeyEnv))
} }
return gatewayservice.ServiceWallet{ return gatewayshared.ServiceWallet{
Network: cfg.Chain, Network: cfg.Chain,
Address: address, Address: address,
PrivateKey: privateKey, PrivateKey: privateKey,

View File

@@ -1,7 +1,7 @@
package server package server
import ( import (
serverimp "github.com/tech/sendico/chain/gateway/internal/server/internal" serverimp "github.com/tech/sendico/gateway/chain/internal/server/internal"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/server" "github.com/tech/sendico/pkg/server"
) )

View File

@@ -0,0 +1,44 @@
package commands
import (
"context"
"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands/transfer"
"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands/wallet"
"github.com/tech/sendico/pkg/api/routers/gsresponse"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
)
type Unary[TReq any, TResp any] interface {
Execute(context.Context, *TReq) gsresponse.Responder[TResp]
}
type Registry struct {
CreateManagedWallet Unary[chainv1.CreateManagedWalletRequest, chainv1.CreateManagedWalletResponse]
GetManagedWallet Unary[chainv1.GetManagedWalletRequest, chainv1.GetManagedWalletResponse]
ListManagedWallets Unary[chainv1.ListManagedWalletsRequest, chainv1.ListManagedWalletsResponse]
GetWalletBalance Unary[chainv1.GetWalletBalanceRequest, chainv1.GetWalletBalanceResponse]
SubmitTransfer Unary[chainv1.SubmitTransferRequest, chainv1.SubmitTransferResponse]
GetTransfer Unary[chainv1.GetTransferRequest, chainv1.GetTransferResponse]
ListTransfers Unary[chainv1.ListTransfersRequest, chainv1.ListTransfersResponse]
EstimateTransfer Unary[chainv1.EstimateTransferFeeRequest, chainv1.EstimateTransferFeeResponse]
}
type RegistryDeps struct {
Wallet wallet.Deps
Transfer transfer.Deps
}
func NewRegistry(deps RegistryDeps) Registry {
return Registry{
CreateManagedWallet: wallet.NewCreateManagedWallet(deps.Wallet.WithLogger("wallet.create")),
GetManagedWallet: wallet.NewGetManagedWallet(deps.Wallet.WithLogger("wallet.get")),
ListManagedWallets: wallet.NewListManagedWallets(deps.Wallet.WithLogger("wallet.list")),
GetWalletBalance: wallet.NewGetWalletBalance(deps.Wallet.WithLogger("wallet.balance")),
SubmitTransfer: transfer.NewSubmitTransfer(deps.Transfer.WithLogger("transfer.submit")),
GetTransfer: transfer.NewGetTransfer(deps.Transfer.WithLogger("transfer.get")),
ListTransfers: transfer.NewListTransfers(deps.Transfer.WithLogger("transfer.list")),
EstimateTransfer: transfer.NewEstimateTransfer(deps.Transfer.WithLogger("transfer.estimate_fee")),
}
}

View File

@@ -0,0 +1,43 @@
package transfer
import (
"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"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
)
func convertFees(fees []*chainv1.ServiceFeeBreakdown, currency string) ([]model.ServiceFee, decimal.Decimal, error) {
result := make([]model.ServiceFee, 0, len(fees))
sum := decimal.NewFromInt(0)
for _, fee := range fees {
if fee == nil || fee.GetAmount() == nil {
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount is required")
}
amtCurrency := strings.ToUpper(strings.TrimSpace(fee.GetAmount().GetCurrency()))
if amtCurrency != strings.ToUpper(currency) {
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee currency mismatch")
}
amtValue := strings.TrimSpace(fee.GetAmount().GetAmount())
if amtValue == "" {
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount is required")
}
dec, err := decimal.NewFromString(amtValue)
if err != nil {
return nil, decimal.Decimal{}, merrors.InvalidArgument("invalid fee amount")
}
if dec.IsNegative() {
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount must be non-negative")
}
sum = sum.Add(dec)
result = append(result, model.ServiceFee{
FeeCode: strings.TrimSpace(fee.GetFeeCode()),
Amount: shared.CloneMoney(fee.GetAmount()),
Description: strings.TrimSpace(fee.GetDescription()),
})
}
return result, sum, nil
}

View File

@@ -0,0 +1,26 @@
package transfer
import (
"context"
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
"github.com/tech/sendico/gateway/chain/storage"
clockpkg "github.com/tech/sendico/pkg/clock"
"github.com/tech/sendico/pkg/mlogger"
)
type Deps struct {
Logger mlogger.Logger
Networks map[string]shared.Network
Storage storage.Repository
Clock clockpkg.Clock
EnsureRepository func(context.Context) error
LaunchExecution func(transferRef, sourceWalletRef string, network shared.Network)
}
func (d Deps) WithLogger(name string) Deps {
if d.Logger != nil {
d.Logger = d.Logger.Named(name)
}
return d
}

View File

@@ -0,0 +1,50 @@
package transfer
import (
"context"
"strings"
"github.com/tech/sendico/gateway/chain/storage/model"
"github.com/tech/sendico/pkg/merrors"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"go.uber.org/zap"
)
func resolveDestination(ctx context.Context, deps Deps, dest *chainv1.TransferDestination, source *model.ManagedWallet) (model.TransferDestination, error) {
if dest == nil {
return model.TransferDestination{}, merrors.InvalidArgument("destination is required")
}
managedRef := strings.TrimSpace(dest.GetManagedWalletRef())
external := strings.TrimSpace(dest.GetExternalAddress())
if managedRef != "" && external != "" {
deps.Logger.Warn("both managed and external destination provided")
return model.TransferDestination{}, merrors.InvalidArgument("destination must be managed_wallet_ref or external_address")
}
if managedRef != "" {
wallet, err := deps.Storage.Wallets().Get(ctx, managedRef)
if err != nil {
deps.Logger.Warn("destination wallet lookup failed", zap.Error(err), zap.String("managed_wallet_ref", managedRef))
return model.TransferDestination{}, err
}
if !strings.EqualFold(wallet.Network, source.Network) {
deps.Logger.Warn("destination wallet network mismatch", zap.String("source_network", source.Network), zap.String("dest_network", wallet.Network))
return model.TransferDestination{}, merrors.InvalidArgument("destination wallet network mismatch")
}
if strings.TrimSpace(wallet.DepositAddress) == "" {
deps.Logger.Warn("destination wallet missing deposit address", zap.String("managed_wallet_ref", managedRef))
return model.TransferDestination{}, merrors.InvalidArgument("destination wallet missing deposit address")
}
return model.TransferDestination{
ManagedWalletRef: wallet.WalletRef,
Memo: strings.TrimSpace(dest.GetMemo()),
}, nil
}
if external == "" {
deps.Logger.Warn("destination external address missing")
return model.TransferDestination{}, merrors.InvalidArgument("destination is required")
}
return model.TransferDestination{
ExternalAddress: strings.ToLower(external),
Memo: strings.TrimSpace(dest.GetMemo()),
}, nil
}

View File

@@ -0,0 +1,26 @@
package transfer
import (
"context"
"strings"
"github.com/tech/sendico/gateway/chain/storage/model"
"github.com/tech/sendico/pkg/merrors"
)
func destinationAddress(ctx context.Context, deps Deps, dest model.TransferDestination) (string, error) {
if ref := strings.TrimSpace(dest.ManagedWalletRef); ref != "" {
wallet, err := deps.Storage.Wallets().Get(ctx, ref)
if err != nil {
return "", err
}
if strings.TrimSpace(wallet.DepositAddress) == "" {
return "", merrors.Internal("destination wallet missing deposit address")
}
return wallet.DepositAddress, nil
}
if addr := strings.TrimSpace(dest.ExternalAddress); addr != "" {
return strings.ToLower(addr), nil
}
return "", merrors.InvalidArgument("transfer destination address not resolved")
}

View File

@@ -0,0 +1,248 @@
package transfer
import (
"context"
"errors"
"math/big"
"strings"
"time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"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/api/routers/gsresponse"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"go.uber.org/zap"
)
type estimateTransferFeeCommand struct {
deps Deps
}
func NewEstimateTransfer(deps Deps) *estimateTransferFeeCommand {
return &estimateTransferFeeCommand{deps: deps}
}
func (c *estimateTransferFeeCommand) Execute(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) gsresponse.Responder[chainv1.EstimateTransferFeeResponse] {
if err := c.deps.EnsureRepository(ctx); err != nil {
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
return gsresponse.Unavailable[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
}
if req == nil {
c.deps.Logger.Warn("nil request")
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("request is required"))
}
sourceWalletRef := strings.TrimSpace(req.GetSourceWalletRef())
if sourceWalletRef == "" {
c.deps.Logger.Warn("source wallet ref missing")
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("source_wallet_ref is required"))
}
amount := req.GetAmount()
if amount == nil || strings.TrimSpace(amount.GetAmount()) == "" || strings.TrimSpace(amount.GetCurrency()) == "" {
c.deps.Logger.Warn("amount missing or incomplete")
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("amount is required"))
}
sourceWallet, err := c.deps.Storage.Wallets().Get(ctx, sourceWalletRef)
if err != nil {
if errors.Is(err, merrors.ErrNoData) {
c.deps.Logger.Warn("source wallet not found", zap.String("source_wallet_ref", sourceWalletRef))
return gsresponse.NotFound[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
}
c.deps.Logger.Warn("storage get wallet failed", zap.Error(err), zap.String("source_wallet_ref", sourceWalletRef))
return gsresponse.Auto[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
}
networkKey := strings.ToLower(strings.TrimSpace(sourceWallet.Network))
networkCfg, ok := c.deps.Networks[networkKey]
if !ok {
c.deps.Logger.Warn("unsupported chain", zap.String("network", networkKey))
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet"))
}
dest, err := resolveDestination(ctx, c.deps, req.GetDestination(), sourceWallet)
if err != nil {
if errors.Is(err, merrors.ErrNoData) {
c.deps.Logger.Warn("destination not found", zap.String("destination_wallet_ref", req.GetDestination().GetManagedWalletRef()))
return gsresponse.NotFound[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
}
c.deps.Logger.Warn("invalid destination", zap.Error(err))
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
}
destinationAddress, err := destinationAddress(ctx, c.deps, dest)
if err != nil {
c.deps.Logger.Warn("failed to resolve destination address", zap.Error(err))
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
}
feeMoney, err := estimateNetworkFee(ctx, c.deps.Logger, networkCfg, sourceWallet, destinationAddress, amount)
if err != nil {
c.deps.Logger.Warn("fee estimation failed", zap.Error(err))
return gsresponse.Auto[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
}
resp := &chainv1.EstimateTransferFeeResponse{
NetworkFee: feeMoney,
EstimationContext: "erc20_transfer",
}
return gsresponse.Success(resp)
}
func estimateNetworkFee(ctx context.Context, logger mlogger.Logger, network shared.Network, wallet *model.ManagedWallet, destination string, amount *moneyv1.Money) (*moneyv1.Money, error) {
rpcURL := strings.TrimSpace(network.RPCURL)
if rpcURL == "" {
return nil, merrors.InvalidArgument("network rpc url not configured")
}
if strings.TrimSpace(wallet.ContractAddress) == "" {
return nil, merrors.NotImplemented("native token transfers not supported")
}
if !common.IsHexAddress(wallet.ContractAddress) {
return nil, merrors.InvalidArgument("invalid token contract address")
}
if !common.IsHexAddress(wallet.DepositAddress) {
return nil, merrors.InvalidArgument("invalid source wallet address")
}
if !common.IsHexAddress(destination) {
return nil, merrors.InvalidArgument("invalid destination address")
}
client, err := ethclient.DialContext(ctx, rpcURL)
if err != nil {
return nil, merrors.Internal("failed to connect to rpc: " + err.Error())
}
defer client.Close()
timeoutCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
tokenABI, err := abi.JSON(strings.NewReader(erc20TransferABI))
if err != nil {
return nil, merrors.Internal("failed to parse erc20 abi: " + err.Error())
}
tokenAddr := common.HexToAddress(wallet.ContractAddress)
toAddr := common.HexToAddress(destination)
fromAddr := common.HexToAddress(wallet.DepositAddress)
decimals, err := erc20Decimals(timeoutCtx, client, tokenABI, tokenAddr)
if err != nil {
logger.Warn("failed to read token decimals", zap.Error(err))
return nil, err
}
amountBase, err := toBaseUnits(strings.TrimSpace(amount.GetAmount()), decimals)
if err != nil {
return nil, err
}
input, err := tokenABI.Pack("transfer", toAddr, amountBase)
if err != nil {
return nil, merrors.Internal("failed to encode transfer call: " + err.Error())
}
gasPrice, err := client.SuggestGasPrice(timeoutCtx)
if err != nil {
return nil, merrors.Internal("failed to suggest gas price: " + err.Error())
}
callMsg := ethereum.CallMsg{
From: fromAddr,
To: &tokenAddr,
GasPrice: gasPrice,
Data: input,
}
gasLimit, err := client.EstimateGas(timeoutCtx, callMsg)
if err != nil {
return nil, merrors.Internal("failed to estimate gas: " + err.Error())
}
fee := new(big.Int).Mul(gasPrice, new(big.Int).SetUint64(gasLimit))
feeDec := decimal.NewFromBigInt(fee, 0)
currency := strings.ToUpper(strings.TrimSpace(network.NativeToken))
if currency == "" {
currency = strings.ToUpper(network.Name)
}
return &moneyv1.Money{
Currency: currency,
Amount: feeDec.String(),
}, nil
}
func erc20Decimals(ctx context.Context, client *ethclient.Client, tokenABI abi.ABI, token common.Address) (uint8, error) {
callData, err := tokenABI.Pack("decimals")
if err != nil {
return 0, merrors.Internal("failed to encode decimals call: " + err.Error())
}
msg := ethereum.CallMsg{
To: &token,
Data: callData,
}
output, err := client.CallContract(ctx, msg, nil)
if err != nil {
return 0, merrors.Internal("decimals call failed: " + err.Error())
}
values, err := tokenABI.Unpack("decimals", output)
if err != nil {
return 0, merrors.Internal("failed to unpack decimals: " + err.Error())
}
if len(values) == 0 {
return 0, merrors.Internal("decimals call returned no data")
}
decimals, ok := values[0].(uint8)
if !ok {
return 0, merrors.Internal("decimals call returned unexpected type")
}
return decimals, nil
}
func toBaseUnits(amount string, decimals uint8) (*big.Int, error) {
value, err := decimal.NewFromString(strings.TrimSpace(amount))
if err != nil {
return nil, merrors.InvalidArgument("invalid amount " + amount + ": " + err.Error())
}
if value.IsNegative() {
return nil, merrors.InvalidArgument("amount must be positive")
}
multiplier := decimal.NewFromInt(1).Shift(int32(decimals))
scaled := value.Mul(multiplier)
if !scaled.Equal(scaled.Truncate(0)) {
return nil, merrors.InvalidArgument("amount " + amount + " exceeds token precision")
}
return scaled.BigInt(), nil
}
const erc20TransferABI = `
[
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [{ "name": "", "type": "uint8" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_to", "type": "address" },
{ "name": "_value", "type": "uint256" }
],
"name": "transfer",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
}
]`

View File

@@ -0,0 +1,47 @@
package transfer
import (
"context"
"errors"
"strings"
"github.com/tech/sendico/pkg/api/routers/gsresponse"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mservice"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"go.uber.org/zap"
)
type getTransferCommand struct {
deps Deps
}
func NewGetTransfer(deps Deps) *getTransferCommand {
return &getTransferCommand{deps: deps}
}
func (c *getTransferCommand) Execute(ctx context.Context, req *chainv1.GetTransferRequest) gsresponse.Responder[chainv1.GetTransferResponse] {
if err := c.deps.EnsureRepository(ctx); err != nil {
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
return gsresponse.Unavailable[chainv1.GetTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
}
if req == nil {
c.deps.Logger.Warn("nil request")
return gsresponse.InvalidArgument[chainv1.GetTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
}
transferRef := strings.TrimSpace(req.GetTransferRef())
if transferRef == "" {
c.deps.Logger.Warn("transfer_ref missing")
return gsresponse.InvalidArgument[chainv1.GetTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("transfer_ref is required"))
}
transfer, err := c.deps.Storage.Transfers().Get(ctx, transferRef)
if err != nil {
if errors.Is(err, merrors.ErrNoData) {
c.deps.Logger.Warn("not found", zap.String("transfer_ref", transferRef))
return gsresponse.NotFound[chainv1.GetTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
}
c.deps.Logger.Warn("storage get failed", zap.Error(err), zap.String("transfer_ref", transferRef))
return gsresponse.Auto[chainv1.GetTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
}
return gsresponse.Success(&chainv1.GetTransferResponse{Transfer: toProtoTransfer(transfer)})
}

View File

@@ -0,0 +1,58 @@
package transfer
import (
"context"
"strings"
"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"
"github.com/tech/sendico/pkg/mservice"
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"go.uber.org/zap"
)
type listTransfersCommand struct {
deps Deps
}
func NewListTransfers(deps Deps) *listTransfersCommand {
return &listTransfersCommand{deps: deps}
}
func (c *listTransfersCommand) Execute(ctx context.Context, req *chainv1.ListTransfersRequest) gsresponse.Responder[chainv1.ListTransfersResponse] {
if err := c.deps.EnsureRepository(ctx); err != nil {
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
return gsresponse.Unavailable[chainv1.ListTransfersResponse](c.deps.Logger, mservice.ChainGateway, err)
}
filter := model.TransferFilter{}
if req != nil {
filter.SourceWalletRef = strings.TrimSpace(req.GetSourceWalletRef())
filter.DestinationWalletRef = strings.TrimSpace(req.GetDestinationWalletRef())
if status := shared.TransferStatusToModel(req.GetStatus()); status != "" {
filter.Status = status
}
if page := req.GetPage(); page != nil {
filter.Cursor = strings.TrimSpace(page.GetCursor())
filter.Limit = page.GetLimit()
}
}
result, err := c.deps.Storage.Transfers().List(ctx, filter)
if err != nil {
c.deps.Logger.Warn("storage list failed", zap.Error(err))
return gsresponse.Auto[chainv1.ListTransfersResponse](c.deps.Logger, mservice.ChainGateway, err)
}
protoTransfers := make([]*chainv1.Transfer, 0, len(result.Items))
for _, transfer := range result.Items {
protoTransfers = append(protoTransfers, toProtoTransfer(transfer))
}
resp := &chainv1.ListTransfersResponse{
Transfers: protoTransfers,
Page: &paginationv1.CursorPageResponse{NextCursor: result.NextCursor},
}
return gsresponse.Success(resp)
}

View File

@@ -0,0 +1,53 @@
package transfer
import (
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
"github.com/tech/sendico/gateway/chain/storage/model"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"google.golang.org/protobuf/types/known/timestamppb"
)
func toProtoTransfer(transfer *model.Transfer) *chainv1.Transfer {
if transfer == nil {
return nil
}
destination := &chainv1.TransferDestination{}
if transfer.Destination.ManagedWalletRef != "" {
destination.Destination = &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: transfer.Destination.ManagedWalletRef}
} else if transfer.Destination.ExternalAddress != "" {
destination.Destination = &chainv1.TransferDestination_ExternalAddress{ExternalAddress: transfer.Destination.ExternalAddress}
}
destination.Memo = transfer.Destination.Memo
protoFees := make([]*chainv1.ServiceFeeBreakdown, 0, len(transfer.Fees))
for _, fee := range transfer.Fees {
protoFees = append(protoFees, &chainv1.ServiceFeeBreakdown{
FeeCode: fee.FeeCode,
Amount: shared.CloneMoney(fee.Amount),
Description: fee.Description,
})
}
asset := &chainv1.Asset{
Chain: shared.ChainEnumFromName(transfer.Network),
TokenSymbol: transfer.TokenSymbol,
ContractAddress: transfer.ContractAddress,
}
return &chainv1.Transfer{
TransferRef: transfer.TransferRef,
IdempotencyKey: transfer.IdempotencyKey,
OrganizationRef: transfer.OrganizationRef,
SourceWalletRef: transfer.SourceWalletRef,
Destination: destination,
Asset: asset,
RequestedAmount: shared.CloneMoney(transfer.RequestedAmount),
NetAmount: shared.CloneMoney(transfer.NetAmount),
Fees: protoFees,
Status: shared.TransferStatusToProto(transfer.Status),
TransactionHash: transfer.TxHash,
FailureReason: transfer.FailureReason,
CreatedAt: timestamppb.New(transfer.CreatedAt.UTC()),
UpdatedAt: timestamppb.New(transfer.UpdatedAt.UTC()),
}
}

View File

@@ -0,0 +1,148 @@
package transfer
import (
"context"
"errors"
"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/api/routers/gsresponse"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mservice"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"go.uber.org/zap"
)
type submitTransferCommand struct {
deps Deps
}
func NewSubmitTransfer(deps Deps) *submitTransferCommand {
return &submitTransferCommand{deps: deps}
}
func (c *submitTransferCommand) Execute(ctx context.Context, req *chainv1.SubmitTransferRequest) gsresponse.Responder[chainv1.SubmitTransferResponse] {
if err := c.deps.EnsureRepository(ctx); err != nil {
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
return gsresponse.Unavailable[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
}
if req == nil {
c.deps.Logger.Warn("nil request")
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
}
idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey())
if idempotencyKey == "" {
c.deps.Logger.Warn("missing idempotency key")
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required"))
}
organizationRef := strings.TrimSpace(req.GetOrganizationRef())
if organizationRef == "" {
c.deps.Logger.Warn("missing organization ref")
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref is required"))
}
sourceWalletRef := strings.TrimSpace(req.GetSourceWalletRef())
if sourceWalletRef == "" {
c.deps.Logger.Warn("missing source wallet ref")
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("source_wallet_ref is required"))
}
amount := req.GetAmount()
if amount == nil {
c.deps.Logger.Warn("missing amount")
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("amount is required"))
}
amountCurrency := strings.ToUpper(strings.TrimSpace(amount.GetCurrency()))
if amountCurrency == "" {
c.deps.Logger.Warn("missing amount currency")
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("amount.currency is required"))
}
amountValue := strings.TrimSpace(amount.GetAmount())
if amountValue == "" {
c.deps.Logger.Warn("missing amount value")
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("amount.amount is required"))
}
sourceWallet, err := c.deps.Storage.Wallets().Get(ctx, sourceWalletRef)
if err != nil {
if errors.Is(err, merrors.ErrNoData) {
c.deps.Logger.Warn("source wallet not found", zap.String("source_wallet_ref", sourceWalletRef))
return gsresponse.NotFound[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
}
c.deps.Logger.Warn("storage get wallet failed", zap.Error(err), zap.String("source_wallet_ref", sourceWalletRef))
return gsresponse.Auto[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
}
if !strings.EqualFold(sourceWallet.OrganizationRef, organizationRef) {
c.deps.Logger.Warn("organization mismatch", zap.String("wallet_org", sourceWallet.OrganizationRef), zap.String("req_org", organizationRef))
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref mismatch with wallet"))
}
networkKey := strings.ToLower(strings.TrimSpace(sourceWallet.Network))
networkCfg, ok := c.deps.Networks[networkKey]
if !ok {
c.deps.Logger.Warn("unsupported chain", zap.String("network", networkKey))
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet"))
}
destination, err := resolveDestination(ctx, c.deps, req.GetDestination(), sourceWallet)
if err != nil {
if errors.Is(err, merrors.ErrNoData) {
c.deps.Logger.Warn("destination not found", zap.String("destination_wallet_ref", req.GetDestination().GetManagedWalletRef()))
return gsresponse.NotFound[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
}
c.deps.Logger.Warn("invalid destination", zap.Error(err))
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
}
fees, feeSum, err := convertFees(req.GetFees(), amountCurrency)
if err != nil {
c.deps.Logger.Warn("fee conversion failed", zap.Error(err))
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
}
amountDec, err := decimal.NewFromString(amountValue)
if err != nil {
c.deps.Logger.Warn("invalid amount", zap.Error(err))
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("invalid amount"))
}
netDec := amountDec.Sub(feeSum)
if netDec.IsNegative() {
c.deps.Logger.Warn("fees exceed amount", zap.String("amount", amountValue), zap.String("fee_sum", feeSum.String()))
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("fees exceed amount"))
}
netAmount := shared.CloneMoney(amount)
netAmount.Amount = netDec.String()
transfer := &model.Transfer{
IdempotencyKey: idempotencyKey,
TransferRef: shared.GenerateTransferRef(),
OrganizationRef: organizationRef,
SourceWalletRef: sourceWalletRef,
Destination: destination,
Network: sourceWallet.Network,
TokenSymbol: sourceWallet.TokenSymbol,
ContractAddress: sourceWallet.ContractAddress,
RequestedAmount: shared.CloneMoney(amount),
NetAmount: netAmount,
Fees: fees,
Status: model.TransferStatusPending,
ClientReference: strings.TrimSpace(req.GetClientReference()),
LastStatusAt: c.deps.Clock.Now().UTC(),
}
saved, err := c.deps.Storage.Transfers().Create(ctx, transfer)
if err != nil {
if errors.Is(err, merrors.ErrDataConflict) {
c.deps.Logger.Debug("transfer already exists", zap.String("transfer_ref", transfer.TransferRef), zap.String("idempotency_key", idempotencyKey))
return gsresponse.Success(&chainv1.SubmitTransferResponse{Transfer: toProtoTransfer(saved)})
}
c.deps.Logger.Warn("storage create failed", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef))
return gsresponse.Auto[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
}
if c.deps.LaunchExecution != nil {
c.deps.LaunchExecution(saved.TransferRef, sourceWalletRef, networkCfg)
}
return gsresponse.Success(&chainv1.SubmitTransferResponse{Transfer: toProtoTransfer(saved)})
}

View File

@@ -0,0 +1,77 @@
package wallet
import (
"context"
"errors"
"strings"
"github.com/tech/sendico/pkg/api/routers/gsresponse"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mservice"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"go.uber.org/zap"
"google.golang.org/protobuf/types/known/timestamppb"
)
type getWalletBalanceCommand struct {
deps Deps
}
func NewGetWalletBalance(deps Deps) *getWalletBalanceCommand {
return &getWalletBalanceCommand{deps: deps}
}
func (c *getWalletBalanceCommand) Execute(ctx context.Context, req *chainv1.GetWalletBalanceRequest) gsresponse.Responder[chainv1.GetWalletBalanceResponse] {
if err := c.deps.EnsureRepository(ctx); err != nil {
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
return gsresponse.Unavailable[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
}
if req == nil {
c.deps.Logger.Warn("nil request")
return gsresponse.InvalidArgument[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
}
walletRef := strings.TrimSpace(req.GetWalletRef())
if walletRef == "" {
c.deps.Logger.Warn("wallet_ref missing")
return gsresponse.InvalidArgument[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required"))
}
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.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
}
c.deps.Logger.Warn("storage get failed", zap.Error(err), zap.String("wallet_ref", walletRef))
return gsresponse.Auto[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
}
balance, chainErr := onChainWalletBalance(ctx, c.deps, wallet)
if chainErr != nil {
c.deps.Logger.Warn("on-chain balance fetch failed, falling back to stored balance", zap.Error(chainErr), zap.String("wallet_ref", walletRef))
stored, err := c.deps.Storage.Wallets().GetBalance(ctx, walletRef)
if err != nil {
if errors.Is(err, merrors.ErrNoData) {
c.deps.Logger.Warn("stored balance not found", zap.String("wallet_ref", walletRef))
return gsresponse.NotFound[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
}
return gsresponse.Auto[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
}
return gsresponse.Success(&chainv1.GetWalletBalanceResponse{Balance: toProtoWalletBalance(stored)})
}
return gsresponse.Success(&chainv1.GetWalletBalanceResponse{Balance: onChainBalanceToProto(balance)})
}
func onChainBalanceToProto(balance *moneyv1.Money) *chainv1.WalletBalance {
if balance == nil {
return nil
}
zero := &moneyv1.Money{Currency: balance.Currency, Amount: "0"}
return &chainv1.WalletBalance{
Available: balance,
PendingInbound: zero,
PendingOutbound: zero,
CalculatedAt: timestamppb.Now(),
}
}

View File

@@ -0,0 +1,123 @@
package wallet
import (
"context"
"errors"
"strings"
"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"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mservice"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"go.uber.org/zap"
)
type createManagedWalletCommand struct {
deps Deps
}
func NewCreateManagedWallet(deps Deps) *createManagedWalletCommand {
return &createManagedWalletCommand{deps: deps}
}
func (c *createManagedWalletCommand) Execute(ctx context.Context, req *chainv1.CreateManagedWalletRequest) gsresponse.Responder[chainv1.CreateManagedWalletResponse] {
if err := c.deps.EnsureRepository(ctx); err != nil {
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
return gsresponse.Unavailable[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
}
if req == nil {
c.deps.Logger.Warn("nil request")
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
}
idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey())
if idempotencyKey == "" {
c.deps.Logger.Warn("missing idempotency key")
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required"))
}
organizationRef := strings.TrimSpace(req.GetOrganizationRef())
if organizationRef == "" {
c.deps.Logger.Warn("missing organization ref")
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref is required"))
}
ownerRef := strings.TrimSpace(req.GetOwnerRef())
if ownerRef == "" {
c.deps.Logger.Warn("missing owner ref")
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("owner_ref is required"))
}
asset := req.GetAsset()
if asset == nil {
c.deps.Logger.Warn("missing asset")
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("asset is required"))
}
chainKey, _ := shared.ChainKeyFromEnum(asset.GetChain())
if chainKey == "" {
c.deps.Logger.Warn("unsupported chain", zap.Any("chain", asset.GetChain()))
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
}
networkCfg, ok := c.deps.Networks[chainKey]
if !ok {
c.deps.Logger.Warn("unsupported chain in config", zap.String("chain", chainKey))
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
}
tokenSymbol := strings.ToUpper(strings.TrimSpace(asset.GetTokenSymbol()))
if tokenSymbol == "" {
c.deps.Logger.Warn("missing token symbol")
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("asset.token_symbol is required"))
}
contractAddress := strings.ToLower(strings.TrimSpace(asset.GetContractAddress()))
if contractAddress == "" {
contractAddress = shared.ResolveContractAddress(networkCfg.TokenConfigs, tokenSymbol)
if contractAddress == "" {
c.deps.Logger.Warn("unsupported token", zap.String("token", tokenSymbol), zap.String("chain", chainKey))
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported token for chain"))
}
}
walletRef := shared.GenerateWalletRef()
if c.deps.KeyManager == nil {
c.deps.Logger.Warn("key manager missing")
return gsresponse.Internal[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.Internal("key manager not configured"))
}
keyInfo, err := c.deps.KeyManager.CreateManagedWalletKey(ctx, walletRef, chainKey)
if err != nil {
c.deps.Logger.Warn("key manager error", zap.Error(err))
return gsresponse.Auto[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
}
if keyInfo == nil || strings.TrimSpace(keyInfo.Address) == "" {
c.deps.Logger.Warn("key manager returned empty address")
return gsresponse.Internal[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.Internal("key manager returned empty address"))
}
wallet := &model.ManagedWallet{
IdempotencyKey: idempotencyKey,
WalletRef: walletRef,
OrganizationRef: organizationRef,
OwnerRef: ownerRef,
Network: chainKey,
TokenSymbol: tokenSymbol,
ContractAddress: contractAddress,
DepositAddress: strings.ToLower(keyInfo.Address),
KeyReference: keyInfo.KeyID,
Status: model.ManagedWalletStatusActive,
Metadata: shared.CloneMetadata(req.GetMetadata()),
}
created, err := c.deps.Storage.Wallets().Create(ctx, wallet)
if err != nil {
if errors.Is(err, merrors.ErrDataConflict) {
c.deps.Logger.Debug("wallet already exists", zap.String("wallet_ref", walletRef), zap.String("idempotency_key", idempotencyKey))
return gsresponse.Success(&chainv1.CreateManagedWalletResponse{Wallet: toProtoManagedWallet(created)})
}
c.deps.Logger.Warn("storage create failed", zap.Error(err), zap.String("wallet_ref", walletRef))
return gsresponse.Auto[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
}
return gsresponse.Success(&chainv1.CreateManagedWalletResponse{Wallet: toProtoManagedWallet(created)})
}

View File

@@ -0,0 +1,25 @@
package wallet
import (
"context"
"github.com/tech/sendico/gateway/chain/internal/keymanager"
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
"github.com/tech/sendico/gateway/chain/storage"
"github.com/tech/sendico/pkg/mlogger"
)
type Deps struct {
Logger mlogger.Logger
Networks map[string]shared.Network
KeyManager keymanager.Manager
Storage storage.Repository
EnsureRepository func(context.Context) error
}
func (d Deps) WithLogger(name string) Deps {
if d.Logger != nil {
d.Logger = d.Logger.Named(name)
}
return d
}

View File

@@ -0,0 +1,47 @@
package wallet
import (
"context"
"errors"
"strings"
"github.com/tech/sendico/pkg/api/routers/gsresponse"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mservice"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"go.uber.org/zap"
)
type getManagedWalletCommand struct {
deps Deps
}
func NewGetManagedWallet(deps Deps) *getManagedWalletCommand {
return &getManagedWalletCommand{deps: deps}
}
func (c *getManagedWalletCommand) Execute(ctx context.Context, req *chainv1.GetManagedWalletRequest) gsresponse.Responder[chainv1.GetManagedWalletResponse] {
if err := c.deps.EnsureRepository(ctx); err != nil {
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
return gsresponse.Unavailable[chainv1.GetManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
}
if req == nil {
c.deps.Logger.Warn("nil request")
return gsresponse.InvalidArgument[chainv1.GetManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
}
walletRef := strings.TrimSpace(req.GetWalletRef())
if walletRef == "" {
c.deps.Logger.Warn("wallet_ref missing")
return gsresponse.InvalidArgument[chainv1.GetManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required"))
}
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)
}
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)
}
return gsresponse.Success(&chainv1.GetManagedWalletResponse{Wallet: toProtoManagedWallet(wallet)})
}

View File

@@ -0,0 +1,59 @@
package wallet
import (
"context"
"strings"
"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"
"github.com/tech/sendico/pkg/mservice"
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"go.uber.org/zap"
)
type listManagedWalletsCommand struct {
deps Deps
}
func NewListManagedWallets(deps Deps) *listManagedWalletsCommand {
return &listManagedWalletsCommand{deps: deps}
}
func (c *listManagedWalletsCommand) Execute(ctx context.Context, req *chainv1.ListManagedWalletsRequest) gsresponse.Responder[chainv1.ListManagedWalletsResponse] {
if err := c.deps.EnsureRepository(ctx); err != nil {
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
return gsresponse.Unavailable[chainv1.ListManagedWalletsResponse](c.deps.Logger, mservice.ChainGateway, err)
}
filter := model.ManagedWalletFilter{}
if req != nil {
filter.OrganizationRef = strings.TrimSpace(req.GetOrganizationRef())
filter.OwnerRef = strings.TrimSpace(req.GetOwnerRef())
if asset := req.GetAsset(); asset != nil {
filter.Network, _ = shared.ChainKeyFromEnum(asset.GetChain())
filter.TokenSymbol = strings.TrimSpace(asset.GetTokenSymbol())
}
if page := req.GetPage(); page != nil {
filter.Cursor = strings.TrimSpace(page.GetCursor())
filter.Limit = page.GetLimit()
}
}
result, err := c.deps.Storage.Wallets().List(ctx, filter)
if err != nil {
c.deps.Logger.Warn("storage list failed", zap.Error(err))
return gsresponse.Auto[chainv1.ListManagedWalletsResponse](c.deps.Logger, mservice.ChainGateway, err)
}
protoWallets := make([]*chainv1.ManagedWallet, 0, len(result.Items))
for _, wallet := range result.Items {
protoWallets = append(protoWallets, toProtoManagedWallet(wallet))
}
resp := &chainv1.ListManagedWalletsResponse{
Wallets: protoWallets,
Page: &paginationv1.CursorPageResponse{NextCursor: result.NextCursor},
}
return gsresponse.Success(resp)
}

View File

@@ -0,0 +1,124 @@
package wallet
import (
"context"
"math/big"
"strings"
"time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/shopspring/decimal"
"github.com/tech/sendico/gateway/chain/storage/model"
"github.com/tech/sendico/pkg/merrors"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
)
func onChainWalletBalance(ctx context.Context, deps Deps, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
network := deps.Networks[strings.ToLower(strings.TrimSpace(wallet.Network))]
rpcURL := strings.TrimSpace(network.RPCURL)
if rpcURL == "" {
return nil, merrors.Internal("network rpc url is not configured")
}
contract := strings.TrimSpace(wallet.ContractAddress)
if contract == "" || !common.IsHexAddress(contract) {
return nil, merrors.InvalidArgument("invalid contract address")
}
if wallet.DepositAddress == "" || !common.IsHexAddress(wallet.DepositAddress) {
return nil, merrors.InvalidArgument("invalid wallet address")
}
client, err := ethclient.DialContext(ctx, rpcURL)
if err != nil {
return nil, merrors.Internal("failed to connect rpc: " + err.Error())
}
defer client.Close()
timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
tokenABI, err := abi.JSON(strings.NewReader(erc20ABIJSON))
if err != nil {
return nil, merrors.Internal("failed to parse erc20 abi: " + err.Error())
}
tokenAddr := common.HexToAddress(contract)
walletAddr := common.HexToAddress(wallet.DepositAddress)
decimals, err := readDecimals(timeoutCtx, client, tokenABI, tokenAddr)
if err != nil {
return nil, err
}
bal, err := readBalanceOf(timeoutCtx, client, tokenABI, tokenAddr, walletAddr)
if err != nil {
return nil, err
}
dec := decimal.NewFromBigInt(bal, 0).Shift(-int32(decimals))
return &moneyv1.Money{Currency: wallet.TokenSymbol, Amount: dec.String()}, nil
}
func readDecimals(ctx context.Context, client *ethclient.Client, tokenABI abi.ABI, token common.Address) (uint8, error) {
data, err := tokenABI.Pack("decimals")
if err != nil {
return 0, merrors.Internal("failed to encode decimals call: " + err.Error())
}
msg := ethereum.CallMsg{To: &token, Data: data}
out, err := client.CallContract(ctx, msg, nil)
if err != nil {
return 0, merrors.Internal("decimals call failed: " + err.Error())
}
values, err := tokenABI.Unpack("decimals", out)
if err != nil || len(values) == 0 {
return 0, merrors.Internal("failed to unpack decimals")
}
if val, ok := values[0].(uint8); ok {
return val, nil
}
return 0, merrors.Internal("decimals returned unexpected type")
}
func readBalanceOf(ctx context.Context, client *ethclient.Client, tokenABI abi.ABI, token common.Address, wallet common.Address) (*big.Int, error) {
data, err := tokenABI.Pack("balanceOf", wallet)
if err != nil {
return nil, merrors.Internal("failed to encode balanceOf: " + err.Error())
}
msg := ethereum.CallMsg{To: &token, Data: data}
out, err := client.CallContract(ctx, msg, nil)
if err != nil {
return nil, merrors.Internal("balanceOf call failed: " + err.Error())
}
values, err := tokenABI.Unpack("balanceOf", out)
if err != nil || len(values) == 0 {
return nil, merrors.Internal("failed to unpack balanceOf")
}
raw, ok := values[0].(*big.Int)
if !ok {
return nil, merrors.Internal("balanceOf returned unexpected type")
}
return decimal.NewFromBigInt(raw, 0).BigInt(), nil
}
const erc20ABIJSON = `
[
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [{ "name": "", "type": "uint8" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "_owner", "type": "address" }],
"name": "balanceOf",
"outputs": [{ "name": "balance", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]`

View File

@@ -0,0 +1,42 @@
package wallet
import (
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
"github.com/tech/sendico/gateway/chain/storage/model"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"google.golang.org/protobuf/types/known/timestamppb"
)
func toProtoManagedWallet(wallet *model.ManagedWallet) *chainv1.ManagedWallet {
if wallet == nil {
return nil
}
asset := &chainv1.Asset{
Chain: shared.ChainEnumFromName(wallet.Network),
TokenSymbol: wallet.TokenSymbol,
ContractAddress: wallet.ContractAddress,
}
return &chainv1.ManagedWallet{
WalletRef: wallet.WalletRef,
OrganizationRef: wallet.OrganizationRef,
OwnerRef: wallet.OwnerRef,
Asset: asset,
DepositAddress: wallet.DepositAddress,
Status: shared.ManagedWalletStatusToProto(wallet.Status),
Metadata: shared.CloneMetadata(wallet.Metadata),
CreatedAt: timestamppb.New(wallet.CreatedAt.UTC()),
UpdatedAt: timestamppb.New(wallet.UpdatedAt.UTC()),
}
}
func toProtoWalletBalance(balance *model.WalletBalance) *chainv1.WalletBalance {
if balance == nil {
return nil
}
return &chainv1.WalletBalance{
Available: shared.CloneMoney(balance.Available),
PendingInbound: shared.CloneMoney(balance.PendingInbound),
PendingOutbound: shared.CloneMoney(balance.PendingOutbound),
CalculatedAt: timestamppb.New(balance.CalculatedAt.UTC()),
}
}

View File

@@ -14,18 +14,19 @@ import (
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/tech/sendico/chain/gateway/internal/keymanager" "github.com/tech/sendico/gateway/chain/internal/keymanager"
"github.com/tech/sendico/chain/gateway/storage/model" "github.com/tech/sendico/gateway/chain/storage/model"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
) )
// TransferExecutor handles on-chain submission of transfers. // TransferExecutor handles on-chain submission of transfers.
type TransferExecutor interface { type TransferExecutor interface {
SubmitTransfer(ctx context.Context, transfer *model.Transfer, source *model.ManagedWallet, destinationAddress string, network Network) (string, error) SubmitTransfer(ctx context.Context, transfer *model.Transfer, source *model.ManagedWallet, destinationAddress string, network shared.Network) (string, error)
AwaitConfirmation(ctx context.Context, network Network, txHash string) (*types.Receipt, error) AwaitConfirmation(ctx context.Context, network shared.Network, txHash string) (*types.Receipt, error)
} }
// NewOnChainExecutor constructs a TransferExecutor that talks to an EVM-compatible chain. // NewOnChainExecutor constructs a TransferExecutor that talks to an EVM-compatible chain.
@@ -45,7 +46,7 @@ type onChainExecutor struct {
clients map[string]*ethclient.Client clients map[string]*ethclient.Client
} }
func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Transfer, source *model.ManagedWallet, destinationAddress string, network Network) (string, error) { func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Transfer, source *model.ManagedWallet, destinationAddress string, network shared.Network) (string, error) {
if o.keyManager == nil { if o.keyManager == nil {
o.logger.Error("key manager not configured") o.logger.Error("key manager not configured")
return "", executorInternal("key manager is not configured", nil) return "", executorInternal("key manager is not configured", nil)
@@ -237,7 +238,7 @@ func (o *onChainExecutor) getClient(ctx context.Context, rpcURL string) (*ethcli
return c, nil return c, nil
} }
func (o *onChainExecutor) AwaitConfirmation(ctx context.Context, network Network, txHash string) (*types.Receipt, error) { func (o *onChainExecutor) AwaitConfirmation(ctx context.Context, network shared.Network, txHash string) (*types.Receipt, error) {
if strings.TrimSpace(txHash) == "" { if strings.TrimSpace(txHash) == "" {
o.logger.Warn("missing transaction hash for confirmation", zap.String("network", network.Name)) o.logger.Warn("missing transaction hash for confirmation", zap.String("network", network.Name))
return nil, executorInvalid("tx hash is required") return nil, executorInvalid("tx hash is required")

View File

@@ -3,35 +3,14 @@ package gateway
import ( import (
"strings" "strings"
"github.com/tech/sendico/chain/gateway/internal/keymanager" "github.com/tech/sendico/gateway/chain/internal/keymanager"
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
clockpkg "github.com/tech/sendico/pkg/clock" clockpkg "github.com/tech/sendico/pkg/clock"
) )
// Option configures the Service. // Option configures the Service.
type Option func(*Service) type Option func(*Service)
// Network describes a supported blockchain network and known token contracts.
type Network struct {
Name string
RPCURL string
ChainID uint64
NativeToken string
TokenConfigs []TokenContract
}
// TokenContract captures the metadata needed to work with a specific on-chain token.
type TokenContract struct {
Symbol string
ContractAddress string
}
// ServiceWallet captures the managed service wallet configuration.
type ServiceWallet struct {
Network string
Address string
PrivateKey string
}
// WithKeyManager configures the service key manager. // WithKeyManager configures the service key manager.
func WithKeyManager(manager keymanager.Manager) Option { func WithKeyManager(manager keymanager.Manager) Option {
return func(s *Service) { return func(s *Service) {
@@ -47,13 +26,13 @@ func WithTransferExecutor(executor TransferExecutor) Option {
} }
// WithNetworks configures supported blockchain networks. // WithNetworks configures supported blockchain networks.
func WithNetworks(networks []Network) Option { func WithNetworks(networks []shared.Network) Option {
return func(s *Service) { return func(s *Service) {
if len(networks) == 0 { if len(networks) == 0 {
return return
} }
if s.networks == nil { if s.networks == nil {
s.networks = make(map[string]Network, len(networks)) s.networks = make(map[string]shared.Network, len(networks))
} }
for _, network := range networks { for _, network := range networks {
if network.Name == "" { if network.Name == "" {
@@ -61,7 +40,7 @@ func WithNetworks(networks []Network) Option {
} }
clone := network clone := network
if clone.TokenConfigs == nil { if clone.TokenConfigs == nil {
clone.TokenConfigs = []TokenContract{} clone.TokenConfigs = []shared.TokenContract{}
} }
for i := range clone.TokenConfigs { for i := range clone.TokenConfigs {
clone.TokenConfigs[i].Symbol = strings.ToUpper(strings.TrimSpace(clone.TokenConfigs[i].Symbol)) clone.TokenConfigs[i].Symbol = strings.ToUpper(strings.TrimSpace(clone.TokenConfigs[i].Symbol))
@@ -74,7 +53,7 @@ func WithNetworks(networks []Network) Option {
} }
// WithServiceWallet configures the service wallet binding. // WithServiceWallet configures the service wallet binding.
func WithServiceWallet(wallet ServiceWallet) Option { func WithServiceWallet(wallet shared.ServiceWallet) Option {
return func(s *Service) { return func(s *Service) {
s.serviceWallet = wallet s.serviceWallet = wallet
} }

View File

@@ -0,0 +1,153 @@
package gateway
import (
"context"
"github.com/tech/sendico/gateway/chain/internal/keymanager"
"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands"
"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands/transfer"
"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands/wallet"
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
"github.com/tech/sendico/gateway/chain/storage"
"github.com/tech/sendico/pkg/api/routers"
"github.com/tech/sendico/pkg/api/routers/gsresponse"
clockpkg "github.com/tech/sendico/pkg/clock"
msg "github.com/tech/sendico/pkg/messaging"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"google.golang.org/grpc"
)
type serviceError string
func (e serviceError) Error() string {
return string(e)
}
var (
errStorageUnavailable = serviceError("chain_gateway: storage not initialised")
)
// Service implements the ChainGatewayService RPC contract.
type Service struct {
logger mlogger.Logger
storage storage.Repository
producer msg.Producer
clock clockpkg.Clock
networks map[string]shared.Network
serviceWallet shared.ServiceWallet
keyManager keymanager.Manager
executor TransferExecutor
commands commands.Registry
chainv1.UnimplementedChainGatewayServiceServer
}
// NewService constructs the chain gateway service skeleton.
func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Producer, opts ...Option) *Service {
svc := &Service{
logger: logger.Named("service"),
storage: repo,
producer: producer,
clock: clockpkg.System{},
networks: map[string]shared.Network{},
}
initMetrics()
for _, opt := range opts {
if opt != nil {
opt(svc)
}
}
if svc.clock == nil {
svc.clock = clockpkg.System{}
}
if svc.networks == nil {
svc.networks = map[string]shared.Network{}
}
svc.commands = commands.NewRegistry(commands.RegistryDeps{
Wallet: commandsWalletDeps(svc),
Transfer: commandsTransferDeps(svc),
})
return svc
}
// Register wires the service onto the provided gRPC router.
func (s *Service) Register(router routers.GRPC) error {
return router.Register(func(reg grpc.ServiceRegistrar) {
chainv1.RegisterChainGatewayServiceServer(reg, s)
})
}
func (s *Service) CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error) {
return executeUnary(ctx, s, "CreateManagedWallet", s.commands.CreateManagedWallet.Execute, req)
}
func (s *Service) GetManagedWallet(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error) {
return executeUnary(ctx, s, "GetManagedWallet", s.commands.GetManagedWallet.Execute, req)
}
func (s *Service) ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error) {
return executeUnary(ctx, s, "ListManagedWallets", s.commands.ListManagedWallets.Execute, req)
}
func (s *Service) GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error) {
return executeUnary(ctx, s, "GetWalletBalance", s.commands.GetWalletBalance.Execute, req)
}
func (s *Service) SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
return executeUnary(ctx, s, "SubmitTransfer", s.commands.SubmitTransfer.Execute, req)
}
func (s *Service) GetTransfer(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error) {
return executeUnary(ctx, s, "GetTransfer", s.commands.GetTransfer.Execute, req)
}
func (s *Service) ListTransfers(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error) {
return executeUnary(ctx, s, "ListTransfers", s.commands.ListTransfers.Execute, req)
}
func (s *Service) EstimateTransferFee(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error) {
return executeUnary(ctx, s, "EstimateTransferFee", s.commands.EstimateTransfer.Execute, req)
}
func (s *Service) ensureRepository(ctx context.Context) error {
if s.storage == nil {
return errStorageUnavailable
}
return s.storage.Ping(ctx)
}
func commandsWalletDeps(s *Service) wallet.Deps {
return wallet.Deps{
Logger: s.logger.Named("command"),
Networks: s.networks,
KeyManager: s.keyManager,
Storage: s.storage,
EnsureRepository: s.ensureRepository,
}
}
func commandsTransferDeps(s *Service) transfer.Deps {
return transfer.Deps{
Logger: s.logger.Named("transfer_cmd"),
Networks: s.networks,
Storage: s.storage,
Clock: s.clock,
EnsureRepository: s.ensureRepository,
LaunchExecution: s.launchTransferExecution,
}
}
func executeUnary[TReq any, TResp any](ctx context.Context, svc *Service, method string, handler func(context.Context, *TReq) gsresponse.Responder[TResp], req *TReq) (*TResp, error) {
start := svc.clock.Now()
resp, err := gsresponse.Unary(svc.logger, mservice.ChainGateway, handler)(ctx, req)
observeRPC(method, err, svc.clock.Now().Sub(start))
return resp, err
}

View File

@@ -11,15 +11,16 @@ import (
"time" "time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
igatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1" ichainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap" "go.uber.org/zap"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"github.com/tech/sendico/chain/gateway/internal/keymanager" "github.com/tech/sendico/gateway/chain/internal/keymanager"
"github.com/tech/sendico/chain/gateway/storage" "github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
"github.com/tech/sendico/chain/gateway/storage/model" "github.com/tech/sendico/gateway/chain/storage"
"github.com/tech/sendico/gateway/chain/storage/model"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1" paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
@@ -40,12 +41,12 @@ func TestCreateManagedWallet_Idempotent(t *testing.T) {
svc, repo := newTestService(t) svc, repo := newTestService(t)
ctx := context.Background() ctx := context.Background()
req := &igatewayv1.CreateManagedWalletRequest{ req := &ichainv1.CreateManagedWalletRequest{
IdempotencyKey: "idem-1", IdempotencyKey: "idem-1",
OrganizationRef: "org-1", OrganizationRef: "org-1",
OwnerRef: "owner-1", OwnerRef: "owner-1",
Asset: &igatewayv1.Asset{ Asset: &ichainv1.Asset{
Chain: igatewayv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET, Chain: ichainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET,
TokenSymbol: "USDC", TokenSymbol: "USDC",
}, },
} }
@@ -69,12 +70,12 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
ctx := context.Background() ctx := context.Background()
// create source wallet // create source wallet
srcResp, err := svc.CreateManagedWallet(ctx, &igatewayv1.CreateManagedWalletRequest{ srcResp, err := svc.CreateManagedWallet(ctx, &ichainv1.CreateManagedWalletRequest{
IdempotencyKey: "idem-src", IdempotencyKey: "idem-src",
OrganizationRef: "org-1", OrganizationRef: "org-1",
OwnerRef: "owner-1", OwnerRef: "owner-1",
Asset: &igatewayv1.Asset{ Asset: &ichainv1.Asset{
Chain: igatewayv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET, Chain: ichainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET,
TokenSymbol: "USDC", TokenSymbol: "USDC",
}, },
}) })
@@ -82,27 +83,27 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
srcRef := srcResp.GetWallet().GetWalletRef() srcRef := srcResp.GetWallet().GetWalletRef()
// destination wallet // destination wallet
dstResp, err := svc.CreateManagedWallet(ctx, &igatewayv1.CreateManagedWalletRequest{ dstResp, err := svc.CreateManagedWallet(ctx, &ichainv1.CreateManagedWalletRequest{
IdempotencyKey: "idem-dst", IdempotencyKey: "idem-dst",
OrganizationRef: "org-1", OrganizationRef: "org-1",
OwnerRef: "owner-2", OwnerRef: "owner-2",
Asset: &igatewayv1.Asset{ Asset: &ichainv1.Asset{
Chain: igatewayv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET, Chain: ichainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET,
TokenSymbol: "USDC", TokenSymbol: "USDC",
}, },
}) })
require.NoError(t, err) require.NoError(t, err)
dstRef := dstResp.GetWallet().GetWalletRef() dstRef := dstResp.GetWallet().GetWalletRef()
transferResp, err := svc.SubmitTransfer(ctx, &igatewayv1.SubmitTransferRequest{ transferResp, err := svc.SubmitTransfer(ctx, &ichainv1.SubmitTransferRequest{
IdempotencyKey: "transfer-1", IdempotencyKey: "transfer-1",
OrganizationRef: "org-1", OrganizationRef: "org-1",
SourceWalletRef: srcRef, SourceWalletRef: srcRef,
Destination: &igatewayv1.TransferDestination{ Destination: &ichainv1.TransferDestination{
Destination: &igatewayv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: dstRef}, Destination: &ichainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: dstRef},
}, },
Amount: &moneyv1.Money{Currency: "USDC", Amount: "100"}, Amount: &moneyv1.Money{Currency: "USDC", Amount: "100"},
Fees: []*igatewayv1.ServiceFeeBreakdown{ Fees: []*ichainv1.ServiceFeeBreakdown{
{ {
FeeCode: "service", FeeCode: "service",
Amount: &moneyv1.Money{Currency: "USDC", Amount: "5"}, Amount: &moneyv1.Money{Currency: "USDC", Amount: "5"},
@@ -118,12 +119,12 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
require.Equal(t, model.TransferStatusPending, stored.Status) require.Equal(t, model.TransferStatusPending, stored.Status)
// GetTransfer // GetTransfer
getResp, err := svc.GetTransfer(ctx, &igatewayv1.GetTransferRequest{TransferRef: stored.TransferRef}) getResp, err := svc.GetTransfer(ctx, &ichainv1.GetTransferRequest{TransferRef: stored.TransferRef})
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, stored.TransferRef, getResp.GetTransfer().GetTransferRef()) require.Equal(t, stored.TransferRef, getResp.GetTransfer().GetTransferRef())
// ListTransfers // ListTransfers
listResp, err := svc.ListTransfers(ctx, &igatewayv1.ListTransfersRequest{ listResp, err := svc.ListTransfers(ctx, &ichainv1.ListTransfersRequest{
SourceWalletRef: srcRef, SourceWalletRef: srcRef,
Page: &paginationv1.CursorPageRequest{Limit: 10}, Page: &paginationv1.CursorPageRequest{Limit: 10},
}) })
@@ -136,7 +137,7 @@ func TestGetWalletBalance_NotFound(t *testing.T) {
svc, _ := newTestService(t) svc, _ := newTestService(t)
ctx := context.Background() ctx := context.Background()
_, err := svc.GetWalletBalance(ctx, &igatewayv1.GetWalletBalanceRequest{WalletRef: "missing"}) _, err := svc.GetWalletBalance(ctx, &ichainv1.GetWalletBalanceRequest{WalletRef: "missing"})
require.Error(t, err) require.Error(t, err)
st, _ := status.FromError(err) st, _ := status.FromError(err)
require.Equal(t, codes.NotFound, st.Code()) require.Equal(t, codes.NotFound, st.Code())
@@ -530,13 +531,13 @@ func newTestService(_ *testing.T) (*Service, *inMemoryRepository) {
logger := zap.NewNop() logger := zap.NewNop()
svc := NewService(logger, repo, nil, svc := NewService(logger, repo, nil,
WithKeyManager(&fakeKeyManager{}), WithKeyManager(&fakeKeyManager{}),
WithNetworks([]Network{{ WithNetworks([]shared.Network{{
Name: "ethereum_mainnet", Name: "ethereum_mainnet",
TokenConfigs: []TokenContract{ TokenConfigs: []shared.TokenContract{
{Symbol: "USDC", ContractAddress: "0xusdc"}, {Symbol: "USDC", ContractAddress: "0xusdc"},
}, },
}}), }}),
WithServiceWallet(ServiceWallet{Network: "ethereum_mainnet", Address: "0xservice"}), WithServiceWallet(shared.ServiceWallet{Network: "ethereum_mainnet", Address: "0xservice"}),
) )
return svc, repo return svc, repo
} }

View File

@@ -0,0 +1,142 @@
package shared
import (
"strings"
"github.com/tech/sendico/gateway/chain/storage/model"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// CloneMoney defensively copies a Money proto.
func CloneMoney(m *moneyv1.Money) *moneyv1.Money {
if m == nil {
return nil
}
return &moneyv1.Money{Amount: m.GetAmount(), Currency: m.GetCurrency()}
}
// CloneMetadata defensively copies metadata maps.
func CloneMetadata(input map[string]string) map[string]string {
if len(input) == 0 {
return nil
}
clone := make(map[string]string, len(input))
for k, v := range input {
clone[k] = v
}
return clone
}
// ResolveContractAddress finds a token contract for a symbol in a case-insensitive manner.
func ResolveContractAddress(tokens []TokenContract, symbol string) string {
upper := strings.ToUpper(symbol)
for _, token := range tokens {
if strings.EqualFold(token.Symbol, upper) && token.ContractAddress != "" {
return strings.ToLower(token.ContractAddress)
}
}
return ""
}
func GenerateWalletRef() string {
return primitive.NewObjectID().Hex()
}
func GenerateTransferRef() string {
return primitive.NewObjectID().Hex()
}
func ChainKeyFromEnum(chain chainv1.ChainNetwork) (string, chainv1.ChainNetwork) {
if name, ok := chainv1.ChainNetwork_name[int32(chain)]; ok {
key := strings.ToLower(strings.TrimPrefix(name, "CHAIN_NETWORK_"))
return key, chain
}
return "", chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
}
func ChainEnumFromName(name string) chainv1.ChainNetwork {
if name == "" {
return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
}
upper := strings.ToUpper(strings.ReplaceAll(strings.ReplaceAll(name, " ", "_"), "-", "_"))
key := "CHAIN_NETWORK_" + upper
if val, ok := chainv1.ChainNetwork_value[key]; ok {
return chainv1.ChainNetwork(val)
}
return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
}
func ManagedWalletStatusToProto(status model.ManagedWalletStatus) chainv1.ManagedWalletStatus {
switch status {
case model.ManagedWalletStatusActive:
return chainv1.ManagedWalletStatus_MANAGED_WALLET_ACTIVE
case model.ManagedWalletStatusSuspended:
return chainv1.ManagedWalletStatus_MANAGED_WALLET_SUSPENDED
case model.ManagedWalletStatusClosed:
return chainv1.ManagedWalletStatus_MANAGED_WALLET_CLOSED
default:
return chainv1.ManagedWalletStatus_MANAGED_WALLET_STATUS_UNSPECIFIED
}
}
func TransferStatusToModel(status chainv1.TransferStatus) model.TransferStatus {
switch status {
case chainv1.TransferStatus_TRANSFER_PENDING:
return model.TransferStatusPending
case chainv1.TransferStatus_TRANSFER_SIGNING:
return model.TransferStatusSigning
case chainv1.TransferStatus_TRANSFER_SUBMITTED:
return model.TransferStatusSubmitted
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
return model.TransferStatusConfirmed
case chainv1.TransferStatus_TRANSFER_FAILED:
return model.TransferStatusFailed
case chainv1.TransferStatus_TRANSFER_CANCELLED:
return model.TransferStatusCancelled
default:
return ""
}
}
func TransferStatusToProto(status model.TransferStatus) chainv1.TransferStatus {
switch status {
case model.TransferStatusPending:
return chainv1.TransferStatus_TRANSFER_PENDING
case model.TransferStatusSigning:
return chainv1.TransferStatus_TRANSFER_SIGNING
case model.TransferStatusSubmitted:
return chainv1.TransferStatus_TRANSFER_SUBMITTED
case model.TransferStatusConfirmed:
return chainv1.TransferStatus_TRANSFER_CONFIRMED
case model.TransferStatusFailed:
return chainv1.TransferStatus_TRANSFER_FAILED
case model.TransferStatusCancelled:
return chainv1.TransferStatus_TRANSFER_CANCELLED
default:
return chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
}
}
// Network describes a supported blockchain network and known token contracts.
type Network struct {
Name string
RPCURL string
ChainID uint64
NativeToken string
TokenConfigs []TokenContract
}
// TokenContract captures the metadata needed to work with a specific on-chain token.
type TokenContract struct {
Symbol string
ContractAddress string
}
// ServiceWallet captures the managed service wallet configuration.
type ServiceWallet struct {
Network string
Address string
PrivateKey string
}

View File

@@ -7,17 +7,19 @@ import (
"time" "time"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/tech/sendico/chain/gateway/storage/model" "github.com/tech/sendico/gateway/chain/storage/model"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
) )
func (s *Service) launchTransferExecution(transferRef, sourceWalletRef string, network Network) { func (s *Service) launchTransferExecution(transferRef, sourceWalletRef string, network shared.Network) {
if s.executor == nil { if s.executor == nil {
return return
} }
go func(ref, walletRef string, net Network) { go func(ref, walletRef string, net shared.Network) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel() defer cancel()
@@ -27,7 +29,7 @@ func (s *Service) launchTransferExecution(transferRef, sourceWalletRef string, n
}(transferRef, sourceWalletRef, network) }(transferRef, sourceWalletRef, network)
} }
func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWalletRef string, network Network) error { func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWalletRef string, network shared.Network) error {
transfer, err := s.storage.Transfers().Get(ctx, transferRef) transfer, err := s.storage.Transfers().Get(ctx, transferRef)
if err != nil { if err != nil {
return err return err

View File

@@ -1,8 +1,8 @@
package main package main
import ( import (
"github.com/tech/sendico/chain/gateway/internal/appversion" "github.com/tech/sendico/gateway/chain/internal/appversion"
si "github.com/tech/sendico/chain/gateway/internal/server" si "github.com/tech/sendico/gateway/chain/internal/server"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/server" "github.com/tech/sendico/pkg/server"
smain "github.com/tech/sendico/pkg/server/main" smain "github.com/tech/sendico/pkg/server/main"

View File

@@ -4,8 +4,8 @@ import (
"context" "context"
"time" "time"
"github.com/tech/sendico/chain/gateway/storage" "github.com/tech/sendico/gateway/chain/storage"
"github.com/tech/sendico/chain/gateway/storage/mongo/store" "github.com/tech/sendico/gateway/chain/storage/mongo/store"
"github.com/tech/sendico/pkg/db" "github.com/tech/sendico/pkg/db"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"

View File

@@ -6,8 +6,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/tech/sendico/chain/gateway/storage" "github.com/tech/sendico/gateway/chain/storage"
"github.com/tech/sendico/chain/gateway/storage/model" "github.com/tech/sendico/gateway/chain/storage/model"
"github.com/tech/sendico/pkg/db/repository" "github.com/tech/sendico/pkg/db/repository"
ri "github.com/tech/sendico/pkg/db/repository/index" ri "github.com/tech/sendico/pkg/db/repository/index"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"

View File

@@ -6,8 +6,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/tech/sendico/chain/gateway/storage" "github.com/tech/sendico/gateway/chain/storage"
"github.com/tech/sendico/chain/gateway/storage/model" "github.com/tech/sendico/gateway/chain/storage/model"
"github.com/tech/sendico/pkg/db/repository" "github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/db/repository/builder" "github.com/tech/sendico/pkg/db/repository/builder"
ri "github.com/tech/sendico/pkg/db/repository/index" ri "github.com/tech/sendico/pkg/db/repository/index"

View File

@@ -6,8 +6,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/tech/sendico/chain/gateway/storage" "github.com/tech/sendico/gateway/chain/storage"
"github.com/tech/sendico/chain/gateway/storage/model" "github.com/tech/sendico/gateway/chain/storage/model"
"github.com/tech/sendico/pkg/db/repository" "github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/db/repository/builder" "github.com/tech/sendico/pkg/db/repository/builder"
ri "github.com/tech/sendico/pkg/db/repository/index" ri "github.com/tech/sendico/pkg/db/repository/index"

View File

@@ -3,7 +3,7 @@ package storage
import ( import (
"context" "context"
"github.com/tech/sendico/chain/gateway/storage/model" "github.com/tech/sendico/gateway/chain/storage/model"
) )
type storageError string type storageError string

View File

@@ -16,6 +16,8 @@ import (
// Client exposes typed helpers around the ledger gRPC API. // Client exposes typed helpers around the ledger gRPC API.
type Client interface { type Client interface {
CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error)
ListAccounts(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error)
PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error)
PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error)
TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error)
@@ -29,6 +31,8 @@ type Client interface {
} }
type grpcLedgerClient interface { type grpcLedgerClient interface {
CreateAccount(ctx context.Context, in *ledgerv1.CreateAccountRequest, opts ...grpc.CallOption) (*ledgerv1.CreateAccountResponse, error)
ListAccounts(ctx context.Context, in *ledgerv1.ListAccountsRequest, opts ...grpc.CallOption) (*ledgerv1.ListAccountsResponse, error)
PostCreditWithCharges(ctx context.Context, in *ledgerv1.PostCreditRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error) PostCreditWithCharges(ctx context.Context, in *ledgerv1.PostCreditRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
PostDebitWithCharges(ctx context.Context, in *ledgerv1.PostDebitRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error) PostDebitWithCharges(ctx context.Context, in *ledgerv1.PostDebitRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
TransferInternal(ctx context.Context, in *ledgerv1.TransferRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error) TransferInternal(ctx context.Context, in *ledgerv1.TransferRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
@@ -91,6 +95,18 @@ func (c *ledgerClient) Close() error {
return nil return nil
} }
func (c *ledgerClient) CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error) {
ctx, cancel := c.callContext(ctx)
defer cancel()
return c.client.CreateAccount(ctx, req)
}
func (c *ledgerClient) ListAccounts(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error) {
ctx, cancel := c.callContext(ctx)
defer cancel()
return c.client.ListAccounts(ctx, req)
}
func (c *ledgerClient) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) { func (c *ledgerClient) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
ctx, cancel := c.callContext(ctx) ctx, cancel := c.callContext(ctx)
defer cancel() defer cancel()

View File

@@ -8,6 +8,8 @@ import (
// Fake implements Client for tests. // Fake implements Client for tests.
type Fake struct { type Fake struct {
CreateAccountFn func(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error)
ListAccountsFn func(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error)
PostCreditWithChargesFn func(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) PostCreditWithChargesFn func(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error)
PostDebitWithChargesFn func(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) PostDebitWithChargesFn func(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error)
TransferInternalFn func(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) TransferInternalFn func(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error)
@@ -18,6 +20,20 @@ type Fake struct {
CloseFn func() error CloseFn func() error
} }
func (f *Fake) CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error) {
if f.CreateAccountFn != nil {
return f.CreateAccountFn(ctx, req)
}
return &ledgerv1.CreateAccountResponse{}, nil
}
func (f *Fake) ListAccounts(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error) {
if f.ListAccountsFn != nil {
return f.ListAccountsFn(ctx, req)
}
return &ledgerv1.ListAccountsResponse{}, nil
}
func (f *Fake) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) { func (f *Fake) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
if f.PostCreditWithChargesFn != nil { if f.PostCreditWithChargesFn != nil {
return f.PostCreditWithChargesFn(ctx, req) return f.PostCreditWithChargesFn(ctx, req)

View File

@@ -34,14 +34,14 @@ require (
github.com/montanaflynn/stats v0.7.1 // indirect github.com/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.47.0 // indirect github.com/nats-io/nats.go v1.47.0 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nkeys v0.4.12 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect github.com/prometheus/procfs v0.19.2 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
@@ -51,5 +51,5 @@ require (
golang.org/x/sync v0.18.0 // indirect golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect golang.org/x/text v0.31.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
) )

View File

@@ -97,8 +97,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM= github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -143,8 +143,8 @@ github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfj
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
@@ -214,8 +214,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= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=

View File

@@ -0,0 +1,47 @@
package ledger
import (
"context"
"strings"
"github.com/tech/sendico/pkg/api/routers/gsresponse"
"github.com/tech/sendico/pkg/merrors"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
"go.uber.org/zap"
)
func (s *Service) listAccountsResponder(_ context.Context, req *ledgerv1.ListAccountsRequest) gsresponse.Responder[ledgerv1.ListAccountsResponse] {
return func(ctx context.Context) (*ledgerv1.ListAccountsResponse, error) {
if s.storage == nil {
return nil, errStorageNotInitialized
}
if req == nil {
return nil, merrors.InvalidArgument("request is required")
}
orgRefStr := strings.TrimSpace(req.GetOrganizationRef())
if orgRefStr == "" {
return nil, merrors.InvalidArgument("organization_ref is required")
}
orgRef, err := parseObjectID(orgRefStr)
if err != nil {
return nil, err
}
// No pagination requested; return all accounts for the organization.
accounts, err := s.storage.Accounts().ListByOrganization(ctx, orgRef, 0, 0)
if err != nil {
s.logger.Warn("failed to list ledger accounts", zap.Error(err), zap.String("organizationRef", orgRef.Hex()))
return nil, err
}
resp := &ledgerv1.ListAccountsResponse{
Accounts: make([]*ledgerv1.LedgerAccount, 0, len(accounts)),
}
for _, acc := range accounts {
resp.Accounts = append(resp.Accounts, toProtoAccount(acc))
}
return resp, nil
}
}

View File

@@ -249,7 +249,7 @@ func (s *Service) getStatementResponder(_ context.Context, req *ledgerv1.GetStat
func parseCursor(cursor string) (int, error) { func parseCursor(cursor string) (int, error) {
decoded, err := base64.StdEncoding.DecodeString(cursor) decoded, err := base64.StdEncoding.DecodeString(cursor)
if err != nil { if err != nil {
return 0, merrors.InvalidArgumentWrap(err, "invalid cursor base64 encoding") return 0, merrors.InvalidArgumentWrap(err, "invalid base64")
} }
parts := strings.Split(string(decoded), ":") parts := strings.Split(string(decoded), ":")
if len(parts) != 2 || parts[0] != "offset" { if len(parts) != 2 || parts[0] != "offset" {
@@ -257,7 +257,7 @@ func parseCursor(cursor string) (int, error) {
} }
offset, err := strconv.Atoi(parts[1]) offset, err := strconv.Atoi(parts[1])
if err != nil { if err != nil {
return 0, merrors.InvalidArgumentWrap(err, "invalid cursor offset") return 0, merrors.InvalidArgumentWrap(err, "invalid offset")
} }
return offset, nil return offset, nil
} }

View File

@@ -81,6 +81,12 @@ func (s *Service) Register(router routers.GRPC) error {
}) })
} }
// ListAccounts lists ledger accounts for an organization.
func (s *Service) ListAccounts(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error) {
responder := s.listAccountsResponder(ctx, req)
return responder(ctx)
}
// CreateAccount provisions a new ledger account scoped to an organization. // CreateAccount provisions a new ledger account scoped to an organization.
func (s *Service) CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error) { func (s *Service) CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error) {
responder := s.createAccountResponder(ctx, req) responder := s.createAccountResponder(ctx, req)

View File

@@ -1 +1,2 @@
notification notification
.gocache

View File

@@ -51,7 +51,7 @@ api:
settings: settings:
username_env: MAIL_USER username_env: MAIL_USER
password_env: MAIL_SECRET password_env: MAIL_SECRET
host: "smtp.mail.ru" host: "mail.sendico.io"
port: 465 port: 465
from: "Sendico Tech" from: "Sendico Tech"
network_timeout: 10 network_timeout: 10
@@ -65,7 +65,7 @@ api:
localizer: localizer:
path: "./i18n" path: "./i18n"
languages: ["en", "ru", "uk"] languages: ["en", "ru"]
service_name: "Sendico" service_name: "Sendico"
support: "support@sendico.io" support: "support@sendico.io"

View File

@@ -34,7 +34,7 @@ require (
github.com/montanaflynn/stats v0.7.1 // indirect github.com/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.47.0 // indirect github.com/nats-io/nats.go v1.47.0 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nkeys v0.4.12 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
@@ -43,7 +43,7 @@ require (
github.com/sendgrid/rest v2.6.9+incompatible // indirect github.com/sendgrid/rest v2.6.9+incompatible // indirect
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // indirect github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
@@ -52,7 +52,7 @@ require (
golang.org/x/net v0.47.0 // indirect golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect golang.org/x/sys v0.38.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
google.golang.org/grpc v1.77.0 // indirect google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect google.golang.org/protobuf v1.36.10 // indirect
) )

View File

@@ -101,8 +101,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM= github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ= github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ=
@@ -154,8 +154,8 @@ github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 h1:q0hKh5a5FRkhuTb5
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns= github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA= github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
@@ -227,8 +227,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= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -46,7 +46,7 @@ func (m *EmailNotificationTemplate) prepareUnsubscribe(msg mmail.Message) error
return err return err
} }
localization.AddLocData(d, "UnsubscribeLink", unsLink) localization.AddLocData(d, "UnsubscribeLink", unsLink)
if block, err = m.l.LocalizeTemplate("mail.template.unsubscribe.block", d, nil, msg.Locale()); err != nil { if block, err = renderUnsubscribeBlock(d); err != nil {
return err return err
} }
} }
@@ -58,7 +58,7 @@ func (m *EmailNotificationTemplate) prepareButton(msg mmail.Message) error {
var block string var block string
if m.hasButton { if m.hasButton {
var err error var err error
if block, err = m.l.LocalizeTemplate("mail.template.btn.block", m.data, nil, msg.Locale()); err != nil { if block, err = renderButtonBlock(m.data); err != nil {
return err return err
} }
} }
@@ -91,7 +91,7 @@ func (m *EmailNotificationTemplate) SignatureData(msg mmail.Message, content, su
return "", err return "", err
} }
return m.l.LocalizeTemplate("mail.template.one_button", m.data, nil, msg.Locale()) return renderOneButtonEmail(m.data)
} }
func (m *EmailNotificationTemplate) putOnHTMLTemplate(msg mmail.Message, content, subj string) (string, error) { func (m *EmailNotificationTemplate) putOnHTMLTemplate(msg mmail.Message, content, subj string) (string, error) {

View File

@@ -0,0 +1,285 @@
package mailimp
import (
"bytes"
"text/template"
)
var (
unsubscribeBlockTpl = template.Must(template.New("unsubscribeBlock").Parse(`
<a
target="_blank"
style="-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;text-decoration:none;color:#2D3142;font-size:13px"
href=""
>
</a>
<a
target="_blank"
style="-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;text-decoration:none;color:#2D3142;font-size:14px"
href="{{.UnsubscribeLink}}"
>
{{.Unsubscribe}}
</a>`))
buttonBlockTpl = template.Must(template.New("buttonBlock").Parse(`
<tr>
<td align="center" style="padding:0;Margin:0">
<!--[if mso]>
<a href="" target="_blank" hidden>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" esdevVmlButton href="{{.ButtonLink}}"
style="height:56px; v-text-anchor:middle; width:520px" arcsize="14%" stroke="f" fillcolor="#0b58ff">
<w:anchorlock></w:anchorlock>
<center style='color:#ffffff; font-family:montserrat, roboto; font-size:22px; font-weight:700; line-height:22px; mso-text-raise:1px'>{{.ButtonText}}</center>
</v:roundrect>
</a>
<![endif]-->
<!--[if !mso]><!-- -->
<span class="msohide es-button-border" style="border-style:solid;border-color:#0b58ff;background:#0b58ff;border-width:0px;display:block;border-radius:8px;width:auto;mso-border-alt:10px;mso-hide:all;width:520px">
<a
href="{{.ButtonLink}}"
class="es-button msohide"
target="_blank"
style="mso-style-priority:100 !important;text-decoration:none;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;color:#ffffff;font-size:22px;padding:15px 20px 15px 20px;display:block;background:#0b58ff;border-radius:8px;font-family:montserrat, roboto;font-weight:bold;font-style:normal;line-height:26px;width:auto;text-align:center;border-color:#0b58ff;mso-hide:all;padding-left:5px;padding-right:5px"
>
{{.ButtonText}}
</a>
</span>
<!--<![endif]-->
</td>
</tr>`))
oneButtonEmailTpl = template.Must(template.New("oneButtonEmail").Parse(`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office" style="font-family:montserrat, roboto">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1" name="viewport">
<meta name="x-apple-disable-message-reformatting">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta content="telephone=no" name="format-detection">
<title>{{.MessageTitle}}</title>
<!--[if (mso 16)]>
<style type="text/css">
a {text-decoration: none;}
</style>
<![endif]-->
<!--[if gte mso 9]><style>sup { font-size: 100% !important; }</style><![endif]-->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG></o:AllowPNG>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if !mso]><!-- -->
<link href="https://fonts.googleapis.com/css2?family=Imprima&display=swap" rel="stylesheet">
<!--<![endif]-->
<!--[if !mso]><!-- -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat:100,300,400,500,700,900">
<!--<![endif]-->
<style type="text/css">
#outlook a { padding:0; }
.es-button { mso-style-priority:100!important; text-decoration:none!important; }
a[x-apple-data-detectors] {
color:inherit!important;
text-decoration:none!important;
font-size:inherit!important;
font-family:inherit!important;
font-weight:inherit!important;
line-height:inherit!important;
}
.es-desk-hidden { display:none; float:left; overflow:hidden; width:0; max-height:0; line-height:0; mso-hide:all; }
@media only screen and (max-width:600px) {
p, ul li, ol li, a { line-height:150%!important }
h1, h2, h3, h1 a, h2 a, h3 a { line-height:120% }
h1 { font-size:30px!important; text-align:left }
h2 { font-size:24px!important; text-align:left }
h3 { font-size:20px!important; text-align:left }
.es-header-body h1 a, .es-content-body h1 a, .es-footer-body h1 a { font-size:30px!important; text-align:left }
.es-header-body h2 a, .es-content-body h2 a, .es-footer-body h2 a { font-size:24px!important; text-align:left }
.es-header-body h3 a, .es-content-body h3 a, .es-footer-body h3 a { font-size:20px!important; text-align:left }
.es-menu td a { font-size:14px!important }
.es-header-body p, .es-header-body ul li, .es-header-body ol li, .es-header-body a { font-size:14px!important }
.es-content-body p, .es-content-body ul li, .es-content-body ol li, .es-content-body a { font-size:14px!important }
.es-footer-body p, .es-footer-body ul li, .es-footer-body ol li, .es-footer-body a { font-size:14px!important }
.es-infoblock p, .es-infoblock ul li, .es-infoblock ol li, .es-infoblock a { font-size:12px!important }
*[class="gmail-fix"] { display:none!important }
.es-m-txt-c, .es-m-txt-c h1, .es-m-txt-c h2, .es-m-txt-c h3 { text-align:center!important }
.es-m-txt-r, .es-m-txt-r h1, .es-m-txt-r h2, .es-m-txt-r h3 { text-align:right!important }
.es-m-txt-l, .es-m-txt-l h1, .es-m-txt-l h2, .es-m-txt-l h3 { text-align:left!important }
.es-m-txt-r img, .es-m-txt-c img, .es-m-txt-l img { display:inline!important }
.es-button-border { display:block!important }
a.es-button, button.es-button { font-size:18px!important; display:block!important; border-right-width:0px!important; border-left-width:0px!important; border-top-width:15px!important; border-bottom-width:15px!important; padding-left:0px!important; padding-right:0px!important }
.es-adaptive table, .es-left, .es-right { width:100%!important }
.es-content table, .es-header table, .es-footer table, .es-content, .es-footer, .es-header { width:100%!important; max-width:600px!important }
.es-adapt-td { display:block!important; width:100%!important }
.adapt-img { width:100%!important; height:auto!important }
.es-m-p0 { padding:0px!important }
.es-m-p0r { padding-right:0px!important }
.es-m-p0l { padding-left:0px!important }
.es-m-p0t { padding-top:0px!important }
.es-m-p0b { padding-bottom:0!important }
.es-m-p20b { padding-bottom:20px!important }
.es-mobile-hidden, .es-hidden { display:none!important }
tr.es-desk-hidden, td.es-desk-hidden, table.es-desk-hidden { width:auto!important; overflow:visible!important; float:none!important; max-height:inherit!important; line-height:inherit!important }
tr.es-desk-hidden { display:table-row!important }
table.es-desk-hidden { display:table!important }
td.es-desk-menu-hidden { display:table-cell!important }
.es-menu td { width:1%!important }
table.es-table-not-adapt, .esd-block-html table { width:auto!important }
table.es-social { display:inline-block!important }
table.es-social td { display:inline-block!important }
.es-desk-hidden { display:table-row!important; width:auto!important; overflow:visible!important; max-height:inherit!important }
}
</style>
</head>
<body style="width:100%;font-family:montserrat, roboto;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;padding:0;Margin:0">
<div class="es-wrapper-color" style="background-color:#FFFFFF">
<!--[if gte mso 9]>
<v:background xmlns:v="urn:schemas-microsoft-com:vml" fill="t">
<v:fill type="tile" color="#ffffff"></v:fill>
</v:background>
<![endif]-->
<table class="es-wrapper" width="100%" cellspacing="0" cellpadding="0" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;padding:0;Margin:0;width:100%;height:100%;background-repeat:repeat;background-position:center top;background-color:#FFFFFF">
<tr>
<td valign="top" style="padding:0;Margin:0">
<table cellpadding="0" cellspacing="0" class="es-content" align="center" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;table-layout:fixed !important;width:100%">
<tr>
<td align="center" style="padding:0;Margin:0">
<table bgcolor="#ffffff" class="es-content-body" align="center" cellpadding="0" cellspacing="0" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;background-color:#FFFFFF;border-radius:20px 20px 0 0;width:600px">
<tr>
<td align="left" style="padding:0;Margin:0;padding-top:20px;padding-left:40px;padding-right:40px;border-radius:8px 8px 0px 0px">
<table cellpadding="0" cellspacing="0" width="100%" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px">
<tr>
<td align="center" valign="top" style="padding:0;Margin:0;width:520px">
<table cellpadding="0" cellspacing="0" width="100%" bgcolor="#fafafa" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:separate;border-spacing:0px;background-color:#fafafa;border-radius:10px" role="presentation">
<tr>
<td align="left" style="padding:20px;Margin:0">
<h3 style="Margin:0;line-height:34px;mso-line-height-rule:exactly;font-family:montserrat, roboto;font-size:28px;font-style:normal;font-weight:bold;color:#2D3142">
{{.Greeting}}
</h3>
<p style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-family:montserrat, roboto;line-height:27px;color:#2D3142;font-size:18px"><br></p>
<p style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-family:montserrat, roboto;line-height:27px;color:#2D3142;font-size:18px">
{{.Content}}
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<table cellpadding="0" cellspacing="0" class="es-content" align="center" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;table-layout:fixed !important;width:100%">
{{.ButtonBlock}}
</table>
<table cellpadding="0" cellspacing="0" class="es-footer" align="center" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;table-layout:fixed !important;width:100%;background-color:transparent;background-repeat:repeat;background-position:center top">
<tr>
<td align="center" style="padding:0;Margin:0">
<table bgcolor="#bcb8b1" class="es-footer-body" align="center" cellpadding="0" cellspacing="0" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;background-color:#FFFFFF;width:600px">
<tr>
<td align="left" style="Margin:0;padding-left:20px;padding-right:20px;padding-bottom:30px;padding-top:40px">
<!--[if mso]><table style="width:560px" cellpadding="0" cellspacing="0"><tr><td style="width:82px" valign="top"><![endif]-->
<table cellpadding="0" cellspacing="0" align="left" class="es-left" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;float:left">
<tr>
<td align="left" class="es-m-p20b" style="padding:0;Margin:0;width:82px">
<table cellpadding="0" cellspacing="0" width="100%" role="presentation" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px">
<tr>
<td
align="center"
style="padding:0;Margin:0;padding-left:20px;font-size:0px">
<img class="adapt-img"
src="{{.LogoLink}}"
alt
style="display:block;border:0;outline:none;text-decoration:none;-ms-interpolation-mode:bicubic" width="62"
/>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso]></td><td style="width:20px"></td><td style="width:458px" valign="top"><![endif]-->
<table cellpadding="0" cellspacing="0" class="es-right" align="right" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;float:right">
<tr>
<td align="left" style="padding:0;Margin:0;width:458px">
<table cellpadding="0" cellspacing="0" width="100%" role="presentation" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px">
<tr>
<td align="left" style="padding:0;Margin:0">
<p style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-family:montserrat, roboto;line-height:20px;color:#2D3142;font-size:13px">
<a target="_blank" style="-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;text-decoration:none;color:#2D3142;font-size:14px" href=""></a>
<a
target="_blank"
style="-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;text-decoration:none;color:#2D3142;font-size:14px"
href="{{.PolicyLink}}"
>
{{.Privacy}}
</a>
{{.UnsubscribeBlock}}
</p>
</td>
</tr>
<tr>
<td align="left" style="padding:0;Margin:0">
<p style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-family:montserrat, roboto;line-height:20px;color:#2D3142;font-size:13px">
{{.ServiceOwner}}
</p>
</td>
</tr>
<tr>
<td align="left" style="padding:0;Margin:0">
<p style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-family:montserrat, roboto;line-height:20px;color:#2D3142;font-size:13px">
{{.OwnerAddress}}
</p>
</td>
</tr>
<tr>
<td align="left" style="padding:0;Margin:0">
<p style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-family:montserrat, roboto;line-height:20px;color:#2D3142;font-size:13px">
{{.OwnerPhone}}
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso]></td></tr></table><![endif]-->
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</body>
</html>`))
)
func renderTemplate(tpl *template.Template, data any) (string, error) {
var buf bytes.Buffer
if err := tpl.Execute(&buf, data); err != nil {
return "", err
}
return buf.String(), nil
}
func renderUnsubscribeBlock(data any) (string, error) {
return renderTemplate(unsubscribeBlockTpl, data)
}
func renderButtonBlock(data any) (string, error) {
return renderTemplate(buttonBlockTpl, data)
}
func renderOneButtonEmail(data any) (string, error) {
return renderTemplate(oneButtonEmailTpl, data)
}

View File

@@ -6,7 +6,7 @@ replace github.com/tech/sendico/pkg => ../../pkg
replace github.com/tech/sendico/billing/fees => ../../billing/fees replace github.com/tech/sendico/billing/fees => ../../billing/fees
replace github.com/tech/sendico/chain/gateway => ../../chain/gateway replace github.com/tech/sendico/gateway/chain => ../../gateway/chain
replace github.com/tech/sendico/fx/oracle => ../../fx/oracle replace github.com/tech/sendico/fx/oracle => ../../fx/oracle
@@ -15,8 +15,8 @@ replace github.com/tech/sendico/ledger => ../../ledger
require ( require (
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/shopspring/decimal v1.4.0 github.com/shopspring/decimal v1.4.0
github.com/tech/sendico/chain/gateway v0.0.0-00010101000000-000000000000
github.com/tech/sendico/fx/oracle v0.0.0-00010101000000-000000000000 github.com/tech/sendico/fx/oracle v0.0.0-00010101000000-000000000000
github.com/tech/sendico/gateway/chain v0.0.0-00010101000000-000000000000
github.com/tech/sendico/ledger v0.0.0-00010101000000-000000000000 github.com/tech/sendico/ledger v0.0.0-00010101000000-000000000000
github.com/tech/sendico/pkg v0.1.0 github.com/tech/sendico/pkg v0.1.0
go.mongodb.org/mongo-driver v1.17.6 go.mongodb.org/mongo-driver v1.17.6
@@ -36,20 +36,20 @@ require (
github.com/go-chi/chi/v5 v5.2.3 // indirect github.com/go-chi/chi/v5 v5.2.3 // indirect
github.com/golang/snappy v1.0.0 // indirect github.com/golang/snappy v1.0.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect github.com/klauspost/compress v1.18.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect github.com/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.47.0 // indirect github.com/nats-io/nats.go v1.47.0 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nkeys v0.4.12 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect github.com/prometheus/procfs v0.19.2 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
@@ -59,5 +59,5 @@ require (
golang.org/x/sync v0.18.0 // indirect golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect golang.org/x/text v0.31.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
) )

View File

@@ -59,8 +59,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -97,8 +97,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM= github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -144,8 +144,8 @@ github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9R
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
@@ -215,8 +215,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= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=

View File

@@ -7,7 +7,7 @@ import (
"strings" "strings"
"time" "time"
chainclient "github.com/tech/sendico/chain/gateway/client" chainclient "github.com/tech/sendico/gateway/chain/client"
oracleclient "github.com/tech/sendico/fx/oracle/client" oracleclient "github.com/tech/sendico/fx/oracle/client"
ledgerclient "github.com/tech/sendico/ledger/client" ledgerclient "github.com/tech/sendico/ledger/client"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrator" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrator"

View File

@@ -6,9 +6,9 @@ import (
"github.com/tech/sendico/payments/orchestrator/storage/model" "github.com/tech/sendico/payments/orchestrator/storage/model"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1" fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1" paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1" orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
@@ -327,11 +327,11 @@ func protoFailureFromModel(code model.PaymentFailureCode) orchestratorv1.Payment
} }
} }
func cloneAsset(asset *gatewayv1.Asset) *gatewayv1.Asset { func cloneAsset(asset *chainv1.Asset) *chainv1.Asset {
if asset == nil { if asset == nil {
return nil return nil
} }
return &gatewayv1.Asset{ return &chainv1.Asset{
Chain: asset.GetChain(), Chain: asset.GetChain(),
TokenSymbol: asset.GetTokenSymbol(), TokenSymbol: asset.GetTokenSymbol(),
ContractAddress: asset.GetContractAddress(), ContractAddress: asset.GetContractAddress(),
@@ -358,11 +358,11 @@ func cloneFXQuote(quote *oraclev1.Quote) *oraclev1.Quote {
return nil return nil
} }
func cloneNetworkEstimate(resp *gatewayv1.EstimateTransferFeeResponse) *gatewayv1.EstimateTransferFeeResponse { func cloneNetworkEstimate(resp *chainv1.EstimateTransferFeeResponse) *chainv1.EstimateTransferFeeResponse {
if resp == nil { if resp == nil {
return nil return nil
} }
if cloned, ok := proto.Clone(resp).(*gatewayv1.EstimateTransferFeeResponse); ok { if cloned, ok := proto.Clone(resp).(*chainv1.EstimateTransferFeeResponse); ok {
return cloned return cloned
} }
return nil return nil

View File

@@ -10,7 +10,7 @@ import (
"github.com/tech/sendico/payments/orchestrator/storage/model" "github.com/tech/sendico/payments/orchestrator/storage/model"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1" chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1" orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
@@ -29,7 +29,7 @@ func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *orc
} }
feeTotal := extractFeeTotal(feeQuote.GetLines(), amount.GetCurrency()) feeTotal := extractFeeTotal(feeQuote.GetLines(), amount.GetCurrency())
var networkFee *gatewayv1.EstimateTransferFeeResponse var networkFee *chainv1.EstimateTransferFeeResponse
if shouldEstimateNetworkFee(intent) { if shouldEstimateNetworkFee(intent) {
networkFee, err = s.estimateNetworkFee(ctx, intent) networkFee, err = s.estimateNetworkFee(ctx, intent)
if err != nil { if err != nil {
@@ -90,25 +90,25 @@ func (s *Service) quoteFees(ctx context.Context, orgRef string, req *orchestrato
return resp, nil return resp, nil
} }
func (s *Service) estimateNetworkFee(ctx context.Context, intent *orchestratorv1.PaymentIntent) (*gatewayv1.EstimateTransferFeeResponse, error) { func (s *Service) estimateNetworkFee(ctx context.Context, intent *orchestratorv1.PaymentIntent) (*chainv1.EstimateTransferFeeResponse, error) {
if !s.gateway.available() { if !s.gateway.available() {
return nil, nil return nil, nil
} }
req := &gatewayv1.EstimateTransferFeeRequest{ req := &chainv1.EstimateTransferFeeRequest{
Amount: cloneMoney(intent.GetAmount()), Amount: cloneMoney(intent.GetAmount()),
} }
if src := intent.GetSource().GetManagedWallet(); src != nil { if src := intent.GetSource().GetManagedWallet(); src != nil {
req.SourceWalletRef = strings.TrimSpace(src.GetManagedWalletRef()) req.SourceWalletRef = strings.TrimSpace(src.GetManagedWalletRef())
} }
if dst := intent.GetDestination().GetManagedWallet(); dst != nil { if dst := intent.GetDestination().GetManagedWallet(); dst != nil {
req.Destination = &gatewayv1.TransferDestination{ req.Destination = &chainv1.TransferDestination{
Destination: &gatewayv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(dst.GetManagedWalletRef())}, Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(dst.GetManagedWalletRef())},
} }
} }
if dst := intent.GetDestination().GetExternalChain(); dst != nil { if dst := intent.GetDestination().GetExternalChain(); dst != nil {
req.Destination = &gatewayv1.TransferDestination{ req.Destination = &chainv1.TransferDestination{
Destination: &gatewayv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(dst.GetAddress())}, Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(dst.GetAddress())},
Memo: strings.TrimSpace(dst.GetMemo()), Memo: strings.TrimSpace(dst.GetMemo()),
} }
req.Asset = dst.GetAsset() req.Asset = dst.GetAsset()
@@ -320,7 +320,7 @@ func (s *Service) applyFX(ctx context.Context, payment *model.Payment, quote *or
return nil return nil
} }
func (s *Service) submitChainTransfer(ctx context.Context, payment *model.Payment, quote *orchestratorv1.PaymentQuote) (*gatewayv1.SubmitTransferResponse, error) { func (s *Service) submitChainTransfer(ctx context.Context, payment *model.Payment, quote *orchestratorv1.PaymentQuote) (*chainv1.SubmitTransferResponse, error) {
intent := payment.Intent intent := payment.Intent
source := intent.Source.ManagedWallet source := intent.Source.ManagedWallet
destination := intent.Destination destination := intent.Destination
@@ -336,7 +336,7 @@ func (s *Service) submitChainTransfer(ctx context.Context, payment *model.Paymen
return nil, merrors.InvalidArgument("chain: amount is required") return nil, merrors.InvalidArgument("chain: amount is required")
} }
fees := feeBreakdownFromQuote(quote) fees := feeBreakdownFromQuote(quote)
req := &gatewayv1.SubmitTransferRequest{ req := &chainv1.SubmitTransferRequest{
IdempotencyKey: payment.IdempotencyKey, IdempotencyKey: payment.IdempotencyKey,
OrganizationRef: payment.OrganizationRef.Hex(), OrganizationRef: payment.OrganizationRef.Hex(),
SourceWalletRef: strings.TrimSpace(source.ManagedWalletRef), SourceWalletRef: strings.TrimSpace(source.ManagedWalletRef),
@@ -437,21 +437,21 @@ func hasManagedWallet(endpoint model.PaymentEndpoint) bool {
return endpoint.Type == model.EndpointTypeManagedWallet && endpoint.ManagedWallet != nil && strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef) != "" return endpoint.Type == model.EndpointTypeManagedWallet && endpoint.ManagedWallet != nil && strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef) != ""
} }
func toGatewayDestination(endpoint model.PaymentEndpoint) (*gatewayv1.TransferDestination, error) { func toGatewayDestination(endpoint model.PaymentEndpoint) (*chainv1.TransferDestination, error) {
switch endpoint.Type { switch endpoint.Type {
case model.EndpointTypeManagedWallet: case model.EndpointTypeManagedWallet:
if endpoint.ManagedWallet == nil || strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef) == "" { if endpoint.ManagedWallet == nil || strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef) == "" {
return nil, merrors.InvalidArgument("chain: destination managed wallet is required") return nil, merrors.InvalidArgument("chain: destination managed wallet is required")
} }
return &gatewayv1.TransferDestination{ return &chainv1.TransferDestination{
Destination: &gatewayv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef)}, Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef)},
}, nil }, nil
case model.EndpointTypeExternalChain: case model.EndpointTypeExternalChain:
if endpoint.ExternalChain == nil || strings.TrimSpace(endpoint.ExternalChain.Address) == "" { if endpoint.ExternalChain == nil || strings.TrimSpace(endpoint.ExternalChain.Address) == "" {
return nil, merrors.InvalidArgument("chain: external address is required") return nil, merrors.InvalidArgument("chain: external address is required")
} }
return &gatewayv1.TransferDestination{ return &chainv1.TransferDestination{
Destination: &gatewayv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(endpoint.ExternalChain.Address)}, Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(endpoint.ExternalChain.Address)},
Memo: strings.TrimSpace(endpoint.ExternalChain.Memo), Memo: strings.TrimSpace(endpoint.ExternalChain.Memo),
}, nil }, nil
default: default:
@@ -459,7 +459,7 @@ func toGatewayDestination(endpoint model.PaymentEndpoint) (*gatewayv1.TransferDe
} }
} }
func applyTransferStatus(event *gatewayv1.TransferStatusChangedEvent, payment *model.Payment) { func applyTransferStatus(event *chainv1.TransferStatusChangedEvent, payment *model.Payment) {
if payment.Execution == nil { if payment.Execution == nil {
payment.Execution = &model.ExecutionRefs{} payment.Execution = &model.ExecutionRefs{}
} }
@@ -473,21 +473,21 @@ func applyTransferStatus(event *gatewayv1.TransferStatusChangedEvent, payment *m
reason = strings.TrimSpace(transfer.GetFailureReason()) reason = strings.TrimSpace(transfer.GetFailureReason())
} }
switch transfer.GetStatus() { switch transfer.GetStatus() {
case gatewayv1.TransferStatus_TRANSFER_CONFIRMED: case chainv1.TransferStatus_TRANSFER_CONFIRMED:
payment.State = model.PaymentStateSettled payment.State = model.PaymentStateSettled
payment.FailureCode = model.PaymentFailureCodeUnspecified payment.FailureCode = model.PaymentFailureCodeUnspecified
payment.FailureReason = "" payment.FailureReason = ""
case gatewayv1.TransferStatus_TRANSFER_FAILED: case chainv1.TransferStatus_TRANSFER_FAILED:
payment.State = model.PaymentStateFailed payment.State = model.PaymentStateFailed
payment.FailureCode = model.PaymentFailureCodeChain payment.FailureCode = model.PaymentFailureCodeChain
payment.FailureReason = reason payment.FailureReason = reason
case gatewayv1.TransferStatus_TRANSFER_CANCELLED: case chainv1.TransferStatus_TRANSFER_CANCELLED:
payment.State = model.PaymentStateCancelled payment.State = model.PaymentStateCancelled
payment.FailureCode = model.PaymentFailureCodePolicy payment.FailureCode = model.PaymentFailureCodePolicy
payment.FailureReason = reason payment.FailureReason = reason
case gatewayv1.TransferStatus_TRANSFER_SIGNING, case chainv1.TransferStatus_TRANSFER_SIGNING,
gatewayv1.TransferStatus_TRANSFER_PENDING, chainv1.TransferStatus_TRANSFER_PENDING,
gatewayv1.TransferStatus_TRANSFER_SUBMITTED: chainv1.TransferStatus_TRANSFER_SUBMITTED:
payment.State = model.PaymentStateSubmitted payment.State = model.PaymentStateSubmitted
default: default:
// retain previous state // retain previous state

View File

@@ -12,9 +12,9 @@ import (
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
accountingv1 "github.com/tech/sendico/pkg/proto/common/accounting/v1" accountingv1 "github.com/tech/sendico/pkg/proto/common/accounting/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
) )
func cloneMoney(input *moneyv1.Money) *moneyv1.Money { func cloneMoney(input *moneyv1.Money) *moneyv1.Money {
@@ -108,7 +108,7 @@ func extractFeeTotal(lines []*feesv1.DerivedPostingLine, currency string) *money
} }
} }
func computeAggregates(base, fee *moneyv1.Money, network *gatewayv1.EstimateTransferFeeResponse) (*moneyv1.Money, *moneyv1.Money) { func computeAggregates(base, fee *moneyv1.Money, network *chainv1.EstimateTransferFeeResponse) (*moneyv1.Money, *moneyv1.Money) {
if base == nil { if base == nil {
return nil, nil return nil, nil
} }
@@ -219,12 +219,12 @@ func ledgerLineTypeFromAccounting(lineType accountingv1.PostingLineType) ledgerv
} }
} }
func feeBreakdownFromQuote(quote *orchestratorv1.PaymentQuote) []*gatewayv1.ServiceFeeBreakdown { func feeBreakdownFromQuote(quote *orchestratorv1.PaymentQuote) []*chainv1.ServiceFeeBreakdown {
if quote == nil { if quote == nil {
return nil return nil
} }
lines := quote.GetFeeLines() lines := quote.GetFeeLines()
breakdown := make([]*gatewayv1.ServiceFeeBreakdown, 0, len(lines)+1) breakdown := make([]*chainv1.ServiceFeeBreakdown, 0, len(lines)+1)
for _, line := range lines { for _, line := range lines {
if line == nil { if line == nil {
continue continue
@@ -241,7 +241,7 @@ func feeBreakdownFromQuote(quote *orchestratorv1.PaymentQuote) []*gatewayv1.Serv
code = line.GetLineType().String() code = line.GetLineType().String()
} }
desc := strings.TrimSpace(line.GetMeta()["description"]) desc := strings.TrimSpace(line.GetMeta()["description"])
breakdown = append(breakdown, &gatewayv1.ServiceFeeBreakdown{ breakdown = append(breakdown, &chainv1.ServiceFeeBreakdown{
FeeCode: code, FeeCode: code,
Amount: amount, Amount: amount,
Description: desc, Description: desc,
@@ -250,7 +250,7 @@ func feeBreakdownFromQuote(quote *orchestratorv1.PaymentQuote) []*gatewayv1.Serv
if quote.GetNetworkFee() != nil && quote.GetNetworkFee().GetNetworkFee() != nil { if quote.GetNetworkFee() != nil && quote.GetNetworkFee().GetNetworkFee() != nil {
networkAmount := cloneMoney(quote.GetNetworkFee().GetNetworkFee()) networkAmount := cloneMoney(quote.GetNetworkFee().GetNetworkFee())
if networkAmount != nil { if networkAmount != nil {
breakdown = append(breakdown, &gatewayv1.ServiceFeeBreakdown{ breakdown = append(breakdown, &chainv1.ServiceFeeBreakdown{
FeeCode: "network_fee", FeeCode: "network_fee",
Amount: networkAmount, Amount: networkAmount,
Description: strings.TrimSpace(quote.GetNetworkFee().GetEstimationContext()), Description: strings.TrimSpace(quote.GetNetworkFee().GetEstimationContext()),

View File

@@ -3,7 +3,7 @@ package orchestrator
import ( import (
"time" "time"
chainclient "github.com/tech/sendico/chain/gateway/client" chainclient "github.com/tech/sendico/gateway/chain/client"
oracleclient "github.com/tech/sendico/fx/oracle/client" oracleclient "github.com/tech/sendico/fx/oracle/client"
ledgerclient "github.com/tech/sendico/ledger/client" ledgerclient "github.com/tech/sendico/ledger/client"
clockpkg "github.com/tech/sendico/pkg/clock" clockpkg "github.com/tech/sendico/pkg/clock"

View File

@@ -7,14 +7,14 @@ import (
"testing" "testing"
"time" "time"
chainclient "github.com/tech/sendico/chain/gateway/client" chainclient "github.com/tech/sendico/gateway/chain/client"
ledgerclient "github.com/tech/sendico/ledger/client" ledgerclient "github.com/tech/sendico/ledger/client"
"github.com/tech/sendico/payments/orchestrator/storage" "github.com/tech/sendico/payments/orchestrator/storage"
"github.com/tech/sendico/payments/orchestrator/storage/model" "github.com/tech/sendico/payments/orchestrator/storage/model"
"github.com/tech/sendico/pkg/api/routers/gsresponse" "github.com/tech/sendico/pkg/api/routers/gsresponse"
mo "github.com/tech/sendico/pkg/model" mo "github.com/tech/sendico/pkg/model"
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1" orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
@@ -88,7 +88,7 @@ func TestExecutePayment_ChainFailure(t *testing.T) {
clock: testClock{now: time.Now()}, clock: testClock{now: time.Now()},
storage: repo, storage: repo,
gateway: gatewayDependency{client: &chainclient.Fake{ gateway: gatewayDependency{client: &chainclient.Fake{
SubmitTransferFn: func(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error) { SubmitTransferFn: func(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
return nil, errors.New("chain failure") return nil, errors.New("chain failure")
}, },
}}, }},
@@ -147,10 +147,10 @@ func TestProcessTransferUpdateHandler_Settled(t *testing.T) {
} }
req := &orchestratorv1.ProcessTransferUpdateRequest{ req := &orchestratorv1.ProcessTransferUpdateRequest{
Event: &gatewayv1.TransferStatusChangedEvent{ Event: &chainv1.TransferStatusChangedEvent{
Transfer: &gatewayv1.Transfer{ Transfer: &chainv1.Transfer{
TransferRef: "transfer-1", TransferRef: "transfer-1",
Status: gatewayv1.TransferStatus_TRANSFER_CONFIRMED, Status: chainv1.TransferStatus_TRANSFER_CONFIRMED,
}, },
}, },
} }
@@ -190,7 +190,7 @@ func TestProcessDepositObservedHandler_MatchesPayment(t *testing.T) {
} }
req := &orchestratorv1.ProcessDepositObservedRequest{ req := &orchestratorv1.ProcessDepositObservedRequest{
Event: &gatewayv1.WalletDepositObservedEvent{ Event: &chainv1.WalletDepositObservedEvent{
WalletRef: "wallet-dst", WalletRef: "wallet-dst",
Amount: &moneyv1.Money{Currency: "USD", Amount: "40"}, Amount: &moneyv1.Money{Currency: "USD", Amount: "40"},
}, },

View File

@@ -7,9 +7,9 @@ import (
"github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1" fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
) )
@@ -67,15 +67,15 @@ type LedgerEndpoint struct {
// ManagedWalletEndpoint describes managed wallet routing. // ManagedWalletEndpoint describes managed wallet routing.
type ManagedWalletEndpoint struct { type ManagedWalletEndpoint struct {
ManagedWalletRef string `bson:"managedWalletRef" json:"managedWalletRef"` ManagedWalletRef string `bson:"managedWalletRef" json:"managedWalletRef"`
Asset *gatewayv1.Asset `bson:"asset,omitempty" json:"asset,omitempty"` Asset *chainv1.Asset `bson:"asset,omitempty" json:"asset,omitempty"`
} }
// ExternalChainEndpoint describes an external address. // ExternalChainEndpoint describes an external address.
type ExternalChainEndpoint struct { type ExternalChainEndpoint struct {
Asset *gatewayv1.Asset `bson:"asset,omitempty" json:"asset,omitempty"` Asset *chainv1.Asset `bson:"asset,omitempty" json:"asset,omitempty"`
Address string `bson:"address" json:"address"` Address string `bson:"address" json:"address"`
Memo string `bson:"memo,omitempty" json:"memo,omitempty"` Memo string `bson:"memo,omitempty" json:"memo,omitempty"`
} }
// PaymentEndpoint is a polymorphic payment destination/source. // PaymentEndpoint is a polymorphic payment destination/source.
@@ -111,14 +111,14 @@ type PaymentIntent struct {
// PaymentQuoteSnapshot stores the latest quote info. // PaymentQuoteSnapshot stores the latest quote info.
type PaymentQuoteSnapshot struct { type PaymentQuoteSnapshot struct {
DebitAmount *moneyv1.Money `bson:"debitAmount,omitempty" json:"debitAmount,omitempty"` DebitAmount *moneyv1.Money `bson:"debitAmount,omitempty" json:"debitAmount,omitempty"`
ExpectedSettlementAmount *moneyv1.Money `bson:"expectedSettlementAmount,omitempty" json:"expectedSettlementAmount,omitempty"` ExpectedSettlementAmount *moneyv1.Money `bson:"expectedSettlementAmount,omitempty" json:"expectedSettlementAmount,omitempty"`
ExpectedFeeTotal *moneyv1.Money `bson:"expectedFeeTotal,omitempty" json:"expectedFeeTotal,omitempty"` ExpectedFeeTotal *moneyv1.Money `bson:"expectedFeeTotal,omitempty" json:"expectedFeeTotal,omitempty"`
FeeLines []*feesv1.DerivedPostingLine `bson:"feeLines,omitempty" json:"feeLines,omitempty"` FeeLines []*feesv1.DerivedPostingLine `bson:"feeLines,omitempty" json:"feeLines,omitempty"`
FeeRules []*feesv1.AppliedRule `bson:"feeRules,omitempty" json:"feeRules,omitempty"` FeeRules []*feesv1.AppliedRule `bson:"feeRules,omitempty" json:"feeRules,omitempty"`
FXQuote *oraclev1.Quote `bson:"fxQuote,omitempty" json:"fxQuote,omitempty"` FXQuote *oraclev1.Quote `bson:"fxQuote,omitempty" json:"fxQuote,omitempty"`
NetworkFee *gatewayv1.EstimateTransferFeeResponse `bson:"networkFee,omitempty" json:"networkFee,omitempty"` NetworkFee *chainv1.EstimateTransferFeeResponse `bson:"networkFee,omitempty" json:"networkFee,omitempty"`
FeeQuoteToken string `bson:"feeQuoteToken,omitempty" json:"feeQuoteToken,omitempty"` FeeQuoteToken string `bson:"feeQuoteToken,omitempty" json:"feeQuoteToken,omitempty"`
} }
// ExecutionRefs links to downstream systems. // ExecutionRefs links to downstream systems.

4
api/pkg/.gitignore vendored
View File

@@ -1,6 +1,8 @@
proto/billing proto/billing
proto/common proto/common
proto/chain proto/chain
proto/gateway
proto/ledger proto/ledger
proto/oracle proto/oracle
proto/payments proto/payments
.gocache

View File

@@ -47,6 +47,12 @@ func (n *Enforcer) Enforce(
permissionRef, accountRef, organizationRef, objectRef primitive.ObjectID, permissionRef, accountRef, organizationRef, objectRef primitive.ObjectID,
action model.Action, action model.Action,
) (bool, error) { ) (bool, error) {
if organizationRef.IsZero() {
n.logger.Warn("Missing organization context", mzap.ObjRef("account_ref", accountRef),
mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef),
mzap.ObjRef("object", objectRef), zap.String("action", string(action)))
return false, merrors.InvalidArgument("organization context missing", "organizationRef")
}
roleAssignments, err := n.rdb.Roles(ctx, accountRef, organizationRef) roleAssignments, err := n.rdb.Roles(ctx, accountRef, organizationRef)
if errors.Is(err, merrors.ErrNoData) { if errors.Is(err, merrors.ErrNoData) {
n.logger.Debug("No roles defined for account", mzap.ObjRef("account_ref", accountRef)) n.logger.Debug("No roles defined for account", mzap.ObjRef("account_ref", accountRef))

View File

@@ -7,7 +7,9 @@ import (
mongoimpl "github.com/tech/sendico/pkg/db/internal/mongo" mongoimpl "github.com/tech/sendico/pkg/db/internal/mongo"
"github.com/tech/sendico/pkg/db/invitation" "github.com/tech/sendico/pkg/db/invitation"
"github.com/tech/sendico/pkg/db/organization" "github.com/tech/sendico/pkg/db/organization"
"github.com/tech/sendico/pkg/db/paymethod"
"github.com/tech/sendico/pkg/db/policy" "github.com/tech/sendico/pkg/db/policy"
"github.com/tech/sendico/pkg/db/recipient"
"github.com/tech/sendico/pkg/db/refreshtokens" "github.com/tech/sendico/pkg/db/refreshtokens"
"github.com/tech/sendico/pkg/db/role" "github.com/tech/sendico/pkg/db/role"
"github.com/tech/sendico/pkg/db/transaction" "github.com/tech/sendico/pkg/db/transaction"
@@ -23,6 +25,8 @@ type Factory interface {
NewAccountDB() (account.DB, error) NewAccountDB() (account.DB, error)
NewOrganizationDB() (organization.DB, error) NewOrganizationDB() (organization.DB, error)
NewInvitationsDB() (invitation.DB, error) NewInvitationsDB() (invitation.DB, error)
NewRecipientsDB() (recipient.DB, error)
NewPaymentMethodsDB() (paymethod.DB, error)
NewRolesDB() (role.DB, error) NewRolesDB() (role.DB, error)
NewPoliciesDB() (policy.DB, error) NewPoliciesDB() (policy.DB, error)

View File

@@ -2,7 +2,10 @@ package mongo
import ( import (
"context" "context"
"fmt"
"net"
"os" "os"
"strings"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"github.com/tech/sendico/pkg/auth" "github.com/tech/sendico/pkg/auth"
@@ -12,13 +15,17 @@ import (
"github.com/tech/sendico/pkg/db/internal/mongo/confirmationdb" "github.com/tech/sendico/pkg/db/internal/mongo/confirmationdb"
"github.com/tech/sendico/pkg/db/internal/mongo/invitationdb" "github.com/tech/sendico/pkg/db/internal/mongo/invitationdb"
"github.com/tech/sendico/pkg/db/internal/mongo/organizationdb" "github.com/tech/sendico/pkg/db/internal/mongo/organizationdb"
"github.com/tech/sendico/pkg/db/internal/mongo/paymethoddb"
"github.com/tech/sendico/pkg/db/internal/mongo/policiesdb" "github.com/tech/sendico/pkg/db/internal/mongo/policiesdb"
"github.com/tech/sendico/pkg/db/internal/mongo/recipientdb"
"github.com/tech/sendico/pkg/db/internal/mongo/refreshtokensdb" "github.com/tech/sendico/pkg/db/internal/mongo/refreshtokensdb"
"github.com/tech/sendico/pkg/db/internal/mongo/rolesdb" "github.com/tech/sendico/pkg/db/internal/mongo/rolesdb"
"github.com/tech/sendico/pkg/db/internal/mongo/transactionimp" "github.com/tech/sendico/pkg/db/internal/mongo/transactionimp"
"github.com/tech/sendico/pkg/db/invitation" "github.com/tech/sendico/pkg/db/invitation"
"github.com/tech/sendico/pkg/db/organization" "github.com/tech/sendico/pkg/db/organization"
"github.com/tech/sendico/pkg/db/paymethod"
"github.com/tech/sendico/pkg/db/policy" "github.com/tech/sendico/pkg/db/policy"
"github.com/tech/sendico/pkg/db/recipient"
"github.com/tech/sendico/pkg/db/refreshtokens" "github.com/tech/sendico/pkg/db/refreshtokens"
"github.com/tech/sendico/pkg/db/repository" "github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/db/role" "github.com/tech/sendico/pkg/db/role"
@@ -28,33 +35,40 @@ import (
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
mutil "github.com/tech/sendico/pkg/mutil/config" mutil "github.com/tech/sendico/pkg/mutil/config"
"go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref" "go.mongodb.org/mongo-driver/mongo/readpref"
"go.uber.org/zap" "go.uber.org/zap"
) )
// Config represents configuration // Config represents configuration
type Config struct { type Config struct {
Port *string `mapstructure:"port"` Port *string `mapstructure:"port"`
PortEnv *string `mapstructure:"port_env"` PortEnv *string `mapstructure:"port_env"`
User *string `mapstructure:"user"` User *string `mapstructure:"user"`
UserEnv *string `mapstructure:"user_env"` UserEnv *string `mapstructure:"user_env"`
PasswordEnv string `mapstructure:"password_env"` PasswordEnv string `mapstructure:"password_env"`
Database *string `mapstructure:"database"` Database *string `mapstructure:"database"`
DatabaseEnv *string `mapstructure:"database_env"` DatabaseEnv *string `mapstructure:"database_env"`
Host *string `mapstructure:"host"` Host *string `mapstructure:"host"`
HostEnv *string `mapstructure:"host_env"` HostEnv *string `mapstructure:"host_env"`
AuthSource *string `mapstructure:"auth_source,omitempty"` Hosts []string `mapstructure:"hosts,omitempty"`
AuthSourceEnv *string `mapstructure:"auth_source_env,omitempty"` HostsEnvPrefix *string `mapstructure:"hosts_env_prefix,omitempty"`
AuthMechanism *string `mapstructure:"auth_mechanism,omitempty"` HostsEnvPrefixEnv *string `mapstructure:"hosts_env_prefix_env,omitempty"`
AuthMechanismEnv *string `mapstructure:"auth_mechanism_env,omitempty"` PortsEnvPrefix *string `mapstructure:"ports_env_prefix,omitempty"`
ReplicaSet *string `mapstructure:"replica_set,omitempty"` PortsEnvPrefixEnv *string `mapstructure:"ports_env_prefix_env,omitempty"`
ReplicaSetEnv *string `mapstructure:"replica_set_env,omitempty"` URI *string `mapstructure:"uri,omitempty"`
Enforcer *auth.Config `mapstructure:"enforcer"` URIEnv *string `mapstructure:"uri_env,omitempty"`
AuthSource *string `mapstructure:"auth_source,omitempty"`
AuthSourceEnv *string `mapstructure:"auth_source_env,omitempty"`
AuthMechanism *string `mapstructure:"auth_mechanism,omitempty"`
AuthMechanismEnv *string `mapstructure:"auth_mechanism_env,omitempty"`
ReplicaSet *string `mapstructure:"replica_set,omitempty"`
ReplicaSetEnv *string `mapstructure:"replica_set_env,omitempty"`
Enforcer *auth.Config `mapstructure:"enforcer"`
} }
type DBSettings struct { type DBSettings struct {
Host string Host string
Hosts []string
Port string Port string
User string User string
Password string Password string
@@ -62,6 +76,7 @@ type DBSettings struct {
AuthSource string AuthSource string
AuthMechanism string AuthMechanism string
ReplicaSet string ReplicaSet string
URI string
} }
func newProtectedDB[T any]( func newProtectedDB[T any](
@@ -87,6 +102,19 @@ func Config2DBSettings(logger mlogger.Logger, config *Config) *DBSettings {
p.AuthSource = mutil.GetConfigValue(logger, "auth_source", "auth_source_env", config.AuthSource, config.AuthSourceEnv) p.AuthSource = mutil.GetConfigValue(logger, "auth_source", "auth_source_env", config.AuthSource, config.AuthSourceEnv)
p.AuthMechanism = mutil.GetConfigValue(logger, "auth_mechanism", "auth_mechanism_env", config.AuthMechanism, config.AuthMechanismEnv) p.AuthMechanism = mutil.GetConfigValue(logger, "auth_mechanism", "auth_mechanism_env", config.AuthMechanism, config.AuthMechanismEnv)
p.ReplicaSet = mutil.GetConfigValue(logger, "replica_set", "replica_set_env", config.ReplicaSet, config.ReplicaSetEnv) p.ReplicaSet = mutil.GetConfigValue(logger, "replica_set", "replica_set_env", config.ReplicaSet, config.ReplicaSetEnv)
p.URI = mutil.GetConfigValue(logger, "uri", "uri_env", config.URI, config.URIEnv)
hostPrefix := mutil.GetConfigValue(logger, "hosts_env_prefix", "hosts_env_prefix_env", config.HostsEnvPrefix, config.HostsEnvPrefixEnv)
portPrefix := mutil.GetConfigValue(logger, "ports_env_prefix", "ports_env_prefix_env", config.PortsEnvPrefix, config.PortsEnvPrefixEnv)
if hostPrefix == "" && p.ReplicaSet != "" {
hostPrefix = "MONGO_HOSTS_"
}
if portPrefix == "" && p.ReplicaSet != "" {
portPrefix = "MONGO_PORTS_"
}
p.Hosts = collectReplicaHosts(config.Hosts, p.ReplicaSet, p.Port, hostPrefix, portPrefix)
return p return p
} }
@@ -101,21 +129,19 @@ func decodeConfig(logger mlogger.Logger, settings model.SettingsT) (*Config, *DB
} }
func dialMongo(logger mlogger.Logger, dbSettings *DBSettings) (*mongo.Client, error) { func dialMongo(logger mlogger.Logger, dbSettings *DBSettings) (*mongo.Client, error) {
cred := options.Credential{ opts := buildOptions(dbSettings)
AuthMechanism: dbSettings.AuthMechanism,
AuthSource: dbSettings.AuthSource,
Username: dbSettings.User,
Password: dbSettings.Password,
}
dbURI := buildURI(dbSettings)
client, err := mongo.Connect(context.Background(), options.Client().ApplyURI(dbURI).SetAuth(cred)) client, err := mongo.Connect(context.Background(), opts)
if err != nil { if err != nil {
logger.Error("Unable to connect to database", zap.Error(err)) logger.Error("Unable to connect to database", zap.Error(err))
return nil, err return nil, err
} }
logger.Info("Connected successfully", zap.String("uri", dbURI)) if dbSettings.URI != "" {
logger.Info("Connected successfully", zap.Bool("uri_provided", true))
} else {
logger.Info("Connected successfully", zap.Strings("hosts", opts.Hosts), zap.String("replica_set", dbSettings.ReplicaSet))
}
if err := client.Ping(context.Background(), readpref.Primary()); err != nil { if err := client.Ping(context.Background(), readpref.Primary()); err != nil {
logger.Error("Unable to ping database", zap.Error(err)) logger.Error("Unable to ping database", zap.Error(err))
@@ -179,6 +205,29 @@ func (db *DB) NewOrganizationDB() (organization.DB, error) {
return organizationDB, nil return organizationDB, nil
} }
func (db *DB) NewRecipientsDB() (recipient.DB, error) {
pmdb, err := db.NewPaymentMethodsDB()
if err != nil {
db.logger.Warn("Failed to create payment methods database", zap.Error(err))
return nil, err
}
create := func(ctx context.Context,
logger mlogger.Logger,
enforcer auth.Enforcer,
pdb policy.DB,
db *mongo.Database,
) (recipient.DB, error) {
return recipientdb.Create(ctx, logger, enforcer, pdb, pmdb, db)
}
return newProtectedDB(db, create)
}
func (db *DB) NewPaymentMethodsDB() (paymethod.DB, error) {
return newProtectedDB(db, paymethoddb.Create)
}
func (db *DB) NewRefreshTokensDB() (refreshtokens.DB, error) { func (db *DB) NewRefreshTokensDB() (refreshtokens.DB, error) {
return refreshtokensdb.Create(db.logger, db.db()) return refreshtokensdb.Create(db.logger, db.db())
} }
@@ -199,6 +248,70 @@ func (db *DB) TransactionFactory() transaction.Factory {
return transactionimp.CreateFactory(db.client) return transactionimp.CreateFactory(db.client)
} }
func collectReplicaHosts(configuredHosts []string, replicaSet, defaultPort, hostPrefix, portPrefix string) []string {
normalize := func(host, port string) string {
host = strings.TrimSpace(host)
port = strings.TrimSpace(port)
if host == "" {
return ""
}
// If host already has a port, keep it; otherwise apply provided/default port.
if _, _, err := net.SplitHostPort(host); err == nil {
return host
}
if port != "" {
return net.JoinHostPort(host, port)
}
if defaultPort != "" {
return net.JoinHostPort(host, defaultPort)
}
return host
}
appendHost := func(list []string, host, port string) []string {
if normalized := normalize(host, port); normalized != "" {
return append(list, normalized)
}
return list
}
var hosts []string
for _, h := range configuredHosts {
hosts = appendHost(hosts, h, "")
}
if replicaSet == "" || hostPrefix == "" {
return hosts
}
index := 0
for {
hostEnv := os.Getenv(fmt.Sprintf("%s%d", hostPrefix, index))
portEnv := ""
if portPrefix != "" {
portEnv = os.Getenv(fmt.Sprintf("%s%d", portPrefix, index))
}
if hostEnv == "" && index == 0 {
hostEnv = os.Getenv(fmt.Sprintf("%s%d", hostPrefix, 1))
if portPrefix != "" {
portEnv = os.Getenv(fmt.Sprintf("%s%d", portPrefix, 1))
}
if hostEnv == "" {
break
}
index = 1
} else if hostEnv == "" {
break
}
hosts = appendHost(hosts, hostEnv, portEnv)
index++
}
return hosts
}
func (db *DB) Permissions() auth.Provider { func (db *DB) Permissions() auth.Provider {
return db return db
} }

View File

@@ -1,22 +1,49 @@
package mongo package mongo
import ( import (
"net/url" "net"
"strings"
"go.mongodb.org/mongo-driver/mongo/options"
) )
func buildURI(s *DBSettings) string { func buildOptions(s *DBSettings) *options.ClientOptions {
u := &url.URL{ opts := options.Client()
Scheme: "mongodb",
Host: s.Host, if s.URI != "" {
Path: "/" + url.PathEscape(s.Database), // /my%20db return opts.ApplyURI(s.URI)
}
hosts := make([]string, 0, len(s.Hosts)+1)
for _, h := range s.Hosts {
if trimmed := strings.TrimSpace(h); trimmed != "" {
hosts = append(hosts, trimmed)
}
}
if len(hosts) == 0 && s.Host != "" {
host := s.Host
if _, _, err := net.SplitHostPort(host); err != nil && s.Port != "" {
host = net.JoinHostPort(host, s.Port)
}
hosts = append(hosts, host)
}
if len(hosts) > 0 {
opts.SetHosts(hosts)
} }
q := url.Values{}
if s.ReplicaSet != "" { if s.ReplicaSet != "" {
q.Set("replicaSet", s.ReplicaSet) opts.SetReplicaSet(s.ReplicaSet)
} }
u.RawQuery = q.Encode() cred := options.Credential{
AuthMechanism: s.AuthMechanism,
AuthSource: s.AuthSource,
Username: s.User,
Password: s.Password,
}
opts.SetAuth(cred)
return u.String() return opts
} }

View File

@@ -0,0 +1,20 @@
package paymethoddb
import (
"context"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
func (db *PaymentMethodsDB) SetArchived(ctx context.Context, accountRef, organizationRef, objectRef primitive.ObjectID, isArchived, cascade bool) error {
// Use the ArchivableDB for the main archiving logic
if err := db.ArchivableDB.SetArchived(ctx, accountRef, objectRef, isArchived); err != nil {
db.DBImp.Logger.Warn("Failed to chnage object archive status", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef),
mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", isArchived), zap.Bool("cascade", cascade))
return err
}
return nil
}

View File

@@ -0,0 +1,49 @@
package paymethoddb
import (
"context"
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/db/policy"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
"go.mongodb.org/mongo-driver/mongo"
)
type PaymentMethodsDB struct {
auth.ProtectedDBImp[*model.PaymentMethod]
auth.ArchivableDB[*model.PaymentMethod]
}
func Create(ctx context.Context,
logger mlogger.Logger,
enforcer auth.Enforcer,
pdb policy.DB,
db *mongo.Database,
) (*PaymentMethodsDB, error) {
p, err := auth.CreateDBImp[*model.PaymentMethod](ctx, logger, pdb, enforcer, mservice.PaymentMethods, db)
if err != nil {
return nil, err
}
createEmpty := func() *model.PaymentMethod {
return &model.PaymentMethod{}
}
getArchivable := func(c *model.PaymentMethod) model.Archivable {
return &c.ArchivableBase
}
res := &PaymentMethodsDB{
ProtectedDBImp: *p,
ArchivableDB: auth.NewArchivableDB(
p.DBImp,
logger,
p.Enforcer,
createEmpty,
getArchivable,
),
}
return res, nil
}

View File

@@ -0,0 +1,28 @@
package paymethoddb
import (
"context"
"errors"
"github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
mauth "github.com/tech/sendico/pkg/mutil/db/auth"
"go.mongodb.org/mongo-driver/bson/primitive"
)
func (db *PaymentMethodsDB) List(ctx context.Context, accountRef, organizationRef, recipientRef primitive.ObjectID, cursor *model.ViewCursor) ([]model.PaymentMethod, error) {
res, err := mauth.GetProtectedObjects[model.PaymentMethod](
ctx,
db.DBImp.Logger,
accountRef, organizationRef, model.ActionRead,
repository.OrgFilter(organizationRef).And(repository.Filter("recipientRef", recipientRef)),
cursor,
db.Enforcer,
db.DBImp.Repository,
)
if errors.Is(err, merrors.ErrNoData) {
return []model.PaymentMethod{}, nil
}
return res, err
}

Some files were not shown because too many files have changed in this diff Show More