Skip to content
This repository has been archived by the owner on Apr 13, 2023. It is now read-only.

Commit

Permalink
Expose data field for accounts. (#91)
Browse files Browse the repository at this point in the history
* Expose data field for accounts.

This important and necessary field was not part of the domain object until now.

* Fix url ecoding for account data key

* Fix NetworkSpec expectation by encoding the data key

... just like the class under test does.

* update changelog
  • Loading branch information
Synesso authored Jun 10, 2019
1 parent 9472dd2 commit f78b223
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 15 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,22 @@ As this project is pre 1.0, breaking changes may happen for minor version bumps.

## Next version

_Nothing yet_

## 0.7.1

### Added

- [#74](https://github.com/Synesso/scala-stellar-sdk/issues/74) Failed network calls to Horizon will now automatically
retry several times.

### Fixed

- [#86](https://github.com/Synesso/scala-stellar-sdk/issues/86) Horizon response of `TooManyRequests` will result in
a `HorizonRateLimitExceeded` response. That exception type includes the duration until the next rate limit
window opens.
- [#70](https://github.com/Synesso/scala-stellar-sdk/issues/70) Account responses now include account data.
- [#76](https://github.com/Synesso/scala-stellar-sdk/issues/76) MemoIds are parsed as Longs and accept zero as a value.

## 0.7.0

Expand Down
23 changes: 17 additions & 6 deletions src/it/scala/stellar/sdk/LocalNetworkIntegrationSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class LocalNetworkIntegrationSpec(implicit ee: ExecutionEnv) extends Specificati
CreateAccountOperation(accnB, lumens(1000)),
CreateAccountOperation(accnC, lumens(1000)),
WriteDataOperation("life_universe_everything", "42", Some(accnB)),
WriteDataOperation("brain the size of a planet", "and they ask me to open a door", Some(accnB)),
WriteDataOperation("fenton", "FENTON!", Some(accnC)),
DeleteDataOperation("fenton", Some(accnC)),
SetOptionsOperation(setFlags = Some(Set(AuthorizationRequiredFlag, AuthorizationRevocableFlag)), sourceAccount = Some(accnA)),
Expand Down Expand Up @@ -152,12 +153,22 @@ class LocalNetworkIntegrationSpec(implicit ee: ExecutionEnv) extends Specificati
"account endpoint" should {
"fetch account response details" >> {
network.account(accnA) must beLike[AccountResponse] {
case AccountResponse(id, _, _, _, _, _, balances, _) =>
case AccountResponse(id, _, _, _, _, _, balances, _, data) =>
id mustEqual accnA
balances must containTheSameElementsAs(Seq(
Balance(lumens(1000.000495), buyingLiabilities = 1600),
Balance(IssuedAmount(1, Asset.apply("Chinchilla", masterAccountKey)), limit = Some(100000000))
))
data must beEmpty
}.awaitFor(30 seconds)
}

"provide access to custom data" >> {
network.account(accnB) must beLike[AccountResponse] { case r =>
r.data mustEqual Map(
"life_universe_everything" -> "42",
"brain the size of a planet" -> "and they ask me to open a door"
)
}.awaitFor(30 seconds)
}

Expand Down Expand Up @@ -195,7 +206,7 @@ class LocalNetworkIntegrationSpec(implicit ee: ExecutionEnv) extends Specificati
}

"fetch nothing if no data exists for the account" >> {
network.accountData(accnB, "brain_size_of_planet") must throwA[Exception].like { case HorizonEntityNotFound(uri, body) =>
network.accountData(accnB, "vogon poetry") must throwA[Exception].like { case HorizonEntityNotFound(uri, body) =>
body mustEqual ("type" -> "https://stellar.org/horizon-errors/not_found") ~
("title" -> "Resource Missing") ~
("status" -> 404) ~
Expand Down Expand Up @@ -239,7 +250,7 @@ class LocalNetworkIntegrationSpec(implicit ee: ExecutionEnv) extends Specificati
"effect endpoint" should {
"parse all effects" >> {
val effects = network.effects()
effects.map(_.size) must beEqualTo(230).awaitFor(10 seconds)
effects.map(_.size) must beEqualTo(231).awaitFor(10 seconds)
}

"filter effects by account" >> {
Expand Down Expand Up @@ -330,7 +341,7 @@ class LocalNetworkIntegrationSpec(implicit ee: ExecutionEnv) extends Specificati

"operation endpoint" should {
"list all operations" >> {
network.operations().map(_.size) must beEqualTo(127).awaitFor(10.seconds)
network.operations().map(_.size) must beEqualTo(128).awaitFor(10.seconds)
}

"list operations by account" >> {
Expand Down Expand Up @@ -446,8 +457,8 @@ class LocalNetworkIntegrationSpec(implicit ee: ExecutionEnv) extends Specificati
byAccount.map(_.head) must beLike[TransactionHistory] {
case t =>
t.account must beEquivalentTo(masterAccountKey)
t.feePaid mustEqual NativeAmount(1400)
t.operationCount mustEqual 14
t.feePaid mustEqual NativeAmount(1500)
t.operationCount mustEqual 15
t.memo mustEqual NoMemo
}.awaitFor(10.seconds)
}
Expand Down
8 changes: 6 additions & 2 deletions src/main/scala/stellar/sdk/Network.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package stellar.sdk

import java.net.URLEncoder
import java.nio.charset.StandardCharsets.UTF_8

import akka.NotUsed
Expand Down Expand Up @@ -49,8 +50,11 @@ trait Network extends LazyLogging {
* @param dataKey the key for the data field
* @see [[https://www.stellar.org/developers/horizon/reference/endpoints/data-for-account.html endpoint doc]]
*/
def accountData(pubKey: PublicKeyOps, dataKey: String)(implicit ec: ExecutionContext): Future[String] =
horizon.get[DataValueResponse](s"/accounts/${pubKey.accountId}/data/$dataKey").map(_.v).map(base64).map(new String(_, "UTF-8"))
def accountData(pubKey: PublicKeyOps, dataKey: String)(implicit ec: ExecutionContext): Future[String] = {
val encodedKey = URLEncoder.encode(dataKey, "UTF-8")
horizon.get[DataValueResponse](s"/accounts/${pubKey.accountId}/data/$encodedKey")
.map(_.v).map(base64).map(new String(_, UTF_8))
}

/**
* Fetch a stream of assets, optionally filtered by code, issuer or neither
Expand Down
10 changes: 7 additions & 3 deletions src/main/scala/stellar/sdk/model/response/AccountResponse.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package stellar.sdk.model.response

import org.apache.commons.codec.binary.Base64
import org.json4s.DefaultFormats
import org.json4s.JsonAST.{JArray, JObject}
import stellar.sdk.model.Amount.toBaseUnits
import stellar.sdk._
import stellar.sdk.model.Amount.toBaseUnits
import stellar.sdk.model._

case class AccountResponse(id: PublicKey,
Expand All @@ -13,7 +14,8 @@ case class AccountResponse(id: PublicKey,
authRequired: Boolean,
authRevocable: Boolean,
balances: List[Balance],
signers: List[Signer]) {
signers: List[Signer],
data: Map[String, String]) {

def toAccount: Account = Account(id, lastSequence + 1)
}
Expand Down Expand Up @@ -60,8 +62,10 @@ object AccountRespDeserializer extends ResponseParser[AccountResponse]({ o: JObj
Signer(key, weight)
case _ => throw new RuntimeException(s"Expected js object at 'signers'")
}
val JObject(dataFields) = o \ "data"
val data = dataFields.toMap.mapValues(_.extract[String]).mapValues(Base64.decodeBase64).mapValues(new String(_))

AccountResponse(id, seq, subEntryCount, Thresholds(lowThreshold, mediumThreshold, highThreshold), authRequired,
authRevocable, balances, signers)
authRevocable, balances, signers, data)

})
14 changes: 11 additions & 3 deletions src/test/scala/stellar/sdk/ArbitraryInput.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package stellar.sdk

import java.nio.charset.StandardCharsets.UTF_8
import java.time.temporal.ChronoField
import java.time.{Instant, ZoneId, ZonedDateTime}
import java.util.Locale
Expand All @@ -8,12 +9,12 @@ import org.apache.commons.codec.binary.Base64
import org.scalacheck.{Arbitrary, Gen}
import org.specs2.ScalaCheck
import stellar.sdk.model._
import stellar.sdk.util.ByteArrays.trimmedByteArray
import stellar.sdk.model.op._
import stellar.sdk.model.response._
import stellar.sdk.model.result.TransactionResult._
import stellar.sdk.model.result.{PathPaymentResult, _}
import stellar.sdk.model.response._
import stellar.sdk.util.ByteArrays
import stellar.sdk.util.ByteArrays.trimmedByteArray

import scala.util.Random

Expand Down Expand Up @@ -467,7 +468,14 @@ trait ArbitraryInput extends ScalaCheck {
authRevocable <- Gen.oneOf(true, false)
balances <- Gen.nonEmptyListOf(genBalance)
signers <- Gen.nonEmptyListOf(genSigner)
} yield AccountResponse(id, lastSequence, subEntryCount, thresholds, authRequired, authRevocable, balances, signers)
data <- genDataMap
} yield AccountResponse(id, lastSequence, subEntryCount, thresholds, authRequired, authRevocable, balances, signers, data)

def genDataMap: Gen[Map[String, String]] = for {
qty <- Gen.choose(0, 30)
keys <- Gen.listOfN(qty, Gen.identifier)
values <- Gen.listOfN(qty, Gen.asciiPrintableStr.map(_.getBytes(UTF_8)).map(Base64.encodeBase64String))
} yield keys.zip(values).toMap

def genSigner: Gen[Signer] = for {
weight <- Gen.choose(0, 255)
Expand Down
5 changes: 4 additions & 1 deletion src/test/scala/stellar/sdk/NetworkSpec.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package stellar.sdk

import java.net.URLEncoder

import akka.NotUsed
import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
Expand Down Expand Up @@ -77,7 +79,8 @@ class NetworkSpec(implicit ee: ExecutionEnv) extends Specification with Arbitrar
val network = new MockNetwork
val response = DataValueResponse(ByteArrays.base64(value.getBytes("UTF-8")))
val expected = Future(response)
network.horizon.get[DataValueResponse](s"/accounts/${pk.accountId}/data/$key") returns expected
val encodedKey = URLEncoder.encode(key, "UTF-8")
network.horizon.get[DataValueResponse](s"/accounts/${pk.accountId}/data/$encodedKey") returns expected
network.accountData(pk, key) must beEqualTo(value).await
}

Expand Down

0 comments on commit f78b223

Please sign in to comment.