mirror of
https://github.com/corda/corda.git
synced 2025-01-23 21:08:48 +00:00
action output contract check
This commit is contained in:
parent
dc9fb2a3d4
commit
334dfb9d7b
@ -42,16 +42,15 @@ class GenericContract : Contract {
|
|||||||
|
|
||||||
val value = cmd.value
|
val value = cmd.value
|
||||||
|
|
||||||
// val xxx = actions(state.details)
|
|
||||||
|
|
||||||
when (value) {
|
when (value) {
|
||||||
is Commands.Action -> {
|
is Commands.Action -> {
|
||||||
val inState = tx.inStates.single() as State
|
val inState = tx.inStates.single() as State
|
||||||
val actions = actions(inState.details)
|
val actions = actions(inState.details)
|
||||||
|
val actions2 = actions2(inState.details)
|
||||||
requireThat {
|
requireThat {
|
||||||
"action must be defined" by ( actions.containsKey(value.name) )
|
"action must be defined" by ( actions2.containsKey(value.name) )
|
||||||
"action must be authorized" by ( cmd.signers.any { actions[ value.name ]!!.contains(it) } )
|
"action must be authorized" by ( cmd.signers.any { actions[ value.name ]!!.contains(it) } )
|
||||||
|
"output state must match action result state" by ( actions2[ value.name ]!!.kontract.equals(outState.details))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Commands.Issue -> {
|
is Commands.Issue -> {
|
||||||
|
@ -12,41 +12,43 @@ import java.util.*
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
open class Kontract {
|
interface Kontract {
|
||||||
|
|
||||||
class Zero : Kontract()
|
|
||||||
|
|
||||||
// should be replaced with something that uses Corda assets and/or cash
|
|
||||||
class Transfer(val amount: Observable<Long>, val currency: Currency, val from: Party, val to: Party) : Kontract() {
|
|
||||||
constructor(amount: Amount<Currency>, from: Party, to: Party ) : this(const(amount.pennies), amount.token, from, to)
|
|
||||||
}
|
|
||||||
|
|
||||||
class And(val kontracts: Array<Kontract>) : Kontract()
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
class Action(val name: String, val condition: Observable<Boolean>, val actors: Array<Party>, val kontract: Kontract) : Kontract() {
|
|
||||||
constructor(name: String, condition: Observable<Boolean>, actor: Party, kontract: Kontract) : this(name, condition, arrayOf(actor), kontract)
|
|
||||||
}
|
|
||||||
|
|
||||||
// only actions can be or'ed together
|
|
||||||
class Or(val contracts: Array<Action>) : Kontract()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class Zero(val dummy: Int = 0) : Kontract
|
||||||
|
|
||||||
|
// should be replaced with something that uses Corda assets and/or cash
|
||||||
|
data class Transfer(val amount: Observable<Long>, val currency: Currency, val from: Party, val to: Party) : Kontract {
|
||||||
|
constructor(amount: Amount<Currency>, from: Party, to: Party ) : this(const(amount.pennies), amount.token, from, to)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class And(val kontracts: Array<Kontract>) : Kontract
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
data class Action(val name: String, val condition: Observable<Boolean>, val actors: Array<Party>, val kontract: Kontract) : Kontract {
|
||||||
|
constructor(name: String, condition: Observable<Boolean>, actor: Party, kontract: Kontract) : this(name, condition, arrayOf(actor), kontract)
|
||||||
|
}
|
||||||
|
|
||||||
|
// only actions can be or'ed together
|
||||||
|
data class Or(val contracts: Array<Action>) : Kontract
|
||||||
|
|
||||||
/** returns list of involved parties for a given contract */
|
/** returns list of involved parties for a given contract */
|
||||||
fun liableParties(contract: Kontract) : Set<PublicKey> {
|
fun liableParties(contract: Kontract) : Set<PublicKey> {
|
||||||
|
|
||||||
fun visit(contract: Kontract) : ImmutableSet<PublicKey> {
|
fun visit(contract: Kontract) : ImmutableSet<PublicKey> {
|
||||||
when (contract) {
|
when (contract) {
|
||||||
is Kontract.Zero -> return ImmutableSet.of<PublicKey>()
|
is Zero -> return ImmutableSet.of<PublicKey>()
|
||||||
is Kontract.Transfer -> return ImmutableSet.of(contract.from.owningKey)
|
is Transfer -> return ImmutableSet.of(contract.from.owningKey)
|
||||||
is Kontract.Action ->
|
is Action ->
|
||||||
if (contract.actors.size != 1)
|
if (contract.actors.size != 1)
|
||||||
return visit(contract.kontract)
|
return visit(contract.kontract)
|
||||||
else
|
else
|
||||||
return Sets.difference(visit(contract.kontract), ImmutableSet.of(contract.actors.single())).immutableCopy()
|
return Sets.difference(visit(contract.kontract), ImmutableSet.of(contract.actors.single())).immutableCopy()
|
||||||
is Kontract.And -> return contract.kontracts.fold( ImmutableSet.builder<PublicKey>(), { builder, k -> builder.addAll( visit(k)) } ).build()
|
is And ->
|
||||||
is Kontract.Or -> return contract.contracts.fold( ImmutableSet.builder<PublicKey>(), { builder, k -> builder.addAll( visit(k)) } ).build()
|
return contract.kontracts.fold( ImmutableSet.builder<PublicKey>(), { builder, k -> builder.addAll( visit(k)) } ).build()
|
||||||
|
is Or ->
|
||||||
|
return contract.contracts.fold( ImmutableSet.builder<PublicKey>(), { builder, k -> builder.addAll( visit(k)) } ).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
throw IllegalArgumentException()
|
throw IllegalArgumentException()
|
||||||
@ -58,13 +60,22 @@ fun liableParties(contract: Kontract) : Set<PublicKey> {
|
|||||||
fun actions(contract: Kontract) : Map<String, Set<PublicKey>> {
|
fun actions(contract: Kontract) : Map<String, Set<PublicKey>> {
|
||||||
|
|
||||||
when (contract) {
|
when (contract) {
|
||||||
is Kontract.Zero -> return mapOf()
|
is Zero -> return mapOf()
|
||||||
is Kontract.Transfer -> return mapOf()
|
is Transfer -> return mapOf()
|
||||||
is Kontract.Action -> return mapOf( contract.name to contract.actors.map { it.owningKey }.toSet() )
|
is Action -> return mapOf( contract.name to contract.actors.map { it.owningKey }.toSet() )
|
||||||
is Kontract.Or -> {
|
is Or -> return contract.contracts.map { it.name to it.actors.map { it.owningKey }.toSet() }.toMap()
|
||||||
val xx = contract.contracts.map { it.name to it.actors.map { it.owningKey }.toSet() }.toMap()
|
}
|
||||||
return xx
|
|
||||||
}
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun actions2(contract: Kontract) : Map<String, Action> {
|
||||||
|
|
||||||
|
when (contract) {
|
||||||
|
is Zero -> return mapOf()
|
||||||
|
is Transfer -> return mapOf()
|
||||||
|
is Action -> return mapOf( contract.name to contract )
|
||||||
|
is Or -> return contract.contracts.map { it.name to it }.toMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
throw IllegalArgumentException()
|
throw IllegalArgumentException()
|
||||||
|
@ -9,7 +9,7 @@ import java.util.*
|
|||||||
* Created by sofusmortensen on 23/05/16.
|
* Created by sofusmortensen on 23/05/16.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
open class Observable<T>
|
interface Observable<T>
|
||||||
|
|
||||||
enum class Comparison {
|
enum class Comparison {
|
||||||
LT, LTE, GT, GTE
|
LT, LTE, GT, GTE
|
||||||
@ -18,14 +18,14 @@ enum class Comparison {
|
|||||||
/**
|
/**
|
||||||
* Constant observable
|
* Constant observable
|
||||||
*/
|
*/
|
||||||
class Const<T>(val value: T) : Observable<T>()
|
data class Const<T>(val value: T) : Observable<T>
|
||||||
|
|
||||||
fun<T> const(k: T) = Const(k)
|
fun<T> const(k: T) = Const(k)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable based on time
|
* Observable based on time
|
||||||
*/
|
*/
|
||||||
class TimeObservable(val cmp: Comparison, val instant: Instant) : Observable<Boolean>()
|
data class TimeObservable(val cmp: Comparison, val instant: Instant) : Observable<Boolean>
|
||||||
|
|
||||||
fun parseInstant(str: String) = DateFormat.getDateInstance(DateFormat.SHORT, Locale.US).parse(str).toInstant()
|
fun parseInstant(str: String) = DateFormat.getDateInstance(DateFormat.SHORT, Locale.US).parse(str).toInstant()
|
||||||
|
|
||||||
@ -34,17 +34,17 @@ fun after(expiry: Instant) = TimeObservable(Comparison.GTE, expiry)
|
|||||||
fun before(expiry: String) = TimeObservable(Comparison.LTE, parseInstant(expiry))
|
fun before(expiry: String) = TimeObservable(Comparison.LTE, parseInstant(expiry))
|
||||||
fun after(expiry: String) = TimeObservable(Comparison.GTE, parseInstant(expiry))
|
fun after(expiry: String) = TimeObservable(Comparison.GTE, parseInstant(expiry))
|
||||||
|
|
||||||
class ObservableAnd(val left: Observable<Boolean>, val right: Observable<Boolean>) : Observable<Boolean>()
|
data class ObservableAnd(val left: Observable<Boolean>, val right: Observable<Boolean>) : Observable<Boolean>
|
||||||
infix fun Observable<Boolean>.and(obs: Observable<Boolean>) = ObservableAnd(this, obs)
|
infix fun Observable<Boolean>.and(obs: Observable<Boolean>) = ObservableAnd(this, obs)
|
||||||
|
|
||||||
class ObservableOr(val left: Observable<Boolean>, val right: Observable<Boolean>) : Observable<Boolean>()
|
data class ObservableOr(val left: Observable<Boolean>, val right: Observable<Boolean>) : Observable<Boolean>
|
||||||
infix fun Observable<Boolean>.or(obs: Observable<Boolean>) = ObservableOr(this, obs)
|
infix fun Observable<Boolean>.or(obs: Observable<Boolean>) = ObservableOr(this, obs)
|
||||||
|
|
||||||
class CurrencyCross(val foreign: Currency, val domestic: Currency) : Observable<BigDecimal>()
|
data class CurrencyCross(val foreign: Currency, val domestic: Currency) : Observable<BigDecimal>
|
||||||
|
|
||||||
operator fun Currency.div(currency: Currency) = CurrencyCross(this, currency)
|
operator fun Currency.div(currency: Currency) = CurrencyCross(this, currency)
|
||||||
|
|
||||||
class ObservableComparison<T>(val left: Observable<T>, val cmp: Comparison, val right: Observable<T>) : Observable<Boolean>()
|
data class ObservableComparison<T>(val left: Observable<T>, val cmp: Comparison, val right: Observable<T>) : Observable<Boolean>
|
||||||
|
|
||||||
infix fun Observable<BigDecimal>.lt(n: BigDecimal) = ObservableComparison<BigDecimal>(this, Comparison.LT, const(n))
|
infix fun Observable<BigDecimal>.lt(n: BigDecimal) = ObservableComparison<BigDecimal>(this, Comparison.LT, const(n))
|
||||||
infix fun Observable<BigDecimal>.gt(n: BigDecimal) = ObservableComparison<BigDecimal>(this, Comparison.GT, const(n))
|
infix fun Observable<BigDecimal>.gt(n: BigDecimal) = ObservableComparison<BigDecimal>(this, Comparison.GT, const(n))
|
||||||
@ -60,7 +60,7 @@ enum class Operation {
|
|||||||
PLUS, MINUS, TIMES, DIV
|
PLUS, MINUS, TIMES, DIV
|
||||||
}
|
}
|
||||||
|
|
||||||
class ObservableOperation<T>(val left: Observable<T>, val op: Operation, val right: Observable<T>) : Observable<T>()
|
data class ObservableOperation<T>(val left: Observable<T>, val op: Operation, val right: Observable<T>) : Observable<T>
|
||||||
|
|
||||||
infix fun Observable<BigDecimal>.plus(n: BigDecimal) = ObservableOperation<BigDecimal>(this, Operation.PLUS, const(n))
|
infix fun Observable<BigDecimal>.plus(n: BigDecimal) = ObservableOperation<BigDecimal>(this, Operation.PLUS, const(n))
|
||||||
infix fun Observable<BigDecimal>.minus(n: BigDecimal) = ObservableOperation<BigDecimal>(this, Operation.MINUS, const(n))
|
infix fun Observable<BigDecimal>.minus(n: BigDecimal) = ObservableOperation<BigDecimal>(this, Operation.MINUS, const(n))
|
||||||
|
@ -11,12 +11,12 @@ import java.util.*
|
|||||||
val Int.M: Long get() = this.toLong() * 1000000
|
val Int.M: Long get() = this.toLong() * 1000000
|
||||||
val Int.K: Long get() = this.toLong() * 1000
|
val Int.K: Long get() = this.toLong() * 1000
|
||||||
|
|
||||||
val zero = Kontract.Zero()
|
val zero = Zero()
|
||||||
|
|
||||||
infix fun Kontract.and(kontract: Kontract) = Kontract.And( arrayOf(this, kontract) )
|
infix fun Kontract.and(kontract: Kontract) = And( arrayOf(this, kontract) )
|
||||||
infix fun Kontract.Action.or(kontract: Kontract.Action) = Kontract.Or( arrayOf(this, kontract) )
|
infix fun Action.or(kontract: Action) = Or( arrayOf(this, kontract) )
|
||||||
infix fun Kontract.Or.or(kontract: Kontract.Action) = Kontract.Or( this.contracts.plusElement( kontract ) )
|
infix fun Or.or(kontract: Action) = Or( this.contracts.plusElement( kontract ) )
|
||||||
infix fun Kontract.Or.or(ors: Kontract.Or) = Kontract.Or( this.contracts.plus(ors.contracts) )
|
infix fun Or.or(ors: Or) = Or( this.contracts.plus(ors.contracts) )
|
||||||
|
|
||||||
operator fun Long.times(currency: Currency) = Amount(this.toLong(), currency)
|
operator fun Long.times(currency: Currency) = Amount(this.toLong(), currency)
|
||||||
operator fun Double.times(currency: Currency) = Amount(BigDecimal(this.toDouble()), currency)
|
operator fun Double.times(currency: Currency) = Amount(BigDecimal(this.toDouble()), currency)
|
@ -9,18 +9,18 @@ import java.util.*
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
fun swap(partyA: Party, amountA: Amount<Currency>, partyB: Party, amountB: Amount<Currency>) =
|
fun swap(partyA: Party, amountA: Amount<Currency>, partyB: Party, amountB: Amount<Currency>) =
|
||||||
Kontract.Transfer(amountA, partyA, partyB) and Kontract.Transfer(amountB, partyB, partyA)
|
Transfer(amountA, partyA, partyB) and Transfer(amountB, partyB, partyA)
|
||||||
|
|
||||||
fun fx_swap(expiry: String, notional: Long, strike: Double,
|
fun fx_swap(expiry: String, notional: Long, strike: Double,
|
||||||
foreignCurrency: Currency, domesticCurrency: Currency,
|
foreignCurrency: Currency, domesticCurrency: Currency,
|
||||||
partyA: Party, partyB: Party) =
|
partyA: Party, partyB: Party) =
|
||||||
Kontract.Action("execute", after(expiry), arrayOf(partyA, partyB),
|
Action("execute", after(expiry), arrayOf(partyA, partyB),
|
||||||
Kontract.Transfer(notional * strike * domesticCurrency, partyA, partyB)
|
Transfer(notional * strike * domesticCurrency, partyA, partyB)
|
||||||
and Kontract.Transfer(notional * foreignCurrency, partyB, partyA))
|
and Transfer(notional * foreignCurrency, partyB, partyA))
|
||||||
|
|
||||||
// building an fx swap using abstract swap
|
// building an fx swap using abstract swap
|
||||||
fun fx_swap2(expiry: String, notional: Long, strike: Double,
|
fun fx_swap2(expiry: String, notional: Long, strike: Double,
|
||||||
foreignCurrency: Currency, domesticCurrency: Currency,
|
foreignCurrency: Currency, domesticCurrency: Currency,
|
||||||
partyA: Party, partyB: Party) =
|
partyA: Party, partyB: Party) =
|
||||||
Kontract.Action("execute", after(expiry), arrayOf(partyA, partyB),
|
Action("execute", after(expiry), arrayOf(partyA, partyB),
|
||||||
swap(partyA, notional * strike * domesticCurrency, partyB, notional * foreignCurrency))
|
swap(partyA, notional * strike * domesticCurrency, partyB, notional * foreignCurrency))
|
||||||
|
@ -9,12 +9,14 @@ import java.util.*
|
|||||||
* Created by sofusmortensen on 23/05/16.
|
* Created by sofusmortensen on 23/05/16.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
class DummyObservable<T> : Observable<T>
|
||||||
|
|
||||||
// observable of type T
|
// observable of type T
|
||||||
// example:
|
// example:
|
||||||
val acmeCorporationHasDefaulted = Observable<Boolean>()
|
val acmeCorporationHasDefaulted = DummyObservable<Boolean>()
|
||||||
|
|
||||||
// example:
|
// example:
|
||||||
val euribor3monthFixing = Observable<BigDecimal>()
|
val euribor3monthFixing = DummyObservable<BigDecimal>()
|
||||||
|
|
||||||
val roadRunner = Party("Road Runner", generateKeyPair().public)
|
val roadRunner = Party("Road Runner", generateKeyPair().public)
|
||||||
val wileECoyote = Party("Wile E. Coyote", generateKeyPair().public)
|
val wileECoyote = Party("Wile E. Coyote", generateKeyPair().public)
|
||||||
|
@ -12,14 +12,14 @@ class Builder2 {
|
|||||||
val contracts = mutableListOf<Kontract>()
|
val contracts = mutableListOf<Kontract>()
|
||||||
|
|
||||||
fun Party.gives(beneficiary: Party, amount: Amount<Currency>) {
|
fun Party.gives(beneficiary: Party, amount: Amount<Currency>) {
|
||||||
contracts.add( Kontract.Transfer(amount, this, beneficiary))
|
contracts.add( Transfer(amount, this, beneficiary))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun final() =
|
fun final() =
|
||||||
when (contracts.size) {
|
when (contracts.size) {
|
||||||
0 -> zero
|
0 -> zero
|
||||||
1 -> contracts[0]
|
1 -> contracts[0]
|
||||||
else -> Kontract.And(contracts.toTypedArray())
|
else -> And(contracts.toTypedArray())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,12 +28,12 @@ interface GivenThatResolve {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Builder(val actors: Array<Party>) {
|
class Builder(val actors: Array<Party>) {
|
||||||
val actions = mutableListOf<Kontract.Action>()
|
val actions = mutableListOf<Action>()
|
||||||
|
|
||||||
fun String.givenThat(condition: Observable<Boolean>, init: Builder2.() -> Unit ) {
|
fun String.givenThat(condition: Observable<Boolean>, init: Builder2.() -> Unit ) {
|
||||||
val b = Builder2()
|
val b = Builder2()
|
||||||
b.init()
|
b.init()
|
||||||
actions.add( Kontract.Action(this, condition, actors, b.final() ) )
|
actions.add( Action(this, condition, actors, b.final() ) )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ class Builder(val actors: Array<Party>) {
|
|||||||
val This = this
|
val This = this
|
||||||
return object : GivenThatResolve {
|
return object : GivenThatResolve {
|
||||||
override fun resolve(contract: Kontract) {
|
override fun resolve(contract: Kontract) {
|
||||||
actions.add(Kontract.Action(This, condition, actors, contract))
|
actions.add(Action(This, condition, actors, contract))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -49,20 +49,20 @@ class Builder(val actors: Array<Party>) {
|
|||||||
fun String.anytime(init: Builder2.() -> Unit ) {
|
fun String.anytime(init: Builder2.() -> Unit ) {
|
||||||
val b = Builder2()
|
val b = Builder2()
|
||||||
b.init()
|
b.init()
|
||||||
actions.add( Kontract.Action(this, const(true), actors, b.final() ) )
|
actions.add( Action(this, const(true), actors, b.final() ) )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Party.may(init: Builder.() -> Unit) : Kontract.Or {
|
fun Party.may(init: Builder.() -> Unit) : Or {
|
||||||
val b = Builder(arrayOf(this))
|
val b = Builder(arrayOf(this))
|
||||||
b.init()
|
b.init()
|
||||||
return Kontract.Or(b.actions.toTypedArray())
|
return Or(b.actions.toTypedArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Array<Party>.may(init: Builder.() -> Unit) : Kontract.Or {
|
fun Array<Party>.may(init: Builder.() -> Unit) : Or {
|
||||||
val b = Builder(this)
|
val b = Builder(this)
|
||||||
b.init()
|
b.init()
|
||||||
return Kontract.Or(b.actions.toTypedArray())
|
return Or(b.actions.toTypedArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
infix fun Party.or(party: Party) = arrayOf(this, party)
|
infix fun Party.or(party: Party) = arrayOf(this, party)
|
||||||
|
@ -18,10 +18,18 @@ class ZCB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val transfer = kontract { wileECoyote.gives(roadRunner, 100.K*GBP) }
|
val transfer = kontract { wileECoyote.gives(roadRunner, 100.K*GBP) }
|
||||||
|
val transferWrong = kontract { wileECoyote.gives(roadRunner, 80.K*GBP) }
|
||||||
|
|
||||||
val inState = GenericContract.State( DUMMY_NOTARY, contract )
|
val inState = GenericContract.State( DUMMY_NOTARY, contract )
|
||||||
|
|
||||||
val outState = GenericContract.State( DUMMY_NOTARY, transfer )
|
val outState = GenericContract.State( DUMMY_NOTARY, transfer )
|
||||||
|
val outStateWrong = GenericContract.State( DUMMY_NOTARY, transferWrong )
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun basic() {
|
||||||
|
assert( Zero().equals(Zero()))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `issue - signature`() {
|
fun `issue - signature`() {
|
||||||
@ -60,7 +68,7 @@ class ZCB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `execute - authorized`() {
|
fun `execute - not authorized`() {
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
output { outState }
|
output { outState }
|
||||||
@ -70,4 +78,15 @@ class ZCB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `execute - outState mismatch`() {
|
||||||
|
transaction {
|
||||||
|
input { inState }
|
||||||
|
output { outStateWrong }
|
||||||
|
|
||||||
|
arg(roadRunner.owningKey) { GenericContract.Commands.Action("execute") }
|
||||||
|
this `fails requirement` "output state must match action result state"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user