Skip to content

Commit d5d4ad8

Browse files
committed
Merge #8883: Add all standard TXO types to bitcoin-tx
0c50909 testcases: explicitly specify transaction version 1 (John Newbery) b7e144b Add test cases to test new bitcoin-tx functionality (jnewbery) 61a1534 Add all transaction output types to bitcoin-tx. (jnewbery) 1814b08 add p2sh and segwit options to bitcoin-tx outscript command (Stanislas Marion)
2 parents fac0f30 + 0c50909 commit d5d4ad8

24 files changed

+541
-31
lines changed

src/bitcoin-tx.cpp

Lines changed: 164 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,16 @@ static int AppInitRawTx(int argc, char* argv[])
7878
strUsage += HelpMessageOpt("locktime=N", _("Set TX lock time to N"));
7979
strUsage += HelpMessageOpt("nversion=N", _("Set TX version to N"));
8080
strUsage += HelpMessageOpt("outaddr=VALUE:ADDRESS", _("Add address-based output to TX"));
81+
strUsage += HelpMessageOpt("outpubkey=VALUE:PUBKEY[:FLAGS]", _("Add pay-to-pubkey output to TX") + ". " +
82+
_("Optionally add the \"W\" flag to produce a pay-to-witness-pubkey-hash output") + ". " +
83+
_("Optionally add the \"S\" flag to wrap the output in a pay-to-script-hash."));
8184
strUsage += HelpMessageOpt("outdata=[VALUE:]DATA", _("Add data-based output to TX"));
82-
strUsage += HelpMessageOpt("outscript=VALUE:SCRIPT", _("Add raw script output to TX"));
85+
strUsage += HelpMessageOpt("outscript=VALUE:SCRIPT[:FLAGS]", _("Add raw script output to TX") + ". " +
86+
_("Optionally add the \"W\" flag to produce a pay-to-witness-script-hash output") + ". " +
87+
_("Optionally add the \"S\" flag to wrap the output in a pay-to-script-hash."));
88+
strUsage += HelpMessageOpt("outmultisig=VALUE:REQUIRED:PUBKEYS:PUBKEY1:PUBKEY2:....[:FLAGS]", _("Add Pay To n-of-m Multi-sig output to TX. n = REQUIRED, m = PUBKEYS") + ". " +
89+
_("Optionally add the \"W\" flag to produce a pay-to-witness-script-hash output") + ". " +
90+
_("Optionally add the \"S\" flag to wrap the output in a pay-to-script-hash."));
8391
strUsage += HelpMessageOpt("sign=SIGHASH-FLAGS", _("Add zero or more signatures to transaction") + ". " +
8492
_("This command requires JSON registers:") +
8593
_("prevtxs=JSON object") + ", " +
@@ -168,6 +176,14 @@ static void RegisterLoad(const std::string& strInput)
168176
RegisterSetJson(key, valStr);
169177
}
170178

