Merge pull request #15 from zcash/modes

Modes
This commit is contained in:
arcalinea 2017-08-02 21:42:26 -07:00 committed by GitHub
commit 29c2249a53
9 changed files with 96 additions and 102 deletions

2
.gitignore vendored
View File

@ -1,3 +1,3 @@
*.pyc
xcatdb/
xcat.egg-info/
.tmp/

View File

@ -50,11 +50,15 @@ Since this is beta software that is still under active development, we recommend
We use the term "seller" for whoever initiates the trade, and "buyer" for whoever fulfills the other end.
### Seller Setup:
To start a trade using default addresses, edit the buyer and seller addresses for the chosen network in `xcatconf.py`. You will initiate the trade using these default addresses by passing in the variable through a `-conf` flag.
### Seller:
To initiate a new trade, the seller enters all the necessary trade data and saves it under a tradeid. We will use the tradeid "testtrade" for this example. The amounts being traded will have to be agreed upon in advance, and the seller will need the buyer's destination addresses on both chains.
To initiate a new trade, the seller enters all the necessary trade data and saves it under a tradeid. We will use the tradeid "testtrade" for this example. The amounts being traded will have to be agreed upon in advance, and the seller will need the buyer's destination addresses on both chains. The `-c` field will pass in the name of the network addresses set in `xcatconf.py`. (If addresses are not set, initiator addresses are generated on your computer, and you can enter the buyer's addresses on the command line.)
`xcat newtrade testtrade`
`xcat -c=regtest newtrade testtrade`
After creating, the seller exports the trade data encoded as a hex string to transfer the terms of the trade to the buyer. They can send this hex string to the buyer through email or any other channel, but there is also built-in support to transfer the data directly between two computers through [magic-wormhole](https://github.com/warner/magic-wormhole).

View File

@ -8,9 +8,9 @@ setup(
entry_points = {
"console_scripts": ['xcat = xcat.cli:main']
},
description="Xcat is a package to facilitate cross-chain atomic transactions.",
description="Xcat is a package that creates cross-chain atomic transactions.",
author="arcalinea and arielgabizon",
author_email="xcat@z.cash",
author_email="arcalinea@z.cash",
license="MIT",
url="http://github.com/zcash/xcat",
packages=find_packages()

View File

@ -27,9 +27,7 @@ def validateaddress(addr):
return bitcoind.validateaddress(addr)
def find_secret(p2sh, fundtx_input):
print("fundtx_input:", fundtx_input)
txs = bitcoind.call('listtransactions', "*", 20, 0, True)
print('Length of txs from listtransactions():', len(txs))
for tx in txs:
raw = bitcoind.gettransaction(lx(tx['txid']))['hex']
decoded = bitcoind.decoderawtransaction(raw)
@ -43,12 +41,13 @@ def find_secret(p2sh, fundtx_input):
return
def parse_secret(txid):
decoded = bitcoind.getrawtransaction(lx(txid), 1)
print("Decoded", decoded)
# decoded = bitcoind.decoderawtransaction(raw)
asm = decoded['vin'][0]['scriptSig']['asm'].split(" ")
secret = asm[2]
print("Found secret: ", secret)
raw = zcashd.gettransaction(txid, True)['hex']
decoded = zcashd.call('decoderawtransaction', raw)
scriptSig = decoded['vin'][0]['scriptSig']
asm = scriptSig['asm'].split(" ")
pubkey = asm[1]
secret = x2s(asm[2])
redeemPubkey = P2PKHBitcoinAddress.from_pubkey(x(pubkey))
return secret
def get_keys(funder_address, redeemer_address):
@ -68,14 +67,13 @@ def hashtimelockcontract(funder, redeemer, commitment, locktime):
commitment = x(commitment)
# h = sha256(secret)
blocknum = bitcoind.getblockcount()
print("Current blocknum", blocknum)
print("Current blocknum on Bitcoin: ", blocknum)
redeemblocknum = blocknum + locktime
print("REDEEMBLOCKNUM BITCOIN", redeemblocknum)
print("COMMITMENT", commitment)
print("Redeemblocknum on Bitcoin: ", redeemblocknum)
redeemScript = CScript([OP_IF, OP_SHA256, commitment, OP_EQUALVERIFY,OP_DUP, OP_HASH160,
redeemerAddr, OP_ELSE, redeemblocknum, OP_CHECKLOCKTIMEVERIFY, OP_DROP, OP_DUP, OP_HASH160,
funderAddr, OP_ENDIF,OP_EQUALVERIFY, OP_CHECKSIG])
print("Redeem script for p2sh contract on Bitcoin blockchain:", b2x(redeemScript))
# print("Redeem script for p2sh contract on Bitcoin blockchain: {0}".format(b2x(redeemScript)))
txin_scriptPubKey = redeemScript.to_p2sh_scriptPubKey()
# Convert the P2SH scriptPubKey to a base58 Bitcoin address
txin_p2sh_address = CBitcoinAddress.from_scriptPubKey(txin_scriptPubKey)
@ -105,7 +103,7 @@ def get_fund_status(p2sh):
bitcoind.importaddress(p2sh, "", False)
amount = bitcoind.getreceivedbyaddress(p2sh, 0)
amount = amount/COIN
print("Amount in bitcoin get_fund_status", amount, p2sh)
print("Amount in bitcoin p2sh: ", amount, p2sh)
if amount > 0:
return 'funded'
else:
@ -152,7 +150,7 @@ def redeem_contract(contract, secret):
quit()
fundtx = find_transaction_to_address(p2sh)
amount = fundtx['amount'] / COIN
print("Found fundtx:", fundtx)
# print("Found fund_tx: ", fundtx)
p2sh = P2SHBitcoinAddress(p2sh)
if fundtx['address'] == p2sh:
print("Found {0} in p2sh {1}, redeeming...".format(amount, p2sh))
@ -170,21 +168,17 @@ def redeem_contract(contract, secret):
# TODO: protect privkey better, separate signing from rawtx creation
privkey = bitcoind.dumpprivkey(redeemPubKey)
sig = privkey.sign(sighash) + bytes([SIGHASH_ALL])
print("SECRET", secret)
preimage = secret.encode('utf-8')
txin.scriptSig = CScript([sig, privkey.pub, preimage, OP_TRUE, zec_redeemScript])
print("txin.scriptSig", b2x(txin.scriptSig))
# print("txin.scriptSig", b2x(txin.scriptSig))
txin_scriptPubKey = zec_redeemScript.to_p2sh_scriptPubKey()
print('Redeem txhex', b2x(tx.serialize()))
print('Raw redeem transaction hex: ', b2x(tx.serialize()))
VerifyScript(txin.scriptSig, txin_scriptPubKey, tx, 0, (SCRIPT_VERIFY_P2SH,))
print("script verified, sending raw tx")
print("Script verified, sending raw transaction...")
txid = bitcoind.sendrawtransaction(tx)
fund_tx = str(fundtx['outpoint'])
redeem_tx = b2x(lx(b2x(txid)))
print("Returning fund_tx", fund_tx)
print("Txid of submitted redeem tx: ", redeem_tx)
print("TXID SUCCESSFULLY REDEEMED")
return {"redeem_tx": redeem_tx, "fund_tx": fund_tx}
else:
print("nLocktime exceeded, refunding")
@ -192,9 +186,6 @@ def redeem_contract(contract, secret):
txid = bitcoind.sendtoaddress(refundPubKey, fundtx['amount'] - FEE)
fund_tx = str(fundtx['outpoint'])
refund_tx = b2x(lx(b2x(txid)))
print("Returning fund_tx", fund_tx)
print("Txid of submitted refund tx: ", refund_tx)
print("TXID SUCCESSFULLY REDEEMED")
return {"refund_tx": refund_tx, "fund_tx": fund_tx}
else:
print("No contract for this p2sh found in database", p2sh)
@ -222,7 +213,6 @@ def find_transaction_to_address(p2sh):
for tx in txs:
if tx['address'] == CBitcoinAddress(p2sh):
print("Found tx to p2sh", p2sh)
print(tx)
return tx
def new_bitcoin_addr():

View File

@ -13,7 +13,7 @@ def save_state(trade, tradeid):
def checkSellStatus(tradeid):
trade = db.get(tradeid)
status = seller_check_status(trade)
print("In checkSellStatus", status)
print("Trade status: {0}\n".format(status))
if status == 'init':
userInput.authorize_fund_sell(trade)
fund_tx = fund_sell_contract(trade)
@ -22,11 +22,9 @@ def checkSellStatus(tradeid):
save_state(trade, tradeid)
elif status == 'buyerFunded':
secret = db.get_secret(tradeid)
print("SECRET found in checksellactions", secret)
print("Retrieved secret to redeem funds for {0}: {1}".format(tradeid, secret))
txs = seller_redeem_p2sh(trade, secret)
print("TXS IN SELLER REDEEM BUYER TX", txs)
trade.buy.redeem_tx = txs['redeem_tx']
print("TRADE SUCCESSFULLY REDEEMED", trade)
save_state(trade, tradeid)
# Remove from db? Or just from temporary file storage
cleanup(tradeid)
@ -71,7 +69,7 @@ def seller_check_status(trade):
def checkBuyStatus(tradeid):
trade = db.get(tradeid)
status = buyer_check_status(trade)
print("In checkBuyStatus", status)
print("Trade status: {0}\n".format(status))
if status == 'init':
print("Trade has not yet started, waiting for seller to fund the sell p2sh.")
elif status == 'buyerRedeemed':
@ -81,19 +79,16 @@ def checkBuyStatus(tradeid):
print("Trade commitment", trade.commitment)
# if verify_p2sh(trade):
fund_tx = fund_contract(trade.buy)
print("\nBuyer's funding tx: ", fund_tx)
print("\nYou sent this funding tx: ", fund_tx)
trade.buy.fund_tx = fund_tx
save_state(trade, tradeid)
elif status == 'sellerRedeemed':
print("FUND TX CLI", trade.buy.fund_tx)
secret = find_secret_from_fundtx(trade.buy.currency, trade.buy.p2sh, trade.buy.fund_tx)
print("Secret in cli", secret)
if secret != None:
print("Found secret", secret)
print("Found secret on blockchain in seller's redeem tx: ", secret)
txs = redeem_p2sh(trade.sell, secret)
print("TXS IN SELLER REDEEMED", txs)
trade.sell.redeem_tx = txs['redeem_tx']
print("TXID after buyer redeem", trade.sell.redeem_tx)
print("Redeem txid: ", trade.sell.redeem_tx)
save_state(trade, tradeid)
print("XCAT trade complete!")
else:
@ -156,12 +151,11 @@ def checktrade(tradeid):
role = 'buyer'
checkBuyStatus(tradeid)
def newtrade(tradeid):
erase_trade()
role = 'seller'
def newtrade(tradeid, **kwargs):
print("Creating new XCAT trade...")
trade = seller_init(tradeid)
print("Use 'xcat exporttrade <tradeid> to export the trade and sent to the buyer.'")
erase_trade()
trade = seller_init(tradeid, **kwargs)
print("\nUse 'xcat exporttrade [tradeid]' to export the trade and sent to the buyer.\n")
save_state(trade, tradeid)
def main():
@ -178,20 +172,19 @@ def main():
parser.add_argument("command", action="store", help="list commands")
parser.add_argument("arguments", action="store", nargs="*", help="add arguments")
parser.add_argument("-w", "--wormhole", action="store_true", help="Transfer trade data through magic-wormhole")
parser.add_argument("-c", "--conf", action="store", help="Use default trade data in conf file.")
parser.add_argument("-n", "--network", action="store", help="Set network to regtest or mainnet. Defaults to testnet while in beta.")
# parser.add_argument("--daemon", "-d", action="store_true", help="Run as daemon process")
# TODO: function to view available trades
# TODO: function to tell if tradeid already exists for newtrade
args = parser.parse_args()
# how to hold state of role
command = args.command
if command == 'importtrade':
if args.wormhole:
wormhole_importtrade()
else:
if len(args.arguments) != 2:
print("Usage: importtrade [tradeid] [hexstring]")
exit()
if len(args.arguments) != 2: throw("Usage: importtrade [tradeid] [hexstring]")
tradeid = args.arguments[0]
hexstr = args.arguments[1]
importtrade(tradeid, hexstr)
@ -206,13 +199,9 @@ def main():
tradeid = args.arguments[0]
checktrade(tradeid)
elif command == 'newtrade':
print("in new trade")
try:
tradeid = args.arguments[0]
newtrade(tradeid)
except:
tradeid = userInput.enter_trade_id()
newtrade(tradeid)
if len(args.arguments) < 1: throw("Usage: newtrade [tradeid]")
tradeid = args.arguments[0]
newtrade(tradeid, network=args.network, conf=args.conf)
elif command == "daemon":
#TODO: implement
print("Run as daemon process")
@ -221,13 +210,11 @@ def main():
tradeid = args.arguments[0]
checkSellStatus(tradeid)
elif command == "step2":
# trade = get_trade()
tradeid = args.arguments[0]
checkBuyStatus(tradeid)
elif command == "step3":
tradeid = args.arguments[0]
checkSellStatus(tradeid)
# TODO: When trade finishes, delete wormhole file in tmp dir.
elif command == "step4":
tradeid = args.arguments[0]
checkBuyStatus(tradeid)

View File

@ -10,7 +10,6 @@ import xcat.db as db
from xcatconf import *
def find_secret_from_fundtx(currency, p2sh, fundtx):
print("Fund tx in protocol.py", fundtx)
if currency == 'bitcoin':
secret = bitcoinRPC.find_secret(p2sh, fundtx)
else:
@ -48,7 +47,6 @@ def check_fund_status(currency, address):
# print("Compiled p2sh for htlc does not match what seller sent.")
def create_htlc(currency, funder, redeemer, commitment, locktime):
print("Commitment in create_htlc", commitment)
if currency == 'bitcoin':
sell_p2sh = bitcoinRPC.hashtimelockcontract(funder, redeemer, commitment, locktime)
else:
@ -60,12 +58,10 @@ def fund_htlc(currency, p2sh, amount):
txid = bitcoinRPC.fund_htlc(p2sh, amount)
else:
txid = zcashRPC.fund_htlc(p2sh, amount)
print("Fund_htlc txid", txid )
return txid
def fund_contract(contract):
txid = fund_htlc(contract.currency, contract.p2sh, contract.amount)
print("TXID coming back from fund_contract", txid)
return txid
def fund_sell_contract(trade):
@ -89,8 +85,7 @@ def create_sell_p2sh(trade, commitment, locktime):
def create_buy_p2sh(trade, commitment, locktime):
## CREATE BUY CONTRACT
buy = trade.buy
print("Now creating buy contract on the {0} blockchain where you will wait for the buyer to send funds...".format(buy.currency))
print("HTLC DETAILS", buy.currency, buy.fulfiller, buy.initiator, commitment, locktime)
print("\nNow creating buy contract on the {0} blockchain where you will wait for the buyer to send funds...".format(buy.currency))
buy_contract = create_htlc(buy.currency, buy.fulfiller, buy.initiator, commitment, locktime)
print("Buy contract", buy_contract)
@ -98,7 +93,7 @@ def create_buy_p2sh(trade, commitment, locktime):
setattr(trade.buy, 'redeemScript', buy_contract['redeemScript'])
setattr(trade.buy, 'redeemblocknum', buy_contract['redeemblocknum'])
setattr(trade.buy, 'locktime', buy_contract['locktime'])
print("Now contact the buyer and tell them to send funds to this p2sh: ", trade.buy.p2sh)
print("\nNow contact the buyer and tell them to send funds to this p2sh: {0}\n".format(trade.buy.p2sh))
save(trade)
@ -167,22 +162,31 @@ def buyer_fulfill(trade):
print("Please wait for the seller to remove your funds from escrow to complete the trade.")
print_trade('buyer')
def seller_init(tradeid):
def seller_init(tradeid, **kwargs):
if kwargs['conf']:
conf = kwargs['conf'].upper()
if conf.upper() == 'REGTEST':
init_addrs = REGTEST['initiator']
fulfill_addrs = REGTEST['fulfiller']
elif conf.upper() == 'TESTNET':
init_addrs = TESTNET['initiator']
fulfill_addrs = TESTNET['fulfiller']
else:
init_addrs = userInput.get_initiator_addresses()
fulfill_addrs = userInput.get_fulfiller_addresses()
trade = Trade()
# TODO: pass in amounts, or get from cli. {"amounts": {"buy": {}, "sell": {}}}
amounts = userInput.get_trade_amounts()
sell = amounts['sell']
buy = amounts['buy']
sell_currency = sell['currency']
buy_currency = buy['currency']
# Get addresses
init_addrs = userInput.get_initiator_addresses()
sell['initiator'] = init_addrs[sell_currency]
buy['initiator'] = init_addrs[buy_currency]
fulfill_addrs = userInput.get_fulfiller_addresses()
sell['fulfiller'] = fulfill_addrs[sell_currency]
buy['fulfiller'] = fulfill_addrs[buy_currency]
# initializing contract classes with addresses, currencies, and amounts
trade.sell = Contract(sell)
trade.buy = Contract(buy)

View File

@ -77,6 +77,14 @@ def sha256(secret):
h = hashlib.sha256(preimage).digest()
return h
############################################
######## Error handling for CLI ############
############################################
def throw(err):
print(err)
exit()
#############################################
######### xcat.json temp file #############
#############################################
@ -84,7 +92,6 @@ def sha256(secret):
xcatjson = os.path.join(ROOT_DIR, '.tmp/xcat.json')
def save_trade(trade):
print("Trade in save_trade", trade)
with open(xcatjson, 'w+') as outfile:
json.dump(trade, outfile)

View File

@ -48,20 +48,19 @@ def hashtimelockcontract(funder, redeemer, commitment, locktime):
commitment = x(commitment)
# h = sha256(secret)
blocknum = zcashd.getblockcount()
print("Current blocknum", blocknum)
print("Current blocknum on Zcash: ", blocknum)
redeemblocknum = blocknum + locktime
print("REDEEMBLOCKNUM ZCASH", redeemblocknum)
print("COMMITMENT on zxcat", commitment)
print("Redeemblocknum on Zcash: ", redeemblocknum)
# can rm op_dup and op_hash160 if you replace addrs with pubkeys (as raw hex/bin data?), and can rm last op_equalverify (for direct pubkey comparison)
zec_redeemScript = CScript([OP_IF, OP_SHA256, commitment, OP_EQUALVERIFY,OP_DUP, OP_HASH160,
redeemerAddr, OP_ELSE, redeemblocknum, OP_CHECKLOCKTIMEVERIFY, OP_DROP, OP_DUP, OP_HASH160,
funderAddr, OP_ENDIF,OP_EQUALVERIFY, OP_CHECKSIG])
print("Redeem script for p2sh contract on Zcash blockchain:", b2x(zec_redeemScript))
# print("Redeem script for p2sh contract on Zcash blockchain: ", b2x(zec_redeemScript))
txin_scriptPubKey = zec_redeemScript.to_p2sh_scriptPubKey()
# Convert the P2SH scriptPubKey to a base58 Bitcoin address
txin_p2sh_address = CBitcoinAddress.from_scriptPubKey(txin_scriptPubKey)
p2sh = str(txin_p2sh_address)
print("p2sh computed", p2sh)
print("p2sh computed: ", p2sh)
# Import address as soon as you create it
zcashd.importaddress(p2sh, "", False)
# Returning all this to be saved locally in p2sh.json
@ -73,17 +72,13 @@ def fund_htlc(p2sh, amount):
zcashd.importaddress(p2sh, "", False)
fund_txid = zcashd.sendtoaddress(p2sh, send_amount)
txid = b2x(lx(b2x(fund_txid)))
print("txid at end of fund_htlc in zcashRPC", txid)
# print("Dif version of txid", b2x(lx(fund_txid)))
return txid
# Following two functions are about the same
def check_funds(p2sh):
zcashd.importaddress(p2sh, "", False)
print("Imported address", p2sh)
# Get amount in address
amount = zcashd.getreceivedbyaddress(p2sh, 0)
print("Amount in address", amount)
amount = amount/COIN
return amount
@ -91,7 +86,7 @@ def get_fund_status(p2sh):
zcashd.importaddress(p2sh, "", False)
amount = zcashd.getreceivedbyaddress(p2sh, 0)
amount = amount/COIN
print("Amount in zcash get_fund_status", amount, p2sh)
print("Amount in zcash p2sh: ", amount, p2sh)
if amount > 0:
return 'funded'
else:
@ -107,17 +102,13 @@ def find_transaction_to_address(p2sh):
for tx in txs:
if tx['address'] == CBitcoinAddress(p2sh):
print("Found tx to p2sh", p2sh)
print(tx)
return tx
def find_secret(p2sh, fundtx_input):
print("fundtx_input:", fundtx_input)
txs = zcashd.call('listtransactions', "*", 20, 0, True)
print('Length of txs from listtransactions():', len(txs))
for tx in txs:
raw = zcashd.gettransaction(lx(tx['txid']))['hex']
decoded = zcashd.decoderawtransaction(raw)
print("TXINFO", decoded['vin'][0])
if('txid' in decoded['vin'][0]):
sendid = decoded['vin'][0]['txid']
if (sendid == fundtx_input ):
@ -134,22 +125,19 @@ def parse_secret(txid):
pubkey = asm[1]
secret = x2s(asm[2])
redeemPubkey = P2PKHBitcoinAddress.from_pubkey(x(pubkey))
print('Veryify redeemPubkey: ', redeemPubkey)
print("Found secret in parse_secret:", secret)
return secret
def redeem_contract(contract, secret):
# How to find redeemScript and redeemblocknum from blockchain?
print("Contract in redeem contract", contract.__dict__)
p2sh = contract.p2sh
#checking there are funds in the address
amount = check_funds(p2sh)
if(amount == 0):
print("address ", p2sh, " not funded")
print("Address ", p2sh, " not funded")
quit()
fundtx = find_transaction_to_address(p2sh)
amount = fundtx['amount'] / COIN
print("Found fundtx:", fundtx)
# print("Found fund_tx: ", fundtx)
p2sh = P2SHBitcoinAddress(p2sh)
if fundtx['address'] == p2sh:
print("Found {0} in p2sh {1}, redeeming...".format(amount, p2sh))
@ -176,18 +164,13 @@ def redeem_contract(contract, secret):
print("SECRET", secret)
preimage = secret.encode('utf-8')
txin.scriptSig = CScript([sig, privkey.pub, preimage, OP_TRUE, zec_redeemScript])
print("txin.scriptSig", b2x(txin.scriptSig))
txin_scriptPubKey = zec_redeemScript.to_p2sh_scriptPubKey()
print('Redeem txhex', b2x(tx.serialize()))
print('Raw redeem transaction hex: ', b2x(tx.serialize()))
VerifyScript(txin.scriptSig, txin_scriptPubKey, tx, 0, (SCRIPT_VERIFY_P2SH,))
print("script verified, sending raw tx")
print("Script verified, sending raw redeem transaction...")
txid = zcashd.sendrawtransaction(tx)
redeem_tx = b2x(lx(b2x(txid)))
fund_tx = str(fundtx['outpoint'])
print("Returning fund_tx", fund_tx)
print("Txid of submitted redeem tx: ", redeem_tx)
print("TXID SUCCESSFULLY REDEEMED")
return {"redeem_tx": redeem_tx, "fund_tx": fund_tx}
else:
print("nLocktime exceeded, refunding")
@ -196,9 +179,6 @@ def redeem_contract(contract, secret):
txid = zcashd.sendtoaddress(refundPubKey, fundtx['amount'] - FEE)
refund_tx = b2x(lx(b2x(txid)))
fund_tx = str(fundtx['outpoint'])
print("Returning fund_tx", fund_tx)
print("Txid of submitted refund tx: ", refund_tx)
print("TXID SUCCESSFULLY REDEEMED")
return {"refund_tx": refund_tx, "fund_tx": fund_tx}
else:
print("No contract for this p2sh found in database", p2sh)

22
xcatconf.py Normal file
View File

@ -0,0 +1,22 @@
# Replace with your own addresses
REGTEST = {
"initiator": {
"bitcoin": "mvc56qCEVj6p57xZ5URNC3v7qbatudHQ9b",
"zcash": "tmTF7LMLjvEsGdcepWPUsh4vgJNrKMWwEyc"
},
"fulfiller": {
"bitcoin": "moRt56gJQGDNK46Y6fYy2HbooKnQXrTGDN",
"zcash": "tmK3rGzHDqa78MCwEicx9VcY9ZWX9gCF2nd"
}
}
TESTNET = {
"initiator": {
"bitcoin": "mvc56qCEVj6p57xZ5URNC3v7qbatudHQ9b",
"zcash": "tmTF7LMLjvEsGdcepWPUsh4vgJNrKMWwEyc"
},
"fulfiller": {
"bitcoin": "mgRG44X4PQC1ZCA4V654UZjJGJ3pxbApj2",
"zcash": "tmLZu7MdjNdA6vbPTNTwdsZo91LnnrVTYB5"
}
}