Skip to content
Draft
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ ahash = "0.8.12"
bincode = "1.3.3"
faster-hex = "0.9.0"
futures = "0.3.31"
hex = "0.4.3"
kaspa-addresses = { git = "https://github.com/kaspanet/rusty-kaspa.git", rev = "1a2f98a" }
kaspa-bip32 = { git = "https://github.com/kaspanet/rusty-kaspa.git", rev = "1a2f98a" }
kaspa-consensus-client = { git = "https://github.com/kaspanet/rusty-kaspa.git", rev = "1a2f98a" }
Expand All @@ -27,6 +28,7 @@ kaspa-txscript = { git = "https://github.com/kaspanet/rusty-kaspa.git", rev = "1
kaspa-utils = { git = "https://github.com/kaspanet/rusty-kaspa.git", rev = "1a2f98a" }
kaspa-wallet-core = { git = "https://github.com/kaspanet/rusty-kaspa.git", rev = "1a2f98a" }
kaspa-wallet-keys = { git = "https://github.com/kaspanet/rusty-kaspa.git", rev = "1a2f98a" }
kaspa-wallet-pskt = { git = "https://github.com/kaspanet/rusty-kaspa.git", rev = "1a2f98a" }
kaspa-wrpc-client = { git = "https://github.com/kaspanet/rusty-kaspa.git", rev = "1a2f98a" }
paste = "1.0"
pyo3 = { version = "0.27.1", features = ['multiple-pymethods'] }
Expand Down
142 changes: 142 additions & 0 deletions examples/transactions/pskt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import asyncio
from kaspa import (
Hash,
Mnemonic,
Opcodes,
PSKT,
Resolver,
RpcClient,
ScriptBuilder,
TransactionInput,
TransactionOutpoint,
UtxoEntryReference,
XPrv,
address_from_script_public_key,
calculate_transaction_mass,
create_transaction,
sign_transaction,
)


def derive(seed, account_index):
xprv = XPrv(seed).derive_path(f"m/45'/111111'/{account_index}'")
xpub = xprv.to_xpub()
prv = xprv.derive_child(1).to_private_key()
pub = xpub.derive_child(1).to_public_key()
return prv, pub


async def main():
#######################################################
# Derive 3 accounts to use for Multisig PSKT demo
#######################################################
seed = Mnemonic((
'predict cloud noise economy home stereo tag cancel adult pistol act remove '
'equip cricket man summer neutral black art miracle foam world clown say'
)).to_seed()

prv1, pub1 = derive(seed, 0)
print(f'Account 1:\n - prv: {prv1.to_string()}\n - pub: {pub1.to_string()}\n')

prv2, pub2 = derive(seed, 1)
print(f'Account 2:\n - prv: {prv2.to_string()}\n - pub: {pub2.to_string()}\n')

prv3, pub3 = derive(seed, 2)
print(f'Account 3:\n - prv: {prv3.to_string()}\n - pub: {pub3.to_string()}\n')

#######################################################
# Create Multisig address
#######################################################
redeem_script = ScriptBuilder()\
.add_i64(2)\
.add_data(pub1.to_x_only_public_key().to_string())\
.add_data(pub2.to_x_only_public_key().to_string())\
.add_data(pub3.to_x_only_public_key().to_string())\
.add_i64(3)\
.add_op(Opcodes.OpCheckMultiSig)
spk = redeem_script.create_pay_to_script_hash_script()
address = address_from_script_public_key(spk, "testnet")

print(f"Multisig address: {address}")

while True:
if input("Send funds to address (y to proceed): ") == "y":
break

#######################################################
# Get address's UTXOs
#######################################################
client = RpcClient(resolver=Resolver(), network_id='testnet-10')
await client.connect(strategy='fallback')
utxos = await client.get_utxos_by_addresses(request={'addresses': [address]})
utxos = utxos["entries"]
utxos = sorted(utxos, key=lambda x: x['utxoEntry']['amount'], reverse=True)
total = sum(item["utxoEntry"]["amount"] for item in utxos)
print(utxos)
# utxo = utxos["entries"][0]

#######################################################
# Placeholder TX for fee calculation
#######################################################
# outputs = [
# {"address": address, "amount": int(total)}
# ]
# tx = create_transaction(utxos, outputs, 0, None, 2)
# mass = calculate_transaction_mass("testnet-10", tx)

#######################################################
# Get feerates & create actual TX
#######################################################
# fee_rates = await client.get_fee_estimate()
# fee_rate = int(fee_rates["estimate"]["priorityBucket"]["feerate"])

# outputs = [
# {"address": address, "amount": int(total - (fee_rate * mass)), "scriptPublicKey": ""}
# ]
# tx = create_transaction(utxos, outputs, 0, None, 1)
# tx_signed = sign_transaction(tx, [prv1], True)

#######################################################
# Create PSKT
#######################################################
pskt = PSKT()
pskt_serialized = pskt.serialize()
print(pskt_serialized)

#######################################################
# Create input
#######################################################
input0 = TransactionInput.from_dict({
'previousOutpoint': { 'transactionId': 'c38eb7191a2e0df6089b05cf7df9c92dc559db618184b11cbb8c5ba30b024bce', 'index': 1 },
'signatureScript': '',
'sequence': 0,
'sigOpCount': 1,
'utxo': {
'utxo': {
'address': 'kaspatest:prganzek6uhsn4rv29g6qkeh8rduae6n3ul0xk5fnzjtugqhfaxcx0ee2dn47',
'outpoint': {'transactionId': 'c38eb7191a2e0df6089b05cf7df9c92dc559db618184b11cbb8c5ba30b024bce', 'index': 1},
'amount': 98699920028,
'scriptPublicKey': '0000aa20d1d98b36d72f09d46c5151a05b3738dbcee7538f3ef35a8998a4be20174f4d8387',
'blockDaaScore': 354263497,
'isCoinbase': False
}
}
})
# pskt = pskt.input(input)

# previous_outpoint = TransactionOutpoint(
# transaction_id=Hash(utxo["outpoint"]['transactionId']),
# index=utxo["outpoint"]['index']
# )
# input_0 = TransactionInput(
# previous_outpoint=previous_outpoint,
# signature_script=b"",
# sequence=0,
# sig_op_count=2,
# utxo=None
# )
# pskt.to_constructor().input(input_0)
# print(pskt.serialize())

if __name__ == "__main__":
asyncio.run(main())
151 changes: 136 additions & 15 deletions kaspa.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,57 @@ class Outputs:
"""
...

@typing.final
class PSKT:
r"""
Partially Signed Kaspa Transaction
"""
@property
def role(self) -> builtins.str: ...
@property
def payload(self) -> builtins.str: ...
def __new__(cls, payload: typing.Optional[typing.Any] = None) -> PSKT: ...
def serialize(self) -> builtins.str: ...
def creator(self) -> PSKT:
r"""
Change role to `CREATOR`
"""
def to_constructor(self) -> PSKT:
r"""
Change role to `CONSTRUCTOR`
"""
def to_updater(self) -> PSKT:
r"""
Change role to `UPDATER`
"""
def to_signer(self) -> PSKT:
r"""
Change role to `SIGNER`
"""
def to_combiner(self) -> PSKT:
r"""
Change role to `COMBINER`
"""
def to_finalizer(self) -> PSKT:
r"""
Change role to `FINALIZER`
"""
def to_extractor(self) -> PSKT:
r"""
Change role to `EXTRACTOR`
"""
def fallback_lock_time(self, lock_time: builtins.int) -> PSKT: ...
def inputs_modifiable(self) -> PSKT: ...
def outputs_modifiable(self) -> PSKT: ...
def no_more_inputs(self) -> PSKT: ...
def no_more_outputs(self) -> PSKT: ...
def input_and_redeem_script(self, input: TransactionInput, data: builtins.str) -> PSKT: ...
def input(self, input: TransactionInput) -> PSKT: ...
def output(self, output: TransactionOutput) -> PSKT: ...
def set_sequence(self, n: builtins.int, input_index: builtins.int) -> PSKT: ...
def calculate_id(self) -> Hash: ...
def calculate_mass(self, data: NetworkId) -> builtins.int: ...

@typing.final
class PaymentOutput:
r"""
Expand Down Expand Up @@ -1403,6 +1454,76 @@ class PublicKeyGenerator:
str: The generator info string.
"""

@typing.final
class PyPsktConsensusClientError(builtins.Exception):
r"""
PSKT Consensus Client Error
"""
...

@typing.final
class PyPsktCreateNotAllowedError(builtins.Exception):
r"""
PSKT Creation Not Allowed Error
"""
...

@typing.final
class PyPsktCtorError(builtins.Exception):
r"""
PSKT Constructor Error
"""
...

@typing.final
class PyPsktCustomError(builtins.Exception):
r"""
Custom PSKT Error
"""
...

@typing.final
class PyPsktError(builtins.Exception):
r"""
PSKT Error
"""
...

@typing.final
class PyPsktExpectedStateError(builtins.Exception):
r"""
PSKT Expected State Error
"""
...

@typing.final
class PyPsktInvalidPayloadError(builtins.Exception):
r"""
PSKT Invalid Payload Error
"""
...

@typing.final
class PyPsktNotInitializedError(builtins.Exception):
r"""
PSKT Not Initialized Error
"""
...

@typing.final
class PyPsktStateError(builtins.Exception):
r"""
PSKT State Error
"""
...

@typing.final
class PyPsktTxNotFinalizedError(builtins.Exception):
r"""
PSKT Tx Not Finalized Error
"""
...

@typing.final
class Resolver:
r"""
Expand Down Expand Up @@ -2461,6 +2582,21 @@ class UtxoContext:
Return a range of mature UTXO entries.
"""

@typing.final
class UtxoEntries:
r"""
UTXO entries collection for flexible input handling.

This type is not intended to be instantiated directly from Python.
It serves as a helper type that allows Rust functions to accept a list
of UTXO entries in multiple convenient forms.

Accepts:
list[UtxoEntryReference]: A list of UtxoEntryReference objects.
list[dict]: A list of dicts with UtxoEntryReference-compatible keys.
"""
...

@typing.final
class UtxoEntries:
r"""
Expand Down Expand Up @@ -2505,21 +2641,6 @@ class UtxoEntries:
"""
def __eq__(self, other: UtxoEntries) -> builtins.bool: ...

@typing.final
class UtxoEntries:
r"""
UTXO entries collection for flexible input handling.

This type is not intended to be instantiated directly from Python.
It serves as a helper type that allows Rust functions to accept a list
of UTXO entries in multiple convenient forms.

Accepts:
list[UtxoEntryReference]: A list of UtxoEntryReference objects.
list[dict]: A list of dicts with UtxoEntryReference-compatible keys.
"""
...

@typing.final
class UtxoEntry:
r"""
Expand Down
Loading