Skip to content
Merged
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: 1 addition & 1 deletion .github/workflows/javascript.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
run: brew install llvm

- name: Install wasm-bindgen
run: cargo install wasm-bindgen-cli
run: cargo install --locked wasm-bindgen-cli

- name: "Install dependencies"
run: npm ci
Expand Down
120 changes: 68 additions & 52 deletions payjoin-ffi/python/test/test_payjoin_integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@
from typing import Optional
import unittest

try:
import payjoin.bitcoin as bitcoinffi
except ImportError:
bitcoinffi = None
raise unittest.SkipTest("bitcoin_ffi helpers are not available in this binding")
Comment on lines -10 to -14
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whats the rationale for not wanting to use bitcoin-ffi types in these tests? Seems like they are generally help reduce boilerplate e.g in IsScriptOwnedCallback.callback generating an address from spk.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a question for @chavic I think since iirc he authored the change

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also curious what the original rationale was since this seems intentional per #1287 (comment)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

payjoin-ffi now only accepts primitive data types and no longer re-exports bitcoin-ffi per #738, so import payjoin.bitcoin as bitcoinffi always errors. To reduce boilerplate I think each downstream language could use whatever bitcoin library exists in that language for common operations like script parsing, etc.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sflr, I believe the pattern here is supposed to follow Dart and JS. The skip was supposed to be a tmp measure during the transition

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no sweat. I think this is ready to merge


