From c217308194f70c2fb313b892cb649cce14fda169 Mon Sep 17 00:00:00 2001 From: BuzzLightyear <11892559+swift1337@users.noreply.github.com> Date: Sun, 23 Jul 2023 17:27:06 +0300 Subject: [PATCH 1/4] =?UTF-8?q?feat(bsc):=20Support=20for=20BNB=20Chain=20?= =?UTF-8?q?(ex=20Binance=20Smart=20Chain)=20=F0=9F=9A=80=20(#11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update README.md * Add support for BSC wallets in KMS * Add BNB coin. Subscribe to Tatum's notifications * Support for incoming BNB payments. - Add BNB icon - Add support for chain whose coin and chain name are different (BSC has BNB native coin) - Add test cases for incoming BNB payments * Fix QR code generation for BNB. Fix tatum webhook parsing * Support transaction receipts for BSC * Implement transaction creation for BSC * Implement internal transfers for BSC blockchain * Add BNB logo in readme * Support BSC Withdrawal addresses * Fix withdrawal fees for BSC * Support BSC withdrawals; add BSC explorer links --- Makefile | 2 +- README.md | 8 +- api/proto/kms/kms-v1.yml | 3 + api/proto/kms/v1/wallet.yml | 41 ++- api/proto/merchant/v1/merchant_address.yml | 4 +- internal/db/repository/helpers.go | 9 + internal/kms/api/handler.go | 42 +++ internal/kms/api/handler_test.go | 83 ++++++ internal/kms/app.go | 1 + internal/kms/wallet/service.go | 31 ++- internal/kms/wallet/wallet.go | 7 +- internal/money/money.go | 4 + internal/provider/tatum/provider.go | 5 +- internal/provider/tatum/provider_rpc.go | 30 ++- internal/scheduler/handler.go | 8 +- internal/scheduler/handler_test.go | 2 +- internal/server/http/internalapi/scheduler.go | 39 ++- internal/server/http/internalapi/wallet.go | 12 +- .../server/http/merchantapi/address_test.go | 7 + .../http/merchantapi/withdrawal_test.go | 7 +- internal/service/blockchain/currencies.go | 23 +- internal/service/blockchain/currencies.json | 12 + .../service/blockchain/service_broadcaster.go | 51 ++-- internal/service/blockchain/service_fees.go | 115 ++++++++- internal/service/merchant/service_address.go | 10 +- .../service/payment/service_withdrawal.go | 2 +- internal/service/processing/service.go | 6 +- .../service/processing/service_incoming.go | 7 +- .../processing/service_incoming_test.go | 84 +++++- .../service/processing/service_internal.go | 8 +- .../processing/service_internal_test.go | 129 ++++++++++ .../service/processing/service_webhook.go | 4 +- .../service/processing/service_withdrawal.go | 2 +- .../processing/service_withdrawal_test.go | 207 +++++++++++++++ internal/service/transaction/service.go | 9 +- .../service/transaction/service_update.go | 2 +- internal/service/wallet/service.go | 1 - internal/service/wallet/service_balance.go | 4 + .../service/wallet/service_transaction.go | 35 +++ internal/test/fakes/fees.go | 28 ++ internal/test/integration_kms.go | 1 + internal/test/mocks_kms.go | 29 +++ internal/test/must.go | 7 + .../model/create_merchant_address_request.go | 7 +- .../v1/model/merchant_address.go | 7 +- .../create_b_s_c_transaction_parameters.go | 172 +++++++++++++ .../create_b_s_c_transaction_responses.go | 107 ++++++++ pkg/api-kms/v1/client/wallet/wallet_client.go | 52 +++- pkg/api-kms/v1/mock/ClientOption.go | 11 +- pkg/api-kms/v1/mock/ClientService.go | 74 +++++- pkg/api-kms/v1/model/b_s_c_transaction.go | 51 ++++ pkg/api-kms/v1/model/blockchain.go | 5 +- .../model/create_b_s_c_transaction_request.go | 239 ++++++++++++++++++ ui-dashboard/src/assets/icons/crypto/bnb.svg | 20 ++ ui-dashboard/src/types/index.ts | 20 +- ui-payment/src/assets/icons/crypto/bnb.svg | 20 ++ 56 files changed, 1722 insertions(+), 184 deletions(-) create mode 100644 pkg/api-kms/v1/client/wallet/create_b_s_c_transaction_parameters.go create mode 100644 pkg/api-kms/v1/client/wallet/create_b_s_c_transaction_responses.go create mode 100644 pkg/api-kms/v1/model/b_s_c_transaction.go create mode 100644 pkg/api-kms/v1/model/create_b_s_c_transaction_request.go create mode 100644 ui-dashboard/src/assets/icons/crypto/bnb.svg create mode 100644 ui-payment/src/assets/icons/crypto/bnb.svg diff --git a/Makefile b/Makefile index 915bbca..b74dba6 100644 --- a/Makefile +++ b/Makefile @@ -73,7 +73,7 @@ require-deps: ## Require cli tools for development go install github.com/rubenv/sql-migrate/...@latest go install github.com/kyleconroy/sqlc/cmd/sqlc@latest go install github.com/cespare/reflex@latest - go install github.com/vektra/mockery/v2@latest + go install github.com/vektra/mockery/v2@v2.32.0 go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.53.3 # todo go-swagger as swagger diff --git a/README.md b/README.md index 62008cb..c70d20a 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,10 @@ Accept ETH, MATIC, TRON, USDT, and USDC with ease. Open new opportunities for yo tron
TRON
+ + bnb +
BNB
+ usdt
USDT
@@ -52,7 +56,7 @@ Accept ETH, MATIC, TRON, USDT, and USDC with ease. Open new opportunities for yo ## Documentation 📚 -Visit [docs.o2pay.co](https://docs.o2pay.co) for setup guides. +Visit [docs.o2pay.co](https://docs.o2pay.co) for setup guides. If you have any questions, feel free to ask them in our [telegram community](https://t.me/oxygenpay_en) ## Roadmap 🛣️ @@ -67,4 +71,4 @@ Visit [docs.o2pay.co](https://docs.o2pay.co) for setup guides. ## License 📑 -This software is licensed under [Apache License 2.0](./LICENSE). \ No newline at end of file +This software is licensed under [Apache License 2.0](./LICENSE). diff --git a/api/proto/kms/kms-v1.yml b/api/proto/kms/kms-v1.yml index 98740fe..bbf26a0 100644 --- a/api/proto/kms/kms-v1.yml +++ b/api/proto/kms/kms-v1.yml @@ -22,6 +22,9 @@ paths: /wallet/{walletId}/transaction/matic: $ref: './v1/wallet.yml#/paths/~1wallet~1{walletId}~1transaction~1matic' + /wallet/{walletId}/transaction/bsc: + $ref: './v1/wallet.yml#/paths/~1wallet~1{walletId}~1transaction~1bsc' + /wallet/{walletId}/transaction/tron: $ref: './v1/wallet.yml#/paths/~1wallet~1{walletId}~1transaction~1tron' diff --git a/api/proto/kms/v1/wallet.yml b/api/proto/kms/v1/wallet.yml index da509a5..9b2a537 100644 --- a/api/proto/kms/v1/wallet.yml +++ b/api/proto/kms/v1/wallet.yml @@ -75,6 +75,8 @@ definitions: CreateMaticTransactionRequest: *createEthTransaction + CreateBSCTransactionRequest: *createEthTransaction + CreateTronTransactionRequest: type: object required: [ assetType, recipient, amount ] @@ -114,7 +116,7 @@ definitions: Blockchain: type: string description: Supported blockchain - enum: [ BTC, ETH, TRON, MATIC ] + enum: [ BTC, ETH, TRON, MATIC, BSC ] x-nullable: false x-omitempty: false @@ -147,7 +149,7 @@ definitions: description: Created At example: 1656696522 - EthereumTransaction: + EthereumTransaction: ðTransaction type: object properties: rawTransaction: @@ -157,15 +159,9 @@ definitions: x-nullable: false x-omitempty: false - MaticTransaction: - type: object - properties: - rawTransaction: - type: string - description: RLP-encoded transaction - example: '0xf86e83014b2985048ccb44b1827530944675c7e5baafbffbca748158becba61ef3b0a26387c2a454bcf91b3f8026a0db0be3dcc25213b286e08d018fe8143eb85a3b7bb5cf3749245e907158e9c8daa033c7ec9362ee890d63b89e9dbfcfcb6edd9432321102c1d2ea7921c6cc07009e' - x-nullable: false - x-omitempty: false + MaticTransaction: *ethTransaction + + BSCTransaction: *ethTransaction TronTransaction: type: object @@ -292,6 +288,29 @@ paths: schema: $ref: '../kms-v1.yml#/definitions/ErrorResponse' + /wallet/{walletId}/transaction/bsc: + post: + summary: Create BSC Transaction + operationId: createBSCTransaction + tags: [ Wallet ] + parameters: + - $ref: '#/parameters/WalletId' + - in: body + name: data + required: true + schema: + $ref: '#/definitions/CreateBSCTransactionRequest' + responses: + 201: + description: Transaction Created + schema: + $ref: '#/definitions/BSCTransaction' + 400: + description: Validation error / Not found + schema: + $ref: '../kms-v1.yml#/definitions/ErrorResponse' + + /wallet/{walletId}/transaction/tron: post: summary: Create Tron Transaction diff --git a/api/proto/merchant/v1/merchant_address.yml b/api/proto/merchant/v1/merchant_address.yml index 983936e..a33fe5b 100644 --- a/api/proto/merchant/v1/merchant_address.yml +++ b/api/proto/merchant/v1/merchant_address.yml @@ -18,7 +18,7 @@ definitions: properties: blockchain: type: string - enum: [ BTC, ETH, TRON, MATIC ] + enum: [ BTC, ETH, TRON, MATIC, BSC ] example: ETH x-nullable: false address: @@ -68,7 +68,7 @@ definitions: x-omitempty: false blockchain: type: string - enum: [ ETH, TRON, MATIC ] + enum: [ ETH, TRON, MATIC, BSC ] example: ETH x-nullable: false x-omitempty: false diff --git a/internal/db/repository/helpers.go b/internal/db/repository/helpers.go index ea2a39f..b3ac667 100644 --- a/internal/db/repository/helpers.go +++ b/internal/db/repository/helpers.go @@ -96,6 +96,15 @@ func NumericToMoney(num pgtype.Numeric, moneyType money.Type, ticker string, dec return money.NewFromBigInt(moneyType, ticker, bigInt, decimals) } +func NumericToCrypto(num pgtype.Numeric, currency money.CryptoCurrency) (money.Money, error) { + bigInt, err := NumericToBigInt(num) + if err != nil { + return money.Money{}, err + } + + return currency.MakeAmountFromBigInt(bigInt) +} + func MoneyToNumeric(m money.Money) pgtype.Numeric { bigInt, _ := m.BigInt() return BigIntToNumeric(bigInt) diff --git a/internal/kms/api/handler.go b/internal/kms/api/handler.go index c7547f9..e98b076 100644 --- a/internal/kms/api/handler.go +++ b/internal/kms/api/handler.go @@ -29,6 +29,7 @@ func SetupRoutes(handler *Handler) httpServer.Opt { kmsAPI.POST("/wallet/:walletId/transaction/eth", handler.CreateEthereumTransaction) kmsAPI.POST("/wallet/:walletId/transaction/matic", handler.CreateMaticTransaction) + kmsAPI.POST("/wallet/:walletId/transaction/bsc", handler.CreateBSCTransaction) kmsAPI.POST("/wallet/:walletId/transaction/tron", handler.CreateTronTransaction) } } @@ -180,6 +181,47 @@ func (h *Handler) CreateMaticTransaction(c echo.Context) error { return c.JSON(http.StatusCreated, &model.EthereumTransaction{RawTransaction: raw}) } +func (h *Handler) CreateBSCTransaction(c echo.Context) error { + ctx := c.Request().Context() + + id, err := common.UUID(c, paramWalletID) + if err != nil { + return err + } + + w, err := h.wallets.GetWallet(ctx, id, false) + + switch { + case errors.Is(err, wallet.ErrNotFound): + return common.NotFoundResponse(c, wallet.ErrNotFound.Error()) + case err != nil: + return err + } + + var req model.CreateBSCTransactionRequest + if valid := common.BindAndValidateRequest(c, &req); !valid { + return nil + } + + raw, err := h.wallets.CreateBSCTransaction(ctx, w, wallet.EthTransactionParams{ + Type: wallet.AssetType(req.AssetType), + Recipient: req.Recipient, + ContractAddress: req.ContractAddress, + Amount: req.Amount, + NetworkID: req.NetworkID, + Nonce: *req.Nonce, + MaxPriorityFeePerGas: req.MaxPriorityPerGas, + MaxFeePerGas: req.MaxFeePerGas, + Gas: req.Gas, + }) + + if err != nil { + return transactionCreationFailed(c, err) + } + + return c.JSON(http.StatusCreated, &model.EthereumTransaction{RawTransaction: raw}) +} + func (h *Handler) CreateTronTransaction(c echo.Context) error { ctx := c.Request().Context() diff --git a/internal/kms/api/handler_test.go b/internal/kms/api/handler_test.go index eb4476d..3220970 100644 --- a/internal/kms/api/handler_test.go +++ b/internal/kms/api/handler_test.go @@ -24,6 +24,7 @@ func TestHandlerRoutes(t *testing.T) { walletRoute = "/api/kms/v1/wallet/:walletId" ethereumTransactionRoute = "/api/kms/v1/wallet/:walletId/transaction/eth" polygonTransactionRoute = "/api/kms/v1/wallet/:walletId/transaction/matic" + bscTransactionRoute = "/api/kms/v1/wallet/:walletId/transaction/bsc" tronTransactionRoute = "/api/kms/v1/wallet/:walletId/transaction/tron" ) @@ -240,6 +241,88 @@ func TestHandlerRoutes(t *testing.T) { } }) + t.Run("CreateBSCTransaction", func(t *testing.T) { + const usdtContract = "0xdac17f958d2ee523a2206206994597c13d831ec7" + + for testCaseIndex, testCase := range []struct { + wallet *wallet.Wallet + req model.CreateBSCTransactionRequest + assert func(t *testing.T, res *test.Response) + }{ + { + wallet: createWallet(wallet.BSC), + req: model.CreateBSCTransactionRequest{ + AssetType: "coin", + Amount: "123", + Gas: 1, + MaxFeePerGas: "123", + MaxPriorityPerGas: "456", + NetworkID: 1, + Nonce: util.Ptr(int64(0)), + Recipient: "0x690b9a9e9aa1c9db991c7721a92d351db4fac990", + }, + assert: func(t *testing.T, res *test.Response) { + var body model.BSCTransaction + + assert.Equal(t, http.StatusCreated, res.StatusCode(), res.String()) + assert.NoError(t, res.JSON(&body)) + assert.NotEmpty(t, body.RawTransaction) + }, + }, + { + wallet: createWallet(wallet.BSC), + req: model.CreateBSCTransactionRequest{ + AssetType: "token", + Amount: "123", + ContractAddress: usdtContract, + Gas: 1, + MaxFeePerGas: "123", + MaxPriorityPerGas: "456", + NetworkID: 5, + Nonce: util.Ptr(int64(0)), + Recipient: "0x690b9a9e9aa1c9db991c7721a92d351db4fac990", + }, + assert: func(t *testing.T, res *test.Response) { + var body model.BSCTransaction + + assert.Equal(t, http.StatusCreated, res.StatusCode(), res.String()) + assert.NoError(t, res.JSON(&body)) + assert.NotEmpty(t, body.RawTransaction) + }, + }, + { + // blockchain mismatch + wallet: createWallet(wallet.ETH), + req: model.CreateBSCTransactionRequest{ + AssetType: "coin", + Amount: "123", + Gas: 1, + MaxFeePerGas: "123", + MaxPriorityPerGas: "456", + NetworkID: 1, + Nonce: util.Ptr(int64(0)), + Recipient: "0x690b9a9e9aa1c9db991c7721a92d351db4fac990", + }, + assert: func(t *testing.T, res *test.Response) { + assert.Equal(t, http.StatusBadRequest, res.StatusCode(), res.String()) + }, + }, + } { + t.Run(strconv.Itoa(testCaseIndex+1), func(t *testing.T) { + // ACT + res := tc.Client. + POST(). + Path(bscTransactionRoute). + Param(paramWalletID, testCase.wallet.UUID.String()). + JSON(&testCase.req). + Do() + + // ASSERT + testCase.assert(t, res) + }) + } + }) + t.Run("CreateTronTransaction", func(t *testing.T) { const usdtContract = "TBnt7Wzvd226i24r95pE82MZpHba63ehQY" diff --git a/internal/kms/app.go b/internal/kms/app.go index 0e01535..8c40de1 100644 --- a/internal/kms/app.go +++ b/internal/kms/app.go @@ -63,6 +63,7 @@ func (app *App) runWebServer(ctx context.Context) { walletGenerator := wallet.NewGenerator(). AddProvider(&wallet.EthProvider{Blockchain: wallet.ETH, CryptoReader: cryptorand.Reader}). AddProvider(&wallet.EthProvider{Blockchain: wallet.MATIC, CryptoReader: cryptorand.Reader}). + AddProvider(&wallet.EthProvider{Blockchain: wallet.BSC, CryptoReader: cryptorand.Reader}). AddProvider(&wallet.BitcoinProvider{Blockchain: wallet.BTC, CryptoReader: cryptorand.Reader}). AddProvider(&wallet.TronProvider{ Blockchain: wallet.TRON, diff --git a/internal/kms/wallet/service.go b/internal/kms/wallet/service.go index 40be226..8b71e73 100644 --- a/internal/kms/wallet/service.go +++ b/internal/kms/wallet/service.go @@ -34,11 +34,7 @@ var ( ErrUnknownBlockchain = errors.New("unknown blockchain") ) -func New( - repo *Repository, - generator *Generator, - logger *zerolog.Logger, -) *Service { +func New(repo *Repository, generator *Generator, logger *zerolog.Logger) *Service { log := logger.With().Str("channel", "kms_service").Logger() return &Service{ @@ -78,9 +74,7 @@ func (s *Service) DeleteWallet(ctx context.Context, id uuid.UUID) error { } // CreateEthereumTransaction creates and sings new raw Ethereum transaction based on provided input. -func (s *Service) CreateEthereumTransaction( - _ context.Context, wallet *Wallet, params EthTransactionParams, -) (string, error) { +func (s *Service) CreateEthereumTransaction(_ context.Context, wt *Wallet, params EthTransactionParams) (string, error) { if _, ok := s.generator.providers[ETH]; !ok { return "", errors.New("ETH provider not found") } @@ -90,12 +84,10 @@ func (s *Service) CreateEthereumTransaction( return "", errors.New("ETH provider is invalid") } - return eth.NewTransaction(wallet, params) + return eth.NewTransaction(wt, params) } -func (s *Service) CreateMaticTransaction( - _ context.Context, wallet *Wallet, params EthTransactionParams, -) (string, error) { +func (s *Service) CreateMaticTransaction(_ context.Context, wt *Wallet, params EthTransactionParams) (string, error) { if _, ok := s.generator.providers[MATIC]; !ok { return "", errors.New("MATIC provider not found") } @@ -105,7 +97,20 @@ func (s *Service) CreateMaticTransaction( return "", errors.New("MATIC provider is invalid") } - return matic.NewTransaction(wallet, params) + return matic.NewTransaction(wt, params) +} + +func (s *Service) CreateBSCTransaction(_ context.Context, wt *Wallet, params EthTransactionParams) (string, error) { + if _, ok := s.generator.providers[BSC]; !ok { + return "", errors.New("BSC provider not found") + } + + bsc, ok := s.generator.providers[BSC].(*EthProvider) + if !ok { + return "", errors.New("BSC provider is invalid") + } + + return bsc.NewTransaction(wt, params) } func (s *Service) CreateTronTransaction( diff --git a/internal/kms/wallet/wallet.go b/internal/kms/wallet/wallet.go index 0d68074..e8f3fe5 100644 --- a/internal/kms/wallet/wallet.go +++ b/internal/kms/wallet/wallet.go @@ -16,9 +16,10 @@ const ( ETH Blockchain = "ETH" TRON Blockchain = "TRON" MATIC Blockchain = "MATIC" + BSC Blockchain = "BSC" ) -var blockchains = []Blockchain{BTC, ETH, TRON, MATIC} +var blockchains = []Blockchain{BTC, ETH, TRON, MATIC, BSC} func ListBlockchains() []Blockchain { result := make([]Blockchain, len(blockchains)) @@ -68,9 +69,7 @@ func ValidateAddress(blockchain Blockchain, address string) error { switch blockchain { case BTC: isValid = validateBitcoinAddress(address) - case ETH: - isValid = validateEthereumAddress(address) - case MATIC: + case ETH, MATIC, BSC: isValid = validateEthereumAddress(address) case TRON: isValid = validateTronAddress(address) diff --git a/internal/money/money.go b/internal/money/money.go index 9982c5a..7495751 100644 --- a/internal/money/money.go +++ b/internal/money/money.go @@ -110,6 +110,10 @@ func (c CryptoCurrency) MakeAmount(raw string) (Money, error) { return CryptoFromRaw(c.Ticker, raw, c.Decimals) } +func (c CryptoCurrency) MakeAmountFromBigInt(amount *big.Int) (Money, error) { + return NewFromBigInt(Crypto, c.Ticker, amount, c.Decimals) +} + // MONEY ------------------ type Type string diff --git a/internal/provider/tatum/provider.go b/internal/provider/tatum/provider.go index e2abc32..8b03ae2 100644 --- a/internal/provider/tatum/provider.go +++ b/internal/provider/tatum/provider.go @@ -25,8 +25,7 @@ type Config struct { TestAPIKey string `yaml:"test_api_key" env:"TATUM_TEST_API_KEY" env-description:"Tatum Test API Key"` HMACSecret string `yaml:"tatum_hmac_secret" env:"TATUM_HMAC_SECRET" env-description:"Tatum HMAC Secret. Use any random string with 8+ chars"` - // HMACForceSet will make "set hmac set" request on every service start. - // Useful if HMAC secret was changed. + // HMACForceSet will make "set hmac set" request on every service start. Useful if HMAC secret was changed. HMACForceSet bool `yaml:"tatum_hmac_force_set" env:"TATUM_HMAC_FORCE_SET" env-description:"Internal variable"` } @@ -102,7 +101,7 @@ type SubscriptionResponse struct { ID string `json:"id"` } -// SubscribeToWebhook fcking auto-generated sdk throws an error on this request, so it's rewritten manually. +// SubscribeToWebhook auto-generated sdk throws an error on this request, so it's rewritten manually. func (p *Provider) SubscribeToWebhook(ctx context.Context, params SubscriptionParams) (string, error) { url := fmt.Sprintf("%s/v3/subscription", p.config.BasePath) diff --git a/internal/provider/tatum/provider_rpc.go b/internal/provider/tatum/provider_rpc.go index 7b4473a..1cb29df 100644 --- a/internal/provider/tatum/provider_rpc.go +++ b/internal/provider/tatum/provider_rpc.go @@ -3,28 +3,34 @@ package tatum import ( "context" "fmt" + "strings" "github.com/ethereum/go-ethereum/ethclient" ) func (p *Provider) EthereumRPC(ctx context.Context, isTest bool) (*ethclient.Client, error) { - const path = "v3/blockchain/node/ETH" - - url := fmt.Sprintf("%s/%s/%s", p.config.BasePath, path, p.config.APIKey) - if isTest { - url = fmt.Sprintf("%s/%s/%s?testnetType=%s", p.config.BasePath, path, p.config.TestAPIKey, EthTestnet) - } - - return ethclient.DialContext(ctx, url) + return ethclient.DialContext(ctx, p.rpcPath("v3/blockchain/node/ETH", isTest)) } func (p *Provider) MaticRPC(ctx context.Context, isTest bool) (*ethclient.Client, error) { - const path = "v3/blockchain/node/MATIC" + return ethclient.DialContext(ctx, p.rpcPath("v3/blockchain/node/MATIC", isTest)) +} +func (p *Provider) BinanceSmartChainRPC(ctx context.Context, isTest bool) (*ethclient.Client, error) { + return ethclient.DialContext(ctx, p.rpcPath("v3/blockchain/node/BSC", isTest)) +} + +func (p *Provider) rpcPath(path string, isTest bool) string { url := fmt.Sprintf("%s/%s/%s", p.config.BasePath, path, p.config.APIKey) - if isTest { - url = fmt.Sprintf("%s/%s/%s", p.config.BasePath, path, p.config.TestAPIKey) + if !isTest { + return url + } + + url = fmt.Sprintf("%s/%s/%s", p.config.BasePath, path, p.config.TestAPIKey) + + if strings.HasSuffix(path, "ETH") { + url += "?testnetType=" + EthTestnet } - return ethclient.DialContext(ctx, url) + return url } diff --git a/internal/scheduler/handler.go b/internal/scheduler/handler.go index c7548b5..519d55b 100644 --- a/internal/scheduler/handler.go +++ b/internal/scheduler/handler.go @@ -93,7 +93,7 @@ func (h *Handler) PerformInternalWalletTransfer(ctx context.Context) error { logger := zerolog.Ctx(ctx) // 1. Ensure outbound wallets exist in DB - if err := h.ensureOutboundWallets(ctx, logger); err != nil { + if err := h.EnsureOutboundWallets(ctx); err != nil { return errors.Wrap(err, "unable to ensure outbound wallets") } @@ -224,7 +224,7 @@ func (h *Handler) PerformWithdrawalsCreation(ctx context.Context) error { logger := zerolog.Ctx(ctx) // 1. Ensure outbound wallets exist in DB - if err := h.ensureOutboundWallets(ctx, logger); err != nil { + if err := h.EnsureOutboundWallets(ctx); err != nil { return errors.Wrap(err, "unable to ensure outbound wallets") } @@ -264,7 +264,9 @@ func (h *Handler) PerformWithdrawalsCreation(ctx context.Context) error { return nil } -func (h *Handler) ensureOutboundWallets(ctx context.Context, logger *zerolog.Logger) error { +func (h *Handler) EnsureOutboundWallets(ctx context.Context) error { + logger := zerolog.Ctx(ctx) + group, ctx := errgroup.WithContext(ctx) group.SetLimit(4) diff --git a/internal/scheduler/handler_test.go b/internal/scheduler/handler_test.go index 1ebc73d..49d98e5 100644 --- a/internal/scheduler/handler_test.go +++ b/internal/scheduler/handler_test.go @@ -180,7 +180,7 @@ func TestScheduler(t *testing.T) { // Check job logs // "fetched inbound wallets" + "matched inbound balances" + "created internal transactions" tc.AssertTableRows(t, "job_logs", 3) - tc.AssertTableRows(t, "wallets", 3) + tc.AssertTableRows(t, "wallets", 4) // Check that duplicate outbound wallet duplicate creation is not possible tc.SetupCreateWalletWithSubscription("ETH", "0x2222", "0x123-pub-key") diff --git a/internal/server/http/internalapi/scheduler.go b/internal/server/http/internalapi/scheduler.go index dc3984f..ab21c3c 100644 --- a/internal/server/http/internalapi/scheduler.go +++ b/internal/server/http/internalapi/scheduler.go @@ -23,40 +23,31 @@ func (h *Handler) RunSchedulerJob(c echo.Context) error { return nil } - allJobs := []string{ - "performInternalWalletTransfer", - "checkInternalTransferProgress", - "performWithdrawalsCreation", - "checkWithdrawalsProgress", - "cancelExpiredPayments", - } - jobID := fmt.Sprintf("%s-web-%d", req.Job, time.Now().UTC().Unix()) ctx = context.WithValue(ctx, scheduler.ContextJobID{}, jobID) ctx = h.logger.WithContext(ctx) - var errJob error - switch req.Job { - case "checkIncomingTransactionsProgress": - errJob = h.scheduler.CheckIncomingTransactionsProgress(ctx) - case "performInternalWalletTransfer": - errJob = h.scheduler.PerformInternalWalletTransfer(ctx) - case "checkInternalTransferProgress": - errJob = h.scheduler.CheckInternalTransferProgress(ctx) - case "performWithdrawalsCreation": - errJob = h.scheduler.PerformWithdrawalsCreation(ctx) - case "checkWithdrawalsProgress": - errJob = h.scheduler.CheckWithdrawalsProgress(ctx) - case "cancelExpiredPayments": - errJob = h.scheduler.CancelExpiredPayments(ctx) - default: + jobs := map[string]func(context.Context) error{ + "checkIncomingTransactionsProgress": h.scheduler.CheckIncomingTransactionsProgress, + "performInternalWalletTransfer": h.scheduler.PerformInternalWalletTransfer, + "checkInternalTransferProgress": h.scheduler.CheckInternalTransferProgress, + "performWithdrawalsCreation": h.scheduler.PerformWithdrawalsCreation, + "checkWithdrawalsProgress": h.scheduler.CheckWithdrawalsProgress, + "cancelExpiredPayments": h.scheduler.CancelExpiredPayments, + "ensureOutboundWallets": h.scheduler.EnsureOutboundWallets, + } + + job, exists := jobs[req.Job] + if !exists { return common.ValidationErrorResponse(c, fmt.Sprintf( "job %s not found. Available jobs: %s", req.Job, - strings.Join(allJobs, ", "), + strings.Join(util.Keys(jobs), ", "), )) } + errJob := job(ctx) + logs, err := h.scheduler.JobLogger().ListByJobID(ctx, jobID, 1000) if err != nil { return common.ErrorResponse(c, err.Error()) diff --git a/internal/server/http/internalapi/wallet.go b/internal/server/http/internalapi/wallet.go index 42fb55a..1ad528b 100644 --- a/internal/server/http/internalapi/wallet.go +++ b/internal/server/http/internalapi/wallet.go @@ -79,7 +79,7 @@ func (h *Handler) CalculateTransactionFee(c echo.Context) error { return common.ErrorResponse(c, err.Error()) } - baseCurrency, err := h.blockchain.GetCurrencyByTicker(currency.Blockchain.String()) + baseCurrency, err := h.blockchain.GetNativeCoin(currency.Blockchain) if err != nil { return common.ErrorResponse(c, err.Error()) } @@ -98,12 +98,14 @@ func (h *Handler) CalculateTransactionFee(c echo.Context) error { return c.JSON(http.StatusOK, v) } - switch currency.Blockchain { - case kms.ETH.ToMoneyBlockchain(): + switch kms.Blockchain(currency.Blockchain) { + case kms.ETH: return response(fee.ToEthFee()) - case kms.MATIC.ToMoneyBlockchain(): + case kms.MATIC: return response(fee.ToMaticFee()) - case kms.TRON.ToMoneyBlockchain(): + case kms.BSC: + return response(fee.ToBSCFee()) + case kms.TRON: return response(fee.ToTronFee()) } diff --git a/internal/server/http/merchantapi/address_test.go b/internal/server/http/merchantapi/address_test.go index c243e45..e901238 100644 --- a/internal/server/http/merchantapi/address_test.go +++ b/internal/server/http/merchantapi/address_test.go @@ -109,6 +109,13 @@ func TestAddressRoutes(t *testing.T) { Address: "0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5", }, }, + { + req: model.CreateMerchantAddressRequest{ + Name: "A3", + Blockchain: string(kmswallet.BSC), + Address: "0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5", + }, + }, { req: model.CreateMerchantAddressRequest{ Name: "A4", diff --git a/internal/server/http/merchantapi/withdrawal_test.go b/internal/server/http/merchantapi/withdrawal_test.go index 1b4b745..41bb64b 100644 --- a/internal/server/http/merchantapi/withdrawal_test.go +++ b/internal/server/http/merchantapi/withdrawal_test.go @@ -359,7 +359,7 @@ func TestWithdrawalRoutes(t *testing.T) { ) require.NoError(t, err) - baseCurrency, err := tc.Services.Blockchain.GetCurrencyByTicker(currency.Blockchain.String()) + baseCurrency, err := tc.Services.Blockchain.GetNativeCoin(currency.Blockchain) require.NoError(t, err) tc.Providers.TatumMock.SetupRates(currency.Ticker, money.USD, 2) @@ -405,6 +405,11 @@ func TestWithdrawalRoutes(t *testing.T) { expectedFeeUSD: "0.01", expectedFeeCrypto: "0.005", }, + { + balance: makeBalance(asset("BNB"), false, usd(0.02)), + expectedFeeUSD: "0.02", + expectedFeeCrypto: "0.01", + }, { // in testnets money "cost" $0 balance: makeBalance(asset("TRON"), true, usd(1.5)), diff --git a/internal/service/blockchain/currencies.go b/internal/service/blockchain/currencies.go index 078d5de..10c00a2 100644 --- a/internal/service/blockchain/currencies.go +++ b/internal/service/blockchain/currencies.go @@ -10,6 +10,7 @@ import ( "strings" "sync" + kms "github.com/oxygenpay/oxygen/internal/kms/wallet" "github.com/oxygenpay/oxygen/internal/money" "github.com/oxygenpay/oxygen/internal/util" "github.com/pkg/errors" @@ -19,6 +20,7 @@ type Resolver interface { ListSupportedCurrencies() []money.CryptoCurrency ListBlockchainCurrencies(blockchain money.Blockchain) []money.CryptoCurrency GetCurrencyByTicker(ticker string) (money.CryptoCurrency, error) + GetNativeCoin(blockchain money.Blockchain) (money.CryptoCurrency, error) GetCurrencyByBlockchainAndContract(bc money.Blockchain, networkID, addr string) (money.CryptoCurrency, error) GetMinimalWithdrawalByTicker(ticker string) (money.Money, error) GetUSDMinimalInternalTransferByTicker(ticker string) (money.Money, error) @@ -58,6 +60,19 @@ func (r *CurrencyResolver) GetCurrencyByTicker(ticker string) (money.CryptoCurre return c, nil } +// GetNativeCoin returns native coin by blockchain. Example: ETH -> ETH; BSC -> BNB. +func (r *CurrencyResolver) GetNativeCoin(chain money.Blockchain) (money.CryptoCurrency, error) { + list := r.ListBlockchainCurrencies(chain) + + for i := range list { + if list[i].Type == money.Coin { + return list[i], nil + } + } + + return money.CryptoCurrency{}, ErrCurrencyNotFound +} + // GetMinimalWithdrawalByTicker returns minimal withdrawal amount in USD for selected ticker. func (r *CurrencyResolver) GetMinimalWithdrawalByTicker(ticker string) (money.Money, error) { r.mu.RLock() @@ -290,10 +305,10 @@ func DefaultSetup(s *CurrencyResolver) error { } func CreatePaymentLink(addr string, currency money.CryptoCurrency, amount money.Money, isTest bool) (string, error) { - switch currency.Blockchain { - case "ETH", "MATIC": + switch kms.Blockchain(currency.Blockchain) { + case kms.ETH, kms.MATIC, kms.BSC: return ethPaymentLink(addr, currency, amount, isTest), nil - case "TRON": + case kms.TRON: return tronPaymentLink(addr, currency, amount, isTest), nil } @@ -331,6 +346,8 @@ var explorers = map[string]string{ "ETH/5": "https://goerli.etherscan.io/tx/%s", "MATIC/137": "https://polygonscan.com/tx/%s", "MATIC/80001": "https://mumbai.polygonscan.com/tx/%s", + "BSC/56": "https://bscscan.com/tx/%s", + "BSC/97": "https://testnet.bscscan.com/tx/%s", "TRON/mainnet": "https://tronscan.org/#/transaction/%s", "TRON/testnet": "https://shasta.tronscan.org/#/transaction/%s", } diff --git a/internal/service/blockchain/currencies.json b/internal/service/blockchain/currencies.json index 9ac4a78..5fe59a7 100644 --- a/internal/service/blockchain/currencies.json +++ b/internal/service/blockchain/currencies.json @@ -101,5 +101,17 @@ "testNetworkId": "testnet", "minimal_withdrawal_amount_usd": "10", "minimal_instant_internal_transfer_amount_usd": "30" + }, + { + "blockchain": "BSC", + "blockchainName": "BNB Chain", + "ticker": "BNB", + "type": "coin", + "name": "BNB", + "decimals": "18", + "networkId": "56", + "testNetworkId": "97", + "minimal_withdrawal_amount_usd": "10", + "minimal_instant_internal_transfer_amount_usd": "30" } ] \ No newline at end of file diff --git a/internal/service/blockchain/service_broadcaster.go b/internal/service/blockchain/service_broadcaster.go index eebf2d9..3b84ccd 100644 --- a/internal/service/blockchain/service_broadcaster.go +++ b/internal/service/blockchain/service_broadcaster.go @@ -3,6 +3,7 @@ package blockchain import ( "context" "encoding/json" + "fmt" "strconv" "strings" "sync" @@ -35,17 +36,19 @@ func (s *Service) BroadcastTransaction(ctx context.Context, blockchain money.Blo err error ) - switch blockchain { - case kms.ETH.ToMoneyBlockchain(): + switch kms.Blockchain(blockchain) { + case kms.ETH: opts := &client.EthereumApiEthBroadcastOpts{} if isTest { opts.XTestnetType = optional.NewString(tatum.EthTestnet) } txHash, _, err = api.EthereumApi.EthBroadcast(ctx, client.BroadcastKms{TxData: rawTX}, opts) - case kms.MATIC.ToMoneyBlockchain(): + case kms.MATIC: txHash, _, err = api.PolygonApi.PolygonBroadcast(ctx, client.BroadcastKms{TxData: rawTX}) - case kms.TRON.ToMoneyBlockchain(): + case kms.BSC: + txHash, _, err = api.BNBSmartChainApi.BscBroadcast(ctx, client.BroadcastKms{TxData: rawTX}) + case kms.TRON: hashID, errTron := s.providers.Trongrid.BroadcastTransaction(ctx, []byte(rawTX), isTest) if errTron != nil { err = errTron @@ -53,7 +56,7 @@ func (s *Service) BroadcastTransaction(ctx context.Context, blockchain money.Blo txHash.TxId = hashID } default: - return "", ErrCurrencyNotFound + return "", fmt.Errorf("broadcast for %q is not implemented yet", blockchain) } if err != nil { @@ -114,36 +117,45 @@ func (s *Service) getTransactionReceipt( isTest bool, ) (*TransactionReceipt, error) { const ( - ethDecimals = 18 - maticDecimals = 18 - tronDecimals = 6 - ethConfirmations = 12 maticConfirmations = 30 + bscConfirmations = 15 ) - switch blockchain { - case kms.ETH.ToMoneyBlockchain(): + nativeCoin, err := s.GetNativeCoin(blockchain) + if err != nil { + return nil, errors.Wrapf(err, "native coin for %q is not found", blockchain) + } + + switch kms.Blockchain(blockchain) { + case kms.ETH: rpc, err := s.providers.Tatum.EthereumRPC(ctx, isTest) if err != nil { return nil, err } - return s.getEthReceipt(ctx, rpc, kms.ETH.ToMoneyBlockchain(), transactionID, ethDecimals, ethConfirmations, isTest) - case kms.MATIC.ToMoneyBlockchain(): + return s.getEthReceipt(ctx, rpc, nativeCoin, transactionID, ethConfirmations, isTest) + case kms.MATIC: rpc, err := s.providers.Tatum.MaticRPC(ctx, isTest) if err != nil { return nil, err } - return s.getEthReceipt(ctx, rpc, kms.MATIC.ToMoneyBlockchain(), transactionID, maticDecimals, maticConfirmations, isTest) - case kms.TRON.ToMoneyBlockchain(): + return s.getEthReceipt(ctx, rpc, nativeCoin, transactionID, maticConfirmations, isTest) + case kms.BSC: + rpc, err := s.providers.Tatum.BinanceSmartChainRPC(ctx, isTest) + if err != nil { + return nil, err + } + + return s.getEthReceipt(ctx, rpc, nativeCoin, transactionID, bscConfirmations, isTest) + case kms.TRON: receipt, err := s.providers.Trongrid.GetTransactionReceipt(ctx, transactionID, isTest) if err != nil { return nil, errors.Wrap(err, "unable to get tron transaction receipt") } - networkFee, err := money.CryptoFromRaw(blockchain.String(), strconv.Itoa(int(receipt.Fee)), tronDecimals) + networkFee, err := nativeCoin.MakeAmount(strconv.Itoa(int(receipt.Fee))) if err != nil { return nil, errors.Wrap(err, "unable to calculate network fee") } @@ -167,9 +179,8 @@ func (s *Service) getTransactionReceipt( func (s *Service) getEthReceipt( ctx context.Context, rpc *ethclient.Client, - blockchain money.Blockchain, + nativeCoin money.CryptoCurrency, txID string, - decimals int64, requiredConfirmations int64, isTest bool, ) (*TransactionReceipt, error) { @@ -226,7 +237,7 @@ func (s *Service) getEthReceipt( return nil, err } - gasPrice, err := money.NewFromBigInt(money.Crypto, blockchain.String(), receipt.EffectiveGasPrice, decimals) + gasPrice, err := nativeCoin.MakeAmountFromBigInt(receipt.EffectiveGasPrice) if err != nil { return nil, errors.Wrap(err, "unable to construct network fee") } @@ -244,7 +255,7 @@ func (s *Service) getEthReceipt( confirmations := latestBlock - receipt.BlockNumber.Int64() return &TransactionReceipt{ - Blockchain: blockchain, + Blockchain: nativeCoin.Blockchain, IsTest: isTest, Sender: sender.String(), Recipient: tx.To().String(), diff --git a/internal/service/blockchain/service_fees.go b/internal/service/blockchain/service_fees.go index 7bfe2d2..4debbb2 100644 --- a/internal/service/blockchain/service_fees.go +++ b/internal/service/blockchain/service_fees.go @@ -26,12 +26,14 @@ func (s *Service) CalculateFee(ctx context.Context, baseCurrency, currency money return Fee{}, errors.New("invalid arguments") } - switch currency.Blockchain { - case kmswallet.ETH.ToMoneyBlockchain(): + switch kmswallet.Blockchain(currency.Blockchain) { + case kmswallet.ETH: return s.ethFee(ctx, baseCurrency, currency, isTest) - case kmswallet.MATIC.ToMoneyBlockchain(): + case kmswallet.MATIC: return s.maticFee(ctx, baseCurrency, currency, isTest) - case kmswallet.TRON.ToMoneyBlockchain(): + case kmswallet.BSC: + return s.bscFee(ctx, baseCurrency, currency, isTest) + case kmswallet.TRON: return s.tronFee(ctx, baseCurrency, currency, isTest) } @@ -52,14 +54,17 @@ func (s *Service) CalculateWithdrawalFeeUSD( var usdFee money.Money - switch fee.Currency.Blockchain { - case kmswallet.ETH.ToMoneyBlockchain(): + switch kmswallet.Blockchain(fee.Currency.Blockchain) { + case kmswallet.ETH: f, _ := fee.ToEthFee() usdFee = f.totalCostUSD - case kmswallet.MATIC.ToMoneyBlockchain(): + case kmswallet.MATIC: f, _ := fee.ToMaticFee() usdFee = f.totalCostUSD - case kmswallet.TRON.ToMoneyBlockchain(): + case kmswallet.BSC: + f, _ := fee.ToBSCFee() + usdFee = f.totalCostUSD + case kmswallet.TRON: f, _ := fee.ToTronFee() usdFee = f.feeLimitUSD default: @@ -287,6 +292,100 @@ func (s *Service) maticFee(ctx context.Context, baseCurrency, currency money.Cry }), nil } +type BSCFee struct { + GasUnits uint `json:"gasUnits"` + GasPrice string `json:"gasPrice"` + PriorityFee string `json:"priorityFee"` + TotalCostWEI string `json:"totalCostWei"` + TotalCostBNB string `json:"totalCostBNB"` + TotalCostUSD string `json:"totalCostUsd"` + + totalCostUSD money.Money +} + +func (f *Fee) ToBSCFee() (BSCFee, error) { + if fee, ok := f.raw.(BSCFee); ok { + return fee, nil + } + + return BSCFee{}, errors.New("invalid fee type assertion for BSC") +} + +func (s *Service) bscFee(ctx context.Context, baseCurrency, currency money.CryptoCurrency, isTest bool) (Fee, error) { + const ( + gasUnitsForCoin = 21_000 + gasUnitsForToken = 65_000 + + gasConfidentRate = 1.10 + ) + + // 1. Connect to BSC node + client, err := s.providers.Tatum.BinanceSmartChainRPC(ctx, isTest) + if err != nil { + return Fee{}, errors.Wrap(err, "unable to setup RPC") + } + + // 2. Calculate gasPrice + gasPrice, err := client.SuggestGasPrice(ctx) + if err != nil { + return Fee{}, errors.Wrap(err, "unable to suggest gas price") + } + + gasPriceMATIC, err := baseCurrency.MakeAmountFromBigInt(gasPrice) + if err != nil { + return Fee{}, errors.Wrap(err, "unable to make BSC from gas price") + } + + // In order to be confident that tx will be processed, let's multiply price by gasConfidentRate + gasPriceMATICConfident, err := gasPriceMATIC.MultiplyFloat64(gasConfidentRate) + if err != nil { + return Fee{}, errors.Wrap(err, "unable to multiply BSC gas price") + } + + // 3. Calculate priorityFee + priorityFee, err := client.SuggestGasTipCap(ctx) + if err != nil { + return Fee{}, errors.Wrap(err, "unable to suggest BSC gas tip cap") + } + + priorityFeeBSC, err := baseCurrency.MakeAmountFromBigInt(priorityFee) + if err != nil { + return Fee{}, errors.Wrap(err, "unable to suggest make BSC from priorityFee") + } + + // 4. Calculate gasUnits and total cost in WEI + totalFeePerGas, err := gasPriceMATICConfident.Add(priorityFeeBSC) + if err != nil { + return Fee{}, errors.Wrap(err, "unable to calculate total fee per gas") + } + + gasUnits := gasUnitsForCoin + if currency.Type == money.Token { + gasUnits = gasUnitsForToken + } + + totalCost, err := totalFeePerGas.MultiplyFloat64(float64(gasUnits)) + if err != nil { + return Fee{}, errors.Wrap(err, "unable to calculate total tx cost") + } + + conv, err := s.CryptoToFiat(ctx, totalCost, money.USD) + if err != nil { + return Fee{}, errors.Wrap(err, "unable to calculate total cost in USD") + } + + return NewFee(currency, time.Now().UTC(), isTest, BSCFee{ + GasUnits: uint(gasUnits), + GasPrice: gasPriceMATICConfident.StringRaw(), + PriorityFee: priorityFeeBSC.StringRaw(), + TotalCostWEI: totalCost.StringRaw(), + TotalCostBNB: totalCost.String(), + TotalCostUSD: conv.To.String(), + + totalCostUSD: conv.To, + }), nil +} + type TronFee struct { FeeLimitSun uint64 `json:"feeLimit"` FeeLimitTRX string `json:"feeLimitTrx"` diff --git a/internal/service/merchant/service_address.go b/internal/service/merchant/service_address.go index 3839399..e07d622 100644 --- a/internal/service/merchant/service_address.go +++ b/internal/service/merchant/service_address.go @@ -137,10 +137,8 @@ func (s *Service) DeleteMerchantAddress(ctx context.Context, merchantID int64, i } func (s *Service) entryToAddress(entry repository.MerchantAddress) (*Address, error) { - var blockchainName string - if c, err := s.blockchain.GetCurrencyByTicker(entry.Blockchain); err == nil { - blockchainName = c.BlockchainName - } + blockchain := wallet.Blockchain(entry.Blockchain) + coin, _ := s.blockchain.GetNativeCoin(blockchain.ToMoneyBlockchain()) return &Address{ ID: entry.ID, @@ -149,8 +147,8 @@ func (s *Service) entryToAddress(entry repository.MerchantAddress) (*Address, er UpdatedAt: entry.UpdatedAt, Name: entry.Name, MerchantID: entry.MerchantID, - Blockchain: wallet.Blockchain(entry.Blockchain), - BlockchainName: blockchainName, + Blockchain: blockchain, + BlockchainName: coin.BlockchainName, Address: entry.Address, }, nil } diff --git a/internal/service/payment/service_withdrawal.go b/internal/service/payment/service_withdrawal.go index 13f7222..51f7163 100644 --- a/internal/service/payment/service_withdrawal.go +++ b/internal/service/payment/service_withdrawal.go @@ -188,7 +188,7 @@ func (s *Service) GetWithdrawalFee(ctx context.Context, merchantID int64, balanc } // e.g. ETH - baseCurrency, err := s.blockchain.GetCurrencyByTicker(currency.Blockchain.String()) + baseCurrency, err := s.blockchain.GetNativeCoin(currency.Blockchain) if err != nil { return nil, errors.Wrap(err, "unable to get currency by ticker") } diff --git a/internal/service/processing/service.go b/internal/service/processing/service.go index 42035fb..1ba9e38 100644 --- a/internal/service/processing/service.go +++ b/internal/service/processing/service.go @@ -218,11 +218,7 @@ func (s *Service) LockPaymentOptions(ctx context.Context, merchantID, paymentID } // SetPaymentMethod created/changes payment's underlying transaction. -func (s *Service) SetPaymentMethod( - ctx context.Context, - p *payment.Payment, - ticker string, -) (*payment.Method, error) { +func (s *Service) SetPaymentMethod(ctx context.Context, p *payment.Payment, ticker string) (*payment.Method, error) { if p == nil { return nil, errors.New("payment is nil") } diff --git a/internal/service/processing/service_incoming.go b/internal/service/processing/service_incoming.go index 2b9e00c..0bf5694 100644 --- a/internal/service/processing/service_incoming.go +++ b/internal/service/processing/service_incoming.go @@ -215,6 +215,7 @@ func (s *Service) BatchCheckIncomingTransactions(ctx context.Context, transactio evt.Int64("checked_transactions_count", checked). Ints64("transaction_ids", transactionIDs). + Ints64("failed_transaction_ids", failedTXs). Msg("Checked incoming transactions") return err @@ -240,12 +241,8 @@ func (s *Service) checkIncomingTransaction(ctx context.Context, txID int64) erro } receipt, err := s.blockchain.GetTransactionReceipt(ctx, tx.Currency.Blockchain, *tx.HashID, tx.IsTest) - - switch { - case err != nil: + if err != nil { return errors.Wrap(err, "unable to get transaction receipt") - case tx.Currency.Blockchain.String() != receipt.NetworkFee.Ticker(): - return errors.Wrap(err, "invalid receipt network fee") } if !receipt.IsConfirmed { diff --git a/internal/service/processing/service_incoming_test.go b/internal/service/processing/service_incoming_test.go index 86c0317..3d948ea 100644 --- a/internal/service/processing/service_incoming_test.go +++ b/internal/service/processing/service_incoming_test.go @@ -33,6 +33,7 @@ func TestService_BatchCheckIncomingTransactions(t *testing.T) { eth := tc.Must.GetCurrency(t, "ETH") ethUSDT := tc.Must.GetCurrency(t, "ETH_USDT") tron := tc.Must.GetCurrency(t, "TRON") + bnb := tc.Must.GetCurrency(t, "BNB") // Given shortcut for imitating incoming tx incomingTX := func(fiat money.FiatCurrency, price float64, crypto money.CryptoCurrency, isTest bool) *transaction.Transaction { @@ -99,7 +100,7 @@ func TestService_BatchCheckIncomingTransactions(t *testing.T) { // Given a shortcut for receipt mocking makeReceipt := func(confirmations int64, isConfirmed, isSuccess bool) func(*transaction.Transaction) *blockchain.TransactionReceipt { return func(tx *transaction.Transaction) *blockchain.TransactionReceipt { - coin := tc.Must.GetCurrency(t, tx.Currency.Blockchain.String()) + coin := tc.Must.GetBlockchainCoin(t, tx.Currency.Blockchain) return &blockchain.TransactionReceipt{ Blockchain: tx.Currency.Blockchain, @@ -200,6 +201,59 @@ func TestService_BatchCheckIncomingTransactions(t *testing.T) { assertUpdateStatusEventSent(t, true) }, }, + { + name: "success BNB", + transaction: func(isTest bool) *transaction.Transaction { + tx := incomingTX(money.USD, 100, bnb, isTest) + factAmount := lo.Must(tx.Currency.MakeAmount("100_000_000_000_000_000_000")) + + return whReceived(tx, "0x123-hash-abc", factAmount, transaction.StatusInProgress) + }, + receipt: makeReceipt(10, true, true), + assert: func(t *testing.T, tx *transaction.Transaction, pt *payment.Payment) { + assert.Equal(t, payment.StatusSuccess, pt.Status) + + assert.Equal(t, transaction.StatusCompleted, tx.Status) + assert.Equal(t, tx.Amount, *tx.FactAmount) + assert.False(t, tx.ServiceFee.IsZero()) + + wtBalance, mtBalance := loadBalances(t, tx) + + assert.Equal(t, "100", wtBalance.Amount.String()) + assert.Equal(t, "98.500000000000000056", mtBalance.Amount.String()) + + tc.AssertTableRows(t, "wallet_locks", 0) + + assertUpdateStatusEventSent(t, true) + }, + }, + { + name: "success BNB (testnet)", + isTest: true, + transaction: func(isTest bool) *transaction.Transaction { + tx := incomingTX(money.USD, 50, bnb, isTest) + factAmount := lo.Must(tx.Currency.MakeAmount("50_000_000_000_000_000_000")) + + return whReceived(tx, "0x123-hash-abc", factAmount, transaction.StatusInProgress) + }, + receipt: makeReceipt(10, true, true), + assert: func(t *testing.T, tx *transaction.Transaction, pt *payment.Payment) { + assert.Equal(t, payment.StatusSuccess, pt.Status) + + assert.Equal(t, transaction.StatusCompleted, tx.Status) + assert.Equal(t, tx.Amount, *tx.FactAmount) + assert.False(t, tx.ServiceFee.IsZero()) + + wtBalance, mtBalance := loadBalances(t, tx) + + assert.Equal(t, "50", wtBalance.Amount.String()) + assert.Equal(t, "49.250000000000000028", mtBalance.Amount.String()) + + tc.AssertTableRows(t, "wallet_locks", 0) + + assertUpdateStatusEventSent(t, true) + }, + }, { name: "success TRON: network fee is not zero", transaction: func(isTest bool) *transaction.Transaction { @@ -315,7 +369,7 @@ func TestService_BatchCheckIncomingTransactions(t *testing.T) { }, }, { - name: "transaction is not confirmed yet", + name: "ETH transaction is not confirmed yet", transaction: func(isTest bool) *transaction.Transaction { tx := incomingTX(money.USD, 100, eth, isTest) factAmount := lo.Must(tx.Currency.MakeAmount("100_000_000_000_000_000_000")) @@ -332,6 +386,24 @@ func TestService_BatchCheckIncomingTransactions(t *testing.T) { assertUpdateStatusEventSent(t, false) }, }, + { + name: "BNB transaction is not confirmed yet", + transaction: func(isTest bool) *transaction.Transaction { + tx := incomingTX(money.USD, 100, bnb, isTest) + factAmount := lo.Must(tx.Currency.MakeAmount("100_000_000_000_000_000_000")) + + return whReceived(tx, "0x123-hash-abc", factAmount, transaction.StatusInProgress) + }, + receipt: makeReceipt(1, false, true), + assert: func(t *testing.T, tx *transaction.Transaction, pt *payment.Payment) { + assert.Equal(t, payment.StatusInProgress, pt.Status) + assert.Equal(t, transaction.StatusInProgress, tx.Status) + + tc.AssertTableRows(t, "wallet_locks", 0) + + assertUpdateStatusEventSent(t, false) + }, + }, { name: "transaction reverted by blockchain", transaction: func(isTest bool) *transaction.Transaction { @@ -365,13 +437,7 @@ func TestService_BatchCheckIncomingTransactions(t *testing.T) { receipt := testCase.receipt(tx) // And mocked transaction receipt - tc.Fakes.SetupGetTransactionReceipt( - tx.Currency.Blockchain, - *tx.HashID, - tx.IsTest, - receipt, - nil, - ) + tc.Fakes.SetupGetTransactionReceipt(tx.Currency.Blockchain, *tx.HashID, tx.IsTest, receipt, nil) // ACT err := tc.Services.Processing.BatchCheckIncomingTransactions(tc.Context, []int64{tx.ID}) diff --git a/internal/service/processing/service_internal.go b/internal/service/processing/service_internal.go index 43780b0..c440400 100644 --- a/internal/service/processing/service_internal.go +++ b/internal/service/processing/service_internal.go @@ -230,7 +230,7 @@ func (s *Service) createInternalTransfer( out := internalTransferOutput{} // 0. Get currency & baseCurrency (e.g. ETH and ETH_USDT) - baseCurrency, err := s.blockchain.GetCurrencyByTicker(sender.Blockchain.String()) + baseCurrency, err := s.blockchain.GetNativeCoin(sender.Blockchain.ToMoneyBlockchain()) if err != nil { return out, errors.Wrap(err, "unable to get base currency") } @@ -392,12 +392,8 @@ func (s *Service) checkInternalTransaction(ctx context.Context, txID int64) erro } receipt, err := s.blockchain.GetTransactionReceipt(ctx, tx.Currency.Blockchain, *tx.HashID, tx.IsTest) - - switch { - case err != nil: + if err != nil { return errors.Wrap(err, "unable to get transaction receipt") - case tx.Currency.Blockchain.String() != receipt.NetworkFee.Ticker(): - return errors.Wrap(err, "invalid receipt network fee") } if !receipt.IsConfirmed { diff --git a/internal/service/processing/service_internal_test.go b/internal/service/processing/service_internal_test.go index 099a6c1..80908b5 100644 --- a/internal/service/processing/service_internal_test.go +++ b/internal/service/processing/service_internal_test.go @@ -33,6 +33,8 @@ func TestService_BatchCreateInternalTransfers(t *testing.T) { tron := tc.Must.GetCurrency(t, "TRON") tronUSDT := tc.Must.GetCurrency(t, "TRON_USDT") + bnb := tc.Must.GetCurrency(t, "BNB") + // Mock tx fees tc.Fakes.SetupAllFees(t, tc.Services.Blockchain) @@ -40,6 +42,7 @@ func TestService_BatchCreateInternalTransfers(t *testing.T) { tc.Providers.TatumMock.SetupRates("ETH", money.USD, 1600) tc.Providers.TatumMock.SetupRates("ETH_USDT", money.USD, 1) tc.Providers.TatumMock.SetupRates("TRON", money.USD, 0.066) + tc.Providers.TatumMock.SetupRates("BNB", money.USD, 240) t.Run("Creates transactions", func(t *testing.T) { isTest := false @@ -277,6 +280,62 @@ func TestService_BatchCreateInternalTransfers(t *testing.T) { // check that balance has decremented assert.True(t, b1Fresh.Amount.LessThan(b1.Amount), b1Fresh.Amount.String()) }) + + t.Run("Creates BNB transaction", func(t *testing.T) { + // ARRANGE + // Given outbound BNB balance + tc.Must.CreateWalletWithBalance(t, "BSC", wallet.TypeOutbound, withBalance(bnb, "0", isTest)) + + // Given an inbound balance with 0.5 BNB + w1, b1 := tc.Must.CreateWalletWithBalance(t, "BSC", wallet.TypeInbound, withBalance(bnb, "500_000_000_000_000_000", isTest)) + + const ( + rawTxData = "0x123456" + txHashID = "0xffffff" + ) + + // And mocked ethereum transaction creation & broadcast + tc.SetupCreateBSCTransactionWildcard(rawTxData) + tc.Fakes.SetupBroadcastTransaction(bnb.Blockchain, rawTxData, false, txHashID, nil) + + // ACT + // Create internal transfer + result, err := tc.Services.Processing.BatchCreateInternalTransfers(tc.Context, []*wallet.Balance{b1}) + + // ASSERT + assert.NoError(t, err) + assert.Len(t, result.CreatedTransactions, 1) + assert.Empty(t, result.RollbackedTransactionIDs) + assert.Empty(t, result.TotalErrors) + + // Get fresh transaction from DB + tx, err := tc.Services.Transaction.GetByID(tc.Context, 0, result.CreatedTransactions[0].ID) + require.NoError(t, err) + + // Check that tx was created + assert.NotNil(t, tx) + assert.Equal(t, w1.ID, *tx.SenderWalletID) + assert.Equal(t, w1.Address, *tx.SenderAddress) + assert.Equal(t, b1.Currency, tx.Amount.Ticker()) + assert.Equal(t, txHashID, *tx.HashID) + assert.True(t, tx.ServiceFee.IsZero()) + assert.Nil(t, tx.NetworkFee) + assert.NotEqual(t, tx.Amount, b1.Amount) + + // Get fresh wallet from DB + wt, err := tc.Services.Wallet.GetByID(tc.Context, *tx.SenderWalletID) + require.NoError(t, err) + + // check pending tx counter + assert.Equal(t, int64(1), wt.PendingMainnetTransactions) + + // Get fresh balance from DB + b1Fresh, err := tc.Services.Wallet.GetBalanceByUUID(tc.Context, wallet.EntityTypeWallet, wt.ID, b1.UUID) + require.NoError(t, err) + + // check that balance has decremented + assert.True(t, b1Fresh.Amount.LessThan(b1.Amount), b1Fresh.Amount.String()) + }) }) t.Run("Tolerates errors", func(t *testing.T) { @@ -594,6 +653,9 @@ func TestService_BatchCheckInternalTransfers(t *testing.T) { tronUSDT := tc.Must.GetCurrency(t, "TRON_USDT") tronNetworkFee := lo.Must(tron.MakeAmount("1000")) + bnb := tc.Must.GetCurrency(t, "BNB") + bnbNetworkFee := lo.Must(bnb.MakeAmount("2000")) + createTransfer := func( sender, recipient *wallet.Wallet, senderBalance *wallet.Balance, @@ -922,6 +984,73 @@ func TestService_BatchCheckInternalTransfers(t *testing.T) { assert.Equal(t, "1000", balanceInCoin.Amount.StringRaw()) }) + t.Run("Confirms BNB transfer", func(t *testing.T) { + // ARRANGE + tc.Clear.Wallets(t) + isTest := false + + // Given INBOUND wallet with BNB balance + withBNB1 := test.WithBalanceFromCurrency(bnb, "500_000_000", isTest) + wtIn, balanceIn := tc.Must.CreateWalletWithBalance(t, bnb.Blockchain.String(), wallet.TypeInbound, withBNB1) + + // And OUTBOUND wallet with zero balance + withBNB2 := test.WithBalanceFromCurrency(bnb, "0", isTest) + wtOut, balanceOut := tc.Must.CreateWalletWithBalance(t, bnb.Blockchain.String(), wallet.TypeOutbound, withBNB2) + + // And created internal transfer + amount := money.MustCryptoFromRaw(bnb.Ticker, "300_000_000", bnb.Decimals) + + tx, _ := createTransfer(wtIn, wtOut, balanceIn, bnb, amount, isTest) + + // And decremented sender balance + balanceIn, err := tc.Services.Wallet.GetBalanceByUUID(ctx, wallet.EntityTypeWallet, wtIn.ID, balanceIn.UUID) + require.NoError(t, err) + require.Equal(t, "200000000", balanceIn.Amount.StringRaw()) + + // And mocked network fee + receipt := &blockchain.TransactionReceipt{ + Blockchain: bnb.Blockchain, + IsTest: tx.IsTest, + Sender: wtIn.Address, + Recipient: wtOut.Address, + Hash: *tx.HashID, + NetworkFee: bnbNetworkFee, + Success: true, + Confirmations: 5, + IsConfirmed: true, + } + + tc.Fakes.SetupGetTransactionReceipt(bnb.Blockchain, *tx.HashID, tx.IsTest, receipt, nil) + + // ACT + err = tc.Services.Processing.BatchCheckInternalTransfers(ctx, []int64{tx.ID}) + + // ASSERT + assert.NoError(t, err) + + // Check that tx is successful + tx, err = tc.Services.Transaction.GetByID(ctx, 0, tx.ID) + require.NoError(t, err) + + assert.Equal(t, transaction.TypeInternal, tx.Type) + assert.Equal(t, transaction.StatusCompleted, tx.Status) + assert.Equal(t, amount, tx.Amount) + assert.Equal(t, amount, *tx.FactAmount) + assert.Equal(t, bnbNetworkFee, *tx.NetworkFee) + assert.Equal(t, wtIn.ID, *tx.SenderWalletID) + assert.Equal(t, wtOut.ID, *tx.RecipientWalletID) + + // Check that sender balance equals to 500_000_000 - 300_000_000 - network fee (2000) + balanceIn, err = tc.Services.Wallet.GetBalanceByUUID(ctx, wallet.EntityTypeWallet, wtIn.ID, balanceIn.UUID) + require.NoError(t, err) + assert.Equal(t, "199998000", balanceIn.Amount.StringRaw()) + + // Check that recipient balance equals to $amount + balanceOut, err = tc.Services.Wallet.GetBalanceByUUID(ctx, wallet.EntityTypeWallet, wtOut.ID, balanceOut.UUID) + require.NoError(t, err) + assert.Equal(t, amount.StringRaw(), balanceOut.Amount.StringRaw()) + }) + t.Run("Transaction is not confirmed yet", func(t *testing.T) { // ARRANGE tc.Clear.Wallets(t) diff --git a/internal/service/processing/service_webhook.go b/internal/service/processing/service_webhook.go index 3a1304e..9c7d838 100644 --- a/internal/service/processing/service_webhook.go +++ b/internal/service/processing/service_webhook.go @@ -12,7 +12,7 @@ import ( "github.com/pkg/errors" ) -// see https://apidoc.tatum.io/tag/Notification-subscriptions#operation/createSubscription +// TatumWebhook see https://apidoc.tatum.io/tag/Notification-subscriptions#operation/createSubscription type TatumWebhook struct { SubscriptionType string `json:"subscriptionType"` TransactionID string `json:"txId"` @@ -210,7 +210,7 @@ func (s *Service) resolveCurrencyFromWebhook(bc money.Blockchain, networkID stri ) if isCoin { - currency, err = s.blockchain.GetCurrencyByTicker(wh.Asset) + currency, err = s.blockchain.GetNativeCoin(bc) } else { currency, err = s.blockchain.GetCurrencyByBlockchainAndContract(bc, networkID, wh.Asset) } diff --git a/internal/service/processing/service_withdrawal.go b/internal/service/processing/service_withdrawal.go index 399b5c1..fc9aab2 100644 --- a/internal/service/processing/service_withdrawal.go +++ b/internal/service/processing/service_withdrawal.go @@ -202,7 +202,7 @@ func (s *Service) createWithdrawal(ctx context.Context, params withdrawalInput) out := withdrawalOutput{} // 0. Get currency & baseCurrency (e.g. ETH and ETH_USDT) - baseCurrency, err := s.blockchain.GetCurrencyByTicker(params.MerchantBalance.Network) + baseCurrency, err := s.blockchain.GetNativeCoin(params.MerchantBalance.Blockchain()) if err != nil { return out, errors.Wrap(err, "unable to get base currency") } diff --git a/internal/service/processing/service_withdrawal_test.go b/internal/service/processing/service_withdrawal_test.go index 497144e..83e6b1e 100644 --- a/internal/service/processing/service_withdrawal_test.go +++ b/internal/service/processing/service_withdrawal_test.go @@ -33,6 +33,7 @@ func TestService_BatchCreateWithdrawals(t *testing.T) { ethUSDT := tc.Must.GetCurrency(t, "ETH_USDT") tron := tc.Must.GetCurrency(t, "TRON") tronUSDT := tc.Must.GetCurrency(t, "TRON_USDT") + bnb := tc.Must.GetCurrency(t, "BNB") // Mock tx fees tc.Fakes.SetupAllFees(t, tc.Services.Blockchain) @@ -41,6 +42,7 @@ func TestService_BatchCreateWithdrawals(t *testing.T) { tc.Providers.TatumMock.SetupRates("ETH", money.USD, 1600) tc.Providers.TatumMock.SetupRates("ETH_USDT", money.USD, 1) tc.Providers.TatumMock.SetupRates("TRON", money.USD, 0.08) + tc.Providers.TatumMock.SetupRates("BNB", money.USD, 240) t.Run("Creates transactions", func(t *testing.T) { t.Run("Creates ETH transaction", func(t *testing.T) { @@ -409,6 +411,97 @@ func TestService_BatchCreateWithdrawals(t *testing.T) { require.NoError(t, err) assert.Equal(t, payment.StatusInProgress, withdrawal.Status) }) + + t.Run("Creates BNB transaction", func(t *testing.T) { + isTest := false + + // ARRANGE + // Given merchant + mt, _ := tc.Must.CreateMerchant(t, 1) + + // With BSC address + addr, err := tc.Services.Merchants.CreateMerchantAddress(ctx, mt.ID, merchant.CreateMerchantAddressParams{ + Name: "Bob's Address", + Blockchain: kmswallet.Blockchain(bnb.Blockchain), + Address: "0x95222290dd7278003ddd389cc1e1d165cc4bafe0", + }) + require.NoError(t, err) + + // And BNB 0.5 balance + withBalance := test.WithBalanceFromCurrency(bnb, "500_000_000_000_000_000", isTest) + merchantBalance := tc.Must.CreateBalance(t, wallet.EntityTypeMerchant, mt.ID, withBalance) + + // Given withdrawal + amount := lo.Must(bnb.MakeAmount("400_000_000_000_000_000")) + withdrawal, err := tc.Services.Payment.CreateWithdrawal(ctx, mt.ID, payment.CreateWithdrawalProps{ + BalanceID: merchantBalance.UUID, + AddressID: addr.UUID, + AmountRaw: amount.String(), + }) + require.NoError(t, err) + + // Given OUTBOUND wallet with balance of 1 BNB + withETH := test.WithBalanceFromCurrency(bnb, "1_000_000_000_000_000_000", isTest) + outboundWallet, outboundBalance := tc.Must.CreateWalletWithBalance(t, "ETH", wallet.TypeOutbound, withETH) + + // Given service fee mock for withdrawal + serviceFeeUSD := lo.Must(money.FiatFromFloat64(money.USD, 3)) + serviceFeeCrypto := lo.Must(tc.Services.Blockchain.FiatToCrypto(ctx, serviceFeeUSD, bnb)).To + + const ( + rawTxData = "0x123456" + txHashID = "0xffffff" + expectedMerchantBalance = "875" + "00000000000001" // 0.5 BNB - 0.4 BNB - $3 = 0.1 BNB - $3: = 0.1 BNB - 0.0125 BNB + expectedWalletBalance = "6" + "00000000000000000" // 0.6 BNB: 1 BNB - 0.4 BNB + ) + + tc.Fakes.SetupCalculateWithdrawalFeeUSD(bnb, bnb, isTest, serviceFeeUSD) + + // Given mocked ETH transaction creation & broadcast + tc.SetupCreateBSCTransactionWildcard(rawTxData) + tc.Fakes.SetupBroadcastTransaction(bnb.Blockchain, rawTxData, isTest, txHashID, nil) + + // ACT + result, err := tc.Services.Processing.BatchCreateWithdrawals(ctx, []int64{withdrawal.ID}) + + // ASSERT + assert.NoError(t, err) + assert.Empty(t, result.TotalErrors) + assert.Empty(t, result.UnhandledErrors) + assert.Len(t, result.CreatedTransactions, 1) + + // Get fresh transaction from DB + tx, err := tc.Services.Transaction.GetByID(tc.Context, mt.ID, result.CreatedTransactions[0].ID) + require.NoError(t, err) + + // Check that tx was created and properties are correct + assert.NotNil(t, tx) + assert.Equal(t, transaction.TypeWithdrawal, tx.Type) + assert.Equal(t, transaction.StatusPending, tx.Status) + assert.Equal(t, outboundWallet.ID, *tx.SenderWalletID) + assert.Equal(t, outboundWallet.Address, *tx.SenderAddress) + assert.Equal(t, addr.Address, tx.RecipientAddress) + assert.Equal(t, outboundBalance.Currency, tx.Amount.Ticker()) + assert.Equal(t, txHashID, *tx.HashID) + assert.Equal(t, serviceFeeCrypto, tx.ServiceFee) + assert.Equal(t, withdrawal.Price, tx.Amount) + assert.Nil(t, tx.NetworkFee) + + // Get fresh merchant balance and check balance + merchantBalance, err = tc.Services.Wallet.GetMerchantBalanceByUUID(ctx, mt.ID, merchantBalance.UUID) + require.NoError(t, err) + assert.Equal(t, expectedMerchantBalance, merchantBalance.Amount.StringRaw()) + + // Check outbound wallet's balance + outboundBalance, err = tc.Services.Wallet.GetBalanceByID(ctx, wallet.EntityTypeWallet, outboundWallet.ID, outboundBalance.ID) + require.NoError(t, err) + assert.Equal(t, expectedWalletBalance, outboundBalance.Amount.StringRaw()) + + // Check withdrawal + withdrawal, err = tc.Services.Payment.GetByID(ctx, mt.ID, withdrawal.ID) + require.NoError(t, err) + assert.Equal(t, payment.StatusInProgress, withdrawal.Status) + }) }) t.Run("Creates 2 ETH transactions, one failed", func(t *testing.T) { @@ -783,6 +876,7 @@ func TestService_BatchCheckWithdrawals(t *testing.T) { eth := tc.Must.GetCurrency(t, "ETH") ethUSDT := tc.Must.GetCurrency(t, "ETH_USDT") + bnb := tc.Must.GetCurrency(t, "BNB") // Mock tx fees tc.Fakes.SetupAllFees(t, tc.Services.Blockchain) @@ -791,6 +885,7 @@ func TestService_BatchCheckWithdrawals(t *testing.T) { tc.Providers.TatumMock.SetupRates("ETH", money.USD, 1600) tc.Providers.TatumMock.SetupRates("ETH_USDT", money.USD, 1) tc.Providers.TatumMock.SetupRates("TRON", money.USD, 0.08) + tc.Providers.TatumMock.SetupRates("BNB", money.USD, 240) t.Run("Confirms ETH transaction", func(t *testing.T) { isTest := false @@ -1013,6 +1108,118 @@ func TestService_BatchCheckWithdrawals(t *testing.T) { assert.Equal(t, payment.StatusSuccess, withdrawal.Status) }) + t.Run("Confirms BNB transaction", func(t *testing.T) { + isTest := false + + // ARRANGE + // Given merchant + mt, _ := tc.Must.CreateMerchant(t, 1) + + // With BNB address + addr, err := tc.Services.Merchants.CreateMerchantAddress(ctx, mt.ID, merchant.CreateMerchantAddressParams{ + Name: "Bob's Address", + Blockchain: kmswallet.Blockchain(bnb.Blockchain), + Address: "0x85222290dd7278ff3ddd389cc1e1d165cc4bafe5", + }) + require.NoError(t, err) + + // And BNB balance + withBalance := test.WithBalanceFromCurrency(bnb, "600_000_000_000_000_000", isTest) + merchantBalance := tc.Must.CreateBalance(t, wallet.EntityTypeMerchant, mt.ID, withBalance) + + // Given withdrawal + amount := lo.Must(bnb.MakeAmount("500_000_000_000_000_000")) + withdrawal, err := tc.Services.Payment.CreateWithdrawal(ctx, mt.ID, payment.CreateWithdrawalProps{ + BalanceID: merchantBalance.UUID, + AddressID: addr.UUID, + AmountRaw: amount.String(), + }) + require.NoError(t, err) + + // Given OUTBOUND wallet with balance of 1 BNB + withBNB := test.WithBalanceFromCurrency(bnb, "1_000_000_000_000_000_000", isTest) + outboundWallet, outboundBalance := tc.Must.CreateWalletWithBalance(t, "BSC", wallet.TypeOutbound, withBNB) + + // Given service fee mock for withdrawal + serviceFeeUSD := lo.Must(money.FiatFromFloat64(money.USD, 3)) + tc.Fakes.SetupCalculateWithdrawalFeeUSD(bnb, bnb, isTest, serviceFeeUSD) + + const ( + rawTxData = "0x123456" + txHashID = "0xffffff" + ) + + // Given mocked BNB transaction creation & broadcast + tc.SetupCreateBSCTransactionWildcard(rawTxData) + tc.Fakes.SetupBroadcastTransaction(bnb.Blockchain, rawTxData, isTest, txHashID, nil) + + // Given successful tx creation & broadcasting + result, err := tc.Services.Processing.BatchCreateWithdrawals(ctx, []int64{withdrawal.ID}) + require.NoError(t, err) + require.Len(t, result.CreatedTransactions, 1) + + txID := result.CreatedTransactions[0].ID + + // ... time goes by ... + + // Given transaction receipt + networkFee := lo.Must(bnb.MakeAmount("1000")) + receipt := &blockchain.TransactionReceipt{ + Blockchain: bnb.Blockchain, + IsTest: isTest, + Sender: outboundWallet.Address, + Recipient: addr.Address, + Hash: txHashID, + Nonce: 0, + NetworkFee: networkFee, + Success: true, + Confirmations: 10, + IsConfirmed: true, + } + + tc.Fakes.SetupGetTransactionReceipt(bnb.Blockchain, txHashID, isTest, receipt, nil) + + // ACT + // Check for withdrawal progress + err = tc.Services.Processing.BatchCheckWithdrawals(ctx, []int64{txID}) + + // ASSERT + assert.NoError(t, err) + + // Check transaction + tx, err := tc.Services.Transaction.GetByID(ctx, mt.ID, txID) + assert.NoError(t, err) + assert.Equal(t, transaction.StatusCompleted, tx.Status) + assert.Equal(t, networkFee, *tx.NetworkFee) + + // Check outbound wallet & balance + outboundWallet, err = tc.Services.Wallet.GetByID(ctx, outboundWallet.ID) + assert.NoError(t, err) + assert.Equal(t, int64(0), outboundWallet.PendingMainnetTransactions) + + // Check that outbound balance was decremented by tx amount and network fee + outboundAmountBefore := outboundBalance.Amount + outboundBalance, err = tc.Services.Wallet.GetBalanceByID(ctx, wallet.EntityTypeWallet, outboundWallet.ID, outboundBalance.ID) + assert.NoError(t, err) + assert.Equal( + t, + outboundAmountBefore, + lo.Must(lo.Must(outboundBalance.Amount.Add(tx.Amount)).Add(receipt.NetworkFee)), + ) + + // Check withdrawal + withdrawal, err = tc.Services.Payment.GetByPublicID(ctx, withdrawal.PublicID) + assert.NoError(t, err) + assert.Equal(t, payment.StatusSuccess, withdrawal.Status) + + // Extra assertion from merchant's perspective + related, err := tc.Services.Payment.GetByMerchantOrderIDWithRelations(ctx, mt.ID, withdrawal.MerchantOrderUUID) + assert.NoError(t, err) + assert.Equal(t, tx.ID, related.Transaction.ID) + assert.Equal(t, merchantBalance.ID, related.Balance.ID) + assert.Equal(t, addr.ID, related.Address.ID) + }) + t.Run("Transaction is not confirmed yet", func(t *testing.T) { tc.Clear.Wallets(t) diff --git a/internal/service/transaction/service.go b/internal/service/transaction/service.go index a477bc7..01f010f 100644 --- a/internal/service/transaction/service.go +++ b/internal/service/transaction/service.go @@ -193,7 +193,7 @@ func (s *Service) Create( recipientWalletID = repository.Int64ToNullable(params.RecipientWallet.ID) } - networkCurrency, err := s.blockchain.GetCurrencyByTicker(params.Currency.Blockchain.String()) + networkCurrency, err := s.blockchain.GetNativeCoin(params.Currency.Blockchain) if err != nil { return nil, errors.Wrap(err, "unable to get network currency") } @@ -433,7 +433,12 @@ func (s *Service) entryToTransaction(tx repository.Transaction) (*Transaction, e var networkFee *money.Money if tx.NetworkFee.Status == pgtype.Present { - netFee, errM := repository.NumericToMoney(tx.NetworkFee, money.Crypto, tx.Blockchain, int64(tx.NetworkDecimals)) + coin, errCoin := s.blockchain.GetNativeCoin(money.Blockchain(tx.Blockchain)) + if errCoin != nil { + return nil, errors.Wrapf(errCoin, "unable to get native coin for %q", tx.Blockchain) + } + + netFee, errM := repository.NumericToCrypto(tx.NetworkFee, coin) if errM != nil { return nil, errors.Wrap(errM, "unable to construct networkFee") } diff --git a/internal/service/transaction/service_update.go b/internal/service/transaction/service_update.go index b574ac1..913d941 100644 --- a/internal/service/transaction/service_update.go +++ b/internal/service/transaction/service_update.go @@ -297,7 +297,7 @@ func (s *Service) updateBalancesAfterTxConfirmation( networkCurrency := func() (money.CryptoCurrency, error) { currency := tx.Currency if tx.Currency.Type == money.Token { - cur, err := s.blockchain.GetCurrencyByTicker(tx.Currency.Blockchain.String()) + cur, err := s.blockchain.GetNativeCoin(tx.Currency.Blockchain) if err != nil { return money.CryptoCurrency{}, errors.Wrap(err, "unable to get currency for fees") } diff --git a/internal/service/wallet/service.go b/internal/service/wallet/service.go index f46e801..ea7bae1 100644 --- a/internal/service/wallet/service.go +++ b/internal/service/wallet/service.go @@ -63,7 +63,6 @@ type Pagination struct { FilterByType Type } -// TatumSubscription type TatumSubscription struct { MainnetSubscriptionID string TestnetSubscriptionID string diff --git a/internal/service/wallet/service_balance.go b/internal/service/wallet/service_balance.go index 3e9f784..f4b9acf 100644 --- a/internal/service/wallet/service_balance.go +++ b/internal/service/wallet/service_balance.go @@ -47,6 +47,10 @@ func (b *Balance) Covers(expenses ...money.Money) error { return nil } +func (b *Balance) Blockchain() money.Blockchain { + return money.Blockchain(b.Network) +} + func (b *Balance) compatibleTo(a *Balance) bool { return b.Currency == a.Currency && b.NetworkID == a.NetworkID } diff --git a/internal/service/wallet/service_transaction.go b/internal/service/wallet/service_transaction.go index dc111df..6dc91ee 100644 --- a/internal/service/wallet/service_transaction.go +++ b/internal/service/wallet/service_transaction.go @@ -48,6 +48,7 @@ func (s *Service) CreateSignedTransaction( return txRaw, errCreate } +//nolint:gocyclo func (s *Service) createSignedTransaction( ctx context.Context, sender *Wallet, @@ -126,6 +127,40 @@ func (s *Service) createSignedTransaction( return res.Payload.RawTransaction, nil } + if currency.Blockchain == kms.BSC.ToMoneyBlockchain() { + networkID, err := strconv.Atoi(currency.ChooseNetwork(isTest)) + if err != nil { + return "", errors.Wrap(err, "unable to parse network id") + } + + bscFee, err := fee.ToBSCFee() + if err != nil { + return "", errors.Wrap(err, "fee is not BSC") + } + + res, err := s.kms.CreateBSCTransaction(&kmsclient.CreateBSCTransactionParams{ + Context: ctx, + WalletID: sender.UUID.String(), + Data: &kmsmodel.CreateBSCTransactionRequest{ + Amount: amount.StringRaw(), + AssetType: kmsmodel.AssetType(currency.Type), + ContractAddress: currency.ChooseContractAddress(isTest), + Gas: int64(bscFee.GasUnits), + MaxFeePerGas: bscFee.GasPrice, + MaxPriorityPerGas: bscFee.PriorityFee, + NetworkID: int64(networkID), + Nonce: util.Ptr(nonce), + Recipient: recipient, + }, + }) + + if err != nil { + return "", errors.Wrap(err, "unable to create BSC transaction") + } + + return res.Payload.RawTransaction, nil + } + if currency.Blockchain == kms.TRON.ToMoneyBlockchain() { tronFee, err := fee.ToTronFee() if err != nil { diff --git a/internal/test/fakes/fees.go b/internal/test/fakes/fees.go index 8a7c2a7..1554aa7 100644 --- a/internal/test/fakes/fees.go +++ b/internal/test/fakes/fees.go @@ -100,6 +100,7 @@ func (m *FeeCalculator) SetupAllFees(t *testing.T, service *blockchain.Service) // ETH eth := getCurrency("ETH") ethUSDT := getCurrency("ETH_USDT") + ethUSDC := getCurrency("ETH_USDC") ethFee := blockchain.EthFee{ GasUnits: 21000, GasPrice: "52860219500", @@ -113,16 +114,21 @@ func (m *FeeCalculator) SetupAllFees(t *testing.T, service *blockchain.Service) m.SetupCalculateFee(eth, eth, true, blockchain.NewFee(eth, now, true, ethFee)) m.SetupCalculateFee(eth, ethUSDT, false, blockchain.NewFee(eth, now, false, ethFee)) m.SetupCalculateFee(eth, ethUSDT, true, blockchain.NewFee(eth, now, true, ethFee)) + m.SetupCalculateFee(eth, ethUSDC, false, blockchain.NewFee(eth, now, false, ethFee)) + m.SetupCalculateFee(eth, ethUSDC, true, blockchain.NewFee(eth, now, true, ethFee)) // withdrawal fees m.SetupCalculateWithdrawalFeeUSD(eth, eth, false, lo.Must(money.USD.MakeAmount("100"))) m.SetupCalculateWithdrawalFeeUSD(eth, eth, true, lo.Must(money.USD.MakeAmount("100"))) m.SetupCalculateWithdrawalFeeUSD(eth, ethUSDT, false, lo.Must(money.USD.MakeAmount("300"))) m.SetupCalculateWithdrawalFeeUSD(eth, ethUSDT, true, lo.Must(money.USD.MakeAmount("300"))) + m.SetupCalculateWithdrawalFeeUSD(eth, ethUSDC, true, lo.Must(money.USD.MakeAmount("300"))) + m.SetupCalculateWithdrawalFeeUSD(eth, ethUSDC, true, lo.Must(money.USD.MakeAmount("300"))) // MATIC matic := getCurrency("MATIC") maticUSDT := getCurrency("MATIC_USDT") + maticUSDC := getCurrency("MATIC_USDC") maticFee := blockchain.MaticFee{ GasUnits: 21000, GasPrice: "115243093692", @@ -136,12 +142,34 @@ func (m *FeeCalculator) SetupAllFees(t *testing.T, service *blockchain.Service) m.SetupCalculateFee(matic, matic, true, blockchain.NewFee(matic, now, true, maticFee)) m.SetupCalculateFee(matic, maticUSDT, false, blockchain.NewFee(matic, now, false, maticFee)) m.SetupCalculateFee(matic, maticUSDT, true, blockchain.NewFee(matic, now, true, maticFee)) + m.SetupCalculateFee(matic, maticUSDC, false, blockchain.NewFee(matic, now, false, maticFee)) + m.SetupCalculateFee(matic, maticUSDC, true, blockchain.NewFee(matic, now, true, maticFee)) // withdrawal fees m.SetupCalculateWithdrawalFeeUSD(matic, matic, false, lo.Must(money.USD.MakeAmount("10"))) m.SetupCalculateWithdrawalFeeUSD(matic, matic, true, lo.Must(money.USD.MakeAmount("10"))) m.SetupCalculateWithdrawalFeeUSD(matic, maticUSDT, false, lo.Must(money.USD.MakeAmount("20"))) m.SetupCalculateWithdrawalFeeUSD(matic, maticUSDT, true, lo.Must(money.USD.MakeAmount("20"))) + m.SetupCalculateWithdrawalFeeUSD(matic, maticUSDC, false, lo.Must(money.USD.MakeAmount("20"))) + m.SetupCalculateWithdrawalFeeUSD(matic, maticUSDC, true, lo.Must(money.USD.MakeAmount("20"))) + + // BSC + bnb := getCurrency("BNB") + bscFee := blockchain.BSCFee{ + GasUnits: 21000, + GasPrice: "115243093692", + PriorityFee: "30000000000", + TotalCostWEI: "9440801089980000", + TotalCostBNB: "0.00944080108998", + TotalCostUSD: "0.01", + } + + m.SetupCalculateFee(bnb, bnb, false, blockchain.NewFee(bnb, now, false, bscFee)) + m.SetupCalculateFee(bnb, bnb, true, blockchain.NewFee(bnb, now, true, bscFee)) + + // withdrawal fees + m.SetupCalculateWithdrawalFeeUSD(bnb, bnb, false, lo.Must(money.USD.MakeAmount("10"))) + m.SetupCalculateWithdrawalFeeUSD(bnb, bnb, true, lo.Must(money.USD.MakeAmount("10"))) // TRON tron := getCurrency("TRON") diff --git a/internal/test/integration_kms.go b/internal/test/integration_kms.go index ff7ef56..018f89f 100644 --- a/internal/test/integration_kms.go +++ b/internal/test/integration_kms.go @@ -33,6 +33,7 @@ func setupKMS(t *testing.T, trongridProvider *trongrid.Provider, logger *zerolog wallet.NewGenerator(). AddProvider(&wallet.EthProvider{Blockchain: wallet.ETH, CryptoReader: cryptorand.Reader}). AddProvider(&wallet.EthProvider{Blockchain: wallet.MATIC, CryptoReader: cryptorand.Reader}). + AddProvider(&wallet.EthProvider{Blockchain: wallet.BSC, CryptoReader: cryptorand.Reader}). AddProvider(&wallet.BitcoinProvider{Blockchain: wallet.BTC, CryptoReader: cryptorand.Reader}). AddProvider(&wallet.TronProvider{ Blockchain: wallet.TRON, diff --git a/internal/test/mocks_kms.go b/internal/test/mocks_kms.go index 72761c0..e7f8a82 100644 --- a/internal/test/mocks_kms.go +++ b/internal/test/mocks_kms.go @@ -103,6 +103,35 @@ func (i *IntegrationTest) SetupCreateMaticTransactionWildcard(rawTx string) { i.Providers.KMS.On("CreateMaticTransaction", mock.Anything).Return(res, nil) } +func (i *IntegrationTest) SetupCreateBSCTransaction( + walletID uuid.UUID, + input kmsmodel.CreateBSCTransactionRequest, + rawTx string, +) { + req := &kmswallet.CreateBSCTransactionParams{ + Data: &input, + WalletID: walletID.String(), + } + + res := &kmswallet.CreateBSCTransactionCreated{ + Payload: &kmsmodel.BSCTransaction{ + RawTransaction: rawTx, + }, + } + + i.Providers.KMS.On("CreateBSCTransaction", req).Return(res, nil) +} + +func (i *IntegrationTest) SetupCreateBSCTransactionWildcard(rawTx string) { + res := &kmswallet.CreateBSCTransactionCreated{ + Payload: &kmsmodel.BSCTransaction{ + RawTransaction: rawTx, + }, + } + + i.Providers.KMS.On("CreateBSCTransaction", mock.Anything).Return(res, nil) +} + func (i *IntegrationTest) SetupCreateTronTransaction( walletID uuid.UUID, input kmsmodel.CreateTronTransactionRequest, diff --git a/internal/test/must.go b/internal/test/must.go index 708e537..d4bd286 100644 --- a/internal/test/must.go +++ b/internal/test/must.go @@ -202,3 +202,10 @@ func (m *Must) GetCurrency(t *testing.T, ticker string) money.CryptoCurrency { return c } + +func (m *Must) GetBlockchainCoin(t *testing.T, chain money.Blockchain) money.CryptoCurrency { + c, err := m.tc.Services.Blockchain.GetNativeCoin(chain) + require.NoError(t, err) + + return c +} diff --git a/pkg/api-dashboard/v1/model/create_merchant_address_request.go b/pkg/api-dashboard/v1/model/create_merchant_address_request.go index 78691be..14e4a71 100644 --- a/pkg/api-dashboard/v1/model/create_merchant_address_request.go +++ b/pkg/api-dashboard/v1/model/create_merchant_address_request.go @@ -29,7 +29,7 @@ type CreateMerchantAddressRequest struct { // blockchain // Example: ETH // Required: true - // Enum: [BTC ETH TRON MATIC] + // Enum: [BTC ETH TRON MATIC BSC] Blockchain string `json:"blockchain"` // Name @@ -79,7 +79,7 @@ var createMerchantAddressRequestTypeBlockchainPropEnum []interface{} func init() { var res []string - if err := json.Unmarshal([]byte(`["BTC","ETH","TRON","MATIC"]`), &res); err != nil { + if err := json.Unmarshal([]byte(`["BTC","ETH","TRON","MATIC","BSC"]`), &res); err != nil { panic(err) } for _, v := range res { @@ -100,6 +100,9 @@ const ( // CreateMerchantAddressRequestBlockchainMATIC captures enum value "MATIC" CreateMerchantAddressRequestBlockchainMATIC string = "MATIC" + + // CreateMerchantAddressRequestBlockchainBSC captures enum value "BSC" + CreateMerchantAddressRequestBlockchainBSC string = "BSC" ) // prop value enum diff --git a/pkg/api-dashboard/v1/model/merchant_address.go b/pkg/api-dashboard/v1/model/merchant_address.go index 2580d73..c713c8a 100644 --- a/pkg/api-dashboard/v1/model/merchant_address.go +++ b/pkg/api-dashboard/v1/model/merchant_address.go @@ -27,7 +27,7 @@ type MerchantAddress struct { // blockchain // Example: ETH - // Enum: [ETH TRON MATIC] + // Enum: [ETH TRON MATIC BSC] Blockchain string `json:"blockchain"` // Blockchain name @@ -83,7 +83,7 @@ var merchantAddressTypeBlockchainPropEnum []interface{} func init() { var res []string - if err := json.Unmarshal([]byte(`["ETH","TRON","MATIC"]`), &res); err != nil { + if err := json.Unmarshal([]byte(`["ETH","TRON","MATIC","BSC"]`), &res); err != nil { panic(err) } for _, v := range res { @@ -101,6 +101,9 @@ const ( // MerchantAddressBlockchainMATIC captures enum value "MATIC" MerchantAddressBlockchainMATIC string = "MATIC" + + // MerchantAddressBlockchainBSC captures enum value "BSC" + MerchantAddressBlockchainBSC string = "BSC" ) // prop value enum diff --git a/pkg/api-kms/v1/client/wallet/create_b_s_c_transaction_parameters.go b/pkg/api-kms/v1/client/wallet/create_b_s_c_transaction_parameters.go new file mode 100644 index 0000000..e25e00a --- /dev/null +++ b/pkg/api-kms/v1/client/wallet/create_b_s_c_transaction_parameters.go @@ -0,0 +1,172 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package wallet + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + + "github.com/oxygenpay/oxygen/pkg/api-kms/v1/model" +) + +// NewCreateBSCTransactionParams creates a new CreateBSCTransactionParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewCreateBSCTransactionParams() *CreateBSCTransactionParams { + return &CreateBSCTransactionParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewCreateBSCTransactionParamsWithTimeout creates a new CreateBSCTransactionParams object +// with the ability to set a timeout on a request. +func NewCreateBSCTransactionParamsWithTimeout(timeout time.Duration) *CreateBSCTransactionParams { + return &CreateBSCTransactionParams{ + timeout: timeout, + } +} + +// NewCreateBSCTransactionParamsWithContext creates a new CreateBSCTransactionParams object +// with the ability to set a context for a request. +func NewCreateBSCTransactionParamsWithContext(ctx context.Context) *CreateBSCTransactionParams { + return &CreateBSCTransactionParams{ + Context: ctx, + } +} + +// NewCreateBSCTransactionParamsWithHTTPClient creates a new CreateBSCTransactionParams object +// with the ability to set a custom HTTPClient for a request. +func NewCreateBSCTransactionParamsWithHTTPClient(client *http.Client) *CreateBSCTransactionParams { + return &CreateBSCTransactionParams{ + HTTPClient: client, + } +} + +/* +CreateBSCTransactionParams contains all the parameters to send to the API endpoint + + for the create b s c transaction operation. + + Typically these are written to a http.Request. +*/ +type CreateBSCTransactionParams struct { + + // Data. + Data *model.CreateBSCTransactionRequest + + /* WalletID. + + Wallet UUID + */ + WalletID string + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the create b s c transaction params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *CreateBSCTransactionParams) WithDefaults() *CreateBSCTransactionParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the create b s c transaction params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *CreateBSCTransactionParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the create b s c transaction params +func (o *CreateBSCTransactionParams) WithTimeout(timeout time.Duration) *CreateBSCTransactionParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the create b s c transaction params +func (o *CreateBSCTransactionParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the create b s c transaction params +func (o *CreateBSCTransactionParams) WithContext(ctx context.Context) *CreateBSCTransactionParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the create b s c transaction params +func (o *CreateBSCTransactionParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the create b s c transaction params +func (o *CreateBSCTransactionParams) WithHTTPClient(client *http.Client) *CreateBSCTransactionParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the create b s c transaction params +func (o *CreateBSCTransactionParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithData adds the data to the create b s c transaction params +func (o *CreateBSCTransactionParams) WithData(data *model.CreateBSCTransactionRequest) *CreateBSCTransactionParams { + o.SetData(data) + return o +} + +// SetData adds the data to the create b s c transaction params +func (o *CreateBSCTransactionParams) SetData(data *model.CreateBSCTransactionRequest) { + o.Data = data +} + +// WithWalletID adds the walletID to the create b s c transaction params +func (o *CreateBSCTransactionParams) WithWalletID(walletID string) *CreateBSCTransactionParams { + o.SetWalletID(walletID) + return o +} + +// SetWalletID adds the walletId to the create b s c transaction params +func (o *CreateBSCTransactionParams) SetWalletID(walletID string) { + o.WalletID = walletID +} + +// WriteToRequest writes these params to a swagger request +func (o *CreateBSCTransactionParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + if o.Data != nil { + if err := r.SetBodyParam(o.Data); err != nil { + return err + } + } + + // path param walletId + if err := r.SetPathParam("walletId", o.WalletID); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/pkg/api-kms/v1/client/wallet/create_b_s_c_transaction_responses.go b/pkg/api-kms/v1/client/wallet/create_b_s_c_transaction_responses.go new file mode 100644 index 0000000..fd4a0f7 --- /dev/null +++ b/pkg/api-kms/v1/client/wallet/create_b_s_c_transaction_responses.go @@ -0,0 +1,107 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package wallet + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + "github.com/oxygenpay/oxygen/pkg/api-kms/v1/model" +) + +// CreateBSCTransactionReader is a Reader for the CreateBSCTransaction structure. +type CreateBSCTransactionReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *CreateBSCTransactionReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 201: + result := NewCreateBSCTransactionCreated() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 400: + result := NewCreateBSCTransactionBadRequest() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + default: + return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code()) + } +} + +// NewCreateBSCTransactionCreated creates a CreateBSCTransactionCreated with default headers values +func NewCreateBSCTransactionCreated() *CreateBSCTransactionCreated { + return &CreateBSCTransactionCreated{} +} + +/* + CreateBSCTransactionCreated describes a response with status code 201, with default header values. + +Transaction Created +*/ +type CreateBSCTransactionCreated struct { + Payload *model.BSCTransaction +} + +func (o *CreateBSCTransactionCreated) Error() string { + return fmt.Sprintf("[POST /wallet/{walletId}/transaction/bsc][%d] createBSCTransactionCreated %+v", 201, o.Payload) +} +func (o *CreateBSCTransactionCreated) GetPayload() *model.BSCTransaction { + return o.Payload +} + +func (o *CreateBSCTransactionCreated) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(model.BSCTransaction) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewCreateBSCTransactionBadRequest creates a CreateBSCTransactionBadRequest with default headers values +func NewCreateBSCTransactionBadRequest() *CreateBSCTransactionBadRequest { + return &CreateBSCTransactionBadRequest{} +} + +/* + CreateBSCTransactionBadRequest describes a response with status code 400, with default header values. + +Validation error / Not found +*/ +type CreateBSCTransactionBadRequest struct { + Payload *model.ErrorResponse +} + +func (o *CreateBSCTransactionBadRequest) Error() string { + return fmt.Sprintf("[POST /wallet/{walletId}/transaction/bsc][%d] createBSCTransactionBadRequest %+v", 400, o.Payload) +} +func (o *CreateBSCTransactionBadRequest) GetPayload() *model.ErrorResponse { + return o.Payload +} + +func (o *CreateBSCTransactionBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(model.ErrorResponse) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/pkg/api-kms/v1/client/wallet/wallet_client.go b/pkg/api-kms/v1/client/wallet/wallet_client.go index 9b19f64..0e60c17 100644 --- a/pkg/api-kms/v1/client/wallet/wallet_client.go +++ b/pkg/api-kms/v1/client/wallet/wallet_client.go @@ -30,6 +30,8 @@ type ClientOption func(*runtime.ClientOperation) // ClientService is the interface for Client methods type ClientService interface { + CreateBSCTransaction(params *CreateBSCTransactionParams, opts ...ClientOption) (*CreateBSCTransactionCreated, error) + CreateEthereumTransaction(params *CreateEthereumTransactionParams, opts ...ClientOption) (*CreateEthereumTransactionCreated, error) CreateMaticTransaction(params *CreateMaticTransactionParams, opts ...ClientOption) (*CreateMaticTransactionCreated, error) @@ -46,7 +48,45 @@ type ClientService interface { } /* - CreateEthereumTransaction creates ethereum transaction +CreateBSCTransaction creates b s c transaction +*/ +func (a *Client) CreateBSCTransaction(params *CreateBSCTransactionParams, opts ...ClientOption) (*CreateBSCTransactionCreated, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewCreateBSCTransactionParams() + } + op := &runtime.ClientOperation{ + ID: "createBSCTransaction", + Method: "POST", + PathPattern: "/wallet/{walletId}/transaction/bsc", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &CreateBSCTransactionReader{formats: a.formats}, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*CreateBSCTransactionCreated) + if ok { + return success, nil + } + // unexpected success response + // safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for createBSCTransaction: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + +/* +CreateEthereumTransaction creates ethereum transaction */ func (a *Client) CreateEthereumTransaction(params *CreateEthereumTransactionParams, opts ...ClientOption) (*CreateEthereumTransactionCreated, error) { // TODO: Validate the params before sending @@ -84,7 +124,7 @@ func (a *Client) CreateEthereumTransaction(params *CreateEthereumTransactionPara } /* - CreateMaticTransaction creates polygon transaction +CreateMaticTransaction creates polygon transaction */ func (a *Client) CreateMaticTransaction(params *CreateMaticTransactionParams, opts ...ClientOption) (*CreateMaticTransactionCreated, error) { // TODO: Validate the params before sending @@ -122,7 +162,7 @@ func (a *Client) CreateMaticTransaction(params *CreateMaticTransactionParams, op } /* - CreateTronTransaction creates tron transaction +CreateTronTransaction creates tron transaction */ func (a *Client) CreateTronTransaction(params *CreateTronTransactionParams, opts ...ClientOption) (*CreateTronTransactionCreated, error) { // TODO: Validate the params before sending @@ -160,7 +200,7 @@ func (a *Client) CreateTronTransaction(params *CreateTronTransactionParams, opts } /* - CreateWallet creates wallet +CreateWallet creates wallet */ func (a *Client) CreateWallet(params *CreateWalletParams, opts ...ClientOption) (*CreateWalletCreated, error) { // TODO: Validate the params before sending @@ -198,7 +238,7 @@ func (a *Client) CreateWallet(params *CreateWalletParams, opts ...ClientOption) } /* - DeleteWallet deletes wallet +DeleteWallet deletes wallet */ func (a *Client) DeleteWallet(params *DeleteWalletParams, opts ...ClientOption) (*DeleteWalletNoContent, error) { // TODO: Validate the params before sending @@ -236,7 +276,7 @@ func (a *Client) DeleteWallet(params *DeleteWalletParams, opts ...ClientOption) } /* - GetWallet gets wallet +GetWallet gets wallet */ func (a *Client) GetWallet(params *GetWalletParams, opts ...ClientOption) (*GetWalletOK, error) { // TODO: Validate the params before sending diff --git a/pkg/api-kms/v1/mock/ClientOption.go b/pkg/api-kms/v1/mock/ClientOption.go index 2845604..5a874fa 100644 --- a/pkg/api-kms/v1/mock/ClientOption.go +++ b/pkg/api-kms/v1/mock/ClientOption.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.32.0. DO NOT EDIT. package mock @@ -17,13 +17,12 @@ func (_m *ClientOption) Execute(_a0 *runtime.ClientOperation) { _m.Called(_a0) } -type mockConstructorTestingTNewClientOption interface { +// NewClientOption creates a new instance of ClientOption. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewClientOption(t interface { mock.TestingT Cleanup(func()) -} - -// NewClientOption creates a new instance of ClientOption. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewClientOption(t mockConstructorTestingTNewClientOption) *ClientOption { +}) *ClientOption { mock := &ClientOption{} mock.Mock.Test(t) diff --git a/pkg/api-kms/v1/mock/ClientService.go b/pkg/api-kms/v1/mock/ClientService.go index bf443b3..a65a9e5 100644 --- a/pkg/api-kms/v1/mock/ClientService.go +++ b/pkg/api-kms/v1/mock/ClientService.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.32.0. DO NOT EDIT. package mock @@ -14,6 +14,39 @@ type ClientService struct { mock.Mock } +// CreateBSCTransaction provides a mock function with given fields: params, opts +func (_m *ClientService) CreateBSCTransaction(params *wallet.CreateBSCTransactionParams, opts ...wallet.ClientOption) (*wallet.CreateBSCTransactionCreated, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, params) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *wallet.CreateBSCTransactionCreated + var r1 error + if rf, ok := ret.Get(0).(func(*wallet.CreateBSCTransactionParams, ...wallet.ClientOption) (*wallet.CreateBSCTransactionCreated, error)); ok { + return rf(params, opts...) + } + if rf, ok := ret.Get(0).(func(*wallet.CreateBSCTransactionParams, ...wallet.ClientOption) *wallet.CreateBSCTransactionCreated); ok { + r0 = rf(params, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*wallet.CreateBSCTransactionCreated) + } + } + + if rf, ok := ret.Get(1).(func(*wallet.CreateBSCTransactionParams, ...wallet.ClientOption) error); ok { + r1 = rf(params, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // CreateEthereumTransaction provides a mock function with given fields: params, opts func (_m *ClientService) CreateEthereumTransaction(params *wallet.CreateEthereumTransactionParams, opts ...wallet.ClientOption) (*wallet.CreateEthereumTransactionCreated, error) { _va := make([]interface{}, len(opts)) @@ -26,6 +59,10 @@ func (_m *ClientService) CreateEthereumTransaction(params *wallet.CreateEthereum ret := _m.Called(_ca...) var r0 *wallet.CreateEthereumTransactionCreated + var r1 error + if rf, ok := ret.Get(0).(func(*wallet.CreateEthereumTransactionParams, ...wallet.ClientOption) (*wallet.CreateEthereumTransactionCreated, error)); ok { + return rf(params, opts...) + } if rf, ok := ret.Get(0).(func(*wallet.CreateEthereumTransactionParams, ...wallet.ClientOption) *wallet.CreateEthereumTransactionCreated); ok { r0 = rf(params, opts...) } else { @@ -34,7 +71,6 @@ func (_m *ClientService) CreateEthereumTransaction(params *wallet.CreateEthereum } } - var r1 error if rf, ok := ret.Get(1).(func(*wallet.CreateEthereumTransactionParams, ...wallet.ClientOption) error); ok { r1 = rf(params, opts...) } else { @@ -56,6 +92,10 @@ func (_m *ClientService) CreateMaticTransaction(params *wallet.CreateMaticTransa ret := _m.Called(_ca...) var r0 *wallet.CreateMaticTransactionCreated + var r1 error + if rf, ok := ret.Get(0).(func(*wallet.CreateMaticTransactionParams, ...wallet.ClientOption) (*wallet.CreateMaticTransactionCreated, error)); ok { + return rf(params, opts...) + } if rf, ok := ret.Get(0).(func(*wallet.CreateMaticTransactionParams, ...wallet.ClientOption) *wallet.CreateMaticTransactionCreated); ok { r0 = rf(params, opts...) } else { @@ -64,7 +104,6 @@ func (_m *ClientService) CreateMaticTransaction(params *wallet.CreateMaticTransa } } - var r1 error if rf, ok := ret.Get(1).(func(*wallet.CreateMaticTransactionParams, ...wallet.ClientOption) error); ok { r1 = rf(params, opts...) } else { @@ -86,6 +125,10 @@ func (_m *ClientService) CreateTronTransaction(params *wallet.CreateTronTransact ret := _m.Called(_ca...) var r0 *wallet.CreateTronTransactionCreated + var r1 error + if rf, ok := ret.Get(0).(func(*wallet.CreateTronTransactionParams, ...wallet.ClientOption) (*wallet.CreateTronTransactionCreated, error)); ok { + return rf(params, opts...) + } if rf, ok := ret.Get(0).(func(*wallet.CreateTronTransactionParams, ...wallet.ClientOption) *wallet.CreateTronTransactionCreated); ok { r0 = rf(params, opts...) } else { @@ -94,7 +137,6 @@ func (_m *ClientService) CreateTronTransaction(params *wallet.CreateTronTransact } } - var r1 error if rf, ok := ret.Get(1).(func(*wallet.CreateTronTransactionParams, ...wallet.ClientOption) error); ok { r1 = rf(params, opts...) } else { @@ -116,6 +158,10 @@ func (_m *ClientService) CreateWallet(params *wallet.CreateWalletParams, opts .. ret := _m.Called(_ca...) var r0 *wallet.CreateWalletCreated + var r1 error + if rf, ok := ret.Get(0).(func(*wallet.CreateWalletParams, ...wallet.ClientOption) (*wallet.CreateWalletCreated, error)); ok { + return rf(params, opts...) + } if rf, ok := ret.Get(0).(func(*wallet.CreateWalletParams, ...wallet.ClientOption) *wallet.CreateWalletCreated); ok { r0 = rf(params, opts...) } else { @@ -124,7 +170,6 @@ func (_m *ClientService) CreateWallet(params *wallet.CreateWalletParams, opts .. } } - var r1 error if rf, ok := ret.Get(1).(func(*wallet.CreateWalletParams, ...wallet.ClientOption) error); ok { r1 = rf(params, opts...) } else { @@ -146,6 +191,10 @@ func (_m *ClientService) DeleteWallet(params *wallet.DeleteWalletParams, opts .. ret := _m.Called(_ca...) var r0 *wallet.DeleteWalletNoContent + var r1 error + if rf, ok := ret.Get(0).(func(*wallet.DeleteWalletParams, ...wallet.ClientOption) (*wallet.DeleteWalletNoContent, error)); ok { + return rf(params, opts...) + } if rf, ok := ret.Get(0).(func(*wallet.DeleteWalletParams, ...wallet.ClientOption) *wallet.DeleteWalletNoContent); ok { r0 = rf(params, opts...) } else { @@ -154,7 +203,6 @@ func (_m *ClientService) DeleteWallet(params *wallet.DeleteWalletParams, opts .. } } - var r1 error if rf, ok := ret.Get(1).(func(*wallet.DeleteWalletParams, ...wallet.ClientOption) error); ok { r1 = rf(params, opts...) } else { @@ -176,6 +224,10 @@ func (_m *ClientService) GetWallet(params *wallet.GetWalletParams, opts ...walle ret := _m.Called(_ca...) var r0 *wallet.GetWalletOK + var r1 error + if rf, ok := ret.Get(0).(func(*wallet.GetWalletParams, ...wallet.ClientOption) (*wallet.GetWalletOK, error)); ok { + return rf(params, opts...) + } if rf, ok := ret.Get(0).(func(*wallet.GetWalletParams, ...wallet.ClientOption) *wallet.GetWalletOK); ok { r0 = rf(params, opts...) } else { @@ -184,7 +236,6 @@ func (_m *ClientService) GetWallet(params *wallet.GetWalletParams, opts ...walle } } - var r1 error if rf, ok := ret.Get(1).(func(*wallet.GetWalletParams, ...wallet.ClientOption) error); ok { r1 = rf(params, opts...) } else { @@ -199,13 +250,12 @@ func (_m *ClientService) SetTransport(transport runtime.ClientTransport) { _m.Called(transport) } -type mockConstructorTestingTNewClientService interface { +// NewClientService creates a new instance of ClientService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewClientService(t interface { mock.TestingT Cleanup(func()) -} - -// NewClientService creates a new instance of ClientService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewClientService(t mockConstructorTestingTNewClientService) *ClientService { +}) *ClientService { mock := &ClientService{} mock.Mock.Test(t) diff --git a/pkg/api-kms/v1/model/b_s_c_transaction.go b/pkg/api-kms/v1/model/b_s_c_transaction.go new file mode 100644 index 0000000..54be04f --- /dev/null +++ b/pkg/api-kms/v1/model/b_s_c_transaction.go @@ -0,0 +1,51 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package model + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// BSCTransaction b s c transaction +// +// swagger:model bSCTransaction +type BSCTransaction struct { + + // RLP-encoded transaction + // Example: 0xf86e83014b2985048ccb44b1827530944675c7e5baafbffbca748158becba61ef3b0a26387c2a454bcf91b3f8026a0db0be3dcc25213b286e08d018fe8143eb85a3b7bb5cf3749245e907158e9c8daa033c7ec9362ee890d63b89e9dbfcfcb6edd9432321102c1d2ea7921c6cc07009e + RawTransaction string `json:"rawTransaction"` +} + +// Validate validates this b s c transaction +func (m *BSCTransaction) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this b s c transaction based on context it is used +func (m *BSCTransaction) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *BSCTransaction) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *BSCTransaction) UnmarshalBinary(b []byte) error { + var res BSCTransaction + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/api-kms/v1/model/blockchain.go b/pkg/api-kms/v1/model/blockchain.go index 9f01f16..faf3944 100644 --- a/pkg/api-kms/v1/model/blockchain.go +++ b/pkg/api-kms/v1/model/blockchain.go @@ -32,6 +32,9 @@ const ( // BlockchainMATIC captures enum value "MATIC" BlockchainMATIC Blockchain = "MATIC" + + // BlockchainBSC captures enum value "BSC" + BlockchainBSC Blockchain = "BSC" ) // for schema @@ -39,7 +42,7 @@ var blockchainEnum []interface{} func init() { var res []Blockchain - if err := json.Unmarshal([]byte(`["BTC","ETH","TRON","MATIC"]`), &res); err != nil { + if err := json.Unmarshal([]byte(`["BTC","ETH","TRON","MATIC","BSC"]`), &res); err != nil { panic(err) } for _, v := range res { diff --git a/pkg/api-kms/v1/model/create_b_s_c_transaction_request.go b/pkg/api-kms/v1/model/create_b_s_c_transaction_request.go new file mode 100644 index 0000000..eeccc4a --- /dev/null +++ b/pkg/api-kms/v1/model/create_b_s_c_transaction_request.go @@ -0,0 +1,239 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package model + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// CreateBSCTransactionRequest create b s c transaction request +// +// swagger:model createBSCTransactionRequest +type CreateBSCTransactionRequest struct { + + // Raw amount in wei or contract decimals + // Example: 100000000000000000 + // Required: true + Amount string `json:"amount"` + + // asset type + // Required: true + AssetType AssetType `json:"assetType"` + + // ERC-20 contract address + // Example: 0x5e41bc5922370522800103f826c3bb9cd5d83f1a + ContractAddress string `json:"contractAddress,omitempty"` + + // Transaction Gas amount + // Example: 3 + // Required: true + // Minimum: 1 + Gas int64 `json:"gas"` + + // Max Fee Per Gas (wei) + // Example: 200000000 + // Required: true + MaxFeePerGas string `json:"maxFeePerGas"` + + // Max Priority Fee Per Gas (wei) + // Example: 2000000 + // Required: true + MaxPriorityPerGas string `json:"maxPriorityPerGas"` + + // Network (chain) Id + // Example: 1 + // Required: true + NetworkID int64 `json:"networkId"` + + // Transaction nonce + // Example: 40 + // Required: true + // Minimum: 0 + Nonce *int64 `json:"nonce"` + + // Recipient address + // Example: 0x5e41bc5922370522800103f826c3bb9cd5d83f1a + // Required: true + Recipient string `json:"recipient"` +} + +// Validate validates this create b s c transaction request +func (m *CreateBSCTransactionRequest) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateAmount(formats); err != nil { + res = append(res, err) + } + + if err := m.validateAssetType(formats); err != nil { + res = append(res, err) + } + + if err := m.validateGas(formats); err != nil { + res = append(res, err) + } + + if err := m.validateMaxFeePerGas(formats); err != nil { + res = append(res, err) + } + + if err := m.validateMaxPriorityPerGas(formats); err != nil { + res = append(res, err) + } + + if err := m.validateNetworkID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateNonce(formats); err != nil { + res = append(res, err) + } + + if err := m.validateRecipient(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *CreateBSCTransactionRequest) validateAmount(formats strfmt.Registry) error { + + if err := validate.RequiredString("amount", "body", m.Amount); err != nil { + return err + } + + return nil +} + +func (m *CreateBSCTransactionRequest) validateAssetType(formats strfmt.Registry) error { + + if err := validate.Required("assetType", "body", AssetType(m.AssetType)); err != nil { + return err + } + + if err := m.AssetType.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("assetType") + } + return err + } + + return nil +} + +func (m *CreateBSCTransactionRequest) validateGas(formats strfmt.Registry) error { + + if err := validate.Required("gas", "body", int64(m.Gas)); err != nil { + return err + } + + if err := validate.MinimumInt("gas", "body", m.Gas, 1, false); err != nil { + return err + } + + return nil +} + +func (m *CreateBSCTransactionRequest) validateMaxFeePerGas(formats strfmt.Registry) error { + + if err := validate.RequiredString("maxFeePerGas", "body", m.MaxFeePerGas); err != nil { + return err + } + + return nil +} + +func (m *CreateBSCTransactionRequest) validateMaxPriorityPerGas(formats strfmt.Registry) error { + + if err := validate.RequiredString("maxPriorityPerGas", "body", m.MaxPriorityPerGas); err != nil { + return err + } + + return nil +} + +func (m *CreateBSCTransactionRequest) validateNetworkID(formats strfmt.Registry) error { + + if err := validate.Required("networkId", "body", int64(m.NetworkID)); err != nil { + return err + } + + return nil +} + +func (m *CreateBSCTransactionRequest) validateNonce(formats strfmt.Registry) error { + + if err := validate.Required("nonce", "body", m.Nonce); err != nil { + return err + } + + if err := validate.MinimumInt("nonce", "body", *m.Nonce, 0, false); err != nil { + return err + } + + return nil +} + +func (m *CreateBSCTransactionRequest) validateRecipient(formats strfmt.Registry) error { + + if err := validate.RequiredString("recipient", "body", m.Recipient); err != nil { + return err + } + + return nil +} + +// ContextValidate validate this create b s c transaction request based on the context it is used +func (m *CreateBSCTransactionRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateAssetType(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *CreateBSCTransactionRequest) contextValidateAssetType(ctx context.Context, formats strfmt.Registry) error { + + if err := m.AssetType.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("assetType") + } + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *CreateBSCTransactionRequest) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *CreateBSCTransactionRequest) UnmarshalBinary(b []byte) error { + var res CreateBSCTransactionRequest + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/ui-dashboard/src/assets/icons/crypto/bnb.svg b/ui-dashboard/src/assets/icons/crypto/bnb.svg new file mode 100644 index 0000000..f39dba1 --- /dev/null +++ b/ui-dashboard/src/assets/icons/crypto/bnb.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/ui-dashboard/src/types/index.ts b/ui-dashboard/src/types/index.ts index 9964c87..3b67533 100644 --- a/ui-dashboard/src/types/index.ts +++ b/ui-dashboard/src/types/index.ts @@ -15,10 +15,21 @@ interface WebhookSettings { url: string; } -const BLOCKCHAIN = ["ETH", "TRON", "MATIC"] as const; +const BLOCKCHAIN = ["ETH", "TRON", "MATIC", "BSC"] as const; type Blockchain = typeof BLOCKCHAIN[number]; -const BLOCKCHAIN_TICKER = ["ETH", "ETH_USDT", "MATIC", "MATIC_USDT", "TRON", "TRON_USDT"] as const; +const BLOCKCHAIN_TICKER = [ + "ETH", + "ETH_USDT", + "ETH_USDC", + "MATIC", + "MATIC_USDT", + "MATIC_USDC", + "TRON", + "TRON_USDT", + "BNB" +] as const; + type BlockchainTicker = typeof BLOCKCHAIN_TICKER[number]; interface PaymentMethod { @@ -81,10 +92,13 @@ const CURRENCY_SYMBOL: Record = { EUR: "€", ETH: "", ETH_USDT: "", + ETH_USDC: "", MATIC: "", MATIC_USDT: "", + MATIC_USDC: "", TRON: "", - TRON_USDT: "" + TRON_USDT: "", + BNB: "" }; type PaymentType = "payment" | "withdrawal"; diff --git a/ui-payment/src/assets/icons/crypto/bnb.svg b/ui-payment/src/assets/icons/crypto/bnb.svg new file mode 100644 index 0000000..f39dba1 --- /dev/null +++ b/ui-payment/src/assets/icons/crypto/bnb.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + From a3d6854f66217e917559d7e5631916ce563dbc26 Mon Sep 17 00:00:00 2001 From: BuzzLightyear <11892559+swift1337@users.noreply.github.com> Date: Sun, 23 Jul 2023 17:53:00 +0300 Subject: [PATCH 2/4] Add USDT token for BSC (#12) --- internal/server/http/webhook/tatum_test.go | 21 +++++++++++++ internal/service/blockchain/currencies.json | 14 +++++++++ .../processing/service_incoming_test.go | 30 +++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/internal/server/http/webhook/tatum_test.go b/internal/server/http/webhook/tatum_test.go index 3346041..176e1f9 100644 --- a/internal/server/http/webhook/tatum_test.go +++ b/internal/server/http/webhook/tatum_test.go @@ -26,6 +26,7 @@ const ( paramNetworkID = "networkId" EthUsdAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7" + BSCUSDTAddress = "0x55d398326f99059fF775485246999027B3197955" TronUsdAddressMainnet = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" TronUsdAddressTestnet = "TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs" @@ -202,6 +203,26 @@ func TestHandler_ReceiveTatum(t *testing.T) { assertUpdateStatusEventSent(t, false) }, }, + { + name: "success BSC_USDT", + selectedCurrency: "BSC_USDT", + payment: func() *payment.Payment { return setupPayment(money.USD, 50) }, + req: func(tx *transaction.Transaction, wt *wallet.Wallet) *processing.TatumWebhook { + return webhook(wt.Address, "0xTX_BSC", BSCUSDTAddress, typeToken, "50") + }, + assert: func(t *testing.T, pt *payment.Payment, tx *transaction.Transaction) { + assert.Equal(t, payment.StatusInProgress, pt.Status) + + assert.Equal(t, transaction.StatusInProgress, tx.Status) + assert.Equal(t, "50", tx.FactAmount.String()) + assert.Equal(t, "0xTX_BSC", *tx.HashID) + assert.Equal(t, "0x123sender456", *tx.SenderAddress) + + assertUpdateStatusEventSent(t, true) + + tc.AssertTableRows(t, "wallet_locks", 0) + }, + }, { // Imitation of Tatum's "weird" webhook when they send ticker instead of contract address name: "success TRON_USDT", diff --git a/internal/service/blockchain/currencies.json b/internal/service/blockchain/currencies.json index 5fe59a7..14d6971 100644 --- a/internal/service/blockchain/currencies.json +++ b/internal/service/blockchain/currencies.json @@ -113,5 +113,19 @@ "testNetworkId": "97", "minimal_withdrawal_amount_usd": "10", "minimal_instant_internal_transfer_amount_usd": "30" + }, + { + "blockchain": "BSC", + "blockchainName": "BNB Chain", + "ticker": "BSC_USDT", + "type": "token", + "name": "USDT", + "tokenAddress": "0x55d398326f99059fF775485246999027B3197955", + "testTokenAddress": "0xed24fc36d5ee211ea25a80239fb8c4cfd80f12ee", + "decimals": "18", + "networkId": "56", + "testNetworkId": "97", + "minimal_withdrawal_amount_usd": "10", + "minimal_instant_internal_transfer_amount_usd": "30" } ] \ No newline at end of file diff --git a/internal/service/processing/service_incoming_test.go b/internal/service/processing/service_incoming_test.go index 3d948ea..97012c3 100644 --- a/internal/service/processing/service_incoming_test.go +++ b/internal/service/processing/service_incoming_test.go @@ -34,6 +34,7 @@ func TestService_BatchCheckIncomingTransactions(t *testing.T) { ethUSDT := tc.Must.GetCurrency(t, "ETH_USDT") tron := tc.Must.GetCurrency(t, "TRON") bnb := tc.Must.GetCurrency(t, "BNB") + bscUSDT := tc.Must.GetCurrency(t, "BSC_USDT") // Given shortcut for imitating incoming tx incomingTX := func(fiat money.FiatCurrency, price float64, crypto money.CryptoCurrency, isTest bool) *transaction.Transaction { @@ -254,6 +255,35 @@ func TestService_BatchCheckIncomingTransactions(t *testing.T) { assertUpdateStatusEventSent(t, true) }, }, + { + name: "success BSC USDT", + transaction: func(isTest bool) *transaction.Transaction { + tx := incomingTX(money.USD, 100, bscUSDT, isTest) + factAmount := lo.Must(tx.Currency.MakeAmount("100_000_000_000_000_000_000")) + + return whReceived(tx, "0x123-hash-abc", factAmount, transaction.StatusInProgress) + }, + receipt: makeReceipt(10, true, true), + assert: func(t *testing.T, tx *transaction.Transaction, pt *payment.Payment) { + assert.Equal(t, payment.StatusSuccess, pt.Status) + + assert.Equal(t, transaction.StatusCompleted, tx.Status) + assert.Equal(t, tx.Amount, *tx.FactAmount) + assert.False(t, tx.ServiceFee.IsZero()) + assert.Equal(t, "BNB", tx.NetworkFee.Ticker()) + + wtBalance, mtBalance := loadBalances(t, tx) + + assert.Equal(t, "100", wtBalance.Amount.String()) + assert.Equal(t, "98.500000000000000056", mtBalance.Amount.String()) + assert.Equal(t, "BSC_USDT", mtBalance.Currency) + assert.Equal(t, "BSC", wtBalance.Network) + + tc.AssertTableRows(t, "wallet_locks", 0) + + assertUpdateStatusEventSent(t, true) + }, + }, { name: "success TRON: network fee is not zero", transaction: func(isTest bool) *transaction.Transaction { From 7979dbd919e14bdeb9e0e0848cb473191b78da48 Mon Sep 17 00:00:00 2001 From: BuzzLightyear <11892559+swift1337@users.noreply.github.com> Date: Sun, 23 Jul 2023 18:19:06 +0300 Subject: [PATCH 3/4] Add BUSD token for BSC (#13) --- internal/service/blockchain/currencies.json | 14 ++++++++++++++ ui-dashboard/src/assets/icons/crypto/busd.svg | 12 ++++++++++++ .../src/pages/balance-page/balance-page.tsx | 11 +++-------- ui-dashboard/src/types/index.ts | 8 ++++++-- ui-payment/src/assets/icons/crypto/busd.svg | 12 ++++++++++++ 5 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 ui-dashboard/src/assets/icons/crypto/busd.svg create mode 100644 ui-payment/src/assets/icons/crypto/busd.svg diff --git a/internal/service/blockchain/currencies.json b/internal/service/blockchain/currencies.json index 14d6971..90ed399 100644 --- a/internal/service/blockchain/currencies.json +++ b/internal/service/blockchain/currencies.json @@ -121,6 +121,20 @@ "type": "token", "name": "USDT", "tokenAddress": "0x55d398326f99059fF775485246999027B3197955", + "testTokenAddress": "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd", + "decimals": "18", + "networkId": "56", + "testNetworkId": "97", + "minimal_withdrawal_amount_usd": "10", + "minimal_instant_internal_transfer_amount_usd": "30" + }, + { + "blockchain": "BSC", + "blockchainName": "BNB Chain", + "ticker": "BSC_BUSD", + "type": "token", + "name": "BUSD", + "tokenAddress": "0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56", "testTokenAddress": "0xed24fc36d5ee211ea25a80239fb8c4cfd80f12ee", "decimals": "18", "networkId": "56", diff --git a/ui-dashboard/src/assets/icons/crypto/busd.svg b/ui-dashboard/src/assets/icons/crypto/busd.svg new file mode 100644 index 0000000..9d3224c --- /dev/null +++ b/ui-dashboard/src/assets/icons/crypto/busd.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/ui-dashboard/src/pages/balance-page/balance-page.tsx b/ui-dashboard/src/pages/balance-page/balance-page.tsx index c3e81c1..ce21378 100644 --- a/ui-dashboard/src/pages/balance-page/balance-page.tsx +++ b/ui-dashboard/src/pages/balance-page/balance-page.tsx @@ -39,15 +39,10 @@ const BalancePage: React.FC = () => { const {merchantId} = useSharedMerchantId(); const renderIconName = (name: string) => { - if (name.length < 4) { - return name; - } - - if (name.slice(-4) == "usdt") { - return "usdt"; - } + // ETH or ETH_USDT => "eth" or "usdt" + const lowered = name.toLowerCase(); - return name; + return lowered.includes("_") ? lowered.split("_")[1] : lowered; }; const balancesColumns: ColumnsType = [ diff --git a/ui-dashboard/src/types/index.ts b/ui-dashboard/src/types/index.ts index 3b67533..f70c892 100644 --- a/ui-dashboard/src/types/index.ts +++ b/ui-dashboard/src/types/index.ts @@ -27,7 +27,9 @@ const BLOCKCHAIN_TICKER = [ "MATIC_USDC", "TRON", "TRON_USDT", - "BNB" + "BNB", + "BSC_USDT", + "BSC_BUSD" ] as const; type BlockchainTicker = typeof BLOCKCHAIN_TICKER[number]; @@ -98,7 +100,9 @@ const CURRENCY_SYMBOL: Record = { MATIC_USDC: "", TRON: "", TRON_USDT: "", - BNB: "" + BNB: "", + BSC_USDT: "", + BSC_BUSD: "" }; type PaymentType = "payment" | "withdrawal"; diff --git a/ui-payment/src/assets/icons/crypto/busd.svg b/ui-payment/src/assets/icons/crypto/busd.svg new file mode 100644 index 0000000..9d3224c --- /dev/null +++ b/ui-payment/src/assets/icons/crypto/busd.svg @@ -0,0 +1,12 @@ + + + + + + + + + + From f03af5e4868d0937ae75c410dadbbc7f1a16ee6f Mon Sep 17 00:00:00 2001 From: Dmitry S Date: Sun, 23 Jul 2023 17:23:15 +0200 Subject: [PATCH 4/4] Update readme --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c70d20a..c80662e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

[OxygenPay](https://o2pay.co) is a cloud or self-hosted crypto payment gateway. -Accept ETH, MATIC, TRON, USDT, and USDC with ease. Open new opportunities for your product by accepting cryptocurrency. +Receive crypto including stablecoins with ease. Open new opportunities for your product by accepting cryptocurrency. demo @@ -37,6 +37,10 @@ Accept ETH, MATIC, TRON, USDT, and USDC with ease. Open new opportunities for yo usdc
USDC
+ + busd +
BUSD
+ @@ -56,12 +60,13 @@ Accept ETH, MATIC, TRON, USDT, and USDC with ease. Open new opportunities for yo ## Documentation 📚 -Visit [docs.o2pay.co](https://docs.o2pay.co) for setup guides. If you have any questions, feel free to ask them in our [telegram community](https://t.me/oxygenpay_en) +Visit [docs.o2pay.co](https://docs.o2pay.co) for setup guides. If you have any questions, +feel free to ask them in our [telegram community](https://t.me/oxygenpay_en) ## Roadmap 🛣️ - [x] Support for USDC -- [ ] Support for Binance Smart Chain (BNB, BUSD) +- [x] Support for Binance Smart Chain (BNB, BUSD) - [ ] Donations feature - [ ] Support for [WalletConnect](https://walletconnect.com/) - [ ] SDKs for (Python, JavaScript, PHP, etc...)