diff --git a/action_transaction.go b/action_transaction.go index aff14c45..b8150cb4 100644 --- a/action_transaction.go +++ b/action_transaction.go @@ -24,103 +24,15 @@ import ( // txHex is the raw transaction hex // draftID is the unique draft id from a previously started New() transaction (draft_transaction.ID) // opts are model options and can include "metadata" -func (c *Client) RecordTransaction(ctx context.Context, xPubKey, txHex, draftID string, - opts ...ModelOps, -) (*Transaction, error) { - // Check for existing NewRelic transaction +func (c *Client) RecordTransaction(ctx context.Context, xPubKey, txHex, draftID string, opts ...ModelOps) (*Transaction, error) { ctx = c.GetOrStartTxn(ctx, "record_transaction") - // Create the model & set the default options (gives options from client->model) - newOpts := c.DefaultModelOptions(append(opts, WithXPub(xPubKey), New())...) - transaction := newTransactionWithDraftID( - txHex, draftID, newOpts..., - ) - - // Ensure that we have a transaction id (created from the txHex) - id := transaction.GetID() - if len(id) == 0 { - return nil, ErrMissingTxHex - } - - var ( - unlock func() - err error - ) - // Create the lock and set the release for after the function completes - // Waits for the moment when the transaction is unlocked and creates a new lock - // Relevant for bux to bux transactions, as we have 1 tx but need to record 2 txs - outgoing and incoming - for { - unlock, err = newWriteLock( - ctx, fmt.Sprintf(lockKeyRecordTx, id), c.Cachestore(), - ) - if err == nil { - break - } - time.Sleep(time.Second * 1) - } - defer unlock() - - // OPTION: check incoming transactions (if enabled, will add to queue for checking on-chain) - if !c.IsITCEnabled() { - transaction.DebugLog("incoming transaction check is disabled") - } else { - - // Incoming (external/unknown) transaction (no draft id was given) - if len(transaction.DraftID) == 0 { - - // Process & save the model - incomingTx := newIncomingTransaction( - transaction.ID, txHex, newOpts..., - ) - if err = incomingTx.Save(ctx); err != nil { - return nil, err - } - - // Check if sync transaction exist. And if not, we should create it - if syncTx, _ := GetSyncTransactionByID(ctx, transaction.ID, transaction.client.DefaultModelOptions()...); syncTx == nil { - // Create the sync transaction model - sync := newSyncTransaction( - transaction.GetID(), - transaction.Client().DefaultSyncConfig(), - transaction.GetOptions(true)..., - ) - - // Skip broadcasting and skip P2P (incoming tx should have been broadcasted already) - sync.BroadcastStatus = SyncStatusSkipped // todo: this is an assumption - sync.P2PStatus = SyncStatusSkipped // The owner of the Tx should have already notified paymail providers - - // Use the same metadata - sync.Metadata = transaction.Metadata - - // If all the options are skipped, do not make a new model (ignore the record) - if !sync.isSkipped() { - if err = sync.Save(ctx); err != nil { - return nil, err - } - } - } - // Added to queue - return newTransactionFromIncomingTransaction(incomingTx), nil - } - - // Internal tx (must match draft tx) - if transaction.draftTransaction, err = getDraftTransactionID( - ctx, transaction.XPubID, transaction.DraftID, - transaction.GetOptions(false)..., - ); err != nil { - return nil, err - } else if transaction.draftTransaction == nil { - return nil, ErrDraftNotFound - } - } - - // Process & save the transaction model - if err = transaction.Save(ctx); err != nil { + rts, err := getRecordTxStrategy(ctx, c, xPubKey, txHex, draftID) + if err != nil { return nil, err } - // Return the response - return transaction, nil + return recordTransaction(ctx, c, rts, opts...) } // RecordRawTransaction will parse the transaction and save it into the Datastore directly, without any checks @@ -154,7 +66,7 @@ func recordMonitoredTransaction(ctx context.Context, client ClientInterface, txH return nil, err } - if transaction.BlockHash == "" { + if transaction.BlockHash == "" { // @arkadiusz: it's always true, i guess // Create the sync transaction model sync := newSyncTransaction( transaction.GetID(), @@ -198,11 +110,30 @@ func (c *Client) recordTxHex(ctx context.Context, txHex string, opts ...ModelOps return nil, err } - // run before create to see whether xpub_in_ids or xpub_out_ids is set - if err = transaction.BeforeCreating(ctx); err != nil { + // Logic moved from BeforeCreating hook - should be refactorized in next iteration + + // If we are external and the user disabled incoming transaction checking, check outputs + if transaction.isExternal() && !transaction.Client().IsITCEnabled() { + // Check that the transaction has >= 1 known destination + if !transaction.TransactionBase.hasOneKnownDestination(ctx, transaction.Client(), transaction.GetOptions(false)...) { + return nil, ErrNoMatchingOutputs + } + } + + // Process the UTXOs + if err = transaction.processUtxos(ctx); err != nil { return nil, err } + // Set the values from the inputs/outputs and draft tx + transaction.TotalValue, transaction.Fee = transaction.getValues() + + // Add values + transaction.NumberOfInputs = uint32(len(transaction.TransactionBase.parsedTx.Inputs)) + transaction.NumberOfOutputs = uint32(len(transaction.TransactionBase.parsedTx.Outputs)) + + // /Logic moved from BeforeCreating hook - should be refactorized in next iteration + monitor := c.options.chainstate.Monitor() if monitor != nil { @@ -213,7 +144,7 @@ func (c *Client) recordTxHex(ctx context.Context, txHex string, opts ...ModelOps } } - // Process & save the transaction model + // save the transaction model if err = transaction.Save(ctx); err != nil { return nil, err } @@ -418,7 +349,7 @@ func (c *Client) UpdateTransactionMetadata(ctx context.Context, xPubID, id strin return nil, err } - // Save the model + // Save the model // update existing record if err = transaction.Save(ctx); err != nil { return nil, err } @@ -550,7 +481,7 @@ func (c *Client) RevertTransaction(ctx context.Context, id string) error { transaction.XpubOutputValue = XpubOutputValue{"reverted": 0} transaction.DeletedAt.Valid = true transaction.DeletedAt.Time = time.Now() - err = transaction.Save(ctx) + err = transaction.Save(ctx) // update existing record return err } diff --git a/paymail_service_provider.go b/paymail_service_provider.go index 4baadc94..3cff77fd 100644 --- a/paymail_service_provider.go +++ b/paymail_service_provider.go @@ -4,7 +4,6 @@ import ( "context" "database/sql" "encoding/hex" - "errors" "fmt" "time" @@ -12,8 +11,6 @@ import ( "github.com/bitcoin-sv/go-paymail/server" "github.com/bitcoinschema/go-bitcoin/v2" "github.com/libsv/go-bk/bec" - "github.com/libsv/go-bt/v2" - "github.com/mrz1836/go-datastore" customTypes "github.com/mrz1836/go-datastore/custom_types" "github.com/BuxOrg/bux/chainstate" @@ -149,47 +146,32 @@ func (p *PaymailDefaultServiceProvider) CreateP2PDestinationResponse( } // RecordTransaction will record the transaction -func (p *PaymailDefaultServiceProvider) RecordTransaction( - ctx context.Context, - p2pTx *paymail.P2PTransaction, - requestMetadata *server.RequestMetadata, -) (*paymail.P2PTransactionPayload, error) { +// TODO: rename to HandleReceivedP2pTransaction +func (p *PaymailDefaultServiceProvider) RecordTransaction(ctx context.Context, + p2pTx *paymail.P2PTransaction, requestMetadata *server.RequestMetadata) (*paymail.P2PTransactionPayload, error) { + // Create the metadata - metadata := p.createMetadata(requestMetadata, "RecordTransaction") + metadata := p.createMetadata(requestMetadata, "HandleReceivedP2pTransaction") metadata[p2pMetadataField] = p2pTx.MetaData metadata[ReferenceIDField] = p2pTx.Reference - var draftID string - if tx, _ := p.client.GetTransactionByHex(ctx, p2pTx.Hex); tx != nil { - draftID = tx.DraftID - } - // Record the transaction - transaction, err := p.client.RecordTransaction( - ctx, "", p2pTx.Hex, draftID, []ModelOps{WithMetadatas(metadata)}..., - ) - // do not return an error if we already have the transaction - if err != nil && !errors.Is(err, datastore.ErrDuplicateKey) { + rts, err := getRecordTxStrategy(ctx, p.client, "", p2pTx.Hex, "") + if err != nil { return nil, err } - // we need to set the tx ID here, since our transaction will be empty if we already had it in the DB - txID := "" - if transaction != nil { - txID = transaction.ID - } else { - var btTx *bt.Tx - btTx, err = bt.NewTxFromString(p2pTx.Hex) - if err != nil { - return nil, err - } - txID = btTx.TxID() + rts.(recordIncomingTxStrategy).ForceBroadcast(true) + + transaction, err := recordTransaction(ctx, p.client, rts, WithMetadatas(metadata)) + if err != nil { + return nil, err } // Return the response from the p2p request return &paymail.P2PTransactionPayload{ Note: p2pTx.MetaData.Note, - TxID: txID, + TxID: transaction.ID, }, nil }