Skip to content

Commit

Permalink
Add ordinals and inscriptions functionality (#142)
Browse files Browse the repository at this point in the history
  • Loading branch information
jadwahab authored Mar 29, 2023
1 parent f7971e5 commit ce5d9c0
Show file tree
Hide file tree
Showing 23 changed files with 1,365 additions and 106 deletions.
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ linters:
- dupl
- misspell
- dogsled
- revive
# - revive
- prealloc
- exportloopref
- exhaustive
Expand Down
2 changes: 1 addition & 1 deletion .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ build:
# Github Release
# ---------------------------
release:
prerelease: true
# prerelease: true
name_template: "Release v{{.Version}}"
2 changes: 1 addition & 1 deletion .make/go.mk
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ install-go: ## Install the application (Using Native Go)

lint: ## Run the golangci-lint application (install if not found)
@echo "downloading golangci-lint..."
@curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- v1.45.2
@curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- v1.52.1
@echo "running golangci-lint..."
@GOGC=20 ./bin/golangci-lint run

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
**go-bt** requires a [supported release of Go](https://golang.org/doc/devel/release.html#policy).

```shell script
go get -u github.com/libsv/go-bt
go get -u github.com/libsv/go-bt/v2
```

<br/>
Expand Down
5 changes: 5 additions & 0 deletions bscript/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ var (
ErrUnsupportedAddress = errors.New("address not supported")
)

// Sentinel errors raised by inscriptions.
var (
ErrP2PKHInscriptionNotFound = errors.New("no P2PKH inscription found")
)

// Sentinel errors raised through encoding.
var (
ErrEncodingBadChar = errors.New("bad char")
Expand Down
15 changes: 15 additions & 0 deletions bscript/inscriptions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package bscript

// InscriptionArgs contains the Ordinal inscription data.
type InscriptionArgs struct {
LockingScriptPrefix *Script
Data []byte
ContentType string
EnrichedArgs *EnrichedInscriptionArgs
}

// EnrichedInscriptionArgs contains data needed for enriched inscription
// functionality found here: https://docs.1satordinals.com/op_return.
type EnrichedInscriptionArgs struct {
OpReturnData [][]byte
}
135 changes: 128 additions & 7 deletions bscript/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ import (

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

// Script type
Expand Down Expand Up @@ -321,6 +322,123 @@ func (s *Script) IsData() bool {
(len(b) > 1 && b[0] == OpFALSE && b[1] == OpRETURN)
}

// 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
}

// IsP2PKHInscription checks if it's a standard
// inscription with a P2PKH prefix script.
func (s *Script) IsP2PKHInscription() bool {
p, err := DecodeParts(*s)
if err != nil {
return false
}

return isP2PKHInscriptionHelper(p)
}

// isP2PKHInscriptionHelper helper so that we don't need to call
// `DecodeParts()` multiple times, such as in `ParseInscription()`
func isP2PKHInscriptionHelper(parts [][]byte) bool {
// TODO: cleanup
return len(parts) == 13 &&
parts[0][0] == OpDUP &&
parts[1][0] == OpHASH160 &&
parts[3][0] == OpEQUALVERIFY &&
parts[4][0] == OpCHECKSIG &&
parts[5][0] == OpFALSE &&
parts[6][0] == OpIF &&
parts[7][0] == 0x6f && parts[7][1] == 0x72 && parts[7][2] == 0x64 && // op_push "ord"
parts[8][0] == OpTRUE &&
parts[10][0] == OpFALSE &&
parts[12][0] == OpENDIF
}

// ParseInscription parses the script to
// return the inscription found. Will return
// an error if the scription doesn't contain
// any inscriptions.
func (s *Script) ParseInscription() (*InscriptionArgs, error) {
p, err := DecodeParts(*s)
if err != nil {
return nil, err
}

if !isP2PKHInscriptionHelper(p) {
return nil, ErrP2PKHInscriptionNotFound
}

// FIXME: make it dynamic based on order.
// right now if the content type and the content change order
// then this will fail. My understanding is that the content
// always needs to be last and the previous fields can be
// reordered - this is based on the original ordinals
// indexer: https://github.com/casey/ord
return &InscriptionArgs{
LockingScriptPrefix: s.Slice(0, 25),
Data: p[11],
ContentType: string(p[9]),
// EnrichedArgs: , // TODO:
}, nil
}

// Slice a script to get back a subset of that script.
func (s *Script) Slice(start, end uint64) *Script {
ss := *s
sss := ss[start:end]
return &sss
}

// IsMultiSigOut returns true if this is a multisig output script.
func (s *Script) IsMultiSigOut() bool {
parts, err := DecodeParts(*s)
Expand Down Expand Up @@ -385,6 +503,9 @@ func (s *Script) ScriptType() string {
if s.IsData() {
return ScriptTypeNullData
}
if s.IsP2PKHInscription() {
return ScriptTypePubKeyHashInscription
}
return ScriptTypeNonStandard
}

Expand Down
18 changes: 18 additions & 0 deletions bscript/script_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"reflect"
"strings"
"testing"

Expand Down Expand Up @@ -564,3 +565,20 @@ func TestRunScriptExample3(t *testing.T) {
t.Errorf("\nExpected %q\ngot %q", expected, asm)
}
}

func TestParseInscription(t *testing.T) {
ec := "text/plain;charset=utf-8"
ed := []byte("Hello, world!")
es, _ := hex.DecodeString("76a914b6aa34534d2b11e66b438c7525f819aee01e397c88ac0063036f72645118746578742f706c61696e3b636861727365743d7574662d38000d48656c6c6f2c20776f726c642168")
elsp, _ := hex.DecodeString("76a914b6aa34534d2b11e66b438c7525f819aee01e397c88ac")
s := bscript.Script(es)

pi, err := s.ParseInscription()
assert.NoError(t, err)
assert.Equal(t, hex.EncodeToString(elsp), pi.LockingScriptPrefix.String())

assert.Equal(t, ec, pi.ContentType)
if !reflect.DeepEqual(ed, pi.Data) {
t.Errorf("expected %v, but got %v", ed, pi.Data)
}
}
21 changes: 20 additions & 1 deletion errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ var (
var (
ErrInputNoExist = errors.New("specified input does not exist")
ErrInputTooShort = errors.New("input length too short")

// You should not be able to spend an input with 0 Satoshi value.
// Most likely the input Satoshi value is not provided.
ErrInputSatsZero = errors.New("input satoshi value is not provided")
)

// Sentinal errors reported by outputs.
Expand Down Expand Up @@ -46,11 +50,26 @@ var (
ErrUnknownFeeType = errors.New("unknown fee type")
)

// Sentinel errors reported by Fund
// Sentinel errors reported by Fund.
var (
// ErrNoUTXO signals the UTXOGetterFunc has reached the end of its input.
ErrNoUTXO = errors.New("no remaining utxos")

// ErrInsufficientFunds insufficient funds provided for funding
ErrInsufficientFunds = errors.New("insufficient funds provided")
)

// Sentinal errors reported by ordinal inscriptions.
var (
ErrOutputsNotEmpty = errors.New("transaction outputs must be empty to avoid messing with Ordinal ordering scheme")
)

// 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")
)
83 changes: 83 additions & 0 deletions examples/create_ordinal_bid/create_ordinal_bid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package main

import (
"context"
"encoding/hex"
"fmt"
"log"

"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"
)

func main() {
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 := 100000000

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("fc136d44114bdaa99f2d7d06a0fee514d376d974af53a3909fc43a79a3644653")
return t
}(),
Vout: uint32(0),
LockingScript: fundingScript,
Satoshis: 100027971,
Unlocker: &fundingUnlocker,
},
}

mba := &ord.MakeBidArgs{
BidAmount: uint64(bidAmount),
OrdinalTxID: "e17d7856c375640427943395d2341b6ed75f73afc8b22bb3681987278978a584",
OrdinalVOut: 81,
BidderUTXOs: us,
BuyerReceiveOrdinalScript: func() *bscript.Script {
s, _ := bscript.NewP2PKHFromAddress("1JPxYgWSYCb3ZEBBkcum84AHHdPWQzHGXj")
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(),
}

pstx, err := ord.MakeBidToBuy1SatOrdinal(context.Background(), mba)
if err != nil {
log.Fatal(err.Error())
}
fmt.Println(pstx.String())
}
8 changes: 5 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ go 1.17
require (
github.com/libsv/go-bk v0.1.6
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.8.0
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
github.com/stretchr/testify v1.8.2
golang.org/x/crypto v0.5.0
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading

0 comments on commit ce5d9c0

Please sign in to comment.