Skip to content

Commit

Permalink
refactor!: replace 2 dummies with 1 dummy (list)
Browse files Browse the repository at this point in the history
  • Loading branch information
jadwahab committed Apr 5, 2023
1 parent 969d775 commit b6c7e64
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 39 deletions.
14 changes: 8 additions & 6 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,12 @@ var (

// Sentinal errors reported by PSBTs.
var (
ErrDummyInput = errors.New("failed to add dummy input 0")
ErrInsufficientUTXOs = errors.New("need at least 3 utxos")
ErrUTXOInputMismatch = errors.New("utxo and input mismatch")
ErrInvalidSellOffer = errors.New("invalid sell offer (partially signed tx)")
ErrOrdinalOutputNoExist = errors.New("ordinal output expected in index 2 doesn't exist")
ErrOrdinalInputNoExist = errors.New("ordinal input expected in index 2 doesn't exist")
ErrDummyInput = errors.New("failed to add dummy input 0")
ErrInsufficientUTXOs = errors.New("need at least 2 utxos")
ErrInsufficientUTXOValue = errors.New("need at least 1 utxos which is > ordinal price")
ErrUTXOInputMismatch = errors.New("utxo and input mismatch")
ErrInvalidSellOffer = errors.New("invalid sell offer (partially signed tx)")
ErrOrdinalOutputNoExist = errors.New("ordinal output expected in index 2 doesn't exist")
ErrOrdinalInputNoExist = errors.New("ordinal input expected in index 2 doesn't exist")
ErrEmptyScripts = errors.New("at least one of needed scripts is empty")
)
50 changes: 35 additions & 15 deletions ord/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,59 +93,79 @@ type AcceptListingArgs struct {

// AcceptOrdinalSaleListing accepts a partially signed Bitcoin
// transaction offer to sell an ordinal. When accepting the offer,
// you will need to provide at least 3 UTXOs - with the first 2
// being dummy utxos that will just pass through, and the rest with
// the required payment and tx fees.
// you will need to provide at least 2 UTXOs - with at least 1 being
// larger than the listed ordinal price.
func AcceptOrdinalSaleListing(ctx context.Context, vla *ValidateListingArgs, asoa *AcceptListingArgs) (*bt.Tx, error) {
if valid := vla.Validate(asoa.PSTx); !valid {
return nil, bt.ErrInvalidSellOffer
}
sellerOrdinalInput := asoa.PSTx.Inputs[0]
sellerOutput := asoa.PSTx.Outputs[0]

if len(asoa.UTXOs) < 3 {
if len(asoa.UTXOs) < 2 {
return nil, bt.ErrInsufficientUTXOs
}

if asoa.BuyerReceiveOrdinalScript == nil ||
asoa.DummyOutputScript == nil ||
asoa.ChangeScript == nil {
return nil, bt.ErrEmptyScripts
}

// check at least 1 utxo is larger than the listed ordinal price
validUTXOFound := false
for i, u := range asoa.UTXOs {
if u.Satoshis > sellerOutput.Satoshis {
// Move the UTXO at index i to the beginning
asoa.UTXOs = append([]*bt.UTXO{asoa.UTXOs[i]}, append(asoa.UTXOs[:i], asoa.UTXOs[i+1:]...)...)
validUTXOFound = true
break
}
}
if !validUTXOFound {
return nil, bt.ErrInsufficientUTXOValue
}

tx := bt.NewTx()

// add dummy inputs
err := tx.FromUTXOs(asoa.UTXOs[0], asoa.UTXOs[1])
// add first input to pay for ordinal
err := tx.FromUTXOs(asoa.UTXOs[0])
if err != nil {
return nil, fmt.Errorf(`failed to add inputs: %w`, err)
return nil, fmt.Errorf(`failed to add input: %w`, err)
}

tx.Inputs = append(tx.Inputs, sellerOrdinalInput)

// add payment input(s)
err = tx.FromUTXOs(asoa.UTXOs[2:]...)
// add input(s) to pay for tx fees
err = tx.FromUTXOs(asoa.UTXOs[1:]...)
if err != nil {
return nil, fmt.Errorf(`failed to add inputs: %w`, err)
}

// add dummy output to passthrough dummy inputs
// add dummy output
tx.AddOutput(&bt.Output{
LockingScript: asoa.DummyOutputScript,
Satoshis: asoa.UTXOs[0].Satoshis + asoa.UTXOs[1].Satoshis,
Satoshis: asoa.UTXOs[0].Satoshis - sellerOutput.Satoshis,
})

tx.AddOutput(sellerOutput)

// add ordinal receive output
tx.AddOutput(&bt.Output{
LockingScript: asoa.BuyerReceiveOrdinalScript,
Satoshis: 1,
})

tx.AddOutput(sellerOutput)

err = tx.Change(asoa.ChangeScript, asoa.FQ)
if err != nil {
return nil, err
}

//nolint:dupl // false positive
for i, u := range asoa.UTXOs {
// skip 3rd input (ordinals input)
// skip 2nd input (ordinals input)
j := i
if i >= 2 {
if i >= 1 {
j++
}

Expand Down
88 changes: 88 additions & 0 deletions ord/list2dummies.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package ord

import (
"bytes"
"context"
"fmt"

"github.com/libsv/go-bt/v2"
)

// AcceptOrdinalSaleListing2Dummies accepts a partially signed Bitcoin
// transaction offer to sell an ordinal. When accepting the offer,
// you will need to provide at least 3 UTXOs - with the first 2
// being dummy utxos that will just pass through, and the rest with
// the required payment and tx fees.
func AcceptOrdinalSaleListing2Dummies(ctx context.Context, vla *ValidateListingArgs,
asoa *AcceptListingArgs) (*bt.Tx, error) {

if valid := vla.Validate(asoa.PSTx); !valid {
return nil, bt.ErrInvalidSellOffer
}
sellerOrdinalInput := asoa.PSTx.Inputs[0]
sellerOutput := asoa.PSTx.Outputs[0]

if len(asoa.UTXOs) < 3 {
return nil, bt.ErrInsufficientUTXOs
}

tx := bt.NewTx()

// add dummy inputs
err := tx.FromUTXOs(asoa.UTXOs[0], asoa.UTXOs[1])
if err != nil {
return nil, fmt.Errorf(`failed to add inputs: %w`, err)
}

tx.Inputs = append(tx.Inputs, sellerOrdinalInput)

// add payment input(s)
err = tx.FromUTXOs(asoa.UTXOs[2:]...)
if err != nil {
return nil, fmt.Errorf(`failed to add inputs: %w`, err)
}

// add dummy output to passthrough dummy inputs
tx.AddOutput(&bt.Output{
LockingScript: asoa.DummyOutputScript,
Satoshis: asoa.UTXOs[0].Satoshis + asoa.UTXOs[1].Satoshis,
})

// add ordinal receive output
tx.AddOutput(&bt.Output{
LockingScript: asoa.BuyerReceiveOrdinalScript,
Satoshis: 1,
})

tx.AddOutput(sellerOutput)

err = tx.Change(asoa.ChangeScript, asoa.FQ)
if err != nil {
return nil, err
}

//nolint:dupl // false positive
for i, u := range asoa.UTXOs {
// skip 3rd input (ordinals input)
j := i
if i >= 2 {
j++
}

if tx.Inputs[j] == nil {
return nil, fmt.Errorf("input expected at index %d doesn't exist", j)
}
if !(bytes.Equal(u.TxID, tx.Inputs[j].PreviousTxID())) {
return nil, bt.ErrUTXOInputMismatch
}
if *u.Unlocker == nil {
return nil, fmt.Errorf("UTXO unlocker at index %d not found", i)
}
err = tx.FillInput(ctx, *u.Unlocker, bt.UnlockerParams{InputIdx: uint32(j)})
if err != nil {
return nil, err
}
}

return tx, nil
}
64 changes: 46 additions & 18 deletions ord/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func TestOfferToSellPSBTNoErrors(t *testing.T) {

pstx, CreateListingError := ord.ListOrdinalForSale(context.Background(), &ord.ListOrdinalArgs{
SellerReceiveOutput: &bt.Output{
Satoshis: 500,
Satoshis: 1000,
LockingScript: func() *bscript.Script {
s, _ := bscript.NewP2PKHFromAddress("1C3V9TTJefP8Hft96sVf54mQyDJh8Ze4w4") // L1JWiLZtCkkqin41XtQ2Jxo1XGxj1R4ydT2zmxPiaeQfuyUK631D
return s
Expand All @@ -58,8 +58,50 @@ func TestOfferToSellPSBTNoErrors(t *testing.T) {
assert.True(t, vla.Validate(pstx))
})

us := []*bt.UTXO{
{
TxID: func() []byte {
t, _ := hex.DecodeString("8f027fb1361ae46ac165e1d90e5436ed9c11d4eeaa60669ab90386a3abd9ce6a")
return t
}(),
Vout: uint32(1),
LockingScript: ordPrefixScript,
Satoshis: 953,
Unlocker: &ordUnlocker,
},
{
TxID: func() []byte {
t, _ := hex.DecodeString("fcc55cd1a4275e5750070381028d3e3edf99b238bdc56199ff8bdc17dfb599d1")
return t
}(),
Vout: uint32(3),
LockingScript: ordPrefixScript,
Satoshis: 27601,
Unlocker: &ordUnlocker,
},
}
buyerOrdS, _ := bscript.NewP2PKHFromAddress("1HebepswCi6huw1KJ7LvkrgemAV63TyVUs") // KwQq67d4Jds3wxs3kQHB8PPwaoaBQfNKkzAacZeMesb7zXojVYpj
dummyS, _ := bscript.NewP2PKHFromAddress("19NfKd8aTwvb5ngfP29RxgfQzZt8KAYtQo") // L5W2nyKUCsDStVUBwZj2Q3Ph5vcae4bgdzprZDYqDpvZA8AFguFH
changeS, _ := bscript.NewP2PKHFromAddress("19NfKd8aTwvb5ngfP29RxgfQzZt8KAYtQo") // L5W2nyKUCsDStVUBwZj2Q3Ph5vcae4bgdzprZDYqDpvZA8AFguFH

t.Run("no errors when accepting listing", func(t *testing.T) {
us := []*bt.UTXO{

_, err := ord.AcceptOrdinalSaleListing(context.Background(), &ord.ValidateListingArgs{
ListedOrdinalUTXO: ordUTXO,
},
&ord.AcceptListingArgs{
PSTx: pstx,
UTXOs: us,
BuyerReceiveOrdinalScript: buyerOrdS,
DummyOutputScript: dummyS,
ChangeScript: changeS,
FQ: bt.NewFeeQuote(),
})
assert.NoError(t, err)
})

t.Run("no errors when accepting listing using 2 dummies", func(t *testing.T) {
us = append([]*bt.UTXO{
{
TxID: func() []byte {
t, _ := hex.DecodeString("61dfcc313763eb5332c036131facdf92c2ca9d663ffb96e4b997086a0643d635")
Expand All @@ -80,23 +122,9 @@ func TestOfferToSellPSBTNoErrors(t *testing.T) {
Satoshis: 10,
Unlocker: &ordUnlocker,
},
{
TxID: func() []byte {
t, _ := hex.DecodeString("8f027fb1361ae46ac165e1d90e5436ed9c11d4eeaa60669ab90386a3abd9ce6a")
return t
}(),
Vout: uint32(1),
LockingScript: ordPrefixScript,
Satoshis: 953,
Unlocker: &ordUnlocker,
},
}
}, us...)

buyerOrdS, _ := bscript.NewP2PKHFromAddress("1HebepswCi6huw1KJ7LvkrgemAV63TyVUs") // KwQq67d4Jds3wxs3kQHB8PPwaoaBQfNKkzAacZeMesb7zXojVYpj
dummyS, _ := bscript.NewP2PKHFromAddress("19NfKd8aTwvb5ngfP29RxgfQzZt8KAYtQo") // L5W2nyKUCsDStVUBwZj2Q3Ph5vcae4bgdzprZDYqDpvZA8AFguFH
changeS, _ := bscript.NewP2PKHFromAddress("19NfKd8aTwvb5ngfP29RxgfQzZt8KAYtQo") // L5W2nyKUCsDStVUBwZj2Q3Ph5vcae4bgdzprZDYqDpvZA8AFguFH

_, err := ord.AcceptOrdinalSaleListing(context.Background(), &ord.ValidateListingArgs{
_, err := ord.AcceptOrdinalSaleListing2Dummies(context.Background(), &ord.ValidateListingArgs{
ListedOrdinalUTXO: ordUTXO,
},
&ord.AcceptListingArgs{
Expand Down

0 comments on commit b6c7e64

Please sign in to comment.