mirror of
https://github.com/corda/corda.git
synced 2025-01-21 03:55:00 +00:00
Docs: fix a few typos and rewrap a few code samples in the state machines article.
This commit is contained in:
parent
bf647f6c15
commit
e3cfe0ae49
@ -57,7 +57,7 @@ Theory
|
||||
|
||||
A *continuation* is a suspended stack frame stored in a regular object that can be passed around, serialised,
|
||||
unserialised and resumed from where it was suspended. This may sound abstract but don't worry, the examples below
|
||||
will make it clearer. The JVM does not natively support continuations, so we implement them using a a library called
|
||||
will make it clearer. The JVM does not natively support continuations, so we implement them using a library called
|
||||
JavaFlow which works through behind-the-scenes bytecode rewriting. You don't have to know how this works to benefit
|
||||
from it, however.
|
||||
|
||||
@ -153,9 +153,9 @@ Let's unpack what this code does:
|
||||
to be unguessable. 63 bits is good enough.
|
||||
|
||||
Alright, so using this protocol shouldn't be too hard: in the simplest case we can just pass in the details of the trade
|
||||
to either runBuyer or runSeller, depending on who we are, and then call ``.get()`` on the resulting future to block the
|
||||
calling thread until the protocol has finished. Or we could register a callback on the returned future that will be
|
||||
invoked when it's done, where we could e.g. update a user interface.
|
||||
to either runBuyer or runSeller, depending on who we are, and then call ``.resultFuture.get()`` on resulting object to
|
||||
block the calling thread until the protocol has finished. Or we could register a callback on the returned future that
|
||||
will be invoked when it's done, where we could e.g. update a user interface.
|
||||
|
||||
The only tricky part is how to get one of these things. We need a ``StateMachineManager``. Where does that come from
|
||||
and why do we need one?
|
||||
@ -221,7 +221,8 @@ It could be as simple as a chat room or as complex as a 24/7 exchange.
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
// This object is serialised to the network and is the first protocol message the seller sends to the buyer.
|
||||
// This object is serialised to the network and is the first protocol message
|
||||
// the seller sends to the buyer.
|
||||
class SellerTradeInfo(
|
||||
val assetForSale: StateAndRef<OwnableState>,
|
||||
val price: Amount,
|
||||
@ -244,8 +245,8 @@ Next we add some code to the ``SellerImpl.call`` method:
|
||||
// Make the first message we'll send to kick off the protocol.
|
||||
val hello = SellerTradeInfo(args.assetToSell, args.price, args.myKeyPair.public, sessionID)
|
||||
|
||||
// Zero is a special session ID that is being listened to by the buyer (i.e. before a session is started).
|
||||
val partialTX = sendAndReceive<SignedWireTransaction>(TRADE_TOPIC, args.buyerSessionID, sessionID, hello)
|
||||
val partialTX = sendAndReceive<SignedWireTransaction>(TRADE_TOPIC, args.buyerSessionID,
|
||||
sessionID, hello)
|
||||
logger().trace { "Received partially signed transaction" }
|
||||
|
||||
That's pretty straightforward. We generate a session ID to identify what's happening on the seller side, fill out
|
||||
@ -347,16 +348,18 @@ OK, let's do the same for the buyer side:
|
||||
if (!args.typeToBuy.isInstance(tradeRequest.assetForSale.state))
|
||||
throw AssetMismatchException(args.typeToBuy.name, assetTypeName)
|
||||
|
||||
// TODO: Either look up the stateref here in our local db, or accept a long chain of states and
|
||||
// validate them to audit the other side and ensure it actually owns the state we are being offered!
|
||||
// For now, just assume validity!
|
||||
// TODO: Either look up the stateref here in our local db, or accept a long chain
|
||||
// of states and validate them to audit the other side and ensure it actually owns
|
||||
// the state we are being offered! For now, just assume validity!
|
||||
|
||||
// Generate the shared transaction that both sides will sign, using the data we have.
|
||||
val ptx = PartialTransaction()
|
||||
// Add input and output states for the movement of cash, by using the Cash contract to generate the states.
|
||||
// Add input and output states for the movement of cash, by using the Cash contract
|
||||
// to generate the states.
|
||||
val wallet = serviceHub.walletService.currentWallet
|
||||
val cashStates = wallet.statesOfType<Cash.State>()
|
||||
val cashSigningPubKeys = Cash().craftSpend(ptx, tradeRequest.price, tradeRequest.sellerOwnerKey, cashStates)
|
||||
val cashSigningPubKeys = Cash().craftSpend(ptx, tradeRequest.price,
|
||||
tradeRequest.sellerOwnerKey, cashStates)
|
||||
// Add inputs/outputs/a command for the movement of the asset.
|
||||
ptx.addInputState(tradeRequest.assetForSale.ref)
|
||||
// Just pick some new public key for now.
|
||||
@ -378,14 +381,17 @@ OK, let's do the same for the buyer side:
|
||||
|
||||
logger().trace { "Sending partially signed transaction to seller" }
|
||||
|
||||
// TODO: Protect against the buyer terminating here and leaving us in the lurch without the final tx.
|
||||
// TODO: Protect against a malicious buyer sending us back a different transaction to the one we built.
|
||||
// TODO: Protect against the buyer terminating here and leaving us in the lurch without
|
||||
// the final tx.
|
||||
// TODO: Protect against a malicious buyer sending us back a different transaction to
|
||||
// the one we built.
|
||||
val fullySigned = sendAndReceive<TimestampedWireTransaction>(TRADE_TOPIC,
|
||||
tradeRequest.sessionID, args.sessionID, stx)
|
||||
|
||||
logger().trace { "Got fully signed transaction, verifying ... "}
|
||||
|
||||
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.timestampingService, serviceHub.identityService)
|
||||
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.timestampingService,
|
||||
serviceHub.identityService)
|
||||
|
||||
logger().trace { "Fully signed transaction was valid. Trade complete! :-)" }
|
||||
|
||||
@ -396,7 +402,8 @@ OK, let's do the same for the buyer side:
|
||||
This code is fairly straightforward. Here are some things to pay attention to:
|
||||
|
||||
1. We do some sanity checking on the received message to ensure we're being offered what we expected to be offered.
|
||||
2. We create a cash spend in the normal way, by using ``Cash().craftSpend``.
|
||||
2. We create a cash spend in the normal way, by using ``Cash().craftSpend``. See the contracts tutorial if this isn't
|
||||
clear.
|
||||
3. We access the *service hub* when we need it to access things that are transient and may change or be recreated
|
||||
whilst a protocol is suspended, things like the wallet or the timestamping service. Remember that a protocol may
|
||||
be suspended when it waits to receive a message across node or computer restarts, so objects representing a service
|
||||
@ -411,6 +418,6 @@ the fact that it takes minimal resources and can survive node restarts.
|
||||
If you do this then next time your protocol waits to receive an object, the system will try and serialise all your
|
||||
local variables and end up trying to serialise, e.g. the timestamping service, which doesn't make any conceptual
|
||||
sense. The ``serviceHub`` field is defined by the ``ProtocolStateMachine`` superclass and is marked transient so
|
||||
this problem doesn't occur. It's also restored for you after a protocol state machine is restored after a node
|
||||
this problem doesn't occur. It's also restored for you when a protocol state machine is restored after a node
|
||||
restart.
|
||||
|
||||
|
45
docs/build/html/protocol-state-machines.html
vendored
45
docs/build/html/protocol-state-machines.html
vendored
@ -188,7 +188,7 @@ construction of them that automatically handles many of the concerns outlined ab
|
||||
<h2>Theory<a class="headerlink" href="#theory" title="Permalink to this headline">¶</a></h2>
|
||||
<p>A <em>continuation</em> is a suspended stack frame stored in a regular object that can be passed around, serialised,
|
||||
unserialised and resumed from where it was suspended. This may sound abstract but don’t worry, the examples below
|
||||
will make it clearer. The JVM does not natively support continuations, so we implement them using a a library called
|
||||
will make it clearer. The JVM does not natively support continuations, so we implement them using a library called
|
||||
JavaFlow which works through behind-the-scenes bytecode rewriting. You don’t have to know how this works to benefit
|
||||
from it, however.</p>
|
||||
<p>We use continuations for the following reasons:</p>
|
||||
@ -274,13 +274,13 @@ and returns it, with a <code class="docutils literal"><span class="pre">StateMac
|
||||
</ul>
|
||||
<div class="admonition note">
|
||||
<p class="first admonition-title">Note</p>
|
||||
<p class="last">Session IDs keep different traffic streams separated, so for security they must be large and random enough</p>
|
||||
<p class="last">Session IDs keep different traffic streams separated, so for security they must be large and random enough
|
||||
to be unguessable. 63 bits is good enough.</p>
|
||||
</div>
|
||||
<p>to be unguessable. 63 bits is good enough.</p>
|
||||
<p>Alright, so using this protocol shouldn’t be too hard: in the simplest case we can just pass in the details of the trade
|
||||
to either runBuyer or runSeller, depending on who we are, and then call <code class="docutils literal"><span class="pre">.get()</span></code> on the resulting future to block the
|
||||
calling thread until the protocol has finished. Or we could register a callback on the returned future that will be
|
||||
invoked when it’s done, where we could e.g. update a user interface.</p>
|
||||
to either runBuyer or runSeller, depending on who we are, and then call <code class="docutils literal"><span class="pre">.resultFuture.get()</span></code> on resulting object to
|
||||
block the calling thread until the protocol has finished. Or we could register a callback on the returned future that
|
||||
will be invoked when it’s done, where we could e.g. update a user interface.</p>
|
||||
<p>The only tricky part is how to get one of these things. We need a <code class="docutils literal"><span class="pre">StateMachineManager</span></code>. Where does that come from
|
||||
and why do we need one?</p>
|
||||
</div>
|
||||
@ -335,7 +335,8 @@ each side will use.</p>
|
||||
we want to trade. Remember: this data comes from whatever system was used to find the trading partner to begin with.
|
||||
It could be as simple as a chat room or as complex as a 24/7 exchange.</p>
|
||||
<div class="codeset container">
|
||||
<div class="highlight-kotlin"><div class="highlight"><pre><span class="c1">// This object is serialised to the network and is the first protocol message the seller sends to the buyer.</span>
|
||||
<div class="highlight-kotlin"><div class="highlight"><pre><span class="c1">// This object is serialised to the network and is the first protocol message</span>
|
||||
<span class="c1">// the seller sends to the buyer.</span>
|
||||
<span class="k">class</span> <span class="nc">SellerTradeInfo</span><span class="p">(</span>
|
||||
<span class="k">val</span> <span class="py">assetForSale</span><span class="p">:</span> <span class="n">StateAndRef</span><span class="p"><</span><span class="n">OwnableState</span><span class="p">>,</span>
|
||||
<span class="k">val</span> <span class="py">price</span><span class="p">:</span> <span class="n">Amount</span><span class="p">,</span>
|
||||
@ -355,8 +356,8 @@ trade’s messages, and a pointer to where the asset that is being sold can
|
||||
<span class="c1">// Make the first message we'll send to kick off the protocol.</span>
|
||||
<span class="k">val</span> <span class="py">hello</span> <span class="p">=</span> <span class="n">SellerTradeInfo</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">assetToSell</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">price</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">myKeyPair</span><span class="p">.</span><span class="k">public</span><span class="p">,</span> <span class="n">sessionID</span><span class="p">)</span>
|
||||
|
||||
<span class="c1">// Zero is a special session ID that is being listened to by the buyer (i.e. before a session is started).</span>
|
||||
<span class="k">val</span> <span class="py">partialTX</span> <span class="p">=</span> <span class="n">sendAndReceive</span><span class="p"><</span><span class="n">SignedWireTransaction</span><span class="p">>(</span><span class="n">TRADE_TOPIC</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">buyerSessionID</span><span class="p">,</span> <span class="n">sessionID</span><span class="p">,</span> <span class="n">hello</span><span class="p">)</span>
|
||||
<span class="k">val</span> <span class="py">partialTX</span> <span class="p">=</span> <span class="n">sendAndReceive</span><span class="p"><</span><span class="n">SignedWireTransaction</span><span class="p">>(</span><span class="n">TRADE_TOPIC</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">buyerSessionID</span><span class="p">,</span>
|
||||
<span class="n">sessionID</span><span class="p">,</span> <span class="n">hello</span><span class="p">)</span>
|
||||
<span class="n">logger</span><span class="p">().</span><span class="n">trace</span> <span class="p">{</span> <span class="s">"Received partially signed transaction"</span> <span class="p">}</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
@ -453,16 +454,18 @@ forms.</p>
|
||||
if (!args.typeToBuy.isInstance(tradeRequest.assetForSale.state))
|
||||
throw AssetMismatchException(args.typeToBuy.name, assetTypeName)
|
||||
|
||||
// TODO: Either look up the stateref here in our local db, or accept a long chain of states and
|
||||
// validate them to audit the other side and ensure it actually owns the state we are being offered!
|
||||
// For now, just assume validity!
|
||||
// TODO: Either look up the stateref here in our local db, or accept a long chain
|
||||
// of states and validate them to audit the other side and ensure it actually owns
|
||||
// the state we are being offered! For now, just assume validity!
|
||||
|
||||
// Generate the shared transaction that both sides will sign, using the data we have.
|
||||
val ptx = PartialTransaction()
|
||||
// Add input and output states for the movement of cash, by using the Cash contract to generate the states.
|
||||
// Add input and output states for the movement of cash, by using the Cash contract
|
||||
// to generate the states.
|
||||
val wallet = serviceHub.walletService.currentWallet
|
||||
val cashStates = wallet.statesOfType<Cash.State>()
|
||||
val cashSigningPubKeys = Cash().craftSpend(ptx, tradeRequest.price, tradeRequest.sellerOwnerKey, cashStates)
|
||||
val cashSigningPubKeys = Cash().craftSpend(ptx, tradeRequest.price,
|
||||
tradeRequest.sellerOwnerKey, cashStates)
|
||||
// Add inputs/outputs/a command for the movement of the asset.
|
||||
ptx.addInputState(tradeRequest.assetForSale.ref)
|
||||
// Just pick some new public key for now.
|
||||
@ -484,14 +487,17 @@ forms.</p>
|
||||
|
||||
logger().trace { "Sending partially signed transaction to seller" }
|
||||
|
||||
// TODO: Protect against the buyer terminating here and leaving us in the lurch without the final tx.
|
||||
// TODO: Protect against a malicious buyer sending us back a different transaction to the one we built.
|
||||
// TODO: Protect against the buyer terminating here and leaving us in the lurch without
|
||||
// the final tx.
|
||||
// TODO: Protect against a malicious buyer sending us back a different transaction to
|
||||
// the one we built.
|
||||
val fullySigned = sendAndReceive<TimestampedWireTransaction>(TRADE_TOPIC,
|
||||
tradeRequest.sessionID, args.sessionID, stx)
|
||||
|
||||
logger().trace { "Got fully signed transaction, verifying ... "}
|
||||
|
||||
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.timestampingService, serviceHub.identityService)
|
||||
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.timestampingService,
|
||||
serviceHub.identityService)
|
||||
|
||||
logger().trace { "Fully signed transaction was valid. Trade complete! :-)" }
|
||||
|
||||
@ -504,7 +510,8 @@ forms.</p>
|
||||
<p>This code is fairly straightforward. Here are some things to pay attention to:</p>
|
||||
<ol class="arabic simple">
|
||||
<li>We do some sanity checking on the received message to ensure we’re being offered what we expected to be offered.</li>
|
||||
<li>We create a cash spend in the normal way, by using <code class="docutils literal"><span class="pre">Cash().craftSpend</span></code>.</li>
|
||||
<li>We create a cash spend in the normal way, by using <code class="docutils literal"><span class="pre">Cash().craftSpend</span></code>. See the contracts tutorial if this isn’t
|
||||
clear.</li>
|
||||
<li>We access the <em>service hub</em> when we need it to access things that are transient and may change or be recreated
|
||||
whilst a protocol is suspended, things like the wallet or the timestamping service. Remember that a protocol may
|
||||
be suspended when it waits to receive a message across node or computer restarts, so objects representing a service
|
||||
@ -520,7 +527,7 @@ the fact that it takes minimal resources and can survive node restarts.</p>
|
||||
If you do this then next time your protocol waits to receive an object, the system will try and serialise all your
|
||||
local variables and end up trying to serialise, e.g. the timestamping service, which doesn’t make any conceptual
|
||||
sense. The <code class="docutils literal"><span class="pre">serviceHub</span></code> field is defined by the <code class="docutils literal"><span class="pre">ProtocolStateMachine</span></code> superclass and is marked transient so
|
||||
this problem doesn’t occur. It’s also restored for you after a protocol state machine is restored after a node
|
||||
this problem doesn’t occur. It’s also restored for you when a protocol state machine is restored after a node
|
||||
restart.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
2
docs/build/html/searchindex.js
vendored
2
docs/build/html/searchindex.js
vendored
File diff suppressed because one or more lines are too long
@ -57,7 +57,7 @@ Theory
|
||||
|
||||
A *continuation* is a suspended stack frame stored in a regular object that can be passed around, serialised,
|
||||
unserialised and resumed from where it was suspended. This may sound abstract but don't worry, the examples below
|
||||
will make it clearer. The JVM does not natively support continuations, so we implement them using a a library called
|
||||
will make it clearer. The JVM does not natively support continuations, so we implement them using a library called
|
||||
JavaFlow which works through behind-the-scenes bytecode rewriting. You don't have to know how this works to benefit
|
||||
from it, however.
|
||||
|
||||
@ -153,9 +153,9 @@ Let's unpack what this code does:
|
||||
to be unguessable. 63 bits is good enough.
|
||||
|
||||
Alright, so using this protocol shouldn't be too hard: in the simplest case we can just pass in the details of the trade
|
||||
to either runBuyer or runSeller, depending on who we are, and then call ``.get()`` on the resulting future to block the
|
||||
calling thread until the protocol has finished. Or we could register a callback on the returned future that will be
|
||||
invoked when it's done, where we could e.g. update a user interface.
|
||||
to either runBuyer or runSeller, depending on who we are, and then call ``.resultFuture.get()`` on resulting object to
|
||||
block the calling thread until the protocol has finished. Or we could register a callback on the returned future that
|
||||
will be invoked when it's done, where we could e.g. update a user interface.
|
||||
|
||||
The only tricky part is how to get one of these things. We need a ``StateMachineManager``. Where does that come from
|
||||
and why do we need one?
|
||||
@ -221,7 +221,8 @@ It could be as simple as a chat room or as complex as a 24/7 exchange.
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
// This object is serialised to the network and is the first protocol message the seller sends to the buyer.
|
||||
// This object is serialised to the network and is the first protocol message
|
||||
// the seller sends to the buyer.
|
||||
class SellerTradeInfo(
|
||||
val assetForSale: StateAndRef<OwnableState>,
|
||||
val price: Amount,
|
||||
@ -244,8 +245,8 @@ Next we add some code to the ``SellerImpl.call`` method:
|
||||
// Make the first message we'll send to kick off the protocol.
|
||||
val hello = SellerTradeInfo(args.assetToSell, args.price, args.myKeyPair.public, sessionID)
|
||||
|
||||
// Zero is a special session ID that is being listened to by the buyer (i.e. before a session is started).
|
||||
val partialTX = sendAndReceive<SignedWireTransaction>(TRADE_TOPIC, args.buyerSessionID, sessionID, hello)
|
||||
val partialTX = sendAndReceive<SignedWireTransaction>(TRADE_TOPIC, args.buyerSessionID,
|
||||
sessionID, hello)
|
||||
logger().trace { "Received partially signed transaction" }
|
||||
|
||||
That's pretty straightforward. We generate a session ID to identify what's happening on the seller side, fill out
|
||||
@ -347,16 +348,18 @@ OK, let's do the same for the buyer side:
|
||||
if (!args.typeToBuy.isInstance(tradeRequest.assetForSale.state))
|
||||
throw AssetMismatchException(args.typeToBuy.name, assetTypeName)
|
||||
|
||||
// TODO: Either look up the stateref here in our local db, or accept a long chain of states and
|
||||
// validate them to audit the other side and ensure it actually owns the state we are being offered!
|
||||
// For now, just assume validity!
|
||||
// TODO: Either look up the stateref here in our local db, or accept a long chain
|
||||
// of states and validate them to audit the other side and ensure it actually owns
|
||||
// the state we are being offered! For now, just assume validity!
|
||||
|
||||
// Generate the shared transaction that both sides will sign, using the data we have.
|
||||
val ptx = PartialTransaction()
|
||||
// Add input and output states for the movement of cash, by using the Cash contract to generate the states.
|
||||
// Add input and output states for the movement of cash, by using the Cash contract
|
||||
// to generate the states.
|
||||
val wallet = serviceHub.walletService.currentWallet
|
||||
val cashStates = wallet.statesOfType<Cash.State>()
|
||||
val cashSigningPubKeys = Cash().craftSpend(ptx, tradeRequest.price, tradeRequest.sellerOwnerKey, cashStates)
|
||||
val cashSigningPubKeys = Cash().craftSpend(ptx, tradeRequest.price,
|
||||
tradeRequest.sellerOwnerKey, cashStates)
|
||||
// Add inputs/outputs/a command for the movement of the asset.
|
||||
ptx.addInputState(tradeRequest.assetForSale.ref)
|
||||
// Just pick some new public key for now.
|
||||
@ -378,14 +381,17 @@ OK, let's do the same for the buyer side:
|
||||
|
||||
logger().trace { "Sending partially signed transaction to seller" }
|
||||
|
||||
// TODO: Protect against the buyer terminating here and leaving us in the lurch without the final tx.
|
||||
// TODO: Protect against a malicious buyer sending us back a different transaction to the one we built.
|
||||
// TODO: Protect against the buyer terminating here and leaving us in the lurch without
|
||||
// the final tx.
|
||||
// TODO: Protect against a malicious buyer sending us back a different transaction to
|
||||
// the one we built.
|
||||
val fullySigned = sendAndReceive<TimestampedWireTransaction>(TRADE_TOPIC,
|
||||
tradeRequest.sessionID, args.sessionID, stx)
|
||||
|
||||
logger().trace { "Got fully signed transaction, verifying ... "}
|
||||
|
||||
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.timestampingService, serviceHub.identityService)
|
||||
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.timestampingService,
|
||||
serviceHub.identityService)
|
||||
|
||||
logger().trace { "Fully signed transaction was valid. Trade complete! :-)" }
|
||||
|
||||
@ -396,7 +402,8 @@ OK, let's do the same for the buyer side:
|
||||
This code is fairly straightforward. Here are some things to pay attention to:
|
||||
|
||||
1. We do some sanity checking on the received message to ensure we're being offered what we expected to be offered.
|
||||
2. We create a cash spend in the normal way, by using ``Cash().craftSpend``.
|
||||
2. We create a cash spend in the normal way, by using ``Cash().craftSpend``. See the contracts tutorial if this isn't
|
||||
clear.
|
||||
3. We access the *service hub* when we need it to access things that are transient and may change or be recreated
|
||||
whilst a protocol is suspended, things like the wallet or the timestamping service. Remember that a protocol may
|
||||
be suspended when it waits to receive a message across node or computer restarts, so objects representing a service
|
||||
@ -411,6 +418,6 @@ the fact that it takes minimal resources and can survive node restarts.
|
||||
If you do this then next time your protocol waits to receive an object, the system will try and serialise all your
|
||||
local variables and end up trying to serialise, e.g. the timestamping service, which doesn't make any conceptual
|
||||
sense. The ``serviceHub`` field is defined by the ``ProtocolStateMachine`` superclass and is marked transient so
|
||||
this problem doesn't occur. It's also restored for you after a protocol state machine is restored after a node
|
||||
this problem doesn't occur. It's also restored for you when a protocol state machine is restored after a node
|
||||
restart.
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user