Skip to content

Commit

Permalink
feat(SPV-1241): migrate FinalizeTransaction and SendToRecipients
Browse files Browse the repository at this point in the history
  • Loading branch information
dzolt committed Dec 2, 2024
1 parent f89e3d8 commit 13253ba
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 45 deletions.
16 changes: 16 additions & 0 deletions commands/transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,19 @@ type UpdateTransactionMetadata struct {
ID string `json:"-"` // Unique identifier of the transaction to be updated.
Metadata querybuilders.Metadata `json:"metadata"` // New metadata to associate with the transaction.
}

// Recipients represents a single recipient in a transaction.
// It includes details about the recipient address, the amount to send,
// and an optional OP_RETURN script for including additional data in the transaction.
type Recipients struct {
OpReturn *response.OpReturn `json:"op_return"` // Optional OP_RETURN script for attaching data to the transaction.
Satoshis uint64 `json:"satoshis"` // Amount to send to the recipient, in satoshis.
To string `json:"to"` // Paymails address of the recipient.
}

// SendToRecipients holds the arguments required to send a transaction to multiple recipients.
// This includes the list of recipients with their details and optional metadata for the transaction.
type SendToRecipients struct {
Recipients []*Recipients `json:"recipients"` // List of recipients for the transaction.
Metadata querybuilders.Metadata `json:"metadata"` // Metadata associated with the transaction.
}
8 changes: 7 additions & 1 deletion examples/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version: '3'
version: "3"

tasks:
default:
Expand Down Expand Up @@ -98,6 +98,12 @@ tasks:
cmds:
- go run ./update_transaction_metadata/update_transaction_metadata.go

create-transaction:
desc: "Send OP return."
silent: true
cmds:
- go run ./send_op_return/send_op_return.go

fetch-user-utxos:
desc: "Fetch user UTXOs page."
silent: true
Expand Down
36 changes: 0 additions & 36 deletions examples/draft_transaction/draft_transaction.go

This file was deleted.

65 changes: 65 additions & 0 deletions examples/send_op_return/send_op_return.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package main

import (
"context"
"fmt"
"log"

wallet "github.com/bitcoin-sv/spv-wallet-go-client"
"github.com/bitcoin-sv/spv-wallet-go-client/commands"
"github.com/bitcoin-sv/spv-wallet-go-client/examples"
"github.com/bitcoin-sv/spv-wallet-go-client/examples/exampleutil"
"github.com/bitcoin-sv/spv-wallet/models/response"
)

