635 lines
17 KiB
Go
635 lines
17 KiB
Go
// Copyright (c) 2019-2020 The Zcash developers
|
|
// Distributed under the MIT software license, see the accompanying
|
|
// file COPYING or https://www.opensource.org/licenses/mit-license.php .
|
|
|
|
// Package parser deserializes (full) transactions (zcashd).
|
|
package parser
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"fmt"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/adityapk00/lightwalletd/parser/internal/bytestring"
|
|
"github.com/adityapk00/lightwalletd/walletrpc"
|
|
)
|
|
|
|
type rawTransaction struct {
|
|
fOverwintered bool
|
|
version uint32
|
|
nVersionGroupID uint32
|
|
consensusBranchID uint32
|
|
transparentInputs []txIn
|
|
transparentOutputs []txOut
|
|
//nLockTime uint32
|
|
//nExpiryHeight uint32
|
|
//valueBalanceSapling int64
|
|
shieldedSpends []spend
|
|
shieldedOutputs []output
|
|
joinSplits []joinSplit
|
|
//joinSplitPubKey []byte
|
|
//joinSplitSig []byte
|
|
//bindingSigSapling []byte
|
|
orchardActions []action
|
|
}
|
|
|
|
// Txin format as described in https://en.bitcoin.it/wiki/Transaction
|
|
type txIn struct {
|
|
// SHA256d of a previous (to-be-used) transaction
|
|
//PrevTxHash []byte
|
|
|
|
// Index of the to-be-used output in the previous tx
|
|
//PrevTxOutIndex uint32
|
|
|
|
// CompactSize-prefixed, could be a pubkey or a script
|
|
ScriptSig []byte
|
|
|
|
// Bitcoin: "normally 0xFFFFFFFF; irrelevant unless transaction's lock_time > 0"
|
|
//SequenceNumber uint32
|
|
}
|
|
|
|
func (tx *txIn) ParseFromSlice(data []byte) ([]byte, error) {
|
|
s := bytestring.String(data)
|
|
|
|
if !s.Skip(32) {
|
|
return nil, errors.New("could not skip PrevTxHash")
|
|
}
|
|
|
|
if !s.Skip(4) {
|
|
return nil, errors.New("could not skip PrevTxOutIndex")
|
|
}
|
|
|
|
if !s.ReadCompactLengthPrefixed((*bytestring.String)(&tx.ScriptSig)) {
|
|
return nil, errors.New("could not read ScriptSig")
|
|
}
|
|
|
|
if !s.Skip(4) {
|
|
return nil, errors.New("could not skip SequenceNumber")
|
|
}
|
|
|
|
return []byte(s), nil
|
|
}
|
|
|
|
// Txout format as described in https://en.bitcoin.it/wiki/Transaction
|
|
type txOut struct {
|
|
// Non-negative int giving the number of zatoshis to be transferred
|
|
Value uint64
|
|
|
|
// Script. CompactSize-prefixed.
|
|
//Script []byte
|
|
}
|
|
|
|
func (tx *txOut) ParseFromSlice(data []byte) ([]byte, error) {
|
|
s := bytestring.String(data)
|
|
|
|
if !s.Skip(8) {
|
|
return nil, errors.New("could not skip txOut value")
|
|
}
|
|
|
|
if !s.SkipCompactLengthPrefixed() {
|
|
return nil, errors.New("could not skip txOut script")
|
|
}
|
|
|
|
return []byte(s), nil
|
|
}
|
|
|
|
// parse the transparent parts of the transaction
|
|
func (tx *Transaction) ParseTransparent(data []byte) ([]byte, error) {
|
|
s := bytestring.String(data)
|
|
var txInCount int
|
|
if !s.ReadCompactSize(&txInCount) {
|
|
return nil, errors.New("could not read tx_in_count")
|
|
}
|
|
var err error
|
|
tx.transparentInputs = make([]txIn, txInCount)
|
|
for i := 0; i < txInCount; i++ {
|
|
ti := &tx.transparentInputs[i]
|
|
s, err = ti.ParseFromSlice([]byte(s))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "while parsing transparent input")
|
|
}
|
|
}
|
|
|
|
var txOutCount int
|
|
if !s.ReadCompactSize(&txOutCount) {
|
|
return nil, errors.New("could not read tx_out_count")
|
|
}
|
|
tx.transparentOutputs = make([]txOut, txOutCount)
|
|
for i := 0; i < txOutCount; i++ {
|
|
to := &tx.transparentOutputs[i]
|
|
s, err = to.ParseFromSlice([]byte(s))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "while parsing transparent output")
|
|
}
|
|
}
|
|
return []byte(s), nil
|
|
}
|
|
|
|
// spend is a Sapling Spend Description as described in 7.3 of the Zcash
|
|
// protocol specification.
|
|
type spend struct {
|
|
//cv []byte // 32
|
|
//anchor []byte // 32
|
|
nullifier []byte // 32
|
|
//rk []byte // 32
|
|
//zkproof []byte // 192
|
|
//spendAuthSig []byte // 64
|
|
}
|
|
|
|
func (p *spend) ParseFromSlice(data []byte, version uint32) ([]byte, error) {
|
|
s := bytestring.String(data)
|
|
|
|
if !s.Skip(32) {
|
|
return nil, errors.New("could not skip cv")
|
|
}
|
|
|
|
if version <= 4 && !s.Skip(32) {
|
|
return nil, errors.New("could not skip anchor")
|
|
}
|
|
|
|
if !s.ReadBytes(&p.nullifier, 32) {
|
|
return nil, errors.New("could not read nullifier")
|
|
}
|
|
|
|
if !s.Skip(32) {
|
|
return nil, errors.New("could not skip rk")
|
|
}
|
|
|
|
if version <= 4 && !s.Skip(192) {
|
|
return nil, errors.New("could not skip zkproof")
|
|
}
|
|
|
|
if version <= 4 && !s.Skip(64) {
|
|
return nil, errors.New("could not skip spendAuthSig")
|
|
}
|
|
|
|
return []byte(s), nil
|
|
}
|
|
|
|
func (p *spend) ToCompact() *walletrpc.CompactSaplingSpend {
|
|
return &walletrpc.CompactSaplingSpend{
|
|
Nf: p.nullifier,
|
|
}
|
|
}
|
|
|
|
// output is a Sapling Output Description as described in section 7.4 of the
|
|
// Zcash protocol spec.
|
|
type output struct {
|
|
//cv []byte // 32
|
|
cmu []byte // 32
|
|
ephemeralKey []byte // 32
|
|
encCiphertext []byte // 580
|
|
//outCiphertext []byte // 80
|
|
//zkproof []byte // 192
|
|
}
|
|
|
|
func (p *output) ParseFromSlice(data []byte, version uint32) ([]byte, error) {
|
|
s := bytestring.String(data)
|
|
|
|
if !s.Skip(32) {
|
|
return nil, errors.New("could not skip cv")
|
|
}
|
|
|
|
if !s.ReadBytes(&p.cmu, 32) {
|
|
return nil, errors.New("could not read cmu")
|
|
}
|
|
|
|
if !s.ReadBytes(&p.ephemeralKey, 32) {
|
|
return nil, errors.New("could not read ephemeralKey")
|
|
}
|
|
|
|
if !s.ReadBytes(&p.encCiphertext, 580) {
|
|
return nil, errors.New("could not read encCiphertext")
|
|
}
|
|
|
|
if !s.Skip(80) {
|
|
return nil, errors.New("could not skip outCiphertext")
|
|
}
|
|
|
|
if version <= 4 && !s.Skip(192) {
|
|
return nil, errors.New("could not skip zkproof")
|
|
}
|
|
|
|
return []byte(s), nil
|
|
}
|
|
|
|
func (p *output) ToCompact() *walletrpc.CompactSaplingOutput {
|
|
return &walletrpc.CompactSaplingOutput{
|
|
Cmu: p.cmu,
|
|
Epk: p.ephemeralKey,
|
|
Ciphertext: p.encCiphertext[:52],
|
|
}
|
|
}
|
|
|
|
// joinSplit is a JoinSplit description as described in 7.2 of the Zcash
|
|
// protocol spec. Its exact contents differ by transaction version and network
|
|
// upgrade level. Only version 4 is supported, no need for proofPHGR13.
|
|
type joinSplit struct {
|
|
//vpubOld uint64
|
|
//vpubNew uint64
|
|
//anchor []byte // 32
|
|
//nullifiers [2][]byte // 64 [N_old][32]byte
|
|
//commitments [2][]byte // 64 [N_new][32]byte
|
|
//ephemeralKey []byte // 32
|
|
//randomSeed []byte // 32
|
|
//vmacs [2][]byte // 64 [N_old][32]byte
|
|
//proofGroth16 []byte // 192 (version 4 only)
|
|
//encCiphertexts [2][]byte // 1202 [N_new][601]byte
|
|
}
|
|
|
|
func (p *joinSplit) ParseFromSlice(data []byte) ([]byte, error) {
|
|
s := bytestring.String(data)
|
|
|
|
if !s.Skip(8) {
|
|
return nil, errors.New("could not skip vpubOld")
|
|
}
|
|
|
|
if !s.Skip(8) {
|
|
return nil, errors.New("could not skip vpubNew")
|
|
}
|
|
|
|
if !s.Skip(32) {
|
|
return nil, errors.New("could not skip anchor")
|
|
}
|
|
|
|
for i := 0; i < 2; i++ {
|
|
if !s.Skip(32) {
|
|
return nil, errors.New("could not skip a nullifier")
|
|
}
|
|
}
|
|
|
|
for i := 0; i < 2; i++ {
|
|
if !s.Skip(32) {
|
|
return nil, errors.New("could not skip a commitment")
|
|
}
|
|
}
|
|
|
|
if !s.Skip(32) {
|
|
return nil, errors.New("could not skip ephemeralKey")
|
|
}
|
|
|
|
if !s.Skip(32) {
|
|
return nil, errors.New("could not skip randomSeed")
|
|
}
|
|
|
|
for i := 0; i < 2; i++ {
|
|
if !s.Skip(32) {
|
|
return nil, errors.New("could not skip a vmac")
|
|
}
|
|
}
|
|
|
|
if !s.Skip(192) {
|
|
return nil, errors.New("could not skip Groth16 proof")
|
|
}
|
|
|
|
for i := 0; i < 2; i++ {
|
|
if !s.Skip(601) {
|
|
return nil, errors.New("could not skip an encCiphertext")
|
|
}
|
|
}
|
|
|
|
return []byte(s), nil
|
|
}
|
|
|
|
type action struct {
|
|
//cv []byte // 32
|
|
nullifier []byte // 32
|
|
//rk []byte // 32
|
|
cmx []byte // 32
|
|
ephemeralKey []byte // 32
|
|
encCiphertext []byte // 580
|
|
//outCiphertext []byte // 80
|
|
}
|
|
|
|
func (a *action) ParseFromSlice(data []byte) ([]byte, error) {
|
|
s := bytestring.String(data)
|
|
if !s.Skip(32) {
|
|
return nil, errors.New("could not read action cv")
|
|
}
|
|
if !s.ReadBytes(&a.nullifier, 32) {
|
|
return nil, errors.New("could not read action nullifier")
|
|
}
|
|
if !s.Skip(32) {
|
|
return nil, errors.New("could not read action rk")
|
|
}
|
|
if !s.ReadBytes(&a.cmx, 32) {
|
|
return nil, errors.New("could not read action cmx")
|
|
}
|
|
if !s.ReadBytes(&a.ephemeralKey, 32) {
|
|
return nil, errors.New("could not read action ephemeralKey")
|
|
}
|
|
if !s.ReadBytes(&a.encCiphertext, 580) {
|
|
return nil, errors.New("could not read action encCiphertext")
|
|
}
|
|
if !s.Skip(80) {
|
|
return nil, errors.New("could not read action outCiphertext")
|
|
}
|
|
return []byte(s), nil
|
|
}
|
|
|
|
func (p *action) ToCompact() *walletrpc.CompactOrchardAction {
|
|
return &walletrpc.CompactOrchardAction{
|
|
Nullifier: p.nullifier,
|
|
Cmx: p.cmx,
|
|
EphemeralKey: p.ephemeralKey,
|
|
Ciphertext: p.encCiphertext[:52],
|
|
}
|
|
}
|
|
|
|
// Transaction encodes a full (zcashd) transaction.
|
|
type Transaction struct {
|
|
*rawTransaction
|
|
rawBytes []byte
|
|
cachedTxID []byte // cached for performance
|
|
}
|
|
|
|
// GetDisplayHash returns the transaction hash in big-endian display order.
|
|
func (tx *Transaction) GetDisplayHash() []byte {
|
|
if tx.cachedTxID != nil {
|
|
return tx.cachedTxID
|
|
}
|
|
|
|
// SHA256d
|
|
digest := sha256.Sum256(tx.rawBytes)
|
|
digest = sha256.Sum256(digest[:])
|
|
// Convert to big-endian
|
|
tx.cachedTxID = Reverse(digest[:])
|
|
return tx.cachedTxID
|
|
}
|
|
|
|
// GetEncodableHash returns the transaction hash in little-endian wire format order.
|
|
func (tx *Transaction) GetEncodableHash() []byte {
|
|
digest := sha256.Sum256(tx.rawBytes)
|
|
digest = sha256.Sum256(digest[:])
|
|
return digest[:]
|
|
}
|
|
|
|
// Bytes returns a full transaction's raw bytes.
|
|
func (tx *Transaction) Bytes() []byte {
|
|
return tx.rawBytes
|
|
}
|
|
|
|
// HasShieldedElements indicates whether a transaction has
|
|
// at least one shielded input or output.
|
|
func (tx *Transaction) HasShieldedElements() bool {
|
|
nshielded := len(tx.shieldedSpends) + len(tx.shieldedOutputs) + len(tx.orchardActions)
|
|
return tx.version >= 4 && nshielded > 0
|
|
}
|
|
|
|
// ToCompact converts the given (full) transaction to compact format.
|
|
func (tx *Transaction) ToCompact(index int) *walletrpc.CompactTx {
|
|
ctx := &walletrpc.CompactTx{
|
|
Index: uint64(index), // index is contextual
|
|
Hash: tx.GetEncodableHash(),
|
|
//Fee: 0, // TODO: calculate fees
|
|
Spends: make([]*walletrpc.CompactSaplingSpend, len(tx.shieldedSpends)),
|
|
Outputs: make([]*walletrpc.CompactSaplingOutput, len(tx.shieldedOutputs)),
|
|
Actions: make([]*walletrpc.CompactOrchardAction, len(tx.orchardActions)),
|
|
}
|
|
for i, spend := range tx.shieldedSpends {
|
|
ctx.Spends[i] = spend.ToCompact()
|
|
}
|
|
for i, output := range tx.shieldedOutputs {
|
|
ctx.Outputs[i] = output.ToCompact()
|
|
}
|
|
for i, a := range tx.orchardActions {
|
|
ctx.Actions[i] = a.ToCompact()
|
|
}
|
|
return ctx
|
|
}
|
|
|
|
// parse version 4 transaction data after the nVersionGroupId field.
|
|
func (tx *Transaction) parseV4(data []byte) ([]byte, error) {
|
|
s := bytestring.String(data)
|
|
var err error
|
|
if tx.nVersionGroupID != 0x892F2085 {
|
|
return nil, errors.New(fmt.Sprintf("version group ID %x must be 0x892F2085", tx.nVersionGroupID))
|
|
}
|
|
s, err = tx.ParseTransparent([]byte(s))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !s.Skip(4) {
|
|
return nil, errors.New("could not skip nLockTime")
|
|
}
|
|
|
|
if !s.Skip(4) {
|
|
return nil, errors.New("could not skip nExpiryHeight")
|
|
}
|
|
|
|
var spendCount, outputCount int
|
|
|
|
if !s.Skip(8) {
|
|
return nil, errors.New("could not skip valueBalance")
|
|
}
|
|
if !s.ReadCompactSize(&spendCount) {
|
|
return nil, errors.New("could not read nShieldedSpend")
|
|
}
|
|
tx.shieldedSpends = make([]spend, spendCount)
|
|
for i := 0; i < spendCount; i++ {
|
|
newSpend := &tx.shieldedSpends[i]
|
|
s, err = newSpend.ParseFromSlice([]byte(s), 4)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "while parsing shielded Spend")
|
|
}
|
|
}
|
|
if !s.ReadCompactSize(&outputCount) {
|
|
return nil, errors.New("could not read nShieldedOutput")
|
|
}
|
|
tx.shieldedOutputs = make([]output, outputCount)
|
|
for i := 0; i < outputCount; i++ {
|
|
newOutput := &tx.shieldedOutputs[i]
|
|
s, err = newOutput.ParseFromSlice([]byte(s), 4)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "while parsing shielded Output")
|
|
}
|
|
}
|
|
var joinSplitCount int
|
|
if !s.ReadCompactSize(&joinSplitCount) {
|
|
return nil, errors.New("could not read nJoinSplit")
|
|
}
|
|
|
|
tx.joinSplits = make([]joinSplit, joinSplitCount)
|
|
if joinSplitCount > 0 {
|
|
for i := 0; i < joinSplitCount; i++ {
|
|
js := &tx.joinSplits[i]
|
|
s, err = js.ParseFromSlice([]byte(s))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "while parsing JoinSplit")
|
|
}
|
|
}
|
|
|
|
if !s.Skip(32) {
|
|
return nil, errors.New("could not skip joinSplitPubKey")
|
|
}
|
|
|
|
if !s.Skip(64) {
|
|
return nil, errors.New("could not skip joinSplitSig")
|
|
}
|
|
}
|
|
if spendCount+outputCount > 0 && !s.Skip(64) {
|
|
return nil, errors.New("could not skip bindingSigSapling")
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// parse version 5 transaction data after the nVersionGroupId field.
|
|
func (tx *Transaction) parseV5(data []byte) ([]byte, error) {
|
|
s := bytestring.String(data)
|
|
var err error
|
|
if !s.ReadUint32(&tx.consensusBranchID) {
|
|
return nil, errors.New("could not read nVersionGroupId")
|
|
}
|
|
if tx.nVersionGroupID != 0x26A7270A {
|
|
return nil, errors.New(fmt.Sprintf("version group ID %d must be 0x26A7270A", tx.nVersionGroupID))
|
|
}
|
|
if tx.consensusBranchID != 0x37519621 {
|
|
return nil, errors.New("unknown consensusBranchID")
|
|
}
|
|
if !s.Skip(4) {
|
|
return nil, errors.New("could not skip nLockTime")
|
|
}
|
|
if !s.Skip(4) {
|
|
return nil, errors.New("could not skip nExpiryHeight")
|
|
}
|
|
s, err = tx.ParseTransparent([]byte(s))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var spendCount, outputCount int
|
|
if !s.ReadCompactSize(&spendCount) {
|
|
return nil, errors.New("could not read nShieldedSpend")
|
|
}
|
|
if spendCount >= (1 << 16) {
|
|
return nil, errors.New(fmt.Sprintf("spentCount (%d) must be less than 2^16", spendCount))
|
|
}
|
|
tx.shieldedSpends = make([]spend, spendCount)
|
|
for i := 0; i < spendCount; i++ {
|
|
newSpend := &tx.shieldedSpends[i]
|
|
s, err = newSpend.ParseFromSlice([]byte(s), tx.version)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "while parsing shielded Spend")
|
|
}
|
|
}
|
|
if !s.ReadCompactSize(&outputCount) {
|
|
return nil, errors.New("could not read nShieldedOutput")
|
|
}
|
|
if outputCount >= (1 << 16) {
|
|
return nil, errors.New(fmt.Sprintf("outputCount (%d) must be less than 2^16", outputCount))
|
|
}
|
|
tx.shieldedOutputs = make([]output, outputCount)
|
|
for i := 0; i < outputCount; i++ {
|
|
newOutput := &tx.shieldedOutputs[i]
|
|
s, err = newOutput.ParseFromSlice([]byte(s), tx.version)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "while parsing shielded Output")
|
|
}
|
|
}
|
|
if spendCount+outputCount > 0 && !s.Skip(8) {
|
|
return nil, errors.New("could not read valueBalance")
|
|
}
|
|
if spendCount > 0 && !s.Skip(32) {
|
|
return nil, errors.New("could not skip anchorSapling")
|
|
}
|
|
if !s.Skip(192 * spendCount) {
|
|
return nil, errors.New("could not skip vSpendProofsSapling")
|
|
}
|
|
if !s.Skip(64 * spendCount) {
|
|
return nil, errors.New("could not skip vSpendAuthSigsSapling")
|
|
}
|
|
if !s.Skip(192 * outputCount) {
|
|
return nil, errors.New("could not skip vOutputProofsSapling")
|
|
}
|
|
if spendCount+outputCount > 0 && !s.Skip(64) {
|
|
return nil, errors.New("could not skip bindingSigSapling")
|
|
}
|
|
var actionsCount int
|
|
if !s.ReadCompactSize(&actionsCount) {
|
|
return nil, errors.New("could not read nActionsOrchard")
|
|
}
|
|
if actionsCount >= (1 << 16) {
|
|
return nil, errors.New(fmt.Sprintf("actionsCount (%d) must be less than 2^16", actionsCount))
|
|
}
|
|
tx.orchardActions = make([]action, actionsCount)
|
|
for i := 0; i < actionsCount; i++ {
|
|
a := &tx.orchardActions[i]
|
|
s, err = a.ParseFromSlice([]byte(s))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "while parsing orchard action")
|
|
}
|
|
}
|
|
if actionsCount > 0 {
|
|
if !s.Skip(1) {
|
|
return nil, errors.New("could not skip flagsOrchard")
|
|
}
|
|
if !s.Skip(8) {
|
|
return nil, errors.New("could not skip valueBalanceOrchard")
|
|
}
|
|
if !s.Skip(32) {
|
|
return nil, errors.New("could not skip anchorOrchard")
|
|
}
|
|
var proofsCount int
|
|
if !s.ReadCompactSize(&proofsCount) {
|
|
return nil, errors.New("could not read sizeProofsOrchard")
|
|
}
|
|
if !s.Skip(proofsCount) {
|
|
return nil, errors.New("could not skip proofsOrchard")
|
|
}
|
|
if !s.Skip(64 * actionsCount) {
|
|
return nil, errors.New("could not skip vSpendAuthSigsOrchard")
|
|
}
|
|
if !s.Skip(64) {
|
|
return nil, errors.New("could not skip bindingSigOrchard")
|
|
}
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// ParseFromSlice deserializes a single transaction from the given data.
|
|
func (tx *Transaction) ParseFromSlice(data []byte) ([]byte, error) {
|
|
s := bytestring.String(data)
|
|
|
|
// declare here to prevent shadowing problems in cryptobyte assignments
|
|
var err error
|
|
|
|
var header uint32
|
|
if !s.ReadUint32(&header) {
|
|
return nil, errors.New("could not read header")
|
|
}
|
|
|
|
tx.fOverwintered = (header >> 31) == 1
|
|
if !tx.fOverwintered {
|
|
return nil, errors.New("fOverwinter flag must be set")
|
|
}
|
|
tx.version = header & 0x7FFFFFFF
|
|
if tx.version < 4 {
|
|
return nil, errors.New(fmt.Sprintf("version number %d must be greater or equal to 4", tx.version))
|
|
}
|
|
|
|
if !s.ReadUint32(&tx.nVersionGroupID) {
|
|
return nil, errors.New("could not read nVersionGroupId")
|
|
}
|
|
// parse the main part of the transaction
|
|
if tx.version <= 4 {
|
|
s, err = tx.parseV4([]byte(s))
|
|
} else {
|
|
s, err = tx.parseV5([]byte(s))
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// TODO: implement rawBytes with MarshalBinary() instead
|
|
txLen := len(data) - len(s)
|
|
tx.rawBytes = data[:txLen]
|
|
|
|
return []byte(s), nil
|
|
}
|
|
|
|
// NewTransaction is the constructor for a full transaction.
|
|
func NewTransaction() *Transaction {
|
|
return &Transaction{
|
|
rawTransaction: new(rawTransaction),
|
|
}
|
|
}
|