Transacting

Transactions are how changes such as payments, offers to trade or account creation are made to the network’s ledger.

Creating

Every transaction must originate from an existing account on the network and correctly specify the next sequence number. Creating a new Transaction instance requires these two values, wrapped in an .

Additionally, a Network must be implicit in scope. The choice of network will affect how the transaction is serialised.

implicit val network = TestNetwork
val account = Account(AccountId(accn.publicKey), sequence)
Transaction(account, maxFee = NativeAmount(100), timeBounds = TimeBounds.Unbounded)

Sequence Number

The sequence number for a new account is the id of the ledger in which is was created. The id increments by one with every submitted transaction. In simple architectures, it is possible to keep track of the next sequence number without querying the network. However, if this is not possible, or the number is unknown, you can directly substitute the response from the account query into the Transaction constructor.

for {
  sourceAccount <- network.account(payerKeyPair)
  response <- model.Transaction(sourceAccount, timeBounds = Unbounded, maxFee = NativeAmount(100))
    .add(PaymentOperation(payeePublicKey.toAccountId, lumens(5000)))
    .sign(payerKeyPair)
    .submit()
} yield response

As this example shows, transactions require additional data before they can be successfully processed.

Operations

Without any s, a transaction is not very useful. There can be as few as one and as many and one hundred operations added to each Transaction. These can be provided when constructing the Transaction.

val account = Account(sourceKey.toAccountId, nextSequenceNumber)
val txn = Transaction(account, Seq(
  CreateAccountOperation(aliceKey.toAccountId),
  CreateAccountOperation(bobKey.toAccountId),
  PaymentOperation(charlieKey.toAccountId, Amount.lumens(42))
), timeBounds = Unbounded, maxFee = NativeAmount(100))

Or they can be added afterwards.

val txn = Transaction(account, timeBounds = Unbounded, maxFee = NativeAmount(100))
  .add(PaymentOperation(aliceKey.toAccountId, Amount.lumens(100)))
  .add(PaymentOperation(bobKey.toAccountId, Amount.lumens(77)))
  .add(PaymentOperation(charlieKey.toAccountId, Amount.lumens(4.08)))
  .add(CreateSellOfferOperation(
    selling = Amount.lumens(100),
    buying = Asset("FRUITCAKE42", aliceKey),
    price = Price(100, 1)
  ))

The available operations are:

Operations need not originate from the same account as the transaction. In this way a single transaction can be issued that affects multiple accounts. This enables techniques such as the channel pattern. Each operation has an optional constructor parameter sourceAccount: Option[PublicKey] where the source account can be specified.

TimeBounds

As of v0.9.0, the valid date range for a transaction must be specified. The network will reject any transaction submitted outside of the range defined. To help define TimeBounds, the object contains the constant Unbounded (representing all time) and the method timeout, which will return a TimeBound from the current time until some given timeout duration in the future.

Maximum Fee

As of v0.9.0, the maximum fee payable must be specified explicitly. The network will reject the transaction if the maximum fee is not at least 100 stroops x number of operations. If accepted the actual fee charged will be the cheapest it can be, given current network demand but will never exceed the specified maximum fee. If the maximum fee is too low for the network’s demand at time of submission, the transaction will not be accepted.

Signatures

Before a transaction will be accepted by the network, it must be signed with at least one key. In the most basic case, the transaction only needs to be signed by the source account. This is done by calling .sign(KeyPair).

val transaction = Transaction(account, timeBounds = Unbounded, maxFee = NativeAmount(100)).add(operation)
val signedTransaction: SignedTransaction = transaction.sign(sourceKey.publicKey)

It may be that the source account has been modified to require more than one signature. Or, as mentioned earlier, one or more of the operations may affect other accounts. In either of these cases, the transaction will not be valid until it has received all necessary signatures.

val transaction = Transaction(jointAccount, timeBounds = Unbounded, maxFee = NativeAmount(100)).add(operation)
val signedTransaction: SignedTransaction = transaction.sign(aliceKey, bobKey)

Additionally, a transaction will fail if it has too many signatures.

Finally, a transaction may be signed with any arbitrary byte array in order to match a hash signer. See Hash(x) signing for a summary.

Submitting

Once a transaction is signed (and therefore is of type SignedTransaction) it can be submitted to the network.

val transaction = Transaction(account, timeBounds = Unbounded, maxFee = NativeAmount(100)).add(operation).sign(sourceKey)
val response: Future[TransactionPostResponse] = transaction.submit()

The eventual resulting contains metadata about the processed transaction, including the full results encoded as XDR. Additionally, the XDR can be decoded on the fly by calling the relevant convenience methods.

Transaction(account, timeBounds = Unbounded, maxFee = NativeAmount(100)).add(operation).sign(sourceKey).submit().foreach {
  case ok: TransactionApproved => println(ok.feeCharged)
  case ko => println(ko)
}

XDR

Transactions can be serialized to a base64-encoding of their XDR form. This is a strictly-defined format for transactions that is compatible across all supporting Stellar libraries and tooling. Given this, it is possible to save and load transaction state via XDR strings.

val encoded: String = txn.encodeXDR
val decoded: Transaction = Transaction.decodeXDR(encoded)
decoded must beEquivalentTo(txn)

Transactions with signatures are a different data structure (signatures are included in an envelope along with the transaction) and need to be decoded via a similar method on SignedTransaction.

val encoded: String = signedTxn.encodeXDR
val decoded: SignedTransaction = SignedTransaction.decodeXDR(encoded)
decoded must beEquivalentTo(signedTxn)

Meta Data

All transaction post and history responses include an XDR payload that describes the effects that the transaction had on the ledger. The field resultMetaXDR is the base64-encoded XDR payload. The method ledgerEntries will decode the payload into an instance of .

Similarly, the ledger effect of the fees is made available via the feeMetaXDR field and the feeLedgerEntries method.

Continue reading to learn how to obtain historical data from network via Queries.