# The below sys path setting is required to use the 'payjoin' module in the 'src' directory
# This script is in the 'tests' directory and the 'payjoin' module is in the 'src' directory
sys.path.insert(
Expand Down Expand Up @@ -101,15 +95,13 @@ async def process_receiver_proposal(

def create_receiver_context(
self,
receiver_address: bitcoinffi.Address,
address: str,
directory: str,
ohttp_keys: OhttpKeys,
recv_persister: InMemoryReceiverSessionEventLog,
) -> Initialized:
receiver = (
ReceiverBuilder(
address=receiver_address, directory=directory, ohttp_keys=ohttp_keys
)
ReceiverBuilder(address=address, directory=directory, ohttp_keys=ohttp_keys)
.build()
.save(recv_persister)
)
Expand Down Expand Up @@ -207,10 +199,7 @@ async def process_provisional_proposal(

async def test_integration_v2_to_v2(self):
try:
receiver_address = bitcoinffi.Address(
json.loads(self.receiver.call("getnewaddress", [])),
bitcoinffi.Network.REGTEST,
)
receiver_address = json.loads(self.receiver.call("getnewaddress", []))
init_tracing()
services = TestServices.initialize()

Expand Down Expand Up @@ -282,32 +271,37 @@ async def test_integration_v2_to_v2(self):
headers={"Content-Type": request.request.content_type},
content=request.request.body,
)
checked_payjoin_proposal_psbt = (
send_ctx.process_response(response.content, request.ohttp_ctx)
.save(sender_persister)
.inner
)
print(f"checked_payjoin_proposal_psbt: {checked_payjoin_proposal_psbt}")
self.assertIsNotNone(checked_payjoin_proposal_psbt)
poll_outcome = send_ctx.process_response(
response.content, request.ohttp_ctx
).save(sender_persister)
print(f"poll_outcome: {poll_outcome}")
self.assertIsNotNone(poll_outcome)
self.assertTrue(poll_outcome.is_PROGRESS())
payjoin_psbt = json.loads(
self.sender.call(
"walletprocesspsbt",
[checked_payjoin_proposal_psbt.serialize_base64()],
[poll_outcome.psbt_base64],
)
)["psbt"]
final_psbt = json.loads(
self.sender.call("finalizepsbt", [payjoin_psbt, json.dumps(False)])
)["psbt"]
payjoin_tx = bitcoinffi.Psbt.deserialize_base64(final_psbt).extract_tx()
self.sender.call(
"sendrawtransaction", [json.dumps(payjoin_tx.serialize().hex())]
)
final_tx_hex = json.loads(
self.sender.call("finalizepsbt", [final_psbt, json.dumps(True)])
)["hex"]
self.sender.call("sendrawtransaction", [json.dumps(final_tx_hex)])

# Check resulting transaction and balances
network_fees = bitcoinffi.Psbt.deserialize_base64(final_psbt).fee().to_btc()
decoded_psbt = json.loads(
self.sender.call("decodepsbt", [json.dumps(final_psbt)])
)
network_fees = float(decoded_psbt["fee"])
decoded_tx = json.loads(
self.sender.call("decoderawtransaction", [json.dumps(final_tx_hex)])
)
# Sender sent the entire value of their utxo to receiver (minus fees)
self.assertEqual(len(payjoin_tx.input()), 2)
self.assertEqual(len(payjoin_tx.output()), 1)
self.assertEqual(len(decoded_tx["vin"]), 2)
self.assertEqual(len(decoded_tx["vout"]), 1)
self.assertEqual(
float(
json.loads(self.receiver.call("getbalances", []))["mine"][
Expand All @@ -323,7 +317,7 @@ async def test_integration_v2_to_v2(self):
raise


def build_sweep_psbt(sender: RpcClient, pj_uri: PjUri) -> bitcoinffi.Psbt:
def build_sweep_psbt(sender: RpcClient, pj_uri: PjUri) -> str:
outputs = {}
outputs[pj_uri.address()] = 50
psbt = json.loads(
Expand Down Expand Up @@ -354,25 +348,21 @@ def build_sweep_psbt(sender: RpcClient, pj_uri: PjUri) -> bitcoinffi.Psbt:
def get_inputs(rpc_connection: RpcClient) -> list[InputPair]:
utxos = json.loads(rpc_connection.call("listunspent", []))
inputs = []
for utxo in utxos[:1]:
txin = bitcoinffi.TxIn(
previous_output=bitcoinffi.OutPoint(txid=utxo["txid"], vout=utxo["vout"]),
script_sig=bitcoinffi.Script(bytes()),
for utxo in utxos:
txid = utxo["txid"]
vout = utxo["vout"]
script_pubkey = bytes.fromhex(utxo["scriptPubKey"])
amount_sat = round(utxo["amount"] * 100_000_000)

txin = PlainTxIn(
previous_output=PlainOutPoint(txid=txid, vout=vout),
script_sig=bytes(),
sequence=0,
witness=[],
)
raw_tx = json.loads(
rpc_connection.call(
"gettransaction",
[json.dumps(utxo["txid"]), json.dumps(True), json.dumps(True)],
)
)
prev_out = raw_tx["decoded"]["vout"][utxo["vout"]]
prev_spk = bitcoinffi.Script(bytes.fromhex(prev_out["scriptPubKey"]["hex"]))
prev_amount = bitcoinffi.Amount.from_btc(prev_out["value"])
tx_out = bitcoinffi.TxOut(value=prev_amount, script_pubkey=prev_spk)
psbt_in = PsbtInput(
witness_utxo=tx_out, redeem_script=None, witness_script=None
witness_utxo = PlainTxOut(value_sat=amount_sat, script_pubkey=script_pubkey)
psbt_in = PlainPsbtInput(
witness_utxo=witness_utxo, redeem_script=None, witness_script=None
)
inputs.append(InputPair(txin=txin, psbtin=psbt_in, expected_weight=None))

Expand Down Expand Up @@ -402,15 +392,41 @@ def __init__(self, connection: RpcClient):

def callback(self, script):
try:
address = bitcoinffi.Address.from_script(
bitcoinffi.Script(script), bitcoinffi.Network.REGTEST
script_hex = bytes(script).hex()
decoded_script = json.loads(
self.connection.call("decodescript", [json.dumps(script_hex)])
)
return json.loads(self.connection.call("getaddressinfo", [str(address)]))[
"ismine"
]

candidates = []
if isinstance(decoded_script.get("address"), str):
candidates.append(decoded_script["address"])
if isinstance(decoded_script.get("addresses"), list):
candidates.extend(
addr
for addr in decoded_script["addresses"]
if isinstance(addr, str)
)
if isinstance(decoded_script.get("p2sh"), str):
candidates.append(decoded_script["p2sh"])
segwit = decoded_script.get("segwit")
if isinstance(segwit, dict):
if isinstance(segwit.get("address"), str):
candidates.append(segwit["address"])
if isinstance(segwit.get("addresses"), list):
candidates.extend(
addr for addr in segwit["addresses"] if isinstance(addr, str)
)

for addr in candidates:
info = json.loads(
self.connection.call("getaddressinfo", [json.dumps(addr)])
)
if info.get("ismine") is True:
return True
return False
except Exception as e:
print(f"An error occurred: {e}")
return None
return False


class CheckInputsNotSeenCallback(IsOutputKnown):
Expand Down
Loading