Skip to content

Commit 4d1ba50

Browse files
rename TimelockScript, update README
1 parent 57aa653 commit 4d1ba50

File tree

6 files changed

+68
-17
lines changed

6 files changed

+68
-17
lines changed

README.md

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ the following hierarchy
300300
* `NulldataScript`
301301
* `MultisigScript`
302302
* `IfElseScript`
303-
* `TimelockScript`
303+
* `AbsoluteTimelockScript`
304304
* `RelativeTimelockScript`
305305
* `Hashlock256Script`
306306
* `Hashlock160Script`
@@ -468,6 +468,17 @@ Traceback (most recent call last):
468468
btcpy.structs.address.WrongScriptType: Trying to produce P2pkhAddress from P2shScript script
469469
```
470470

471+
On the other hand, addresses can also be directly converted to the scripts they represent:
472+
473+
```python
474+
>>> a = Address.from_string('mkGY1QBotzNCrpJaEsje3BpYJsktksi3gJ')
475+
>>> a.to_script()
476+
P2pkhScript('341e8815a2e5987d465c6c5c1fb56395cb96e400')
477+
>>> a = Address.from_string('tb1qxs0gs9dzukv863jud3wpldtrjh9edeqqqzahcz')
478+
>>> a.to_script()
479+
P2wpkhScript('341e8815a2e5987d465c6c5c1fb56395cb96e400')
480+
```
481+
471482
## Transactions
472483

473484
### Creating transactions
@@ -486,6 +497,9 @@ methods for creation:
486497
[BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki) specification.
487498
* `max()`, this automatically creates a `Sequence` object with the maximum sequence number
488499
(i.e. `0xffffffff`).
500+
* `TimebasedSequence`, behaves like a `Sequence` but assumes the sequence expresses time. Can
501+
also be instantiated from a `timedelta` object through its `from_timedelta` method
502+
* `HeightBasedSequence`, behaves like a `Sequence` but assumes the sequence expresses a block height.
489503
* `ScriptSig`, this can be initialised with a `bytearray` representing the script, but offers
490504
the following static methods:
491505
* `empty()`, this creates an empty `ScriptSig`, useful when initialising a transaction
@@ -499,10 +513,12 @@ sent.
499513
* `ScriptPubKey` and derived classes, they take as input a `bytearray` representing the script
500514
but can also be created through the `ScriptBuilder.identify()` method or in the way displayed
501515
later in this section.
502-
* `Locktime`, takes as input a number representing the transaction's locktime field.
516+
* `Locktime`, takes as input a number representing the transaction's locktime field. Can also
517+
be constructed from a `datetime` object through its `from_datetime` method
503518
* `Transaction`, takes as inputs: a version number, a list of `TxIn`s, a list of `TxOut`s, a
504519
`Locktime`.
505520
* `SegWitTransaction`, has the same interface as `Transaction`
521+
* `TransactionFactory` used to instantiate a generic transaction from a json or hex string
506522

507523
All the aforementioned classes are `Immutable`, this means that, after construction, their
508524
attributes can't be mutated. This helps caching values returned by their methods. The classes
@@ -511,6 +527,39 @@ attributes can't be mutated. This helps caching values returned by their methods
511527
versions are mainly used to create unsigned transactions which then are mutated
512528
to add signatures to them. We will see how to use these in the rest of this section.
513529

530+
Transactions can be deserialized both from json and from a hex string, see the following examples:
531+
```python
532+
>>> tx = Transaction.unhexlify('0100000001e4da173fbefe5e60ff63dfd38566ade407532294db655463b77a783f379ce605000000006b483045022100af246c27890c2bc07a0b7450d3d82509702a44a4defdff766355240b114ee2ac02207bb67b468452fa1b325dd5583879f5c1412e0bb4dae1c2c96c7a408796ab76f1012102ab9e8575536a1e99604a158fc60fe2ebd1cb1839e919b4ca42b8d050cfad71b2ffffffff0100c2eb0b000000001976a914df76c017354ac39bde796abe4294d31de8b5788a88ac00000000')
533+
>>> tx.to_json()
534+
{'hex': '0100000001e4da173fbefe5e60ff63dfd38566ade407532294db655463b77a783f379ce605000000006b483045022100af246c27890c2bc07a0b7450d3d82509702a44a4defdff766355240b114ee2ac02207bb67b468452fa1b325dd5583879f5c1412e0bb4dae1c2c96c7a408796ab76f1012102ab9e8575536a1e99604a158fc60fe2ebd1cb1839e919b4ca42b8d050cfad71b2ffffffff0100c2eb0b000000001976a914df76c017354ac39bde796abe4294d31de8b5788a88ac00000000', 'txid': 'e977c07090c2a1dcaefd3f3c4ebf4e231f4116cb272f805b0b22a85e7eece09c', 'hash': 'e977c07090c2a1dcaefd3f3c4ebf4e231f4116cb272f805b0b22a85e7eece09c', 'size': 192, 'vsize': 192, 'version': 1, 'locktime': 0, 'vin': [{'txid': '05e69c373f787ab7635465db94225307e4ad6685d3df63ff605efebe3f17dae4', 'vout': 0, 'scriptSig': {'asm': '3045022100af246c27890c2bc07a0b7450d3d82509702a44a4defdff766355240b114ee2ac02207bb67b468452fa1b325dd5583879f5c1412e0bb4dae1c2c96c7a408796ab76f101 02ab9e8575536a1e99604a158fc60fe2ebd1cb1839e919b4ca42b8d050cfad71b2', 'hex': '483045022100af246c27890c2bc07a0b7450d3d82509702a44a4defdff766355240b114ee2ac02207bb67b468452fa1b325dd5583879f5c1412e0bb4dae1c2c96c7a408796ab76f1012102ab9e8575536a1e99604a158fc60fe2ebd1cb1839e919b4ca42b8d050cfad71b2'}, 'sequence': '4294967295'}], 'vout': [{'value': '2.00000000', 'n': 0, 'scriptPubKey': {'asm': 'OP_DUP OP_HASH160 df76c017354ac39bde796abe4294d31de8b5788a OP_EQUALVERIFY OP_CHECKSIG', 'hex': '76a914df76c017354ac39bde796abe4294d31de8b5788a88ac', 'type': 'p2pkh', 'address': '1MNZwhTBHN3QTXkwob7NvhVaTVKUm7MRCg'}}]}
535+
>>> tx = SegWitTransaction.unhexlify('0100000001e4da173fbefe5e60ff63dfd38566ade407532294db655463b77a783f379ce605000000006b483045022100af246c27890c2bc07a0b7450d3d82509702a44a4defdff766355240b114ee2ac02207bb67b468452fa1b325dd5583879f5c1412e0bb4dae1c2c96c7a408796ab76f1012102ab9e8575536a1e99604a158fc60fe2ebd1cb1839e919b4ca42b8d050cfad71b2ffffffff0100c2eb0b000000001976a914df76c017354ac39bde796abe4294d31de8b5788a88ac00000000')
536+
Traceback (most recent call last):
537+
File "<stdin>", line 1, in <module>
538+
File "/home/rael/Dropbox/projects/btcpy/btcpy/structs/transaction.py", line 457, in unhexlify
539+
return cls.deserialize(bytearray(unhexlify(string)))
540+
File "/home/rael/Dropbox/projects/btcpy/btcpy/structs/transaction.py", line 466, in deserialize
541+
raise TypeError('Trying to load transaction from wrong transaction serialization')
542+
TypeError: Trying to load transaction from wrong transaction serialization
543+
>>> tx = Transaction.from_json({'hex': '0100000001e4da173fbefe5e60ff63dfd38566ade407532294db655463b77a783f379ce605000000006b483045022100af246c27890c2bc07a0b7450d3d82509702a44a4defdff766355240b114ee2ac02207bb67b468452fa1b325dd5583879f5c1412e0bb4dae1c2c96c7a408796ab76f1012102ab9e8575536a1e99604a158fc60fe2ebd1cb1839e919b4ca42b8d050cfad71b2ffffffff0100c2eb0b000000001976a914df76c017354ac39bde796abe4294d31de8b5788a88ac00000000', 'txid': 'e977c07090c2a1dcaefd3f3c4ebf4e231f4116cb272f805b0b22a85e7eece09c', 'hash': 'e977c07090c2a1dcaefd3f3c4ebf4e231f4116cb272f805b0b22a85e7eece09c', 'size': 192, 'vsize': 192, 'version': 1, 'locktime': 0, 'vin': [{'txid': '05e69c373f787ab7635465db94225307e4ad6685d3df63ff605efebe3f17dae4', 'vout': 0, 'scriptSig': {'asm': '3045022100af246c27890c2bc07a0b7450d3d82509702a44a4defdff766355240b114ee2ac02207bb67b468452fa1b325dd5583879f5c1412e0bb4dae1c2c96c7a408796ab76f101 02ab9e8575536a1e99604a158fc60fe2ebd1cb1839e919b4ca42b8d050cfad71b2', 'hex': '483045022100af246c27890c2bc07a0b7450d3d82509702a44a4defdff766355240b114ee2ac02207bb67b468452fa1b325dd5583879f5c1412e0bb4dae1c2c96c7a408796ab76f1012102ab9e8575536a1e99604a158fc60fe2ebd1cb1839e919b4ca42b8d050cfad71b2'}, 'sequence': '4294967295'}], 'vout': [{'value': '2.00000000', 'n': 0, 'scriptPubKey': {'asm': 'OP_DUP OP_HASH160 df76c017354ac39bde796abe4294d31de8b5788a OP_EQUALVERIFY OP_CHECKSIG', 'hex': '76a914df76c017354ac39bde796abe4294d31de8b5788a88ac', 'type': 'p2pkh', 'address': '1MNZwhTBHN3QTXkwob7NvhVaTVKUm7MRCg'}}]})
544+
>>> tx = SegWitTransaction.from_json({'hex': '0100000001e4da173fbefe5e60ff63dfd38566ade407532294db655463b77a783f379ce605000000006b483045022100af246c27890c2bc07a0b7450d3d82509702a44a4defdff766355240b114ee2ac02207bb67b468452fa1b325dd5583879f5c1412e0bb4dae1c2c96c7a408796ab76f1012102ab9e8575536a1e99604a158fc60fe2ebd1cb1839e919b4ca42b8d050cfad71b2ffffffff0100c2eb0b000000001976a914df76c017354ac39bde796abe4294d31de8b5788a88ac00000000', 'txid': 'e977c07090c2a1dcaefd3f3c4ebf4e231f4116cb272f805b0b22a85e7eece09c', 'hash': 'e977c07090c2a1dcaefd3f3c4ebf4e231f4116cb272f805b0b22a85e7eece09c', 'size': 192, 'vsize': 192, 'version': 1, 'locktime': 0, 'vin': [{'txid': '05e69c373f787ab7635465db94225307e4ad6685d3df63ff605efebe3f17dae4', 'vout': 0, 'scriptSig': {'asm': '3045022100af246c27890c2bc07a0b7450d3d82509702a44a4defdff766355240b114ee2ac02207bb67b468452fa1b325dd5583879f5c1412e0bb4dae1c2c96c7a408796ab76f101 02ab9e8575536a1e99604a158fc60fe2ebd1cb1839e919b4ca42b8d050cfad71b2', 'hex': '483045022100af246c27890c2bc07a0b7450d3d82509702a44a4defdff766355240b114ee2ac02207bb67b468452fa1b325dd5583879f5c1412e0bb4dae1c2c96c7a408796ab76f1012102ab9e8575536a1e99604a158fc60fe2ebd1cb1839e919b4ca42b8d050cfad71b2'}, 'sequence': '4294967295'}], 'vout': [{'value': '2.00000000', 'n': 0, 'scriptPubKey': {'asm': 'OP_DUP OP_HASH160 df76c017354ac39bde796abe4294d31de8b5788a OP_EQUALVERIFY OP_CHECKSIG', 'hex': '76a914df76c017354ac39bde796abe4294d31de8b5788a88ac', 'type': 'p2pkh', 'address': '1MNZwhTBHN3QTXkwob7NvhVaTVKUm7MRCg'}}]})
545+
Traceback (most recent call last):
546+
File "<stdin>", line 1, in <module>
547+
File "/home/rael/Dropbox/projects/btcpy/btcpy/structs/transaction.py", line 737, in from_json
548+
raise TypeError('Trying to load segwit transaction from non-segwit transaction json')
549+
TypeError: Trying to load segwit transaction from non-segwit transaction json
550+
```
551+
552+
As you can see from the previous example, `Transaction` and `SegWitTransaction` classes can deserialise only
553+
json and hex strings of the appropriate type. To deserialize a generic json or hex string and build the
554+
appropriate object, one can use the `TransactionFactory`:
555+
556+
```python
557+
>>> TransactionFactory.unhexlify('0100000001e4da173fbefe5e60ff63dfd38566ade407532294db655463b77a783f379ce605000000006b483045022100af246c27890c2bc07a0b7450d3d82509702a44a4defdff766355240b114ee2ac02207bb67b468452fa1b325dd5583879f5c1412e0bb4dae1c2c96c7a408796ab76f1012102ab9e8575536a1e99604a158fc60fe2ebd1cb1839e919b4ca42b8d050cfad71b2ffffffff0100c2eb0b000000001976a914df76c017354ac39bde796abe4294d31de8b5788a88ac00000000')
558+
<btcpy.structs.transaction.Transaction object at 0x7f3717961be0>
559+
>>> TransactionFactory.from_json({'hex': '0100000001e4da173fbefe5e60ff63dfd38566ade407532294db655463b77a783f379ce605000000006b483045022100af246c27890c2bc07a0b7450d3d82509702a44a4defdff766355240b114ee2ac02207bb67b468452fa1b325dd5583879f5c1412e0bb4dae1c2c96c7a408796ab76f1012102ab9e8575536a1e99604a158fc60fe2ebd1cb1839e919b4ca42b8d050cfad71b2ffffffff0100c2eb0b000000001976a914df76c017354ac39bde796abe4294d31de8b5788a88ac00000000', 'txid': 'e977c07090c2a1dcaefd3f3c4ebf4e231f4116cb272f805b0b22a85e7eece09c', 'hash': 'e977c07090c2a1dcaefd3f3c4ebf4e231f4116cb272f805b0b22a85e7eece09c', 'size': 192, 'vsize': 192, 'version': 1, 'locktime': 0, 'vin': [{'txid': '05e69c373f787ab7635465db94225307e4ad6685d3df63ff605efebe3f17dae4', 'vout': 0, 'scriptSig': {'asm': '3045022100af246c27890c2bc07a0b7450d3d82509702a44a4defdff766355240b114ee2ac02207bb67b468452fa1b325dd5583879f5c1412e0bb4dae1c2c96c7a408796ab76f101 02ab9e8575536a1e99604a158fc60fe2ebd1cb1839e919b4ca42b8d050cfad71b2', 'hex': '483045022100af246c27890c2bc07a0b7450d3d82509702a44a4defdff766355240b114ee2ac02207bb67b468452fa1b325dd5583879f5c1412e0bb4dae1c2c96c7a408796ab76f1012102ab9e8575536a1e99604a158fc60fe2ebd1cb1839e919b4ca42b8d050cfad71b2'}, 'sequence': '4294967295'}], 'vout': [{'value': '2.00000000', 'n': 0, 'scriptPubKey': {'asm': 'OP_DUP OP_HASH160 df76c017354ac39bde796abe4294d31de8b5788a OP_EQUALVERIFY OP_CHECKSIG', 'hex': '76a914df76c017354ac39bde796abe4294d31de8b5788a88ac', 'type': 'p2pkh', 'address': '1MNZwhTBHN3QTXkwob7NvhVaTVKUm7MRCg'}}]})
560+
<btcpy.structs.transaction.Transaction object at 0x7f3717971518>
561+
```
562+
514563
Example of a transaction creation:
515564

516565
```python
@@ -574,7 +623,7 @@ parameters:
574623
| `NulldataScript` | An OP_RETURN script | A `StackData` representing the data to store in the transaction |
575624
| `MultisigScript` | A multisig script, where m out of n keys are needed to spend | `m`, the number of signatures needed to spend this output, an arbitrary number of `PublicKeys`, `n` the number of public keys provided |
576625
| `IfElseScript` | A script consisting of an `OP_IF`, a script, an `OP_ELSE`, another script and an `OP_ENDIF` | Two `ScriptPubKey` scripts, the first to be executed in the if branch, the second to be executed in the else branch |
577-
| `TimelockScript` | A script consisting of `<pushdata> OP_CHECKLOCKTIMEVERIFY OP_DROP` and a subsequent script which can be spent only after the absolute time expressed by the `<pushdata>` is expired | A `Locktime`, expressing the absolute time/number of blocks after which the subsequent script can be spent, and the locked `ScriptPubKey` |
626+
| `AbsoluteTimelockScript` | A script consisting of `<pushdata> OP_CHECKLOCKTIMEVERIFY OP_DROP` and a subsequent script which can be spent only after the absolute time expressed by the `<pushdata>` is expired | A `Locktime`, expressing the absolute time/number of blocks after which the subsequent script can be spent, and the locked `ScriptPubKey` |
578627
| `RelativeTimelockScript` | A script consisting of `<pushdata> OP_CHECKSEQUENCEVERIFY OP_DROP` and a subsequent script which can be spent only after the relative time time expressed by the `<pushdata>` is expired | A `Sequence`, expressing the relative time/ number of blocks after which the subsequent script can be spent, and the locked `ScriptPubKey` |
579628
| `Hashlock256Script` | A script consisting of `OP_HASH256 <pushdata> OP_EQUALVERIFY` and a subsequent script which can be spent only after providing the preimage of `<pushdata>` for the double SHA256 hash function | Either a `bytearray` or `StackData` representing the hashed value that locks the subsequent script, plus the locked `ScriptPubKey` |
580629
| `Hashlock160Script` | A script consisting of `OP_HASH160 <pushdata> OP_EQUALVERIFY` and a subsequent script which can be spent only after providing the preimage of `<pushdata>` for the RIPEMPD160 of the SHA256 hash function | Either a `bytearray` or `StackData` representing the hashed value that locks the subsequent script, plus the locked `ScriptPubKey` |
@@ -610,7 +659,9 @@ Additionally, the following solvers are available and they take the following in
610659
| `P2wshV0Solver` | a `ScriptPubKey`, representing the witnessScript and a `Solver` which solves the inner witnessScript | `P2wshV0Script` |
611660
| `MultisigSolver` | an arbitrary number of `PrivateKey`s | `MultisigScript` |
612661
| `IfElseSolver` | an object of type `Branch`. This is an enum and its values are `Branch.IF` and `Branch.ELSE`, these are used to specify whether we are spending the `if` or `else` branch of the script. The second parameter is a `Solver` for the script inside the desired branch. | `IfElseScript` |
613-
| `TimelockSolver` | a `Solver` of the inner timelocked script | `TimelockedScript`, `RelativeTimelockScript` |
662+
| `TimelockSolver` | a `Solver` of the inner timelocked script | `AbsoluteTimelockScript`, `RelativeTimelockScript` |
663+
| `RelativeTimelockSolver` | a `Solver` of the inner timelocked script, with absolute timelocks | `RelativeTimelockScript` |
664+
| `AbsoluteTimelockSolver` | a `Solver` of the inner timelocked script, with relative timelocks | `AbsoluteTimelockScript` |
614665
| `HashlockSolver` | the preimage needed to spend the script, as a `bytearray`, and a `Solver` for the hashlocked script | `Hashlock256Script`, `Hashlock160Script` |
615666

