Merge pull request 'fixed quotation currency inference' (#630) from pq-626 into main
All checks were successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/callbacks Pipeline was successful
ci/woodpecker/push/discovery 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/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_methods Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful

Reviewed-on: #630
This commit was merged in pull request #630.
This commit is contained in:
2026-03-04 04:13:55 +00:00
35 changed files with 928 additions and 182 deletions

View File

@@ -5,10 +5,10 @@ go 1.25.7
replace github.com/tech/sendico/pkg => ../../pkg
require (
github.com/aws/aws-sdk-go-v2 v1.41.2
github.com/aws/aws-sdk-go-v2/config v1.32.10
github.com/aws/aws-sdk-go-v2/credentials v1.19.10
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2
github.com/aws/aws-sdk-go-v2 v1.41.3
github.com/aws/aws-sdk-go-v2/config v1.32.11
github.com/aws/aws-sdk-go-v2/credentials v1.19.11
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.3
github.com/jung-kurt/gofpdf v1.16.2
github.com/prometheus/client_golang v1.23.2
github.com/shopspring/decimal v1.4.0
@@ -20,20 +20,20 @@ require (
)
require (
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect

View File

@@ -4,42 +4,42 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI=
github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw=
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 h1:fJvQ5mIBVfKtiyx0AHY6HeWcRX5LGANLpq8SVR+Uazs=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 h1:M1A9AjcFwlxTLuf0Faj88L8Iqw0n/AJHjpZTQzMMsSc=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2/go.mod h1:KsdTV6Q9WKUZm2mNJnUFmIoXfZux91M3sr/a4REX8e0=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs=
github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA=
github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 h1:N4lRUXZpZ1KVEUn6hxtco/1d2lgYhNn1fHkkl8WhlyQ=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs=
github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo=
github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc=
github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.19 h1:3Y4oma5TiV7tT9wa8zRcdoXwZkGz9Q/wxbEUK7cMuAM=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.19/go.mod h1:V1K+TeJVD5JOk3D9e5tsX2KUdL7BlB+FV6cBhdobN8c=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 h1:BYf7XNsJMzl4mObARUBUib+j2tf0U//JAAtTnYqvqCw=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11/go.mod h1:aEUS4WrNk/+FxkBZZa7tVgp4pGH+kFGW40Y8rCPqt5g=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 h1:JnQeStZvPHFHeyky/7LbMlyQjUa+jIBj36OlWm0pzIk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19/go.mod h1:HGyasyHvYdFQeJhvDHfH7HXkHh57htcJGKDZ+7z+I24=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.3 h1:+d0SsTvxtIJt4tSJ6wr+jrxEMDa6XeupjRv8H7Qitkk=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.3/go.mod h1:ROUNFvFWPwBlOu687WJNQ9cPvd2ccpFrnCiA1YGz50o=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=

View File

@@ -15,10 +15,10 @@ replace github.com/tech/sendico/payments/storage => ../../payments/storage
replace github.com/tech/sendico/gateway/tron => ../../gateway/tron
require (
github.com/aws/aws-sdk-go-v2 v1.41.2
github.com/aws/aws-sdk-go-v2/config v1.32.10
github.com/aws/aws-sdk-go-v2/credentials v1.19.10
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2
github.com/aws/aws-sdk-go-v2 v1.41.3
github.com/aws/aws-sdk-go-v2/config v1.32.11
github.com/aws/aws-sdk-go-v2/credentials v1.19.11
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.3
github.com/go-chi/chi/v5 v5.2.5
github.com/go-chi/cors v1.2.2
github.com/go-chi/jwtauth/v5 v5.4.0
@@ -54,20 +54,20 @@ require (
dario.cat/mergo v1.0.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect

View File

@@ -6,42 +6,42 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI=
github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw=
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 h1:fJvQ5mIBVfKtiyx0AHY6HeWcRX5LGANLpq8SVR+Uazs=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 h1:M1A9AjcFwlxTLuf0Faj88L8Iqw0n/AJHjpZTQzMMsSc=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2/go.mod h1:KsdTV6Q9WKUZm2mNJnUFmIoXfZux91M3sr/a4REX8e0=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs=
github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA=
github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 h1:N4lRUXZpZ1KVEUn6hxtco/1d2lgYhNn1fHkkl8WhlyQ=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs=
github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo=
github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc=
github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.19 h1:3Y4oma5TiV7tT9wa8zRcdoXwZkGz9Q/wxbEUK7cMuAM=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.19/go.mod h1:V1K+TeJVD5JOk3D9e5tsX2KUdL7BlB+FV6cBhdobN8c=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 h1:BYf7XNsJMzl4mObARUBUib+j2tf0U//JAAtTnYqvqCw=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11/go.mod h1:aEUS4WrNk/+FxkBZZa7tVgp4pGH+kFGW40Y8rCPqt5g=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 h1:JnQeStZvPHFHeyky/7LbMlyQjUa+jIBj36OlWm0pzIk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19/go.mod h1:HGyasyHvYdFQeJhvDHfH7HXkHh57htcJGKDZ+7z+I24=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.3 h1:+d0SsTvxtIJt4tSJ6wr+jrxEMDa6XeupjRv8H7Qitkk=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.3/go.mod h1:ROUNFvFWPwBlOu687WJNQ9cPvd2ccpFrnCiA1YGz50o=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=

View File

@@ -14,6 +14,7 @@ import (
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
"github.com/tech/sendico/server/interface/api/srequest"
"go.mongodb.org/mongo-driver/v2/bson"
)
@@ -59,7 +60,7 @@ func mapQuoteIntent(intent *srequest.PaymentIntent) (*quotationv2.QuoteIntent, e
SettlementMode: resolvedSettlementMode,
FeeTreatment: resolvedFeeTreatment,
SettlementCurrency: settlementCurrency,
FxSide: mapFXSide(intent),
Fx: mapFXIntent(intent),
}
if comment := strings.TrimSpace(intent.Attributes["comment"]); comment != "" {
quoteIntent.Comment = comment
@@ -67,17 +68,30 @@ func mapQuoteIntent(intent *srequest.PaymentIntent) (*quotationv2.QuoteIntent, e
return quoteIntent, nil
}
func mapFXSide(intent *srequest.PaymentIntent) fxv1.Side {
if intent == nil || intent.FX == nil {
return fxv1.Side_SIDE_UNSPECIFIED
func mapFXIntent(intent *srequest.PaymentIntent) *sharedv1.FXIntent {
if intent == nil || intent.FX == nil || intent.FX.Pair == nil {
return nil
}
side := fxv1.Side_SIDE_UNSPECIFIED
switch strings.TrimSpace(string(intent.FX.Side)) {
case string(srequest.FXSideBuyBaseSellQuote):
return fxv1.Side_BUY_BASE_SELL_QUOTE
side = fxv1.Side_BUY_BASE_SELL_QUOTE
case string(srequest.FXSideSellBaseBuyQuote):
return fxv1.Side_SELL_BASE_BUY_QUOTE
default:
return fxv1.Side_SIDE_UNSPECIFIED
side = fxv1.Side_SELL_BASE_BUY_QUOTE
}
if side == fxv1.Side_SIDE_UNSPECIFIED {
side = fxv1.Side_SELL_BASE_BUY_QUOTE
}
return &sharedv1.FXIntent{
Pair: &fxv1.CurrencyPair{
Base: strings.ToUpper(strings.TrimSpace(intent.FX.Pair.Base)),
Quote: strings.ToUpper(strings.TrimSpace(intent.FX.Pair.Quote)),
},
Side: side,
Firm: intent.FX.Firm,
TtlMs: intent.FX.TTLms,
PreferredProvider: strings.TrimSpace(intent.FX.PreferredProvider),
MaxAgeMs: intent.FX.MaxAgeMs,
}
}

View File

@@ -202,8 +202,14 @@ func TestMapQuoteIntent_DerivesSettlementCurrencyFromFX(t *testing.T) {
if got.GetSettlementCurrency() != "RUB" {
t.Fatalf("unexpected settlement currency: got=%q", got.GetSettlementCurrency())
}
if got.GetFxSide() != fxv1.Side_SELL_BASE_BUY_QUOTE {
t.Fatalf("unexpected fx_side: got=%s", got.GetFxSide().String())
if got.GetFx() == nil || got.GetFx().GetPair() == nil {
t.Fatalf("expected fx intent")
}
if got.GetFx().GetSide() != fxv1.Side_SELL_BASE_BUY_QUOTE {
t.Fatalf("unexpected fx side: got=%s", got.GetFx().GetSide().String())
}
if got.GetFx().GetPair().GetBase() != "USDT" || got.GetFx().GetPair().GetQuote() != "RUB" {
t.Fatalf("unexpected fx pair: got=%s/%s", got.GetFx().GetPair().GetBase(), got.GetFx().GetPair().GetQuote())
}
}
@@ -246,8 +252,11 @@ func TestMapQuoteIntent_PropagatesFXSideBuyBaseSellQuote(t *testing.T) {
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got.GetFxSide() != fxv1.Side_BUY_BASE_SELL_QUOTE {
t.Fatalf("unexpected fx_side: got=%s", got.GetFxSide().String())
if got.GetFx() == nil || got.GetFx().GetPair() == nil {
t.Fatalf("expected fx intent")
}
if got.GetFx().GetSide() != fxv1.Side_BUY_BASE_SELL_QUOTE {
t.Fatalf("unexpected fx side: got=%s", got.GetFx().GetSide().String())
}
if got.GetSettlementCurrency() != "RUB" {
t.Fatalf("unexpected settlement currency: got=%q", got.GetSettlementCurrency())

View File

@@ -7,7 +7,7 @@ replace github.com/tech/sendico/pkg => ../../pkg
replace github.com/tech/sendico/gateway/common => ../common
require (
github.com/ethereum/go-ethereum v1.17.0
github.com/ethereum/go-ethereum v1.17.1
github.com/mitchellh/mapstructure v1.5.0
github.com/prometheus/client_golang v1.23.2
github.com/shopspring/decimal v1.4.0

View File

@@ -76,8 +76,8 @@ github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn2
github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw=
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk=
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8=
github.com/ethereum/go-ethereum v1.17.0 h1:2D+1Fe23CwZ5tQoAS5DfwKFNI1HGcTwi65/kRlAVxes=
github.com/ethereum/go-ethereum v1.17.0/go.mod h1:2W3msvdosS/MCWytpqTcqgFiRYbTH59FxDJzqah120o=
github.com/ethereum/go-ethereum v1.17.1 h1:IjlQDjgxg2uL+GzPRkygGULPMLzcYWncEI7wbaizvho=
github.com/ethereum/go-ethereum v1.17.1/go.mod h1:7UWOVHL7K3b8RfVRea022btnzLCaanwHtBuH1jUCH/I=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=

View File

@@ -43,8 +43,7 @@ func NewPayouts(logger mlogger.Logger, db *mongo.Database) (*Payouts, error) {
Sparse: true,
}); err != nil {
logger.Error("Failed to create payouts operation index",
zap.Error(err),
zap.String("index_field", payoutOpField))
zap.Error(err), zap.String("index_field", payoutOpField))
return nil, err
}
if err := repo.CreateIndex(&ri.Definition{
@@ -52,8 +51,7 @@ func NewPayouts(logger mlogger.Logger, db *mongo.Database) (*Payouts, error) {
Unique: true,
}); err != nil {
logger.Error("Failed to create payouts idempotency index",
zap.Error(err),
zap.String("index_field", payoutIdemField))
zap.Error(err), zap.String("index_field", payoutIdemField))
return nil, err
}

View File

@@ -80,8 +80,6 @@ github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn2
github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw=
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk=
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8=
github.com/ethereum/go-ethereum v1.17.0 h1:2D+1Fe23CwZ5tQoAS5DfwKFNI1HGcTwi65/kRlAVxes=
github.com/ethereum/go-ethereum v1.17.0/go.mod h1:2W3msvdosS/MCWytpqTcqgFiRYbTH59FxDJzqah120o=
github.com/ethereum/go-ethereum v1.17.1 h1:IjlQDjgxg2uL+GzPRkygGULPMLzcYWncEI7wbaizvho=
github.com/ethereum/go-ethereum v1.17.1/go.mod h1:7UWOVHL7K3b8RfVRea022btnzLCaanwHtBuH1jUCH/I=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=

View File

@@ -51,6 +51,12 @@ func (i *Imp) initDependencies(cfg *config) *clientDependencies {
i.discoveryClients = newDiscoveryClientResolver(i.logger, i.discoveryReg)
deps.gatewayResolver = discoveryChainGatewayResolver{resolver: i.discoveryClients}
deps.gatewayInvokeResolver = discoveryGatewayInvokeResolver{resolver: i.discoveryClients}
ledgerClient, ledgerErr := i.discoveryClients.LedgerClient(context.Background())
if ledgerErr != nil {
i.logger.Warn("Failed to initialise ledger client from discovery", zap.Error(ledgerErr))
} else {
deps.ledgerClient = ledgerClient
}
} else if i != nil && i.logger != nil {
i.logger.Warn("Discovery registry unavailable; chain gateway clients disabled")
}

View File

@@ -11,9 +11,11 @@ import (
"time"
chainclient "github.com/tech/sendico/gateway/chain/client"
ledgerclient "github.com/tech/sendico/ledger/client"
"github.com/tech/sendico/pkg/discovery"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
"go.uber.org/zap"
)
@@ -35,7 +37,9 @@ type discoveryClientResolver struct {
mu sync.Mutex
chainClients map[string]chainclient.Client
chainClients map[string]chainclient.Client
ledgerClient ledgerclient.Client
ledgerEndpoint discoveryEndpoint
lastSelection map[string]string
lastMissing map[string]time.Time
@@ -66,6 +70,10 @@ func (r *discoveryClientResolver) Close() {
}
delete(r.chainClients, key)
}
if r.ledgerClient != nil {
_ = r.ledgerClient.Close()
r.ledgerClient = nil
}
}
type discoveryGatewayInvokeResolver struct {
@@ -130,6 +138,43 @@ func (r *discoveryClientResolver) ChainClientByNetwork(ctx context.Context, netw
return r.ChainClientByInvokeURI(ctx, entry.InvokeURI)
}
func (r *discoveryClientResolver) LedgerClient(ctx context.Context) (ledgerclient.Client, error) {
entry, ok := r.findLedgerEntry()
if !ok {
return nil, merrors.NoData("discovery: ledger service unavailable")
}
endpoint, err := parseDiscoveryEndpoint(entry.InvokeURI)
if err != nil {
r.logMissing("ledger", "invalid ledger invoke uri", entry.InvokeURI, err)
return nil, err
}
if ctx == nil {
ctx = context.Background()
}
r.mu.Lock()
defer r.mu.Unlock()
if r.ledgerClient == nil || r.ledgerEndpoint.key() != endpoint.key() || r.ledgerEndpoint.address != endpoint.address {
if r.ledgerClient != nil {
_ = r.ledgerClient.Close()
r.ledgerClient = nil
}
client, dialErr := ledgerclient.New(ctx, ledgerclient.Config{
Address: endpoint.address,
Insecure: endpoint.insecure,
})
if dialErr != nil {
r.logMissing("ledger", "failed to dial ledger service", endpoint.raw, dialErr)
return nil, dialErr
}
r.ledgerClient = client
r.ledgerEndpoint = endpoint
}
return r.ledgerClient, nil
}
func (r *discoveryClientResolver) findChainEntry(network string) (*discovery.RegistryEntry, bool) {
if r == nil || r.registry == nil {
r.logMissing("chain", "discovery registry unavailable", "", nil)
@@ -172,6 +217,44 @@ func (r *discoveryClientResolver) findChainEntry(network string) (*discovery.Reg
return &entry, true
}
func (r *discoveryClientResolver) findLedgerEntry() (*discovery.RegistryEntry, bool) {
if r == nil || r.registry == nil {
r.logMissing("ledger", "discovery registry unavailable", "", nil)
return nil, false
}
entries := r.registry.List(time.Now(), true)
matches := make([]discovery.RegistryEntry, 0)
for _, entry := range entries {
if !strings.EqualFold(strings.TrimSpace(entry.Service), string(mservice.Ledger)) {
continue
}
if strings.TrimSpace(entry.InvokeURI) == "" {
continue
}
matches = append(matches, entry)
}
if len(matches) == 0 {
r.logMissing("ledger", "discovery ledger entry missing", "", nil)
return nil, false
}
sort.Slice(matches, func(i, j int) bool {
if matches[i].RoutingPriority != matches[j].RoutingPriority {
return matches[i].RoutingPriority > matches[j].RoutingPriority
}
if matches[i].ID != matches[j].ID {
return matches[i].ID < matches[j].ID
}
return matches[i].InstanceID < matches[j].InstanceID
})
entry := matches[0]
entryKey := discoveryEntryKey(entry)
r.logSelection("ledger", entryKey, entry)
return &entry, true
}
func (r *discoveryClientResolver) logSelection(key, entryKey string, entry discovery.RegistryEntry) {
if r == nil {
return

View File

@@ -51,6 +51,9 @@ func (i *Imp) Start() error {
if i.deps.oracleClient != nil {
opts = append(opts, quotesvc.WithOracleClient(i.deps.oracleClient))
}
if i.deps.ledgerClient != nil {
opts = append(opts, quotesvc.WithLedgerClient(i.deps.ledgerClient))
}
if i.deps.gatewayResolver != nil {
opts = append(opts, quotesvc.WithChainGatewayResolver(i.deps.gatewayResolver))
}

View File

@@ -2,6 +2,7 @@ package serverimp
import (
oracleclient "github.com/tech/sendico/fx/oracle/client"
ledgerclient "github.com/tech/sendico/ledger/client"
quotesvc "github.com/tech/sendico/payments/quotation/internal/service/quotation"
"github.com/tech/sendico/payments/storage"
"github.com/tech/sendico/pkg/discovery"
@@ -20,6 +21,7 @@ type clientDependencies struct {
feesConn *grpc.ClientConn
feesClient feesv1.FeeEngineClient
oracleClient oracleclient.Client
ledgerClient ledgerclient.Client
gatewayResolver quotesvc.ChainGatewayResolver
gatewayInvokeResolver quotesvc.GatewayInvokeResolver
}

View File

@@ -0,0 +1,90 @@
package quotation
import (
"context"
"strings"
"github.com/tech/sendico/pkg/merrors"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
"go.uber.org/zap"
)
type ledgerAccountCurrencyResolver struct {
client ledgerClientForCurrency
logger *zap.Logger
}
type ledgerClientForCurrency interface {
GetBalance(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, error)
ListAccounts(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error)
}
func newLedgerAccountCurrencyResolver(core *Service) *ledgerAccountCurrencyResolver {
if core == nil || core.deps.ledger == nil {
return nil
}
logger := core.logger
if logger == nil {
logger = zap.NewNop()
}
return &ledgerAccountCurrencyResolver{
client: core.deps.ledger,
logger: logger,
}
}
func (r *ledgerAccountCurrencyResolver) ResolveLedgerAccountCurrency(
ctx context.Context,
organizationRef string,
ledgerAccountRef string,
) (string, error) {
if r == nil || r.client == nil {
return "", merrors.NoData("ledger client unavailable")
}
accountRef := strings.TrimSpace(ledgerAccountRef)
if accountRef == "" {
return "", merrors.InvalidArgument("ledger_account_ref is required")
}
balanceResp, err := r.client.GetBalance(ctx, &ledgerv1.GetBalanceRequest{
LedgerAccountRef: accountRef,
})
if err != nil {
return "", err
}
if balance := balanceResp.GetBalance(); balance != nil {
if currency := strings.ToUpper(strings.TrimSpace(balance.GetCurrency())); currency != "" {
return currency, nil
}
}
orgRef := strings.TrimSpace(organizationRef)
if orgRef == "" {
return "", merrors.NoData("ledger account currency is missing")
}
listResp, err := r.client.ListAccounts(ctx, &ledgerv1.ListAccountsRequest{
OrganizationRef: orgRef,
})
if err != nil {
return "", err
}
for _, account := range listResp.GetAccounts() {
if strings.TrimSpace(account.GetLedgerAccountRef()) != accountRef {
continue
}
if currency := strings.ToUpper(strings.TrimSpace(account.GetCurrency())); currency != "" {
return currency, nil
}
break
}
if r.logger != nil {
r.logger.Warn("Failed to resolve ledger account currency",
zap.String("organization_ref", orgRef),
zap.String("ledger_account_ref", accountRef),
)
}
return "", merrors.NoData("ledger account currency is missing")
}

View File

@@ -159,9 +159,13 @@ func WithClock(clock clockpkg.Clock) Option {
}
}
// WithLedgerClient is retained for backward compatibility and is currently a no-op.
func WithLedgerClient(_ ledgerclient.Client) Option {
return func(*Service) {}
// WithLedgerClient wires the ledger client used for account-currency inference.
func WithLedgerClient(client ledgerclient.Client) Option {
return func(s *Service) {
if s != nil && client != nil {
s.deps.ledger = client
}
}
}
// WithProviderSettlementGatewayClient is retained for backward compatibility and is currently a no-op.

View File

@@ -9,6 +9,7 @@ import (
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
@@ -29,6 +30,9 @@ type quoteIntentLogSummary struct {
SettlementMode string `json:"settlementMode,omitempty"`
FeeTreatment string `json:"feeTreatment,omitempty"`
SettlementCurrency string `json:"settlementCurrency,omitempty"`
HasFX bool `json:"hasFx"`
FXPair string `json:"fxPair,omitempty"`
FXSide string `json:"fxSide,omitempty"`
HasComment bool `json:"hasComment"`
}
@@ -59,6 +63,7 @@ func summarizeQuoteIntent(intent *quotationv2.QuoteIntent) *quoteIntentLogSummar
if intent == nil {
return nil
}
fxPair, fxSide, hasFX := summarizeFXIntent(intent.GetFx())
return &quoteIntentLogSummary{
Source: summarizeEndpoint(intent.GetSource()),
Destination: summarizeEndpoint(intent.GetDestination()),
@@ -66,6 +71,9 @@ func summarizeQuoteIntent(intent *quotationv2.QuoteIntent) *quoteIntentLogSummar
SettlementMode: enumLogValue(intent.GetSettlementMode().String()),
FeeTreatment: enumLogValue(intent.GetFeeTreatment().String()),
SettlementCurrency: strings.ToUpper(strings.TrimSpace(intent.GetSettlementCurrency())),
HasFX: hasFX,
FXPair: fxPair,
FXSide: fxSide,
HasComment: strings.TrimSpace(intent.GetComment()) != "",
}
}
@@ -142,3 +150,15 @@ func moneyLogValue(m *moneyv1.Money) string {
func enumLogValue(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}
func summarizeFXIntent(fx *sharedv1.FXIntent) (string, string, bool) {
if fx == nil || fx.GetPair() == nil {
return "", "", false
}
base := strings.ToUpper(strings.TrimSpace(fx.GetPair().GetBase()))
quote := strings.ToUpper(strings.TrimSpace(fx.GetPair().GetQuote()))
if base == "" && quote == "" {
return "", enumLogValue(fx.GetSide().String()), false
}
return strings.TrimSpace(base + "/" + quote), enumLogValue(fx.GetSide().String()), true
}

View File

@@ -56,6 +56,9 @@ func newQuoteComputationService(core *Service) *quote_computation_service.QuoteC
if resolver := newManagedWalletNetworkResolver(core); resolver != nil {
opts = append(opts, quote_computation_service.WithManagedWalletNetworkResolver(resolver))
}
if resolver := newLedgerAccountCurrencyResolver(core); resolver != nil {
opts = append(opts, quote_computation_service.WithLedgerAccountCurrencyResolver(resolver))
}
if resolver := fundingProfileResolver(core); resolver != nil {
opts = append(opts, quote_computation_service.WithFundingProfileResolver(resolver))
}

View File

@@ -0,0 +1,87 @@
package quote_computation_service
import (
"context"
"strings"
"github.com/tech/sendico/payments/storage/model"
)
type endpointCurrencyInference struct {
SourceCurrency string
DestinationCurrency string
SourceInferred bool
DestinationInferred bool
}
func (s *QuoteComputationService) inferEndpointCurrencies(
ctx context.Context,
organizationRef string,
intent model.PaymentIntent,
ledgerCurrencyCache map[string]string,
) (endpointCurrencyInference, error) {
sourceCurrency, sourceInferred, err := s.inferEndpointCurrency(
ctx,
organizationRef,
intent.Source,
ledgerCurrencyCache,
)
if err != nil {
return endpointCurrencyInference{}, err
}
destinationCurrency, destinationInferred, err := s.inferEndpointCurrency(
ctx,
organizationRef,
intent.Destination,
ledgerCurrencyCache,
)
if err != nil {
return endpointCurrencyInference{}, err
}
return endpointCurrencyInference{
SourceCurrency: sourceCurrency,
DestinationCurrency: destinationCurrency,
SourceInferred: sourceInferred,
DestinationInferred: destinationInferred,
}, nil
}
func (s *QuoteComputationService) inferEndpointCurrency(
ctx context.Context,
organizationRef string,
endpoint model.PaymentEndpoint,
ledgerCurrencyCache map[string]string,
) (string, bool, error) {
if token := sourceAssetToken(endpoint); token != "" {
return token, true, nil
}
if endpoint.Ledger == nil {
return "", false, nil
}
ledgerAccountRef := strings.TrimSpace(endpoint.Ledger.LedgerAccountRef)
if ledgerAccountRef == "" {
return "", false, nil
}
if cached := normalizeAsset(ledgerCurrencyCache[ledgerAccountRef]); cached != "" {
return cached, true, nil
}
if s == nil || s.ledgerAccountCurrencyResolver == nil {
return "", false, nil
}
currency, err := s.ledgerAccountCurrencyResolver.ResolveLedgerAccountCurrency(
ctx,
strings.TrimSpace(organizationRef),
ledgerAccountRef,
)
if err != nil {
return "", false, err
}
currency = normalizeAsset(currency)
if currency == "" {
return "", false, nil
}
if ledgerCurrencyCache != nil {
ledgerCurrencyCache[ledgerAccountRef] = currency
}
return currency, true, nil
}

View File

@@ -0,0 +1,86 @@
package quote_computation_service
import (
"context"
"testing"
"github.com/tech/sendico/payments/storage/model"
paymenttypes "github.com/tech/sendico/pkg/payments/types"
)
func TestInferEndpointCurrencies_UsesEndpointAssets(t *testing.T) {
svc := New(nil)
out, err := svc.inferEndpointCurrencies(context.Background(), "org-1", model.PaymentIntent{
Source: model.PaymentEndpoint{
Type: model.EndpointTypeManagedWallet,
ManagedWallet: &model.ManagedWalletEndpoint{
ManagedWalletRef: "mw-src",
Asset: &paymenttypes.Asset{TokenSymbol: "USDT"},
},
},
Destination: model.PaymentEndpoint{
Type: model.EndpointTypeExternalChain,
ExternalChain: &model.ExternalChainEndpoint{
Asset: &paymenttypes.Asset{TokenSymbol: "RUB"},
},
},
}, map[string]string{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got, want := out.SourceCurrency, "USDT"; got != want {
t.Fatalf("unexpected source currency: got=%q want=%q", got, want)
}
if got, want := out.DestinationCurrency, "RUB"; got != want {
t.Fatalf("unexpected destination currency: got=%q want=%q", got, want)
}
if !out.SourceInferred || !out.DestinationInferred {
t.Fatalf("expected both currencies inferred: %#v", out)
}
}
func TestInferEndpointCurrencies_UsesLedgerResolver(t *testing.T) {
resolver := &fakeLedgerCurrencyResolver{
currencies: map[string]string{
"ledger-src": "USDT",
"ledger-dst": "RUB",
},
}
svc := New(nil, WithLedgerAccountCurrencyResolver(resolver))
out, err := svc.inferEndpointCurrencies(context.Background(), "org-1", model.PaymentIntent{
Source: model.PaymentEndpoint{
Type: model.EndpointTypeLedger,
Ledger: &model.LedgerEndpoint{
LedgerAccountRef: "ledger-src",
},
},
Destination: model.PaymentEndpoint{
Type: model.EndpointTypeLedger,
Ledger: &model.LedgerEndpoint{
LedgerAccountRef: "ledger-dst",
},
},
}, map[string]string{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got, want := out.SourceCurrency, "USDT"; got != want {
t.Fatalf("unexpected source currency: got=%q want=%q", got, want)
}
if got, want := out.DestinationCurrency, "RUB"; got != want {
t.Fatalf("unexpected destination currency: got=%q want=%q", got, want)
}
if resolver.calls != 2 {
t.Fatalf("unexpected resolver calls: got=%d want=%d", resolver.calls, 2)
}
}
type fakeLedgerCurrencyResolver struct {
currencies map[string]string
calls int
}
func (f *fakeLedgerCurrencyResolver) ResolveLedgerAccountCurrency(_ context.Context, _ string, ledgerAccountRef string) (string, error) {
f.calls++
return f.currencies[ledgerAccountRef], nil
}

View File

@@ -8,6 +8,15 @@ import (
paymenttypes "github.com/tech/sendico/pkg/payments/types"
)
type fxDerivationResult struct {
InferredSourceCurrency string
InferredDestinationCurrency string
EffectiveSourceCurrency string
EffectiveDestinationCurrency string
ExplicitOverrideApplied bool
RequiresFXInferred bool
}
func modelIntentFromQuoteIntent(src *transfer_intent_hydrator.QuoteIntent) model.PaymentIntent {
if src == nil {
return model.PaymentIntent{}
@@ -33,18 +42,44 @@ func modelIntentFromQuoteIntent(src *transfer_intent_hydrator.QuoteIntent) model
}
func fxIntentFromHydratedIntent(src *transfer_intent_hydrator.QuoteIntent) *model.FXIntent {
if src == nil {
if src == nil || src.FX == nil {
return nil
}
if strings.TrimSpace(string(src.FXSide)) == "" || src.FXSide == paymenttypes.FXSideUnspecified {
result := &model.FXIntent{
Side: src.FX.Side,
Firm: src.FX.Firm,
TTLMillis: src.FX.TTLms,
PreferredProvider: strings.TrimSpace(src.FX.PreferredProvider),
MaxAgeMillis: src.FX.MaxAgeMs,
}
if src.FX.Pair != nil {
result.Pair = &paymenttypes.CurrencyPair{
Base: normalizeAsset(src.FX.Pair.GetBase()),
Quote: normalizeAsset(src.FX.Pair.GetQuote()),
}
}
if result.Side == paymenttypes.FXSideUnspecified &&
result.Pair == nil &&
!result.Firm &&
result.TTLMillis == 0 &&
result.PreferredProvider == "" &&
result.MaxAgeMillis == 0 {
return nil
}
return &model.FXIntent{Side: src.FXSide}
return result
}
func ensureDerivedFXIntent(intent *model.PaymentIntent) {
func ensureDerivedFXIntent(
intent *model.PaymentIntent,
inferredSourceCurrency string,
inferredDestinationCurrency string,
) fxDerivationResult {
result := fxDerivationResult{
InferredSourceCurrency: normalizeAsset(inferredSourceCurrency),
InferredDestinationCurrency: normalizeAsset(inferredDestinationCurrency),
}
if intent == nil {
return
return result
}
amountCurrency := ""
@@ -52,6 +87,9 @@ func ensureDerivedFXIntent(intent *model.PaymentIntent) {
amountCurrency = normalizeAsset(intent.Amount.GetCurrency())
}
settlementCurrency := normalizeAsset(intent.SettlementCurrency)
if result.InferredDestinationCurrency != "" {
settlementCurrency = result.InferredDestinationCurrency
}
if settlementCurrency == "" {
settlementCurrency = amountCurrency
}
@@ -59,42 +97,98 @@ func ensureDerivedFXIntent(intent *model.PaymentIntent) {
intent.SettlementCurrency = settlementCurrency
}
sourceCurrency := sourceAssetToken(intent.Source)
sourceCurrency := firstNonEmpty(result.InferredSourceCurrency, sourceAssetToken(intent.Source))
sourceCurrencyBeforeExplicit := sourceCurrency
settlementCurrencyBeforeExplicit := settlementCurrency
requiresFXBeforeExplicit := intent.RequiresFX
// For FIX_RECEIVED, destination amounts can be provided in payout currency.
// Derive FX necessity from source asset currency when available.
if !intent.RequiresFX &&
intent.SettlementMode == model.SettlementModeFixReceived &&
explicitSourceCurrency, explicitDestinationCurrency := fxTradeCurrencies(intent.FX)
if explicitSourceCurrency != "" && explicitDestinationCurrency != "" {
sourceCurrency = explicitSourceCurrency
settlementCurrency = explicitDestinationCurrency
intent.SettlementCurrency = settlementCurrency
intent.RequiresFX = true
if sourceCurrencyBeforeExplicit == "" ||
settlementCurrencyBeforeExplicit == "" ||
!strings.EqualFold(sourceCurrencyBeforeExplicit, explicitSourceCurrency) ||
!strings.EqualFold(settlementCurrencyBeforeExplicit, explicitDestinationCurrency) ||
!requiresFXBeforeExplicit {
result.ExplicitOverrideApplied = true
}
} else if !intent.RequiresFX &&
sourceCurrency != "" &&
settlementCurrency != "" &&
!strings.EqualFold(sourceCurrency, settlementCurrency) {
intent.RequiresFX = true
result.RequiresFXInferred = true
}
if !intent.RequiresFX {
return
result.EffectiveSourceCurrency = sourceCurrency
result.EffectiveDestinationCurrency = settlementCurrency
return result
}
baseCurrency := firstNonEmpty(sourceCurrency, amountCurrency)
quoteCurrency := settlementCurrency
if baseCurrency == "" || quoteCurrency == "" {
return
result.EffectiveSourceCurrency = sourceCurrency
result.EffectiveDestinationCurrency = settlementCurrency
return result
}
if intent.FX == nil {
intent.FX = &model.FXIntent{}
}
if strings.TrimSpace(string(intent.FX.Side)) == "" || intent.FX.Side == paymenttypes.FXSideUnspecified {
intent.FX.Side = paymenttypes.FXSideSellBaseBuyQuote
}
if intent.FX.Pair == nil {
intent.FX.Pair = &paymenttypes.CurrencyPair{}
}
desiredBase, desiredQuote := fxPairFromTradeCurrencies(intent.FX.Side, baseCurrency, quoteCurrency)
if normalizeAsset(intent.FX.Pair.Base) == "" {
intent.FX.Pair.Base = baseCurrency
intent.FX.Pair.Base = desiredBase
}
if normalizeAsset(intent.FX.Pair.Quote) == "" {
intent.FX.Pair.Quote = quoteCurrency
intent.FX.Pair.Quote = desiredQuote
}
if strings.TrimSpace(string(intent.FX.Side)) == "" || intent.FX.Side == paymenttypes.FXSideUnspecified {
intent.FX.Side = paymenttypes.FXSideSellBaseBuyQuote
result.EffectiveSourceCurrency, result.EffectiveDestinationCurrency = fxTradeCurrencies(intent.FX)
if result.EffectiveSourceCurrency == "" {
result.EffectiveSourceCurrency = sourceCurrency
}
if result.EffectiveDestinationCurrency == "" {
result.EffectiveDestinationCurrency = settlementCurrency
}
return result
}
func fxPairFromTradeCurrencies(side paymenttypes.FXSide, sourceCurrency, destinationCurrency string) (string, string) {
sourceCurrency = normalizeAsset(sourceCurrency)
destinationCurrency = normalizeAsset(destinationCurrency)
switch side {
case paymenttypes.FXSideBuyBaseSellQuote:
return destinationCurrency, sourceCurrency
default:
return sourceCurrency, destinationCurrency
}
}
func fxTradeCurrencies(fx *model.FXIntent) (string, string) {
if fx == nil || fx.Pair == nil {
return "", ""
}
base := normalizeAsset(fx.Pair.GetBase())
quote := normalizeAsset(fx.Pair.GetQuote())
if base == "" || quote == "" {
return "", ""
}
switch fx.Side {
case paymenttypes.FXSideBuyBaseSellQuote:
return quote, base
default:
return base, quote
}
}

View File

@@ -16,7 +16,7 @@ func TestEnsureDerivedFXIntent_DefaultsSideWhenEmpty(t *testing.T) {
FX: &model.FXIntent{},
}
ensureDerivedFXIntent(intent)
ensureDerivedFXIntent(intent, "", "")
if intent.FX == nil {
t.Fatal("expected fx intent")
@@ -34,7 +34,7 @@ func TestEnsureDerivedFXIntent_DefaultsSideWhenUnspecified(t *testing.T) {
FX: &model.FXIntent{Side: paymenttypes.FXSideUnspecified},
}
ensureDerivedFXIntent(intent)
ensureDerivedFXIntent(intent, "", "")
if intent.FX == nil {
t.Fatal("expected fx intent")
@@ -56,11 +56,17 @@ func TestEnsureDerivedFXIntent_PreservesExplicitSideFromHydratedIntent(t *testin
Amount: &paymenttypes.Money{Amount: "100", Currency: "USDT"},
SettlementCurrency: "RUB",
RequiresFX: true,
FXSide: paymenttypes.FXSideBuyBaseSellQuote,
FX: &transfer_intent_hydrator.QuoteFXIntent{
Pair: &paymenttypes.CurrencyPair{
Base: "RUB",
Quote: "USDT",
},
Side: paymenttypes.FXSideBuyBaseSellQuote,
},
}
intent := modelIntentFromQuoteIntent(hydrated)
ensureDerivedFXIntent(&intent)
ensureDerivedFXIntent(&intent, "", "")
if intent.FX == nil {
t.Fatal("expected fx intent")
@@ -69,3 +75,45 @@ func TestEnsureDerivedFXIntent_PreservesExplicitSideFromHydratedIntent(t *testin
t.Fatalf("unexpected side: got=%q want=%q", got, want)
}
}
func TestEnsureDerivedFXIntent_ExplicitFXOverridesInferredCurrencies(t *testing.T) {
intent := &model.PaymentIntent{
RequiresFX: false,
SettlementCurrency: "EUR",
Amount: &paymenttypes.Money{Amount: "10", Currency: "EUR"},
FX: &model.FXIntent{
Pair: &paymenttypes.CurrencyPair{
Base: "USDT",
Quote: "RUB",
},
Side: paymenttypes.FXSideSellBaseBuyQuote,
},
}
out := ensureDerivedFXIntent(intent, "BTC", "EUR")
if !out.ExplicitOverrideApplied {
t.Fatalf("expected explicit override flag")
}
if !intent.RequiresFX {
t.Fatalf("expected requires_fx=true")
}
if got, want := intent.SettlementCurrency, "RUB"; got != want {
t.Fatalf("unexpected settlement currency: got=%q want=%q", got, want)
}
if intent.FX == nil || intent.FX.Pair == nil {
t.Fatalf("expected fx pair")
}
if got, want := intent.FX.Pair.GetBase(), "USDT"; got != want {
t.Fatalf("unexpected base currency: got=%q want=%q", got, want)
}
if got, want := intent.FX.Pair.GetQuote(), "RUB"; got != want {
t.Fatalf("unexpected quote currency: got=%q want=%q", got, want)
}
if got, want := out.EffectiveSourceCurrency, "USDT"; got != want {
t.Fatalf("unexpected effective source currency: got=%q want=%q", got, want)
}
if got, want := out.EffectiveDestinationCurrency, "RUB"; got != want {
t.Fatalf("unexpected effective destination currency: got=%q want=%q", got, want)
}
}

View File

@@ -53,9 +53,10 @@ func (s *QuoteComputationService) BuildPlan(ctx context.Context, in ComputeInput
Items: make([]*QuoteComputationPlanItem, 0, len(in.Intents)),
}
managedWalletNetworks := map[string]string{}
ledgerAccountCurrencies := map[string]string{}
for i, intent := range in.Intents {
item, err := s.buildPlanItem(ctx, in, i, intent, managedWalletNetworks)
item, err := s.buildPlanItem(ctx, in, i, intent, managedWalletNetworks, ledgerAccountCurrencies)
if err != nil {
s.logger.Warn("Computation plan item build failed",
zap.String("org_ref", in.OrganizationRef),
@@ -84,6 +85,7 @@ func (s *QuoteComputationService) buildPlanItem(
index int,
intent *transfer_intent_hydrator.QuoteIntent,
managedWalletNetworks map[string]string,
ledgerAccountCurrencies map[string]string,
) (*QuoteComputationPlanItem, error) {
if intent == nil {
s.logger.Warn("Plan item build failed: intent is nil", zap.Int("index", index))
@@ -137,7 +139,59 @@ func (s *QuoteComputationService) buildPlanItem(
}
modelIntent.Source = clonePaymentEndpoint(source)
modelIntent.Destination = clonePaymentEndpoint(destination)
ensureDerivedFXIntent(&modelIntent)
currencyInference, err := s.inferEndpointCurrencies(
ctx,
strings.TrimSpace(in.OrganizationRef),
modelIntent,
ledgerAccountCurrencies,
)
if err != nil {
return nil, merrors.InternalWrap(err, "resolve endpoint currencies")
}
fxDecision := ensureDerivedFXIntent(
&modelIntent,
currencyInference.SourceCurrency,
currencyInference.DestinationCurrency,
)
if currencyInference.SourceInferred || currencyInference.DestinationInferred {
s.logger.Info("Resolved endpoint currencies for quote intent",
zap.Int("index", index),
zap.String("intent_ref", strings.TrimSpace(modelIntent.Ref)),
zap.String("inferred_source_currency", fxDecision.InferredSourceCurrency),
zap.String("inferred_destination_currency", fxDecision.InferredDestinationCurrency),
zap.String("effective_source_currency", fxDecision.EffectiveSourceCurrency),
zap.String("effective_destination_currency", fxDecision.EffectiveDestinationCurrency),
)
}
if fxDecision.ExplicitOverrideApplied {
fxBase, fxQuote, fxSide := "", "", ""
if modelIntent.FX != nil {
fxSide = strings.TrimSpace(string(modelIntent.FX.Side))
if modelIntent.FX.Pair != nil {
fxBase = strings.TrimSpace(modelIntent.FX.Pair.GetBase())
fxQuote = strings.TrimSpace(modelIntent.FX.Pair.GetQuote())
}
}
s.logger.Info("Applied explicit FX override to inferred endpoint currencies",
zap.Int("index", index),
zap.String("intent_ref", strings.TrimSpace(modelIntent.Ref)),
zap.String("inferred_source_currency", fxDecision.InferredSourceCurrency),
zap.String("inferred_destination_currency", fxDecision.InferredDestinationCurrency),
zap.String("effective_source_currency", fxDecision.EffectiveSourceCurrency),
zap.String("effective_destination_currency", fxDecision.EffectiveDestinationCurrency),
zap.String("fx_base_currency", fxBase),
zap.String("fx_quote_currency", fxQuote),
zap.String("fx_side", fxSide),
)
}
if fxDecision.RequiresFXInferred {
s.logger.Info("Inferred FX requirement from endpoint currencies",
zap.Int("index", index),
zap.String("intent_ref", strings.TrimSpace(modelIntent.Ref)),
zap.String("source_currency", fxDecision.EffectiveSourceCurrency),
zap.String("destination_currency", fxDecision.EffectiveDestinationCurrency),
)
}
sourceRail, sourceNetwork, err := plan.RailFromEndpoint(source, modelIntent.Attributes, true)
if err != nil {

View File

@@ -19,16 +19,21 @@ type ManagedWalletNetworkResolver interface {
ResolveManagedWalletNetwork(ctx context.Context, managedWalletRef string) (string, error)
}
type LedgerAccountCurrencyResolver interface {
ResolveLedgerAccountCurrency(ctx context.Context, organizationRef, ledgerAccountRef string) (string, error)
}
type Option func(*QuoteComputationService)
type QuoteComputationService struct {
core Core
fundingResolver gateway_funding_profile.FundingProfileResolver
gatewayRegistry plan.GatewayRegistry
managedWalletNetworkResolver ManagedWalletNetworkResolver
routeStore plan.RouteStore
pathFinder *graph_path_finder.GraphPathFinder
logger mlogger.Logger
core Core
fundingResolver gateway_funding_profile.FundingProfileResolver
gatewayRegistry plan.GatewayRegistry
managedWalletNetworkResolver ManagedWalletNetworkResolver
ledgerAccountCurrencyResolver LedgerAccountCurrencyResolver
routeStore plan.RouteStore
pathFinder *graph_path_finder.GraphPathFinder
logger mlogger.Logger
}
func New(core Core, opts ...Option) *QuoteComputationService {
@@ -69,6 +74,14 @@ func WithManagedWalletNetworkResolver(resolver ManagedWalletNetworkResolver) Opt
}
}
func WithLedgerAccountCurrencyResolver(resolver LedgerAccountCurrencyResolver) Option {
return func(svc *QuoteComputationService) {
if svc != nil {
svc.ledgerAccountCurrencyResolver = resolver
}
}
}
func WithRouteStore(store plan.RouteStore) Option {
return func(svc *QuoteComputationService) {
if svc != nil {

View File

@@ -1,6 +1,7 @@
package quotation
import (
ledgerclient "github.com/tech/sendico/ledger/client"
"github.com/tech/sendico/payments/storage"
clockpkg "github.com/tech/sendico/pkg/clock"
"github.com/tech/sendico/pkg/mlogger"
@@ -35,6 +36,7 @@ type serviceDependencies struct {
fees feesDependency
gateway gatewayDependency
oracle oracleDependency
ledger ledgerclient.Client
gatewayRegistry GatewayRegistry
gatewayInvokeResolver GatewayInvokeResolver
cardRoutes map[string]CardGatewayRoute

View File

@@ -13,6 +13,7 @@ import (
paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1"
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
"go.mongodb.org/mongo-driver/v2/bson"
"google.golang.org/grpc"
)
@@ -123,7 +124,16 @@ func (h *TransferIntentHydrator) HydrateOne(ctx context.Context, in HydrateOneIn
if settlementCurrency == "" {
settlementCurrency = strings.ToUpper(strings.TrimSpace(amount.Currency))
}
requiresFX := !strings.EqualFold(amount.Currency, settlementCurrency)
fxIntent := fxIntentFromProto(in.Intent.GetFx())
if settlementCurrency == "" {
settlementCurrency = settlementCurrencyFromFX(fxIntent)
}
requiresFX := false
if fxIntent != nil && fxIntent.Pair != nil {
requiresFX = true
} else {
requiresFX = !strings.EqualFold(amount.Currency, settlementCurrency)
}
intent := &QuoteIntent{
Ref: h.newRef(),
@@ -134,7 +144,7 @@ func (h *TransferIntentHydrator) HydrateOne(ctx context.Context, in HydrateOneIn
Comment: strings.TrimSpace(in.Intent.GetComment()),
SettlementMode: settlementMode,
FeeTreatment: feeTreatment,
FXSide: fxSideFromProto(in.Intent.GetFxSide()),
FX: fxIntent,
SettlementCurrency: settlementCurrency,
RequiresFX: requiresFX,
Attributes: map[string]string{
@@ -223,3 +233,55 @@ func fxSideFromProto(side fxv1.Side) paymenttypes.FXSide {
return paymenttypes.FXSideUnspecified
}
}
func fxIntentFromProto(src *sharedv1.FXIntent) *QuoteFXIntent {
if src == nil || src.GetPair() == nil {
return nil
}
base := strings.ToUpper(strings.TrimSpace(src.GetPair().GetBase()))
quote := strings.ToUpper(strings.TrimSpace(src.GetPair().GetQuote()))
if base == "" || quote == "" {
return nil
}
side := fxSideFromProto(src.GetSide())
if side == paymenttypes.FXSideUnspecified {
side = paymenttypes.FXSideSellBaseBuyQuote
}
return &QuoteFXIntent{
Pair: &paymenttypes.CurrencyPair{
Base: base,
Quote: quote,
},
Side: side,
Firm: src.GetFirm(),
TTLms: src.GetTtlMs(),
PreferredProvider: strings.TrimSpace(src.GetPreferredProvider()),
MaxAgeMs: src.GetMaxAgeMs(),
}
}
func settlementCurrencyFromFX(fx *QuoteFXIntent) string {
if fx == nil || fx.Pair == nil {
return ""
}
base := strings.ToUpper(strings.TrimSpace(fx.Pair.GetBase()))
quote := strings.ToUpper(strings.TrimSpace(fx.Pair.GetQuote()))
switch fx.Side {
case paymenttypes.FXSideBuyBaseSellQuote:
return firstNonEmpty(base, quote)
case paymenttypes.FXSideSellBaseBuyQuote:
return firstNonEmpty(quote, base)
default:
return firstNonEmpty(quote, base)
}
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
trimmed := strings.TrimSpace(value)
if trimmed != "" {
return trimmed
}
}
return ""
}

View File

@@ -64,6 +64,15 @@ type QuoteCardEndpoint struct {
MaskedPan string
}
type QuoteFXIntent struct {
Pair *paymenttypes.CurrencyPair
Side paymenttypes.FXSide
Firm bool
TTLms int64
PreferredProvider string
MaxAgeMs int32
}
type QuoteEndpoint struct {
Type QuoteEndpointType
PaymentMethodRef string
@@ -84,7 +93,7 @@ type QuoteIntent struct {
Comment string
SettlementMode QuoteSettlementMode
FeeTreatment QuoteFeeTreatment
FXSide paymenttypes.FXSide
FX *QuoteFXIntent
SettlementCurrency string
RequiresFX bool
Attributes map[string]string

View File

@@ -16,6 +16,7 @@ import (
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1"
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
"go.mongodb.org/mongo-driver/v2/bson"
"google.golang.org/grpc"
)
@@ -100,7 +101,7 @@ func TestHydrateOne_SuccessWithInlineMethods(t *testing.T) {
}
}
func TestHydrateOne_PropagatesFXSide(t *testing.T) {
func TestHydrateOne_PropagatesFXIntent(t *testing.T) {
h := New(nil, WithRefFactory(func() string { return "q-intent-fx-side" }))
intent := &quotationv2.QuoteIntent{
Source: &endpointv1.PaymentEndpoint{
@@ -126,7 +127,17 @@ func TestHydrateOne_PropagatesFXSide(t *testing.T) {
},
Amount: newMoney("10", "USDT"),
SettlementCurrency: "RUB",
FxSide: fxv1.Side_BUY_BASE_SELL_QUOTE,
Fx: &sharedv1.FXIntent{
Pair: &fxv1.CurrencyPair{
Base: "USDT",
Quote: "RUB",
},
Side: fxv1.Side_BUY_BASE_SELL_QUOTE,
Firm: true,
TtlMs: 12_000,
PreferredProvider: "bestfx",
MaxAgeMs: 1_000,
},
}
got, err := h.HydrateOne(context.Background(), HydrateOneInput{
@@ -140,8 +151,65 @@ func TestHydrateOne_PropagatesFXSide(t *testing.T) {
if got == nil {
t.Fatalf("expected hydrated intent")
}
if got.FXSide != paymenttypes.FXSideBuyBaseSellQuote {
t.Fatalf("unexpected fx side: got=%q", got.FXSide)
if got.FX == nil || got.FX.Pair == nil {
t.Fatalf("expected hydrated fx intent")
}
if got.FX.Side != paymenttypes.FXSideBuyBaseSellQuote {
t.Fatalf("unexpected fx side: got=%q", got.FX.Side)
}
if got.FX.Pair.GetBase() != "USDT" || got.FX.Pair.GetQuote() != "RUB" {
t.Fatalf("unexpected fx pair: got=%s/%s", got.FX.Pair.GetBase(), got.FX.Pair.GetQuote())
}
if !got.FX.Firm || got.FX.TTLms != 12_000 || got.FX.PreferredProvider != "bestfx" || got.FX.MaxAgeMs != 1_000 {
t.Fatalf("unexpected fx extras: %#v", got.FX)
}
}
func TestHydrateOne_RequiresFXWhenExplicitFXProvided(t *testing.T) {
h := New(nil, WithRefFactory(func() string { return "q-intent-fx-required" }))
intent := &quotationv2.QuoteIntent{
Source: &endpointv1.PaymentEndpoint{
Source: &endpointv1.PaymentEndpoint_PaymentMethod{
PaymentMethod: &endpointv1.PaymentMethod{
Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_LEDGER,
Data: mustMarshalBSON(t, map[string]string{"ledgerAccountRef": "ledger-src"}),
},
},
},
Destination: &endpointv1.PaymentEndpoint{
Source: &endpointv1.PaymentEndpoint_PaymentMethod{
PaymentMethod: &endpointv1.PaymentMethod{
Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD,
Data: mustMarshalBSON(t, pkgmodel.CardPaymentData{
Pan: "4111111111111111",
ExpMonth: "12",
ExpYear: "2030",
Country: "US",
}),
},
},
},
Amount: newMoney("10", "RUB"),
SettlementCurrency: "RUB",
Fx: &sharedv1.FXIntent{
Pair: &fxv1.CurrencyPair{Base: "USDT", Quote: "RUB"},
Side: fxv1.Side_SELL_BASE_BUY_QUOTE,
},
}
got, err := h.HydrateOne(context.Background(), HydrateOneInput{
OrganizationRef: bson.NewObjectID().Hex(),
InitiatorRef: bson.NewObjectID().Hex(),
Intent: intent,
})
if err != nil {
t.Fatalf("HydrateOne returned error: %v", err)
}
if got == nil {
t.Fatalf("expected hydrated intent")
}
if !got.RequiresFX {
t.Fatalf("expected requires_fx=true when explicit fx is supplied")
}
}

View File

@@ -5,7 +5,7 @@ go 1.25.0
require (
github.com/casbin/casbin/v2 v2.135.0
github.com/casbin/mongodb-adapter/v4 v4.3.0
github.com/ethereum/go-ethereum v1.17.0
github.com/ethereum/go-ethereum v1.17.1
github.com/go-chi/chi/v5 v5.2.5
github.com/google/uuid v1.6.0
github.com/hashicorp/vault/api v1.22.0

View File

@@ -61,8 +61,8 @@ github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls=
github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw=
github.com/ethereum/go-ethereum v1.17.0 h1:2D+1Fe23CwZ5tQoAS5DfwKFNI1HGcTwi65/kRlAVxes=
github.com/ethereum/go-ethereum v1.17.0/go.mod h1:2W3msvdosS/MCWytpqTcqgFiRYbTH59FxDJzqah120o=
github.com/ethereum/go-ethereum v1.17.1 h1:IjlQDjgxg2uL+GzPRkygGULPMLzcYWncEI7wbaizvho=
github.com/ethereum/go-ethereum v1.17.1/go.mod h1:7UWOVHL7K3b8RfVRea022btnzLCaanwHtBuH1jUCH/I=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=

View File

@@ -6,7 +6,6 @@ option go_package = "github.com/tech/sendico/pkg/proto/payments/quotation/v2;quo
import "api/proto/payments/shared/v1/shared.proto";
import "api/proto/common/money/v1/money.proto";
import "api/proto/common/fx/v1/fx.proto";
import "api/proto/common/payment/v1/settlement.proto";
import "api/proto/payments/endpoint/v1/endpoint.proto";
import "api/proto/payments/quotation/v2/interface.proto";
@@ -20,7 +19,7 @@ message QuoteIntent {
payments.quotation.v2.FeeTreatment fee_treatment = 5;
string settlement_currency = 6;
string comment = 7;
common.fx.v1.Side fx_side = 8;
payments.shared.v1.FXIntent fx = 8;
}
// QuotePaymentRequest is the request to quote a single v2 payment.

View File

@@ -39,8 +39,7 @@ class PaymentProvider extends ChangeNotifier {
String? clientPaymentRef,
Map<String, String>? metadata,
}) async {
if (!_organization.isOrganizationSet)
throw StateError('Organization is not set');
if (!_organization.isOrganizationSet) throw StateError('Organization is not set');
final quoteRef = _quotation.quotation?.quoteRef;
if (quoteRef == null || quoteRef.isEmpty) {
throw StateError('Quotation reference is not set');

View File

@@ -98,7 +98,7 @@ class QuotationIntentBuilder {
}
return FxIntent(
pair: CurrencyPair(base: base, quote: quote),
side: FxSide.buyBaseSellQuote,
side: FxSide.sellBaseBuyQuote,
);
}

View File

View File

@@ -1,5 +0,0 @@
pan;first_name;last_name;exp_month;exp_year;amount
2204310159722456;Anastasiia;Limonova;06;2028;100
2204320167919754;Anastasiia;Limonova;10;2027;100
2200242558874568;Anastasiia;Limonova;07;2032;100
2203410113188371;Vladimir;Burmakin;02;2033;100
1 pan first_name last_name exp_month exp_year amount
2 2204310159722456 Anastasiia Limonova 06 2028 100
3 2204320167919754 Anastasiia Limonova 10 2027 100
4 2200242558874568 Anastasiia Limonova 07 2032 100
5 2203410113188371 Vladimir Burmakin 02 2033 100