mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-21 19:14:19 +01:00
BlindedMessages is now List[BlindedMessage] (no blinded_messages field) and PostMintRequest (new) is now with outputs field
This commit is contained in:
@@ -41,6 +41,27 @@ class Proof(BaseModel):
|
|||||||
self.__setattr__(key, val)
|
self.__setattr__(key, val)
|
||||||
|
|
||||||
|
|
||||||
|
class Proofs(BaseModel):
|
||||||
|
# NOTE: not used in Pydantic validation
|
||||||
|
__root__: List[Proof]
|
||||||
|
|
||||||
|
|
||||||
|
class BlindedMessage(BaseModel):
|
||||||
|
amount: int
|
||||||
|
B_: str
|
||||||
|
|
||||||
|
|
||||||
|
class BlindedSignature(BaseModel):
|
||||||
|
id: Union[str, None] = None
|
||||||
|
amount: int
|
||||||
|
C_: str
|
||||||
|
|
||||||
|
|
||||||
|
class BlindedMessages(BaseModel):
|
||||||
|
# NOTE: not used in Pydantic validation
|
||||||
|
__root__: List[BlindedMessage] = []
|
||||||
|
|
||||||
|
|
||||||
# ------- LIGHTNING INVOICE -------
|
# ------- LIGHTNING INVOICE -------
|
||||||
|
|
||||||
|
|
||||||
@@ -72,10 +93,8 @@ class KeysetsResponse(BaseModel):
|
|||||||
# ------- API: MINT -------
|
# ------- API: MINT -------
|
||||||
|
|
||||||
|
|
||||||
class BlindedSignature(BaseModel):
|
class PostMintRequest(BaseModel):
|
||||||
id: Union[str, None] = None
|
outputs: List[BlindedMessage]
|
||||||
amount: int
|
|
||||||
C_: str
|
|
||||||
|
|
||||||
|
|
||||||
class PostMintResponseLegacy(BaseModel):
|
class PostMintResponseLegacy(BaseModel):
|
||||||
@@ -108,32 +127,10 @@ class GetMeltResponse(BaseModel):
|
|||||||
# ------- API: SPLIT -------
|
# ------- API: SPLIT -------
|
||||||
|
|
||||||
|
|
||||||
class BlindedMessage(BaseModel):
|
|
||||||
amount: int
|
|
||||||
B_: str
|
|
||||||
|
|
||||||
|
|
||||||
class BlindedMessages(BaseModel):
|
|
||||||
blinded_messages: List[BlindedMessage] = []
|
|
||||||
|
|
||||||
|
|
||||||
class SplitRequest(BaseModel):
|
class SplitRequest(BaseModel):
|
||||||
proofs: List[Proof]
|
proofs: List[Proof]
|
||||||
amount: int
|
amount: int
|
||||||
output_data: Union[
|
outputs: Union[List[BlindedMessage], None] = None
|
||||||
BlindedMessages, None
|
|
||||||
] = None # backwards compatibility with clients that called this output_data and not outputs < v0.2.2
|
|
||||||
outputs: Union[BlindedMessages, None] = None
|
|
||||||
|
|
||||||
def __init__(self, **data):
|
|
||||||
super().__init__(**data)
|
|
||||||
self.backwards_compatibility_v021()
|
|
||||||
|
|
||||||
def backwards_compatibility_v021(self):
|
|
||||||
# before v0.2.2: output_data, after: outputs
|
|
||||||
if self.output_data:
|
|
||||||
self.outputs = self.output_data
|
|
||||||
self.output_data = None
|
|
||||||
|
|
||||||
|
|
||||||
class PostSplitResponse(BaseModel):
|
class PostSplitResponse(BaseModel):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from fastapi import APIRouter
|
|||||||
from secp256k1 import PublicKey
|
from secp256k1 import PublicKey
|
||||||
|
|
||||||
from cashu.core.base import (
|
from cashu.core.base import (
|
||||||
BlindedMessages,
|
BlindedMessage,
|
||||||
BlindedSignature,
|
BlindedSignature,
|
||||||
CheckFeesRequest,
|
CheckFeesRequest,
|
||||||
CheckFeesResponse,
|
CheckFeesResponse,
|
||||||
@@ -14,6 +14,7 @@ from cashu.core.base import (
|
|||||||
KeysetsResponse,
|
KeysetsResponse,
|
||||||
KeysResponse,
|
KeysResponse,
|
||||||
MeltRequest,
|
MeltRequest,
|
||||||
|
PostMintRequest,
|
||||||
PostMintResponse,
|
PostMintResponse,
|
||||||
PostSplitResponse,
|
PostSplitResponse,
|
||||||
SplitRequest,
|
SplitRequest,
|
||||||
@@ -68,7 +69,7 @@ async def request_mint(amount: int = 0) -> GetMintResponse:
|
|||||||
|
|
||||||
@router.post("/mint")
|
@router.post("/mint")
|
||||||
async def mint(
|
async def mint(
|
||||||
mint_request: BlindedMessages,
|
payload: PostMintRequest,
|
||||||
payment_hash: Union[str, None] = None,
|
payment_hash: Union[str, None] = None,
|
||||||
) -> Union[PostMintResponse, CashuError]:
|
) -> Union[PostMintResponse, CashuError]:
|
||||||
"""
|
"""
|
||||||
@@ -77,9 +78,7 @@ async def mint(
|
|||||||
Call this endpoint after `GET /mint`.
|
Call this endpoint after `GET /mint`.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
promises = await ledger.mint(
|
promises = await ledger.mint(payload.outputs, payment_hash=payment_hash)
|
||||||
mint_request.blinded_messages, payment_hash=payment_hash
|
|
||||||
)
|
|
||||||
blinded_signatures = PostMintResponse(promises=promises)
|
blinded_signatures = PostMintResponse(promises=promises)
|
||||||
return blinded_signatures
|
return blinded_signatures
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -124,8 +123,7 @@ async def split(
|
|||||||
proofs = payload.proofs
|
proofs = payload.proofs
|
||||||
amount = payload.amount
|
amount = payload.amount
|
||||||
|
|
||||||
# NOTE: backwards compatibility with clients < v0.2.2
|
outputs = payload.outputs or None
|
||||||
outputs = payload.outputs.blinded_messages if payload.outputs else None
|
|
||||||
|
|
||||||
assert outputs, Exception("no outputs provided.")
|
assert outputs, Exception("no outputs provided.")
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import cashu.core.b_dhke as b_dhke
|
|||||||
import cashu.core.bolt11 as bolt11
|
import cashu.core.bolt11 as bolt11
|
||||||
from cashu.core.base import (
|
from cashu.core.base import (
|
||||||
BlindedMessage,
|
BlindedMessage,
|
||||||
BlindedMessages,
|
|
||||||
BlindedSignature,
|
BlindedSignature,
|
||||||
CheckFeesRequest,
|
CheckFeesRequest,
|
||||||
CheckRequest,
|
CheckRequest,
|
||||||
@@ -23,6 +22,7 @@ from cashu.core.base import (
|
|||||||
KeysetsResponse,
|
KeysetsResponse,
|
||||||
MeltRequest,
|
MeltRequest,
|
||||||
P2SHScript,
|
P2SHScript,
|
||||||
|
PostMintRequest,
|
||||||
PostMintResponse,
|
PostMintResponse,
|
||||||
PostMintResponseLegacy,
|
PostMintResponseLegacy,
|
||||||
Proof,
|
Proof,
|
||||||
@@ -162,20 +162,20 @@ class LedgerAPI:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _construct_outputs(amounts: List[int], secrets: List[str]):
|
def _construct_outputs(amounts: List[int], secrets: List[str]):
|
||||||
"""Takes a list of amounts and secrets and returns outputs.
|
"""Takes a list of amounts and secrets and returns outputs.
|
||||||
Outputs are blinded messages `payloads` and blinding factors `rs`"""
|
Outputs are blinded messages `outputs` and blinding factors `rs`"""
|
||||||
assert len(amounts) == len(
|
assert len(amounts) == len(
|
||||||
secrets
|
secrets
|
||||||
), f"len(amounts)={len(amounts)} not equal to len(secrets)={len(secrets)}"
|
), f"len(amounts)={len(amounts)} not equal to len(secrets)={len(secrets)}"
|
||||||
payloads: BlindedMessages = BlindedMessages()
|
outputs: List[BlindedMessage] = []
|
||||||
rs = []
|
rs = []
|
||||||
for secret, amount in zip(secrets, amounts):
|
for secret, amount in zip(secrets, amounts):
|
||||||
B_, r = b_dhke.step1_alice(secret)
|
B_, r = b_dhke.step1_alice(secret)
|
||||||
rs.append(r)
|
rs.append(r)
|
||||||
payload: BlindedMessage = BlindedMessage(
|
output: BlindedMessage = BlindedMessage(
|
||||||
amount=amount, B_=B_.serialize().hex()
|
amount=amount, B_=B_.serialize().hex()
|
||||||
)
|
)
|
||||||
payloads.blinded_messages.append(payload)
|
outputs.append(output)
|
||||||
return payloads, rs
|
return outputs, rs
|
||||||
|
|
||||||
async def _check_used_secrets(self, secrets):
|
async def _check_used_secrets(self, secrets):
|
||||||
for s in secrets:
|
for s in secrets:
|
||||||
@@ -251,11 +251,12 @@ class LedgerAPI:
|
|||||||
"""Mints new coins and returns a proof of promise."""
|
"""Mints new coins and returns a proof of promise."""
|
||||||
secrets = [self._generate_secret() for s in range(len(amounts))]
|
secrets = [self._generate_secret() for s in range(len(amounts))]
|
||||||
await self._check_used_secrets(secrets)
|
await self._check_used_secrets(secrets)
|
||||||
payloads, rs = self._construct_outputs(amounts, secrets)
|
outputs, rs = self._construct_outputs(amounts, secrets)
|
||||||
|
outputs_payload = PostMintRequest(outputs=outputs)
|
||||||
self.s = self._set_requests()
|
self.s = self._set_requests()
|
||||||
resp = self.s.post(
|
resp = self.s.post(
|
||||||
self.url + "/mint",
|
self.url + "/mint",
|
||||||
json=payloads.dict(),
|
json=outputs_payload.dict(),
|
||||||
params={"payment_hash": payment_hash},
|
params={"payment_hash": payment_hash},
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
@@ -303,8 +304,8 @@ class LedgerAPI:
|
|||||||
amounts
|
amounts
|
||||||
), "number of secrets does not match number of outputs"
|
), "number of secrets does not match number of outputs"
|
||||||
await self._check_used_secrets(secrets)
|
await self._check_used_secrets(secrets)
|
||||||
payloads, rs = self._construct_outputs(amounts, secrets)
|
outputs, rs = self._construct_outputs(amounts, secrets)
|
||||||
split_payload = SplitRequest(proofs=proofs, amount=amount, outputs=payloads)
|
split_payload = SplitRequest(proofs=proofs, amount=amount, outputs=outputs)
|
||||||
|
|
||||||
# construct payload
|
# construct payload
|
||||||
def _splitrequest_include_fields(proofs):
|
def _splitrequest_include_fields(proofs):
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ Mint: `Bob`
|
|||||||
# Blind Diffie-Hellmann key exchange (BDHKE)
|
# Blind Diffie-Hellmann key exchange (BDHKE)
|
||||||
|
|
||||||
- Mint `Bob` publishes `K = kG`
|
- Mint `Bob` publishes `K = kG`
|
||||||
- `Alice` picks secret `x` and computes `Y = hash_to_curve(x)`
|
- `Alice` picks secret `x` and computes `Y = hash_to_curve(x)`
|
||||||
- `Alice` sends to `Bob`: `T = Y + rG` with `r` being a random nonce
|
- `Alice` sends to `Bob`: `T = Y + rG` with `r` being a random nonce (**blinding**)
|
||||||
- `Bob` sends back to `Alice` blinded key: `Q = kT` (these two steps are the DH key exchange)
|
- `Bob` sends back to `Alice` blinded key: `Q = kT` (these two steps are the DH key exchange) (**signing**)
|
||||||
- `Alice` can calculate the unblinded key as `Q - rK = kY + krG - krG = kY = Z`
|
- `Alice` can calculate the unblinded key as `Q - rK = kY + krG - krG = kY = Z` (**unblinding**)
|
||||||
- Alice can take the pair `(x, Z)` as a token and can send it to `Carol`.
|
- Alice can take the pair `(x, Z)` as a token and can send it to `Carol`.
|
||||||
- `Carol` can send `(x, Z)` to `Bob` who then checks that `k*hash_to_curve(x) == Z`, and if so treats it as a valid spend of a token, adding `x` to the list of spent secrets.
|
- `Carol` can send `(x, Z)` to `Bob` who then checks that `k*hash_to_curve(x) == Z` (**verification**), and if so treats it as a valid spend of a token, adding `x` to the list of spent secrets.
|
||||||
|
|
||||||
## 0.1 - Models
|
## 0.1 - Models
|
||||||
|
|
||||||
|
|||||||
@@ -77,3 +77,23 @@ def derive_keyset_id(keys: Dict[int, PublicKey]):
|
|||||||
).decode()[:12]
|
).decode()[:12]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
[00]: 00.md
|
||||||
|
[01]: 02.md
|
||||||
|
[03]: 03.md
|
||||||
|
[04]: 04.md
|
||||||
|
[05]: 05.md
|
||||||
|
[06]: 06.md
|
||||||
|
[07]: 07.md
|
||||||
|
[08]: 08.md
|
||||||
|
[09]: 09.md
|
||||||
|
[10]: 10.md
|
||||||
|
[11]: 11.md
|
||||||
|
[12]: 12.md
|
||||||
|
[13]: 13.md
|
||||||
|
[14]: 14.md
|
||||||
|
[15]: 15.md
|
||||||
|
[16]: 16.md
|
||||||
|
[17]: 17.md
|
||||||
|
[18]: 18.md
|
||||||
|
[19]: 19.md
|
||||||
|
[20]: 20.md
|
||||||
@@ -27,4 +27,25 @@ Response of `Bob`:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
with `pr` being the bolt11 payment request and `hash` the hash of the invoice. A wallet **MUST** store the `hash` and `amount_sat` in its database to later request the tokens upon paying the invoice. A wallet **SHOULD** then present the payment request (for example via QR code) to the user such that they can pay the invoice with another Lightning wallet. After the user has paid the invoice, a wallet **MUST** continue with #4 - Minting tokens [TODO: Link to #4].
|
with `pr` being the bolt11 payment request and `hash` the hash of the invoice. A wallet **MUST** store the `hash` and `amount_sat` in its database to later request the tokens upon paying the invoice. A wallet **SHOULD** then present the payment request (for example via QR code) to the user such that they can pay the invoice with another Lightning wallet. After the user has paid the invoice, a wallet **MUST** continue with #4 - Minting tokens [TODO: Link to #4].
|
||||||
|
|
||||||
|
[00]: 00.md
|
||||||
|
[01]: 02.md
|
||||||
|
[03]: 03.md
|
||||||
|
[04]: 04.md
|
||||||
|
[05]: 05.md
|
||||||
|
[06]: 06.md
|
||||||
|
[07]: 07.md
|
||||||
|
[08]: 08.md
|
||||||
|
[09]: 09.md
|
||||||
|
[10]: 10.md
|
||||||
|
[11]: 11.md
|
||||||
|
[12]: 12.md
|
||||||
|
[13]: 13.md
|
||||||
|
[14]: 14.md
|
||||||
|
[15]: 15.md
|
||||||
|
[16]: 16.md
|
||||||
|
[17]: 17.md
|
||||||
|
[18]: 18.md
|
||||||
|
[19]: 19.md
|
||||||
|
[20]: 20.md
|
||||||
@@ -12,25 +12,22 @@ Request of `Alice`:
|
|||||||
POST https://mint.host:3338/mint&payment_hash=67d1d9ea6ada225c115418671b64a
|
POST https://mint.host:3338/mint&payment_hash=67d1d9ea6ada225c115418671b64a
|
||||||
```
|
```
|
||||||
|
|
||||||
With the data being of the form `BlindedMessages`:
|
With the json payload data being of the form `PostMintRequest`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"blinded_messages":
|
"outputs": `BlindedMessages`
|
||||||
[
|
|
||||||
BlindedMessage,
|
|
||||||
...
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`BlindedMessages` is a list (array) of `BlindedMessage`s (see [NUT-0][00]).
|
||||||
|
|
||||||
With curl:
|
With curl:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -X POST https://mint.host:3338/mint&payment_hash=67d1d9ea6ada225c115418671b64a -d \
|
curl -X POST https://mint.host:3338/mint&payment_hash=67d1d9ea6ada225c115418671b64a -d \
|
||||||
{
|
{
|
||||||
"blinded_messages":
|
"outputs":
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"amount": 2,
|
"amount": 2,
|
||||||
@@ -39,7 +36,6 @@ curl -X POST https://mint.host:3338/mint&payment_hash=67d1d9ea6ada225c115418671b
|
|||||||
{
|
{
|
||||||
"amount": 8,
|
"amount": 8,
|
||||||
"B_": "03b54ab451b15005f2c64d38fc512fca695914c8fd5094ee044e5724ad41fda247"
|
"B_": "03b54ab451b15005f2c64d38fc512fca695914c8fd5094ee044e5724ad41fda247"
|
||||||
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -47,7 +43,7 @@ curl -X POST https://mint.host:3338/mint&payment_hash=67d1d9ea6ada225c115418671b
|
|||||||
|
|
||||||
Response of `Bob`:
|
Response of `Bob`:
|
||||||
|
|
||||||
If the invoice was successfully paid, `Bob` responds with a `PostMintResponse` which is essentially a list of `BlindedSignature`'s [TODO: Link PostMintResponse]
|
If the invoice was successfully paid, `Bob` responds with a `PostMintResponse` which is a list of `BlindedSignature`'s (see [NUT-0][00]).
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -72,7 +68,7 @@ If the invoice was not paid yet, `Bob` responds with an error. In that case, `Al
|
|||||||
|
|
||||||
## Unblinding signatures
|
## Unblinding signatures
|
||||||
|
|
||||||
Upon receiving the `PostMintResponse` with the list of `BlindedSignature`'s from the mint `Bob`, a wallet `Alice` **MUST** then unblind the `BlindedSignature`'s from `Bob` (see #0 Notation [TODO: Link to unblinding]) to generate a list of `Proof`'s. A `Proof` is effectively an ecash `Token` and can later be used to redeem the token. The wallet **MUST** store the `Proof` in its database.
|
Upon receiving the `PostMintResponse` with the list of `BlindedSignature`'s from the mint `Bob`, a wallet `Alice` **MUST** then unblind the `BlindedSignature`'s from `Bob` (see BDHKE [NUT-0][00]) to generate a list of `Proof`'s. A `Proof` is effectively an ecash `Token` and can later be used to redeem the token. The wallet **MUST** store the `Proof` in its database.
|
||||||
|
|
||||||
A list multiple `Proof`'s is called `Proofs` and has the form:
|
A list multiple `Proof`'s is called `Proofs` and has the form:
|
||||||
|
|
||||||
@@ -91,4 +87,25 @@ A list multiple `Proof`'s is called `Proofs` and has the form:
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
[00]: 00.md
|
||||||
|
[01]: 02.md
|
||||||
|
[03]: 03.md
|
||||||
|
[04]: 04.md
|
||||||
|
[05]: 05.md
|
||||||
|
[06]: 06.md
|
||||||
|
[07]: 07.md
|
||||||
|
[08]: 08.md
|
||||||
|
[09]: 09.md
|
||||||
|
[10]: 10.md
|
||||||
|
[11]: 11.md
|
||||||
|
[12]: 12.md
|
||||||
|
[13]: 13.md
|
||||||
|
[14]: 14.md
|
||||||
|
[15]: 15.md
|
||||||
|
[16]: 16.md
|
||||||
|
[17]: 17.md
|
||||||
|
[18]: 18.md
|
||||||
|
[19]: 19.md
|
||||||
|
[20]: 20.md
|
||||||
@@ -56,4 +56,25 @@ curl -X POST https://mint.host:3338/mint&payment_hash=67d1d9ea6ada225c115418671b
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Only if the `paid==true`, the wallet `Alice` **MUST** delete the `Proofs` from her database (or move them to a history). If `paid==false`, `Alice` **CAN** repeat the same multiple times until the payment is successful.
|
Only if the `paid==true`, the wallet `Alice` **MUST** delete the `Proofs` from her database (or move them to a history). If `paid==false`, `Alice` **CAN** repeat the same multiple times until the payment is successful.
|
||||||
|
|
||||||
|
[00]: 00.md
|
||||||
|
[01]: 02.md
|
||||||
|
[03]: 03.md
|
||||||
|
[04]: 04.md
|
||||||
|
[05]: 05.md
|
||||||
|
[06]: 06.md
|
||||||
|
[07]: 07.md
|
||||||
|
[08]: 08.md
|
||||||
|
[09]: 09.md
|
||||||
|
[10]: 10.md
|
||||||
|
[11]: 11.md
|
||||||
|
[12]: 12.md
|
||||||
|
[13]: 13.md
|
||||||
|
[14]: 14.md
|
||||||
|
[15]: 15.md
|
||||||
|
[16]: 16.md
|
||||||
|
[17]: 17.md
|
||||||
|
[18]: 18.md
|
||||||
|
[19]: 19.md
|
||||||
|
[20]: 20.md
|
||||||
@@ -32,6 +32,8 @@ With the data being of the form `SplitRequest`:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`BlindedMessages` is a list (array) of `BlindedMessage`s (see [NUT-0][00]).
|
||||||
|
|
||||||
With curl:
|
With curl:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -49,8 +51,7 @@ curl -X POST https://mint.host:3338/split -d \
|
|||||||
...
|
...
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outputs":{
|
"outputs":
|
||||||
"blinded_messages":
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"amount": 2,
|
"amount": 2,
|
||||||
@@ -59,10 +60,38 @@ curl -X POST https://mint.host:3338/split -d \
|
|||||||
{
|
{
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
},
|
|
||||||
"amount": 40
|
"amount": 40
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
If successful, `Bob` will respond
|
If successful, `Bob` will respond with a `PostSplitResponse`
|
||||||
|
|
||||||
|
```python
|
||||||
|
class PostSplitResponse(BaseModel):
|
||||||
|
fst: BlindedSignatures
|
||||||
|
snd: BlindedSignatures
|
||||||
|
```
|
||||||
|
|
||||||
|
`BlindedSignatures` is a list (array) of `BlindedSignature`s (see [NUT-0][00]).
|
||||||
|
|
||||||
|
[00]: 00.md
|
||||||
|
[01]: 02.md
|
||||||
|
[03]: 03.md
|
||||||
|
[04]: 04.md
|
||||||
|
[05]: 05.md
|
||||||
|
[06]: 06.md
|
||||||
|
[07]: 07.md
|
||||||
|
[08]: 08.md
|
||||||
|
[09]: 09.md
|
||||||
|
[10]: 10.md
|
||||||
|
[11]: 11.md
|
||||||
|
[12]: 12.md
|
||||||
|
[13]: 13.md
|
||||||
|
[14]: 14.md
|
||||||
|
[15]: 15.md
|
||||||
|
[16]: 16.md
|
||||||
|
[17]: 17.md
|
||||||
|
[18]: 18.md
|
||||||
|
[19]: 19.md
|
||||||
|
[20]: 20.md
|
||||||
Reference in New Issue
Block a user