wire: TransferableOut/In envelopes (AssetID + inner fx) — multi-asset XVMBaseTx

X-Chain is multi-asset, but XVMBaseTx.OutAt/InAt returned bare fx
TransferOutput/TransferInput with no AssetID. Add the reserved cross-fx
envelopes TransferableOut (ShapeKindTransferableOut=0x0B) and
TransferableIn (ShapeKindTransferableIn=0x0C) in wire/transferable.go,
mirroring wire/utxo.go's AssetID+Output layout, and rewire OutAt/InAt to
return them so callers recover the per-out/per-in AssetID (and, for
inputs, the spent UTXOID).

- TransferableOut: AssetID 32B @0, Output bytes @32 (size 40);
  TypeKindReserved + ShapeKindTransferableOut.
- TransferableIn: TxID 32B @0, OutputIndex u32 @32, AssetID 32B @36,
  Input bytes @68 (size 76); TypeKindReserved + ShapeKindTransferableIn.
- Both Wrap* reject trailing bytes (msg.Size()!=len) via ErrTrailingBytes,
  matching the proven node block/blockwire.go ErrExtraSpace canonical gate.
- NewXVMBaseTx unchanged (Outs/Ins are pre-built envelopes).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
zeekay
2026-07-11 23:13:51 -07:00
co-authored by Hanzo Dev
parent 1ef876c1ed
commit f106a1abb9
4 changed files with 531 additions and 12 deletions
+7
View File
@@ -86,6 +86,13 @@ var (
ErrWrongTypeKind = errors.New("wire: TypeKind discriminator does not match expected fx family")
ErrWrongShapeKind = errors.New("wire: ShapeKind discriminator does not match expected primitive shape")
ErrShortEnvelope = errors.New("wire: envelope shorter than 2-byte discriminator prefix")
// ErrTrailingBytes is returned when an envelope carries bytes beyond the
// self-delimiting ZAP message. zap.Parse truncates to the header size
// field, so a buffer with a trailing tail would wrap the same message
// but hash to a different id — a malleability vector. Canonical
// envelopes MUST consume every byte; mirrors the proven node-side
// block/blockwire.go ErrExtraSpace (msg.Size() != len(bytes)).
ErrTrailingBytes = errors.New("wire: trailing bytes after zap message (non-canonical envelope)")
)
// EnvelopePrefix is the 2-byte discriminator prefix that every fxs
+281
View File
@@ -0,0 +1,281 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wire
import (
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
// This file closes the multi-asset correctness gap in wire.XVMBaseTx.
// X-Chain is a universal multi-asset settlement layer, so an output/input
// on the wire is NOT a bare fx TransferOutput/TransferInput — it MUST name
// the asset it moves. TransferableOut and TransferableIn are the reserved
// cross-fx envelopes that bind an AssetID (and, for inputs, the spent
// UTXOID) to a concrete inner fx primitive.
//
// Both are cross-fx CONTAINERS: TypeKind is TypeKindReserved (0x00) — the
// envelope itself is not fx-owned; the owning fx TypeKind travels on the
// INNER Output/Input envelope's own discriminator prefix. This mirrors the
// UTXO envelope (wire/utxo.go), which is likewise reserved-TypeKind and
// carries an fx-typed Output.
// ---- TransferableOut (ShapeKindTransferableOut = 0x0B) ----
//
// TransferableOut binds an AssetID to a concrete spending Output. It is the
// output-list counterpart of UTXO's (AssetID, Output) tail — the same
// AssetID+Output layout, minus the UTXOID (TxID+OutputIndex), because a
// freshly-created output is not yet a spent-UTXO reference.
//
// The Output bytes field carries the inner fx primitive's wire envelope
// (2-byte discriminator prefix + ZAP message): a TransferOutput, MintOutput,
// or one of the NFT* output shapes. Consumers dispatch on the inner
// envelope's (TypeKind, ShapeKind) pair via OutputDiscriminator, then
// WrapTransferOutput / WrapMintOutput / WrapNFT*.
//
// Fixed-section layout (size 40 bytes):
//
// AssetID 32B @ 0
// Output bytes @ 32 (relOffset + length, 8 bytes)
//
// Wire prefix: TypeKind=0x00 (reserved), ShapeKind=0x0B (TransferableOut).
const (
OffsetTransferableOut_AssetID = 0 // 32B
OffsetTransferableOut_Output = 32 // bytes (relOffset + length, 8 bytes)
SizeTransferableOut = 40
)
// TransferableOut is the zero-copy typed accessor over a ZAP-encoded
// TransferableOut wire envelope.
//
// READ-ONLY: every field aliases the underlying ZAP buffer. Mutation
// corrupts any TxID = hash(buffer) computed downstream. Use
// append([]byte(nil), ...) to take ownership of OutputBytes when handing
// off to another goroutine.
type TransferableOut struct {
b []byte
msg *zap.Message
obj zap.Object
}
// AssetID returns the asset identifier this output moves.
func (t TransferableOut) AssetID() ids.ID {
var out ids.ID
for i := 0; i < 32; i++ {
out[i] = t.obj.Uint8(OffsetTransferableOut_AssetID + i)
}
return out
}
// OutputBytes returns the inner fx Output wire envelope (2-byte
// discriminator prefix + ZAP message). Dispatch on OutputDiscriminator,
// then WrapTransferOutput / WrapMintOutput / WrapNFT*.
//
// READ-ONLY: aliases the underlying buffer.
func (t TransferableOut) OutputBytes() []byte {
return t.obj.Bytes(OffsetTransferableOut_Output)
}
// OutputDiscriminator returns the (TypeKind, ShapeKind) pair embedded at the
// head of OutputBytes(). Returns (0, 0) when OutputBytes is shorter than the
// 2-byte prefix. This is the type accessor consumers dispatch on to pick the
// inner fx Wrap*.
func (t TransferableOut) OutputDiscriminator() (TypeKind, ShapeKind) {
b := t.OutputBytes()
if len(b) < EnvelopePrefix {
return 0, 0
}
return TypeKind(b[0]), ShapeKind(b[1])
}
// Bytes returns the full wire envelope (2-byte discriminator prefix + ZAP
// message). Stable across calls — backed by the originally-parsed buffer.
func (t TransferableOut) Bytes() []byte { return t.b }
// IsZero reports whether the accessor wraps a parsed message.
func (t TransferableOut) IsZero() bool { return t.msg == nil }
// WrapTransferableOut parses a TransferableOut wire envelope into a typed
// accessor.
//
// Returns ErrShortEnvelope when the buffer is shorter than the 2-byte
// discriminator prefix; ErrWrongShapeKind when the prefix names a
// non-TransferableOut shape; ErrTrailingBytes when the buffer carries bytes
// beyond the self-delimiting ZAP message (non-canonical).
func WrapTransferableOut(b []byte) (TransferableOut, error) {
_, sk, zapBytes, err := readEnvelopePrefix(b)
if err != nil {
return TransferableOut{}, err
}
if sk != ShapeKindTransferableOut {
return TransferableOut{}, ErrWrongShapeKind
}
msg, err := zap.Parse(zapBytes)
if err != nil {
return TransferableOut{}, err
}
if msg.Size() != len(zapBytes) {
return TransferableOut{}, ErrTrailingBytes
}
return TransferableOut{b: b, msg: msg, obj: msg.Root()}, nil
}
// NewTransferableOut builds a TransferableOut wire envelope from an asset id
// and an already-built inner fx Output envelope (from NewTransferOutput /
// NewMintOutput / NewNFT*). The inner envelope's bytes are stored verbatim
// in the Output field.
func NewTransferableOut(assetID ids.ID, innerEnvelope []byte) []byte {
capEstimate := zap.HeaderSize + SizeTransferableOut + len(innerEnvelope) + 64
b := zap.NewBuilder(capEstimate)
ob := b.StartObject(SizeTransferableOut)
for i := 0; i < 32; i++ {
ob.SetUint8(OffsetTransferableOut_AssetID+i, assetID[i])
}
ob.SetBytes(OffsetTransferableOut_Output, innerEnvelope)
ob.FinishAsRoot()
return writeEnvelopePrefix(TypeKindReserved, ShapeKindTransferableOut, b.Finish())
}
// ---- TransferableIn (ShapeKindTransferableIn = 0x0C) ----
//
// TransferableIn binds a UTXO reference (TxID + OutputIndex + AssetID) to a
// spending Input. Every consumed input on the wire must name the UTXO it
// spends AND the asset that UTXO holds — so an XVMBaseTx input is a
// TransferableIn (UTXOID + AssetID + inner fx Input), NOT a bare
// TransferInput.
//
// This mirrors UTXO's fixed-section layout exactly (TxID + OutputIndex +
// AssetID + inner-envelope pointer): a TransferableIn IS a reference to a
// UTXO plus the Input that unlocks it.
//
// The Input bytes field carries the inner fx primitive's wire envelope
// (2-byte discriminator prefix + ZAP message): a TransferInput. Consumers
// dispatch on the inner (TypeKind, ShapeKind) pair via InputDiscriminator,
// then WrapTransferInput.
//
// Fixed-section layout (size 76 bytes; uint32/uint64 reads alignment-tolerant):
//
// TxID 32B @ 0
// OutputIndex uint32 @ 32
// AssetID 32B @ 36
// Input bytes @ 68 (relOffset + length, 8 bytes)
//
// Wire prefix: TypeKind=0x00 (reserved), ShapeKind=0x0C (TransferableIn).
const (
OffsetTransferableIn_TxID = 0 // 32B
OffsetTransferableIn_OutputIndex = 32 // uint32
OffsetTransferableIn_AssetID = 36 // 32B
OffsetTransferableIn_Input = 68 // bytes (relOffset + length, 8 bytes)
SizeTransferableIn = 76
)
// TransferableIn is the zero-copy typed accessor over a ZAP-encoded
// TransferableIn wire envelope.
//
// READ-ONLY: every field aliases the underlying ZAP buffer. Use
// append([]byte(nil), ...) to take ownership of InputBytes when handing off
// to another goroutine.
type TransferableIn struct {
b []byte
msg *zap.Message
obj zap.Object
}
// TxID returns the spent UTXO's tx id.
func (t TransferableIn) TxID() ids.ID {
var out ids.ID
for i := 0; i < 32; i++ {
out[i] = t.obj.Uint8(OffsetTransferableIn_TxID + i)
}
return out
}
// OutputIndex returns the spent UTXO's output index.
func (t TransferableIn) OutputIndex() uint32 {
return t.obj.Uint32(OffsetTransferableIn_OutputIndex)
}
// AssetID returns the asset identifier the spent UTXO holds.
func (t TransferableIn) AssetID() ids.ID {
var out ids.ID
for i := 0; i < 32; i++ {
out[i] = t.obj.Uint8(OffsetTransferableIn_AssetID + i)
}
return out
}
// InputBytes returns the inner fx Input wire envelope (2-byte discriminator
// prefix + ZAP message). Dispatch on InputDiscriminator, then
// WrapTransferInput.
//
// READ-ONLY: aliases the underlying buffer.
func (t TransferableIn) InputBytes() []byte {
return t.obj.Bytes(OffsetTransferableIn_Input)
}
// InputDiscriminator returns the (TypeKind, ShapeKind) pair embedded at the
// head of InputBytes(). Returns (0, 0) when InputBytes is shorter than the
// 2-byte prefix. This is the type accessor consumers dispatch on to pick the
// inner fx Wrap*.
func (t TransferableIn) InputDiscriminator() (TypeKind, ShapeKind) {
b := t.InputBytes()
if len(b) < EnvelopePrefix {
return 0, 0
}
return TypeKind(b[0]), ShapeKind(b[1])
}
// Bytes returns the full wire envelope (2-byte discriminator prefix + ZAP
// message). Stable across calls — backed by the originally-parsed buffer.
func (t TransferableIn) Bytes() []byte { return t.b }
// IsZero reports whether the accessor wraps a parsed message.
func (t TransferableIn) IsZero() bool { return t.msg == nil }
// WrapTransferableIn parses a TransferableIn wire envelope into a typed
// accessor.
//
// Returns ErrShortEnvelope when the buffer is shorter than the 2-byte
// discriminator prefix; ErrWrongShapeKind when the prefix names a
// non-TransferableIn shape; ErrTrailingBytes when the buffer carries bytes
// beyond the self-delimiting ZAP message (non-canonical).
func WrapTransferableIn(b []byte) (TransferableIn, error) {
_, sk, zapBytes, err := readEnvelopePrefix(b)
if err != nil {
return TransferableIn{}, err
}
if sk != ShapeKindTransferableIn {
return TransferableIn{}, ErrWrongShapeKind
}
msg, err := zap.Parse(zapBytes)
if err != nil {
return TransferableIn{}, err
}
if msg.Size() != len(zapBytes) {
return TransferableIn{}, ErrTrailingBytes
}
return TransferableIn{b: b, msg: msg, obj: msg.Root()}, nil
}
// NewTransferableIn builds a TransferableIn wire envelope from a UTXO
// reference (txID + outputIndex + assetID) and an already-built inner fx
// Input envelope (from NewTransferInput). The inner envelope's bytes are
// stored verbatim in the Input field.
func NewTransferableIn(txID ids.ID, outputIndex uint32, assetID ids.ID, innerEnvelope []byte) []byte {
capEstimate := zap.HeaderSize + SizeTransferableIn + len(innerEnvelope) + 64
b := zap.NewBuilder(capEstimate)
ob := b.StartObject(SizeTransferableIn)
for i := 0; i < 32; i++ {
ob.SetUint8(OffsetTransferableIn_TxID+i, txID[i])
}
ob.SetUint32(OffsetTransferableIn_OutputIndex, outputIndex)
for i := 0; i < 32; i++ {
ob.SetUint8(OffsetTransferableIn_AssetID+i, assetID[i])
}
ob.SetBytes(OffsetTransferableIn_Input, innerEnvelope)
ob.FinishAsRoot()
return writeEnvelopePrefix(TypeKindReserved, ShapeKindTransferableIn, b.Finish())
}
+223
View File
@@ -0,0 +1,223 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wire
import (
"bytes"
"testing"
"github.com/luxfi/ids"
)
// innerSecp256k1TransferOutput builds the exact wire envelope that
// secp256k1fx.TransferOutput.Bytes() produces (TypeKindSecp256k1 +
// ShapeKindTransferOutput). Built via the wire constructor here to avoid the
// secp256k1fx -> wire import cycle in this in-package test.
func innerSecp256k1TransferOutput(amount uint64, addr ids.ShortID) []byte {
return NewTransferOutput(TransferOutputInput{
TypeKind: TypeKindSecp256k1,
Amount: amount,
Locktime: 0,
Threshold: 1,
Addresses: []ids.ShortID{addr},
})
}
// innerSecp256k1TransferInput mirrors secp256k1fx.TransferInput.Bytes().
func innerSecp256k1TransferInput(amount uint64, sigIndices []uint32) []byte {
return NewTransferInput(TransferInputInput{
TypeKind: TypeKindSecp256k1,
Amount: amount,
SigIndices: sigIndices,
})
}
func TestTransferableOut_RoundTrip(t *testing.T) {
addr := ids.ShortID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}
inner := innerSecp256k1TransferOutput(2_500_000, addr)
assetID := ids.ID{32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
envelope := NewTransferableOut(assetID, inner)
// Canonical: the envelope must consume every byte (no trailing tail).
if _, _, zapBytes, err := readEnvelopePrefix(envelope); err != nil {
t.Fatalf("readEnvelopePrefix: %v", err)
} else if len(zapBytes) == 0 {
t.Fatal("empty zap body")
}
got, err := WrapTransferableOut(envelope)
if err != nil {
t.Fatalf("WrapTransferableOut: %v", err)
}
if got.AssetID() != assetID {
t.Errorf("AssetID: got %x, want %x", got.AssetID(), assetID)
}
outBytes := got.OutputBytes()
if !bytes.Equal(outBytes, inner) {
t.Errorf("OutputBytes mismatch: got %x, want %x", outBytes, inner)
}
tk, sk := got.OutputDiscriminator()
if tk != TypeKindSecp256k1 {
t.Errorf("OutputDiscriminator TypeKind: got %x, want %x", tk, TypeKindSecp256k1)
}
if sk != ShapeKindTransferOutput {
t.Errorf("OutputDiscriminator ShapeKind: got %x, want %x", sk, ShapeKindTransferOutput)
}
// Round-trip the inner secp256k1fx output.
innerGot, err := WrapTransferOutput(outBytes)
if err != nil {
t.Fatalf("WrapTransferOutput inner: %v", err)
}
if innerGot.Amount() != 2_500_000 {
t.Errorf("inner Amount: got %d, want 2_500_000", innerGot.Amount())
}
if innerGot.TypeKind() != TypeKindSecp256k1 {
t.Errorf("inner TypeKind: got %x, want %x", innerGot.TypeKind(), TypeKindSecp256k1)
}
// Wrong-shape rejection: a TransferableOut buffer must not Wrap as
// TransferableIn.
if _, err := WrapTransferableIn(envelope); err != ErrWrongShapeKind {
t.Errorf("WrapTransferableIn(out envelope): got %v, want ErrWrongShapeKind", err)
}
// Canonical rejection: a trailing byte must be refused.
tampered := append(append([]byte(nil), envelope...), 0xFF)
if _, err := WrapTransferableOut(tampered); err != ErrTrailingBytes {
t.Errorf("WrapTransferableOut(trailing): got %v, want ErrTrailingBytes", err)
}
}
func TestTransferableIn_RoundTrip(t *testing.T) {
inner := innerSecp256k1TransferInput(2_500_000, []uint32{0, 3})
txID := ids.ID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}
assetID := ids.ID{32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
envelope := NewTransferableIn(txID, 7, assetID, inner)
got, err := WrapTransferableIn(envelope)
if err != nil {
t.Fatalf("WrapTransferableIn: %v", err)
}
if got.TxID() != txID {
t.Errorf("TxID: got %x, want %x", got.TxID(), txID)
}
if got.OutputIndex() != 7 {
t.Errorf("OutputIndex: got %d, want 7", got.OutputIndex())
}
if got.AssetID() != assetID {
t.Errorf("AssetID: got %x, want %x", got.AssetID(), assetID)
}
inBytes := got.InputBytes()
if !bytes.Equal(inBytes, inner) {
t.Errorf("InputBytes mismatch: got %x, want %x", inBytes, inner)
}
tk, sk := got.InputDiscriminator()
if tk != TypeKindSecp256k1 {
t.Errorf("InputDiscriminator TypeKind: got %x, want %x", tk, TypeKindSecp256k1)
}
if sk != ShapeKindTransferInput {
t.Errorf("InputDiscriminator ShapeKind: got %x, want %x", sk, ShapeKindTransferInput)
}
// Round-trip the inner secp256k1fx input.
innerGot, err := WrapTransferInput(inBytes)
if err != nil {
t.Fatalf("WrapTransferInput inner: %v", err)
}
if innerGot.Amount() != 2_500_000 {
t.Errorf("inner Amount: got %d, want 2_500_000", innerGot.Amount())
}
sigs := innerGot.SigIndices()
if len(sigs) != 2 || sigs[0] != 0 || sigs[1] != 3 {
t.Errorf("inner SigIndices: got %v, want [0 3]", sigs)
}
// Wrong-shape + canonical rejection.
if _, err := WrapTransferableOut(envelope); err != ErrWrongShapeKind {
t.Errorf("WrapTransferableOut(in envelope): got %v, want ErrWrongShapeKind", err)
}
tampered := append(append([]byte(nil), envelope...), 0xFF)
if _, err := WrapTransferableIn(tampered); err != ErrTrailingBytes {
t.Errorf("WrapTransferableIn(trailing): got %v, want ErrTrailingBytes", err)
}
}
// TestXVMBaseTx_MultiAsset is the end-to-end proof that the multi-asset gap is
// closed: an XVMBaseTx built from TransferableOut/TransferableIn envelopes
// yields per-out/per-in AssetIDs via OutAt/InAt.
func TestXVMBaseTx_MultiAsset(t *testing.T) {
addr := ids.ShortID{9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9}
assetA := ids.ID{0xA1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}
assetB := ids.ID{0xB2, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}
txID := ids.ID{7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7}
out0 := NewTransferableOut(assetA, innerSecp256k1TransferOutput(1_000, addr))
out1 := NewTransferableOut(assetB, innerSecp256k1TransferOutput(2_000, addr))
in0 := NewTransferableIn(txID, 0, assetA, innerSecp256k1TransferInput(1_000, []uint32{0}))
in1 := NewTransferableIn(txID, 1, assetB, innerSecp256k1TransferInput(2_000, []uint32{0}))
envelope := NewXVMBaseTx(XVMBaseTxInput{
NetworkID: 96369,
BlockchainID: [32]byte{1, 2, 3},
Outs: [][]byte{out0, out1},
Ins: [][]byte{in0, in1},
Memo: []byte("multi-asset"),
})
tx, err := WrapXVMBaseTx(envelope)
if err != nil {
t.Fatalf("WrapXVMBaseTx: %v", err)
}
if tx.OutsCount() != 2 {
t.Fatalf("OutsCount: got %d, want 2", tx.OutsCount())
}
if tx.InsCount() != 2 {
t.Fatalf("InsCount: got %d, want 2", tx.InsCount())
}
// Outputs carry their AssetIDs (the gap that OutAt previously dropped).
gotOut0, err := tx.OutAt(0)
if err != nil {
t.Fatalf("OutAt(0): %v", err)
}
if gotOut0.AssetID() != assetA {
t.Errorf("OutAt(0).AssetID: got %x, want %x", gotOut0.AssetID(), assetA)
}
gotOut1, err := tx.OutAt(1)
if err != nil {
t.Fatalf("OutAt(1): %v", err)
}
if gotOut1.AssetID() != assetB {
t.Errorf("OutAt(1).AssetID: got %x, want %x", gotOut1.AssetID(), assetB)
}
inner1, err := WrapTransferOutput(gotOut1.OutputBytes())
if err != nil {
t.Fatalf("inner WrapTransferOutput: %v", err)
}
if inner1.Amount() != 2_000 {
t.Errorf("OutAt(1) inner Amount: got %d, want 2000", inner1.Amount())
}
// Inputs carry their UTXOID + AssetID (the gap that InAt previously dropped).
gotIn0, err := tx.InAt(0)
if err != nil {
t.Fatalf("InAt(0): %v", err)
}
if gotIn0.AssetID() != assetA || gotIn0.TxID() != txID || gotIn0.OutputIndex() != 0 {
t.Errorf("InAt(0): got (asset=%x, tx=%x, idx=%d)", gotIn0.AssetID(), gotIn0.TxID(), gotIn0.OutputIndex())
}
gotIn1, err := tx.InAt(1)
if err != nil {
t.Fatalf("InAt(1): %v", err)
}
if gotIn1.AssetID() != assetB || gotIn1.OutputIndex() != 1 {
t.Errorf("InAt(1): got (asset=%x, idx=%d), want (assetB, 1)", gotIn1.AssetID(), gotIn1.OutputIndex())
}
if !bytes.Equal(tx.Memo(), []byte("multi-asset")) {
t.Errorf("Memo: got %q, want %q", tx.Memo(), "multi-asset")
}
}
+20 -12
View File
@@ -14,10 +14,14 @@ import "github.com/luxfi/zap"
//
// NetworkID uint32 — network this chain lives on
// BlockchainID 32B — chain id (replay-protection)
// Outs []TransferableOutput (each carries its own fx TypeKind)
// Ins []TransferableInput (each carries its own fx TypeKind)
// Outs []TransferableOut (AssetID + inner fx Output)
// Ins []TransferableIn (UTXOID + AssetID + inner fx Input)
// Memo bytes — arbitrary, up to MaxMemoSize on the verifier
//
// X-Chain is multi-asset: each out/in is a TransferableOut / TransferableIn
// envelope that NAMES the asset it moves (the inner fx Output/Input carries
// the owning fx TypeKind). See wire/transferable.go.
//
// Outs/Ins are variable-stride byte envelopes (each carries its own
// TypeKind+ShapeKind+ZAP message of independent length). The list is
// encoded as `Count uint32 + Bytes (concatenated envelopes)` — the same
@@ -75,7 +79,7 @@ func (t XVMBaseTx) OutsCount() uint32 {
return t.obj.Uint32(OffsetXVMBaseTx_OutsCount)
}
// OutsBytes returns the concatenated TransferableOutput envelopes blob.
// OutsBytes returns the concatenated TransferableOut envelopes blob.
// Each entry is a self-describing wire envelope; see OutAt for the
// index walk.
//
@@ -84,13 +88,15 @@ func (t XVMBaseTx) OutsBytes() []byte {
return t.obj.Bytes(OffsetXVMBaseTx_OutsBytes)
}
// OutAt parses the i'th TransferableOutput envelope.
func (t XVMBaseTx) OutAt(i uint32) (TransferOutput, error) {
// OutAt parses the i'th TransferableOut envelope (AssetID + inner fx
// Output). Callers get the AssetID via the returned accessor — X-Chain is
// multi-asset, so every output names the asset it moves.
func (t XVMBaseTx) OutAt(i uint32) (TransferableOut, error) {
env, err := nthEnvelope(t.OutsBytes(), t.OutsCount(), i)
if err != nil {
return TransferOutput{}, err
return TransferableOut{}, err
}
return WrapTransferOutput(env)
return WrapTransferableOut(env)
}
// InsCount returns the number of transferable inputs.
@@ -98,20 +104,22 @@ func (t XVMBaseTx) InsCount() uint32 {
return t.obj.Uint32(OffsetXVMBaseTx_InsCount)
}
// InsBytes returns the concatenated TransferableInput envelopes blob.
// InsBytes returns the concatenated TransferableIn envelopes blob.
//
// READ-ONLY: aliases the underlying buffer.
func (t XVMBaseTx) InsBytes() []byte {
return t.obj.Bytes(OffsetXVMBaseTx_InsBytes)
}
// InAt parses the i'th TransferableInput envelope.
func (t XVMBaseTx) InAt(i uint32) (TransferInput, error) {
// InAt parses the i'th TransferableIn envelope (UTXOID + AssetID + inner fx
// Input). Callers get the spent UTXO reference + AssetID via the returned
// accessor.
func (t XVMBaseTx) InAt(i uint32) (TransferableIn, error) {
env, err := nthEnvelope(t.InsBytes(), t.InsCount(), i)
if err != nil {
return TransferInput{}, err
return TransferableIn{}, err
}
return WrapTransferInput(env)
return WrapTransferableIn(env)
}
// Memo returns the memo bytes.