Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(WIP) Add name_autorenew command #252

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 122 additions & 2 deletions electrum_nmc/electrum/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@
from itertools import repeat
from decimal import Decimal
from typing import Optional, TYPE_CHECKING, Dict, List
from math import ceil

from .import util, ecc
from .util import bfh, bh2u, format_satoshis, json_decode, json_encode, is_hash256_str, is_hex_str, to_bytes, timestamp_to_datetime
from .util import standardize_path
from .util import standardize_path, NotEnoughFunds
from . import bitcoin
from .bitcoin import is_address, hash_160, COIN
from .bip32 import BIP32Node
Expand Down Expand Up @@ -930,7 +931,7 @@ async def name_autoregister(self, identifier, value, destination=None, amount=0.
new_txid,
value,
destination=destination,
amount=amount,
amount="!",
fee=fee,
feerate=feerate,
from_addr=new_addr,
Expand All @@ -948,6 +949,125 @@ async def name_autoregister(self, identifier, value, destination=None, amount=0.

await self.broadcast(new_tx)

@command('wp')
async def name_autorenew(self, identifier, blocks_to_renew, blocks_before_expire, value=None, amount=0.0, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None, nocheck=False, rbf=None, password=None, locktime=None, wallet: Abstract_Wallet = None):
"""Queues a sequence of name_update (renewal) transactions for a name."""
blocks_between_renewals = constants.net.NAME_EXPIRATION - blocks_before_expire
renewals_count = ceil(blocks_to_renew / blocks_between_renewals)

list_results = await self.name_list(identifier, wallet=wallet)
list_results = list_results[0]

# Allow renewing a name without any value changes by omitting the
# value.
renew = False
if value is None:
# This check is in place to prevent an attack where an ElectrumX
# server supplies an unconfirmed name_update transaction with a
# malicious value and then tricks the wallet owner into signing a
# name renewal with that malicious value. expires_in is None when
# the transaction has 0 confirmations.
expires_in = list_results["expires_in"]
if expires_in is None or expires_in > constants.net.NAME_EXPIRATION - 12:
raise NameUpdatedTooRecentlyError("Name was updated too recently to safely determine current value. Either wait or specify an explicit value.")

value = list_results["value"]
renew = True

# Construct name op (it's always the same for each update)
# TODO: support non-ASCII encodings
# TODO: enforce length limits on identifier and value
identifier_bytes = identifier.encode("ascii")
value_bytes = value.encode("ascii")
name_op = {"op": OP_NAME_UPDATE, "name": identifier_bytes, "value": value_bytes}

# Construct memo for initial update
memo = ("Renew: " if renew else "Update: ") + format_name_identifier(identifier_bytes)

# Get addresses for each renewal
destinations = []
for i in range(renewals_count):
# TODO: make request persist longer duration
request = await self.add_request(None, memo=memo, wallet=wallet)
destinations.append(request['address'])
# After the initial update, it's always a renew
memo = "Renew: " + format_name_identifier(identifier_bytes)

# Create the renewal transactions
extra_input_amount = 0.0
extra_input_present = False
init_address = list_results["address"]
init_txid = list_results["txid"]
while True:
renew_txs = []
try:
for i in range(renewals_count):
if i == 0:
if extra_input_present:
domain = from_addr
domain_coins = from_coins
amount = extra_input_amount
else:
domain = init_address
domain_coins = None
amount = "!"
else:
domain = destinations[i-1]
domain_coins = None
amount = "!"

next_renew_tx = await self.name_update(identifier,
value=value,
destination=destinations[i],
amount=amount,
fee=fee,
feerate=feerate,
from_addr=domain,
from_coins=domain_coins,
change_addr=change_addr,
nocheck=nocheck,
rbf=rbf,
password=password,
locktime=locktime, # TODO
wallet=wallet)
renew_txs.append(next_renew_tx)
status = await self.addtransaction(next_renew_tx, wallet=wallet)
if not status:
raise Exception("Error adding name pre-registration to wallet")
except NotEnoughFunds:
extra_input_amount += 0.005
extra_input_present = True
for tx_remove in renew_txs:
txid_remove = Transaction(tx_remove).txid()
await self.removelocaltx(txid_remove, wallet=wallet)
continue
except Exception as e:
for tx_remove in renew_txs:
txid_remove = Transaction(tx_remove).txid()
await self.removelocaltx(txid_remove, wallet=wallet)
raise e

break

# Delete the transactions from the main wallet, since they haven't
# taken effect yet.
for tx_remove in renew_txs:
txid_remove = Transaction(tx_remove).txid()
await self.removelocaltx(txid_remove, wallet=wallet)

# TODO: delete all existing queued updates for this name

if extra_input_present:
await self.broadcast(renew_txs[0])
else:
await self.queuetransaction(renew_txs[0], blocks_between_renewals, trigger_txid=init_txid, wallet=wallet)

prev_txid = Transaction(renew_txs[0]).txid()

for renew_tx in renew_txs[1:]:
await self.queuetransaction(renew_tx, blocks_between_renewals, trigger_txid=prev_txid, wallet=wallet)
prev_txid = Transaction(renew_tx).txid()

@command('w')
async def onchain_history(self, year=None, show_addresses=False, show_fiat=False, wallet: Abstract_Wallet = None):
"""Wallet onchain history. Returns the transaction history of your wallet."""
Expand Down
15 changes: 14 additions & 1 deletion electrum_nmc/electrum/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@

from . import ecc, bitcoin, constants, segwit_addr, bip32
from .bip32 import BIP32Node
from .util import profiler, to_bytes, bh2u, bfh, chunks, is_hex_str
from .util import profiler, to_bytes, bh2u, bfh, chunks, is_hex_str, NotEnoughFunds
from .bitcoin import (TYPE_ADDRESS, TYPE_SCRIPT, hash_160,
hash160_to_p2sh, hash160_to_p2pkh, hash_to_segwit_addr,
var_int, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN,
Expand Down Expand Up @@ -188,6 +188,15 @@ def value_display(self, value: Union[int, str]):
# add here.
self.value = value + COIN // 100

def validate_value(self):
value_display = self.value_display

if isinstance(value_display, str):
return

if value_display < 0:
raise NotEnoughFunds("Name output below minimum amount")

def __repr__(self):
return f"<TxOutput script={self.scriptpubkey.hex()} address={self.address} value={self.value}>"

Expand Down Expand Up @@ -634,6 +643,10 @@ def outputs(self) -> Sequence[TxOutput]:
self.deserialize()
return self._outputs

def validate_value(self):
for o in self.outputs():
o.validate_value()

# If expect_trailing_data == True, returns start position (in bytes) of
# trailing data.
def deserialize(self) -> Optional[int]:
Expand Down
1 change: 1 addition & 0 deletions electrum_nmc/electrum/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,7 @@ def fee_estimator(size: Union[int, float, Decimal]) -> int:
raise NotEnoughFunds()
outputs[i_max].value = amount
tx = PartialTransaction.from_io(list(coins), list(outputs))
tx.validate_value()

# Timelock tx to current height.
tx.locktime = get_locktime_for_new_transaction(self.network)
Expand Down