Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ This monorepo contains a number of sdk's:
Official project releases may be found here: https://github.com/stellar/go-stellar-sdk/releases
## Pending

### Breaking Changes
* xdr: Regenerated from stellar-xdr@ee040cd6 with `CAP_0084_MUXED_CONTRACT` enabled. Contract spec UDT and event names are widened to `SC_SPEC_TYPE_NAME_LIMIT` (1024 bytes), and `ScSpecEventV0.Name` is now a `string` instead of an `ScSymbol`.

### New Features
* xdr: Add the CAP-0084 `ScAddressTypeScAddressTypeMuxedContract` arm of `ScAddress`. `ScAddress.String()` renders it as a `W...` muxed contract strkey.
* strkey: Add `VersionByteMuxedContract` (`W...`), `MuxedContract`, `DecodeMuxedContract`, and `IsValidMuxedContractAddress`.
* xdr: Added `LedgerCloseMetaView.LedgerHeader()`, exposing the version-resolving header accessor that already backs `LedgerSequence`, `LedgerCloseTime`, `LedgerHash`, and `PreviousLedgerHash` ([#5982](https://github.com/stellar/go-stellar-sdk/pull/5982))
* rpcclient: Add `Client.URL()` to expose the configured RPC server URL ([#5885](https://github.com/stellar/go-stellar-sdk/issues/5885))
* protocols/rpc: Add queryEvents wire types and request validation (`QueryEventsRequest`/`QueryEventsResponse`, `QueryEventsFilter`, scan statuses, typed `error.data` payloads), transcribed from the [accepted proposal](https://github.com/orgs/stellar/discussions/1872) ([#5971](https://github.com/stellar/go-stellar-sdk/pull/5971))
Expand Down
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ xdr/Stellar-exporter.x


XDRGEN_COMMIT=b423e1da9504239fb3136cbcc5f9beeb37795837
# Protocol 28 GA: stellar-xdr@9c9c1459, the commit stellar-core 28.0.0 pins as
# src/protocol-curr/xdr (v28.0 tag pending; CAP-0083 + CAP-0085 are ungated here).
XDR_COMMIT=9c9c145953e80990d6ff1ae3a6a973a0ce6d0694
# Protocol 30: stellar-xdr@ee040cd6, the commit stellar-core pins as src/protocol-curr/xdr
# (CAP-0084 gated behind CAP_0084_MUXED_CONTRACT, see XDR_FEATURES below).
XDR_COMMIT=ee040cd65310cd4f66d41fc74ebbbb3b6604dcc6

.PHONY: xdr xdr-clean xdr-update

Expand Down Expand Up @@ -69,7 +69,7 @@ xdr/xdr_generated.go: $(XDRS)

# Optional comma-separated features for #ifdef resolution in the XDR files.
# Empty = no features enabled; only unconditional definitions are emitted.
XDR_FEATURES ?=
XDR_FEATURES ?= CAP_0084_MUXED_CONTRACT

# Generates xdr/xdr_views_generated.go via a two-step pipeline:
# 1. The rust `generator-definitions-json` tool parses the .x files and
Expand Down
80 changes: 65 additions & 15 deletions gxdr/xdr_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions processors/token_transfer/contract_events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ var (

someContract2 = strkey.MustEncode(strkey.VersionByteContract, someContractId2[:])

someMuxedContract1 = strkey.MustEncode(strkey.VersionByteMuxedContract, append(someContractId1[:], 0, 0, 0, 0, 0, 0, 0, 7))

processor = &EventsProcessor{
networkPassphrase: someNetworkPassphrase,
}
Expand Down Expand Up @@ -60,6 +62,17 @@ func createAddress(address string) xdr.ScVal {
scAddress.Type = xdr.ScAddressTypeScAddressTypeAccount
scAddress.AccountId = xdr.MustAddressPtr(address)

case strkey.IsValidMuxedContractAddress(address) == true:
muxed, err := strkey.DecodeMuxedContract(address)
if err != nil {
panic(err)
}
scAddress.Type = xdr.ScAddressTypeScAddressTypeMuxedContract
scAddress.MuxedContract = &xdr.MuxedContract{
Id: xdr.Uint64(muxed.ID()),
ContractId: muxed.Contract(),
}

default:
panic(fmt.Errorf("unsupported address: %s", address))
}
Expand Down Expand Up @@ -1211,6 +1224,24 @@ func TestValidSep41EventsWithExtraTopicsAndDataV4(t *testing.T) {
assert.Equal(t, thousandStr, event.GetTransfer().Amount)
assert.Nil(t, event.Meta.GetToMuxedInfo())
},
}, {
name: "Transfer Event with muxed contract address in topics - Valid Sep-41 token",

@karthikiyer56 karthikiyer56 Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this tests one case but not the other.
This case has the W.... address in the to topic and a bare i128 in data.
Only a custom SEP-41 token emits that shape. The SAC never does.

Per CAP-0084 Events, a SAC transfer to a W... address looks like this:

topics: ["transfer", from: G..., to: C... (plain, mux stripped), "USDC:GA..."]
data:   { amount: 1000, to_muxed_id: 7 }

The processor already handles it (same path as M destinations) but no test pins it.

Can we add this case, plus a a test for mint as well?

{
    name: "SAC transfer to muxed contract: plain C in topic, to_muxed_id in data",
    setupEvent: func() xdr.ContractEvent {
        topics := []xdr.ScVal{
            createSymbol(TransferEvent),
            createAddress(randomAccount),              // from
            createAddress(someContract1),              // to: plain C, not W
            createString(usdcAsset.StringCanonical()), // sep0011 asset
        }
        data := createScMap("amount", createInt128(thousand), "to_muxed_id", uint64(7))
        return createContract(contractIdFromAsset(usdcAsset), topics, data)
    },
    validateEvent: func(t *testing.T, event *TokenTransferEvent) {
        assert.Equal(t, someContract1, event.GetTransfer().To)
        assert.EqualValues(t, 7, event.Meta.GetToMuxedInfo().GetId())
        assert.NotNil(t, event.GetTransfer().Asset)
    },
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added TestSacEventsToMuxedContractV4 in f981faf. It covers a SAC transfer and a SAC mint with the plain C... address in the topics and {amount, to_muxed_id: 7} as data, and checks To, Amount, the asset and ToMuxedInfo. The existing "V4 Transfer SAC Event - With Memo" case already had the transfer shape, but nothing tied it to CAP-0084, and mint was not covered.

setupEvent: func() xdr.ContractEvent {
topics := []xdr.ScVal{
createSymbol(TransferEvent),
createAddress(randomAccount), // from
createAddress(someMuxedContract1), // to
Comment on lines +1231 to +1233
}
data := createInt128(thousand)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/stellar/stellar-protocol/blob/master/core/cap-0084.md#events

to/from are not the W muxed contract address. It should just be the plain address
The data should be a map instead of i128

@chowbao chowbao Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah actually this is valid for a non-sac transfer

Maybe we should just add a test for sac transfer with muxed contract as linked in the cap above

Or should TTP intentionally convert to W address whenever possible?

Edit: nvm I don't think TTP should intentionally convert. We already accept M addresses the same way

return createContract(&someContractId1, topics, data)
},
validateEvent: func(t *testing.T, event *TokenTransferEvent) {
assert.NotNil(t, event.GetTransfer())
assert.Equal(t, randomAccount, event.GetTransfer().From)
assert.Equal(t, someMuxedContract1, event.GetTransfer().To)
assert.Equal(t, thousandStr, event.GetTransfer().Amount)
assert.Nil(t, event.Meta.GetToMuxedInfo())
},
},

{
Expand Down
2 changes: 1 addition & 1 deletion processors/token_transfer/verify_events.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type balanceKey struct {
// updateBalanceMap updates the map and removes the entry if the value becomes 0
func updateBalanceMap(m map[balanceKey]*big.Int, key balanceKey, delta *big.Int) {
// We dont include movement to/from contract address is balance delta tracking, since there is no standard way to derive/verify from contractData
if strkey.IsValidContractAddress(key.holder) {
if strkey.IsValidContractAddress(key.holder) || strkey.IsValidMuxedContractAddress(key.holder) {
return
}
if delta.Sign() == 0 {
Expand Down
17 changes: 17 additions & 0 deletions processors/token_transfer/verify_events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,23 @@ func TestFindBalanceDeltasFromEvents_Int64Amounts(t *testing.T) {
assert.Equal(t, big.NewInt(1000), deltas[toKey])
}

func TestFindBalanceDeltasFromEvents_SkipsContractHolders(t *testing.T) {
from := accountA.Address()
asset := xlmAsset
protoAsset := assetProto.NewProtoAsset(asset)

meta := &EventMeta{TxHash: "abc123"}
toContract := NewTransferEvent(meta, from, someContract1, "1000", protoAsset)
toMuxedContract := NewTransferEvent(meta, from, someMuxedContract1, "500", protoAsset)

deltas, err := findBalanceDeltasFromEvents([]*TokenTransferEvent{toContract, toMuxedContract})
require.NoError(t, err)

fromKey := balanceKey{holder: from, asset: asset.StringCanonical()}
assert.Equal(t, big.NewInt(-1500), deltas[fromKey])
assert.Len(t, deltas, 1)
}

func TestFindBalanceDeltasFromEvents_AmountExceedingInt64(t *testing.T) {
// This is the scenario from issue #5929: a SAC token balance that
// exceeded int64 max through cumulative mints, then was fully burned.
Expand Down
1 change: 1 addition & 0 deletions strkey/decode_length_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func TestDecodeRejectsNonCanonicalPayloadLength(t *testing.T) {
{"liquidity_pool", VersionByteLiquidityPool, 32},
{"claimable_balance", VersionByteClaimableBalance, 33},
{"muxed_account", VersionByteMuxedAccount, 40},
{"muxed_contract", VersionByteMuxedContract, 40},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
Expand Down
12 changes: 12 additions & 0 deletions strkey/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ func TestDecode(t *testing.T) {
0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
},
{
Name: "MuxedContract",
Address: "WA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAAAAAAAAAPCIA6IG",
ExpectedVersionByte: VersionByteMuxedContract,
ExpectedPayload: []byte{
0x36, 0x3e, 0xaa, 0x38, 0x67, 0x84, 0x1f, 0xba,
0xd0, 0xf4, 0xed, 0x88, 0xc7, 0x79, 0xe4, 0xfe,
0x66, 0xe5, 0x6a, 0x24, 0x70, 0xdc, 0x98, 0xc0,
0xec, 0x9c, 0x07, 0x3d, 0x05, 0xc7, 0xb1, 0x03,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xe2, 0x40,
},
},
{
Name: "Seed",
Address: "SBU2RRGLXH3E5CQHTD3ODLDF2BWDCYUSSBLLZ5GNW7JXHDIYKXZWHOKR",
Expand Down
Loading
Loading