616667

@@ -820,7 +871,7 @@ Let's write the solvers for this script:
820871
>>> solver_if = IfElseSolver(Branch.IF, # branch selection
821872
... MultisigSolver(privk, privk2)) # inner solver
822873
>>> solver_else = IfElseSolver(Branch.ELSE,
823-
... TimelockSolver(P2pkhSolver(privk)))
874+
... RelativeTimelockSolver(Sequence(5), P2pkhSolver(privk)))
824875
```
825876

826877
### Low-level signing

btcpy/structs/script.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -922,7 +922,7 @@ def type(self):
922922

923923

924924
# noinspection PyUnresolvedReferences
925-
class TimelockScript(ScriptPubKey):
925+
class AbsoluteTimelockScript(ScriptPubKey):
926926

927927
@staticmethod
928928
def verify(bytes_):
@@ -969,10 +969,10 @@ def __init__(self, *args):
969969
super().__init__(script_body.serialize())
970970

971971
else:
972-
raise TypeError('Wrong number of params for TimelockScript __init__: {}'.format(len(args)))
972+
raise TypeError('Wrong number of params for AbsoluteTimelockScript __init__: {}'.format(len(args)))
973973

974974
def __repr__(self):
975-
return 'TimelockScript({}, {})'.format(self.locktime, self.locked_script)
975+
return 'AbsoluteTimelockScript({}, {})'.format(self.locktime, self.locked_script)
976976

977977
@property
978978
def type(self):
@@ -1152,7 +1152,7 @@ class ScriptBuilder(object):
11521152
MultisigScript,
11531153
IfElseScript,
11541154
RelativeTimelockScript,
1155-
TimelockScript,
1155+
AbsoluteTimelockScript,
11561156
Hashlock256Script,
11571157
Hashlock160Script,
11581158
P2wpkhV0Script,

btcpy/structs/sig.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from ..lib.types import Immutable, HexSerializable
1616
from .script import (Script, P2shScript, ScriptSig, P2pkhScript, P2wpkhV0Script, P2wshV0Script,
17-
P2pkScript, MultisigScript, TimelockScript, RelativeTimelockScript,
17+
P2pkScript, MultisigScript, AbsoluteTimelockScript, RelativeTimelockScript,
1818
IfElseScript, HashlockScript, StackData)
1919
from ..lib.parsing import Stream
2020

tests/data/scripts.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"if_else_timelock": {
2121
"hex": "6352210384478d41e71dc6c3f9edde0f928a47d1b724c05984ebfb4e7d0422e80abe95ff2103eb27fa93667e4f48e36071eb21c7229e5416ff0abd2886d59c8f314fb3cbee4052ae67037b9710b175210384478d41e71dc6c3f9edde0f928a47d1b724c05984ebfb4e7d0422e80abe95ffac68",
2222
"asm": "OP_IF OP_2 0384478d41e71dc6c3f9edde0f928a47d1b724c05984ebfb4e7d0422e80abe95ff 03eb27fa93667e4f48e36071eb21c7229e5416ff0abd2886d59c8f314fb3cbee40 OP_2 OP_CHECKMULTISIG OP_ELSE 7b9710 OP_CHECKLOCKTIMEVERIFY OP_DROP 0384478d41e71dc6c3f9edde0f928a47d1b724c05984ebfb4e7d0422e80abe95ff OP_CHECKSIG OP_ENDIF",
23-
"code": "IfElseScript(MultisigScript(2, PublicKey.unhexlify(\"0384478d41e71dc6c3f9edde0f928a47d1b724c05984ebfb4e7d0422e80abe95ff\"), PublicKey.unhexlify(\"03eb27fa93667e4f48e36071eb21c7229e5416ff0abd2886d59c8f314fb3cbee40\"), 2), TimelockScript(Locktime(1087355), P2pkScript(PublicKey.unhexlify(\"0384478d41e71dc6c3f9edde0f928a47d1b724c05984ebfb4e7d0422e80abe95ff\"))))",
23+
"code": "IfElseScript(MultisigScript(2, PublicKey.unhexlify(\"0384478d41e71dc6c3f9edde0f928a47d1b724c05984ebfb4e7d0422e80abe95ff\"), PublicKey.unhexlify(\"03eb27fa93667e4f48e36071eb21c7229e5416ff0abd2886d59c8f314fb3cbee40\"), 2), AbsoluteTimelockScript(Locktime(1087355), P2pkScript(PublicKey.unhexlify(\"0384478d41e71dc6c3f9edde0f928a47d1b724c05984ebfb4e7d0422e80abe95ff\"))))",
2424
"type": "if{ multisig }else{ [timelock] p2pk }"
2525
},
2626
"relativetimelock": {

tests/integration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ def get_name():
172172

173173
@staticmethod
174174
def get_script_cls():
175-
return TimelockScript
175+
return AbsoluteTimelockScript
176176

177177
@staticmethod
178178
def get_args():

0 commit comments

Comments
 (0)