diff --git a/chain/substrate/alpha.go b/chain/substrate/alpha.go new file mode 100644 index 00000000..ce0991b5 --- /dev/null +++ b/chain/substrate/alpha.go @@ -0,0 +1,22 @@ +package substrate + +import ( + "fmt" + "strconv" + "strings" +) + +// ParseAlphaContract parses an alpha token contract address. +// The contract is just the netuid as a string (e.g., "64"). +// Returns the parsed netuid. +func ParseAlphaContract(contract string) (netuid uint16, err error) { + contract = strings.TrimSpace(contract) + if contract == "" { + return 0, fmt.Errorf("invalid alpha contract: empty string") + } + netuidVal, err := strconv.ParseUint(contract, 10, 16) + if err != nil { + return 0, fmt.Errorf("invalid alpha contract, expected a subnet netuid (uint16): %s", contract) + } + return uint16(netuidVal), nil +} diff --git a/chain/substrate/alpha_test.go b/chain/substrate/alpha_test.go new file mode 100644 index 00000000..27a9d842 --- /dev/null +++ b/chain/substrate/alpha_test.go @@ -0,0 +1,49 @@ +package substrate + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseAlphaContract(t *testing.T) { + require := require.New(t) + + netuid, err := ParseAlphaContract("18") + require.NoError(err) + require.Equal(uint16(18), netuid) + + netuid, err = ParseAlphaContract("0") + require.NoError(err) + require.Equal(uint16(0), netuid) + + netuid, err = ParseAlphaContract("65535") + require.NoError(err) + require.Equal(uint16(65535), netuid) + + // with whitespace + netuid, err = ParseAlphaContract(" 64 ") + require.NoError(err) + require.Equal(uint16(64), netuid) + + // empty + _, err = ParseAlphaContract("") + require.Error(err) + + // not a number + _, err = ParseAlphaContract("abc") + require.Error(err) + require.Contains(err.Error(), "expected a subnet netuid") + + // overflow + _, err = ParseAlphaContract("70000") + require.Error(err) + + // negative + _, err = ParseAlphaContract("-1") + require.Error(err) + + // old hotkey/netuid format should fail + _, err = ParseAlphaContract("5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty/18") + require.Error(err) +} diff --git a/chain/substrate/builder/alpha_tao.go b/chain/substrate/builder/alpha_tao.go new file mode 100644 index 00000000..9cab1ba2 --- /dev/null +++ b/chain/substrate/builder/alpha_tao.go @@ -0,0 +1,167 @@ +package builder + +import ( + "fmt" + + "github.com/centrifuge/go-substrate-rpc-client/v4/types" + "github.com/centrifuge/go-substrate-rpc-client/v4/types/codec" + "github.com/centrifuge/go-substrate-rpc-client/v4/types/extrinsic" + xc "github.com/cordialsys/crosschain" + xcbuilder "github.com/cordialsys/crosschain/builder" + "github.com/cordialsys/crosschain/chain/substrate" + "github.com/cordialsys/crosschain/chain/substrate/address" + "github.com/cordialsys/crosschain/chain/substrate/tx" + "github.com/cordialsys/crosschain/chain/substrate/tx_input" +) + +// TaoAlphaBuilder handles alpha token transfer building for Bittensor (TAO) chain +type TaoAlphaBuilder struct { + txBuilder *TxBuilder +} + +// NewTaoAlphaBuilder creates a new TAO alpha token builder +func NewTaoAlphaBuilder(txBuilder *TxBuilder) *TaoAlphaBuilder { + return &TaoAlphaBuilder{ + txBuilder: txBuilder, + } +} + +// TransferStake builds one or more SubtensorModule.transfer_stake extrinsics to transfer +// alpha tokens from one coldkey to another. Positions are selected UTXO-style from the +// sender's alpha holdings across hotkeys on the target subnet. +func (b *TaoAlphaBuilder) TransferStake(args xcbuilder.TransferArgs, input xc.TxInput) (xc.Tx, error) { + txInput := input.(*tx_input.TxInput) + + contract, ok := args.GetContract() + if !ok { + return nil, fmt.Errorf("contract address required for alpha transfer") + } + + netuid, err := substrate.ParseAlphaContract(contract) + if err != nil { + return nil, err + } + + sender, err := address.DecodeMulti(args.GetFrom()) + if err != nil { + return nil, fmt.Errorf("invalid sender address: %v", err) + } + + destinationColdkey, err := address.Decode(args.GetTo()) + if err != nil { + return nil, fmt.Errorf("invalid destination address: %v", err) + } + + if len(txInput.AlphaPositions) == 0 { + return nil, fmt.Errorf("no alpha positions available on subnet %d", netuid) + } + + transferAmount := args.GetAmount().Uint64() + if transferAmount == 0 { + return nil, fmt.Errorf("transfer amount must be greater than zero") + } + + // Select positions UTXO-style (positions are pre-sorted descending by amount) + calls, err := b.selectAndBuildCalls(txInput, destinationColdkey, netuid, transferAmount) + if err != nil { + return nil, err + } + + var call types.Call + if len(calls) == 1 { + // Single position covers the transfer — no batching needed + call = calls[0] + } else { + // Multiple positions needed — wrap in Utility.batch_all + call, err = b.buildBatchCall(txInput, calls) + if err != nil { + return nil, err + } + } + + return tx.NewTx(extrinsic.NewDynamicExtrinsic(&call), sender, txInput.Tip, txInput) +} + +// selectAndBuildCalls picks alpha positions to cover the transfer amount and builds +// individual transfer_stake calls for each. +func (b *TaoAlphaBuilder) selectAndBuildCalls( + txInput *tx_input.TxInput, + destinationColdkey *types.AccountID, + netuid uint16, + remaining uint64, +) ([]types.Call, error) { + originNetuid := types.NewU16(netuid) + destinationNetuid := types.NewU16(netuid) + + var calls []types.Call + for _, pos := range txInput.AlphaPositions { + if remaining == 0 { + break + } + + hotkey, err := address.Decode(xc.Address(pos.Hotkey)) + if err != nil { + return nil, fmt.Errorf("invalid hotkey address in position: %v", err) + } + + spend := pos.Amount + if spend > remaining { + spend = remaining + } + + call, err := tx_input.NewCall( + &txInput.Meta, + "SubtensorModule.transfer_stake", + destinationColdkey, + hotkey, + originNetuid, + destinationNetuid, + types.NewU64(spend), + ) + if err != nil { + return nil, err + } + + calls = append(calls, call) + remaining -= spend + } + + if remaining > 0 { + return nil, fmt.Errorf("insufficient alpha balance: still need %d more", remaining) + } + + return calls, nil +} + +// buildBatchCall wraps multiple calls into a Utility.batch_all call. +// batch_all takes a Vec, encoded as a SCALE compact-length-prefixed array. +func (b *TaoAlphaBuilder) buildBatchCall(txInput *tx_input.TxInput, calls []types.Call) (types.Call, error) { + // Encode each call individually, then build the Vec encoding + var encodedCalls []byte + + // SCALE Vec prefix: compact-encoded length + lenPrefix, err := codec.Encode(types.NewUCompactFromUInt(uint64(len(calls)))) + if err != nil { + return types.Call{}, fmt.Errorf("failed to encode batch length: %v", err) + } + encodedCalls = append(encodedCalls, lenPrefix...) + + for _, call := range calls { + encoded, err := codec.Encode(call) + if err != nil { + return types.Call{}, fmt.Errorf("failed to encode call for batch: %v", err) + } + encodedCalls = append(encodedCalls, encoded...) + } + + // Look up the Utility.batch_all call index + callIndex, err := txInput.Meta.FindCallIndex("Utility.batch_all") + if err != nil { + return types.Call{}, err + } + + return types.Call{ + CallIndex: callIndex, + Args: encodedCalls, + }, nil +} diff --git a/chain/substrate/builder/alpha_tao_test.go b/chain/substrate/builder/alpha_tao_test.go new file mode 100644 index 00000000..91acd1b4 --- /dev/null +++ b/chain/substrate/builder/alpha_tao_test.go @@ -0,0 +1,184 @@ +package builder_test + +import ( + "encoding/json" + "testing" + + xc "github.com/cordialsys/crosschain" + "github.com/cordialsys/crosschain/builder/buildertest" + "github.com/cordialsys/crosschain/chain/substrate/builder" + "github.com/cordialsys/crosschain/chain/substrate/tx_input" + "github.com/stretchr/testify/require" +) + +func taoAlphaInput(t *testing.T, positions []tx_input.AlphaPosition) *tx_input.TxInput { + t.Helper() + inputBz := `{ + "type":"substrate", + "meta":{"calls":[ + {"name":"SubtensorModule.transfer_stake","section":7,"method":5}, + {"name":"Utility.batch_all","section":24,"method":2} + ],"signed_extensions":["CheckNonZeroSender","CheckSpecVersion","CheckTxVersion","CheckGenesis","CheckMortality","CheckNonce","CheckWeight","ChargeTransactionPayment","SubtensorSignedExtension"]}, + "genesis_hash":"0x2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03", + "current_hash":"0xaabbccdd00000000000000000000000000000000000000000000000000000000", + "runtime_version":{"apis":[],"authoringVersion":0,"implName":"node-subtensor","implVersion":0,"specName":"node-subtensor","specVersion":100,"transactionVersion":1}, + "current_height":1000, + "account_nonce":5, + "tip":0 + }` + input := &tx_input.TxInput{} + err := json.Unmarshal([]byte(inputBz), input) + require.NoError(t, err) + input.AlphaPositions = positions + return input +} + +func TestAlphaTransferSinglePosition(t *testing.T) { + require := require.New(t) + + b, err := builder.NewTxBuilder(xc.NewChainConfig(xc.TAO).WithDecimals(9).Base()) + require.NoError(err) + + from := xc.Address("5GL7deqCmoKpgmhq3b12DXSAu62VQ3DCqN3Z7Bet6fx9qAyb") + to := xc.Address("5FUh5YJztrDvQe58YcDr5rDhkx1kSZcxQFu81wamrPuVyZSW") + amount := xc.NewAmountBlockchainFromUint64(500000000) + + input := taoAlphaInput(t, []tx_input.AlphaPosition{ + {Hotkey: "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", Amount: 1000000000}, + }) + + chainCfg := xc.NewChainConfig(xc.TAO).WithDecimals(9).Base() + args := buildertest.MustNewTransferArgs(chainCfg, from, to, amount, buildertest.OptionContractAddress("18")) + + tx, err := b.Transfer(args, input) + require.NoError(err) + require.NotNil(tx) +} + +func TestAlphaTransferMultiplePositions(t *testing.T) { + require := require.New(t) + + b, err := builder.NewTxBuilder(xc.NewChainConfig(xc.TAO).WithDecimals(9).Base()) + require.NoError(err) + + from := xc.Address("5GL7deqCmoKpgmhq3b12DXSAu62VQ3DCqN3Z7Bet6fx9qAyb") + to := xc.Address("5FUh5YJztrDvQe58YcDr5rDhkx1kSZcxQFu81wamrPuVyZSW") + // Amount that spans both positions + amount := xc.NewAmountBlockchainFromUint64(1500000000) + + input := taoAlphaInput(t, []tx_input.AlphaPosition{ + {Hotkey: "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", Amount: 1000000000}, + {Hotkey: "5GL7deqCmoKpgmhq3b12DXSAu62VQ3DCqN3Z7Bet6fx9qAyb", Amount: 800000000}, + }) + + chainCfg := xc.NewChainConfig(xc.TAO).WithDecimals(9).Base() + args := buildertest.MustNewTransferArgs(chainCfg, from, to, amount, buildertest.OptionContractAddress("18")) + + // Should produce a batched transaction + tx, err := b.Transfer(args, input) + require.NoError(err) + require.NotNil(tx) +} + +func TestAlphaTransferExactAmount(t *testing.T) { + require := require.New(t) + + b, err := builder.NewTxBuilder(xc.NewChainConfig(xc.TAO).WithDecimals(9).Base()) + require.NoError(err) + + from := xc.Address("5GL7deqCmoKpgmhq3b12DXSAu62VQ3DCqN3Z7Bet6fx9qAyb") + to := xc.Address("5FUh5YJztrDvQe58YcDr5rDhkx1kSZcxQFu81wamrPuVyZSW") + amount := xc.NewAmountBlockchainFromUint64(1000000000) + + input := taoAlphaInput(t, []tx_input.AlphaPosition{ + {Hotkey: "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", Amount: 1000000000}, + }) + + chainCfg := xc.NewChainConfig(xc.TAO).WithDecimals(9).Base() + args := buildertest.MustNewTransferArgs(chainCfg, from, to, amount, buildertest.OptionContractAddress("18")) + + tx, err := b.Transfer(args, input) + require.NoError(err) + require.NotNil(tx) +} + +func TestAlphaTransferInsufficientBalance(t *testing.T) { + require := require.New(t) + + b, err := builder.NewTxBuilder(xc.NewChainConfig(xc.TAO).WithDecimals(9).Base()) + require.NoError(err) + + from := xc.Address("5GL7deqCmoKpgmhq3b12DXSAu62VQ3DCqN3Z7Bet6fx9qAyb") + to := xc.Address("5FUh5YJztrDvQe58YcDr5rDhkx1kSZcxQFu81wamrPuVyZSW") + amount := xc.NewAmountBlockchainFromUint64(2000000000) + + input := taoAlphaInput(t, []tx_input.AlphaPosition{ + {Hotkey: "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", Amount: 500000000}, + }) + + chainCfg := xc.NewChainConfig(xc.TAO).WithDecimals(9).Base() + args := buildertest.MustNewTransferArgs(chainCfg, from, to, amount, buildertest.OptionContractAddress("18")) + + _, err = b.Transfer(args, input) + require.Error(err) + require.Contains(err.Error(), "insufficient alpha balance") +} + +func TestAlphaTransferNoPositions(t *testing.T) { + require := require.New(t) + + b, err := builder.NewTxBuilder(xc.NewChainConfig(xc.TAO).WithDecimals(9).Base()) + require.NoError(err) + + from := xc.Address("5GL7deqCmoKpgmhq3b12DXSAu62VQ3DCqN3Z7Bet6fx9qAyb") + to := xc.Address("5FUh5YJztrDvQe58YcDr5rDhkx1kSZcxQFu81wamrPuVyZSW") + amount := xc.NewAmountBlockchainFromUint64(1000000000) + + input := taoAlphaInput(t, nil) + + chainCfg := xc.NewChainConfig(xc.TAO).WithDecimals(9).Base() + args := buildertest.MustNewTransferArgs(chainCfg, from, to, amount, buildertest.OptionContractAddress("18")) + + _, err = b.Transfer(args, input) + require.Error(err) + require.Contains(err.Error(), "no alpha positions") +} + +func TestAlphaTransferInvalidContract(t *testing.T) { + require := require.New(t) + + b, err := builder.NewTxBuilder(xc.NewChainConfig(xc.TAO).WithDecimals(9).Base()) + require.NoError(err) + + from := xc.Address("5GL7deqCmoKpgmhq3b12DXSAu62VQ3DCqN3Z7Bet6fx9qAyb") + to := xc.Address("5FUh5YJztrDvQe58YcDr5rDhkx1kSZcxQFu81wamrPuVyZSW") + amount := xc.NewAmountBlockchainFromUint64(1000000000) + + input := taoAlphaInput(t, nil) + + chainCfg := xc.NewChainConfig(xc.TAO).WithDecimals(9).Base() + args := buildertest.MustNewTransferArgs(chainCfg, from, to, amount, buildertest.OptionContractAddress("invalid")) + + _, err = b.Transfer(args, input) + require.Error(err) + require.Contains(err.Error(), "expected a subnet netuid") +} + +func TestNonTaoTokenTransferRejected(t *testing.T) { + require := require.New(t) + + b, err := builder.NewTxBuilder(xc.NewChainConfig(xc.DOT).Base()) + require.NoError(err) + + from := xc.Address("5GL7deqCmoKpgmhq3b12DXSAu62VQ3DCqN3Z7Bet6fx9qAyb") + to := xc.Address("5FUh5YJztrDvQe58YcDr5rDhkx1kSZcxQFu81wamrPuVyZSW") + amount := xc.NewAmountBlockchainFromUint64(1000000000) + + chainCfg := xc.NewChainConfig(xc.DOT).Base() + args := buildertest.MustNewTransferArgs(chainCfg, from, to, amount, buildertest.OptionContractAddress("some-contract")) + + input := &tx_input.TxInput{} + _, err = b.Transfer(args, input) + require.Error(err) + require.Contains(err.Error(), "token transfers not supported on substrate") +} diff --git a/chain/substrate/builder/builder.go b/chain/substrate/builder/builder.go index dd9094b6..32aa193a 100644 --- a/chain/substrate/builder/builder.go +++ b/chain/substrate/builder/builder.go @@ -30,8 +30,11 @@ func NewTxBuilder(cfgI *xc.ChainBaseConfig) (TxBuilder, error) { // NewTransfer creates a new transfer for an Asset, either native or token func (txBuilder TxBuilder) Transfer(args xcbuilder.TransferArgs, input xc.TxInput) (xc.Tx, error) { - if _, ok := args.GetContract(); ok { - return nil, fmt.Errorf("token transfers not supported on substrate") + if contract, ok := args.GetContract(); ok { + if txBuilder.Asset.Chain == xc.TAO { + return NewTaoAlphaBuilder(&txBuilder).TransferStake(args, input) + } + return nil, fmt.Errorf("token transfers not supported on substrate: %s", contract) } txInput := input.(*tx_input.TxInput) diff --git a/chain/substrate/client/alpha_tao.go b/chain/substrate/client/alpha_tao.go new file mode 100644 index 00000000..8fa8bc49 --- /dev/null +++ b/chain/substrate/client/alpha_tao.go @@ -0,0 +1,148 @@ +package client + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + "sort" + + "github.com/centrifuge/go-substrate-rpc-client/v4/scale" + "github.com/centrifuge/go-substrate-rpc-client/v4/types" + xc "github.com/cordialsys/crosschain" + "github.com/cordialsys/crosschain/chain/substrate" + "github.com/cordialsys/crosschain/chain/substrate/address" + "github.com/cordialsys/crosschain/chain/substrate/tx_input" +) + +// FetchAlphaBalance queries all alpha token positions for a coldkey on a given subnet +// and returns the total balance summed across all hotkeys. +func (client *Client) FetchAlphaBalance(ctx context.Context, addr xc.Address, contract string) (xc.AmountBlockchain, error) { + zero := xc.NewAmountBlockchainFromUint64(0) + + netuid, err := substrate.ParseAlphaContract(contract) + if err != nil { + return zero, err + } + + positions, err := client.fetchAlphaPositions(addr, netuid) + if err != nil { + return zero, err + } + + var total uint64 + for _, pos := range positions { + total += pos.Amount + } + + return xc.NewAmountBlockchainFromUint64(total), nil +} + +// FetchAlphaPositions queries all alpha positions for a coldkey on a specific subnet. +// It first looks up all hotkeys the coldkey stakes with, then queries Alpha balance for each. +func (client *Client) fetchAlphaPositions(addr xc.Address, netuid uint16) ([]tx_input.AlphaPosition, error) { + meta, err := client.DotClient.RPC.State.GetMetadataLatest() + if err != nil { + return nil, fmt.Errorf("failed to get metadata: %v", err) + } + + coldkeyAddr, err := address.Decode(addr) + if err != nil { + return nil, fmt.Errorf("invalid coldkey address: %v", err) + } + + // Query StakingHotkeys(coldkey) -> Vec + hotkeys, err := client.fetchStakingHotkeys(meta, coldkeyAddr) + if err != nil { + return nil, err + } + + netuidBytes := make([]byte, 2) + binary.LittleEndian.PutUint16(netuidBytes, netuid) + + var positions []tx_input.AlphaPosition + for _, hotkey := range hotkeys { + balance, err := client.fetchSingleAlphaBalance(meta, &hotkey, coldkeyAddr, netuidBytes) + if err != nil { + return nil, err + } + if balance > 0 { + // Encode hotkey back to SS58 address + hotkeyAddr, err := address.NewAddressBuilder(client.Asset.GetChain().Base()) + if err != nil { + return nil, fmt.Errorf("failed to create address builder: %v", err) + } + hotkeyStr, err := hotkeyAddr.GetAddressFromPublicKey(hotkey.ToBytes()) + if err != nil { + return nil, fmt.Errorf("failed to encode hotkey address: %v", err) + } + positions = append(positions, tx_input.AlphaPosition{ + Hotkey: string(hotkeyStr), + Amount: balance, + }) + } + } + + // Sort by amount descending so UTXO selection takes biggest positions first + sort.Slice(positions, func(i, j int) bool { + return positions[i].Amount > positions[j].Amount + }) + + return positions, nil +} + +// fetchStakingHotkeys queries SubtensorModule.StakingHotkeys(coldkey) -> Vec +func (client *Client) fetchStakingHotkeys(meta *types.Metadata, coldkey *types.AccountID) ([]types.AccountID, error) { + key, err := types.CreateStorageKey(meta, "SubtensorModule", "StakingHotkeys", coldkey.ToBytes()) + if err != nil { + return nil, fmt.Errorf("failed to create storage key for StakingHotkeys: %v", err) + } + + var rawData types.StorageDataRaw + ok, err := client.DotClient.RPC.State.GetStorageLatest(key, &rawData) + if err != nil { + return nil, fmt.Errorf("failed to query StakingHotkeys: %v", err) + } + if !ok || len(rawData) == 0 { + return nil, nil + } + + // Decode Vec using SCALE codec + decoder := scale.NewDecoder(bytes.NewReader(rawData)) + // Vec prefix: compact-encoded length + compactLen := types.UCompact{} + err = decoder.Decode(&compactLen) + if err != nil { + return nil, fmt.Errorf("failed to decode StakingHotkeys length: %v", err) + } + count := compactLen.Int64() + + hotkeys := make([]types.AccountID, count) + for i := int64(0); i < count; i++ { + err = decoder.Decode(&hotkeys[i]) + if err != nil { + return nil, fmt.Errorf("failed to decode hotkey at index %d: %v", i, err) + } + } + + return hotkeys, nil +} + +// fetchSingleAlphaBalance queries SubtensorModule.Alpha(hotkey, coldkey, netuid) -> u64 +func (client *Client) fetchSingleAlphaBalance(meta *types.Metadata, hotkey, coldkey *types.AccountID, netuidBytes []byte) (uint64, error) { + key, err := types.CreateStorageKey(meta, "SubtensorModule", "Alpha", hotkey.ToBytes(), coldkey.ToBytes(), netuidBytes) + if err != nil { + return 0, fmt.Errorf("failed to create storage key for Alpha: %v", err) + } + + var alphaBalance types.U64 + ok, err := client.DotClient.RPC.State.GetStorageLatest(key, &alphaBalance) + if err != nil { + return 0, fmt.Errorf("failed to query Alpha balance: %v", err) + } + if !ok { + return 0, nil + } + + return uint64(alphaBalance), nil +} diff --git a/chain/substrate/client/api/events.go b/chain/substrate/client/api/events.go index ee3db8e7..a480bb79 100644 --- a/chain/substrate/client/api/events.go +++ b/chain/substrate/client/api/events.go @@ -97,6 +97,33 @@ var SupportedEvents = []EventDescriptor{ }, } +var SupportedAlphaEvents = []EventDescriptor{ + { + Module: "SubtensorModule", + Event: "StakeTransferred", + Attributes: []*EventAttributeDescriptor{ + { + Name: "from", + Index: 0, + Bind: BindFrom, + Type: EventAddress, + }, + { + Name: "to", + Index: 1, + Bind: BindTo, + Type: EventAddress, + }, + { + Name: "amount", + Index: 4, + Bind: BindAmount, + Type: EventInteger, + }, + }, + }, +} + var SupportedStakingEvents = []EventDescriptor{ { Module: "SubtensorModule", @@ -207,6 +234,9 @@ func init() { for _, ev := range SupportedEvents { supportedEventMap[eventHandle(ev.Module, ev.Event)] = ev } + for _, ev := range SupportedAlphaEvents { + supportedEventMap[eventHandle(ev.Module, ev.Event)] = ev + } for _, ev := range SupportedStakingEvents { supportedStakingEventMap[eventHandle(ev.Module, ev.Event)] = ev } diff --git a/chain/substrate/client/client.go b/chain/substrate/client/client.go index 8b812231..40724075 100644 --- a/chain/substrate/client/client.go +++ b/chain/substrate/client/client.go @@ -13,6 +13,7 @@ import ( "github.com/centrifuge/go-substrate-rpc-client/v4/types/codec" xc "github.com/cordialsys/crosschain" xcbuilder "github.com/cordialsys/crosschain/builder" + "github.com/cordialsys/crosschain/chain/substrate" "github.com/cordialsys/crosschain/chain/substrate/address" "github.com/cordialsys/crosschain/chain/substrate/client/api" "github.com/cordialsys/crosschain/chain/substrate/client/api/graphql" @@ -163,6 +164,19 @@ func (client *Client) FetchTransferInput(ctx context.Context, args xcbuilder.Tra } txInput.Tip = amt + // For TAO alpha token transfers, populate the sender's alpha positions on the target subnet + if contract, ok := args.GetContract(); ok && client.Asset.GetChain().Chain == xc.TAO { + netuid, err := substrate.ParseAlphaContract(contract) + if err != nil { + return &tx_input.TxInput{}, err + } + positions, err := client.fetchAlphaPositions(args.GetFrom(), netuid) + if err != nil { + return &tx_input.TxInput{}, err + } + txInput.AlphaPositions = positions + } + return txInput, nil } func (client *Client) FetchLegacyTxInput(ctx context.Context, from xc.Address, to xc.Address) (xc.TxInput, error) { @@ -477,9 +491,11 @@ func (client *Client) FetchBalance(ctx context.Context, args *xclient.BalanceArg contract, _ := args.Contract() if contract == "" { return client.FetchNativeBalance(ctx, args.Address()) - } else { - return xc.AmountBlockchain{}, fmt.Errorf("token balance is not supported for substrate: %v", contract) } + if client.Asset.GetChain().Chain == xc.TAO { + return client.FetchAlphaBalance(ctx, args.Address(), contract) + } + return xc.AmountBlockchain{}, fmt.Errorf("token balance is not supported for substrate: %v", contract) } // EstimateTip looks at the latest extrinsics to try to calculate an average tip paid diff --git a/chain/substrate/tx_input/calls.go b/chain/substrate/tx_input/calls.go index 4c6e2f39..c8381b82 100644 --- a/chain/substrate/tx_input/calls.go +++ b/chain/substrate/tx_input/calls.go @@ -15,6 +15,8 @@ var usedSubstrateCalls = []string{ "Assets.transfer", "SubtensorModule.add_stake", "SubtensorModule.remove_stake", + "SubtensorModule.transfer_stake", + "Utility.batch_all", "NominationPools.join", "NominationPools.bond_extra", "NominationPools.unbond", diff --git a/chain/substrate/tx_input/tx_input.go b/chain/substrate/tx_input/tx_input.go index 1bbfde79..d34e6bbc 100644 --- a/chain/substrate/tx_input/tx_input.go +++ b/chain/substrate/tx_input/tx_input.go @@ -18,6 +18,9 @@ type TxInput struct { Tip uint64 `json:"tip,omitempty"` Nonce uint64 `json:"account_nonce,omitempty"` + // For TAO alpha token transfers - positions of alpha held across hotkeys on a subnet + AlphaPositions []AlphaPosition `json:"alpha_positions,omitempty"` + // For nomination pools staking - indicates if account already joined a pool AlreadyJoinedPool bool `json:"already_joined_pool,omitempty"` JoinedPoolId uint32 `json:"joined_pool_id,omitempty"` @@ -27,6 +30,12 @@ type TxInput struct { NumSlashingSpans uint32 `json:"num_slashing_spans,omitempty"` } +// AlphaPosition represents a single alpha token holding on a specific hotkey +type AlphaPosition struct { + Hotkey string `json:"hotkey"` + Amount uint64 `json:"amount"` +} + var _ xc.TxInput = &TxInput{} var _ NonceGetter = &TxInput{} var _ xc.StakeTxInput = &TxInput{}