Skip to content

Commit

Permalink
Merge pull request #23 from libsv/tx_json
Browse files Browse the repository at this point in the history
Marshall and Unmarshall Bitcoin Transaction JSON
  • Loading branch information
jadwahab authored Jun 1, 2021
2 parents 9313a4d + 9b9d78d commit b2889d5
Show file tree
Hide file tree
Showing 7 changed files with 630 additions and 11 deletions.
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ linters:
- gocritic # use this for very opinionated linting
- gochecknoglobals
- whitespace
- gci
- wsl
- goerr113
- godot
Expand Down
57 changes: 51 additions & 6 deletions bscript/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ var (
ErrNotP2PKH = errors.New("not a P2PKH")
)

// ScriptKey types.
const (
ScriptTypePubKey = "pubkey"
ScriptTypePubKeyHash = "pubkeyhash"
ScriptTypeNonStandard = "nonstandard"
ScriptTypeMultiSig = "multisig"
ScriptTypeNullData = "nulldata"
)

// Script type
type Script []byte

Expand Down Expand Up @@ -87,7 +96,7 @@ func NewP2PKHFromPubKeyHash(pubKeyHash []byte) (*Script, error) {
b := []byte{
OpDUP,
OpHASH160,
0x14,
OpDATA20,
}
b = append(b, pubKeyHash...)
b = append(b, OpEQUALVERIFY)
Expand Down Expand Up @@ -223,7 +232,7 @@ func (s *Script) IsP2PKH() bool {
return len(b) == 25 &&
b[0] == OpDUP &&
b[1] == OpHASH160 &&
b[2] == 0x14 &&
b[2] == OpDATA20 &&
b[23] == OpEQUALVERIFY &&
b[24] == OpCHECKSIG
}
Expand Down Expand Up @@ -255,7 +264,7 @@ func (s *Script) IsP2SH() bool {

return len(b) == 23 &&
b[0] == OpHASH160 &&
b[1] == 0x14 &&
b[1] == OpDATA20 &&
b[22] == OpEQUAL
}

Expand All @@ -264,8 +273,8 @@ func (s *Script) IsP2SH() bool {
func (s *Script) IsData() bool {
b := []byte(*s)

return b[0] == 0x6a ||
b[0] == 0x00 && b[1] == 0x6a
return b[0] == OpRETURN ||
b[0] == OpFALSE && b[1] == OpRETURN
}

// IsMultiSigOut returns true if this is a multisig output script.
Expand Down Expand Up @@ -303,7 +312,7 @@ func (s *Script) PublicKeyHash() ([]byte, error) {
return nil, ErrEmptyScript
}

if (*s)[0] != 0x76 || (*s)[1] != 0xa9 {
if (*s)[0] != OpDUP || (*s)[1] != OpHASH160 {
return nil, ErrNotP2PKH
}

Expand All @@ -315,6 +324,42 @@ func (s *Script) PublicKeyHash() ([]byte, error) {
return parts[0], nil
}

// ScriptType returns the type of script this is as a string.
func (s *Script) ScriptType() string {
if s.IsP2PKH() {
return ScriptTypePubKeyHash
}
if s.IsP2PK() {
return ScriptTypePubKey
}
if s.IsMultiSigOut() {
return ScriptTypeMultiSig
}
if s.IsData() {
return ScriptTypeNullData
}
return ScriptTypeNonStandard
}

// Addresses will return all addresses found in the script, if any.
func (s *Script) Addresses() ([]string, error) {
addresses := make([]string, 0)
if s.IsP2PKH() {
pkh, err := s.PublicKeyHash()
if err != nil {
return nil, err
}
a, err := NewAddressFromPublicKeyHash(pkh, true)
if err != nil {
return nil, err
}
addresses = []string{a.AddressString}
}
// TODO: handle multisig, and other outputs
// https://github.com/libsv/go-bt/issues/6
return addresses, nil
}

// Equals will compare the script to b and return true if they match.
func (s *Script) Equals(b *Script) bool {
return bytes.Equal(*s, *b)
Expand Down
65 changes: 65 additions & 0 deletions input.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package bt

import (
"encoding/hex"
"encoding/json"
"fmt"

"github.com/libsv/go-bt/bscript"
Expand Down Expand Up @@ -35,6 +36,70 @@ type Input struct {
SequenceNumber uint32
}

// inputJSON is used to covnert an input to and from json.
// Script is duplicated as we have our own name for unlockingScript
// but want to be compatible with node json also.
type inputJSON struct {
UnlockingScript *struct {
Asm string `json:"asm"`
Hex string `json:"hex"`
} `json:"unlockingScript,omitempty"`
ScriptSig *struct {
Asm string `json:"asm"`
Hex string `json:"hex"`
} `json:"scriptSig,omitempty"`
TxID string `json:"txid"`
Vout uint32 `json:"vout"`
Sequence uint32 `json:"sequence"`
}

// MarshalJSON will convert an input to json, expanding upon the
// input struct to add additional fields.
func (i *Input) MarshalJSON() ([]byte, error) {
asm, err := i.UnlockingScript.ToASM()
if err != nil {
return nil, err
}
input := &inputJSON{
TxID: hex.EncodeToString(i.PreviousTxIDBytes),
Vout: i.PreviousTxOutIndex,
UnlockingScript: &struct {
Asm string `json:"asm"`
Hex string `json:"hex"`
}{
Asm: asm,
Hex: i.UnlockingScript.String(),
},
Sequence: i.SequenceNumber,
}
return json.Marshal(input)
}

// UnmarshalJSON will convert a JSON input to an input.
func (i *Input) UnmarshalJSON(b []byte) error {
var ij inputJSON
if err := json.Unmarshal(b, &ij); err != nil {
return err
}
ptxID, err := hex.DecodeString(ij.TxID)
if err != nil {
return err
}
sig := ij.UnlockingScript
if sig == nil {
sig = ij.ScriptSig
}
s, err := bscript.NewFromHexString(sig.Hex)
if err != nil {
return err
}
i.UnlockingScript = s
i.PreviousTxIDBytes = ptxID
i.PreviousTxOutIndex = ij.Vout
i.SequenceNumber = ij.Sequence
return nil
}

// PreviousTxIDStr returns the Previous TxID as a hex string.
func (i *Input) PreviousTxIDStr() string {
return hex.EncodeToString(i.PreviousTxIDBytes)
Expand Down
74 changes: 74 additions & 0 deletions output.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package bt
import (
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"

"github.com/libsv/go-bt/bscript"
Expand All @@ -24,6 +25,79 @@ Txout-script / scriptPubKey Script <out-s
type Output struct {
Satoshis uint64
LockingScript *bscript.Script
index int
}

type outputJSON struct {
Value float64 `json:"value"`
Satoshis uint64 `json:"satoshis"`
Index int `json:"n"`
ScriptPubKey *struct {
Asm string `json:"asm"`
Hex string `json:"hex"`
ReqSigs int `json:"reqSigs,omitempty"`
Type string `json:"type"`
} `json:"scriptPubKey,omitempty"`
LockingScript *struct {
Asm string `json:"asm"`
Hex string `json:"hex"`
ReqSigs int `json:"reqSigs,omitempty"`
Type string `json:"type"`
} `json:"lockingScript,omitempty"`
}

// MarshalJSON will serialise an output to json.
func (o *Output) MarshalJSON() ([]byte, error) {
asm, err := o.LockingScript.ToASM()
if err != nil {
return nil, err
}
addresses, err := o.LockingScript.Addresses()
if err != nil {
return nil, err
}

output := &outputJSON{
Value: float64(o.Satoshis) / 100000000,
Satoshis: o.Satoshis,
Index: o.index,
LockingScript: &struct {
Asm string `json:"asm"`
Hex string `json:"hex"`
ReqSigs int `json:"reqSigs,omitempty"`
Type string `json:"type"`
}{
Asm: asm,
Hex: o.LockingScriptHexString(),
ReqSigs: len(addresses),
Type: o.LockingScript.ScriptType(),
},
}
return json.Marshal(output)
}

// UnmarshalJSON will convert a json serialised output to a bt Output.
func (o *Output) UnmarshalJSON(b []byte) error {
var oj outputJSON
if err := json.Unmarshal(b, &oj); err != nil {
return err
}
script := oj.LockingScript
if script == nil {
script = oj.ScriptPubKey
}
s, err := bscript.NewFromHexString(script.Hex)
if err != nil {
return err
}
if oj.Satoshis > 0 {
o.Satoshis = oj.Satoshis
} else {
o.Satoshis = uint64(oj.Value * 100000000)
}
o.index = oj.Index
o.LockingScript = s
return nil
}

// LockingScriptHexString returns the locking script
Expand Down
65 changes: 61 additions & 4 deletions tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"bytes"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
"fmt"

"github.com/libsv/go-bk/crypto"
Expand Down Expand Up @@ -36,10 +38,64 @@ lock_time if non-zero and sequence numbers are < 0xFFFFFFFF: block height
// DO NOT CHANGE ORDER - Optimised memory via malign
//
type Tx struct {
Inputs []*Input
Outputs []*Output
Version uint32
LockTime uint32
Inputs []*Input `json:"vin"`
Outputs []*Output `json:"vout"`
Version uint32 `json:"version"`
LockTime uint32 `json:"locktime"`
}

type txJSON struct {
Version uint32 `json:"version"`
LockTime uint32 `json:"locktime"`
TxID string `json:"txid"`
Hash string `json:"hash"`
Size int `json:"size"`
Hex string `json:"hex"`
Inputs []*Input `json:"vin"`
Outputs []*Output `json:"vout"`
}

// MarshalJSON will serialise a transaction to json.
func (tx *Tx) MarshalJSON() ([]byte, error) {
if tx == nil {
return nil, errors.New("tx is nil so cannot be marshalled")
}
for i, o := range tx.Outputs {
o.index = i
}
txj := txJSON{
Version: tx.Version,
LockTime: tx.LockTime,
Inputs: tx.Inputs,
Outputs: tx.Outputs,
TxID: tx.TxID(),
Hash: tx.TxID(),
Size: len(tx.ToBytes()),
Hex: tx.String(),
}
return json.Marshal(txj)
}

// UnmarshalJSON will unmarshall a transaction that has been marshalled with this library.
func (tx *Tx) UnmarshalJSON(b []byte) error {
var txj txJSON
if err := json.Unmarshal(b, &txj); err != nil {
return err
}
// quick convert
if txj.Hex != "" {
t, err := NewTxFromString(txj.Hex)
if err != nil {
return err
}
*tx = *t
return nil
}
tx.Inputs = txj.Inputs
tx.Outputs = txj.Outputs
tx.LockTime = txj.LockTime
tx.Version = txj.Version
return nil
}

// NewTx creates a new transaction object with default values.
Expand Down Expand Up @@ -115,6 +171,7 @@ func NewTxFromStream(b []byte) (*Tx, int, error) {
if err != nil {
return nil, 0, err
}
output.index = int(i)
offset += size
t.Outputs = append(t.Outputs, output)
}
Expand Down
Loading

0 comments on commit b2889d5

Please sign in to comment.