@@ -2,13 +2,13 @@ package stellar.sdk
22
33import java .io .EOFException
44import java .time .{Instant , Period }
5-
65import com .typesafe .scalalogging .LazyLogging
76import okhttp3 .HttpUrl
87import okio .ByteString
98import org .json4s .JsonDSL ._
109import org .specs2 .concurrent .ExecutionEnv
1110import org .specs2 .mutable .Specification
11+ import org .stellar .xdr .TrustLineFlags
1212import stellar .sdk .inet .HorizonEntityNotFound
1313import stellar .sdk .model .Amount .lumens
1414import stellar .sdk .model .ClaimPredicate .{AbsolutelyBefore , Or , Unconditional }
@@ -40,12 +40,14 @@ class LocalNetworkIntegrationSpec(implicit ee: ExecutionEnv) extends Specificati
4040 val accnC = KeyPair .fromPassphrase(" account c" )
4141 val accnD = KeyPair .fromPassphrase(" account d" )
4242 val accnE = KeyPair .fromPassphrase(" account e" )
43+ val accnF = KeyPair .fromPassphrase(" account f" )
4344
4445 logger.debug(s " Account A = ${accnA.accountId}" )
4546 logger.debug(s " Account B = ${accnB.accountId}" )
4647 logger.debug(s " Account C = ${accnC.accountId}" )
4748 logger.debug(s " Account D = ${accnD.accountId}" )
4849 logger.debug(s " Account E = ${accnE.accountId}" )
50+ logger.debug(s " Account F = ${accnF.accountId}" )
4951
5052 val accounts = Set (accnA, accnB, accnC, accnD)
5153
@@ -101,6 +103,7 @@ class LocalNetworkIntegrationSpec(implicit ee: ExecutionEnv) extends Specificati
101103 private val chinchillaA = Asset (" Chinchilla" , accnA)
102104 private val chinchillaMaster = Asset (" Chinchilla" , masterAccountKey)
103105 private val dachshundB = Asset (" Dachshund" , accnB)
106+ private val clawbackAsset = Asset (" XYZ" , accnF)
104107
105108 // Transaction hashes. These will changed when setup operations change.
106109 private val txnHash2 = " e13447898b27dbf278d4411022e2e6d0aae78ef70670c7af7834a1f2a6d191d8"
@@ -116,6 +119,7 @@ class LocalNetworkIntegrationSpec(implicit ee: ExecutionEnv) extends Specificati
116119 CreateAccountOperation (accnB.toAccountId, lumens(1000 )),
117120 CreateAccountOperation (accnC.toAccountId, lumens(1000 )),
118121 CreateAccountOperation (accnD.toAccountId, lumens(1000 )),
122+ CreateAccountOperation (accnF.toAccountId, lumens(1000 )),
119123 WriteDataOperation (" life_universe_everything" , " 42" , Some (accnB)),
120124 WriteDataOperation (" brain the size of a planet" , " and they ask me to open a door" , Some (accnB)),
121125 WriteDataOperation (" fenton" , " FENTON!" , Some (accnC)),
@@ -272,6 +276,8 @@ class LocalNetworkIntegrationSpec(implicit ee: ExecutionEnv) extends Specificati
272276 " list all assets" >> {
273277 val eventualResps = network.assets().map(_.toSeq)
274278 eventualResps must containTheSameElementsAs(Seq (
279+ AssetResponse (aardvarkA, 0 , 0 , authRequired = true , authRevocable = true ),
280+ AssetResponse (beaverA, 0 , 0 , authRequired = true , authRevocable = true ),
275281 AssetResponse (chinchillaA, 101 , 1 , authRequired = true , authRevocable = true ),
276282 AssetResponse (chinchillaMaster, 101 , 1 , authRequired = false , authRevocable = false ),
277283 )).awaitFor(10 seconds)
@@ -285,7 +291,7 @@ class LocalNetworkIntegrationSpec(implicit ee: ExecutionEnv) extends Specificati
285291
286292 " filter assets by issuer" >> {
287293 val byIssuer = network.assets(issuer = Some (accnA)).map(_.take(10 ).toList)
288- byIssuer.map(_.size) must beEqualTo(1 ).awaitFor(10 seconds)
294+ byIssuer.map(_.size) must beEqualTo(3 ).awaitFor(10 seconds)
289295 byIssuer.map(_.map(_.asset.issuer.accountId).distinct) must beEqualTo(Seq (accnA.accountId)).awaitFor(10 seconds)
290296 }
291297
@@ -300,7 +306,7 @@ class LocalNetworkIntegrationSpec(implicit ee: ExecutionEnv) extends Specificati
300306 " effect endpoint" should {
301307 " parse all effects" >> {
302308 val effects = network.effects()
303- effects.map(_.size) must beEqualTo(240 ).awaitFor(10 seconds)
309+ effects.map(_.size) must beEqualTo(246 ).awaitFor(10 seconds)
304310 }
305311
306312 " filter effects by account" >> {
@@ -329,13 +335,16 @@ class LocalNetworkIntegrationSpec(implicit ee: ExecutionEnv) extends Specificati
329335 EffectAccountDebited (_, _, accn1, amount1),
330336 EffectAccountCredited (_, _, accn2, amount2),
331337 EffectAccountRemoved (_, _, accn3),
332- EffectTrustLineDeauthorized (_, created, accn4, IssuedAsset12 (code, accn5))
338+ EffectTrustLineDeauthorized (_, _, accn4, IssuedAsset12 (code, accn5)),
339+ EffectTrustLineFlagsUpdated (_, _, accn6, asset5, false , false , false )
333340 ) =>
334341 accn1 must beEquivalentTo(accnC)
335342 accn2 must beEquivalentTo(accnB)
336343 accn3 must beEquivalentTo(accnC)
337344 accn4 must beEquivalentTo(accnB)
338345 accn5 must beEquivalentTo(accnA)
346+ accn6 must beEquivalentTo(accnB)
347+ asset5 mustEqual aardvarkA
339348 amount1 mustEqual lumens(1000 )
340349 amount2 mustEqual lumens(1000 )
341350 code mustEqual " Aardvark"
@@ -458,7 +467,7 @@ class LocalNetworkIntegrationSpec(implicit ee: ExecutionEnv) extends Specificati
458467
459468 " operation endpoint" should {
460469 " list all operations" >> {
461- network.operations().map(_.size) must beEqualTo(131 ).awaitFor(10 .seconds)
470+ network.operations().map(_.size) must beEqualTo(132 ).awaitFor(10 .seconds)
462471 }
463472
464473 " list operations by account" >> {
@@ -595,8 +604,8 @@ class LocalNetworkIntegrationSpec(implicit ee: ExecutionEnv) extends Specificati
595604 byAccount.map(_.head) must beLike[TransactionHistory ] {
596605 case t =>
597606 t.account must beEquivalentTo(masterAccountKey)
598- t.feeCharged mustEqual NativeAmount (1700 )
599- t.operationCount mustEqual 17
607+ t.feeCharged mustEqual NativeAmount (1800 )
608+ t.operationCount mustEqual 18
600609 t.memo mustEqual NoMemo
601610 t.ledgerEntries must not(throwAn[EOFException ])
602611 t.feeLedgerEntries must not(throwAn[EOFException ])
@@ -877,5 +886,113 @@ class LocalNetworkIntegrationSpec(implicit ee: ExecutionEnv) extends Specificati
877886 }.awaitFor(5 .seconds)
878887 }
879888 }
889+
890+ " Clawbacks" should {
891+ " be used to revoke funds from an account" >> {
892+ val setClawbackEnabled = for {
893+ account <- network.account(masterAccountKey)
894+ txn = Transaction (
895+ source = account,
896+ operations = List (
897+ SetOptionsOperation (
898+ setFlags = Some (Set (AuthorizationRevocableFlag , AuthorizationClawbackEnabledFlag )),
899+ sourceAccount = Some (accnF)
900+ ),
901+ ChangeTrustOperation (IssuedAmount (99_999L , clawbackAsset), Some (accnA)),
902+ PaymentOperation (accnA.toAccountId, IssuedAmount (50_000L , clawbackAsset), Some (accnF))
903+ ),
904+ timeBounds = TimeBounds .Unbounded ,
905+ maxFee = NativeAmount (1000 )
906+ ).sign(masterAccountKey, accnF, accnA)
907+ txnResult <- txn.submit()
908+ } yield txnResult
909+ setClawbackEnabled must beLike[TransactionPostResponse ] { case r : TransactionApproved =>
910+ r.isSuccess must beTrue
911+ }.awaitFor(60 .seconds)
912+ network.account(accnA).map(_.balances) must beLike[List [Balance ]] { balances =>
913+ balances must contain(Balance (IssuedAmount (50_000L , clawbackAsset), Some (99_999L ), authorized = true , authorizedToMaintainLiabilities = true ))
914+ }.awaitFor(3 .seconds)
915+
916+ val clawbackTheFunds = for {
917+ account <- network.account(accnF)
918+ txn = Transaction (
919+ source = account,
920+ operations = List (
921+ ClawBackOperation (accnA.toAccountId, IssuedAmount (40_000L , clawbackAsset), Some (accnF))
922+ ),
923+ timeBounds = TimeBounds .Unbounded ,
924+ maxFee = NativeAmount (1000 )
925+ ).sign(accnF)
926+ txnResult <- txn.submit()
927+ } yield txnResult
928+ clawbackTheFunds must beLike[TransactionPostResponse ] { case r : TransactionApproved =>
929+ r.isSuccess must beTrue
930+ }.awaitFor(60 .seconds)
931+
932+ network.account(accnA).map(_.balances) must beLike[List [Balance ]] { balances =>
933+ balances must contain(Balance (IssuedAmount (10_000L , clawbackAsset), Some (99_999L ), authorized = true , authorizedToMaintainLiabilities = true ))
934+ }.awaitFor(3 .seconds)
935+ }
936+
937+ " be able to claw back claimable balances" >> {
938+ Await .result(for {
939+ account <- network.account(accnF)
940+ txn = Transaction (
941+ source = account,
942+ operations = List (
943+ CreateClaimableBalanceOperation (
944+ amount = Amount (1000 , clawbackAsset),
945+ claimants = List (AccountIdClaimant (accnA, Unconditional ))
946+ )
947+ ),
948+ timeBounds = TimeBounds .Unbounded ,
949+ maxFee = NativeAmount (200 )
950+ ).sign(accnF)
951+ txnResult <- txn.submit()
952+ } yield txnResult must beLike[TransactionPostResponse ] { case r : TransactionApproved =>
953+ r.isSuccess must beTrue
954+ }, 60 .seconds)
955+
956+ val clawbackTheFunds = for {
957+ balanceId <- network.claimsByClaimant(accnA).map(_.head.id)
958+ account <- network.account(accnF)
959+ txn = Transaction (
960+ source = account,
961+ operations = List (ClawBackClaimableBalanceOperation (balanceId)),
962+ timeBounds = TimeBounds .Unbounded ,
963+ maxFee = NativeAmount (1000 )
964+ ).sign(accnF)
965+ txnResult <- txn.submit()
966+ } yield txnResult
967+ clawbackTheFunds must beLike[TransactionPostResponse ] { case r : TransactionApproved =>
968+ r.isSuccess must beTrue
969+ }.awaitFor(60 .seconds)
970+
971+ network.claimsByAsset(dachshundB) must beEmpty[Seq [ClaimableBalance ]].awaitFor(10 .seconds)
972+ }
973+ }
974+
975+ " Trustline flags" should {
976+ " be settable" >> {
977+ (for {
978+ account <- network.account(accnF)
979+ txn = Transaction (
980+ source = account,
981+ operations = List (SetTrustLineFlagsOperation (
982+ asset = clawbackAsset,
983+ trustor = accnA,
984+ setFlags = Set (TrustLineFlags .AUTHORIZED_FLAG ),
985+ clearFlags = Set .empty[TrustLineFlags ]
986+ )),
987+ timeBounds = TimeBounds .Unbounded ,
988+ maxFee = NativeAmount (1000 )
989+ ).sign(accnF)
990+ txnResult <- txn.submit()
991+ } yield txnResult) must beLike[TransactionPostResponse ] { case r : TransactionApproved =>
992+ r.isSuccess must beTrue
993+ }.awaitFor(60 .seconds)
994+
995+ }
996+ }
880997}
881998
0 commit comments