Overview
In this guide, you’ll learn how to deploy a Stellar Asset Contract (SAC) for a Stellar asset using the Stellar SDK. The Stellar SDK is a set of tools and libraries designed to help developers build applications that interact with the Stellar blockchain network.Prerequisites:
- For JavaScript: Node.js and npm installed, and the Stellar SDK for JavaScript installed
- For Python: the Stellar SDK for Python installed
- For Go: Go 1.25 or later, and the Stellar SDK for Go installed
- For Java: Java 17 or later, and the Stellar SDK for Java (
network.lightsail:stellar-sdk:6.0.0) installed - Knowledge about Issuing an Asset on Stellar
- An understanding of the rudimentary, retry-enabled transaction polling function
submitTx(submit_txin Python) which we outlined in another guide
Code overview
import * as StellarSdk from "@stellar/stellar-sdk";
const networkRPC = "https://soroban-testnet.stellar.org";
const server = new StellarSdk.rpc.Server(networkRPC);
const networkPassphrase = StellarSdk.Networks.TESTNET;
const deployStellarAssetContract = async () => {
const sourceSecrets =
"SASI6PA4K52GQJF6BC263GLYOADVKFJ4SZ7TFX4QQF2U76T3EJ54DT7Y"; // Replace with your Secret Key
const sourceKeypair = StellarSdk.Keypair.fromSecret(sourceSecrets);
const sourceAccount = await server.getAccount(sourceKeypair.publicKey());
try {
const assetCode = "JOEBOY";
const issuerPublicKey = sourceKeypair.publicKey();
const customAsset = new StellarSdk.Asset(assetCode, issuerPublicKey);
// Bid what recent transactions paid, capped at what you're willing to spend.
// prepareTransaction adds the resource fee on top of this inclusion fee.
const { sorobanInclusionFee } = await server.getFeeStats();
const MAX_FEE = 100_000; // stroops per operation (0.01 XLM): pick your own cap
const fee = String(Math.min(Number(sorobanInclusionFee.p90), MAX_FEE));
const transaction = new StellarSdk.TransactionBuilder(sourceAccount, {
fee,
networkPassphrase,
})
.addOperation(
StellarSdk.Operation.createStellarAssetContract({
asset: customAsset,
}),
)
.setTimeout(30)
.build();
const uploadTx = await server.prepareTransaction(transaction);
uploadTx.sign(sourceKeypair);
const feedback = await submitTx(uploadTx);
const contract = StellarSdk.Address.fromScAddress(
feedback.returnValue.address(),
);
console.log(
`ContractID of Our ${customAsset.code} Asset`,
contract.toString(),
);
} catch (e) {
console.error("An error occurred while Deploying assets:", e);
}
};
await deployStellarAssetContract();
from stellar_sdk import Asset, Keypair, Network, SorobanServer, TransactionBuilder, scval
from stellar_sdk.xdr import TransactionMeta
network_rpc = "https://soroban-testnet.stellar.org"
server = SorobanServer(network_rpc)
network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE
def deploy_stellar_asset_contract():
# Replace with your Secret Key
source_secrets = "SASI6PA4K52GQJF6BC263GLYOADVKFJ4SZ7TFX4QQF2U76T3EJ54DT7Y"
source_keypair = Keypair.from_secret(source_secrets)
source_account = server.load_account(source_keypair.public_key)
try:
asset_code = "JOEBOY"
issuer_public_key = source_keypair.public_key
custom_asset = Asset(asset_code, issuer_public_key)
# Bid what recent transactions paid, capped at what you're willing to spend.
# prepare_transaction adds the resource fee on top of this inclusion fee.
MAX_FEE = 100_000 # stroops per operation (0.01 XLM): pick your own cap
fee = min(server.get_fee_stats().soroban_inclusion_fee.p90, MAX_FEE)
transaction = (
TransactionBuilder(
source_account=source_account,
network_passphrase=network_passphrase,
base_fee=fee,
)
.append_create_stellar_asset_contract_from_asset_op(asset=custom_asset)
.set_timeout(30)
.build()
)
upload_tx = server.prepare_transaction(transaction)
upload_tx.sign(source_keypair)
feedback = submit_tx(upload_tx)
# The return value sits in the transaction meta (v4 since Protocol 23)
meta = TransactionMeta.from_xdr(feedback.result_meta_xdr)
return_value = (meta.v4 or meta.v3).soroban_meta.return_value
contract = scval.from_address(return_value)
print(f"ContractID of Our {custom_asset.code} Asset", contract.address)
except Exception as e:
print("An error occurred while Deploying assets:", e)
deploy_stellar_asset_contract()
package main
import (
"context"
"fmt"
"log"
"github.com/stellar/go-stellar-sdk/keypair"
"github.com/stellar/go-stellar-sdk/network"
"github.com/stellar/go-stellar-sdk/txnbuild"
"github.com/stellar/go-stellar-sdk/xdr"
client "github.com/stellar/go-stellar-sdk/clients/rpcclient"
protocol "github.com/stellar/go-stellar-sdk/protocols/rpc"
)
// Add submitTx() from the submitTx guide to this package. It uses the
// `server` declared here, so leave out its `rpcServer` and `server` lines.
// Pasted into this file, it also needs "errors" and "time" in the imports
// above. In a separate file, copy its imports but drop the rpcclient one.
const networkRPC = "https://soroban-testnet.stellar.org"
const networkPassphrase = network.TestNetworkPassphrase
var server = client.NewClient(networkRPC, nil)
func deployStellarAssetContract(ctx context.Context) error {
sourceSecrets := "SASI6PA4K52GQJF6BC263GLYOADVKFJ4SZ7TFX4QQF2U76T3EJ54DT7Y" // Replace with your Secret Key
sourceKeypair := keypair.MustParseFull(sourceSecrets)
sourceAccount, err := server.LoadAccount(ctx, sourceKeypair.Address())
if err != nil {
return err
}
assetCode := "JOEBOY"
issuerPublicKey := sourceKeypair.Address()
customAsset := txnbuild.CreditAsset{Code: assetCode, Issuer: issuerPublicKey}
assetXDR, err := customAsset.ToXDR()
if err != nil {
return err
}
// Bid what recent transactions paid, capped at what you're willing to spend.
// prepareTransaction adds the resource fee on top of this inclusion fee.
feeStats, err := server.GetFeeStats(ctx)
if err != nil {
return err
}
const maxFee = 100_000 // stroops per operation (0.01 XLM): pick your own cap
fee := min(int64(feeStats.SorobanInclusionFee.P90), maxFee)
operation := &txnbuild.InvokeHostFunction{
HostFunction: xdr.HostFunction{
Type: xdr.HostFunctionTypeHostFunctionTypeCreateContract,
CreateContract: &xdr.CreateContractArgs{
ContractIdPreimage: xdr.ContractIdPreimage{
Type: xdr.ContractIdPreimageTypeContractIdPreimageFromAsset,
FromAsset: &assetXDR,
},
Executable: xdr.ContractExecutable{
Type: xdr.ContractExecutableTypeContractExecutableStellarAsset,
},
},
},
}
uploadTx, err := prepareTransaction(ctx, txnbuild.TransactionParams{
SourceAccount: sourceAccount,
IncrementSequenceNum: true,
BaseFee: fee,
Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(30)},
Operations: []txnbuild.Operation{operation},
})
if err != nil {
return err
}
uploadTx, err = uploadTx.Sign(networkPassphrase, sourceKeypair)
if err != nil {
return err
}
feedback, err := submitTx(ctx, uploadTx.ToGenericTransaction())
if err != nil {
return err
}
// The return value sits in the transaction meta (v4 since Protocol 23)
var meta xdr.TransactionMeta
if err := xdr.SafeUnmarshalBase64(feedback.ResultMetaXDR, &meta); err != nil {
return err
}
var returnValue xdr.ScVal
switch {
case meta.V4 != nil && meta.V4.SorobanMeta != nil && meta.V4.SorobanMeta.ReturnValue != nil:
returnValue = *meta.V4.SorobanMeta.ReturnValue
case meta.V3 != nil && meta.V3.SorobanMeta != nil:
returnValue = meta.V3.SorobanMeta.ReturnValue
}
address, ok := returnValue.GetAddress()
if !ok {
return fmt.Errorf("no contract address in the return value")
}
contract, err := address.String()
if err != nil {
return err
}
fmt.Printf("ContractID of Our %s Asset %s\n", customAsset.Code, contract)
return nil
}
// prepareTransaction simulates the transaction, then builds it again with the
// simulated resources, resource fee, and authorization entries.
func prepareTransaction(ctx context.Context, params txnbuild.TransactionParams) (*txnbuild.Transaction, error) {
tx, err := txnbuild.NewTransaction(params)
if err != nil {
return nil, err
}
envelope, err := tx.Base64()
if err != nil {
return nil, err
}
sim, err := server.SimulateTransaction(ctx, protocol.SimulateTransactionRequest{Transaction: envelope})
if err != nil {
return nil, err
}
if sim.Error != "" {
return nil, fmt.Errorf("simulation failed: %s", sim.Error)
}
var sorobanData xdr.SorobanTransactionData
if err := xdr.SafeUnmarshalBase64(sim.TransactionDataXDR, &sorobanData); err != nil {
return nil, err
}
// Takes one InvokeHostFunction operation; any other type panics here.
operation := params.Operations[0].(*txnbuild.InvokeHostFunction)
operation.Ext = xdr.TransactionExt{V: 1, SorobanData: &sorobanData}
// Keep the operation's own auth entries; otherwise use the simulated ones.
if len(operation.Auth) == 0 && len(sim.Results) > 0 && sim.Results[0].AuthXDR != nil {
for _, entryXDR := range *sim.Results[0].AuthXDR {
var entry xdr.SorobanAuthorizationEntry
if err := xdr.SafeUnmarshalBase64(entryXDR, &entry); err != nil {
return nil, err
}
operation.Auth = append(operation.Auth, entry)
}
}
// Build again with the same sequence number.
source := tx.SourceAccount()
params.SourceAccount = &source
params.IncrementSequenceNum = false
return txnbuild.NewTransaction(params)
}
func main() {
if err := deployStellarAssetContract(context.Background()); err != nil {
log.Println("An error occurred while Deploying assets:", err)
}
}
import org.stellar.sdk.Address;
import org.stellar.sdk.Asset;
import org.stellar.sdk.KeyPair;
import org.stellar.sdk.Network;
import org.stellar.sdk.SorobanServer;
import org.stellar.sdk.Transaction;
import org.stellar.sdk.TransactionBuilder;
import org.stellar.sdk.TransactionBuilderAccount;
import org.stellar.sdk.operations.InvokeHostFunctionOperation;
import org.stellar.sdk.responses.sorobanrpc.GetTransactionResponse;
import org.stellar.sdk.xdr.SCVal;
import org.stellar.sdk.xdr.TransactionMeta;
// Put SubmitTx.java from the submitTx guide in the same package.
public class DeploySac {
static final String networkRPC = "https://soroban-testnet.stellar.org";
static final SorobanServer server = new SorobanServer(networkRPC);
static final Network network = Network.TESTNET;
static void deployStellarAssetContract() throws Exception {
String sourceSecrets = "SASI6PA4K52GQJF6BC263GLYOADVKFJ4SZ7TFX4QQF2U76T3EJ54DT7Y"; // Replace with your Secret Key
KeyPair sourceKeypair = KeyPair.fromSecretSeed(sourceSecrets);
TransactionBuilderAccount sourceAccount = server.getAccount(sourceKeypair.getAccountId());
String assetCode = "JOEBOY";
String issuerPublicKey = sourceKeypair.getAccountId();
Asset customAsset = Asset.createNonNativeAsset(assetCode, issuerPublicKey);
// Bid what recent transactions paid, capped at what you're willing to spend.
// prepareTransaction adds the resource fee on top of this inclusion fee.
long maxFee = 100_000L; // stroops per operation (0.01 XLM): pick your own cap
long fee = Math.min(server.getFeeStats().getSorobanInclusionFee().getP90(), maxFee);
Transaction transaction = new TransactionBuilder(sourceAccount, network)
.setBaseFee(fee)
.addOperation(
InvokeHostFunctionOperation.createStellarAssetContractOperationBuilder(customAsset).build())
.setTimeout(30)
.build();
Transaction uploadTx = server.prepareTransaction(transaction);
uploadTx.sign(sourceKeypair);
GetTransactionResponse feedback = SubmitTx.submitTx(uploadTx);
Address contract = Address.fromSCVal(getReturnValue(feedback));
System.out.println("ContractID of Our " + assetCode + " Asset " + contract);
}
// The return value sits in the transaction meta (v4 since Protocol 23)
static SCVal getReturnValue(GetTransactionResponse response) {
TransactionMeta meta = response.parseResultMetaXdr();
if (meta.getV4() != null && meta.getV4().getSorobanMeta() != null
&& meta.getV4().getSorobanMeta().getReturnValue() != null) {
return meta.getV4().getSorobanMeta().getReturnValue();
}
if (meta.getV3() != null && meta.getV3().getSorobanMeta() != null) {
return meta.getV3().getSorobanMeta().getReturnValue();
}
throw new IllegalStateException("no return value in the transaction meta");
}
public static void main(String[] args) {
try {
deployStellarAssetContract();
} catch (Exception e) {
System.err.println("An error occurred while Deploying assets: " + e);
}
}
}
Code explanation
Server Setupimport * as StellarSdk from "@stellar/stellar-sdk";
const networkRPC = "https://soroban-testnet.stellar.org";
const server = new StellarSdk.rpc.Server(networkRPC);
const networkPassphrase = StellarSdk.Networks.TESTNET;
from stellar_sdk import Asset, Keypair, Network, SorobanServer, TransactionBuilder, scval
from stellar_sdk.xdr import TransactionMeta
network_rpc = "https://soroban-testnet.stellar.org"
server = SorobanServer(network_rpc)
network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE
const networkRPC = "https://soroban-testnet.stellar.org"
const networkPassphrase = network.TestNetworkPassphrase
var server = client.NewClient(networkRPC, nil)
static final String networkRPC = "https://soroban-testnet.stellar.org";
static final SorobanServer server = new SorobanServer(networkRPC);
static final Network network = Network.TESTNET;
networkRPC: The URL for the Soroban testnet.server: A new instance ofrpc.Serveris created, which will be used to interact with the Soroban testnet.networkPassphrase: sets the network passphrase to the TESTNET
DeployStellarAssetContract function
const deployStellarAssetContract = async () => {
const sourceSecrets =
"SASI6PA4K52GQJF6BC263GLYOADVKFJ4SZ7TFX4QQF2U76T3EJ54DT7Y"; // Replace with your Secret Key
const sourceKeypair = StellarSdk.Keypair.fromSecret(sourceSecrets);
const sourceAccount = await server.getAccount(sourceKeypair.publicKey());
try {
const assetCode = "JOEBOY";
const issuerPublicKey = sourceKeypair.publicKey();
const customAsset = new StellarSdk.Asset(assetCode, issuerPublicKey);
// Bid what recent transactions paid, capped at what you're willing to spend.
// prepareTransaction adds the resource fee on top of this inclusion fee.
const { sorobanInclusionFee } = await server.getFeeStats();
const MAX_FEE = 100_000; // stroops per operation (0.01 XLM): pick your own cap
const fee = String(Math.min(Number(sorobanInclusionFee.p90), MAX_FEE));
const transaction = new StellarSdk.TransactionBuilder(sourceAccount, {
fee,
networkPassphrase,
})
.addOperation(
StellarSdk.Operation.createStellarAssetContract({
asset: customAsset,
}),
)
.setTimeout(30)
.build();
const uploadTx = await server.prepareTransaction(transaction);
uploadTx.sign(sourceKeypair);
const feedback = await submitTx(uploadTx);
const contract = StellarSdk.Address.fromScAddress(
feedback.returnValue.address(),
);
console.log(
`ContractID of Our ${customAsset.code} Asset`,
contract.toString(),
);
} catch (e) {
console.error("An error occurred while Deploying assets:", e);
}
};
def deploy_stellar_asset_contract():
# Replace with your Secret Key
source_secrets = "SASI6PA4K52GQJF6BC263GLYOADVKFJ4SZ7TFX4QQF2U76T3EJ54DT7Y"
source_keypair = Keypair.from_secret(source_secrets)
source_account = server.load_account(source_keypair.public_key)
try:
asset_code = "JOEBOY"
issuer_public_key = source_keypair.public_key
custom_asset = Asset(asset_code, issuer_public_key)
# Bid what recent transactions paid, capped at what you're willing to spend.
# prepare_transaction adds the resource fee on top of this inclusion fee.
MAX_FEE = 100_000 # stroops per operation (0.01 XLM): pick your own cap
fee = min(server.get_fee_stats().soroban_inclusion_fee.p90, MAX_FEE)
transaction = (
TransactionBuilder(
source_account=source_account,
network_passphrase=network_passphrase,
base_fee=fee,
)
.append_create_stellar_asset_contract_from_asset_op(asset=custom_asset)
.set_timeout(30)
.build()
)
upload_tx = server.prepare_transaction(transaction)
upload_tx.sign(source_keypair)
feedback = submit_tx(upload_tx)
# The return value sits in the transaction meta (v4 since Protocol 23)
meta = TransactionMeta.from_xdr(feedback.result_meta_xdr)
return_value = (meta.v4 or meta.v3).soroban_meta.return_value
contract = scval.from_address(return_value)
print(f"ContractID of Our {custom_asset.code} Asset", contract.address)
except Exception as e:
print("An error occurred while Deploying assets:", e)
func deployStellarAssetContract(ctx context.Context) error {
sourceSecrets := "SASI6PA4K52GQJF6BC263GLYOADVKFJ4SZ7TFX4QQF2U76T3EJ54DT7Y" // Replace with your Secret Key
sourceKeypair := keypair.MustParseFull(sourceSecrets)
sourceAccount, err := server.LoadAccount(ctx, sourceKeypair.Address())
if err != nil {
return err
}
assetCode := "JOEBOY"
issuerPublicKey := sourceKeypair.Address()
customAsset := txnbuild.CreditAsset{Code: assetCode, Issuer: issuerPublicKey}
assetXDR, err := customAsset.ToXDR()
if err != nil {
return err
}
// Bid what recent transactions paid, capped at what you're willing to spend.
// prepareTransaction adds the resource fee on top of this inclusion fee.
feeStats, err := server.GetFeeStats(ctx)
if err != nil {
return err
}
const maxFee = 100_000 // stroops per operation (0.01 XLM): pick your own cap
fee := min(int64(feeStats.SorobanInclusionFee.P90), maxFee)
operation := &txnbuild.InvokeHostFunction{
HostFunction: xdr.HostFunction{
Type: xdr.HostFunctionTypeHostFunctionTypeCreateContract,
CreateContract: &xdr.CreateContractArgs{
ContractIdPreimage: xdr.ContractIdPreimage{
Type: xdr.ContractIdPreimageTypeContractIdPreimageFromAsset,
FromAsset: &assetXDR,
},
Executable: xdr.ContractExecutable{
Type: xdr.ContractExecutableTypeContractExecutableStellarAsset,
},
},
},
}
uploadTx, err := prepareTransaction(ctx, txnbuild.TransactionParams{
SourceAccount: sourceAccount,
IncrementSequenceNum: true,
BaseFee: fee,
Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(30)},
Operations: []txnbuild.Operation{operation},
})
if err != nil {
return err
}
uploadTx, err = uploadTx.Sign(networkPassphrase, sourceKeypair)
if err != nil {
return err
}
feedback, err := submitTx(ctx, uploadTx.ToGenericTransaction())
if err != nil {
return err
}
// The return value sits in the transaction meta (v4 since Protocol 23)
var meta xdr.TransactionMeta
if err := xdr.SafeUnmarshalBase64(feedback.ResultMetaXDR, &meta); err != nil {
return err
}
var returnValue xdr.ScVal
switch {
case meta.V4 != nil && meta.V4.SorobanMeta != nil && meta.V4.SorobanMeta.ReturnValue != nil:
returnValue = *meta.V4.SorobanMeta.ReturnValue
case meta.V3 != nil && meta.V3.SorobanMeta != nil:
returnValue = meta.V3.SorobanMeta.ReturnValue
}
address, ok := returnValue.GetAddress()
if !ok {
return fmt.Errorf("no contract address in the return value")
}
contract, err := address.String()
if err != nil {
return err
}
fmt.Printf("ContractID of Our %s Asset %s\n", customAsset.Code, contract)
return nil
}
static void deployStellarAssetContract() throws Exception {
String sourceSecrets = "SASI6PA4K52GQJF6BC263GLYOADVKFJ4SZ7TFX4QQF2U76T3EJ54DT7Y"; // Replace with your Secret Key
KeyPair sourceKeypair = KeyPair.fromSecretSeed(sourceSecrets);
TransactionBuilderAccount sourceAccount = server.getAccount(sourceKeypair.getAccountId());
String assetCode = "JOEBOY";
String issuerPublicKey = sourceKeypair.getAccountId();
Asset customAsset = Asset.createNonNativeAsset(assetCode, issuerPublicKey);
// Bid what recent transactions paid, capped at what you're willing to spend.
// prepareTransaction adds the resource fee on top of this inclusion fee.
long maxFee = 100_000L; // stroops per operation (0.01 XLM): pick your own cap
long fee = Math.min(server.getFeeStats().getSorobanInclusionFee().getP90(), maxFee);
Transaction transaction = new TransactionBuilder(sourceAccount, network)
.setBaseFee(fee)
.addOperation(
InvokeHostFunctionOperation.createStellarAssetContractOperationBuilder(customAsset).build())
.setTimeout(30)
.build();
Transaction uploadTx = server.prepareTransaction(transaction);
uploadTx.sign(sourceKeypair);
GetTransactionResponse feedback = SubmitTx.submitTx(uploadTx);
Address contract = Address.fromSCVal(getReturnValue(feedback));
System.out.println("ContractID of Our " + assetCode + " Asset " + contract);
}
- Secret Key: It starts by defining the secret key for the source account (
sourceSecrets), which you must replace with your own. - Keypair and Account: Generates the keypair from the secret key and fetches the account details from the Soroban server.
- Custom Asset: Defines a custom asset with the code
JOEBOYand the issuer’s public key. - Fee: Bids the 90th percentile of recent Soroban inclusion fees (from
getFeeStats()), capped atMAX_FEE. Do not bidBASE_FEE: it is the network minimum and gets outbid when the network is busy. - Transaction Building: A transaction is built using the
TransactionBuilder, which includes thecreateStellarAssetContractoperation for the custom asset. The transaction is then prepared and signed. The Go SDK has noprepareTransaction, so the Go version defines one: it simulates the transaction, then adds the simulated resources, resource fee, and authorization entries. The Java SDK hasprepareTransactionandcreateStellarAssetContractOperationBuilder, so the Java version needs no helper for this step. - Send Transaction: The signed transaction is sent to the network using
server.sendTransaction. - Feedback Handling: It waits for the transaction feedback using the
submitTxfunction to ensure it has succeeded. Then, it extracts the return value and converts it to a contract ID usingStellarSdk.Address. Finally, it logs the contract ID for the deployed asset. The Java SDK’s response has no return value field, so the Java version reads it from the transaction meta withgetReturnValueand converts it withAddress.fromSCVal.