1
1
from dataclasses import dataclass
2
2
from enum import Enum
3
3
from io import BytesIO
4
- from typing import Callable
4
+ from typing import Callable , Optional
5
5
6
- from .argtypes import ArgType
6
+ from .argtypes import ArgType , SignerType
7
7
from .btctools import script , key
8
8
from .btctools .auth_proxy import AuthServiceProxy
9
9
from .btctools .messages import COutPoint , CTransaction , CTxIn , CTxInWitness
@@ -245,6 +245,36 @@ def __repr__(self):
245
245
return f"{ self .__class__ .__name__ } (naked_internal_pubkey={ self .naked_internal_pubkey .hex ()} )"
246
246
247
247
248
+ # Encapsulates a blind signer for one or more known keys.
249
+ # Used byt the ContractManager to sign for the clause arguments of SignerType type.
250
+ #
251
+ # In the real world, we wouldn't blindly sign a hash, so the `sign` method
252
+ # would include other info to help the signer decide (e.g.: the transaction)
253
+ # There are no bad people here, though, so we keep it simple for now.
254
+ class SchnorrSigner :
255
+ def __init__ (self , keys : key .ExtendedKey | list [key .ExtendedKey ]):
256
+ if not isinstance (keys , list ):
257
+ keys = [keys ]
258
+
259
+ for key in keys :
260
+ if not key .is_private :
261
+ raise ValueError ("The SchnorrSigner needs the private keys" )
262
+
263
+ self .keys = keys
264
+
265
+ def sign (self , msg : bytes , pubkey : bytes ) -> bytes | None :
266
+ if len (msg ) != 32 :
267
+ raise ValueError ("msg should be 32 bytes long" )
268
+ if len (pubkey ) != 32 :
269
+ raise ValueError ("pubkey should be an x-only pubkey" )
270
+
271
+ for k in self .keys :
272
+ if k .pubkey [1 :] == pubkey :
273
+ return key .sign_schnorr (k .privkey , msg )
274
+
275
+ return None
276
+
277
+
248
278
class ContractInstanceStatus (Enum ):
249
279
ABSTRACT = 0
250
280
FUNDED = 1
@@ -258,6 +288,8 @@ def __init__(self, contract: StandardP2TR | StandardAugmentedP2TR):
258
288
259
289
self .data_expanded = None # TODO: figure out a good API for this
260
290
291
+ self .manager : ContractManager = None
292
+
261
293
self .last_height = 0
262
294
263
295
self .status = ContractInstanceStatus .ABSTRACT
@@ -303,6 +335,15 @@ def __repr__(self):
303
335
value = self .funding_tx .vout [self .outpoint .n ].nValue
304
336
return f"{ self .__class__ .__name__ } (contract={ self .contract } , data={ self .data if self .data is None else self .data .hex ()} , value={ value } , status={ self .status } , outpoint={ self .outpoint } )"
305
337
338
+ def __call__ (self , clause_name : str , * , signer : Optional [SchnorrSigner ] = None , outputs : list [CTxOut ] = [], ** kwargs ) -> list ['ContractInstance' ]:
339
+ if self .manager is None :
340
+ raise ValueError ("Direct invocation is only allowed after adding the instance to a ContractManager" )
341
+
342
+ if self .status != ContractInstanceStatus .FUNDED :
343
+ raise ValueError ("Only implemented for FUNDED instances" )
344
+
345
+ return self .manager .spend_instance (self , clause_name , kwargs , signer = signer , outputs = outputs )
346
+
306
347
307
348
class ContractManager :
308
349
def __init__ (self , contract_instances : list [ContractInstance ], rpc : AuthServiceProxy , * , poll_interval : float = 1 , mine_automatically : bool = False ):
@@ -324,6 +365,10 @@ def _check_instance(self, instance: ContractInstance, exp_statuses: None | Contr
324
365
raise ValueError ("Instance not in this manager" )
325
366
326
367
def add_instance (self , instance : ContractInstance ):
368
+ if instance .manager is not None :
369
+ raise ValueError ("The instance can only be added to one ContractManager" )
370
+
371
+ instance .manager = self
327
372
self .instances .append (instance )
328
373
329
374
def wait_for_outpoint (self , instance : ContractInstance , txid : str | None = None ):
@@ -544,3 +589,54 @@ def wait_for_spend(self, instances: ContractInstance | list[ContractInstance]) -
544
589
for instance in result :
545
590
self .add_instance (instance )
546
591
return result
592
+
593
+ def fund_instance (self , contract : StandardP2TR | StandardAugmentedP2TR , amount : int , data : Optional [bytes ] = None ) -> ContractInstance :
594
+ """
595
+ Convenience method to create an instance of a contract, add it to the ContractManager,
596
+ and send a transaction to fund it with a certain amount.
597
+ """
598
+ instance = ContractInstance (contract )
599
+
600
+ if isinstance (contract , StandardP2TR ) and data is not None :
601
+ raise ValueError ("The data must None for a contract with no embedded data" )
602
+
603
+ if isinstance (contract , StandardAugmentedP2TR ):
604
+ if data is None :
605
+ raise ValueError ("The data must be provided for an augmented P2TR contract instance" )
606
+ instance .data = data
607
+ self .add_instance (instance )
608
+ txid = self .rpc .sendtoaddress (instance .get_address (), amount / 100_000_000 )
609
+ self .wait_for_outpoint (instance , txid )
610
+ return instance
611
+
612
+ def spend_instance (self , instance : ContractInstance , clause_name : str , args : dict , * , signer : Optional [SchnorrSigner ], outputs : Optional [list [CTxOut ]] = None ) -> list [ContractInstance ]:
613
+ """
614
+ Creates and broadcasts a transaction that spends a contract instance using a specified clause and arguments.
615
+
616
+ :param instance: The ContractInstance to spend from.
617
+ :param clause_name: The name of the clause to be executed in the contract.
618
+ :param args: A dictionary of arguments required for the clause.
619
+ :param outputs: if not None, a list of CTxOut to add at the end of the list of
620
+ outputs generated by the clause.
621
+ :return: A list of ContractInstances resulting from the spend transaction.
622
+ """
623
+ spend_tx , sighashes = self .get_spend_tx ((instance , clause_name , args ))
624
+
625
+ assert len (sighashes ) == 1
626
+
627
+ sighash = sighashes [0 ]
628
+
629
+ if outputs is not None :
630
+ spend_tx .vout .extend (outputs )
631
+
632
+ clause = instance .contract ._clauses_dict [clause_name ] # TODO: refactor, accessing private member
633
+ for arg_name , arg_type in clause .arg_specs :
634
+ if isinstance (arg_type , SignerType ):
635
+ if signer is None :
636
+ raise ValueError ("No signer was provided, but the witness requires signatures" )
637
+ args [arg_name ] = signer .sign (sighash , arg_type .pubkey )
638
+
639
+ spend_tx .wit .vtxinwit = [self .get_spend_wit (instance , clause_name , args )]
640
+ result = self .spend_and_wait (instance , spend_tx )
641
+
642
+ return result
0 commit comments