Skip to content
Merged
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
2 changes: 1 addition & 1 deletion chain/filecoin/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
txinfo "github.com/cordialsys/crosschain/client/tx_info"
xctypes "github.com/cordialsys/crosschain/client/types"
log "github.com/sirupsen/logrus"
"github.com/stellar/go/support/time"
"github.com/stellar/go-stellar-sdk/support/time"
)

// Client for Filecoin
Expand Down
272 changes: 219 additions & 53 deletions chain/xlm/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
common "github.com/cordialsys/crosschain/chain/xlm/common"
xlmtx "github.com/cordialsys/crosschain/chain/xlm/tx"
xlminput "github.com/cordialsys/crosschain/chain/xlm/tx_input"
"github.com/stellar/go/xdr"
"github.com/stellar/go-stellar-sdk/xdr"
)

type TxBuilder struct {
Expand Down Expand Up @@ -93,57 +93,8 @@ func (builder TxBuilder) Transfer(args xcbuilder.TransferArgs, input xc.TxInput)
txe.Tx.Memo = xdrMemo
}

destinationMuxedAccount, err := common.MuxedAccountFromAddress(to)
if err != nil {
return &xlmtx.Tx{}, fmt.Errorf("invalid `to` address: %w", err)
}
xdrAmount := xdr.Int64(amount.Int().Int64())

var xdrOperationBody xdr.OperationBody
_, isToken := args.GetContract()

if !txInput.DestinationFunded && !isToken {
// Use CreateAccount for new/unfunded native XLM destinations
destinationAccountId, err := xdr.AddressToAccountId(string(to))
if err != nil {
return &xlmtx.Tx{}, fmt.Errorf("invalid `to` address: %w", err)
}
xdrCreateAccount := xdr.CreateAccountOp{
Destination: destinationAccountId,
StartingBalance: xdrAmount,
}
xdrOperationBody, err = xdr.NewOperationBody(xdr.OperationTypeCreateAccount, xdrCreateAccount)
if err != nil {
return &xlmtx.Tx{}, fmt.Errorf("failed to create operation body: %w", err)
}
} else {
// Use Payment for funded accounts or token transfers
var xdrAsset xdr.Asset
if contract, ok := args.GetContract(); ok {
contractDetails, err := common.GetAssetAndIssuerFromContract(string(contract))
if err != nil {
return &xlmtx.Tx{}, fmt.Errorf("failed to get contract details: %w", err)
}

xdrAsset, err = common.CreateAssetFromContractDetails(contractDetails)
if err != nil {
return &xlmtx.Tx{}, fmt.Errorf("failed to create token details: %w", err)
}
} else {
xdrAsset.Type = xdr.AssetTypeAssetTypeNative
}

xdrPayment := xdr.PaymentOp{
Destination: destinationMuxedAccount,
Amount: xdrAmount,
Asset: xdrAsset,
}
xdrOperationBody, err = xdr.NewOperationBody(xdr.OperationTypePayment, xdrPayment)
if err != nil {
return &xlmtx.Tx{}, fmt.Errorf("failed to create operation body: %w", err)
}
}

var operations []xdr.Operation

// If the sender needs a trustline for the token, prepend a ChangeTrust operation.
Expand Down Expand Up @@ -171,12 +122,82 @@ func (builder TxBuilder) Transfer(args xcbuilder.TransferArgs, input xc.TxInput)
}
}

