diff --git a/pkg/service/checkout.go b/pkg/service/checkout.go
index 916a9bd2..62e1f847 100644
--- a/pkg/service/checkout.go
+++ b/pkg/service/checkout.go
@@ -74,7 +74,7 @@ func AuthorizeCharge(amount float64, userWallet string, tokenId string) (auth Au
Type: checkoutCommon.Card,
Number: "4242424242424242", // Success
// Number: "4273149019799094", // succeed authorize, fail capture
- // Number: "4544249167673670", // Declined - Insufficient funds
+ // Number: "4544249167673670", // Declined - Insufficient funds
// Number: "5148447461737269", // Invalid transaction
ExpiryMonth: 2,
ExpiryYear: 2024,
diff --git a/pkg/service/transaction.go b/pkg/service/transaction.go
index f305a5b3..5341a402 100644
--- a/pkg/service/transaction.go
+++ b/pkg/service/transaction.go
@@ -50,6 +50,22 @@ type transaction struct {
ids InternalIds
}
+type transactionProcessingData struct {
+ userId *string
+ deviceId *string
+ executor *Executor
+ processingFeeAsset *model.Asset
+ transactionModel *model.Transaction
+ chain *Chain
+ executionRequest *model.ExecutionRequest
+ cardAuthorization *AuthorizedCharge
+ preBalance *float64
+ recipientWalletId *string
+ txId *string
+ cumulativeValue *big.Int
+ trueGas *uint64
+}
+
func NewTransaction(repos repository.Repositories, redis store.RedisStore) Transaction {
return &transaction{repos: repos, redis: redis}
}
@@ -89,214 +105,264 @@ func (t transaction) Quote(d model.TransactionRequest) (model.ExecutionRequest,
return res, nil
}
-func (t transaction) Execute(e model.ExecutionRequest, userId string, deviceId string) (model.TransactionReceipt, error) {
- res := model.TransactionReceipt{}
+func (t transaction) Execute(e model.ExecutionRequest, userId string, deviceId string) (res model.TransactionReceipt, err error) {
t.getStringInstrumentsAndUserId()
- user, err := t.repos.User.GetById(userId)
+ p := transactionProcessingData{executionRequest: &e, userId: &userId, deviceId: &deviceId}
+
+ // Pre-flight transaction setup
+ p, err = t.transactionSetup(p)
if err != nil {
return res, common.StringError(err)
}
- if user.ID != userId {
- return res, common.StringError(errors.New("not logged in"))
- }
- // Pull chain info needed for execution from repository
- chain, err := ChainInfo(uint64(e.ChainID), t.repos.Network, t.repos.Asset)
+ // Run safety checks
+ p, err = t.safetyCheck(p)
if err != nil {
return res, common.StringError(err)
}
- // Create new Tx in repository, populate it with known info
- db, err := t.repos.Transaction.Create(model.Transaction{Status: "Created", NetworkID: chain.UUID, DeviceID: deviceId, PlatformID: t.ids.StringPlatformId})
+ // Send request to the blockchain and update model status, hash, transaction amount
+ p, err = t.initiateTransaction(p)
if err != nil {
return res, common.StringError(err)
}
- updateDB := &model.TransactionUpdates{}
- processingFeeAsset, err := t.populateInitialTxModelData(e, updateDB)
+ // this Executor will not exist in scope of postProcess
+ (*p.executor).Close()
+
+ // Send required information to new thread and return txId to the endpoint
+ go t.postProcess(p)
+
+ return model.TransactionReceipt{TxID: *p.txId, TxURL: p.chain.Explorer + "/tx/" + *p.txId}, nil
+}
+
+func (t transaction) postProcess(p transactionProcessingData) {
+ // Reinitialize Executor
+ executor := NewExecutor()
+ p.executor = &executor
+ err := executor.Initialize(p.chain.RPC)
if err != nil {
- return res, common.StringError(err)
+ log.Printf("Failed to initialized executor in postProcess: %s", common.StringError(err))
+ // TODO: Handle error instead of returning it
}
- err = t.repos.Transaction.Update(db.ID, updateDB)
+
+ // Update TX Status
+ updateDB := model.TransactionUpdates{}
+ status := "Post Process RPC Dialed"
+ updateDB.Status = &status
+ err = t.repos.Transaction.Update(p.transactionModel.ID, updateDB)
if err != nil {
- fmt.Printf("\nERROR = %+v", err)
- return res, common.StringError(err)
+ log.Printf("Failed to update transaction repo with status 'Post Process RPC Dialed': %s", common.StringError(err))
+ // TODO: Handle error instead of returning it
}
- // Dial the RPC and update model status
- executor := NewExecutor()
- err = executor.Initialize(chain.RPC)
+ // confirm the Tx on the EVM
+ trueGas, err := confirmTx(executor, *p.txId)
+ p.trueGas = &trueGas
if err != nil {
- return res, common.StringError(err)
+ log.Printf("Failed to confirm transaction: %s", common.StringError(err))
+ // TODO: Handle error instead of returning it
}
- status := "RPC Dialed"
+
+ // Update DB status and NetworkFee
+ status = "Tx Confirmed"
updateDB.Status = &status
- err = t.repos.Transaction.Update(db.ID, updateDB)
+ networkFee := strconv.FormatUint(trueGas, 10)
+ updateDB.NetworkFee = &networkFee // geth uses uint64 for gas
+ err = t.repos.Transaction.Update(p.transactionModel.ID, updateDB)
if err != nil {
- return res, common.StringError(err)
+ log.Printf("Failed to update transaction repo with status 'Tx Confirmed': %s", common.StringError(err))
+ // TODO: Handle error instead of returning it
}
- // Test the Tx and update model status
- estimateUSD, estimateETH, err := t.testTransaction(executor, e.TransactionRequest, chain, false)
+ // Get new string wallet balance after executing the transaction
+ postBalance, err := executor.GetBalance()
if err != nil {
- return res, common.StringError(err)
+ log.Printf("Failed to get executor balance: %s", common.StringError(err))
+ // TODO: handle error instead of returning it
}
- status = "Tested and Estimated"
- updateDB.Status = &status
- err = t.repos.Transaction.Update(db.ID, updateDB)
- if err != nil {
- return res, common.StringError(err)
+
+ // We can close the executor because we aren't using it after this
+ executor.Close()
+
+ // If threshold was crossed, notify devs
+ // TODO: store threshold on a per-network basis in the repo
+ threshold := 10.0
+ if *p.preBalance >= threshold && postBalance < threshold {
+ msg := fmt.Sprintf("STRING-API: %s balance is < %.2f at %.2f", p.chain.OwlracleName, threshold, postBalance)
+ err = MessageStaff(msg)
+ if err != nil {
+ log.Printf("Failed to send staff with low balance threshold message: %s", common.StringError(err))
+ // Not seeing any e
+ // TODO: handle error instead of returning it
+ }
}
- // Verify the Quote and update model status
- _, err = verifyQuote(e, estimateUSD)
+ // compute profit
+ // TODO: factor request.processingFeeAsset in the event of crypto-to-usd
+ profit, err := t.tenderTransaction(p)
if err != nil {
- return res, common.StringError(err)
+ log.Printf("Failed to tender transaction: %s", common.StringError(err))
+ // TODO: Handle error instead of returning it
}
- status = "Quote Verified"
+ stringFee := floatToFixedString(profit, 6)
+ processingFee := floatToFixedString(profit, 6) // TODO: set processingFee based on payment method, and location
+
+ // update db status and processing fees to db
+ updateDB.StringFee = &stringFee // string fee is always USD with 6 digits
+ updateDB.ProcessingFee = &processingFee
+ status = "Profit Tendered"
updateDB.Status = &status
- err = t.repos.Transaction.Update(db.ID, updateDB)
+ err = t.repos.Transaction.Update(p.transactionModel.ID, updateDB)
if err != nil {
- return res, common.StringError(err)
+ log.Printf("Failed to update transaction repo with status 'Profit Tendered': %s", common.StringError(err))
+ // TODO: Handle error instead of returning it
}
- // Get current balance of primary token
- preBalance, err := executor.GetBalance()
+ // charge the users CC
+ err = t.chargeCard(p)
if err != nil {
- return res, common.StringError(err)
- }
- if preBalance < estimateETH {
- msg := fmt.Sprintf("STRING-API: %s balance is too low to execute %.2f transaction at %.2f", chain.OwlracleName, estimateETH, preBalance)
- MessageStaff(msg)
- return res, common.StringError(errors.New("hot wallet ETH balance too low"))
+ log.Printf("Error, failed to charge card: %+v", common.StringError(err))
+ // TODO: Handle error instead of returning it
}
- // Authorize quoted cost on end-user CC and update model status
- cardAuthorization, err := t.authCard(e.UserAddress, e.CardToken, e.TotalUSD, processingFeeAsset, db.ID, userId)
+ // Update status upon success
+ status = "Card Charged"
+ updateDB.Status = &status
+ // TODO: Figure out how much we paid the CC payment processor and deduct it
+ // and use it to populate processing_fee and processing_fee_asset in the table
+ err = t.repos.Transaction.Update(p.transactionModel.ID, updateDB)
if err != nil {
- return res, common.StringError(err)
+ log.Printf("Failed to update transaction repo with status 'Card Charged': %s", common.StringError(err))
+ // TODO: Handle error instead of returning it
}
- status = "Card " + cardAuthorization.Status
+ // Transaction complete! Update status
+ status = "Completed"
updateDB.Status = &status
- err = t.repos.Transaction.Update(db.ID, updateDB)
+ err = t.repos.Transaction.Update(p.transactionModel.ID, updateDB)
if err != nil {
- return res, common.StringError(err)
+ log.Printf("Failed to update transaction repo with status 'Completed': %s", common.StringError(err))
}
- recipientWalletId, err := t.addWalletInstrumentIdIfNew(e.UserAddress, user.ID)
+ // Create Transaction data in Unit21
+ err = t.unit21CreateTransaction(p.transactionModel.ID)
if err != nil {
- return res, common.StringError(err)
+ log.Printf("Error creating Unit21 transaction: %s", common.StringError(err))
}
- // TODO: Determine the output of the transaction (destination leg) with Tracers
- destinationLeg := model.TxLeg{
- Timestamp: time.Now(), // Required by the db. Should be updated when the tx occurs
- Amount: "0", // Required by Unit21. The amount of the asset received by the user
- Value: "0", // Default to '0'. The value of the asset received by the user
- AssetID: chain.GasTokenID, // Required by the db. the asset received by the user
- UserID: user.ID, // the user who received the asset
- InstrumentID: recipientWalletId, // Required by the db. the instrument which received the asset (wallet usually)
+ // send email receipt
+ err = t.sendEmailReceipt(p)
+ if err != nil {
+ log.Printf("Error sending email receipt to user: %s", common.StringError(err))
}
+}
- destinationLeg, err = t.repos.TxLeg.Create(destinationLeg)
+func (t transaction) transactionSetup(p transactionProcessingData) (transactionProcessingData, error) {
+ // get user object
+ _, err := t.repos.User.GetById(*p.userId)
if err != nil {
- return res, common.StringError(err)
+ return p, common.StringError(err)
}
- txLeg := model.TransactionUpdates{DestinationTxLegID: &destinationLeg.ID}
-
- err = t.repos.Transaction.Update(db.ID, txLeg)
+ // Pull chain info needed for execution from repository
+ chain, err := ChainInfo(uint64(p.executionRequest.ChainID), t.repos.Network, t.repos.Asset)
+ p.chain = &chain
if err != nil {
- return res, common.StringError(err)
+ return p, common.StringError(err)
}
- if !cardAuthorization.Approved {
- err := t.unit21CreateTransaction(db.ID)
- if err != nil {
- return res, common.StringError(err)
- }
+ // Create new Tx in repository, populate it with known info
+ transactionModel, err := t.repos.Transaction.Create(model.Transaction{Status: "Created", NetworkID: chain.UUID, DeviceID: *p.deviceId, PlatformID: t.ids.StringPlatformId})
+ p.transactionModel = &transactionModel
+ if err != nil {
+ return p, common.StringError(err)
+ }
- return res, common.StringError(common.StringError(errors.New("payment: Authorization Declined by Checkout")))
+ updateDB := &model.TransactionUpdates{}
+ processingFeeAsset, err := t.populateInitialTxModelData(*p.executionRequest, updateDB)
+ p.processingFeeAsset = &processingFeeAsset
+ if err != nil {
+ return p, common.StringError(err)
+ }
+ err = t.repos.Transaction.Update(transactionModel.ID, updateDB)
+ if err != nil {
+ fmt.Printf("\nERROR = %+v", common.StringError(err))
+ return p, common.StringError(err)
}
- // Validate Transaction through Real Time Rules engine
- u21auth, err := t.unit21Evaluate(db.ID)
+ // Dial the RPC and update model status
+ executor := NewExecutor()
+ p.executor = &executor
+ err = executor.Initialize(chain.RPC)
if err != nil {
- return res, common.StringError(err)
+ return p, common.StringError(err)
}
- if !u21auth {
- status = "Failed"
- updateDB.Status = &status
- err = t.repos.Transaction.Update(db.ID, updateDB)
- if err != nil {
- return res, common.StringError(err)
- }
+ err = t.updateTransactionStatus("RPC Dialed", transactionModel.ID)
+ if err != nil {
+ return p, common.StringError(err)
+ }
- err = t.unit21CreateTransaction(db.ID)
- if err != nil {
- return res, common.StringError(err)
- }
+ return p, err
+}
- return res, common.StringError(errors.New("risk: Transaction Failed Unit21 Real Time Rules Evaluation"))
+func (t transaction) safetyCheck(p transactionProcessingData) (transactionProcessingData, error) {
+ // Test the Tx and update model status
+ estimateUSD, estimateETH, err := t.testTransaction(*p.executor, p.executionRequest.TransactionRequest, *p.chain, false)
+ if err != nil {
+ return p, common.StringError(err)
}
- status = "Unit21 Authorized"
- updateDB.Status = &status
- err = t.repos.Transaction.Update(db.ID, updateDB)
+ err = t.updateTransactionStatus("Tested and Estimated", p.transactionModel.ID)
if err != nil {
- return res, common.StringError(err)
+ return p, common.StringError(err)
}
- // Send request to the blockchain and update model status, hash, transaction amount
- txID, value, err := t.initiateTransaction(executor, e, processingFeeAsset, db.ID, userId)
+ // Verify the Quote and update model status
+ _, err = verifyQuote(*p.executionRequest, estimateUSD)
if err != nil {
- return res, common.StringError(err)
+ return p, common.StringError(err)
}
- status = "Transaction Initiated"
- updateDB.Status = &status
- updateDB.TransactionHash = &txID
- txAmount := value.String()
- updateDB.TransactionAmount = &txAmount
- err = t.repos.Transaction.Update(db.ID, updateDB)
+ err = t.updateTransactionStatus("Quote Verified", p.transactionModel.ID)
if err != nil {
- return res, common.StringError(err)
+ return p, common.StringError(err)
}
- // this Executor will not exist in scope of postProcess
- executor.Close()
+ // Get current balance of primary token
+ preBalance, err := (*p.executor).GetBalance()
+ p.preBalance = &preBalance
+ if err != nil {
+ return p, common.StringError(err)
+ }
+ if preBalance < estimateETH {
+ msg := fmt.Sprintf("STRING-API: %s balance is too low to execute %.2f transaction at %.2f", p.chain.OwlracleName, estimateETH, preBalance)
+ MessageStaff(msg)
+ return p, common.StringError(errors.New("hot wallet ETH balance too low"))
+ }
- // Send required information to new thread and return TxID to the endpoint
- post := postProcessRequest{
- TxID: txID,
- Chain: chain,
- Authorization: cardAuthorization,
- UserAddress: e.UserAddress,
- CumulativeValue: value,
- Quote: e.Quote,
- TxDBID: db.ID,
- processingFeeAsset: processingFeeAsset,
- preBalance: preBalance,
- userId: userId,
- recipientWalletId: recipientWalletId,
- }
- go t.postProcess(post)
-
- return model.TransactionReceipt{TxID: txID, TxURL: chain.Explorer + "/tx/" + txID}, nil
-}
+ // Authorize quoted cost on end-user CC and update model status
+ p, err = t.authCard(p)
+ if err != nil {
+ return p, common.StringError(err)
+ }
-func (t *transaction) getStringInstrumentsAndUserId() {
- t.ids = GetStringIdsFromEnv()
+ // Validate Transaction through Real Time Rules engine
+ err = t.unit21Evaluate(p.transactionModel.ID)
+ if err != nil {
+ return p, common.StringError(err)
+ }
+
+ return p, nil
}
func (t transaction) populateInitialTxModelData(e model.ExecutionRequest, m *model.TransactionUpdates) (model.Asset, error) {
txType := "fiat-to-crypto"
m.Type = &txType
- // TODO populate db.Tags with key-val pairs for Unit21
- // TODO populate db.DeviceID with info from fingerprint
- // TODO populate db.IPAddress with info from fingerprint
- // TODO populate db.PlatformID with UUID of customer
+ // TODO populate transactionModel.Tags with key-val pairs for Unit21
+ // TODO populate transactionModel.DeviceID with info from fingerprint
+ // TODO populate transactionModel.IPAddress with info from fingerprint
+ // TODO populate transactionModel.PlatformID with UUID of customer
// bytes, err := json.Marshal()
contractParams := pq.StringArray(e.CxParams)
@@ -424,79 +490,136 @@ func (t transaction) addWalletInstrumentIdIfNew(address string, id string) (stri
return instrument.ID, nil
}
-func (t transaction) authCard(userWallet string, cardToken string, usd float64, chargeAsset model.Asset, dbID string, userId string) (AuthorizedCharge, error) {
+func (t transaction) authCard(p transactionProcessingData) (transactionProcessingData, error) {
// auth their card
- auth, err := AuthorizeCharge(usd, userWallet, cardToken)
+ auth, err := AuthorizeCharge(p.executionRequest.TotalUSD, p.executionRequest.UserAddress, p.executionRequest.CardToken)
if err != nil {
- return auth, common.StringError(err)
+ return p, common.StringError(err)
}
// Add Checkout Instrument ID to our DB if it's not there already and associate it with the user
- instrumentId, err := t.addCardInstrumentIdIfNew(auth.CheckoutFingerprint, userId, auth.Last4, auth.CardType)
+ instrumentId, err := t.addCardInstrumentIdIfNew(auth.CheckoutFingerprint, *p.userId, auth.Last4, auth.CardType)
if err != nil {
- return auth, common.StringError(err)
+ return p, common.StringError(err)
}
// Create Origin Tx leg
- usdWei := floatToFixedString(usd, int(chargeAsset.Decimals))
+ usdWei := floatToFixedString(p.executionRequest.TotalUSD, int(p.processingFeeAsset.Decimals))
origin := model.TxLeg{
Timestamp: time.Now(),
Amount: usdWei,
Value: usdWei,
- AssetID: chargeAsset.ID,
- UserID: userId,
+ AssetID: p.processingFeeAsset.ID,
+ UserID: *p.userId,
InstrumentID: instrumentId,
}
origin, err = t.repos.TxLeg.Create(origin)
if err != nil {
- return auth, common.StringError(err)
+ return p, common.StringError(err)
+ }
+ txLegUpdates := model.TransactionUpdates{OriginTxLegID: &origin.ID}
+ err = t.repos.Transaction.Update(p.transactionModel.ID, txLegUpdates)
+ if err != nil {
+ return p, common.StringError(err)
+ }
+
+ p.cardAuthorization = &auth
+ if err != nil {
+ return p, common.StringError(err)
+ }
+ err = t.updateTransactionStatus("Card "+auth.Status, p.transactionModel.ID)
+ if err != nil {
+ return p, common.StringError(err)
}
- txLeg := model.TransactionUpdates{OriginTxLegID: &origin.ID}
- err = t.repos.Transaction.Update(dbID, txLeg)
+
+ recipientWalletId, err := t.addWalletInstrumentIdIfNew(p.executionRequest.UserAddress, *p.userId)
+ p.recipientWalletId = &recipientWalletId
+ if err != nil {
+ return p, common.StringError(err)
+ }
+
+ // TODO: Determine the output of the transaction (destination leg) with Tracers
+ destinationLeg := model.TxLeg{
+ Timestamp: time.Now(), // Required by the db. Should be updated when the tx occurs
+ Amount: "0", // Required by Unit21. The amount of the asset received by the user
+ Value: "0", // Default to '0'. The value of the asset received by the user
+ AssetID: p.chain.GasTokenID, // Required by the db. the asset received by the user
+ UserID: *p.userId, // the user who received the asset
+ InstrumentID: recipientWalletId, // Required by the db. the instrument which received the asset (wallet usually)
+ }
+
+ destinationLeg, err = t.repos.TxLeg.Create(destinationLeg)
if err != nil {
- return auth, common.StringError(err)
+ return p, common.StringError(err)
}
- return auth, nil
+ txLegUpdates = model.TransactionUpdates{DestinationTxLegID: &destinationLeg.ID}
+
+ err = t.repos.Transaction.Update(p.transactionModel.ID, txLegUpdates)
+ if err != nil {
+ return p, common.StringError(err)
+ }
+
+ if !auth.Approved {
+ err := t.unit21CreateTransaction(p.transactionModel.ID)
+ if err != nil {
+ return p, common.StringError(err)
+ }
+
+ return p, common.StringError(errors.New("payment: Authorization Declined by Checkout"))
+ }
+
+ return p, nil
}
-func (t transaction) initiateTransaction(executor Executor, e model.ExecutionRequest, chargeAsset model.Asset, txUUID string, userId string) (string, *big.Int, error) {
+func (t transaction) initiateTransaction(p transactionProcessingData) (transactionProcessingData, error) {
call := ContractCall{
- CxAddr: e.CxAddr,
- CxFunc: e.CxFunc,
- CxReturn: e.CxReturn,
- CxParams: e.CxParams,
- TxValue: e.TxValue,
- TxGasLimit: e.TxGasLimit,
+ CxAddr: p.executionRequest.CxAddr,
+ CxFunc: p.executionRequest.CxFunc,
+ CxReturn: p.executionRequest.CxReturn,
+ CxParams: p.executionRequest.CxParams,
+ TxValue: p.executionRequest.TxValue,
+ TxGasLimit: p.executionRequest.TxGasLimit,
}
- txID, value, err := executor.Initiate(call)
+
+ txID, value, err := (*p.executor).Initiate(call)
+ p.cumulativeValue = value
if err != nil {
- return "", nil, common.StringError(err)
+ return p, common.StringError(err)
}
+ p.txId = &txID
// Create Response Tx leg
eth := common.WeiToEther(value)
wei := floatToFixedString(eth, 18)
- usd := floatToFixedString(e.TotalUSD, int(chargeAsset.Decimals))
+ usd := floatToFixedString(p.executionRequest.TotalUSD, int(p.processingFeeAsset.Decimals))
responseLeg := model.TxLeg{
Timestamp: time.Now(),
Amount: wei,
Value: usd,
- AssetID: chargeAsset.ID,
- UserID: userId,
+ AssetID: p.processingFeeAsset.ID,
+ UserID: *p.userId,
InstrumentID: t.ids.StringWalletId,
}
responseLeg, err = t.repos.TxLeg.Create(responseLeg)
if err != nil {
- return txID, value, common.StringError(err)
+ return p, common.StringError(err)
}
txLeg := model.TransactionUpdates{ResponseTxLegID: &responseLeg.ID}
- err = t.repos.Transaction.Update(txUUID, txLeg)
+ err = t.repos.Transaction.Update(p.transactionModel.ID, txLeg)
if err != nil {
- return txID, value, common.StringError(err)
+ return p, common.StringError(err)
}
- return txID, value, nil
+ status := "Transaction Initiated"
+ txAmount := p.cumulativeValue.String()
+ updateDB := &model.TransactionUpdates{Status: &status, TransactionHash: p.txId, TransactionAmount: &txAmount}
+ err = t.repos.Transaction.Update(p.transactionModel.ID, updateDB)
+ if err != nil {
+ return p, common.StringError(err)
+ }
+
+ return p, nil
}
func confirmTx(executor Executor, txID string) (uint64, error) {
@@ -507,45 +630,16 @@ func confirmTx(executor Executor, txID string) (uint64, error) {
return trueGas, nil
}
-func (t transaction) chargeCard(userWallet string, authorizationID string, usd float64, chargeAsset model.Asset, txUUID string, userId string) error {
- _, err := CaptureCharge(usd, userWallet, authorizationID)
- if err != nil {
- return common.StringError(err)
- }
-
- // Create Receipt Tx leg
- usdWei := floatToFixedString(usd, int(chargeAsset.Decimals))
- receiptLeg := model.TxLeg{
- Timestamp: time.Now(),
- Amount: usdWei,
- Value: usdWei,
- AssetID: chargeAsset.ID,
- UserID: t.ids.StringUserId,
- InstrumentID: t.ids.StringBankId,
- }
- receiptLeg, err = t.repos.TxLeg.Create(receiptLeg)
- if err != nil {
- return common.StringError(err)
- }
- txLeg := model.TransactionUpdates{ReceiptTxLegID: &receiptLeg.ID}
- err = t.repos.Transaction.Update(txUUID, txLeg)
- if err != nil {
- return common.StringError(err)
- }
-
- return nil
-}
-
// TODO: rewrite this transaction to reference the asset(s) received by the user, not what we paid
-func (t transaction) tenderTransaction(cumulativeValue *big.Int, cumulativeGas uint64, quotedTotal float64, chain Chain, txUUID string, recipientId string, userWalletId string) (float64, error) {
+func (t transaction) tenderTransaction(p transactionProcessingData) (float64, error) {
cost := NewCost(t.redis)
- trueWei := big.NewInt(0).Add(cumulativeValue, big.NewInt(int64(cumulativeGas)))
+ trueWei := big.NewInt(0).Add(p.cumulativeValue, big.NewInt(int64(*p.trueGas)))
trueEth := common.WeiToEther(trueWei)
- trueUSD, err := cost.LookupUSD(chain.CoingeckoName, trueEth)
+ trueUSD, err := cost.LookupUSD(p.chain.CoingeckoName, trueEth)
if err != nil {
return 0, common.StringError(err)
}
- profit := quotedTotal - trueUSD
+ profit := p.executionRequest.Quote.TotalUSD - trueUSD
// Create Receive Tx leg
asset, err := t.repos.Asset.GetName("ETH")
@@ -553,21 +647,21 @@ func (t transaction) tenderTransaction(cumulativeValue *big.Int, cumulativeGas u
return profit, common.StringError(err)
}
wei := floatToFixedString(trueEth, int(asset.Decimals))
- usd := floatToFixedString(quotedTotal, 6)
+ usd := floatToFixedString(p.executionRequest.Quote.TotalUSD, 6)
- txModel, err := t.repos.Transaction.GetById(txUUID)
+ txModel, err := t.repos.Transaction.GetById(p.transactionModel.ID)
if err != nil {
return profit, common.StringError(err)
}
now := time.Now()
destinationLeg := model.TxLegUpdates{
- Timestamp: &now, // updated based on *when the transaction occured* not time.Now()
- Amount: &wei, // Should be the amount of the asset received by the user
- Value: &usd, // The value of the asset received by the user
- AssetID: &asset.ID, // the asset received by the user
- UserID: &recipientId, // the user who received the asset
- InstrumentID: &userWalletId, // the instrument which received the asset (wallet usually)
+ Timestamp: &now, // updated based on *when the transaction occured* not time.Now()
+ Amount: &wei, // Should be the amount of the asset received by the user
+ Value: &usd, // The value of the asset received by the user
+ AssetID: &asset.ID, // the asset received by the user
+ UserID: p.userId, // the user who received the asset
+ InstrumentID: p.recipientWalletId, // the instrument which received the asset (wallet usually)
}
// We now update the destination leg instead of creating it
@@ -579,127 +673,45 @@ func (t transaction) tenderTransaction(cumulativeValue *big.Int, cumulativeGas u
return profit, nil
}
-type postProcessRequest struct {
- TxID string
- Chain Chain
- Authorization AuthorizedCharge
- UserAddress string
- CumulativeGas uint64
- CumulativeValue *big.Int
- Quote model.Quote
- TxDBID string
- processingFeeAsset model.Asset
- preBalance float64
- userId string
- recipientWalletId string
-}
-
-func (t transaction) postProcess(request postProcessRequest) {
- executor := NewExecutor()
- err := executor.Initialize(request.Chain.RPC)
- if err != nil {
- // TODO: Handle error instead of returning it
- }
- updateDB := model.TransactionUpdates{}
- status := "Post Process RPC Dialed"
- updateDB.Status = &status
- err = t.repos.Transaction.Update(request.TxDBID, updateDB)
- if err != nil {
- // TODO: Handle error instead of returning it
- }
-
- // confirm the Tx on the EVM, update db status and NetworkFee
- trueGas, err := confirmTx(executor, request.TxID)
- if err != nil {
- // TODO: Handle error instead of returning it
- }
- status = "Tx Confirmed"
- updateDB.Status = &status
- networkFee := strconv.FormatUint(trueGas, 10)
- updateDB.NetworkFee = &networkFee // geth uses uint64 for gas
- err = t.repos.Transaction.Update(request.TxDBID, updateDB)
- if err != nil {
- // TODO: Handle error instead of returning it
- }
-
- // Check and see if balance threshold was crossed
- postBalance, err := executor.GetBalance()
- if err != nil {
- // TODO: handle error instead of returning it
- }
- // TODO: store threshold on a per-network basis in the repo
- threshold := 10.0
- if request.preBalance >= threshold && postBalance < threshold {
- msg := fmt.Sprintf("STRING-API: %s balance is < %.2f at %.2f", request.Chain.OwlracleName, threshold, postBalance)
- MessageStaff(msg)
- if err != nil {
- // TODO: handle error instead of returning it
- }
- }
-
- // compute profit, update db status and processing fees to db
- // TODO: factor request.processingFeeAsset in the event of crypto-to-usd
- profit, err := t.tenderTransaction(request.CumulativeValue, trueGas, request.Quote.TotalUSD, request.Chain, request.TxDBID, request.userId, request.recipientWalletId)
- if err != nil {
- // TODO: Handle error instead of returning it
- }
- fmt.Printf("PROFIT=%+v", profit)
- status = "Profit Tendered"
- updateDB.Status = &status
- stringFee := floatToFixedString(profit, 6)
- processingFee := floatToFixedString(profit, 6) // TODO: set processingFee based on payment method, and location
- updateDB.StringFee = &stringFee // string fee is always USD with 6 digits
- updateDB.ProcessingFee = &processingFee
- err = t.repos.Transaction.Update(request.TxDBID, updateDB)
+func (t transaction) chargeCard(p transactionProcessingData) error {
+ _, err := CaptureCharge(p.executionRequest.Quote.TotalUSD, p.executionRequest.UserAddress, p.cardAuthorization.AuthID)
if err != nil {
- // TODO: Handle error instead of returning it
+ return common.StringError(err)
}
- // charge the users CC
- err = t.chargeCard(request.UserAddress, request.Authorization.AuthID, request.Quote.TotalUSD, request.processingFeeAsset, request.TxDBID, request.userId)
- if err != nil {
- // TODO: Handle error instead of returning it
- }
- status = "Card Charged"
- updateDB.Status = &status
- // TODO: Figure out how much we paid the CC payment processor and deduct it
- // and use it to populate processing_fee and processing_fee_asset in the table
- err = t.repos.Transaction.Update(request.TxDBID, updateDB)
- if err != nil {
- // TODO: Handle error instead of returning it
+ // Create Receipt Tx leg
+ usdWei := floatToFixedString(p.executionRequest.Quote.TotalUSD, int(p.processingFeeAsset.Decimals))
+ receiptLeg := model.TxLeg{
+ Timestamp: time.Now(),
+ Amount: usdWei,
+ Value: usdWei,
+ AssetID: p.processingFeeAsset.ID,
+ UserID: t.ids.StringUserId,
+ InstrumentID: t.ids.StringBankId,
}
-
- status = "Completed"
- updateDB.Status = &status
- err = t.repos.Transaction.Update(request.TxDBID, updateDB)
+ receiptLeg, err = t.repos.TxLeg.Create(receiptLeg)
if err != nil {
- // TODO: Handle error instead of returning it
+ return common.StringError(err)
}
- executor.Close()
- // Create Transaction data in Unit21
-
- err = t.unit21CreateTransaction(request.TxDBID)
+ txLeg := model.TransactionUpdates{ReceiptTxLegID: &receiptLeg.ID}
+ err = t.repos.Transaction.Update(p.transactionModel.ID, txLeg)
if err != nil {
- log.Printf("Error creating Unit21 transaction: %s", err)
+ return common.StringError(err)
}
- // send email receipt
- err = t.sendEmailReceipt(request)
- if err != nil {
- log.Printf("Error sending email receipt to user: %s", err)
- }
+ return nil
}
-func (t transaction) sendEmailReceipt(request postProcessRequest) error {
- user, err := t.repos.User.GetById(request.userId)
+func (t transaction) sendEmailReceipt(p transactionProcessingData) error {
+ user, err := t.repos.User.GetById(*p.userId)
if err != nil {
- log.Printf("Error getting user from repo: %s", err)
- return err
+ log.Printf("Error getting user from repo: %s", common.StringError(err))
+ return common.StringError(err)
}
- contact, err := t.repos.Contact.GetByUserId(request.userId)
+ contact, err := t.repos.Contact.GetByUserId(user.ID)
if err != nil {
- log.Printf("Error getting user contact from repo: %s", err)
- return err
+ log.Printf("Error getting user contact from repo: %s", common.StringError(err))
+ return common.StringError(err)
}
name := user.FirstName // + " " + user.MiddleName + " " + user.LastName
if name == "" {
@@ -708,27 +720,27 @@ func (t transaction) sendEmailReceipt(request postProcessRequest) error {
receiptParams := common.ReceiptGenerationParams{
ReceiptType: "NFT Purchase", // TODO: retrieve dynamically
CustomerName: name,
- StringPaymentId: request.TxDBID,
+ StringPaymentId: p.transactionModel.ID,
PaymentDescriptor: "String Digital Asset", // TODO: retrieve dynamically
TransactionDate: time.Now().Format(time.RFC1123),
}
receiptBody := [][2]string{
- {"Transaction ID", "" + request.TxID + ""},
- {"Destination Wallet", "" + request.UserAddress + ""},
+ {"Transaction ID", "" + *p.txId + ""},
+ {"Destination Wallet", "" + p.executionRequest.UserAddress + ""},
{"Payment Descriptor", receiptParams.PaymentDescriptor},
- {"Payment Method", request.Authorization.Issuer + " " + request.Authorization.Last4},
+ {"Payment Method", p.cardAuthorization.Issuer + " " + p.cardAuthorization.Last4},
{"Platform", "String Demo"}, // TODO: retrieve dynamically
{"Item Ordered", "String Fighter NFT"}, // TODO: retrieve dynamically
{"Token ID", "1234"}, // TODO: retrieve dynamically, maybe after building token transfer detection
- {"Subtotal", common.FloatToUSDString(request.Quote.BaseUSD + request.Quote.TokenUSD)},
- {"Network Fee:", common.FloatToUSDString(request.Quote.GasUSD)},
- {"Processing Fee", common.FloatToUSDString(request.Quote.ServiceUSD)},
- {"Total Charge", common.FloatToUSDString(request.Quote.TotalUSD)},
+ {"Subtotal", common.FloatToUSDString(p.executionRequest.Quote.BaseUSD + p.executionRequest.Quote.TokenUSD)},
+ {"Network Fee:", common.FloatToUSDString(p.executionRequest.Quote.GasUSD)},
+ {"Processing Fee", common.FloatToUSDString(p.executionRequest.Quote.ServiceUSD)},
+ {"Total Charge", common.FloatToUSDString(p.executionRequest.Quote.TotalUSD)},
}
err = common.EmailReceipt(contact.Data, receiptParams, receiptBody)
if err != nil {
- log.Printf("Error sending email receipt to user: %s", err)
- return err
+ log.Printf("Error sending email receipt to user: %s", common.StringError(err))
+ return common.StringError(err)
}
return nil
}
@@ -748,7 +760,7 @@ func (t transaction) unit21CreateInstrument(instrument model.Instrument) (err er
u21InstrumentId, err := u21Instrument.Create(instrument)
if err != nil {
fmt.Printf("Error creating new instrument in Unit21")
- return
+ return common.StringError(err)
}
// Log create instrument action w/ Unit21
@@ -762,7 +774,7 @@ func (t transaction) unit21CreateInstrument(instrument model.Instrument) (err er
_, err = u21Action.Create(instrument, "Creation", u21InstrumentId, "Creation")
if err != nil {
fmt.Printf("Error creating a new instrument action in Unit21")
- return
+ return common.StringError(err)
}
return
@@ -771,8 +783,8 @@ func (t transaction) unit21CreateInstrument(instrument model.Instrument) (err er
func (t transaction) unit21CreateTransaction(transactionId string) (err error) {
txModel, err := t.repos.Transaction.GetById(transactionId)
if err != nil {
- log.Printf("Error getting tx model in Unit21 in Tx Postprocess: %s", err)
- return
+ log.Printf("Error getting tx model in Unit21 in Tx Postprocess: %s", common.StringError(err))
+ return common.StringError(err)
}
u21Repo := unit21.TransactionRepo{
@@ -784,19 +796,19 @@ func (t transaction) unit21CreateTransaction(transactionId string) (err error) {
u21Tx := unit21.NewTransaction(u21Repo)
_, err = u21Tx.Create(txModel)
if err != nil {
- log.Printf("Error updating Unit21 in Tx Postprocess: %s", err)
- return
+ log.Printf("Error updating Unit21 in Tx Postprocess: %s", common.StringError(err))
+ return common.StringError(err)
}
- return
+ return nil
}
-func (t transaction) unit21Evaluate(transactionId string) (evaluation bool, err error) {
+func (t transaction) unit21Evaluate(transactionId string) (err error) {
//Check transaction in Unit21
txModel, err := t.repos.Transaction.GetById(transactionId)
if err != nil {
- log.Printf("Error getting tx model in Unit21 in Tx Evaluate: %s", err)
- return evaluation, common.StringError(err)
+ log.Printf("Error getting tx model in Unit21 in Tx Evaluate: %s", common.StringError(err))
+ return common.StringError(err)
}
u21Repo := unit21.TransactionRepo{
@@ -806,12 +818,43 @@ func (t transaction) unit21Evaluate(transactionId string) (evaluation bool, err
}
u21Tx := unit21.NewTransaction(u21Repo)
- evaluation, err = u21Tx.Evaluate(txModel)
+ evaluation, err := u21Tx.Evaluate(txModel)
if err != nil {
- log.Printf("Error evaluating transaction in Unit21: %s", err)
- return evaluation, common.StringError(err)
+ log.Printf("Error evaluating transaction in Unit21: %s", common.StringError(err))
+ return common.StringError(err)
}
- return
+ if !evaluation {
+ err = t.updateTransactionStatus("Failed", transactionId)
+ if err != nil {
+ return common.StringError(err)
+ }
+ err = t.unit21CreateTransaction(transactionId)
+ if err != nil {
+ return common.StringError(err)
+ }
+
+ return common.StringError(errors.New("risk: Transaction Failed Unit21 Real Time Rules Evaluation"))
+ }
+ err = t.updateTransactionStatus("Unit21 Authorized", transactionId)
+ if err != nil {
+ return common.StringError(err)
+ }
+
+ return nil
+}
+
+func (t transaction) updateTransactionStatus(status string, transactionId string) (err error) {
+ updateDB := &model.TransactionUpdates{Status: &status}
+ err = t.repos.Transaction.Update(transactionId, updateDB)
+ if err != nil {
+ return common.StringError(err)
+ }
+
+ return nil
+}
+
+func (t *transaction) getStringInstrumentsAndUserId() {
+ t.ids = GetStringIdsFromEnv()
}