Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
22 changes: 22 additions & 0 deletions chain/substrate/alpha.go
Original file line number Diff line number Diff line change
@@ -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
}
49 changes: 49 additions & 0 deletions chain/substrate/alpha_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
167 changes: 167 additions & 0 deletions chain/substrate/builder/alpha_tao.go
Original file line number Diff line number Diff line change
@@ -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"

Check failure on line 11 in chain/substrate/builder/alpha_tao.go

View workflow job for this annotation

GitHub Actions / lint

could not import github.com/cordialsys/crosschain/chain/substrate (-: import cycle not allowed: import stack: [github.com/cordialsys/crosschain/chain/crosschain github.com/cordialsys/crosschain/factory/drivers github.com/cordialsys/crosschain/chain/substrate github.com/cordialsys/crosschain/chain/substrate/client github.com/cordialsys/crosschain/chain/substrate]) (typecheck)
"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<Call>, 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<Call> 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
}
Loading
Loading