From 969d7757e8b920c9533835c74495fe16f023321c Mon Sep 17 00:00:00 2001 From: Jad Wahab <15110087+jadwahab@users.noreply.github.com> Date: Wed, 5 Apr 2023 16:54:07 +0300 Subject: [PATCH 1/5] refactor: rename files --- ord/{bidding.go => bid.go} | 0 ord/{bidding_test.go => bid_test.go} | 0 ord/{listing.go => list.go} | 0 ord/{listing_test.go => list_test.go} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename ord/{bidding.go => bid.go} (100%) rename ord/{bidding_test.go => bid_test.go} (100%) rename ord/{listing.go => list.go} (100%) rename ord/{listing_test.go => list_test.go} (100%) diff --git a/ord/bidding.go b/ord/bid.go similarity index 100% rename from ord/bidding.go rename to ord/bid.go diff --git a/ord/bidding_test.go b/ord/bid_test.go similarity index 100% rename from ord/bidding_test.go rename to ord/bid_test.go diff --git a/ord/listing.go b/ord/list.go similarity index 100% rename from ord/listing.go rename to ord/list.go diff --git a/ord/listing_test.go b/ord/list_test.go similarity index 100% rename from ord/listing_test.go rename to ord/list_test.go From b6c7e6440cf360764b32f83e66686ade781ea802 Mon Sep 17 00:00:00 2001 From: Jad Wahab <15110087+jadwahab@users.noreply.github.com> Date: Wed, 5 Apr 2023 20:58:43 +0300 Subject: [PATCH 2/5] refactor!: replace 2 dummies with 1 dummy (list) --- errors.go | 14 ++++---- ord/list.go | 50 ++++++++++++++++++-------- ord/list2dummies.go | 88 +++++++++++++++++++++++++++++++++++++++++++++ ord/list_test.go | 64 +++++++++++++++++++++++---------- 4 files changed, 177 insertions(+), 39 deletions(-) create mode 100644 ord/list2dummies.go diff --git a/errors.go b/errors.go index c62fb8a6..ed077c2b 100644 --- a/errors.go +++ b/errors.go @@ -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") ) diff --git a/ord/list.go b/ord/list.go index 6489dff8..4c6160a6 100644 --- a/ord/list.go +++ b/ord/list.go @@ -93,9 +93,8 @@ 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 @@ -103,49 +102,70 @@ func AcceptOrdinalSaleListing(ctx context.Context, vla *ValidateListingArgs, aso 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++ } diff --git a/ord/list2dummies.go b/ord/list2dummies.go new file mode 100644 index 00000000..7cd55b37 --- /dev/null +++ b/ord/list2dummies.go @@ -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 +} diff --git a/ord/list_test.go b/ord/list_test.go index 05901dd8..2614a01d 100644 --- a/ord/list_test.go +++ b/ord/list_test.go @@ -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 @@ -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") @@ -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{ From a64b4a0b573433349a243ee3d3d48a4892696a9a Mon Sep 17 00:00:00 2001 From: Jad Wahab <15110087+jadwahab@users.noreply.github.com> Date: Sun, 9 Apr 2023 14:29:43 +0300 Subject: [PATCH 3/5] fix: nasty pointer bug in the new code --- ord/list.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ord/list.go b/ord/list.go index 4c6160a6..134d8818 100644 --- a/ord/list.go +++ b/ord/list.go @@ -117,7 +117,7 @@ func AcceptOrdinalSaleListing(ctx context.Context, vla *ValidateListingArgs, aso 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:]...)...) + asoa.UTXOs = append([]*bt.UTXO{u}, append(asoa.UTXOs[:i], asoa.UTXOs[i+1:]...)...) validUTXOFound = true break } @@ -161,7 +161,7 @@ func AcceptOrdinalSaleListing(ctx context.Context, vla *ValidateListingArgs, aso return nil, err } - //nolint:dupl // false positive + //nolint:dupl // TODO: are 2 dummies useful or to be removed? for i, u := range asoa.UTXOs { // skip 2nd input (ordinals input) j := i From 42d5cabc4ba36333d43a52f58e8c31868af84806 Mon Sep 17 00:00:00 2001 From: Jad Wahab <15110087+jadwahab@users.noreply.github.com> Date: Sun, 9 Apr 2023 14:31:39 +0300 Subject: [PATCH 4/5] refactor!: use 1 dummy with bids --- errors.go | 3 +- ord/2dummies.go | 321 +++++++++++++++++++++++++++++++++++++++++++ ord/2dummies_test.go | 128 +++++++++++++++++ ord/bid.go | 133 ++++++++---------- ord/bid_test.go | 61 ++++---- ord/list2dummies.go | 88 ------------ ord/list_test.go | 2 + unlocker.go | 2 + 8 files changed, 539 insertions(+), 199 deletions(-) create mode 100644 ord/2dummies.go create mode 100644 ord/2dummies_test.go delete mode 100644 ord/list2dummies.go diff --git a/errors.go b/errors.go index ed077c2b..2caa8dc7 100644 --- a/errors.go +++ b/errors.go @@ -71,7 +71,6 @@ var ( 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") + ErrInsufficientFees = errors.New("fee paid not enough with new locking script") ) diff --git a/ord/2dummies.go b/ord/2dummies.go new file mode 100644 index 00000000..b5110ee4 --- /dev/null +++ b/ord/2dummies.go @@ -0,0 +1,321 @@ +package ord + +import ( + "bytes" + "context" + "encoding/hex" + "fmt" + + "github.com/libsv/go-bt/v2" + "github.com/libsv/go-bt/v2/bscript" + "github.com/libsv/go-bt/v2/sighash" + "github.com/pkg/errors" +) + +// TODO: are 2 dummies useful or to be removed? + +// 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 // TODO: are 2 dummies useful or to be removed? + 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 +} + +// MakeBid2DArgs contains the arguments +// needed to make a bid to buy an +// ordinal. +type MakeBid2DArgs struct { + BidAmount uint64 + OrdinalTxID string + OrdinalVOut uint32 + BidderUTXOs []*bt.UTXO + BuyerReceiveOrdinalScript *bscript.Script + DummyOutputScript *bscript.Script + ChangeScript *bscript.Script + FQ *bt.FeeQuote +} + +// MakeBidToBuy1SatOrdinal makes a bid offer to buy a 1 sat ordinal +// at a specific price - this tx will be partially signed and will +// need to be completed by the seller if they accept the bid. Multiple +// people can make different bids and the seller will need to choose +// only one to go through and broadcast to the node network. +// +// Note: this function is meant for ordinals in 1 satoshi outputs instead +// of ordinal ranges in 1 output (>1 satoshi outputs). +func MakeBidToBuy1SatOrdinal2Dummies(ctx context.Context, mba *MakeBid2DArgs) (*bt.Tx, error) { + if len(mba.BidderUTXOs) < 3 { + return nil, bt.ErrInsufficientUTXOs + } + + tx := bt.NewTx() + + // add dummy inputs + err := tx.FromUTXOs(mba.BidderUTXOs[0], mba.BidderUTXOs[1]) + if err != nil { + return nil, fmt.Errorf(`failed to add inputs: %w`, err) + } + + OrdinalTxIDBytes, err := hex.DecodeString(mba.OrdinalTxID) + if err != nil { + return nil, err + } + emptyOrdInput := &bt.Input{ + PreviousTxOutIndex: mba.OrdinalVOut, + PreviousTxScript: func() *bscript.Script { + //nolint:lll // add dummy ordinal PreviousTxScript + // so that the change function can estimate + // UnlockingScript sizes + s, _ := bscript.NewFromHexString("76a914c25e9a2b70ec83d7b4fbd0f36f00a86723a48e6b88ac0063036f72645118746578742f706c61696e3b636861727365743d7574662d38000d48656c6c6f2c20776f726c642168") // hello world (text/plain) test inscription + return s + }(), + } + err = emptyOrdInput.PreviousTxIDAdd(OrdinalTxIDBytes) + if err != nil { + return nil, fmt.Errorf(`failed to add ordinal input: %w`, err) + } + tx.Inputs = append(tx.Inputs, emptyOrdInput) + + // add payment input(s) + err = tx.FromUTXOs(mba.BidderUTXOs[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: mba.DummyOutputScript, + Satoshis: mba.BidderUTXOs[0].Satoshis + mba.BidderUTXOs[1].Satoshis, + }) + + // add ordinal receive output + tx.AddOutput(&bt.Output{ + LockingScript: mba.BuyerReceiveOrdinalScript, + Satoshis: 1, + }) + + tx.AddOutput(&bt.Output{ + Satoshis: mba.BidAmount, + LockingScript: func() *bscript.Script { // add dummy p2pkh script to calc fees accurately + s, _ := bscript.NewP2PKHFromAddress("1FunnyJoke111111111111111112AVXh5") + return s + }(), + }) + + err = tx.Change(mba.ChangeScript, mba.FQ) + if err != nil { + return nil, err + } + + //nolint: dupl // TODO: are 2 dummies useful or to be removed? + for i, u := range mba.BidderUTXOs { + // 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), + SigHashFlags: sighash.SingleForkID, + }) + if err != nil { + return nil, err + } + } + + return tx, nil +} + +// ValidateBid2DArgs are the arguments needed to +// validate a specific bid to buy an ordinal. +// +// Note: index 2 should be the listed ordinal input. +type ValidateBid2DArgs struct { + PreviousUTXOs []*bt.UTXO // index 2 should be the listed ordinal input + BidAmount uint64 + ExpectedFQ *bt.FeeQuote +} + +// Validate a bid to buy an ordinal +// given specific validation parameters. +func (vba *ValidateBid2DArgs) Validate(pstx *bt.Tx) bool { + if pstx.InputCount() < 4 { + return false + } + if pstx.OutputCount() < 4 { + return false + } + + // check previous utxos match inputs + if len(vba.PreviousUTXOs) != pstx.InputCount() { + return false + } + for i := range vba.PreviousUTXOs { + if !bytes.Equal(pstx.Inputs[i].PreviousTxID(), vba.PreviousUTXOs[i].TxID) { + return false + } + if uint64(pstx.Inputs[i].PreviousTxOutIndex) != uint64(vba.PreviousUTXOs[i].Vout) { + return false + } + } + + // check passthrough dummy inputs and output to avoid + // mismatching and losing the ordinal to another output + if (vba.PreviousUTXOs[0].Satoshis + vba.PreviousUTXOs[1].Satoshis) != pstx.Outputs[0].Satoshis { + return false + } + + // check lou (ListedOrdinalUTXO) matches supplied pstx input index 2 + pstxOrdinalInput := pstx.Inputs[2] + if !bytes.Equal(pstxOrdinalInput.PreviousTxID(), vba.PreviousUTXOs[2].TxID) { + return false + } + if uint64(pstxOrdinalInput.PreviousTxOutIndex) != uint64(vba.PreviousUTXOs[2].Vout) { + return false + } + + // check enough fees paid + pstx.Outputs[2].Satoshis = vba.BidAmount + enough, err := pstx.IsFeePaidEnough(vba.ExpectedFQ) + if err != nil || !enough { + return false + } + + // TODO: check signatures valid + + return true +} + +// AcceptBid2DArgs contains the arguments +// needed to accept a bid to buy an +// ordinal. +type AcceptBid2DArgs struct { + PSTx *bt.Tx + SellerReceiveOrdinalScript *bscript.Script + OrdinalUnlocker bt.Unlocker + ExtraUTXOs []*bt.UTXO +} + +// AcceptBidToBuy1SatOrdinal2Dummies creates a PBST (Partially Signed Bitcoin +// Transaction) that offers a specific ordinal UTXO for sale at a +// specific price. +func AcceptBidToBuy1SatOrdinal2Dummies(ctx context.Context, vba *ValidateBid2DArgs, + aba *AcceptBid2DArgs) (*bt.Tx, error) { + + if valid := vba.Validate(aba.PSTx); !valid { + return nil, bt.ErrInvalidSellOffer + } + + if !aba.SellerReceiveOrdinalScript.IsP2PKH() { + // TODO: if a script different to/bigger than p2pkh is used to + // receive the ordinal, then the seller may need to add extra + // utxos `aba.ExtraUTXOs` to cover the extra bytes since the + // bidder only accounted for p2pkh script when calculating their + // change. + return nil, errors.New("only receive to p2pkh supported for now") + } + + tx, err := bt.NewTxFromBytes(aba.PSTx.Bytes()) + if err != nil { + return nil, err + } + + if tx.Outputs[2] == nil { + return nil, errors.New("ordinal output expected in index 2 doesn't exist") + } + tx.Outputs[2].LockingScript = aba.SellerReceiveOrdinalScript + + if tx.Inputs[2] == nil { + return nil, errors.New("ordinal input expected in index 2 doesn't exist") + } + tx.Inputs[2].PreviousTxScript = vba.PreviousUTXOs[2].LockingScript + tx.Inputs[2].PreviousTxSatoshis = vba.PreviousUTXOs[2].Satoshis + err = tx.FillInput(ctx, aba.OrdinalUnlocker, bt.UnlockerParams{InputIdx: 2}) + if err != nil { + return nil, err + } + + return tx, nil +} diff --git a/ord/2dummies_test.go b/ord/2dummies_test.go new file mode 100644 index 00000000..7c2172b8 --- /dev/null +++ b/ord/2dummies_test.go @@ -0,0 +1,128 @@ +package ord_test + +import ( + "context" + "encoding/hex" + "testing" + + "github.com/libsv/go-bk/wif" + "github.com/libsv/go-bt/v2" + "github.com/libsv/go-bt/v2/bscript" + "github.com/libsv/go-bt/v2/ord" + "github.com/libsv/go-bt/v2/unlocker" + "github.com/stretchr/testify/assert" +) + +func TestBidToBuyPSBT2DNoErrors(t *testing.T) { + fundingWif, _ := wif.DecodeWIF("L5W2nyKUCsDStVUBwZj2Q3Ph5vcae4bgdzprZDYqDpvZA8AFguFH") // 19NfKd8aTwvb5ngfP29RxgfQzZt8KAYtQo + fundingAddr, _ := bscript.NewAddressFromPublicKeyString(hex.EncodeToString(fundingWif.SerialisePubKey()), true) + fundingScript, _ := bscript.NewP2PKHFromAddress(fundingAddr.AddressString) + fundingUnlockerGetter := unlocker.Getter{PrivateKey: fundingWif.PrivKey} + fundingUnlocker, _ := fundingUnlockerGetter.Unlocker(context.Background(), fundingScript) + + bidAmount := 250 + + us := []*bt.UTXO{ + { + TxID: func() []byte { + t, _ := hex.DecodeString("411084d83d4f380cfc331ed849946bd7f354ca17138dbd723a6420ec9f5f4bd1") + return t + }(), + Vout: uint32(0), + LockingScript: fundingScript, + Satoshis: 20, + Unlocker: &fundingUnlocker, + }, + { + TxID: func() []byte { + t, _ := hex.DecodeString("411084d83d4f380cfc331ed849946bd7f354ca17138dbd723a6420ec9f5f4bd1") + return t + }(), + Vout: uint32(1), + LockingScript: fundingScript, + Satoshis: 20, + Unlocker: &fundingUnlocker, + }, + { + TxID: func() []byte { + t, _ := hex.DecodeString("4d815adc39a740810cb438eb285f6e08ae3957fdc4e4806399babfa806dfc456") + return t + }(), + Vout: uint32(0), + LockingScript: fundingScript, + Satoshis: 100000000, + Unlocker: &fundingUnlocker, + }, + } + + ordWif, _ := wif.DecodeWIF("KwQq67d4Jds3wxs3kQHB8PPwaoaBQfNKkzAacZeMesb7zXojVYpj") // 1HebepswCi6huw1KJ7LvkrgemAV63TyVUs + ordPrefixAddr, _ := bscript.NewAddressFromPublicKeyString(hex.EncodeToString(ordWif.SerialisePubKey()), true) + ordPrefixScript, _ := bscript.NewP2PKHFromAddress(ordPrefixAddr.AddressString) + ordUnlockerGetter := unlocker.Getter{PrivateKey: ordWif.PrivKey} + ordUnlocker, _ := ordUnlockerGetter.Unlocker(context.Background(), ordPrefixScript) + + ordUTXO := &bt.UTXO{ + TxID: func() []byte { + t, _ := hex.DecodeString("e17d7856c375640427943395d2341b6ed75f73afc8b22bb3681987278978a584") + return t + }(), + Vout: uint32(81), + LockingScript: func() *bscript.Script { + s, _ := bscript.NewFromHexString("76a914b69e544cbf33c4eabdd5cf8792cd4e53f5ed6d1788ac") + return s + }(), + Satoshis: 1, + } + + pstx, CreateBidError := ord.MakeBidToBuy1SatOrdinal2Dummies(context.Background(), &ord.MakeBid2DArgs{ + BidAmount: uint64(bidAmount), + OrdinalTxID: ordUTXO.TxIDStr(), + OrdinalVOut: ordUTXO.Vout, + BidderUTXOs: us, + BuyerReceiveOrdinalScript: func() *bscript.Script { + s, _ := bscript.NewP2PKHFromAddress("12R2qFEoUtWwwVecgrkxwMZNnMq6GB8pQW") // L3kLQ9rpDBLgbh3GfPSbXDGwxgmK2Dcb6Qrp4JZRRcne8FMDZWDc + return s + }(), + DummyOutputScript: func() *bscript.Script { + s, _ := bscript.NewP2PKHFromAddress("19NfKd8aTwvb5ngfP29RxgfQzZt8KAYtQo") // L1JWiLZtCkkqin41XtQ2Jxo1XGxj1R4ydT2zmxPiaeQfuyUK631D + return s + }(), + ChangeScript: func() *bscript.Script { + s, _ := bscript.NewP2PKHFromAddress("19NfKd8aTwvb5ngfP29RxgfQzZt8KAYtQo") // L1JWiLZtCkkqin41XtQ2Jxo1XGxj1R4ydT2zmxPiaeQfuyUK631D + return s + }(), + FQ: bt.NewFeeQuote(), + }) + + t.Run("no errors creating bid to buy ordinal", func(t *testing.T) { + assert.NoError(t, CreateBidError) + }) + + t.Run("validate PSBT bid to buy ordinal", func(t *testing.T) { + vba := &ord.ValidateBid2DArgs{ + BidAmount: uint64(bidAmount), + ExpectedFQ: bt.NewFeeQuote(), + // insert ordinal utxo at index 2 + PreviousUTXOs: append(us[:2], append([]*bt.UTXO{ordUTXO}, us[2:]...)...), + } + assert.True(t, vba.Validate(pstx)) + }) + + t.Run("no errors when accepting bid", func(t *testing.T) { + _, err := ord.AcceptBidToBuy1SatOrdinal2Dummies(context.Background(), &ord.ValidateBid2DArgs{ + BidAmount: uint64(bidAmount), + ExpectedFQ: bt.NewFeeQuote(), + PreviousUTXOs: append(us[:2], append([]*bt.UTXO{ordUTXO}, us[2:]...)...), + }, + &ord.AcceptBid2DArgs{ + PSTx: pstx, + SellerReceiveOrdinalScript: func() *bscript.Script { + s, _ := bscript.NewP2PKHFromAddress("1C3V9TTJefP8Hft96sVf54mQyDJh8Ze4w4") // L1JWiLZtCkkqin41XtQ2Jxo1XGxj1R4ydT2zmxPiaeQfuyUK631D + return s + }(), + OrdinalUnlocker: ordUnlocker, + }) + + assert.NoError(t, err) + }) +} diff --git a/ord/bid.go b/ord/bid.go index 867ec50b..0ef5f9b9 100644 --- a/ord/bid.go +++ b/ord/bid.go @@ -9,7 +9,6 @@ import ( "github.com/libsv/go-bt/v2" "github.com/libsv/go-bt/v2/bscript" "github.com/libsv/go-bt/v2/sighash" - "github.com/pkg/errors" ) // MakeBidArgs contains the arguments @@ -35,14 +34,28 @@ type MakeBidArgs struct { // Note: this function is meant for ordinals in 1 satoshi outputs instead // of ordinal ranges in 1 output (>1 satoshi outputs). func MakeBidToBuy1SatOrdinal(ctx context.Context, mba *MakeBidArgs) (*bt.Tx, error) { - if len(mba.BidderUTXOs) < 3 { + if len(mba.BidderUTXOs) < 2 { return nil, bt.ErrInsufficientUTXOs } + // check at least 1 utxo is larger than the listed ordinal price + validUTXOFound := false + for i, u := range mba.BidderUTXOs { + if u.Satoshis > mba.BidAmount { + // Move the UTXO at index i to the beginning + mba.BidderUTXOs = append([]*bt.UTXO{u}, append(mba.BidderUTXOs[:i], mba.BidderUTXOs[i+1:]...)...) + validUTXOFound = true + break + } + } + if !validUTXOFound { + return nil, bt.ErrInsufficientUTXOValue + } + tx := bt.NewTx() // add dummy inputs - err := tx.FromUTXOs(mba.BidderUTXOs[0], mba.BidderUTXOs[1]) + err := tx.FromUTXOs(mba.BidderUTXOs[0]) if err != nil { return nil, fmt.Errorf(`failed to add inputs: %w`, err) } @@ -68,21 +81,15 @@ func MakeBidToBuy1SatOrdinal(ctx context.Context, mba *MakeBidArgs) (*bt.Tx, err tx.Inputs = append(tx.Inputs, emptyOrdInput) // add payment input(s) - err = tx.FromUTXOs(mba.BidderUTXOs[2:]...) + err = tx.FromUTXOs(mba.BidderUTXOs[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: mba.DummyOutputScript, - Satoshis: mba.BidderUTXOs[0].Satoshis + mba.BidderUTXOs[1].Satoshis, - }) - - // add ordinal receive output - tx.AddOutput(&bt.Output{ - LockingScript: mba.BuyerReceiveOrdinalScript, - Satoshis: 1, + Satoshis: mba.BidderUTXOs[0].Satoshis - mba.BidAmount, }) tx.AddOutput(&bt.Output{ @@ -93,15 +100,22 @@ func MakeBidToBuy1SatOrdinal(ctx context.Context, mba *MakeBidArgs) (*bt.Tx, err }(), }) + // add ordinal receive output + tx.AddOutput(&bt.Output{ + LockingScript: mba.BuyerReceiveOrdinalScript, + Satoshis: 1, + }) + err = tx.Change(mba.ChangeScript, mba.FQ) if err != nil { return nil, err } + //nolint: dupl // TODO: are 2 dummies useful or to be removed? for i, u := range mba.BidderUTXOs { - // skip 3rd input (ordinals input) + // skip 2nd input (ordinals input) j := i - if i >= 2 { + if i >= 1 { j++ } @@ -128,54 +142,36 @@ func MakeBidToBuy1SatOrdinal(ctx context.Context, mba *MakeBidArgs) (*bt.Tx, err // ValidateBidArgs are the arguments needed to // validate a specific bid to buy an ordinal. -// -// Note: index 2 should be the listed ordinal input. +// as they appear in the tx. type ValidateBidArgs struct { - PreviousUTXOs []*bt.UTXO // index 2 should be the listed ordinal input - BidAmount uint64 - ExpectedFQ *bt.FeeQuote + OrdinalUTXO *bt.UTXO + BidAmount uint64 + ExpectedFQ *bt.FeeQuote } // Validate a bid to buy an ordinal // given specific validation parameters. func (vba *ValidateBidArgs) Validate(pstx *bt.Tx) bool { - if pstx.InputCount() < 4 { + if pstx.InputCount() < 3 { return false } - if pstx.OutputCount() < 4 { + if pstx.OutputCount() < 3 { // technically should have 4 including change return false } - // check previous utxos match inputs - if len(vba.PreviousUTXOs) != pstx.InputCount() { + // check OrdinalUTXO matches supplied pstx input index 1 + pstxOrdinalInput := pstx.Inputs[1] + if !bytes.Equal(pstxOrdinalInput.PreviousTxID(), vba.OrdinalUTXO.TxID) { return false } - for i := range vba.PreviousUTXOs { - if !bytes.Equal(pstx.Inputs[i].PreviousTxID(), vba.PreviousUTXOs[i].TxID) { - return false - } - if uint64(pstx.Inputs[i].PreviousTxOutIndex) != uint64(vba.PreviousUTXOs[i].Vout) { - return false - } - } - - // check passthrough dummy inputs and output to avoid - // mismatching and losing the ordinal to another output - if (vba.PreviousUTXOs[0].Satoshis + vba.PreviousUTXOs[1].Satoshis) != pstx.Outputs[0].Satoshis { + if uint64(pstxOrdinalInput.PreviousTxOutIndex) != uint64(vba.OrdinalUTXO.Vout) { return false } - // check lou (ListedOrdinalUTXO) matches supplied pstx input index 2 - pstxOrdinalInput := pstx.Inputs[2] - if !bytes.Equal(pstxOrdinalInput.PreviousTxID(), vba.PreviousUTXOs[2].TxID) { - return false - } - if uint64(pstxOrdinalInput.PreviousTxOutIndex) != uint64(vba.PreviousUTXOs[2].Vout) { - return false - } + // set the value of the output for the bid amount + pstx.Outputs[1].Satoshis = vba.BidAmount - // check enough funds paid - pstx.Outputs[2].Satoshis = vba.BidAmount + // check enough fees paid enough, err := pstx.IsFeePaidEnough(vba.ExpectedFQ) if err != nil || !enough { return false @@ -187,48 +183,35 @@ func (vba *ValidateBidArgs) Validate(pstx *bt.Tx) bool { } // AcceptBidArgs contains the arguments -// needed to make an offer to sell an +// needed to accept a bid to buy an // ordinal. type AcceptBidArgs struct { - PSTx *bt.Tx - SellerReceiveOrdinalScript *bscript.Script - OrdinalUnlocker bt.Unlocker - ExtraUTXOs []*bt.UTXO + PSTx *bt.Tx + SellerReceiveScript *bscript.Script + OrdinalUnlocker bt.Unlocker } -// AcceptBidToBuy1SatOrdinal creates a PBST (Partially Signed Bitcoin -// Transaction) that offers a specific ordinal UTXO for sale at a -// specific price. +// AcceptBidToBuy1SatOrdinal accepts a partially signed Bitcoin +// transaction bid to buy an ordinal. +// func AcceptBidToBuy1SatOrdinal(ctx context.Context, vba *ValidateBidArgs, aba *AcceptBidArgs) (*bt.Tx, error) { if valid := vba.Validate(aba.PSTx); !valid { return nil, bt.ErrInvalidSellOffer } - if !aba.SellerReceiveOrdinalScript.IsP2PKH() { - // TODO: if a script different to/bigger than p2pkh is used to - // receive the ordinal, then the seller may need to add extra - // utxos `aba.ExtraUTXOs` to cover the extra bytes since the - // bidder only accounted for p2pkh script when calculating their - // change. - return nil, errors.New("only receive to p2pkh supported for now") - } - - tx, err := bt.NewTxFromBytes(aba.PSTx.Bytes()) - if err != nil { - return nil, err - } + tx := aba.PSTx.Clone() - if tx.Outputs[2] == nil { - return nil, bt.ErrOrdinalOutputNoExist + tx.Outputs[1].LockingScript = aba.SellerReceiveScript + // check if fees paid are still enough with new + // locking script + enough, err := tx.IsFeePaidEnough(vba.ExpectedFQ) + if err != nil || !enough { + return nil, bt.ErrInsufficientFees } - tx.Outputs[2].LockingScript = aba.SellerReceiveOrdinalScript - if tx.Inputs[2] == nil { - return nil, bt.ErrOrdinalInputNoExist - } - tx.Inputs[2].PreviousTxScript = vba.PreviousUTXOs[2].LockingScript - tx.Inputs[2].PreviousTxSatoshis = vba.PreviousUTXOs[2].Satoshis - err = tx.FillInput(ctx, aba.OrdinalUnlocker, bt.UnlockerParams{InputIdx: 2}) + tx.Inputs[1].PreviousTxScript = vba.OrdinalUTXO.LockingScript + tx.Inputs[1].PreviousTxSatoshis = vba.OrdinalUTXO.Satoshis + err = tx.FillInput(ctx, aba.OrdinalUnlocker, bt.UnlockerParams{InputIdx: 1}) if err != nil { return nil, err } diff --git a/ord/bid_test.go b/ord/bid_test.go index 9286803f..c8257351 100644 --- a/ord/bid_test.go +++ b/ord/bid_test.go @@ -3,6 +3,7 @@ package ord_test import ( "context" "encoding/hex" + "fmt" "testing" "github.com/libsv/go-bk/wif" @@ -14,61 +15,45 @@ import ( ) func TestBidToBuyPSBTNoErrors(t *testing.T) { - fundingWif, _ := wif.DecodeWIF("L5W2nyKUCsDStVUBwZj2Q3Ph5vcae4bgdzprZDYqDpvZA8AFguFH") // 19NfKd8aTwvb5ngfP29RxgfQzZt8KAYtQo + fundingWif, _ := wif.DecodeWIF("L42PyNwEKE4XRaa8PzPh7JZurSAWJmx49nbVfaXYuiQg3RCubwn7") // 1JijRHzVfub38S2hizxkxEcVKQwuCTZmxJ fundingAddr, _ := bscript.NewAddressFromPublicKeyString(hex.EncodeToString(fundingWif.SerialisePubKey()), true) fundingScript, _ := bscript.NewP2PKHFromAddress(fundingAddr.AddressString) fundingUnlockerGetter := unlocker.Getter{PrivateKey: fundingWif.PrivKey} fundingUnlocker, _ := fundingUnlockerGetter.Unlocker(context.Background(), fundingScript) - bidAmount := 250 + bidAmount := 500 us := []*bt.UTXO{ { TxID: func() []byte { - t, _ := hex.DecodeString("411084d83d4f380cfc331ed849946bd7f354ca17138dbd723a6420ec9f5f4bd1") + t, _ := hex.DecodeString("e3e0c0b46826ae1cd8932daf70b280d686104cdd5c685dbe6bed823e437f9040") return t }(), Vout: uint32(0), LockingScript: fundingScript, - Satoshis: 20, + Satoshis: 900, Unlocker: &fundingUnlocker, }, { TxID: func() []byte { - t, _ := hex.DecodeString("411084d83d4f380cfc331ed849946bd7f354ca17138dbd723a6420ec9f5f4bd1") - return t - }(), - Vout: uint32(1), - LockingScript: fundingScript, - Satoshis: 20, - Unlocker: &fundingUnlocker, - }, - { - TxID: func() []byte { - t, _ := hex.DecodeString("4d815adc39a740810cb438eb285f6e08ae3957fdc4e4806399babfa806dfc456") + t, _ := hex.DecodeString("44ab22c6996ce2dee4829fa171dd2543f16bd35b7373aa446b3060bdbf43b588") return t }(), Vout: uint32(0), LockingScript: fundingScript, - Satoshis: 100000000, + Satoshis: 500, Unlocker: &fundingUnlocker, }, } - ordWif, _ := wif.DecodeWIF("KwQq67d4Jds3wxs3kQHB8PPwaoaBQfNKkzAacZeMesb7zXojVYpj") // 1HebepswCi6huw1KJ7LvkrgemAV63TyVUs - ordPrefixAddr, _ := bscript.NewAddressFromPublicKeyString(hex.EncodeToString(ordWif.SerialisePubKey()), true) - ordPrefixScript, _ := bscript.NewP2PKHFromAddress(ordPrefixAddr.AddressString) - ordUnlockerGetter := unlocker.Getter{PrivateKey: ordWif.PrivKey} - ordUnlocker, _ := ordUnlockerGetter.Unlocker(context.Background(), ordPrefixScript) - ordUTXO := &bt.UTXO{ TxID: func() []byte { - t, _ := hex.DecodeString("e17d7856c375640427943395d2341b6ed75f73afc8b22bb3681987278978a584") + t, _ := hex.DecodeString("75e24ffd0161f094a5e419dba42684c69faeacbeb805a1d9afdb29f6f4ac81ad") return t }(), - Vout: uint32(81), + Vout: uint32(0), LockingScript: func() *bscript.Script { - s, _ := bscript.NewFromHexString("76a914b69e544cbf33c4eabdd5cf8792cd4e53f5ed6d1788ac") + s, _ := bscript.NewFromHexString("76a914fabef78de0d136d0f9b13f047312fc4df094da9c88ac0063036f72645109696d6167652f706e67004dcf0e89504e470d0a1a0a0000000d4948445200000180000001800806000000a4c7b5bf00000006624b474400ff00ff00ffa0bda79300000e8449444154789cedddfb93ddf55dc7f1b339bb9b4d36091b2021d484d2909f8ab6dafa030816158233b6462c6da509f710508142200912728329500a4941e496001129424ba1741cb5b5056d404b2df687d21116103b4d099799e5127673367b76cffa47bc985977de8fc7ef2fce59f6ec79e6fbcbe7d3680000000000000000000000000000000000000000000000001fb4aea97e034cad93ffe094c9643fd988e68da79e7a32fa0c7efa8f3e13bd81f787df4fe68d3d7b7e18bdffe5a79c1abdffd183a3c9bcf1f4d37b7c07143663aadf0000534300008a120080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a29c053ec53ef9c9df8ece83efede98d5e7ff7ee07a27d57f8115a7dc179d1fe6f77ff5db49f9cccee33b8f892bf88f637dfbc3dda0f0c0c44fb95abce88f6e917c833fffe8cefa029e40900a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008aea9eea37b060c182e840f6fefe39d1ebffe217ff1b9d47fed18f1e1bbdff8bfff29264def8d0877e2dda5f78d105d17ef3a6add1be6f565fb4efe9ed89f66f0f0d45fbb1f658b43f78f060b45f159ee77fdbadb747fbd75e7b2ddabfb77f63f4f7f3fcf33f8bfe7e972d5b16bdfe8c19cd64de78e9a5c129bd0fc11300405102005094000014250000450900405102005094000014250000450900405102005094000014250000450900405102005094000014159f457dc8218744e7699f78e2ef46afff918f2c8df64f3df983687fdd755f8ef6ed763bdadff6d7b746fb679ffd51f41938e9532745bfff6677762545dfccec3e8103ad03d1be199e07dfdd93fdfcdffbde77a3dfdf09279c18fdfed6afbb2a9937dae17d0ad7df90fdfd9dbafcd468fff2cb2f47fb1f3dfb1fd1fe8d37de887eff9e00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0a8f83e80d4c0c040741ef9e74eff7cf4fa73e7ce8bf64b9766f711dc7fffbdd1beab2bfb15ce9f7f68b4df78f535d17ecbb6cdd1bebfbf3fda4f8c8f47fb76b87fe8eb0f47fb73ce392bda0f8f0c47fbf43e8b73ce3e37da2f5c7844b47feeb9ff8cf68f7c23fbfdeddbb76f4abf833d0100142500004509004051020050940000142500004509004051020050940000142500004509004051020050940000142500004509004051537e1f406ac9e225d17d022bfee4b4e8f5bffffd7f89f63d3d3dd17efdbaaba2fdb265cba2fdd51bb3d7ef9f1d9ee73f3111edbb7bbaa3fd783bbb0f20fd0bdcb5f3be68ff3fafbc12edaf5c7f45b41f0fef13b8ecb2b5d17ec7d7b647fb175ef8ef69fd1dea0900a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008aca0e43ff7f203dcffdc8458ba2fdb6add746fb77de7927daefdc7957b4ef4c46d7293476dd736fb4bfe4d28ba3fd64237bff5d5dd971ee336664ff867af49b8f45fb15a77d26dab7c7c6a2fd9d77dc1ded878686a2fdcf7ffe7cb41f1d1d8df6d39d270080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100286adadf07303e3e1eedd3f3c87b7a7aa3fd830f3e10ede7ce9b17edaffff20dd1fef0c31744fbd9b36747fbf787df8ff6e97d02ad562bdabff1e61bd17ef3a6add17ee3357f15edd76fb832da9f7fdeea681f5ee7d0e8edcdfe7ea73b4f00004509004051020050940000142500004509004051020050940000142500004509004051020050940000142500004509004051020050d4b4bf0fa06f665fb43ffcf0c3a3fda2458ba2fdecfefe68df6c36a37d7a1efa9967ad8cf6add1ec3cfdf4e70faf0368f487bfbff3579f1bedefdd797fb44fdf7f7a1fc7c4c444b43fe288ecefaf3a4f00004509004051020050940000142500004509004051020050940000142500004509004051020050940000142500004509004051020050d4b4bf0f60786424dacf9a353bdaefdfbf3fdab70e1c88f6dbb65e17ed3b9dec40fc6ddbae8df637dc787db41f1e1e8ef6139dec3cfa766b2cda3ff6ad27a2fd2bafbc1cedd3cfcf359bae8ef6b36767f711bcf7debbd1be3d96fdfea63b4f00004509004051020050940000142500004509004051020050940000142500004509004051020050940000142500004509004051020050d4b4bf0fa0afaf2fda8f1cc8ee1378e8a1af47fb175f7ca12bd96fde724d74a07f7f7f761e7b7777f611ea743ad1bed96c46fb59b36645fbce44f6fe3ff7f93f8df6b3c3fb2cde7e7b28daffe4b99f449fdf919191e8f3bbf6f22b9279637232bb0f63baf304005094000014250000450900405102005094000014250000450900405102005094000014250000450900405102005094000014250000454dfbfb00daed76b41f1acace43ef09cfc33ff6d85f8f0e24ffca8d3745af3f7ffea1d17ee1c285d1feadb7de8af61ffb8d8f45fbbd7b7f19ed7b67ce8cf6edb1ecf3fbdefef7a2fdfc81f9d17ed5995f8c3ebfe9fb6fb55ad17e46b3f6bf816bfff4008509004051020050940000142500004509004051020050940000142500004509004051020050940000142500004509004051020050d494df07b060c1c2e83cf1952b5745af7fe4a223a3fdc4c444b44fcf931f08cf736f369bd1fec5175f8cf61f3eeaa8687ff2f2df8ff6a9fb76ed8ef6938de8e3df38e28845d1fedd77df89f6cdeeecf3d31d7efede7cebcd687fc5da75d17efb8e9ba35fe0abafbeda15bd8190270080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100282a3e8bfab0c30e8bcec33ee18413a3d74fcfa33f75f9a9d1fee31fffcd687fef7dbba27d4f774fb44f3f01b3fa6645fb894e769f425757f603743a9d689fea84f7494c66d709c4f7110c0f0f47fbad5bb645fbc1c1c168bfebde9dd1feb8e38e8ff64f3ef98368fffaebfba23f004f000045090040510200509400001425000045090040510200509400001425000045090040510200509400001425000045090040510200505477fa1f181a1a8acea37efae93dd181e4871e7a68326f2c5ebc24daefd8714bb4dfbefd6bd1fe9a4d1ba3fd2d376f8ff603870c44fba38efa70b46fb55ad17ed1914746fb919191687f707434da379bcd687fda675744fbf5eb3644fb2d5b3645fb73cf3d3fdab7db63d1fe99a7f744fbf43cff94270080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100282abe0f203567cedc68bf7cf91f46fbd1d1ec3cf9f1898968bf65ebe668dfdbdb1bedd3fb10d6acc9ce63ef9a91fd1be4d16f3c16ed0f1e3c18ed57acf874b46f84a7c13fb0fbc1689f7e7eeeb9e7ae68df13befedcb9d9f7c7972ebd3cdadf7adb8e683fd53c0100142500004509004051020050940000142500004509004051020050940000142500004509004051020050940000142500004509004051f17d0073e6cc994cf6471f7d74f4fa4f3cf178b4df78f5a6689fbafcb2b5d17efb8e5ba27dab95dd8730d66e47fb7ff8ce3f46fb2ffcd9e9d13e3d8ffedb8f7f27daaf3aeb8bd1bed9dd8cf65de185046bd75e19ed376fc9fefefafa6645fbf43cff157f7c5ab4fff6138f47df9ffbf6bd16fd023d0100142500004509004051020050940000142500004509004051020050940000142500004509004051020050940000142500004509004051f17d00e979dc4b162f89f6fbf6ed8bf677dcf937d17efdba0dd17e71f8f3cf9b3b37da5ff4e76ba27d7a1efeca556744fbf6f878b4ef74a2e3d81b67acfc42b47ffc5b4f44fbcf9e9e9d473fd61e8bf6232323d17edbd6eba2fda64d5747fb836307a3fdde5fed8df693939d689ff20400509400001425000045090040510200509400001425000045090040510200509400001425000045090040510200509400001425000045754df51b58b278497420fbde5fed8d7e8663961e13bdfecc99339379e3908181683fda6a45fb471e7934dabffefaebd1fe2b5fb921da4f7426a27da7939dc7dedbdb1bedefbe6b67b41f1c1c8cf6377df5c6683fdeceee63d8fffefe68dfd5957d85fdf4a7ff15fd07962c392afbfedafbcb29fd0ef60400509400001425000045090040510200509400001425000045090040510200509400001425000045090040510200509400001425000045754ff51b88cff33f263bcfffbc73cf4fe68d471ffd66b44fedda757fb4bff0a20ba2fde464f4bfbff1c0ee07a3fdea35e745fbee66f62770fb6d7744fb3517ae8ef6fbf767e7e96fdb7a6db4bff6daadd1bea7a727da9f75e6d9d17e646438fa000f0e0e4ef99d2a094f00004509004051020050940000142500004509004051020050940000142500004509004051020050940000142500004509004051020050d4b43ecbfa83b07469769fc0a5977c297afdf43cf4871ffefb68bf61c355d1fe778e3f21da9f7dce99d1fe9fbffb4f53fa193ee5e4e5d1e7e7a69b6e8e5e7f4eff9c689fdea7b07edd8668dfe974a2fd96ad9ba3fdf3cfffacf477a0270080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a028010028aaf459d81f844ffcd627a2f3e02fbef8d2e8f5efbefbce683f77debc68df887efa46e3a97f7db2f467f0a44ffd5ef47f70ac3d16bd7e734633dadf72cb8e68bfe6c2d5d1befa79fe294f0000450900405102005094000014250000450900405102005094000014250000450900405102005094000014250000450900405102005054f754bf81e9ee40eb40b46fb55ad1fef6dbb3fb002ebb3cbb8fe0c73f7ed679ec811feef9b7e8ffdff1c71d1fdd27909ee7ffd24b83d1bebb3bbb8f808c270080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a028010028eaff0080a891cbdf3cd1d00000000049454e44ae426082686a223150755161374b36324d694b43747373534c4b79316b683536575755374d74555235035345540474797065036f72640a636f6c6c656374696f6e0c734d6f6e2053696e6e657273036170700a74616c656f6673687561076d6f6e5479706508637265617475726505617564696f44623a2f2f333139616138346165323332663838353735353631616366323234303266343561663261306537616465313232623536353166623936633033356161643531300573746174734c4c7b22737472656e677468223a312c22766974616c697479223a332c226167696c697479223a322c22696e74656c6c6967656e6365223a342c226c75636b223a342c22737069726974223a357d04677569642433656330663636302d643437632d313165642d396530662d3835333264323665613832380373696e057072696465066e617475726508776174636866756c0472616365056669656e64046e616d6517576174636866756c204669656e64206f66205072696465") return s }(), Satoshis: 1, @@ -100,23 +85,30 @@ func TestBidToBuyPSBTNoErrors(t *testing.T) { t.Run("validate PSBT bid to buy ordinal", func(t *testing.T) { vba := &ord.ValidateBidArgs{ - BidAmount: uint64(bidAmount), - ExpectedFQ: bt.NewFeeQuote(), - // insert ordinal utxo at index 2 - PreviousUTXOs: append(us[:2], append([]*bt.UTXO{ordUTXO}, us[2:]...)...), + BidAmount: uint64(bidAmount), + ExpectedFQ: bt.NewFeeQuote(), + OrdinalUTXO: ordUTXO, } assert.True(t, vba.Validate(pstx)) }) + fmt.Println(pstx.String()) + t.Run("no errors when accepting bid", func(t *testing.T) { - _, err := ord.AcceptBidToBuy1SatOrdinal(context.Background(), &ord.ValidateBidArgs{ - BidAmount: uint64(bidAmount), - ExpectedFQ: bt.NewFeeQuote(), - PreviousUTXOs: append(us[:2], append([]*bt.UTXO{ordUTXO}, us[2:]...)...), + ordWif, _ := wif.DecodeWIF("KwQq67d4Jds3wxs3kQHB8PPwaoaBQfNKkzAacZeMesb7zXojVYpj") // 1HebepswCi6huw1KJ7LvkrgemAV63TyVUs + ordPrefixAddr, _ := bscript.NewAddressFromPublicKeyString(hex.EncodeToString(ordWif.SerialisePubKey()), true) + ordPrefixScript, _ := bscript.NewP2PKHFromAddress(ordPrefixAddr.AddressString) + ordUnlockerGetter := unlocker.Getter{PrivateKey: ordWif.PrivKey} + ordUnlocker, _ := ordUnlockerGetter.Unlocker(context.Background(), ordPrefixScript) + + tx, err := ord.AcceptBidToBuy1SatOrdinal(context.Background(), &ord.ValidateBidArgs{ + BidAmount: uint64(bidAmount), + ExpectedFQ: bt.NewFeeQuote(), + OrdinalUTXO: ordUTXO, }, &ord.AcceptBidArgs{ PSTx: pstx, - SellerReceiveOrdinalScript: func() *bscript.Script { + SellerReceiveScript: func() *bscript.Script { s, _ := bscript.NewP2PKHFromAddress("1C3V9TTJefP8Hft96sVf54mQyDJh8Ze4w4") // L1JWiLZtCkkqin41XtQ2Jxo1XGxj1R4ydT2zmxPiaeQfuyUK631D return s }(), @@ -124,5 +116,6 @@ func TestBidToBuyPSBTNoErrors(t *testing.T) { }) assert.NoError(t, err) + fmt.Println(tx.String()) }) } diff --git a/ord/list2dummies.go b/ord/list2dummies.go deleted file mode 100644 index 7cd55b37..00000000 --- a/ord/list2dummies.go +++ /dev/null @@ -1,88 +0,0 @@ -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 -} diff --git a/ord/list_test.go b/ord/list_test.go index 2614a01d..4b0e3245 100644 --- a/ord/list_test.go +++ b/ord/list_test.go @@ -100,6 +100,7 @@ func TestOfferToSellPSBTNoErrors(t *testing.T) { assert.NoError(t, err) }) + // TODO: are 2 dummies useful or to be removed? t.Run("no errors when accepting listing using 2 dummies", func(t *testing.T) { us = append([]*bt.UTXO{ { @@ -137,4 +138,5 @@ func TestOfferToSellPSBTNoErrors(t *testing.T) { }) assert.NoError(t, err) }) + // } diff --git a/unlocker.go b/unlocker.go index 8db59848..c69e010b 100644 --- a/unlocker.go +++ b/unlocker.go @@ -13,6 +13,8 @@ type UnlockerParams struct { InputIdx uint32 // SigHashFlags the be applied [DEFAULT ALL|FORKID] SigHashFlags sighash.Flag + // TODO: add previous tx script and sats here instead of in + // input (and potentially remove from input) - see issue #143 } // Unlocker interface to allow custom implementations of different unlocking mechanisms. From 07f43a37aa09ef020e6b5a2d50bdb90b92b10503 Mon Sep 17 00:00:00 2001 From: Jad Wahab <15110087+jadwahab@users.noreply.github.com> Date: Sun, 9 Apr 2023 14:52:16 +0300 Subject: [PATCH 5/5] feat: add IsInscribed() method and cleanup --- bscript/script.go | 65 ++++++++---------------------------------- bscript/script_test.go | 27 ++++++++++++++++++ 2 files changed, 39 insertions(+), 53 deletions(-) diff --git a/bscript/script.go b/bscript/script.go index 480285da..216106e8 100644 --- a/bscript/script.go +++ b/bscript/script.go @@ -325,55 +325,8 @@ func (s *Script) IsData() bool { // IsInscribed returns true if this script includes an // inscription with any prepended script (not just p2pkh). func (s *Script) IsInscribed() bool { - /* TODO: write full code - - code generated by ChatGPT to use as a start: - - // bytesContainsTemplate searches for a template sequence of bytes in the byte array. - // The template sequence is a slice of byte slices, where each byte slice represents a sequence of bytes to search for. - // An empty byte slice represents any sequence of bytes. - // The function returns true if the template sequence is found in the byte array, and false otherwise. - func bytesContainsTemplate(byteArray []byte, templateSequence [][]byte) bool { - if len(templateSequence) == 0 { - return true - } - - currentIndex := 0 - for _, searchSequence := range templateSequence { - index := bytesIndex(byteArray[currentIndex:], searchSequence) - if index == -1 { - return false - } - currentIndex += index + len(searchSequence) - } - return true - } - - // bytesIndex returns the index of the first occurrence of the search sequence in the byte array, - // or -1 if the search sequence is not found - func bytesIndex(byteArray []byte, searchSequence []byte) int { - if len(searchSequence) == 0 { - return 0 - } - - for i := 0; i < len(byteArray); i++ { - if byteArray[i] == searchSequence[0] && i+len(searchSequence) <= len(byteArray) { - match := true - for j := 1; j < len(searchSequence); j++ { - if searchSequence[j] != 0 && byteArray[i+j] != searchSequence[j] { - match = false - break - } - } - if match { - return i - } - } - } - return -1 - } - */ - return false + isncPattern, _ := hex.DecodeString("0063036f7264") + return bytes.Contains(*s, isncPattern) } // IsP2PKHInscription checks if it's a standard @@ -390,9 +343,10 @@ 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 && + if len(parts) < 13 { + return false + } + valid := parts[0][0] == OpDUP && parts[1][0] == OpHASH160 && parts[3][0] == OpEQUALVERIFY && parts[4][0] == OpCHECKSIG && @@ -402,11 +356,16 @@ func isP2PKHInscriptionHelper(parts [][]byte) bool { parts[8][0] == OpTRUE && parts[10][0] == OpFALSE && parts[12][0] == OpENDIF + + if len(parts) > 13 { + return parts[13][0] == OpRETURN && valid + } + return valid } // ParseInscription parses the script to // return the inscription found. Will return -// an error if the scription doesn't contain +// an error if the script doesn't contain // any inscriptions. func (s *Script) ParseInscription() (*InscriptionArgs, error) { p, err := DecodeParts(*s) diff --git a/bscript/script_test.go b/bscript/script_test.go index 93144a4c..4de6dfa4 100644 --- a/bscript/script_test.go +++ b/bscript/script_test.go @@ -582,3 +582,30 @@ func TestParseInscription(t *testing.T) { t.Errorf("expected %v, but got %v", ed, pi.Data) } } + +func TestIsInscription(t *testing.T) { + tests := map[string]struct { + script string + }{ + "P2PKH inscription without suffix": { + // 2093057c94904d6e835b4c0050a510d58ee2a0c028cc74fd6380d3cc367df964_0 + script: "76a914a1e0ac34fc681008717c63dacb11e2f3d61afcaf88ac0063036f7264510a696d6167652f77656270004d9461524946468c61000057454250565038580a00000030000000ff0100ff010049434350c8010000000001c800000000043000006d6e74725247422058595a2000000000000000000000000061637370000000000000000000000000000000000000000000000000000000010000f6d6000100000000d32d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000964657363000000f0000000247258595a00000114000000146758595a00000128000000146258595a0000013c00000014777470740000015000000014725452430000016400000028675452430000016400000028625452430000016400000028637072740000018c0000003c6d6c756300000000000000010000000c656e5553000000080000001c007300520047004258595a200000000000006fa2000038f50000039058595a2000000000000062990000b785000018da58595a2000000000000024a000000f840000b6cf58595a20000000000000f6d6000100000000d32d706172610000000000040000000266660000f2a700000d59000013d000000a5b00000000000000006d6c756300000000000000010000000c656e5553000000200000001c0047006f006f0067006c006500200049006e0063002e00200032003000310036414c50484a3200000d24056ddb4875f9b3deee8110111320ab9c3bf12531ab0770c3f6fffda734daf9fafdfe283083748699018632d2c5de25b1c4c4f4deade931d1f48d6e5c6bace9c56e4c4c75351b7b59cddaa2291863070b51d145c04291aaccfff77af0fd7c7ff3c7cc7adf3e8b8809a0466b9b2149fa224badb56d1bbf6cdbb68db16ddbb66ddbb6ddeea92ea42332d6de373a56151113e00ddbb6b3d1f6ff3bce344dda666adc786cdbb66d3fb775d9d7dcb6cd0bb76ddbb66dabf6344d9a36ef6e14b36cf32e22268032fecff83fe3ff8cff33fecff8ffeffba1ff1532d3afe364a664ae5f254167a25f97098a58b708fd2ac88ebf5dc2071e8d8a7edd252a1c74fa9e230f3ff2a01bafbdfeb31ffbcc4a8a0abd65a247dfce74e36d342afdfa4a548ebcff7def7a7bc3430edbb6ebf413dff4f2d77fc1b4446f81d47c8c2bfd7a4a6acf777cd79dedf7837ef0db5efecccf42916e1d3263a169a3d2af9fa47b9f75475bfcc8df7ce6f31425ddaa54746659a551e9d74b52773ed1d6ffd1e2d76fa654ba35a934c6495435eaeba5a9b8457fe6ce3fdf14956ec934e69785aaf4eb25c42dfc63796929956e4d905196aa547dbd340d9dbbe46dfffaf12bafde3ce6a85ddff903735ef4dfae56a874bfa288d92cd04243bf0e9242069ffdf37fb8d5ee134e3cfa888d2f5cf5d957bff9fb1f3fe37b5fad8a4af707818e2c27b4a9af9746036ffbd34b769eb0bb6673cddf5ff0a7330ffe3baa95dada34196559a534fd7a095117fff7eed8b551993445def8e6678eeefede7d14b59581985d5255ffdf9ffd0add8aec5fb72a5bd1b9ec4fb728fbd7513af8a7e77cf2e81d0d142af0ea7b1d3570ec552d4ad399cca0319b8552e85741f6affb97fdead6652b3ac77e89fc2b084ad62982ae1394ac5382ae91b98e325364ae64ae04dd9f50b24e092a259a2b2e38f2a04653a96945faaf771fedfc5c6a45cd862293c6ba8b4a51d15b2614992b82ce05256b94a06b65ae83ac51325764a6043db045b9f579773cfef0630ebef6ea2f7de6c31fb9d6b044475136ee78feee63771d73c3155fb8ecdffef96ad3121d44c900473ce8bcdd271c75e92597beef4dfb52e824cbbb9fbf7bd709cb2b3ef78937bf671f94e81aa136ee7afece9dc71d75cd659fbdfcedefd984226a9a8646d3aca42a887ce076a36e54536826b5b8d3034e3a71d7573e79c925975efcd1742e51ea168fca097739eed8630ebfe18b577fea3f2e91420515ca8e6f3a7bc78ee396975d7ec5456ffebc69898e4291098e78d079bb4e3cead24b2e7ddf9bf635e820cbbb9fbf7bf7eee5159ffbc49bdfb32fa5440f5c51877cf74f9c6f78d811b739f19c332f7ffd9bde7903948c2ae73fec7b0c0fdd71cccebbdfebc23f79e53e2819fcd603d13cf2423ce8a7ee6bba7bcfee3bdce1792ffa428d6ff7f01f32dc71dca90f3cfda92fb8114a7410e5ee3ff33d8687efdc75fc379efe8ae7fd07298594d46ca94603d96efc99139552e3dd8ffa11d35b9dbce7a4dda75efbbc575d3f4a6ab6b74068eefe3ddf7e1bd3438f3ee2e43b1df2c6bf7ac78a461195437ffa874e31dc79fc8eb31ef8f1fff917379856a8e8e31e68fac80bf1a09fbaafe9ee3dbbef7087e7fdd9e7ccdeeee13f64b8e3b8531f78fa539f7f9369a507aad441bff4ebf6f7b6dff62df7f9ebe77fc0b4a39cf3d4c3ede7bd1e75e66fbdc1b893bbdddaf41397b9c3b3ac7ddc2f7ef74fbcb7931d8fbf97b50f7bc237fff0470b2525ca9edfde6bfdc37ee0e73efcf84bd1d0d46c29aa4123dc6174e16d379ad6fcc18f7a94f5effcd3df71c148d0c1d647e58e4f3adafac73de8217b9ff737971a562c7ee4c9f6f35e3ffccd4ffcfb151451ee76ebc1272e73876759fbb85ffcee9ffc8f4aed78fcbdac7dd813bef9873f8246d30353eaec17d8da9ff9d9a7bda2d470f9e4efb485673df35f9fd952d3d430873ef5def6f7d8d73ceb95ba7cd4afdbef53fee6672e549434c5839f620b1ffae4dff8fb5449a151aa6aed2cef34fae01950456aef2b6ce1ddd669d0ad4bd9f627e7d9c2dd8ffcf1f73de43a347ad4dfdbc2ddcf3ae647af2e6a981ae6d0a7dedbfe1efb9a67bd52978ffa75fb7dcadffccc85aad2f44094e6f6cfb5e587de440d0f79912d7ed2ae476eaaf1ccb9cfb3952f7dc2c547bdc0561ef6bc87ad282525bf72075bfbb7ef7caea81aa8891a96083f68fc8e33433329777ca15b329aba255376ff95adfef67fba8ee28497dadafb3eeea19797b4448dcf7d9ead7ce9133e7ef40b6ce561cf7fe8aaa9a6e98127cdde17d9fa0535ed112fb3e50f39f60fd1c1ecefd8da67fff5736ded59b77d5d6b5ae47fdcc656ffe165bfdb426956a96605958ae9377eebe88ddbce925534e85d9feb1652b764caedfec2d62f5474cf5fdbeadbfdf1432fa5c61dfd8ead7df6df3edbd69e75dbd7a6aa697aa04993679bfff0bf5cfeb9c5b1c79e778fb92c0a95e798ffc70b3f7ef155bb4fdd7b973b8f3ce5f5ff069576b4ee75dbd739ff89d6bd6efb1a1ef7745a1a7ccbddcdffdb5b2ebff6f0e3ef77b7194f7eebdbd36a4a55350a152977fe75c3ebfff476d140e5f01759f3ba8f5fb1d8b9f398f56ee9548efc2b6b7ef682cfdf74ecce3b6f9f5bd23ae845e6affb878bbe98a3cffadeed23a73ceee7f7d154939975afdbbecef94fb0ee75dbd7f0b867ac284dd3034b1adf6df6b92f5dec387e473ffff90f7cf8aef77fc4608956fad366dff5abc71e77fccea32efdc4c5ef39f9a9db075ef00bab42b35aefc2bf7ddba51bd76fbfeb8f7ce768cdd7bcfc839f3ff4869df7fbc59346e79cf4a64271ec33cd3eff4f771cbff3b02f5dfeb64f3dea11232ff9b52b295a54d3220d9a6f78e411c64f3cef28f3f187e6ffecd51fdb797c2fbfec9abddffcddb79dc9a05b96b278b1f9df7be5f53b8e3be40b97bd7fef037eec9839f56b66fff3b7fee3f89dc7f48b1f79cd9d9fbc67e01b6fffec52d3fdb8f06fdff6d98debb7dde547bf73b4e66b5ff6c1cf1f72e3cefbfde249a3734e7a532975804df1889947ecd8b9ab86d7befb2d2ffb9e9f3a9545143df7e746d7fdf74b8fdfa5a6fd5f7ff45bdf34d87bea3fa634bace458fb961f709bb0f71ddbfffcff37e6abd3ffbfde377eedab1ece56f7df9b34e19f8953fa695f2c7c6d77deff1bb8e13f4737ffcbebfde3e38f6371e5bac5055353d7471ab5dbb779ef0ade67febbcf38cd3f8ee6d3317fcc6aedd276e14fbfef3cdafdef7d0070fa46ec1943cdaecfff98de377ed8eb2ef7dffe765e73ffa246c58c9b9df3bf3677fbd73d712e9beb73ff5113f3af0db7ff791a2d98f8b1e73e3eeddbb0f71c3bfbff4bc9f5aefc5bf77fcceddc76dac2e7bdbcb9f75cac0affc4955354d0f24a447de73f42b3b4f68e8845eff8a17dfedb1db1769a8c7183fe8f85d81063ef7a4c71e3ff10bcf6bda58ebb7ff6dcf9e8aa69e7af44fadf1895fdd7dc2d114b9e2d14f3965708f77a3c5dedb8fbef0d05d2756aac17b9ff2e7db267ee4824baa59a1948adb7cc27e5efcf8db9da704c26f19bfe58527ee5151d425fff283337a0b20db7f75e6e56fd8754291c2be7ff8d37b3f858dd2471b3fffd2ddb75634fae5279ff1e3038f7a0e6dd4ba4ffdd73d7b42e1a947ffd41a9ff895dd271c5d882b1ef5dba70ceef16ea54d5307d034cdc9c66f3bb1a1d6dcfcbb17fcd0038e579c7afbd17fdb750294066efabdc70cee7bf16595768db73ee9b4bd1b54d3d41f3f7cdbccefbd79cfc9a5417dfeaf7e69e0e86b56d2f2b3c6bf7ac2498d9afde80b9e35f0932f6955d1a6a61bd6bffed9ff72f6b94849c337195ff487271d0c45859a4fa5d2ad898719bfe5b527deaaa8d9cdbf7de1efdc65118e3f7ff4bed79fb828355e3de507ce1c3cf8cd9f87c66ae6ad4fdabb7743534afee8e1db667eefcd7bf69052f2f9bffca581a3af691b55e9012325768daebbf958857612f8fc8b4edc85e6e70ddff4c513a32815bc61c71d26eef25e4556a3677df28c13ab50e4a6b73d70f0c507df76efadaab282f4ede78f4eba94e2f0878c5e7fe8c99452c29b17f71b3ce21fae53da9a6d649dcb1ef3bebd676e508d69fceaccef9c7c70270af5d599c6ad7f7574dd6fef89894e2af2a9dfbceec4e378b0f163f72c4b532a6c3efe29030ffb9b364547cffae499bb5134f4cb6f7be0e08b0fbeedde5ba9d2286f3f7f74d2a568a9036c72d068bb9b9a55cd56488566e311a3dfdc93d21a36f4af1f3cb8eb7b9bd2998f9e9315b4a4b8e8ccd10da7a4a52872d3a74e1f9c7099c27d8d9f7b0ad59acff37f7ab0fdceefd452a80aebed7cc68dfff8ba9b5a68a4d975fae8a2cf9d16d55248e7d25b40b88ff11fecd955b42841b379e11e7cdfe81f4f384d4bcd7ff0eafb0cbef7d51456a38f9d5d342bb3179d39bafed4b44a4d73d3a74e1f9c70196ddae881249a4f9f38f05daf53daa63484a8703bc377ed3986aa52485c72d2e06eef518d8e9650a51a7cfaa4c1e1d7a5d25a29211fdf3b47b9ebe8d3d7ee2da53a88f71fbf63e22eef6d5ab486b1bfdff4803b3cebada552897b18ffe1de45ab55681a19355b97e2de33af3c6d95aaaa710872c491a357ef6da9ea24e4d5df3938ebf22f5135bb48aaaa94e4d3270e0ebf1e93151a5cbc77aeac5207d014e9257b464f7bd907aaaad68c10779f39956a4b8d17fff784c1715f6c9ace6551d52a84eb0e1b1c76836a150ad71e3ed87db97651f71ebd622f6dcb2a93a8d77cf7e01eef56aa6ec11d8ff38cffdb5488fb8e3e72f919adb2329f995b34e47ea3d79c76945aa95a3344ee60fcf6334a5b356de26df71bb8e3076ada1954954ab9feb0c1613768d33655c8970e1fecbe0c4da9f400310d572fb70ffcdd457ff1e9096d2681887b8dfefdb66545150de9970eda3639fa6a4dd7414ba9e9cd070f2c689516e2a64307b7da47ea903346179dd1b6b459212c5c78cee02e177eb90a55995cf5cdb7cef6638e3d6ec7771d3de0be3ff10b5f6a1ae101a3b79e116d8baac072aed168f62bc5a1b71ffddbe9ad5aa14d278946dc79f4d1930fd1d6a488b8fe8b270feef21fa9c66a94548b16e2cb870c2c94555a35b8e9d0c1adf6d194461d30a3d13f7fc4c80f3dfca637bffba3d7d4ba1172c7d1cb6ebb5a35b2481269bbeaca70db57364de7da6829456e386c94467585a6e4a66d83a48b36bb8d3f719ab252cd4a885c7cfac0aecbd3b49a1ac687f62d6ebceaca2f7ee18d67ffd08306763cf2f11ae4d0e3469f3d415b6d56c6c962ee164cb3c7f8c2339b55696bcd4464cfe8c2b3aa555515111f3a6f70f267d0ea08a5a5d170e3f65168b3d2a6889bb60d12144a7aa0208d3ff8a9ed234eff96bb9d7dd57f7ce0a28bbe340811d935b2cd2d78d80d4a67b0426b18376e1f514a4b236edc3e128ddbcc7cf684b655351b9f397974f8754d8b8a42482336fff97f5dfad281071dfe5284a38caf38beb45a1d848d5165cbd2e4f0994f9c9695b69422a6b170e4e83ff7aa76321f9f3a6d70f875355da3543585dc70d868da2a35cd8ddb072294427a204849c5552ffc8db9e92977b9fdd9675ff2cad75c0221b995afc66527e64ad5a0e1e643e6ac06a6a99b0f19481a73d71fbaa8496954d26cdebc6df4a568aa51e310487df8057f30f0db2fbc441347ce5cbed34aadacbb58cc44b32529e991a3eb0f3a78a52d45433392a347d7dea6a5aa33e1da2306475d0d5da34a5511dd77c8a89a966a90de7cc82052d2461d3053d278c195bfb9d6f076dff39d57fce5cb6a9a23be2a92669d52d5b4914a3746d554e9846431439ac347571e45b58aa0c20d870d8eb8b68d52d34063dae8dbdefb2b03dff15ad11c31f3b91db5523a93663928d26c414a98b9f636d5d2428a8a881c33ba615bb5134421376e5fa7e94c55a9692add9869da54a111cb8168d481b9cf79cf9f6cdb1fdceda1b7f9e52f100efbea809aad169a3684e568252d8d4e624e480f1ddd78685bd5a04823871b7ef96069d3e864cdd4f06fbe67dbe05bdf683a737db7b72955a2e9c262704b46e3b0d14d8768ab2aad403a70ece8cbb7d636352d420e35bc615b34b5062d0d25b298d1a69aa6a25964a669a007908606defe8c6ffba11dfbc5831ef72b1704abc5e8af563a5db51d24592489de7cfb43a133dab464b550d3519b525985345d230dbe7cabc1a137a8a66abe8b5e7df460fbf529690d3b4969a8b4fff82d83733f7903b11a7df9e06a952e9434cd6873912d23cd976f3538f6f3d5ac48bba0484de3fac306077d05ad69432abdeef0c16daeb59f2b8d4a216539a842d120cd62249454f4c0d0d0e8c4e75ef18a93bee7bedbf6c7b17ff0f3d792ab0f1fbde806dad5ca4adb882459842ccfd9884a472d8d4aa52119684d9b46d358988d26d71c3653daa019698e31fcd2e1c84a2a35aca6682a3e76e6c0d157a1d76e1f1c75a51aa4424332586e56b62235cd95470eb66d5c534d352a50d1e08bc70eb65d5f4d69cc3687195e7da4281d152bd1542a2c0656a18d71259911a8d481b45294d57fbcea1d479f7dee1dce5ecbb13ff94c5cbb7d74c435100d52d3aad926355b8d9a0cd75052f391511a5c7df8e0a8ab5ad4ba690e1b5d779861a3a68552e36b8e5aa7b9eed081e53ed47c1a461a5b1e7ad5c8f1976b8dd3c96c5c79f4e0b06b555348a570f8e8aaa30cd7a8a8359351d56c03eb1c881b4aa5483ffde10fbee763a79d7bdffbcf79e45b3f92aeaedf3ed875d984680c4b750699294a14d1986b682a252a33d2e8b5870d1c7fb9369aa641d39c687ced6dd0a861a5aa6a1a366f35bacdf5c5b5db463b2fab4a918ae98c66ab5271f5e13397a1a4d28c2a0dd71c3138e5934543d36894bda3ab8fa4a9f952b3d15843a3880a319b924a0f249a893610b1f8cac51f7ccb3fdff3d7f78edce3ddd54f9f38b8fb059386d08932692a9091a628316de64c1bd30673a4f9dcb1a3bd1f47439ad2e0cc992f1e2d543aa058a53ac1e1c6371d9a72cd61a3932e4969340dd2f82abdeac8d1ee4b6bda5833855cb67370ce87950a522adc71f4f963d3349da3681ae37534d0186646a8036a43514da51191c88dfffcfbdff673a3332eaefcf3dd07f779e7248a40294a43d6d29434158dce50a4a9d0d8df6bae3c6570d64513a96194f346ff79dc6129526b5456a8a6c9ee992b8f21fdfcadb70ff65e5cc33414728ba5c8670f3a6670fac79494744e9aca7bee3c38eb3fbfa25252843ae4d4d107ce469ace94541a9aa6315bd1492a5d6f1a3d800cab280d92a4896cfee9f9b71f9cf531edbfdc63b077f95154d0144a69481364d058b3d2986dac59d1ac95fab7bb0fbefd752545261afdbed1bf9f9da652c3926a5335ccfd66ae3eba69bdfb2e83fbbe351d48c5b4d9a28c9a12cd9bbf69f003efbc2cd43468a834bcf78e03f77e27a15151e201c61f9c488d1beb37661b34fe5f59d355aad144931059bdf1418343be8277df69e0e79f43839a368d42a30b24c62525259aca081da55169d621efb9d3e0b48d8fdacfe63ce3f79e6d366ada942a344e387e74c51107a7e2fdb71bdcf5e2ab8c1bcd646b4347a928fee59e033ffd429581a64a34f8cc21c70cbeebd53468ccfef0e88aec506b779d343423453a13cd81ac416fb5bcb1aa52160889ab8e181c719df673579c33b897775195a6694a952024e6d0d09836662bc64d43633fffe59e030f7bc9281da53f3f73c15c4a9ad25451d3c718ffef736afafef3067ee4a533a95bf23e47c5f40bffdca8e9dc8fbceb33d150d24039f406bcfedb060fbcf893526b9f7efae82de736b6bc29b1b51507f446f36b8f7accdfad50e348c85ec3cb7717cffdd981a7bff0621ad550b3dff4e45fbe2042d6daf26684c67ef7d20f3d68f03d9fbd80344d27e9f9278e5e77ce71a3bfd9fec4f7a6694a53d3e2db0f9d79fb3999f49d77dc3678d45f5f35229abafd4fee572a2fdb30bcf2a7d0547ac9c50f18f885671b861a1ffae49f3ced4a5ef2b081273ca5158d12fc81f1ff3c5743b325fbd9a099f97f60d38d43ffe098477f52ad4372afd147ce48f5cd7b4f1e1cfaacdfbf984221c5eedf38da95d708f1d5708bbfe461034fffa32b50c374e7538cfff61c34dc76cf532e7cf2a79b1ad7f09e8f34bee2b3e742ddf4c6ef19f883dfbc79a6d2a31e7717f3e940d46ca44da9fcc9a346dfbbed6f06254afa0dbfc341fbeaa26bee31b8c7b97f46a551d25f357ed7f1a7f99a3435bcff1fbdfdd91f564d91cae2b1c6af3cbb6adfb31f3db0e3797ff3f72b355bce7de4bdb02a09f25faad10bb69f3338f119bf7645934ed25d7f69fc7ee7a8183ee4091f7ef11b6f2e0a65e3971f68f677ce5b4ef08aef1ddde3479f7ce5a461f9e0c75a33359fce58546ae2dff7dd63e049573d7b9f6183539fba03419fffb303ffedca3fb3667fe5a899979c4b91e66b8ad4fc437eeef2bf7cc7e753a272da4f1b5f70fc1ed3376c7efb8027dfedc5afbd8e4e72f603bee360d3550ce3bf76f1d8670c9cf2e217bd6cb351960f7f94d9c79fa3b1e6b7fde4bdfffe9dfffc192addd8f3fd3f61febd5f3c2f3ae97baffdc6816ffe1f2f7ed36789b3bff347ac1f4d27b1c6b2aa50bffd3f467ee5bb9ef9b6af50e961dff5f0934c17d5fc9f1bbe63e0b74e7fc2a72bc5a9bf67f695c79d5ba1e26bcd349d70bf07dfe7d3fffbe35ff8fcea88634ff8ae5b9bfdebf3a0ed137ef4ec91537ef6bb3ff92f975ebb79ec8edddf68becd24feeb562a4d2f7ad56307b63df1feaf7fc715d71cb1f3fedf6bfec967efa56486c37eecfc7b2dde77f9176e3ef5766759f7fa679c9792923ee5f123db1efbed375f71d3eebdf6374526eb26295ae5c37ffe7b23a73ef11eefb8f00bab134e39edce66d3a8c7ffd2d103f77fdaa7def0912f3ae69cefd96bf6f2179f839ae66b0da935eff8c0bd471f63fde79c3b8a1b7fe3c9478d70877b9f78f886036ea3f09293ef3b60e7f7de7fd791d67ef395e7b4a13237dd7d87e38f3dc87efedc7927160dcda75ef92b239cb67b9b5b3074c6465595eaabae7de888ed0fbadd8e83acbd507acde3ff68c403beeb9c6317d6fee5b3b7a9d97c6d91c62dfa92afdc1e4a73c5cffde63de7b63293a6ffa55494d5af7cdb8346fbffaa779c8d56f6670bafff8ddb9d8752d13c7fc7f7ceadffe993065129d2ac9185668536e5e947fefccc164eca05cf7ec9cc7e5fff53679dad94105f63a63e77dcd6fdc5d5b74f4d9bbaf27177fcd9adbbee2b47d5b4a3a6b245cd56155991faf2e3eefa235bf3e71f3d6b4335b0b8fed05be0838f3fe77654d328369f70df6fde8ac77ee1850352c358c34655552bdd7cf6c57fba551f38e1f056eb82df79c9b62df9e223cf385bab69acd1345bd56c51d31c9058bce08f7ffa11dbb7e4a34f3af7bc4551346c3eef2d0fffeead79e92bde7fec3154cd37345bd1946c4543a1583df50bbf79c2fe7de269a79d91b454c877ec78d8376cd1f52ff8f7b34fd02a1ad2ec7bccc98fdaaffffd8c3ddf623e454ae6b2a0ac50156f79fa237e7c2b5ef2779b3b6eada52efaf587fec816fcc54b4f3f63a5549a35349ab59a5153b2159ae64093c6f4132f7ee91dbffdfbb7efcfeb5e71d5d9e72a8aa6c27fbee4821ffa86d3f7e7d5af7cdbce9d3b0f6295ea75db07371ea25299e9be8306d76d6fa8cc34371c3ab87e5b4d6b9ab7fcc1bd1e7ac27a9f78f1bfdff60c6db382a45f7ec35f7df8010fbafff6fdfae48bde72c65907699546056e7ef19b1e7bb7b5def187479f7cda8d070dbe741b15845c75f8e886dba85695c2e52f79d30f3ff0b4f5def7e7afdfb973573b29fde2f3dff373dfb21faffdc3d34e3fa256d430d76f1fdc74b0a664eee68306d76d87669debb70daedfd6540e28694a6ae1cbfffcc6571c79eeed4e3dfcc8db1c7ac3355fbae2c3175eb4f7f4b3962d4aa3a990cb5ef68ecbcf3bf7d8238e3afa90abafbcfab31fbaf0e2134e3cf160d56a78e4ab96cb8d45ce3c9d58fbda530f3a6863b938fe8c54a80cf4e58fded8d8582ece3c034d534d63dfcbff6ae39ef738fef8a3f9c26597bdf39f16a7de76a1ad228845af7aeb9bde79e499679e73d2113b71dd95577ce83def3d64ef19076babd05021bdf8999fb9ff7d4e3899eb3f79c95bdfb2e7e453f4ff3e70b9487adeb94d05e22e9f5c6e2c170ba79c5caa4aa9f0c597bde9ca3b9c73f491471c7ec395d77ce2fd1f38e18413b66b55abc2c79ffff6bb9c7fc6d1c76de3135ff8f0db2e38e5b4d356dab44d43fa8bafdad8582e72e6e9d2a0a1d1eb4edb38686363b1e38c494a467df9a30e5a1eb4cc99a7a751e480311b4d70e9873ff4896baebce686430fbfcd8e934fdd7b585b5a486934b1e04beffbc015577df1f3371c7ee411bb4edab3e7a0520dad614d53514145cda762dca86145da1425e253fffc2f977df68bb7bef1c89d3b4f39f5d456ab85469246b2efff7ef4a31ffef455972d6e7dd3a147ed38e5d4bdbbb4d5b629a92869b8e2adeffccc276f75f3417b4e3aedb4a3a82aa544631a214d4b439b56a549932bdffba1cf5f79e555871c79f89e3d7b8e896abaa2cd4a2cfae577bfe3a22f5cf195d54d271f73f229a71d6c95b6d5340d69544c53692a2a9a9a4d63b651a9d93810c734085151ad6a291a6888b0482a024d53aaa8d24cd2401451299da402a9502829a4344d4512d29b0e5e52a513d5441a098288cd2fdf7a9154abda6a6a9a262b48487c79e3a05033a5899a460a315bb4a64d2d9a4424c6a5adac54531149f88a83d1a6da6ad354048d929234e6db286948a393284da5691aa439d0d0c430668b5615b2128da64948421aa8a6a84ad3a6298d4c1a684a0d03a151c3a6a4690a358d48c4fcaa54494920d2c4344d4a69ab4a85a08b159a88849856a9ca8a08699a49a5625aa9160d3110e984a2d5b4682212499156ab9a55bab07e23a6d1a846a51a8498d6b46949108d0365334791d442a3d12a55d38646d334129234c62d9a46159a0a84a052859226a4024d4b53d218b7314d4432694b95a6096952691081a6d1962a95261a9a6a88909856a9a64823a4a4d06826d3a2948445136926ada654a59a264224a896aa469a499a52928a40a3a812d22068286da08b46680e1034289a06d10445b56a1a519a922602312c544a34aaa62152d128aad23488d050942248a9b48808312c559526108d12d30885d252d290b452e820485368691a82906a54631a52d3b648235d2050d36a94a6692a09312eadc63850d3200d4453a5a691262574944ed24543340788a6a685c67e164590926aaa21325352f30d940682a4516951e3a441144a0da3d1d4b4d288d9b48a98466a5c692028b4a969046295369512314d69a1914626a8864e0241359366151144a5134d433585106b56d394086415da40240d9aa23324c4b4685348230d7140ada2e63329542a0d4251c346a4881a3742da14952e08d228aa94082134cd8a52d20852aa68484a545329844853458394d4b486cd1aa9acd2d24c1a52aaa1a6118b0a545141c4b459291aa281d43885680d1b2114aa69a491a8b495920829525a1491464889aa526942047ac0289a4e3a89c6b41a0d441ab4514d63183468235248d30a5925124da38dd6b4916890a654a1912645b34ab38aa61295a669d3489306a96a94469a49d34e8248235456a952c4b4d154338958542aa58a342246342d8a48a341211ac39255cc460dd388f9d2d0a421a2a655e345431ab4b0120d09a9d481b4592935db684c1b44ccb7689498ad484313d3a25820a469aa68114288a6942a41d24555d34a1b6b96d48226a2866595c6b4516950691208942a8d1aa7d2346d42a48969e9442a210d2aad6a8c2b1ae3844a9b6a14814210441ad5944a6a41d014558a9020355ea1d2041107ce46695a68a3a629d120d2c88c52b57e2a48ac598498a669b4359b26445393a629823468a545a5a3695249d0d0491b3a583774118d1817552a1dac998a9034685a0a49235dd4b44d2bd5c16c90949856a9d089a04142685453684248d3544d8611a2a1148a680239706894a245a55142851062586853d3a60864153153f31184a6508a10a241a91a865069b4509a4e3268d220866db46b54066982102952da54d11441d3200d424ccb5c235d3495d6b40a2593864088a2348a220d698458b38a20a4a129a508310d4da91a8788036dd14651eb06823441291435df209884a69a8120d054d1a049138d69a969431ad1b4b19aa49a49032182d0a2d28a4e66d334a9843468aaa92a1a680ca3698410a52a254d6ad1341ad54aa1d64d43a448514563cd088428a5310dd29816356d08024d51cd8c347a006950aca0691a54341011355b149a8a8a8698069a6a348118364d55d304d240a568d320a4a6152ba9a6a181404811450b4dd74823b5504963da5429348d2283691ab16e1542d234a8142ba90645d085c96c51340d9a06410c534a5349258d69d34a1bd320864d354593465007d24653a8a628423402a90c8a4253a8980662ed8a69e8244563d8a4d2493485c634152a2d945409024911545a69aaa9122510a9a4d2412aad68a52a5219a5d2984dd14c9a0651a22954d3d46c20d6ac695345a4911204456ad844a388461b3452421135ac18a70eac954253eb37691a880ea2d2a6d0583bb636d4b451290da9a869a3d60f1545d35463ed4014513445031534d234881a368a9236f63b1ac8a4a6958a54142a85a6a9660e310d4adad4fe27154ad4b4212a35ad9454c5346adad0c96ca8036e434d2b9d4320d4b8a2342a3a938a69469d0b35db50b3a1c6153a130a158a8acea491921a56a846a3937154106abe42d1e82825030d42110a0d44cd56144a3a4ac5349450e87e04a2a615351f35db50b351b3253a13d401b888dacf1451eb575034661b889acda8d62f99abb58b8c6abe44d1986d10b56605a5b1768350eb57288d71633e50c392c9b8d62e515b18352ca1f63fd47cc94cad5d3253fb5999a9037809ba86505b58083a17a8753bdac2ce6d6147fb5984ce05b5bf85d0f5426d69113a13d34ef6bbebec7f113a17d4da25e81aa1f6bb33fbdf992deccc81beb1bfb5a559dee1fcfb9c7ad0b25dee5b5636373693cd83361756cb7dcb5aecdb58c9e6c6e6c26ab96fd92ef72d2b9b1b9bc9e6c6e6c26ab96f598b7d1b9bc9e6c6e6c26ab96fd92ef72d2b9b1b9b0b9b1bfb9656cb7dcb956c6e6c269b1b9b0babe5be8d76b96f59d9dcd85c582df72ddbe5cdcbcae6c666b2b9b1b9b05aee5bb2d8b7b192cd8dcd85d572dfb25dee5b56f66d6c269b1b9b0babe5be6565b5b1996c6e6c2eac96fb96ed72dfb2b2b9b1b9b0b9b16f69b5dcb7ac6c6e6c269b1b9b0babe5cdcb76b96f63259b1b9b0bab8d7dcb76b96f59d9dcd84cf66d6c2eac96fb96b5d8b7b192d5c6e6c26ab96fd92ef72d2b9b1b9bc9e6befffca777bc7fb36bfdbfbffbb5b5bb1ef67d3799263684a5254b4b1636840d0b96962c6c486c084b4b96962c6c081b962c2d59d890d81096962c6db0b0212c2d595ab2b021b1212c2d59d890d81096962c2d59d810362c585ab2b021b1212c2d595ab2b0212c2d595ab2b021b1212c2d59da606143585ab2b464614362c382a5250b1b121bc2d292a5250b1bc286054b4b1636243684a525cb8dd3cebfdbadffe12f2efb5ae2abf2c867fbff950f3ef3074ff985abbe96caf73dd2ffef7cdfc73de715fddae96bdb08d0377eda6d354cf14582f4f19f754c82641d23503ffeec2716469f7f1f55ecb1031b31faa3601d7e6c3c44d9cf7345f76e2845e8f315b0ce3a692342df198075da454b117abf806d9c7d1021832c8ab908cd586459847021ab819580a81ad3b9e22644cd0858d283281f8b70c56d881a21932be1e93ecf82a8ad45b8725d886c3dc895f0206acb14571e87c8e947b8e212a2816d71e5f910293bc4954f10fb76042c099134635c3904b16a471456ae01915fc90ad636613c13d6b0f28d0022db12ac3961dcb6145636483d9d2b8f49889a09c1da268ceda04e95f49984c88908d5cc248c6d15a24aa640f2060655e481644b982a6682e4b43254050224b76751659820f55a29aa0201d260264a15f340f29b71aac805492a69aa028e52314755c844a994a58a4b9466220aaa908b52d532a11202a5b61984ca0da1d490385492a354332350993194ba6e0c2a2e61f2a6a04a64a3d477c350595194a49485ca0dc1d40d41950ac3f4da7ba1f20d98fa01a812047323a698f2702a65a03271525e80a9384ecf7f54984ee1a4d92653099cea2961dac1c9360248491ba7e08c81947070aae704695fe0641612480907a74e5a907609e8f6145236c7c9b72348b90227272c48a7c338c94b1f442a10387941413a15c5c97f7363a43c02aa63cdf5b9265209a0e4cd8d9092487543481522f5fa7b9092483986222a89542364126522d50c1940c9500094dd8b0145f904744d2580923936508e1b058a4a09e8ae13074af848797614a820404a2a69a08c7ca8ea09a0dce36da4ded818a8481121dd8c01e56649a4da96e2494a42ba94d4790a27a0ea29a0ac932da4dec8183c651513d25d3fcc53fa041fa98ea1f3647884b4ad423c856ca802b6c153ea441f294701951527a48395184fa5c772a4fa12e029e410d49d084f660c2ad5b6783a8a55df33790a3b5069c50c4ff921a806b6c553cc814aea099eca18569d084f26e9be90c0aa6d299c8a09eb6e58706612ab5a92a742c2da357872c0eae8064e1e585d65e29402cbe94768f20db0da4e9a266e82e53a519a7c03ac5e274e931380e5b51334d9122c69c6684ac6d02a65697203dd6745d1aaa668b2236835a30a2687a1558f1830a508ed6688260fae46600aa6045c5563aeaf2b2198d270397602a672b8fabe0e53009794332cc9385e33164b42e0554fb0241ddd276cbc064aa1c4195eafbf4750f6e378f9bec65218affc8682b270f06a4e6928718697783a4a7e08afd2c682b270f1325b6194bc980b572b2d28079ce0eebb6194acb0806b201a4a7e98e0eec40565cef0d20a5994dcb080ab1b1394a520b807af7e00a57448c0e5b91194b84170f7dd304a2ec3cbcf6f84926948bc3441d925c05f793f4ae54ce2d58aa2c409f07206251bb1467caecf5621927cc4aa9609927410ebf816488c2356554990c842cc1e5824d988b9ad34491e623d2f0c927410937206242620ab2541f23964d514490cb2c206207109592fa071e41890bdb191e2c80e43e6f9418e9211c8aa319d23370c59256a70644621530e484e18b2724ae3c86490852b11900872c7148e1dcc64c6e22885993613e628819913128ecb310be6d31ca531f374e1d8c14c0a1b6014d8a03563180907b3811bc28873cca49ce1c807ad1fc0c80940aba439f241b3c318b912b4461c239b81d68a621448d05c15a4281902ad6984280a2468ae17a1c826d09bbd3445166a8e1ba588a3d6ebc4292a82ad9da4c80840935694a27242bd98a388c356ce402471aba620a232d81a518d2119f561b374865831c116321912123733c490b460ab07a20cb172d8ec80c110cff15073dc2843914242bdef990c39791c35694f31143209f66a8a21c971730d86bc5c1fb66a8aa19c4282dd0d2a849227f9b0d59382702c41b037620a216ee0669b1a42768e84adb8a1209c554eb0fb5e10a1d21302d8ea1941386a12ec7d5f47a82407b75e409b65ad3eb5afd1e5e652979bcbd532abe5e662b15a6e2e586e2e5796abe54a2c371756076d2e6ab1da58c96a63155d74a18bd592c56ad1e862b520ab85a4a94517abb05845974d61860cff0eac186eada4ccb2b36d637361b558a5b25a7433ab45699ad6b0bab0695a29d55034ab54aa2a93541a65929aa6d13488948cd86f2703dc064e78b675ac0d960e9238c8920d1b2c6d482c2d585ab2b4646169c1d29285050b4b89a5b0b06061492c6061412c2002f1d57ad8c0ad618666595fab1613eeb5077e42903f1338fffa1f05005a2f8193b3933fe7c76f47c8b7cfda2488cffe1dd0c9251b7f159f7e84bdb7f5eeef83a7a3099e3cb5e701ef4167dc4a82ffd61d8ffc02376ed715a4802f6cb7e837d43855480d0bf3beb7591c9935ed98228833fdccb11b0333b93929e4eda72ff9a8064b72ee4c52ca8bf7fde3ca002af137a3a498b5751fff8d094a713346cad99a5f3af2a3980c6d4f2adabdf3d803df03c9c0a5a4a8f72e9bf72f42cc2a2e29eb8bbb7c7027138fb5954965fbfbbe70a4c586dc5e9d14f78c6377fd2119892ed34979efdd79deef14165e2d526167ded7fe9c6342aee815562229eff6e2893924a6b725551e3c78d2828f2b1cdcd18b499dfd730fffeb521aac5722a4d495233eb0994221bf12a9767bd91b27e440985b8bd45bcc68ddf2e68a02bfe93252f22d55aadf66540824badba4e825af5c57eb840a80798d0d5276afcba1c639aa17ec6d464a3f70d4078f1a4a9768b58cd43e98dffdad4794ae1a23e577bebcecad1395ede18621aa00cc6fbf65d8b18a76fdde54312856b56f78a9921d7b9d54184e6bf0d96bea559ed604c478a5736b8715ebf9f9c2a2dfb4a4ce692ae5ddbfbfe0d86340fd07d4a9bbc37d02e4de465f5d1556a4fab9034132ffd54b9a9ca24437eda20b94f109bb1a8614e8d983854bd97758e50799e2f4f6b845c80c16767fe311b5b1972b8133f1f9e555220af3ea76c2e7d1b61bfa9caa2c97ee2984ca99ed3a5da22683e3af1648d77df3cdc3610579f1d8a6607ae4e3f35b6629c773db0aaa3dcb5e3f5b2ddc7b4e145645d329751e55096fd5e342abcc6ffdd6d52165b0d78504d85d1f9ed7294f111ede5a13648f4e5dd9225b09ee3c48a895fd07b4b9093fefd84b04dc95d53e791ebdc2b4b09bfaf8862fc3d095170bbd5eada29a97e3e69dbe5ef895c307b5bf1435ff949b05e17d955f7d381bb2e78f7505e2cd0d4f6f1d05ec95ad95602c861e7cf774b406b7ae1792dd96935ade05d6ce0f08cbf270fd37ef40cad945179cf77e7e45b3084ccf6f2344a77bac6a7f3a48e71d2c508feddff17c84bad79e2758affaf88d4fc2f0bcb8ca17b0cb1a5df346049ce6cec2b6fc3af1c50dc878c79ea1e02267ccd8d6170073ea8302f8beaacf3e1d03a5b84740105fd3e1c4f698bcf27f2590fb0336363c1e90dbf611ce79b7813d6e44c35973b9a0befbb3f79ec3a2b548682ff9ea9637f38078ea20e1ddabb4a7d345b305ffe4638478b97870bd8dd4aca073fb3502fd9c9abf5f69cc025e9b1710ec0f9ff481ff6813afb18390efac28edf6c90977fa51c2be7dd969877d68a2adb95ff07f66d1567f995cd60a5de6009fd9e5037b98136add373237e81df2c89ae82492abead29ce179c7edf1f5c9533e7c1269c43ba637dd4c4d98d4a7a4179de96ffc223949e4b6de866690e682d7d77f64820ce841fad17bf4a4e90dd584f0a72f232d79f91e7f5ea94d84f81b59a4295f3d21b7dd24385c89f46577fa95351b8cbdf9b549677a971e7af0d7c69bdb721169ce6796acf8911a63560b49dab3bcc95717a5c7d69cb60669d0ee81cf1d191b536b5a9026bde28cadbfab8da178ffa5a44bfdcb0effd7e66aecf81f18a4517bbb7ffc8fb131b3b385415ab5b8e3b3676d3856c6b522ddda7bec90dd3f3c467a2f230d7bdd2edb6c322ecaab18a4650b3bbc6727632c6cfa92746d7f9f37f6498e9ed85e8f34ee09c7eff09b514bd75e4e5af7c9bd167f551b29a7bb419ab7f1efcf2ed9688466b58990f62d9ff4cc91e6c8ac6f475af8f293177c4f8d84d37109e961ffd275fffcdf28581f1aa48ddb3b7e70a13e7487bf209d5cdfebf153b24336bd1ee9e5c10d071ef4b961126d1791767e70f5aa1fea4393df2f4d1a3abfc9fb0e890dc9ea2f1969e9c169af6d9b1a06bea333e96affa08b17fd4dbd6b76a5cda4af074fafd9e4cbea5d12ed22a4b50b5b7c78f7e4bbb2fa1b833477e1bc7b8f89bc0b8b5b91fef62f5db3ef17dfa9a0e754d2e2b7ec38ef5fef4c414d469abc3dff63d3fa3b50f431e9f3de1ef91d3ef476f894cea4d54f3a76bfefbd35de742d69f637977cf9179f367e4c26168e8b91767ff3881bd50f6fbe382aac352b568449c7fbeee3d7acdeed51c6ff19ff67fcaffc565038204c2d0000b0cb009d012a000200023e592c934723a2a1a12352f978700b09636ee1748e3bf2359df71dfb37f6bfd98f1ccce3d43fb1febeff62fda0f9aeacbf51fbeffd47ff67f87e6f1b23fddfa15f8dfe8dfe87fb9fee17fb1ffffffe3ef57f73ff29fcfbfa37f62f92ffab7fea7f75f802fd19ff39fd9ffcf7fd1feddffffffffe6ff445fdcfd00ff36fecbff03fccfefffcb0ffb2ffb7fe63dd77f77ff67ff03dc17fa5ff8bffb9ed8dff5bd8b7fcdffd8f606fe55fe0bff37ae57fe8ff73fbfff46bfb5bffcbfdd7c0a7f3ffecfff83f3ffff87d007fddf500ffadea01e9ffd76ff45e9ffa81d3cbf2af6f799f3db7e34cfe7f79e0cf002f60f83aed07dd7fdd7a02fba9f65ff9de94f34dc803cabff77e109f82ffadec05fcdbfc77abfff89ffaffd379e2fa8bff77fa5f807fe77fdc3fee7adffff2f6e5fb89ffffdce3f647ffb0d0133490a60009214c001242980024853000490a60009214c001242980024853000490a60009214c001242980024853000490a5fff583d99a43129109dbad1bc94f4849d1b62835b3c98982b08b8ef39502b498e87354ad685300048f739f99b2622bad8719644131bae8db04b29966a526402e47201c70064d95b911bbe4b0612f4c807788b621658c1b22fd0c979b9437691dd21d54c2b3eda6f19ca0a620239440ea31f3081aa93e3042b359724bfff589749c7d58d59733a16e0614cc279b8a1834cf2449940506b7488aeed1a8a9580564298bfb5a822278817b3309e434ef7c83814bd06de0a847da052d8b502c095045a7159a75f27837c9974ac2a480096564067b265fdf74a4c6aa41c0277d186332678f09eceb5ca24aa53c771058af98e89201620faf4ecf47a1c241f0c3bbdd3271638ceb60a514dd9a0a1a073281953ab831a4519e559b9e67e16f363b36e06502b57f361316ad08c6b43570ea449041ba87495466aaec94d009d0779d4653dec62b791e22a5ce8775240e8fd13205615b28605901d8cf7f1d63e7fa8ec2e625b56ccb3c63f217e3876318356c86c5ea0b3c600d25533a845adab56b39d59e699ebd74c20a4824d337bdf4e7172d622fb3852fea41512874def36bcc02b74fc9bbdedfd1d949fae0d03e5d0ab8d075dd567470fddee31532c7eac40506425b60892cda4b68b3232418bfdacbdccb3eec6f65e3ac429a4c8146b87b047bffd4c442ae64a1f4b88850e39f902b4d950a34085e2310dc43b2227fc8085f562d5f4cc327beb751fd1a0ce969c7bb1d8cbbcb926310dc31d1ba6342cd6532bd3731c2494b4b8f3721fe5a1c1ea60009214c0020a14c002122f5565ad0a4972c8fabac48c2d63c2776db5b3c1066e6830e7820cdcd061cf0419b9a0c39e083372ff089e08337341873c1066e461c5cdc031f4b780124297c98b1325a7679957f9a2e70445c3649c8fa3a234f4428cf84ca04c2503c44960bacb5a14c00229e11159f1ca850fb9d933169cbeaa63d752ab2d627068bcb5cb086981d3a25a38df82bacb780124293ef31baf413d77b5a714bae75852be2b2d605045d56b9ae69341f2493c9ad35465e52d95596b4254e032d9ee96cdb636eb4193180ed850565cd3fa35a06413967f1cd745f083f84e3b0aeb2de00490a4ddbb5b6dad52f746209a8cbaafd0887d367b67856167470b5fd6bb96aba4d39f931ff41b6c68765ad0a5feb59a63e5a827f6dcc70ad8cffc1d52f776a23ffd5b9dc846aed70e7026b963705760e4352de00490a4d360b5659fb7eb61bd538f0ef3f8d9d44fa1514a4b348fa34ee8b780124297fa85268a5b25bebfcefe2f68308272dc2caac9c9637acb780124297ae89d7f3d65acfd662d382484a4a08efc009214bffe386979bcd2425c94e74b77fe1261d87fb8765ad0a4d0c9482852f73b563e6e794e75823f20490a600091f46f065f516effd07c49c124245f1ad7f4acb5a14c0012561c48369843e5bcea71b492b5074f019e5e763a1acb65c71664ae94b7bb54c82da83bb2789bfacb5a14c00123df31e432530af2bce5b82d6a4cf57d4e020445196ad722d95596b429800247ceabd5b34644081a4aac8bbe61bf223ebaad72d1d44b65565ad0a600091f4a18239ee508eca285265e99253c39981a58c165565ad0a60009214c00ed3f81b6809dcead5ad094848923f06060f7f36853000490a60009214c00160680c6bdeb617718261ddddcc11a7783fb8765ad0a60009214c001242980024853000490a60009214c001242980024853000490a60009214c001242980024853000490a60009214c00124298002485300049090000fefcfea800000000000038fa6a1f784c4c7335a73201a43f3b28069b262d128bd78a2be198ab38e9e5bbe58e4b697fa7f56505d08e5d8c98fa7be914af416ad1918707038e76f6953aaab0a5e2c68d44e9bd8ccae72e130e6b5fe6c7d053c52f864e438e233eba4b622260f145a564321a46dbb047f936c344db90ac19b8c888ed4803ef40742f1ec950f114a13d51338976aa3be39f118699db9230b0f4e99779dc80a94b785b2b0e1e9ab7e179febffaa750bc46b94c074eebbf2b21c11b608d1868d903da90f42a4cc25bc4845e8d1237197dd4ea5d6337214bc841c63a2d2dbc63d1819fd14495ce7d85200091b122c99f35cc99c5ce5de2fffbef8b247c91ccedd4b91bf7f44be7679fe2bb621f5950a6b023d78d7c41722cb874a0807bdd96aecafe27d2002207b233c276677ea95c8d72f88f73d59ea4b24cc68d2b8ff9c377e82bee82f5c23703b41e7e18914c7b69b42cc1d7ed8e4425fc5e2158a3d168ba34e435a2722fbbb404320cefdbc0403c9a6aae103bdaa96b2909798bbcdd58f71dad95f15e7daa1f8a604416c467f973a2bb58fd27938ec64534ead4b7094cf9c9feb802f61bc8c2f5d4ddfb240001f90665635bd49633a03524ab6cb17cd585c3959d85fa177de7dd4d71b210050c035930a8a62af0833387dfb9d5dba7478c5f6e7252b3c7d72485bb8dd9f74037c270896d3541b619ee7f61a4e10f641ad705b8cc6e14168807fc0219d6fdf83baff3e4063fe2563e4445e07bd27f0b0e0fa2fac8f66fe63a09dd3f9973d771bc221af7498f27109ec6678f52b2d12050f27d1908ca8d967d7965366a69dead30a47921bd826108cc82c8f2e44c8cc839e3038f2e0d4b1da778728ee69adbecae7df51faf79a8ce89eeb9beaeccbc6e150cd16d686e17c858e24de73d2f247cc1dd0ac5205f20f19c068fc41d25bcfabab5b2df5fd150e7948f5f44ecd960a0e888db31779fad424863b4bd4f26a9f6f33166a4889c5321ccc4fd33609cb8b5ff8c7d65f76cd039bda3afbfdb507940e19ba5b833092372ca837460781a31fb3dbf9a0ee4b51a40979fd85af9ab8fb146dadd5bd85cb36824a7752530e5bf4fa3607b160ffe8945330e31bd40169ddfad4a5ce32ec5eb1d2ed226fd7cd5c7d53a48972910528ccf826c33c889b11629086e9133509c93e6a5c0634a950d252875156c7842c6c8018f6ee546d63e2b0291a457db64d11c02529934b589a7a5bc9c49d78c0c8e150e50c4f67c043fd017d9289867e0161336de870bfc862dec25bee382597cf8846a9700c8d7aa67d6637e9a8cf27b4e76deb7fcc2b601fe14a1b6bdb087e7cbd389c5ae6ca0d05b747e452cfb6ffda09f0f39fbcad90ae5e218615e40bacf0ee957d41e9719ff137e69c68444e4dfc322f60a6bcefa6b524e1481fbee9a519d78b72c43629ac045a4a53e60f58fa781e0893363e458856c5dc21fa0d672811ff5f265130e7a689043463f9f9963636b9fb49b84e8832c56a83e9c1c0f38df53631d7f0c5496c90b05658135fbfccfaaefaa07e10fc1f5ad2f03d5ffa00aef82ca3cc641d061a64e342a5756040834503571eb6ff774ff59d5380ffe87b5e32530b4189eb1ebe668c75e2fe246c33379418f062d6442292ff0363be9caf76c92e0c213ff37c323f9f59fa0b01816534817906d9de607e1e298418ecfccc8bde69fb8e0ea7c9092695c250b5abe70db708f49932e45f15e731b2ed728c01a7f3f754835b2490a23b3a25eee0892f21f53d7ffaf52cfc0e719ca1630bbb06feae18d0ce0e6e844a4300e586b8665c174b82f9afb426aaa74c56a5ed1668f253c65ece8273c17a30da33ee060a511d1ef5a45c00f5c32598828abc75ff73daacfab2496bc99e98acda3fea34641c858c0527a86c919822ce95c5ad5d5abd27f1855d989cd888214432842da8b446af9430c9e0b1c287189c86334628294e9bcaa34f3947f93a988659d36ebfd1ae2b593db9569db27df907a5dba42e2ade3e7742f142e73463a93cd90c89533bad906b37902ef0961e5533a83e22cf0b1dbb1b04e3aac11ede39375912258e3ed429d96216247a4992f75097d3b3437565160bab14576b8f6bec50d567dd539332a45aa165876820409054bab7f33a6189ba7b30eafdd720f7049bc68aa165ab7d293fd2ca1e5c0502d55b72d0264debf5658deadbfc383ab09167a61cf5e024ca67bc7609f80784a49b3f3f27318e3ce857b45f30d180a21c9968d07c906c4d916646a1da7b953e6cb1ad39dc6bba66e346057fc7fb090e29ff5b6e354aa9cd6d8c00841729581aba34d0eb112adc2ba8a4aa7fdab530ab45ca3c46c15f4e143293bb528595a1bba218b24e3436542e989e3d7e0382daadd4a5da1253bb9f01142eb0744f2dd56b167224ced5e97a2c5905d14c69761baa4fe94f60dccef31f7f2002b7c5c81040796d6fcfdaeb77ebe2b84a64c7fc6b50798d3a4d86ea0b06e11a6a2eb4f437ab17304ed9596c065de1c1ee6b43a50ff30848d8149c03ba48330600af95e52be74f25399b4381c3f4db8e68c6331b78a5280651e4ba19795152b1e746d65bb224402f3922702eb76da5428ebf5f1edea2bc50299835d62471747872304574c721ec564734b91ca7e597aa051c9ab88c160923a2f990782766299490d180ec40d6637ff4507667b2f22751c262ab28fe42ff91e5e034daa32050e3e4876edbe274c943992fa8283c65af5a4bb6c38ede9fdd688a6e19d113ed4c415f9339da77303617e17db911683672388b41de294bdfec43db29d1c878b97d21d8e5536d0e3986e1c3014096f9d938621e4b4bdb48f3c675c7edb3309e07bd8b81c9e3b46f711958979d3ffedc6d6ca15d6f5fe362efa2e59b1cc1aabe81e4ba2f681ae9d80af1169b8493055ba187b99d2cebe04890518260f3952d4f1fd2d251fe7f754c195dd69e5554dc77619930478b3b75224e50be542f00f4db06a39824ce0e1bcf5b1abc29fc555a1e9f09a8d4973098dc6ff162307d31376a11bbc027af6d0f1b0710d8a6b0103c4e900efe2ee36b79036b1dff4ed9535e193595558f96bab99d4f3d58cb18ecc19d7062dd6e439ac9e83d9cf20a0fcad017cd7b20ee33dadcb45c9d0874f5ff4c09c62eed0a1187aa78311886c71b0bc70cb07db8a470be46e35c722ab8e332bc75945d9f1e23b6e3c470a7119eaf35b009ea9ab4b8b21e230d2cc588d7fa76a8858cd492fec45ab04361098b34a871ed9d5a804421df4e0d555ab61f40e4e207a6e21b7beb82073fbc1a30fdb7b5e01a1afdef496748915f7a17b7a756c823710f350657a374768061ea16832aefd3e4cd0e62d9ede10efaedee04bdfa8575604e43eb1b44b867f0b392e7b0affcffe768a2ad618b7bbfa1dac24a1540290eefff8dac971a33e3c5266ac806675e00fe1684f68c6adacee5124b9edbcd0dd3b08ae11c1f6d5f8e96aae915a15aea5e2d7aec959b34edf29b52620fceb7cc9dfe06d09b519e4e81d56d97212f282a77234a3bb9c5312f3221efb5ebe6456f0a6bd7b753fea4d260c920d132e8ee46e08c4d94ef27ed13b0d6988716ed76ac1c9a1bb861ea68272ae8b62a19ad85a9a13141c403e9a2ed2be889dc98bf6848bfce24421c11167eb813d9b31a2ed509c614d7dbb2f58be332406d9c3bb020da11988331093a4d95099c168381b260b8ca46699a81e82a650ffb40448723ec6bf51264e96daa31d9d7287f4ce8486f70aed566e5b9e644856ae715cfe775d6c35541aaa57a17b658da66f28061866cbefebbde3cd394b20673c18c01948ac89e01bf2782ba291cb5259a0bad3ed8092e92bee27de7c4045c94c987d4b6f92bab80400a969584e6caf84708c9d03e010a223f2380881f8d980ce5c2b00d805e53ce27f1a1030b5254f3d99057f4f5fe3cf6103e8f9ccae381e1ab5fd8f86722d7e839d405bc6e631665d309b92788b4246300b4e18e86e1c6a0d318f79d7c9bc7e50aafce07d705c33454a2bfa3fa39c972e0f2c398533dbbbcd8958bcaa34d7301426bb559d9d7ba70bccbde85c9fbaf50e22ce7ed54e6d0b17d6c97de7ed1e39fccae3d7cefdc0e35004658b2faaf847871be42ded81dfbbff7fb42b8593d0e9ccaa815cfb8135a45f179e683b99e388bd0fd9dd3908e875d9f8a9434698f5deab734aa0f734a0f45938709afeffc8b79094f3e894872f3242a46f9e636c58163bbe24b1d07a66492f2a9ab4f7900336adb4a6e24d02f9ac0a859f51956407b67ce9adc6c015a73adac5a97874d5e9197b53e8ff889dca188631ddd8a4e962ec6ce61b683225f995289269b7413fe1e6915bb57ad284493202c5d5713431c350c20829ab589d754d8a15d15a7003ad84341c7e42a8e0bdeaeca9689cad9091067fc0a6ccebd210de9e1ac5723043a62c50bca7b3b973ab500b5711372fd07c63a7a7418e95f62b1c232965844983711bf74ea07c743a491709ad7a043eada4591436b19be38e3c70db3e88ae7fc0011e4d324e17aae3002004b30e0c0ef3fb78372df6a4bda2d8190d344c9c43fa8e8874163aa08273e79f105dda536083d397fed8601bf17b3343730736a68dceae8eb1b7aedf320e3e9b6f18d69f5077246374d966e5566dccfc2bae4c0cfdfadf32ca1debf3b181a9a725c5ae844c31f7b0573f0ca4563a6dc77b5f9bc065117f49be024153d0718ef5f4eff322897965a785eb7c27f9aa11bace25c0ba27a0cfebd68c0902238905a592bd76d8893f1f761bf0fc44270d39c04d77427840b7034304bf31c2099f9cd1b756024e87105f8a8b3c343921902ae699193b8fc7e76494cf22faccc40f7cf2c154544259a6ecf98fba1be5793179d830d22e6f647efa033148c22927a3f9d0a401494cee2e9159bb2ad83bbb2e6babfdedc345ba8b217a2bab92c7cc9b9bacfeb2822a632a8fda9f36f98fc4324ec78e6ead281f03b5640b896d36ef87e51ac2c3d76a1394b43c4865ad993ae716b78dea1650d74e5cbbd2afdf44a0f041141ca4ad7969f15b9fc80b75e8331ce6d6b74d819c71618e06e7bea9d14dfc92fc4e0cfb9ed3c34ad04f837d0b8e73fee73ff0b2e0627ffbfe7eb1fbec1cf41c2ca0a8e40c21dae65b3aed12595a297f627f84d18d1a126433bef57e3fb20b596a5bad6654fa347868c587cf5ec12a046c4969b716e9d954c6df05352f31809db5d7e0e2befc23d93067d0cb83862f9d5cd3bcac48d3e7b1080a9c5c6d32dd60d5755bafe23e87452d330ab3393dbcc4193fb447daa89c436836e421ea4a9c52288fd9c6043a984648e96f167a41aedce87239718f38da7c33e7ca6ac6badced2ec47172489ecc6839b20ca97c0a81ab1b566ecea27f095bd5e60eefcb790054fd983b2c6dfda15828449d65304a9b5ca68ed58e22e32878f4d0ce8b068ba771131f4cae0aec9981db895796325d16f3da62885e207fd9670bc53ae82c60323ee8de958c18621ef60079de094123cf54a4a42f7f0d2d2e951c060f5f1fe07113d908722a2e04db4eb7fb23ce8bf6b5555d9f854409da2721b120cf6069f60c7eed157d40ed6bf2142364890c2d76f0078029d3b14d2fbaa2cf22f721bf61fc57c4d5b2cbfdac61c1241062b5fab6237de2f832c51494483aad1dda91464d628155cccf58329a43b681b8ef469f35cabae4200e922e8703c1c5c6fcfd9d1862cf06a3cf87fcb44b84e848267531738e396d83acc1a20c359a0da435e544ba43d6bdb52a76f000e74c76c3cda69f7c5557f8f610b21fd8915399c6f5eff12c88ac02b5527d94236ce1c94d478685a7df0913fa9d5ace537bc01c97ab00499ed1cad008fdd318074c64f269c9f06bb49007364edda921105afb46a70a9968e225d83158960d7157eb30b1551a3330d771da39daa2f0c5040c5004834dff847414ea2811c02ba0a79d2ba1b2fa155d0d482ccf68e4467329bfd53a47326d58741f5dc9bf6d105125c8650ac0d7c6c09cfba4a17f02dffaa8555cd54bab43ff481216701185ebca3a4de67a2a1bdcbecd551c11099b0494d6b99bfbac51c0d2ca9e89adabe82260d257ab62246c8288eafc06b0bda526df21ee53fcd6e895d8acca070acb606298b39a47566b84d202341311442b7eca1818569166a9a95774884c8920c76b290dd93b15eb74342e04e3e2af033ce6029e6e4a3ac131b5e53b81b002a588d4f7ada3e079768fbe40e0aedd00e98eebdeed38da5efa6abb027dd1782a6c2d4eaa41289284c71eb8ea3d846af516da6d115a6495c24e47024550188611c2951049d001d68ba42cb24bd24fe7e6fc94460b7854356c2fb83483d55371cca4538133140c30abe5d3603666bb92d7c2ce7a8630dad8b2b3a5fad188887652ba17702b9250de7153c0880c8868509a423ee6b241a41a6ffc24124a995b67c8119f50fad7e507b894da6fafe67e36201feedaffab23d0daac41a86919bbdb136d39854632370611c7a291a4fde3d29510b667e558a7c40f4bab7d420fe0c05d8685f53274a128233efe5b49fd8f42a89ad8cd574dfb15dd44c4fb27bca913ccd14e29142542f7b0e079922e86305e4ce9eeb8b8780ed0be0594e5e3cf42639bcb2fdfefcc71aa36fef393396936f1d56fe368c3a0b110c0cfd64cd87bd4a3c6a02f07e8d701666d7518476ab9c4dfe8629a648be10e57b1dc138e7aec54a867a40e20a6ece8c1c4280cc283e24927fc1a7d826f88bc1bf71b02a2be3413030a00f79576091f1537caf7aa59e3fc5417f8413675bd7f107024110f28a0b4826cb35edd39c5ffddb46e747b370018b7d9836d52d40fae2c4ac0edfc5c24ab6957b4584175ae237cd7aaf0525b55c3afd595ff18a3895ecba7921ff32d6767a2ee76505a34114ee13de66548dd1d01c9ade87f7c91e152352a811b695828d2370c58674267658f72bec8dd1f7d17d3f4d510cca3d3e1a575467b959c39fa61ca6bf8d861aa9f3db1a1aca0a5fb6ab4747139ac1eeab4704cbb154e29fcd19d75da7af0e760abe156a54ac8195c89352cce0e5d997fa93cf218165925d4d7adebe96a0e81efe3c622dcaf6bb7837898037641bbd8520e9b19b05b5b499c638fe1f8d710ab06cdf32fe0803480c68d6fad5806c92e30a10001237f41cde8e7dbf657472b12292553accd3ecfd08cbf5bb3360ba526423b85c8cc8e89120f93051d9b777a2f4e40a0abb22963ca37b5052a89a371829fe08f420f48f6a38bc07a4c6802f8adfe98e48100aaf869eda8ff620422495c302f9a1f6d4d79f69e520227c95ca3b771f79520ae9824f08b60f51085bfced15ffdc2021c26405a2200ce32075ab660d073edb184e71145f8756962767a80a66f81324dabdcba5e910f4b9feb2f3196b060d41db6a042d8f86375afea23015a04e1cee720639ce20d7f7950f34dda8c09f7640ba455b9727fac9550b57635db7e513cf920d019a5238b4dfc0fd9538f9e894d18ea7ddc66bff568812cef043049ae18b41823c8e65561577af2901959ba05b6807bcfb70c289c7bf5afceab903de0497f8e48ce3d9c532e52f7ef70dba421ff192d651f4633e2be7d7a0028d39915475e1f466c6f2e24ae117ea0741741cfe3444dea847f00cc0330bc3b2de9ad77f2e0dea815a1d65678197f3f5fbb5d8f3edbaaec28667c643391a5dad2450a2b2daff1fdb73f1d2afba6395336d2cf3d983b92f4f02656f5abd87e814b6f2eaf565d6cb684ba88e386de682fc5ea900642803ef4c61df97b91c6126227eb7c9a7f4949c72f023687ae0261593382ffbc6858a95b948e5079fab329c55bedef03c9a0da044e2f0fb68fd4c19d1253dfad00aa1273d6838a0e5f0484d411f32ebb110d359832943788becc67e3aca3d3f3093d8b76d37a97408420b6ce698301fe7e287f2da6c311ad3d689be4875640dbae166fc5f50e8f292f9744378696e00a44521e5492ea2407a3c36a8274e377a07755a1691c7edc71763151b8ad3de99030fdd4487a07a95c21ffbfd421b47cc7b7d45e654131959eb6a42095d3aec6fa684aff6f9abc3299b58392c3eac7f7114f97fce754e6593d34d6e531fcd70fde4d33840018e41cd73b795a1a95e1d91b7deea8187802246af93eaabe1ddbcb81e928dc4142b14d38548a2b7706fbf8fb152eae85b394c52c342e48e1f94db9b28c0714cd15a9297dccfa5630ac8b1410e9e1cd27f577dc9ac8673b947378fa6ffd1c536acfe67a0051caa489f8da773151b9023abb7773e4fdea6bdf689bbd3d69947044f1f6acb81f1eded5f16bc638b887d8cf20774582f782428bf99784e8daf4d909110527e1d095face7dcb0f13b89ef829105b4ad88e0e8f7ba9c7f90fc7a25a2aaa61108a42b66f20809b1007f06dd4546b74ac3905f14f3cc1fe6a7ebd5bd0993ffa8929d9575dcb902c1f8a283d8a5b85ea625ce336584a99900b518d52801440351e7bc3fd1ff637733ba454e6936bb5bc14ce65d7cc07eafab952cd31f19fddbd93a33e1f2b553fc74aea19789e7d5141831ba003afc81838a3a69cab4eab6c97a26b5e51554eb45d8ac6bff30cac758e9eb5df6ec2364259df682658488995d047e387ddf15c4e838ab91b5eb955663dccaedeebaa6f26a18f7a2cbdaecf5defe7081802280b51592b4d6658adbb985cce59d9e84a050535aa4bbac79e6d57231082d66d4c9e36c8495a9902e7de3a69f49b7907a476aac6833b0889180f94776bb4d2d5a1d6efbfc6ea79878f250502c24e0392a8caf1fa6b438e907c3526897fe27e74ab9751063aaf4ff142bc9ac5b3ad9b600000000002deee1de5cbd1e6f1232285f7b9f3d082a7d305f48f523867941f3608bcf56bdf267e568584a1a7f4bedb7d5a7e095d22e3076c47c98df9d785e4d247215a1e02b0a8b50d35cbd5efc5ce3d5a94103d52659e97abad3347086185392c29c9614e4b0a72585392c29c9614e4b0a725855261ea70fe0e6ba316b48b9d323e269ba558b7ab6bacd454f49bf1f24a3259724798878d47c8c36e3bbe1c9532bfeffb5c92b5795cc134a4017fd145ecba0684670d5562f8d2e97852f486eaa35eaccf65cdf6c3ada82937d48b1ec31bb4e5f6a09ab03e78f4c85a93abc63e45955a0714384e326a45ab02509297dc8dd0972c2796b0083cfa695c299652f14bf7b15676071efdd9f10ec4e7bd66713b5df7fb438f66a95a95a95a95a95a95a95afd5d84249baace70acacadcd7e355d22dd1d3dde53c113acc1750ee8485bf51e97db61e9a2bafb004aa13652326f75d55b98f7ffcb652d80775d46ce9b9dd7b00ed3e5b511c1220ee23ad6118d2182879f2d5a96fe2033cea841e48f67f73e64d76917f8bcda9303b76faf3357d5fbb433765aa91a6414492768ef6bc794ffc6974f469bc9fbec6d75ef23fbe84f95cec8ec895bc77b16353d1682de89b7a69d171123f522ec862d2de89c2f3744b7d0adadbe3886354d486b8035abd35336c345810c4d206e19bcfae02bfabe8097307bada6e0b86fabd173f3af1ed5f0811fd2cd363e4f90a4b71ced2e1e45093574508213cff73b45bb2e970e3b1bf4e69848b75030de5ee4df85e22cbd7bd4e67dab867129bef7aa9805cbef2747b65980fd55fc0b60b9517e8e810f492cc5caaea47821dd786fff2586fd42fd319241e94187a0b3fe50eef481fbc4829e9d54a759e43f4a433be6f065dd1acc36bc25d788300e6a37811c53570809090ce94cfa565615c7fd5478a7dbf4d0047be26fc5da5e964669baf83c50d62ede85b69e00f96e4009da56d4347dbaed9030ac51eae0883729ab4a990e19f7032ca3ddd7be2084a146f4bc40c4d28586dcf1313ea2ae6d8646ff39a164fc3518a8e18873bef6ae692229e3fbfc9d38b0cb2c161e054543dc17b4f30698e1a2bd416a6e4cb04ca596967c866417801bbc5974640e81e91c986d0b69a64a38a37f28eebbc579c3a38b04cba979c41e5fd3ab697522d6a5b98187bf309035a086e2c635f7bc69aa347d382acdee8358d42c677ec80908956feca053e4c9a9d435dae5073f9e7700aad0463d608992e067a173efda3eb32373c39d1b983035efe6af0efe1adf49b96b824b2b427906272269d38b0507ba3f8858e84fa18647d9a5a06da9ab02ef223dc95bd6a5f85317817275c9c21d2896c3e3eaf1ce9d8c066ba097ce0443f0ca689b397b3c4cb5c6376b6c19beb601c8a07b46c76dda1cbafe1e3bfc7848397d0ffc388e2ffbec6187708147612515b351f12100f28e55664b126ffc164e000a188dbc0040ebb40909069e9c055ecb152fa12f90d7db849e904439919d713105064286afa35b47439a0514d28919eefa6e2c6d25fa1c5c102117fc49ae95dac8069c00274d053bd7e8b5f7132e5b42ca11917e9c6a4c249807a1f4282b711ed2dc6a2539bf3bfcafc002dda8ed1e6b0822fbb63b6711e9312e1f286530d726e824ed7d3b75ad45aa3560e6b255c7053f1a8978c6b66b12eb8b3350590705ba7ab4ecb522603d0047e93a6d859dcbbf215f318aa698a00d40191a2ea3baab2c5e66ab46afbd079e6535329f692f7c95a8111c08fe7b94b1c1ca653db2389fb79ecfb0a694d2796bc7904895d986dacbd9c2e85f0939adae1270c0268a9db0ae807722aa5ee5cbc136cc1e2baed4bbf2a10f97ba7301ce9723594545c8dde28a8878362a7de185d26f4ef8df8eccc4f2d9a11f30311ed2354f9c3821e8662fb08187a6c38aa703905cd43e0a49f56ab2fdfa7422629f6dcc6cb95b328d950f645811a947ef2ffe34381909a2c287adda2f0f2e7a774ef640df65cd2406cbf38ba40a85d65227fa9585c99e22fafff04765096dd462253b8a9df91e26161d1d88f88bcd80a67eeb95f889954ffd6570130b4d4606a9d2d92bbdee880023b6c993e47d45d44b0496fbc3c3e5fa323269588b597efa16c4a64bc40649c311d7d7037a61998d356c688f150e5f817af4d172be43896f592549a7d43955d222332abfa2e548db4a27b0a44556073915ec336c9231f3d559cc3c59b683ddaee8d6474bd6f086fac64d6d7ec8c141be3fa0ce5c0ec9bbd9d9d0ea9c35e2ba67bfe116f50a42f807386636471796a7bce7e09bf423b748003d0f79cfb22c1cfbfb3b434f7016b5318a0ea816538bcd96d9541bb3a4835bb9ad9ba225bc7dd8fe6acec34054faa020652f3a92e5c6a3d9ea110c6dc6153952a7e53826b82aa47bbc2ab9289bbf71b58c316ef51df0e41db54adfcb2857ea7b06a7fddf790e3b79afaeb0005195f5e5b21b4800d01be6a9dc12b71026e71826dd53fac4235b4332eb4e0022de701b821756760259680a0c5ee8ee9adcbb6f09904c7bb6d3299cd563ba8eaca9f0d32015e677d3da661f4bb945e137789b54908623ed9507a0b4a40f61367beee555b3b1470c39b916d858f8992675b8aa8f2e50bd177e6e9e671e0a39e0dac6e9ae2d7c4f216743c179be61cf96f7ab32acf047a2325d69c2cd9b6db94d8581e51768ceda6ebf4aa5ba360970f750b7b9084596ded0c3cde699295165b23a72590828b6bd749d8bf4e7c447c340d99ebb70d97e090be04465154dccb55f69c617cf96f93d4be886393535ff8724d929a3f325436b7cf96f436b60f184092176cbad35df4e117c80cff76a32c01705dc89ac6a6bb3fc41204d42f2fa011923be197cb901ac8893b017f01aec37370487c515dd8382dff564bc0b848f7af10bc19af5ccf01db3490205e9230821a89515250070c8d08c2de008c353f0018d1a4573d4fab272bc5b4ba790e1cff61e633804829044004321e9029ba783029a0ca89582278572c55809505fc2a061ff878ef856a7dd6dd9bf08f5180df08ad9095d0af09583f52626a03e7783ba8e040cd0fd1dc5b7561eabf89bdfb5913212afda10682ea0b7de5342078f50c47f7bb79dbd4cf6d722d7552462d5d4e7c49462380d456e0b990234ab05d3fd3267b587b37200b7d591233bddd335a5b224ae3a0013190f6a3d5e60d2e49170e1a1c25eeefbecd2e0386a0a59c97800f4c2df77e3f017409369e6107f69b29db18a2cf5d12364211064ca312840080aead601d70090acce26e393a924815556b864b14f044364bec954a289c60732f19f606e172f397646b527d5b490ca69dcc31e2cf5f49d12548199edaadfa01597744d6c1bf96bfc31eb89c2a4a765241765ed92669d649dfbb1b23a25bad6fbbbbcea300b694f820f151f8f8d098ed26a17992d6227471919081b7f4754c0e78f04691799d54e9f02e1269af540996b452b92c54d6a2100034de64ae9d0c1808144b8e4cb1bdfd76e8f4c9c5bbfac0fd0e3e5bc3e6d1484559d054800b69ccca565a5d01241df8f3b8840cc79fcaa340635ed7e7fa3e834500474391bbd54ed2629700d030acef417b08a39bc8532a9eec188660fbba6804747c7a06fc3232431cd0dcf7204fc22ed1e4bbde9c0f542517467418c35841789a2b2e4d254553ec7f1f42209c0d95c9991a543e17e5c2b17dc246adf278f1935ae47fd7b2a7d50d8d37a122adc294c70c8b589cfb859a14bbaff0cf10aabf38810641b987ff4eac0ba34b7bb80a33516a020da3d2c268d2550bb7ace7dd2a179e47a8f876b60446a208e2403664a75c59ac79acdca8842bc7224a1c61ec15e0657bb7b0a84795c732069a7805be1be69554abb59aecbfa97d5914b4f12e6f828cbeeb2bd9ee8777ebc4c351a37ba2cd6311b5d5807583b5be4795d55d1aed1ceee33c75afdf5b8c338614f090e0d0d069cf48a779556f3664b9c9b47ff7abc41e13a3869a914d15af41e799393af769bcf3b889d73f60735635f7374de39e5cb591e28cfeb9bb5b26c51a1335d38a8642dc5cfe19886642924be76da378a3ba440ac51495368fb5641b2fb0e48ba9776840249fbe6b48fa1b7f53737002ac12a8109f315e59d94297526ea6d7c3aac310e82afae24fbf446a6d2ed718110e8b5ccf98ecf83b7ecc2a8753e1e8b019104d33bc452455465295cdf888402536a6c39822604f5463358e335b6e5c57ce7d6f6303f6ff0e710cfbe05411f50a5b7025bbd32a712bd1d76f40ec3840cd01d325accae49e670fe6e5574e2d42498c14a9c5b75f89292f554677dfb3774698315531cec37aa97c9f2f864a9fe07986ed20ffe7235180008e5fd20edcd3cf05e6dc5fb081f4463895f108db320428da582c409c5f5363eb08fcc3be137d382da720b6809ad70f2b5b096cac9a17e23ef67916e452a9fe62108dcce819dfb27bbc9cadabd5a2034f3762cf6730ce6b05247fa133dc270d2fe78264ab012c405bd8ea8cee73fde1d96538c5c9b55b59600e8dbba755da90173f1c1e961324d5887ffbc0cd9f82b6088ce881a79d6a8417777ea1bd31195072580000aaea243abc109fa94d40d0d1e08651da0604ad27488facde9000a15d5cc5a2bb3b126234f9e455710750982622b9757ddc5ff0ef05dec9a9c68d1b091659039b16290618f86a3741f10e93b26fd00b49991f4257f9a38d40279a71809b0ae5d8be3476ab230300470dcd9928a41f6d3dc6a45a2c061c60c5f5a2a04fc44b3742836809a8e525822e21ea71a4bbf79557d58758dc2a82a10d3de6eeb82e000002ee5869ee5f8fdb4cb853f659651308f13b17dda52a56d47299021fc7707f89f967161e80eff80ebeec4a09613f610d7bda6eb12017f5c8b5e2d315303f4fc76f7c629061c8fceffb3bfe25f0c64deae8e25f712725ad0c03892b5e04b41db0abfe90b8686cbd683c4767c736a7961d6f7d0481fd5f9a39ed498ac258faec77f58eb9aa6ef34d8e4fae919512fa50501842fdf39231390f0e15de0cfa4d0b3055faa9a2500013d73748df06f15be299817887a2382789701f6c125c61fa2f55f1a1d552d754106d1613973deb074f950d171b59514aaeaa75cca7f8b2ee34dd8e46da85abe84cf152b75f1d03bebc936e27517b8e42ebb72daf112970932886fa5a2d9d8ea0e1f7dbd93851fa477be164bbcf37043287d026992b7fcf111b10f71780f6e9dfba728ec046e1779bbf5814907104e7000000000000000000000000068", + }, + "P2PKH inscription with OP_RETURN suffix (S Mon)": { + // 75e24ffd0161f094a5e419dba42684c69faeacbeb805a1d9afdb29f6f4ac81ad_0 + script: "76a914fabef78de0d136d0f9b13f047312fc4df094da9c88ac0063036f72645109696d6167652f706e67004dcf0e89504e470d0a1a0a0000000d4948445200000180000001800806000000a4c7b5bf00000006624b474400ff00ff00ffa0bda79300000e8449444154789cedddfb93ddf55dc7f1b339bb9b4d36091b2021d484d2909f8ab6dafa030816158233b6462c6da509f710508142200912728329500a4941e496001129424ba1741cb5b5056d404b2df687d21116103b4d099799e5127673367b76cffa47bc985977de8fc7ef2fce59f6ec79e6fbcbe7d3680000000000000000000000000000000000000000000000001fb4aea97e034cad93ffe094c9643fd988e68da79e7a32fa0c7efa8f3e13bd81f787df4fe68d3d7b7e18bdffe5a79c1abdffd183a3c9bcf1f4d37b7c07143663aadf0000534300008a120080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a29c053ec53ef9c9df8ece83efede98d5e7ff7ee07a27d57f8115a7dc179d1fe6f77ff5db49f9cccee33b8f892bf88f637dfbc3dda0f0c0c44fb95abce88f6e917c833fffe8cefa029e40900a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008aea9eea37b060c182e840f6fefe39d1ebffe217ff1b9d47fed18f1e1bbdff8bfff29264def8d0877e2dda5f78d105d17ef3a6add1be6f565fb4efe9ed89f66f0f0d45fbb1f658b43f78f060b45f159ee77fdbadb747fbd75e7b2ddabfb77f63f4f7f3fcf33f8bfe7e972d5b16bdfe8c19cd64de78e9a5c129bd0fc11300405102005094000014250000450900405102005094000014250000450900405102005094000014250000450900405102005094000014159f457dc8218744e7699f78e2ef46afff918f2c8df64f3df983687fdd755f8ef6ed763bdadff6d7b746fb679ffd51f41938e9532745bfff6677762545dfccec3e8103ad03d1be199e07dfdd93fdfcdffbde77a3dfdf09279c18fdfed6afbb2a9937dae17d0ad7df90fdfd9dbafcd468fff2cb2f47fb1f3dfb1fd1fe8d37de887eff9e00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0a8f83e80d4c0c040741ef9e74eff7cf4fa73e7ce8bf64b9766f711dc7fffbdd1beab2bfb15ce9f7f68b4df78f535d17ecbb6cdd1bebfbf3fda4f8c8f47fb76b87fe8eb0f47fb73ce392bda0f8f0c47fbf43e8b73ce3e37da2f5c7844b47feeb9ff8cf68f7c23fbfdeddbb76f4abf833d0100142500004509004051020050940000142500004509004051020050940000142500004509004051020050940000142500004509004051537e1f406ac9e225d17d022bfee4b4e8f5bffffd7f89f63d3d3dd17efdbaaba2fdb265cba2fdd51bb3d7ef9f1d9ee73f3111edbb7bbaa3fd783bbb0f20fd0bdcb5f3be68ff3fafbc12edaf5c7f45b41f0fef13b8ecb2b5d17ec7d7b647fb175ef8ef69fd1dea0900a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008aca0e43ff7f203dcffdc8458ba2fdb6add746fb77de7927daefdc7957b4ef4c46d7293476dd736fb4bfe4d28ba3fd64237bff5d5dd971ee336664ff867af49b8f45fb15a77d26dab7c7c6a2fd9d77dc1ded878686a2fdcf7ffe7cb41f1d1d8df6d39d270080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100286adadf07303e3e1eedd3f3c87b7a7aa3fd830f3e10ede7ce9b17edaffff20dd1fef0c31744fbd9b36747fbf787df8ff6e97d02ad562bdabff1e61bd17ef3a6add17ee3357f15edd76fb832da9f7fdeea681f5ee7d0e8edcdfe7ea73b4f00004509004051020050940000142500004509004051020050940000142500004509004051020050940000142500004509004051020050d4b4bf0fa06f665fb43ffcf0c3a3fda2458ba2fdecfefe68df6c36a37d7a1efa9967ad8cf6add1ec3cfdf4e70faf0368f487bfbff3579f1bedefdd797fb44fdf7f7a1fc7c4c444b43fe288ecefaf3a4f00004509004051020050940000142500004509004051020050940000142500004509004051020050940000142500004509004051020050d4b4bf0f60786424dacf9a353bdaefdfbf3fdab70e1c88f6dbb65e17ed3b9dec40fc6ddbae8df637dc787db41f1e1e8ef6139dec3cfa766b2cda3ff6ad27a2fd2bafbc1cedd3cfcf359bae8ef6b36767f711bcf7debbd1be3d96fdfea63b4f00004509004051020050940000142500004509004051020050940000142500004509004051020050940000142500004509004051020050d4b4bf0fa0afaf2fda8f1cc8ee1378e8a1af47fb175f7ca12bd96fde724d74a07f7f7f761e7b7777f611ea743ad1bed96c46fb59b36645fbce44f6fe3ff7f93f8df6b3c3fb2cde7e7b28daffe4b99f449fdf919191e8f3bbf6f22b9279637232bb0f63baf304005094000014250000450900405102005094000014250000450900405102005094000014250000450900405102005094000014250000454dfbfb00daed76b41f1acace43ef09cfc33ff6d85f8f0e24ffca8d3745af3f7ffea1d17ee1c285d1feadb7de8af61ffb8d8f45fbbd7b7f19ed7b67ce8cf6edb1ecf3fbdefef7a2fdfc81f9d17ed5995f8c3ebfe9fb6fb55ad17e46b3f6bf816bfff4008509004051020050940000142500004509004051020050940000142500004509004051020050940000142500004509004051020050d494df07b060c1c2e83cf1952b5745af7fe4a223a3fdc4c444b44fcf931f08cf736f369bd1fec5175f8cf61f3eeaa8687ff2f2df8ff6a9fb76ed8ef6938de8e3df38e28845d1fedd77df89f6cdeeecf3d31d7efede7cebcd687fc5da75d17efb8e9ba35fe0abafbeda15bd8190270080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100282a3e8bfab0c30e8bcec33ee18413a3d74fcfa33f75f9a9d1fee31fffcd687fef7dbba27d4f774fb44f3f01b3fa6645fb894e769f425757f603743a9d689fea84f7494c66d709c4f7110c0f0f47fbad5bb645fbc1c1c168bfebde9dd1feb8e38e8ff64f3ef98368fffaebfba23f004f000045090040510200509400001425000045090040510200509400001425000045090040510200509400001425000045090040510200505477fa1f181a1a8acea37efae93dd181e4871e7a68326f2c5ebc24daefd8714bb4dfbefd6bd1fe9a4d1ba3fd2d376f8ff603870c44fba38efa70b46fb55ad17ed1914746fb919191687f707434da379bcd687fda675744fbf5eb3644fb2d5b3645fb73cf3d3fdab7db63d1fe99a7f744fbf43cff94270080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100282abe0f203567cedc68bf7cf91f46fbd1d1ec3cf9f1898968bf65ebe668dfdbdb1bedd3fb10d6acc9ce63ef9a91fd1be4d16f3c16ed0f1e3c18ed57acf874b46f84a7c13fb0fbc1689f7e7eeeb9e7ae68df13befedcb9d9f7c7972ebd3cdadf7adb8e683fd53c0100142500004509004051020050940000142500004509004051020050940000142500004509004051020050940000142500004509004051f17d0073e6cc994cf6471f7d74f4fa4f3cf178b4df78f5a6689fbafcb2b5d17efb8e5ba27dab95dd8730d66e47fb7ff8ce3f46fb2ffcd9e9d13e3d8ffedb8f7f27daaf3aeb8bd1bed9dd8cf65de185046bd75e19ed376fc9fefefafa6645fbf43cff157f7c5ab4fff6138f47df9ffbf6bd16fd023d0100142500004509004051020050940000142500004509004051020050940000142500004509004051020050940000142500004509004051f17d00e979dc4b162f89f6fbf6ed8bf677dcf937d17efdba0dd17e71f8f3cf9b3b37da5ff4e76ba27d7a1efeca556744fbf6f878b4ef74a2e3d81b67acfc42b47ffc5b4f44fbcf9e9e9d473fd61e8bf6232323d17edbd6eba2fda64d5747fb836307a3fdde5fed8df693939d689ff20400509400001425000045090040510200509400001425000045090040510200509400001425000045090040510200509400001425000045754df51b58b278497420fbde5fed8d7e8663961e13bdfecc99339379e3908181683fda6a45fb471e7934dabffefaebd1fe2b5fb921da4f7426a27da7939dc7dedbdb1bedefbe6b67b41f1c1c8cf6377df5c6683fdeceee63d8fffefe68dfd5957d85fdf4a7ff15fd07962c392afbfedafbcb29fd0ef60400509400001425000045090040510200509400001425000045090040510200509400001425000045090040510200509400001425000045754ff51b88cff33f263bcfffbc73cf4fe68d471ffd66b44fedda757fb4bff0a20ba2fde464f4bfbff1c0ee07a3fdea35e745fbee66f62770fb6d7744fb3517ae8ef6fbf767e7e96fdb7a6db4bff6daadd1bea7a727da9f75e6d9d17e646438fa000f0e0e4ef99d2a094f00004509004051020050940000142500004509004051020050940000142500004509004051020050940000142500004509004051020050d4b43ecbfa83b07469769fc0a5977c297afdf43cf4871ffefb68bf61c355d1fe778e3f21da9f7dce99d1fe9fbffb4f53fa193ee5e4e5d1e7e7a69b6e8e5e7f4eff9c689fdea7b07edd8668dfe974a2fd96ad9ba3fdf3cfffacf477a0270080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a028010028aaf459d81f844ffcd627a2f3e02fbef8d2e8f5efbefbce683f77debc68df887efa46e3a97f7db2f467f0a44ffd5ef47f70ac3d16bd7e734633dadf72cb8e68bfe6c2d5d1befa79fe294f0000450900405102005094000014250000450900405102005094000014250000450900405102005094000014250000450900405102005054f754bf81e9ee40eb40b46fb55ad1fef6dbb3fb002ebb3cbb8fe0c73f7ed679ec811feef9b7e8ffdff1c71d1fdd27909ee7ffd24b83d1bebb3bbb8f808c270080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a0280100284a00008a120080a20400a028010028eaff0080a891cbdf3cd1d00000000049454e44ae426082686a223150755161374b36324d694b43747373534c4b79316b683536575755374d74555235035345540474797065036f72640a636f6c6c656374696f6e0c734d6f6e2053696e6e657273036170700a74616c656f6673687561076d6f6e5479706508637265617475726505617564696f44623a2f2f333139616138346165323332663838353735353631616366323234303266343561663261306537616465313232623536353166623936633033356161643531300573746174734c4c7b22737472656e677468223a312c22766974616c697479223a332c226167696c697479223a322c22696e74656c6c6967656e6365223a342c226c75636b223a342c22737069726974223a357d04677569642433656330663636302d643437632d313165642d396530662d3835333264323665613832380373696e057072696465066e617475726508776174636866756c0472616365056669656e64046e616d6517576174636866756c204669656e64206f66205072696465", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + inscScript, err := bscript.NewFromHexString(test.script) + if err != nil { + t.Error(err) + t.FailNow() + } + assert.True(t, inscScript.IsInscribed()) + assert.True(t, inscScript.IsP2PKHInscription()) + }) + } +}