@@ -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.
@@ -37,6 +37,10 @@ Accept ETH, MATIC, TRON, USDT, and USDC with ease. Open new opportunities for yo
USDC
+
+
+
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...)