179+
static CAmount ExtractAndValidateValue(const std::string& strValue)
180+
{
181+
CAmount value;
182+
if (!ParseMoney(strValue, value))
183+
throw std::runtime_error("invalid TX output value");
184+
return value;
185+
}
186+
171187
static void MutateTxVersion(CMutableTransaction& tx, const std::string& cmdVal)
172188
{
173189
int64_t newVersion = atoi64(cmdVal);
@@ -222,25 +238,18 @@ static void MutateTxAddInput(CMutableTransaction& tx, const std::string& strInpu
222238

223239
static void MutateTxAddOutAddr(CMutableTransaction& tx, const std::string& strInput)
224240
{
225-
// separate VALUE:ADDRESS in string
226-
size_t pos = strInput.find(':');
227-
if ((pos == std::string::npos) ||
228-
(pos == 0) ||
229-
(pos == (strInput.size() - 1)))
230-
throw std::runtime_error("TX output missing separator");
241+
// Separate into VALUE:ADDRESS
242+
std::vector<std::string> vStrInputParts;
243+
boost::split(vStrInputParts, strInput, boost::is_any_of(":"));
231244

232-
// extract and validate VALUE
233-
std::string strValue = strInput.substr(0, pos);
234-
CAmount value;
235-
if (!ParseMoney(strValue, value))
236-
throw std::runtime_error("invalid TX output value");
245+
// Extract and validate VALUE
246+
CAmount value = ExtractAndValidateValue(vStrInputParts[0]);
237247

238248
// extract and validate ADDRESS
239-
std::string strAddr = strInput.substr(pos + 1, std::string::npos);
249+
std::string strAddr = vStrInputParts[1];
240250
CBitcoinAddress addr(strAddr);
241251
if (!addr.IsValid())
242252
throw std::runtime_error("invalid TX output address");
243-
244253
// build standard output script via GetScriptForDestination()
245254
CScript scriptPubKey = GetScriptForDestination(addr.Get());
246255

@@ -249,6 +258,114 @@ static void MutateTxAddOutAddr(CMutableTransaction& tx, const std::string& strIn
249258
tx.vout.push_back(txout);
250259
}
251260

261+
static void MutateTxAddOutPubKey(CMutableTransaction& tx, const std::string& strInput)
262+
{
263+
// Separate into VALUE:PUBKEY[:FLAGS]
264+
std::vector<std::string> vStrInputParts;
265+
boost::split(vStrInputParts, strInput, boost::is_any_of(":"));
266+
267+
// Extract and validate VALUE
268+
CAmount value = ExtractAndValidateValue(vStrInputParts[0]);
269+
270+
// Extract and validate PUBKEY
271+
CPubKey pubkey(ParseHex(vStrInputParts[1]));
272+
if (!pubkey.IsFullyValid())
273+
throw std::runtime_error("invalid TX output pubkey");
274+
CScript scriptPubKey = GetScriptForRawPubKey(pubkey);
275+
CBitcoinAddress addr(scriptPubKey);
276+
277+
// Extract and validate FLAGS
278+
bool bSegWit = false;
279+
bool bScriptHash = false;
280+
if (vStrInputParts.size() == 3) {
281+
std::string flags = vStrInputParts[2];
282+
bSegWit = (flags.find("W") != std::string::npos);
283+
bScriptHash = (flags.find("S") != std::string::npos);
284+
}
285+
286+
if (bSegWit) {
287+
// Call GetScriptForWitness() to build a P2WSH scriptPubKey
288+
scriptPubKey = GetScriptForWitness(scriptPubKey);
289+
}
290+
if (bScriptHash) {
291+
// Get the address for the redeem script, then call
292+
// GetScriptForDestination() to construct a P2SH scriptPubKey.
293+
CBitcoinAddress redeemScriptAddr(scriptPubKey);
294+
scriptPubKey = GetScriptForDestination(redeemScriptAddr.Get());
295+
}
296+
297+
// construct TxOut, append to transaction output list
298+
CTxOut txout(value, scriptPubKey);
299+
tx.vout.push_back(txout);
300+
}
301+
302+
static void MutateTxAddOutMultiSig(CMutableTransaction& tx, const std::string& strInput)
303+
{
304+
// Separate into VALUE:REQUIRED:NUMKEYS:PUBKEY1:PUBKEY2:....[:FLAGS]
305+
std::vector<std::string> vStrInputParts;
306+
boost::split(vStrInputParts, strInput, boost::is_any_of(":"));
307+
308+
// Check that there are enough parameters
309+
if (vStrInputParts.size()<3)
310+
throw std::runtime_error("Not enough multisig parameters");
311+
312+
// Extract and validate VALUE
313+
CAmount value = ExtractAndValidateValue(vStrInputParts[0]);
314+
315+
// Extract REQUIRED
316+
uint32_t required = stoul(vStrInputParts[1]);
317+
318+
// Extract NUMKEYS
319+
uint32_t numkeys = stoul(vStrInputParts[2]);
320+
321+
// Validate there are the correct number of pubkeys
322+
if (vStrInputParts.size() < numkeys + 3)
323+
throw std::runtime_error("incorrect number of multisig pubkeys");
324+
325+
if (required < 1 || required > 20 || numkeys < 1 || numkeys > 20 || numkeys < required)
326+
throw std::runtime_error("multisig parameter mismatch. Required " \
327+
+ std::to_string(required) + " of " + std::to_string(numkeys) + "signatures.");
328+
329+
// extract and validate PUBKEYs
330+
std::vector<CPubKey> pubkeys;
331+
for(int pos = 1; pos <= int(numkeys); pos++) {
332+
CPubKey pubkey(ParseHex(vStrInputParts[pos + 2]));
333+
if (!pubkey.IsFullyValid())
334+
throw std::runtime_error("invalid TX output pubkey");
335+
pubkeys.push_back(pubkey);
336+
}
337+
338+
// Extract FLAGS
339+
bool bSegWit = false;
340+
bool bScriptHash = false;
341+
if (vStrInputParts.size() == numkeys + 4) {
342+
std::string flags = vStrInputParts.back();
343+
bSegWit = (flags.find("W") != std::string::npos);
344+
bScriptHash = (flags.find("S") != std::string::npos);
345+
}
346+
else if (vStrInputParts.size() > numkeys + 4) {
347+
// Validate that there were no more parameters passed
348+
throw std::runtime_error("Too many parameters");
349+
}
350+
351+
CScript scriptPubKey = GetScriptForMultisig(required, pubkeys);
352+
353+
if (bSegWit) {
354+
// Call GetScriptForWitness() to build a P2WSH scriptPubKey
355+
scriptPubKey = GetScriptForWitness(scriptPubKey);
356+
}
357+
if (bScriptHash) {
358+
// Get the address for the redeem script, then call
359+
// GetScriptForDestination() to construct a P2SH scriptPubKey.
360+
CBitcoinAddress addr(scriptPubKey);
361+
scriptPubKey = GetScriptForDestination(addr.Get());
362+
}
363+
364+
// construct TxOut, append to transaction output list
365+
CTxOut txout(value, scriptPubKey);
366+
tx.vout.push_back(txout);
367+
}
368+
252369
static void MutateTxAddOutData(CMutableTransaction& tx, const std::string& strInput)
253370
{
254371
CAmount value = 0;
@@ -260,10 +377,8 @@ static void MutateTxAddOutData(CMutableTransaction& tx, const std::string& strIn
260377
throw std::runtime_error("TX output value not specified");
261378

262379
if (pos != std::string::npos) {
263-
// extract and validate VALUE
264-
std::string strValue = strInput.substr(0, pos);
265-
if (!ParseMoney(strValue, value))
266-
throw std::runtime_error("invalid TX output value");
380+
// Extract and validate VALUE
381+
value = ExtractAndValidateValue(strInput.substr(0, pos));
267382
}
268383

269384
// extract and validate DATA
@@ -280,21 +395,35 @@ static void MutateTxAddOutData(CMutableTransaction& tx, const std::string& strIn
280395

281396
static void MutateTxAddOutScript(CMutableTransaction& tx, const std::string& strInput)
282397
{
283-
// separate VALUE:SCRIPT in string
284-
size_t pos = strInput.find(':');
285-
if ((pos == std::string::npos) ||
286-
(pos == 0))
398+
// separate VALUE:SCRIPT[:FLAGS]
399+
std::vector<std::string> vStrInputParts;
400+
boost::split(vStrInputParts, strInput, boost::is_any_of(":"));
401+
if (vStrInputParts.size() < 2)
287402
throw std::runtime_error("TX output missing separator");
288403

289-
// extract and validate VALUE
290-
std::string strValue = strInput.substr(0, pos);
291-
CAmount value;
292-
if (!ParseMoney(strValue, value))
293-
throw std::runtime_error("invalid TX output value");
404+
// Extract and validate VALUE
405+
CAmount value = ExtractAndValidateValue(vStrInputParts[0]);
294406

295407
// extract and validate script
296-
std::string strScript = strInput.substr(pos + 1, std::string::npos);
297-
CScript scriptPubKey = ParseScript(strScript); // throws on err
408+
std::string strScript = vStrInputParts[1];
409+
CScript scriptPubKey = ParseScript(strScript);
410+
411+
// Extract FLAGS
412+
bool bSegWit = false;
413+
bool bScriptHash = false;
414+
if (vStrInputParts.size() == 3) {
415+
std::string flags = vStrInputParts.back();
416+
bSegWit = (flags.find("W") != std::string::npos);
417+
bScriptHash = (flags.find("S") != std::string::npos);
418+
}
419+
420+
if (bSegWit) {
421+
scriptPubKey = GetScriptForWitness(scriptPubKey);
422+
}
423+
if (bScriptHash) {
424+
CBitcoinAddress addr(scriptPubKey);
425+
scriptPubKey = GetScriptForDestination(addr.Get());
426+
}
298427

299428
// construct TxOut, append to transaction output list
300429
CTxOut txout(value, scriptPubKey);
@@ -538,10 +667,14 @@ static void MutateTx(CMutableTransaction& tx, const std::string& command,
538667
MutateTxDelOutput(tx, commandVal);
539668
else if (command == "outaddr")
540669
MutateTxAddOutAddr(tx, commandVal);
541-
else if (command == "outdata")
542-
MutateTxAddOutData(tx, commandVal);
670+
else if (command == "outpubkey")
671+
MutateTxAddOutPubKey(tx, commandVal);
672+
else if (command == "outmultisig")
673+
MutateTxAddOutMultiSig(tx, commandVal);
543674
else if (command == "outscript")
544675
MutateTxAddOutScript(tx, commandVal);
676+
else if (command == "outdata")
677+
MutateTxAddOutData(tx, commandVal);
545678

546679
else if (command == "sign") {
547680
if (!ecc) { ecc.reset(new Secp256k1Init()); }

src/test/data/bitcoin-util-test.json

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,46 @@
117117
"output_cmp": "txcreate2.json",
118118
"description": "Parses a transation with no inputs and a single output script (output in json)"
119119
},
120+
{ "exec": "./bitcoin-tx",
121+
"args": ["-create", "outscript=0:OP_DROP", "nversion=1"],
122+
"output_cmp": "txcreatescript1.hex",
123+
"description": "Create a new transaction with a single output script (OP_DROP)"
124+
},
125+
{ "exec": "./bitcoin-tx",
126+
"args": ["-json", "-create", "outscript=0:OP_DROP", "nversion=1"],
127+
"output_cmp": "txcreatescript1.json",
128+
"description": "Create a new transaction with a single output script (OP_DROP) (output as json)"
129+
},
130+
{ "exec": "./bitcoin-tx",
131+
"args": ["-create", "outscript=0:OP_DROP:S", "nversion=1"],
132+
"output_cmp": "txcreatescript2.hex",
133+
"description": "Create a new transaction with a single output script (OP_DROP) in a P2SH"
134+
},
135+
{ "exec": "./bitcoin-tx",
136+
"args": ["-json", "-create", "outscript=0:OP_DROP:S", "nversion=1"],
137+
"output_cmp": "txcreatescript2.json",
138+
"description": "Create a new transaction with a single output script (OP_DROP) in a P2SH (output as json)"
139+
},
140+
{ "exec": "./bitcoin-tx",
141+
"args": ["-create", "outscript=0:OP_DROP:W", "nversion=1"],
142+
"output_cmp": "txcreatescript3.hex",
143+
"description": "Create a new transaction with a single output script (OP_DROP) in a P2WSH"
144+
},
145+
{ "exec": "./bitcoin-tx",
146+
"args": ["-json", "-create", "outscript=0:OP_DROP:W", "nversion=1"],
147+
"output_cmp": "txcreatescript3.json",
148+
"description": "Create a new transaction with a single output script (OP_DROP) in a P2WSH (output as json)"
149+
},
150+
{ "exec": "./bitcoin-tx",
151+
"args": ["-create", "outscript=0:OP_DROP:WS", "nversion=1"],
152+
"output_cmp": "txcreatescript4.hex",
153+
"description": "Create a new transaction with a single output script (OP_DROP) in a P2WSH, wrapped in a P2SH"
154+
},
155+
{ "exec": "./bitcoin-tx",
156+
"args": ["-json", "-create", "outscript=0:OP_DROP:WS", "nversion=1"],
157+
"output_cmp": "txcreatescript4.json",
158+
"description": "Create a new transaction with a single output script (OP_DROP) in a P2SH, wrapped in a P2SH (output as json)"
159+
},
120160
{ "exec": "./bitcoin-tx",
121161
"args":
122162
["-create", "nversion=1",
@@ -151,6 +191,42 @@
151191
"output_cmp": "txcreatesignv2.hex",
152192
"description": "Creates a new transaction with a single input and a single output, and then signs the transaction"
153193
},
194+
{ "exec": "./bitcoin-tx",
195+
"args":
196+
["-create", "outpubkey=0:02a5613bd857b7048924264d1e70e08fb2a7e6527d32b7ab1bb993ac59964ff397", "nversion=1"],
197+
"output_cmp": "txcreateoutpubkey1.hex",
198+
"description": "Creates a new transaction with a single pay-to-pubkey output"
199+
},
200+
{ "exec": "./bitcoin-tx",
201+
"args":
202+
["-json", "-create", "outpubkey=0:02a5613bd857b7048924264d1e70e08fb2a7e6527d32b7ab1bb993ac59964ff397", "nversion=1"],
203+
"output_cmp": "txcreateoutpubkey1.json",
204+
"description": "Creates a new transaction with a single pay-to-pubkey output (output as json)"
205+
},
206+
{ "exec": "./bitcoin-tx",
207+
"args":
208+
["-create", "outpubkey=0:02a5613bd857b7048924264d1e70e08fb2a7e6527d32b7ab1bb993ac59964ff397:W", "nversion=1"],
209+
"output_cmp": "txcreateoutpubkey2.hex",
210+
"description": "Creates a new transaction with a single pay-to-witness-pubkey output"
211+
},
212+
{ "exec": "./bitcoin-tx",
213+
"args":
214+
["-json", "-create", "outpubkey=0:02a5613bd857b7048924264d1e70e08fb2a7e6527d32b7ab1bb993ac59964ff397:W", "nversion=1"],
215+
"output_cmp": "txcreateoutpubkey2.json",
216+
"description": "Creates a new transaction with a single pay-to-witness-pubkey output (output as json)"
217+
},
218+
{ "exec": "./bitcoin-tx",
219+
"args":
220+
["-create", "outpubkey=0:02a5613bd857b7048924264d1e70e08fb2a7e6527d32b7ab1bb993ac59964ff397:WS", "nversion=1"],
221+
"output_cmp": "txcreateoutpubkey3.hex",
222+
"description": "Creates a new transaction with a single pay-to-witness-pubkey, wrapped in P2SH output"
223+
},
224+
{ "exec": "./bitcoin-tx",
225+
"args":
226+
["-json", "-create", "outpubkey=0:02a5613bd857b7048924264d1e70e08fb2a7e6527d32b7ab1bb993ac59964ff397:WS", "nversion=1"],
227+
"output_cmp": "txcreateoutpubkey3.json",
228+
"description": "Creates a new transaction with a single pay-to-pub-key output, wrapped in P2SH (output as json)"
229+
},
154230
{ "exec": "./bitcoin-tx",
155231
"args":
156232
["-create",
@@ -236,5 +312,45 @@
236312
"in=5897de6bd6027a475eadd57019d4e6872c396d0716c4875a5f1a6fcfdf385c1f:0:1"],
237313
"output_cmp": "txcreatedata_seq1.json",
238314
"description": "Adds a new input with sequence number to a transaction (output in json)"
315+
},
316+
{ "exec": "./bitcoin-tx",
317+
"args": ["-create", "outmultisig=1:2:3:02a5613bd857b7048924264d1e70e08fb2a7e6527d32b7ab1bb993ac59964ff397:021ac43c7ff740014c3b33737ede99c967e4764553d1b2b83db77c83b8715fa72d:02df2089105c77f266fa11a9d33f05c735234075f2e8780824c6b709415f9fb485", "nversion=1"],
318+
"output_cmp": "txcreatemultisig1.hex",
319+
"description": "Creates a new transaction with a single 2-of-3 multisig output"
320+
},
321+
{ "exec": "./bitcoin-tx",
322+
"args": ["-json", "-create", "outmultisig=1:2:3:02a5613bd857b7048924264d1e70e08fb2a7e6527d32b7ab1bb993ac59964ff397:021ac43c7ff740014c3b33737ede99c967e4764553d1b2b83db77c83b8715fa72d:02df2089105c77f266fa11a9d33f05c735234075f2e8780824c6b709415f9fb485", "nversion=1"],
323+
"output_cmp": "txcreatemultisig1.json",
324+
"description": "Creates a new transaction with a single 2-of-3 multisig output (output in json)"
325+
},
326+
{ "exec": "./bitcoin-tx",
327+
"args": ["-create", "outmultisig=1:2:3:02a5613bd857b7048924264d1e70e08fb2a7e6527d32b7ab1bb993ac59964ff397:021ac43c7ff740014c3b33737ede99c967e4764553d1b2b83db77c83b8715fa72d:02df2089105c77f266fa11a9d33f05c735234075f2e8780824c6b709415f9fb485:S", "nversion=1"],
328+
"output_cmp": "txcreatemultisig2.hex",
329+
"description": "Creates a new transaction with a single 2-of-3 multisig in a P2SH output"
330+
},
331+
{ "exec": "./bitcoin-tx",
332+
"args": ["-json", "-create", "outmultisig=1:2:3:02a5613bd857b7048924264d1e70e08fb2a7e6527d32b7ab1bb993ac59964ff397:021ac43c7ff740014c3b33737ede99c967e4764553d1b2b83db77c83b8715fa72d:02df2089105c77f266fa11a9d33f05c735234075f2e8780824c6b709415f9fb485:S", "nversion=1"],
333+
"output_cmp": "txcreatemultisig2.json",
334+
"description": "Creates a new transaction with a single 2-of-3 multisig in a P2SH output (output in json)"
335+
},
336+
{ "exec": "./bitcoin-tx",
337+
"args": ["-create", "outmultisig=1:2:3:02a5613bd857b7048924264d1e70e08fb2a7e6527d32b7ab1bb993ac59964ff397:021ac43c7ff740014c3b33737ede99c967e4764553d1b2b83db77c83b8715fa72d:02df2089105c77f266fa11a9d33f05c735234075f2e8780824c6b709415f9fb485:W", "nversion=1"],
338+
"output_cmp": "txcreatemultisig3.hex",
339+
"description": "Creates a new transaction with a single 2-of-3 multisig in a P2WSH output"
340+
},
341+
{ "exec": "./bitcoin-tx",
342+
"args": ["-json", "-create", "outmultisig=1:2:3:02a5613bd857b7048924264d1e70e08fb2a7e6527d32b7ab1bb993ac59964ff397:021ac43c7ff740014c3b33737ede99c967e4764553d1b2b83db77c83b8715fa72d:02df2089105c77f266fa11a9d33f05c735234075f2e8780824c6b709415f9fb485:W", "nversion=1"],
343+
"output_cmp": "txcreatemultisig3.json",
344+
"description": "Creates a new transaction with a single 2-of-3 multisig in a P2WSH output (output in json)"
345+
},
346+
{ "exec": "./bitcoin-tx",
347+
"args": ["-create", "outmultisig=1:2:3:02a5613bd857b7048924264d1e70e08fb2a7e6527d32b7ab1bb993ac59964ff397:021ac43c7ff740014c3b33737ede99c967e4764553d1b2b83db77c83b8715fa72d:02df2089105c77f266fa11a9d33f05c735234075f2e8780824c6b709415f9fb485:WS", "nversion=1"],
348+
"output_cmp": "txcreatemultisig4.hex",
349+
"description": "Creates a new transaction with a single 2-of-3 multisig in a P2WSH output, wrapped in P2SH"
350+
},
351+
{ "exec": "./bitcoin-tx",
352+
"args": ["-json", "-create", "outmultisig=1:2:3:02a5613bd857b7048924264d1e70e08fb2a7e6527d32b7ab1bb993ac59964ff397:021ac43c7ff740014c3b33737ede99c967e4764553d1b2b83db77c83b8715fa72d:02df2089105c77f266fa11a9d33f05c735234075f2e8780824c6b709415f9fb485:WS", "nversion=1"],
353+
"output_cmp": "txcreatemultisig4.json",
354+
"description": "Creates a new transaction with a single 2-of-3 multisig in a P2WSH output, wrapped in P2SH (output in json)"
239355
}
240356
]

src/test/data/txcreatemultisig1.hex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
01000000000100e1f5050000000069522102a5613bd857b7048924264d1e70e08fb2a7e6527d32b7ab1bb993ac59964ff39721021ac43c7ff740014c3b33737ede99c967e4764553d1b2b83db77c83b8715fa72d2102df2089105c77f266fa11a9d33f05c735234075f2e8780824c6b709415f9fb48553ae00000000

0 commit comments

Comments
 (0)