// Skip the payment/createAccount operation when amount is 0 (trustline-only transaction)
if xdrAmount > 0 {
if common.IsContractAddress(to) {
// Soroban SAC invocation for contract (C...) address destinations
sacOp, sorobanData, err := builder.buildSACTransfer(args, txInput, from, to)
if err != nil {
return &xlmtx.Tx{}, fmt.Errorf("failed to build SAC transfer: %w", err)
}
operations = append(operations, xdr.Operation{
SourceAccount: &opSourceAccount,
Body: xdrOperationBody,
Body: *sacOp,
})
// Attach Soroban data to the transaction
txe.Tx.Ext, err = xdr.NewTransactionExt(1, *sorobanData)
if err != nil {
return &xlmtx.Tx{}, fmt.Errorf("failed to set soroban transaction data: %w", err)
}
// Soroban fee = base inclusion fee + resource fee
txe.Tx.Fee = xdr.Uint32(txInput.MaxFee) + xdr.Uint32(txInput.SorobanResourceFee)
} else {
_, isToken := args.GetContract()
if !txInput.DestinationFunded && !isToken {
// Use CreateAccount for new/unfunded native XLM destinations
destinationAccountId, err := xdr.AddressToAccountId(string(to))
if err != nil {
return &xlmtx.Tx{}, fmt.Errorf("invalid `to` address: %w", err)
}
xdrCreateAccount := xdr.CreateAccountOp{
Destination: destinationAccountId,
StartingBalance: xdrAmount,
}
xdrOperationBody, err := xdr.NewOperationBody(xdr.OperationTypeCreateAccount, xdrCreateAccount)
if err != nil {
return &xlmtx.Tx{}, fmt.Errorf("failed to create operation body: %w", err)
}
if xdrAmount > 0 {
operations = append(operations, xdr.Operation{
SourceAccount: &opSourceAccount,
Body: xdrOperationBody,
})
}
} else {
// Use Payment for funded accounts or token transfers
destinationMuxedAccount, err := common.MuxedAccountFromAddress(to)
if err != nil {
return &xlmtx.Tx{}, fmt.Errorf("invalid `to` address: %w", err)
}
var xdrAsset xdr.Asset
if contract, ok := args.GetContract(); ok {
contractDetails, err := common.GetAssetAndIssuerFromContract(string(contract))
if err != nil {
return &xlmtx.Tx{}, fmt.Errorf("failed to get contract details: %w", err)
}
xdrAsset, err = common.CreateAssetFromContractDetails(contractDetails)
if err != nil {
return &xlmtx.Tx{}, fmt.Errorf("failed to create token details: %w", err)
}
} else {
xdrAsset.Type = xdr.AssetTypeAssetTypeNative
}

xdrPayment := xdr.PaymentOp{
Destination: destinationMuxedAccount,
Amount: xdrAmount,
Asset: xdrAsset,
}
xdrOperationBody, err := xdr.NewOperationBody(xdr.OperationTypePayment, xdrPayment)
if err != nil {
return &xlmtx.Tx{}, fmt.Errorf("failed to create operation body: %w", err)
}
// Skip the payment operation when amount is 0 (trustline-only transaction)
if xdrAmount > 0 {
operations = append(operations, xdr.Operation{
SourceAccount: &opSourceAccount,
Body: xdrOperationBody,
})
}
}
}

txe.Tx.Operations = operations
Expand All @@ -196,6 +217,151 @@ func (builder TxBuilder) Transfer(args xcbuilder.TransferArgs, input xc.TxInput)
return tx, nil
}