func main() {
usersAPI, err := wallet.NewUserAPIWithXPriv(exampleutil.ExampleConfig, examples.XPriv)
if err != nil {
log.Fatal(err)
}

ctx := context.Background()

metadata := map[string]any{}

opReturn := response.OpReturn{StringParts: []string{"hello", "world"}}
draftTransactionCmd := commands.DraftTransaction{
Config: response.TransactionConfig{
Outputs: []*response.TransactionOutput{
{
OpReturn: &opReturn,
},
},
},
Metadata: metadata,
}

draftTransaction, err := usersAPI.DraftTransaction(ctx, &draftTransactionCmd)
if err != nil {
log.Fatal(err)
}
exampleutil.Print("DraftTransaction response: ", draftTransaction)

finalized, err := usersAPI.FinalizeTransaction(draftTransaction)
if err != nil {
log.Fatal(err)
}
fmt.Println("Finalized transaction hex : ", finalized)

recordTransactionCmd := commands.RecordTransaction{
Hex: finalized,
Metadata: metadata,
ReferenceID: draftTransaction.ID,
}
transaction, err := usersAPI.RecordTransaction(ctx, &recordTransactionCmd)
if err != nil {
log.Fatal(err)
}
fmt.Println("Transaction with OP_RETURN: ", transaction)

transactionG, err := usersAPI.Transaction(context.Background(), transaction.ID)
if err != nil {
log.Fatal(err)
}
fmt.Println("Transaction: ", transactionG)
}
51 changes: 51 additions & 0 deletions internal/api/v1/user/transactions/transactions_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import (
"fmt"
"net/url"

bip32 "github.com/bitcoin-sv/go-sdk/compat/bip32"
"github.com/bitcoin-sv/spv-wallet-go-client/commands"
"github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/errutil"
"github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/querybuilders"
"github.com/bitcoin-sv/spv-wallet-go-client/internal/auth"
"github.com/bitcoin-sv/spv-wallet-go-client/queries"
"github.com/bitcoin-sv/spv-wallet/models/response"
"github.com/go-resty/resty/v2"
Expand All @@ -20,6 +22,55 @@ type API struct {
httpClient *resty.Client
}

func (a *API) FinalizeTransaction(draft *response.DraftTransaction, xPriv *bip32.ExtendedKey) (string, error) {
res, err := auth.GetSignedHex(draft, xPriv)
if err != nil {
return "", err
}

return res, nil
}

func (a *API) DraftToRecipients(ctx context.Context, r *commands.SendToRecipients) (*response.DraftTransaction, error) {
outputs := make([]*response.TransactionOutput, 0)

for _, recipient := range r.Recipients {
outputs = append(outputs, &response.TransactionOutput{
To: recipient.To,
Satoshis: recipient.Satoshis,
OpReturn: recipient.OpReturn,
})
}

draftTransactionCmd := &commands.DraftTransaction{
Config: response.TransactionConfig{
Outputs: outputs,
},
Metadata: r.Metadata,
}

return a.DraftTransaction(ctx, draftTransactionCmd)
}

func (a *API) SendToRecipients(ctx context.Context, r *commands.SendToRecipients, xPriv *bip32.ExtendedKey) (*response.Transaction, error) {
draft, err := a.DraftToRecipients(ctx, r)
if err != nil {
return nil, err
}

var hex string
if hex, err = a.FinalizeTransaction(draft, xPriv); err != nil {
return nil, err
}

recordTransactionCmd := &commands.RecordTransaction{
Metadata: r.Metadata,
Hex: hex,
ReferenceID: draft.ID,
}
return a.RecordTransaction(ctx, recordTransactionCmd)
}

func (a *API) DraftTransaction(ctx context.Context, r *commands.DraftTransaction) (*response.DraftTransaction, error) {
var result response.DraftTransaction

Expand Down
9 changes: 5 additions & 4 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import (
"github.com/bitcoin-sv/go-sdk/transaction/template/p2pkh"
"github.com/bitcoin-sv/spv-wallet-go-client/internal/cryptoutil"
"github.com/bitcoin-sv/spv-wallet/models"
"github.com/bitcoin-sv/spv-wallet/models/response"
)

func GetSignedHex(dt *models.DraftTransaction, xPriv *bip32.ExtendedKey) (string, error) {
func GetSignedHex(dt *response.DraftTransaction, xPriv *bip32.ExtendedKey) (string, error) {
// Create transaction from hex
tx, err := trx.NewTransactionFromHex(dt.Hex)
// we need to reset the inputs as we are going to add them via tx.AddInputFrom (ts-sdk method) and then sign
Expand Down Expand Up @@ -66,7 +67,7 @@ func setSignature(header *http.Header, xPriv *bip32.ExtendedKey, bodyString stri
return nil
}

func prepareLockingScript(dst *models.Destination) (*script.Script, error) {
func prepareLockingScript(dst *response.Destination) (*script.Script, error) {
lockingScript, err := script.NewFromHex(dst.LockingScript)
if err != nil {
return nil, fmt.Errorf("failed to create locking script from hex for destination: %w", err)
Expand All @@ -75,7 +76,7 @@ func prepareLockingScript(dst *models.Destination) (*script.Script, error) {
return lockingScript, nil
}

func prepareUnlockingScript(xPriv *bip32.ExtendedKey, dst *models.Destination) (*p2pkh.P2PKH, error) {
func prepareUnlockingScript(xPriv *bip32.ExtendedKey, dst *response.Destination) (*p2pkh.P2PKH, error) {
key, err := getDerivedKeyForDestination(xPriv, dst)
if err != nil {
return nil, fmt.Errorf("failed to get derived key for destination: %w", err)
Expand All @@ -84,7 +85,7 @@ func prepareUnlockingScript(xPriv *bip32.ExtendedKey, dst *models.Destination) (
return getUnlockingScript(key)
}

func getDerivedKeyForDestination(xPriv *bip32.ExtendedKey, dst *models.Destination) (*ec.PrivateKey, error) {
func getDerivedKeyForDestination(xPriv *bip32.ExtendedKey, dst *response.Destination) (*ec.PrivateKey, error) {
// Derive the child key (m/chain/num)
derivedKey, err := bip32.GetHDKeyByPath(xPriv, dst.Chain, dst.Num)
if err != nil {
Expand Down
38 changes: 34 additions & 4 deletions user_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
// UserAPI methods may return wrapped errors, including models.SPVError or
// ErrUnrecognizedAPIResponse, depending on the behavior of the SPV Wallet API.
type UserAPI struct {
xPriv *bip32.ExtendedKey
xpubAPI *users.XPubAPI
accessKeyAPI *users.AccessKeyAPI
configsAPI *configs.API
Expand Down Expand Up @@ -226,6 +227,34 @@ func (u *UserAPI) Transaction(ctx context.Context, ID string) (*response.Transac
return res, nil
}

// FinalizeTransaction finalizes a draft transaction and returns its signed hex representation.
// It uses the draft transaction details to construct, enrich, and sign the transaction
// through the `auth.GetSignedHex` utility function.
// The response is the signed transaction in hex format.
// Returns an error if the transaction cannot be finalized.
func (u *UserAPI) FinalizeTransaction(draft *response.DraftTransaction) (string, error) {
res, err := u.transactionsAPI.FinalizeTransaction(draft, u.xPriv)
if err != nil {
return "", fmt.Errorf("couldn't finalize transaction with ID: %s, %w", draft.ID, err)
}

return res, nil
}

// SendToRecipients creates, finalizes, and broadcasts a transaction to multiple recipients.
// This method handles the complete process of drafting, finalizing, and recording the transaction
// using the recipient details provided in the command.
// The response is unmarshalled into a *response.Transaction struct.
// Returns an error if the transaction fails at any step, such as drafting, finalization or recording.
func (u *UserAPI) SendToRecipients(ctx context.Context, cmd *commands.SendToRecipients) (*response.Transaction, error) {
res, err := u.transactionsAPI.SendToRecipients(ctx, cmd, u.xPriv)
if err != nil {
return nil, transactions.HTTPErrorFormatter("send to recipients", err).FormatPostErr()
}

return res, nil
}

// XPub retrieves the full xpub information for the current user via the users API.
// The response is unmarshaled into a *response.Xpub.
// Returns an error if the request fails or the response cannot be decoded.
Expand Down Expand Up @@ -395,7 +424,7 @@ func NewUserAPIWithXPub(cfg config.Config, xPub string) (*UserAPI, error) {
return nil, fmt.Errorf("failed to intialized xPub authenticator: %w", err)
}

return initUserAPI(cfg, authenticator)
return initUserAPI(cfg, nil, authenticator)
}

// NewUserAPIWithXPriv initializes a new UserAPI instance using an extended private key (xPriv).
Expand All @@ -414,7 +443,7 @@ func NewUserAPIWithXPriv(cfg config.Config, xPriv string) (*UserAPI, error) {
return nil, fmt.Errorf("failed to intialized xPriv authenticator: %w", err)
}

userAPI, err := initUserAPI(cfg, authenticator)
userAPI, err := initUserAPI(cfg, key, authenticator)
if err != nil {
return nil, fmt.Errorf("failed to create new client: %w", err)
}
Expand All @@ -439,21 +468,22 @@ func NewUserAPIWithAccessKey(cfg config.Config, accessKey string) (*UserAPI, err
return nil, fmt.Errorf("failed to intialized access key authenticator: %w", err)
}

return initUserAPI(cfg, authenticator)
return initUserAPI(cfg, nil, authenticator)
}

type authenticator interface {
Authenticate(r *resty.Request) error
}

func initUserAPI(cfg config.Config, auth authenticator) (*UserAPI, error) {
func initUserAPI(cfg config.Config, xPriv *bip32.ExtendedKey, auth authenticator) (*UserAPI, error) {
url, err := url.Parse(cfg.Addr)
if err != nil {
return nil, fmt.Errorf("failed to parse addr to url.URL: %w", err)
}

httpClient := restyutil.NewHTTPClient(cfg, auth)
return &UserAPI{
xPriv: xPriv,
merkleRootsAPI: merkleroots.NewAPI(url, httpClient),
configsAPI: configs.NewAPI(url, httpClient),
transactionsAPI: transactions.NewAPI(url, httpClient),
Expand Down

0 comments on commit 13253ba

Please sign in to comment.