Skip to content

Commit

Permalink
add accept sell ord offer + add ulocker to UTXO
Browse files Browse the repository at this point in the history
  • Loading branch information
jadwahab committed Mar 26, 2023
1 parent c8811a9 commit bfe9c82
Show file tree
Hide file tree
Showing 11 changed files with 257 additions and 124 deletions.
2 changes: 1 addition & 1 deletion bscript/inscriptions.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package bscript

// InscriptionArgs contains the Ordinal inscription data
// InscriptionArgs contains the Ordinal inscription data.
type InscriptionArgs struct {
LockingScriptPrefix *Script
Data []byte
Expand Down
19 changes: 12 additions & 7 deletions bscript/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ import (

// ScriptKey types.
const (
ScriptTypePubKey = "pubkey"
ScriptTypePubKeyHash = "pubkeyhash"
ScriptTypeNonStandard = "nonstandard"
ScriptTypeEmpty = "empty"
ScriptTypeSecureHash = "securehash"
ScriptTypeMultiSig = "multisig"
ScriptTypeNullData = "nulldata"
// TODO: change to p2pk/p2pkh
ScriptTypePubKey = "pubkey"
ScriptTypePubKeyHash = "pubkeyhash"
ScriptTypeNonStandard = "nonstandard"
ScriptTypeEmpty = "empty"
ScriptTypeMultiSig = "multisig"
ScriptTypeNullData = "nulldata"
ScriptTypePubKeyHashInscription = "pubkeyhashinscription"
)

// Script type
Expand Down Expand Up @@ -389,6 +390,7 @@ func (s *Script) IsP2PKHInscription() bool {
// isP2PKHInscriptionHelper helper so that we don't need to call
// `DecodeParts()` multiple times, such as in `ParseInscription()`
func isP2PKHInscriptionHelper(parts [][]byte) bool {
// TODO: cleanup
return len(parts) == 13 &&
parts[0][0] == OpDUP &&
parts[1][0] == OpHASH160 &&
Expand Down Expand Up @@ -501,6 +503,9 @@ func (s *Script) ScriptType() string {
if s.IsData() {
return ScriptTypeNullData
}
if s.IsP2PKHInscription() {
return ScriptTypePubKeyHashInscription
}
return ScriptTypeNonStandard
}

Expand Down
3 changes: 2 additions & 1 deletion errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,6 @@ var (

// Sentinal errors reported by PSBTs.
var (
ErrDummyInput = errors.New("failed to add dummy input 0")
ErrDummyInput = errors.New("failed to add dummy input 0")
ErrInsufficientUTXOs = errors.New("need at least 3 utxos")
)
4 changes: 3 additions & 1 deletion inscriptions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ func TestInscribe(t *testing.T) {
err := tx.Inscribe(test.ia)
assert.NoError(t, err)

fmt.Println(tx.String())

// Check if the transaction has the expected number of outputs
if len(tx.Outputs) != 1 {
t.Fatalf("Inscribe failed: expected 1 output, got %d", len(tx.Outputs))
Expand Down Expand Up @@ -131,7 +133,7 @@ func TestMultipleInscriptionsIn1Tx(t *testing.T) {
log.Println("tx: ", tx.String())
}

func TestInscription(t *testing.T) {
func TestInscribeFromFile(t *testing.T) {
decodedWif, _ := wif.DecodeWIF("KznpA63DPFrmHecASyL6sFmcRgrNT9oM8Ebso8mwq1dfJF3ZgZ3V")

// get public key bytes and address
Expand Down
115 changes: 99 additions & 16 deletions psbt.go
Original file line number Diff line number Diff line change
@@ -1,43 +1,126 @@
package bt

import (
"bytes"
"context"
"fmt"

"github.com/libsv/go-bt/v2/bscript"
"github.com/libsv/go-bt/v2/sighash"
"github.com/pkg/errors"
)

func MakeOfferToSellOrdinal(ctx context.Context, sellerOutput *Output,
utxo *UTXO, unlocker Unlocker) (*Tx, error) {
// MakeSellOfferArgs contains the arguments
// needed to make an offer to sell an
// ordinal (PSBT).
type MakeSellOfferArgs struct {
SellerReceiveOutput *Output
OrdinalUTXO *UTXO
OrdinalUnlocker Unlocker
}

// MakeOfferToSellOrdinal creates a PBST (Partially Signed Bitcoin
// Transaction) that offers a specific ordinal UTXO for sale at a
// specific price.
func MakeOfferToSellOrdinal(ctx context.Context, msoa *MakeSellOfferArgs) (*Tx, error) {
tx := NewTx()

// add dummy input 0 to balance out tx
// so that the next indices are equal
// for the sighash single as well as
// fo the ordinal theory rules to carry
// over ordinals from input to output
err := tx.From("0000000000000000000000000000000000000000000000000000000000000000", 0, "", 0)
err := tx.FromUTXOs(msoa.OrdinalUTXO)
if err != nil {
return nil, ErrDummyInput
return nil, err
}

err = tx.FromUTXOs(utxo)
tx.AddOutput(msoa.SellerReceiveOutput)

err = tx.FillInput(ctx, msoa.OrdinalUnlocker, UnlockerParams{
InputIdx: 0,
SigHashFlags: sighash.SingleForkID | sighash.AnyOneCanPay,
})
if err != nil {
return nil, err
}

// add dummy output (to be replaced by buyer/taker)
tx.AddDummyOutput()
return tx, nil
}

tx.AddOutput(sellerOutput)
// AcceptSellOfferArgs contains the arguments
// needed to make an offer to sell an
// ordinal (PSBT).
type AcceptSellOfferArgs struct {
PSTx *Tx
Utxos []*UTXO
BuyerReceiveOrdinalScript *bscript.Script
DummyOutputScript *bscript.Script
ChangeScript *bscript.Script
FQ *FeeQuote
}

err = tx.FillInput(ctx, unlocker, UnlockerParams{
InputIdx: 1,
SigHashFlags: sighash.SingleForkID | sighash.AnyOneCanPay,
// AcceptOfferToSellOrdinal 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 AcceptOfferToSellOrdinal(ctx context.Context, asoa *AcceptSellOfferArgs) (*Tx, error) {

// TODO: ValidateSellOffer()
// check if input 1 sat

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

tx := 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)
}

sellerOrdinalInput := asoa.PSTx.Inputs[0] // TODO: get from psbt
tx.addInput(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(&Output{
LockingScript: asoa.DummyOutputScript,
Satoshis: asoa.Utxos[0].Satoshis + asoa.Utxos[1].Satoshis,
})

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

sellerOutput := asoa.PSTx.Outputs[0] // TODO: get from psbt
tx.AddOutput(sellerOutput)

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

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

if !(bytes.Equal(u.TxID, tx.Inputs[j].previousTxID)) {
return nil, errors.New("input and utxo mismatch") // TODO: move to errors.go
}
err = tx.FillInput(context.Background(), *u.Unlocker, UnlockerParams{InputIdx: uint32(j)})
if err != nil {
return nil, err
}
}

return tx, nil
}
86 changes: 67 additions & 19 deletions psbt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package bt_test
import (
"context"
"encoding/hex"
"fmt"
"testing"

"github.com/libsv/go-bk/wif"
Expand All @@ -13,38 +12,87 @@ import (
"github.com/stretchr/testify/assert"
)

func TestMakeOfferToSellPSBT(t *testing.T) {
func TestOfferToSellPSBTNoErrors(t *testing.T) {
t.Run("create PSBT to make an offer to sell ordinal", func(t *testing.T) {

w, _ := wif.DecodeWIF("KzTeKvX5VCWkcaB1DCC2WeU7PML93XpKnqMRhXsyrHqFUhJvJyE9")
pubkey := w.SerialisePubKey()
addr, _ := bscript.NewAddressFromPublicKeyString(hex.EncodeToString(pubkey), true)
s, _ := bscript.NewP2PKHFromAddress(addr.AddressString)
ordWif, _ := wif.DecodeWIF("L42PyNwEKE4XRaa8PzPh7JZurSAWJmx49nbVfaXYuiQg3RCubwn7") // 1JijRHzVfub38S2hizxkxEcVKQwuCTZmxJ
ordPrefixAddr, _ := bscript.NewAddressFromPublicKeyString(hex.EncodeToString(ordWif.SerialisePubKey()), true)
ordPrefixScript, _ := bscript.NewP2PKHFromAddress(ordPrefixAddr.AddressString)

unlocker := unlocker.Getter{PrivateKey: w.PrivKey}
u, _ := unlocker.Unlocker(context.Background(), s)
ordUnlockerGetter := unlocker.Getter{PrivateKey: ordWif.PrivKey}
ordUnlocker, _ := ordUnlockerGetter.Unlocker(context.Background(), ordPrefixScript)

tx, err := bt.MakeOfferToSellOrdinal(context.Background(),
&bt.Output{
Satoshis: 100000,
pstx, err := bt.MakeOfferToSellOrdinal(context.Background(), &bt.MakeSellOfferArgs{
SellerReceiveOutput: &bt.Output{
Satoshis: 500,
LockingScript: func() *bscript.Script {
s, _ := bscript.NewP2PKHFromAddress("1PyWzkfKrq1kakvLTeaCdAL8y8UJAcZAqU")
s, _ := bscript.NewP2PKHFromAddress("1C3V9TTJefP8Hft96sVf54mQyDJh8Ze4w4") // L1JWiLZtCkkqin41XtQ2Jxo1XGxj1R4ydT2zmxPiaeQfuyUK631D
return s
}(),
},
&bt.UTXO{
OrdinalUTXO: &bt.UTXO{
TxID: func() []byte {
t, _ := hex.DecodeString("5a2814071ef18c97248cf6050057cc5df09b34ae9a3105b079c002494b6e277d")
t, _ := hex.DecodeString("8f027fb1361ae46ac165e1d90e5436ed9c11d4eeaa60669ab90386a3abd9ce6a")
return t
}(),
Vout: uint32(0),
LockingScript: func() *bscript.Script {
// hello world (text/plain) test inscription
s, _ := bscript.NewFromHexString("76a914c25e9a2b70ec83d7b4fbd0f36f00a86723a48e6b88ac0063036f72645118746578742f706c61696e3b636861727365743d7574662d38000d48656c6c6f2c20776f726c642168")
return s
}(),
Satoshis: 1,
},
OrdinalUnlocker: ordUnlocker,
})

assert.NoError(t, err)

us := []*bt.UTXO{
{
TxID: func() []byte {
t, _ := hex.DecodeString("61dfcc313763eb5332c036131facdf92c2ca9d663ffb96e4b997086a0643d635")
return t
}(),
Vout: uint32(0),
LockingScript: s,
Satoshis: 2,
LockingScript: ordPrefixScript,
Satoshis: 10,
Unlocker: &ordUnlocker,
},
u)
{
TxID: func() []byte {
t, _ := hex.DecodeString("61dfcc313763eb5332c036131facdf92c2ca9d663ffb96e4b997086a0643d635")
return t
}(),
Vout: uint32(1),
LockingScript: ordPrefixScript,
Satoshis: 10,
Unlocker: &ordUnlocker,
},
{
TxID: func() []byte {
t, _ := hex.DecodeString("8f027fb1361ae46ac165e1d90e5436ed9c11d4eeaa60669ab90386a3abd9ce6a")
return t
}(),
Vout: uint32(1),
LockingScript: ordPrefixScript,
Satoshis: 953,
Unlocker: &ordUnlocker,
},
}

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

_, err = bt.AcceptOfferToSellOrdinal(context.Background(), &bt.AcceptSellOfferArgs{
PSTx: pstx,
Utxos: us,
BuyerReceiveOrdinalScript: buyerOrdS,
DummyOutputScript: dummyS,
ChangeScript: changeS,
FQ: bt.NewFeeQuote(),
})
assert.NoError(t, err)
fmt.Println(tx.String())
// TODO: add checks
})
}
2 changes: 1 addition & 1 deletion tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ func (tx *Tx) estimatedFinalTx() (*Tx, error) {
tempTx := tx.Clone()

for _, in := range tempTx.Inputs {
if !in.PreviousTxScript.IsP2PKH() {
if !(in.PreviousTxScript.IsP2PKH() || in.PreviousTxScript.IsP2PKHInscription()) {
return nil, ErrUnsupportedScript
}
if in.UnlockingScript == nil || len(*in.UnlockingScript) == 0 {
Expand Down
Loading

0 comments on commit bfe9c82

Please sign in to comment.