// buildSACTransfer builds an InvokeHostFunction operation that calls the SAC transfer function.
// The SAC transfer signature is: transfer(from: Address, to: Address, amount: i128)
// All Soroban data (auth, footprint, resources) is constructed natively — no simulation RPC needed.
func (builder TxBuilder) buildSACTransfer(args xcbuilder.TransferArgs, txInput *TxInput, from xc.Address, to xc.Address) (*xdr.OperationBody, *xdr.SorobanTransactionData, error) {
// Derive the SAC contract address from the token asset
contract, ok := args.GetContract()
if !ok {
return nil, nil, fmt.Errorf("contract is required for SAC transfers")
}
contractDetails, err := common.GetAssetAndIssuerFromContract(string(contract))
if err != nil {
return nil, nil, fmt.Errorf("failed to parse contract: %w", err)
}
xdrAsset, err := common.CreateAssetFromContractDetails(contractDetails)
if err != nil {
return nil, nil, fmt.Errorf("failed to create asset: %w", err)
}
sacId, err := xdrAsset.ContractID(txInput.Passphrase)
if err != nil {
return nil, nil, fmt.Errorf("failed to derive SAC contract ID: %w", err)
}
contractId := xdr.ContractId(sacId)
contractAddr := xdr.ScAddress{
Type: xdr.ScAddressTypeScAddressTypeContract,
ContractId: &contractId,
}

// Build the SAC transfer arguments: from, to, amount
fromScVal, err := common.ScValAddress(string(from))
if err != nil {
return nil, nil, fmt.Errorf("failed to encode from address: %w", err)
}
toScVal, err := common.ScValAddress(string(to))
if err != nil {
return nil, nil, fmt.Errorf("failed to encode to address: %w", err)
}
amountScVal := common.ScValI128(args.GetAmount().Int().Int64())

invokeArgs := xdr.InvokeContractArgs{
ContractAddress: contractAddr,
FunctionName: "transfer",
Args: []xdr.ScVal{fromScVal, toScVal, amountScVal},
}

hostFn, err := xdr.NewHostFunction(xdr.HostFunctionTypeHostFunctionTypeInvokeContract, invokeArgs)
if err != nil {
return nil, nil, fmt.Errorf("failed to create host function: %w", err)
}

// Construct auth entry natively.
// For SAC transfer where sender = transaction source, credentials are SOURCE_ACCOUNT
// and the invocation is the transfer call with no sub-invocations.
contractFn, err := xdr.NewSorobanAuthorizedFunction(
xdr.SorobanAuthorizedFunctionTypeSorobanAuthorizedFunctionTypeContractFn,
invokeArgs,
)
if err != nil {
return nil, nil, fmt.Errorf("failed to create authorized function: %w", err)
}
authEntry := xdr.SorobanAuthorizationEntry{
Credentials: xdr.SorobanCredentials{
Type: xdr.SorobanCredentialsTypeSorobanCredentialsSourceAccount,
},
RootInvocation: xdr.SorobanAuthorizedInvocation{
Function: contractFn,
SubInvocations: nil,
},
}

invokeOp := xdr.InvokeHostFunctionOp{
HostFunction: hostFn,
Auth: []xdr.SorobanAuthorizationEntry{authEntry},
}

opBody, err := xdr.NewOperationBody(xdr.OperationTypeInvokeHostFunction, invokeOp)
if err != nil {
return nil, nil, fmt.Errorf("failed to create invoke host function operation: %w", err)
}

// Construct the ledger footprint natively.
// SAC transfer touches:
// ReadOnly: SAC contract instance
// ReadWrite: sender's trustline (G-addr balance), receiver's contract data (C-addr balance)
fromAccountId, err := xdr.AddressToAccountId(string(from))
if err != nil {
return nil, nil, fmt.Errorf("failed to parse from account: %w", err)
}
toContractAddr, err := common.ScAddressFromString(string(to))
if err != nil {
return nil, nil, fmt.Errorf("failed to parse to contract address: %w", err)
}

// SAC contract instance key
sacInstanceKey := xdr.LedgerKey{
Type: xdr.LedgerEntryTypeContractData,
ContractData: &xdr.LedgerKeyContractData{
Contract: contractAddr,
Key: xdr.ScVal{Type: xdr.ScValTypeScvLedgerKeyContractInstance},
Durability: xdr.ContractDataDurabilityPersistent,
},
}

// Sender's classic trustline (SAC uses TrustLine entries for G-address balances)
senderTrustlineKey := xdr.LedgerKey{
Type: xdr.LedgerEntryTypeTrustline,
TrustLine: &xdr.LedgerKeyTrustLine{
AccountId: fromAccountId,
Asset: xdrAsset.ToTrustLineAsset(),
},
}

// Receiver's contract balance (SAC uses ContractData for C-address balances)
// Key format: Vec[Symbol("Balance"), Address(to)]
balanceSym := common.ScValSymbol("Balance")
toAddrScVal := xdr.ScVal{Type: xdr.ScValTypeScvAddress, Address: &toContractAddr}
balanceVec := xdr.ScVec{balanceSym, toAddrScVal}
balanceVecPtr := &balanceVec
receiverBalanceKey := xdr.LedgerKey{
Type: xdr.LedgerEntryTypeContractData,
ContractData: &xdr.LedgerKeyContractData{
Contract: contractAddr,
Key: xdr.ScVal{
Type: xdr.ScValTypeScvVec,
Vec: &balanceVecPtr,
},
Durability: xdr.ContractDataDurabilityPersistent,
},
}

sorobanData := xdr.SorobanTransactionData{
Resources: xdr.SorobanResources{
Footprint: xdr.LedgerFootprint{
ReadOnly: []xdr.LedgerKey{sacInstanceKey},
ReadWrite: []xdr.LedgerKey{senderTrustlineKey, receiverBalanceKey},
},
Instructions: xdr.Uint32(txInput.SorobanInstructions),
DiskReadBytes: xdr.Uint32(txInput.SorobanDiskReadBytes),
WriteBytes: xdr.Uint32(txInput.SorobanWriteBytes),
},
ResourceFee: xdr.Int64(txInput.SorobanResourceFee),
}

return &opBody, &sorobanData, nil
}

func (txBuilder TxBuilder) SupportsMemo() xc.MemoSupport {
// XLM supports memo
return xc.MemoSupportString
Expand Down
2 changes: 1 addition & 1 deletion chain/xlm/builder/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/cordialsys/crosschain/chain/xlm/common"
"github.com/cordialsys/crosschain/chain/xlm/tx"
"github.com/cordialsys/crosschain/chain/xlm/tx_input"
"github.com/stellar/go/xdr"
"github.com/stellar/go-stellar-sdk/xdr"
"github.com/test-go/testify/require"
)

Expand Down
Loading
Loading